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:
@@ -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]))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user