diff --git a/Sources/HTTP/HTTPStream.swift b/Sources/HTTP/HTTPStream.swift index 14c41cafb..5ee5581a8 100644 --- a/Sources/HTTP/HTTPStream.swift +++ b/Sources/HTTP/HTTPStream.swift @@ -14,7 +14,7 @@ open class HTTPStream: NetStream { /// The name of stream. private(set) var name: String? - private lazy var tsWriter = TSFileWriter() + public lazy var tsWriter = TSFileWriter() open func publish(_ name: String?) { lockQueue.async { diff --git a/Sources/HTTP/M3U.swift b/Sources/HTTP/M3U.swift index f2441b15f..675543740 100644 --- a/Sources/HTTP/M3U.swift +++ b/Sources/HTTP/M3U.swift @@ -3,19 +3,22 @@ import Foundation /** - seealso: https://tools.ietf.org/html/draft-pantos-http-live-streaming-19 */ -struct M3U { - static let header: String = "#EXTM3U" - static let defaultVersion: Int = 3 - +public struct M3U { + public static let header: String = "#EXTM3U" + public static let defaultVersion: Int = 3 + var version: Int = M3U.defaultVersion var mediaList: [M3UMediaInfo] = [] - var mediaSequence: Int = 0 - var targetDuration: Double = 5 + public var mediaSequence: Int = 0 + public var targetDuration: Double = 5 + public init() { + + } } extension M3U: CustomStringConvertible { // MARK: CustomStringConvertible - var description: String { + public var description: String { var lines: [String] = [ "#EXTM3U", "#EXT-X-VERSION:\(version)", @@ -23,8 +26,15 @@ extension M3U: CustomStringConvertible { "#EXT-X-TARGETDURATION:\(Int(targetDuration))" ] for info in mediaList { - lines.append("#EXTINF:\(info.duration),") - lines.append(info.url.pathComponents.last!) + if info.isSkipped ?? false { + continue + } + if info.isDiscontinuous { + lines.append("#EXT-X-DISCONTINUITY") + } + lines.append("#EXTINF:\(info.duration),") + lines.append(info.url.pathComponents.last!) + } return lines.joined(separator: "\r\n") } @@ -34,4 +44,6 @@ extension M3U: CustomStringConvertible { struct M3UMediaInfo { let url: URL let duration: Double + var isDiscontinuous: Bool + var isSkipped: Bool? } diff --git a/Sources/MPEG/TSWriter.swift b/Sources/MPEG/TSWriter.swift index 6ca309608..2788ce38f 100644 --- a/Sources/MPEG/TSWriter.swift +++ b/Sources/MPEG/TSWriter.swift @@ -10,6 +10,8 @@ import SwiftPMSupport public protocol TSWriterDelegate: AnyObject { func writer(_ writer: TSWriter, didRotateFileHandle timestamp: CMTime) func writer(_ writer: TSWriter, didOutput data: Data) + func didGenerateTS(_ file: URL) + func didGenerateM3U8(_ file: URL) } public extension TSWriterDelegate { @@ -33,7 +35,11 @@ public class TSWriter: Running { /// This instance is running to process(true) or not(false). public internal(set) var isRunning: Atomic = .init(false) /// The exptected medias = [.video, .audio]. - public var expectedMedias: Set = [] + public var expectedMedias: Set = [] { + didSet { + print("expected medias \(expectedMedias.count)") + } + } var audioContinuityCounter: UInt8 = 0 var videoContinuityCounter: UInt8 = 0 @@ -295,15 +301,19 @@ extension TSWriter: VideoCodecDelegate { } } -class TSFileWriter: TSWriter { - static let defaultSegmentCount: Int = 3 - static let defaultSegmentMaxCount: Int = 12 +public class TSFileWriter: TSWriter { + static let defaultSegmentCount: Int = 10000 + static let defaultSegmentMaxCount: Int = 10000 + public var baseFolder: URL? + public var shouldAppendToStream: Bool = false var segmentMaxCount: Int = TSFileWriter.defaultSegmentMaxCount private(set) var files: [M3UMediaInfo] = [] private var currentFileHandle: FileHandle? private var currentFileURL: URL? private var sequence: Int = 0 + public var isDiscontinuity = false + private var isTerminating: Bool = false var playlist: String { var m3u8 = M3U() @@ -331,7 +341,11 @@ class TSFileWriter: TSWriter { return } let fileManager = FileManager.default - + guard let base = baseFolder else { + + return + } + #if os(OSX) let bundleIdentifier: String? = Bundle.main.bundleIdentifier let temp: String = bundleIdentifier == nil ? NSTemporaryDirectory() : NSTemporaryDirectory() + bundleIdentifier! + "/" @@ -347,13 +361,31 @@ class TSFileWriter: TSWriter { } } - let filename: String = Int(timestamp.seconds).description + ".ts" - let url = URL(fileURLWithPath: temp + filename) - - if let currentFileURL: URL = currentFileURL { - files.append(M3UMediaInfo(url: currentFileURL, duration: duration)) + // let filename: String = Int(timestamp.seconds).description + ".ts" + let playlistUrl = base.appendingPathComponent("ScreenRecording.m3u8") + let filename = String(format: "part%.5i.ts", sequence) + let url = base.appendingPathComponent(filename) + + if isTerminating { return } + + // Toss part0 due to bad duration calculation. + // shouldAppendToStream is true when countdown-completed arrives + if let currentUrl = currentFileURL, sequence > 1 && shouldAppendToStream { + // let asset = AVAsset(url: currentUrl) + // let calculatedDuration = CMTimeGetSeconds(asset.duration) + // Logger.info("Duration: \(duration) Calculated duration: \(calculatedDuration)") + files.append(M3UMediaInfo(url: currentUrl, duration: duration, isDiscontinuous: isDiscontinuity)) + isDiscontinuity = false + fileManager.createFile(atPath: playlistUrl.path, contents: playlist.data(using: .utf8), attributes: nil) + notifyDelegate(tsUrl: currentUrl, playlistUrl: playlistUrl) + } + sequence += 1 - } + if shouldAppendToStream { + segmentDuration = 2 + } else { + segmentDuration = 1 + } fileManager.createFile(atPath: url.path, contents: nil, attributes: nil) if TSFileWriter.defaultSegmentMaxCount <= files.count { @@ -380,7 +412,31 @@ class TSFileWriter: TSWriter { writeProgram() rotatedTimestamp = timestamp } - + + + func notifyDelegate(tsUrl: URL, playlistUrl: URL) { + self.delegate?.didGenerateTS(tsUrl) + self.delegate?.didGenerateM3U8(playlistUrl) + } + + private func writeFinal() { + guard let base = baseFolder else { + return + } + DispatchQueue.main.asyncAfter(deadline: .now()+(TSWriter.defaultSegmentDuration+1)) { + if let currentUrl = self.currentFileURL { + let playlistUrl = base.appendingPathComponent("ScreenRecording.m3u8") + + self.files.append(M3UMediaInfo(url: currentUrl, duration: TSWriter.defaultSegmentDuration, isDiscontinuous: false)) + FileManager.default.createFile(atPath: playlistUrl.path, contents: self.playlist.data(using: .utf8), attributes: nil) + self.notifyDelegate(tsUrl: currentUrl, playlistUrl: playlistUrl) + } + self.currentFileURL = nil + self.currentFileHandle = nil + super.stopRunning() + } + } + override func write(_ data: Data) { nstry({ self.currentFileHandle?.write(data) @@ -391,14 +447,21 @@ class TSFileWriter: TSWriter { super.write(data) } - override func stopRunning() { + public override func stopRunning() { guard !isRunning.value else { return } - currentFileURL = nil - currentFileHandle = nil - removeFiles() - super.stopRunning() + nstry({ + self.currentFileHandle?.synchronizeFile() + }, { exeption in +// Logger.warn("\(exeption)") + + }) + + currentFileHandle?.closeFile() + + writeFinal() + } func getFilePath(_ fileName: String) -> String? { diff --git a/Sources/Media/IOMixer.swift b/Sources/Media/IOMixer.swift index af609b680..7752f923c 100644 --- a/Sources/Media/IOMixer.swift +++ b/Sources/Media/IOMixer.swift @@ -7,7 +7,7 @@ import SwiftPMSupport import UIKit #endif #if os(iOS) || os(macOS) -extension AVCaptureSession.Preset { +public extension AVCaptureSession.Preset { static let `default`: AVCaptureSession.Preset = .hd1280x720 } #endif @@ -215,6 +215,8 @@ public class IOMixer { func useSampleBuffer(sampleBuffer: CMSampleBuffer, mediaType: AVMediaType) -> Bool { switch mediaSync { case .video: + print(videoTimeStamp.seconds) + print(sampleBuffer.presentationTimeStamp.seconds) if mediaType == .audio { return !videoTimeStamp.seconds.isZero && videoTimeStamp.seconds <= sampleBuffer.presentationTimeStamp.seconds } diff --git a/Sources/Media/IOVideoUnit.swift b/Sources/Media/IOVideoUnit.swift index 7f7862736..2fefcd456 100644 --- a/Sources/Media/IOVideoUnit.swift +++ b/Sources/Media/IOVideoUnit.swift @@ -326,7 +326,11 @@ extension IOVideoUnit: AVCaptureVideoDataOutputSampleBufferDelegate { } appendSampleBuffer(sampleBuffer) } else if multiCamCapture.output == captureOutput { - multiCamSampleBuffer = sampleBuffer +// multiCamSampleBuffer = sampleBuffer + guard mixer?.useSampleBuffer(sampleBuffer: sampleBuffer, mediaType: AVMediaType.video) == true else { + return + } + appendSampleBuffer(sampleBuffer) } } }