Conversation
Groundwork for the FFmpeg based media module (#77), which decodes a file natively and must get the frames into WebRTC. Carrying every frame and every 10 ms chunk through Java would mean a copy per frame, Java in the loop of a 100 Hz audio path, and no way to ever pass through encoded media. An extension library now calls into this library through a small C function table instead. NativeApi.tableAddress() hands out the address of a struct webrtc_java_api, NativeApi.handleOf() the address of the native object behind a CustomVideoSource or CustomAudioSource, and the extension pushes straight into that source. The table's video push wraps the caller's planes rather than copying them and releases them through a callback, so a decoder that already produces I420 reaches the encoder without a copy. Neither native library links against the other, which is what makes this work: NativeLoader extracts to a temporary file with a generated name, so there is nothing to link to. NativeLoader also learns to load a library together with the shared libraries it needs, under their real file names, so that a dynamic linker finds them as the same modules. Both sources can now be given the time a frame or chunk was captured, and the Java API exposes that as pushFrame(VideoFrame, long) and pushAudio(..., long), with SyncClock.currentTimeUs() as the clock they are read in. This is what makes a media file play back at its own frame rate: the encoder derives the outgoing RTP timestamp from the frame's NTP capture time, so a caller that supplies presentation times keeps the sent timeline free of the jitter of the pushing thread, and keeps audio and video lined up. Pushing without a timestamp behaves exactly as before. Refs #77
Scaffolding for the media extension (#77): the Maven module, the JPMS module webrtc.java.media, the CMake build of the native library, and the CMake build of FFmpeg itself from the third-party/ffmpeg submodule, which is now tracked in .gitmodules pinned to n7.1.1 and marked shallow. FFmpeg is configured LGPL only, without --enable-gpl and without --enable-nonfree, as shared libraries so an application can replace them, and with everything disabled except the containers, decoders, parsers and the file protocol this module plays. It installs into ffmpeg.install.dir and a later configure reuses it, the way the WebRTC checkout is reused. The native library links against FFmpeg's C ABI and the JVM only. It reaches WebRTC through the function table of webrtc_java_api.h at runtime, so it needs neither WebRTC's headers nor its C++ standard library, and the module builds with the platform's default toolchain. The module stays out of the default reactor behind the with-media-extension profile until it builds on every platform in CI: building FFmpeg needs make, nasm and pkg-config, which the rest of the project does not. What is here so far is a vertical slice rather than the decoder: FFmpeg.version() and FFmpeg.license() prove that the libraries were built, packaged, loaded in dependency order and can be called. MediaInfo is the shape the reader will report. The Java side compiles against the qualified export of dev.onvoid.webrtc.internal; the native side has not been built on any platform yet, so the FFmpeg configure line and the packaging are still unproven. Refs #77
Bringing up windows-x86_64 turned up three things the script had wrong. find_program(bash) picked Git for Windows' bash, which carries neither make nor nasm. MSYS2 is now looked for by path first, with MSYS2_ROOT as an override, and make and nasm are checked for inside it rather than with find_program, since they are only visible from its own shell. FFmpeg's configure probes the compiler, so the shell has to be entered through vcvars, located with vswhere. Inside it the MSVC toolchain directory has to precede /usr/bin: MSYS2 ships a coreutils link that otherwise shadows MSVC's linker, and configure then reports that the C compiler cannot create executables. configure, make and make install are therefore written out as one generated shell script and run once, instead of three execute_process calls, because all three have to share that one shell. An MSVC build of FFmpeg installs the import libraries into bin/ next to the DLLs and leaves only the .def files in lib/, so find_library searches both. FFmpeg n7.1.1 now builds from the submodule in about 140 seconds, and FFmpeg.version() and FFmpeg.license() report n7.1.1 and LGPL version 2.1 or later from the packaged platform jar.
MediaReader opens anything libavformat accepts and selects the video and audio stream that are meant to be played, which gives MediaInfo the native code behind it: duration, video size, frame rate and codec, audio sample rate, channel count and codec. A source holding neither video nor audio fails to open rather than handing back an empty reader, and a failure carries the message FFmpeg gave for it instead of a bare number. This module builds its own natives, so unlike webrtc they are not a jar its tests could resolve. Surefire gets the staging directory the CMake build collects them into on the test class path instead, since NativeLoader looks the libraries up as class path resources and a directory serves them as well as a jar. The jni-check profile mirrors the one in the webrtc module. The test asset is three seconds of 320x240 VP8 and 48 kHz mono Opus in WebM, 61 KB. Six tests cover the video, audio and duration it reports, a missing source, closing twice and using a closed reader; they pass under -Xcheck:jni with no warnings.
This is the part of the module that actually sends a file. Nothing goes through Java: one thread demuxes and decodes, a pacer delivers in real time, and both talk to webrtc-java through the C function table rather than over JNI. VideoDecoder hands a frame on untouched when the decoder already gave yuv420p, which is almost all 8-bit H.264, VP8, VP9 and MPEG-4 content, so the decoded picture reaches the encoder without a copy. What the pacer hands over is a reference to that AVFrame, dropped again in the release callback once WebRTC lets go of the pixels. Anything else goes through swscale first. AudioDecoder resamples to the only shape WebRTC takes, interleaved 16-bit PCM at 48 kHz in mono or stereo, and re-cuts it into exact 10 ms chunks, advancing their timestamps in 10 ms steps so the audio is regular whatever the container's packet timing was. MediaPacer makes one mapping from the source's presentation times onto the WebRTC clock and keeps it, so what reaches the receiver carries the timing of the file rather than of a thread. Video and audio are paced independently: they go to different sources, need no ordering between them, and keeping them apart is what stops a full video queue starving audio. The queues are bounded, so decoding runs ahead only as far as they allow and a whole file is never decoded into memory. Looping keeps the mapping and adds the source's duration to every timestamp, which both hides the seam and keeps capture times rising, as the encoder requires. A seek drops what is queued and starts a fresh mapping. MediaFileSource is the short way to all of it, and the one the README already described: it opens a source, makes a custom source for each kind of media the file has, and wires a player to feed them. Nineteen tests cover this where an application would see it, on the sinks of tracks made from those sources: frame and chunk counts, the 320x240 and 48 kHz mono shape they arrive in, that three seconds of media takes three seconds to play, looping, pause, seek, and the reader being spent once a player adopts it. They pass under -Xcheck:jni with no warnings. The test asset gained a keyframe every second, because seeking lands on a keyframe and it had only the one at the start.
webrtc-java-media declared no dependency on its own classifier artifact, so a project depending on the module got the Java classes and none of the native libraries, and failed at class-initialization time with "Native library 'avutil-59.dll' is not on the classpath". It now carries them the same way webrtc-java carries its own, through a dependency on itself with the platform classifier. Running the new example is what turned this up: it is the first thing to consume the module the way an application would, rather than from inside its own build where the staging directory is already on the class path.
The guide covers adding the module, sending a file with MediaFileSource, reading a source with MediaReader, controlling playback, following it with a listener, and driving your own media sources with MediaPlayer. It says plainly what can be played, that only local files work so far, and that the module is opt-in and built for windows-x86_64 only, so nobody plans around something that is not there yet. MediaFileExample is the same story as code: it opens a file, reports what it contains, makes tracks, adds them to a peer connection and follows playback to the end. Building it needs care, because the examples are a JPMS module and the media module is opt-in. A "requires" cannot be made conditional, and a descriptor naming a module that is not there fails the whole compile, so the example lives in a source directory of its own and is compiled by its own execution into an output directory of its own. That keeps its module descriptor off the sourcepath, leaves the modular build of the other examples untouched, and means a build without the profile does not see the example at all. exec-maven-plugin is pointed at that directory so the command the docs give works as written. When the media module joins the reactor by default, all of it collapses into one dependency and one "requires".
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #77.
Sends a media file over a peer connection in place of a camera and a microphone, and adds the native extension API that makes it possible without carrying every frame through Java.
The extension API
A small, versioned C function table in
webrtc-jni(webrtc_java_api.h) that a native extension library calls directly.NativeApi.tableAddress()hands out its address andNativeApi.handleOf(source)the native handle of aCustomVideoSourceorCustomAudioSource, withdev.onvoid.webrtc.internalqualified-exported towebrtc.java.media.There is no link-time dependency between the two native libraries, which is the point:
NativeLoaderextracts to a temporary file with a generated name, so there is nothing an extension could link against. Any future extension — GStreamer, a capture SDK, a hardware encoder — can use the same table.CustomVideoSource::PushFrameandCustomAudioSource::PushAudioDatanow take a capture time, exposed aspushFrame(VideoFrame, long)andpushAudio(..., long). Existing callers that pass no timestamp keep the old behaviour of being stamped with the source's own clock.Reading
VideoStreamEncoder::OnFramechanged one detail of the design worth recording: the encoder overwrites a frame's RTP timestamp with90 * ntp_time_msand drops any frame whose NTP capture time does not advance. The field that carries a caller's timing to the wire is therefore the NTP capture time, notrtp_timestamp.The media module
webrtc-java-media, a JPMS module (webrtc.java.media) with its own native library, which decodes with FFmpeg and feeds the custom media sources through the table above. Nothing goes through Java: one thread demuxes and decodes, a pacer delivers in real time.yuv420p— almost all 8-bit H.264, VP8, VP9 and MPEG-4 — has itsAVFramehanded on by reference and dropped again in the release callback once WebRTC lets go of the pixels. Anything else goes through swscale first.Java API:
MediaReader,MediaInfo,MediaPlayer,MediaPlayerListener,MediaPlayerState, andMediaFileSourceas the turnkey class the issue asked for.Two departures from the original design
Verified
Measured on the committed test asset — three seconds of 320x240 VP8 at 15 fps and 48 kHz mono Opus — observed at the sinks of real tracks made from the custom sources:
19 tests in the media module cover the frame and chunk counts, the shape the media arrives in, real-time pacing, looping, pause, seek, a missing source, and the reader being spent once a player adopts it. They pass under
-Xcheck:jniwith no warnings at all — not just no fatal errors — including the observer callbacks, which run on the player's own thread and attach to the JVM to make a call.mvn verifyon thewebrtcmodule stays green at 168 tests.FFmpeg build and licensing
Built from the
third-party/ffmpegsubmodule pinned to n7.1.1, LGPL only: no--enable-gpl, no--enable-nonfree,--disable-everythingand then explicit lists of just what this module plays. FFmpeg is linked dynamically and ships as separate files inside the platform jar, so its libraries can be replaced, which is what the LGPL asks. The wrapper code stays Apache-2.0.Bringing the build up on Windows corrected three things, all of which would have bitten anyone else trying it:
find_program(bash)turns up Git for Windows' bash, which carries neithermakenornasm. MSYS2 is now looked for by path, withMSYS2_ROOTas an override.vcvars, and inside it the MSVC toolchain has to precede/usr/bin: MSYS2 ships a coreutilslinkthat otherwise shadows MSVC's linker and makes configure conclude the compiler cannot create executables. This is why configure, make and make install are one generated script run once rather than threeexecute_processcalls.bin/next to the DLLs, leaving only the.deffiles inlib/.Docs and example
docs/guide/media/media-files.mdwith a sidebar entry and a section on the examples page, plus a runnableMediaFileExample. Writing the example is what turned up thatwebrtc-java-mediadeclared no dependency on its own classifier artifact, so depending on the module got the Java classes and none of the native libraries.Not done yet, deliberately
windows-x86_64only. The other six targets and the CI caching by FFmpeg tag and platform are next.with-media-extensionprofile, and stays that way until every platform builds in CI.fileandpipeprotocols. Network sources need no new code, only the protocols turned on in the build.AdaptFrameon pushed video is deferred, because applying encoder resolution and frame-rate requests means cropping and scaling the buffer, which would change behaviour for existingpushFramecallers.A factory fed by this module is sending pushed audio and cannot also send audio captured by its
AudioDeviceModule, which is the same constraint as #289; the docs say so in both places.