BrashUp Replay Kit Live #81

This commit is contained in:
shogo4405 2016-09-15 22:22:05 +09:00
parent 7eacded159
commit bc708b6dae
21 changed files with 516 additions and 217 deletions

View File

@ -30,9 +30,9 @@
<key>NSExtensionPointIdentifier</key>
<string>com.apple.broadcast-services</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).SampleHandler</string>
<string>$(PRODUCT_MODULE_NAME).MovieClipHandler</string>
<key>RPBroadcastProcessMode</key>
<string>RPBroadcastProcessModeSampleBuffer</string>
<string>RPBroadcastProcessModeMP4Clip</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,5 @@
import lf
import Foundation
class MovieClipHandler: RTMPMP4ClipHandler {
}

View File

@ -22,6 +22,10 @@ class BroadcastViewController: UIViewController {
]
let broadcastConfiguration:RPBroadcastConfiguration = RPBroadcastConfiguration()
broadcastConfiguration.clipDuration = 2
broadcastConfiguration.videoCompressionProperties = [
AVVideoProfileLevelKey: AVVideoProfileLevelH264BaselineAutoLevel as NSSecureCoding & NSObjectProtocol,
]
self.extensionContext?.completeRequest(
withBroadcast: broadcastURL,

View File

@ -158,12 +158,14 @@ final class LiveViewController: NSViewController {
httpStream.attach(camera: nil)
rtmpStream.attach(audio: DeviceUtil.device(withLocalizedName: audioPopUpButton.itemTitles[audioPopUpButton.indexOfSelectedItem], mediaType: AVMediaTypeAudio))
rtmpStream.attach(camera: DeviceUtil.device(withLocalizedName: cameraPopUpButton.itemTitles[cameraPopUpButton.indexOfSelectedItem], mediaType: AVMediaTypeVideo))
lfView.attach(stream: rtmpStream)
urlField.stringValue = LiveViewController.defaultURL
case 1:
rtmpStream.attach(audio: nil)
rtmpStream.attach(camera: nil)
httpStream.attach(audio: DeviceUtil.device(withLocalizedName: audioPopUpButton.itemTitles[audioPopUpButton.indexOfSelectedItem], mediaType: AVMediaTypeAudio))
httpStream.attach(camera: DeviceUtil.device(withLocalizedName: cameraPopUpButton.itemTitles[cameraPopUpButton.indexOfSelectedItem], mediaType: AVMediaTypeVideo))
lfView.attach(stream: httpStream)
urlField.stringValue = "http://{ipAddress}:8080/hello/playlist.m3u8"
default:
break

View File

@ -3,20 +3,39 @@ import Foundation
@available(iOS 10.0, *)
public class RTMPBroadcaster: RTMPConnection {
private var stream:RTMPStream?
static let `default`:RTMPBroadcaster = RTMPBroadcaster()
func process(sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
guard let stream:RTMPStream = stream, stream.readyState == .publishing else {
private var stream:RTMPStream!
private var connecting:Bool = false
private let lockQueue:DispatchQueue = DispatchQueue(label: "com.github.shogo4405.lf.RTMPBroadcaster.lock")
override init() {
super.init()
stream = RTMPStream(connection: self)
}
override public func connect(_ command: String, arguments: Any?...) {
lockQueue.sync {
if (connecting) {
return
}
connecting = true
super.connect(command, arguments: arguments)
}
}
override public func close() {
connecting = false
super.close()
}
func processMP4Clip(mp4ClipURL: URL?, completionHandler: MP4Sampler.Handler) {
guard let url:URL = mp4ClipURL else {
completionHandler()
return
}
switch sampleBufferType {
case .video:
stream.mixer.videoIO.captureOutput(nil, didOutputSampleBuffer: sampleBuffer, from: nil)
case .audioApp:
break
case .audioMic:
stream.mixer.audioIO.captureOutput(nil, didOutputSampleBuffer: sampleBuffer, from: nil)
}
stream.append(url: url)
completionHandler()
}
override func on(status:Notification) {
@ -27,8 +46,7 @@ public class RTMPBroadcaster: RTMPConnection {
}
switch code {
case RTMPConnection.Code.connectSuccess.rawValue:
stream = RTMPStream(connection: self)
stream?.publish("live")
stream.publish("live")
default:
break
}
@ -36,34 +54,21 @@ public class RTMPBroadcaster: RTMPConnection {
}
@available(iOS 10.0, *)
open class RTMPSampleHandler: RPBroadcastSampleHandler {
open class RTMPMP4ClipHandler: RPBroadcastMP4ClipHandler {
public static var broadcaster:RTMPBroadcaster?
deinit {
logger.info("deinit: \(self)")
}
override open func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {
logger.info("broadcastStarted:\(setupInfo)")
override open func processMP4Clip(with mp4ClipURL: URL?, setupInfo: [String : NSObject]?, finished: Bool) {
guard let endpointURL:String = setupInfo?["endpointURL"] as? String else {
return
}
RTMPSampleHandler.broadcaster = RTMPBroadcaster()
RTMPSampleHandler.broadcaster?.connect(endpointURL, arguments: nil)
}
override open func broadcastPaused() {
logger.info("broadcastPaused")
}
override open func broadcastResumed() {
logger.info("broadcastResumed")
}
override open func broadcastFinished() {
logger.info("broadcastFinished")
RTMPSampleHandler.broadcaster?.close()
RTMPSampleHandler.broadcaster = nil
}
override open func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
RTMPSampleHandler.broadcaster?.process(sampleBuffer: sampleBuffer, with: sampleBufferType)
RTMPBroadcaster.default.connect(endpointURL, arguments: nil)
RTMPBroadcaster.default.processMP4Clip(mp4ClipURL: mp4ClipURL) {_ in
if (finished) {
RTMPBroadcaster.default.close()
}
}
}
}

View File

@ -37,13 +37,16 @@ class MP4Box {
}
}
var leafNode:Bool {
return false
}
fileprivate(set) var type:String = "undf"
fileprivate(set) var size:UInt32 = 0
fileprivate(set) var offset:UInt64 = 0
fileprivate(set) var parent:MP4Box? = nil
var leafNode:Bool {
return false
init() {
}
init(size: UInt32, type:String) {
@ -51,7 +54,7 @@ class MP4Box {
self.type = type
}
func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
func load(_ fileHandle:FileHandle) throws -> UInt32 {
if (size == 0) {
size = UInt32(fileHandle.seekToEndOfFile() - offset)
return size
@ -60,7 +63,7 @@ class MP4Box {
return size
}
func getBoxesByName(_ name:String) -> [MP4Box] {
func getBoxes(byName:String) -> [MP4Box] {
return []
}
@ -76,8 +79,8 @@ class MP4Box {
}
}
// MARK: CustomStringConvertible
extension MP4Box: CustomStringConvertible {
// MARK: CustomStringConvertible
var description:String {
return Mirror(reflecting: self).description
}
@ -92,29 +95,29 @@ class MP4ContainerBox: MP4Box {
return false
}
override func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
override func load(_ file:FileHandle) throws -> UInt32 {
children.removeAll(keepingCapacity: false)
var offset:UInt32 = parent == nil ? 0 : 8
fileHandle.seek(toFileOffset: self.offset + UInt64(offset))
file.seek(toFileOffset: self.offset + UInt64(offset))
while (size != offset) {
let child:MP4Box = try create(fileHandle.readData(ofLength: 8), offset: offset)
offset += try child.loadFile(fileHandle)
let child:MP4Box = try create(file.readData(ofLength: 8), offset: offset)
offset += try child.load(file)
children.append(child)
}
return offset
}
override func getBoxesByName(_ name:String) -> [MP4Box] {
override func getBoxes(byName:String) -> [MP4Box] {
var list:[MP4Box] = []
for child in children {
if (name == child.type || name == "*" ) {
if (byName == child.type || byName == "*" ) {
list.append(child)
}
if (!child.leafNode) {
list += child.getBoxesByName(name)
list += child.getBoxes(byName: byName)
}
}
return list
@ -139,7 +142,7 @@ final class MP4MediaHeaderBox: MP4Box {
var language:UInt16 = 0
var quality:UInt16 = 0
override func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
override func load(_ fileHandle: FileHandle) throws -> UInt32 {
let buffer:ByteArray = ByteArray(data: fileHandle.readData(ofLength: Int(size) - 8))
version = try buffer.readUInt8()
buffer.position += 3
@ -158,7 +161,7 @@ final class MP4MediaHeaderBox: MP4Box {
final class MP4ChunkOffsetBox: MP4Box {
var entries:[UInt32] = []
override func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
override func load(_ fileHandle: FileHandle) throws -> UInt32 {
let buffer:ByteArray = ByteArray(data: fileHandle.readData(ofLength: Int(size) - 8))
buffer.position += 4
@ -176,7 +179,7 @@ final class MP4ChunkOffsetBox: MP4Box {
final class MP4SyncSampleBox: MP4Box {
var entries:[UInt32] = []
override func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
override func load(_ fileHandle: FileHandle) throws -> UInt32 {
entries.removeAll(keepingCapacity: false)
let buffer:ByteArray = ByteArray(data: fileHandle.readData(ofLength: Int(size) - 8))
@ -209,7 +212,7 @@ final class MP4TimeToSampleBox: MP4Box {
var entries:[Entry] = []
override func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
override func load(_ fileHandle: FileHandle) throws -> UInt32 {
entries.removeAll(keepingCapacity: false)
let buffer:ByteArray = ByteArray(data: fileHandle.readData(ofLength: Int(size) - 8))
@ -231,7 +234,7 @@ final class MP4TimeToSampleBox: MP4Box {
final class MP4SampleSizeBox: MP4Box {
var entries:[UInt32] = []
override func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
override func load(_ fileHandle: FileHandle) throws -> UInt32 {
entries.removeAll(keepingCapacity: false)
let buffer:ByteArray = ByteArray(data: fileHandle.readData(ofLength: Int(self.size) - 8))
@ -266,7 +269,7 @@ final class MP4ElementaryStreamDescriptorBox: MP4ContainerBox {
var ocrStreamFlag:UInt8 = 0
var streamPriority:UInt8 = 0
override func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
override func load(_ fileHandle: FileHandle) throws -> UInt32 {
var tagSize:UInt8 = 0
let buffer:ByteArray = ByteArray(data: fileHandle.readData(ofLength: Int(self.size) - 8))
buffer.position += 4
@ -329,7 +332,7 @@ final class MP4AudioSampleEntryBox: MP4ContainerBox {
var soundVersion2Data:[UInt8] = []
override func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
override func load(_ fileHandle: FileHandle) throws -> UInt32 {
let buffer:ByteArray = ByteArray(data: fileHandle.readData(ofLength: Int(size) - 8))
buffer.position += 8
@ -361,7 +364,7 @@ final class MP4AudioSampleEntryBox: MP4ContainerBox {
fileHandle.seek(toFileOffset: self.offset + UInt64(offset))
let esds:MP4Box = try create(fileHandle.readData(ofLength: 8), offset: offset)
offset += try esds.loadFile(fileHandle)
offset += try esds.load(fileHandle)
children.append(esds)
// skip
@ -383,7 +386,7 @@ final class MP4VisualSampleEntryBox: MP4ContainerBox {
var compressorname:String = ""
var depth:UInt16 = 16
override func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
override func load(_ fileHandle: FileHandle) throws -> UInt32 {
let buffer:ByteArray = ByteArray(data: fileHandle.readData(ofLength: MP4VisualSampleEntryBox.dataSize))
buffer.position += 24
@ -402,7 +405,7 @@ final class MP4VisualSampleEntryBox: MP4ContainerBox {
let child:MP4Box = try MP4Box.create(fileHandle.readData(ofLength: 8))
child.parent = self
child.offset = self.offset + UInt64(offset) + 8
offset += try child.loadFile(fileHandle)
offset += try child.load(fileHandle)
children.append(child)
// skip
@ -414,7 +417,7 @@ final class MP4VisualSampleEntryBox: MP4ContainerBox {
// MARK: -
final class MP4SampleDescriptionBox: MP4ContainerBox {
override func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
override func load(_ fileHandle: FileHandle) throws -> UInt32 {
children.removeAll(keepingCapacity: false)
let buffer:ByteArray = ByteArray(data: fileHandle.readData(ofLength: 8))
@ -424,7 +427,7 @@ final class MP4SampleDescriptionBox: MP4ContainerBox {
let numberOfEntries:UInt32 = try buffer.readUInt32()
for _ in 0..<numberOfEntries {
let child:MP4Box = try create(fileHandle.readData(ofLength: 8), offset: offset)
offset += try child.loadFile(fileHandle)
offset += try child.load(fileHandle)
children.append(child)
}
@ -452,7 +455,7 @@ final class MP4SampleToChunkBox: MP4Box {
var entries:[Entry] = []
override func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
override func load(_ fileHandle: FileHandle) throws -> UInt32 {
let buffer:ByteArray = ByteArray(data: fileHandle.readData(ofLength: Int(size) - 8))
buffer.position += 4
@ -491,7 +494,7 @@ final class MP4EditListBox: MP4Box {
var version:UInt32 = 0
var entries:[Entry] = []
override func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
override func load(_ fileHandle: FileHandle) throws -> UInt32 {
let buffer:ByteArray = ByteArray(data: fileHandle.readData(ofLength: Int(size) - 8))
version = try buffer.readUInt32()
@ -511,7 +514,7 @@ final class MP4EditListBox: MP4Box {
}
// MARK: -
final class MP4File: MP4ContainerBox {
final class MP4Reader: MP4ContainerBox {
var url:URL? = nil {
didSet {
if (url == nil) {
@ -525,46 +528,46 @@ final class MP4File: MP4ContainerBox {
}
}
fileprivate var fileHandle:FileHandle? = nil
func isEmpty() -> Bool {
return getBoxesByName("mdhd").isEmpty
var isEmpty:Bool {
return getBoxes(byName: "mdhd").isEmpty
}
func readDataOfLength(_ length: Int) -> Data {
return fileHandle!.readData(ofLength: length)
private var fileHandle:FileHandle? = nil
func seek(toFileOffset: UInt64) {
return fileHandle!.seek(toFileOffset: toFileOffset)
}
func seekToFileOffset(_ offset: UInt64) {
return fileHandle!.seek(toFileOffset: offset)
func readData(ofLength: Int) -> Data {
return fileHandle!.readData(ofLength: ofLength)
}
func readDataOfBox(_ box:MP4Box) -> Data {
if (fileHandle == nil) {
func readData(ofBox:MP4Box) -> Data {
guard let fileHandle:FileHandle = fileHandle else {
return Data()
}
let currentOffsetInFile:UInt64 = fileHandle!.offsetInFile
fileHandle!.seek(toFileOffset: box.offset + 8)
let data:Data = fileHandle!.readData(ofLength: Int(box.size) - 8)
fileHandle!.seek(toFileOffset: currentOffsetInFile)
let currentOffsetInFile:UInt64 = fileHandle.offsetInFile
fileHandle.seek(toFileOffset: ofBox.offset + 8)
let data:Data = fileHandle.readData(ofLength: Int(ofBox.size) - 8)
fileHandle.seek(toFileOffset: currentOffsetInFile)
return data
}
func loadFile() throws -> UInt32 {
if (fileHandle == nil) {
func load() throws -> UInt32 {
guard let fileHandle:FileHandle = self.fileHandle else {
return 0
}
return try self.loadFile(fileHandle!)
return try load(fileHandle)
}
func closeFile() {
func close() {
fileHandle?.closeFile()
}
override func loadFile(_ fileHandle: FileHandle) throws -> UInt32 {
override func load(_ fileHandle: FileHandle) throws -> UInt32 {
let size:UInt64 = fileHandle.seekToEndOfFile()
fileHandle.seek(toFileOffset: 0)
self.size = UInt32(size)
return try super.loadFile(fileHandle)
return try super.load(fileHandle)
}
}

View File

@ -0,0 +1,215 @@
import Foundation
struct MP4SampleTable {
var trak:MP4Box
var currentOffset:UInt64 {
return UInt64(offset[cursor])
}
var currentIsKeyframe:Bool {
return keyframe[cursor] != nil
}
var currentDuration:Double {
return Double(totalTimeToSample) * 1000 / Double(timeScale)
}
var currentTimeToSample:Double {
return Double(timeToSample[cursor]) * 1000 / Double(timeScale)
}
var currentSampleSize:Int {
return Int((sampleSize.count == 1) ? sampleSize[0] : sampleSize[cursor])
}
private var cursor:Int = 0
private var offset:[UInt32] = []
private var keyframe:[Int:Bool] = [:]
private var timeScale:UInt32 = 0
private var sampleSize:[UInt32] = []
private var timeToSample:[UInt32] = []
private var totalTimeToSample:UInt32 = 0
init(trak:MP4Box) {
self.trak = trak
let mdhd:MP4Box? = trak.getBoxes(byName: "mdhd").first
if let mdhd:MP4MediaHeaderBox = mdhd as? MP4MediaHeaderBox {
timeScale = mdhd.timeScale
}
let stss:MP4Box? = trak.getBoxes(byName: "stss").first
if let stss:MP4SyncSampleBox = stss as? MP4SyncSampleBox {
var keyframes:[UInt32] = stss.entries
for i in 0..<keyframes.count {
keyframe[Int(keyframes[i])] = true
}
}
let stts:MP4Box? = trak.getBoxes(byName: "stts").first
if let stts:MP4TimeToSampleBox = stts as? MP4TimeToSampleBox {
var timeToSample:[MP4TimeToSampleBox.Entry] = stts.entries
for i in 0..<timeToSample.count {
let entry:MP4TimeToSampleBox.Entry = timeToSample[i]
for _ in 0..<entry.sampleCount {
self.timeToSample.append(entry.sampleDuration)
}
}
}
let stsz:MP4Box? = trak.getBoxes(byName: "stsz").first
if let stsz:MP4SampleSizeBox = stsz as? MP4SampleSizeBox {
sampleSize = stsz.entries
}
let stco:MP4Box = trak.getBoxes(byName: "stco").first!
let stsc:MP4Box = trak.getBoxes(byName: "stsc").first!
var offsets:[UInt32] = (stco as! MP4ChunkOffsetBox).entries
var sampleToChunk:[MP4SampleToChunkBox.Entry] = (stsc as! MP4SampleToChunkBox).entries
var index:Int = 0
let count:Int = sampleToChunk.count
for i in 0..<count {
let j:Int = Int(sampleToChunk[i].firstChunk) - 1
let m:Int = (i + 1 < count) ? Int(sampleToChunk[i + 1].firstChunk) - 1 : offsets.count
for _ in j..<m {
var offset:UInt32 = offsets[j]
for _ in 0..<sampleToChunk[i].samplesPerChunk {
self.offset.append(offset)
offset += sampleSize[index]
index += 1
}
}
}
totalTimeToSample = timeToSample[cursor]
}
func hasNext() -> Bool {
return cursor + 1 < offset.count
}
mutating func next() {
defer {
cursor += 1
}
totalTimeToSample += timeToSample[cursor]
}
}
extension MP4SampleTable: CustomStringConvertible {
// MARK: CustomStringConvertible
var description:String {
return Mirror(reflecting: self).description
}
}
// MARK: -
protocol MP4SamplerDelegate: class {
func didSet(avcC: Data, withType:Int)
func didSet(audioDecorderSpecificConfig: Data, withType:Int)
func output(data: Data, withType:Int, currentTime:Double, keyframe:Bool)
}
// MARK: -
class MP4Sampler {
typealias Handler = () -> Void
weak var delegate:MP4SamplerDelegate?
fileprivate(set) var running:Bool = false
fileprivate var files:[URL:Handler?] = [:]
fileprivate let fileQueue:DispatchQueue = DispatchQueue(label: "com.github.shogo4405.lf.MP4Sampler.file")
fileprivate let lockQueue:DispatchQueue = DispatchQueue(label: "com.github.shgoo4405.lf.MP4Sampler.lock")
private var reader:MP4Reader = MP4Reader()
private var sampleTables:[MP4SampleTable] = []
func append(file:URL, completionHandler: Handler? = nil) {
fileQueue.async {
self.files[file] = completionHandler
}
}
fileprivate func execute(url:URL) {
reader.url = url
do {
let _:UInt32 = try reader.load()
} catch {
logger.warning("")
return
}
sampleTables.removeAll()
var traks:[MP4Box] = reader.getBoxes(byName: "trak")
for i in 0..<traks.count {
sampleTables.append(MP4SampleTable(trak: traks[i]))
}
for i in 0..<sampleTables.count {
if let avcC:MP4Box = sampleTables[i].trak.getBoxes(byName: "avcC").first {
delegate?.didSet(avcC: reader.readData(ofBox: avcC), withType: i)
}
if let esds:MP4ElementaryStreamDescriptorBox = sampleTables[i].trak.getBoxes(byName: "esds").first as? MP4ElementaryStreamDescriptorBox {
delegate?.didSet(audioDecorderSpecificConfig: Data(esds.audioDecorderSpecificConfig), withType: i)
}
}
repeat {
for i in 0..<sampleTables.count {
autoreleasepool {
reader.seek(toFileOffset: sampleTables[i].currentOffset)
let length:Int = sampleTables[i].currentSampleSize
delegate?.output(
data: reader.readData(ofLength: length),
withType: i,
currentTime: sampleTables[i].currentTimeToSample,
keyframe: sampleTables[i].currentIsKeyframe
)
}
if (sampleTables[i].hasNext()) {
sampleTables[i].next()
}
}
}
while inLoop(sampleTables: sampleTables)
reader.close()
}
fileprivate func run() {
if (files.isEmpty) {
return
}
let (key: url, value: handler) = files.popFirst()!
execute(url: url)
handler?()
}
private func inLoop(sampleTables:[MP4SampleTable]) -> Bool{
for i in sampleTables {
if (i.hasNext()) {
return true
}
}
return false
}
}
extension MP4Sampler: Runnable {
// MARK: Runnable
final func startRunning() {
lockQueue.async {
self.running = true
while (self.running) {
self.fileQueue.async {
self.run()
}
}
}
}
final func stopRunning() {
lockQueue.async {
self.running = false
}
}
}

View File

@ -194,7 +194,7 @@ struct PacketizedElementaryStream: PESPacketHeader {
packet.adaptationFieldFlag = true
packet.adaptationField = TSAdaptationField()
packet.adaptationField?.PCRFlag = true
packet.adaptationField?.PCR = TSProgramClockReference.encode(0, UInt16(PCR))
packet.adaptationField?.PCR = TSProgramClockReference.encode(PCR, 0)
packet.adaptationField?.compute()
}
packet.payloadUnitStartIndicator = true

View File

@ -58,10 +58,11 @@ class TSWriter {
}
func writeSampleBuffer(_ PID:UInt16, streamID:UInt8, sampleBuffer:CMSampleBuffer) {
let presentationTimeStamp:CMTime = sampleBuffer.presentationTimeStamp
if (timestamps[PID] == nil) {
timestamps[PID] = sampleBuffer.presentationTimeStamp
timestamps[PID] = presentationTimeStamp
if (PCRPID == PID) {
PCRTimestamp = sampleBuffer.presentationTimeStamp
PCRTimestamp = presentationTimeStamp
}
}
let config:Any? = streamID == 192 ? audioConfig : videoConfig
@ -71,9 +72,14 @@ class TSWriter {
return
}
PES.streamID = streamID
let decodeTimeStamp:CMTime = sampleBuffer.decodeTimeStamp
var decodeTimeStamp:CMTime = sampleBuffer.decodeTimeStamp
if (decodeTimeStamp == kCMTimeInvalid) {
decodeTimeStamp = presentationTimeStamp
}
var packets:[TSPacket] = split(PID, PES: PES, timestamp: decodeTimeStamp)
if (PCRPID == PID && rorateFileHandle(decodeTimeStamp, next: sampleBuffer.decodeTimeStamp)) {
if (PCRPID == PID && rorateFileHandle(decodeTimeStamp)) {
packets[0].adaptationField?.randomAccessIndicator = true
packets[0].adaptationField?.discontinuityIndicator = true
}
@ -106,7 +112,7 @@ class TSWriter {
return packets
}
func rorateFileHandle(_ timestamp:CMTime, next:CMTime) -> Bool {
func rorateFileHandle(_ timestamp:CMTime) -> Bool {
let duration:Double = timestamp.seconds - rotatedTimestamp.seconds
if (duration <= segmentDuration) {
return false

View File

@ -10,7 +10,7 @@ final class NetClient: NetSocket {
static let defaultBufferSize:Int = 8192
weak var delegate:NetClientDelegate?
fileprivate(set) var service:Foundation.NetService?
private(set) var service:Foundation.NetService?
init(service:Foundation.NetService, inputStream:InputStream, outputStream:OutputStream) {
super.init()

View File

@ -153,50 +153,51 @@ enum FLVAudioCodec:UInt8 {
}
// MARK: -
struct FLVTag {
enum FLVTagType:UInt8 {
case audio = 8
case video = 9
case data = 18
enum TagType: UInt8 {
case audio = 8
case video = 9
case data = 18
var streamId:UInt16 {
switch self {
case .audio:
return RTMPChunk.StreamID.audio.rawValue
case .video:
return RTMPChunk.StreamID.video.rawValue
case .data:
return 0
}
}
var headerSize:Int {
switch self {
case .audio:
return 2
case .video:
return 5
case .data:
return 0
}
}
func createMessage(_ streamId: UInt32, timestamp: UInt32, buffer:Foundation.Data) -> RTMPMessage {
switch self {
case .audio:
return RTMPAudioMessage(streamId: streamId, timestamp: timestamp, buffer: buffer)
case .video:
return RTMPVideoMessage(streamId: streamId, timestamp: timestamp, buffer: buffer)
case .data:
return RTMPDataMessage(objectEncoding: 0x00)
}
var streamId:UInt16 {
switch self {
case .audio:
return RTMPChunk.StreamID.audio.rawValue
case .video:
return RTMPChunk.StreamID.video.rawValue
case .data:
return 0
}
}
var headerSize:Int {
switch self {
case .audio:
return 2
case .video:
return 5
case .data:
return 0
}
}
func message(with streamId: UInt32, timestamp: UInt32, buffer:Data) -> RTMPMessage {
switch self {
case .audio:
return RTMPAudioMessage(streamId: streamId, timestamp: timestamp, buffer: buffer)
case .video:
return RTMPVideoMessage(streamId: streamId, timestamp: timestamp, buffer: buffer)
case .data:
return RTMPDataMessage(objectEncoding: 0x00)
}
}
}
// MARK: -
struct FLVTag {
static let headerSize = 11
var tagType:TagType = .data
var tagType:FLVTagType = .data
var dataSize:UInt32 = 0
var timestamp:UInt32 = 0
var timestampExtended:UInt8 = 0

View File

@ -5,10 +5,12 @@ import Foundation
*/
open class Responder: NSObject {
private var result:(_ data:[Any?]) -> Void
private var status:((_ data:[Any?]) -> Void)?
public typealias Handler = (_ data:[Any?]) -> Void
public init(result:@escaping (_ data:[Any?]) -> Void, status:((_ data:[Any?]) -> Void)? = nil) {
private var result:Handler
private var status:Handler?
public init(result:@escaping Handler, status:Handler? = nil) {
self.result = result
self.status = status
}
@ -330,18 +332,6 @@ open class RTMPConnection: EventDispatcher {
}
}
@objc private func on(timer:Timer) {
let totalBytesIn:Int64 = self.totalBytesIn
let totalBytesOut:Int64 = self.totalBytesOut
currentBytesInPerSecond = Int32(totalBytesIn - previousTotalBytesIn)
currentBytesOutPerSecond = Int32(totalBytesOut - previousTotalBytesOut)
previousTotalBytesIn = totalBytesIn
previousTotalBytesOut = totalBytesOut
for (_, stream) in streams {
stream.on(timer: timer)
}
}
fileprivate func createConnectionChunk() -> RTMPChunk? {
guard let uri:URL = uri else {
return nil
@ -378,6 +368,18 @@ open class RTMPConnection: EventDispatcher {
return RTMPChunk(message: message)
}
@objc private func on(timer:Timer) {
let totalBytesIn:Int64 = self.totalBytesIn
let totalBytesOut:Int64 = self.totalBytesOut
currentBytesInPerSecond = Int32(totalBytesIn - previousTotalBytesIn)
currentBytesOutPerSecond = Int32(totalBytesOut - previousTotalBytesOut)
previousTotalBytesIn = totalBytesIn
previousTotalBytesOut = totalBytesOut
for (_, stream) in streams {
stream.on(timer: timer)
}
}
}
extension RTMPConnection: RTMPSocketDelegate {

View File

@ -345,7 +345,7 @@ final class RTMPCommandMessage: RTMPMessage {
case "close":
connection.close()
default:
connection.dispatch(type: Event.RTMP_STATUS, bubbles: false, data: arguments.isEmpty ? nil : arguments[0])
connection.dispatch(Event.RTMP_STATUS, bubbles: false, data: arguments.isEmpty ? nil : arguments[0])
}
return
}
@ -371,7 +371,7 @@ final class RTMPDataMessage: RTMPMessage {
var handlerName:String = ""
var arguments:[Any?] = []
fileprivate var serializer:AMFSerializer = AMF0Serializer()
private var serializer:AMFSerializer = AMF0Serializer()
override var payload:[UInt8] {
get {
@ -533,10 +533,10 @@ final class RTMPSharedObjectMessage: RTMPMessage {
final class RTMPAudioMessage: RTMPMessage {
var config:AudioSpecificConfig?
fileprivate(set) var codec:FLVAudioCodec = .unknown
fileprivate(set) var soundRate:FLVSoundRate = .kHz44
fileprivate(set) var soundSize:FLVSoundSize = .snd8bit
fileprivate(set) var soundType:FLVSoundType = .stereo
private(set) var codec:FLVAudioCodec = .unknown
private(set) var soundRate:FLVSoundRate = .kHz44
private(set) var soundSize:FLVSoundSize = .snd8bit
private(set) var soundType:FLVSoundType = .stereo
var soundData:[UInt8] {
let data:[UInt8] = payload.isEmpty ? [] : Array(payload[codec.headerSize..<payload.count])
@ -618,8 +618,8 @@ final class RTMPAudioMessage: RTMPMessage {
7.1.5. Video Message (9)
*/
final class RTMPVideoMessage: RTMPMessage {
fileprivate(set) var codec:FLVVideoCodec = .unknown
fileprivate(set) var status:OSStatus = noErr
private(set) var codec:FLVVideoCodec = .unknown
private(set) var status:OSStatus = noErr
init() {
super.init(type: .video)
@ -638,7 +638,7 @@ final class RTMPVideoMessage: RTMPMessage {
return
}
OSAtomicAdd64(Int64(payload.count), &stream.info.byteCount)
guard FLVTag.TagType.video.headerSize < payload.count else {
guard FLVTagType.video.headerSize < payload.count else {
return
}
switch payload[1] {
@ -661,7 +661,7 @@ final class RTMPVideoMessage: RTMPMessage {
decodeTimeStamp: kCMTimeInvalid
)
let bytes:[UInt8] = Array(payload[FLVTag.TagType.video.headerSize..<payload.count])
let bytes:[UInt8] = Array(payload[FLVTagType.video.headerSize..<payload.count])
var sample:[UInt8] = bytes
let sampleSize:Int = bytes.count
var blockBuffer:CMBlockBuffer?
@ -680,7 +680,7 @@ final class RTMPVideoMessage: RTMPMessage {
func createFormatDescription(_ stream: RTMPStream) -> OSStatus {
var config:AVCConfigurationRecord = AVCConfigurationRecord()
config.bytes = Array(payload[FLVTag.TagType.video.headerSize..<payload.count])
config.bytes = Array(payload[FLVTagType.video.headerSize..<payload.count])
return config.createFormatDescription(&stream.mixer.videoIO.formatDescription)
}
}
@ -764,7 +764,7 @@ final class RTMPUserControlMessage: RTMPMessage {
message: RTMPUserControlMessage(event: .pong, value: value)
))
case .bufferEmpty, .bufferFull:
connection.streams[UInt32(value)]?.dispatch(type: "rtmpStatus", bubbles: false, data: [
connection.streams[UInt32(value)]?.dispatch("rtmpStatus", bubbles: false, data: [
"level": "status",
"code": description,
"description": ""

View File

@ -8,8 +8,12 @@ protocol RTMPMuxerDelegate: class {
// MARK: -
final class RTMPMuxer {
weak var delegate:RTMPMuxerDelegate? = nil
static let aac:UInt8 = FLVAudioCodec.aac.rawValue << 4 | FLVSoundRate.kHz44.rawValue << 2 | FLVSoundSize.snd16bit.rawValue << 1 | FLVSoundType.stereo.rawValue
weak var delegate:RTMPMuxerDelegate? = nil
fileprivate var avcC:Data?
fileprivate var audioDecorderSpecificConfig:Data?
fileprivate var timestamps:[Int:Double] = [:]
fileprivate var audioTimestamp:CMTime = kCMTimeZero
fileprivate var videoTimestamp:CMTime = kCMTimeZero
@ -21,7 +25,6 @@ final class RTMPMuxer {
extension RTMPMuxer: AudioEncoderDelegate {
// MARK: AudioEncoderDelegate
private static let aac:UInt8 = FLVAudioCodec.aac.rawValue << 4 | FLVSoundRate.kHz44.rawValue << 2 | FLVSoundSize.snd16bit.rawValue << 1 | FLVSoundType.stereo.rawValue
func didSetFormatDescription(audio formatDescription: CMFormatDescription?) {
guard let formatDescription:CMFormatDescription = formatDescription else {
@ -95,3 +98,49 @@ extension RTMPMuxer: VideoEncoderDelegate {
videoTimestamp = decodeTimeStamp
}
}
extension RTMPMuxer: MP4SamplerDelegate {
// MP4SampleDelegate
func didSet(avcC: Data, withType:Int) {
if (avcC == self.avcC) {
return
}
var buffer:Data = Data([FLVFrameType.key.rawValue << 4 | FLVVideoCodec.avc.rawValue, FLVAVCPacketType.seq.rawValue, 0, 0, 0])
buffer.append(avcC)
delegate?.sampleOutput(video: buffer, withTimestamp: 0, muxer: self)
self.avcC = avcC
}
func didSet(audioDecorderSpecificConfig: Data, withType:Int) {
if (withType == 2) {
return
}
if (audioDecorderSpecificConfig == self.audioDecorderSpecificConfig) {
return
}
var buffer:Data = Data([RTMPMuxer.aac, FLVAACPacketType.raw.rawValue])
buffer.append(audioDecorderSpecificConfig)
delegate?.sampleOutput(audio: buffer, withTimestamp: 0, muxer: self)
self.audioDecorderSpecificConfig = audioDecorderSpecificConfig
}
func output(data:Data, withType:Int, currentTime:Double, keyframe:Bool) {
let delta:Double = (timestamps[withType] == nil) ? 0 : timestamps[withType]!
switch withType {
case 0:
print(delta)
let compositionTime:Int32 = 0
var buffer:Data = Data([((keyframe ? FLVFrameType.key.rawValue : FLVFrameType.inter.rawValue) << 4) | FLVVideoCodec.avc.rawValue, FLVAVCPacketType.nal.rawValue])
buffer.append(contentsOf: compositionTime.bigEndian.bytes[1..<4])
buffer.append(data)
delegate?.sampleOutput(video: buffer, withTimestamp: delta, muxer: self)
case 1:
var buffer:Data = Data([RTMPMuxer.aac, FLVAACPacketType.raw.rawValue])
buffer.append(data)
delegate?.sampleOutput(audio: buffer, withTimestamp: delta, muxer: self)
default:
break
}
timestamps[withType] = currentTime
}
}

View File

@ -192,7 +192,7 @@ open class RTMPSharedObject: EventDispatcher {
}
changeList.append(change)
}
dispatch(type: Event.SYNC, bubbles: false, data: changeList)
dispatch(Event.SYNC, bubbles: false, data: changeList)
}
func createChunk(_ events:[RTMPSharedObjectEvent]) -> RTMPChunk {

View File

@ -150,7 +150,7 @@ final class RTMPSocket: NetSocket, RTMPSocketCompatible {
override func didTimeout() {
deinitConnection(isDisconnected: false)
delegate?.dispatch(type: Event.IO_ERROR, bubbles: false, data: nil)
delegate?.dispatch(Event.IO_ERROR, bubbles: false, data: nil)
logger.warning("connection timedout")
}
}

View File

@ -224,7 +224,7 @@ open class RTMPStream: NetStream {
open static let defaultVideoBitrate:UInt32 = AVCEncoder.defaultBitrate
open internal(set) var info:RTMPStreamInfo = RTMPStreamInfo()
open fileprivate(set) var objectEncoding:UInt8 = RTMPConnection.defaultObjectEncoding
open fileprivate(set) dynamic var currentFPS:UInt8 = 0
open fileprivate(set) dynamic var currentFPS:UInt16 = 0
open var soundTransform:SoundTransform {
get { return audioPlayback.soundTransform }
set { audioPlayback.soundTransform = newValue }
@ -242,6 +242,7 @@ open class RTMPStream: NetStream {
send(handlerName: "@setDataFrame", arguments: "onMetaData", createMetaData())
mixer.audioIO.encoder.startRunning()
mixer.videoIO.encoder.startRunning()
sampler.startRunning()
if (howToPublish == .localRecord) {
mixer.recorder.fileName = info.resourceName
mixer.recorder.startRunning()
@ -256,8 +257,9 @@ open class RTMPStream: NetStream {
var videoTimestamp:Double = 0
fileprivate(set) var audioPlayback:RTMPAudioPlayback = RTMPAudioPlayback()
fileprivate var muxer:RTMPMuxer = RTMPMuxer()
fileprivate var frameCount:UInt8 = 0
fileprivate var chunkTypes:[FLVTag.TagType:Bool] = [:]
fileprivate var sampler:MP4Sampler = MP4Sampler()
fileprivate var frameCount:UInt16 = 0
fileprivate var chunkTypes:[FLVTagType:Bool] = [:]
fileprivate var dispatcher:IEventDispatcher!
fileprivate var howToPublish:RTMPStream.HowToPublish = .live
fileprivate var rtmpConnection:RTMPConnection
@ -390,6 +392,7 @@ open class RTMPStream: NetStream {
self.mixer.videoIO.encoder.delegate = nil
self.mixer.audioIO.encoder.stopRunning()
self.mixer.videoIO.encoder.stopRunning()
self.sampler.stopRunning()
self.mixer.recorder.stopRunning()
self.FCUnpublish()
self.rtmpConnection.socket.doOutput(chunk: RTMPChunk(
@ -419,6 +422,7 @@ open class RTMPStream: NetStream {
#endif
self.mixer.audioIO.encoder.delegate = self.muxer
self.mixer.videoIO.encoder.delegate = self.muxer
self.sampler.delegate = self.muxer
self.mixer.startRunning()
self.chunkTypes.removeAll()
self.FCPublish()
@ -491,6 +495,10 @@ open class RTMPStream: NetStream {
return metadata
}
func append(url:URL, completionHandler: MP4Sampler.Handler? = nil) {
sampler.append(file: url, completionHandler: completionHandler)
}
func on(timer:Timer) {
currentFPS = frameCount
frameCount = 0
@ -541,8 +549,8 @@ extension RTMPStream: IEventDispatcher {
public func dispatch(event:Event) {
dispatcher.dispatch(event: event)
}
public func dispatch(type:String, bubbles:Bool, data:Any?) {
dispatcher.dispatch(type: type, bubbles: bubbles, data: data)
public func dispatch(_ type:String, bubbles:Bool, data:Any?) {
dispatcher.dispatch(type, bubbles: bubbles, data: data)
}
}
@ -552,11 +560,11 @@ extension RTMPStream: RTMPMuxerDelegate {
guard readyState == .publishing else {
return
}
let type:FLVTag.TagType = .audio
let type:FLVTagType = .audio
let length:Int = rtmpConnection.socket.doOutput(chunk: RTMPChunk(
type: chunkTypes[type] == nil ? .zero : .one,
streamId: type.streamId,
message: type.createMessage(id, timestamp: UInt32(audioTimestamp), buffer: buffer)
message: type.message(with: id, timestamp: UInt32(audioTimestamp), buffer: buffer)
))
chunkTypes[type] = true
OSAtomicAdd64(Int64(length), &info.byteCount)
@ -567,15 +575,15 @@ extension RTMPStream: RTMPMuxerDelegate {
guard readyState == .publishing else {
return
}
let type:FLVTag.TagType = .video
let type:FLVTagType = .video
let length:Int = rtmpConnection.socket.doOutput(chunk: RTMPChunk(
type: chunkTypes[type] == nil ? .zero : .one,
streamId: type.streamId,
message: type.createMessage(id, timestamp: UInt32(videoTimestamp), buffer: buffer)
message: type.message(with: id, timestamp: UInt32(videoTimestamp), buffer: buffer)
))
chunkTypes[type] = true
OSAtomicAdd64(Int64(length), &info.byteCount)
videoTimestamp = withTimestamp + (videoTimestamp - floor(videoTimestamp))
frameCount = (frameCount + 1) & 0xFF
frameCount += 1
}
}

View File

@ -43,15 +43,16 @@ final class RTMPTSocket: NSObject, RTMPSocketCompatible {
}
private var delay:UInt8 = 1
private var mutex:Mutex = Mutex()
private var index:Int64 = 0
private var events:[Event] = []
private var baseURL:URL!
private var session:URLSession!
private var c2packet:[UInt8] = []
private var isPending:Bool = false
private let outputQueue:DispatchQueue = DispatchQueue(label: "com.github.shgoo4405.lf.RTMPTSocket.output")
private var connectionID:String!
private var outputBuffer:[UInt8] = []
private var lastResponse:Date = Date()
override init() {
super.init()
@ -77,20 +78,13 @@ final class RTMPTSocket: NSObject, RTMPSocketCompatible {
for chunk in chunks {
bytes.append(contentsOf: chunk)
}
do {
try mutex.lock()
outputBuffer.append(contentsOf: bytes)
if (!isPending) {
isPending = true
doOutput(bytes: outputBuffer)
outputBuffer.removeAll()
outputQueue.sync {
self.outputBuffer.append(contentsOf: bytes)
if (!self.isPending) {
self.isPending = true
self.doOutput(bytes: self.outputBuffer)
self.outputBuffer.removeAll()
}
mutex.unlock()
} catch {
logger.warning("")
}
if (logger.isEnabledForLogLevel(.verbose)) {
logger.verbose("\(chunk)")
}
return bytes.count
}
@ -121,17 +115,14 @@ final class RTMPTSocket: NSObject, RTMPSocketCompatible {
return
}
do {
try mutex.lock()
if (outputBuffer.isEmpty) {
isPending = false
lastResponse = Date()
outputQueue.sync {
if (self.outputBuffer.isEmpty) {
self.isPending = false
} else {
doOutput(bytes: outputBuffer)
outputBuffer.removeAll()
self.doOutput(bytes: outputBuffer)
self.outputBuffer.removeAll()
}
mutex.unlock()
} catch {
logger.warning()
}
guard

View File

@ -7,7 +7,7 @@ public protocol IEventDispatcher: class {
func addEventListener(_ type:String, selector:Selector, observer:AnyObject?, useCapture:Bool)
func removeEventListener(_ type:String, selector:Selector, observer:AnyObject?, useCapture:Bool)
func dispatch(event:Event)
func dispatch(type:String, bubbles:Bool, data:Any?)
func dispatch(_ type:String, bubbles:Bool, data:Any?)
}
public enum EventPhase: UInt8 {
@ -92,7 +92,7 @@ open class EventDispatcher: NSObject, IEventDispatcher {
event.target = nil
}
public final func dispatch(type:String, bubbles:Bool, data:Any?) {
public final func dispatch(_ type:String, bubbles:Bool, data:Any?) {
dispatch(event: Event(type: type, bubbles: bubbles, data: data))
}
}

View File

@ -26,6 +26,9 @@
29245AEE1D32347E00AFFB9A /* VideoGravityUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29245AEC1D3233EB00AFFB9A /* VideoGravityUtil.swift */; };
29245AEF1D32348400AFFB9A /* VideoGravityUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29245AEC1D3233EB00AFFB9A /* VideoGravityUtil.swift */; };
292AC17C1CF4C871004F5730 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2942424C1CF4C01300D65DCB /* MD5.swift */; };
292D8A331D8B293300DBECE2 /* MP4Sampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292D8A321D8B293300DBECE2 /* MP4Sampler.swift */; };
292D8A341D8B294900DBECE2 /* MP4Sampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292D8A321D8B293300DBECE2 /* MP4Sampler.swift */; };
292D8A351D8B294E00DBECE2 /* MP4Reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29798E511CE5DF1900F5CBD0 /* MP4Reader.swift */; };
2931204C1D4522CF00B14211 /* RTSPRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2931204B1D4522CF00B14211 /* RTSPRequest.swift */; };
2931204E1D4522E400B14211 /* RTSPResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2931204D1D4522E400B14211 /* RTSPResponse.swift */; };
2931204F1D4529F900B14211 /* RTSPRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2931204B1D4522CF00B14211 /* RTSPRequest.swift */; };
@ -36,6 +39,11 @@
2942424F1CF4C02300D65DCB /* MD5Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2942424E1CF4C02300D65DCB /* MD5Tests.swift */; };
294852571D852499002DE492 /* RTMPTSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294852551D84BFAD002DE492 /* RTMPTSocket.swift */; };
2955F51F1D09EBAD004CC995 /* VisualEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296897461CDB01D20074D5F0 /* VisualEffect.swift */; };
2962425E1D8BFC7B00C451A3 /* MovieClipHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 292D8A301D8B233C00DBECE2 /* MovieClipHandler.swift */; };
296242611D8DB86500C451A3 /* TSReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2962425F1D8DB86500C451A3 /* TSReader.swift */; };
296242621D8DB86500C451A3 /* TSWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296242601D8DB86500C451A3 /* TSWriter.swift */; };
296242631D8DBA8C00C451A3 /* TSReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2962425F1D8DB86500C451A3 /* TSReader.swift */; };
296242641D8DBA9000C451A3 /* TSWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296242601D8DB86500C451A3 /* TSWriter.swift */; };
2965434F1D62FAED00734698 /* RTMPConnectionTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2965434E1D62FAED00734698 /* RTMPConnectionTests.m */; };
296543601D62FE6A00734698 /* AudioUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296543561D62FE6200734698 /* AudioUtil.swift */; };
296543611D62FE7100734698 /* GLLFView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 296543571D62FE6200734698 /* GLLFView.swift */; };
@ -54,7 +62,7 @@
2976A4821D4902CE00B53EF2 /* IOComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2976A4801D49025B00B53EF2 /* IOComponent.swift */; };
2976A4861D4903C300B53EF2 /* DeviceUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2976A4851D4903C300B53EF2 /* DeviceUtil.swift */; };
2976A4871D49045700B53EF2 /* DeviceUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2976A4851D4903C300B53EF2 /* DeviceUtil.swift */; };
29798E521CE5DF1A00F5CBD0 /* MP4File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29798E511CE5DF1900F5CBD0 /* MP4File.swift */; };
29798E521CE5DF1A00F5CBD0 /* MP4Reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29798E511CE5DF1900F5CBD0 /* MP4Reader.swift */; };
29798E671CE610F500F5CBD0 /* lf.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29B8761B1CD701F900FC07DA /* lf.framework */; };
29798E691CE6110F00F5CBD0 /* ASClassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B876CE1CD70CE700FC07DA /* ASClassTests.swift */; };
29798E6A1CE6110F00F5CBD0 /* ByteArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B876CF1CD70CE700FC07DA /* ByteArrayTests.swift */; };
@ -81,7 +89,6 @@
299B131D1D35272D00A1E8F5 /* ScreenCaptureSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 299B131C1D35272D00A1E8F5 /* ScreenCaptureSession.swift */; };
299B13271D3B751400A1E8F5 /* LFView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 299B13261D3B751400A1E8F5 /* LFView.swift */; };
29A39C8E1D85BF6F007C27E9 /* BroadcastViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A39C831D85BF21007C27E9 /* BroadcastViewController.swift */; };
29A39C901D85BF9C007C27E9 /* SampleHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A39C8A1D85BF30007C27E9 /* SampleHandler.swift */; };
29A39C921D85CF5F007C27E9 /* RTMPSampleHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29A39C911D85CF5E007C27E9 /* RTMPSampleHandler.swift */; };
29AF3FCF1D7C744C00E41212 /* NetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29AF3FCE1D7C744C00E41212 /* NetStream.swift */; };
29AF3FD01D7C745200E41212 /* NetStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29AF3FCE1D7C744C00E41212 /* NetStream.swift */; };
@ -98,8 +105,6 @@
29B876781CD70ACE00FC07DA /* HTTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B876711CD70ACE00FC07DA /* HTTPService.swift */; };
29B876791CD70ACE00FC07DA /* HTTPStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B876721CD70ACE00FC07DA /* HTTPStream.swift */; };
29B8767A1CD70ACE00FC07DA /* M3U.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B876731CD70ACE00FC07DA /* M3U.swift */; };
29B8767B1CD70ACE00FC07DA /* TSReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B876741CD70ACE00FC07DA /* TSReader.swift */; };
29B8767C1CD70ACE00FC07DA /* TSWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B876751CD70ACE00FC07DA /* TSWriter.swift */; };
29B876831CD70AE800FC07DA /* AudioSpecificConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B8767D1CD70AE800FC07DA /* AudioSpecificConfig.swift */; };
29B876841CD70AE800FC07DA /* H264+AVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B8767E1CD70AE800FC07DA /* H264+AVC.swift */; };
29B876851CD70AE800FC07DA /* NALUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B8767F1CD70AE800FC07DA /* NALUnit.swift */; };
@ -143,8 +148,6 @@
29B876F81CD70D5900FC07DA /* HTTPService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B876711CD70ACE00FC07DA /* HTTPService.swift */; };
29B876F91CD70D5900FC07DA /* HTTPStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B876721CD70ACE00FC07DA /* HTTPStream.swift */; };
29B876FA1CD70D5900FC07DA /* M3U.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B876731CD70ACE00FC07DA /* M3U.swift */; };
29B876FB1CD70D5A00FC07DA /* TSReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B876741CD70ACE00FC07DA /* TSReader.swift */; };
29B876FC1CD70D5A00FC07DA /* TSWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B876751CD70ACE00FC07DA /* TSWriter.swift */; };
29B876FD1CD70D5A00FC07DA /* AudioSpecificConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B8767D1CD70AE800FC07DA /* AudioSpecificConfig.swift */; };
29B876FE1CD70D5A00FC07DA /* H264+AVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B8767E1CD70AE800FC07DA /* H264+AVC.swift */; };
29B876FF1CD70D5A00FC07DA /* NALUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29B8767F1CD70AE800FC07DA /* NALUnit.swift */; };
@ -282,6 +285,8 @@
291F4E361CF206E200F59C51 /* Icon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = Icon.png; path = Examples/iOS/Icon.png; sourceTree = "<group>"; };
2923A1FA1D63011E0019FBCD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Examples/macOS/Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
29245AEC1D3233EB00AFFB9A /* VideoGravityUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = VideoGravityUtil.swift; path = Sources/Util/VideoGravityUtil.swift; sourceTree = SOURCE_ROOT; };
292D8A301D8B233C00DBECE2 /* MovieClipHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MovieClipHandler.swift; path = Examples/iOS/Screencast/MovieClipHandler.swift; sourceTree = "<group>"; };
292D8A321D8B293300DBECE2 /* MP4Sampler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MP4Sampler.swift; path = Sources/ISO/MP4Sampler.swift; sourceTree = SOURCE_ROOT; };
2931204B1D4522CF00B14211 /* RTSPRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RTSPRequest.swift; path = Sources/RTSP/RTSPRequest.swift; sourceTree = SOURCE_ROOT; };
2931204D1D4522E400B14211 /* RTSPResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RTSPResponse.swift; path = Sources/RTSP/RTSPResponse.swift; sourceTree = SOURCE_ROOT; };
293C74361D85D56D001ED43C /* MainInterface.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = MainInterface.storyboard; path = Examples/iOS/ScreencastUI/Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
@ -289,6 +294,8 @@
2942424E1CF4C02300D65DCB /* MD5Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MD5Tests.swift; sourceTree = "<group>"; };
2945CBBD1B4BE66000104112 /* lf.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = lf.framework; sourceTree = BUILT_PRODUCTS_DIR; };
294852551D84BFAD002DE492 /* RTMPTSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RTMPTSocket.swift; path = Sources/RTMP/RTMPTSocket.swift; sourceTree = SOURCE_ROOT; };
2962425F1D8DB86500C451A3 /* TSReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TSReader.swift; path = Sources/ISO/TSReader.swift; sourceTree = SOURCE_ROOT; };
296242601D8DB86500C451A3 /* TSWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TSWriter.swift; path = Sources/ISO/TSWriter.swift; sourceTree = SOURCE_ROOT; };
2965434E1D62FAED00734698 /* RTMPConnectionTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RTMPConnectionTests.m; sourceTree = "<group>"; };
296543561D62FE6200734698 /* AudioUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AudioUtil.swift; path = Platforms/macOS/AudioUtil.swift; sourceTree = "<group>"; };
296543571D62FE6200734698 /* GLLFView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GLLFView.swift; path = Platforms/macOS/GLLFView.swift; sourceTree = "<group>"; };
@ -312,7 +319,7 @@
2976A47D1D48C5C700B53EF2 /* AVMixerRecorder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AVMixerRecorder.swift; path = Sources/Media/AVMixerRecorder.swift; sourceTree = SOURCE_ROOT; };
2976A4801D49025B00B53EF2 /* IOComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = IOComponent.swift; path = Sources/Media/IOComponent.swift; sourceTree = SOURCE_ROOT; };
2976A4851D4903C300B53EF2 /* DeviceUtil.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DeviceUtil.swift; path = Sources/Util/DeviceUtil.swift; sourceTree = SOURCE_ROOT; };
29798E511CE5DF1900F5CBD0 /* MP4File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MP4File.swift; path = Sources/ISO/MP4File.swift; sourceTree = SOURCE_ROOT; };
29798E511CE5DF1900F5CBD0 /* MP4Reader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MP4Reader.swift; path = Sources/ISO/MP4Reader.swift; sourceTree = SOURCE_ROOT; };
29798E591CE60E5300F5CBD0 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
29798E5D1CE60E5300F5CBD0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
2981B7F31D7345D5002FA821 /* SessionDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SessionDescription.swift; path = Sources/RTSP/SessionDescription.swift; sourceTree = SOURCE_ROOT; };
@ -331,7 +338,6 @@
29A39C831D85BF21007C27E9 /* BroadcastViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BroadcastViewController.swift; path = Examples/iOS/ScreencastUI/BroadcastViewController.swift; sourceTree = "<group>"; };
29A39C841D85BF21007C27E9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Examples/iOS/ScreencastUI/Info.plist; sourceTree = "<group>"; };
29A39C881D85BF30007C27E9 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Examples/iOS/Screencast/Info.plist; sourceTree = "<group>"; };
29A39C8A1D85BF30007C27E9 /* SampleHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SampleHandler.swift; path = Examples/iOS/Screencast/SampleHandler.swift; sourceTree = "<group>"; };
29A39C911D85CF5E007C27E9 /* RTMPSampleHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RTMPSampleHandler.swift; path = Platforms/iOS/RTMPSampleHandler.swift; sourceTree = "<group>"; };
29AF3FCE1D7C744C00E41212 /* NetStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NetStream.swift; path = Sources/Net/NetStream.swift; sourceTree = SOURCE_ROOT; };
29B8761B1CD701F900FC07DA /* lf.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = lf.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@ -348,8 +354,6 @@
29B876711CD70ACE00FC07DA /* HTTPService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPService.swift; path = Sources/HTTP/HTTPService.swift; sourceTree = SOURCE_ROOT; };
29B876721CD70ACE00FC07DA /* HTTPStream.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPStream.swift; path = Sources/HTTP/HTTPStream.swift; sourceTree = SOURCE_ROOT; };
29B876731CD70ACE00FC07DA /* M3U.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = M3U.swift; path = Sources/HTTP/M3U.swift; sourceTree = SOURCE_ROOT; };
29B876741CD70ACE00FC07DA /* TSReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TSReader.swift; path = Sources/HTTP/TSReader.swift; sourceTree = SOURCE_ROOT; };
29B876751CD70ACE00FC07DA /* TSWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TSWriter.swift; path = Sources/HTTP/TSWriter.swift; sourceTree = SOURCE_ROOT; };
29B8767D1CD70AE800FC07DA /* AudioSpecificConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AudioSpecificConfig.swift; path = Sources/ISO/AudioSpecificConfig.swift; sourceTree = SOURCE_ROOT; };
29B8767E1CD70AE800FC07DA /* H264+AVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "H264+AVC.swift"; path = "Sources/ISO/H264+AVC.swift"; sourceTree = SOURCE_ROOT; };
29B8767F1CD70AE800FC07DA /* NALUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NALUnit.swift; path = Sources/ISO/NALUnit.swift; sourceTree = SOURCE_ROOT; };
@ -666,11 +670,14 @@
children = (
29B8767D1CD70AE800FC07DA /* AudioSpecificConfig.swift */,
29B8767E1CD70AE800FC07DA /* H264+AVC.swift */,
29798E511CE5DF1900F5CBD0 /* MP4File.swift */,
29798E511CE5DF1900F5CBD0 /* MP4Reader.swift */,
292D8A321D8B293300DBECE2 /* MP4Sampler.swift */,
29B8767F1CD70AE800FC07DA /* NALUnit.swift */,
29B876801CD70AE800FC07DA /* PacketizedElementaryStream.swift */,
29B876811CD70AE800FC07DA /* ProgramSpecific.swift */,
29B876821CD70AE800FC07DA /* TransportStream.swift */,
2962425F1D8DB86500C451A3 /* TSReader.swift */,
296242601D8DB86500C451A3 /* TSWriter.swift */,
);
name = ISO;
sourceTree = "<group>";
@ -683,8 +690,6 @@
29B876711CD70ACE00FC07DA /* HTTPService.swift */,
29B876721CD70ACE00FC07DA /* HTTPStream.swift */,
29B876731CD70ACE00FC07DA /* M3U.swift */,
29B876741CD70ACE00FC07DA /* TSReader.swift */,
29B876751CD70ACE00FC07DA /* TSWriter.swift */,
);
name = HTTP;
sourceTree = "<group>";
@ -823,7 +828,7 @@
isa = PBXGroup;
children = (
29A39C881D85BF30007C27E9 /* Info.plist */,
29A39C8A1D85BF30007C27E9 /* SampleHandler.swift */,
292D8A301D8B233C00DBECE2 /* MovieClipHandler.swift */,
);
name = Screencast;
sourceTree = "<group>";
@ -1485,7 +1490,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
29A39C901D85BF9C007C27E9 /* SampleHandler.swift in Sources */,
2962425E1D8BFC7B00C451A3 /* MovieClipHandler.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1511,7 +1516,6 @@
29B8766E1CD70AB300FC07DA /* SiwftCore+Extension.swift in Sources */,
2901A4EE1D437170002BBD23 /* ClockedQueue.swift in Sources */,
29B876941CD70AFE00FC07DA /* SoundTransform.swift in Sources */,
29B8767B1CD70ACE00FC07DA /* TSReader.swift in Sources */,
2931204C1D4522CF00B14211 /* RTSPRequest.swift in Sources */,
29B876861CD70AE800FC07DA /* PacketizedElementaryStream.swift in Sources */,
29B876761CD70ACE00FC07DA /* HTTPRequest.swift in Sources */,
@ -1519,6 +1523,7 @@
299AE0E51D44EC7800D26A49 /* RTSPSocket.swift in Sources */,
29DD70E61D68CF020021904A /* RTPPacket.swift in Sources */,
29B876AD1CD70B2800FC07DA /* ASClass.swift in Sources */,
296242611D8DB86500C451A3 /* TSReader.swift in Sources */,
29B8766B1CD70AB300FC07DA /* Foundation+Extension.swift in Sources */,
2981B7F41D7345D5002FA821 /* SessionDescription.swift in Sources */,
2931204E1D4522E400B14211 /* RTSPResponse.swift in Sources */,
@ -1529,10 +1534,10 @@
299B13271D3B751400A1E8F5 /* LFView.swift in Sources */,
29B876AF1CD70B2800FC07DA /* RTMPChunk.swift in Sources */,
29B876841CD70AE800FC07DA /* H264+AVC.swift in Sources */,
296242621D8DB86500C451A3 /* TSWriter.swift in Sources */,
29B8769B1CD70B1100FC07DA /* MIME.swift in Sources */,
29A39C921D85CF5F007C27E9 /* RTMPSampleHandler.swift in Sources */,
29B8769C1CD70B1100FC07DA /* NetClient.swift in Sources */,
29B8767C1CD70ACE00FC07DA /* TSWriter.swift in Sources */,
29B876871CD70AE800FC07DA /* ProgramSpecific.swift in Sources */,
29B876B01CD70B2800FC07DA /* RTMPConnection.swift in Sources */,
29B876B61CD70B2800FC07DA /* RTMPStream.swift in Sources */,
@ -1558,7 +1563,8 @@
29B8769D1CD70B1100FC07DA /* NetService.swift in Sources */,
29B8769E1CD70B1100FC07DA /* NetSocket.swift in Sources */,
29B876791CD70ACE00FC07DA /* HTTPStream.swift in Sources */,
29798E521CE5DF1A00F5CBD0 /* MP4File.swift in Sources */,
292D8A331D8B293300DBECE2 /* MP4Sampler.swift in Sources */,
29798E521CE5DF1A00F5CBD0 /* MP4Reader.swift in Sources */,
29B876AC1CD70B2800FC07DA /* AMF3Serializer.swift in Sources */,
29B876921CD70AFE00FC07DA /* AVMixer.swift in Sources */,
29B876911CD70AFE00FC07DA /* AudioStreamPlayback.swift in Sources */,
@ -1616,16 +1622,18 @@
29B876F91CD70D5900FC07DA /* HTTPStream.swift in Sources */,
296543631D62FE9000734698 /* LFView.swift in Sources */,
29B876FA1CD70D5900FC07DA /* M3U.swift in Sources */,
29B876FB1CD70D5A00FC07DA /* TSReader.swift in Sources */,
29B876FC1CD70D5A00FC07DA /* TSWriter.swift in Sources */,
292D8A341D8B294900DBECE2 /* MP4Sampler.swift in Sources */,
29B876FD1CD70D5A00FC07DA /* AudioSpecificConfig.swift in Sources */,
296242631D8DBA8C00C451A3 /* TSReader.swift in Sources */,
29B876FE1CD70D5A00FC07DA /* H264+AVC.swift in Sources */,
294852571D852499002DE492 /* RTMPTSocket.swift in Sources */,
29245AEE1D32347E00AFFB9A /* VideoGravityUtil.swift in Sources */,
292D8A351D8B294E00DBECE2 /* MP4Reader.swift in Sources */,
29B876FF1CD70D5A00FC07DA /* NALUnit.swift in Sources */,
29B877001CD70D5A00FC07DA /* PacketizedElementaryStream.swift in Sources */,
29B877011CD70D5A00FC07DA /* ProgramSpecific.swift in Sources */,
29B877021CD70D5A00FC07DA /* TransportStream.swift in Sources */,
296242641D8DBA9000C451A3 /* TSWriter.swift in Sources */,
29B877031CD70D5A00FC07DA /* AudioIOComponent.swift in Sources */,
29B877041CD70D5A00FC07DA /* AudioStreamPlayback.swift in Sources */,
2931204F1D4529F900B14211 /* RTSPRequest.swift in Sources */,