How to play videos in Compose Web?

Muhammad Khubaib Imtiaz
4 min readJun 15, 2024

--

Photo by Johnny McClung on Unsplash

Today, We’re going to learn how can we play YouTube Videos in Compose Web. This tutorial is really simple and strait forward. Let’s get into it.

Playing YouTube Videos or Simple Videos can be really tricky because there are not enough resource available. We can play YouTube & Simple Videos as well using this method.

First of all, You need to create a HTMLVideoPlayer.kt inside the jsMain or wasmMain.

Demo Path

After that we need to use this code inside the HTMLVideoPlayer.kt .

package org.company.app.html

import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateObserver
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.round
import kotlinx.browser.document
import kotlinx.dom.appendElement
import kotlinx.dom.createElement
import org.w3c.dom.Document
import org.w3c.dom.Element
import org.w3c.dom.HTMLElement
import kotlin.random.Random

val NoOpUpdate: Element.() -> Unit = {}

private class ComponentInfo<T : Element> {
lateinit var container: Element
lateinit var component: T
lateinit var updater: Updater<T>
}


private class FocusSwitcher<T : Element>(
private val info: ComponentInfo<T>,
private val focusManager: FocusManager
) {
private val backwardRequester = FocusRequester()
private val forwardRequester = FocusRequester()
private var isRequesting = false

fun moveBackward() {
try {
isRequesting = true
backwardRequester.requestFocus()
} finally {
isRequesting = false
}
focusManager.moveFocus(FocusDirection.Previous)
}

fun moveForward() {
try {
isRequesting = true
forwardRequester.requestFocus()
} finally {
isRequesting = false
}
focusManager.moveFocus(FocusDirection.Next)
}

@Composable
fun Content() {
Box(
Modifier
.focusRequester(backwardRequester)
.onFocusChanged {
if (it.isFocused && !isRequesting) {
focusManager.clearFocus(force = true)
val component = info.container.firstElementChild
if(component != null) {
requestFocus(component)
}else {
moveForward()
}
}
}
.focusTarget()
)
Box(
Modifier
.focusRequester(forwardRequester)
.onFocusChanged {
if (it.isFocused && !isRequesting) {
focusManager.clearFocus(force = true)

val component = info.container.lastElementChild
if(component != null) {
requestFocus(component)
}else {
moveBackward()
}
}
}
.focusTarget()
)
}
}

private fun requestFocus(element: Element) : Unit = js("""
{
element.focus();
}
""")

private fun initializingElement(element: Element) : Unit = js("""
{
element.style.position = 'absolute';
element.style.margin = '0px';
}
""")

private fun changeCoordinates(element: Element,width: Float,height: Float,x: Float,y: Float) : Unit = js("""
{
element.style.width = width + 'px';
element.style.height = height + 'px';
element.style.left = x + 'px';
element.style.top = y + 'px';
}
""")



@Composable
fun <T : Element> HtmlView(
factory: Document.() -> T,
modifier: Modifier = Modifier,
update: (T) -> Unit = NoOpUpdate
) {



val componentInfo = remember { ComponentInfo<T>() }

val root = LocalLayerContainer.current
val density = LocalDensity.current.density
val focusManager = LocalFocusManager.current
val focusSwitcher = remember { FocusSwitcher(componentInfo, focusManager) }

Box(
modifier = modifier.onGloballyPositioned { coordinates ->
val location = coordinates.positionInWindow().round()
val size = coordinates.size
changeCoordinates(componentInfo.component,size.width / density, size.height / density, location.x / density,location.y / density)
}
) {
focusSwitcher.Content()
}

DisposableEffect(factory) {
componentInfo.container = document.createElement("div",NoOpUpdate)
componentInfo.component = document.factory()
root.insertBefore(componentInfo.container,root.firstChild)
componentInfo.container.append(componentInfo.component)
componentInfo.updater = Updater(componentInfo.component, update)
initializingElement(componentInfo.component)
onDispose {
root.removeChild(componentInfo.container)
componentInfo.updater.dispose()
}
}

SideEffect {
componentInfo.updater.update = update
}
}


private class Updater<T : Element>(
private val component: T,
update: (T) -> Unit
) {
private var isDisposed = false

private val snapshotObserver = SnapshotStateObserver { command ->
command()
}

private val scheduleUpdate = { _: T ->
if(isDisposed.not()) {
performUpdate()
}
}

var update: (T) -> Unit = update
set(value) {
if (field != value) {
field = value
performUpdate()
}
}

private fun performUpdate() {
snapshotObserver.observeReads(component, scheduleUpdate) {
update(component)
}
}

init {
snapshotObserver.start()
performUpdate()
}

fun dispose() {
snapshotObserver.stop()
snapshotObserver.clear()
isDisposed = true
}
}

val LocalLayerContainer = staticCompositionLocalOf<Element> {
error("CompositionLocal LayerContainer not provided")
// you can replace this with document.body!!
}

Expect / Actual:

Now we need to use the Expect and actual mechanism to provide the Video Player Functionality inside the Compose Multiplatform Or Kotlin Multiplatform.

// composeApp/src/jsMain/kotlin/org/company/app/App.js.kt

@Composable
internal actual fun VideoPlayer(modifier: Modifier, url: String?, thumbnail: String?) {
val videoId = extractVideoId(url.toString())
CompositionLocalProvider(LocalLayerContainer provides document.body!!) {
HTMLVideoPlayer(videoId)
}

}

Demo:

Conclusion:
In this article, We have covered the topic, How to play Videos in Compose Web. If you want to play videos on Android, iOS, Web and Desktop then checkout YouTube-Clone-KMP.

--

--

Muhammad Khubaib Imtiaz
Muhammad Khubaib Imtiaz

Written by Muhammad Khubaib Imtiaz

Software Engineer Android | Kotlin Mutlipaltform | Compose Multiplatform. Community Lead @Kotzilla

No responses yet