1 /*
2  * Copyright (C) 2020 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 android.media.mediatranscoding.cts;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertNull;
23 import static org.junit.Assert.assertThrows;
24 import static org.junit.Assert.assertTrue;
25 
26 import android.content.ContentResolver;
27 import android.content.Context;
28 import android.content.pm.PackageManager;
29 import android.content.res.AssetFileDescriptor;
30 import android.media.ApplicationMediaCapabilities;
31 import android.media.MediaCodec;
32 import android.media.MediaCodecInfo;
33 import android.media.MediaExtractor;
34 import android.media.MediaFormat;
35 import android.media.MediaTranscodingManager;
36 import android.media.MediaTranscodingManager.TranscodingRequest;
37 import android.media.MediaTranscodingManager.TranscodingSession;
38 import android.media.MediaTranscodingManager.VideoTranscodingRequest;
39 import android.net.Uri;
40 import android.os.Bundle;
41 import android.os.FileUtils;
42 import android.os.ParcelFileDescriptor;
43 import android.os.SystemProperties;
44 import android.platform.test.annotations.AppModeFull;
45 import android.platform.test.annotations.RequiresDevice;
46 import android.util.Log;
47 
48 import androidx.test.filters.SdkSuppress;
49 import androidx.test.platform.app.InstrumentationRegistry;
50 import androidx.test.runner.AndroidJUnit4;
51 
52 import com.android.compatibility.common.util.ModuleSpecificTest;
53 
54 import org.junit.After;
55 import org.junit.Assume;
56 import org.junit.Before;
57 import org.junit.Test;
58 import org.junit.runner.RunWith;
59 
60 import java.io.File;
61 import java.io.FileInputStream;
62 import java.io.FileOutputStream;
63 import java.io.IOException;
64 import java.io.InputStream;
65 import java.io.OutputStream;
66 import java.util.List;
67 import java.util.concurrent.CountDownLatch;
68 import java.util.concurrent.Executor;
69 import java.util.concurrent.Executors;
70 import java.util.concurrent.Semaphore;
71 import java.util.concurrent.TimeUnit;
72 import java.util.concurrent.atomic.AtomicInteger;
73 
74 @RequiresDevice
75 @AppModeFull(reason = "Instant apps cannot access the SD card")
76 @ModuleSpecificTest
77 @SdkSuppress(minSdkVersion = 31, codeName = "S")
78 @RunWith(AndroidJUnit4.class)
79 public class MediaTranscodingManagerTest {
80     private static final String TAG = "MediaTranscodingManagerTest";
81     /** The time to wait for the transcode operation to complete before failing the test. */
82     private static final int TRANSCODE_TIMEOUT_SECONDS = 10;
83     /** Copy the transcoded video to /storage/emulated/0/Download/ */
84     private static final boolean DEBUG_TRANSCODED_VIDEO = false;
85     /** Dump both source yuv and transcode YUV to /storage/emulated/0/Download/ */
86     private static final boolean DEBUG_YUV = false;
87 
88     private Context mContext;
89     private ContentResolver mContentResolver;
90     private MediaTranscodingManager mMediaTranscodingManager = null;
91     private Uri mSourceHEVCVideoUri = null;
92     private Uri mSourceAVCVideoUri = null;
93     private Uri mDestinationUri = null;
94 
95     // Default setting for transcoding to H.264.
96     private static final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
97     private static final int BIT_RATE = 4000000;            // 4Mbps
98     private static final int WIDTH = 720;
99     private static final int HEIGHT = 480;
100     private static final int FRAME_RATE = 30;
101     private static final int INT_NOT_SET = Integer.MIN_VALUE;
102 
103     // Threshold for the psnr to make sure the transcoded video is valid.
104     private static final int PSNR_THRESHOLD = 20;
105 
106     // Copy the resource to cache.
resourceToUri(int resId, String name)107     private Uri resourceToUri(int resId, String name) throws IOException {
108         Uri cacheUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
109                 + mContext.getCacheDir().getAbsolutePath() + "/" + name);
110 
111         InputStream is = mContext.getResources().openRawResource(resId);
112         OutputStream os = mContext.getContentResolver().openOutputStream(cacheUri);
113 
114         FileUtils.copy(is, os);
115         return cacheUri;
116     }
117 
generateNewUri(Context context, String filename)118     private static Uri generateNewUri(Context context, String filename) {
119         File outFile = new File(context.getExternalCacheDir(), filename);
120         return Uri.fromFile(outFile);
121     }
122 
123     // Generates an invalid uri which will let the service return transcoding failure.
generateInvalidTranscodingUri(Context context)124     private static Uri generateInvalidTranscodingUri(Context context) {
125         File outFile = new File(context.getExternalCacheDir(), "InvalidUri.mp4");
126         return Uri.fromFile(outFile);
127     }
128 
129     /**
130      * Creates a MediaFormat with the default settings.
131      */
createDefaultMediaFormat()132     private static MediaFormat createDefaultMediaFormat() {
133         return createMediaFormat(MIME_TYPE, WIDTH, HEIGHT, INT_NOT_SET /* frameRate */,
134                 BIT_RATE /* bitrate */);
135     }
136 
137     /**
138      * Creates a MediaFormat with custom settings.
139      */
createMediaFormat(String mime, int width, int height, int frameRate, int bitrate)140     private static MediaFormat createMediaFormat(String mime, int width, int height, int frameRate,
141             int bitrate) {
142         MediaFormat format = new MediaFormat();
143         if (mime != null) {
144             format.setString(MediaFormat.KEY_MIME, mime);
145         }
146         if (width != INT_NOT_SET) {
147             format.setInteger(MediaFormat.KEY_WIDTH, width);
148         }
149         if (height != INT_NOT_SET) {
150             format.setInteger(MediaFormat.KEY_HEIGHT, height);
151         }
152         if (frameRate != INT_NOT_SET) {
153             format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
154         }
155         if (bitrate != INT_NOT_SET) {
156             format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
157         }
158         return format;
159     }
160 
161     @Before
setUp()162     public void setUp() throws Exception {
163         Log.d(TAG, "setUp");
164 
165         Assume.assumeTrue("Media transcoding disabled",
166                 SystemProperties.getBoolean("sys.fuse.transcode_enabled", false));
167 
168         PackageManager pm =
169                 InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager();
170         Assume.assumeFalse("Unsupported device type (TV, Watch, Car)",
171                 pm.hasSystemFeature(pm.FEATURE_LEANBACK)
172                 || pm.hasSystemFeature(pm.FEATURE_WATCH)
173                 || pm.hasSystemFeature(pm.FEATURE_AUTOMOTIVE));
174 
175         mContext = InstrumentationRegistry.getInstrumentation().getContext();
176         mContentResolver = mContext.getContentResolver();
177 
178         InstrumentationRegistry.getInstrumentation().getUiAutomation()
179                 .adoptShellPermissionIdentity("android.permission.WRITE_MEDIA_STORAGE");
180         mMediaTranscodingManager = mContext.getSystemService(MediaTranscodingManager.class);
181         assertNotNull(mMediaTranscodingManager);
182         androidx.test.InstrumentationRegistry.registerInstance(
183                 InstrumentationRegistry.getInstrumentation(), new Bundle());
184 
185         // Setup default source HEVC 480p file uri.
186         mSourceHEVCVideoUri = resourceToUri(R.raw.Video_HEVC_480p_30Frames,
187                 "Video_HEVC_480p_30Frames.mp4");
188 
189         // Setup source AVC file uri.
190         mSourceAVCVideoUri = resourceToUri(R.raw.Video_AVC_30Frames,
191                 "Video_AVC_30Frames.mp4");
192 
193         // Setup destination file.
194         mDestinationUri = generateNewUri(mContext, "transcoded.mp4");
195     }
196 
197     @After
tearDown()198     public void tearDown() throws Exception {
199         InstrumentationRegistry
200                 .getInstrumentation().getUiAutomation().dropShellPermissionIdentity();
201     }
202 
203     /**
204      * Verify that setting null destination uri will throw exception.
205      */
206     @Test
testCreateTranscodingRequestWithNullDestinationUri()207     public void testCreateTranscodingRequestWithNullDestinationUri() throws Exception {
208         assertThrows(IllegalArgumentException.class, () -> {
209             VideoTranscodingRequest request =
210                     new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, null,
211                             createDefaultMediaFormat())
212                             .build();
213         });
214     }
215 
216     /**
217      * Verify that setting invalid pid will throw exception.
218      */
219     @Test
testCreateTranscodingWithInvalidClientPid()220     public void testCreateTranscodingWithInvalidClientPid() throws Exception {
221         assertThrows(IllegalArgumentException.class, () -> {
222             VideoTranscodingRequest request =
223                     new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, mDestinationUri,
224                             createDefaultMediaFormat())
225                             .setClientPid(-1)
226                             .build();
227         });
228     }
229 
230     /**
231      * Verify that setting invalid uid will throw exception.
232      */
233     @Test
testCreateTranscodingWithInvalidClientUid()234     public void testCreateTranscodingWithInvalidClientUid() throws Exception {
235         assertThrows(IllegalArgumentException.class, () -> {
236             VideoTranscodingRequest request =
237                     new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, mDestinationUri,
238                             createDefaultMediaFormat())
239                             .setClientUid(-1)
240                             .build();
241         });
242     }
243 
244     /**
245      * Verify that setting null source uri will throw exception.
246      */
247     @Test
testCreateTranscodingRequestWithNullSourceUri()248     public void testCreateTranscodingRequestWithNullSourceUri() throws Exception {
249         assertThrows(IllegalArgumentException.class, () -> {
250             VideoTranscodingRequest request =
251                     new VideoTranscodingRequest.Builder(null, mDestinationUri,
252                             createDefaultMediaFormat())
253                             .build();
254         });
255     }
256 
257     /**
258      * Verify that not setting source uri will throw exception.
259      */
260     @Test
testCreateTranscodingRequestWithoutSourceUri()261     public void testCreateTranscodingRequestWithoutSourceUri() throws Exception {
262         assertThrows(IllegalArgumentException.class, () -> {
263             VideoTranscodingRequest request =
264                     new VideoTranscodingRequest.Builder(null, mDestinationUri,
265                             createDefaultMediaFormat())
266                             .build();
267         });
268     }
269 
270     /**
271      * Verify that not setting destination uri will throw exception.
272      */
273     @Test
testCreateTranscodingRequestWithoutDestinationUri()274     public void testCreateTranscodingRequestWithoutDestinationUri() throws Exception {
275         assertThrows(IllegalArgumentException.class, () -> {
276             VideoTranscodingRequest request =
277                     new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, null,
278                             createDefaultMediaFormat())
279                             .build();
280         });
281     }
282 
283 
284     /**
285      * Verify that setting video transcoding without setting video format will throw exception.
286      */
287     @Test
testCreateTranscodingRequestWithoutVideoFormat()288     public void testCreateTranscodingRequestWithoutVideoFormat() throws Exception {
289         assertThrows(IllegalArgumentException.class, () -> {
290             VideoTranscodingRequest request =
291                     new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, mDestinationUri, null)
292                             .build();
293         });
294     }
295 
testTranscodingWithExpectResult(Uri srcUri, Uri dstUri, int expectedResult)296     private void testTranscodingWithExpectResult(Uri srcUri, Uri dstUri, int expectedResult)
297             throws Exception {
298         Semaphore transcodeCompleteSemaphore = new Semaphore(0);
299 
300         VideoTranscodingRequest request =
301                 new VideoTranscodingRequest.Builder(srcUri, dstUri, createDefaultMediaFormat())
302                         .build();
303         Executor listenerExecutor = Executors.newSingleThreadExecutor();
304 
305         TranscodingSession session = mMediaTranscodingManager.enqueueRequest(
306                 request,
307                 listenerExecutor,
308                 transcodingSession -> {
309                     Log.d(TAG,
310                             "Transcoding completed with result: " + transcodingSession.getResult());
311                     transcodeCompleteSemaphore.release();
312                     assertEquals(expectedResult, transcodingSession.getResult());
313                 });
314         assertNotNull(session);
315 
316         if (session != null) {
317             Log.d(TAG, "testMediaTranscodingManager - Waiting for transcode to complete.");
318             boolean finishedOnTime = transcodeCompleteSemaphore.tryAcquire(
319                     TRANSCODE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
320             assertTrue("Transcode failed to complete in time.", finishedOnTime);
321         }
322 
323         File dstFile = new File(dstUri.getPath());;
324         if (expectedResult == TranscodingSession.RESULT_SUCCESS) {
325             // Checks the destination file get generated.
326             assertTrue("Failed to create destination file", dstFile.exists());
327         }
328 
329         if (dstFile.exists()) {
330             dstFile.delete();
331         }
332     }
333 
334     // Tests transcoding from invalid file uri and expects failure.
335     @Test
testTranscodingInvalidSrcUri()336     public void testTranscodingInvalidSrcUri() throws Exception {
337         Uri invalidSrcUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
338                 + mContext.getPackageName() + "/source.mp4");
339         // Create a file Uri: android.resource://android.media.cts/temp.mp4
340         Uri destinationUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
341                 + mContext.getPackageName() + "/temp.mp4");
342         Log.d(TAG, "Transcoding " + invalidSrcUri + "to destination: " + destinationUri);
343 
344         testTranscodingWithExpectResult(invalidSrcUri, destinationUri,
345                 TranscodingSession.RESULT_ERROR);
346     }
347 
348     // Tests transcoding to a uri in res folder and expects failure as test could not write to res
349     // folder.
350     @Test
testTranscodingToResFolder()351     public void testTranscodingToResFolder() throws Exception {
352         // Create a file Uri:  android.resource://android.media.cts/temp.mp4
353         Uri destinationUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
354                 + mContext.getPackageName() + "/temp.mp4");
355         Log.d(TAG, "Transcoding to destination: " + destinationUri);
356 
357         testTranscodingWithExpectResult(mSourceHEVCVideoUri, destinationUri,
358                 TranscodingSession.RESULT_ERROR);
359     }
360 
361     // Tests transcoding to a uri in internal cache folder and expects success.
362     @Test
testTranscodingToCacheDir()363     public void testTranscodingToCacheDir() throws Exception {
364         // Create a file Uri: file:///data/user/0/android.media.cts/cache/temp.mp4
365         Uri destinationUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
366                 + mContext.getCacheDir().getAbsolutePath() + "/temp.mp4");
367         Log.d(TAG, "Transcoding to cache: " + destinationUri);
368 
369         testTranscodingWithExpectResult(mSourceHEVCVideoUri, destinationUri,
370                 TranscodingSession.RESULT_SUCCESS);
371     }
372 
373     // Tests transcoding to a uri in internal files directory and expects success.
374     @Test
testTranscodingToInternalFilesDir()375     public void testTranscodingToInternalFilesDir() throws Exception {
376         // Create a file Uri: file:///data/user/0/android.media.cts/files/temp.mp4
377         Uri destinationUri = Uri.fromFile(new File(mContext.getFilesDir(), "temp.mp4"));
378         Log.i(TAG, "Transcoding to files dir: " + destinationUri);
379 
380         testTranscodingWithExpectResult(mSourceHEVCVideoUri, destinationUri,
381                 TranscodingSession.RESULT_SUCCESS);
382     }
383 
384     @Test
testHevcTranscoding720PVideo30FramesWithoutAudio()385     public void testHevcTranscoding720PVideo30FramesWithoutAudio() throws Exception {
386         transcodeFile(resourceToUri(R.raw.Video_HEVC_720p_30Frames,
387                 "Video_HEVC_720p_30Frames.mp4"), false /* testFileDescriptor */);
388     }
389 
390     @Test
testAvcTranscoding1080PVideo30FramesWithoutAudio()391     public void testAvcTranscoding1080PVideo30FramesWithoutAudio() throws Exception {
392         transcodeFile(resourceToUri(R.raw.Video_AVC_30Frames, "Video_AVC_30Frames.mp4"),
393                 false /* testFileDescriptor */);
394     }
395 
396     @Test
testHevcTranscoding1080PVideo30FramesWithoutAudio()397     public void testHevcTranscoding1080PVideo30FramesWithoutAudio() throws Exception {
398         transcodeFile(
399                 resourceToUri(R.raw.Video_HEVC_30Frames, "Video_HEVC_30Frames.mp4"),
400                 false /* testFileDescriptor */);
401     }
402 
403     // Enable this after fixing b/175641397
404     @Test
testHevcTranscoding1080PVideo1FrameWithAudio()405     public void testHevcTranscoding1080PVideo1FrameWithAudio() throws Exception {
406         transcodeFile(resourceToUri(R.raw.Video_HEVC_1Frame_Audio,
407                 "Video_HEVC_1Frame_Audio.mp4"), false /* testFileDescriptor */);
408     }
409 
410     @Test
testHevcTranscoding1080PVideo37FramesWithAudio()411     public void testHevcTranscoding1080PVideo37FramesWithAudio() throws Exception {
412         transcodeFile(resourceToUri(R.raw.Video_HEVC_37Frames_Audio,
413                 "Video_HEVC_37Frames_Audio.mp4"), false /* testFileDescriptor */);
414     }
415 
416     @Test
testHevcTranscoding1080PVideo72FramesWithAudio()417     public void testHevcTranscoding1080PVideo72FramesWithAudio() throws Exception {
418         transcodeFile(resourceToUri(R.raw.Video_HEVC_72Frames_Audio,
419                 "Video_HEVC_72Frames_Audio.mp4"), false /* testFileDescriptor */);
420     }
421 
422     // This test will only run when the device support decoding and encoding 4K video.
423     @Test
testHevcTranscoding4KVideo64FramesWithAudio()424     public void testHevcTranscoding4KVideo64FramesWithAudio() throws Exception {
425         transcodeFile(resourceToUri(R.raw.Video_4K_HEVC_64Frames_Audio,
426                 "Video_4K_HEVC_64Frames_Audio.mp4"), false /* testFileDescriptor */);
427     }
428 
429     @Test
testHevcTranscodingWithFileDescriptor()430     public void testHevcTranscodingWithFileDescriptor() throws Exception {
431         transcodeFile(resourceToUri(R.raw.Video_HEVC_37Frames_Audio,
432                 "Video_HEVC_37Frames_Audio.mp4"), true /* testFileDescriptor */);
433     }
434 
transcodeFile(Uri fileUri, boolean testFileDescriptor)435     private void transcodeFile(Uri fileUri, boolean testFileDescriptor) throws Exception {
436         Semaphore transcodeCompleteSemaphore = new Semaphore(0);
437 
438         // Create a file Uri: file:///data/user/0/android.media.cts/cache/HevcTranscode.mp4
439         Uri destinationUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
440                 + mContext.getCacheDir().getAbsolutePath() + "/HevcTranscode.mp4");
441 
442         ApplicationMediaCapabilities clientCaps =
443                 new ApplicationMediaCapabilities.Builder().build();
444 
445         MediaFormat srcVideoFormat = getVideoTrackFormat(fileUri);
446         assertNotNull(srcVideoFormat);
447 
448         int width = srcVideoFormat.getInteger(MediaFormat.KEY_WIDTH);
449         int height = srcVideoFormat.getInteger(MediaFormat.KEY_HEIGHT);
450 
451         TranscodingRequest.VideoFormatResolver
452                 resolver = new TranscodingRequest.VideoFormatResolver(clientCaps,
453                 MediaFormat.createVideoFormat(
454                         MediaFormat.MIMETYPE_VIDEO_HEVC, width, height));
455         assertTrue(resolver.shouldTranscode());
456         MediaFormat videoTrackFormat = resolver.resolveVideoFormat();
457         assertNotNull(videoTrackFormat);
458 
459         // Return if the source or target video format is not supported
460         if (!isFormatSupported(srcVideoFormat, false)
461                 || !isFormatSupported(videoTrackFormat, true)) {
462             return;
463         }
464 
465         int pid = android.os.Process.myPid();
466         int uid = android.os.Process.myUid();
467 
468         VideoTranscodingRequest.Builder builder =
469                 new VideoTranscodingRequest.Builder(fileUri, destinationUri, videoTrackFormat)
470                         .setClientPid(pid)
471                         .setClientUid(uid);
472 
473         AssetFileDescriptor srcFd = null;
474         AssetFileDescriptor dstFd = null;
475         if (testFileDescriptor) {
476             // Open source Uri.
477             srcFd = mContentResolver.openAssetFileDescriptor(fileUri,
478                     "r");
479             builder.setSourceFileDescriptor(srcFd.getParcelFileDescriptor());
480             // Open destination Uri
481             dstFd = mContentResolver.openAssetFileDescriptor(destinationUri, "rw");
482             builder.setDestinationFileDescriptor(dstFd.getParcelFileDescriptor());
483         }
484         VideoTranscodingRequest request = builder.build();
485         Executor listenerExecutor = Executors.newSingleThreadExecutor();
486         assertEquals(pid, request.getClientPid());
487         assertEquals(uid, request.getClientUid());
488 
489         Log.d(TAG, "transcoding to format: " + videoTrackFormat);
490 
491         TranscodingSession session = mMediaTranscodingManager.enqueueRequest(
492                 request,
493                 listenerExecutor,
494                 transcodingSession -> {
495                     Log.d(TAG,
496                             "Transcoding completed with result: " + transcodingSession.getResult());
497                     assertEquals(TranscodingSession.RESULT_SUCCESS, transcodingSession.getResult());
498                     transcodeCompleteSemaphore.release();
499                 });
500         assertNotNull(session);
501         assertTrue(compareFormat(videoTrackFormat, request.getVideoTrackFormat()));
502         assertEquals(fileUri, request.getSourceUri());
503         assertEquals(destinationUri, request.getDestinationUri());
504         if (testFileDescriptor) {
505             assertEquals(srcFd.getParcelFileDescriptor(), request.getSourceFileDescriptor());
506             assertEquals(dstFd.getParcelFileDescriptor(), request.getDestinationFileDescriptor());
507         }
508 
509         if (session != null) {
510             Log.d(TAG, "testMediaTranscodingManager - Waiting for transcode to cancel.");
511             boolean finishedOnTime = transcodeCompleteSemaphore.tryAcquire(
512                     TRANSCODE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
513             assertTrue("Transcode failed to complete in time.", finishedOnTime);
514         }
515 
516         if (DEBUG_TRANSCODED_VIDEO) {
517             try {
518                 // Add the system time to avoid duplicate that leads to write failure.
519                 String filename =
520                         "transcoded_" + System.nanoTime() + "_" + fileUri.getLastPathSegment();
521                 String path = "/storage/emulated/0/Download/" + filename;
522                 final File file = new File(path);
523                 ParcelFileDescriptor pfd = mContext.getContentResolver().openFileDescriptor(
524                         destinationUri, "r");
525                 FileInputStream fis = new FileInputStream(pfd.getFileDescriptor());
526                 FileOutputStream fos = new FileOutputStream(file);
527                 FileUtils.copy(fis, fos);
528             } catch (IOException e) {
529                 Log.e(TAG, "Failed to copy file", e);
530             }
531         }
532 
533         assertEquals(TranscodingSession.STATUS_FINISHED, session.getStatus());
534         assertEquals(TranscodingSession.RESULT_SUCCESS, session.getResult());
535         assertEquals(TranscodingSession.ERROR_NONE, session.getErrorCode());
536 
537         // TODO(hkuang): Validate the transcoded video's width and height, framerate.
538 
539         // Validates the transcoded video's psnr.
540         // Enable this after fixing b/175644377
541         MediaTranscodingTestUtil.VideoTranscodingStatistics stats =
542                 MediaTranscodingTestUtil.computeStats(mContext, fileUri, destinationUri, DEBUG_YUV);
543         assertTrue("PSNR: " + stats.mAveragePSNR + " is too low",
544                 stats.mAveragePSNR >= PSNR_THRESHOLD);
545 
546         if (srcFd != null) {
547             srcFd.close();
548         }
549         if (dstFd != null) {
550             dstFd.close();
551         }
552     }
553 
testVideoFormatResolverShouldTranscode(String mime, int width, int height, int frameRate)554     private void testVideoFormatResolverShouldTranscode(String mime, int width, int height,
555             int frameRate) {
556         ApplicationMediaCapabilities clientCaps =
557                 new ApplicationMediaCapabilities.Builder().build();
558 
559         MediaFormat mediaFormat = createMediaFormat(mime, width, height, frameRate, BIT_RATE);
560 
561         TranscodingRequest.VideoFormatResolver
562                 resolver = new TranscodingRequest.VideoFormatResolver(clientCaps,
563                 mediaFormat);
564         assertTrue(resolver.shouldTranscode());
565         MediaFormat videoTrackFormat = resolver.resolveVideoFormat();
566         assertNotNull(videoTrackFormat);
567     }
568 
569     @Test
testVideoFormatResolverValidArgs()570     public void testVideoFormatResolverValidArgs() {
571         testVideoFormatResolverShouldTranscode(MediaFormat.MIMETYPE_VIDEO_HEVC, WIDTH, HEIGHT,
572                 FRAME_RATE);
573     }
574 
575     @Test
testVideoFormatResolverAv1Mime()576     public void testVideoFormatResolverAv1Mime() {
577         ApplicationMediaCapabilities clientCaps =
578                 new ApplicationMediaCapabilities.Builder().build();
579 
580         MediaFormat mediaFormat = createMediaFormat(MediaFormat.MIMETYPE_VIDEO_AV1, WIDTH, HEIGHT,
581                 FRAME_RATE, BIT_RATE);
582 
583         TranscodingRequest.VideoFormatResolver
584                 resolver = new TranscodingRequest.VideoFormatResolver(clientCaps,
585                 mediaFormat);
586         assertFalse(resolver.shouldTranscode());
587         MediaFormat videoTrackFormat = resolver.resolveVideoFormat();
588         assertNull(videoTrackFormat);
589     }
590 
testVideoFormatResolverInvalidArgs(String mime, int width, int height, int frameRate)591     private void testVideoFormatResolverInvalidArgs(String mime, int width, int height,
592             int frameRate) {
593         ApplicationMediaCapabilities clientCaps =
594                 new ApplicationMediaCapabilities.Builder().build();
595 
596         MediaFormat mediaFormat = createMediaFormat(mime, width, height, frameRate, BIT_RATE);
597 
598         TranscodingRequest.VideoFormatResolver
599                 resolver = new TranscodingRequest.VideoFormatResolver(clientCaps,
600                 mediaFormat);
601 
602         assertThrows(IllegalArgumentException.class, () -> {
603             MediaFormat videoTrackFormat = resolver.resolveVideoFormat();
604         });
605     }
606 
607     @Test
testVideoFormatResolverZeroWidth()608     public void testVideoFormatResolverZeroWidth() {
609         testVideoFormatResolverInvalidArgs(MediaFormat.MIMETYPE_VIDEO_HEVC, 0 /* width */,
610                 HEIGHT, FRAME_RATE);
611     }
612 
613     @Test
testVideoFormatResolverZeroHeight()614     public void testVideoFormatResolverZeroHeight() {
615         testVideoFormatResolverInvalidArgs(MediaFormat.MIMETYPE_VIDEO_HEVC, WIDTH,
616                 0 /* height */, FRAME_RATE);
617     }
618 
619     @Test
testVideoFormatResolverZeroFrameRate()620     public void testVideoFormatResolverZeroFrameRate() {
621         testVideoFormatResolverInvalidArgs(MediaFormat.MIMETYPE_VIDEO_HEVC, WIDTH,
622                 HEIGHT, 0 /* frameRate */);
623     }
624 
625     @Test
testVideoFormatResolverNegativeWidth()626     public void testVideoFormatResolverNegativeWidth() {
627         testVideoFormatResolverInvalidArgs(MediaFormat.MIMETYPE_VIDEO_HEVC, -WIDTH,
628                 HEIGHT, FRAME_RATE);
629     }
630 
631     @Test
testVideoFormatResolverNegativeHeight()632     public void testVideoFormatResolverNegativeHeight() {
633         testVideoFormatResolverInvalidArgs(MediaFormat.MIMETYPE_VIDEO_HEVC, WIDTH,
634                 -HEIGHT, FRAME_RATE);
635     }
636 
637     @Test
testVideoFormatResolverNegativeFrameRate()638     public void testVideoFormatResolverNegativeFrameRate() {
639         testVideoFormatResolverInvalidArgs(MediaFormat.MIMETYPE_VIDEO_HEVC, WIDTH,
640                 HEIGHT, -FRAME_RATE);
641     }
642 
643     @Test
testVideoFormatResolverMissingWidth()644     public void testVideoFormatResolverMissingWidth() {
645         testVideoFormatResolverInvalidArgs(MediaFormat.MIMETYPE_VIDEO_HEVC, INT_NOT_SET /* width*/,
646                 HEIGHT /* height */, FRAME_RATE);
647     }
648 
649     @Test
testVideoFormatResolverMissingHeight()650     public void testVideoFormatResolverMissingHeight() {
651         testVideoFormatResolverInvalidArgs(MediaFormat.MIMETYPE_VIDEO_HEVC, WIDTH,
652                 INT_NOT_SET /* height */, FRAME_RATE);
653     }
654 
655     @Test
testVideoFormatResolverMissingFrameRate()656     public void testVideoFormatResolverMissingFrameRate() {
657         testVideoFormatResolverShouldTranscode(MediaFormat.MIMETYPE_VIDEO_HEVC, WIDTH, HEIGHT,
658                 INT_NOT_SET /* frameRate */);
659     }
660 
compareFormat(MediaFormat fmt1, MediaFormat fmt2)661     private boolean compareFormat(MediaFormat fmt1, MediaFormat fmt2) {
662         if (fmt1 == fmt2) return true;
663         if (fmt1 == null || fmt2 == null) return false;
664 
665         return (fmt1.getString(MediaFormat.KEY_MIME) == fmt2.getString(MediaFormat.KEY_MIME) &&
666                 fmt1.getInteger(MediaFormat.KEY_WIDTH) == fmt2.getInteger(MediaFormat.KEY_WIDTH) &&
667                 fmt1.getInteger(MediaFormat.KEY_HEIGHT) == fmt2.getInteger(MediaFormat.KEY_HEIGHT)
668                 && fmt1.getInteger(MediaFormat.KEY_BIT_RATE) == fmt2.getInteger(
669                 MediaFormat.KEY_BIT_RATE));
670     }
671 
672     @Test
testCancelTranscoding()673     public void testCancelTranscoding() throws Exception {
674         Log.d(TAG, "Starting: testCancelTranscoding");
675         Semaphore transcodeCompleteSemaphore = new Semaphore(0);
676         final CountDownLatch statusLatch = new CountDownLatch(1);
677 
678         Uri destinationUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
679                 + mContext.getCacheDir().getAbsolutePath() + "/HevcTranscode.mp4");
680 
681         VideoTranscodingRequest request =
682                 new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, destinationUri,
683                         createDefaultMediaFormat())
684                         .build();
685         Executor listenerExecutor = Executors.newSingleThreadExecutor();
686 
687         TranscodingSession session = mMediaTranscodingManager.enqueueRequest(
688                 request,
689                 listenerExecutor,
690                 transcodingSession -> {
691                     Log.d(TAG,
692                             "Transcoding completed with result: " + transcodingSession.getResult());
693                     assertEquals(TranscodingSession.RESULT_CANCELED,
694                             transcodingSession.getResult());
695                     transcodeCompleteSemaphore.release();
696                 });
697         assertNotNull(session);
698 
699         assertTrue(session.getSessionId() != -1);
700 
701         // Wait for progress update before cancel the transcoding.
702         session.setOnProgressUpdateListener(listenerExecutor,
703                 new TranscodingSession.OnProgressUpdateListener() {
704                     @Override
705                     public void onProgressUpdate(TranscodingSession session, int newProgress) {
706                         if (newProgress > 0) {
707                             statusLatch.countDown();
708                         }
709                         assertEquals(newProgress, session.getProgress());
710                     }
711                 });
712 
713         statusLatch.await(2, TimeUnit.MILLISECONDS);
714         session.cancel();
715 
716         Log.d(TAG, "testMediaTranscodingManager - Waiting for transcode to cancel.");
717         boolean finishedOnTime = transcodeCompleteSemaphore.tryAcquire(
718                 30, TimeUnit.MILLISECONDS);
719 
720         assertEquals(TranscodingSession.STATUS_FINISHED, session.getStatus());
721         assertEquals(TranscodingSession.RESULT_CANCELED, session.getResult());
722         assertEquals(TranscodingSession.ERROR_NONE, session.getErrorCode());
723         assertTrue("Fails to cancel transcoding", finishedOnTime);
724     }
725 
726     @Test
testTranscodingProgressUpdate()727     public void testTranscodingProgressUpdate() throws Exception {
728         Log.d(TAG, "Starting: testTranscodingProgressUpdate");
729 
730         Semaphore transcodeCompleteSemaphore = new Semaphore(0);
731 
732         // Create a file Uri: file:///data/user/0/android.media.mediatranscoding.cts/cache/HevcTranscode.mp4
733         Uri destinationUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
734                 + mContext.getCacheDir().getAbsolutePath() + "/HevcTranscode.mp4");
735 
736         VideoTranscodingRequest request =
737                 new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, destinationUri,
738                         createDefaultMediaFormat())
739                         .build();
740         Executor listenerExecutor = Executors.newSingleThreadExecutor();
741 
742         TranscodingSession session = mMediaTranscodingManager.enqueueRequest(request,
743                 listenerExecutor,
744                 TranscodingSession -> {
745                     Log.d(TAG,
746                             "Transcoding completed with result: " + TranscodingSession.getResult());
747                     assertEquals(TranscodingSession.RESULT_SUCCESS, TranscodingSession.getResult());
748                     transcodeCompleteSemaphore.release();
749                 });
750         assertNotNull(session);
751 
752         AtomicInteger progressUpdateCount = new AtomicInteger(0);
753 
754         // Set progress update executor and use the same executor as result listener.
755         session.setOnProgressUpdateListener(listenerExecutor,
756                 new TranscodingSession.OnProgressUpdateListener() {
757                     int mPreviousProgress = 0;
758 
759                     @Override
760                     public void onProgressUpdate(TranscodingSession session, int newProgress) {
761                         assertTrue("Invalid proress update", newProgress > mPreviousProgress);
762                         assertTrue("Invalid proress update", newProgress <= 100);
763                         mPreviousProgress = newProgress;
764                         progressUpdateCount.getAndIncrement();
765                         Log.i(TAG, "Get progress update " + newProgress);
766                     }
767                 });
768 
769         boolean finishedOnTime = transcodeCompleteSemaphore.tryAcquire(
770                 TRANSCODE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
771         assertTrue("Transcode failed to complete in time.", finishedOnTime);
772         assertTrue("Failed to receive at least 10 progress updates",
773                 progressUpdateCount.get() > 10);
774     }
775 
776     @Test
testClearOnProgressUpdateListener()777     public void testClearOnProgressUpdateListener() throws Exception {
778         Log.d(TAG, "Starting: testClearOnProgressUpdateListener");
779 
780         Semaphore transcodeCompleteSemaphore = new Semaphore(0);
781 
782         // Create a file Uri: file:///data/user/0/android.media.mediatranscoding.cts/cache/HevcTranscode.mp4
783         Uri destinationUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
784                 + mContext.getCacheDir().getAbsolutePath() + "/HevcTranscode.mp4");
785 
786         VideoTranscodingRequest request =
787                 new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, destinationUri,
788                         createDefaultMediaFormat())
789                         .build();
790         Executor listenerExecutor = Executors.newSingleThreadExecutor();
791 
792         TranscodingSession session = mMediaTranscodingManager.enqueueRequest(request,
793                 listenerExecutor,
794                 TranscodingSession -> {
795                     Log.d(TAG,
796                             "Transcoding completed with result: " + TranscodingSession.getResult());
797                     assertEquals(TranscodingSession.RESULT_SUCCESS, TranscodingSession.getResult());
798                     transcodeCompleteSemaphore.release();
799                 });
800         assertNotNull(session);
801 
802         AtomicInteger progressUpdateCount = new AtomicInteger(0);
803 
804         // Set progress update executor and use the same executor as result listener.
805         session.setOnProgressUpdateListener(listenerExecutor,
806                 new TranscodingSession.OnProgressUpdateListener() {
807                     int mPreviousProgress = 0;
808 
809                     @Override
810                     public void onProgressUpdate(TranscodingSession session, int newProgress) {
811                         if (mPreviousProgress == 0) {
812                             // Clear listener the first time this is called.
813                             session.clearOnProgressUpdateListener();
814                             // Reset the progress update count in case calls are pending now.
815                             listenerExecutor.execute(() -> progressUpdateCount.set(1));
816                         }
817                         mPreviousProgress = newProgress;
818                         progressUpdateCount.getAndIncrement();
819                         Log.i(TAG, "Get progress update " + newProgress);
820                     }
821                 });
822 
823         boolean finishedOnTime = transcodeCompleteSemaphore.tryAcquire(
824                 TRANSCODE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
825         assertTrue("Transcode failed to complete in time.", finishedOnTime);
826         assertTrue("Expected exactly one progress update", progressUpdateCount.get() == 1);
827     }
828 
829     @Test
testAddingClientUids()830     public void testAddingClientUids() throws Exception {
831         Log.d(TAG, "Starting: testTranscodingProgressUpdate");
832 
833         Semaphore transcodeCompleteSemaphore = new Semaphore(0);
834 
835         // Create a file Uri: file:///data/user/0/android.media.mediatranscoding.cts/cache/HevcTranscode.mp4
836         Uri destinationUri = Uri.parse(ContentResolver.SCHEME_FILE + "://"
837                 + mContext.getCacheDir().getAbsolutePath() + "/HevcTranscode.mp4");
838 
839         VideoTranscodingRequest request =
840                 new VideoTranscodingRequest.Builder(mSourceHEVCVideoUri, destinationUri,
841                         createDefaultMediaFormat())
842                         .build();
843         Executor listenerExecutor = Executors.newSingleThreadExecutor();
844 
845         TranscodingSession session = mMediaTranscodingManager.enqueueRequest(request,
846                 listenerExecutor,
847                 TranscodingSession -> {
848                     Log.d(TAG,
849                             "Transcoding completed with result: " + TranscodingSession.getResult());
850                     assertEquals(TranscodingSession.RESULT_SUCCESS, TranscodingSession.getResult());
851                     transcodeCompleteSemaphore.release();
852                 });
853         assertNotNull(session);
854 
855         session.addClientUid(1898 /* test_uid */);
856         session.addClientUid(1899 /* test_uid */);
857         session.addClientUid(1900 /* test_uid */);
858 
859         List<Integer> uids = session.getClientUids();
860         assertTrue(uids.size() == 4);  // At least 4 uid included the original request uid.
861         assertTrue(uids.contains(1898));
862         assertTrue(uids.contains(1899));
863         assertTrue(uids.contains(1900));
864 
865         AtomicInteger progressUpdateCount = new AtomicInteger(0);
866 
867         // Set progress update executor and use the same executor as result listener.
868         session.setOnProgressUpdateListener(listenerExecutor,
869                 new TranscodingSession.OnProgressUpdateListener() {
870                     int mPreviousProgress = 0;
871 
872                     @Override
873                     public void onProgressUpdate(TranscodingSession session, int newProgress) {
874                         assertTrue("Invalid proress update", newProgress > mPreviousProgress);
875                         assertTrue("Invalid proress update", newProgress <= 100);
876                         mPreviousProgress = newProgress;
877                         progressUpdateCount.getAndIncrement();
878                         Log.i(TAG, "Get progress update " + newProgress);
879                     }
880                 });
881 
882         boolean finishedOnTime = transcodeCompleteSemaphore.tryAcquire(
883                 TRANSCODE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
884         assertTrue("Transcode failed to complete in time.", finishedOnTime);
885         assertTrue("Failed to receive at least 10 progress updates",
886                 progressUpdateCount.get() > 10);
887     }
888 
getVideoTrackFormat(Uri fileUri)889     private MediaFormat getVideoTrackFormat(Uri fileUri) throws IOException {
890         MediaFormat videoFormat = null;
891         MediaExtractor extractor = new MediaExtractor();
892         extractor.setDataSource(fileUri.toString());
893         // Find video track format
894         for (int trackID = 0; trackID < extractor.getTrackCount(); trackID++) {
895             MediaFormat format = extractor.getTrackFormat(trackID);
896             if (format.getString(MediaFormat.KEY_MIME).startsWith("video/")) {
897                 videoFormat = format;
898                 break;
899             }
900         }
901         extractor.release();
902         return videoFormat;
903     }
904 
isFormatSupported(MediaFormat format, boolean isEncoder)905     private boolean isFormatSupported(MediaFormat format, boolean isEncoder) {
906         String mime = format.getString(MediaFormat.KEY_MIME);
907         MediaCodec codec = null;
908         try {
909             // The underlying transcoder library uses AMediaCodec_createEncoderByType
910             // to create encoder. So we cannot perform an exhaustive search of
911             // all codecs that support the format. This is because the codec that
912             // advertises support for the format during search may not be the one
913             // instantiated by the transcoder library. So, we have to check whether
914             // the codec returned by createEncoderByType supports the format.
915             // The same point holds for decoder too.
916             if (isEncoder) {
917                 codec = MediaCodec.createEncoderByType(mime);
918             } else {
919                 codec = MediaCodec.createDecoderByType(mime);
920             }
921             MediaCodecInfo info = codec.getCodecInfo();
922             MediaCodecInfo.CodecCapabilities caps = info.getCapabilitiesForType(mime);
923             if (caps != null && caps.isFormatSupported(format) && info.isHardwareAccelerated()) {
924                 return true;
925             }
926         } catch (IOException e) {
927             Log.d(TAG, "Exception: " + e);
928         } finally {
929             if (codec != null) {
930                 codec.release();
931             }
932         }
933         return false;
934     }
935 }
936