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 }