feat(avlivewire): incremental stream demuxer

Add StreamDemuxer that accepts arbitrary byte chunks from a
non-frame-aligned stream and emits complete (FrameHeader, Data)
frames, resyncing on the magic prefix after corruption.
This commit is contained in:
L'électron rare
2026-05-18 15:49:08 +02:00
parent ec4be7bd1f
commit e7b3b63f5f
2 changed files with 90 additions and 0 deletions
@@ -0,0 +1,49 @@
import Foundation
public struct StreamDemuxer {
public struct Frame: Equatable {
public let header: FrameHeader
public let payload: Data
}
private var buffer = Data()
public init() {}
/// Append bytes; return every complete frame now available.
public mutating func feed(_ chunk: Data) -> [Frame] {
buffer.append(chunk)
var out: [Frame] = []
while true {
guard let start = findMagic() else {
// keep at most 3 trailing bytes (partial magic)
if buffer.count > 3 {
buffer = buffer.suffix(3)
}
break
}
if start > 0 { buffer.removeFirst(start) }
guard buffer.count >= FrameHeader.byteCount,
let h = FrameHeader(decoding: buffer) else { break }
let total = FrameHeader.byteCount + Int(h.length)
guard buffer.count >= total else { break }
let payloadStart = buffer.index(
buffer.startIndex, offsetBy: FrameHeader.byteCount)
let payloadEnd = buffer.index(
buffer.startIndex, offsetBy: total)
out.append(Frame(header: h,
payload: Data(buffer[payloadStart..<payloadEnd])))
buffer.removeFirst(total)
}
return out
}
private func findMagic() -> Int? {
let m = FrameHeader.magic
let bytes = [UInt8](buffer)
guard bytes.count >= m.count else { return nil }
for i in 0...(bytes.count - m.count) {
if Array(bytes[i..<i+m.count]) == m { return i }
}
return nil
}
}
@@ -0,0 +1,41 @@
import XCTest
@testable import AVLiveWire
final class StreamDemuxerTests: XCTestCase {
private func frame(_ tag: FrameTag, _ payload: Data) -> Data {
let h = FrameHeader(tag: tag, pid: 0, timestamp: 1,
length: UInt32(payload.count))
return h.encoded() + payload
}
func testSingleFrame() {
var d = StreamDemuxer()
let out = d.feed(frame(.video, Data([1, 2, 3])))
XCTAssertEqual(out.count, 1)
XCTAssertEqual(out[0].payload, Data([1, 2, 3]))
}
func testSplitAcrossChunks() {
var d = StreamDemuxer()
let whole = frame(.video, Data([9, 9, 9, 9, 9]))
XCTAssertTrue(d.feed(whole.prefix(10)).isEmpty)
let out = d.feed(whole.dropFirst(10))
XCTAssertEqual(out.count, 1)
XCTAssertEqual(out[0].payload, Data([9, 9, 9, 9, 9]))
}
func testTwoFramesOneChunk() {
var d = StreamDemuxer()
let out = d.feed(frame(.meta, Data([1]))
+ frame(.video, Data([2, 2])))
XCTAssertEqual(out.count, 2)
}
func testResyncAfterGarbage() {
var d = StreamDemuxer()
let out = d.feed(Data([0xDE, 0xAD])
+ frame(.video, Data([7])))
XCTAssertEqual(out.count, 1)
XCTAssertEqual(out[0].payload, Data([7]))
}
}