blob: 2c5268b07aa2d5b63bbf17a8e47b4097a0b1f4ff [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.NonNull;
import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.system.Os;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import java.io.FileNotFoundException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
MediaTranscodeManager 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 file could be of any of the following types:
<ul>
<li> Video file with single video track. </li>
<li> Video file with multiple video track. </li>
<li> Video file with multiple video tracks and audio tracks. </li>
<li> Video file with video/audio tracks and metadata track. Note that metadata track will be passed
through only if it could be recognized by {@link MediaExtractor}.
TODO(hkuang): Finalize the metadata track behavior. </li>
</ul>
<p class=note>
Note that currently only support transcoding video file in mp4 format.
<h3>Transcoding Request</h3>
<p>
To transcode a media file, first create a {@link TranscodingRequest} through its builder class
{@link TranscodingRequest.Builder}. Transcode requests are then enqueue to the manager through
{@link MediaTranscodeManager#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 TranscodingJob 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>
TranscodingRequest request =
new TranscodingRequest.Builder()
.setSourceUri(srcUri)
.setDestinationUri(dstUri)
.setType(MediaTranscodeManager.TRANSCODING_TYPE_VIDEO)
.setPriority(REALTIME)
.setVideoTrackFormat(videoFormat)
.build();
}</pre>
TODO(hkuang): Add architecture diagram showing the transcoding service and api.
TODO(hkuang): Add sample code when API is settled.
TODO(hkuang): Clarify whether multiple video tracks is supported or not.
TODO(hkuang): Clarify whether image/audio transcoding is supported or not.
TODO(hkuang): Clarify what will happen if there is unrecognized track in the source.
TODO(hkuang): Clarify whether supports scaling.
TODO(hkuang): Clarify whether supports framerate conversion.
@hide
*/
public final class MediaTranscodeManager {
private static final String TAG = "MediaTranscodeManager";
private static final String MEDIA_TRANSCODING_SERVICE = "media.transcoding";
/**
* 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 file.
* <p>Note that currently only support transcoding video file in mp4 format.
*/
public static final int TRANSCODING_TYPE_VIDEO = 1;
/**
* TRANSCODING_TYPE_IMAGE indicates that client wants to perform transcoding on an image file.
* @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 jobs with higher priority.
* TODO(hkuang): Add more description of this when priority is finalized.
*/
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>Jobs 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.
* TODO(hkuang): Add more description of this when priority is finalized.
*/
public static final int PRIORITY_OFFLINE = 2;
/** @hide */
@IntDef(prefix = {"PRIORITY_"}, value = {
PRIORITY_UNKNOWN,
PRIORITY_REALTIME,
PRIORITY_OFFLINE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface TranscodingPriority {}
/**
* 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
* TranscodingJob to check the result, i.e. whether the operation succeeded, was canceled or
* if an error occurred.
*
* @param transcodingJob The TranscodingJob instance for the finished transcoding operation.
*/
void onTranscodingFinished(@NonNull TranscodingJob transcodingJob);
}
private final Context mContext;
private ContentResolver mContentResolver;
private final String mPackageName;
private final int mPid;
private final int mUid;
private final ExecutorService mCallbackExecutor = Executors.newSingleThreadExecutor();
private static MediaTranscodeManager sMediaTranscodeManager;
private final HashMap<Integer, TranscodingJob> mPendingTranscodingJobs = new HashMap();
@NonNull private ITranscodingClient mTranscodingClient;
private void handleTranscodingFinished(int jobId, TranscodingResultParcel result) {
synchronized (mPendingTranscodingJobs) {
// Gets the job associated with the jobId and removes it from
// mPendingTranscodingJobs.
final TranscodingJob job = mPendingTranscodingJobs.remove(jobId);
if (job == null) {
// This should not happen in reality.
Log.e(TAG, "Job " + jobId + " is not in PendingJobs");
return;
}
// Updates the job status and result.
job.updateStatusAndResult(TranscodingJob.STATUS_FINISHED,
TranscodingJob.RESULT_SUCCESS);
// Notifies client the job is done.
if (job.mListener != null && job.mListenerExecutor != null) {
job.mListenerExecutor.execute(() -> job.mListener.onTranscodingFinished(job));
}
}
}
private void handleTranscodingFailed(int jobId, int errorCodec) {
synchronized (mPendingTranscodingJobs) {
// Gets the job associated with the jobId and removes it from
// mPendingTranscodingJobs.
final TranscodingJob job = mPendingTranscodingJobs.remove(jobId);
if (job == null) {
// This should not happen in reality.
Log.e(TAG, "Job " + jobId + " is not in PendingJobs");
return;
}
// Updates the job status and result.
job.updateStatusAndResult(TranscodingJob.STATUS_FINISHED,
TranscodingJob.RESULT_ERROR);
// Notifies client the job is done.
if (job.mListener != null && job.mListenerExecutor != null) {
job.mListenerExecutor.execute(() -> job.mListener.onTranscodingFinished(job));
}
}
}
// Just forwards all the events to the event handler.
private ITranscodingClientCallback mTranscodingClientCallback =
new ITranscodingClientCallback.Stub() {
@Override
public void onTranscodingFinished(int jobId, TranscodingResultParcel result)
throws RemoteException {
handleTranscodingFinished(jobId, result);
}
@Override
public void onTranscodingFailed(int jobId, int errorCode) throws RemoteException {
handleTranscodingFailed(jobId, errorCode);
}
@Override
public void onAwaitNumberOfJobsChanged(int jobId, int oldAwaitNumber,
int newAwaitNumber) throws RemoteException {
//TODO(hkuang): Implement this.
}
@Override
public void onProgressUpdate(int jobId, int progress) throws RemoteException {
//TODO(hkuang): Implement this.
}
};
/* Private constructor. */
private MediaTranscodeManager(@NonNull Context context,
IMediaTranscodingService transcodingService) {
mContext = context;
mContentResolver = mContext.getContentResolver();
mPackageName = mContext.getPackageName();
mPid = Os.getuid();
mUid = Os.getpid();
try {
// Registers the client with MediaTranscoding service.
mTranscodingClient = transcodingService.registerClient(
mTranscodingClientCallback,
mPackageName,
mPackageName,
IMediaTranscodingService.USE_CALLING_UID,
IMediaTranscodingService.USE_CALLING_PID);
} catch (RemoteException re) {
Log.e(TAG, "Failed to register new client due to exception " + re);
throw new UnsupportedOperationException("Failed to register new client");
}
}
public static final class TranscodingRequest {
/** Uri of the source media file. */
private @NonNull Uri mSourceUri;
/** Uri of the destination media file. */
private @NonNull Uri mDestinationUri;
/** Type of the transcoding. */
private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN;
/** Priority of the transcoding. */
private @TranscodingPriority int mPriority = PRIORITY_UNKNOWN;
/**
* 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.
* <p>
*/
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.
* @hide
*/
private @Nullable MediaFormat mAudioTrackFormat = null;
/**
* 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 int mProcessingDelayMs = 0;
private TranscodingRequest(Builder b) {
mSourceUri = b.mSourceUri;
mDestinationUri = b.mDestinationUri;
mPriority = b.mPriority;
mType = b.mType;
mVideoTrackFormat = b.mVideoTrackFormat;
mAudioTrackFormat = b.mAudioTrackFormat;
mImageFormat = b.mImageFormat;
mProcessingDelayMs = b.mProcessingDelayMs;
}
/** Return the type of the transcoding. */
@TranscodingType
int getType() {
return mType;
}
/** Return source uri of the transcoding. */
@NonNull
Uri getSourceUri() {
return mSourceUri;
}
/** Return destination uri of the transcoding. */
@NonNull
Uri getDestinationUri() {
return mDestinationUri;
}
/** Return priority of the transcoding. */
@TranscodingPriority
int getPriority() {
return mPriority;
}
/**
* Return the video track format of the transcoding.
* This will be null is the transcoding is not for video transcoding.
*/
MediaFormat getVideoTrackFormat() {
return mVideoTrackFormat;
}
/* Writes the TranscodingRequest to a parcel. */
private TranscodingRequestParcel writeToParcel() {
TranscodingRequestParcel parcel = new TranscodingRequestParcel();
// TODO(hkuang): Implement all the fields here to pass to service.
parcel.priority = mPriority;
parcel.transcodingType = mType;
if (mProcessingDelayMs != 0) {
parcel.isForTesting = true;
parcel.testConfig = new TranscodingTestConfig();
parcel.testConfig.processingDelayMs = mProcessingDelayMs;
}
return parcel;
}
/**
* Builder class for {@link TranscodingRequest} objects.
* Use this class to configure and create a <code>TranscodingRequest</code> instance.
*/
public static class Builder {
private @NonNull Uri mSourceUri;
private @NonNull Uri mDestinationUri;
private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN;
private @TranscodingPriority int mPriority = PRIORITY_UNKNOWN;
private @Nullable MediaFormat mVideoTrackFormat;
private @Nullable MediaFormat mAudioTrackFormat;
private @Nullable MediaFormat mImageFormat;
private int mProcessingDelayMs = 0;
/**
* Specifies the uri of source media file.
*
* @param sourceUri Content uri for the source media file.
* @return The same builder instance.
* @throws IllegalArgumentException if Uri is null or empty.
*/
@NonNull
public Builder setSourceUri(@NonNull Uri sourceUri) throws IllegalArgumentException {
if (sourceUri == null || Uri.EMPTY.equals(sourceUri)) {
throw new IllegalArgumentException(
"You must specify a non-empty source Uri.");
}
mSourceUri = sourceUri;
return this;
}
/**
* Specifies the uri of the destination media file.
*
* @param destinationUri Content uri for the destination media file.
* @return The same builder instance.
* @throws IllegalArgumentException if Uri is null or empty.
*/
@NonNull
public Builder setDestinationUri(@NonNull Uri destinationUri)
throws IllegalArgumentException {
if (destinationUri == null || Uri.EMPTY.equals(destinationUri)) {
throw new IllegalArgumentException(
"You must specify a non-empty destination Uri.");
}
mDestinationUri = destinationUri;
return this;
}
/**
* 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.
*/
@NonNull
public Builder setPriority(@TranscodingPriority int priority)
throws IllegalArgumentException {
if (priority != PRIORITY_OFFLINE && priority != PRIORITY_REALTIME) {
throw new IllegalArgumentException("Invalid priority: " + priority);
}
mPriority = priority;
return this;
}
/**
* Specifies the type of transcoding.
* <p> Clients must provide the source and destination that corresponds to the
* transcoding type.
*
* @param type Must be one of the {@code TRANSCODING_TYPE_*}
* @return The same builder instance.
* @throws IllegalArgumentException if flags is invalid.
*/
@NonNull
public Builder setType(@TranscodingType int type)
throws IllegalArgumentException {
if (type != TRANSCODING_TYPE_VIDEO && type != TRANSCODING_TYPE_IMAGE) {
throw new IllegalArgumentException("Invalid transcoding type");
}
mType = type;
return this;
}
/**
* Specifies the desired video track format in the destination media file.
* <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.
* TODO(hkuang): Add MediaTranscodeUtil to help client generate transcoding setting.
* TODO(hkuang): Add MediaTranscodeUtil to check if the setting is valid.
*
* @param videoFormat MediaFormat containing the settings that client wants override in
* the original video's video track.
* @return The same builder instance.
* @throws IllegalArgumentException if videoFormat is invalid.
*/
@NonNull
public Builder setVideoTrackFormat(@NonNull MediaFormat videoFormat)
throws IllegalArgumentException {
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 this;
}
/**
* Sets the delay in processing this request.
* @param processingDelayMs delay in milliseconds.
* @return The same builder instance.
*/
@VisibleForTesting
public Builder setProcessingDelayMs(int processingDelayMs) {
mProcessingDelayMs = processingDelayMs;
return this;
}
/**
* @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 TranscodingRequest build() throws UnsupportedOperationException {
if (mSourceUri == null) {
throw new UnsupportedOperationException("Source URI must not be null");
}
if (mDestinationUri == null) {
throw new UnsupportedOperationException("Destination URI must not be null");
}
if (mPriority == PRIORITY_UNKNOWN) {
throw new UnsupportedOperationException("Must specify transcoding priority");
}
// Only support video transcoding now.
if (mType != TRANSCODING_TYPE_VIDEO) {
throw new UnsupportedOperationException("Only supports video transcoding now");
}
// Must provide video track format for video transcoding.
if (mType == TRANSCODING_TYPE_VIDEO && mVideoTrackFormat == null) {
throw new UnsupportedOperationException(
"Must provide video track format for video transcoding");
}
return new TranscodingRequest(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 TranscodingJob {
/** The job is enqueued but not yet running. */
public static final int STATUS_PENDING = 1;
/** The job is currently running. */
public static final int STATUS_RUNNING = 2;
/** The job is finished. */
public static final int STATUS_FINISHED = 3;
@IntDef(prefix = { "STATUS_" }, value = {
STATUS_PENDING,
STATUS_RUNNING,
STATUS_FINISHED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Status {}
/** The job does not have a result yet. */
public static final int RESULT_NONE = 1;
/** The job completed successfully. */
public static final int RESULT_SUCCESS = 2;
/** The job encountered an error while running. */
public static final int RESULT_ERROR = 3;
/** The job was canceled by the caller. */
public static final int RESULT_CANCELED = 4;
@IntDef(prefix = { "RESULT_" }, value = {
RESULT_NONE,
RESULT_SUCCESS,
RESULT_ERROR,
RESULT_CANCELED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Result {}
/** 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 that the job has not yet started and 100 means that it has finished.
* @param progress The new progress ranging from 0 ~ 100 inclusive.
*/
void onProgressUpdate(int progress);
}
private final ITranscodingClient mJobOwner;
private final Executor mListenerExecutor;
private final OnTranscodingFinishedListener mListener;
private int mJobId = -1;
@GuardedBy("this")
private Executor mProgressUpdateExecutor = null;
@GuardedBy("this")
private OnProgressUpdateListener mProgressUpdateListener = null;
@GuardedBy("this")
private int mProgress = 0;
@GuardedBy("this")
private int mProgressUpdateInterval = 0;
@GuardedBy("this")
private @Status int mStatus = STATUS_PENDING;
@GuardedBy("this")
private @Result int mResult = RESULT_NONE;
private TranscodingJob(
@NonNull ITranscodingClient jobOwner,
@NonNull TranscodingJobParcel parcel,
@NonNull @CallbackExecutor Executor executor,
@NonNull OnTranscodingFinishedListener listener) {
Objects.requireNonNull(jobOwner, "JobOwner must not be null");
Objects.requireNonNull(parcel, "TranscodingJobParcel must not be null");
Objects.requireNonNull(executor, "listenerExecutor must not be null");
Objects.requireNonNull(listener, "listener must not be null");
mJobOwner = jobOwner;
mJobId = parcel.jobId;
mListenerExecutor = executor;
mListener = listener;
}
/**
* 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) {
setOnProgressUpdateListener(
0 /* minProgressUpdateInterval */,
executor, listener);
}
/**
* Set a progress listener with specified progress update interval.
* @param minProgressUpdateInterval The minimum interval between each progress update.
* @param executor The executor on which listener will be invoked.
* @param listener The progress listener.
*/
public synchronized void setOnProgressUpdateListener(
int minProgressUpdateInterval,
@NonNull @CallbackExecutor Executor executor,
@Nullable OnProgressUpdateListener listener) {
Objects.requireNonNull(executor, "listenerExecutor must not be null");
Objects.requireNonNull(listener, "listener must not be null");
mProgressUpdateExecutor = executor;
mProgressUpdateListener = listener;
}
private synchronized void updateStatusAndResult(@Status int jobStatus,
@Result int jobResult) {
mStatus = jobStatus;
mResult = jobResult;
}
/**
* Cancels the transcoding job and notify the listener.
* If the job happened to finish before being canceled this call is effectively a no-op and
* will not update the result in that case.
*/
public synchronized void cancel() {
// Check if the job is finished already.
if (mStatus != STATUS_FINISHED) {
try {
mJobOwner.cancelJob(mJobId);
} catch (RemoteException re) {
//TODO(hkuang): Find out what to do if failing to cancel the job.
Log.e(TAG, "Failed to cancel the job due to exception: " + re);
}
mStatus = STATUS_FINISHED;
mResult = RESULT_CANCELED;
// Notifies client the job is canceled.
mListenerExecutor.execute(() -> mListener.onTranscodingFinished(this));
}
}
/**
* Gets the progress of the transcoding job. The progress is between 0 and 1, where 0 means
* that the job has not yet started and 1 means that it is finished.
* @return The progress.
*/
public synchronized int getProgress() {
return mProgress;
}
/**
* Gets the status of the transcoding job.
* @return The status.
*/
public synchronized @Status int getStatus() {
return mStatus;
}
/**
* Gets jobId of the transcoding job.
* @return job id.
*/
public int getJobId() {
return mJobId;
}
/**
* Gets the result of the transcoding job.
* @return The result.
*/
public synchronized @Result int getResult() {
return mResult;
}
private void setJobProgress(int newProgress) {
synchronized (this) {
mProgress = newProgress;
}
// Notify listener.
OnProgressUpdateListener onProgressUpdateListener = mProgressUpdateListener;
if (mProgressUpdateListener != null) {
mProgressUpdateExecutor.execute(
() -> onProgressUpdateListener.onProgressUpdate(mProgress));
}
}
}
/**
* Gets the MediaTranscodeManager singleton instance.
*
* @param context The application context.
* @return the {@link MediaTranscodeManager} singleton instance.
* @throws UnsupportedOperationException if failing to acquire the MediaTranscodeManager.
*/
public static MediaTranscodeManager getInstance(@NonNull Context context) {
// Acquires the MediaTranscoding service.
IMediaTranscodingService service = IMediaTranscodingService.Stub.asInterface(
ServiceManager.getService(MEDIA_TRANSCODING_SERVICE));
return getInstance(context, service);
}
/** Similar as above, but allow injecting transcodingService for testing. */
@VisibleForTesting
public static MediaTranscodeManager getInstance(@NonNull Context context,
IMediaTranscodingService transcodingService) {
Objects.requireNonNull(context, "context must not be null");
synchronized (MediaTranscodeManager.class) {
if (sMediaTranscodeManager == null) {
sMediaTranscodeManager = new MediaTranscodeManager(context.getApplicationContext(),
transcodingService);
}
return sMediaTranscodeManager;
}
}
/**
* Enqueues a TranscodingRequest for execution.
* <p> Upon successfully accepting the request, MediaTranscodeManager will return a
* {@link TranscodingJob} to the client. Client should use {@link TranscodingJob} to track the
* progress and get the result.
*
* @param transcodingRequest The TranscodingRequest to enqueue.
* @param listenerExecutor Executor on which the listener is notified.
* @param listener Listener to get notified when the transcoding job is finished.
* @return A TranscodingJob for this operation.
* @throws FileNotFoundException if the source Uri or destination Uri could not be opened.
* @throws UnsupportedOperationException if the request could not be fulfilled.
*/
@NonNull
public TranscodingJob enqueueRequest(
@NonNull TranscodingRequest transcodingRequest,
@NonNull @CallbackExecutor Executor listenerExecutor,
@NonNull OnTranscodingFinishedListener listener)
throws UnsupportedOperationException, FileNotFoundException {
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");
TranscodingRequestParcel requestParcel = transcodingRequest.writeToParcel();
// TODO(hkuang): move to use Uri string instead of FileDescriptor.
// Open the source file descriptor.
AssetFileDescriptor sourceFd = mContentResolver.openAssetFileDescriptor(
transcodingRequest.getSourceUri(), "r");
// Open the destination file descriptor.
AssetFileDescriptor destinationFd = mContentResolver.openAssetFileDescriptor(
transcodingRequest.getDestinationUri(), "w");
// Submits the request to MediaTranscoding service.
TranscodingJobParcel jobParcel = new TranscodingJobParcel();
try {
// Synchronizes the access to mPendingTranscodingJobs to make sure the job Id is
// inserted in the mPendingTranscodingJobs in the callback handler.
synchronized (mPendingTranscodingJobs) {
if (!mTranscodingClient.submitRequest(requestParcel, jobParcel)) {
throw new UnsupportedOperationException("Failed to enqueue request");
}
// Wraps the TranscodingJobParcel into a TranscodingJob and returns it to client for
// tracking.
TranscodingJob job = new TranscodingJob(mTranscodingClient, jobParcel,
listenerExecutor,
listener);
// Adds the new job into pending jobs.
mPendingTranscodingJobs.put(job.getJobId(), job);
return job;
}
} catch (RemoteException re) {
throw new UnsupportedOperationException(
"Failed to submit request to Transcoding service");
}
}
}