diff --git a/Android.bp b/Android.bp index 7a7464f1d5b8b..18ae76ca2f80b 100644 --- a/Android.bp +++ b/Android.bp @@ -572,6 +572,7 @@ java_library { // If MimeMap ever becomes its own APEX, then this dependency would need to be removed // in favor of an API stubs dependency in java_library "framework" below. "mimemap", + "mediatranscoding_aidl_interface-java", ], // For backwards compatibility. stem: "framework", @@ -618,6 +619,7 @@ java_library { static_libs: [ "exoplayer2-extractor", "android.hardware.wifi-V1.0-java-constants", + "mediatranscoding_aidl_interface-java", // Additional dependencies needed to build the ike API classes. "ike-internals", diff --git a/media/java/android/media/MediaTranscodeManager.java b/media/java/android/media/MediaTranscodeManager.java index 510da81665657..2c5268b07aa2d 100644 --- a/media/java/android/media/MediaTranscodeManager.java +++ b/media/java/android/media/MediaTranscodeManager.java @@ -20,17 +20,26 @@ 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.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.Executor; -import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; /** MediaTranscodeManager provides an interface to the system's media transcoding service and can be @@ -54,7 +63,7 @@ import java.util.concurrent.locks.ReentrantLock;
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#enqueueTranscodingRequest(
+ {@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
@@ -87,6 +96,8 @@ import java.util.concurrent.locks.ReentrantLock;
public final class MediaTranscodeManager {
private static final String TAG = "MediaTranscodeManager";
+ private static final String MEDIA_TRANSCODING_SERVICE = "media.transcoding";
+
/**
* Default transcoding type.
* @hide
@@ -147,34 +158,6 @@ public final class MediaTranscodeManager {
@Retention(RetentionPolicy.SOURCE)
public @interface TranscodingPriority {}
- // 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;
-
- /** @hide */
- @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 ConcurrentMapTranscodingRequest instance.
@@ -280,10 +386,10 @@ public final class MediaTranscodeManager {
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.
@@ -389,6 +495,17 @@ public final class MediaTranscodeManager {
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 Builder.
@@ -467,51 +584,102 @@ public final class MediaTranscodeManager {
/** Listener that gets notified when the progress changes. */
@FunctionalInterface
- public interface OnProgressChangedListener {
-
+ public interface OnProgressUpdateListener {
/**
- * 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.
+ * 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 onProgressChanged(float progress);
+ void onProgressUpdate(int progress);
}
- private final Executor mExecutor;
+ private final ITranscodingClient mJobOwner;
+ private final Executor mListenerExecutor;
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 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(long id, @NonNull @CallbackExecutor Executor executor,
+ private TranscodingJob(
+ @NonNull ITranscodingClient jobOwner,
+ @NonNull TranscodingJobParcel parcel,
+ @NonNull @CallbackExecutor Executor executor,
@NonNull OnTranscodingFinishedListener listener) {
- mID = id;
- mExecutor = executor;
+ 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 setOnProgressChangedListener(@NonNull @CallbackExecutor Executor executor,
- @Nullable OnProgressChangedListener listener) {
- mProgressChangedExecutor = executor;
- mProgressChangedListener = listener;
+ public void setOnProgressUpdateListener(
+ @NonNull @CallbackExecutor Executor executor,
+ @Nullable OnProgressUpdateListener listener) {
+ setOnProgressUpdateListener(
+ 0 /* minProgressUpdateInterval */,
+ executor, 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.
+ * 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 void cancel() {
- setJobFinished(RESULT_CANCELED);
- sMediaTranscodeManager.native_cancelTranscodingRequest(mID);
+ 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));
+ }
}
/**
@@ -519,7 +687,7 @@ public final class MediaTranscodeManager {
* that the job has not yet started and 1 means that it is finished.
* @return The progress.
*/
- public float getProgress() {
+ public synchronized int getProgress() {
return mProgress;
}
@@ -527,100 +695,38 @@ public final class MediaTranscodeManager {
* Gets the status of the transcoding job.
* @return The status.
*/
- public @Status int getStatus() {
+ 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 @Result int getResult() {
+ public synchronized @Result int getResult() {
return mResult;
}
- private void setJobStarted() {
- mStatus = STATUS_RUNNING;
- }
-
- private void setJobProgress(float newProgress) {
- mProgress = newProgress;
+ private void setJobProgress(int newProgress) {
+ synchronized (this) {
+ mProgress = newProgress;
+ }
// Notify listener.
- OnProgressChangedListener onProgressChangedListener = mProgressChangedListener;
- if (onProgressChangedListener != null) {
- mProgressChangedExecutor.execute(
- () -> onProgressChangedListener.onProgressChanged(mProgress));
+ OnProgressUpdateListener onProgressUpdateListener = mProgressUpdateListener;
+ if (mProgressUpdateListener != null) {
+ mProgressUpdateExecutor.execute(
+ () -> onProgressUpdateListener.onProgressUpdate(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);
-
- /** 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);
}
/**
@@ -628,12 +734,26 @@ public final class MediaTranscodeManager {
*
* @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());
+ sMediaTranscodeManager = new MediaTranscodeManager(context.getApplicationContext(),
+ transcodingService);
}
return sMediaTranscodeManager;
}
@@ -649,41 +769,54 @@ public final class MediaTranscodeManager {
* @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 UnsupportedOperationException if the result could not be fulfilled.
+ * @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 enqueueTranscodingRequest(
+ public TranscodingJob enqueueRequest(
@NonNull TranscodingRequest transcodingRequest,
@NonNull @CallbackExecutor Executor listenerExecutor,
- @NonNull OnTranscodingFinishedListener listener) throws UnsupportedOperationException {
- Log.i(TAG, "enqueueTranscodingRequest called.");
+ @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");
- // Reserve a job ID.
- // TODO(hkuang): Remove this.
- long jobID = native_requestUniqueJobID();
- if (jobID == ID_INVALID) {
- throw new UnsupportedOperationException("Transcoding request could not be fulfilled");
+ 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");
}
-
- // 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);
- throw new UnsupportedOperationException("Transcoding request could not be fulfilled");
- }
-
- return transcodingJob;
- }
-
- static {
- System.loadLibrary("mediatranscodemanager_jni");
- native_init();
}
}
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/mediatranscodemanager/MediaTranscodeManagerTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/mediatranscodemanager/MediaTranscodeManagerTest.java
index 41a1f748e8dc9..c07e38f1df18f 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/mediatranscodemanager/MediaTranscodeManagerTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/mediatranscodemanager/MediaTranscodeManagerTest.java
@@ -20,11 +20,18 @@ import static org.testng.Assert.assertThrows;
import android.content.ContentResolver;
import android.content.Context;
+import android.media.IMediaTranscodingService;
+import android.media.ITranscodingClient;
+import android.media.ITranscodingClientCallback;
import android.media.MediaFormat;
import android.media.MediaTranscodeManager;
import android.media.MediaTranscodeManager.TranscodingJob;
import android.media.MediaTranscodeManager.TranscodingRequest;
+import android.media.TranscodingJobParcel;
+import android.media.TranscodingRequestParcel;
+import android.media.TranscodingResultParcel;
import android.net.Uri;
+import android.os.RemoteException;
import android.test.ActivityInstrumentationTestCase2;
import android.util.Log;
@@ -34,10 +41,16 @@ import com.android.mediaframeworktest.R;
import org.junit.Test;
import java.io.File;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
/*
* Functional tests for MediaTranscodeManager in the media framework.
@@ -63,12 +76,124 @@ public class MediaTranscodeManagerTest
private Uri mSourceHEVCVideoUri = null;
private Uri mDestinationUri = null;
+ // Use mock transcoding service for testing the api.
+ private MockTranscodingService mTranscodingService = null;
+
// Setting for transcoding to H.264.
private static final String MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
private static final int BIT_RATE = 2000000; // 2Mbps
private static final int WIDTH = 1920;
private static final int HEIGHT = 1080;
+ // A mock transcoding service that will take constant 300ms to process each transcoding job.
+ // Instead of doing real transcoding, it will return the dst uri directly.
+ class MockTranscodingService extends IMediaTranscodingService.Stub {
+ private final ScheduledExecutorService mJobScheduler = Executors.newScheduledThreadPool(1);
+ private int mNumOfClients = 0;
+ private AtomicInteger mJobId = new AtomicInteger();
+
+ // A runnable that will process the job.
+ private class ProcessingJobRunnable implements Runnable {
+ private TranscodingJobParcel mJob;
+ private ITranscodingClientCallback mCallback;
+ private ConcurrentMap