1 /* 2 * Copyright (C) 2019 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; 18 19 import android.annotation.CallbackExecutor; 20 import android.annotation.IntDef; 21 import android.annotation.IntRange; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.SystemApi; 25 import android.app.ActivityManager; 26 import android.content.ContentResolver; 27 import android.content.Context; 28 import android.content.res.AssetFileDescriptor; 29 import android.net.Uri; 30 import android.os.Build; 31 import android.os.ParcelFileDescriptor; 32 import android.os.RemoteException; 33 import android.os.ServiceSpecificException; 34 import android.system.Os; 35 import android.util.Log; 36 37 import com.android.internal.annotations.GuardedBy; 38 import com.android.internal.annotations.VisibleForTesting; 39 import com.android.modules.annotation.MinSdk; 40 import com.android.modules.utils.build.SdkLevel; 41 42 import java.io.FileNotFoundException; 43 import java.lang.annotation.Retention; 44 import java.lang.annotation.RetentionPolicy; 45 import java.util.ArrayList; 46 import java.util.HashMap; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.Objects; 50 import java.util.concurrent.Executor; 51 import java.util.concurrent.ExecutorService; 52 import java.util.concurrent.Executors; 53 54 /** 55 Android 12 introduces Compatible media transcoding feature. See 56 <a href="https://developer.android.com/about/versions/12/features#compatible_media_transcoding"> 57 Compatible media transcoding</a>. MediaTranscodingManager provides an interface to the system's media 58 transcoding service and can be used to transcode media files, e.g. transcoding a video from HEVC to 59 AVC. 60 61 <h3>Transcoding Types</h3> 62 <h4>Video Transcoding</h4> 63 When transcoding a video file, the video track will be transcoded based on the desired track format 64 and the audio track will be pass through without any modification. 65 <p class=note> 66 Note that currently only support transcoding video file in mp4 format and with single video track. 67 68 <h3>Transcoding Request</h3> 69 <p> 70 To transcode a media file, first create a {@link TranscodingRequest} through its builder class 71 {@link VideoTranscodingRequest.Builder}. Transcode requests are then enqueue to the manager through 72 {@link MediaTranscodingManager#enqueueRequest( 73 TranscodingRequest, Executor, OnTranscodingFinishedListener)} 74 TranscodeRequest are processed based on client process's priority and request priority. When a 75 transcode operation is completed the caller is notified via its 76 {@link OnTranscodingFinishedListener}. 77 In the meantime the caller may use the returned TranscodingSession object to cancel or check the 78 status of a specific transcode operation. 79 <p> 80 Here is an example where <code>Builder</code> is used to specify all parameters 81 82 <pre class=prettyprint> 83 VideoTranscodingRequest request = 84 new VideoTranscodingRequest.Builder(srcUri, dstUri, videoFormat).build(); 85 }</pre> 86 @hide 87 */ 88 @MinSdk(Build.VERSION_CODES.S) 89 @SystemApi 90 public final class MediaTranscodingManager { 91 private static final String TAG = "MediaTranscodingManager"; 92 93 /** Maximum number of retry to connect to the service. */ 94 private static final int CONNECT_SERVICE_RETRY_COUNT = 100; 95 96 /** Interval between trying to reconnect to the service. */ 97 private static final int INTERVAL_CONNECT_SERVICE_RETRY_MS = 40; 98 99 /** Default bpp(bits-per-pixel) to use for calculating default bitrate. */ 100 private static final float BPP = 0.25f; 101 102 /** 103 * Listener that gets notified when a transcoding operation has finished. 104 * This listener gets notified regardless of how the operation finished. It is up to the 105 * listener implementation to check the result and take appropriate action. 106 */ 107 @FunctionalInterface 108 public interface OnTranscodingFinishedListener { 109 /** 110 * Called when the transcoding operation has finished. The receiver may use the 111 * TranscodingSession to check the result, i.e. whether the operation succeeded, was 112 * canceled or if an error occurred. 113 * 114 * @param session The TranscodingSession instance for the finished transcoding operation. 115 */ onTranscodingFinished(@onNull TranscodingSession session)116 void onTranscodingFinished(@NonNull TranscodingSession session); 117 } 118 119 private final Context mContext; 120 private ContentResolver mContentResolver; 121 private final String mPackageName; 122 private final int mPid; 123 private final int mUid; 124 private final boolean mIsLowRamDevice; 125 private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); 126 private final HashMap<Integer, TranscodingSession> mPendingTranscodingSessions = new HashMap(); 127 private final Object mLock = new Object(); 128 @GuardedBy("mLock") 129 @NonNull private ITranscodingClient mTranscodingClient = null; 130 private static MediaTranscodingManager sMediaTranscodingManager; 131 handleTranscodingFinished(int sessionId, TranscodingResultParcel result)132 private void handleTranscodingFinished(int sessionId, TranscodingResultParcel result) { 133 synchronized (mPendingTranscodingSessions) { 134 // Gets the session associated with the sessionId and removes it from 135 // mPendingTranscodingSessions. 136 final TranscodingSession session = mPendingTranscodingSessions.remove(sessionId); 137 138 if (session == null) { 139 // This should not happen in reality. 140 Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions"); 141 return; 142 } 143 144 // Updates the session status and result. 145 session.updateStatusAndResult(TranscodingSession.STATUS_FINISHED, 146 TranscodingSession.RESULT_SUCCESS, 147 TranscodingSession.ERROR_NONE); 148 149 // Notifies client the session is done. 150 if (session.mListener != null && session.mListenerExecutor != null) { 151 session.mListenerExecutor.execute( 152 () -> session.mListener.onTranscodingFinished(session)); 153 } 154 } 155 } 156 handleTranscodingFailed(int sessionId, int errorCode)157 private void handleTranscodingFailed(int sessionId, int errorCode) { 158 synchronized (mPendingTranscodingSessions) { 159 // Gets the session associated with the sessionId and removes it from 160 // mPendingTranscodingSessions. 161 final TranscodingSession session = mPendingTranscodingSessions.remove(sessionId); 162 163 if (session == null) { 164 // This should not happen in reality. 165 Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions"); 166 return; 167 } 168 169 // Updates the session status and result. 170 session.updateStatusAndResult(TranscodingSession.STATUS_FINISHED, 171 TranscodingSession.RESULT_ERROR, errorCode); 172 173 // Notifies client the session failed. 174 if (session.mListener != null && session.mListenerExecutor != null) { 175 session.mListenerExecutor.execute( 176 () -> session.mListener.onTranscodingFinished(session)); 177 } 178 } 179 } 180 handleTranscodingProgressUpdate(int sessionId, int newProgress)181 private void handleTranscodingProgressUpdate(int sessionId, int newProgress) { 182 synchronized (mPendingTranscodingSessions) { 183 // Gets the session associated with the sessionId. 184 final TranscodingSession session = mPendingTranscodingSessions.get(sessionId); 185 186 if (session == null) { 187 // This should not happen in reality. 188 Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions"); 189 return; 190 } 191 192 // Updates the session progress. 193 session.updateProgress(newProgress); 194 195 // Notifies client the progress update. 196 if (session.mProgressUpdateExecutor != null 197 && session.mProgressUpdateListener != null) { 198 session.mProgressUpdateExecutor.execute( 199 () -> session.mProgressUpdateListener.onProgressUpdate(session, 200 newProgress)); 201 } 202 } 203 } 204 getService(boolean retry)205 private IMediaTranscodingService getService(boolean retry) { 206 // Do not try to get the service on pre-S. The service is lazy-start and getting the 207 // service could block. 208 if (!SdkLevel.isAtLeastS()) { 209 return null; 210 } 211 // Do not try to get the service on AndroidGo (low-ram) devices. 212 if (mIsLowRamDevice) { 213 return null; 214 } 215 int retryCount = !retry ? 1 : CONNECT_SERVICE_RETRY_COUNT; 216 Log.i(TAG, "get service with retry " + retryCount); 217 for (int count = 1; count <= retryCount; count++) { 218 Log.d(TAG, "Trying to connect to service. Try count: " + count); 219 IMediaTranscodingService service = IMediaTranscodingService.Stub.asInterface( 220 MediaFrameworkInitializer 221 .getMediaServiceManager() 222 .getMediaTranscodingServiceRegisterer() 223 .get()); 224 if (service != null) { 225 return service; 226 } 227 try { 228 // Sleep a bit before retry. 229 Thread.sleep(INTERVAL_CONNECT_SERVICE_RETRY_MS); 230 } catch (InterruptedException ie) { 231 /* ignore */ 232 } 233 } 234 Log.w(TAG, "Failed to get service"); 235 return null; 236 } 237 238 /* 239 * Handle client binder died event. 240 * Upon receiving a binder died event of the client, we will do the following: 241 * 1) For the session that is running, notify the client that the session is failed with 242 * error code, so client could choose to retry the session or not. 243 * TODO(hkuang): Add a new error code to signal service died error. 244 * 2) For the sessions that is still pending or paused, we will resubmit the session 245 * once we successfully reconnect to the service and register a new client. 246 * 3) When trying to connect to the service and register a new client. The service may need time 247 * to reboot or never boot up again. So we will retry for a number of times. If we still 248 * could not connect, we will notify client session failure for the pending and paused 249 * sessions. 250 */ onClientDied()251 private void onClientDied() { 252 synchronized (mLock) { 253 mTranscodingClient = null; 254 } 255 256 // Delegates the session notification and retry to the executor as it may take some time. 257 mExecutor.execute(() -> { 258 // List to track the sessions that we want to retry. 259 List<TranscodingSession> retrySessions = new ArrayList<TranscodingSession>(); 260 261 // First notify the client of session failure for all the running sessions. 262 synchronized (mPendingTranscodingSessions) { 263 for (Map.Entry<Integer, TranscodingSession> entry : 264 mPendingTranscodingSessions.entrySet()) { 265 TranscodingSession session = entry.getValue(); 266 267 if (session.getStatus() == TranscodingSession.STATUS_RUNNING) { 268 session.updateStatusAndResult(TranscodingSession.STATUS_FINISHED, 269 TranscodingSession.RESULT_ERROR, 270 TranscodingSession.ERROR_SERVICE_DIED); 271 272 // Remove the session from pending sessions. 273 mPendingTranscodingSessions.remove(entry.getKey()); 274 275 if (session.mListener != null && session.mListenerExecutor != null) { 276 Log.i(TAG, "Notify client session failed"); 277 session.mListenerExecutor.execute( 278 () -> session.mListener.onTranscodingFinished(session)); 279 } 280 } else if (session.getStatus() == TranscodingSession.STATUS_PENDING 281 || session.getStatus() == TranscodingSession.STATUS_PAUSED) { 282 // Add the session to retrySessions to handle them later. 283 retrySessions.add(session); 284 } 285 } 286 } 287 288 // Try to register with the service once it boots up. 289 IMediaTranscodingService service = getService(true /*retry*/); 290 boolean haveTranscodingClient = false; 291 if (service != null) { 292 synchronized (mLock) { 293 mTranscodingClient = registerClient(service); 294 if (mTranscodingClient != null) { 295 haveTranscodingClient = true; 296 } 297 } 298 } 299 300 for (TranscodingSession session : retrySessions) { 301 // Notify the session failure if we fails to connect to the service or fail 302 // to retry the session. 303 if (!haveTranscodingClient) { 304 // TODO(hkuang): Return correct error code to the client. 305 handleTranscodingFailed(session.getSessionId(), 0 /*unused */); 306 } 307 308 try { 309 // Do not set hasRetried for retry initiated by MediaTranscodingManager. 310 session.retryInternal(false /*setHasRetried*/); 311 } catch (Exception re) { 312 // TODO(hkuang): Return correct error code to the client. 313 handleTranscodingFailed(session.getSessionId(), 0 /*unused */); 314 } 315 } 316 }); 317 } 318 updateStatus(int sessionId, int status)319 private void updateStatus(int sessionId, int status) { 320 synchronized (mPendingTranscodingSessions) { 321 final TranscodingSession session = mPendingTranscodingSessions.get(sessionId); 322 323 if (session == null) { 324 // This should not happen in reality. 325 Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions"); 326 return; 327 } 328 329 // Updates the session status. 330 session.updateStatus(status); 331 } 332 } 333 334 // Just forwards all the events to the event handler. 335 private ITranscodingClientCallback mTranscodingClientCallback = 336 new ITranscodingClientCallback.Stub() { 337 // TODO(hkuang): Add more unit test to test difference file open mode. 338 @Override 339 public ParcelFileDescriptor openFileDescriptor(String fileUri, String mode) 340 throws RemoteException { 341 if (!mode.equals("r") && !mode.equals("w") && !mode.equals("rw")) { 342 Log.e(TAG, "Unsupport mode: " + mode); 343 return null; 344 } 345 346 Uri uri = Uri.parse(fileUri); 347 try { 348 AssetFileDescriptor afd = mContentResolver.openAssetFileDescriptor(uri, 349 mode); 350 if (afd != null) { 351 return afd.getParcelFileDescriptor(); 352 } 353 } catch (FileNotFoundException e) { 354 Log.w(TAG, "Cannot find content uri: " + uri, e); 355 } catch (SecurityException e) { 356 Log.w(TAG, "Cannot open content uri: " + uri, e); 357 } catch (Exception e) { 358 Log.w(TAG, "Unknown content uri: " + uri, e); 359 } 360 return null; 361 } 362 363 @Override 364 public void onTranscodingStarted(int sessionId) throws RemoteException { 365 updateStatus(sessionId, TranscodingSession.STATUS_RUNNING); 366 } 367 368 @Override 369 public void onTranscodingPaused(int sessionId) throws RemoteException { 370 updateStatus(sessionId, TranscodingSession.STATUS_PAUSED); 371 } 372 373 @Override 374 public void onTranscodingResumed(int sessionId) throws RemoteException { 375 updateStatus(sessionId, TranscodingSession.STATUS_RUNNING); 376 } 377 378 @Override 379 public void onTranscodingFinished(int sessionId, TranscodingResultParcel result) 380 throws RemoteException { 381 handleTranscodingFinished(sessionId, result); 382 } 383 384 @Override 385 public void onTranscodingFailed(int sessionId, int errorCode) 386 throws RemoteException { 387 handleTranscodingFailed(sessionId, errorCode); 388 } 389 390 @Override 391 public void onAwaitNumberOfSessionsChanged(int sessionId, int oldAwaitNumber, 392 int newAwaitNumber) throws RemoteException { 393 //TODO(hkuang): Implement this. 394 } 395 396 @Override 397 public void onProgressUpdate(int sessionId, int newProgress) 398 throws RemoteException { 399 handleTranscodingProgressUpdate(sessionId, newProgress); 400 } 401 }; 402 registerClient(IMediaTranscodingService service)403 private ITranscodingClient registerClient(IMediaTranscodingService service) { 404 synchronized (mLock) { 405 try { 406 // Registers the client with MediaTranscoding service. 407 mTranscodingClient = service.registerClient( 408 mTranscodingClientCallback, 409 mPackageName, 410 mPackageName); 411 412 if (mTranscodingClient != null) { 413 mTranscodingClient.asBinder().linkToDeath(() -> onClientDied(), /* flags */ 0); 414 } 415 } catch (Exception ex) { 416 Log.e(TAG, "Failed to register new client due to exception " + ex); 417 mTranscodingClient = null; 418 } 419 } 420 return mTranscodingClient; 421 } 422 423 /** 424 * @hide 425 */ MediaTranscodingManager(@onNull Context context)426 public MediaTranscodingManager(@NonNull Context context) { 427 mContext = context; 428 mContentResolver = mContext.getContentResolver(); 429 mPackageName = mContext.getPackageName(); 430 mUid = Os.getuid(); 431 mPid = Os.getpid(); 432 mIsLowRamDevice = mContext.getSystemService(ActivityManager.class).isLowRamDevice(); 433 } 434 435 /** 436 * Abstract base class for all the TranscodingRequest. 437 * <p> TranscodingRequest encapsulates the desired configuration for the transcoding. 438 */ 439 public abstract static class TranscodingRequest { 440 /** 441 * 442 * Default transcoding type. 443 * @hide 444 */ 445 public static final int TRANSCODING_TYPE_UNKNOWN = 0; 446 447 /** 448 * TRANSCODING_TYPE_VIDEO indicates that client wants to perform transcoding on a video. 449 * <p>Note that currently only support transcoding video file in mp4 format. 450 * @hide 451 */ 452 public static final int TRANSCODING_TYPE_VIDEO = 1; 453 454 /** 455 * TRANSCODING_TYPE_IMAGE indicates that client wants to perform transcoding on an image. 456 * @hide 457 */ 458 public static final int TRANSCODING_TYPE_IMAGE = 2; 459 460 /** @hide */ 461 @IntDef(prefix = {"TRANSCODING_TYPE_"}, value = { 462 TRANSCODING_TYPE_UNKNOWN, 463 TRANSCODING_TYPE_VIDEO, 464 TRANSCODING_TYPE_IMAGE, 465 }) 466 @Retention(RetentionPolicy.SOURCE) 467 public @interface TranscodingType {} 468 469 /** 470 * Default value. 471 * 472 * @hide 473 */ 474 public static final int PRIORITY_UNKNOWN = 0; 475 /** 476 * PRIORITY_REALTIME indicates that the transcoding request is time-critical and that the 477 * client wants the transcoding result as soon as possible. 478 * <p> Set PRIORITY_REALTIME only if the transcoding is time-critical as it will involve 479 * performance penalty due to resource reallocation to prioritize the sessions with higher 480 * priority. 481 * 482 * @hide 483 */ 484 public static final int PRIORITY_REALTIME = 1; 485 486 /** 487 * PRIORITY_OFFLINE indicates the transcoding is not time-critical and the client does not 488 * need the transcoding result as soon as possible. 489 * <p>Sessions with PRIORITY_OFFLINE will be scheduled behind PRIORITY_REALTIME. Always set 490 * to 491 * PRIORITY_OFFLINE if client does not need the result as soon as possible and could accept 492 * delay of the transcoding result. 493 * 494 * @hide 495 * 496 */ 497 public static final int PRIORITY_OFFLINE = 2; 498 499 /** @hide */ 500 @IntDef(prefix = {"PRIORITY_"}, value = { 501 PRIORITY_UNKNOWN, 502 PRIORITY_REALTIME, 503 PRIORITY_OFFLINE, 504 }) 505 @Retention(RetentionPolicy.SOURCE) 506 public @interface TranscodingPriority {} 507 508 /** Uri of the source media file. */ 509 private @NonNull Uri mSourceUri; 510 511 /** Uri of the destination media file. */ 512 private @NonNull Uri mDestinationUri; 513 514 /** FileDescriptor of the source media file. */ 515 private @Nullable ParcelFileDescriptor mSourceFileDescriptor; 516 517 /** FileDescriptor of the destination media file. */ 518 private @Nullable ParcelFileDescriptor mDestinationFileDescriptor; 519 520 /** 521 * The UID of the client that the TranscodingRequest is for. Only privileged caller could 522 * set this Uid as only they could do the transcoding on behalf of the client. 523 * -1 means not available. 524 */ 525 private int mClientUid = -1; 526 527 /** 528 * The Pid of the client that the TranscodingRequest is for. Only privileged caller could 529 * set this Uid as only they could do the transcoding on behalf of the client. 530 * -1 means not available. 531 */ 532 private int mClientPid = -1; 533 534 /** Type of the transcoding. */ 535 private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN; 536 537 /** Priority of the transcoding. */ 538 private @TranscodingPriority int mPriority = PRIORITY_UNKNOWN; 539 540 /** 541 * Desired image format for the destination file. 542 * <p> If this is null, source file's image track will be passed through and copied to the 543 * destination file. 544 * @hide 545 */ 546 private @Nullable MediaFormat mImageFormat = null; 547 548 @VisibleForTesting 549 private TranscodingTestConfig mTestConfig = null; 550 551 /** 552 * Prevent public constructor access. 553 */ TranscodingRequest()554 /* package private */ TranscodingRequest() { 555 } 556 TranscodingRequest(Builder b)557 private TranscodingRequest(Builder b) { 558 mSourceUri = b.mSourceUri; 559 mSourceFileDescriptor = b.mSourceFileDescriptor; 560 mDestinationUri = b.mDestinationUri; 561 mDestinationFileDescriptor = b.mDestinationFileDescriptor; 562 mClientUid = b.mClientUid; 563 mClientPid = b.mClientPid; 564 mPriority = b.mPriority; 565 mType = b.mType; 566 mTestConfig = b.mTestConfig; 567 } 568 569 /** 570 * Return the type of the transcoding. 571 * @hide 572 */ 573 @TranscodingType getType()574 public int getType() { 575 return mType; 576 } 577 578 /** Return source uri of the transcoding. */ 579 @NonNull getSourceUri()580 public Uri getSourceUri() { 581 return mSourceUri; 582 } 583 584 /** 585 * Return source file descriptor of the transcoding. 586 * This will be null if client has not provided it. 587 */ 588 @Nullable getSourceFileDescriptor()589 public ParcelFileDescriptor getSourceFileDescriptor() { 590 return mSourceFileDescriptor; 591 } 592 593 /** Return the UID of the client that this request is for. -1 means not available. */ getClientUid()594 public int getClientUid() { 595 return mClientUid; 596 } 597 598 /** Return the PID of the client that this request is for. -1 means not available. */ getClientPid()599 public int getClientPid() { 600 return mClientPid; 601 } 602 603 /** Return destination uri of the transcoding. */ 604 @NonNull getDestinationUri()605 public Uri getDestinationUri() { 606 return mDestinationUri; 607 } 608 609 /** 610 * Return destination file descriptor of the transcoding. 611 * This will be null if client has not provided it. 612 */ 613 @Nullable getDestinationFileDescriptor()614 public ParcelFileDescriptor getDestinationFileDescriptor() { 615 return mDestinationFileDescriptor; 616 } 617 618 /** 619 * Return priority of the transcoding. 620 * @hide 621 */ 622 @TranscodingPriority getPriority()623 public int getPriority() { 624 return mPriority; 625 } 626 627 /** 628 * Return TestConfig of the transcoding. 629 * @hide 630 */ 631 @Nullable getTestConfig()632 public TranscodingTestConfig getTestConfig() { 633 return mTestConfig; 634 } 635 writeFormatToParcel(TranscodingRequestParcel parcel)636 abstract void writeFormatToParcel(TranscodingRequestParcel parcel); 637 638 /* Writes the TranscodingRequest to a parcel. */ writeToParcel(@onNull Context context)639 private TranscodingRequestParcel writeToParcel(@NonNull Context context) { 640 TranscodingRequestParcel parcel = new TranscodingRequestParcel(); 641 switch (mPriority) { 642 case PRIORITY_OFFLINE: 643 parcel.priority = TranscodingSessionPriority.kUnspecified; 644 break; 645 case PRIORITY_REALTIME: 646 case PRIORITY_UNKNOWN: 647 default: 648 parcel.priority = TranscodingSessionPriority.kNormal; 649 break; 650 } 651 parcel.transcodingType = mType; 652 parcel.sourceFilePath = mSourceUri.toString(); 653 parcel.sourceFd = mSourceFileDescriptor; 654 parcel.destinationFilePath = mDestinationUri.toString(); 655 parcel.destinationFd = mDestinationFileDescriptor; 656 parcel.clientUid = mClientUid; 657 parcel.clientPid = mClientPid; 658 if (mClientUid < 0) { 659 parcel.clientPackageName = context.getPackageName(); 660 } else { 661 String packageName = context.getPackageManager().getNameForUid(mClientUid); 662 // PackageName is optional as some uid does not have package name. Set to 663 // "Unavailable" string in this case. 664 if (packageName == null) { 665 Log.w(TAG, "Failed to find package for uid: " + mClientUid); 666 packageName = "Unavailable"; 667 } 668 parcel.clientPackageName = packageName; 669 } 670 writeFormatToParcel(parcel); 671 if (mTestConfig != null) { 672 parcel.isForTesting = true; 673 parcel.testConfig = mTestConfig; 674 } 675 return parcel; 676 } 677 678 /** 679 * Builder to build a {@link TranscodingRequest} object. 680 * 681 * @param <T> The subclass to be built. 682 */ 683 abstract static class Builder<T extends Builder<T>> { 684 private @NonNull Uri mSourceUri; 685 private @NonNull Uri mDestinationUri; 686 private @Nullable ParcelFileDescriptor mSourceFileDescriptor = null; 687 private @Nullable ParcelFileDescriptor mDestinationFileDescriptor = null; 688 private int mClientUid = -1; 689 private int mClientPid = -1; 690 private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN; 691 private @TranscodingPriority int mPriority = PRIORITY_UNKNOWN; 692 private TranscodingTestConfig mTestConfig; 693 self()694 abstract T self(); 695 696 /** 697 * Creates a builder for building {@link TranscodingRequest}s. 698 * 699 * Client must set the source Uri. If client also provides the source fileDescriptor 700 * through is provided by {@link #setSourceFileDescriptor(ParcelFileDescriptor)}, 701 * TranscodingSession will use the fd instead of calling back to the client to open the 702 * sourceUri. 703 * 704 * 705 * @param type The transcoding type. 706 * @param sourceUri Content uri for the source media file. 707 * @param destinationUri Content uri for the destination media file. 708 * 709 */ Builder(@ranscodingType int type, @NonNull Uri sourceUri, @NonNull Uri destinationUri)710 private Builder(@TranscodingType int type, @NonNull Uri sourceUri, 711 @NonNull Uri destinationUri) { 712 mType = type; 713 714 if (sourceUri == null || Uri.EMPTY.equals(sourceUri)) { 715 throw new IllegalArgumentException( 716 "You must specify a non-empty source Uri."); 717 } 718 mSourceUri = sourceUri; 719 720 if (destinationUri == null || Uri.EMPTY.equals(destinationUri)) { 721 throw new IllegalArgumentException( 722 "You must specify a non-empty destination Uri."); 723 } 724 mDestinationUri = destinationUri; 725 } 726 727 /** 728 * Specifies the fileDescriptor opened from the source media file. 729 * 730 * This call is optional. If the source fileDescriptor is provided, TranscodingSession 731 * will use it directly instead of opening the uri from {@link #Builder(int, Uri, Uri)}. 732 * It is client's responsibility to make sure the fileDescriptor is opened from the 733 * source uri. 734 * @param fileDescriptor a {@link ParcelFileDescriptor} opened from source media file. 735 * @return The same builder instance. 736 * @throws IllegalArgumentException if fileDescriptor is invalid. 737 */ 738 @NonNull setSourceFileDescriptor(@onNull ParcelFileDescriptor fileDescriptor)739 public T setSourceFileDescriptor(@NonNull ParcelFileDescriptor fileDescriptor) { 740 if (fileDescriptor == null || fileDescriptor.getFd() < 0) { 741 throw new IllegalArgumentException( 742 "Invalid source descriptor."); 743 } 744 mSourceFileDescriptor = fileDescriptor; 745 return self(); 746 } 747 748 /** 749 * Specifies the fileDescriptor opened from the destination media file. 750 * 751 * This call is optional. If the destination fileDescriptor is provided, 752 * TranscodingSession will use it directly instead of opening the source uri from 753 * {@link #Builder(int, Uri, Uri)} upon transcoding starts. It is client's 754 * responsibility to make sure the fileDescriptor is opened from the destination uri. 755 * @param fileDescriptor a {@link ParcelFileDescriptor} opened from destination media 756 * file. 757 * @return The same builder instance. 758 * @throws IllegalArgumentException if fileDescriptor is invalid. 759 */ 760 @NonNull setDestinationFileDescriptor( @onNull ParcelFileDescriptor fileDescriptor)761 public T setDestinationFileDescriptor( 762 @NonNull ParcelFileDescriptor fileDescriptor) { 763 if (fileDescriptor == null || fileDescriptor.getFd() < 0) { 764 throw new IllegalArgumentException( 765 "Invalid destination descriptor."); 766 } 767 mDestinationFileDescriptor = fileDescriptor; 768 return self(); 769 } 770 771 /** 772 * Specify the UID of the client that this request is for. 773 * <p> 774 * Only privilege caller with android.permission.WRITE_MEDIA_STORAGE could forward the 775 * pid. Note that the permission check happens on the service side upon starting the 776 * transcoding. If the client does not have the permission, the transcoding will fail. 777 * 778 * @param uid client Uid. 779 * @return The same builder instance. 780 * @throws IllegalArgumentException if uid is invalid. 781 */ 782 @NonNull setClientUid(int uid)783 public T setClientUid(int uid) { 784 if (uid < 0) { 785 throw new IllegalArgumentException("Invalid Uid"); 786 } 787 mClientUid = uid; 788 return self(); 789 } 790 791 /** 792 * Specify the pid of the client that this request is for. 793 * <p> 794 * Only privilege caller with android.permission.WRITE_MEDIA_STORAGE could forward the 795 * pid. Note that the permission check happens on the service side upon starting the 796 * transcoding. If the client does not have the permission, the transcoding will fail. 797 * 798 * @param pid client Pid. 799 * @return The same builder instance. 800 * @throws IllegalArgumentException if pid is invalid. 801 */ 802 @NonNull setClientPid(int pid)803 public T setClientPid(int pid) { 804 if (pid < 0) { 805 throw new IllegalArgumentException("Invalid pid"); 806 } 807 mClientPid = pid; 808 return self(); 809 } 810 811 /** 812 * Specifies the priority of the transcoding. 813 * 814 * @param priority Must be one of the {@code PRIORITY_*} 815 * @return The same builder instance. 816 * @throws IllegalArgumentException if flags is invalid. 817 * @hide 818 */ 819 @NonNull setPriority(@ranscodingPriority int priority)820 public T setPriority(@TranscodingPriority int priority) { 821 if (priority != PRIORITY_OFFLINE && priority != PRIORITY_REALTIME) { 822 throw new IllegalArgumentException("Invalid priority: " + priority); 823 } 824 mPriority = priority; 825 return self(); 826 } 827 828 /** 829 * Sets the delay in processing this request. 830 * @param config test config. 831 * @return The same builder instance. 832 * @hide 833 */ 834 @VisibleForTesting 835 @NonNull setTestConfig(@onNull TranscodingTestConfig config)836 public T setTestConfig(@NonNull TranscodingTestConfig config) { 837 mTestConfig = config; 838 return self(); 839 } 840 } 841 842 /** 843 * Abstract base class for all the format resolvers. 844 */ 845 abstract static class MediaFormatResolver { 846 private @NonNull ApplicationMediaCapabilities mClientCaps; 847 848 /** 849 * Prevents public constructor access. 850 */ MediaFormatResolver()851 /* package private */ MediaFormatResolver() { 852 } 853 854 /** 855 * Constructs MediaFormatResolver object. 856 * 857 * @param clientCaps An ApplicationMediaCapabilities object containing the client's 858 * capabilities. 859 */ MediaFormatResolver(@onNull ApplicationMediaCapabilities clientCaps)860 MediaFormatResolver(@NonNull ApplicationMediaCapabilities clientCaps) { 861 if (clientCaps == null) { 862 throw new IllegalArgumentException("Client capabilities must not be null"); 863 } 864 mClientCaps = clientCaps; 865 } 866 867 /** 868 * Returns the client capabilities. 869 */ 870 @NonNull getClientCapabilities()871 /* package */ ApplicationMediaCapabilities getClientCapabilities() { 872 return mClientCaps; 873 } 874 shouldTranscode()875 abstract boolean shouldTranscode(); 876 } 877 878 /** 879 * VideoFormatResolver for deciding if video transcoding is needed, and if so, the track 880 * formats to use. 881 */ 882 public static class VideoFormatResolver extends MediaFormatResolver { 883 private static final int BIT_RATE = 20000000; // 20Mbps 884 885 private MediaFormat mSrcVideoFormatHint; 886 private MediaFormat mSrcAudioFormatHint; 887 888 /** 889 * Constructs a new VideoFormatResolver object. 890 * 891 * @param clientCaps An ApplicationMediaCapabilities object containing the client's 892 * capabilities. 893 * @param srcVideoFormatHint A MediaFormat object containing information about the 894 * source's video track format that could affect the 895 * transcoding decision. Such information could include video 896 * codec types, color spaces, whether special format info (eg. 897 * slow-motion markers) are present, etc.. If a particular 898 * information is not present, it will not be used to make the 899 * decision. 900 */ VideoFormatResolver(@onNull ApplicationMediaCapabilities clientCaps, @NonNull MediaFormat srcVideoFormatHint)901 public VideoFormatResolver(@NonNull ApplicationMediaCapabilities clientCaps, 902 @NonNull MediaFormat srcVideoFormatHint) { 903 super(clientCaps); 904 mSrcVideoFormatHint = srcVideoFormatHint; 905 } 906 907 /** 908 * Constructs a new VideoFormatResolver object. 909 * 910 * @param clientCaps An ApplicationMediaCapabilities object containing the client's 911 * capabilities. 912 * @param srcVideoFormatHint A MediaFormat object containing information about the 913 * source's video track format that could affect the 914 * transcoding decision. Such information could include video 915 * codec types, color spaces, whether special format info (eg. 916 * slow-motion markers) are present, etc.. If a particular 917 * information is not present, it will not be used to make the 918 * decision. 919 * @param srcAudioFormatHint A MediaFormat object containing information about the 920 * source's audio track format that could affect the 921 * transcoding decision. 922 * @hide 923 */ VideoFormatResolver(@onNull ApplicationMediaCapabilities clientCaps, @NonNull MediaFormat srcVideoFormatHint, @NonNull MediaFormat srcAudioFormatHint)924 VideoFormatResolver(@NonNull ApplicationMediaCapabilities clientCaps, 925 @NonNull MediaFormat srcVideoFormatHint, 926 @NonNull MediaFormat srcAudioFormatHint) { 927 super(clientCaps); 928 mSrcVideoFormatHint = srcVideoFormatHint; 929 mSrcAudioFormatHint = srcAudioFormatHint; 930 } 931 932 /** 933 * Returns whether the source content should be transcoded. 934 * 935 * @return true if the source should be transcoded. 936 */ shouldTranscode()937 public boolean shouldTranscode() { 938 boolean supportHevc = getClientCapabilities().isVideoMimeTypeSupported( 939 MediaFormat.MIMETYPE_VIDEO_HEVC); 940 if (!supportHevc && MediaFormat.MIMETYPE_VIDEO_HEVC.equals( 941 mSrcVideoFormatHint.getString(MediaFormat.KEY_MIME))) { 942 return true; 943 } 944 // TODO: add more checks as needed below. 945 return false; 946 } 947 948 /** 949 * Retrieves the video track format to be used on 950 * {@link VideoTranscodingRequest.Builder#setVideoTrackFormat(MediaFormat)} for this 951 * configuration. 952 * 953 * @return the video track format to be used if transcoding should be performed, 954 * and null otherwise. 955 */ 956 @Nullable resolveVideoFormat()957 public MediaFormat resolveVideoFormat() { 958 if (!shouldTranscode()) { 959 return null; 960 } 961 962 MediaFormat videoTrackFormat = new MediaFormat(mSrcVideoFormatHint); 963 videoTrackFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); 964 965 int width = mSrcVideoFormatHint.getInteger(MediaFormat.KEY_WIDTH); 966 int height = mSrcVideoFormatHint.getInteger(MediaFormat.KEY_HEIGHT); 967 if (width <= 0 || height <= 0) { 968 throw new IllegalArgumentException( 969 "Source Width and height must be larger than 0"); 970 } 971 972 float frameRate = 30.0f; // default to 30fps. 973 if (mSrcVideoFormatHint.containsKey(MediaFormat.KEY_FRAME_RATE)) { 974 frameRate = mSrcVideoFormatHint.getFloat(MediaFormat.KEY_FRAME_RATE); 975 if (frameRate <= 0) { 976 throw new IllegalArgumentException( 977 "frameRate must be larger than 0"); 978 } 979 } 980 981 int bitrate = getAVCBitrate(width, height, frameRate); 982 videoTrackFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); 983 return videoTrackFormat; 984 } 985 986 /** 987 * Generate a default bitrate with the fixed bpp(bits-per-pixel) 0.25. 988 * This maps to: 989 * 1080P@30fps -> 16Mbps 990 * 1080P@60fps-> 32Mbps 991 * 4K@30fps -> 62Mbps 992 */ getDefaultBitrate(int width, int height, float frameRate)993 private static int getDefaultBitrate(int width, int height, float frameRate) { 994 return (int) (width * height * frameRate * BPP); 995 } 996 997 /** 998 * Query the bitrate from CamcorderProfile. If there are two profiles that match the 999 * width/height/framerate, we will use the higher one to get better quality. 1000 * Return default bitrate if could not find any match profile. 1001 */ getAVCBitrate(int width, int height, float frameRate)1002 private static int getAVCBitrate(int width, int height, float frameRate) { 1003 int bitrate = -1; 1004 int[] cameraIds = {0, 1}; 1005 1006 // Profiles ordered in decreasing order of preference. 1007 int[] preferQualities = { 1008 CamcorderProfile.QUALITY_2160P, 1009 CamcorderProfile.QUALITY_1080P, 1010 CamcorderProfile.QUALITY_720P, 1011 CamcorderProfile.QUALITY_480P, 1012 CamcorderProfile.QUALITY_LOW, 1013 }; 1014 1015 for (int cameraId : cameraIds) { 1016 for (int quality : preferQualities) { 1017 // Check if camera id has profile for the quality level. 1018 if (!CamcorderProfile.hasProfile(cameraId, quality)) { 1019 continue; 1020 } 1021 CamcorderProfile profile = CamcorderProfile.get(cameraId, quality); 1022 // Check the width/height/framerate/codec, also consider portrait case. 1023 if (((width == profile.videoFrameWidth 1024 && height == profile.videoFrameHeight) 1025 || (height == profile.videoFrameWidth 1026 && width == profile.videoFrameHeight)) 1027 && (int) frameRate == profile.videoFrameRate 1028 && profile.videoCodec == MediaRecorder.VideoEncoder.H264) { 1029 if (bitrate < profile.videoBitRate) { 1030 bitrate = profile.videoBitRate; 1031 } 1032 break; 1033 } 1034 } 1035 } 1036 1037 if (bitrate == -1) { 1038 Log.w(TAG, "Failed to find CamcorderProfile for w: " + width + "h: " + height 1039 + " fps: " 1040 + frameRate); 1041 bitrate = getDefaultBitrate(width, height, frameRate); 1042 } 1043 Log.d(TAG, "Using bitrate " + bitrate + " for " + width + " " + height + " " 1044 + frameRate); 1045 return bitrate; 1046 } 1047 1048 /** 1049 * Retrieves the audio track format to be used for transcoding. 1050 * 1051 * @return the audio track format to be used if transcoding should be performed, and 1052 * null otherwise. 1053 * @hide 1054 */ 1055 @Nullable resolveAudioFormat()1056 public MediaFormat resolveAudioFormat() { 1057 if (!shouldTranscode()) { 1058 return null; 1059 } 1060 // Audio transcoding is not supported yet, always return null. 1061 return null; 1062 } 1063 } 1064 } 1065 1066 /** 1067 * VideoTranscodingRequest encapsulates the configuration for transcoding a video. 1068 */ 1069 public static final class VideoTranscodingRequest extends TranscodingRequest { 1070 /** 1071 * Desired output video format of the destination file. 1072 * <p> If this is null, source file's video track will be passed through and copied to the 1073 * destination file. 1074 */ 1075 private @Nullable MediaFormat mVideoTrackFormat = null; 1076 1077 /** 1078 * Desired output audio format of the destination file. 1079 * <p> If this is null, source file's audio track will be passed through and copied to the 1080 * destination file. 1081 */ 1082 private @Nullable MediaFormat mAudioTrackFormat = null; 1083 VideoTranscodingRequest(VideoTranscodingRequest.Builder builder)1084 private VideoTranscodingRequest(VideoTranscodingRequest.Builder builder) { 1085 super(builder); 1086 mVideoTrackFormat = builder.mVideoTrackFormat; 1087 mAudioTrackFormat = builder.mAudioTrackFormat; 1088 } 1089 1090 /** 1091 * Return the video track format of the transcoding. 1092 * This will be null if client has not specified the video track format. 1093 */ 1094 @NonNull getVideoTrackFormat()1095 public MediaFormat getVideoTrackFormat() { 1096 return mVideoTrackFormat; 1097 } 1098 1099 @Override writeFormatToParcel(TranscodingRequestParcel parcel)1100 void writeFormatToParcel(TranscodingRequestParcel parcel) { 1101 parcel.requestedVideoTrackFormat = convertToVideoTrackFormat(mVideoTrackFormat); 1102 } 1103 1104 /* Converts the MediaFormat to TranscodingVideoTrackFormat. */ convertToVideoTrackFormat(MediaFormat format)1105 private static TranscodingVideoTrackFormat convertToVideoTrackFormat(MediaFormat format) { 1106 if (format == null) { 1107 throw new IllegalArgumentException("Invalid MediaFormat"); 1108 } 1109 1110 TranscodingVideoTrackFormat trackFormat = new TranscodingVideoTrackFormat(); 1111 1112 if (format.containsKey(MediaFormat.KEY_MIME)) { 1113 String mime = format.getString(MediaFormat.KEY_MIME); 1114 if (MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) { 1115 trackFormat.codecType = TranscodingVideoCodecType.kAvc; 1116 } else if (MediaFormat.MIMETYPE_VIDEO_HEVC.equals(mime)) { 1117 trackFormat.codecType = TranscodingVideoCodecType.kHevc; 1118 } else { 1119 throw new UnsupportedOperationException("Only support transcode to avc/hevc"); 1120 } 1121 } 1122 1123 if (format.containsKey(MediaFormat.KEY_BIT_RATE)) { 1124 int bitrateBps = format.getInteger(MediaFormat.KEY_BIT_RATE); 1125 if (bitrateBps <= 0) { 1126 throw new IllegalArgumentException("Bitrate must be larger than 0"); 1127 } 1128 trackFormat.bitrateBps = bitrateBps; 1129 } 1130 1131 if (format.containsKey(MediaFormat.KEY_WIDTH) && format.containsKey( 1132 MediaFormat.KEY_HEIGHT)) { 1133 int width = format.getInteger(MediaFormat.KEY_WIDTH); 1134 int height = format.getInteger(MediaFormat.KEY_HEIGHT); 1135 if (width <= 0 || height <= 0) { 1136 throw new IllegalArgumentException("Width and height must be larger than 0"); 1137 } 1138 // TODO: Validate the aspect ratio after adding scaling. 1139 trackFormat.width = width; 1140 trackFormat.height = height; 1141 } 1142 1143 if (format.containsKey(MediaFormat.KEY_PROFILE)) { 1144 int profile = format.getInteger(MediaFormat.KEY_PROFILE); 1145 if (profile <= 0) { 1146 throw new IllegalArgumentException("Invalid codec profile"); 1147 } 1148 // TODO: Validate the profile according to codec type. 1149 trackFormat.profile = profile; 1150 } 1151 1152 if (format.containsKey(MediaFormat.KEY_LEVEL)) { 1153 int level = format.getInteger(MediaFormat.KEY_LEVEL); 1154 if (level <= 0) { 1155 throw new IllegalArgumentException("Invalid codec level"); 1156 } 1157 // TODO: Validate the level according to codec type. 1158 trackFormat.level = level; 1159 } 1160 1161 return trackFormat; 1162 } 1163 1164 /** 1165 * Builder class for {@link VideoTranscodingRequest}. 1166 */ 1167 public static final class Builder extends 1168 TranscodingRequest.Builder<VideoTranscodingRequest.Builder> { 1169 /** 1170 * Desired output video format of the destination file. 1171 * <p> If this is null, source file's video track will be passed through and 1172 * copied to the destination file. 1173 */ 1174 private @Nullable MediaFormat mVideoTrackFormat = null; 1175 1176 /** 1177 * Desired output audio format of the destination file. 1178 * <p> If this is null, source file's audio track will be passed through and copied 1179 * to the destination file. 1180 */ 1181 private @Nullable MediaFormat mAudioTrackFormat = null; 1182 1183 /** 1184 * Creates a builder for building {@link VideoTranscodingRequest}s. 1185 * 1186 * <p> Client could only specify the settings that matters to them, e.g. codec format or 1187 * bitrate. And by default, transcoding will preserve the original video's settings 1188 * (bitrate, framerate, resolution) if not provided. 1189 * <p>Note that some settings may silently fail to apply if the device does not support 1190 * them. 1191 * @param sourceUri Content uri for the source media file. 1192 * @param destinationUri Content uri for the destination media file. 1193 * @param videoFormat MediaFormat containing the settings that client wants override in 1194 * the original video's video track. 1195 * @throws IllegalArgumentException if videoFormat is invalid. 1196 */ Builder(@onNull Uri sourceUri, @NonNull Uri destinationUri, @NonNull MediaFormat videoFormat)1197 public Builder(@NonNull Uri sourceUri, @NonNull Uri destinationUri, 1198 @NonNull MediaFormat videoFormat) { 1199 super(TRANSCODING_TYPE_VIDEO, sourceUri, destinationUri); 1200 setVideoTrackFormat(videoFormat); 1201 } 1202 1203 @Override 1204 @NonNull setClientUid(int uid)1205 public Builder setClientUid(int uid) { 1206 super.setClientUid(uid); 1207 return self(); 1208 } 1209 1210 @Override 1211 @NonNull setClientPid(int pid)1212 public Builder setClientPid(int pid) { 1213 super.setClientPid(pid); 1214 return self(); 1215 } 1216 1217 @Override 1218 @NonNull setSourceFileDescriptor(@onNull ParcelFileDescriptor fd)1219 public Builder setSourceFileDescriptor(@NonNull ParcelFileDescriptor fd) { 1220 super.setSourceFileDescriptor(fd); 1221 return self(); 1222 } 1223 1224 @Override 1225 @NonNull setDestinationFileDescriptor(@onNull ParcelFileDescriptor fd)1226 public Builder setDestinationFileDescriptor(@NonNull ParcelFileDescriptor fd) { 1227 super.setDestinationFileDescriptor(fd); 1228 return self(); 1229 } 1230 setVideoTrackFormat(@onNull MediaFormat videoFormat)1231 private void setVideoTrackFormat(@NonNull MediaFormat videoFormat) { 1232 if (videoFormat == null) { 1233 throw new IllegalArgumentException("videoFormat must not be null"); 1234 } 1235 1236 // Check if the MediaFormat is for video by looking at the MIME type. 1237 String mime = videoFormat.containsKey(MediaFormat.KEY_MIME) 1238 ? videoFormat.getString(MediaFormat.KEY_MIME) : null; 1239 if (mime == null || !mime.startsWith("video/")) { 1240 throw new IllegalArgumentException("Invalid video format: wrong mime type"); 1241 } 1242 1243 mVideoTrackFormat = videoFormat; 1244 } 1245 1246 /** 1247 * @return a new {@link TranscodingRequest} instance successfully initialized 1248 * with all the parameters set on this <code>Builder</code>. 1249 * @throws UnsupportedOperationException if the parameters set on the 1250 * <code>Builder</code> were incompatible, or 1251 * if they are not supported by the 1252 * device. 1253 */ 1254 @NonNull build()1255 public VideoTranscodingRequest build() { 1256 return new VideoTranscodingRequest(this); 1257 } 1258 1259 @Override self()1260 VideoTranscodingRequest.Builder self() { 1261 return this; 1262 } 1263 } 1264 } 1265 1266 /** 1267 * Handle to an enqueued transcoding operation. An instance of this class represents a single 1268 * enqueued transcoding operation. The caller can use that instance to query the status or 1269 * progress, and to get the result once the operation has completed. 1270 */ 1271 public static final class TranscodingSession { 1272 /** The session is enqueued but not yet running. */ 1273 public static final int STATUS_PENDING = 1; 1274 /** The session is currently running. */ 1275 public static final int STATUS_RUNNING = 2; 1276 /** The session is finished. */ 1277 public static final int STATUS_FINISHED = 3; 1278 /** The session is paused. */ 1279 public static final int STATUS_PAUSED = 4; 1280 1281 /** @hide */ 1282 @IntDef(prefix = { "STATUS_" }, value = { 1283 STATUS_PENDING, 1284 STATUS_RUNNING, 1285 STATUS_FINISHED, 1286 STATUS_PAUSED, 1287 }) 1288 @Retention(RetentionPolicy.SOURCE) 1289 public @interface Status {} 1290 1291 /** The session does not have a result yet. */ 1292 public static final int RESULT_NONE = 1; 1293 /** The session completed successfully. */ 1294 public static final int RESULT_SUCCESS = 2; 1295 /** The session encountered an error while running. */ 1296 public static final int RESULT_ERROR = 3; 1297 /** The session was canceled by the caller. */ 1298 public static final int RESULT_CANCELED = 4; 1299 1300 /** @hide */ 1301 @IntDef(prefix = { "RESULT_" }, value = { 1302 RESULT_NONE, 1303 RESULT_SUCCESS, 1304 RESULT_ERROR, 1305 RESULT_CANCELED, 1306 }) 1307 @Retention(RetentionPolicy.SOURCE) 1308 public @interface Result {} 1309 1310 1311 // The error code exposed here should be in sync with: 1312 // frameworks/av/media/libmediatranscoding/aidl/android/media/TranscodingErrorCode.aidl 1313 /** @hide */ 1314 @IntDef(prefix = { "TRANSCODING_SESSION_ERROR_" }, value = { 1315 ERROR_NONE, 1316 ERROR_DROPPED_BY_SERVICE, 1317 ERROR_SERVICE_DIED}) 1318 @Retention(RetentionPolicy.SOURCE) 1319 public @interface TranscodingSessionErrorCode{} 1320 /** 1321 * Constant indicating that no error occurred. 1322 */ 1323 public static final int ERROR_NONE = 0; 1324 1325 /** 1326 * Constant indicating that the session is dropped by Transcoding service due to hitting 1327 * the limit, e.g. too many back to back transcoding happen in a short time frame. 1328 */ 1329 public static final int ERROR_DROPPED_BY_SERVICE = 1; 1330 1331 /** 1332 * Constant indicating the backing transcoding service is died. Client should enqueue the 1333 * the request again. 1334 */ 1335 public static final int ERROR_SERVICE_DIED = 2; 1336 1337 /** Listener that gets notified when the progress changes. */ 1338 @FunctionalInterface 1339 public interface OnProgressUpdateListener { 1340 /** 1341 * Called when the progress changes. The progress is in percentage between 0 and 1, 1342 * where 0 means the session has not yet started and 100 means that it has finished. 1343 * 1344 * @param session The session associated with the progress. 1345 * @param progress The new progress ranging from 0 ~ 100 inclusive. 1346 */ onProgressUpdate(@onNull TranscodingSession session, @IntRange(from = 0, to = 100) int progress)1347 void onProgressUpdate(@NonNull TranscodingSession session, 1348 @IntRange(from = 0, to = 100) int progress); 1349 } 1350 1351 private final MediaTranscodingManager mManager; 1352 private Executor mListenerExecutor; 1353 private OnTranscodingFinishedListener mListener; 1354 private int mSessionId = -1; 1355 // Lock for internal state. 1356 private final Object mLock = new Object(); 1357 @GuardedBy("mLock") 1358 private Executor mProgressUpdateExecutor = null; 1359 @GuardedBy("mLock") 1360 private OnProgressUpdateListener mProgressUpdateListener = null; 1361 @GuardedBy("mLock") 1362 private int mProgress = 0; 1363 @GuardedBy("mLock") 1364 private int mProgressUpdateInterval = 0; 1365 @GuardedBy("mLock") 1366 private @Status int mStatus = STATUS_PENDING; 1367 @GuardedBy("mLock") 1368 private @Result int mResult = RESULT_NONE; 1369 @GuardedBy("mLock") 1370 private @TranscodingSessionErrorCode int mErrorCode = ERROR_NONE; 1371 @GuardedBy("mLock") 1372 private boolean mHasRetried = false; 1373 // The original request that associated with this session. 1374 private final TranscodingRequest mRequest; 1375 TranscodingSession( @onNull MediaTranscodingManager manager, @NonNull TranscodingRequest request, @NonNull TranscodingSessionParcel parcel, @NonNull @CallbackExecutor Executor executor, @NonNull OnTranscodingFinishedListener listener)1376 private TranscodingSession( 1377 @NonNull MediaTranscodingManager manager, 1378 @NonNull TranscodingRequest request, 1379 @NonNull TranscodingSessionParcel parcel, 1380 @NonNull @CallbackExecutor Executor executor, 1381 @NonNull OnTranscodingFinishedListener listener) { 1382 Objects.requireNonNull(manager, "manager must not be null"); 1383 Objects.requireNonNull(parcel, "parcel must not be null"); 1384 Objects.requireNonNull(executor, "listenerExecutor must not be null"); 1385 Objects.requireNonNull(listener, "listener must not be null"); 1386 mManager = manager; 1387 mSessionId = parcel.sessionId; 1388 mListenerExecutor = executor; 1389 mListener = listener; 1390 mRequest = request; 1391 } 1392 1393 /** 1394 * Set a progress listener. 1395 * @param executor The executor on which listener will be invoked. 1396 * @param listener The progress listener. 1397 */ setOnProgressUpdateListener( @onNull @allbackExecutor Executor executor, @Nullable OnProgressUpdateListener listener)1398 public void setOnProgressUpdateListener( 1399 @NonNull @CallbackExecutor Executor executor, 1400 @Nullable OnProgressUpdateListener listener) { 1401 synchronized (mLock) { 1402 Objects.requireNonNull(executor, "listenerExecutor must not be null"); 1403 Objects.requireNonNull(listener, "listener must not be null"); 1404 mProgressUpdateExecutor = executor; 1405 mProgressUpdateListener = listener; 1406 } 1407 } 1408 updateStatusAndResult(@tatus int sessionStatus, @Result int sessionResult, @TranscodingSessionErrorCode int errorCode)1409 private void updateStatusAndResult(@Status int sessionStatus, 1410 @Result int sessionResult, @TranscodingSessionErrorCode int errorCode) { 1411 synchronized (mLock) { 1412 mStatus = sessionStatus; 1413 mResult = sessionResult; 1414 mErrorCode = errorCode; 1415 } 1416 } 1417 1418 /** 1419 * Retrieve the error code associated with the RESULT_ERROR. 1420 */ getErrorCode()1421 public @TranscodingSessionErrorCode int getErrorCode() { 1422 synchronized (mLock) { 1423 return mErrorCode; 1424 } 1425 } 1426 1427 /** 1428 * Resubmit the transcoding session to the service. 1429 * Note that only the session that fails or gets cancelled could be retried and each session 1430 * could be retried only once. After that, Client need to enqueue a new request if they want 1431 * to try again. 1432 * 1433 * @return true if successfully resubmit the job to service. False otherwise. 1434 * @throws UnsupportedOperationException if the retry could not be fulfilled. 1435 * @hide 1436 */ retry()1437 public boolean retry() { 1438 return retryInternal(true /*setHasRetried*/); 1439 } 1440 1441 // TODO(hkuang): Add more test for it. retryInternal(boolean setHasRetried)1442 private boolean retryInternal(boolean setHasRetried) { 1443 synchronized (mLock) { 1444 if (mStatus == STATUS_PENDING || mStatus == STATUS_RUNNING) { 1445 throw new UnsupportedOperationException( 1446 "Failed to retry as session is in processing"); 1447 } 1448 1449 if (mHasRetried) { 1450 throw new UnsupportedOperationException("Session has been retried already"); 1451 } 1452 1453 // Get the client interface. 1454 ITranscodingClient client = mManager.getTranscodingClient(); 1455 if (client == null) { 1456 Log.e(TAG, "Service rebooting. Try again later"); 1457 return false; 1458 } 1459 1460 synchronized (mManager.mPendingTranscodingSessions) { 1461 try { 1462 // Submits the request to MediaTranscoding service. 1463 TranscodingSessionParcel sessionParcel = new TranscodingSessionParcel(); 1464 if (!client.submitRequest(mRequest.writeToParcel(mManager.mContext), 1465 sessionParcel)) { 1466 mHasRetried = true; 1467 throw new UnsupportedOperationException("Failed to enqueue request"); 1468 } 1469 1470 // Replace the old session id wit the new one. 1471 mSessionId = sessionParcel.sessionId; 1472 // Adds the new session back into pending sessions. 1473 mManager.mPendingTranscodingSessions.put(mSessionId, this); 1474 } catch (RemoteException re) { 1475 return false; 1476 } 1477 mStatus = STATUS_PENDING; 1478 mHasRetried = setHasRetried ? true : false; 1479 } 1480 } 1481 return true; 1482 } 1483 1484 /** 1485 * Cancels the transcoding session and notify the listener. 1486 * If the session happened to finish before being canceled this call is effectively a no-op 1487 * and will not update the result in that case. 1488 */ cancel()1489 public void cancel() { 1490 synchronized (mLock) { 1491 // Check if the session is finished already. 1492 if (mStatus != STATUS_FINISHED) { 1493 try { 1494 ITranscodingClient client = mManager.getTranscodingClient(); 1495 // The client may be gone. 1496 if (client != null) { 1497 client.cancelSession(mSessionId); 1498 } 1499 } catch (RemoteException re) { 1500 //TODO(hkuang): Find out what to do if failing to cancel the session. 1501 Log.e(TAG, "Failed to cancel the session due to exception: " + re); 1502 } 1503 mStatus = STATUS_FINISHED; 1504 mResult = RESULT_CANCELED; 1505 1506 // Notifies client the session is canceled. 1507 mListenerExecutor.execute(() -> mListener.onTranscodingFinished(this)); 1508 } 1509 } 1510 } 1511 1512 /** 1513 * Gets the progress of the transcoding session. The progress is between 0 and 100, where 0 1514 * means that the session has not yet started and 100 means that it is finished. For the 1515 * cancelled session, the progress will be the last updated progress before it is cancelled. 1516 * @return The progress. 1517 */ 1518 @IntRange(from = 0, to = 100) getProgress()1519 public int getProgress() { 1520 synchronized (mLock) { 1521 return mProgress; 1522 } 1523 } 1524 1525 /** 1526 * Gets the status of the transcoding session. 1527 * @return The status. 1528 */ getStatus()1529 public @Status int getStatus() { 1530 synchronized (mLock) { 1531 return mStatus; 1532 } 1533 } 1534 1535 /** 1536 * Adds a client uid that is also waiting for this transcoding session. 1537 * <p> 1538 * Only privilege caller with android.permission.WRITE_MEDIA_STORAGE could add the 1539 * uid. Note that the permission check happens on the service side upon starting the 1540 * transcoding. If the client does not have the permission, the transcoding will fail. 1541 * @param uid the additional client uid to be added. 1542 * @return true if successfully added, false otherwise. 1543 */ addClientUid(int uid)1544 public boolean addClientUid(int uid) { 1545 if (uid < 0) { 1546 throw new IllegalArgumentException("Invalid Uid"); 1547 } 1548 1549 // Get the client interface. 1550 ITranscodingClient client = mManager.getTranscodingClient(); 1551 if (client == null) { 1552 Log.e(TAG, "Service is dead..."); 1553 return false; 1554 } 1555 1556 try { 1557 if (!client.addClientUid(mSessionId, uid)) { 1558 Log.e(TAG, "Failed to add client uid"); 1559 return false; 1560 } 1561 } catch (Exception ex) { 1562 Log.e(TAG, "Failed to get client uids due to " + ex); 1563 return false; 1564 } 1565 return true; 1566 } 1567 1568 /** 1569 * Query all the client that waiting for this transcoding session 1570 * @return a list containing all the client uids. 1571 */ 1572 @NonNull getClientUids()1573 public List<Integer> getClientUids() { 1574 List<Integer> uidList = new ArrayList<Integer>(); 1575 1576 // Get the client interface. 1577 ITranscodingClient client = mManager.getTranscodingClient(); 1578 if (client == null) { 1579 Log.e(TAG, "Service is dead..."); 1580 return uidList; 1581 } 1582 1583 try { 1584 int[] clientUids = client.getClientUids(mSessionId); 1585 for (int i : clientUids) { 1586 uidList.add(i); 1587 } 1588 } catch (Exception ex) { 1589 Log.e(TAG, "Failed to get client uids due to " + ex); 1590 } 1591 1592 return uidList; 1593 } 1594 1595 /** 1596 * Gets sessionId of the transcoding session. 1597 * @return session id. 1598 */ getSessionId()1599 public int getSessionId() { 1600 return mSessionId; 1601 } 1602 1603 /** 1604 * Gets the result of the transcoding session. 1605 * @return The result. 1606 */ getResult()1607 public @Result int getResult() { 1608 synchronized (mLock) { 1609 return mResult; 1610 } 1611 } 1612 1613 @Override toString()1614 public String toString() { 1615 String result; 1616 String status; 1617 1618 switch (mResult) { 1619 case RESULT_NONE: 1620 result = "RESULT_NONE"; 1621 break; 1622 case RESULT_SUCCESS: 1623 result = "RESULT_SUCCESS"; 1624 break; 1625 case RESULT_ERROR: 1626 result = "RESULT_ERROR(" + mErrorCode + ")"; 1627 break; 1628 case RESULT_CANCELED: 1629 result = "RESULT_CANCELED"; 1630 break; 1631 default: 1632 result = String.valueOf(mResult); 1633 break; 1634 } 1635 1636 switch (mStatus) { 1637 case STATUS_PENDING: 1638 status = "STATUS_PENDING"; 1639 break; 1640 case STATUS_PAUSED: 1641 status = "STATUS_PAUSED"; 1642 break; 1643 case STATUS_RUNNING: 1644 status = "STATUS_RUNNING"; 1645 break; 1646 case STATUS_FINISHED: 1647 status = "STATUS_FINISHED"; 1648 break; 1649 default: 1650 status = String.valueOf(mStatus); 1651 break; 1652 } 1653 return String.format(" session: {id: %d, status: %s, result: %s, progress: %d}", 1654 mSessionId, status, result, mProgress); 1655 } 1656 updateProgress(int newProgress)1657 private void updateProgress(int newProgress) { 1658 synchronized (mLock) { 1659 mProgress = newProgress; 1660 } 1661 } 1662 updateStatus(int newStatus)1663 private void updateStatus(int newStatus) { 1664 synchronized (mLock) { 1665 mStatus = newStatus; 1666 } 1667 } 1668 } 1669 getTranscodingClient()1670 private ITranscodingClient getTranscodingClient() { 1671 synchronized (mLock) { 1672 return mTranscodingClient; 1673 } 1674 } 1675 1676 /** 1677 * Enqueues a TranscodingRequest for execution. 1678 * <p> Upon successfully accepting the request, MediaTranscodingManager will return a 1679 * {@link TranscodingSession} to the client. Client should use {@link TranscodingSession} to 1680 * track the progress and get the result. 1681 * <p> MediaTranscodingManager will return null if fails to accept the request due to service 1682 * rebooting. Client could retry again after receiving null. 1683 * 1684 * @param transcodingRequest The TranscodingRequest to enqueue. 1685 * @param listenerExecutor Executor on which the listener is notified. 1686 * @param listener Listener to get notified when the transcoding session is finished. 1687 * @return A TranscodingSession for this operation. 1688 * @throws UnsupportedOperationException if the request could not be fulfilled. 1689 */ 1690 @Nullable enqueueRequest( @onNull TranscodingRequest transcodingRequest, @NonNull @CallbackExecutor Executor listenerExecutor, @NonNull OnTranscodingFinishedListener listener)1691 public TranscodingSession enqueueRequest( 1692 @NonNull TranscodingRequest transcodingRequest, 1693 @NonNull @CallbackExecutor Executor listenerExecutor, 1694 @NonNull OnTranscodingFinishedListener listener) { 1695 Log.i(TAG, "enqueueRequest called."); 1696 Objects.requireNonNull(transcodingRequest, "transcodingRequest must not be null"); 1697 Objects.requireNonNull(listenerExecutor, "listenerExecutor must not be null"); 1698 Objects.requireNonNull(listener, "listener must not be null"); 1699 1700 // Converts the request to TranscodingRequestParcel. 1701 TranscodingRequestParcel requestParcel = transcodingRequest.writeToParcel(mContext); 1702 1703 Log.i(TAG, "Getting transcoding request " + transcodingRequest.getSourceUri()); 1704 1705 // Submits the request to MediaTranscoding service. 1706 try { 1707 TranscodingSessionParcel sessionParcel = new TranscodingSessionParcel(); 1708 // Synchronizes the access to mPendingTranscodingSessions to make sure the session Id is 1709 // inserted in the mPendingTranscodingSessions in the callback handler. 1710 synchronized (mPendingTranscodingSessions) { 1711 synchronized (mLock) { 1712 if (mTranscodingClient == null) { 1713 // Try to register with the service again. 1714 IMediaTranscodingService service = getService(false /*retry*/); 1715 if (service == null) { 1716 Log.w(TAG, "Service rebooting. Try again later"); 1717 return null; 1718 } 1719 mTranscodingClient = registerClient(service); 1720 // If still fails, throws an exception to tell client to try later. 1721 if (mTranscodingClient == null) { 1722 Log.w(TAG, "Service rebooting. Try again later"); 1723 return null; 1724 } 1725 } 1726 1727 if (!mTranscodingClient.submitRequest(requestParcel, sessionParcel)) { 1728 throw new UnsupportedOperationException("Failed to enqueue request"); 1729 } 1730 } 1731 1732 // Wraps the TranscodingSessionParcel into a TranscodingSession and returns it to 1733 // client for tracking. 1734 TranscodingSession session = new TranscodingSession(this, transcodingRequest, 1735 sessionParcel, 1736 listenerExecutor, 1737 listener); 1738 1739 // Adds the new session into pending sessions. 1740 mPendingTranscodingSessions.put(session.getSessionId(), session); 1741 return session; 1742 } 1743 } catch (RemoteException ex) { 1744 Log.w(TAG, "Service rebooting. Try again later"); 1745 return null; 1746 } catch (ServiceSpecificException ex) { 1747 throw new UnsupportedOperationException( 1748 "Failed to submit request to Transcoding service. Error: " + ex); 1749 } 1750 } 1751 } 1752