1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media;
18 
19 import android.annotation.CallbackExecutor;
20 import android.annotation.IntDef;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.Context;
24 import android.net.Uri;
25 import android.util.Log;
26 
27 import com.android.internal.util.Preconditions;
28 
29 import java.lang.annotation.Retention;
30 import java.lang.annotation.RetentionPolicy;
31 import java.util.concurrent.ConcurrentHashMap;
32 import java.util.concurrent.ConcurrentMap;
33 import java.util.concurrent.Executor;
34 import java.util.concurrent.locks.ReentrantLock;
35 
36 /**
37  * MediaTranscodeManager provides an interface to the system's media transcode service.
38  * Transcode requests are put in a queue and processed in order. When a transcode operation is
39  * completed the caller is notified via its OnTranscodingFinishedListener. In the meantime the
40  * caller may use the returned TranscodingJob object to cancel or check the status of a specific
41  * transcode operation.
42  * The currently supported media types are video and still images.
43  *
44  * TODO(lnilsson): Add sample code when API is settled.
45  *
46  * @hide
47  */
48 public final class MediaTranscodeManager {
49     private static final String TAG = "MediaTranscodeManager";
50 
51     // Invalid ID passed from native means the request was never enqueued.
52     private static final long ID_INVALID = -1;
53 
54     // Events passed from native.
55     private static final int EVENT_JOB_STARTED = 1;
56     private static final int EVENT_JOB_PROGRESSED = 2;
57     private static final int EVENT_JOB_FINISHED = 3;
58 
59     @IntDef(prefix = { "EVENT_" }, value = {
60             EVENT_JOB_STARTED,
61             EVENT_JOB_PROGRESSED,
62             EVENT_JOB_FINISHED,
63     })
64     @Retention(RetentionPolicy.SOURCE)
65     public @interface Event {}
66 
67     private static MediaTranscodeManager sMediaTranscodeManager;
68     private final ConcurrentMap<Long, TranscodingJob> mPendingTranscodingJobs =
69             new ConcurrentHashMap<>();
70     private final Context mContext;
71 
72     /**
73      * Listener that gets notified when a transcoding operation has finished.
74      * This listener gets notified regardless of how the operation finished. It is up to the
75      * listener implementation to check the result and take appropriate action.
76      */
77     @FunctionalInterface
78     public interface OnTranscodingFinishedListener {
79         /**
80          * Called when the transcoding operation has finished. The receiver may use the
81          * TranscodingJob to check the result, i.e. whether the operation succeeded, was canceled or
82          * if an error occurred.
83          * @param transcodingJob The TranscodingJob instance for the finished transcoding operation.
84          */
onTranscodingFinished(@onNull TranscodingJob transcodingJob)85         void onTranscodingFinished(@NonNull TranscodingJob transcodingJob);
86     }
87 
88     /**
89      * Class describing a transcode operation to be performed. The caller uses this class to
90      * configure a transcoding operation that can then be enqueued using MediaTranscodeManager.
91      */
92     public static final class TranscodingRequest {
93         private Uri mSrcUri;
94         private Uri mDstUri;
95         private MediaFormat mDstFormat;
96 
TranscodingRequest(Builder b)97         private TranscodingRequest(Builder b) {
98             mSrcUri = b.mSrcUri;
99             mDstUri = b.mDstUri;
100             mDstFormat = b.mDstFormat;
101         }
102 
103         /** TranscodingRequest builder class. */
104         public static class Builder {
105             private Uri mSrcUri;
106             private Uri mDstUri;
107             private MediaFormat mDstFormat;
108 
109             /**
110              * Specifies the source media file.
111              * @param uri Content uri for the source media file.
112              * @return The builder instance.
113              */
setSourceUri(Uri uri)114             public Builder setSourceUri(Uri uri) {
115                 mSrcUri = uri;
116                 return this;
117             }
118 
119             /**
120              * Specifies the destination media file.
121              * @param uri Content uri for the destination media file.
122              * @return The builder instance.
123              */
setDestinationUri(Uri uri)124             public Builder setDestinationUri(Uri uri) {
125                 mDstUri = uri;
126                 return this;
127             }
128 
129             /**
130              * Specifies the media format of the transcoded media file.
131              * @param dstFormat MediaFormat containing the desired destination format.
132              * @return The builder instance.
133              */
setDestinationFormat(MediaFormat dstFormat)134             public Builder setDestinationFormat(MediaFormat dstFormat) {
135                 mDstFormat = dstFormat;
136                 return this;
137             }
138 
139             /**
140              * Builds a new TranscodingRequest with the configuration set on this builder.
141              * @return A new TranscodingRequest.
142              */
build()143             public TranscodingRequest build() {
144                 return new TranscodingRequest(this);
145             }
146         }
147     }
148 
149     /**
150      * Handle to an enqueued transcoding operation. An instance of this class represents a single
151      * enqueued transcoding operation. The caller can use that instance to query the status or
152      * progress, and to get the result once the operation has completed.
153      */
154     public static final class TranscodingJob {
155         /** The job is enqueued but not yet running. */
156         public static final int STATUS_PENDING = 1;
157         /** The job is currently running. */
158         public static final int STATUS_RUNNING = 2;
159         /** The job is finished. */
160         public static final int STATUS_FINISHED = 3;
161 
162         @IntDef(prefix = { "STATUS_" }, value = {
163                 STATUS_PENDING,
164                 STATUS_RUNNING,
165                 STATUS_FINISHED,
166         })
167         @Retention(RetentionPolicy.SOURCE)
168         public @interface Status {}
169 
170         /** The job does not have a result yet. */
171         public static final int RESULT_NONE = 1;
172         /** The job completed successfully. */
173         public static final int RESULT_SUCCESS = 2;
174         /** The job encountered an error while running. */
175         public static final int RESULT_ERROR = 3;
176         /** The job was canceled by the caller. */
177         public static final int RESULT_CANCELED = 4;
178 
179         @IntDef(prefix = { "RESULT_" }, value = {
180                 RESULT_NONE,
181                 RESULT_SUCCESS,
182                 RESULT_ERROR,
183                 RESULT_CANCELED,
184         })
185         @Retention(RetentionPolicy.SOURCE)
186         public @interface Result {}
187 
188         /** Listener that gets notified when the progress changes. */
189         @FunctionalInterface
190         public interface OnProgressChangedListener {
191 
192             /**
193              * Called when the progress changes. The progress is between 0 and 1, where 0 means
194              * that the job has not yet started and 1 means that it has finished.
195              * @param progress The new progress.
196              */
onProgressChanged(float progress)197             void onProgressChanged(float progress);
198         }
199 
200         private final Executor mExecutor;
201         private final OnTranscodingFinishedListener mListener;
202         private final ReentrantLock mStatusChangeLock = new ReentrantLock();
203         private Executor mProgressChangedExecutor;
204         private OnProgressChangedListener mProgressChangedListener;
205         private long mID;
206         private float mProgress = 0.0f;
207         private @Status int mStatus = STATUS_PENDING;
208         private @Result int mResult = RESULT_NONE;
209 
TranscodingJob(long id, @NonNull @CallbackExecutor Executor executor, @NonNull OnTranscodingFinishedListener listener)210         private TranscodingJob(long id, @NonNull @CallbackExecutor Executor executor,
211                 @NonNull OnTranscodingFinishedListener listener) {
212             mID = id;
213             mExecutor = executor;
214             mListener = listener;
215         }
216 
217         /**
218          * Set a progress listener.
219          * @param listener The progress listener.
220          */
setOnProgressChangedListener(@onNull @allbackExecutor Executor executor, @Nullable OnProgressChangedListener listener)221         public void setOnProgressChangedListener(@NonNull @CallbackExecutor Executor executor,
222                 @Nullable OnProgressChangedListener listener) {
223             mProgressChangedExecutor = executor;
224             mProgressChangedListener = listener;
225         }
226 
227         /**
228          * Cancels the transcoding job and notify the listener. If the job happened to finish before
229          * being canceled this call is effectively a no-op and will not update the result in that
230          * case.
231          */
cancel()232         public void cancel() {
233             setJobFinished(RESULT_CANCELED);
234             sMediaTranscodeManager.native_cancelTranscodingRequest(mID);
235         }
236 
237         /**
238          * Gets the progress of the transcoding job. The progress is between 0 and 1, where 0 means
239          * that the job has not yet started and 1 means that it is finished.
240          * @return The progress.
241          */
getProgress()242         public float getProgress() {
243             return mProgress;
244         }
245 
246         /**
247          * Gets the status of the transcoding job.
248          * @return The status.
249          */
getStatus()250         public @Status int getStatus() {
251             return mStatus;
252         }
253 
254         /**
255          * Gets the result of the transcoding job.
256          * @return The result.
257          */
getResult()258         public @Result int getResult() {
259             return mResult;
260         }
261 
setJobStarted()262         private void setJobStarted() {
263             mStatus = STATUS_RUNNING;
264         }
265 
setJobProgress(float newProgress)266         private void setJobProgress(float newProgress) {
267             mProgress = newProgress;
268 
269             // Notify listener.
270             OnProgressChangedListener onProgressChangedListener = mProgressChangedListener;
271             if (onProgressChangedListener != null) {
272                 mProgressChangedExecutor.execute(
273                         () -> onProgressChangedListener.onProgressChanged(mProgress));
274             }
275         }
276 
setJobFinished(int result)277         private void setJobFinished(int result) {
278             boolean doNotifyListener = false;
279 
280             // Prevent conflicting simultaneous status updates from native (finished) and from the
281             // caller (cancel).
282             try {
283                 mStatusChangeLock.lock();
284                 if (mStatus != STATUS_FINISHED) {
285                     mStatus = STATUS_FINISHED;
286                     mResult = result;
287                     doNotifyListener = true;
288                 }
289             } finally {
290                 mStatusChangeLock.unlock();
291             }
292 
293             if (doNotifyListener) {
294                 mExecutor.execute(() -> mListener.onTranscodingFinished(this));
295             }
296         }
297 
processJobEvent(@vent int event, int arg)298         private void processJobEvent(@Event int event, int arg) {
299             switch (event) {
300                 case EVENT_JOB_STARTED:
301                     setJobStarted();
302                     break;
303                 case EVENT_JOB_PROGRESSED:
304                     setJobProgress((float) arg / 100);
305                     break;
306                 case EVENT_JOB_FINISHED:
307                     setJobFinished(arg);
308                     break;
309                 default:
310                     Log.e(TAG, "Unsupported event: " + event);
311                     break;
312             }
313         }
314     }
315 
316     // Initializes the native library.
native_init()317     private static native void native_init();
318     // Requests a new job ID from the native service.
native_requestUniqueJobID()319     private native long native_requestUniqueJobID();
320     // Enqueues a transcoding request to the native service.
native_enqueueTranscodingRequest( long id, @NonNull TranscodingRequest transcodingRequest, @NonNull Context context)321     private native boolean native_enqueueTranscodingRequest(
322             long id, @NonNull TranscodingRequest transcodingRequest, @NonNull Context context);
323     // Cancels an enqueued transcoding request.
native_cancelTranscodingRequest(long id)324     private native void native_cancelTranscodingRequest(long id);
325 
326     // Private constructor.
MediaTranscodeManager(@onNull Context context)327     private MediaTranscodeManager(@NonNull Context context) {
328         mContext = context;
329     }
330 
331     // Events posted from the native service.
332     @SuppressWarnings("unused")
postEventFromNative(@vent int event, long id, int arg)333     private void postEventFromNative(@Event int event, long id, int arg) {
334         Log.d(TAG, String.format("postEventFromNative. Event %d, ID %d, arg %d", event, id, arg));
335 
336         TranscodingJob transcodingJob = mPendingTranscodingJobs.get(id);
337 
338         // Job IDs are added to the tracking set before the job is enqueued so it should never
339         // be null unless the service misbehaves.
340         if (transcodingJob == null) {
341             Log.e(TAG, "No matching transcode job found for id " + id);
342             return;
343         }
344 
345         transcodingJob.processJobEvent(event, arg);
346     }
347 
348     /**
349      * Gets the MediaTranscodeManager singleton instance.
350      * @param context The application context.
351      * @return the {@link MediaTranscodeManager} singleton instance.
352      */
getInstance(@onNull Context context)353     public static MediaTranscodeManager getInstance(@NonNull Context context) {
354         Preconditions.checkNotNull(context);
355         synchronized (MediaTranscodeManager.class) {
356             if (sMediaTranscodeManager == null) {
357                 sMediaTranscodeManager = new MediaTranscodeManager(context.getApplicationContext());
358             }
359             return sMediaTranscodeManager;
360         }
361     }
362 
363     /**
364      * Enqueues a TranscodingRequest for execution.
365      * @param transcodingRequest The TranscodingRequest to enqueue.
366      * @param listenerExecutor Executor on which the listener is notified.
367      * @param listener Listener to get notified when the transcoding job is finished.
368      * @return A TranscodingJob for this operation.
369      */
enqueueTranscodingRequest( @onNull TranscodingRequest transcodingRequest, @NonNull @CallbackExecutor Executor listenerExecutor, @NonNull OnTranscodingFinishedListener listener)370     public @Nullable TranscodingJob enqueueTranscodingRequest(
371             @NonNull TranscodingRequest transcodingRequest,
372             @NonNull @CallbackExecutor Executor listenerExecutor,
373             @NonNull OnTranscodingFinishedListener listener) {
374         Log.i(TAG, "enqueueTranscodingRequest called.");
375         Preconditions.checkNotNull(transcodingRequest);
376         Preconditions.checkNotNull(listenerExecutor);
377         Preconditions.checkNotNull(listener);
378 
379         // Reserve a job ID.
380         long jobID = native_requestUniqueJobID();
381         if (jobID == ID_INVALID) {
382             return null;
383         }
384 
385         // Add the job to the tracking set.
386         TranscodingJob transcodingJob = new TranscodingJob(jobID, listenerExecutor, listener);
387         mPendingTranscodingJobs.put(jobID, transcodingJob);
388 
389         // Enqueue the request with the native service.
390         boolean enqueued = native_enqueueTranscodingRequest(jobID, transcodingRequest, mContext);
391         if (!enqueued) {
392             mPendingTranscodingJobs.remove(jobID);
393             return null;
394         }
395 
396         return transcodingJob;
397     }
398 
399     static {
400         System.loadLibrary("media_jni");
native_init()401         native_init();
402     }
403 }
404