Blog Infos
Author
Published
Topics
, , ,
Published

Unlocking Cross-Platform Delight: Introducing Compose Multiplatform Video Players for iOS, Android & Desktop!

Many of you who have worked with XML and the old ExoPlayer days know how challenging it was to integrate a video player into an app. Fortunately, with Jetpack Compose, things have improved significantly. But even better than Compose itself is Compose Multiplatform, which empowers Android developers to become ‘iOS lite’ developers.

However, in this puzzle, some pieces are missing, and as Doctor Strange says, “In the grand calculus of the multiverse, some views should stay native, or else there will be no nativeness.”. One of these views is a video player; it must be platform-specific, or the app will lack the native feel of each platform. No one wants to use an iPhone and get an Android-looking video player.

So, let’s start.

Expect Actual Mechanism:

If you have prior experience with KMM, you might be familiar with the expect/actual mechanism. We can use it to create a VideoPlayer function as an expect function, and each platform will provide its actual implementation.

@Composable
expect fun VideoPlayer(modifier: Modifier, url: String)

In this approach, passing the Modifier as the first parameter to every Composable function is considered a good practice. It allows you to customize the component without altering its inner modifier chain.

Each platform (Android, iOS, etc.) will then implement the actual VideoPlayer function with platform-specific code while adhering to the same expect function signature. Add the actual function to all platforms.

@Composable
actual fun VideoPlayer(modifier: Modifier, url: String) {
    // Platform-specific implementation here
}

Android Implementation:

As an Android developer, you might be familiar with ExoPlayer, a media player library for Android.

Step 1:

Add ExoPlayer’s dependencies to the Android Main in your build.gradle file of shared module

val androidMain by getting {
           dependencies {
               implementation("androidx.media3:media3-exoplayer:1.1.0")
               implementation("androidx.media3:media3-exoplayer-dash:1.1.0")
               implementation("androidx.media3:media3-ui:1.1.0")
           }
       }

Now, you have to add the actual implementation of your Video player. For that, you have to go to your Android main where you created the actual function and add this code.

@Composable
actual fun VideoPlayer(modifier: Modifier, url: String){
    AndroidView(
        modifier = modifier,
        factory = { context ->
            VideoView(context).apply {
                setVideoPath(url)
                val mediaController = MediaController(context)
                mediaController.setAnchorView(this)
                setMediaController(mediaController)
                start()
            }
        },
        update = {})
}

In the actual implementation of the video player for Android, we’ll use the AndroidView composable to wrap the ExoPlayer functionality inside an Android View. The VideoPlayer for Android is using ExoPlayer internally through the VideoView wrapped inside the AndroidView composable, allowing us to play videos in compose multiplatform.

Fairly simple?

IOS Implementation:

In our iOS implementation, we can integrate AVPlayer and UIKit views using Kotlin’s C-Interop. First, we create the AVPlayer instance with the provided URL and use AVPlayerLayer and AVPlayerViewController. AVPlayerViewController handles playback controls and provides a native feel. AVPlayer is similar to ExoPlayer in Android.

val player = remember { AVPlayer(uRL = NSURL.URLWithString(url)!!) }
   val playerLayer = remember { AVPlayerLayer() }
   val avPlayerViewController = remember { AVPlayerViewController() }
   avPlayerViewController.player = player
   avPlayerViewController.showsPlaybackControls = true
   playerLayer.player = player

The UIKitView composable is used to integrate AVPlayerViewController with existing UIKit views. The player’s container view is created, and AVPlayerViewController’s view is added as a subview. The onResize callback ensures the player’s frame is adjusted correctly. When the view is updated, the player starts playing. You can see here the modifier that we passed can be used directly on UIKitView

// Use a UIKitView to integrate with your existing UIKit views
   UIKitView(
       factory = {
           // Create a UIView to hold the AVPlayerLayer
           val playerContainer = UIView()
           playerContainer.addSubview(avPlayerViewController.view)
           // Return the playerContainer as the root UIView
           playerContainer
       },
       onResize = { view: UIView, rect: CValue<CGRect> ->
           CATransaction.begin()
           CATransaction.setValue(true, kCATransactionDisableActions)
           view.layer.setFrame(rect)
           playerLayer.setFrame(rect)
           avPlayerViewController.view.layer.frame = rect
           CATransaction.commit()
       },
       update = { view ->
           player.play()
           avPlayerViewController.player!!.play()
       },
       modifier = modifier)

Our Final function should look like this,

@Composable
actual fun VideoPlayer(modifier: Modifier, url: String) {
    val player = remember { AVPlayer(uRL = NSURL.URLWithString(url)!!) }
    val playerLayer = remember { AVPlayerLayer() }
    val avPlayerViewController = remember { AVPlayerViewController() }
    avPlayerViewController.player = player
    avPlayerViewController.showsPlaybackControls = true

    playerLayer.player = player
    // Use a UIKitView to integrate with your existing UIKit views
    UIKitView(
        factory = {
            // Create a UIView to hold the AVPlayerLayer
            val playerContainer = UIView()
            playerContainer.addSubview(avPlayerViewController.view)
            // Return the playerContainer as the root UIView
            playerContainer
        },
        onResize = { view: UIView, rect: CValue<CGRect> ->
            CATransaction.begin()
            CATransaction.setValue(true, kCATransactionDisableActions)
            view.layer.setFrame(rect)
            playerLayer.setFrame(rect)
            avPlayerViewController.view.layer.frame = rect
            CATransaction.commit()
        },
        update = { view ->
            player.play()
            avPlayerViewController.player!!.play()
        },
        modifier = modifier)
}

and we are done. You can use all the available uikit views using this method such as audio player, camera, etc.

and we are done.

Desktop Implementation:

Integrating video playback in Compose Desktop is a bit intricate but achievable. We use SwingPanel, adding VLC dependency to Desktop Main

SwingPanel(
    factory = factory,
    background = Color.Transparent,
    modifier = modifier,
    update = {
        
    }
)
val desktopMain by getting {
        dependencies {
         
            implementation("uk.co.caprica:vlcj:4.7.0")

        }
    }

Here’s a simple VideoPlayer implementation with VLCJ. It initializes the media player, plays the video URL, and handles macOS-specific player components. Now you can enjoy video playback on Compose Desktop! 🎉❤️

@Composable
 fun VideoPlayerImpl(
    url: String,
    modifier: Modifier,
) {
    val mediaPlayerComponent = remember { initializeMediaPlayerComponent() }
    val mediaPlayer = remember { mediaPlayerComponent.mediaPlayer() }

    val factory = remember { { mediaPlayerComponent } }

    LaunchedEffect(url) { mediaPlayer.media().play/*OR .start*/(url) }
    DisposableEffect(Unit) { onDispose(mediaPlayer::release) }
    SwingPanel(
        factory = factory,
        background = Color.Transparent,
        modifier = modifier,
        update = {

        }
    )
}

private fun initializeMediaPlayerComponent(): Component {
    NativeDiscovery().discover()
    return if (isMacOS()) {
        CallbackMediaPlayerComponent()
    } else {
        EmbeddedMediaPlayerComponent()
    }
}


private fun Component.mediaPlayer() = when (this) {
    is CallbackMediaPlayerComponent -> mediaPlayer()
    is EmbeddedMediaPlayerComponent -> mediaPlayer()
    else -> error("mediaPlayer() can only be called on vlcj player components")
}

private fun isMacOS(): Boolean {
    val os = System
        .getProperty("os.name", "generic")
        .lowercase(Locale.ENGLISH)
    return "mac" in os || "darwin" in os
}

and done ❤.

The compose multiplatform video player for desktop is experimental and can be found here.

Result:

Repo link: https://github.com/Kashif-E/Compose-Multiplatform-Video-Player

Thanks for reading ❤ do clap if you learned something.

let’s connect on Twitter and LinkedIn and discuss more ideas.

This article was previously published on proandrdoiddev.com

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

Jobs

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
Hi, today I come to you with a quick tip on how to update…
READ MORE
blog
Automation is a key point of Software Testing once it make possible to reproduce…
READ MORE
blog
Drag and Drop reordering in Recyclerview can be achieved with ItemTouchHelper (checkout implementation reference).…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu