In this article, we will write text on the video programmatically using Media3 Transformer APIs.
We will use Jetpack Compose for this and upcoming articles.
We will start with adding dependencies of Transformer APIs for adding text overlay over video. We will also add dependencies of ExoPlayer API to preview the changes we are making.
implementation("androidx.media3:media3-transformer:1.4.1")
implementation("androidx.media3:media3-effect:1.4.1")
implementation("androidx.media3:media3-common:1.4.1")
implementation("androidx.media3:media3-muxer:1.4.1")
implementation("androidx.media3:media3-exoplayer:1.4.1")
implementation("androidx.media3:media3-ui:1.4.1")
Now we will add code for picking up a video file from the gallery. This used to be such a lengthy and complex thing back in those days 😅
val videoUri = remember { mutableStateOf<Uri?>(null) }
val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
videoUri.value = result.data?.data
}
}
// UI
Button(
onClick = {
val intent = Intent(Intent.ACTION_PICK).apply {
type = "video/*"
}
launcher.launch(intent)
}) {
Text("Pick Video")
}
Now we will display the video preview to the user, so they can check the original video and compare it with the result once generated. Here is the composable function for it.
@Composable
fun PlayVideoFromUri(modifier: Modifier, videoUri: Uri?) {
if (videoUri == null) {
Box(
modifier = modifier
.height(200.dp)
.fillMaxWidth()
) {
}
} else {
val context = LocalContext.current
val player = ExoPlayer.Builder(context).build()
val mediaItem = MediaItem.Builder().setUri(videoUri).build()
player.setMediaItem(mediaItem)
player.prepare()
DisposableEffect(Unit) {
onDispose {
player.release()
}
}
AndroidView(
modifier = modifier
.height(200.dp)
.fillMaxWidth(),
factory = {
PlayerView(context).apply {
this.player = player
}
}
)
}
}
Now we will add a text input field for adding text to video. Below is the composable function for that.
@OptIn(UnstableApi::class)
@Composable
fun PlayVideoFromUriWithText(modifier: Modifier, videoUri: Uri?, text: String) {
val context = LocalContext.current
if (videoUri == null) {
Box(
modifier = modifier
.height(200.dp)
.fillMaxWidth()
) {
}
} else {
val mediaItem = MediaItem.Builder()
.setUri(videoUri)
.build()
val textEffect = TextOverlay.createStaticTextOverlay(
SpannableString(text),
)
val player = ExoPlayer.Builder(context)
.build()
.also { exoPlayer ->
exoPlayer.setMediaItem(mediaItem)
exoPlayer.setVideoEffects(listOf(OverlayEffect(ImmutableList.of(textEffect))))
exoPlayer.prepare()
}
DisposableEffect(Unit) {
onDispose {
player.release()
}
}
AndroidView(
modifier = modifier
.height(200.dp)
.fillMaxWidth(),
factory = {
PlayerView(context).apply {
this.player = player
}
}
)
}
}
Finally, the code to export the result video to a file on the disk.
private fun exportResultVideo(
videoUri: MutableState<Uri?>,
text: MutableState<String>,
context: Context,
transformer: Transformer,
onCompleted: () -> Unit,
onError: (ExportException) -> Unit
) {
val mediaItem = MediaItem.Builder()
.setUri(videoUri.value)
.build()
val textEffect = TextOverlay.createStaticTextOverlay(
SpannableString(text.value),
)
val effects = Effects(
listOf(),
listOf(OverlayEffect(ImmutableList.of(textEffect))),
)
val editedMediaItem = EditedMediaItem.Builder(mediaItem)
.setEffects(effects)
.build()
val outputFile = File(context.filesDir, "test.mp4")
transformer.start(editedMediaItem, outputFile.absolutePath)
transformer.addListener(object : Transformer.Listener {
override fun onCompleted(composition: Composition, exportResult: ExportResult) {
onCompleted()
}
override fun onError(
composition: Composition,
exportResult: ExportResult,
exportException: ExportException
) {
onError(exportException)
}
})
}
One last thing, if you want to get real-time progress of the export result, you can get it in the following way.
val progress = remember { mutableIntStateOf(0) }
val mainHandler = Handler(getMainLooper())
val progressHolder = ProgressHolder()
mainHandler.post(
object : Runnable {
override fun run() {
if (transformer.getProgress(progressHolder) != PROGRESS_STATE_NOT_STARTED) {
// this will be 0 to 100
progress.intValue = progressHolder.progress
mainHandler.postDelayed(this, 500)
}
}
})
The final result will look like this.
You can find the complete code here: https://github.com/dakshbhatt21/a-computer-engineer















