This change is the first iteration of a new high-level media transcode API. At this point the API is not hooked up to the underlying native media transcoding service. More transcoding options will be added in following CLs. Test: Ran MediaTranscodeManagerTest, verified call sequence using logcat. Flashed build on blueline and verified basic media functionality. Change-Id: I190e41b468265bd339d0fbca243db2a196e5a04e
404 lines
15 KiB
Java
404 lines
15 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.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();
|
|
}
|
|
}
|