blob: 5a5ead5ba279fd5d95c6985f3bfba3c60fc1dcf7 [file] [log] [blame]
Aurimas Liutikasbcfbe3a2023-10-30 15:25:21 -07001/*
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
17package android.media;
18
19import android.annotation.CallbackExecutor;
20import android.annotation.IntDef;
21import android.annotation.IntRange;
22import android.annotation.NonNull;
23import android.annotation.Nullable;
24import android.annotation.SystemApi;
25import android.content.ContentResolver;
26import android.content.Context;
27import android.content.res.AssetFileDescriptor;
28import android.net.Uri;
29import android.os.Build;
30import android.os.ParcelFileDescriptor;
31import android.os.RemoteException;
32import android.os.ServiceSpecificException;
33import android.system.Os;
34import android.util.Log;
35
36import androidx.annotation.RequiresApi;
37
38import com.android.internal.annotations.GuardedBy;
39import com.android.internal.annotations.VisibleForTesting;
40import com.android.modules.annotation.MinSdk;
41import com.android.modules.utils.build.SdkLevel;
42
43import java.io.FileNotFoundException;
44import java.lang.annotation.Retention;
45import java.lang.annotation.RetentionPolicy;
46import java.util.ArrayList;
47import java.util.HashMap;
48import java.util.List;
49import java.util.Map;
50import java.util.Objects;
51import java.util.concurrent.Executor;
52import java.util.concurrent.ExecutorService;
53import 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
92public 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 */
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
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
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
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
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 */
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
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
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 */
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 */
543 /* package private */ TranscodingRequest() {
544 }
545
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
563 public int getType() {
564 return mType;
565 }
566
567 /** Return source uri of the transcoding. */
568 @NonNull
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
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. */
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. */
588 public int getClientPid() {
589 return mClientPid;
590 }
591
592 /** Return destination uri of the transcoding. */
593 @NonNull
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
603 public ParcelFileDescriptor getDestinationFileDescriptor() {
604 return mDestinationFileDescriptor;
605 }
606
607 /**
608 * Return priority of the transcoding.
609 * @hide
610 */
611 @TranscodingPriority
612 public int getPriority() {
613 return mPriority;
614 }
615
616 /**
617 * Return TestConfig of the transcoding.
618 * @hide
619 */
620 @Nullable
621 public TranscodingTestConfig getTestConfig() {
622 return mTestConfig;
623 }
624
625 abstract void writeFormatToParcel(TranscodingRequestParcel parcel);
626
627 /* Writes the TranscodingRequest to a parcel. */
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
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 */
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
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
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
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
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
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
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 */
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 */
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
860 /* package */ ApplicationMediaCapabilities getClientCapabilities() {
861 return mClientCaps;
862 }
863
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 */
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 */
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 */
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
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 */
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 */
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
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
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
1085 public MediaFormat getVideoTrackFormat() {
1086 return mVideoTrackFormat;
1087 }
1088
1089 @Override
1090 void writeFormatToParcel(TranscodingRequestParcel parcel) {
1091 parcel.requestedVideoTrackFormat = convertToVideoTrackFormat(mVideoTrackFormat);
1092 }
1093
1094 /* Converts the MediaFormat to TranscodingVideoTrackFormat. */
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 */
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
1195 public Builder setClientUid(int uid) {
1196 super.setClientUid(uid);
1197 return self();
1198 }
1199
1200 @Override
1201 @NonNull
1202 public Builder setClientPid(int pid) {
1203 super.setClientPid(pid);
1204 return self();
1205 }
1206
1207 @Override
1208 @NonNull
1209 public Builder setSourceFileDescriptor(@NonNull ParcelFileDescriptor fd) {
1210 super.setSourceFileDescriptor(fd);
1211 return self();
1212 }
1213
1214 @Override
1215 @NonNull
1216 public Builder setDestinationFileDescriptor(@NonNull ParcelFileDescriptor fd) {
1217 super.setDestinationFileDescriptor(fd);
1218 return self();
1219 }
1220
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
1245 public VideoTranscodingRequest build() {
1246 return new VideoTranscodingRequest(this);
1247 }
1248
1249 @Override
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 */
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
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 */
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. */
1400 public void clearOnProgressUpdateListener() {
1401 synchronized (mLock) {
1402 mProgressUpdateExecutor = null;
1403 mProgressUpdateListener = null;
1404 }
1405 }
1406
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 */
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 */
1435 public boolean retry() {
1436 return retryInternal(true /*setHasRetried*/);
1437 }
1438
1439 // TODO(hkuang): Add more test for it.
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 */
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)
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 */
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 */
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
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 */
1597 public int getSessionId() {
1598 return mSessionId;
1599 }
1600
1601 /**
1602 * Gets the result of the transcoding session.
1603 * @return The result.
1604 */
1605 public @Result int getResult() {
1606 synchronized (mLock) {
1607 return mResult;
1608 }
1609 }
1610
1611 @Override
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
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
1666 private void updateStatus(int newStatus) {
1667 synchronized (mLock) {
1668 mStatus = newStatus;
1669 }
1670 }
1671 }
1672
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
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}