|
| 1 | +import Testing |
| 2 | +import Libavcodec |
| 3 | +@testable import AetherEngine |
| 4 | + |
| 5 | +/// #365 round 2: when the source config record is Annex B and the packets are Annex B too, the record |
| 6 | +/// is forwarded and **movenc builds the hvcC itself**. `ff_isom_write_hvcc` collects five NAL types, |
| 7 | +/// not three (`array_idx_to_type` in libavformat/hevc.c: VPS, SPS, PPS, SEI_PREFIX, SEI_SUFFIX), so a |
| 8 | +/// prefix SEI in the CodecPrivate lands in the init sample description. That is exactly the record |
| 9 | +/// Apple TV hardware rejects (AE#187), and `canonicalizeHEVCConfigRecord` cannot defend this door: it |
| 10 | +/// guards on `configurationVersion == 1`, which an Annex-B buffer fails by construction, and the |
| 11 | +/// muxer-built record never passes through the engine at all. |
| 12 | +struct Issue365AnnexBSEILeakTests { |
| 13 | + |
| 14 | + /// VPS / SPS / PPS of a real 1080p Main10 PQ stream, Annex B with 3-byte start codes. |
| 15 | + private static let parameterSets: [UInt8] = [ |
| 16 | + 0x00, 0x00, 0x01, 0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, |
| 17 | + 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x78, 0x95, 0x94, 0x09, 0x00, 0x00, 0x01, |
| 18 | + 0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, |
| 19 | + 0x03, 0x00, 0x78, 0xa0, 0x03, 0xc0, 0x80, 0x11, 0x07, 0xca, 0xd9, 0x65, 0x65, 0x4a, 0x4c, |
| 20 | + 0x2f, 0x01, 0x6a, 0x12, 0x20, 0x12, 0x08, 0x00, 0x00, 0x03, 0x00, 0x08, 0x00, 0x00, 0x03, |
| 21 | + 0x00, 0xc0, 0x40, 0x00, 0x00, 0x01, 0x44, 0x01, 0xc0, 0x73, 0xc1, 0x89, |
| 22 | + ] |
| 23 | + |
| 24 | + /// A prefix SEI (NAL type 39) carrying an unregistered user-data payload, the shape x265 writes its |
| 25 | + /// options string in. 500 payload bytes so the blob lands near the 726 B the #365 reporter's |
| 26 | + /// CodecPrivate carries, which is far more than VPS + SPS + PPS can account for on their own. |
| 27 | + private static func userDataSEI(payloadBytes: Int = 500) -> [UInt8] { |
| 28 | + var nal: [UInt8] = [0x4E, 0x01] // nal_type 39, nuh_layer_id 0, temporal_id_plus1 1 |
| 29 | + nal.append(0x05) // payloadType: user_data_unregistered |
| 30 | + var remaining = payloadBytes |
| 31 | + while remaining >= 255 { nal.append(0xFF); remaining -= 255 } |
| 32 | + nal.append(UInt8(remaining)) |
| 33 | + nal += [UInt8](repeating: 0x42, count: payloadBytes) // 0x42 filler cannot emulate a start code |
| 34 | + nal.append(0x80) // rbsp_trailing_bits |
| 35 | + return [0x00, 0x00, 0x01] + nal |
| 36 | + } |
| 37 | + |
| 38 | + /// NAL types of the arrays in an hvcC, in the order the record lists them. |
| 39 | + private static func hvcCArrayTypes(_ record: [UInt8]) -> [Int] { |
| 40 | + guard record.count >= 23, record[0] == 1 else { return [] } |
| 41 | + var types: [Int] = [] |
| 42 | + var offset = 23 |
| 43 | + for _ in 0..<Int(record[22]) { |
| 44 | + guard offset + 3 <= record.count else { return types } |
| 45 | + types.append(Int(record[offset]) & 0x3F) |
| 46 | + let numNalus = (Int(record[offset + 1]) << 8) | Int(record[offset + 2]) |
| 47 | + offset += 3 |
| 48 | + for _ in 0..<numNalus { |
| 49 | + guard offset + 2 <= record.count else { return types } |
| 50 | + offset += 2 + ((Int(record[offset]) << 8) | Int(record[offset + 1])) |
| 51 | + } |
| 52 | + } |
| 53 | + return types |
| 54 | + } |
| 55 | + |
| 56 | + private static func splitAnnexB(_ bytes: [UInt8]) -> [[UInt8]] { |
| 57 | + var starts: [Int] = [] |
| 58 | + var i = 0 |
| 59 | + while i + 3 <= bytes.count { |
| 60 | + if bytes[i] == 0, bytes[i + 1] == 0, bytes[i + 2] == 1 { |
| 61 | + starts.append(i + 3) |
| 62 | + i += 3 |
| 63 | + } else { |
| 64 | + i += 1 |
| 65 | + } |
| 66 | + } |
| 67 | + return starts.enumerated().map { idx, start in |
| 68 | + let end = idx + 1 < starts.count ? starts[idx + 1] - 3 : bytes.count |
| 69 | + return Array(bytes[start..<end]) |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + private static func nalTypes(_ annexB: [UInt8]) -> [Int] { |
| 74 | + splitAnnexB(annexB).map { (Int($0[0]) >> 1) & 0x3F } |
| 75 | + } |
| 76 | + |
| 77 | + // MARK: - The leak, measured on the muxer rather than assumed |
| 78 | + |
| 79 | + @Test("movenc writes the prefix SEI into the hvcC it builds from an Annex-B record") |
| 80 | + func muxerBuiltRecordCarriesTheSEIArray() throws { |
| 81 | + let withSEI = Self.parameterSets + Self.userDataSEI() |
| 82 | + let record = try #require(VideoConfigRecord.fromAnnexB( |
| 83 | + withSEI, codecID: AV_CODEC_ID_HEVC, width: 1920, height: 1080)) |
| 84 | + // Four arrays, the fourth being SEI_PREFIX: this is the AE#187 record shape, reached through a |
| 85 | + // door the AE#187 defense does not cover. |
| 86 | + #expect(Self.hvcCArrayTypes(record) == [32, 33, 34, 39]) |
| 87 | + } |
| 88 | + |
| 89 | + // MARK: - The fix |
| 90 | + |
| 91 | + @Test("Canonicalizing the Annex-B record drops the SEI and keeps VPS/SPS/PPS") |
| 92 | + func canonicalizationDropsTheSEINAL() throws { |
| 93 | + let withSEI = Self.parameterSets + Self.userDataSEI() |
| 94 | + let canonical = try #require(VideoConfigRecord.canonicalizeAnnexBHEVCConfigRecord(withSEI)) |
| 95 | + |
| 96 | + #expect(Self.nalTypes(canonical) == [32, 33, 34]) |
| 97 | + // Still Annex B, so movenc makes the same call about the samples it made before: these packets |
| 98 | + // are Annex B and have to be converted. Handing it an hvcC here would leave them unconverted. |
| 99 | + #expect(VideoConfigRecord.isAnnexB(canonical)) |
| 100 | + // The parameter sets themselves are untouched, byte for byte. |
| 101 | + #expect(Self.splitAnnexB(canonical) == Self.splitAnnexB(Self.parameterSets)) |
| 102 | + } |
| 103 | + |
| 104 | + @Test("The record movenc builds from the canonicalized blob has no SEI array") |
| 105 | + func canonicalizedRecordSurvivesTheMuxer() throws { |
| 106 | + let withSEI = Self.parameterSets + Self.userDataSEI() |
| 107 | + let canonical = try #require(VideoConfigRecord.canonicalizeAnnexBHEVCConfigRecord(withSEI)) |
| 108 | + let record = try #require(VideoConfigRecord.fromAnnexB( |
| 109 | + canonical, codecID: AV_CODEC_ID_HEVC, width: 1920, height: 1080)) |
| 110 | + |
| 111 | + #expect(Self.hvcCArrayTypes(record) == [32, 33, 34]) |
| 112 | + #expect(record[22] == 3) |
| 113 | + } |
| 114 | + |
| 115 | + @Test("Also drops a suffix SEI and leaves an unknown NAL type out") |
| 116 | + func dropsSuffixSEIAndUnknownTypes() throws { |
| 117 | + var blob = Self.parameterSets |
| 118 | + blob += [0x00, 0x00, 0x01, 0x50, 0x01, 0x03, 0x04, 0x80] // nal_type 40, SEI_SUFFIX |
| 119 | + blob += [0x00, 0x00, 0x01, 0x7C, 0x01, 0x11, 0x22] // nal_type 62, unspecified (DV RPU) |
| 120 | + let canonical = try #require(VideoConfigRecord.canonicalizeAnnexBHEVCConfigRecord(blob)) |
| 121 | + #expect(Self.nalTypes(canonical) == [32, 33, 34]) |
| 122 | + } |
| 123 | + |
| 124 | + @Test("A record that is already parameter-sets-only returns nil (nothing to rewrite)") |
| 125 | + func alreadyCanonicalReturnsNil() { |
| 126 | + #expect(VideoConfigRecord.canonicalizeAnnexBHEVCConfigRecord(Self.parameterSets) == nil) |
| 127 | + } |
| 128 | + |
| 129 | + @Test("An hvcC is refused: this canonicalizer only speaks Annex B") |
| 130 | + func refusesAnHvcC() throws { |
| 131 | + let record = try #require(VideoConfigRecord.fromAnnexB( |
| 132 | + Self.parameterSets, codecID: AV_CODEC_ID_HEVC, width: 1920, height: 1080)) |
| 133 | + #expect(VideoConfigRecord.canonicalizeAnnexBHEVCConfigRecord(record) == nil) |
| 134 | + } |
| 135 | + |
| 136 | + @Test("A blob with no parameter sets at all is left alone rather than emptied") |
| 137 | + func refusesToEmitAnEmptyRecord() { |
| 138 | + #expect(VideoConfigRecord.canonicalizeAnnexBHEVCConfigRecord(Self.userDataSEI()) == nil) |
| 139 | + } |
| 140 | + |
| 141 | + // MARK: - The witness |
| 142 | + |
| 143 | + @Test("The Annex-B summary names every NAL type and what it costs in bytes") |
| 144 | + func summaryNamesTypesAndSizes() { |
| 145 | + var blob: [UInt8] = [] |
| 146 | + blob += [0, 0, 1, 0x40, 0x01] + [UInt8](repeating: 0x11, count: 20) // VPS, 22 B |
| 147 | + blob += [0, 0, 1, 0x42, 0x01] + [UInt8](repeating: 0x22, count: 50) // SPS, 52 B |
| 148 | + blob += [0, 0, 1, 0x44, 0x01] + [UInt8](repeating: 0x33, count: 5) // PPS, 7 B |
| 149 | + blob += [0, 0, 1, 0x4E, 0x01] + [UInt8](repeating: 0x44, count: 400) // SEI_PREFIX, 402 B |
| 150 | + #expect(VideoConfigRecord.annexBNALSummary(blob) |
| 151 | + == "VPS×1 (22 B), SPS×1 (52 B), PPS×1 (7 B), SEI_PREFIX×1 (402 B)") |
| 152 | + } |
| 153 | + |
| 154 | + @Test("Repeated NAL types are counted together, unknown types are named by number") |
| 155 | + func summaryCountsRepeatsAndNamesUnknownTypes() { |
| 156 | + var blob: [UInt8] = [] |
| 157 | + blob += [0, 0, 1, 0x42, 0x01] + [UInt8](repeating: 0x22, count: 10) // SPS, 12 B |
| 158 | + blob += [0, 0, 1, 0x42, 0x01] + [UInt8](repeating: 0x22, count: 20) // SPS, 22 B |
| 159 | + blob += [0, 0, 1, 0x7C, 0x01] + [UInt8](repeating: 0x33, count: 4) // type 62, 6 B |
| 160 | + #expect(VideoConfigRecord.annexBNALSummary(blob) == "SPS×2 (34 B), NAL62×1 (6 B)") |
| 161 | + } |
| 162 | + |
| 163 | + @Test("Four-byte start codes are handled the same as three-byte ones") |
| 164 | + func handlesFourByteStartCodes() throws { |
| 165 | + var blob: [UInt8] = [] |
| 166 | + for nal in Self.splitAnnexB(Self.parameterSets) { blob += [0x00, 0x00, 0x00, 0x01] + nal } |
| 167 | + blob += [0x00, 0x00, 0x00, 0x01] + Array(Self.userDataSEI().dropFirst(3)) |
| 168 | + let canonical = try #require(VideoConfigRecord.canonicalizeAnnexBHEVCConfigRecord(blob)) |
| 169 | + #expect(Self.nalTypes(canonical) == [32, 33, 34]) |
| 170 | + #expect(VideoConfigRecord.isAnnexB(canonical)) |
| 171 | + } |
| 172 | +} |
0 commit comments