Releases: OxideAV/oxideav-rtmp
v0.0.6
Other
- strongly-typed colorInfo HDR metadata for VideoPacketType.Metadata
- decode AMF0 object references (marker 0x07) per FLV §E.4.4.2
- decode externalizable objects via registered per-class handlers (§3.12 U29O-traits-ext)
- RTMP §5.3 Acknowledgement honoured on received-byte window
- Enhanced RTMP v2 NetConnection.Connect.ReconnectRequest end-to-end
- RTMP §5.2 Abort Message builder + reader partial-discard
- typed UserControlEvent enum + round-trip parser
- drop release-plz.toml — use release-plz defaults across the workspace
- bump publisher-close drain windows for Ubuntu CI scheduling
- RTMP §3.7 StreamDry / StreamIsRecorded / PingResponse + builders
- typed MessageStreamKind accessor + spec-§5 protocol-control invariant validator
- bind set_read_timeout to the reader's actual socket clone
- replace drop(client) with close() so Windows CI doesn't race the flush
- route Aggregate Messages (type 22) through next_packet + poll_event
- fold ModEx TimestampOffsetNano onto the Packet timeline
- Aggregate Message (type 22) parser + builder
- Enhanced RTMP v1+v2 NetConnection connect capability negotiation
- rephrase FlvReader::with_max_tag_size docs
- FLV file / byte-stream reader (Annex E)
- FLV file / byte-stream writer (Annex E)
Added
-
Strongly-typed
colorInfoHDR metadata forVideoPacketType.Metadata
(src/flv.rs). Enhanced RTMP §"Metadata Frame" defines the
VideoPacketType.Metadata(= 4) video message as an AMF-encoded
sequence of[name, value]pairs, the only defined name being
"colorInfo"— an HDR metadata object carryingcolorConfig
(bitDepth + the ITU-T H.273 colourPrimaries / transferCharacteristics /
matrixCoefficients enumeration indices),hdrCll(maxFall / maxCLL
content light level in cd/m2) andhdrMdcv(SMPTE ST 2086:2018
mastering-display chromaticity coordinates + min/max luminance). The
body previously passed through as opaque AMF bytes; it now lifts into
the typed [ColorInfo] / [ColorConfig] / [HdrCll] / [HdrMdcv]
views viaVideoTag::color_info(), andVideoTag::color_info_tag(fourcc, &ColorInfo)rebuilds the matching outbound metadata tag. Every property
isOption<f64>(AMF's native double, kept byte-exact rather than
coerced to an integer that would lose a fractional luminance value), and
each sub-object isOptionso a partialcolorInfo— e.g. only
colorConfig— round-trips. The spec's "reset to original color state"
signal is preserved: anUndefinedvalue (the RECOMMENDED form) and an
empty{}object both decode to [ColorInfo::is_reset], and a reset
ColorInfore-encodes asUndefined. AcolorInfovalue of the wrong
AMF type is a cleanError::Other; a metadata tag with a different
(future) pair name yieldsOk(None)rather than an error. Seven tests
cover full-HDR10 round-trip, colorConfig-only partial, the two reset
forms, the non-metadata-tag / non-colorInfo-nameNonecases, and the
wrong-type rejection. -
AMF0 object references (marker 0x07) — decoded and dereferenced
transparently (src/amf.rs). The FLV v10.1 spec §E.4.4.2
SCRIPTDATAVALUEtable definesType == 7with the wire shape
IF Type == 7 { UI16 }— a 16-bit big-endian index into the table of
complex objects (Object, ECMA array, strict array) serialized so far
in the same context. The decoder previously rejected marker 0x07
outright, so anyonMetaData/command payload that used a reference to
deduplicate a repeated complex value failed to decode entirely. The
decoder now maintains a per-context reference table (scoped to one
decode_allpacket, or one top-level value fordecode), appends
each complex value as it is decoded — reserving the slot before the
body so a reference appearing inside that body still resolves — and
resolves a reference to a clone of the indexed value. Callers never
see aReferencevariant in the value graph; encoding always emits
the expanded (inline) form, which is byte-valid AMF0. An
out-of-range or truncated reference index surfaces a clean
InvalidAmf0error rather than a panic. Six tests cover prior-object
resolution, second-object indexing, in-body references, out-of-range
and truncated indices, and the per-value scope ofdecode. -
AMF3 §3.12
U29O-traits-extexternalizable objects — decodable via
registered per-class handlers (src/amf3.rs). The spec encodes an
externalizable object's body as "an indeterminable number of bytes as
*(U8)" whose framing is a private agreement between the sending and
receiving classes; the generic decoder cannot know where the body
ends and so previously refused every externalizable object outright,
even though the encoder already re-emitted theexternalizable_body
field verbatim.Decoder::register_externalizable(class_name, reader)
closes that asymmetry: a caller that knows a specific class's
IExternalizable.writeExternalframing registers a body-length
resolver (ExternalizableReader = Box<dyn Fn(&[u8], usize) -> Result<usize>>), anddecodethen captures exactly that many body
bytes intoAmf3Value::Object::externalizable_body, advancespos
past them, and inserts the object into the object reference table like
any other complex value. An over-long body length is rejected before
any out-of-bounds read; an unregistered externalizable class is still
refused loudly (the decoder never guesses). Handlers are decoder
configuration and survivereset_tables. Six tests: fixed-length and
length-prefixed handlers, decode→encode wire round-trip, overrun
rejection, handler persistence across reset, and object-reference
participation. -
RTMP 1.0 §5.3 Acknowledgement / §5.5 Window Acknowledgement Size —
honoured end-to-end (src/chunk.rs,src/server.rs,
src/client.rs). Until now both peers advertised a Window
Acknowledgement Size but neither side ever sent the §5.3
Acknowledgement the spec mandates ("the client or the server sends
the acknowledgment to the peer after receiving bytes equal to the
window size") — the client code even carried a// future refinementcomment in its control-message branch.ChunkReadernow
counts every byte it consumes off the wire (basic header, message
header, extended timestamp, payload) as the §5.3 sequence number via
a newread_exact_countedfunnel, stores the peer-negotiated window
(set_window_ack_size, fed from inbound §5.5 Window Ack Size and the
§5.6 Set Peer Bandwidth output-bandwidth value, which the spec
defines as equal to the window size), and exposesack_due()—
returnsSome(seq)the first time the received-byte count crosses
the window, re-arming only after another full window so a steady
stream never spams acks.RtmpSession::next_packet(server) and
RtmpClient::poll_event(client) call it after each wire read and
emitbuild_ack(seq)when one is owed; both setup paths
(drive_until_publish/wait_for_result) seed the window so the
obligation is live before the first media frame.received_bytes()
/window_ack_size()accessors round out the public surface.
Resetting the window re-bases the byte accounting so an
already-counted byte never instantly owes an ack, and with no window
negotiated the obligation stays dormant (byte-identical to the
pre-§5.3 behaviour). Newtests/acknowledgement_window.rsdrives a
raw publisher (built from the publicchunk/message/
handshakemodules) that advertises a 256-byte window and confirms
the realRtmpServeracks back with a plausible sequence number;
fourchunk.rsunit tests cover the byte-count, window-crossing,
one-ack-per-window, and re-base behaviours. -
Enhanced RTMP v2 Reconnect Request — end-to-end
(src/message.rs,src/server.rs,src/client.rs). Round 277 wires
theNetConnection.Connect.ReconnectRequeststatus event from
enhanced-rtmp-v2.pdf§"Reconnect Request" — until now the crate
advertised the matchingcapsExReconnectbit (CAPS_EX_RECONNECT)
during connect-capability negotiation but had no way to send or
react to the event itself. New
message::build_reconnect_request(tc_url, description)emits the
spec's NetConnection-level onStatus shape —["onStatus", 0.0, null, info]on message stream 0, with the Info Object carrying the
mandatorycode = NetConnection.Connect.ReconnectRequest
(exported asRECONNECT_REQUEST_CODE) +level = "status"pairs
and the optionaltcUrl/descriptionproperties (omitted from
the wire whenNone, per their "optional" marking in the spec's
Info Object table).RtmpSession::send_reconnect_requestis the
ingest-side helper — used "prior to the shutdown of the live
streaming server or when the server intends to remap the client to
another server instance," after which the old server keeps
processing publisher messages per spec (sonext_packetpumping
continues unchanged). On the publisher side,
RtmpClient::poll_eventnow classifies the event into the new typed
ClientEvent::ReconnectRequest { tc_url, description }variant
(code match alone is not enough — the spec says level MUST be
status, so a mismatched level falls through as a plain
OnStatus). The newresolve_tc_url(base, reference)/
RtmpClient::resolve_reconnect_url(Option<&str>)helpers apply the
spec's target-resolution rule — "if not specified, use the tcUrl for
the current connection. A relative URI reference should be resolved
relative to the tcUrl for the current connection" — covering all
four reference shapes the Info Object table gives as examples
(absolutertmp://host:port/app, network-path//host/app,
absolute-path/app, and relative-pathapp);RtmpClient::tc_url()
exposes the stored base. Tests: wire-shape + optional-property
omission insrc/message.rs, classification + resolution tables in
src/client.rs, and `tests/reconnect_requ...
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 { stre...
v0.0.4
Other
- graceful FIN on close to stop teardown truncating A/V frames
- Enhanced RTMP v2 ModEx packet-type prelude (audio + video)
- route AMF3 data/command messages into message dispatch
- AMF3 wire-format parser + builder (full §3.1 + §1.3.1 + §2.2)
- Enhanced RTMP v2 video FourCC additions (vp08 / avc1 / vvc1)
- Enhanced RTMP v2 audio framing (FourCC Opus / FLAC / AC-3 / E-AC-3 / MP3 / AAC)
- Enhanced RTMP v1 video framing (FourCC HEVC / AV1 / VP9)
Fixed
- Client teardown no longer truncates in-flight A/V frames
(src/client.rs).RtmpClient::closepreviously did
TcpStream::shutdown(Shutdown::Both)immediately after writing the
closeStreamcommand. Closing the read half at the same instant lets
the kernel answer the peer's still-unacked data with a RST on some
platforms, which discards any audio/video messages the peer hasn't yet
drained from its receive buffer — so the last frames pluscloseStream
could vanish mid-stream.closenow shuts down only the write half
(Shutdown::Write, a graceful FIN); the peer drains every buffered
frame and ourcloseStreamcommand before observing EOF. This
resolves the intermittentloopback_publishfailure (server saw 2 of
4 video tags) that surfaced on fast Linux CI runners.
Added
- Enhanced RTMP v2 ModEx prelude (
src/flv.rs).flv::parse_video
/flv::build_videoandflv::parse_audio/flv::build_audionow
decode and re-emit theModExpacket-type prelude chain per
enhanced-rtmp-v2.pdf§"ExVideoTagHeader" / §"ExAudioTagHeader" (the
while (packetType == ModEx)loop). When the PacketType nibble of the
header byte isModEx (7 for video, 7 for audio), a chain of
size-prefixed entries precedes the FourCC: each entry is a
modExDataSize(UI8 + 1, escaping to a0xFFsentinel +UI16 + 1
for 256..=65536 bytes), themodExDatabytes, and a single byte whose
high nibble is themodExType(UB[4]) and whose low nibble is the
next PacketType (UB[4]) — looping until a non-ModEx PacketType
terminates the chain. Newflv::ModEx { mod_ex_type, data }captures
each entry; newVideoTag::mod_ex/AudioTag::mod_exfields hold the
ordered chain and round-trip it verbatim ahead of the real packet
type. The onlymod_ex_typedefined today is
TimestampOffsetNano = 0(abytesToUI240..=999_999 ns
sub-millisecond presentation offset);ModEx::timestamp_offset_nano,
ModEx::timestamp_offset_nano_entry, and
VideoTag::timestamp_offset_nano/AudioTag::timestamp_offset_nano
expose it. Crucially, after parsing,ex_packet_typeholds the real
PacketType recovered from the chain (notModEx), so the
video_to_packet/audio_to_packetadapters route a ModEx-prefixed
tag to the correct CodecId + packet flags transparently — previously
the header's7nibble would have been mis-read as an unknown
PacketType and the chain bytes mistaken for the FourCC. New public
constantsEX_PACKET_TYPE_MOD_EX,EX_PACKET_TYPE_MULTITRACK,
MOD_EX_TYPE_TIMESTAMP_OFFSET_NANO;flv::ModExre-exported at the
crate root. - 9 new tests (8 unit in
src/flv.rs, 1 integration in
tests/enhanced_rtmp_video.rs) cover: video + audio
TimestampOffsetNano single-entry round-trips, a two-entry chain
(TimestampOffsetNano + an unknown subtype preserved verbatim), the
UI16 size escape (300-byte modExData), the accessor rejecting the
wrong subtype / short data, controlled-failure on truncated chains
(missing data / nibble / FourCC) for both audio and video, byte-exact
no-prelude output for an emptymod_ex, and a full
ModEx-wire-bytes →parse_video→video_to_packet→ CodecId
resolution +build_videoround-trip proving the prelude is
transparent to the adapter. - AMF3 data / command message routing (
src/amf3.rs,
src/server.rs,src/client.rs). Wires the r93 AMF3 parser into the
RTMP message-dispatch path so AMF3-encodedonMetaData/
data-messages (message type id 15) and AMF3 commands (type 17) decode
end-to-end. Per AMF 3 spec §4.1 + AMF 0 spec §3.1, the outer
NetConnection message structure is AMF0 and a value switches to AMF3
via theavmplus-object-marker(0x11); new
amf3::decode_data_messageparses a type-15/17 body that is either
0x11-prefixed (the spec-mandated switch) or already-AMF3
(no-prefix, for channels negotiated to AMF3 from the start), sharing
one reference-table context across the whole body. New
Amf3Value::to_amf0()bridges the decoded AMF3 value graph onto the
Amf0Valueenum soserver::RtmpSession::next_packetsurfaces AMF3
metadata through the sameStreamPacket::Metadata(Amf0Value)path as
AMF0 —Integer/Datecollapse toNumber/Date, sealed +
dynamic object members concatenate into one orderedObject, the
AMF3Arraydense slot becomes an ECMA-array under stringified
ordinal keys, andByteArray/Vector/Dictionary/Xml*map to
their nearest AMF0 shape. The server'sMSG_DATA_AMF3/
MSG_COMMAND_AMF3arms now route (the AMF3 command path detects the
samecloseStream/deleteStream/FCUnpublishteardown as
AMF0). NewRtmpClient::send_metadata_amf3emits an AMF3-encoded
onMetaDatafor peers on an AMF3 channel.pub const amf3::AVMPLUS_OBJECT_MARKER;Amf0Value/Amf3Valuere-exported
at the crate root. This resolves the r93 follow-up noted below. - 9 new tests: 4 unit tests in
src/amf3.rscoverdecode_data_message
framing (avmplus-wrapped sequence, unprefixed-AMF3, shared reference
context, dangling-marker error); 5 cover theto_amf0bridge
(scalars with Integer/Date collapse, sealed+dynamic merge ordering,
Array→ECMA ordinal keys, Vector/ByteArray→StrictArray, and a full
realisticonMetaDatabody bridged into an AMF0 object). A new
tests/amf3_metadata.rsintegration test drives a full
client→server loopback publishing an AMF3onMetaDataand asserts
the server surfaces every field throughStreamPacket::Metadata. - AMF3 wire-format parser + builder (
src/amf3.rs). Implements the
full Adobe "Action Message Format -- AMF 3" (January 2013)
specification mirrored underdocs/streaming/rtmp/amf3-file-format- spec-adobe.pdf: all thirteen value markers (Undefined / Null /
False / True / Integer / Double / String / XMLDocument / Date /
Array / Object / XML / ByteArray / Vector{Int,UInt,Double,Object} /
Dictionary), U29 variable-length integers (§1.3.1) with explicit
sign-extension for the Integer marker (§3.6), and the three
reference tables (strings / objects / traits) maintained per
decode_allinvocation per §2.2. Object support distinguishes
anonymous / typed / dynamic / externalizable shapes (§3.12);
externalizable bodies surface asSome(Vec<u8>)on the
Amf3Value::Object::externalizable_bodyfield for round-tripping,
with generic decode refusing externalizable inputs (no class
handler registered) rather than silently corruptingpos.
Decoder::reset_tables()provides the §4.1 packet-boundary reset.
Encoder always emits literal (non-reference) values — the wire
bytes remain valid per spec, and any literal can re-enter the
decoder which will resolve references encountered later in the
same payload. New helpersanon_object/dynamic_object/
anon_object_unorderedmirror the AMF0 builder ergonomics. - New 26 unit tests in
src/amf3.rsexercise: U29 length-class
boundaries (1-byte, 2-byte, 3-byte, 4-byte) and a spec-Table-1
canonical-bytes check for 0x7F / 0x80 / 0x4000 / 0x200000; all
simple-marker (Undefined/Null/Boolean) round-trips;
Integer sign extension at the negative boundary plus the
out-of-range fallback to Double; literal-then-reference for both
string and object tables; empty-string-never-in-table per §1.3.2;
Date / ByteArray round-trips; dense + associative Array shapes;
anonymous, dynamic, and typed-with-sealed-and-dynamic Object
shapes; externalizable-without-handler refuses cleanly;
Vector.<int>/<uint>/<Number>/<Object>(mixed-type)
round-trips;Dictionarywith both String and Integer keys;
Xml/XmlDocumentround-trips; multi-value packet sharing the
string table across values; dangling-reference rejected; unknown
marker rejected; trait reference re-used between two consecutive
typed-object encodings; and object reference resolving to the
same Date value.
Notes
-
AMF3 message routing via the AMF0
avmplus-object-marker(0x11) is
now wired into the server's message dispatch (see the r96 entry
above) throughamf3::decode_data_message+Amf3Value::to_amf0,
rather than by extendingamf::Amf0Valuewith a wrapping variant.
The standaloneamf::decodepath still consumes pure AMF0 only;
AMF3-channel callers use theamf3module (directly or via the
server / client routing) — the cleaner split given AMF3's
per-message reference-table context. -
Enhanced RTMP v2 video FourCC additions (Veovera 2026).
flv::parse_video/flv::build_videonow recognise the three
newVideoFourCcvalues fromenhanced-rtmp-v2.pdf
§"Enhanced Video":vp08(VP8 —VPCodecConfigurationRecord
for SequenceStart, no SI24 on the wire),avc1(FourCC-mode
AVC/H.264 —AVCDecoderConfigurationRecordfor SequenceStart,
SI24compositionTimeOffseton the wire forCodedFramesand
implied-zero forCodedFramesX, mirroring the legacy AVC path),
andvvc1(VVC/H.266 —VVCDecoderConfigurationRecordfor
SequenceStart, SI24 on the wire forCodedFramesand
implied-zero forCodedFramesX, parallel to HEVC's row). The
parse-sideneeds_ctsrule and the build-sidects_on_wire
rule are widened symmetrically: the three NALU-based FourCCs
(hvc1/avc1/vvc1) all carry the SI24 with
CodedFrames; the non-NALU FourCCs (av01/vp09/vp08)
and the SequenceStart / SequenceEnd / Metadata / CodedFramesX
shapes never do. -
**Fo...
v0.0.3
Other
- replace never-match regex with semver_check = false
- migrate to centralized OxideAV/.github reusable workflows
- SourceRegistry PacketSource for rtmp:// URIs
- pin release-plz to patch-only bumps
Added
SourceRegistryintegration viaregister(registry). New
RtmpPacketSourceadapter implements
oxideav_core::PacketSource, wrapping anRtmpSessionand
emittingoxideav_core::Packets on stream 0 (audio) and
stream 1 (video) withTimeBase 1/1000(RTMP's native
millisecond unit). Codec ids resolved from the first audio +
video tags viaaudio_codec_id/video_codec_id
(aac/mp3/pcm_*/speex/nellymoserfor audio,
h264/h263/vp6f/vp6a/flashsv/flashsv2for video).
AVC composition-time offsets are applied to PTS;
sequence-header packets carry theheaderflag and
AVCDecoderConfigurationRecord / AudioSpecificConfig in
CodecParameters::extradata. Listen-style opener:
rtmp://host:port/app/stream-nameURIs bind a one-shot
listener that accepts one publisher and validates the
announced app + stream-name against the URL path. The
historicalRtmpServer::accept/RtmpClient::connectAPI is
unchanged — registry support is purely additive.- New integration test (
tests/packet_source.rs) round-trips a
synthetic publisher → registry opener →PacketSourceflow,
asserting stream descriptors, packet ordering, pts/dts, and
theheader/keyframeflags.
v0.0.2
v0.0.1
chore: Release package oxideav-rtmp version 0.0.1