v0.0.5
Other
- Enhanced RTMP v2 Multitrack body parser + builder (audio + video)
- decode Enhanced-RTMP v2 MultichannelConfig audio body
- injection-robust property tests + AMF nesting depth guards
- poll_event surfaces server-originated UserControl + onStatus
- emit UserControl StreamEOF before Unpublish.Success on close
Added
-
Enhanced RTMP v2
Multitrackaudio + video body parser + builder
(src/flv.rs,tests/multitrack.rs). TheVideoPacketType.Multitrack = 6
andAudioPacketType.Multitrack = 5body shapes from
enhanced-rtmp-v2.pdf§"ExVideoTagBody" / §"ExAudioTagBody" are now
decoded end-to-end. ThemultitrackType (UB[4]) | realPacketType (UB[4])byte plus the optional shared FourCC (omitted in
ManyTracksManyCodecsmode per spec) are consumed inline by
parse_video/parse_audioahead of the existing FourCC slot, and
the per-track list ((trackFourCc if ManyTracksManyCodecs) | trackId(UI8) | (sizeOfTrack(UI24) if not OneTrack) | body) is lifted
into a typedMultitrack { multitrack_type, tracks }struct on the
newVideoTag::multitrack/AudioTag::multitrackfields. The
outer tag'sex_packet_typenow holds the real inner PacketType
(e.g.CodedFrames,SequenceStart) so a downstreamex_packet_type == SequenceStartcheck still works for multitrack tags, and
fourcc/audio_fourcchold the shared codec forOneTrack/
ManyTracksmodes (andNoneforManyTracksManyCodecs, where each
MultitrackTrack::fourcccarries the per-track codec).
VideoTag::multitrack_tagandAudioTag::multitrack_tagare the
outbound builders;VideoTag::is_multitrack()/AudioTag::is_multitrack()
are the discriminators. New constantsAV_MULTITRACK_TYPE_ONE_TRACK
/AV_MULTITRACK_TYPE_MANY_TRACKS/AV_MULTITRACK_TYPE_MANY_TRACKS_MANY_CODECS
cover the spec'senum AvMultitrackType. Reserved discriminators
(3..=15) round-trip verbatim throughMultitrack::parse/
Multitrack::encodeso a forwarding ingest preserves future modes.
The spec invariant that the inner real PacketType MUST NOT itself be
Multitrackis enforced — a forged inner nibble of6(video) or
5(audio) surfaces a cleanError::Other("…MUST NOT…")rather
than recursing, and a truncatedsizeOfTrackoverrun yields a clean
…overruns remaining N byteserror. 23 new tests (11 unit in
src/flv.rs, 12 integration intests/multitrack.rs) cover: video
OneTrackAVC CodedFrames byte-exact wire layout; videoManyTracks
HEVC two-track byte-exact UI24 sizes; videoManyTracksManyCodecs
HEVC+AV1; videoSequenceStartManyTracksVVC; audioOneTrack
Opus CodedFrames; audioManyTracksManyCodecsOpus+FLAC mixed-codec;
audioManyTracksAAC; audioSequenceStartManyTracksAAC with
per-track ASC; the inner-PacketType-MUST-NOT-be-Multitrack invariant
for both audio and video; size-overrun-error and three other
truncation paths; track ordering preserved verbatim through round-trip
(trackIds[7, 0, 3]stay[7, 0, 3]); empty per-track body
(sizeOfTrack = 0) round-trips;ManyTracksModEx-prelude
composition withTimestampOffsetNano = 123_456(proves the
ModEx + Multitrack preludes compose on the wire); and a reserved
multitrack_type = 4directMultitrack::encode+parsesymmetry.
Resolves theMultitrackportion of the r177 / r186 README notes
"Multitrackstill parses to an opaque body pending a follow-up
round." Total integration-test count: 49 → 61 (+12); total tests:
191 → 202. -
Enhanced RTMP v2
MultichannelConfigaudio body parser + builder
(src/flv.rs). TheAudioPacketType.MultichannelConfig = 4body
shape fromenhanced-rtmp-v2.pdf§"ExAudioTagBody" is now decoded
end-to-end via the newMultichannelConfigstruct + the
MultichannelConfigOrderdiscriminated union (Unspecified/
Native { flags: u32 }/Custom { mapping: Vec<u8> }/
Reserved(u8)). On the wire the body is
audioChannelOrder(UI8) | channelCount(UI8) | (mapping[UI8] | flags(UI32) | nothing); lengths line up at 2 bytes forUnspecified,
6 bytes forNative, and2 + channelCountforCustom. Truncated
bodies, stray trailing bytes onUnspecified, and shortCustom
mappings all returnErr(Error::Other)cleanly; an unrecognised
audioChannelOrdervalue is preserved as
MultichannelConfigOrder::Reservedwith the trailing bytes in
extraso a forwarding tag never silently loses data.
AudioTag::is_multichannel_config(),
AudioTag::multichannel_config(), and
AudioTag::multichannel_config_tag(fourcc, &cfg)provide the
lift / round-trip helpers; the existingparse_audio/build_audio
bytes path is unchanged (the body is still carried verbatim through
AudioTag::body). Newaudio_channelandaudio_channel_mask
public submodules expose the 24 spec-defined channel positions and
their corresponding UI32 mask bits (including the 22.2 surround
extensions per SMPTE ST 2036-2-2008), plusaudio_channel::UNUSED
(0xFE) andUNKNOWN(0xFF) sentinels. New
AUDIO_CHANNEL_ORDER_UNSPECIFIED/_NATIVE/_CUSTOMconstants
cover the order discriminator. 10 new unit tests cover: 2-byte
Unspecifiedround-trip; 6-byteNative5.1 mask round-trip with
byte-exact wire bytes;Customstereo round-trip; full 22.2
Custom(24-byte mapping) round-trip exercising every
audio_channel::*constant;UNUSED/UNKNOWNsentinel
preservation; six truncation paths (empty body, partial header,
partial flags, short mapping, stray bytes onUnspecified);
Reservedorder verbatim round-trip; end-to-end build → parse →
lift throughbuild_audio+parse_audioon an Opus FourCC tag;
accessor returnsNonefor non-MultichannelConfig packet types and
for legacy tags whose body happens to start with valid-looking
bytes; and a bit-position check confirming1 << channel_index
equals the matchingaudio_channel_maskentry for every one of the
24 channels. Resolves theMultichannelConfigportion of the r177
README note "MultitrackandMultichannelConfigAudioPacketTypes
parse to opaque bodies pending follow-up rounds."Multitrack
remains deferred — itsAvMultitrackType + per-track FourCc + track id + sizeOfAudioTrackframing needs a richer follow-up. -
Injection-robustness property tests + AMF0/AMF3 nested-container
depth guards (src/amf.rs,src/amf3.rs,
tests/injection_robustness.rs). Every public parser surface — AMF0
(decode/decode_all), AMF3 (decode/decode_all/
decode_data_message), FLV (parse_video/parse_audio), the
chunk-stream reader (ChunkReader::read_message), and both
handshake directions (client_handshake/server_handshake) — is
now fuzzed with a deterministic xorshift PRNG (noranddep): 1024
random-byte iterations for each AMF surface, 2048 for each FLV
surface, 512 forChunkReader, plus a 1024-iteration "valid frame
with 1..=4 random byte flips" mutation pass on a built
onMetaDatapayload. Adversarial structural inputs are also covered:
truncated handshakes from both directions and at every truncation
boundary, wrong RTMP version bytes (0x00/0x01/0x06/
0xFF), AMF0M_STRICT_ARRAYwith au32::MAXlength, AMF0
M_STRINGclaiming 65535 bytes from a 3-byte buffer, a fmt-0 chunk
with an oversize 24-bitmsg_lengthand a forged fmt-1 chunk
arriving with no prior fmt-0 state. The runtime guarantee: every
call either returnsOkorErr, never panics, never spins, and
never over-allocates (amf0_strict_array_with_huge_count_errors_fast
asserts the error path is under 100 ms even withu32::MAX).
Stack-overflow protection:amf::MAX_DECODE_DEPTH = 64and
amf3::MAX_DECODE_DEPTH = 64cap nested-container recursion before
the call stack runs out; AMF0 routes through a new
decode_at_depth(buf, pos, depth)and AMF3 tracksdepthas a
field onDecoder(incremented on entry todecode, decremented on
return). Tests build 2_000-level-deep forged Object frames in both
formats and assert the guard surfaces a cleanError::InvalidAmf0
before the default 8 MiB stack overflows. Integration-test count:
28 → 49 (+21). -
RtmpClient::poll_eventsurfaces server-originated events; symmetric
UserControl StreamEOFrecognition on the client side (src/client.rs,
src/lib.rs). Round 154 added the server-side teardown that emits a
UserControl StreamEOF(stream_id)(RTMP 1.0 §7.1.7) before
onStatus(NetStream.Unpublish.Success)and the write-half FIN. The
pre-r158RtmpClientswallowed those server-originated bytes — the
readerfield was#[allow(dead_code)]-flagged and the only
post-publish reads happened opportunistically when the underlying
TcpStreamdropped, so a publisher couldn't tell "server cleanly
closed our publish" from "TCP connection died." This round wires up a
poll_event(&mut self) -> Result<Option<ClientEvent>>surface: each
call reads one inbound RTMP message, handles protocol-control
housekeeping internally (Set Chunk Size, Window Ack Size, Set Peer
Bandwidth, Ping Request → Ping Response auto-reply), and returns the
externally-visible notifications as a newClientEventenum:
StreamBegin { stream_id },StreamEof { stream_id },
OnStatus { level, code, description },
Result { transaction_id, values },
ErrorReply { transaction_id, values }, andOther.StreamEofis
not itself terminal — the server's close path emits onStatus after
StreamEOF, sopoll_eventkeeps reading until the TCP read half
observes EOF / connection-reset, at which point aread_eoflatch
makes subsequent calls returnOk(None)immediately without
re-entering the chunk reader on a dead socket. New
tests/client_stream_eof.rscovers end-to-end against our own
RtmpServer::close: the client observes
ClientEvent::StreamEof { stream_id: 1 }followed by
ClientEvent::OnStatus { code: "NetStream.Unpublish.Success", .. },
and a separate test verifies the post-EOF latch returnsOk(None)in
under 50 ms. Four new unit tests insrc/client.rscover the UCM
payload parser (parse_user_control+ucm_stream_id) and the AMF0
command classifier (classify_commandforonStatus/_result/
_error). -
Server session close emits
UserControl StreamEOFbefore
onStatus(NetStream.Unpublish.Success)(src/server.rs,
src/message.rs). The publish-side end-of-stream signal is now an
explicit RTMP wire event rather than a bare TCP FIN.RtmpSession::close
emits, in order: aUserControl StreamEOF(stream_id)event (RTMP 1.0
§7.1.7 — thethe stream is dryevent the spec assigns to the
server-to-client direction, re-used symmetrically here for an
end-of-publish notification), the existing
onStatus("NetStream.Unpublish.Success")command, a chunk-writer
flush()so every buffered chunk reaches the kernel, then a write-half
Shutdown::Write— mirroring the client-side r152 fix so the peer drains
every buffered frame and command before observing EOF. New
message::build_user_control_stream_eof(u32)builder exposes the event
for callers that want to emit it on a non-publish stream. New
tests/session_close.rsintegration test drains raw bytes off the
client socket and asserts the StreamEOF six-byte body precedes the
literal AMF0NetStream.Unpublish.Successstring.