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