Files
2025-07-13 12:43:53 +09:00

231 lines
7.8 KiB
Swift

import AVFoundation
import CoreMedia
import Foundation
/// An object that represents writes MPEG-2 transport stream data.
final class TSWriter {
static let defaultPATPID: UInt16 = 0
static let defaultPMTPID: UInt16 = 4095
static let defaultVideoPID: UInt16 = 256
static let defaultAudioPID: UInt16 = 257
static let defaultSegmentDuration: Double = 2
/// An asynchronous sequence for writing data.
public var output: AsyncStream<Data> {
AsyncStream { continuation in
self.continuation = continuation
}
}
/// Specifies the exptected medias = [.video, .audio].
var expectedMedias: Set<AVMediaType> = []
/// Specifies the audio format.
var audioFormat: AVAudioFormat? {
didSet {
guard let audioFormat, audioFormat != oldValue else {
return
}
var data = ESSpecificData()
data.streamType = audioFormat.formatDescription.streamType
data.elementaryPID = Self.defaultAudioPID
pmt.elementaryStreamSpecificData.append(data)
audioContinuityCounter = 0
writeProgramIfNeeded()
}
}
/// Specifies the video format.
var videoFormat: CMFormatDescription? {
didSet {
guard let videoFormat, videoFormat != oldValue else {
return
}
var data = ESSpecificData()
data.streamType = videoFormat.streamType
data.elementaryPID = Self.defaultVideoPID
pmt.elementaryStreamSpecificData.append(data)
videoContinuityCounter = 0
writeProgramIfNeeded()
}
}
private(set) var pat: TSProgramAssociation = {
let PAT: TSProgramAssociation = .init()
PAT.programs = [1: TSWriter.defaultPMTPID]
return PAT
}()
private(set) var pmt: TSProgramMap = .init()
private var pcrPID: UInt16 = TSWriter.defaultVideoPID
private var canWriteFor: Bool {
guard !expectedMedias.isEmpty else {
return true
}
if expectedMedias.contains(.audio) && expectedMedias.contains(.video) {
return audioFormat != nil && videoFormat != nil
}
if expectedMedias.contains(.video) {
return videoFormat != nil
}
if expectedMedias.contains(.audio) {
return audioFormat != nil
}
return false
}
private var videoTimeStamp: CMTime = .invalid
private var audioTimeStamp: CMTime = .invalid
private var clockTimeStamp: CMTime = .zero
private var segmentDuration: Double = TSWriter.defaultSegmentDuration
private var rotatedTimeStamp: CMTime = .zero
private var audioContinuityCounter: UInt8 = 0
private var videoContinuityCounter: UInt8 = 0
private var continuation: AsyncStream<Data>.Continuation? {
didSet {
oldValue?.finish()
}
}
/// Creates a new instance with segument duration.
init(segmentDuration: Double = 2.0) {
self.segmentDuration = segmentDuration
}
/// Appends a buffer.
func append(_ audioBuffer: AVAudioBuffer, when: AVAudioTime) {
guard let audioBuffer = audioBuffer as? AVAudioCompressedBuffer, canWriteFor else {
return
}
if audioTimeStamp == .invalid {
audioTimeStamp = when.makeTime()
if pcrPID == TSWriter.defaultAudioPID {
clockTimeStamp = audioTimeStamp
}
}
if var pes = PacketizedElementaryStream(audioBuffer, when: when, timeStamp: audioTimeStamp) {
pes.streamID = 192
writePacketizedElementaryStream(
TSWriter.defaultAudioPID,
PES: pes,
timeStamp: when.makeTime(),
randomAccessIndicator: true
)
}
}
/// Appends a buffer.
func append(_ sampleBuffer: CMSampleBuffer) {
guard canWriteFor else {
return
}
switch sampleBuffer.formatDescription?.mediaType {
case .video:
if videoTimeStamp == .invalid {
videoTimeStamp = sampleBuffer.presentationTimeStamp
if pcrPID == Self.defaultVideoPID {
clockTimeStamp = videoTimeStamp
}
}
if var pes = PacketizedElementaryStream(sampleBuffer, timeStamp: videoTimeStamp) {
let timestamp = sampleBuffer.decodeTimeStamp == .invalid ?
sampleBuffer.presentationTimeStamp : sampleBuffer.decodeTimeStamp
pes.streamID = 224
writePacketizedElementaryStream(
Self.defaultVideoPID,
PES: pes,
timeStamp: timestamp,
randomAccessIndicator: !sampleBuffer.isNotSync
)
}
default:
break
}
}
/// Clears the writer object for new transport stream.
func clear() {
audioFormat = nil
audioContinuityCounter = 0
videoFormat = nil
videoContinuityCounter = 0
pcrPID = Self.defaultVideoPID
pat.programs.removeAll()
pat.programs = [1: Self.defaultPMTPID]
pmt = TSProgramMap()
videoTimeStamp = .invalid
audioTimeStamp = .invalid
clockTimeStamp = .zero
rotatedTimeStamp = .zero
expectedMedias.removeAll()
continuation = nil
}
private func writePacketizedElementaryStream(_ PID: UInt16, PES: PacketizedElementaryStream, timeStamp: CMTime, randomAccessIndicator: Bool) {
let packets: [TSPacket] = split(PID, PES: PES, timestamp: timeStamp)
rotateFileHandle(timeStamp)
packets[0].adaptationField?.randomAccessIndicator = randomAccessIndicator
var bytes = Data()
for var packet in packets {
switch PID {
case Self.defaultAudioPID:
packet.continuityCounter = audioContinuityCounter
audioContinuityCounter = (audioContinuityCounter + 1) & 0x0f
case Self.defaultVideoPID:
packet.continuityCounter = videoContinuityCounter
videoContinuityCounter = (videoContinuityCounter + 1) & 0x0f
default:
break
}
bytes.append(packet.data)
}
write(bytes)
}
private func rotateFileHandle(_ timestamp: CMTime) {
let duration = timestamp.seconds - rotatedTimeStamp.seconds
guard segmentDuration < duration else {
return
}
writeProgramIfNeeded()
rotatedTimeStamp = timestamp
}
private func write(_ data: Data) {
continuation?.yield(data)
}
private func writeProgram() {
pmt.PCRPID = pcrPID
var bytes = Data()
var packets: [TSPacket] = []
packets.append(contentsOf: pat.arrayOfPackets(Self.defaultPATPID))
packets.append(contentsOf: pmt.arrayOfPackets(Self.defaultPMTPID))
for packet in packets {
bytes.append(packet.data)
}
write(bytes)
}
private func writeProgramIfNeeded() {
guard !expectedMedias.isEmpty else {
return
}
guard canWriteFor else {
return
}
writeProgram()
}
private func split(_ PID: UInt16, PES: PacketizedElementaryStream, timestamp: CMTime) -> [TSPacket] {
var PCR: UInt64?
let duration: Double = timestamp.seconds - clockTimeStamp.seconds
if pcrPID == PID && 0.02 <= duration {
PCR = UInt64((timestamp.seconds - (PID == Self.defaultVideoPID ? videoTimeStamp : audioTimeStamp).seconds) * TSTimestamp.resolution)
clockTimeStamp = timestamp
}
var packets: [TSPacket] = []
for packet in PES.arrayOfPackets(PID, PCR: PCR) {
packets.append(packet)
}
return packets
}
}