I'm using jetpack compose to build a screen with a background video using exoplayer and, as we know, exoplayer didn't have native support with compose yet, so we need to start a player using AndroidView. But I need to take a screenshot from the composable, I'm taking the screenshot using:
LocalView.current.drawToBitmap()
And the problem is that the video didn't appear on the bitmap. I've create a demo screen to show what's happening:
Above you can see a rectangle composable with the video and a transparent red composable just to see something above the video.
Below is an Image composable that show the bitmap captured. And the video didn't appear, just the red rectangle.
Any idea about what's happening here? I've tried two libs that help take screenshot from compose (capturable and compose-screenshot) and both didn't work.
My code:
@Composable
fun DemoScreen() {
var backgroundSnapShot by remember { mutableStateOf<Bitmap?>(null) }
Column(
Modifier
.fillMaxSize()
.background(Color.LightGray)
.padding(16.dp)
) {
Background(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
capture = {
backgroundSnapShot = it
}
)
VerticalSpacer(30.dp)
Box(
Modifier
.fillMaxWidth()
.height(120.dp)
.background(Color.DarkGray)
.clip(RoundedCornerShape(8.dp))
) {
backgroundSnapShot?.let {
Image(
it.asImageBitmap(),
contentDescription = "",
modifier = Modifier
.fillMaxSize(),
contentScale = ContentScale.Fit
)
}
}
}
}
@Composable
fun Background(modifier: Modifier, capture: (Bitmap?) -> Unit) {
val playerState = rememberVideoPlayerState(
"my video url",
prepare = true,
autoPlay = true,
)
Box(modifier.clip(RoundedCornerShape(8.dp))) {
VideoPlayer(
state = playerState,
Modifier.fillMaxSize(),
)
Indicator(
modifier = Modifier
.fillMaxWidth()
.height(120.dp),
capture = capture,
)
}
}
@Composable
fun Indicator(modifier: Modifier, capture: (Bitmap?) -> Unit) {
var offsetY by remember { mutableStateOf(0f) }
val view = LocalView.current
Box(
modifier
.offset { IntOffset(0, offsetY.roundToInt()) }
.background(Color.Red.copy(0.3f))
.clip(RoundedCornerShape(8.dp))
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
offsetY += dragAmount.y
capture.invoke(view.drawToBitmap())
//yes, I moved the box to invoke the capture method, you can see this in the print that the box has Y offset
}
}
)
}
The exoplayer implementation on compose:
@Composable
fun rememberVideoPlayerState(
video: String,
prepare: Boolean = true,
autoPlay: Boolean = true,
frameListener: VideoFrameMetadataListener? = null,
): VideoPlayerState {
val context = LocalContext.current
val currentWindow by remember { mutableStateOf(0) }
val playbackPosition by remember { mutableStateOf(0L) }
val mediaItem by remember { mutableStateOf(MediaItem.Builder()) }
val exoPlayer = remember(context) {
val extractorsFactory = DefaultExtractorsFactory()
.setMp4ExtractorFlags(Mp4Extractor.FLAG_WORKAROUND_IGNORE_EDIT_LISTS)
.setFragmentedMp4ExtractorFlags(FragmentedMp4Extractor.FLAG_WORKAROUND_IGNORE_EDIT_LISTS)
val mediaSourceFactory = DefaultMediaSourceFactory(context, extractorsFactory)
val player = SimpleExoPlayer.Builder(context).apply {
setLoadControl(DefaultLoadControl())
setTrackSelector(DefaultTrackSelector(context))
setMediaSourceFactory(mediaSourceFactory)
setBandwidthMeter(DefaultBandwidthMeter.getSingletonInstance(context))
}.build().apply {
mediaItem.setUri(video)
setMediaItem(mediaItem.build())
playWhenReady = true
seekTo(currentWindow, playbackPosition)
repeatMode = ExoPlayer.REPEAT_MODE_ONE
if (prepare) prepare()
}
VideoPlayerState(player)
}
if (autoPlay) exoPlayer.start()
return exoPlayer
}
@Composable
fun VideoPlayer(state: VideoPlayerState, modifier: Modifier = Modifier) = AndroidView(
modifier = modifier,
factory = { context ->
StyledPlayerView(context).apply {
useController = false
resizeMode = AspectRatioFrameLayout.RESIZE_MODE_ZOOM
setKeepContentOnPlayerReset(true)
}
},
) {
it.player = state.inner
}
