Skeletal implementation of a MediaTranscodeManager API with corresponding JNI layer.

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
This commit is contained in:
Linus Nilsson
2019-12-19 11:11:21 -08:00
parent 88233e4c73
commit 5a1184d45e
6 changed files with 587 additions and 0 deletions

View File

@@ -0,0 +1,403 @@
/*
* 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();
}
}

View File

@@ -19,6 +19,7 @@ cc_library_shared {
"android_media_MediaProfiles.cpp",
"android_media_MediaRecorder.cpp",
"android_media_MediaSync.cpp",
"android_media_MediaTranscodeManager.cpp",
"android_media_ResampleInputStream.cpp",
"android_media_Streams.cpp",
"android_media_SyncParams.cpp",

View File

@@ -1453,6 +1453,7 @@ extern int register_android_media_MediaProfiles(JNIEnv *env);
extern int register_android_mtp_MtpDatabase(JNIEnv *env);
extern int register_android_mtp_MtpDevice(JNIEnv *env);
extern int register_android_mtp_MtpServer(JNIEnv *env);
extern int register_android_media_MediaTranscodeManager(JNIEnv *env);
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
@@ -1565,6 +1566,11 @@ jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
goto bail;
}
if (register_android_media_MediaTranscodeManager(env) < 0) {
ALOGE("ERROR: MediaTranscodeManager native registration failed");
goto bail;
}
/* success -- return valid version number */
result = JNI_VERSION_1_4;

View File

@@ -0,0 +1,102 @@
/*
* Copyright 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.
*/
//#define LOG_NDEBUG 0
#define LOG_TAG "MediaTranscodeManager_JNI"
#include "android_runtime/AndroidRuntime.h"
#include "jni.h"
#include <nativehelper/JNIHelp.h>
#include <utils/Log.h>
namespace {
// NOTE: Keep these enums in sync with their equivalents in MediaTranscodeManager.java.
enum {
ID_INVALID = -1
};
enum {
EVENT_JOB_STARTED = 1,
EVENT_JOB_PROGRESSED = 2,
EVENT_JOB_FINISHED = 3,
};
enum {
RESULT_NONE = 1,
RESULT_SUCCESS = 2,
RESULT_ERROR = 3,
RESULT_CANCELED = 4,
};
struct {
jmethodID postEventFromNative;
} gMediaTranscodeManagerClassInfo;
using namespace android;
void android_media_MediaTranscodeManager_native_init(JNIEnv *env, jclass clazz) {
ALOGV("android_media_MediaTranscodeManager_native_init");
gMediaTranscodeManagerClassInfo.postEventFromNative = env->GetMethodID(
clazz, "postEventFromNative", "(IJI)V");
LOG_ALWAYS_FATAL_IF(gMediaTranscodeManagerClassInfo.postEventFromNative == NULL,
"can't find android/media/MediaTranscodeManager.postEventFromNative");
}
jlong android_media_MediaTranscodeManager_requestUniqueJobID(
JNIEnv *env __unused, jobject thiz __unused) {
ALOGV("android_media_MediaTranscodeManager_reserveUniqueJobID");
static std::atomic_int32_t sJobIDCounter{0};
jlong id = (jlong)++sJobIDCounter;
return id;
}
jboolean android_media_MediaTranscodeManager_enqueueTranscodingRequest(
JNIEnv *env, jobject thiz, jlong id, jobject request, jobject context __unused) {
ALOGV("android_media_MediaTranscodeManager_enqueueTranscodingRequest");
if (!request) {
return ID_INVALID;
}
env->CallVoidMethod(thiz, gMediaTranscodeManagerClassInfo.postEventFromNative,
EVENT_JOB_FINISHED, id, RESULT_ERROR);
return true;
}
void android_media_MediaTranscodeManager_cancelTranscodingRequest(
JNIEnv *env __unused, jobject thiz __unused, jlong jobID __unused) {
ALOGV("android_media_MediaTranscodeManager_cancelTranscodingRequest");
}
const JNINativeMethod gMethods[] = {
{ "native_init", "()V",
(void *)android_media_MediaTranscodeManager_native_init },
{ "native_requestUniqueJobID", "()J",
(void *)android_media_MediaTranscodeManager_requestUniqueJobID },
{ "native_enqueueTranscodingRequest",
"(JLandroid/media/MediaTranscodeManager$TranscodingRequest;Landroid/content/Context;)Z",
(void *)android_media_MediaTranscodeManager_enqueueTranscodingRequest },
{ "native_cancelTranscodingRequest", "(J)V",
(void *)android_media_MediaTranscodeManager_cancelTranscodingRequest },
};
} // namespace anonymous
int register_android_media_MediaTranscodeManager(JNIEnv *env) {
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaTranscodeManager", gMethods, NELEM(gMethods));
}

View File

@@ -7,6 +7,7 @@ android_test {
],
static_libs: [
"mockito-target-minus-junit4",
"androidx.test.ext.junit",
"androidx.test.rules",
"android-ex-camera2",
],

View File

@@ -0,0 +1,74 @@
/*
* 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 com.android.mediaframeworktest.functional.mediatranscodemanager;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import android.media.MediaTranscodeManager;
import android.util.Log;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
public class MediaTranscodeManagerTest {
private static final String TAG = "MediaTranscodeManagerTest";
/** The time to wait for the transcode operation to complete before failing the test. */
private static final int TRANSCODE_TIMEOUT_SECONDS = 2;
@Test
public void testMediaTranscodeManager() throws InterruptedException {
Log.d(TAG, "Starting: testMediaTranscodeManager");
Semaphore transcodeCompleteSemaphore = new Semaphore(0);
MediaTranscodeManager.TranscodingRequest request =
new MediaTranscodeManager.TranscodingRequest.Builder().build();
Executor listenerExecutor = Executors.newSingleThreadExecutor();
MediaTranscodeManager mediaTranscodeManager =
MediaTranscodeManager.getInstance(ApplicationProvider.getApplicationContext());
assertNotNull(mediaTranscodeManager);
MediaTranscodeManager.TranscodingJob job;
job = mediaTranscodeManager.enqueueTranscodingRequest(request, listenerExecutor,
transcodingJob -> {
Log.d(TAG, "Transcoding completed with result: " + transcodingJob.getResult());
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(
TRANSCODE_TIMEOUT_SECONDS, TimeUnit.SECONDS);
assertTrue("Transcode failed to complete in time.", finishedOnTime);
}
}
}