diff options
author | Linus Nilsson <lnilsson@google.com> | 2019-12-19 11:11:21 -0800 |
---|---|---|
committer | Linus Nilsson <lnilsson@google.com> | 2020-01-09 10:24:41 -0800 |
commit | 5a1184d45ea7669e2235ebe8da63134ea2834231 (patch) | |
tree | fde1bc1ad7096711ecf89f390dda71807462e193 | |
parent | 88233e4c731856bd4d3fdb8a598b4804f28c4024 (diff) |
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
6 files changed, 587 insertions, 0 deletions
diff --git a/media/java/android/media/MediaTranscodeManager.java b/media/java/android/media/MediaTranscodeManager.java new file mode 100644 index 000000000000..2c431b98fc7a --- /dev/null +++ b/media/java/android/media/MediaTranscodeManager.java @@ -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(); + } +} diff --git a/media/jni/Android.bp b/media/jni/Android.bp index 4ca23a1084a2..7acd62e83300 100644 --- a/media/jni/Android.bp +++ b/media/jni/Android.bp @@ -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", diff --git a/media/jni/android_media_MediaPlayer.cpp b/media/jni/android_media_MediaPlayer.cpp index 963b650292e4..5cb42a9a96cc 100644 --- a/media/jni/android_media_MediaPlayer.cpp +++ b/media/jni/android_media_MediaPlayer.cpp @@ -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; diff --git a/media/jni/android_media_MediaTranscodeManager.cpp b/media/jni/android_media_MediaTranscodeManager.cpp new file mode 100644 index 000000000000..0b4048c1170c --- /dev/null +++ b/media/jni/android_media_MediaTranscodeManager.cpp @@ -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)); +} diff --git a/media/tests/MediaFrameworkTest/Android.bp b/media/tests/MediaFrameworkTest/Android.bp index f0fbc509cc9d..ecbe2b3eb8b7 100644 --- a/media/tests/MediaFrameworkTest/Android.bp +++ b/media/tests/MediaFrameworkTest/Android.bp @@ -7,6 +7,7 @@ android_test { ], static_libs: [ "mockito-target-minus-junit4", + "androidx.test.ext.junit", "androidx.test.rules", "android-ex-camera2", ], 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 new file mode 100644 index 000000000000..eeda50e5c095 --- /dev/null +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/mediatranscodemanager/MediaTranscodeManagerTest.java @@ -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); + } + } +} |