blob: 2c431b98fc7a14344298f0271424725a841c8900 [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.Context;
import android.net.Uri;
import android.util.Log;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;
/**
* MediaTranscodeManager provides an interface to the system's media transcode service.
* Transcode requests are put in a queue and processed in order. When a transcode operation is
* completed the caller is notified via its OnTranscodingFinishedListener. In the meantime the
* caller may use the returned TranscodingJob object to cancel or check the status of a specific
* transcode operation.
* The currently supported media types are video and still images.
*
* TODO(lnilsson): Add sample code when API is settled.
*
* @hide
*/
public final class MediaTranscodeManager {
private static final String TAG = "MediaTranscodeManager";
// Invalid ID passed from native means the request was never enqueued.
private static final long ID_INVALID = -1;
// Events passed from native.
private static final int EVENT_JOB_STARTED = 1;
private static final int EVENT_JOB_PROGRESSED = 2;
private static final int EVENT_JOB_FINISHED = 3;
@IntDef(prefix = { "EVENT_" }, value = {
EVENT_JOB_STARTED,
EVENT_JOB_PROGRESSED,
EVENT_JOB_FINISHED,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Event {}
private static MediaTranscodeManager sMediaTranscodeManager;
private final ConcurrentMap<Long, TranscodingJob> mPendingTranscodingJobs =
new ConcurrentHashMap<>();
private final Context mContext;
/**
* 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);
}
/**
* Class describing a transcode operation to be performed. The caller uses this class to
* configure a transcoding operation that can then be enqueued using MediaTranscodeManager.
*/
public static final class TranscodingRequest {
private Uri mSrcUri;
private Uri mDstUri;
private MediaFormat mDstFormat;
private TranscodingRequest(Builder b) {
mSrcUri = b.mSrcUri;
mDstUri = b.mDstUri;
mDstFormat = b.mDstFormat;
}
/** TranscodingRequest builder class. */
public static class Builder {
private Uri mSrcUri;
private Uri mDstUri;
private MediaFormat mDstFormat;
/**
* Specifies the source media file.
* @param uri Content uri for the source media file.
* @return The builder instance.
*/
public Builder setSourceUri(Uri uri) {
mSrcUri = uri;
return this;
}
/**
* Specifies the destination media file.
* @param uri Content uri for the destination media file.
* @return The builder instance.
*/
public Builder setDestinationUri(Uri uri) {
mDstUri = uri;
return this;
}
/**
* Specifies the media format of the transcoded media file.
* @param dstFormat MediaFormat containing the desired destination format.
* @return The builder instance.
*/
public Builder setDestinationFormat(MediaFormat dstFormat) {
mDstFormat = dstFormat;
return this;
}
/**
* Builds a new TranscodingRequest with the configuration set on this builder.
* @return A new TranscodingRequest.
*/
public TranscodingRequest build() {
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 OnProgressChangedListener {
/**
* Called when the progress changes. The progress is between 0 and 1, where 0 means
* that the job has not yet started and 1 means that it has finished.
* @param progress The new progress.
*/
void onProgressChanged(float progress);
}
private final Executor mExecutor;
private final OnTranscodingFinishedListener mListener;
private final ReentrantLock mStatusChangeLock = new ReentrantLock();
private Executor mProgressChangedExecutor;
private OnProgressChangedListener mProgressChangedListener;
private long mID;
private float mProgress = 0.0f;
private @Status int mStatus = STATUS_PENDING;
private @Result int mResult = RESULT_NONE;
private TranscodingJob(long id, @NonNull @CallbackExecutor Executor executor,
@NonNull OnTranscodingFinishedListener listener) {
mID = id;
mExecutor = executor;
mListener = listener;
}
/**
* Set a progress listener.
* @param listener The progress listener.
*/
public void setOnProgressChangedListener(@NonNull @CallbackExecutor Executor executor,
@Nullable OnProgressChangedListener listener) {
mProgressChangedExecutor = executor;
mProgressChangedListener = listener;
}
/**
* 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 void cancel() {
setJobFinished(RESULT_CANCELED);
sMediaTranscodeManager.native_cancelTranscodingRequest(mID);
}
/**
* 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 float getProgress() {
return mProgress;
}
/**
* Gets the status of the transcoding job.
* @return The status.
*/
public @Status int getStatus() {
return mStatus;
}
/**
* Gets the result of the transcoding job.
* @return The result.
*/
public @Result int getResult() {
return mResult;
}
private void setJobStarted() {
mStatus = STATUS_RUNNING;
}
private void setJobProgress(float newProgress) {
mProgress = newProgress;
// Notify listener.
OnProgressChangedListener onProgressChangedListener = mProgressChangedListener;
if (onProgressChangedListener != null) {
mProgressChangedExecutor.execute(
() -> onProgressChangedListener.onProgressChanged(mProgress));
}
}
private void setJobFinished(int result) {
boolean doNotifyListener = false;
// Prevent conflicting simultaneous status updates from native (finished) and from the
// caller (cancel).
try {
mStatusChangeLock.lock();
if (mStatus != STATUS_FINISHED) {
mStatus = STATUS_FINISHED;
mResult = result;
doNotifyListener = true;
}
} finally {
mStatusChangeLock.unlock();
}
if (doNotifyListener) {
mExecutor.execute(() -> mListener.onTranscodingFinished(this));
}
}
private void processJobEvent(@Event int event, int arg) {
switch (event) {
case EVENT_JOB_STARTED:
setJobStarted();
break;
case EVENT_JOB_PROGRESSED:
setJobProgress((float) arg / 100);
break;
case EVENT_JOB_FINISHED:
setJobFinished(arg);
break;
default:
Log.e(TAG, "Unsupported event: " + event);
break;
}
}
}
// Initializes the native library.
private static native void native_init();
// Requests a new job ID from the native service.
private native long native_requestUniqueJobID();
// Enqueues a transcoding request to the native service.
private native boolean native_enqueueTranscodingRequest(
long id, @NonNull TranscodingRequest transcodingRequest, @NonNull Context context);
// Cancels an enqueued transcoding request.
private native void native_cancelTranscodingRequest(long id);
// Private constructor.
private MediaTranscodeManager(@NonNull Context context) {
mContext = context;
}
// Events posted from the native service.
@SuppressWarnings("unused")
private void postEventFromNative(@Event int event, long id, int arg) {
Log.d(TAG, String.format("postEventFromNative. Event %d, ID %d, arg %d", event, id, arg));
TranscodingJob transcodingJob = mPendingTranscodingJobs.get(id);
// Job IDs are added to the tracking set before the job is enqueued so it should never
// be null unless the service misbehaves.
if (transcodingJob == null) {
Log.e(TAG, "No matching transcode job found for id " + id);
return;
}
transcodingJob.processJobEvent(event, arg);
}
/**
* Gets the MediaTranscodeManager singleton instance.
* @param context The application context.
* @return the {@link MediaTranscodeManager} singleton instance.
*/
public static MediaTranscodeManager getInstance(@NonNull Context context) {
Preconditions.checkNotNull(context);
synchronized (MediaTranscodeManager.class) {
if (sMediaTranscodeManager == null) {
sMediaTranscodeManager = new MediaTranscodeManager(context.getApplicationContext());
}
return sMediaTranscodeManager;
}
}
/**
* Enqueues a TranscodingRequest for execution.
* @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.
*/
public @Nullable TranscodingJob enqueueTranscodingRequest(
@NonNull TranscodingRequest transcodingRequest,
@NonNull @CallbackExecutor Executor listenerExecutor,
@NonNull OnTranscodingFinishedListener listener) {
Log.i(TAG, "enqueueTranscodingRequest called.");
Preconditions.checkNotNull(transcodingRequest);
Preconditions.checkNotNull(listenerExecutor);
Preconditions.checkNotNull(listener);
// Reserve a job ID.
long jobID = native_requestUniqueJobID();
if (jobID == ID_INVALID) {
return null;
}
// Add the job to the tracking set.
TranscodingJob transcodingJob = new TranscodingJob(jobID, listenerExecutor, listener);
mPendingTranscodingJobs.put(jobID, transcodingJob);
// Enqueue the request with the native service.
boolean enqueued = native_enqueueTranscodingRequest(jobID, transcodingRequest, mContext);
if (!enqueued) {
mPendingTranscodingJobs.remove(jobID);
return null;
}
return transcodingJob;
}
static {
System.loadLibrary("media_jni");
native_init();
}
}