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