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