Transcoding API: Hook up with media.transcoding service.
Note the test is tested with a mock transcoding service.
Another test will be added when the service's transcoding
is up and running.
Also remove the jni call and talk with service directly.
Bug: 145628554
Test: atest MediaTranscodeManagerTest
Change-Id: Ifd7babb235fcd034ba7ee1758985a9e4d031170c
diff --git a/Android.bp b/Android.bp
index 7a7464f..18ae76c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -572,6 +572,7 @@
// 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 @@
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 510da81..2c5268b 100644
--- a/media/java/android/media/MediaTranscodeManager.java
+++ b/media/java/android/media/MediaTranscodeManager.java
@@ -20,17 +20,26 @@
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 @@
<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#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 @@
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 @@
@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 ConcurrentMap<Long, TranscodingJob> mPendingTranscodingJobs =
- new ConcurrentHashMap<>();
- private final Context mContext;
-
- /* Private constructor. */
- private MediaTranscodeManager(@NonNull Context context) {
- mContext = context;
- }
-
/**
* 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
@@ -192,6 +175,111 @@
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;
@@ -229,6 +317,9 @@
*/
private @Nullable MediaFormat mImageFormat = null;
+ @VisibleForTesting
+ private int mProcessingDelayMs = 0;
+
private TranscodingRequest(Builder b) {
mSourceUri = b.mSourceUri;
mDestinationUri = b.mDestinationUri;
@@ -237,6 +328,7 @@
mVideoTrackFormat = b.mVideoTrackFormat;
mAudioTrackFormat = b.mAudioTrackFormat;
mImageFormat = b.mImageFormat;
+ mProcessingDelayMs = b.mProcessingDelayMs;
}
/** Return the type of the transcoding. */
@@ -271,6 +363,20 @@
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.
@@ -280,10 +386,10 @@
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.
@@ -390,6 +496,17 @@
}
/**
+ * 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
@@ -467,51 +584,102 @@
/** 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 @@
* 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 @@
* 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 @@
*
* @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 @@
* @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 41a1f74..c07e38f 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 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 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 @@
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<Integer, ScheduledFuture<?>> mJobMap;
+
+ ProcessingJobRunnable(ITranscodingClientCallback callback,
+ TranscodingJobParcel job,
+ ConcurrentMap<Integer, ScheduledFuture<?>> jobMap) {
+ mJob = job;
+ mCallback = callback;
+ mJobMap = jobMap;
+ }
+
+ @Override
+ public void run() {
+ Log.d(TAG, "Start to process job " + mJob.jobId);
+ TranscodingResultParcel result = new TranscodingResultParcel();
+ try {
+ mCallback.onTranscodingFinished(mJob.jobId, result);
+ // Removes the job from job map.
+ mJobMap.remove(mJob.jobId);
+ } catch (RemoteException re) {
+ Log.e(TAG, "Failed to callback to client");
+ }
+ }
+ }
+
+ @Override
+ public ITranscodingClient registerClient(ITranscodingClientCallback callback,
+ String clientName, String opPackageName, int clientUid, int clientPid)
+ throws RemoteException {
+ Log.d(TAG, "MockTranscodingService creates one client");
+
+ ITranscodingClient client = new ITranscodingClient.Stub() {
+ private final ConcurrentMap<Integer, ScheduledFuture<?>> mPendingTranscodingJobs =
+ new ConcurrentHashMap<Integer, ScheduledFuture<?>>();
+
+ @Override
+ public boolean submitRequest(TranscodingRequestParcel inRequest,
+ TranscodingJobParcel outjob) {
+ Log.d(TAG, "Mock client gets submitRequest");
+ try {
+ outjob.request = inRequest;
+ outjob.jobId = mJobId.getAndIncrement();
+ Log.d(TAG, "Generate new job " + outjob.jobId);
+
+ // Schedules the job to run after inRequest.processingDelayMs.
+ ScheduledFuture<?> transcodingFuture = mJobScheduler.schedule(
+ new ProcessingJobRunnable(callback, outjob,
+ mPendingTranscodingJobs),
+ inRequest.testConfig == null ? 0
+ : inRequest.testConfig.processingDelayMs,
+ TimeUnit.MILLISECONDS);
+ mPendingTranscodingJobs.put(outjob.jobId, transcodingFuture);
+ } catch (RejectedExecutionException e) {
+ Log.e(TAG, "Failed to schedule transcoding job: " + e);
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean cancelJob(int jobId) throws RemoteException {
+ Log.d(TAG, "Mock client gets cancelJob " + jobId);
+ // Cancels the job is still in the mPendingTranscodingJobs.
+ if (mPendingTranscodingJobs.containsKey(jobId)) {
+ // Cancel the future task for transcoding.
+ mPendingTranscodingJobs.get(jobId).cancel(true);
+
+ // Remove the job from the mPendingTranscodingJobs.
+ mPendingTranscodingJobs.remove(jobId);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean getJobWithId(int jobId, TranscodingJobParcel job)
+ throws RemoteException {
+ // This will be implemented this if needed in the test.
+ return true;
+ }
+
+ @Override
+ public void unregister() throws RemoteException {
+ Log.d(TAG, "Mock client gets unregister");
+ // This will be implemented this if needed in the test.
+ mNumOfClients--;
+ }
+ };
+ mNumOfClients++;
+ return client;
+ }
+
+ @Override
+ public int getNumOfClients() throws RemoteException {
+ return mNumOfClients;
+ }
+ }
+
public MediaTranscodeManagerTest() {
super("com.android.MediaTranscodeManagerTest", MediaFrameworkTest.class);
}
@@ -89,6 +214,12 @@
return Uri.fromFile(outFile);
}
+ // Generates a invalid uri which will let the mock service return transcoding failure.
+ private static Uri generateInvalidTranscodingUri(Context context) {
+ File outFile = new File(context.getExternalCacheDir(), "InvalidUri.mp4");
+ return Uri.fromFile(outFile);
+ }
+
/**
* Creates a MediaFormat with the basic set of values.
*/
@@ -102,8 +233,9 @@
public void setUp() throws Exception {
Log.d(TAG, "setUp");
super.setUp();
+ mTranscodingService = new MockTranscodingService();
mContext = getInstrumentation().getContext();
- mMediaTranscodeManager = MediaTranscodeManager.getInstance(mContext);
+ mMediaTranscodeManager = MediaTranscodeManager.getInstance(mContext, mTranscodingService);
assertNotNull(mMediaTranscodeManager);
// Setup source HEVC file uri.
@@ -217,7 +349,7 @@
}
@Test
- public void testNormalTranscoding() throws InterruptedException {
+ public void testTranscodingOneVideo() throws Exception {
Log.d(TAG, "Starting: testMediaTranscodeManager");
Semaphore transcodeCompleteSemaphore = new Semaphore(0);
@@ -228,20 +360,17 @@
.setType(MediaTranscodeManager.TRANSCODING_TYPE_VIDEO)
.setPriority(MediaTranscodeManager.PRIORITY_REALTIME)
.setVideoTrackFormat(createMediaFormat())
+ .setProcessingDelayMs(300 /* delayMs */)
.build();
Executor listenerExecutor = Executors.newSingleThreadExecutor();
- TranscodingJob job;
- job = mMediaTranscodeManager.enqueueTranscodingRequest(request, listenerExecutor,
+ TranscodingJob job = mMediaTranscodeManager.enqueueRequest(request, listenerExecutor,
transcodingJob -> {
- Log.d(TAG, "Transcoding completed with result: " + transcodingJob.getResult());
+ Log.d(TAG, "Transcoding completed with result: ");
transcodeCompleteSemaphore.release();
});
assertNotNull(job);
- job.setOnProgressChangedListener(
- listenerExecutor, progress -> Log.d(TAG, "Progress: " + progress));
-
if (job != null) {
Log.d(TAG, "testMediaTranscodeManager - Waiting for transcode to complete.");
boolean finishedOnTime = transcodeCompleteSemaphore.tryAcquire(
@@ -249,4 +378,5 @@
assertTrue("Transcode failed to complete in time.", finishedOnTime);
}
}
+
}