Skip to content

build: build FFmpeg on every platform and make the media module part of the normal build - #292

Merged
devopvoid merged 33 commits into
feat/native-extension-apifrom
feat/media-platform-builds
Sep 26, 2026
Merged

devopvoid merged 33 commits into
feat/native-extension-apifrom
feat/media-platform-builds

Conversation

@devopvoid

Copy link
Copy Markdown
Owner

Stacked on #291, which adds the media module itself. This is the part that makes it build everywhere rather than only on the machine it was written on.

What this does

Cross-compilation for all seven targets. The media module had no platform profiles beyond Windows, so a Linux or macOS build had no compiler or sysroot for its target and CMake simply built for the host. It now takes a toolchain file the way webrtc-jni does, and its profiles mirror webrtc-jni's by name and by the file each one selects, so both native libraries of a given classifier are built for the same target the same way.

Reusing those toolchain files rather than writing a second set is deliberate: they are where this project says which compiler and which sysroot a target uses, and two answers would drift apart.

What configure is told about the target is worked out from what the toolchain file already set — the architecture from CMAKE_SYSTEM_PROCESSOR, the target OS from the platform, the compiler from CMAKE_C_COMPILER, the target flags from CMAKE_C_FLAGS, the sysroot from CMAKE_SYSROOT — so a target CMake can already build for needs no new case here. Apple is the exception it has to make: there the architecture is a compiler flag rather than a different compiler.

Cross-compiled arm and aarch64 start with --disable-asm. An assembler for a foreign architecture is a build prerequisite of its own and nothing here is fast enough yet to need one; correctness on every platform first, speed where it is measured.

CI. Windows gets MSYS2 with make and nasm, because FFmpeg's configure and makefiles are shell scripts and building it needs a POSIX shell even with MSVC; MSYS2_ROOT is exported so the module's CMake finds it wherever the runner image put it. Linux and macOS get nasm, which macOS needs even though the runner is Apple Silicon, because the Intel build is cross-compiled on it.

The FFmpeg install directory is cached the same way the WebRTC checkout already is, keyed by FFmpeg version, platform and the hash of the dependency script and the module pom, so only the first build of a platform pays for it. The three native build jobs in both workflows now check out submodules, since the FFmpeg source is one; test-natives does not, because it runs against downloaded natives and never builds the module.

The module joins the normal build. That also collapses the awkward part of the examples module. The media example needed a source directory, a compile execution and an output directory of its own, purely because a JPMS requires cannot be made conditional and the module it names has to exist. With the module always there, the example moves in beside the others and the whole profile becomes one dependency and one requires.

What is verified, and what is not

Verified on this machine:

  • Full reactor mvn verify green: 168 tests in webrtc, 19 in webrtc-java-media, all five modules succeeding
  • FFmpeg still builds from nothing for windows-x86_64, its configure line unchanged apart from an explicit --arch=x86_64
  • MediaFileExample runs from a plain mvn exec:java, with no profile and no extra class path
  • Every workflow and action file parses, and the VitePress build is green

Not verified: six of the seven platforms. Only windows-x86_64 can be built where this was written — no ARM64 cross tools, no Linux, no macOS. Everything for the other six is written from FFmpeg's documented cross-compile flags and this project's existing toolchain files, and CI is the only thing that can prove it. Expect the first run to want corrections.

Worth deciding before merge

The last commit makes the media module part of the normal build, which means anything building this project now needs the third-party/ffmpeg submodule checked out, plus make and nasm to build it. That is a new prerequisite for every contributor, not just for those who want the media module.

The original plan put that step after every platform builds in CI, not before. It is here so that CI builds the module at all and can prove the other six targets. If CI is not green, or if the prerequisite is not wanted yet, 4399bb2 is the tip of the branch and drops cleanly on its own; the first two commits stand without it.

The media module had no platform profiles beyond Windows, so a Linux or
macOS build had no compiler or sysroot for its target and CMake simply
built for the host.

It now takes a toolchain file the way webrtc-jni does, and its profiles
mirror webrtc-jni's by name and by the file each one selects, so both
native libraries of a given classifier are built for the same target the
same way. Keeping one set of toolchain files rather than a second copy
is deliberate: they are where this project says which compiler and which
sysroot a target uses, and two answers would drift.

What configure is told about the target is worked out from what the
toolchain file already set -- the architecture from
CMAKE_SYSTEM_PROCESSOR, the target OS from the platform, the compiler
from CMAKE_C_COMPILER, the target flags from CMAKE_C_FLAGS, the sysroot
from CMAKE_SYSROOT -- so a target CMake can already build for needs no
new case. Apple is the exception it has to make: there the architecture
is a compiler flag rather than a different compiler.

Cross-compiled arm and aarch64 start with --disable-asm. An assembler
for a foreign architecture is a build prerequisite of its own, and
nothing here is fast enough yet to need one; correctness on every
platform first.

Only windows-x86_64 is verified, because it is the only target this
machine can build. It still builds from nothing and its configure line
is unchanged apart from an explicit --arch=x86_64, and the module's 19
tests pass. Everything else waits on CI.
The media module is about to become part of the normal build, so every
native build job has to be able to produce its FFmpeg, and none of them
could.

Windows gets MSYS2 with make and nasm, because FFmpeg's configure and
makefiles are shell scripts and building it needs a POSIX shell even
with MSVC. MSYS2_ROOT is exported so the media module's CMake finds it
wherever the runner image put it. Linux and macOS get nasm, which is
what FFmpeg assembles its x86 code with; macOS needs it even though the
runner is Apple Silicon, because the Intel build is cross-compiled on it.

The FFmpeg install directory is cached the same way the WebRTC checkout
already is, keyed by FFmpeg version, platform and the hash of the
dependency script and the module pom, so only the first build of a
platform pays for it.

The three native build jobs in each workflow now check out submodules,
since the FFmpeg source is one. test-natives does not, because it runs
against downloaded natives and never builds the module.
The module was opt-in behind a profile while it was being brought up.
It now builds with everything else, which is also what lets CI prove it
on the platforms this machine cannot build for.

That collapses the awkward part of the examples module. The media
example needed a source directory, a compile execution and an output
directory of its own, purely because a JPMS "requires" cannot be made
conditional and the module it names has to exist. With the module always
there, the example moves in beside the others and the whole profile
becomes one dependency and one "requires", exactly as its comment said
it would.

Anything building this project now needs the third-party/ffmpeg
submodule checked out, and make and nasm to build it. The README and the
guide say so where they used to describe the profile.
Every CI job failed at dependency resolution before a single mojo ran:

  Could not find artifact
  dev.onvoid.webrtc:webrtc-java-media:jar:windows-x86_64:0.19.0-SNAPSHOT

The module declared a dependency on its own classifier artifact so that
anything using it would get the natives along the way. That cannot work
on a clean machine: the artifact is built and attached by this same
module, so at the moment its dependencies are resolved nothing has
produced it yet. It only passed locally because an earlier install had
left the artifact in the local repository.

webrtc-java can do this because webrtc-jni, a module earlier in the
reactor, is what builds its natives, and because the artifact has been
published besides. Neither is true here.

So the dependency is gone, and anything using the module asks for the
natives itself with a second dependency carrying the platform
classifier. webrtc-examples does, and the guide says so. The module's
own tests never needed it: Surefire is given the directory the CMake
build collects the natives into.

Verified by emptying dev/onvoid/webrtc/webrtc-java-media out of the
local repository first, which reproduced the CI failure exactly, and
then building clean: 168 tests in webrtc and 19 in webrtc-java-media,
all five modules green.
Every job failed configuring the media module:

  CMake Error at dependencies/ffmpeg/CMakeLists.txt:162 (set):
    when parsing string
      C:\Users\runneradmin/ffmpeg/windows-x86_64
    Invalid character escape '\U'

Two things had to line up for this, which is why it never showed up
locally. Maven hands the install directory over as ${user.home} plus a
suffix, and on Windows that is a backslash path, which CMake reads as
the start of an escape sequence. And to_shell_path was a macro, so its
argument was substituted into the body as text rather than passed as a
value, leaving CMake to parse C:\Users\... as code.

It stayed hidden because that code only runs when FFmpeg has to be
built. This machine already had it installed, so every local build took
the short-circuit and never reached the macro. CI, starting from
nothing, reached it immediately.

The path is now normalised with file(TO_CMAKE_PATH) as it arrives, and
to_shell_path is a function, which passes its arguments as values.
Either change alone would have been enough; both are worth having,
since the first fixes this input and the second fixes the class of it.

Verified the way it should have been the first time: by deleting the
FFmpeg install directory so that the build path is the one that runs.
The failure reproduces exactly beforehand, and afterwards
"mvn clean package -DskipTests" builds FFmpeg from nothing and every
module succeeds, with the module's 19 tests green.
… with

The first CI run that reached FFmpeg turned up three separate faults,
all of them from reusing webrtc-jni's toolchain files. Those files are
not neutral descriptions of a target: they encode how webrtc-jni links
WebRTC's C++ ABI, and this module links no C++ ABI at all.

On Linux they compile with -nostdinc++, because webrtc-jni supplies
WebRTC's own libc++ through include paths of its own. This module
inherited the flag and none of the paths, so its first #include <string>
failed. It now uses toolchain files of its own that name the
distribution's cross compilers and nothing else. linux-x86_64 needs no
file at all, being built natively on the runner.

Cross builds also tripped over stripping. make install strips what it
installs with whichever strip is on the PATH, and the host's cannot read
a binary for another architecture: "unable to recognise the format of
the input file". FFmpeg is configured with --disable-stripping when
cross-compiling, and the toolchain files name the cross binutils.

On macOS the toolchain file set CMAKE_SYSTEM_NAME, which makes CMake
report a cross build even when host and target agree, so configure was
handed --enable-cross-compile for a native build along with
--arch=aarch64. Apple's clang knows that architecture as arm64 and
rejects the other outright, which configure reports only as being unable
to create an executable. Neither macOS target is a cross build -- the
Intel one runs under "arch -x86_64" -- so neither takes a file now, and
the architecture is spelled the way Apple spells it.

windows-aarch64 passed and keeps webrtc-jni's file, which suits it:
there the C++ standard library comes from MSVC either way.

Verified as far as this machine can: windows-x86_64 unaffected, with the
full reactor green. The other targets wait on CI again.
windows-x86_64 built everything, including the media module, and then
failed in webrtc-java-examples:

  Could not find artifact
  dev.onvoid.webrtc:webrtc-java-media:jar:windows-x86_64:0.19.0-SNAPSHOT

The examples asked for the media module's natives as a dependency. CI
runs "mvn package" and then "mvn -B jar:jar surefire:test", and the
second of those invokes goals directly rather than running a lifecycle,
so nothing reaches the package phase where that jar is attached and
there is nothing to resolve against. The first command had succeeded
moments earlier, which is why only the second failed.

webrtc-java survives the same pattern only because its classifier
artifact has been published and is already in the local repository.

So the natives are off the dependency graph entirely. The example needs
them at run time, and exec-maven-plugin is pointed at the directory the
CMake build collects them into, which is how the media module's own
tests have always found them.

Verified by running both CI commands in order against a local repository
with the media artifacts deleted: 168 tests in webrtc and 19 in
webrtc-java-media, every module green, and the example still plays.
Both macOS jobs still stopped at the same place:

  cc is unable to create an executable file.
  C compiler test failed.

The configure line was right by then -- --arch=arm64, no spurious
cross-compile -- and the only unusual thing left on it was --cc, naming
the compiler inside the Xcode toolchain:

  --cc=/Applications/Xcode_16.1.app/.../XcodeDefault.xctoolchain/usr/bin/cc

Calling that binary directly skips the /usr/bin/cc shim, and the shim is
what runs xcrun to point SDKROOT at an SDK. Without one there is no
libSystem to link against, so the link test fails and configure reports
it as being unable to create an executable. So --cc is no longer passed
on Apple: configure's own default is plain "cc", which goes through the
shim. Linux still gets it, since there the compiler is chosen
deliberately and there is no shim to lose.

That diagnosis came from reading a configure line and reasoning about
it, which is a poor way to fix a build. configure writes the real reason
into ffbuild/config.log and says almost nothing on the console, and on a
build machine nobody can reach that is the same as writing it nowhere.
The tail of that file is now printed when the build fails, so the next
one of these is read rather than guessed at.

Windows and Linux are unaffected: the argument was already withheld on
Windows, and the rest only runs after a failure. Verified by rebuilding
windows-x86_64 from nothing.
The media natives were built with the distribution's compilers and no
sysroot, so they needed a newer glibc than webrtc-java's do. A machine
could have run one and not the other, which is not a sensible thing to
ship in the same release.

They now build through webrtc-jni's toolchain files, which is what the
plan said in the first place, and so against the same sysroot. That also
means -nostdinc++, since everything Linux in this project uses the libc++
WebRTC bundles rather than whatever the build machine happens to have.
Not having understood that is what sent this down the distribution
toolchain road: the flag arrived without the include paths that go with
it and the first #include <string> failed, which looked like a reason to
abandon those files rather than to finish using them.

So this module now takes libc++ from webrtc.install.dir, and webrtc-jni
has to be built before it on Linux. That is the only thing it takes from
that build, and it is a C++ standard library, not any part of WebRTC: no
WebRTC symbol is linked here, and the two native libraries still meet
only through the C function table. The build says so plainly if the
headers are not there.

While in here, cross-compilation is decided by comparing host and target
rather than by CMAKE_CROSSCOMPILING, which every toolchain file in this
project sets simply by naming a system. Believing it is what handed
configure --enable-cross-compile for a native macOS build earlier, and
it would have done the same for linux-x86_64.

Verified on windows-x86_64, which is unaffected and still builds FFmpeg
from nothing with 19 tests green. Linux waits on CI.
The test-natives lane existed to prove that a cross compiled native
library actually runs on the machine it was built for, and it only ever
did that for webrtc-java. The media natives were built, packaged,
uploaded nowhere and never loaded on a real arm or arm64 machine, so
nothing said whether FFmpeg and the media library work there or merely
compile.

Both jars are uploaded now. The lane installs webrtc-java's as before,
and unpacks the media one into the directory the media module's tests
already read natives from, which is where its own build collects them.
Nothing else had to be taught where to look.

The native build is skipped there with -DskipNativeBuild, which turns
the two CMake executions off by moving them to no phase. Building FFmpeg
again on a runner whose whole job is to run it would prove nothing and
cost a quarter of an hour.

webrtc is installed rather than only tested, so that the media module
resolves it from the repository. Both in one reactor does not work, and
the reason is worth writing down because it is the second time it has
come up: Maven substitutes a module's own output for a dependency on it,
and a classifier artifact of that same module, which is where the
natives live, never reaches the class path. That is the same shape as
the failure that broke the examples lane.

Verified by doing locally what the lane does: unpacking the classifier
jar into target/natives and running both commands. 168 tests in webrtc,
19 in webrtc-java-media, no CMake in the second, and the media tests
load the unpacked libraries rather than any this machine built. The
first attempt, both modules in one reactor, failed exactly as described
above, which is how the reason is known rather than guessed.
The two arm lanes failed compiling the module, every translation unit
dying inside libc++ before it reached a line of ours:

  _LIBCPP_HARDENING_MODE_DEFAULT is not defined. This definition should
  be set at configuration time

linux-x86_64 passed, and the difference is in the toolchain files:
x86_64-linux-clang.cmake sets -nostdinc++ and the hardening mode itself,
while aarch64-linux-clang.cmake and aarch32-linux-clang.cmake set
neither. webrtc-jni never notices, because it links the webrtc target
and takes both from its PUBLIC flags. Nothing carries them to a module
that links no WebRTC target, which is this one by design.

So the module sets them on itself. The bundled libc++ has wanted its
hardening mode chosen at configuration time since branch-heads/7977, and
-nostdinc++ is what keeps clang from putting the GCC libstdc++ headers
it finds in the sysroot on the implicit search path, where they collide
with the bundled ones.

This is the same lesson as the last two rounds, arriving once more:
webrtc-jni's toolchain files describe half of an arrangement, and the
other half lives on the webrtc target. Reading only the file leaves the
half that matters behind.

Windows is untouched and still builds with 19 tests green. The arm lanes
wait on CI.
All three test-natives lanes failed, and neither half of what I wrote
survived contact with that runner.

Installing webrtc so that the media module could resolve it walked into
its package phase, where an antrun task attaches the host natives from
webrtc-jni/target. On a runner that only downloaded a jar there is no
such directory:

  attach-artifact on project webrtc-java: File does not exist:
  ../webrtc-jni/target/webrtc-java-0.19.0-SNAPSHOT-linux-aarch64.jar

That is why the original lane stopped at test rather than going further,
and it stops there again.

Both modules are back in one reactor, which is what makes webrtc-java
resolvable, and the reason that failed before is now handled where it
belongs. webrtc-java's natives are a classifier artifact of a module in
that same reactor, and Maven answers a dependency on such a thing with
the module's output directory, which holds no native library at all.
Both libraries are therefore unpacked into the directory the media
tests already read from, and are found as files rather than resolved as
artifacts.

Verified by the comparison that shows it: the same reactor command fails
with NoClassDefFound on AudioDeviceModule when only the media natives
are staged there, and passes with 168 and 19 tests when webrtc's are
staged beside them. The local repository was identical in both runs, so
the staging is what carries them.
CustomVideoSource extends AdaptedVideoTrackSource but handed every frame
straight to OnFrame without calling AdaptFrame. The encoder's requests for
a lower resolution or frame rate never took effect, so when the bitrate
could not carry the pushed frames, the encoder kept receiving them at full
size and dropped most of them instead. A 1080p file played through a peer
connection arrived at around 5 fps.

Frames are now adapted the way the desktop and camera sources already do:
dropped when the requested frame rate says so, and cropped and scaled to
the requested size otherwise.
The AVI demuxer was enabled, but not the decoders for what AVI files
typically hold beyond MPEG-4: Microsoft's MPEG-4 variants (MPG4, MP42,
DIV3), and MP2, AC-3 and ADPCM audio. Such a file opened, then failed as
soon as playback tried to decode it. E-AC-3 is enabled alongside AC-3,
whose decoder calls into its tables and which MSVC otherwise fails to link.

A configure that found FFmpeg already installed skipped the build
regardless of which components it had been built with, so a new decoder
never reached an existing install. The install now records its version
and components, and a mismatch rebuilds it.

The test asset is the first quarter second of Big Buck Bunny as the AVI
it is distributed in, cut without re-encoding.
One thread decodes both streams in the order the container stores them,
and it blocked whenever the audio queue held its second of audio. Coarse
interleaving, and a frame-threaded decoder that returns each frame several
packets after it was fed, can put video more than that behind audio in the
file. The video queue then ran dry and frames arrived late and in bursts,
which WebRTC's encoder answers by dropping all but the last frame of each:
an H.264 MOV played at 5 to 10 fps although every frame was decoded.

Audio may now run past its capacity, up to ten seconds, while the video
queue is below half full. The test asset stores all of its audio ahead of
its video, in Matroska, which is read strictly in file order.
MediaFilePlayerExample plays a media file through two peer connections in
the same process, without a signaling server, and shows the received video
in a Swing window while the received audio plays on the speakers. Start
and Stop set up and tear down the whole session.

Beside the video, a panel shows what the file contains, where playback is,
and what the receiver gets for each track, read from its statistics: codec,
resolution, frame rate, bitrate, packet loss, jitter and audio level.

The video sender's maximum bitrate is raised above WebRTC's default of
about 2 Mbit/s, which would otherwise hold a 1080p file to a fraction of
its resolution.
The window used Swing's cross-platform default. It now switches to the
system look and feel before the first component is created, and falls
back to the default with a warning if that is not available.
@devopvoid
devopvoid added this pull request to stack #293 September 25, 2026 22:33
…failed

coarseInterleavingKeepsVideoEven counted frames that arrived close
together, which also catches a runner that is merely slow for a moment:
both macOS builds saw 17 and 18 such frames. A player that starves its
video delivers it seconds late instead, about two seconds with this asset.
The test now measures how late the worst frame is against the first audio
chunk, which shares its timeline, and fails above a second, printing when
each frame arrived.

lowBitrateScalesDown failed on the armv7 runner with nothing to say why.
It now waits up to 30 seconds for slow runners, and its failure message
carries the sender's last outbound stats: whether it encoded at all, with
which encoder, and what limited it.
RTCStats::toJava handed NewObject the webrtc::Timestamp itself where the
Java constructor declares a long. NewObject takes C varargs, so nothing
converted it. On 64-bit targets the object travels in the same register
as a jlong would and the value came through intact. On 32-bit ARM a jlong
is aligned to a register pair and the object is not, so every argument
after it was shifted: each stats entry reached Java with a garbage type,
a corrupted id string and garbage attributes. The report still had
entries, which was all the existing test checked, but no entry could be
found by its type, so the armv7 runner never saw any outbound RTP stats.

The getStats test now checks that every entry carries a type, the id the
report files it under, a timestamp and its attributes.
The platform jars carried FFmpeg's and WebRTC's binaries but none of the
license texts those licenses ask to travel with them.

The media platform jars now carry the LGPL text and a notice under
META-INF/licenses/ffmpeg. The notice names the FFmpeg release the
libraries are built from, unmodified, where its source is, and where the
build configuration that selects its components lives, and points out
that some of the formats it decodes may be covered by patents.

The webrtc platform jars now carry, under META-INF/licenses/webrtc, the
licenses of WebRTC and of the third-party code linked into it, generated
from the build with WebRTC's own tool, and WebRTC's patent grant, which
the tool leaves out. How to regenerate the file when the WebRTC branch
changes is noted where it is installed.

NOTICE, the media module's README and the media guide say where to find
them.
…player

The package was named after the library behind it rather than what it
offers. The classes read media files and network streams and play them
into a peer connection, so they now live in dev.onvoid.webrtc.media.player.

dev.onvoid.webrtc.media itself cannot be used: the webrtc.java module
already owns that package, and a second module carrying it would be a
split package that the module system rejects.

The native entry points, the MediaInfo lookup, the examples and the
media guide follow the new name.
Listener calls arrive on the player's decode thread. Closing the player
from one, which is the natural thing to do in onEndOfStream, released
the native player on that same thread: it joined itself, which throws
and takes the JVM down, and it deleted the player while the player was
still on the stack below the call. MediaFileSource.close() did the same.

A close from within a listener call now closes the player at once, so
every later command does nothing and the state reads as closed, and
leaves the release to another thread. That thread first waits for any
command still running, since that command may be what made the call,
and then releases the player without holding the lock, so the listener
stays free to call the player while its thread is waited for.
A player pushes into its custom sources from a thread of its own, long
after it was given their handles, but held no reference to them.
Disposing of a source and its track while the player still ran freed
the native source under the pacing thread, which then pushed into
freed memory.

The extension interface gains retain and release functions for both
kinds of source, appended to the table as its versioning allows, so
the version stays 1 and an extension tells from the size whether they
are there. The pacer holds a reference to each source it feeds for as
long as it exists, and the media module refuses a table too short to
provide them.
When the decoders failed to open, the IOException was thrown first and
the half made player released afterwards. Releasing it reported the
closed state to the Java observer, a call into Java with an exception
pending, and the observer then cleared what it took for an exception of
its own: the IOException. The constructor returned normally with a
player that did nothing.

The player is now released before the exception is thrown, with its
observer detached, since nothing listens yet. The observer also no
longer calls into Java at all while an exception is pending, so it can
neither make such a call nor swallow an exception that is not its own.
swresample was set up once, from the format the stream started with,
and every frame was handed to it as if it still had that format. A
stream that changes its channel layout, sample format or sample rate
along the way, as live streams and some files do, had its frames read
with the wrong layout: a stereo setup reading a mono frame reads past
its end, and splits its samples between the two channels.

The decoder now remembers the format swresample was set up for, and
sets it up again for the first frame that differs. The output keeps
its channel count, since the source it feeds expects one.
A native library is extracted to a temporary file to be loaded. Windows
keeps a loaded library locked until the process ends, so deleting it on
exit never succeeds, and every run left a copy behind: the core library
under a random name, and since the media module the FFmpeg libraries
beside it. They add up to gigabytes.

Each process now extracts into a directory of its own, under the
libraries' own names. Where a loaded library can be deleted, as on Linux
and macOS, the directory goes as soon as loading is done. On Windows the
process holds a lock on a file in it until it ends, and the first load
in every process sweeps away the directories whose lock is free. It
waits until a directory is a minute old, so that it never sweeps one a
process has only just created and not yet locked. The sweep also takes
the randomly named copies earlier versions left behind; one still loaded
somewhere cannot be deleted, and is left.

Loading is synchronized as well, so that two classes initialized on
different threads no longer extract and load the same library twice.
A failure to read or decode ended the player's thread, but left it
reporting that it was playing, and every later play or seek did
nothing, since there was no thread left to act on it.

A failure now stops playback where it happened, as a pause does, and is
reported as such before the error itself. The thread stays, so playing
again carries on past what failed, and a seek can move away from it.

The playing flag, the pacer and the recorded state now change together
under the player's lock, since the failure is reported from the decode
thread and could otherwise interleave with a play from another. The
observer is still called only once the lock is released.
swresample holds the last samples of a stream back, waiting for more
input to filter them with. Nothing asked for them at the end, so audio
that had to be resampled ended a few milliseconds short, and the gap
was filled with silence.

The decoder now drains swresample at the end of the stream, and before
setting it up again for a change of format, where the samples it holds
belong before the new frame. A seek or a loop resets it instead, since
what it holds belongs to the position being left.
The submodule moves to n8.1, and the libraries the module loads to the
major versions that release carries: avutil 60, avcodec and avformat 62,
swresample 6 and swscale 9. FFmpeg 8 removed libpostproc, and with it
the configure option that turned it off. Everything else the build
enables is still there under the same name.

Updating showed that neither the install nor the jar ever lost a
library. Rebuilding FFmpeg installed over the earlier release, and the
jar's staging directory only ever gained files, so the first build
shipped the 7.1.1 libraries beside the new ones. A rebuild now starts
from an empty install directory, cleared only when the record of an
earlier build of ours shows it is ours to clear, and the staging
directory is filled afresh on every build.

A test now checks that the loaded FFmpeg is the release the pom pins,
and that it is an LGPL build.
CI reads the FFmpeg version out of the media module's pom by the name
of its element, and the property that hands the version to the tests
had that same name. The lookup found both, and wrote the second one,
unexpanded, into the environment of every build, which rejected it
before anything was built.

The tests now get the version under a name of their own.
FFmpeg names its version after the git tag it finds in its source, and
the shallow submodule checkout CI makes has no tags, so the libraries
built there called themselves 9047fa1 rather than n8.1, and the test
checking that the pinned release is loaded failed on every platform
that runs it.

The build now hands make the release the source says it is, read from
its RELEASE file, so the name is the same however the source was
checked out. It goes to make as REVISION: the makefile sets the revision
version.sh reads from that variable, over anything in the environment.
The README gains a feature entry and a short section on sending media
files: what the module does, a five line example with MediaFileSource,
the formats it reads, and a link to the guide. The formats listed are
the ones the FFmpeg build enables, and only files are named, since the
build leaves networking out.

The documentation home gets a feature card for it, and the guide
overview now lists the Media Files guide, which only the sidebar did
so far. The link to MediaFileExample's source pointed into a source
directory that does not exist; it points to the example now.
A source can now be a live stream behind an rtsp:// URL, such as an IP
camera's, as well as a local file. The FFmpeg build enables networking,
the RTSP demuxer and the RTP, UDP and TCP protocols. FFmpeg asks for RTP
over UDP first and falls back to TCP, interleaved on the RTSP
connection, when the server refuses UDP or nothing arrives.

A network source can stall or vanish, so every call that waits on one
now runs under a deadline, enforced through FFmpeg's interrupt
callback: opening, probing, each read, seeking and closing. Running out
of time reads as a timeout. The timeout is 10 seconds unless another is
passed to MediaReader or MediaFileSource. Closing a player interrupts
its reader first, so it never waits on a stalled stream, and the error
that interruption causes is not reported to the listener.

Every source is opened with a protocol whitelist. The RTSP demuxer
brings the HTTP protocol along for tunnelling, and a URL passed on from
a user must not be able to fetch through it.

Testing against a server showed three more things to fix. An error
opening a source quoted its URL, password included, and now leaves the
credentials out. A looping player ignored a failed rewind, so a live
stream that ended while looping reported a connection error; a source
that cannot rewind now simply ends. And network errors read as bare
numbers on Windows, where the C runtime has no words for them and FFmpeg
passes most Winsock codes through; the common ones are now named, in a
single helper that replaces three copies.

The examples take stream URLs as well: the player shows a stream's URL
without its credentials and disables looping for a live source, and
both describe how to run them against a camera. The Media Files guide
covers live streams, their timeouts and their limits.

RtspStreamTest plays against a small RTSP server in the test sources,
which refuses UDP and streams PCM audio over TCP.
@devopvoid
devopvoid merged commit cf30e90 into main Sep 26, 2026
11 checks passed
@devopvoid
devopvoid deleted the feat/media-platform-builds branch September 26, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant