1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
//! [`AvcRuntime`] — the per-stream state machine the orchestrator drives.
use super::{
build_avcc, parse_avcc_parameter_sets, parse_parameter_sets, CodecProjection,
ConfigFingerprint,
};
use crate::core::packet_sink::nal_framing::{
push_length_prefixed, walk_annexb, walk_length_prefixed, NAL_LENGTH_SIZE, NAL_PPS, NAL_SPS,
};
use crate::error::PacketSinkError;
/// Per-stream H.264 runtime state.
pub(crate) struct AvcRuntime {
/// Decided once, from the extradata form: Annex-B extradata means
/// Annex-B packets (rewritten per packet), avcC extradata means already
/// length-prefixed packets (validated in place). The forms never mix
/// within one encoder.
annexb_packets: bool,
/// Composite S8 baseline, part 1: the identity-keyed fingerprint of
/// the effective configuration ([`ConfigFingerprint`] — wrapper-
/// independent, last-wins per parameter-set id).
baseline: ConfigFingerprint,
/// Composite S8 baseline, part 2: the derived codec projection the
/// stream configuration exposes — same source as `on_stream_info`, so a
/// reordering that changes what consumers were told (profile /
/// compatibility / level) is a configuration change even when the set
/// identities are unchanged.
projection: CodecProjection,
}
impl AvcRuntime {
/// Builds the runtime from finalized encoder extradata; returns the
/// runtime plus the avcC to deliver in the stream configuration (a
/// pre-existing avcC passes through after validation; Annex-B parameter
/// sets are synthesized into one, preserving wrapper order like movenc,
/// with the synthesized record reparsed to prove it reproduces the
/// validated configuration).
pub(crate) fn from_extradata(
extradata: &[u8],
stream_index: usize,
) -> Result<(Self, Vec<u8>, CodecProjection), PacketSinkError> {
let invalid = |reason: String| PacketSinkError::InvalidExtradata {
stream_index,
reason,
};
let annexb_packets = extradata.first() != Some(&1);
let config = parse_parameter_sets(extradata).map_err(&invalid)?;
let projection = CodecProjection::from_ordered_sets(&config.sets).map_err(&invalid)?;
// Header/SPS agreement needs no separate step here: a pass-through
// record was already held against its own first SPS by
// `check_avcc_consistency` inside the parse (bytes 1..4 plus the
// profile-extension fields), and a synthesized record copies those
// bytes from that same first SPS.
let delivered = if annexb_packets {
let record = build_avcc(&config.sets).map_err(&invalid)?;
// Synthesis-fidelity gate. Annex-B resolves each PPS against
// the last PRECEDING same-id SPS, while the record's arrays
// put every SPS ahead of every PPS, so reparsing the record
// binds each PPS to the last same-id SPS OVERALL. An ordering
// that re-sends a PPS's SPS generation after the PPS therefore
// synthesizes a record describing a different configuration —
// or one whose PPS no longer parses under the rebound SPS at
// all. The delivered record must mean exactly the
// configuration validated above, so it is reparsed through the
// same avcC parser and its identity fingerprint (bindings and
// the extension triple included) must agree. Single-generation
// orderings with each
// PPS after its SPS — every real encoder handoff — bind
// identically on both reads and pass unchanged.
let reparsed = parse_avcc_parameter_sets(&record).map_err(|reason| {
invalid(format!(
"the parameter-set interleaving has no faithful avcC \
representation (the synthesized record fails to \
reparse: {reason})"
))
})?;
if reparsed.fingerprint != config.fingerprint {
return Err(invalid(
"the parameter-set interleaving has no faithful avcC \
representation (reparsing the synthesized record binds \
a PPS to a different SPS generation)"
.to_string(),
));
}
record
} else {
extradata.to_vec()
};
Ok((
Self {
annexb_packets,
baseline: config.fingerprint,
projection,
},
delivered,
projection,
))
}
/// S8: a `NEW_EXTRADATA` announcement — one whose EFFECTIVE
/// configuration (the identity-keyed [`ConfigFingerprint`]) is
/// unchanged is redundant and passes; anything else is a mid-stream
/// configuration change.
///
/// An avcC-form announcement passes through the same full record
/// validation as initial construction (`parse_avcc_record` via
/// [`parse_parameter_sets`]), including the header-vs-first-SPS
/// consistency check — so tampered header bytes over unchanged SPS/PPS
/// fail here rather than slipping past the set-identity comparison.
pub(crate) fn check_new_extradata(
&self,
bytes: &[u8],
stream_index: usize,
) -> Result<(), PacketSinkError> {
let config = parse_parameter_sets(bytes).map_err(|reason| {
PacketSinkError::ConfigChange {
stream_index,
what: format!("invalid NEW_EXTRADATA ({reason})"),
}
})?;
// Composite baseline, part 2: the derived projection consumers were
// told (profile/compatibility/level, from the announcement's own
// ordered first SPS — the same derivation on_stream_info used). A
// reorder that changes it IS a configuration change even when the
// effective identity map below is unchanged.
let projection = CodecProjection::from_ordered_sets(&config.sets).map_err(|reason| {
PacketSinkError::ConfigChange {
stream_index,
what: format!("invalid NEW_EXTRADATA SPS ({reason})"),
}
})?;
if projection != self.projection {
return Err(PacketSinkError::ConfigChange {
stream_index,
what: format!(
"derived codec projection changed ({} -> {})",
self.projection.codec_string(),
projection.codec_string()
),
});
}
// Composite baseline, part 1: the effective identity map (and the
// SPS-EXT list) must be unchanged.
if config.fingerprint != self.baseline {
return Err(PacketSinkError::ConfigChange {
stream_index,
what: "NEW_EXTRADATA changes the effective parameter sets".to_string(),
});
}
Ok(())
}
/// Payload normalization: validates NAL boundaries, classifies types
/// and (for Annex-B input) rewrites into `scratch` as a 4-byte
/// length-prefixed access unit. Annex-B input takes TWO linear walks —
/// an allocation-free census that reserves the exact output size, then
/// the write walk, which therefore never reallocates. The
/// already-length-prefixed path is a single validate-only pass,
/// zero-copy.
///
/// Returns `(is_key, payload)` where `is_key` is IDR presence and
/// `payload` borrows either `scratch` or the input.
pub(crate) fn normalize_au<'a>(
&self,
payload: &'a [u8],
scratch: &'a mut Vec<u8>,
stream_index: usize,
) -> Result<(bool, &'a [u8]), PacketSinkError> {
let malformed = |reason: String| PacketSinkError::MalformedPacket {
stream_index,
reason,
};
let (scan, data): (_, &'a [u8]) = if self.annexb_packets {
scratch.clear();
// Reserve the EXACT output size from a start-code census: a
// first, allocation-free walk sums `4 + trimmed_nal_len` per
// NAL, so the write walk below can never reallocate — not even
// for an AU with arbitrarily many 3-byte start codes. Both walks
// are linear scans of one AU; the census also front-loads the
// boundary validation.
let mut exact = 0usize;
walk_annexb(payload, |nal| exact += NAL_LENGTH_SIZE + nal.len())
.map_err(&malformed)?;
scratch.reserve(exact);
let scan = walk_annexb(payload, |nal| push_length_prefixed(nal, scratch))
.map_err(&malformed)?;
(scan, scratch.as_slice())
} else {
let scan = walk_length_prefixed(payload, |_| {}).map_err(malformed)?;
(scan, payload)
};
if scan.has_parameter_set {
// Cold path: collect the in-band sets for the S8 comparison
// (differing sets are a config change; value-equal sets are still
// rejected — strict-tier configuration stays out-of-band).
self.check_inband_parameter_sets(data, stream_index)?;
return Err(PacketSinkError::InBandParameterSets { stream_index });
}
Ok((scan.has_idr, data))
}
/// In-band SPS/PPS: sets differing from the ACTIVE baseline (a
/// replaced same-id predecessor is no longer active) are a
/// configuration change; value-equal sets fall through to the
/// strict-tier in-band rejection at the caller. `data` is
/// length-prefixed (post-normalization).
fn check_inband_parameter_sets(
&self,
data: &[u8],
stream_index: usize,
) -> Result<(), PacketSinkError> {
let mut mismatch = false;
let _ = walk_length_prefixed(data, |nal| {
let matches_baseline = match nal[0] & 0x1F {
NAL_SPS => self.baseline.has_active_sps(nal),
NAL_PPS => self.baseline.has_active_pps(nal),
_ => return,
};
if !matches_baseline {
mismatch = true;
}
});
if mismatch {
return Err(PacketSinkError::ConfigChange {
stream_index,
what: "in-band SPS/PPS differ from the stream configuration".to_string(),
});
}
Ok(())
}
}