feat: 初始化 iOS 远程控制端项目
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import CoreMedia
|
||||
|
||||
/// “自编码”串流模式解码器:
|
||||
/// 从 video_channel DataChannel 接收被控端 MediaCodec 编码的 H.264 裸流,
|
||||
/// 重组分片 -> 解析单元 -> 转换为 AVCC 格式 -> 通过 AVSampleBufferDisplayLayer 硬解渲染。
|
||||
///
|
||||
/// 二进制协议(与被控端 SelfCodecEncoder 保持一致,均为大端序):
|
||||
/// 分片: MAGIC(0xAB,1B) + seq(int32) + total(int16) + idx(int16) + len(int32) + payload
|
||||
/// 单元: type(1B: 1=CONFIG 2=FRAME) + pts(uint32,ms) + isKey(1B) + len(int32) + H.264 Annex-B 数据
|
||||
final class SelfCodecDecoder {
|
||||
|
||||
// ---- 必须与被控端 SelfCodecEncoder 保持一致 ----
|
||||
private static let magic: UInt8 = 0xAB
|
||||
private static let unitTypeConfig: UInt8 = 1
|
||||
private static let unitTypeFrame: UInt8 = 2
|
||||
|
||||
/// 渲染图层(由 SelfCodecDisplayView 提供)
|
||||
weak var displayLayer: AVSampleBufferDisplayLayer?
|
||||
/// 分辨率变化回调(主线程)
|
||||
var onResolutionUpdate: ((Int, Int) -> Void)?
|
||||
|
||||
private(set) var videoWidth = 0
|
||||
private(set) var videoHeight = 0
|
||||
|
||||
private let queue = DispatchQueue(label: "com.ttstd.selfcodec.decoder")
|
||||
private var enabled = false
|
||||
|
||||
// 分片重组
|
||||
private final class ChunkBuffer {
|
||||
var total = 0
|
||||
var received = 0
|
||||
var chunks: [Data?] = []
|
||||
}
|
||||
private var assembling: [Int32: ChunkBuffer] = [:]
|
||||
private var lastSeq: Int32 = -1
|
||||
|
||||
// H.264 参数集与格式
|
||||
private var sps: Data?
|
||||
private var pps: Data?
|
||||
private var formatDescription: CMVideoFormatDescription?
|
||||
|
||||
// MARK: - 开关
|
||||
|
||||
func setEnabled(_ on: Bool) {
|
||||
queue.async {
|
||||
self.enabled = on
|
||||
if !on {
|
||||
self.resetLocked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func release() {
|
||||
queue.async {
|
||||
self.enabled = false
|
||||
self.resetLocked()
|
||||
}
|
||||
}
|
||||
|
||||
private func resetLocked() {
|
||||
assembling.removeAll()
|
||||
lastSeq = -1
|
||||
sps = nil
|
||||
pps = nil
|
||||
formatDescription = nil
|
||||
videoWidth = 0
|
||||
videoHeight = 0
|
||||
displayLayer?.flushAndRemoveImage()
|
||||
}
|
||||
|
||||
// MARK: - 入口:DataChannel 二进制消息
|
||||
|
||||
func onBinaryMessage(_ raw: Data) {
|
||||
queue.async {
|
||||
self.handleChunk(raw)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleChunk(_ raw: Data) {
|
||||
guard enabled, raw.count >= 13 else { return }
|
||||
var r = BigEndianReader(data: raw)
|
||||
guard r.readByte() == Self.magic else { return }
|
||||
guard let seq = r.readInt32() else { return }
|
||||
|
||||
// 过期检查:同一单元的所有分片 seq 相同,只丢弃明显更旧的单元
|
||||
if lastSeq != -1 && seq < lastSeq && (lastSeq - seq) < 1000 {
|
||||
return
|
||||
}
|
||||
guard let total = r.readInt16(), let idx = r.readInt16(),
|
||||
let len = r.readInt32(), let chunk = r.readData(Int(len)),
|
||||
total > 0 else { return }
|
||||
|
||||
let cb: ChunkBuffer
|
||||
if let existing = assembling[seq] {
|
||||
cb = existing
|
||||
} else {
|
||||
cb = ChunkBuffer()
|
||||
cb.total = Int(total)
|
||||
cb.chunks = Array(repeating: nil, count: Int(total))
|
||||
assembling[seq] = cb
|
||||
if assembling.count > 24, let minKey = assembling.keys.min() {
|
||||
assembling.removeValue(forKey: minKey)
|
||||
}
|
||||
}
|
||||
|
||||
let i = Int(idx)
|
||||
if i >= 0 && i < cb.total && cb.chunks[i] == nil {
|
||||
cb.chunks[i] = chunk
|
||||
cb.received += 1
|
||||
}
|
||||
if cb.received == cb.total {
|
||||
assembling.removeValue(forKey: seq)
|
||||
if seq > lastSeq { lastSeq = seq }
|
||||
var unit = Data()
|
||||
for c in cb.chunks where c != nil { unit.append(c!) }
|
||||
handleUnit(unit)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 单元处理
|
||||
|
||||
private func handleUnit(_ unit: Data) {
|
||||
var r = BigEndianReader(data: unit)
|
||||
guard let type = r.readByte(),
|
||||
let ptsRaw = r.readInt32(),
|
||||
let isKey = r.readByte(),
|
||||
let len = r.readInt32(),
|
||||
let data = r.readData(Int(len)) else { return }
|
||||
let ptsMs = Int64(UInt32(bitPattern: ptsRaw))
|
||||
|
||||
if type == Self.unitTypeConfig {
|
||||
handleConfig(data)
|
||||
} else if type == Self.unitTypeFrame {
|
||||
handleFrame(data, ptsMs: ptsMs, isKey: isKey != 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// CONFIG 单元:MediaCodec 的 csd-0(SPS)/csd-1(PPS),Annex-B 格式
|
||||
private func handleConfig(_ data: Data) {
|
||||
for nal in Self.annexBNalUnits(in: data) {
|
||||
guard let first = nal.first else { continue }
|
||||
switch first & 0x1F {
|
||||
case 7: if sps != nal { sps = nal; formatDescription = nil }
|
||||
case 8: if pps != nal { pps = nal; formatDescription = nil }
|
||||
default: break
|
||||
}
|
||||
}
|
||||
rebuildFormatDescriptionIfNeeded()
|
||||
}
|
||||
|
||||
private func rebuildFormatDescriptionIfNeeded() {
|
||||
guard formatDescription == nil, let sps, let pps else { return }
|
||||
var desc: CMVideoFormatDescription?
|
||||
let status = sps.withUnsafeBytes { spsPtr -> OSStatus in
|
||||
pps.withUnsafeBytes { ppsPtr -> OSStatus in
|
||||
let paramSets: [UnsafePointer<UInt8>] = [
|
||||
spsPtr.bindMemory(to: UInt8.self).baseAddress!,
|
||||
ppsPtr.bindMemory(to: UInt8.self).baseAddress!
|
||||
]
|
||||
let sizes: [Int] = [sps.count, pps.count]
|
||||
return CMVideoFormatDescriptionCreateFromH264ParameterSets(
|
||||
allocator: kCFAllocatorDefault,
|
||||
parameterSetCount: 2,
|
||||
parameterSetPointers: paramSets,
|
||||
parameterSetSizes: sizes,
|
||||
nalUnitHeaderLength: 4,
|
||||
formatDescriptionOut: &desc)
|
||||
}
|
||||
}
|
||||
guard status == noErr, let desc else {
|
||||
NSLog("[SelfCodec] create format description failed: \(status)")
|
||||
return
|
||||
}
|
||||
formatDescription = desc
|
||||
displayLayer?.flush()
|
||||
|
||||
let dims = CMVideoFormatDescriptionGetDimensions(desc)
|
||||
let w = Int(dims.width), h = Int(dims.height)
|
||||
if w != videoWidth || h != videoHeight {
|
||||
videoWidth = w
|
||||
videoHeight = h
|
||||
DispatchQueue.main.async {
|
||||
self.onResolutionUpdate?(w, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FRAME 单元:Annex-B -> AVCC(4 字节大端长度前缀),封装 CMSampleBuffer 后送显
|
||||
private func handleFrame(_ data: Data, ptsMs: Int64, isKey: Bool) {
|
||||
// 帧内可能携带 SPS/PPS(部分编码器关键帧前重发参数集)
|
||||
var vclData = Data()
|
||||
for nal in Self.annexBNalUnits(in: data) {
|
||||
guard let first = nal.first else { continue }
|
||||
let nalType = first & 0x1F
|
||||
if nalType == 7 {
|
||||
if sps != nal { sps = nal; formatDescription = nil }
|
||||
continue
|
||||
}
|
||||
if nalType == 8 {
|
||||
if pps != nal { pps = nal; formatDescription = nil }
|
||||
continue
|
||||
}
|
||||
var lenBE = UInt32(nal.count).bigEndian
|
||||
withUnsafeBytes(of: &lenBE) { vclData.append(contentsOf: $0) }
|
||||
vclData.append(nal)
|
||||
}
|
||||
rebuildFormatDescriptionIfNeeded()
|
||||
guard enabled, !vclData.isEmpty, let formatDescription, let layer = displayLayer else { return }
|
||||
|
||||
// 构造 CMBlockBuffer
|
||||
var blockBuffer: CMBlockBuffer?
|
||||
var status = CMBlockBufferCreateWithMemoryBlock(
|
||||
allocator: kCFAllocatorDefault,
|
||||
memoryBlock: nil,
|
||||
blockLength: vclData.count,
|
||||
blockAllocator: kCFAllocatorDefault,
|
||||
customBlockSource: nil,
|
||||
offsetToData: 0,
|
||||
dataLength: vclData.count,
|
||||
flags: 0,
|
||||
blockBufferOut: &blockBuffer)
|
||||
guard status == kCMBlockBufferNoErr, let blockBuffer else { return }
|
||||
status = vclData.withUnsafeBytes { ptr in
|
||||
CMBlockBufferReplaceDataBytes(
|
||||
with: ptr.baseAddress!,
|
||||
blockBuffer: blockBuffer,
|
||||
offsetIntoDestination: 0,
|
||||
dataLength: vclData.count)
|
||||
}
|
||||
guard status == kCMBlockBufferNoErr else { return }
|
||||
|
||||
// 构造 CMSampleBuffer(立即显示,不依赖时间轴)
|
||||
var sampleBuffer: CMSampleBuffer?
|
||||
var timing = CMSampleTimingInfo(
|
||||
duration: .invalid,
|
||||
presentationTimeStamp: CMTime(value: ptsMs, timescale: 1000),
|
||||
decodeTimeStamp: .invalid)
|
||||
var sampleSize = vclData.count
|
||||
status = CMSampleBufferCreateReady(
|
||||
allocator: kCFAllocatorDefault,
|
||||
dataBuffer: blockBuffer,
|
||||
formatDescription: formatDescription,
|
||||
sampleCount: 1,
|
||||
sampleTimingEntryCount: 1,
|
||||
sampleTimingArray: &timing,
|
||||
sampleSizeEntryCount: 1,
|
||||
sampleSizeArray: &sampleSize,
|
||||
sampleBufferOut: &sampleBuffer)
|
||||
guard status == noErr, let sampleBuffer else { return }
|
||||
|
||||
if let attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: true) as? [CFMutableDictionary],
|
||||
let dict = attachments.first {
|
||||
CFDictionarySetValue(
|
||||
dict,
|
||||
Unmanaged.passUnretained(kCMSampleAttachmentKey_DisplayImmediately).toOpaque(),
|
||||
Unmanaged.passUnretained(kCFBooleanTrue).toOpaque())
|
||||
if !isKey {
|
||||
CFDictionarySetValue(
|
||||
dict,
|
||||
Unmanaged.passUnretained(kCMSampleAttachmentKey_NotSync).toOpaque(),
|
||||
Unmanaged.passUnretained(kCFBooleanTrue).toOpaque())
|
||||
}
|
||||
}
|
||||
|
||||
if layer.status == .failed || layer.requiresFlushToResumeDecoding {
|
||||
layer.flush()
|
||||
}
|
||||
layer.enqueue(sampleBuffer)
|
||||
}
|
||||
|
||||
// MARK: - Annex-B 拆分
|
||||
|
||||
/// 拆分 Annex-B 码流(00 00 01 / 00 00 00 01 起始码)为 NAL 单元数组(不含起始码)
|
||||
static func annexBNalUnits(in data: Data) -> [Data] {
|
||||
var result: [Data] = []
|
||||
let bytes = [UInt8](data)
|
||||
let count = bytes.count
|
||||
var starts: [Int] = []
|
||||
var i = 0
|
||||
while i + 2 < count {
|
||||
if bytes[i] == 0 && bytes[i + 1] == 0 && bytes[i + 2] == 1 {
|
||||
starts.append(i + 3)
|
||||
i += 3
|
||||
} else {
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
guard !starts.isEmpty else { return data.isEmpty ? [] : [data] }
|
||||
for (idx, start) in starts.enumerated() {
|
||||
var end = count
|
||||
if idx + 1 < starts.count {
|
||||
end = starts[idx + 1] - 3
|
||||
// 兼容 4 字节起始码(00 00 00 01)
|
||||
if end > start && bytes[end - 1] == 0 { end -= 1 }
|
||||
}
|
||||
if end > start {
|
||||
result.append(Data(bytes[start..<end]))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 大端序读取器
|
||||
|
||||
private struct BigEndianReader {
|
||||
let data: Data
|
||||
private var offset: Int
|
||||
|
||||
init(data: Data) {
|
||||
self.data = data
|
||||
self.offset = data.startIndex
|
||||
}
|
||||
|
||||
mutating func readByte() -> UInt8? {
|
||||
guard offset < data.endIndex else { return nil }
|
||||
defer { offset += 1 }
|
||||
return data[offset]
|
||||
}
|
||||
|
||||
mutating func readInt16() -> Int16? {
|
||||
guard offset + 2 <= data.endIndex else { return nil }
|
||||
let v = (UInt16(data[offset]) << 8) | UInt16(data[offset + 1])
|
||||
offset += 2
|
||||
return Int16(bitPattern: v)
|
||||
}
|
||||
|
||||
mutating func readInt32() -> Int32? {
|
||||
guard offset + 4 <= data.endIndex else { return nil }
|
||||
var v: UInt32 = 0
|
||||
for i in 0..<4 { v = (v << 8) | UInt32(data[offset + i]) }
|
||||
offset += 4
|
||||
return Int32(bitPattern: v)
|
||||
}
|
||||
|
||||
mutating func readData(_ length: Int) -> Data? {
|
||||
guard length >= 0, offset + length <= data.endIndex else { return nil }
|
||||
defer { offset += length }
|
||||
return data.subdata(in: offset..<(offset + length))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user