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