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