OpenJOC provides an experimental external bridge for applications embedding
FFmpeg. openjoc-ffmpeg uses libavformat for demux and packet transport, the
Rust OpenJocSession directly for all decode/render work, and public
libavutil APIs for owned AVFrame output.
This is not a decoder plugin for an installed ffmpeg executable. FFmpeg has
no documented stable out-of-tree decoder-plugin ABI comparable to GStreamer's
loadable element model. Stock FFmpeg therefore does not gain
-c:a openjoc; no FFmpeg source, registration table, parser, demuxer, or
configure script is patched here. The implemented native libavcodec source
wrapper is documented separately in FFMPEG_NATIVE.md.
The minimum supported baseline is FFmpeg 9.0:
| Component | Minimum | Locally tested |
|---|---|---|
| FFmpeg release | 9.0 | 9.0.1 (2026-08-12) |
| libavutil | 61 | 61.1.101 |
| libavcodec | 63 | 63.1.101 |
| libavformat | 63 | 63.1.101 |
FFmpeg 9 is selected deliberately. It provides the current AVChannelLayout
model, AVFrame.duration, true AV_CHAN_BINAURAL_LEFT /
AV_CHAN_BINAURAL_RIGHT identities, and the predefined binaural and 22.2
layouts. Supporting an older line would either lose exact ear semantics or
add version branches without improving the first integration.
The implementation was also audited against official FFmpeg master commit
3bdd895832244780c250713e49135615ac4de003 (2026-08-19): libavutil 61.5.100,
libavcodec 63.8.101, and libavformat 63.6.100. The public functions and fields
used by this bridge have the same contract on that head. Current source and
API references are the official download page,
libavformat demux documentation,
send/receive documentation,
channel-layout header,
and AVFrame allocation source.
The feature check and complete bridge test suite passed against a locally built
minimal shared SDK from that exact development commit.
FFmpeg support is opt-in. A generic workspace build does not compile or advertise a bridge artifact. On Unix-like hosts, install FFmpeg 9 development files providing these pkg-config modules:
libavutil >= 61;libavcodec >= 63;libavformat >= 63.
Then run the authoritative integration path:
cargo build -p openjoc-ffmpeg --release --features ffmpeg --locked
cargo test -p openjoc-ffmpeg --features ffmpeg --locked
scripts/verify-ffmpeg.shThe Rust bridge core builds without FFmpeg:
cargo build -p openjoc-ffmpeg --lockedThe feature build uses a narrow C interop translation unit compiled against
the host's public FFmpeg headers. It exists to keep ABI-owned structures and
allocation in C; Rust calls OpenJocSession directly and never routes back
through OpenJOC's C ABI.
container / raw stream
↓
libavformat → borrowed AVPacket bytes + AVStream.time_base
↓
bounded packet-to-AU assembler and positive JOC admission
↓
OpenJocSession
↓
owned interleaved f32 PCM → semantic channel permutation
↓
public-API-allocated AVFrame / AVBuffer
PacketRef borrows packet bytes only for send_packet. Bytes that must
survive the call are copied into at most 3,538,944 bytes of compressed staging
(16 × the 221,184-byte maximum General short-block AU).
No AVPacket * is retained. The wrapper output queue is bounded to frames
from one session send/drain batch; a caller that does not receive sees
WouldBlock. AvFrame owns a normal AVFrame whose packed audio allocation
comes from av_frame_get_buffer; it remains valid independently of the
temporary OpenJOC PCM vector.
One wrapper is serially accessed by one caller. Separate wrappers contain separate AU, decoder, FinalLinkedGain, HRTF, and timing state and may run on separate threads. There is no global decoder lock or mutable global HRTF state.
libavformat defines a packet as containing one or more encoded frames, so the
bridge never equates an AVPacket with one OpenJOC AU. The shared OpenJOC
E-AC-3 primitives parse:
ordered programme sets (I0 + D0..Dn), accumulated until every selected
programme covers six audio blocks = one admitted General AU
The assembler handles an AU split across packets, I0 and D0..Dn in different
packets, short 1/2/3-block syncframes, mixed short partitioning whose cumulative
total is six blocks, and multiple AUs in one packet. A short-block group starts
at an I0 convsync=1 marker and subsequent temporal I0 frames must carry
convsync=0. Maximum frame, AU, and staging sizes are explicit: at most 54
syncframes and 221,184 coded bytes for one General I0+D0..D7 six-block unit;
whole programmes are never buffered.
General E-AC-3 permits at most eight associated dependents, assigned sequentially as D0 through D7. CMAF remains constrained to optional D0, and the original-syntax AC-3 Annex-J combination remains D0-only.
The legacy original-syntax AC-3 I0 plus E-AC-3 D0 combination is intentionally outside the standard CMAF JOC claim. It remains available to the broader elementary-stream/ordinary-container path only.
The explicit CMAF entry point accepts the constrained profile from ETSI TS 103 420 Annex E and ETSI TS 102 366 Annex F/J:
- an unencrypted E-AC-3 track uses
EC3SampleEntry(ec-3) and anEC3SpecificBox(dec3); - the
dec3descriptor carries the independent/dependent counts and the per-substreamfscod,bsid,acmod,lfeon, and dependentchan_locinformation. The TS 103 420 type-A extension flag and complexity index are retained as the container's JOC claim.ceaois the recommended object-audio compatibility brand; it is not decoder proof; - the supported OpenJOC subset is one E-AC-3 independent I0 and optional D0,
with
numblkscod=3, big-endian syncword0x0B77, 48 kHz, and a Core CMAF bitrate no greater than 3024 kbps; - each CMAF sample is one complete 1536-sample access unit: I0 first, then D0 when present. A sample boundary may not cut a syncframe or split this unit.
CmafJocTrack validates the container contract and
FfmpegDecoder::send_cmaf_sample validates each complete sample before passing
the unchanged compressed bytes to the existing common bridge. The native
Demuxer::cmaf_joc_track/read_cmaf_sample pair uses FFmpeg-exposed
ec-3/dec3 metadata and packet timing when that metadata is available;
missing extradata fails closed rather than being reconstructed from the ES.
The in-band OpenJOC classifier still decides CONFIRMED_JOC; a container JOC
claim cannot turn ordinary E-AC-3 into JOC. The repository-owned fragmented
fixture proves FFprobe sample extraction, complete sample boundaries, and
consecutive 1536-sample decode-time/PTS fields; the feature-enabled native
FFmpeg test additionally exercises packet delivery and decoding when the
development SDK is available. This is synthetic carriage validation, not a
claim about every historical Dolby MP4.
For standards-defined flat-7.X JOC, downmix index 1 is admitted only with the
seven-input Table-47 order L R C Ls Rs Lrs Rrs. The bridge retains two
separate internal PCM meanings: I0-only compatibility PCM and assembled
I0+D0..Dn JOC reconstruction-input PCM. Admitted common-profile Stereo keeps
using the independent I0 compatibility Lo/Ro or Lt/Rt matrix. When the
assembled Flat-7.X plane is selected for Stereo, that same canonical matrix
also admits the distinct Lrs/Rrs rear-surround pair using the existing
same-side surround rule, including its normative 1 / max_sum overflow scale;
the pair is never relabelled as Ls/Rs. Expanded speaker rendering keeps using
all seven reconstruction inputs. With original-syntax I0, compatibility PCM
is the AC-3 core alone and reconstruction input is the AC-3 core plus D0.
Raw E-AC-3, MP4 EC-3, and Matroska packetization may differ by demuxer and file. The same assembler handles all three. Automated acceptance covers a positive synthetic raw JOC stream plus ordinary E-AC-3 controls demuxed from raw, MP4, and Matroska. A private real raw programme is also accepted locally. No redistributable JOC-in-MP4 or JOC-in-Matroska fixture is currently present, so those positive container cases remain a fixture-dependent acceptance step:
target/release/openjoc-avdecode input.mp4 --binaural --null --checksum
target/release/openjoc-avdecode input.mkv --layout 7.1.4 --null --checksumAdmission uses index_syncframes, group_access_units, full audio-frame
parsing, parse_joc_access_unit, and the existing strict/vendor-compatible
JOC validators. It exposes UNKNOWN, CONFIRMED_JOC,
CONFIRMED_NON_JOC, and INVALID_OR_UNSUPPORTED.
CONFIRMED_NON_JOC returns NotJoc; no OpenJOC session is created and no PCM
is emitted. Applications may route that stream to FFmpeg's normal E-AC-3
decoder outside this library. Bad sync, a truncated AU at EOF, unsupported
substream order/profile, impossible timing, and staging overflow are distinct
structured errors. Public state-machine calls contain Rust panics and poison
the affected instance rather than unwinding through an external boundary.
AVPacket.pts is the audio presentation timestamp and is the default anchor.
It is interpreted in the selected AVStream.time_base and converted directly
to a 48-kHz sample position with checked integer rational arithmetic matching
av_rescale_q nearest rounding. No floating-point seconds or incremental
fractional accumulator is used, so absolute sequences at 1/48000, 1/90000,
1/1000, and other rational bases do not accumulate drift.
Missing PTS remains missing by default. TimestampPolicy::PtsThenDts is an
explicit opt-in for applications that have established that DTS equals the
audio presentation timeline; the wrapper never silently substitutes DTS.
AV_NOPTS_VALUE maps to absence. Once a timed segment begins, packet anchors
must agree with the exact sample count; AUs grouped inside one packet receive
the documented monotonic sample-count continuation. A segment that begins
untimed cannot acquire a timestamp part-way through without reset.
Output AVFrame.pts and duration are in 1/48000: PTS is the logical
OpenJOC output sample position and duration is nb_samples. Renderer latency
is not subtracted from timestamps.
Every frame is 48 kHz packed/interleaved AV_SAMPLE_FMT_FLT. The exact path
does not use libswresample, resample, rematrix, mix, downmix, or normalize.
Consumers that need planar PCM or another rate can use FFmpeg conversion after
the bridge.
Physical speaker stereo and binaural are different configurations. Binaural
defaults to a 7.1.4 virtual layout and the built-in SADIE II D1 HRTF, then
emits BIL, BIR. Speaker 2.0 emits physical FL, FR.
| OpenJOC target | FFmpeg representation |
|---|---|
| 2.0 | predefined stereo |
| 5.1 | predefined 5.1(side) |
| 5.1.2 / 5.1.4 | matching predefined layouts |
| 7.1 / 7.1.2 / 7.1.4 | matching predefined layouts |
| 7.1.6 | custom order with explicit TFL/TFR, TSL/TSR, TBL/TBR |
| 9.1 / 9.1.2 / 9.1.4 / 9.1.6 | custom order with explicit WL/WR |
| 22.2 | predefined 22.2, with deterministic permutation |
| binaural | predefined binaural, BIL/BIR |
FFmpeg's predefined 9.1.4/9.1.6 layouts contain FLC/FRC. OpenJOC's admitted 9.1 family contains Wide identities, so the bridge deliberately uses custom WL/WR layouts instead of claiming false semantic equivalence.
Explicit speaker_layout_definition takes precedence over the preset field.
Custom definitions with uniquely mapped existing OpenJOC channel identities and
matching LFE/full-range roles use ordered AV_CHANNEL_ORDER_CUSTOM; no preset
layout is inferred from their name or channel count. Unknown names, duplicate
mapped aliases, and role mismatches return InvalidConfig before decoding.
Custom speaker angles are not transported by AVChannel metadata. See the
Rust API transport boundary
for the direct-session versus FFmpeg/C-stream distinction.
OpenJOC 22.2 order is:
FL FR FC LFE1 BL BR FLc FRc BC LFE2 SiL SiR TpFL TpFR TpFC TpC
TpBL TpBR TpSiL TpSiR TpBC BtFC BtFL BtFR
FFmpeg's native 22.2 order is:
FL FR FC LFE BL BR FLC FRC BC SL SR TC TFL TFC TFR TBL TBC TBR
LFE2 TSL TSR BFC BFL BFR
The boundary permutation is
[0,1,2,3,4,5,6,7,8,10,11,15,12,14,13,16,20,17,9,18,19,21,22,23].
It is a pure reorder. Inverse-reorder tests cover every public layout and
full renderer parity tests cover 22.2.
drain closes compressed assembly, rejects a partial final AU, calls
OpenJocSession::drain, and emits the complete QMF, FinalLinkedGain, and
binaural convolution tails before EOF. flush, reset, or an input packet
marked as a discontinuity discards compressed staging, queued PCM, sample
timing, E-AC-3/JOC/QMF state, dialnorm state, FinalLinkedGain state, and HRTF
history.
The demux helper exposes seek in stream-time-base units through
av_seek_frame. The required application sequence is demuxer.seek(...),
then decoder.reset(), then delivery of preroll packets. Seeking the demuxer
without resetting the wrapper is an application error.
The verified steady-state latency is 609 samples for physical speaker output
(577 QMF + 32 FinalLinkedGain) and 577 samples for built-in or 48 kHz custom
binaural HRIRs. A custom SOFA at another rate adds the resampler's common
causal filter delay; source Data.Delay remains part of the HRIR and is not
added to the reported latency. The delay is
reported separately as samples with a 1/48000 rational, never hidden in PTS.
After a seek, an application should start decoding at least the reported delay
before its desired audible sample when the container permits it, mark those
packets as preroll, and discard preroll output according to its own seek
policy. The bridge decodes preroll normally and does not fabricate or silently
trim samples.
The native decoder uses FFmpeg AVCodecContext.delay for this sample count:
it is the number of samples a decoder must output before output is valid and
the amount to decode before a seek target. The external wrapper does not create
a fake AVCodecContext merely to store it.
openjoc-avdecode intentionally does only integration proof:
cargo build -p openjoc-ffmpeg --release --features ffmpeg --locked
target/release/openjoc-avdecode input.mp4 \
--binaural --output output.wav
target/release/openjoc-avdecode input.mkv \
--layout 7.1.4 --output output.wav
target/release/openjoc-avdecode input.ec3 \
--layout 22.2 --null --checksum --traceIt opens with avformat_open_input, calls avformat_find_stream_info, selects
an E-AC-3 audio stream, reads reference-counted packets with av_read_frame,
and feeds only the chosen packet bytes into OpenJOC. It never calls
avcodec_find_decoder(AV_CODEC_ID_EAC3) or decodes JOC through libavcodec.
--trace reports packet timing, AU hashes/sample PTS, AVFrame timing/layout,
and latency as separate facts. WAV is a convenience sink; AVChannelLayout is
the authoritative multichannel semantic contract.
--semantic-checksum hashes PCM after the inverse FFmpeg transport
permutation, which is useful for direct-session and cross-framework parity.
The wrapper and C shim contain no CoreAudio, AudioToolbox, WASAPI, ALSA,
PipeWire, Media Foundation, DirectShow, or platform spatial renderer. macOS
and Linux feature builds use pkg-config. CI installs FFmpeg development
packages on Ubuntu and Homebrew FFmpeg on macOS. Windows core code builds
without FFmpeg. A native Windows feature job for this external bridge is not
qualified; it would require one pinned shared-development SDK exposing
compatible headers, import libraries/DLLs, and pkg-config metadata.
The reproducible audited route is the official FFmpeg 9.0.1 source under
MSYS2/MinGW-w64 with --enable-shared, following FFmpeg's
Windows platform guide. The current vcpkg
port is still on FFmpeg 8.1 and cannot satisfy this bridge's minimum. A native
Windows feature build/link/run job is not yet qualified.
OpenJOC source remains Apache-2.0. FFmpeg is LGPL-2.1-or-later by default, but
optional build choices can make an FFmpeg build GPL. The locally tested
Homebrew build was configured with --enable-gpl --enable-version3 and reports
GPLv3-or-later. The repository dynamically links the installed libraries and
does not vendor or redistribute FFmpeg, so THIRD_PARTY_NOTICES.md is not
changed for this source-only integration. Anyone distributing combined static
or dynamic binaries must audit the exact FFmpeg configuration and comply with
the applicable terms; see FFmpeg's official
license guidance. This is engineering scope,
not legal advice.
- No stock
ffmpegcommand recognizes an OpenJOC codec name. - There is no positive redistributable JOC MP4/Matroska fixture in this tree.
- The wrapper selects the first E-AC-3 audio stream; richer stream selection belongs in an embedding application.
- It does not enumerate audio devices or infer headphones from two channels.
- mpv integration uses the native
libopenjocwrapper rather than this external embedding bridge.
The bridge proves packet assembly, timing, output ownership, and channel semantics and remains the embedding/reference frontend. The additional native FFmpeg wrapper now exposes the same semantics through libavcodec for custom patched FFmpeg builds. Player integration can select that named decoder after positive JOC probing without duplicating this bridge.
A complete private raw programme was run without copying it into the repository. Binaural, 7.1.4, and 22.2 all completed through libavformat and owned AVFrames. Full-program 22.2 direct-session, GStreamer, and FFmpeg results were bit-identical after the FFmpeg transport permutation was inverted: 8,673 frames and 13,320,224 samples per channel (1,278,741,504 packed PCM bytes). Binaural FFmpeg output was also bit-identical to the retained direct session oracle.
On the local Apple-silicon macOS host, the approximately 277.5-second programme measured:
| Path | Wall time | Approximate RTF | Maximum RSS |
|---|---|---|---|
| FFmpeg bridge, binaural | 228.72 s | 0.824 | 144 MB |
| FFmpeg bridge, 7.1.4 | 82.93 s | 0.299 | 28 MB |
direct OpenJocSession, 22.2 |
88.76 s decode/render | 0.320 | 54 MB process |
| FFmpeg bridge, 22.2 | 92.84 s | 0.335 | 28 MB |
| GStreamer, 22.2 streaming checksum | 88.98 s | 0.321 | 59 MB pipeline |
The instrumented FFmpeg 22.2 run attributed 83.683 s to OpenJocSession,
1.816 s to AU assembly plus positive admission, 0.243 s to channel reorder,
0.111 s to libavformat reads, 0.094 s to AVFrame allocation/copy, and 0.007 s
to packet timestamp/staging work. These are single-run development numbers,
not cross-machine performance guarantees. Compressed staging remains capped at
128 KiB; the wrapper retains no whole-program PCM.