Skip to content

mahozad/compose-video-player

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Note

For a real-world Compose Multiplatform app implementing video player, see https://github.com/mahozad/cutcon.

Alternative approaches

https://www.linkedin.com/pulse/java-media-framework-vs-javafx-api-randula-koralage/

GitHub issues, PRs and discussions about video player

Similar libraries

FFmpeg

About: https://ffmpeg.org/about.html

FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge. No matter if they were designed by some standards committee, the community or a corporation.

FFmpeg in 100 seconds

Building FFmpeg for Windows

The best way to use FFmpeg in Java/Kotlin seems to be through the javacv library. It uses JNI to directly call C libraries of FFmpeg in Java (.dll files on Windows and .so files on Linux) so it is quite performant. JavaCV brings FFmpeg binaries wrapped in JAR files for various platforms. For example, for Windows x86-64, it brings a JAR that contains a small ffmpeg.exe and its various libraries like avformat.dll and so on. The JAR is also quite small (about 20 MB for Windows x86-64). The ffmpeg.exe itself contained in the JAR too uses those libraries. We can also run the FFmpeg executable itself instead of using its libraries by using javacv Loader.load(...) which extracts the ffmpeg.exe and its libraries in a temp folder and returns the path of copied ffmpeg.exe, so we can use it with Java ProcessBuilder or Runtime to execute it. Running a simple command and delegating the logic to ffmpeg itself may be easier but may have a lower performance compared to using the libraries directly because we are creating another process.

See

See the sections below for more information about excluding JavaCV unneeded JARs and so on.

MSYS2 provides up-to-date native builds for FFmpeg, GCC, etc. just to name a few. See https://www.msys2.org/

YouTube probably uses FFmpeg to encode videos. See:

Alternative approaches and ways for using FFmpeg

For an example Kotlin library that uses a C library see Skiko

The org.bytedeco:ffmpeg[-platform][-gpl] dependency

The org.bytedeco:ffmpeg-platform[-gpl] brings many FFmpeg JAR artifacts for various OSs and architectures which are about 250 MB in total. This is so we can run FFmpeg in our Java app in a cross-platform manner (we can simply call Loader.load(ffmpeg::class.java) and it loads the appropriate FFmpeg native executable for the OS/architecture our program is running on). But because we are creating a GUI application and there is currently no way a GUI executable on Windows, for example, can be run on Linux or Android, we don't need the load to be cross-platform (i.e. we don't need all the various JAR native artifacts of the FFmpeg for a Windows exe etc. to be available for app). So, we want to exclude other FFmpeg JARs to dramatically reduce the app size (like what implementation(compose.desktop.currentOs) or JavaFx plugin do).

Shrinking/optimizing the app with proguard (pro.rules file) doesn't seem to help.

The org.bytedeco:ffmpeg-platform[-gpl] is just an aggregator dependency which automatically brings some artifacts of org.bytedeco:ffmpeg dependency (like JUnit 5 org.junit.jupiter:junit-jupiter aggregator except that junit-jupiter aggregates separate dependencies while ffmpeg-platform[-gpl] aggregates some artifacts of a single dependency) (if we use org.bytedeco:ffmpeg-platform-gpl it brings -gpl artifacts of org.bytedewco:ffmpeg (which have more features), otherwise it brings the regular artifacts).

Also see this org.bytedeco guide.

So, instead of the aggregator org.bytedeco:ffmpeg-platform[-gpl], we have used the org.bytedeco:ffmpeg dependency directly. In this case, Gradle will only download the default artifact of the dependency (like it does for other libraries) and none of its other artifacts (i.e. the JARs wrapping FFmpeg native executables) are downloaded (see all artifacts of one of its versions in MVN Repository). To bring those artifacts as well, we included the specific artifact for our current OS/architecture the Gradle is running on using classifiers in the dependency notation (like group:module:version:classifier). See Gradle docs: resolving specific artifacts of a dependency.

Another way that I wrote is to use the org.bytedeco:ffmpeg-platform[-gpl] aggregator and using either of the following approaches to exclude unwanted JAR files:

// See https://stackoverflow.com/q/28181462
// See https://dev.to/autonomousapps/a-crash-course-in-classpaths-build-l08
tasks.withType<KotlinCompile /*OR*/ /*KotlinCompileCommon*/> {
    doLast {
        libraries.map{it.name}.forEach(::println)
        // libraries.removeAll {
        //     "ffmpeg" in it.name
        // }
    }
    exclude { "ffmpeg" in it.name }
}
  • Register a Gradle TransformAction in the build script like this:
/**
 * Note that The code to detect OS and architecture has been adopted and adapted from
 * Compose Gradle plugin -> org/jetbrains/compose/desktop/application/internal/osUtils.kt
 *
 * See https://docs.gradle.org/current/userguide/artifact_transforms.html
 * and https://diarium.usal.es/pmgallardo/2020/10/12/possible-values-of-os-dependent-java-system-properties/
 * and https://stackoverflow.com/q/74389033
 */
abstract class FFmpegTransformer : TransformAction<TransformParameters.None> {

    @get:InputArtifact
    abstract val inputArtifact: Provider<FileSystemLocation>

    // TODO: Exclude irrelevant javacpp JARs as well
    override fun transform(outputs: TransformOutputs) {
        val artifactFileName = inputArtifact.get().asFile.name
        if (shouldTransform(artifactFileName)) {
            val dir = outputs.dir(inputArtifact.get().asFile.nameWithoutExtension) // Request an output location
            dir.resolve(artifactFileName).createNewFile() // Create an empty JAR file
            // OR Files.write(dir.toPath().resolve(fileName), "Generated text")
            println("Transforming $artifactFileName to $dir")
        } else {
            outputs.file(inputArtifact)
        }
    }

    private fun shouldTransform(artifactFileName: String) =
        artifactFileName.contains("ffmpeg") && (
                // If it's Windows, just include the x86 version to be able to run it on both 32- and 64-bit architectures
                // NOTE: Does not seem to work because org.bytedeco:ffmpeg tries to load
                //  64-bit variant on 64-bit Windows which we have excluded
                (currentTarget.os == OS.Windows && artifactFileName.isWin32Artifact()) ||
                (currentTarget.os != OS.Windows && artifactFileName.isForCurrentOS() && artifactFileName.isForCurrentArch()) ||
                (artifactFileName.isMainArtifact())
        ).not()

    private fun String.isWin32Artifact() = endsWith("windows-x86.jar") || endsWith("windows-x86-gpl.jar")

    private fun String.isMainArtifact() = contains("platform") || contains("ffmpeg-5.1.2-1.5.8.jar")

    private fun String.isForCurrentOS() = contains(currentTarget.os.id, ignoreCase = true)

    private fun String.isForCurrentArch() = contains(currentTarget.arch.id, ignoreCase = true)

    private enum class OS(val id: String) {
        Linux("linux"),
        MacOSX("macosx"),
        Windows("windows"),
        Android("android")
    }

    private enum class Arch(val id: String) {
        X86_64("x86_64"),
        X86("x86"),
        Arm("arm"),
        Arm64("arm64"),
    }

    private data class Target(val os: OS, val arch: Arch) {
        val id: String get() = "${os.id}-${arch.id}"
    }

    private val currentTarget by lazy {
        Target(currentOS, currentArch)
    }

    private val currentArch by lazy {
        val osArch = System.getProperty("os.arch")
        when (osArch) {
            "x86" -> Arch.X86
            "x86_64", "amd64" -> Arch.X86_64
            "arm" -> Arch.Arm
            "aarch64" -> Arch.Arm64
            else -> error("Unsupported OS architecture: $osArch")
        }
    }

    private val currentOS: OS by lazy {
        // NOTE: On Android the OS is Linux as well, so we check something else
        //  See https://developer.android.com/reference/java/lang/System#getProperties()
        val vendor = System.getProperty("java.vendor")
        val os = System.getProperty("os.name")
        when {
            vendor.contains("Android", ignoreCase = true) -> OS.Android
            os.startsWith("Linux", ignoreCase = true) -> OS.Linux
            os.equals("Mac OS X", ignoreCase = true) -> OS.MacOSX
            os.contains("Win", ignoreCase = true) -> OS.Windows
            else -> error("Unknown OS name: $os")
        }
    }
}

val artifactType = Attribute.of("artifactType", String::class.java)
val isProcessed = Attribute.of("isProcessed", Boolean::class.javaObjectType) // isProcessed is an arbitrary name
configurations
    .filter(Configuration::isCanBeResolved)
    .map(Configuration::getAttributes)
    .forEach {
        afterEvaluate {
            // Request isProcessed=true on all resolvable configurations
            it.attribute(isProcessed, true)
        }
    }

dependencies {
    attributesSchema {
        attribute(isProcessed)
    }
    artifactTypes.getByName("jar") {
        attributes.attribute(isProcessed, false)
    }
    registerTransform(FFmpegTransformer::class) {
        from.attribute(isProcessed, false).attribute(artifactType, "jar")
        to.attribute(isProcessed, true).attribute(artifactType, "jar")
    }
}

Side note 1: The org.bytedeco:ffmpeg-platform[-gpl] uses POM classifier property in Maven build system to depend on a specific artifact of a dependency (also see https://stackoverflow.com/a/20909695) (Gradle also supports this. See Gradle docs: resolving specific artifacts of a dependency).

Side note 2: The -gpl version of FFmpeg contains more features (like libx264 encoder) and therefore has larger native JARs (about 4 MB larger than the regular version).

Also see:

Creating a video player

We can use the javacv library (org.bytedeco:javacv-platform:1.5.8) and its FrameGrabber and Recorder and draw each frame in a Compose canvas. Note that the library brings JARs for all platforms and architectures (about 250 MB) which may not be needed on the current OS/architecture.

See:

Enable hardware acceleration

Playing audio in Java

Sync the video and audio

Speed up or slow down video and audio

https://github.com/waywardgeek/sonic

Example of speeding up by a factor of 3.0:

ffmpeg -i input.mp4 -vf "setpts=PTS/3.0" -af "atempo=3.0" output.mp4

See:

Test videos

Sample videos for testing can be downloaded from:

Some technical stuff

.ts (MPEG-2 transport stream-A container format for broadcasting MPEG-2 video via terrestrial and satellite networks) and .mp3 file formats are a type of MPEG-2 container format.

Muxing

Is abbreviation of multiplexing

PTS (presentation timestamp)

Transcoding

Other

Add audio spectrum below or over the video like Aimp. See https://github.com/goxr3plus/XR3Player#java-audio-tutorials-and-apis-by-goxr3plus-studio

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages