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