From 18e69bdd9260e0e1263278cd337e14ea7ad6d270 Mon Sep 17 00:00:00 2001 From: mzhsy1 Date: Fri, 10 Oct 2025 11:59:03 +0800 Subject: [PATCH 1/5] Add Compose-based SubtitleView for rendering CueGroup in Jetpack Compose --- .../compose/material3/PlayerSubtitleView.kt | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt diff --git a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt new file mode 100644 index 00000000000..3b2e199c3f1 --- /dev/null +++ b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt @@ -0,0 +1,204 @@ + +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.ui.compose.material3 + +import android.annotation.SuppressLint +import androidx.annotation.OptIn +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.height +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.zIndex + +import androidx.media3.common.text.Cue + +import androidx.media3.common.text.CueGroup +import androidx.media3.common.util.UnstableApi + +/** + * A Material3 composable that renders subtitles provided by a [CueGroup] from Media3. + * + * This component displays both text and bitmap cues according to their layout properties + * (e.g., position, size, anchor). Text cues are rendered using [Text], while bitmap cues + * are rendered using [Image]. The subtitle area is aligned to the bottom center of the + * provided layout space by default. + * Currently supports SRT, PSG, and ASS subtitles. + * ASS subtitles are converted to SRT by ExoPlayer for display. + * SRT subtitles with the same timestamp are displayed vertically in sequence. + * + * @param cueGroup The group of cues to display. If null or empty, nothing is rendered. + * @param modifier The [Modifier] to be applied to the root layout.(Only affects SRT subtitles) + * @param subtitleStyle The [TextStyle] used for rendering text cues. Defaults to + * [MaterialTheme.typography.bodyLarge] with white color and 18sp font size.(Only affects SRT subtitles) + * @param backgroundColor The background color behind text cues. Defaults to fully + * transparent black ([Color.Black.copy(alpha = 0.0f)]).(Only affects SRT subtitles) + * + * @sample androidx.media3.ui.compose.material3.SubtitleViewSample + * + * Here is a basic usage example: + * + * ``` + * @Composable + * fun VideoPlayerWithSubtitles(exoPlayer: ExoPlayer) { + * var currentCueGroup: CueGroup? by remember { mutableStateOf(null) } + * + * DisposableEffect(exoPlayer) { + * val listener = object : Player.Listener { + * override fun onCues(cueGroup: CueGroup) { + * currentCueGroup = cueGroup + * } + * } + * exoPlayer.addListener(listener) + * onDispose { + * exoPlayer.removeListener(listener) + * } + * } + * + * Box { + * // Your video surface or PlayerView here + * SubtitleView( + * cueGroup = currentCueGroup, + * subtitleStyle = MaterialTheme.typography.bodyLarge.copy( + * color = Color.White, + * fontSize = 20.sp + * ), + * backgroundColor = Color.Black.copy(alpha = 0.5f), + * modifier = Modifier.align(Alignment.BottomCenter) + * ) + * } + * } + * ``` + */ +@Composable +@OptIn(UnstableApi::class) +fun SubtitleView( + cueGroup: CueGroup?, + modifier: Modifier = Modifier, + subtitleStyle: TextStyle = MaterialTheme.typography.bodyLarge.copy( + color = Color.White, + fontSize = 18.sp + ), + backgroundColor: Color = Color.Black.copy(alpha = 0.0f) +) { + if (cueGroup == null || cueGroup.cues.isEmpty()) { + return + } + + val (screenWidthDp, screenHeightDp) = getScreenDimensions() + + Box( + modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp), + contentAlignment = Alignment.BottomCenter + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + cueGroup.cues.forEach { cue -> + // Render text cue + cue.text?.toString()?.takeIf { it.isNotEmpty() && it != "null" }?.let { text -> + Text( + text = text, + style = subtitleStyle, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(bottom = 8.dp) + .background(backgroundColor) + ) + } + + // Render bitmap cue + cue.bitmap?.let { bitmap -> + val bitmapWidth = if (cue.size != Cue.DIMEN_UNSET) { + (screenWidthDp * cue.size).toFloat() + } else { + bitmap.width.toFloat() + } + + val bitmapHeight = if (cue.bitmapHeight != Cue.DIMEN_UNSET) { + (screenHeightDp * cue.bitmapHeight).toFloat() + } else { + bitmap.height.toFloat() + } + + val offsetX = when (cue.positionAnchor) { + Cue.ANCHOR_TYPE_START -> screenWidthDp * cue.position + Cue.ANCHOR_TYPE_MIDDLE -> (screenWidthDp * cue.position) - (bitmapWidth / 2) + Cue.ANCHOR_TYPE_END -> (screenWidthDp * cue.position) - bitmapWidth + else -> screenWidthDp * cue.position + } + + val offsetY = when (cue.lineAnchor) { + Cue.ANCHOR_TYPE_START -> screenHeightDp * cue.line + Cue.ANCHOR_TYPE_MIDDLE -> (screenHeightDp * cue.line) - (bitmapHeight / 2) + Cue.ANCHOR_TYPE_END -> (screenHeightDp * cue.line) - bitmapHeight + else -> screenHeightDp * cue.line + } + + Box( + modifier = Modifier + .offset(x = offsetX.dp , y = offsetY.dp) + .fillMaxSize() + .zIndex(cue.zIndex.toFloat()) + ) { + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = "Subtitle bitmap cue", + modifier = Modifier + .width(bitmapWidth.dp) + .height(bitmapHeight.dp), + contentScale = ContentScale.Fit, + alignment = Alignment.Center + ) + } + } + } + } + } +} + +/** + * Returns the current screen dimensions in dp. + * + * @return A [Pair] of [Int] values representing the screen width and height in dp. + */ +@SuppressLint("ConfigurationScreenWidthHeight") +@Composable +private fun getScreenDimensions(): Pair { + val configuration = LocalConfiguration.current + return Pair(configuration.screenWidthDp, configuration.screenHeightDp) +} \ No newline at end of file From 24260069b78b219231e813205d2276338c0add9e Mon Sep 17 00:00:00 2001 From: mzhsy1 Date: Fri, 10 Oct 2025 12:27:41 +0800 Subject: [PATCH 2/5] remove OptIn,SuppressLintSuppressLint --- .../media3/ui/compose/material3/PlayerSubtitleView.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt index 3b2e199c3f1..ca4c0e2ea6c 100644 --- a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt +++ b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt @@ -17,8 +17,7 @@ package androidx.media3.ui.compose.material3 -import android.annotation.SuppressLint -import androidx.annotation.OptIn + import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -104,7 +103,7 @@ import androidx.media3.common.util.UnstableApi * ``` */ @Composable -@OptIn(UnstableApi::class) +@UnstableApi fun SubtitleView( cueGroup: CueGroup?, modifier: Modifier = Modifier, @@ -196,7 +195,6 @@ fun SubtitleView( * * @return A [Pair] of [Int] values representing the screen width and height in dp. */ -@SuppressLint("ConfigurationScreenWidthHeight") @Composable private fun getScreenDimensions(): Pair { val configuration = LocalConfiguration.current From 92d0c97569646b00433908c383f9be09c3c298bc Mon Sep 17 00:00:00 2001 From: mzhsy1 Date: Fri, 10 Oct 2025 14:03:51 +0800 Subject: [PATCH 3/5] fix psg subtitle offset --- .../compose/material3/PlayerSubtitleView.kt | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt index ca4c0e2ea6c..7a2b06e0f2d 100644 --- a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt +++ b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt @@ -15,9 +15,10 @@ * limitations under the License. */ -package androidx.media3.ui.compose.material3 +package org.mz.mzdkplayer.tool +import android.annotation.SuppressLint import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -46,8 +47,10 @@ import androidx.compose.ui.zIndex import androidx.media3.common.text.Cue import androidx.media3.common.text.CueGroup +import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi + /** * A Material3 composable that renders subtitles provided by a [CueGroup] from Media3. * @@ -153,24 +156,25 @@ fun SubtitleView( } else { bitmap.height.toFloat() } - + val x = cue.position + val y = cue.line val offsetX = when (cue.positionAnchor) { - Cue.ANCHOR_TYPE_START -> screenWidthDp * cue.position - Cue.ANCHOR_TYPE_MIDDLE -> (screenWidthDp * cue.position) - (bitmapWidth / 2) - Cue.ANCHOR_TYPE_END -> (screenWidthDp * cue.position) - bitmapWidth - else -> screenWidthDp * cue.position + Cue.ANCHOR_TYPE_START -> screenWidthDp * x + Cue.ANCHOR_TYPE_MIDDLE -> (screenWidthDp * x) - (bitmapWidth / 2) + Cue.ANCHOR_TYPE_END -> (screenWidthDp * x) - bitmapWidth + else -> screenWidthDp * x } val offsetY = when (cue.lineAnchor) { - Cue.ANCHOR_TYPE_START -> screenHeightDp * cue.line - Cue.ANCHOR_TYPE_MIDDLE -> (screenHeightDp * cue.line) - (bitmapHeight / 2) - Cue.ANCHOR_TYPE_END -> (screenHeightDp * cue.line) - bitmapHeight - else -> screenHeightDp * cue.line + Cue.ANCHOR_TYPE_START -> screenHeightDp * y + Cue.ANCHOR_TYPE_MIDDLE -> (screenHeightDp * y) - (bitmapHeight / 2) + Cue.ANCHOR_TYPE_END -> (screenHeightDp * y) - bitmapHeight + else -> screenHeightDp * y } Box( modifier = Modifier - .offset(x = offsetX.dp , y = offsetY.dp) + .offset(x = offsetX.dp-14.dp , y = offsetY.dp-8.dp) // Adjust the offset as needed .fillMaxSize() .zIndex(cue.zIndex.toFloat()) ) { @@ -195,6 +199,7 @@ fun SubtitleView( * * @return A [Pair] of [Int] values representing the screen width and height in dp. */ + @Composable private fun getScreenDimensions(): Pair { val configuration = LocalConfiguration.current From 841722d5c90e335793498477d421399b0ba7d503 Mon Sep 17 00:00:00 2001 From: mzhsy1 Date: Fri, 10 Oct 2025 15:50:22 +0800 Subject: [PATCH 4/5] Fixes the accurate calculation of the app-perceived screen dimensions in dp across devices with different DPIs (e.g., phones at 320 dpi, TVs at 240 dpi), ensuring PSG subtitles are correctly positioned. --- .../ui/compose/material3/PlayerSubtitleView.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt index 7a2b06e0f2d..64810fa3fef 100644 --- a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt +++ b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt @@ -15,7 +15,7 @@ * limitations under the License. */ -package org.mz.mzdkplayer.tool +package androidx.media3.ui.compose.material3 import android.annotation.SuppressLint @@ -49,7 +49,8 @@ import androidx.media3.common.text.Cue import androidx.media3.common.text.CueGroup import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi - +import android.util.DisplayMetrics +import androidx.compose.ui.platform.LocalContext /** * A Material3 composable that renders subtitles provided by a [CueGroup] from Media3. @@ -196,12 +197,16 @@ fun SubtitleView( /** * Returns the current screen dimensions in dp. - * + *Fixes the accurate calculation of the app-perceived screen dimensions in dp across devices with different DPIs + * (e.g., phones at 320 dpi, TVs at 240 dpi), ensuring PSG subtitles are correctly positioned. * @return A [Pair] of [Int] values representing the screen width and height in dp. */ @Composable private fun getScreenDimensions(): Pair { - val configuration = LocalConfiguration.current - return Pair(configuration.screenWidthDp, configuration.screenHeightDp) + val context = LocalContext.current + val displayMetrics: DisplayMetrics = context.resources.displayMetrics + val widthDp = (displayMetrics.widthPixels / displayMetrics.density).toInt() + val heightDp = (displayMetrics.heightPixels / displayMetrics.density).toInt() + return Pair(widthDp, heightDp) } \ No newline at end of file From 4124891e62e1436eb5b79f7ccfa428cca2c9cb37 Mon Sep 17 00:00:00 2001 From: mzhsy1 Date: Fri, 10 Oct 2025 15:53:02 +0800 Subject: [PATCH 5/5] clean unless import --- .../androidx/media3/ui/compose/material3/PlayerSubtitleView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt index 64810fa3fef..54b64f687dc 100644 --- a/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt +++ b/libraries/ui_compose_material3/src/main/java/androidx/media3/ui/compose/material3/PlayerSubtitleView.kt @@ -18,7 +18,7 @@ package androidx.media3.ui.compose.material3 -import android.annotation.SuppressLint + import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement