Skip to content

Releases: fishloa/rust-broadcast

DVB lockstep 11.0.0

Choose a tag to compare

@github-actions github-actions released this 05 Oct 14:30

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

Choose a tag to compare

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. MediaTransport built 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 to remote_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_candidate rejects candidates past the cap with Error::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 candidates
let 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) returns Vec<MediaEvent> (was ()). A failed ICE or DTLS timer drive is surfaced as MediaEvent::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 an Err from handle_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 an Err for a genuine peer packet; it still counts toward the RFC 3711 key-lifetime read counter.
  • DecryptedRtp gained extension: 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::rekey returns Error::Media for SetupRole::Passive instead of tearing down the association. A Passive side never dials out, so the old behaviour left it waiting forever for a ClientHello a 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_candidate and its buffered_candidates field are removed: flush_candidates never read them (it takes its own aggregated fragment), so they were dead, unbounded-until-flush state.
  • WhipClient::flush_candidates, ice_restart, terminate and WhepPlayer::trickle_ice, ice_restart, terminate now record which request is in flight and dispatch the response on that instead of guessing from status and ETag. WhepPlayer::trickle_ice and ice_restart take &mut self (were &self). Fixed defects: a trickle ack answered 200 with an ETag (RFC 9725 permits 204 or 200 plus ETag) was misread as an ICE-restart answer, and a DELETE answered 204 left the client Established forever instead of Closed (#1090).

Behaviour changes

  • If-Match is 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 with Error::ETagMismatch, whose got is now the raw header text ("\"old\""). A duplicated Content-Type on a PATCH is rejected. A weak server ETag is no longer reused as a strong If-Match tag by the WHIP/WHEP clients (treated as absent). A session URL (non-ASCII) or ETag that cannot be sent as a header makes accept answer 500 with no Location instead of emitting a bad header.
  • media::parse_remote_fingerprint reads the SDP with sdp-types. The text must be a well-formed session (v=, o=, s=, t=); the first media section carrying an a=fingerprint wins over the session level; the result is the typed attribute's normalised text (a=fingerprint:SHA-256 ab:cd:0f gives sha-256 AB:CD:0F). A digest without colons is accepted when it is 32 bytes. The first level that carries an a=fingerprint decides: an unparseable one gives None instead of falling through to a later one.
  • WhepSession::on_patch matches Content-Type by media type only, so application/sdp; charset=utf-8 is accepted like bare application/sdp (#1090).

Fixes

  • Browser RTCP discarded (#1071). MediaEvent::RtcpUnsupported was 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), because rtcp_packet::CompoundPacket::parse rejected the whole datagram on the unrecognised PT, discarding a leading SR/RR's real statistics too. Fixed by rtcp-packet 0.4 (RtcpPacket::Unknown); the doc comment of RtcpUnsupported is corrected accordingly.
  • ice::parse_ice_server_links / format_ice_server_links: a Link header parameter value (username, credential) containing ;, , or " (legal in an RFC 8288 quoted-string, for example a static TURN password) now round-trips instead of being split or corrupted; rel matching 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 which handle_timeout does work (rtc-stun collects an expired transaction only when deadline < 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...

Read more

ule 0.4.1

Choose a tag to compare

@github-actions github-actions released this 05 Oct 13:40

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 0xFF were discarded as padding. The receiver treated any leading 0xFF byte as stuffing. A legal D=1 SNDU whose Length is in 0x7F00..=0x7FFE also starts with 0xFF on the wire, so its header was silently thrown away and the receiver lost sync for the rest of the packet. Only the genuine 2-byte 0xFFFF End Indicator, or a single trailing 0xFF byte 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 0xFF padding is still accepted (#1120).

Fixes in serialization

  • PayloadChain::serialize_into panicked or misframed on inconsistent Optional extension headers. It trusted h_len over body.len(): a body longer than 2*h_len-2 could panic on the output slice, a shorter one produced bytes that put the next Type field in the wrong place, and h_len == 0 panicked with a usize underflow inside serialized_len() itself. Every header is now validated before any byte is written, and a bad one returns Error::InvalidExtensionHeader; serialized_len() no longer underflows (#1120).
  • Sndu::serialize_into could emit the End Indicator as a header. D=1 with Length=0x7FFF serializes to 0xFFFF, which every receiver, including this crate's own ts::UleReceiver, reads as "no more SNDUs in this packet", so the SNDU was silently truncated. That combination is now rejected with Error::InvalidLength (#1120).

New API

  • ExtensionHeader::validate(&self) -> Result<()>: for an Optional header it checks that h_len is in 1..=5 and that body.len() == 2*h_len-2; a Mandatory header always passes. This is the same check PayloadChain::serialize_into now 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

Choose a tag to compare

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_attributes and unknown_children; metadata-like types also gained scoped_namespaces; MetadataChild gained an Unknown variant. New fields xml_base, ttm_role / ttm_role_source and xlink_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 with Type::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 (use foreign_attributes, a Vec<ForeignAttribute>) and TtElement::text: Option<String>.
  • Document::to_xml now takes &mut self (was &self) because it assigns fallback prefixes for outer-scope vendor namespaces. A Document you 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:mm

Behaviour 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-tt wrapper root still yields its first <tt> child.
  • A numeric reference to a control character (<p>&#x1;</p>), or a literal one, in text or in an attribute is rejected with Error::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, \n or \r is written as &#9;, &#10;, &#13; (region="x\ny" renders region="x&#10;y"), and a \r in text as &#13;, 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 generated ttmfallbackN prefix.
  • <br> and <span> attributes, <p> child ordering and the xml:space / xml:base attributes 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 no ttp:frameRate was specified, and ignored ttp:frameRateMultiplier in the effective frame rate. The computation is now also checked/saturating u64 rather than unchecked u32.
  • TT-W5: the clock-time and wallclock-time grammars (section 12.3.1) were too lenient. A seconds fraction and a :frames term were accepted together (the grammar makes them exclusive), a single-digit frames term 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_usage checked <body>'s own begin/dur/end inside the <div> loop, so a <body> with no <div> was never checked; it now also covers <div>'s own timing.
  • begin/dur/end values now go through time::parse_time_expression (section 12.3.1), so begin="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

Choose a tag to compare

@github-actions github-actions released this 05 Oct 15:17

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's pcr_pid and elementary_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 terminal Resolved state 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's process, as push already did for packets. Previously only the last operation drained the staging buffer, so the fallback PAT that --regen-psi emits from PsiRegenOp::flush when the input has no PAT slot bypassed repair_continuity and stuffing, 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 (as dvb-tools does) instead of chunks(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 with missing TS sync byte and wrote no output.
  • --regen-psi (PsiRegenOp) (#1037). Every regenerated PAT packet used a fresh SectionPacketiser, so continuity_counter restarted at 0 on each emission; version_number was hard-coded to 0 even when the program mapping changed; and the original PAT's network_pid entry (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_number increments only when the mapping actually changes, and the network_pid entry is preserved when present. The --regen-psi help text and regen_psi docs 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

Choose a tag to compare

@github-actions github-actions released this 05 Oct 13:32

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, \n or \r in an attribute is now written as &#9;, &#10;, &#13; (so profiles = "a\nb" renders profiles="a&#10;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"/> parses id as "a b"; write &#10; to keep it), a duplicate attribute is MalformedAttribute (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.0 Char production (&#x1;) is a structured error. <BaseURL>a<x/>b</BaseURL> (markup inside a text-only element) now yields no base URL instead of MismatchedEndTag.
  • DashParseError and SmoothParseError gain Xml { pos, message }; DashParseError::MismatchedEndTag::expected is now a String. LlDashPackager rewrites the base MPD with quick-xml events 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_url takes the MPD's own URL as its first argument and returns Option<String>: resolve_segment_url(mpd_url: Option<&url::Url>, period, adaptation_set, representation, reference: &str). None means the reference or a BaseURL carries a control character or whitespa...
Read more

timed-metadata 0.6.0

Choose a tag to compare

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, because TimedEvent::from_scte35 only read splice_insert. It now reads TimeSignal.splice_time.pts_time and takes id, kind and duration from the cue's first uncancelled segmentation_descriptor. scte35_to_daterange now errors instead of emitting ID="" 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's Break, Provider/Distributor Advertisement, Provider/Distributor PlacementOpportunity (not the Overlay variants, which composite over the network feed) and Provider/Distributor AdBlock. ProgramStart, ProgramEnd, ChapterStart and ChapterEnd map to EventKind::Chapter. Credits, promos, unscheduled or alternate content, overlay placement opportunities and NetworkStart/NetworkEnd map to Unspecified (id, time and duration are still populated), so a consumer that splices ads on BreakStart cannot mistake them for an ad avail.
  • pts_adjustment (#1039). TimedEvent::from_scte35 ignored splice_info_section's pts_adjustment, so every derived MediaTime and DATERANGE START-DATE was off by that adjustment (SCTE 35 §9.6.1). Every pts_time is now shifted through broadcast_common::clock33::add.
  • A cancelled splice_insert (splice_event_cancel_indicator == true) was classified as BreakEnd, producing a spurious SCTE35-IN; it is now Unspecified.
  • Teletext (#1041). The decoder ran Hamming-8/4 and odd-parity FEC directly on dvb_vbi::TeletextDataField::txt_data_block bytes 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, through TeletextDataField::txt_data_block_logical() rather than a private reverse_bits map (#1106). The synthetic fixture was regenerated in wire bit order and independently verified against TSDuck 3.44's tsp -P teletext over 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 through jiff::civil::DateTime, since jiff::Timestamp stops at 9999-12-30T22:00Z. Out-of-range instants now behave differently: format_rfc3339_ms and TimeAnchor::rfc3339 clamp instead of printing a many-digit year, TimeAnchor::media_to_epoch_ms saturates (it used sign-wrapping u64 as i64 casts and could panic with overflow in a debug build), and convert::scte35_to_daterange returns Error::TimestampOutOfRange for an unrepresentable START-DATE.

New

  • anchor::try_format_rfc3339_ms, TimeAnchor::try_rfc3339 and Error::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 whose earliest_presentation_time + presentation_time_delta overflows u64 is now Error::EmsgPresentationTimeOverflow (was an unchecked add: debug panic, release wrap), and an unknown #[non_exhaustive] PresentationTime variant is Error::UnsupportedPresentationTime instead of unreachable! (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_line no longer panics on a multi-byte character inside a SCTE35-OUT/IN/CMD hex value, and no longer accepts a + or - sign as a hex digit (the hex now goes through the hex crate).
  • daterange hex rendering uses a lookup table instead of a format! per byte (audit r12-TM-O1).

Published from tag timed-metadata-v0.6.0.

st377-1 0.4.0

Choose a tag to compare

@github-actions github-actions released this 05 Oct 13:24

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) is Minimal or Fixed(NonZeroU8): parse records the on-wire token width as Fixed, and a freshly built value defaults to Minimal, 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, LocalSet and LocalSetItem implement Default, so ..Default::default() works for them; PartitionPack does not, so supply len_size: BerLength::Minimal explicitly.
  • PartialEq/Eq on all six compare every field except len_size, treating it as a serialization-form preference rather than part of the value. A freshly built Minimal value and the same value reparsed (which carries Fixed) still compare equal, so "parse, serialize, parse gives an equal value" stays meaningful. Byte identity does depend on len_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::BerLengthTooLong is returned for a hand-built BerLength::Fixed(n) with n > 9.
  • Note: the ber::{ber_length_size_for, encode_ber_length_as} helpers named in the CHANGELOG live in the private ber module; only BerLength is 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::Op1aQualifier byte-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, so external_essence / non_streamable / multi_track are bits 1/2/3 (0x02 / 0x04 / 0x08), not 0/1/2. The crate's real ffmpeg-muxed fixture has qualifier byte 0x09 (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, so Op1aQualifier::default().to_byte() is 0x01 (was 0x00); with_external_essence(), with_non_streamable() and with_multi_track() set 0x02, 0x04, 0x08 (were 0x01, 0x02, 0x04); op1a_ul(qualifier) therefore emits different byte 15 values; and from_byte keeps whatever byte it is given.
  • Duplicates and unreadable keys are rejected. LocalSet::parse_prefix rejects 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::parse rejects a duplicate local tag, or two different local tags mapping to the same UL/UUID (#1108 MX-W4). LocalSet::serialize_into rejects a key whose byte 6 (registry designator) is not a valid ItemLengthMode, instead of falling back to TwoByte mode and producing bytes parse would then reject (#1108 MX-W6).

Fixes

  • 32-bit targets (#1108 MX-W1). partition.rs, primer.rs, random_index_pack.rs and local_set.rs narrowed a 64-bit BER or batch length to usize with a bare as usize, which silently truncates on a 32-bit target instead of rejecting an over-range value (klv.rs and part of local_set.rs already used usize::try_from; these call sites now match). primer.rs and types.rs::parse_uid_batch computed count as usize * <entry size> to validate a header, which also wraps on 32-bit and could then abort the process at Vec::with_capacity(count as usize) on a bogus count; both now use checked_mul and size the allocation from the already-bounded body length, never the untrusted count.
  • ber::encode_ber_length_as with a hand-built BerLength::Fixed(n), n > 9, underflowed 8 - following and panicked on the slice; it returns Error::BerLengthTooLong instead (valid long-form widths are 2..=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

Choose a tag to compare

@github-actions github-actions released this 05 Oct 13:24

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_code is bits, always. BurstPreamble::length_code is interpreted as a count of bits, following ST 337's own text ("the length of the burst_payload in bits", section 7.2.5 / Table 6), for every data_type. A real IEC 61937 E-AC-3 capture (cross-checked with ffmpeg -f spdif) writes Pd as 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_type table of which other types share the quirk (ST 338, which maps data_type to 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 per data_type.
  • Byte order. Pa-Pf and burst_payload are carried as little-endian per 16-bit word (for example SYNC_WORD_PA 0xF872 serializes as [0x72, 0xF8]). ST 337 mandates no endianness for a byte-array form of a 16-bit-word stream; the crate adopted the convention of ffmpeg -f spdif's real burst output. A big-endian-per-word carriage fails with Error::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

Choose a tag to compare

@github-actions github-actions released this 05 Oct 13:23

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 each AncPacket record) now returns Error::BadFixedBits for: a nonzero leading '000000' field of an ANC record (Table 2); a byte-alignment padding region that is not all '1' bits; and nonzero ESCR_flag, ES_rate_flag, DSM_trick_mode_flag, additional_copy_info_flag, PES_CRC_flag or PES_extension_flag bits in the PES optional header. All three were previously skipped unchecked, so a corrupt packet parsed Ok and 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, .. }. (The value field of that error holds the PTS truncated to u32, 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-parity b8 over b0..b7, then b9 = !b8).
  • AncContent::compute_checksum(&self) -> u16: the Checksum_Word for the current did, sdid, data_count and user_data_words (low 9 bits summed mod 2^9, b9 = !b8 of the result).
  • AncContent::verify_checksum(&self) -> Result<()>: compares the stored checksum with the computed one and returns the new Error::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::FieldTooWide if 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.