Repository navigation
Releases: fishloa/rust-broadcast
Release list
DVB lockstep 11.0.0
DVB lockstep 11.0.0 — 2026-10-05
Major. The five lockstep crates move together to 11.0.0 because the serializer
and parser hardening in dvb-si, dvb-t2mi and dvb-bbframe changes behaviour
(over-range lengths and counts are now errors, not silently wrapped bytes), and
because all five cross the mpeg-ts 0.4 → 0.5 and broadcast-common 9.4
dependency epochs. Read the per-crate notes for the migration details; this page
is the map.
| crate | previous | new | what to expect |
|---|---|---|---|
dvb-si |
10.0.1 | 11.0.0 | breaking: every serializer rejects frames it cannot describe correctly; several raw-byte fields (BIOP/carousel, RCT, AIT, INT/UNT) are now typed; many parser and spec-fidelity fixes. See dvb-si 11.0.0. |
dvb-t2mi |
10.0.1 | 11.0.0 | breaking, small: L1Pre::to_bytes, crc32 and serialize_with_crc return Result; a more robust raw-mode T2miPump. See dvb-t2mi 11.0.0. |
dvb-bbframe |
10.0.1 | 11.0.0 | breaking, one signature: packet::NmTsIter::new takes an explicit stride and is fallible; Normal-Mode extraction honours UPL, ISSYI and NPD framing and detects CRC-8 mismatches. See dvb-bbframe 11.0.0. |
dvb-conformance |
10.0.1 | 11.0.0 | major by lockstep, no public API change of its own; TR 101 290 indicator corrections change what the monitor reports on the same input. See dvb-conformance 11.0.0. |
dvb-tools |
10.0.1 | 11.0.0 | major by lockstep; two subcommands now give correct results on multi-program captures. See dvb-tools 11.0.0. |
Required dependencies
dvb-si, dvb-t2mi, dvb-bbframe and dvb-conformance now require
broadcast-common 9.4 (9.4.0), and dvb-si,
dvb-conformance and dvb-tools require mpeg-ts 0.5
(0.5.0). Both are published before this tag.
Upgrade
Move the whole lockstep set together; mixing 10.x and 11.x lockstep crates in one
build is not supported. Code that only parses is unaffected by the serializer
changes. Code that serializes should expect Err where it previously got a
truncated or masked byte, and should handle the three typed-field changes listed
in the dvb-si note.
Published from tag v11.0.0.
webrtc-runtime 0.2.0
webrtc-runtime 0.2.0
Released 2026-10-05.
Breaking (0.x minor), large release. It combines a security fix for the media feature (the ICE + DTLS-SRTP transport) with the de-hand-roll wave's move of the WHIP/WHEP signalling engine onto http/headers types. Upgrade if you use media::MediaTransport: before this release the DTLS peer certificate was never verified. Who must act: anyone constructing MediaTransportConfig (two new required fields), anyone driving MediaTransport (a new now: Instant argument, handle_timeout return type, new events), and anyone using the WHIP/WHEP state machines (HttpRequest/HttpResponse and several signatures changed). The previous published version is 0.1.0. Read together with rtcp-packet-0.4.0.md, which fixes the RTCP decode problem described under Fixes.
Security (media feature)
- GHSA-48qq-7p78-2jvj: the DTLS peer was never authenticated.
MediaTransportbuilt its DTLS configuration with certificate verification off, accepted DTLS from any source address, and replaced the session's SRTP keys on every completed handshake. In the passive (WHIP ingest) role an off-path host could complete its own handshake against the media port and substitute the session's keys. Now the peer's leaf certificate must hash toremote_fingerprint(RFC 8122), checked in the DTLS verify callback and again before any SRTP key is derived (digests compared without an early exit); the passive role requires a client certificate; DTLS is accepted only from the remote address of the ICE-selected pair (dropped before a pair is selected); and once a session's SRTP keys are installed, a handshake completed from a different address returns an error and leaves the keys untouched. - GHSA-89f2-5m24-r6m7: the cap on remote ICE candidates (RFC 8445 §6.1.2.5) could be bypassed: an authenticated STUN Binding Request from an unrecognised source address made the ICE agent create its own peer-reflexive candidate, uncounted by the cap, so a remote peer could grow the candidate and pair count without bound by sending from many source ports. New STUN source addresses are now checked against the same cap first; an address already admitted keeps working.
add_remote_candidaterejects candidates past the cap withError::Media, never silently dropped.
Breaking changes
1. MediaTransportConfig has two new required fields
pub remote_fingerprint: String, // the remote SDP's a=fingerprint, e.g. "sha-256 AB:CD:..."
pub max_remote_candidates: usize, // cap on admitted remote ICE candidateslet remote_fingerprint = webrtc_runtime::media::parse_remote_fingerprint(&remote_sdp)
.ok_or("offer has no a=fingerprint")?;
// max_remote_candidates: webrtc_runtime::media::MAX_REMOTE_CANDIDATES (100, the spec's recommended default)MediaTransport::new rejects a fingerprint that is not sha-256 followed by 32 colon-separated hex bytes (SHA-256 is the hash WebRTC endpoints must support, RFC 8827 §6.5). MediaTransportConfig::remote_fingerprint is validated the same way as parse_remote_fingerprint reads it (see Behaviour changes).
2. MediaTransport takes the clock from the caller
MediaTransport::new(config, now) takes a caller-supplied std::time::Instant; every internal timer (ICE agent, STUN gatherer) is scheduled from it and the transport never reads the wall clock. New poll_timeout() returns the earliest deadline over the ICE agent, every DTLS association, the STUN gatherer and the retired-key purge: schedule handle_timeout there instead of on a fixed tick. New local_candidates() returns the host candidate as an a=candidate: body.
3. MediaTransport event and return-type changes (#1090)
handle_timeout(now)returnsVec<MediaEvent>(was()). A failed ICE or DTLS timer drive is surfaced asMediaEvent::TimerError(String)instead of being silently discarded.- An SRTP/SRTCP authentication failure (spoofed or garbage traffic in the RFC 5764 §5.1.2 band; the expected outcome for unsolicited traffic on an open UDP port) is now
MediaEvent::AuthFailure, not anErrfromhandle_datagram. - An inbound SRTCP packet that authenticates but does not decode as an RFC 3550 §6 compound is
MediaEvent::RtcpUnsupported(rtcp_packet::Error)instead of anErrfor a genuine peer packet; it still counts toward the RFC 3711 key-lifetime read counter. DecryptedRtpgainedextension: Option<DecryptedRtpExtension>(RFC 3550 §5.3.1, for example RFC 8285 CVO, AV1 dependency descriptor,mid,rid), which used to be dropped between decrypt and the caller.MediaTransport::rekeyreturnsError::MediaforSetupRole::Passiveinstead of tearing down the association. A Passive side never dials out, so the old behaviour left it waiting forever for aClientHelloa browser or OBS peer never sends without a new SDP offer. Drive an ICE restart or SDP renegotiation instead.
4. WHIP/WHEP state machines speak http and headers types
The crate is now std (the std feature is kept as a name but no longer gates anything) and left CI's thumbv7em-none-eabi target list. HttpRequest and HttpResponse are one shared definition (re-exported from whip::{client,server} and whep::{player,server}):
// 0.1.0
HttpResponse { status: u16, content_type: Option<&'static str>, headers: Vec<(String, String)>, body }
HttpRequest { method: Method /* crate enum */, url, content_type: Option<&'static str>, headers: Vec<(String, String)>, body }
session.on_patch(fragment, if_match: Option<&str>) // WHIP
session.on_patch(content_type: &str, body, ..) // WHEP
WhepSession::no_publisher(retry_after_secs: Option<u32>)
// 0.2.0
HttpResponse { status: http::StatusCode, headers: http::HeaderMap, body }
HttpRequest { method: http::Method, url, headers: http::HeaderMap, body }
WhipSession::on_patch(fragment: Vec<u8>, headers: &HeaderMap)
WhepSession::on_patch(body: Vec<u8>, headers: &HeaderMap) // content type read from the headers
WhepSession::no_publisher(retry_after: Option<std::time::Duration>)Builders: HttpResponse::new(status), with_body, with_content_type, with_location, with_etag; accessors HttpRequest::content_type() and if_match() return the typed headers values. The old crate-level Method enums are http::Method. New Error::InvalidHeader { header }. Header names are lower-case in the HeaderMap.
WhipClient::add_candidateand itsbuffered_candidatesfield are removed:flush_candidatesnever read them (it takes its own aggregated fragment), so they were dead, unbounded-until-flush state.WhipClient::flush_candidates,ice_restart,terminateandWhepPlayer::trickle_ice,ice_restart,terminatenow record which request is in flight and dispatch the response on that instead of guessing from status andETag.WhepPlayer::trickle_iceandice_restarttake&mut self(were&self). Fixed defects: a trickle ack answered200with anETag(RFC 9725 permits204or200plusETag) was misread as an ICE-restart answer, and aDELETEanswered204left the clientEstablishedforever instead ofClosed(#1090).
Behaviour changes
If-Matchis an RFC 9110 entity-tag list with strong comparison.If-Match: *is an ICE restart and the current quoted tag ("etag1") a trickle update, as before. An unquoted tag (etag1), a quoted star ("*") and a weak tag (W/"etag1") no longer match: they fail withError::ETagMismatch, whosegotis now the raw header text ("\"old\""). A duplicatedContent-Typeon a PATCH is rejected. A weak serverETagis no longer reused as a strongIf-Matchtag by the WHIP/WHEP clients (treated as absent). A session URL (non-ASCII) or ETag that cannot be sent as a header makesacceptanswer500with noLocationinstead of emitting a bad header.media::parse_remote_fingerprintreads the SDP withsdp-types. The text must be a well-formed session (v=,o=,s=,t=); the first media section carrying ana=fingerprintwins over the session level; the result is the typed attribute's normalised text (a=fingerprint:SHA-256 ab:cd:0fgivessha-256 AB:CD:0F). A digest without colons is accepted when it is 32 bytes. The first level that carries ana=fingerprintdecides: an unparseable one givesNoneinstead of falling through to a later one.WhepSession::on_patchmatchesContent-Typeby media type only, soapplication/sdp; charset=utf-8is accepted like bareapplication/sdp(#1090).
Fixes
- Browser RTCP discarded (#1071).
MediaEvent::RtcpUnsupportedwas the outcome for nearly every real browser SRTCP datagram (RFC 4585 PSFB/RTPFB feedback or RFC 3611 XR, most of what a WebRTC peer sends), becausertcp_packet::CompoundPacket::parserejected the whole datagram on the unrecognisedPT, discarding a leading SR/RR's real statistics too. Fixed byrtcp-packet0.4 (RtcpPacket::Unknown); the doc comment ofRtcpUnsupportedis corrected accordingly. ice::parse_ice_server_links/format_ice_server_links: aLinkheader parameter value (username,credential) containing;,,or"(legal in an RFC 8288quoted-string, for example a static TURN password) now round-trips instead of being split or corrupted;relmatching is case-insensitive and accepts a space-separated list (#1090).- The server-reflexive candidate's
stun:URL brackets an IPv6 host (stun:[2001:db8::1]:3478). StunGather's deadline is the first instant at whichhandle_timeoutdoes work (rtc-stuncollects an expired transaction only whendeadline < now), so a driver sleeping until the raw deadline no longer spins.
New feature: test-support
Non-default, #[doc(hidden)], not public API: MediaTransport::with_certificate_for_test, force_next_timer_error, force_stuck_timer and media::certificate_fingerprint, used by multimux's loopback and faul...
ule 0.4.1
ule 0.4.1
Released 2026-10-05.
Patch release: four defects in how the crate frames and reassembles ULE (RFC 4326) SNDUs, all from the #1120 audit, plus one additive public method that exposes a check the serializer now performs. No API is removed or changed; nobody must act. You should upgrade if you feed ts::UleReceiver real transport streams (two of the fixes change which SNDUs it recovers) or if you build PayloadChain/Sndu values by hand (they now fail with an Err where they used to panic or emit a misframed chain).
Fixes in the TS receiver (ts::UleReceiver)
- Legal SNDUs starting with
0xFFwere discarded as padding. The receiver treated any leading0xFFbyte as stuffing. A legalD=1SNDU whoseLengthis in0x7F00..=0x7FFEalso starts with0xFFon the wire, so its header was silently thrown away and the receiver lost sync for the rest of the packet. Only the genuine 2-byte0xFFFFEnd Indicator, or a single trailing0xFFbyte too short to hold any header, now ends the packing walk (#1120). - Leftover bytes after a PUSI=0 continuation were parsed as a new SNDU. RFC 4326 section 6/7 lets an SNDU start only where the Payload Pointer of a PUSI=1 packet says one does. Non-padding bytes that followed the end of a completing SNDU in a PUSI=0 packet were nevertheless walked as a new packed SNDU, corrupting every later packing decision. Such bytes now reset the receiver to the Idle State; trailing
0xFFpadding is still accepted (#1120).
Fixes in serialization
PayloadChain::serialize_intopanicked or misframed on inconsistent Optional extension headers. It trustedh_lenoverbody.len(): abodylonger than2*h_len-2could panic on the output slice, a shorter one produced bytes that put the next Type field in the wrong place, andh_len == 0panicked with ausizeunderflow insideserialized_len()itself. Every header is now validated before any byte is written, and a bad one returnsError::InvalidExtensionHeader;serialized_len()no longer underflows (#1120).Sndu::serialize_intocould emit the End Indicator as a header.D=1withLength=0x7FFFserializes to0xFFFF, which every receiver, including this crate's ownts::UleReceiver, reads as "no more SNDUs in this packet", so the SNDU was silently truncated. That combination is now rejected withError::InvalidLength(#1120).
New API
ExtensionHeader::validate(&self) -> Result<()>: for anOptionalheader it checks thath_lenis in1..=5and thatbody.len() == 2*h_len-2; aMandatoryheader always passes. This is the same checkPayloadChain::serialize_intonow runs, so you can validate a header at construction time instead of at serialize time.
Migration: if you build ExtensionHeader::Optional { h_len, h_type, body } by hand, make sure body.len() == 2 * h_len as usize - 2 and h_len <= 5; previously a violation panicked or produced a bad chain, now it is an Err.
Dependencies
broadcast-common 9.3 -> 9.4 (see broadcast-common-9.4.0.md). No other Cargo.toml change.
Published from tag ule-v0.4.1.
ttml-subtitle 0.3.0
ttml-subtitle 0.3.0
Released 2026-10-05.
Breaking release (0.2 -> 0.3). Three independent things force it: the XML layer moves from roxmltree plus a hand-written serializer to quick-xml, which makes XML support std-only; content in namespaces the crate does not model (vendor extensions, TTML2 embedded resources) is now kept and re-emitted instead of dropped, which adds fields to the element types and removes other_attributes; and a few public types and signatures change. You must act if you build with --no-default-features, if you read other_attributes or TtElement::text, if you call Document::to_xml through a shared reference, or if you build or match WallclockForm::DateTime. Parsed documents and serialized output are identical to 0.2 for every committed fixture, and a number of timing-grammar and validator bugs are fixed.
Breaking changes
1. XML support requires the std feature
document, foreign and validation, their crate-root re-exports and the parse function are now behind std. A --no-default-features build exposes only error and time (the time-expression parser stays no_std + alloc). std is a default feature, so a default build is unaffected. The three from_scratch, parse_document and validate_document examples now declare required-features = ["std"].
# before (0.2): document/validation built without std
ttml-subtitle = { version = "0.2", default-features = false }
# after (0.3): enable std to get Document, parse, Validator, ...
ttml-subtitle = { version = "0.3" } # default features include std
# or, explicitly:
ttml-subtitle = { version = "0.3", default-features = false, features = ["std"] }Cargo.toml delta for the crate itself: roxmltree = { version = "0.20", default-features = false } removed; quick-xml = { version = "0.42", default-features = false, optional = true } added; std = ["broadcast-common/std", "thiserror/std", "dep:quick-xml"] (was ... "roxmltree/std").
2. Foreign and embedded content is modeled; element structs gain fields (#1110 TT-W1, TT-W2)
Attributes and child elements in namespaces the crate does not model used to be dropped. They are now kept as ForeignAttribute values (fields prefix, namespace, local_name, value, scoped_namespaces) and as UnknownElement subtrees, and are re-emitted (TTML2 sections 7.2, 7.3). Each element's unknown_children keep their relative document order, but their position between modeled children, metadata and animations is not tracked: each group lives in its own Vec and is emitted in a fixed group order.
TTML2 section 9 embedded and resource elements are now typed: AudioElement, ChunkElement, DataElement, FontElement, ResourcesElement, SourceElement, the InlineContent::Image and InlineContent::Audio variants, HeadElement::resources and DivElement::audio. Section 9 elements without a typed struct still go through the foreign-content mechanism.
Source-level consequences:
- Every element type gained
foreign_attributesandunknown_children; metadata-like types also gainedscoped_namespaces;MetadataChildgained anUnknownvariant. New fieldsxml_base,ttm_role/ttm_role_sourceandxlink_href/xlink_role(and siblings) were added to the element types that carry them. All of these structs and enums are#[non_exhaustive], so downstream code that builds them withType::default()plus field assignment, and matches with a_arm, keeps compiling. (The CHANGELOG says struct-literal construction "must use..Default::default()"; for a downstream crate,#[non_exhaustive]forbids struct literals altogether, so that advice only applies inside this crate.) - Removed: the
other_attributes: BTreeMap<(String, String), String>field on every element type (useforeign_attributes, aVec<ForeignAttribute>) andTtElement::text: Option<String>. Document::to_xmlnow takes&mut self(was&self) because it assigns fallback prefixes for outer-scope vendor namespaces. ADocumentyou only hold by shared reference can no longer be serialized without cloning it first.
// before (0.2)
fn dump(doc: &Document) -> String { doc.to_xml() }
// after (0.3)
fn dump(doc: &mut Document) -> String { doc.to_xml() }3. WallclockForm::DateTime.seconds is Option<u8> (#1108 TT-W5)
It was u8. None now means the hhmm-time form (no seconds component), distinct from an explicit :00, so format_time_expression no longer inserts a :00 that was not in the source.
// before: WallclockForm::DateTime { seconds: 0, .. }
// after: WallclockForm::DateTime { seconds: Some(0), .. } // or None for hh:mmBehaviour changes (not API breaks)
The new parser is a single pass over quick_xml::NsReader events straight into the typed structs, with an explicit stack of partially-built spans and no document tree, so hostile nesting cannot overflow the stack. Serialization writes quick_xml::Writer events, so the old replace-chain escaper is gone. Malformed input is still a structured Error::XmlParse; a DOCTYPE, undefined entity, unbound namespace prefix, second root element, or content after the root is rejected. The differences from the roxmltree parser all follow XML 1.0:
- A root
<tt>is the document even when it has a<tt>child. For<tt xmlns="http://www.w3.org/ns/ttml"><tt xmlns="http://www.w3.org/ns/ttml"/></tt>the outer element is now parsed (the inner one used to be) and the inner<tt>is kept as an unknown child. A non-ttwrapper root still yields its first<tt>child. - A numeric reference to a control character (
<p></p>), or a literal one, in text or in an attribute is rejected withError::XmlParse. - An entity reference or CDATA section before the root element is rejected.
- More than 128 namespace declarations in scope at once is rejected (quick-xml's namespace-binding limit, per the CHANGELOG).
- On output, an attribute value containing
\t,\nor\ris written as	, , (region="x\ny"rendersregion="x y"), and a\rin text as , so they survive a re-parse.
Fixes
Serialization and namespaces (#1110 TT-W1):
- An inner-scope
xmlns:prefix override no longer silently re-points an outer-scope vendor attribute at the wrong namespace; the outer URI keeps its own declaration under a generatedttmfallbackNprefix. <br>and<span>attributes,<p>child ordering and thexml:space/xml:baseattributes are no longer dropped by the serializer.
Time expressions (#1108):
- TT-W4: the default
ttp:tickRate(section 7.2.11) always multiplied the (possibly defaulted-to-30) frame rate by the sub-frame rate, instead of defaulting to 1 tick per second when nottp:frameRatewas specified, and ignoredttp:frameRateMultiplierin the effective frame rate. The computation is now also checked/saturatingu64rather than uncheckedu32. - TT-W5: the clock-time and wallclock-time grammars (section 12.3.1) were too lenient. A seconds fraction and a
:framesterm were accepted together (the grammar makes them exclusive), a single-digitframesterm was accepted (two or more digits required),"wallclock(10:00)junk"was accepted (trailing content after the closing paren was discarded), and 2-digit wallclock hours, minutes and seconds accepted a leading+. All are now rejected.
Profile validator (#1108 TT-W3):
- Its module doc claimed the full 159-row Feature/Extension disposition table and all of sections 8 and 9; it now documents exactly the smaller set of checks that are implemented. Treat a pass as "none of those checks failed", not as full IMSC conformance.
body_has_frame_usagechecked<body>'s ownbegin/dur/endinside the<div>loop, so a<body>with no<div>was never checked; it now also covers<div>'s own timing.begin/dur/endvalues now go throughtime::parse_time_expression(section 12.3.1), sobegin="garbage"is rejected instead of validating.
Dependencies
broadcast-common 9.3 -> 9.4 (see broadcast-common-9.4.0.md); roxmltree removed; quick-xml 0.42 added behind std. quick-xml is the workspace's single XML dependency for the crates that read or write XML (transmux, dvb-mabr, ttml-subtitle, multimux).
Published from tag ttml-subtitle-v0.3.0.
ts-fix 0.6.0
ts-fix 0.6.0
Released 2026-10-05.
Minor-epoch release (0.x); fixes only, no flag or output-format change. Four defects that produced wrong or undecryptable output are fixed: --service dropped CA/ECM/EMM PIDs, a multi-operation run lost or bypassed the flush output of earlier operations, the CLI aborted on any capture that was not a clean multiple of 188 bytes, and --regen-psi regenerated a PAT with a resetting continuity counter, a fixed version and no network PID. Re-run any pipeline that used --service on a scrambled service or --regen-psi. The dependency epochs move with the wave: see mpeg-ts 0.5.0, mpeg-pes 0.5.0, dvb-si 11.0.0, dvb-conformance 11.0.0 and scte35-splice 3.0.0.
Dependency changes
-broadcast-common = { version = "9.3", default-features = false }
+broadcast-common = { version = "9.4", default-features = false }
-mpeg-ts = { version = "0.4", default-features = false }
+mpeg-ts = { version = "0.5", default-features = false }
-mpeg-pes = { version = "0.4", default-features = false }
+mpeg-pes = { version = "0.5", default-features = false }
-dvb-si = { version = "10", default-features = false }
+dvb-si = { version = "11.0", default-features = false }
-dvb-conformance = { version = "10", default-features = false }
+dvb-conformance = { version = "11.0", default-features = false }
-scte35-splice = { version = "2.1", default-features = false }
+scte35-splice = { version = "3.0", default-features = false }Fixes
--service(service extract,PidFilterOp) (#1101). The keep-set was only the PMT'spcr_pidandelementary_pids, so every ECM PID (from the CA descriptors in the PMT's program-info and ES-info loops, ISO/IEC 13818-1 §2.6.16), the CAT (PID 0x0001) and the EMM PIDs it references were dropped, and extracting a scrambled service produced undecryptable output. CA PIDs, the CAT and its EMM PIDs are now kept. The previously terminalResolvedstate also keeps observing PAT/PMT version changes, so a programme whose PMT PID moves or whose ES PIDs are added mid-stream is no longer silently truncated.- Multi-operation
finish(#1101). Each operation's flush output is now fed through every later operation'sprocess, aspushalready did for packets. Previously only the last operation drained the staging buffer, so the fallback PAT that--regen-psiemits fromPsiRegenOp::flushwhen the input has no PAT slot bypassedrepair_continuityandstuffing, or was dropped entirely when a later operation was configured. - The CLI no longer aborts on a capture that is not a clean, sync-perfect multiple of 188 bytes (#1101). It now resynchronises with
mpeg_ts::resync::TsResync(asdvb-toolsdoes) instead ofchunks(188), so a capture starting mid-packet, a single corrupted sync byte, or a 204-byte RS-coded capture (parity stripped) is repaired, with the resync counts reported on stderr. Before, it failed withmissing TS sync byteand wrote no output. --regen-psi(PsiRegenOp) (#1037). Every regenerated PAT packet used a freshSectionPacketiser, socontinuity_counterrestarted at 0 on each emission;version_numberwas hard-coded to 0 even when the program mapping changed; and the original PAT'snetwork_pidentry (program_number == 0, ISO/IEC 13818-1 §2.4.4.3), which cannot be derived from any PMT, was dropped. The packetiser is now reused across emissions,version_numberincrements only when the mapping actually changes, and thenetwork_pidentry is preserved when present. The--regen-psihelp text andregen_psidocs no longer say the PAT is rebuilt "on flush"; it is replaced in position.
Published from tag ts-fix-v0.6.0.
transmux 0.25.0
transmux 0.25.0
Released 2026-10-05.
Breaking (0.x minor) and security release of the container-muxing hub, and the largest transmux release to date. It is the sum of three things that were never published separately: the twelve-advisory security patch prepared as 0.24.2 (the last tag on crates.io is 0.24.1, so there is no 0.24.2), the audit rounds r04 and r05 (#1079, #1080, #1081, #1129, #1140, #1141, #1148), which fixed dozens of demux/mux paths that returned Ok with wrong, truncated or reordered data, and the de-hand-roll of XML, URL, SDP, date/duration and hex/base64 handling onto quick-xml, url, sdp-types, jiff, hex and base64. Everyone who parses untrusted input should upgrade. Who must act: anyone building with --no-default-features that uses DASH, Smooth or PlayReady (XML now needs std); anyone who calls ESDescriptor::parse, Mpd::resolve_segment_url, ExtXKey::to_tag, the RTMP chunk writer, playready_pro/playready_pssh, or constructs any of the structs listed under "Struct changes"; anyone who stored output made by this crate's H.264 SAMPLE-AES, cbcs default-IV, KLV checksum or CLI HLS/DASH writers (those bytes change, see "Output that changes"); and anyone matching on transmux::Error exhaustively with Eq.
Read together with: broadcast-hls-0.3.0.md, mpeg-ts-0.5.0.md, mpeg-pes-0.5.0.md, mpeg-ps-0.5.0.md, rtcp-packet-0.4.0.md, container-probe-0.1.1.md (the CLI uses it), scte35-splice-3.0.0.md (dev-dependency epoch). Downstream in this wave: multimux-0.11.0.md, hls-runtime-0.7.0.md, media-plane-0.5.0.md, media-doctor-0.9.0.md. An earlier draft changelog described a hand-written RFC 3986 uri module; it never shipped, URL resolution is base_url over the url crate (below).
Security (the unpublished 0.24.2 fixes, now shipping here)
Twelve advisories, each with a regression test that fails on 0.24.1; tests/hostile_input_bounds.rs runs the allocation cases under a per-thread 64 MiB allocator cap.
| Area | Before |
|---|---|
| KLV | a long-form BER length overflowed offset arithmetic and panicked |
esds |
an ES_Descriptor size larger than the box, or a size-0 box, panicked the fMP4 demuxer |
sinf |
an oversized frma or header-only schm panicked |
pssh v1 |
a body with no DataSize panicked |
| progressive demux | one stts/ctts run could allocate about 17 GB from a roughly 100-byte file |
CencDecryptor progressive path |
stsz/stco counts allocated before their length checks |
sgpd |
zero-length entries could be pushed up to 2^32 times |
| H.264/H.265 SPS | read_ue returned 0 at end of data, so a roughly 20-byte SPS could loop effectively for ever |
| RTMP chunk reader | a fmt 1/2 header after an incomplete message carried stale bytes into the new one and underflowed its length |
CencEncryptor::encrypt |
a sample rejected mid-call left earlier samples encrypted with the IV counter unchanged, so a retry reused IVs |
KeyMap, CencEncryptor, CencDecryptor, cli::Args, cli::CliError::BadKey |
derived Debug printed raw content-key bytes or the raw <KID>:<key> argument |
progressive_demux stsc |
a chunk-run first_chunk was iterated to as written (up to u32::MAX) instead of clamped to the real chunk count |
Behaviour that follows: BitReader::read_ue errors at end of data and past 32 leading zeros, and SPS parsing rejects out-of-range fields (chroma_format_idc > 3, bit_depth_*_minus8 above 6 for H.264 or 8 for H.265, num_ref_frames_in_pic_order_cnt_cycle > 255, num_short_term_ref_pic_sets > 64); an esds box with size == 0 extends to the end of its container (ISO/IEC 14496-12 §4.2); CencEncryptor::encrypt plans every sample before encrypting any, so a rejected call leaves the media byte-identical and the IV counter unchanged; KeyMap, CencEncryptor and CencDecryptor hand-write Debug to redact key bytes (KIDs still print), and cli::Args/BadKey carry only the KID half or the argument length.
Breaking changes
XML needs std; quick-xml replaces the hand-rolled parser and writer
dash, dash_parse, smooth, smooth_parse, ll_dash::LlDashPackager and drm::{playready_wrmheader, playready_pro, playready_pssh} are gated behind std (and so are their crate-root re-exports, including rfc6381_codec_string's home module dash). A --no-default-features build keeps everything else (LlSegmenter/Chunk, the Widevine/FairPlay pssh builders, and so on). The private tokenizer (xml_parse) and xml_writer are gone.
# before: DASH/Smooth available no_std + alloc
transmux = { version = "0.24", default-features = false }
# after
transmux = { version = "0.25", default-features = false, features = ["std"] }Rendered MPD, Smooth manifest, LL-DASH MPD and WRMHEADER output is byte-identical for every committed fixture, with these exceptions that follow XML 1.0:
- a value containing
\t,\nor\rin an attribute is now written as	, , (soprofiles = "a\nb"rendersprofiles="a b") so it survives a re-parse; - parsers are stricter: a literal newline, tab or CR in an attribute value is normalised to a space (XML 1.0 §3.3.3:
<Period id="a⏎b"/>parsesidas"a b"; write to keep it), a duplicate attribute isMalformedAttribute(the first value used to win silently), an undefined entity, a raw&in an attribute or a mismatched end tag anywhere is an error, and a character outside the XML 1.0Charproduction () is a structured error.<BaseURL>a<x/>b</BaseURL>(markup inside a text-only element) now yields no base URL instead ofMismatchedEndTag. DashParseErrorandSmoothParseErrorgainXml { pos, message };DashParseError::MismatchedEndTag::expectedis now aString.LlDashPackagerrewrites the base MPD withquick-xmlevents instead of line-based text surgery.
ESDescriptor::parse now reads the framed bytes Serialize writes (#1148)
Parse for ESDescriptor used to read the bare descriptor body; it now reads ES_DescrTag (0x03), the expandable-size varint, then the body, so parse(serialize(x)) == x. The old contract misread the tag and size bytes as ES_ID/flags/URLlength, truncating the descriptor chain silently, and failed with a spurious BufferTooShort once a URLstring over about 100 bytes (up to the 255 its 8-bit URLlength allows) pushed the varint wide. The input contract flipped, and the compiler will not tell you. A caller that stripped the tag and size prefix itself must pass the framed bytes instead, or parse through EsdsBox::parse_box / EsdsBox::parse_body, which strip the box and FullBox framing and pass exactly what this impl expects. The only in-workspace caller was EsdsBox::parse_body.
// 0.24: body only (tag + size already stripped by hand)
let es = ESDescriptor::parse(&descriptor_bytes[prefix_len..])?;
// 0.25: the whole descriptor, tag and varint included
let es = ESDescriptor::parse(descriptor_bytes)?;
// or, from an esds box payload
let esds = transmux::mp4esds::EsdsBox::parse_body(body)?;Feeding bare-body bytes now returns InvalidValue { field: "descriptor_tag", .. }, or a silently misread result if the first body byte happens to be 0x03. Also: a non-UTF-8 URLstring is InvalidValue { field: "URLstring" } instead of a lossy decode (which broke byte-identical round trips and let 765 replacement bytes from a 255-byte input overflow URLlength).
transmux::Error: no longer Eq, new variants, fallible builders
Error now derives only PartialEq (it embeds broadcast_hls::Error, which carries an f64); a bound or derive that needs Eq on transmux::Error fails to compile. New variants (Error is #[non_exhaustive], so additive): FieldOverflow (wrapping broadcast_common::len::FieldOverflow), TooManyElementaryStreams { family, max }, HlsAttrValue(broadcast_hls::Error).
Serializers now return an error instead of silently truncating a length, count or offset that does not fit its wire field (#1129). Builders that were infallible and now return Result: rtmp::write_chunks, rtmp::MessageHeader::write_into, OutTag::write_into, the HevcNalUnit/HevcNalArray serializers, drm::playready_pro, drm::playready_pssh, and sample_aes::ExtXKey::to_tag (#1140). ExtXKey's inherent Display impl is removed: uri, keyformat and keyformatversions are caller-supplied and a ", CR or LF in any used to terminate the attribute list and inject playlist tags; to_tag now builds through broadcast_hls::AttrValue and returns Error::HlsAttrValue for an invalid value.
// 0.24
let line: String = key.to_tag(); // or format!("{key}")
let chunks = write_chunks(&msgs, 128);
// 0.25
let line: String = key.to_tag()?;
let chunks = write_chunks(&msgs, 128)?;The PMT section_length is bounded to 1021 bytes (§2.4.4.4) and returns Error::BufferCapExceeded instead of masking the 12-bit field. Field-range checks were added to avcC/hvcC arrays, mhaC, vpcC, dfLa, esds URLstring, RTMP 24-bit lengths, AMF0 counts, CENC senc/saio/saiz/pssh/tenc, subs/sgpd/sbgp, sidx v0 and per-reference fields, elst v0, and trun/stsz/dref/stsc/stco/co64/stss/stsd counts. A previously accepted over-range value is now an error, never a wrapped field.
URL resolution and the DASH parse model
Mpd::resolve_segment_urltakes the MPD's own URL as its first argument and returnsOption<String>:resolve_segment_url(mpd_url: Option<&url::Url>, period, adaptation_set, representation, reference: &str).Nonemeans the reference or aBaseURLcarries a control character or whitespa...
timed-metadata 0.6.0
timed-metadata 0.6.0
Released 2026-10-05.
Minor-epoch breaking release (0.x), with several SCTE-35 conversion fixes that change output. DateRange::to_tag_line now returns Result<String>, DateRange gains a public extra_attrs field, and scte35-splice moves to the 3.0 epoch (its types appear in this crate's public API, for example TimedEvent::from_scte35). The fixes matter more than the breaks for most users: time_signal() cues were previously lost, every SCTE-35 time was off by pts_adjustment, and DVB Teletext decoded to garbage. Output for the same input will differ. Act if you call to_tag_line, build DateRange with a struct literal, or use SCTE-35 conversion. Read with scte35-splice 3.0.0, broadcast-common 9.4.0 and mp4-emsg 0.4.1.
Dependency and feature changes
-scte35-splice = { version = "2.1", default-features = false }
+scte35-splice = { version = "3.0", default-features = false }
-broadcast-common = { version = "9.3", default-features = false }
+broadcast-common = { version = "9.4", default-features = false }
-cc-data = { version = "0.5", default-features = false, optional = true }
+cc-data = { version = "0.6", default-features = false, optional = true }
+broadcast-hls = { version = "0.3", default-features = false } # new
+jiff = { version = "0.2", default-features = false, features = ["alloc"] } # new, RFC 3339 formatting
+hex = { version = "0.4", default-features = false, features = ["alloc"] } # new, SCTE35-* hex
-std = ["scte35-splice/std", "mp4-emsg/std", "chrono?/std", "serde?/std", "cc-data?/std", "dvb-vbi?/std"]
+std = ["jiff/std", "scte35-splice/std", "mp4-emsg/std", "chrono?/std", "serde?/std", "cc-data?/std", "dvb-vbi?/std", "broadcast-hls/std"]
-serde = ["dep:serde", "scte35-splice/serde", "mp4-emsg/serde", "cc-data?/serde", "dvb-vbi?/serde"]
+serde = ["dep:serde", "scte35-splice/serde", "mp4-emsg/serde", "cc-data?/serde", "dvb-vbi?/serde", "broadcast-hls/serde"]All new dependencies are no_std + alloc, so the crate stays no_std. broadcast-hls is a new dependency edge; it is also what ssai-runtime uses for the same attribute-list tokenizer.
Breaking changes
1. DateRange::to_tag_line returns Result<String> (#1140, audit r12-TM-W4)
ID, CLASS and the SCTE35-* hex token are now built through broadcast_hls::AttrValue's checked constructors and rendered with the shared broadcast_hls::render_attribute_list, instead of hand-formatting ,NAME="VALUE" with no validation. ID and CLASS are often sourced from an upstream SCTE-35 segmentation_upid (network data), so a ", CR or LF in either used to break the attribute list; it is now an Err. DURATION and PLANNED-DURATION that are NaN, infinite or negative are rejected with the new Error::InvalidDuration { what, value }, both on parse and on render (NaN previously rendered as the bare token NaN). The crate's own quoted-comma splitter is replaced by broadcast_hls::parse_attribute_list.
// before
let line: String = range.to_tag_line();
// after
let line: String = range.to_tag_line()?;2. DateRange gains extra_attrs: Vec<(String, AttrValue)>
parse_tag_line used to drop every attribute it did not model (X-* client extensions, END-DATE, END-ON-NEXT, and so on). Unknown attributes are now preserved, sorted by name, and rendered back after the fixed-order fields, so a real EXT-X-DATERANGE with unrecognised attributes round-trips byte-identically. Struct-literal construction of DateRange needs the new field (extra_attrs: Vec::new() keeps the old behaviour).
3. New Error variants
EmsgPresentationTimeOverflow, UnsupportedPresentationTime (#1105), InvalidDuration and TimestampOutOfRange(i64). Error is #[non_exhaustive], so a wildcard arm already covers them.
Behaviour changes that change output
- SCTE-35 time signals (#1040). A
time_signal()cue, the dominant modern form, lost its time, id and kind entirely, becauseTimedEvent::from_scte35only readsplice_insert. It now readsTimeSignal.splice_time.pts_timeand takes id, kind and duration from the cue's first uncancelledsegmentation_descriptor.scte35_to_daterangenow errors instead of emittingID=""for a cue with no id (RFC 8216bis §4.4.5.1 requires unique IDs). - Event-kind classification from segmentation types. Only the types that leave or return to network programming for ad insertion map to
BreakStart/BreakEnd: Table 23'sBreak,Provider/DistributorAdvertisement,Provider/DistributorPlacementOpportunity(not theOverlayvariants, which composite over the network feed) andProvider/DistributorAdBlock.ProgramStart,ProgramEnd,ChapterStartandChapterEndmap toEventKind::Chapter. Credits, promos, unscheduled or alternate content, overlay placement opportunities andNetworkStart/NetworkEndmap toUnspecified(id, time and duration are still populated), so a consumer that splices ads onBreakStartcannot mistake them for an ad avail. pts_adjustment(#1039).TimedEvent::from_scte35ignoredsplice_info_section'spts_adjustment, so every derivedMediaTimeand DATERANGESTART-DATEwas off by that adjustment (SCTE 35 §9.6.1). Everypts_timeis now shifted throughbroadcast_common::clock33::add.- A cancelled
splice_insert(splice_event_cancel_indicator == true) was classified asBreakEnd, producing a spuriousSCTE35-IN; it is nowUnspecified. - Teletext (#1041). The decoder ran Hamming-8/4 and odd-parity FEC directly on
dvb_vbi::TeletextDataField::txt_data_blockbytes without reversing them. EN 300 706 transmits each byte LSB-first, so a real DVB Teletext stream decoded to garbage or produced no cues. Bytes are now bit-reversed first, throughTeletextDataField::txt_data_block_logical()rather than a privatereverse_bitsmap (#1106). The synthetic fixture was regenerated in wire bit order and independently verified against TSDuck 3.44'stsp -P teletextover a real PAT/PMT/PES fixture (tests/webvtt_teletext_tsduck_oracle.rs). - RFC 3339 formatting now uses
jiff. Output is byte-identical for every instant in years -9999..=9999, including the historical{year:04}spelling of negative years (-001-12-31T23:59:59.999Z); the calendar maths goes throughjiff::civil::DateTime, sincejiff::Timestampstops at 9999-12-30T22:00Z. Out-of-range instants now behave differently:format_rfc3339_msandTimeAnchor::rfc3339clamp instead of printing a many-digit year,TimeAnchor::media_to_epoch_mssaturates (it used sign-wrappingu64 as i64casts and could panic with overflow in a debug build), andconvert::scte35_to_daterangereturnsError::TimestampOutOfRangefor an unrepresentableSTART-DATE.
New
anchor::try_format_rfc3339_ms,TimeAnchor::try_rfc3339andError::TimestampOutOfRange(i64): fallible RFC 3339 formatters, for callers that prefer an error to clamping.
Fixes
convert::emsg_to_v1/emsg_to_v0: a v0 emsg whoseearliest_presentation_time + presentation_time_deltaoverflowsu64is nowError::EmsgPresentationTimeOverflow(was an unchecked add: debug panic, release wrap), and an unknown#[non_exhaustive]PresentationTimevariant isError::UnsupportedPresentationTimeinstead ofunreachable!(audit r12-TM-W2/W3, #1105).webvtt::writer::cue_block: an empty line inside cue text ("a\n\nb", also with lone-CR and CRLF terminators) no longer emits a blank line, which ended the cue block early and corrupted the rest of the document (WebVTT §4.1; audit r12-TM-W5, #1105).DateRange::parse_tag_lineno longer panics on a multi-byte character inside aSCTE35-OUT/IN/CMDhex value, and no longer accepts a+or-sign as a hex digit (the hex now goes through thehexcrate).daterangehex rendering uses a lookup table instead of aformat!per byte (audit r12-TM-O1).
Published from tag timed-metadata-v0.6.0.
st377-1 0.4.0
st377-1 0.4.0
Released 2026-10-05.
Breaking release (0.3 -> 0.4) for the SMPTE ST 377-1 MXF parser and serializer. The main change is that KLV-level types now round-trip byte-identically even when the file uses a non-minimal (fixed-width) BER length token, which required a new public len_size field on six types; Preface::identifications also became an Option. Alongside that come a set of parser and serializer fixes (duplicate local tags, 32-bit truncation, a mis-decoded OP1a qualifier byte). If you construct KlvItem, PartitionPack, PrimerPack, RandomIndexPack, LocalSet or LocalSetItem by struct literal, or read Preface::identifications, you must change code; if you use Op1aQualifier or op1a_ul, read the behaviour section, because the bytes you produce change.
Breaking changes
1. len_size: BerLength on six types; byte-identical round trip (issue #1047, audit MX-C1)
KlvItem, PartitionPack, PrimerPack, RandomIndexPack, LocalSet and LocalSetItem each gained pub len_size: BerLength. Previously every serializer re-emitted the canonical minimal BER length form even when the original file used a longer fixed-width one. docs/st377-1.md section 6.3.4 permits any valid form, and real encoders use fixed widths so a pack can be rewritten in place (Open to Closed) without shifting later absolute offsets. Measured against the crate's real ffmpeg-muxed fixture: all 25 of its Partition Packs and 2 of its 27 Header Metadata Sets use a non-minimal length token, and none reproduced their original bytes on reserialize before this fix; all do now.
BerLength(exported at the crate root,#[non_exhaustive],Default = Minimal) isMinimalorFixed(NonZeroU8): parse records the on-wire token width asFixed, and a freshly built value defaults toMinimal, so code that only builds values programmatically sees no change in output.- None of the six types is
#[non_exhaustive], so struct literals must add the field.KlvItem,PrimerPack,RandomIndexPack,LocalSetandLocalSetItemimplementDefault, so..Default::default()works for them;PartitionPackdoes not, so supplylen_size: BerLength::Minimalexplicitly. PartialEq/Eqon all six compare every field exceptlen_size, treating it as a serialization-form preference rather than part of the value. A freshly builtMinimalvalue and the same value reparsed (which carriesFixed) still compare equal, so "parse, serialize, parse gives an equal value" stays meaningful. Byte identity does depend onlen_size.- New
Error::FixedBerLengthTooSmall, reachable only by building or mutating a value into an inconsistent state (a fixed width too narrow for the length), never by re-serializing a value as parsed.Error::BerLengthTooLongis returned for a hand-builtBerLength::Fixed(n)withn > 9. - Note: the
ber::{ber_length_size_for, encode_ber_length_as}helpers named in the CHANGELOG live in the privatebermodule; onlyBerLengthis exported.
// before (0.3)
let item = KlvItem { /* key, value ... */ };
// after (0.4): add the field, or use Default where available
let item = KlvItem { len_size: BerLength::Minimal, /* key, value ... */ };2. Preface::identifications is Option<Vec<UlBytes>> (#1108 MX-W3)
It was Vec<UlBytes>. None means the Identifications property (0x3B06) was absent on parse; Some(vec![]) means present with an empty Batch. Both used to collapse to an empty Vec, so serialize_into always re-emitted the property even when the source never had it, breaking the round trip on real files that omit this encoder-required but decoder-tolerant ("E/req", Annex A.2) property.
// before: preface.identifications.is_empty()
// after: preface.identifications.as_ref().map_or(true, |v| v.is_empty())
// building: identifications: Some(vec![...]) (or None to omit the property)Behaviour changes (not signature breaks)
op1a::Op1aQualifierbyte-15 mapping corrected (issue #1048). It was off by one against SMPTE ST 378M section 6.4: bit 0 is an always-set marker (every real encoder sets it), not a flag, soexternal_essence/non_streamable/multi_trackare bits 1/2/3 (0x02/0x04/0x08), not 0/1/2. The crate's realffmpeg-muxed fixture has qualifier byte0x09(marker plus multi-track, matching its one-Essence-Container, two-track layout); it used to decode as external-essence plus single-track, both wrong. Consequences you will see in code:Op1aQualifier::default()now carries the marker bit, soOp1aQualifier::default().to_byte()is0x01(was0x00);with_external_essence(),with_non_streamable()andwith_multi_track()set0x02,0x04,0x08(were0x01,0x02,0x04);op1a_ul(qualifier)therefore emits different byte 15 values; andfrom_bytekeeps whatever byte it is given.- Duplicates and unreadable keys are rejected.
LocalSet::parse_prefixrejects a Set with a duplicate local tag (section 9.3 forbids it; typed accessors took the first match and the duplicate's value vanished on every round trip) (#1108 MX-W2).PrimerPack::parserejects a duplicate local tag, or two different local tags mapping to the same UL/UUID (#1108 MX-W4).LocalSet::serialize_intorejects a key whose byte 6 (registry designator) is not a validItemLengthMode, instead of falling back toTwoBytemode and producing bytesparsewould then reject (#1108 MX-W6).
Fixes
- 32-bit targets (#1108 MX-W1).
partition.rs,primer.rs,random_index_pack.rsandlocal_set.rsnarrowed a 64-bit BER or batch length tousizewith a bareas usize, which silently truncates on a 32-bit target instead of rejecting an over-range value (klv.rsand part oflocal_set.rsalready usedusize::try_from; these call sites now match).primer.rsandtypes.rs::parse_uid_batchcomputedcount as usize * <entry size>to validate a header, which also wraps on 32-bit and could then abort the process atVec::with_capacity(count as usize)on a bogus count; both now usechecked_muland size the allocation from the already-bounded body length, never the untrusted count. ber::encode_ber_length_aswith a hand-builtBerLength::Fixed(n),n > 9, underflowed8 - followingand panicked on the slice; it returnsError::BerLengthTooLonginstead (valid long-form widths are2..=9). This code is new in this release, so it is not a defect you could have hit in 0.3.0.
Internal
The Serialize skeleton copy-pasted across thirteen typed Sets (ContentStorage, EssenceContainerData, FillerComponent, Identification, Preface, Sequence, SourceClip, TimecodeComponent, MaterialPackage, SourcePackage, TimelineTrack, EventTrack, StaticTrack) is one internal declare_set_serialize!(Type, Kind) line each (audit r13-MX-W5, #1113); no behaviour or API change. tests/fixture_real_op1a.rs now compares against each item's true original bytes at its offset rather than against KlvItem::to_bytes()'s own re-canonicalized output, which could never disagree with a re-canonicalization and so never exercised this bug.
Dependencies
broadcast-common 9.3 -> 9.4 (see broadcast-common-9.4.0.md). No other Cargo.toml change.
Published from tag st377-1-v0.4.0.
st337 0.3.1
st337 0.3.1
Released 2026-10-05.
Documentation-only patch: no code, behaviour or API change (#1118). Nobody must act.
What changed
BurstPreamble::length_code and the burst module docs now say, in rustdoc-visible text rather than only in the docs/ tree (which is excluded from the published crate), two things that readers of the API otherwise trip over:
length_codeis bits, always.BurstPreamble::length_codeis interpreted as a count of bits, following ST 337's own text ("the length of theburst_payloadin bits", section 7.2.5 / Table 6), for everydata_type. A real IEC 61937 E-AC-3 capture (cross-checked withffmpeg -f spdif) writesPdas the wrapped frame's byte count instead; that is a quirk of IEC 61937's own "Burst-info" definition, not of ST 337 or this crate. The crate has no independently verified per-data_typetable of which other types share the quirk (ST 338, which mapsdata_typeto codec, was not available), so it does not guess: if you consume IEC 61937 streams rather than genuine ST 337, apply any bits-versus-bytes correction yourself perdata_type.- Byte order.
Pa-Pfandburst_payloadare carried as little-endian per 16-bit word (for exampleSYNC_WORD_PA0xF872serializes as[0x72, 0xF8]). ST 337 mandates no endianness for a byte-array form of a 16-bit-word stream; the crate adopted the convention offfmpeg -f spdif's real burst output. A big-endian-per-word carriage fails withError::InvalidSync.
The reasoning is recorded in docs/st337.md, scope decision 4, which was already in place; 0.3.1 only makes it visible on docs.rs.
Dependencies
broadcast-common 9.3 -> 9.4 (see broadcast-common-9.4.0.md). No other Cargo.toml change.
Published from tag st337-v0.3.1.
st291 0.4.1
st291 0.4.1
Released 2026-10-05.
Patch release (audit #1116, #1129) that makes the ST 2038 PES path reject input it used to accept and then reserialize to different bytes, stops a PTS from being silently wrapped on serialize, and adds producer and verifier helpers for the ST 291-1 parity bits and Checksum_Word. There are no API removals. The new strictness can turn previously accepted malformed packets into errors, so consumers of captured ST 2038 streams should expect that.
Behaviour changes: stricter parsing and serializing
- Fixed bits are validated (#1116).
AncDataPacket::parse(which reads eachAncPacketrecord) now returnsError::BadFixedBitsfor: a nonzero leading'000000'field of an ANC record (Table 2); a byte-alignment padding region that is not all'1'bits; and nonzeroESCR_flag,ES_rate_flag,DSM_trick_mode_flag,additional_copy_info_flag,PES_CRC_flagorPES_extension_flagbits in the PES optional header. All three were previously skipped unchecked, so a corrupt packet parsedOkand re-serialized to different bytes because the writer always emits the canonical values. - A too-large PTS is an error, not a different timestamp (#1116, #1129). The ST 2038 PES serializer no longer masks a PTS of 2^33 or more; it returns
Error::FieldTooWide { what: "PTS", bits: 33, .. }. (Thevaluefield of that error holds the PTS truncated tou32, so do not rely on it for display.)
New API: parity and checksum (#1116)
Parsing and serializing never validated the ST 291-1 10-bit-word parity bits or the Checksum_Word. AncContent now has:
AncContent::with_parity(value: u8) -> u16: encodes an 8-bit value into the 10-bit word (even-parityb8overb0..b7, thenb9 = !b8).AncContent::compute_checksum(&self) -> u16: theChecksum_Wordfor the currentdid,sdid,data_countanduser_data_words(low 9 bits summed mod 2^9,b9 = !b8of the result).AncContent::verify_checksum(&self) -> Result<()>: compares the storedchecksumwith the computed one and returns the newError::ChecksumMismatch { stored, computed }on a mismatch.AncContent::build(did8: u8, sdid8: u8, udw8s: &[u8]) -> Result<AncContent>: builds a content sequence from raw 8-bit values, filling every parity bit and the checksum;Error::FieldTooWideif the payload is longer than 255 bytes.
parse and write_into do not call verify_checksum automatically: a bit-flipped packet still parses Ok, in line with the crate's transport-layer error model. A caller that wants the RFC 8331 section 7 validation guidance must call it.
Internal
The byte-6 and byte-7 PES flag bitmasks are named constants instead of inline hex literals (#1116).
Dependencies
broadcast-common 9.3 -> 9.4 (see broadcast-common-9.4.0.md). No other Cargo.toml change.
Published from tag st291-v0.4.1.