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
This commit is contained in:
hkuang
2020-05-18 15:40:17 -07:00
parent fa0e5dfdb2
commit 6953d2bb6e
3 changed files with 440 additions and 175 deletions

View File

@@ -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",

View File

@@ -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;
<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 @@ 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 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 @@ public final class MediaTranscodeManager {
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 @@ public final class MediaTranscodeManager {
*/
private @Nullable MediaFormat mImageFormat = null;
@VisibleForTesting
private int mProcessingDelayMs = 0;
private TranscodingRequest(Builder b) {
mSourceUri = b.mSourceUri;
mDestinationUri = b.mDestinationUri;
@@ -237,6 +328,7 @@ public final class MediaTranscodeManager {
mVideoTrackFormat = b.mVideoTrackFormat;
mAudioTrackFormat = b.mAudioTrackFormat;
mImageFormat = b.mImageFormat;
mProcessingDelayMs = b.mProcessingDelayMs;
}
/** Return the type of the transcoding. */
@@ -271,6 +363,20 @@ public final class MediaTranscodeManager {
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 @@ 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 <code>Builder</code>.
@@ -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();
}
}

View File

@@ -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<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 @@ public class MediaTranscodeManagerTest
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 class MediaTranscodeManagerTest
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 @@ public class MediaTranscodeManagerTest
}
@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 @@ public class MediaTranscodeManagerTest
.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 @@ public class MediaTranscodeManagerTest
assertTrue("Transcode failed to complete in time.", finishedOnTime);
}
}
}