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