1 /* 2 * Copyright 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.graphics.filters 18 19 import android.Manifest.permission.READ_EXTERNAL_STORAGE 20 import android.app.Activity 21 import android.content.pm.PackageManager 22 import android.net.Uri 23 import android.os.Bundle 24 import android.os.Handler 25 import android.view.Gravity 26 import android.view.View 27 import android.view.ViewGroup 28 import android.view.ViewGroup.LayoutParams.MATCH_PARENT 29 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT 30 import android.widget.Button 31 import android.widget.FrameLayout 32 import android.widget.LinearLayout 33 import android.widget.ScrollView 34 import android.widget.TextView 35 import androidx.media3.common.Effect 36 import androidx.media3.common.MediaItem 37 import androidx.media3.common.MediaLibraryInfo.TAG 38 import androidx.media3.common.util.Log 39 import androidx.media3.common.util.Util 40 import androidx.media3.exoplayer.ExoPlayer 41 import androidx.media3.transformer.DefaultEncoderFactory 42 import androidx.media3.transformer.ProgressHolder 43 import androidx.media3.transformer.TransformationException 44 import androidx.media3.transformer.TransformationRequest 45 import androidx.media3.transformer.TransformationResult 46 import androidx.media3.transformer.Transformer 47 import androidx.media3.ui.PlayerView 48 import com.google.common.collect.ImmutableList 49 import java.io.File 50 import java.io.IOException 51 52 private val PRESET_FILE_URIS = 53 arrayOf( 54 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4", 55 "https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4", 56 "https://html5demos.com/assets/dizzy.mp4", 57 "https://html5demos.com/assets/dizzy.webm", 58 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4", 59 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/8k24fps_4s.mp4", 60 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/1920w_1080h_4s.mp4", 61 "https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4", 62 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_avc_aac.mp4", 63 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4", 64 "https://storage.googleapis.com/exoplayer-test-media-internal-63834241aced7884c2544af1a" + 65 "3452e01/mp4/slow%20motion/slowMotion_countdown_120fps.mp4", 66 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/" + 67 "slowMotion_stopwatch_240fps_long.mp4", 68 "https://storage.googleapis.com/exoplayer-test-media-1/gen/screens/dash-vod-single-segment/" + 69 "manifest-baseline.mpd", 70 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-s21-hdr-hdr10.mp4", 71 "https://storage.googleapis.com/exoplayer-test-media-1/mp4/Pixel7Pro_HLG_1080P.mp4", 72 "https://storage.googleapis.com/exoplayer-test-media-internal-63834241aced7884c2544af1a3452" + 73 "e01/mp4/sony-hdr-hlg-full-range.mp4" 74 ) 75 76 class TestFiltersActivity : Activity() { 77 78 private var outputFile: File? = null 79 private var transformer: Transformer? = null 80 private var sourcePlayer: ExoPlayer? = null 81 private var filteredPlayer: ExoPlayer? = null 82 private var sourcePlayerView: PlayerView? = null 83 private var filteredPlayerView: PlayerView? = null 84 private var filterButton: Button? = null 85 private var statusBar: TextView? = null 86 onCreatenull87 override fun onCreate(savedInstanceState: Bundle?) { 88 super.onCreate(savedInstanceState) 89 90 sourcePlayerView = PlayerView(this@TestFiltersActivity) 91 sourcePlayerView!!.minimumHeight = 480 92 sourcePlayerView!!.minimumWidth = 640 93 sourcePlayerView!!.layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) 94 filteredPlayerView = PlayerView(this@TestFiltersActivity) 95 filteredPlayerView!!.minimumHeight = 480 96 filteredPlayerView!!.minimumWidth = 640 97 filteredPlayerView!!.layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) 98 99 statusBar = TextView(this) 100 101 setContentView( 102 FrameLayout(this).apply { 103 layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) 104 105 addView( 106 ScrollView(this@TestFiltersActivity).apply { 107 layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT) 108 addView( 109 LinearLayout(this@TestFiltersActivity).apply { 110 layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) 111 orientation = LinearLayout.VERTICAL 112 addView( 113 TextView(this@TestFiltersActivity).apply { 114 layoutParams = 115 LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) 116 .apply { gravity = Gravity.LEFT } 117 text = "Source Video" 118 } 119 ) 120 addView(sourcePlayerView) 121 addView( 122 TextView(this@TestFiltersActivity).apply { 123 layoutParams = 124 LinearLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) 125 .apply { gravity = Gravity.LEFT } 126 text = "Filtered Video" 127 } 128 ) 129 addView(filteredPlayerView) 130 addView(statusBar) 131 addView(createControls()) 132 } 133 ) 134 } 135 ) 136 } 137 ) 138 } 139 createControlsnull140 private fun createControls(): View { 141 this.filterButton = Button(this) 142 this.filterButton!!.text = "Run Filter" 143 this.filterButton!!.setOnClickListener( 144 View.OnClickListener { this@TestFiltersActivity.startTransformation() } 145 ) 146 147 val controls = 148 LinearLayout(this).apply { 149 layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT) 150 orientation = LinearLayout.HORIZONTAL 151 152 addView(this@TestFiltersActivity.filterButton) 153 } 154 155 return controls 156 } 157 startTransformationnull158 private fun startTransformation() { 159 statusBar!!.text = "Request permissions" 160 requestPermission() 161 statusBar!!.text = "Setup transformation" 162 163 val mediaUri = Uri.parse(PRESET_FILE_URIS[0]) 164 try { 165 outputFile = createExternalCacheFile("filters-output.mp4") 166 val outputFilePath: String = outputFile!!.getAbsolutePath() 167 val mediaItem: MediaItem = createMediaItem(mediaUri) 168 var transformer: Transformer = createTransformer(outputFilePath) 169 transformer.startTransformation(mediaItem, outputFilePath) 170 this.transformer = transformer 171 } catch (e: IOException) { 172 throw IllegalStateException(e) 173 } 174 val mainHandler = Handler(mainLooper) 175 val progressHolder = ProgressHolder() 176 mainHandler.post( 177 object : Runnable { 178 override fun run() { 179 if ( 180 transformer?.getProgress(progressHolder) != 181 Transformer.PROGRESS_STATE_NO_TRANSFORMATION 182 ) { 183 mainHandler.postDelayed(/* r= */ this, /* delayMillis= */ 500) 184 } 185 } 186 } 187 ) 188 } 189 createTransformernull190 private fun createTransformer(filePath: String): Transformer { 191 val transformerBuilder = Transformer.Builder(/* context= */ this) 192 val effects: List<Effect> = createEffectsList() 193 194 val requestBuilder = TransformationRequest.Builder() 195 transformerBuilder 196 .setTransformationRequest(requestBuilder.build()) 197 .setEncoderFactory( 198 DefaultEncoderFactory.Builder(this.applicationContext) 199 .setEnableFallback(false) 200 .build() 201 ) 202 transformerBuilder.setVideoEffects(effects) 203 204 return transformerBuilder 205 .addListener( 206 object : Transformer.Listener { 207 override fun onTransformationCompleted( 208 mediaItem: MediaItem, 209 transformationResult: TransformationResult 210 ) { 211 this@TestFiltersActivity.onTransformationCompleted(filePath, mediaItem) 212 } 213 214 override fun onTransformationError( 215 mediaItem: MediaItem, 216 exception: TransformationException 217 ) { 218 this@TestFiltersActivity.onTransformationError(exception) 219 } 220 } 221 ) 222 .build() 223 } 224 onTransformationErrornull225 private fun onTransformationError(exception: TransformationException) { 226 statusBar!!.text = "Transformation error: " + exception.message 227 Log.e(TAG, "Transformation error", exception) 228 } 229 onTransformationCompletednull230 private fun onTransformationCompleted(filePath: String, inputMediaItem: MediaItem?) { 231 statusBar!!.text = "Transformation success!" 232 Log.d(TAG, "Output file path: file://$filePath") 233 playMediaItems(inputMediaItem, MediaItem.fromUri("file://" + filePath)) 234 } 235 playMediaItemsnull236 private fun playMediaItems(inputMediaItem: MediaItem?, outputMediaItem: MediaItem) { 237 sourcePlayerView!!.player = null 238 filteredPlayerView!!.player = null 239 240 releasePlayer() 241 var sourcePlayer = ExoPlayer.Builder(/* context= */ this).build() 242 sourcePlayerView!!.player = sourcePlayer 243 sourcePlayerView!!.controllerAutoShow = false 244 if (inputMediaItem != null) { 245 sourcePlayer.setMediaItem(inputMediaItem) 246 } 247 sourcePlayer.prepare() 248 this.sourcePlayer = sourcePlayer 249 sourcePlayer.volume = 0f 250 var filteredPlayer = ExoPlayer.Builder(/* context= */ this).build() 251 filteredPlayerView!!.player = filteredPlayer 252 filteredPlayerView!!.controllerAutoShow = false 253 filteredPlayer.setMediaItem(outputMediaItem) 254 filteredPlayer.prepare() 255 this.filteredPlayer = filteredPlayer 256 sourcePlayer.play() 257 filteredPlayer.play() 258 } 259 releasePlayernull260 private fun releasePlayer() { 261 if (sourcePlayer != null) { 262 sourcePlayer!!.release() 263 sourcePlayer = null 264 } 265 if (filteredPlayer != null) { 266 filteredPlayer!!.release() 267 filteredPlayer = null 268 } 269 } 270 createEffectsListnull271 private fun createEffectsList(): List<Effect> { 272 val effects = ImmutableList.Builder<Effect>() 273 274 effects.add(Vignette(0.5f, 0.75f)) 275 276 return effects.build() 277 } 278 requestPermissionnull279 private fun requestPermission() { 280 if (Util.SDK_INT < 23) { 281 return 282 } 283 284 if (checkSelfPermission(READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 285 requestPermissions(Array<String>(1) { READ_EXTERNAL_STORAGE }, /* requestCode= */ 0) 286 } 287 } 288 289 @Throws(IOException::class) createExternalCacheFilenull290 private fun createExternalCacheFile(fileName: String): File? { 291 val file = File(externalCacheDir, fileName) 292 check(!(file.exists() && !file.delete())) { 293 "Could not delete the previous transformer output file" 294 } 295 check(file.createNewFile()) { "Could not create the transformer output file" } 296 return file 297 } 298 createMediaItemnull299 private fun createMediaItem(uri: Uri): MediaItem { 300 val mediaItemBuilder = MediaItem.Builder().setUri(uri) 301 return mediaItemBuilder.build() 302 } 303 } 304