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