Skip to content

Commit

Permalink
[Transformer][Sample] Transformer video composition (#144)
Browse files Browse the repository at this point in the history
* This CL contains sample code that demonstrates for Media3 Transformer API

* Removed unused imports

* Add method descriptions

* Remove unused permissions

* Update samples/media/video/src/main/java/com/example/platform/media/video/TransformerVideoComposition.kt

---------

Co-authored-by: Yacine Rezgui <rezgui.y@gmail.com>
  • Loading branch information
droid-girl and yrezgui authored Feb 7, 2024
1 parent 0d83e86 commit 3e09145
Show file tree
Hide file tree
Showing 7 changed files with 467 additions and 2 deletions.
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ androidx-datastore-preferences = { module = "androidx.datastore:datastore-prefer
androidx-draganddrop = "androidx.draganddrop:draganddrop:1.0.0"
androidx-dynamicanimation = "androidx.dynamicanimation:dynamicanimation-ktx:1.0.0-alpha03"
androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" }
androidx-media3-effect = { module = "androidx.media3:media3-effect", version.ref = "media3" }
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
androidx-media3-transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" }
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3" }

fresco = "com.facebook.fresco:fresco:3.0.0"
Expand Down
6 changes: 4 additions & 2 deletions samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,11 @@ buildSpannedString is useful for quickly building a rich text.
- [UltraHDR Image Capture](camera/camera2/src/main/java/com/example/platform/camera/imagecapture/Camera2UltraHDRCapture.kt):
This sample demonstrates how to capture a 10-bit compressed still image and
- [UltraHDR to HDR Video](media/ultrahdr/src/main/java/com/example/platform/media/ultrahdr/video/UltraHDRToHDRVideo.kt):
This sample demonstrates converting a series of UltraHDR images into a HDR
This sample demonstrates converting a series of UltraHDR images into a HDR
- [UltraHDR x OpenGLES SurfaceView](graphics/ultrahdr/src/main/java/com/example/platform/graphics/ultrahdr/opengl/UltraHDRWithOpenGL.kt):
This sample demonstrates displaying an UltraHDR image via and OpenGL Pipeline
This sample demonstrates displaying an UltraHDR image via and OpenGL Pipeline
- [Video Composition using Media3 Transformer](media/video/src/main/java/com/example/platform/media/video/TransformerVideoComposition.kt):
This sample demonstrates concatenation of two video assets using Media3
- [Visualizing an UltraHDR Gainmap](graphics/ultrahdr/src/main/java/com/example/platform/graphics/ultrahdr/display/VisualizingAnUltraHDRGainmap.kt):
This sample demonstrates visualizing the underlying gainmap of an UltraHDR
- [WindowInsetsAnimation](user-interface/window-insets/src/main/java/com/example/platform/ui/insets/WindowInsetsAnimation.kt):
Expand Down
40 changes: 40 additions & 0 deletions samples/media/video/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2023 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.
*/

plugins {
id("com.example.platform.sample")
}

android {
namespace = "com.example.platform.media.video"
viewBinding.isEnabled = true
}

dependencies {
// Media3 Common
implementation(libs.androidx.media3.common)

// Media3 Transformer
implementation(libs.androidx.media3.transformer)

// Media3 ExoPlayer
implementation(libs.androidx.media3.exoplayer)

// Media3 Ui
implementation(libs.androidx.media3.ui)
implementation(libs.androidx.media3.effect)
implementation(libs.material)
}
21 changes: 21 additions & 0 deletions samples/media/video/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 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.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
/*
* Copyright 2023 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 com.example.platform.media.video

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.Effect
import androidx.media3.common.MediaItem
import androidx.media3.common.util.UnstableApi
import androidx.media3.effect.RgbFilter
import androidx.media3.effect.ScaleAndRotateTransformation
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.transformer.Composition
import androidx.media3.transformer.EditedMediaItem
import androidx.media3.transformer.EditedMediaItemSequence
import androidx.media3.transformer.Effects
import androidx.media3.transformer.ExportException
import androidx.media3.transformer.ExportResult
import androidx.media3.transformer.ProgressHolder
import androidx.media3.transformer.Transformer
import com.example.platform.media.video.databinding.TransformerCompositionLayoutBinding
import com.google.android.catalog.framework.annotations.Sample
import com.google.common.base.Stopwatch
import com.google.common.base.Ticker
import com.google.common.collect.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.util.concurrent.TimeUnit

@UnstableApi
@Sample(
name = "Video Composition using Media3 Transformer",
description = "This sample demonstrates concatenation of two video assets using Media3 " +
"Transformer library.",
documentation = "https://developer.android.com/guide/topics/media/transformer",
tags = ["Transformer"],
)
class TransformerVideoComposition : Fragment() {
/**
* Android ViewBinding.
*/
private var _binding: TransformerCompositionLayoutBinding? = null
private val binding get() = _binding!!

/**
* cache file used to save the output result of the transcoding operation.
*/
private var externalCacheFile: File? = null
/**
* [ExoPlayer], used to playback the output of the transcoding operation.
*/
private var player: ExoPlayer? = null
/**
* [Stopwatch], used to track the progress of the transcoding operation.
*/
private var exportStopwatch: Stopwatch? = null

/**
* [Transformer.Listener] receives callbacks for export events.
*/
private val transformerListener: Transformer.Listener =
object : Transformer.Listener {
override fun onCompleted(composition: Composition, result: ExportResult) {
Log.i(TAG, "Transformation is completed")
exportStopwatch?.stop()
playOutput()
binding.exportButton.isEnabled = true
}

override fun onError(
composition: Composition, result: ExportResult,
exception: ExportException,
) {
exportStopwatch?.stop()
Log.i(TAG, "Error during transformation:" + exception.errorCodeName)
binding.exportButton.isEnabled = true
}
}

/**
* Plays export output in [ExoPlayer].
*/
private fun playOutput() {
Log.i(TAG, "Initiate playback using ExoPlayer.")
lifecycleScope.launch { playbackUsingExoPlayer() }
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = TransformerCompositionLayoutBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.exportButton.setOnClickListener {
binding.exportButton.isEnabled = false
exportComposition()
}

exportStopwatch = Stopwatch.createUnstarted(
object : Ticker() {
override fun read(): Long {
return SystemClock.elapsedRealtimeNanos()
}
},
)
try {
externalCacheFile = createExternalCacheFile("transformer-output.mp4")
} catch (e: IOException) {
throw IllegalStateException(e)
}
}

override fun onPause() {
super.onPause()
releasePlayer()
}

override fun onStop() {
super.onStop()
releasePlayer()
}

/**
* Builds a [Composition] that contains 1 [EditedMediaItemSequence] with 2
* video assets, and optionally an audio sequence with one audio track.
*/
private fun createComposition(): Composition {
val video1 = EditedMediaItem.Builder(
// apply effects only on the first item
MediaItem.fromUri(URI_ITEM1))
.setEffects(getSelectedEffects())
.build()
val video2 = EditedMediaItem.Builder(
MediaItem.fromUri(URI_ITEM2))
.build()
val compositionSequences = ArrayList<EditedMediaItemSequence>()
val videoSequence = EditedMediaItemSequence(ImmutableList.of(video1, video2))
compositionSequences.add(videoSequence)

if (binding.backgroundAudioChip.isChecked) {
val backgroundAudio = EditedMediaItem.Builder(MediaItem.fromUri(URI_AUDIO)).build()
// create an audio sequence that will be looping over the duration of the first video
// sequence.
val audioSequence = EditedMediaItemSequence(
ImmutableList.of(backgroundAudio),
/* isLooping*/true
)
compositionSequences.add(audioSequence)
}

return Composition.Builder(compositionSequences).build()
}

/**
* Creates an external cache file that will be used to save the [Composition] output.
*/
@Throws(IOException::class)
private fun createExternalCacheFile(fileName: String): File {
val file = File(requireActivity().externalCacheDir, fileName)
check(!(file.exists() && !file.delete())) { "Could not delete the previous export output file" }
check(file.createNewFile()) { "Could not create the export output file" }
return file
}

/**
* Sets up [Transformer] and [Composition] and starts the transcoding operation.
* [Transformer] internal processing is done on separate thread.
*/
private fun exportComposition() {
val composition = createComposition()
// set up a Transformer instance and add a callback listener.
val transformer = Transformer.Builder(requireContext())
.addListener(transformerListener)
.build()
val filePath: String = externalCacheFile!!.getAbsolutePath()
transformer.start(composition, filePath)
startTimer(transformer)
}

/**
* Gets a list of [Effects].
*/
private fun getSelectedEffects(): Effects {
val selectedEffects = ArrayList<Effect>()
if (binding.grayscaleChip.isChecked) {
selectedEffects.add(RgbFilter.createGrayscaleFilter())
}
if (binding.scaleChip.isChecked) {
selectedEffects.add(ScaleAndRotateTransformation.Builder()
.setScale(.2f, .2f)
.build())
}
return Effects(/* audioProcessors= */ listOf(),
/* videoEffects= */ selectedEffects)
}

/**
* Sets up an [ExoPlayer] instance to playback the output cache file.
*/
private suspend fun playbackUsingExoPlayer() = withContext(Dispatchers.Main) {
binding.mediaPlayer.useController = true

val player = ExoPlayer.Builder(requireContext()).build()
player.setMediaItem(MediaItem.fromUri("file://" + externalCacheFile!!.absolutePath))
player.prepare()

// Attaching player to player view
binding.mediaPlayer.player = player

// Play back video
player.play()
}

/**
* Releases an [ExoPlayer] instance and resets the [Stopwatch].
*/
private fun releasePlayer() {
exportStopwatch!!.reset()
binding.mediaPlayer.player?.stop()
player?.release()
player = null
}

/**
* Sets up a timer and [Handler] to handle progress updates from the transcoding operation.
*/
private fun startTimer(transformer: Transformer) {
exportStopwatch?.reset()
exportStopwatch?.start()
val mainHandler = Handler(Looper.getMainLooper())
val progressHolder = ProgressHolder()
mainHandler.post(
object : Runnable {
override fun run() {
if (transformer.getProgress(progressHolder) != Transformer.PROGRESS_STATE_NOT_STARTED) {
binding.exportProgressText.text = getString(
R.string.export_timer,
exportStopwatch!!.elapsed(
TimeUnit.SECONDS,
),
)
mainHandler.postDelayed(this, 1000)
}
}
},
)
}

companion object {
/**
* Class Tag
*/
private val TAG = TransformerVideoComposition::class.java.simpleName
/**
* Video and audio assets
*/
private const val URI_ITEM1 =
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4"
private const val URI_ITEM2 =
"https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4"
private const val URI_AUDIO =
"https://storage.googleapis.com/exoplayer-test-media-0/play.mp3"
}
}
Loading

0 comments on commit 3e09145

Please sign in to comment.