feat: 初始化 iOS 远程控制端项目

This commit is contained in:
2026-07-29 10:34:02 +08:00
parent 11173eb04c
commit 1e37aa64cc
20 changed files with 2610 additions and 0 deletions

View File

@@ -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 -> AVCC4 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))
}
}