Skip to content

v0.0.5

Choose a tag to compare

@MagicalTux MagicalTux released this 30 May 02:38
· 27 commits to master since this release
a6e0be1

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 Multitrack audio + video body parser + builder
    (src/flv.rs, tests/multitrack.rs). The VideoPacketType.Multitrack = 6
    and AudioPacketType.Multitrack = 5 body shapes from
    enhanced-rtmp-v2.pdf §"ExVideoTagBody" / §"ExAudioTagBody" are now
    decoded end-to-end. The multitrackType (UB[4]) | realPacketType (UB[4]) byte plus the optional shared FourCC (omitted in
    ManyTracksManyCodecs mode per spec) are consumed inline by
    parse_video / parse_audio ahead 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 typed Multitrack { multitrack_type, tracks } struct on the
    new VideoTag::multitrack / AudioTag::multitrack fields. The
    outer tag's ex_packet_type now holds the real inner PacketType
    (e.g. CodedFrames, SequenceStart) so a downstream ex_packet_type == SequenceStart check still works for multitrack tags, and
    fourcc / audio_fourcc hold the shared codec for OneTrack /
    ManyTracks modes (and None for ManyTracksManyCodecs, where each
    MultitrackTrack::fourcc carries the per-track codec).
    VideoTag::multitrack_tag and AudioTag::multitrack_tag are the
    outbound builders; VideoTag::is_multitrack() / AudioTag::is_multitrack()
    are the discriminators. New constants AV_MULTITRACK_TYPE_ONE_TRACK
    / AV_MULTITRACK_TYPE_MANY_TRACKS / AV_MULTITRACK_TYPE_MANY_TRACKS_MANY_CODECS
    cover the spec's enum AvMultitrackType. Reserved discriminators
    (3..=15) round-trip verbatim through Multitrack::parse /
    Multitrack::encode so a forwarding ingest preserves future modes.
    The spec invariant that the inner real PacketType MUST NOT itself be
    Multitrack is enforced — a forged inner nibble of 6 (video) or
    5 (audio) surfaces a clean Error::Other("…MUST NOT…") rather
    than recursing, and a truncated sizeOfTrack overrun yields a clean
    …overruns remaining N bytes error. 23 new tests (11 unit in
    src/flv.rs, 12 integration in tests/multitrack.rs) cover: video
    OneTrack AVC CodedFrames byte-exact wire layout; video ManyTracks
    HEVC two-track byte-exact UI24 sizes; video ManyTracksManyCodecs
    HEVC+AV1; video SequenceStart ManyTracks VVC; audio OneTrack
    Opus CodedFrames; audio ManyTracksManyCodecs Opus+FLAC mixed-codec;
    audio ManyTracks AAC; audio SequenceStart ManyTracks AAC 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; ManyTracks ModEx-prelude
    composition with TimestampOffsetNano = 123_456 (proves the
    ModEx + Multitrack preludes compose on the wire); and a reserved
    multitrack_type = 4 direct Multitrack::encode+parse symmetry.
    Resolves the Multitrack portion of the r177 / r186 README notes
    "Multitrack still parses to an opaque body pending a follow-up
    round." Total integration-test count: 49 → 61 (+12); total tests:
    191 → 202.

  • Enhanced RTMP v2 MultichannelConfig audio body parser + builder
    (src/flv.rs). The AudioPacketType.MultichannelConfig = 4 body
    shape from enhanced-rtmp-v2.pdf §"ExAudioTagBody" is now decoded
    end-to-end via the new MultichannelConfig struct + the
    MultichannelConfigOrder discriminated 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 for Unspecified,
    6 bytes for Native, and 2 + channelCount for Custom. Truncated
    bodies, stray trailing bytes on Unspecified, and short Custom
    mappings all return Err(Error::Other) cleanly; an unrecognised
    audioChannelOrder value is preserved as
    MultichannelConfigOrder::Reserved with the trailing bytes in
    extra so 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 existing parse_audio / build_audio
    bytes path is unchanged (the body is still carried verbatim through
    AudioTag::body). New audio_channel and audio_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), plus audio_channel::UNUSED
    (0xFE) and UNKNOWN (0xFF) sentinels. New
    AUDIO_CHANNEL_ORDER_UNSPECIFIED / _NATIVE / _CUSTOM constants
    cover the order discriminator. 10 new unit tests cover: 2-byte
    Unspecified round-trip; 6-byte Native 5.1 mask round-trip with
    byte-exact wire bytes; Custom stereo round-trip; full 22.2
    Custom (24-byte mapping) round-trip exercising every
    audio_channel::* constant; UNUSED / UNKNOWN sentinel
    preservation; six truncation paths (empty body, partial header,
    partial flags, short mapping, stray bytes on Unspecified);
    Reserved order verbatim round-trip; end-to-end build → parse →
    lift through build_audio + parse_audio on an Opus FourCC tag;
    accessor returns None for non-MultichannelConfig packet types and
    for legacy tags whose body happens to start with valid-looking
    bytes; and a bit-position check confirming 1 << channel_index
    equals the matching audio_channel_mask entry for every one of the
    24 channels. Resolves the MultichannelConfig portion of the r177
    README note "Multitrack and MultichannelConfig AudioPacketTypes
    parse to opaque bodies pending follow-up rounds." Multitrack
    remains deferred — its AvMultitrackType + per-track FourCc + track id + sizeOfAudioTrack framing 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 (no rand dep): 1024
    random-byte iterations for each AMF surface, 2048 for each FLV
    surface, 512 for ChunkReader, plus a 1024-iteration "valid frame
    with 1..=4 random byte flips" mutation pass on a built
    onMetaData payload. Adversarial structural inputs are also covered:
    truncated handshakes from both directions and at every truncation
    boundary, wrong RTMP version bytes (0x00 / 0x01 / 0x06 /
    0xFF), AMF0 M_STRICT_ARRAY with a u32::MAX length, AMF0
    M_STRING claiming 65535 bytes from a 3-byte buffer, a fmt-0 chunk
    with an oversize 24-bit msg_length and a forged fmt-1 chunk
    arriving with no prior fmt-0 state. The runtime guarantee: every
    call either returns Ok or Err, 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 with u32::MAX).
    Stack-overflow protection: amf::MAX_DECODE_DEPTH = 64 and
    amf3::MAX_DECODE_DEPTH = 64 cap nested-container recursion before
    the call stack runs out; AMF0 routes through a new
    decode_at_depth(buf, pos, depth) and AMF3 tracks depth as a
    field on Decoder (incremented on entry to decode, decremented on
    return). Tests build 2_000-level-deep forged Object frames in both
    formats and assert the guard surfaces a clean Error::InvalidAmf0
    before the default 8 MiB stack overflows. Integration-test count:
    28 → 49 (+21).

  • RtmpClient::poll_event surfaces server-originated events; symmetric
    UserControl StreamEOF recognition 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-r158 RtmpClient swallowed those server-originated bytes — the
    reader field was #[allow(dead_code)]-flagged and the only
    post-publish reads happened opportunistically when the underlying
    TcpStream dropped, 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 new ClientEvent enum:
    StreamBegin { stream_id }, StreamEof { stream_id },
    OnStatus { level, code, description },
    Result { transaction_id, values },
    ErrorReply { transaction_id, values }, and Other. StreamEof is
    not itself terminal — the server's close path emits onStatus after
    StreamEOF, so poll_event keeps reading until the TCP read half
    observes EOF / connection-reset, at which point a read_eof latch
    makes subsequent calls return Ok(None) immediately without
    re-entering the chunk reader on a dead socket. New
    tests/client_stream_eof.rs covers 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 returns Ok(None) in
    under 50 ms. Four new unit tests in src/client.rs cover the UCM
    payload parser (parse_user_control + ucm_stream_id) and the AMF0
    command classifier (classify_command for onStatus / _result /
    _error).

  • Server session close emits UserControl StreamEOF before
    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: a UserControl StreamEOF(stream_id) event (RTMP 1.0
    §7.1.7 — the the stream is dry event 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.rs integration test drains raw bytes off the
    client socket and asserts the StreamEOF six-byte body precedes the
    literal AMF0 NetStream.Unpublish.Success string.