Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f986ae1a authored by Hangyu Kuang's avatar Hangyu Kuang Committed by Android (Google) Code Review
Browse files

Merge "Skeletal implementation of a MediaTranscodeManager API with corresponding JNI layer."

parents f16dc070 5a1184d4
Loading
Loading
Loading
Loading
+403 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -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",
+6 −0
Original line number Diff line number Diff line
@@ -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;

+102 −0
Original line number Diff line number Diff line
/*
 * 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));
}
+1 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ android_test {
    ],
    static_libs: [
        "mockito-target-minus-junit4",
        "androidx.test.ext.junit",
        "androidx.test.rules",
        "android-ex-camera2",
    ],
Loading