code wiki / (root) / nx_ts_nal.nx

nx_ts_nal.nx source

↩ module page · 140 lines · 7188 B

1// nx_ts_nal.nx -- codec-aware NAL classification for MPEG-TS elementary streams. 2// 3// WHY A LIB AND NOT INLINE (2026-07-31): this logic lived inside nx_ts_index's 4// single scan loop, where it could not be tested without a real multi-megabyte 5// recording. The HEVC hole below was therefore invisible to every gate. Pulling 6// the classification out makes the DECISION testable independently of the I/O 7// that feeds it -- the bug was never in the scanning, it was in the arithmetic. 8// 9// THE HOLE THIS CLOSES: nx_ts_index accepted only stream_type 0x1b (H.264), so 10// an HEVC recording never got a video PID -- no keyframe index AND no NXMK 11// dead-air/motion markers, permanently, for every H.265 asset in the corpus. 12// 13// THE TWO CODECS ARE NOT A TUNING DIFFERENCE, THEY ARE DIFFERENT GEOMETRY: 14// H.264 1-byte NAL header, type = b & 0x1f keyframe = IDR, the single value 5 15// HEVC 2-byte NAL header, type = (b >> 1) & 0x3f keyframe = IRAP, the RANGE 16..23 16// ★★★ Reading HEVC through the H.264 mask does not fail loudly -- it takes the 17// wrong bits and yields a plausible small integer, so random frames get marked as 18// keyframes. A seek index that is WRONG is worse than one that is ABSENT, because 19// absent is visibly ready:0 while wrong looks like it works and seeks to garbage. 20// license_tier: ORIGINAL 21 22import "nx_syscalls.nx" 23const TSN_MAGIC_1111: i64 = 1111 24const TSN_MAGIC_111111: i64 = 111111 25const TSN_MAGIC_11111: i64 = 11111 26 27const TSN_H264: i64 = 0 28const TSN_HEVC: i64 = 1 29 30// PMT stream_type -> codec id, or -1 if it is not a video stream we can index. 31func tsn_codec_of_stream_type(st: i64) -> i64 { 32 if st == 0x1b { return TSN_H264 } 33 if st == 0x24 { return TSN_HEVC } 34 return 0 - 1 35} 36 37// NAL unit type from the FIRST header byte after the start code. 38func tsn_nal_type(b0: i64, codec: i64) -> i64 { 39 if codec == TSN_HEVC { return (b0 >> 1) & 0x3f } 40 return b0 & 0x1f 41} 42 43// Is this NAL a random-access point (a seekable keyframe)? 44// H.264: IDR only. HEVC: the whole IRAP class 16..23 (BLA_W_LP..RSV_IRAP_VCL23), 45// which includes IDR_W_RADL 19, IDR_N_LP 20 and CRA_NUT 21. Matching only one of 46// those would index a fraction of the real seek points on a CRA-heavy stream. 47func tsn_nal_is_key(nt: i64, codec: i64) -> i64 { 48 if codec == TSN_HEVC { 49 if nt >= 16 { if nt <= 23 { return 1 } } 50 return 0 51 } 52 if nt == 5 { return 1 } 53 return 0 54} 55 56// Is this NAL video-coding-layer (i.e. the picture data)? Scanning an access 57// unit stops here. H.264 VCL = 1..5, HEVC VCL = 0..31. 58func tsn_nal_is_vcl(nt: i64, codec: i64) -> i64 { 59 if codec == TSN_HEVC { 60 if nt <= 31 { return 1 } 61 return 0 62 } 63 if nt >= 1 { if nt <= 5 { return 1 } } 64 return 0 65} 66 67// ---- hvcC: the HEVC decoder-configuration record (ISO/IEC 14496-15 8.3.3.1) -- 68// 69// The fMP4 remux needs this where H.264 uses avcC. avcC is trivial because the 70// profile/compat/level bytes sit at sps[1..3]; hvcC needs the same information 71// from HEVC's profile_tier_level, which is 12 CONTIGUOUS BYTES immediately after 72// the 2-byte NAL header plus the one byte holding sps_video_parameter_set_id / 73// sps_max_sub_layers_minus1 / sps_temporal_id_nesting_flag. So it is a byte copy, 74// NOT bit-parsing -- for the sub-layer case below. 75// 76// ★★★ THE GUARD THAT MAKES THIS HONEST: that 12-byte length holds ONLY when 77// sps_max_sub_layers_minus1 == 0. With sub-layers present the structure grows by 78// per-layer flag bytes and every field after it shifts. Rather than emit a 79// confidently WRONG hvcC -- which yields a player that decodes garbage instead of 80// refusing -- this REFUSES (-1) and the caller falls back. Same rule the HEVC 81// index fix was built on: a wrong answer is worse than an absent one. 82// 83// Returns bytes written, or -1 if the SPS shape is one we will not guess at. 84func tsn_build_hvcc(out: *u8, vps: *u8, vpslen: i64, sps: *u8, spslen: i64, pps: *u8, ppslen: i64) -> i64 { 85 if spslen < 15 { return 0 - 1 } 86 // sps[2]: vps_id(4) max_sub_layers_minus1(3) temporal_id_nesting(1) 87 let msl: i64 = ((sps[2] as i64) >> 1) & 0x07 88 if msl != 0 { return 0 - 1 } 89 90 var o: i64 = 0 91 out[o] = 1 as u8; o = o + 1 // configurationVersion 92 // profile_tier_level copied BYTE-EXACT out of the SPS: general_profile_space/ 93 // tier/idc (1) + compatibility flags (4) + constraint flags (6) + level (1). 94 var i: i64 = 0 95 while i < 12 { out[o] = sps[3 + i]; o = o + 1; i = i + 1 } 96 out[o] = 0xf0 as u8; o = o + 1 // reserved TSN_MAGIC_1111 + min_spatial_segmentation_idc hi 97 out[o] = 0x00 as u8; o = o + 1 // min_spatial_segmentation_idc lo = 0 98 out[o] = 0xfc as u8; o = o + 1 // reserved TSN_MAGIC_111111 + parallelismType = 0 99 // chromaFormat 4:2:0 and 8-bit luma/chroma are DECLARED ASSUMPTIONS, not read 100 // from the SPS -- deriving them needs full VUI bit-parsing. They are correct 101 // for the cam corpus; 10-bit or 4:2:2 sources would need the real parse. 102 out[o] = 0xfd as u8; o = o + 1 // reserved TSN_MAGIC_111111 + chromaFormat = 1 (4:2:0) 103 out[o] = 0xf8 as u8; o = o + 1 // reserved TSN_MAGIC_11111 + bitDepthLumaMinus8 = 0 104 out[o] = 0xf8 as u8; o = o + 1 // reserved TSN_MAGIC_11111 + bitDepthChromaMinus8 = 0 105 out[o] = 0x00 as u8; o = o + 1 // avgFrameRate hi (0 = unspecified) 106 out[o] = 0x00 as u8; o = o + 1 // avgFrameRate lo 107 // constantFrameRate(2)=0 numTemporalLayers(3)=1 temporalIdNested(1)=1 lengthSizeMinusOne(2)=3 108 out[o] = 0x0f as u8; o = o + 1 109 110 var narr: i64 = 0 111 if vpslen > 0 { narr = narr + 1 } 112 if spslen > 0 { narr = narr + 1 } 113 if ppslen > 0 { narr = narr + 1 } 114 out[o] = narr as u8; o = o + 1 // numOfArrays 115 116 // each array: array_completeness(1)=1 | reserved(1)=0 | NAL_unit_type(6), 117 // then numNalus(2), then per NALU a 2-byte length followed by its bytes. 118 if vpslen > 0 { o = tsn_hvcc_array(out, o, 32, vps, vpslen) } 119 if spslen > 0 { o = tsn_hvcc_array(out, o, 33, sps, spslen) } 120 if ppslen > 0 { o = tsn_hvcc_array(out, o, 34, pps, ppslen) } 121 return o 122} 123 124func tsn_hvcc_array(out: *u8, off: i64, nal_type: i64, nal: *u8, nlen: i64) -> i64 { 125 var o: i64 = off 126 out[o] = (0x80 | nal_type) as u8; o = o + 1 // array_completeness = 1 127 out[o] = 0 as u8; o = o + 1 // numNalus hi 128 out[o] = 1 as u8; o = o + 1 // numNalus lo = 1 129 out[o] = ((nlen >> 8) & 0xff) as u8; o = o + 1 130 out[o] = (nlen & 0xff) as u8; o = o + 1 131 var i: i64 = 0 132 while i < nlen { out[o] = nal[i]; o = o + 1; i = i + 1 } 133 return o 134} 135 136// Parameter-set NAL types, which differ by codec: H.264 SPS=7/PPS=8, 137// HEVC SPS=33/PPS=34 (and VPS=32, which H.264 has no equivalent of). 138func tsn_sps_type(codec: i64) -> i64 { if codec == TSN_HEVC { return 33 } return 7 } 139func tsn_pps_type(codec: i64) -> i64 { if codec == TSN_HEVC { return 34 } return 8 } 140func tsn_vps_type(codec: i64) -> i64 { if codec == TSN_HEVC { return 32 } return 0 - 1 }