1755 lines
72 KiB
Java
1755 lines
72 KiB
Java
/*
|
|
* 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.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 androidx.annotation.RequiresApi;
|
|
|
|
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)
|
|
@RequiresApi(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 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;
|
|
}
|
|
|
|
// Update session progress and notify clients.
|
|
session.updateProgress(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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
* @throws IllegalArgumentException if the hinted source video format contains invalid
|
|
* parameters.
|
|
*/
|
|
@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, -1);
|
|
int height = mSrcVideoFormatHint.getInteger(MediaFormat.KEY_HEIGHT, -1);
|
|
if (width <= 0 || height <= 0) {
|
|
throw new IllegalArgumentException(
|
|
"Source Width and height must be larger than 0");
|
|
}
|
|
|
|
float frameRate =
|
|
mSrcVideoFormatHint.getNumber(MediaFormat.KEY_FRAME_RATE, 30.0)
|
|
.floatValue();
|
|
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,
|
|
@NonNull OnProgressUpdateListener listener) {
|
|
synchronized (mLock) {
|
|
Objects.requireNonNull(executor, "listenerExecutor must not be null");
|
|
Objects.requireNonNull(listener, "listener must not be null");
|
|
mProgressUpdateExecutor = executor;
|
|
mProgressUpdateListener = listener;
|
|
}
|
|
}
|
|
|
|
/** Removes the progress listener if any. */
|
|
public void clearOnProgressUpdateListener() {
|
|
synchronized (mLock) {
|
|
mProgressUpdateExecutor = null;
|
|
mProgressUpdateListener = null;
|
|
}
|
|
}
|
|
|
|
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;
|
|
if (mProgressUpdateExecutor != null && mProgressUpdateListener != null) {
|
|
final OnProgressUpdateListener listener = mProgressUpdateListener;
|
|
mProgressUpdateExecutor.execute(
|
|
() -> listener.onProgressUpdate(this, 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);
|
|
}
|
|
}
|
|
}
|