blob: 93d58d07f81af5efc62c932aeffb59c0c4132d6b [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.media;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.system.Os;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.annotation.MinSdk;
import com.android.modules.utils.build.SdkLevel;
import java.io.FileNotFoundException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
Android 12 introduces Compatible media transcoding feature. See
<a href="https://developer.android.com/about/versions/12/features#compatible_media_transcoding">
Compatible media transcoding</a>. MediaTranscodingManager provides an interface to the system's media
transcoding service and can be used to transcode media files, e.g. transcoding a video from HEVC to
AVC.
<h3>Transcoding Types</h3>
<h4>Video Transcoding</h4>
When transcoding a video file, the video track will be transcoded based on the desired track format
and the audio track will be pass through without any modification.
<p class=note>
Note that currently only support transcoding video file in mp4 format and with single video track.
<h3>Transcoding Request</h3>
<p>
To transcode a media file, first create a {@link TranscodingRequest} through its builder class
{@link VideoTranscodingRequest.Builder}. Transcode requests are then enqueue to the manager through
{@link MediaTranscodingManager#enqueueRequest(
TranscodingRequest, Executor, OnTranscodingFinishedListener)}
TranscodeRequest are processed based on client process's priority and request priority. When a
transcode operation is completed the caller is notified via its
{@link OnTranscodingFinishedListener}.
In the meantime the caller may use the returned TranscodingSession object to cancel or check the
status of a specific transcode operation.
<p>
Here is an example where <code>Builder</code> is used to specify all parameters
<pre class=prettyprint>
VideoTranscodingRequest request =
new VideoTranscodingRequest.Builder(srcUri, dstUri, videoFormat).build();
}</pre>
@hide
*/
@MinSdk(Build.VERSION_CODES.S)
@SystemApi
public final class MediaTranscodingManager {
private static final String TAG = "MediaTranscodingManager";
/** Maximum number of retry to connect to the service. */
private static final int CONNECT_SERVICE_RETRY_COUNT = 100;
/** Interval between trying to reconnect to the service. */
private static final int INTERVAL_CONNECT_SERVICE_RETRY_MS = 40;
/** Default bpp(bits-per-pixel) to use for calculating default bitrate. */
private static final float BPP = 0.25f;
/**
* Listener that gets notified when a transcoding operation has finished.
* This listener gets notified regardless of how the operation finished. It is up to the
* listener implementation to check the result and take appropriate action.
*/
@FunctionalInterface
public interface OnTranscodingFinishedListener {
/**
* Called when the transcoding operation has finished. The receiver may use the
* TranscodingSession to check the result, i.e. whether the operation succeeded, was
* canceled or if an error occurred.
*
* @param session The TranscodingSession instance for the finished transcoding operation.
*/
void onTranscodingFinished(@NonNull TranscodingSession session);
}
private final Context mContext;
private ContentResolver mContentResolver;
private final String mPackageName;
private final int mPid;
private final int mUid;
private final boolean mIsLowRamDevice;
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
private final HashMap<Integer, TranscodingSession> mPendingTranscodingSessions = new HashMap();
private final Object mLock = new Object();
@GuardedBy("mLock")
@NonNull private ITranscodingClient mTranscodingClient = null;
private static MediaTranscodingManager sMediaTranscodingManager;
private void handleTranscodingFinished(int sessionId, TranscodingResultParcel result) {
synchronized (mPendingTranscodingSessions) {
// Gets the session associated with the sessionId and removes it from
// mPendingTranscodingSessions.
final TranscodingSession session = mPendingTranscodingSessions.remove(sessionId);
if (session == null) {
// This should not happen in reality.
Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions");
return;
}
// Updates the session status and result.
session.updateStatusAndResult(TranscodingSession.STATUS_FINISHED,
TranscodingSession.RESULT_SUCCESS,
TranscodingSession.ERROR_NONE);
// Notifies client the session is done.
if (session.mListener != null && session.mListenerExecutor != null) {
session.mListenerExecutor.execute(
() -> session.mListener.onTranscodingFinished(session));
}
}
}
private void handleTranscodingFailed(int sessionId, int errorCode) {
synchronized (mPendingTranscodingSessions) {
// Gets the session associated with the sessionId and removes it from
// mPendingTranscodingSessions.
final TranscodingSession session = mPendingTranscodingSessions.remove(sessionId);
if (session == null) {
// This should not happen in reality.
Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions");
return;
}
// Updates the session status and result.
session.updateStatusAndResult(TranscodingSession.STATUS_FINISHED,
TranscodingSession.RESULT_ERROR, errorCode);
// Notifies client the session failed.
if (session.mListener != null && session.mListenerExecutor != null) {
session.mListenerExecutor.execute(
() -> session.mListener.onTranscodingFinished(session));
}
}
}
private void handleTranscodingProgressUpdate(int sessionId, int newProgress) {
synchronized (mPendingTranscodingSessions) {
// Gets the session associated with the sessionId.
final TranscodingSession session = mPendingTranscodingSessions.get(sessionId);
if (session == null) {
// This should not happen in reality.
Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions");
return;
}
// Updates the session progress.
session.updateProgress(newProgress);
// Notifies client the progress update.
if (session.mProgressUpdateExecutor != null
&& session.mProgressUpdateListener != null) {
session.mProgressUpdateExecutor.execute(
() -> session.mProgressUpdateListener.onProgressUpdate(session,
newProgress));
}
}
}
private IMediaTranscodingService getService(boolean retry) {
// Do not try to get the service on pre-S. The service is lazy-start and getting the
// service could block.
if (!SdkLevel.isAtLeastS()) {
return null;
}
// Do not try to get the service on AndroidGo (low-ram) devices.
if (mIsLowRamDevice) {
return null;
}
int retryCount = !retry ? 1 : CONNECT_SERVICE_RETRY_COUNT;
Log.i(TAG, "get service with retry " + retryCount);
for (int count = 1; count <= retryCount; count++) {
Log.d(TAG, "Trying to connect to service. Try count: " + count);
IMediaTranscodingService service = IMediaTranscodingService.Stub.asInterface(
MediaFrameworkInitializer
.getMediaServiceManager()
.getMediaTranscodingServiceRegisterer()
.get());
if (service != null) {
return service;
}
try {
// Sleep a bit before retry.
Thread.sleep(INTERVAL_CONNECT_SERVICE_RETRY_MS);
} catch (InterruptedException ie) {
/* ignore */
}
}
Log.w(TAG, "Failed to get service");
return null;
}
/*
* Handle client binder died event.
* Upon receiving a binder died event of the client, we will do the following:
* 1) For the session that is running, notify the client that the session is failed with
* error code, so client could choose to retry the session or not.
* TODO(hkuang): Add a new error code to signal service died error.
* 2) For the sessions that is still pending or paused, we will resubmit the session
* once we successfully reconnect to the service and register a new client.
* 3) When trying to connect to the service and register a new client. The service may need time
* to reboot or never boot up again. So we will retry for a number of times. If we still
* could not connect, we will notify client session failure for the pending and paused
* sessions.
*/
private void onClientDied() {
synchronized (mLock) {
mTranscodingClient = null;
}
// Delegates the session notification and retry to the executor as it may take some time.
mExecutor.execute(() -> {
// List to track the sessions that we want to retry.
List<TranscodingSession> retrySessions = new ArrayList<TranscodingSession>();
// First notify the client of session failure for all the running sessions.
synchronized (mPendingTranscodingSessions) {
for (Map.Entry<Integer, TranscodingSession> entry :
mPendingTranscodingSessions.entrySet()) {
TranscodingSession session = entry.getValue();
if (session.getStatus() == TranscodingSession.STATUS_RUNNING) {
session.updateStatusAndResult(TranscodingSession.STATUS_FINISHED,
TranscodingSession.RESULT_ERROR,
TranscodingSession.ERROR_SERVICE_DIED);
// Remove the session from pending sessions.
mPendingTranscodingSessions.remove(entry.getKey());
if (session.mListener != null && session.mListenerExecutor != null) {
Log.i(TAG, "Notify client session failed");
session.mListenerExecutor.execute(
() -> session.mListener.onTranscodingFinished(session));
}
} else if (session.getStatus() == TranscodingSession.STATUS_PENDING
|| session.getStatus() == TranscodingSession.STATUS_PAUSED) {
// Add the session to retrySessions to handle them later.
retrySessions.add(session);
}
}
}
// Try to register with the service once it boots up.
IMediaTranscodingService service = getService(true /*retry*/);
boolean haveTranscodingClient = false;
if (service != null) {
synchronized (mLock) {
mTranscodingClient = registerClient(service);
if (mTranscodingClient != null) {
haveTranscodingClient = true;
}
}
}
for (TranscodingSession session : retrySessions) {
// Notify the session failure if we fails to connect to the service or fail
// to retry the session.
if (!haveTranscodingClient) {
// TODO(hkuang): Return correct error code to the client.
handleTranscodingFailed(session.getSessionId(), 0 /*unused */);
}
try {
// Do not set hasRetried for retry initiated by MediaTranscodingManager.
session.retryInternal(false /*setHasRetried*/);
} catch (Exception re) {
// TODO(hkuang): Return correct error code to the client.
handleTranscodingFailed(session.getSessionId(), 0 /*unused */);
}
}
});
}
private void updateStatus(int sessionId, int status) {
synchronized (mPendingTranscodingSessions) {
final TranscodingSession session = mPendingTranscodingSessions.get(sessionId);
if (session == null) {
// This should not happen in reality.
Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions");
return;
}
// Updates the session status.
session.updateStatus(status);
}
}
// Just forwards all the events to the event handler.
private ITranscodingClientCallback mTranscodingClientCallback =
new ITranscodingClientCallback.Stub() {
// TODO(hkuang): Add more unit test to test difference file open mode.
@Override
public ParcelFileDescriptor openFileDescriptor(String fileUri, String mode)
throws RemoteException {
if (!mode.equals("r") && !mode.equals("w") && !mode.equals("rw")) {
Log.e(TAG, "Unsupport mode: " + mode);
return null;
}
Uri uri = Uri.parse(fileUri);
try {
AssetFileDescriptor afd = mContentResolver.openAssetFileDescriptor(uri,
mode);
if (afd != null) {
return afd.getParcelFileDescriptor();
}
} catch (FileNotFoundException e) {
Log.w(TAG, "Cannot find content uri: " + uri, e);
} catch (SecurityException e) {
Log.w(TAG, "Cannot open content uri: " + uri, e);
} catch (Exception e) {
Log.w(TAG, "Unknown content uri: " + uri, e);
}
return null;
}
@Override
public void onTranscodingStarted(int sessionId) throws RemoteException {
updateStatus(sessionId, TranscodingSession.STATUS_RUNNING);
}
@Override
public void onTranscodingPaused(int sessionId) throws RemoteException {
updateStatus(sessionId, TranscodingSession.STATUS_PAUSED);
}
@Override
public void onTranscodingResumed(int sessionId) throws RemoteException {
updateStatus(sessionId, TranscodingSession.STATUS_RUNNING);
}
@Override
public void onTranscodingFinished(int sessionId, TranscodingResultParcel result)
throws RemoteException {
handleTranscodingFinished(sessionId, result);
}
@Override
public void onTranscodingFailed(int sessionId, int errorCode)
throws RemoteException {
handleTranscodingFailed(sessionId, errorCode);
}
@Override
public void onAwaitNumberOfSessionsChanged(int sessionId, int oldAwaitNumber,
int newAwaitNumber) throws RemoteException {
//TODO(hkuang): Implement this.
}
@Override
public void onProgressUpdate(int sessionId, int newProgress)
throws RemoteException {
handleTranscodingProgressUpdate(sessionId, newProgress);
}
};
private ITranscodingClient registerClient(IMediaTranscodingService service) {
synchronized (mLock) {
try {
// Registers the client with MediaTranscoding service.
mTranscodingClient = service.registerClient(
mTranscodingClientCallback,
mPackageName,
mPackageName);
if (mTranscodingClient != null) {
mTranscodingClient.asBinder().linkToDeath(() -> onClientDied(), /* flags */ 0);
}
} catch (Exception ex) {
Log.e(TAG, "Failed to register new client due to exception " + ex);
mTranscodingClient = null;
}
}
return mTranscodingClient;
}
/**
* @hide
*/
public MediaTranscodingManager(@NonNull Context context) {
mContext = context;
mContentResolver = mContext.getContentResolver();
mPackageName = mContext.getPackageName();
mUid = Os.getuid();
mPid = Os.getpid();
mIsLowRamDevice = mContext.getSystemService(ActivityManager.class).isLowRamDevice();
}
/**
* Abstract base class for all the TranscodingRequest.
* <p> TranscodingRequest encapsulates the desired configuration for the transcoding.
*/
public abstract static class TranscodingRequest {
/**
*
* Default transcoding type.
* @hide
*/
public static final int TRANSCODING_TYPE_UNKNOWN = 0;
/**
* TRANSCODING_TYPE_VIDEO indicates that client wants to perform transcoding on a video.
* <p>Note that currently only support transcoding video file in mp4 format.
* @hide
*/
public static final int TRANSCODING_TYPE_VIDEO = 1;
/**
* TRANSCODING_TYPE_IMAGE indicates that client wants to perform transcoding on an image.
* @hide
*/
public static final int TRANSCODING_TYPE_IMAGE = 2;
/** @hide */
@IntDef(prefix = {"TRANSCODING_TYPE_"}, value = {
TRANSCODING_TYPE_UNKNOWN,
TRANSCODING_TYPE_VIDEO,
TRANSCODING_TYPE_IMAGE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface TranscodingType {}
/**
* Default value.
*
* @hide
*/
public static final int PRIORITY_UNKNOWN = 0;
/**
* PRIORITY_REALTIME indicates that the transcoding request is time-critical and that the
* client wants the transcoding result as soon as possible.
* <p> Set PRIORITY_REALTIME only if the transcoding is time-critical as it will involve
* performance penalty due to resource reallocation to prioritize the sessions with higher
* priority.
*
* @hide
*/
public static final int PRIORITY_REALTIME = 1;
/**
* PRIORITY_OFFLINE indicates the transcoding is not time-critical and the client does not
* need the transcoding result as soon as possible.
* <p>Sessions with PRIORITY_OFFLINE will be scheduled behind PRIORITY_REALTIME. Always set
* to
* PRIORITY_OFFLINE if client does not need the result as soon as possible and could accept
* delay of the transcoding result.
*
* @hide
*
*/
public static final int PRIORITY_OFFLINE = 2;
/** @hide */
@IntDef(prefix = {"PRIORITY_"}, value = {
PRIORITY_UNKNOWN,
PRIORITY_REALTIME,
PRIORITY_OFFLINE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface TranscodingPriority {}
/** Uri of the source media file. */
private @NonNull Uri mSourceUri;
/** Uri of the destination media file. */
private @NonNull Uri mDestinationUri;
/** FileDescriptor of the source media file. */
private @Nullable ParcelFileDescriptor mSourceFileDescriptor;
/** FileDescriptor of the destination media file. */
private @Nullable ParcelFileDescriptor mDestinationFileDescriptor;
/**
* The UID of the client that the TranscodingRequest is for. Only privileged caller could
* set this Uid as only they could do the transcoding on behalf of the client.
* -1 means not available.
*/
private int mClientUid = -1;
/**
* The Pid of the client that the TranscodingRequest is for. Only privileged caller could
* set this Uid as only they could do the transcoding on behalf of the client.
* -1 means not available.
*/
private int mClientPid = -1;
/** Type of the transcoding. */
private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN;
/** Priority of the transcoding. */
private @TranscodingPriority int mPriority = PRIORITY_UNKNOWN;
/**
* Desired image format for the destination file.
* <p> If this is null, source file's image track will be passed through and copied to the
* destination file.
* @hide
*/
private @Nullable MediaFormat mImageFormat = null;
@VisibleForTesting
private TranscodingTestConfig mTestConfig = null;
/**
* Prevent public constructor access.
*/
/* package private */ TranscodingRequest() {
}
private TranscodingRequest(Builder b) {
mSourceUri = b.mSourceUri;
mSourceFileDescriptor = b.mSourceFileDescriptor;
mDestinationUri = b.mDestinationUri;
mDestinationFileDescriptor = b.mDestinationFileDescriptor;
mClientUid = b.mClientUid;
mClientPid = b.mClientPid;
mPriority = b.mPriority;
mType = b.mType;
mTestConfig = b.mTestConfig;
}
/**
* Return the type of the transcoding.
* @hide
*/
@TranscodingType
public int getType() {
return mType;
}
/** Return source uri of the transcoding. */
@NonNull
public Uri getSourceUri() {
return mSourceUri;
}
/**
* Return source file descriptor of the transcoding.
* This will be null if client has not provided it.
*/
@Nullable
public ParcelFileDescriptor getSourceFileDescriptor() {
return mSourceFileDescriptor;
}
/** Return the UID of the client that this request is for. -1 means not available. */
public int getClientUid() {
return mClientUid;
}
/** Return the PID of the client that this request is for. -1 means not available. */
public int getClientPid() {
return mClientPid;
}
/** Return destination uri of the transcoding. */
@NonNull
public Uri getDestinationUri() {
return mDestinationUri;
}
/**
* Return destination file descriptor of the transcoding.
* This will be null if client has not provided it.
*/
@Nullable
public ParcelFileDescriptor getDestinationFileDescriptor() {
return mDestinationFileDescriptor;
}
/**
* Return priority of the transcoding.
* @hide
*/
@TranscodingPriority
public int getPriority() {
return mPriority;
}
/**
* Return TestConfig of the transcoding.
* @hide
*/
@Nullable
public TranscodingTestConfig getTestConfig() {
return mTestConfig;
}
abstract void writeFormatToParcel(TranscodingRequestParcel parcel);
/* Writes the TranscodingRequest to a parcel. */
private TranscodingRequestParcel writeToParcel(@NonNull Context context) {
TranscodingRequestParcel parcel = new TranscodingRequestParcel();
switch (mPriority) {
case PRIORITY_OFFLINE:
parcel.priority = TranscodingSessionPriority.kUnspecified;
break;
case PRIORITY_REALTIME:
case PRIORITY_UNKNOWN:
default:
parcel.priority = TranscodingSessionPriority.kNormal;
break;
}
parcel.transcodingType = mType;
parcel.sourceFilePath = mSourceUri.toString();
parcel.sourceFd = mSourceFileDescriptor;
parcel.destinationFilePath = mDestinationUri.toString();
parcel.destinationFd = mDestinationFileDescriptor;
parcel.clientUid = mClientUid;
parcel.clientPid = mClientPid;
if (mClientUid < 0) {
parcel.clientPackageName = context.getPackageName();
} else {
String packageName = context.getPackageManager().getNameForUid(mClientUid);
// PackageName is optional as some uid does not have package name. Set to
// "Unavailable" string in this case.
if (packageName == null) {
Log.w(TAG, "Failed to find package for uid: " + mClientUid);
packageName = "Unavailable";
}
parcel.clientPackageName = packageName;
}
writeFormatToParcel(parcel);
if (mTestConfig != null) {
parcel.isForTesting = true;
parcel.testConfig = mTestConfig;
}
return parcel;
}
/**
* Builder to build a {@link TranscodingRequest} object.
*
* @param <T> The subclass to be built.
*/
abstract static class Builder<T extends Builder<T>> {
private @NonNull Uri mSourceUri;
private @NonNull Uri mDestinationUri;
private @Nullable ParcelFileDescriptor mSourceFileDescriptor = null;
private @Nullable ParcelFileDescriptor mDestinationFileDescriptor = null;
private int mClientUid = -1;
private int mClientPid = -1;
private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN;
private @TranscodingPriority int mPriority = PRIORITY_UNKNOWN;
private TranscodingTestConfig mTestConfig;
abstract T self();
/**
* Creates a builder for building {@link TranscodingRequest}s.
*
* Client must set the source Uri. If client also provides the source fileDescriptor
* through is provided by {@link #setSourceFileDescriptor(ParcelFileDescriptor)},
* TranscodingSession will use the fd instead of calling back to the client to open the
* sourceUri.
*
*
* @param type The transcoding type.
* @param sourceUri Content uri for the source media file.
* @param destinationUri Content uri for the destination media file.
*
*/
private Builder(@TranscodingType int type, @NonNull Uri sourceUri,
@NonNull Uri destinationUri) {
mType = type;
if (sourceUri == null || Uri.EMPTY.equals(sourceUri)) {
throw new IllegalArgumentException(
"You must specify a non-empty source Uri.");
}
mSourceUri = sourceUri;
if (destinationUri == null || Uri.EMPTY.equals(destinationUri)) {
throw new IllegalArgumentException(
"You must specify a non-empty destination Uri.");
}
mDestinationUri = destinationUri;
}
/**
* Specifies the fileDescriptor opened from the source media file.
*
* This call is optional. If the source fileDescriptor is provided, TranscodingSession
* will use it directly instead of opening the uri from {@link #Builder(int, Uri, Uri)}.
* It is client's responsibility to make sure the fileDescriptor is opened from the
* source uri.
* @param fileDescriptor a {@link ParcelFileDescriptor} opened from source media file.
* @return The same builder instance.
* @throws IllegalArgumentException if fileDescriptor is invalid.
*/
@NonNull
public T setSourceFileDescriptor(@NonNull ParcelFileDescriptor fileDescriptor) {
if (fileDescriptor == null || fileDescriptor.getFd() < 0) {
throw new IllegalArgumentException(
"Invalid source descriptor.");
}
mSourceFileDescriptor = fileDescriptor;
return self();
}
/**
* Specifies the fileDescriptor opened from the destination media file.
*
* This call is optional. If the destination fileDescriptor is provided,
* TranscodingSession will use it directly instead of opening the source uri from
* {@link #Builder(int, Uri, Uri)} upon transcoding starts. It is client's
* responsibility to make sure the fileDescriptor is opened from the destination uri.
* @param fileDescriptor a {@link ParcelFileDescriptor} opened from destination media
* file.
* @return The same builder instance.
* @throws IllegalArgumentException if fileDescriptor is invalid.
*/
@NonNull
public T setDestinationFileDescriptor(
@NonNull ParcelFileDescriptor fileDescriptor) {
if (fileDescriptor == null || fileDescriptor.getFd() < 0) {
throw new IllegalArgumentException(
"Invalid destination descriptor.");
}
mDestinationFileDescriptor = fileDescriptor;
return self();
}
/**
* Specify the UID of the client that this request is for.
* <p>
* Only privilege caller with android.permission.WRITE_MEDIA_STORAGE could forward the
* pid. Note that the permission check happens on the service side upon starting the
* transcoding. If the client does not have the permission, the transcoding will fail.
*
* @param uid client Uid.
* @return The same builder instance.
* @throws IllegalArgumentException if uid is invalid.
*/
@NonNull
public T setClientUid(int uid) {
if (uid < 0) {
throw new IllegalArgumentException("Invalid Uid");
}
mClientUid = uid;
return self();
}
/**
* Specify the pid of the client that this request is for.
* <p>
* Only privilege caller with android.permission.WRITE_MEDIA_STORAGE could forward the
* pid. Note that the permission check happens on the service side upon starting the
* transcoding. If the client does not have the permission, the transcoding will fail.
*
* @param pid client Pid.
* @return The same builder instance.
* @throws IllegalArgumentException if pid is invalid.
*/
@NonNull
public T setClientPid(int pid) {
if (pid < 0) {
throw new IllegalArgumentException("Invalid pid");
}
mClientPid = pid;
return self();
}
/**
* Specifies the priority of the transcoding.
*
* @param priority Must be one of the {@code PRIORITY_*}
* @return The same builder instance.
* @throws IllegalArgumentException if flags is invalid.
* @hide
*/
@NonNull
public T setPriority(@TranscodingPriority int priority) {
if (priority != PRIORITY_OFFLINE && priority != PRIORITY_REALTIME) {
throw new IllegalArgumentException("Invalid priority: " + priority);
}
mPriority = priority;
return self();
}
/**
* Sets the delay in processing this request.
* @param config test config.
* @return The same builder instance.
* @hide
*/
@VisibleForTesting
@NonNull
public T setTestConfig(@NonNull TranscodingTestConfig config) {
mTestConfig = config;
return self();
}
}
/**
* Abstract base class for all the format resolvers.
*/
abstract static class MediaFormatResolver {
private @NonNull ApplicationMediaCapabilities mClientCaps;
/**
* Prevents public constructor access.
*/
/* package private */ MediaFormatResolver() {
}
/**
* Constructs MediaFormatResolver object.
*
* @param clientCaps An ApplicationMediaCapabilities object containing the client's
* capabilities.
*/
MediaFormatResolver(@NonNull ApplicationMediaCapabilities clientCaps) {
if (clientCaps == null) {
throw new IllegalArgumentException("Client capabilities must not be null");
}
mClientCaps = clientCaps;
}
/**
* Returns the client capabilities.
*/
@NonNull
/* package */ ApplicationMediaCapabilities getClientCapabilities() {
return mClientCaps;
}
abstract boolean shouldTranscode();
}
/**
* VideoFormatResolver for deciding if video transcoding is needed, and if so, the track
* formats to use.
*/
public static class VideoFormatResolver extends MediaFormatResolver {
private static final int BIT_RATE = 20000000; // 20Mbps
private MediaFormat mSrcVideoFormatHint;
private MediaFormat mSrcAudioFormatHint;
/**
* Constructs a new VideoFormatResolver object.
*
* @param clientCaps An ApplicationMediaCapabilities object containing the client's
* capabilities.
* @param srcVideoFormatHint A MediaFormat object containing information about the
* source's video track format that could affect the
* transcoding decision. Such information could include video
* codec types, color spaces, whether special format info (eg.
* slow-motion markers) are present, etc.. If a particular
* information is not present, it will not be used to make the
* decision.
*/
public VideoFormatResolver(@NonNull ApplicationMediaCapabilities clientCaps,
@NonNull MediaFormat srcVideoFormatHint) {
super(clientCaps);
mSrcVideoFormatHint = srcVideoFormatHint;
}
/**
* Constructs a new VideoFormatResolver object.
*
* @param clientCaps An ApplicationMediaCapabilities object containing the client's
* capabilities.
* @param srcVideoFormatHint A MediaFormat object containing information about the
* source's video track format that could affect the
* transcoding decision. Such information could include video
* codec types, color spaces, whether special format info (eg.
* slow-motion markers) are present, etc.. If a particular
* information is not present, it will not be used to make the
* decision.
* @param srcAudioFormatHint A MediaFormat object containing information about the
* source's audio track format that could affect the
* transcoding decision.
* @hide
*/
VideoFormatResolver(@NonNull ApplicationMediaCapabilities clientCaps,
@NonNull MediaFormat srcVideoFormatHint,
@NonNull MediaFormat srcAudioFormatHint) {
super(clientCaps);
mSrcVideoFormatHint = srcVideoFormatHint;
mSrcAudioFormatHint = srcAudioFormatHint;
}
/**
* Returns whether the source content should be transcoded.
*
* @return true if the source should be transcoded.
*/
public boolean shouldTranscode() {
boolean supportHevc = getClientCapabilities().isVideoMimeTypeSupported(
MediaFormat.MIMETYPE_VIDEO_HEVC);
if (!supportHevc && MediaFormat.MIMETYPE_VIDEO_HEVC.equals(
mSrcVideoFormatHint.getString(MediaFormat.KEY_MIME))) {
return true;
}
// TODO: add more checks as needed below.
return false;
}
/**
* Retrieves the video track format to be used on
* {@link VideoTranscodingRequest.Builder#setVideoTrackFormat(MediaFormat)} for this
* configuration.
*
* @return the video track format to be used if transcoding should be performed,
* and null otherwise.
*/
@Nullable
public MediaFormat resolveVideoFormat() {
if (!shouldTranscode()) {
return null;
}
MediaFormat videoTrackFormat = new MediaFormat(mSrcVideoFormatHint);
videoTrackFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
int width = mSrcVideoFormatHint.getInteger(MediaFormat.KEY_WIDTH);
int height = mSrcVideoFormatHint.getInteger(MediaFormat.KEY_HEIGHT);
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException(
"Source Width and height must be larger than 0");
}
float frameRate = 30.0f; // default to 30fps.
if (mSrcVideoFormatHint.containsKey(MediaFormat.KEY_FRAME_RATE)) {
frameRate = mSrcVideoFormatHint.getFloat(MediaFormat.KEY_FRAME_RATE);
if (frameRate <= 0) {
throw new IllegalArgumentException(
"frameRate must be larger than 0");
}
}
int bitrate = getAVCBitrate(width, height, frameRate);
videoTrackFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
return videoTrackFormat;
}
/**
* Generate a default bitrate with the fixed bpp(bits-per-pixel) 0.25.
* This maps to:
* 1080P@30fps -> 16Mbps
* 1080P@60fps-> 32Mbps
* 4K@30fps -> 62Mbps
*/
private static int getDefaultBitrate(int width, int height, float frameRate) {
return (int) (width * height * frameRate * BPP);
}
/**
* Query the bitrate from CamcorderProfile. If there are two profiles that match the
* width/height/framerate, we will use the higher one to get better quality.
* Return default bitrate if could not find any match profile.
*/
private static int getAVCBitrate(int width, int height, float frameRate) {
int bitrate = -1;
int[] cameraIds = {0, 1};
// Profiles ordered in decreasing order of preference.
int[] preferQualities = {
CamcorderProfile.QUALITY_2160P,
CamcorderProfile.QUALITY_1080P,
CamcorderProfile.QUALITY_720P,
CamcorderProfile.QUALITY_480P,
CamcorderProfile.QUALITY_LOW,
};
for (int cameraId : cameraIds) {
for (int quality : preferQualities) {
// Check if camera id has profile for the quality level.
if (!CamcorderProfile.hasProfile(cameraId, quality)) {
continue;
}
CamcorderProfile profile = CamcorderProfile.get(cameraId, quality);
// Check the width/height/framerate/codec, also consider portrait case.
if (((width == profile.videoFrameWidth
&& height == profile.videoFrameHeight)
|| (height == profile.videoFrameWidth
&& width == profile.videoFrameHeight))
&& (int) frameRate == profile.videoFrameRate
&& profile.videoCodec == MediaRecorder.VideoEncoder.H264) {
if (bitrate < profile.videoBitRate) {
bitrate = profile.videoBitRate;
}
break;
}
}
}
if (bitrate == -1) {
Log.w(TAG, "Failed to find CamcorderProfile for w: " + width + "h: " + height
+ " fps: "
+ frameRate);
bitrate = getDefaultBitrate(width, height, frameRate);
}
Log.d(TAG, "Using bitrate " + bitrate + " for " + width + " " + height + " "
+ frameRate);
return bitrate;
}
/**
* Retrieves the audio track format to be used for transcoding.
*
* @return the audio track format to be used if transcoding should be performed, and
* null otherwise.
* @hide
*/
@Nullable
public MediaFormat resolveAudioFormat() {
if (!shouldTranscode()) {
return null;
}
// Audio transcoding is not supported yet, always return null.
return null;
}
}
}
/**
* VideoTranscodingRequest encapsulates the configuration for transcoding a video.
*/
public static final class VideoTranscodingRequest extends TranscodingRequest {
/**
* Desired output video format of the destination file.
* <p> If this is null, source file's video track will be passed through and copied to the
* destination file.
*/
private @Nullable MediaFormat mVideoTrackFormat = null;
/**
* Desired output audio format of the destination file.
* <p> If this is null, source file's audio track will be passed through and copied to the
* destination file.
*/
private @Nullable MediaFormat mAudioTrackFormat = null;
private VideoTranscodingRequest(VideoTranscodingRequest.Builder builder) {
super(builder);
mVideoTrackFormat = builder.mVideoTrackFormat;
mAudioTrackFormat = builder.mAudioTrackFormat;
}
/**
* Return the video track format of the transcoding.
* This will be null if client has not specified the video track format.
*/
@NonNull
public MediaFormat getVideoTrackFormat() {
return mVideoTrackFormat;
}
@Override
void writeFormatToParcel(TranscodingRequestParcel parcel) {
parcel.requestedVideoTrackFormat = convertToVideoTrackFormat(mVideoTrackFormat);
}
/* Converts the MediaFormat to TranscodingVideoTrackFormat. */
private static TranscodingVideoTrackFormat convertToVideoTrackFormat(MediaFormat format) {
if (format == null) {
throw new IllegalArgumentException("Invalid MediaFormat");
}
TranscodingVideoTrackFormat trackFormat = new TranscodingVideoTrackFormat();
if (format.containsKey(MediaFormat.KEY_MIME)) {
String mime = format.getString(MediaFormat.KEY_MIME);
if (MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) {
trackFormat.codecType = TranscodingVideoCodecType.kAvc;
} else if (MediaFormat.MIMETYPE_VIDEO_HEVC.equals(mime)) {
trackFormat.codecType = TranscodingVideoCodecType.kHevc;
} else {
throw new UnsupportedOperationException("Only support transcode to avc/hevc");
}
}
if (format.containsKey(MediaFormat.KEY_BIT_RATE)) {
int bitrateBps = format.getInteger(MediaFormat.KEY_BIT_RATE);
if (bitrateBps <= 0) {
throw new IllegalArgumentException("Bitrate must be larger than 0");
}
trackFormat.bitrateBps = bitrateBps;
}
if (format.containsKey(MediaFormat.KEY_WIDTH) && format.containsKey(
MediaFormat.KEY_HEIGHT)) {
int width = format.getInteger(MediaFormat.KEY_WIDTH);
int height = format.getInteger(MediaFormat.KEY_HEIGHT);
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Width and height must be larger than 0");
}
// TODO: Validate the aspect ratio after adding scaling.
trackFormat.width = width;
trackFormat.height = height;
}
if (format.containsKey(MediaFormat.KEY_PROFILE)) {
int profile = format.getInteger(MediaFormat.KEY_PROFILE);
if (profile <= 0) {
throw new IllegalArgumentException("Invalid codec profile");
}
// TODO: Validate the profile according to codec type.
trackFormat.profile = profile;
}
if (format.containsKey(MediaFormat.KEY_LEVEL)) {
int level = format.getInteger(MediaFormat.KEY_LEVEL);
if (level <= 0) {
throw new IllegalArgumentException("Invalid codec level");
}
// TODO: Validate the level according to codec type.
trackFormat.level = level;
}
return trackFormat;
}
/**
* Builder class for {@link VideoTranscodingRequest}.
*/
public static final class Builder extends
TranscodingRequest.Builder<VideoTranscodingRequest.Builder> {
/**
* Desired output video format of the destination file.
* <p> If this is null, source file's video track will be passed through and
* copied to the destination file.
*/
private @Nullable MediaFormat mVideoTrackFormat = null;
/**
* Desired output audio format of the destination file.
* <p> If this is null, source file's audio track will be passed through and copied
* to the destination file.
*/
private @Nullable MediaFormat mAudioTrackFormat = null;
/**
* Creates a builder for building {@link VideoTranscodingRequest}s.
*
* <p> Client could only specify the settings that matters to them, e.g. codec format or
* bitrate. And by default, transcoding will preserve the original video's settings
* (bitrate, framerate, resolution) if not provided.
* <p>Note that some settings may silently fail to apply if the device does not support
* them.
* @param sourceUri Content uri for the source media file.
* @param destinationUri Content uri for the destination media file.
* @param videoFormat MediaFormat containing the settings that client wants override in
* the original video's video track.
* @throws IllegalArgumentException if videoFormat is invalid.
*/
public Builder(@NonNull Uri sourceUri, @NonNull Uri destinationUri,
@NonNull MediaFormat videoFormat) {
super(TRANSCODING_TYPE_VIDEO, sourceUri, destinationUri);
setVideoTrackFormat(videoFormat);
}
@Override
@NonNull
public Builder setClientUid(int uid) {
super.setClientUid(uid);
return self();
}
@Override
@NonNull
public Builder setClientPid(int pid) {
super.setClientPid(pid);
return self();
}
@Override
@NonNull
public Builder setSourceFileDescriptor(@NonNull ParcelFileDescriptor fd) {
super.setSourceFileDescriptor(fd);
return self();
}
@Override
@NonNull
public Builder setDestinationFileDescriptor(@NonNull ParcelFileDescriptor fd) {
super.setDestinationFileDescriptor(fd);
return self();
}
private void setVideoTrackFormat(@NonNull MediaFormat videoFormat) {
if (videoFormat == null) {
throw new IllegalArgumentException("videoFormat must not be null");
}
// Check if the MediaFormat is for video by looking at the MIME type.
String mime = videoFormat.containsKey(MediaFormat.KEY_MIME)
? videoFormat.getString(MediaFormat.KEY_MIME) : null;
if (mime == null || !mime.startsWith("video/")) {
throw new IllegalArgumentException("Invalid video format: wrong mime type");
}
mVideoTrackFormat = videoFormat;
}
/**
* @return a new {@link TranscodingRequest} instance successfully initialized
* with all the parameters set on this <code>Builder</code>.
* @throws UnsupportedOperationException if the parameters set on the
* <code>Builder</code> were incompatible, or
* if they are not supported by the
* device.
*/
@NonNull
public VideoTranscodingRequest build() {
return new VideoTranscodingRequest(this);
}
@Override
VideoTranscodingRequest.Builder self() {
return this;
}
}
}
/**
* Handle to an enqueued transcoding operation. An instance of this class represents a single
* enqueued transcoding operation. The caller can use that instance to query the status or
* progress, and to get the result once the operation has completed.
*/
public static final class TranscodingSession {
/** The session is enqueued but not yet running. */
public static final int STATUS_PENDING = 1;
/** The session is currently running. */
public static final int STATUS_RUNNING = 2;
/** The session is finished. */
public static final int STATUS_FINISHED = 3;
/** The session is paused. */
public static final int STATUS_PAUSED = 4;
/** @hide */
@IntDef(prefix = { "STATUS_" }, value = {
STATUS_PENDING,
STATUS_RUNNING,
STATUS_FINISHED,
STATUS_PAUSED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Status {}
/** The session does not have a result yet. */
public static final int RESULT_NONE = 1;
/** The session completed successfully. */
public static final int RESULT_SUCCESS = 2;
/** The session encountered an error while running. */
public static final int RESULT_ERROR = 3;
/** The session was canceled by the caller. */
public static final int RESULT_CANCELED = 4;
/** @hide */
@IntDef(prefix = { "RESULT_" }, value = {
RESULT_NONE,
RESULT_SUCCESS,
RESULT_ERROR,
RESULT_CANCELED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Result {}
// The error code exposed here should be in sync with:
// frameworks/av/media/libmediatranscoding/aidl/android/media/TranscodingErrorCode.aidl
/** @hide */
@IntDef(prefix = { "TRANSCODING_SESSION_ERROR_" }, value = {
ERROR_NONE,
ERROR_DROPPED_BY_SERVICE,
ERROR_SERVICE_DIED})
@Retention(RetentionPolicy.SOURCE)
public @interface TranscodingSessionErrorCode{}
/**
* Constant indicating that no error occurred.
*/
public static final int ERROR_NONE = 0;
/**
* Constant indicating that the session is dropped by Transcoding service due to hitting
* the limit, e.g. too many back to back transcoding happen in a short time frame.
*/
public static final int ERROR_DROPPED_BY_SERVICE = 1;
/**
* Constant indicating the backing transcoding service is died. Client should enqueue the
* the request again.
*/
public static final int ERROR_SERVICE_DIED = 2;
/** Listener that gets notified when the progress changes. */
@FunctionalInterface
public interface OnProgressUpdateListener {
/**
* Called when the progress changes. The progress is in percentage between 0 and 1,
* where 0 means the session has not yet started and 100 means that it has finished.
*
* @param session The session associated with the progress.
* @param progress The new progress ranging from 0 ~ 100 inclusive.
*/
void onProgressUpdate(@NonNull TranscodingSession session,
@IntRange(from = 0, to = 100) int progress);
}
private final MediaTranscodingManager mManager;
private Executor mListenerExecutor;
private OnTranscodingFinishedListener mListener;
private int mSessionId = -1;
// Lock for internal state.
private final Object mLock = new Object();
@GuardedBy("mLock")
private Executor mProgressUpdateExecutor = null;
@GuardedBy("mLock")
private OnProgressUpdateListener mProgressUpdateListener = null;
@GuardedBy("mLock")
private int mProgress = 0;
@GuardedBy("mLock")
private int mProgressUpdateInterval = 0;
@GuardedBy("mLock")
private @Status int mStatus = STATUS_PENDING;
@GuardedBy("mLock")
private @Result int mResult = RESULT_NONE;
@GuardedBy("mLock")
private @TranscodingSessionErrorCode int mErrorCode = ERROR_NONE;
@GuardedBy("mLock")
private boolean mHasRetried = false;
// The original request that associated with this session.
private final TranscodingRequest mRequest;
private TranscodingSession(
@NonNull MediaTranscodingManager manager,
@NonNull TranscodingRequest request,
@NonNull TranscodingSessionParcel parcel,
@NonNull @CallbackExecutor Executor executor,
@NonNull OnTranscodingFinishedListener listener) {
Objects.requireNonNull(manager, "manager must not be null");
Objects.requireNonNull(parcel, "parcel must not be null");
Objects.requireNonNull(executor, "listenerExecutor must not be null");
Objects.requireNonNull(listener, "listener must not be null");
mManager = manager;
mSessionId = parcel.sessionId;
mListenerExecutor = executor;
mListener = listener;
mRequest = request;
}
/**
* Set a progress listener.
* @param executor The executor on which listener will be invoked.
* @param listener The progress listener.
*/
public void setOnProgressUpdateListener(
@NonNull @CallbackExecutor Executor executor,
@Nullable OnProgressUpdateListener listener) {
synchronized (mLock) {
Objects.requireNonNull(executor, "listenerExecutor must not be null");
Objects.requireNonNull(listener, "listener must not be null");
mProgressUpdateExecutor = executor;
mProgressUpdateListener = listener;
}
}
private void updateStatusAndResult(@Status int sessionStatus,
@Result int sessionResult, @TranscodingSessionErrorCode int errorCode) {
synchronized (mLock) {
mStatus = sessionStatus;
mResult = sessionResult;
mErrorCode = errorCode;
}
}
/**
* Retrieve the error code associated with the RESULT_ERROR.
*/
public @TranscodingSessionErrorCode int getErrorCode() {
synchronized (mLock) {
return mErrorCode;
}
}
/**
* Resubmit the transcoding session to the service.
* Note that only the session that fails or gets cancelled could be retried and each session
* could be retried only once. After that, Client need to enqueue a new request if they want
* to try again.
*
* @return true if successfully resubmit the job to service. False otherwise.
* @throws UnsupportedOperationException if the retry could not be fulfilled.
* @hide
*/
public boolean retry() {
return retryInternal(true /*setHasRetried*/);
}
// TODO(hkuang): Add more test for it.
private boolean retryInternal(boolean setHasRetried) {
synchronized (mLock) {
if (mStatus == STATUS_PENDING || mStatus == STATUS_RUNNING) {
throw new UnsupportedOperationException(
"Failed to retry as session is in processing");
}
if (mHasRetried) {
throw new UnsupportedOperationException("Session has been retried already");
}
// Get the client interface.
ITranscodingClient client = mManager.getTranscodingClient();
if (client == null) {
Log.e(TAG, "Service rebooting. Try again later");
return false;
}
synchronized (mManager.mPendingTranscodingSessions) {
try {
// Submits the request to MediaTranscoding service.
TranscodingSessionParcel sessionParcel = new TranscodingSessionParcel();
if (!client.submitRequest(mRequest.writeToParcel(mManager.mContext),
sessionParcel)) {
mHasRetried = true;
throw new UnsupportedOperationException("Failed to enqueue request");
}
// Replace the old session id wit the new one.
mSessionId = sessionParcel.sessionId;
// Adds the new session back into pending sessions.
mManager.mPendingTranscodingSessions.put(mSessionId, this);
} catch (RemoteException re) {
return false;
}
mStatus = STATUS_PENDING;
mHasRetried = setHasRetried ? true : false;
}
}
return true;
}
/**
* Cancels the transcoding session and notify the listener.
* If the session happened to finish before being canceled this call is effectively a no-op
* and will not update the result in that case.
*/
public void cancel() {
synchronized (mLock) {
// Check if the session is finished already.
if (mStatus != STATUS_FINISHED) {
try {
ITranscodingClient client = mManager.getTranscodingClient();
// The client may be gone.
if (client != null) {
client.cancelSession(mSessionId);
}
} catch (RemoteException re) {
//TODO(hkuang): Find out what to do if failing to cancel the session.
Log.e(TAG, "Failed to cancel the session due to exception: " + re);
}
mStatus = STATUS_FINISHED;
mResult = RESULT_CANCELED;
// Notifies client the session is canceled.
mListenerExecutor.execute(() -> mListener.onTranscodingFinished(this));
}
}
}
/**
* Gets the progress of the transcoding session. The progress is between 0 and 100, where 0
* means that the session has not yet started and 100 means that it is finished. For the
* cancelled session, the progress will be the last updated progress before it is cancelled.
* @return The progress.
*/
@IntRange(from = 0, to = 100)
public int getProgress() {
synchronized (mLock) {
return mProgress;
}
}
/**
* Gets the status of the transcoding session.
* @return The status.
*/
public @Status int getStatus() {
synchronized (mLock) {
return mStatus;
}
}
/**
* Adds a client uid that is also waiting for this transcoding session.
* <p>
* Only privilege caller with android.permission.WRITE_MEDIA_STORAGE could add the
* uid. Note that the permission check happens on the service side upon starting the
* transcoding. If the client does not have the permission, the transcoding will fail.
* @param uid the additional client uid to be added.
* @return true if successfully added, false otherwise.
*/
public boolean addClientUid(int uid) {
if (uid < 0) {
throw new IllegalArgumentException("Invalid Uid");
}
// Get the client interface.
ITranscodingClient client = mManager.getTranscodingClient();
if (client == null) {
Log.e(TAG, "Service is dead...");
return false;
}
try {
if (!client.addClientUid(mSessionId, uid)) {
Log.e(TAG, "Failed to add client uid");
return false;
}
} catch (Exception ex) {
Log.e(TAG, "Failed to get client uids due to " + ex);
return false;
}
return true;
}
/**
* Query all the client that waiting for this transcoding session
* @return a list containing all the client uids.
*/
@NonNull
public List<Integer> getClientUids() {
List<Integer> uidList = new ArrayList<Integer>();
// Get the client interface.
ITranscodingClient client = mManager.getTranscodingClient();
if (client == null) {
Log.e(TAG, "Service is dead...");
return uidList;
}
try {
int[] clientUids = client.getClientUids(mSessionId);
for (int i : clientUids) {
uidList.add(i);
}
} catch (Exception ex) {
Log.e(TAG, "Failed to get client uids due to " + ex);
}
return uidList;
}
/**
* Gets sessionId of the transcoding session.
* @return session id.
*/
public int getSessionId() {
return mSessionId;
}
/**
* Gets the result of the transcoding session.
* @return The result.
*/
public @Result int getResult() {
synchronized (mLock) {
return mResult;
}
}
@Override
public String toString() {
String result;
String status;
switch (mResult) {
case RESULT_NONE:
result = "RESULT_NONE";
break;
case RESULT_SUCCESS:
result = "RESULT_SUCCESS";
break;
case RESULT_ERROR:
result = "RESULT_ERROR(" + mErrorCode + ")";
break;
case RESULT_CANCELED:
result = "RESULT_CANCELED";
break;
default:
result = String.valueOf(mResult);
break;
}
switch (mStatus) {
case STATUS_PENDING:
status = "STATUS_PENDING";
break;
case STATUS_PAUSED:
status = "STATUS_PAUSED";
break;
case STATUS_RUNNING:
status = "STATUS_RUNNING";
break;
case STATUS_FINISHED:
status = "STATUS_FINISHED";
break;
default:
status = String.valueOf(mStatus);
break;
}
return String.format(" session: {id: %d, status: %s, result: %s, progress: %d}",
mSessionId, status, result, mProgress);
}
private void updateProgress(int newProgress) {
synchronized (mLock) {
mProgress = newProgress;
}
}
private void updateStatus(int newStatus) {
synchronized (mLock) {
mStatus = newStatus;
}
}
}
private ITranscodingClient getTranscodingClient() {
synchronized (mLock) {
return mTranscodingClient;
}
}
/**
* Enqueues a TranscodingRequest for execution.
* <p> Upon successfully accepting the request, MediaTranscodingManager will return a
* {@link TranscodingSession} to the client. Client should use {@link TranscodingSession} to
* track the progress and get the result.
* <p> MediaTranscodingManager will return null if fails to accept the request due to service
* rebooting. Client could retry again after receiving null.
*
* @param transcodingRequest The TranscodingRequest to enqueue.
* @param listenerExecutor Executor on which the listener is notified.
* @param listener Listener to get notified when the transcoding session is finished.
* @return A TranscodingSession for this operation.
* @throws UnsupportedOperationException if the request could not be fulfilled.
*/
@Nullable
public TranscodingSession enqueueRequest(
@NonNull TranscodingRequest transcodingRequest,
@NonNull @CallbackExecutor Executor listenerExecutor,
@NonNull OnTranscodingFinishedListener listener) {
Log.i(TAG, "enqueueRequest called.");
Objects.requireNonNull(transcodingRequest, "transcodingRequest must not be null");
Objects.requireNonNull(listenerExecutor, "listenerExecutor must not be null");
Objects.requireNonNull(listener, "listener must not be null");
// Converts the request to TranscodingRequestParcel.
TranscodingRequestParcel requestParcel = transcodingRequest.writeToParcel(mContext);
Log.i(TAG, "Getting transcoding request " + transcodingRequest.getSourceUri());
// Submits the request to MediaTranscoding service.
try {
TranscodingSessionParcel sessionParcel = new TranscodingSessionParcel();
// Synchronizes the access to mPendingTranscodingSessions to make sure the session Id is
// inserted in the mPendingTranscodingSessions in the callback handler.
synchronized (mPendingTranscodingSessions) {
synchronized (mLock) {
if (mTranscodingClient == null) {
// Try to register with the service again.
IMediaTranscodingService service = getService(false /*retry*/);
if (service == null) {
Log.w(TAG, "Service rebooting. Try again later");
return null;
}
mTranscodingClient = registerClient(service);
// If still fails, throws an exception to tell client to try later.
if (mTranscodingClient == null) {
Log.w(TAG, "Service rebooting. Try again later");
return null;
}
}
if (!mTranscodingClient.submitRequest(requestParcel, sessionParcel)) {
throw new UnsupportedOperationException("Failed to enqueue request");
}
}
// Wraps the TranscodingSessionParcel into a TranscodingSession and returns it to
// client for tracking.
TranscodingSession session = new TranscodingSession(this, transcodingRequest,
sessionParcel,
listenerExecutor,
listener);
// Adds the new session into pending sessions.
mPendingTranscodingSessions.put(session.getSessionId(), session);
return session;
}
} catch (RemoteException ex) {
Log.w(TAG, "Service rebooting. Try again later");
return null;
} catch (ServiceSpecificException ex) {
throw new UnsupportedOperationException(
"Failed to submit request to Transcoding service. Error: " + ex);
}
}
}