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. -
FourCC →
CodecIdmapping for the v2 video additions.
adapter::video_fourcc_codec_idnow resolvesvp08→
"vp8",avc1→"h264"(the same codec id legacy AVC
reports, so a downstreamoxideav-h264decoder picks both up
unchanged),vvc1→"vvc". The dispatcher
video_codec_id_for_tagalready routes FourCC-mode tags
through this mapper, so the new codecs surface end-to-end on
thePacketSourceadapter without any other change. -
New
FOURCC_VP8/FOURCC_AVC/FOURCC_VVCpublic
constants inflvso callers composingVideoTagliterals
for the v2 set don't have to repeat the spec's ASCII bytes. -
New tests (10 in
src/flv.rs, 5 intests/enhanced_rtmp_video.rs)
exercise VP8 SequenceStart + CodedFrames, AVC FourCC
SequenceStart + CodedFrames-with-SI24 + CodedFramesX, VVC
SequenceStart + negative-CTS CodedFrames + CodedFramesX,
truncated-SI24 controlled-failure, v2-FourCC disjointness from
the v1 set, and the full v1+v2 build → parse → build
idempotence sweep extended with eight new cases. The
AVC-FourCC keyframe test additionally confirms the resulting
Packetresolves toCodecId("h264")and applies the SI24
toptscorrectly.
Notes
-
The
connectcommand'svideoFourCcInfoMapadvertisement
(enhanced-rtmp-v2.pdf§"Enhancing NetConnection connect
Command") still does not list the new v2 codecs — a publisher
usingRtmpClient::connectcontinues to negotiate as a legacy
AVC-only client. Manually-composedVideoTagliterals with
fourcc = Some(FOURCC_VP8 / FOURCC_AVC / FOURCC_VVC)going
throughflv::build_videoproduce correct wire bytes; the
high-level publish helper opts in once the configurable
codec-list follow-up lands. -
Enhanced RTMP v2 audio framing (Veovera 2026).
flv::parse_audio
/flv::build_audionow recognise theExHeader = 9value in the
SoundFormatnibble of the audio-tag header byte and handle the
FourCC-based extended header (Opus/fLaC/ac-3/ec-3/
.mp3/mp4a) perenhanced-rtmp-v2.pdf§"Enhanced Audio" /
"Extended AudioTagHeader" / "ExAudioTagBody". The three core
AudioPacketTypevalues round-trip:SequenceStart(per-codec
sequence header —OpusHead/fLaC + STREAMINFO/
AudioSpecificConfigfor FourCC-AAC),CodedFrames(codec
bitstream — AC-3 / E-AC-3 sync frames, Opus self-delimited packets
per RFC 6716 App. B, MP3 frames, raw AAC frames), andSequenceEnd
(empty body, "no less than the same meaning as a silence message"
per spec). NewAudioTagfieldsex_packet_type: Option<u8>and
audio_fourcc: Option<[u8; 4]>are the discriminators; legacy
publishers leave bothNoneand the parser / builder follow the
pre-2023 SoundFormat / SoundRate / SoundSize / SoundType single-byte
path unchanged. -
FourCC →
CodecIdmapping for audio. New
adapter::audio_fourcc_codec_id([u8; 4]) -> CodecIdresolves
Opus/fLaC/ac-3/ec-3/.mp3/mp4ato
"opus"/"flac"/"ac3"/"eac3"/"mp3"/"aac", and the new
dispatcheradapter::audio_codec_id_for_tag(&AudioTag) -> CodecId
selects legacy vs FourCC offtag.audio_fourcc.is_some().
audio_codec_paramsnow copies the body of any Enhanced-RTMP
PacketTypeSequenceStartaudio tag intoCodecParameters.extradata
(matching the existing AVC / HEVC behaviour), so downstream Opus /
FLAC / AAC decoders pick up their initialisation header without
re-parsing the packet stream. -
Packet flags propagated for Enhanced RTMP audio.
audio_to_packetnow setsflags.header = truefor both legacy
AAC sequence-headers (unchanged) and Enhanced-RTMP
PacketTypeSequenceStart, and also flagsSequenceEndas a header
packet (empty body) so consumers can route it to an end-of-sequence
/ flush boundary without trying to decode an empty payload. The
legacy AAC packet-type marker byte is not prepended in Enhanced
mode — the body is the raw codec data perExAudioTagBody. -
New
AUDIO_FORMAT_EX_HEADER/AUDIO_PACKET_TYPE_*/FOURCC_AC3
/FOURCC_EAC3/FOURCC_OPUS/FOURCC_MP3/FOURCC_FLAC/
FOURCC_AACpublic constants inflvso callers composing
AudioTagliterals (e.g. an Enhanced-RTMP-aware push client) don't
have to repeat the spec's magic numbers. -
New integration test (
tests/enhanced_rtmp_audio.rs, 9 cases)
exercises wire-byte →Packetflow for OpusSequenceStart
(OpusHeadID-header round-trip), AC-3 / E-AC-3 / MP3
CodedFrames, FLACSequenceStart(with the in-bodyfLaC
signature distinguished from the framing FourCC),SequenceEnd,
build-parse-build idempotence across the 5-FourCC × 3-PacketType
matrix, and legacy/Enhanced disjointness.
Notes
-
AudioPacketType
Multitrack,MultichannelConfig, andModEx
(with the only-definedTimestampOffsetNano = 0subtype) parse
paths are not implemented yet. Their nested layouts
(per-track FourCC + size-prefixed track chunks; AudioChannelOrder- channel-count + channel-map / 32-bit AudioChannelFlags mask;
size-prefixed ModEx data + ModExType nibble chain) are spec'd in
enhanced-rtmp-v2.pdf§"ExAudioTagBody" but warrant a dedicated
follow-up round so we can wire them throughaudio_to_packet/
CodecParametersproperly. A tag whoseAudioPacketTypedecodes
toMultitrack,MultichannelConfig, orModExis currently
preserved verbatim (FourCC + raw body) — the parser does not
fail, but the caller is expected to skip the message rather than
interpret the body as a normalCodedFramespayload.
- channel-count + channel-map / 32-bit AudioChannelFlags mask;
-
The
connectcommand'saudioFourCcInfoMap/capsEx/
videoFourCcInfoMapadvertisements (enhanced-rtmp-v2.pdf
§"Enhancing NetConnection connect Command") are still not
populated byRtmpClient. A publisher usingRtmpClient::connect
will negotiate as a legacy AVC + AAC client. Manually-composed
AudioTagliterals withaudio_fourcc = Some(..)going through
flv::build_audiostill produce correct wire bytes; only the
high-level publish helper declines to opt in until a future round
adds a configurable codec list toRtmpClient. -
Enhanced RTMP v1 video framing (Veovera 2023).
flv::parse_video
/flv::build_videonow recognise theIsExHeaderflag in the
high bit of the video-tag header byte and handle the
FourCC-based extended header (hvc1/av01/vp09) per
enhanced-rtmp-v1.pdf§"Defining Additional Video Codecs",
Table 4. All fivePacketTypevalues round-trip:
SequenceStart(codec configuration record —HEVCDecoder ConfigurationRecord/AV1CodecConfigurationRecord/
VPCodecConfigurationRecord),CodedFrames,CodedFramesX
(the SI24=0 wire-size optimisation),SequenceEnd, and
PacketTypeMetadata(HDRcolorInfo). The SI24
CompositionTimeis emitted only for the one shape that
carries it — HEVC ×CodedFrames— matching the spec's
"CompositionTime Offset is implied to equal zero" exception
for the non-HEVC FourCCs andCodedFramesX. NewVideoTag
fieldsex_packet_type: Option<u8>andfourcc: Option<[u8; 4]>are the discriminators; legacy publishers leave both
Noneand the parser/builder follow the pre-2023 single-byte
CodecIDpath unchanged. -
FourCC →
CodecIdmapping. New
adapter::video_fourcc_codec_id([u8; 4]) -> CodecIdresolves
hvc1/av01/vp09to"hevc"/"av1"/"vp9", and the new
dispatcheradapter::video_codec_id_for_tag(&VideoTag) -> CodecIdselects legacy vs FourCC offtag.fourcc.is_some().
video_codec_paramsnow copies the body of any Enhanced-RTMP
PacketTypeSequenceStarttag intoCodecParameters.extradata
(matching the existing AVC behaviour), so downstream HEVC /
AV1 / VP9 decoders pick up their configuration record without
re-parsing the packet stream. -
Packet flags propagated for Enhanced RTMP.
video_to_packet
now setsflags.header = truefor both legacy AVC
sequence-headers and Enhanced-RTMPPacketTypeSequenceStart,
preserves the keyframe bit forCodedFrames(X), and
suppresseskeyframewhile settingheaderfor
PacketTypeMetadata(per spec: "presence of
PacketTypeMetadata means that FrameType flags at the top of
this table should be ignored"). The HEVC ×CodedFrames
SI24 CTS is applied toptsthe same way AVC's CTS is, so a
B-frame publisher with a non-zero composition-time offset
gets the correctdts != ptssplit on the consumer side. -
New
EX_PACKET_TYPE_*/FOURCC_*/VIDEO_IS_EX_HEADER
public constants inflvso callers composing
VideoTagliterals (e.g. an Enhanced-RTMP-aware push
client) don't have to repeat the spec's magic numbers. -
New integration test (
tests/enhanced_rtmp_video.rs)
exercises wire-byte →Packetflow for HEVC keyframes, HEVC
negative-CTS, AV1 CodedFrames, VP9 SequenceStart, and a
build-parse-build idempotence sweep across all six
FourCC × PacketType combinations the spec defines.
Notes
- The
connectcommand'sfourCcListadvertisement (Enhanced
RTMP v1 Table 5) is not populated by the client yet — a
publisher usingRtmpClient::connectwill negotiate as a
legacy AVC-only client. Manually-composedVideoTagliterals
withfourcc = Some(..)going throughflv::build_videostill
produce correct wire bytes; only the high-level publish helper
declines to opt in until a future round adds a configurable
codec list toRtmpClient. - AMF3 message bodies (TagType 15 / Command type 17 / Data
type 15 / Shared-Object type 16) are now decodable via the
newamf3module — see the entry above for the wire-format
parser landing.