Aurimas Liutikas | bcfbe3a | 2023-10-30 15:25:21 -0700 | [diff] [blame] | 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.IntRange; |
| 22 | import android.annotation.NonNull; |
| 23 | import android.annotation.Nullable; |
| 24 | import android.annotation.SystemApi; |
| 25 | import android.content.ContentResolver; |
| 26 | import android.content.Context; |
| 27 | import android.content.res.AssetFileDescriptor; |
| 28 | import android.net.Uri; |
| 29 | import android.os.Build; |
| 30 | import android.os.ParcelFileDescriptor; |
| 31 | import android.os.RemoteException; |
| 32 | import android.os.ServiceSpecificException; |
| 33 | import android.system.Os; |
| 34 | import android.util.Log; |
| 35 | |
| 36 | import androidx.annotation.RequiresApi; |
| 37 | |
| 38 | import com.android.internal.annotations.GuardedBy; |
| 39 | import com.android.internal.annotations.VisibleForTesting; |
| 40 | import com.android.modules.annotation.MinSdk; |
| 41 | import com.android.modules.utils.build.SdkLevel; |
| 42 | |
| 43 | import java.io.FileNotFoundException; |
| 44 | import java.lang.annotation.Retention; |
| 45 | import java.lang.annotation.RetentionPolicy; |
| 46 | import java.util.ArrayList; |
| 47 | import java.util.HashMap; |
| 48 | import java.util.List; |
| 49 | import java.util.Map; |
| 50 | import java.util.Objects; |
| 51 | import java.util.concurrent.Executor; |
| 52 | import java.util.concurrent.ExecutorService; |
| 53 | import java.util.concurrent.Executors; |
| 54 | |
| 55 | /** |
| 56 | Android 12 introduces Compatible media transcoding feature. See |
| 57 | <a href="https://developer.android.com/about/versions/12/features#compatible_media_transcoding"> |
| 58 | Compatible media transcoding</a>. MediaTranscodingManager provides an interface to the system's media |
| 59 | transcoding service and can be used to transcode media files, e.g. transcoding a video from HEVC to |
| 60 | AVC. |
| 61 | |
| 62 | <h3>Transcoding Types</h3> |
| 63 | <h4>Video Transcoding</h4> |
| 64 | When transcoding a video file, the video track will be transcoded based on the desired track format |
| 65 | and the audio track will be pass through without any modification. |
| 66 | <p class=note> |
| 67 | Note that currently only support transcoding video file in mp4 format and with single video track. |
| 68 | |
| 69 | <h3>Transcoding Request</h3> |
| 70 | <p> |
| 71 | To transcode a media file, first create a {@link TranscodingRequest} through its builder class |
| 72 | {@link VideoTranscodingRequest.Builder}. Transcode requests are then enqueue to the manager through |
| 73 | {@link MediaTranscodingManager#enqueueRequest( |
| 74 | TranscodingRequest, Executor, OnTranscodingFinishedListener)} |
| 75 | TranscodeRequest are processed based on client process's priority and request priority. When a |
| 76 | transcode operation is completed the caller is notified via its |
| 77 | {@link OnTranscodingFinishedListener}. |
| 78 | In the meantime the caller may use the returned TranscodingSession object to cancel or check the |
| 79 | status of a specific transcode operation. |
| 80 | <p> |
| 81 | Here is an example where <code>Builder</code> is used to specify all parameters |
| 82 | |
| 83 | <pre class=prettyprint> |
| 84 | VideoTranscodingRequest request = |
| 85 | new VideoTranscodingRequest.Builder(srcUri, dstUri, videoFormat).build(); |
| 86 | }</pre> |
| 87 | @hide |
| 88 | */ |
| 89 | @MinSdk(Build.VERSION_CODES.S) |
| 90 | @RequiresApi(Build.VERSION_CODES.S) |
| 91 | @SystemApi |
| 92 | public final class MediaTranscodingManager { |
| 93 | private static final String TAG = "MediaTranscodingManager"; |
| 94 | |
| 95 | /** Maximum number of retry to connect to the service. */ |
| 96 | private static final int CONNECT_SERVICE_RETRY_COUNT = 100; |
| 97 | |
| 98 | /** Interval between trying to reconnect to the service. */ |
| 99 | private static final int INTERVAL_CONNECT_SERVICE_RETRY_MS = 40; |
| 100 | |
| 101 | /** Default bpp(bits-per-pixel) to use for calculating default bitrate. */ |
| 102 | private static final float BPP = 0.25f; |
| 103 | |
| 104 | /** |
| 105 | * Listener that gets notified when a transcoding operation has finished. |
| 106 | * This listener gets notified regardless of how the operation finished. It is up to the |
| 107 | * listener implementation to check the result and take appropriate action. |
| 108 | */ |
| 109 | @FunctionalInterface |
| 110 | public interface OnTranscodingFinishedListener { |
| 111 | /** |
| 112 | * Called when the transcoding operation has finished. The receiver may use the |
| 113 | * TranscodingSession to check the result, i.e. whether the operation succeeded, was |
| 114 | * canceled or if an error occurred. |
| 115 | * |
| 116 | * @param session The TranscodingSession instance for the finished transcoding operation. |
| 117 | */ |
| 118 | void onTranscodingFinished(@NonNull TranscodingSession session); |
| 119 | } |
| 120 | |
| 121 | private final Context mContext; |
| 122 | private ContentResolver mContentResolver; |
| 123 | private final String mPackageName; |
| 124 | private final int mPid; |
| 125 | private final int mUid; |
| 126 | private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); |
| 127 | private final HashMap<Integer, TranscodingSession> mPendingTranscodingSessions = new HashMap(); |
| 128 | private final Object mLock = new Object(); |
| 129 | @GuardedBy("mLock") |
| 130 | @NonNull private ITranscodingClient mTranscodingClient = null; |
| 131 | private static MediaTranscodingManager sMediaTranscodingManager; |
| 132 | |
| 133 | private void handleTranscodingFinished(int sessionId, TranscodingResultParcel result) { |
| 134 | synchronized (mPendingTranscodingSessions) { |
| 135 | // Gets the session associated with the sessionId and removes it from |
| 136 | // mPendingTranscodingSessions. |
| 137 | final TranscodingSession session = mPendingTranscodingSessions.remove(sessionId); |
| 138 | |
| 139 | if (session == null) { |
| 140 | // This should not happen in reality. |
| 141 | Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions"); |
| 142 | return; |
| 143 | } |
| 144 | |
| 145 | // Updates the session status and result. |
| 146 | session.updateStatusAndResult(TranscodingSession.STATUS_FINISHED, |
| 147 | TranscodingSession.RESULT_SUCCESS, |
| 148 | TranscodingSession.ERROR_NONE); |
| 149 | |
| 150 | // Notifies client the session is done. |
| 151 | if (session.mListener != null && session.mListenerExecutor != null) { |
| 152 | session.mListenerExecutor.execute( |
| 153 | () -> session.mListener.onTranscodingFinished(session)); |
| 154 | } |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | private void handleTranscodingFailed(int sessionId, int errorCode) { |
| 159 | synchronized (mPendingTranscodingSessions) { |
| 160 | // Gets the session associated with the sessionId and removes it from |
| 161 | // mPendingTranscodingSessions. |
| 162 | final TranscodingSession session = mPendingTranscodingSessions.remove(sessionId); |
| 163 | |
| 164 | if (session == null) { |
| 165 | // This should not happen in reality. |
| 166 | Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions"); |
| 167 | return; |
| 168 | } |
| 169 | |
| 170 | // Updates the session status and result. |
| 171 | session.updateStatusAndResult(TranscodingSession.STATUS_FINISHED, |
| 172 | TranscodingSession.RESULT_ERROR, errorCode); |
| 173 | |
| 174 | // Notifies client the session failed. |
| 175 | if (session.mListener != null && session.mListenerExecutor != null) { |
| 176 | session.mListenerExecutor.execute( |
| 177 | () -> session.mListener.onTranscodingFinished(session)); |
| 178 | } |
| 179 | } |
| 180 | } |
| 181 | |
| 182 | private void handleTranscodingProgressUpdate(int sessionId, int newProgress) { |
| 183 | synchronized (mPendingTranscodingSessions) { |
| 184 | // Gets the session associated with the sessionId. |
| 185 | final TranscodingSession session = mPendingTranscodingSessions.get(sessionId); |
| 186 | |
| 187 | if (session == null) { |
| 188 | // This should not happen in reality. |
| 189 | Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions"); |
| 190 | return; |
| 191 | } |
| 192 | |
| 193 | // Update session progress and notify clients. |
| 194 | session.updateProgress(newProgress); |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | private IMediaTranscodingService getService(boolean retry) { |
| 199 | // Do not try to get the service on pre-S. The service is lazy-start and getting the |
| 200 | // service could block. |
| 201 | if (!SdkLevel.isAtLeastS()) { |
| 202 | return null; |
| 203 | } |
| 204 | |
| 205 | int retryCount = !retry ? 1 : CONNECT_SERVICE_RETRY_COUNT; |
| 206 | Log.i(TAG, "get service with retry " + retryCount); |
| 207 | for (int count = 1; count <= retryCount; count++) { |
| 208 | Log.d(TAG, "Trying to connect to service. Try count: " + count); |
| 209 | IMediaTranscodingService service = IMediaTranscodingService.Stub.asInterface( |
| 210 | MediaFrameworkInitializer |
| 211 | .getMediaServiceManager() |
| 212 | .getMediaTranscodingServiceRegisterer() |
| 213 | .get()); |
| 214 | if (service != null) { |
| 215 | return service; |
| 216 | } |
| 217 | try { |
| 218 | // Sleep a bit before retry. |
| 219 | Thread.sleep(INTERVAL_CONNECT_SERVICE_RETRY_MS); |
| 220 | } catch (InterruptedException ie) { |
| 221 | /* ignore */ |
| 222 | } |
| 223 | } |
| 224 | Log.w(TAG, "Failed to get service"); |
| 225 | return null; |
| 226 | } |
| 227 | |
| 228 | /* |
| 229 | * Handle client binder died event. |
| 230 | * Upon receiving a binder died event of the client, we will do the following: |
| 231 | * 1) For the session that is running, notify the client that the session is failed with |
| 232 | * error code, so client could choose to retry the session or not. |
| 233 | * TODO(hkuang): Add a new error code to signal service died error. |
| 234 | * 2) For the sessions that is still pending or paused, we will resubmit the session |
| 235 | * once we successfully reconnect to the service and register a new client. |
| 236 | * 3) When trying to connect to the service and register a new client. The service may need time |
| 237 | * to reboot or never boot up again. So we will retry for a number of times. If we still |
| 238 | * could not connect, we will notify client session failure for the pending and paused |
| 239 | * sessions. |
| 240 | */ |
| 241 | private void onClientDied() { |
| 242 | synchronized (mLock) { |
| 243 | mTranscodingClient = null; |
| 244 | } |
| 245 | |
| 246 | // Delegates the session notification and retry to the executor as it may take some time. |
| 247 | mExecutor.execute(() -> { |
| 248 | // List to track the sessions that we want to retry. |
| 249 | List<TranscodingSession> retrySessions = new ArrayList<TranscodingSession>(); |
| 250 | |
| 251 | // First notify the client of session failure for all the running sessions. |
| 252 | synchronized (mPendingTranscodingSessions) { |
| 253 | for (Map.Entry<Integer, TranscodingSession> entry : |
| 254 | mPendingTranscodingSessions.entrySet()) { |
| 255 | TranscodingSession session = entry.getValue(); |
| 256 | |
| 257 | if (session.getStatus() == TranscodingSession.STATUS_RUNNING) { |
| 258 | session.updateStatusAndResult(TranscodingSession.STATUS_FINISHED, |
| 259 | TranscodingSession.RESULT_ERROR, |
| 260 | TranscodingSession.ERROR_SERVICE_DIED); |
| 261 | |
| 262 | // Remove the session from pending sessions. |
| 263 | mPendingTranscodingSessions.remove(entry.getKey()); |
| 264 | |
| 265 | if (session.mListener != null && session.mListenerExecutor != null) { |
| 266 | Log.i(TAG, "Notify client session failed"); |
| 267 | session.mListenerExecutor.execute( |
| 268 | () -> session.mListener.onTranscodingFinished(session)); |
| 269 | } |
| 270 | } else if (session.getStatus() == TranscodingSession.STATUS_PENDING |
| 271 | || session.getStatus() == TranscodingSession.STATUS_PAUSED) { |
| 272 | // Add the session to retrySessions to handle them later. |
| 273 | retrySessions.add(session); |
| 274 | } |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | // Try to register with the service once it boots up. |
| 279 | IMediaTranscodingService service = getService(true /*retry*/); |
| 280 | boolean haveTranscodingClient = false; |
| 281 | if (service != null) { |
| 282 | synchronized (mLock) { |
| 283 | mTranscodingClient = registerClient(service); |
| 284 | if (mTranscodingClient != null) { |
| 285 | haveTranscodingClient = true; |
| 286 | } |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | for (TranscodingSession session : retrySessions) { |
| 291 | // Notify the session failure if we fails to connect to the service or fail |
| 292 | // to retry the session. |
| 293 | if (!haveTranscodingClient) { |
| 294 | // TODO(hkuang): Return correct error code to the client. |
| 295 | handleTranscodingFailed(session.getSessionId(), 0 /*unused */); |
| 296 | } |
| 297 | |
| 298 | try { |
| 299 | // Do not set hasRetried for retry initiated by MediaTranscodingManager. |
| 300 | session.retryInternal(false /*setHasRetried*/); |
| 301 | } catch (Exception re) { |
| 302 | // TODO(hkuang): Return correct error code to the client. |
| 303 | handleTranscodingFailed(session.getSessionId(), 0 /*unused */); |
| 304 | } |
| 305 | } |
| 306 | }); |
| 307 | } |
| 308 | |
| 309 | private void updateStatus(int sessionId, int status) { |
| 310 | synchronized (mPendingTranscodingSessions) { |
| 311 | final TranscodingSession session = mPendingTranscodingSessions.get(sessionId); |
| 312 | |
| 313 | if (session == null) { |
| 314 | // This should not happen in reality. |
| 315 | Log.e(TAG, "Session " + sessionId + " is not in Pendingsessions"); |
| 316 | return; |
| 317 | } |
| 318 | |
| 319 | // Updates the session status. |
| 320 | session.updateStatus(status); |
| 321 | } |
| 322 | } |
| 323 | |
| 324 | // Just forwards all the events to the event handler. |
| 325 | private ITranscodingClientCallback mTranscodingClientCallback = |
| 326 | new ITranscodingClientCallback.Stub() { |
| 327 | // TODO(hkuang): Add more unit test to test difference file open mode. |
| 328 | @Override |
| 329 | public ParcelFileDescriptor openFileDescriptor(String fileUri, String mode) |
| 330 | throws RemoteException { |
| 331 | if (!mode.equals("r") && !mode.equals("w") && !mode.equals("rw")) { |
| 332 | Log.e(TAG, "Unsupport mode: " + mode); |
| 333 | return null; |
| 334 | } |
| 335 | |
| 336 | Uri uri = Uri.parse(fileUri); |
| 337 | try { |
| 338 | AssetFileDescriptor afd = mContentResolver.openAssetFileDescriptor(uri, |
| 339 | mode); |
| 340 | if (afd != null) { |
| 341 | return afd.getParcelFileDescriptor(); |
| 342 | } |
| 343 | } catch (FileNotFoundException e) { |
| 344 | Log.w(TAG, "Cannot find content uri: " + uri, e); |
| 345 | } catch (SecurityException e) { |
| 346 | Log.w(TAG, "Cannot open content uri: " + uri, e); |
| 347 | } catch (Exception e) { |
| 348 | Log.w(TAG, "Unknown content uri: " + uri, e); |
| 349 | } |
| 350 | return null; |
| 351 | } |
| 352 | |
| 353 | @Override |
| 354 | public void onTranscodingStarted(int sessionId) throws RemoteException { |
| 355 | updateStatus(sessionId, TranscodingSession.STATUS_RUNNING); |
| 356 | } |
| 357 | |
| 358 | @Override |
| 359 | public void onTranscodingPaused(int sessionId) throws RemoteException { |
| 360 | updateStatus(sessionId, TranscodingSession.STATUS_PAUSED); |
| 361 | } |
| 362 | |
| 363 | @Override |
| 364 | public void onTranscodingResumed(int sessionId) throws RemoteException { |
| 365 | updateStatus(sessionId, TranscodingSession.STATUS_RUNNING); |
| 366 | } |
| 367 | |
| 368 | @Override |
| 369 | public void onTranscodingFinished(int sessionId, TranscodingResultParcel result) |
| 370 | throws RemoteException { |
| 371 | handleTranscodingFinished(sessionId, result); |
| 372 | } |
| 373 | |
| 374 | @Override |
| 375 | public void onTranscodingFailed(int sessionId, int errorCode) |
| 376 | throws RemoteException { |
| 377 | handleTranscodingFailed(sessionId, errorCode); |
| 378 | } |
| 379 | |
| 380 | @Override |
| 381 | public void onAwaitNumberOfSessionsChanged(int sessionId, int oldAwaitNumber, |
| 382 | int newAwaitNumber) throws RemoteException { |
| 383 | //TODO(hkuang): Implement this. |
| 384 | } |
| 385 | |
| 386 | @Override |
| 387 | public void onProgressUpdate(int sessionId, int newProgress) |
| 388 | throws RemoteException { |
| 389 | handleTranscodingProgressUpdate(sessionId, newProgress); |
| 390 | } |
| 391 | }; |
| 392 | |
| 393 | private ITranscodingClient registerClient(IMediaTranscodingService service) { |
| 394 | synchronized (mLock) { |
| 395 | try { |
| 396 | // Registers the client with MediaTranscoding service. |
| 397 | mTranscodingClient = service.registerClient( |
| 398 | mTranscodingClientCallback, |
| 399 | mPackageName, |
| 400 | mPackageName); |
| 401 | |
| 402 | if (mTranscodingClient != null) { |
| 403 | mTranscodingClient.asBinder().linkToDeath(() -> onClientDied(), /* flags */ 0); |
| 404 | } |
| 405 | } catch (Exception ex) { |
| 406 | Log.e(TAG, "Failed to register new client due to exception " + ex); |
| 407 | mTranscodingClient = null; |
| 408 | } |
| 409 | } |
| 410 | return mTranscodingClient; |
| 411 | } |
| 412 | |
| 413 | /** |
| 414 | * @hide |
| 415 | */ |
| 416 | public MediaTranscodingManager(@NonNull Context context) { |
| 417 | mContext = context; |
| 418 | mContentResolver = mContext.getContentResolver(); |
| 419 | mPackageName = mContext.getPackageName(); |
| 420 | mUid = Os.getuid(); |
| 421 | mPid = Os.getpid(); |
| 422 | } |
| 423 | |
| 424 | /** |
| 425 | * Abstract base class for all the TranscodingRequest. |
| 426 | * <p> TranscodingRequest encapsulates the desired configuration for the transcoding. |
| 427 | */ |
| 428 | public abstract static class TranscodingRequest { |
| 429 | /** |
| 430 | * |
| 431 | * Default transcoding type. |
| 432 | * @hide |
| 433 | */ |
| 434 | public static final int TRANSCODING_TYPE_UNKNOWN = 0; |
| 435 | |
| 436 | /** |
| 437 | * TRANSCODING_TYPE_VIDEO indicates that client wants to perform transcoding on a video. |
| 438 | * <p>Note that currently only support transcoding video file in mp4 format. |
| 439 | * @hide |
| 440 | */ |
| 441 | public static final int TRANSCODING_TYPE_VIDEO = 1; |
| 442 | |
| 443 | /** |
| 444 | * TRANSCODING_TYPE_IMAGE indicates that client wants to perform transcoding on an image. |
| 445 | * @hide |
| 446 | */ |
| 447 | public static final int TRANSCODING_TYPE_IMAGE = 2; |
| 448 | |
| 449 | /** @hide */ |
| 450 | @IntDef(prefix = {"TRANSCODING_TYPE_"}, value = { |
| 451 | TRANSCODING_TYPE_UNKNOWN, |
| 452 | TRANSCODING_TYPE_VIDEO, |
| 453 | TRANSCODING_TYPE_IMAGE, |
| 454 | }) |
| 455 | @Retention(RetentionPolicy.SOURCE) |
| 456 | public @interface TranscodingType {} |
| 457 | |
| 458 | /** |
| 459 | * Default value. |
| 460 | * |
| 461 | * @hide |
| 462 | */ |
| 463 | public static final int PRIORITY_UNKNOWN = 0; |
| 464 | /** |
| 465 | * PRIORITY_REALTIME indicates that the transcoding request is time-critical and that the |
| 466 | * client wants the transcoding result as soon as possible. |
| 467 | * <p> Set PRIORITY_REALTIME only if the transcoding is time-critical as it will involve |
| 468 | * performance penalty due to resource reallocation to prioritize the sessions with higher |
| 469 | * priority. |
| 470 | * |
| 471 | * @hide |
| 472 | */ |
| 473 | public static final int PRIORITY_REALTIME = 1; |
| 474 | |
| 475 | /** |
| 476 | * PRIORITY_OFFLINE indicates the transcoding is not time-critical and the client does not |
| 477 | * need the transcoding result as soon as possible. |
| 478 | * <p>Sessions with PRIORITY_OFFLINE will be scheduled behind PRIORITY_REALTIME. Always set |
| 479 | * to |
| 480 | * PRIORITY_OFFLINE if client does not need the result as soon as possible and could accept |
| 481 | * delay of the transcoding result. |
| 482 | * |
| 483 | * @hide |
| 484 | * |
| 485 | */ |
| 486 | public static final int PRIORITY_OFFLINE = 2; |
| 487 | |
| 488 | /** @hide */ |
| 489 | @IntDef(prefix = {"PRIORITY_"}, value = { |
| 490 | PRIORITY_UNKNOWN, |
| 491 | PRIORITY_REALTIME, |
| 492 | PRIORITY_OFFLINE, |
| 493 | }) |
| 494 | @Retention(RetentionPolicy.SOURCE) |
| 495 | public @interface TranscodingPriority {} |
| 496 | |
| 497 | /** Uri of the source media file. */ |
| 498 | private @NonNull Uri mSourceUri; |
| 499 | |
| 500 | /** Uri of the destination media file. */ |
| 501 | private @NonNull Uri mDestinationUri; |
| 502 | |
| 503 | /** FileDescriptor of the source media file. */ |
| 504 | private @Nullable ParcelFileDescriptor mSourceFileDescriptor; |
| 505 | |
| 506 | /** FileDescriptor of the destination media file. */ |
| 507 | private @Nullable ParcelFileDescriptor mDestinationFileDescriptor; |
| 508 | |
| 509 | /** |
| 510 | * The UID of the client that the TranscodingRequest is for. Only privileged caller could |
| 511 | * set this Uid as only they could do the transcoding on behalf of the client. |
| 512 | * -1 means not available. |
| 513 | */ |
| 514 | private int mClientUid = -1; |
| 515 | |
| 516 | /** |
| 517 | * The Pid of the client that the TranscodingRequest is for. Only privileged caller could |
| 518 | * set this Uid as only they could do the transcoding on behalf of the client. |
| 519 | * -1 means not available. |
| 520 | */ |
| 521 | private int mClientPid = -1; |
| 522 | |
| 523 | /** Type of the transcoding. */ |
| 524 | private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN; |
| 525 | |
| 526 | /** Priority of the transcoding. */ |
| 527 | private @TranscodingPriority int mPriority = PRIORITY_UNKNOWN; |
| 528 | |
| 529 | /** |
| 530 | * Desired image format for the destination file. |
| 531 | * <p> If this is null, source file's image track will be passed through and copied to the |
| 532 | * destination file. |
| 533 | * @hide |
| 534 | */ |
| 535 | private @Nullable MediaFormat mImageFormat = null; |
| 536 | |
| 537 | @VisibleForTesting |
| 538 | private TranscodingTestConfig mTestConfig = null; |
| 539 | |
| 540 | /** |
| 541 | * Prevent public constructor access. |
| 542 | */ |
| 543 | /* package private */ TranscodingRequest() { |
| 544 | } |
| 545 | |
| 546 | private TranscodingRequest(Builder b) { |
| 547 | mSourceUri = b.mSourceUri; |
| 548 | mSourceFileDescriptor = b.mSourceFileDescriptor; |
| 549 | mDestinationUri = b.mDestinationUri; |
| 550 | mDestinationFileDescriptor = b.mDestinationFileDescriptor; |
| 551 | mClientUid = b.mClientUid; |
| 552 | mClientPid = b.mClientPid; |
| 553 | mPriority = b.mPriority; |
| 554 | mType = b.mType; |
| 555 | mTestConfig = b.mTestConfig; |
| 556 | } |
| 557 | |
| 558 | /** |
| 559 | * Return the type of the transcoding. |
| 560 | * @hide |
| 561 | */ |
| 562 | @TranscodingType |
| 563 | public int getType() { |
| 564 | return mType; |
| 565 | } |
| 566 | |
| 567 | /** Return source uri of the transcoding. */ |
| 568 | @NonNull |
| 569 | public Uri getSourceUri() { |
| 570 | return mSourceUri; |
| 571 | } |
| 572 | |
| 573 | /** |
| 574 | * Return source file descriptor of the transcoding. |
| 575 | * This will be null if client has not provided it. |
| 576 | */ |
| 577 | @Nullable |
| 578 | public ParcelFileDescriptor getSourceFileDescriptor() { |
| 579 | return mSourceFileDescriptor; |
| 580 | } |
| 581 | |
| 582 | /** Return the UID of the client that this request is for. -1 means not available. */ |
| 583 | public int getClientUid() { |
| 584 | return mClientUid; |
| 585 | } |
| 586 | |
| 587 | /** Return the PID of the client that this request is for. -1 means not available. */ |
| 588 | public int getClientPid() { |
| 589 | return mClientPid; |
| 590 | } |
| 591 | |
| 592 | /** Return destination uri of the transcoding. */ |
| 593 | @NonNull |
| 594 | public Uri getDestinationUri() { |
| 595 | return mDestinationUri; |
| 596 | } |
| 597 | |
| 598 | /** |
| 599 | * Return destination file descriptor of the transcoding. |
| 600 | * This will be null if client has not provided it. |
| 601 | */ |
| 602 | @Nullable |
| 603 | public ParcelFileDescriptor getDestinationFileDescriptor() { |
| 604 | return mDestinationFileDescriptor; |
| 605 | } |
| 606 | |
| 607 | /** |
| 608 | * Return priority of the transcoding. |
| 609 | * @hide |
| 610 | */ |
| 611 | @TranscodingPriority |
| 612 | public int getPriority() { |
| 613 | return mPriority; |
| 614 | } |
| 615 | |
| 616 | /** |
| 617 | * Return TestConfig of the transcoding. |
| 618 | * @hide |
| 619 | */ |
| 620 | @Nullable |
| 621 | public TranscodingTestConfig getTestConfig() { |
| 622 | return mTestConfig; |
| 623 | } |
| 624 | |
| 625 | abstract void writeFormatToParcel(TranscodingRequestParcel parcel); |
| 626 | |
| 627 | /* Writes the TranscodingRequest to a parcel. */ |
| 628 | private TranscodingRequestParcel writeToParcel(@NonNull Context context) { |
| 629 | TranscodingRequestParcel parcel = new TranscodingRequestParcel(); |
| 630 | switch (mPriority) { |
| 631 | case PRIORITY_OFFLINE: |
| 632 | parcel.priority = TranscodingSessionPriority.kUnspecified; |
| 633 | break; |
| 634 | case PRIORITY_REALTIME: |
| 635 | case PRIORITY_UNKNOWN: |
| 636 | default: |
| 637 | parcel.priority = TranscodingSessionPriority.kNormal; |
| 638 | break; |
| 639 | } |
| 640 | parcel.transcodingType = mType; |
| 641 | parcel.sourceFilePath = mSourceUri.toString(); |
| 642 | parcel.sourceFd = mSourceFileDescriptor; |
| 643 | parcel.destinationFilePath = mDestinationUri.toString(); |
| 644 | parcel.destinationFd = mDestinationFileDescriptor; |
| 645 | parcel.clientUid = mClientUid; |
| 646 | parcel.clientPid = mClientPid; |
| 647 | if (mClientUid < 0) { |
| 648 | parcel.clientPackageName = context.getPackageName(); |
| 649 | } else { |
| 650 | String packageName = context.getPackageManager().getNameForUid(mClientUid); |
| 651 | // PackageName is optional as some uid does not have package name. Set to |
| 652 | // "Unavailable" string in this case. |
| 653 | if (packageName == null) { |
| 654 | Log.w(TAG, "Failed to find package for uid: " + mClientUid); |
| 655 | packageName = "Unavailable"; |
| 656 | } |
| 657 | parcel.clientPackageName = packageName; |
| 658 | } |
| 659 | writeFormatToParcel(parcel); |
| 660 | if (mTestConfig != null) { |
| 661 | parcel.isForTesting = true; |
| 662 | parcel.testConfig = mTestConfig; |
| 663 | } |
| 664 | return parcel; |
| 665 | } |
| 666 | |
| 667 | /** |
| 668 | * Builder to build a {@link TranscodingRequest} object. |
| 669 | * |
| 670 | * @param <T> The subclass to be built. |
| 671 | */ |
| 672 | abstract static class Builder<T extends Builder<T>> { |
| 673 | private @NonNull Uri mSourceUri; |
| 674 | private @NonNull Uri mDestinationUri; |
| 675 | private @Nullable ParcelFileDescriptor mSourceFileDescriptor = null; |
| 676 | private @Nullable ParcelFileDescriptor mDestinationFileDescriptor = null; |
| 677 | private int mClientUid = -1; |
| 678 | private int mClientPid = -1; |
| 679 | private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN; |
| 680 | private @TranscodingPriority int mPriority = PRIORITY_UNKNOWN; |
| 681 | private TranscodingTestConfig mTestConfig; |
| 682 | |
| 683 | abstract T self(); |
| 684 | |
| 685 | /** |
| 686 | * Creates a builder for building {@link TranscodingRequest}s. |
| 687 | * |
| 688 | * Client must set the source Uri. If client also provides the source fileDescriptor |
| 689 | * through is provided by {@link #setSourceFileDescriptor(ParcelFileDescriptor)}, |
| 690 | * TranscodingSession will use the fd instead of calling back to the client to open the |
| 691 | * sourceUri. |
| 692 | * |
| 693 | * |
| 694 | * @param type The transcoding type. |
| 695 | * @param sourceUri Content uri for the source media file. |
| 696 | * @param destinationUri Content uri for the destination media file. |
| 697 | * |
| 698 | */ |
| 699 | private Builder(@TranscodingType int type, @NonNull Uri sourceUri, |
| 700 | @NonNull Uri destinationUri) { |
| 701 | mType = type; |
| 702 | |
| 703 | if (sourceUri == null || Uri.EMPTY.equals(sourceUri)) { |
| 704 | throw new IllegalArgumentException( |
| 705 | "You must specify a non-empty source Uri."); |
| 706 | } |
| 707 | mSourceUri = sourceUri; |
| 708 | |
| 709 | if (destinationUri == null || Uri.EMPTY.equals(destinationUri)) { |
| 710 | throw new IllegalArgumentException( |
| 711 | "You must specify a non-empty destination Uri."); |
| 712 | } |
| 713 | mDestinationUri = destinationUri; |
| 714 | } |
| 715 | |
| 716 | /** |
| 717 | * Specifies the fileDescriptor opened from the source media file. |
| 718 | * |
| 719 | * This call is optional. If the source fileDescriptor is provided, TranscodingSession |
| 720 | * will use it directly instead of opening the uri from {@link #Builder(int, Uri, Uri)}. |
| 721 | * It is client's responsibility to make sure the fileDescriptor is opened from the |
| 722 | * source uri. |
| 723 | * @param fileDescriptor a {@link ParcelFileDescriptor} opened from source media file. |
| 724 | * @return The same builder instance. |
| 725 | * @throws IllegalArgumentException if fileDescriptor is invalid. |
| 726 | */ |
| 727 | @NonNull |
| 728 | public T setSourceFileDescriptor(@NonNull ParcelFileDescriptor fileDescriptor) { |
| 729 | if (fileDescriptor == null || fileDescriptor.getFd() < 0) { |
| 730 | throw new IllegalArgumentException( |
| 731 | "Invalid source descriptor."); |
| 732 | } |
| 733 | mSourceFileDescriptor = fileDescriptor; |
| 734 | return self(); |
| 735 | } |
| 736 | |
| 737 | /** |
| 738 | * Specifies the fileDescriptor opened from the destination media file. |
| 739 | * |
| 740 | * This call is optional. If the destination fileDescriptor is provided, |
| 741 | * TranscodingSession will use it directly instead of opening the source uri from |
| 742 | * {@link #Builder(int, Uri, Uri)} upon transcoding starts. It is client's |
| 743 | * responsibility to make sure the fileDescriptor is opened from the destination uri. |
| 744 | * @param fileDescriptor a {@link ParcelFileDescriptor} opened from destination media |
| 745 | * file. |
| 746 | * @return The same builder instance. |
| 747 | * @throws IllegalArgumentException if fileDescriptor is invalid. |
| 748 | */ |
| 749 | @NonNull |
| 750 | public T setDestinationFileDescriptor( |
| 751 | @NonNull ParcelFileDescriptor fileDescriptor) { |
| 752 | if (fileDescriptor == null || fileDescriptor.getFd() < 0) { |
| 753 | throw new IllegalArgumentException( |
| 754 | "Invalid destination descriptor."); |
| 755 | } |
| 756 | mDestinationFileDescriptor = fileDescriptor; |
| 757 | return self(); |
| 758 | } |
| 759 | |
| 760 | /** |
| 761 | * Specify the UID of the client that this request is for. |
| 762 | * <p> |
| 763 | * Only privilege caller with android.permission.WRITE_MEDIA_STORAGE could forward the |
| 764 | * pid. Note that the permission check happens on the service side upon starting the |
| 765 | * transcoding. If the client does not have the permission, the transcoding will fail. |
| 766 | * |
| 767 | * @param uid client Uid. |
| 768 | * @return The same builder instance. |
| 769 | * @throws IllegalArgumentException if uid is invalid. |
| 770 | */ |
| 771 | @NonNull |
| 772 | public T setClientUid(int uid) { |
| 773 | if (uid < 0) { |
| 774 | throw new IllegalArgumentException("Invalid Uid"); |
| 775 | } |
| 776 | mClientUid = uid; |
| 777 | return self(); |
| 778 | } |
| 779 | |
| 780 | /** |
| 781 | * Specify the pid of the client that this request is for. |
| 782 | * <p> |
| 783 | * Only privilege caller with android.permission.WRITE_MEDIA_STORAGE could forward the |
| 784 | * pid. Note that the permission check happens on the service side upon starting the |
| 785 | * transcoding. If the client does not have the permission, the transcoding will fail. |
| 786 | * |
| 787 | * @param pid client Pid. |
| 788 | * @return The same builder instance. |
| 789 | * @throws IllegalArgumentException if pid is invalid. |
| 790 | */ |
| 791 | @NonNull |
| 792 | public T setClientPid(int pid) { |
| 793 | if (pid < 0) { |
| 794 | throw new IllegalArgumentException("Invalid pid"); |
| 795 | } |
| 796 | mClientPid = pid; |
| 797 | return self(); |
| 798 | } |
| 799 | |
| 800 | /** |
| 801 | * Specifies the priority of the transcoding. |
| 802 | * |
| 803 | * @param priority Must be one of the {@code PRIORITY_*} |
| 804 | * @return The same builder instance. |
| 805 | * @throws IllegalArgumentException if flags is invalid. |
| 806 | * @hide |
| 807 | */ |
| 808 | @NonNull |
| 809 | public T setPriority(@TranscodingPriority int priority) { |
| 810 | if (priority != PRIORITY_OFFLINE && priority != PRIORITY_REALTIME) { |
| 811 | throw new IllegalArgumentException("Invalid priority: " + priority); |
| 812 | } |
| 813 | mPriority = priority; |
| 814 | return self(); |
| 815 | } |
| 816 | |
| 817 | /** |
| 818 | * Sets the delay in processing this request. |
| 819 | * @param config test config. |
| 820 | * @return The same builder instance. |
| 821 | * @hide |
| 822 | */ |
| 823 | @VisibleForTesting |
| 824 | @NonNull |
| 825 | public T setTestConfig(@NonNull TranscodingTestConfig config) { |
| 826 | mTestConfig = config; |
| 827 | return self(); |
| 828 | } |
| 829 | } |
| 830 | |
| 831 | /** |
| 832 | * Abstract base class for all the format resolvers. |
| 833 | */ |
| 834 | abstract static class MediaFormatResolver { |
| 835 | private @NonNull ApplicationMediaCapabilities mClientCaps; |
| 836 | |
| 837 | /** |
| 838 | * Prevents public constructor access. |
| 839 | */ |
| 840 | /* package private */ MediaFormatResolver() { |
| 841 | } |
| 842 | |
| 843 | /** |
| 844 | * Constructs MediaFormatResolver object. |
| 845 | * |
| 846 | * @param clientCaps An ApplicationMediaCapabilities object containing the client's |
| 847 | * capabilities. |
| 848 | */ |
| 849 | MediaFormatResolver(@NonNull ApplicationMediaCapabilities clientCaps) { |
| 850 | if (clientCaps == null) { |
| 851 | throw new IllegalArgumentException("Client capabilities must not be null"); |
| 852 | } |
| 853 | mClientCaps = clientCaps; |
| 854 | } |
| 855 | |
| 856 | /** |
| 857 | * Returns the client capabilities. |
| 858 | */ |
| 859 | @NonNull |
| 860 | /* package */ ApplicationMediaCapabilities getClientCapabilities() { |
| 861 | return mClientCaps; |
| 862 | } |
| 863 | |
| 864 | abstract boolean shouldTranscode(); |
| 865 | } |
| 866 | |
| 867 | /** |
| 868 | * VideoFormatResolver for deciding if video transcoding is needed, and if so, the track |
| 869 | * formats to use. |
| 870 | */ |
| 871 | public static class VideoFormatResolver extends MediaFormatResolver { |
| 872 | private static final int BIT_RATE = 20000000; // 20Mbps |
| 873 | |
| 874 | private MediaFormat mSrcVideoFormatHint; |
| 875 | private MediaFormat mSrcAudioFormatHint; |
| 876 | |
| 877 | /** |
| 878 | * Constructs a new VideoFormatResolver object. |
| 879 | * |
| 880 | * @param clientCaps An ApplicationMediaCapabilities object containing the client's |
| 881 | * capabilities. |
| 882 | * @param srcVideoFormatHint A MediaFormat object containing information about the |
| 883 | * source's video track format that could affect the |
| 884 | * transcoding decision. Such information could include video |
| 885 | * codec types, color spaces, whether special format info (eg. |
| 886 | * slow-motion markers) are present, etc.. If a particular |
| 887 | * information is not present, it will not be used to make the |
| 888 | * decision. |
| 889 | */ |
| 890 | public VideoFormatResolver(@NonNull ApplicationMediaCapabilities clientCaps, |
| 891 | @NonNull MediaFormat srcVideoFormatHint) { |
| 892 | super(clientCaps); |
| 893 | mSrcVideoFormatHint = srcVideoFormatHint; |
| 894 | } |
| 895 | |
| 896 | /** |
| 897 | * Constructs a new VideoFormatResolver object. |
| 898 | * |
| 899 | * @param clientCaps An ApplicationMediaCapabilities object containing the client's |
| 900 | * capabilities. |
| 901 | * @param srcVideoFormatHint A MediaFormat object containing information about the |
| 902 | * source's video track format that could affect the |
| 903 | * transcoding decision. Such information could include video |
| 904 | * codec types, color spaces, whether special format info (eg. |
| 905 | * slow-motion markers) are present, etc.. If a particular |
| 906 | * information is not present, it will not be used to make the |
| 907 | * decision. |
| 908 | * @param srcAudioFormatHint A MediaFormat object containing information about the |
| 909 | * source's audio track format that could affect the |
| 910 | * transcoding decision. |
| 911 | * @hide |
| 912 | */ |
| 913 | VideoFormatResolver(@NonNull ApplicationMediaCapabilities clientCaps, |
| 914 | @NonNull MediaFormat srcVideoFormatHint, |
| 915 | @NonNull MediaFormat srcAudioFormatHint) { |
| 916 | super(clientCaps); |
| 917 | mSrcVideoFormatHint = srcVideoFormatHint; |
| 918 | mSrcAudioFormatHint = srcAudioFormatHint; |
| 919 | } |
| 920 | |
| 921 | /** |
| 922 | * Returns whether the source content should be transcoded. |
| 923 | * |
| 924 | * @return true if the source should be transcoded. |
| 925 | */ |
| 926 | public boolean shouldTranscode() { |
| 927 | boolean supportHevc = getClientCapabilities().isVideoMimeTypeSupported( |
| 928 | MediaFormat.MIMETYPE_VIDEO_HEVC); |
| 929 | if (!supportHevc && MediaFormat.MIMETYPE_VIDEO_HEVC.equals( |
| 930 | mSrcVideoFormatHint.getString(MediaFormat.KEY_MIME))) { |
| 931 | return true; |
| 932 | } |
| 933 | // TODO: add more checks as needed below. |
| 934 | return false; |
| 935 | } |
| 936 | |
| 937 | /** |
| 938 | * Retrieves the video track format to be used on |
| 939 | * {@link VideoTranscodingRequest.Builder#setVideoTrackFormat(MediaFormat)} for this |
| 940 | * configuration. |
| 941 | * |
| 942 | * @return the video track format to be used if transcoding should be performed, |
| 943 | * and null otherwise. |
| 944 | * @throws IllegalArgumentException if the hinted source video format contains invalid |
| 945 | * parameters. |
| 946 | */ |
| 947 | @Nullable |
| 948 | public MediaFormat resolveVideoFormat() { |
| 949 | if (!shouldTranscode()) { |
| 950 | return null; |
| 951 | } |
| 952 | |
| 953 | MediaFormat videoTrackFormat = new MediaFormat(mSrcVideoFormatHint); |
| 954 | videoTrackFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); |
| 955 | |
| 956 | int width = mSrcVideoFormatHint.getInteger(MediaFormat.KEY_WIDTH, -1); |
| 957 | int height = mSrcVideoFormatHint.getInteger(MediaFormat.KEY_HEIGHT, -1); |
| 958 | if (width <= 0 || height <= 0) { |
| 959 | throw new IllegalArgumentException( |
| 960 | "Source Width and height must be larger than 0"); |
| 961 | } |
| 962 | |
| 963 | float frameRate = |
| 964 | mSrcVideoFormatHint.getNumber(MediaFormat.KEY_FRAME_RATE, 30.0) |
| 965 | .floatValue(); |
| 966 | if (frameRate <= 0) { |
| 967 | throw new IllegalArgumentException( |
| 968 | "frameRate must be larger than 0"); |
| 969 | } |
| 970 | |
| 971 | int bitrate = getAVCBitrate(width, height, frameRate); |
| 972 | videoTrackFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); |
| 973 | return videoTrackFormat; |
| 974 | } |
| 975 | |
| 976 | /** |
| 977 | * Generate a default bitrate with the fixed bpp(bits-per-pixel) 0.25. |
| 978 | * This maps to: |
| 979 | * 1080P@30fps -> 16Mbps |
| 980 | * 1080P@60fps-> 32Mbps |
| 981 | * 4K@30fps -> 62Mbps |
| 982 | */ |
| 983 | private static int getDefaultBitrate(int width, int height, float frameRate) { |
| 984 | return (int) (width * height * frameRate * BPP); |
| 985 | } |
| 986 | |
| 987 | /** |
| 988 | * Query the bitrate from CamcorderProfile. If there are two profiles that match the |
| 989 | * width/height/framerate, we will use the higher one to get better quality. |
| 990 | * Return default bitrate if could not find any match profile. |
| 991 | */ |
| 992 | private static int getAVCBitrate(int width, int height, float frameRate) { |
| 993 | int bitrate = -1; |
| 994 | int[] cameraIds = {0, 1}; |
| 995 | |
| 996 | // Profiles ordered in decreasing order of preference. |
| 997 | int[] preferQualities = { |
| 998 | CamcorderProfile.QUALITY_2160P, |
| 999 | CamcorderProfile.QUALITY_1080P, |
| 1000 | CamcorderProfile.QUALITY_720P, |
| 1001 | CamcorderProfile.QUALITY_480P, |
| 1002 | CamcorderProfile.QUALITY_LOW, |
| 1003 | }; |
| 1004 | |
| 1005 | for (int cameraId : cameraIds) { |
| 1006 | for (int quality : preferQualities) { |
| 1007 | // Check if camera id has profile for the quality level. |
| 1008 | if (!CamcorderProfile.hasProfile(cameraId, quality)) { |
| 1009 | continue; |
| 1010 | } |
| 1011 | CamcorderProfile profile = CamcorderProfile.get(cameraId, quality); |
| 1012 | // Check the width/height/framerate/codec, also consider portrait case. |
| 1013 | if (((width == profile.videoFrameWidth |
| 1014 | && height == profile.videoFrameHeight) |
| 1015 | || (height == profile.videoFrameWidth |
| 1016 | && width == profile.videoFrameHeight)) |
| 1017 | && (int) frameRate == profile.videoFrameRate |
| 1018 | && profile.videoCodec == MediaRecorder.VideoEncoder.H264) { |
| 1019 | if (bitrate < profile.videoBitRate) { |
| 1020 | bitrate = profile.videoBitRate; |
| 1021 | } |
| 1022 | break; |
| 1023 | } |
| 1024 | } |
| 1025 | } |
| 1026 | |
| 1027 | if (bitrate == -1) { |
| 1028 | Log.w(TAG, "Failed to find CamcorderProfile for w: " + width + "h: " + height |
| 1029 | + " fps: " |
| 1030 | + frameRate); |
| 1031 | bitrate = getDefaultBitrate(width, height, frameRate); |
| 1032 | } |
| 1033 | Log.d(TAG, "Using bitrate " + bitrate + " for " + width + " " + height + " " |
| 1034 | + frameRate); |
| 1035 | return bitrate; |
| 1036 | } |
| 1037 | |
| 1038 | /** |
| 1039 | * Retrieves the audio track format to be used for transcoding. |
| 1040 | * |
| 1041 | * @return the audio track format to be used if transcoding should be performed, and |
| 1042 | * null otherwise. |
| 1043 | * @hide |
| 1044 | */ |
| 1045 | @Nullable |
| 1046 | public MediaFormat resolveAudioFormat() { |
| 1047 | if (!shouldTranscode()) { |
| 1048 | return null; |
| 1049 | } |
| 1050 | // Audio transcoding is not supported yet, always return null. |
| 1051 | return null; |
| 1052 | } |
| 1053 | } |
| 1054 | } |
| 1055 | |
| 1056 | /** |
| 1057 | * VideoTranscodingRequest encapsulates the configuration for transcoding a video. |
| 1058 | */ |
| 1059 | public static final class VideoTranscodingRequest extends TranscodingRequest { |
| 1060 | /** |
| 1061 | * Desired output video format of the destination file. |
| 1062 | * <p> If this is null, source file's video track will be passed through and copied to the |
| 1063 | * destination file. |
| 1064 | */ |
| 1065 | private @Nullable MediaFormat mVideoTrackFormat = null; |
| 1066 | |
| 1067 | /** |
| 1068 | * Desired output audio format of the destination file. |
| 1069 | * <p> If this is null, source file's audio track will be passed through and copied to the |
| 1070 | * destination file. |
| 1071 | */ |
| 1072 | private @Nullable MediaFormat mAudioTrackFormat = null; |
| 1073 | |
| 1074 | private VideoTranscodingRequest(VideoTranscodingRequest.Builder builder) { |
| 1075 | super(builder); |
| 1076 | mVideoTrackFormat = builder.mVideoTrackFormat; |
| 1077 | mAudioTrackFormat = builder.mAudioTrackFormat; |
| 1078 | } |
| 1079 | |
| 1080 | /** |
| 1081 | * Return the video track format of the transcoding. |
| 1082 | * This will be null if client has not specified the video track format. |
| 1083 | */ |
| 1084 | @NonNull |
| 1085 | public MediaFormat getVideoTrackFormat() { |
| 1086 | return mVideoTrackFormat; |
| 1087 | } |
| 1088 | |
| 1089 | @Override |
| 1090 | void writeFormatToParcel(TranscodingRequestParcel parcel) { |
| 1091 | parcel.requestedVideoTrackFormat = convertToVideoTrackFormat(mVideoTrackFormat); |
| 1092 | } |
| 1093 | |
| 1094 | /* Converts the MediaFormat to TranscodingVideoTrackFormat. */ |
| 1095 | private static TranscodingVideoTrackFormat convertToVideoTrackFormat(MediaFormat format) { |
| 1096 | if (format == null) { |
| 1097 | throw new IllegalArgumentException("Invalid MediaFormat"); |
| 1098 | } |
| 1099 | |
| 1100 | TranscodingVideoTrackFormat trackFormat = new TranscodingVideoTrackFormat(); |
| 1101 | |
| 1102 | if (format.containsKey(MediaFormat.KEY_MIME)) { |
| 1103 | String mime = format.getString(MediaFormat.KEY_MIME); |
| 1104 | if (MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) { |
| 1105 | trackFormat.codecType = TranscodingVideoCodecType.kAvc; |
| 1106 | } else if (MediaFormat.MIMETYPE_VIDEO_HEVC.equals(mime)) { |
| 1107 | trackFormat.codecType = TranscodingVideoCodecType.kHevc; |
| 1108 | } else { |
| 1109 | throw new UnsupportedOperationException("Only support transcode to avc/hevc"); |
| 1110 | } |
| 1111 | } |
| 1112 | |
| 1113 | if (format.containsKey(MediaFormat.KEY_BIT_RATE)) { |
| 1114 | int bitrateBps = format.getInteger(MediaFormat.KEY_BIT_RATE); |
| 1115 | if (bitrateBps <= 0) { |
| 1116 | throw new IllegalArgumentException("Bitrate must be larger than 0"); |
| 1117 | } |
| 1118 | trackFormat.bitrateBps = bitrateBps; |
| 1119 | } |
| 1120 | |
| 1121 | if (format.containsKey(MediaFormat.KEY_WIDTH) && format.containsKey( |
| 1122 | MediaFormat.KEY_HEIGHT)) { |
| 1123 | int width = format.getInteger(MediaFormat.KEY_WIDTH); |
| 1124 | int height = format.getInteger(MediaFormat.KEY_HEIGHT); |
| 1125 | if (width <= 0 || height <= 0) { |
| 1126 | throw new IllegalArgumentException("Width and height must be larger than 0"); |
| 1127 | } |
| 1128 | // TODO: Validate the aspect ratio after adding scaling. |
| 1129 | trackFormat.width = width; |
| 1130 | trackFormat.height = height; |
| 1131 | } |
| 1132 | |
| 1133 | if (format.containsKey(MediaFormat.KEY_PROFILE)) { |
| 1134 | int profile = format.getInteger(MediaFormat.KEY_PROFILE); |
| 1135 | if (profile <= 0) { |
| 1136 | throw new IllegalArgumentException("Invalid codec profile"); |
| 1137 | } |
| 1138 | // TODO: Validate the profile according to codec type. |
| 1139 | trackFormat.profile = profile; |
| 1140 | } |
| 1141 | |
| 1142 | if (format.containsKey(MediaFormat.KEY_LEVEL)) { |
| 1143 | int level = format.getInteger(MediaFormat.KEY_LEVEL); |
| 1144 | if (level <= 0) { |
| 1145 | throw new IllegalArgumentException("Invalid codec level"); |
| 1146 | } |
| 1147 | // TODO: Validate the level according to codec type. |
| 1148 | trackFormat.level = level; |
| 1149 | } |
| 1150 | |
| 1151 | return trackFormat; |
| 1152 | } |
| 1153 | |
| 1154 | /** |
| 1155 | * Builder class for {@link VideoTranscodingRequest}. |
| 1156 | */ |
| 1157 | public static final class Builder extends |
| 1158 | TranscodingRequest.Builder<VideoTranscodingRequest.Builder> { |
| 1159 | /** |
| 1160 | * Desired output video format of the destination file. |
| 1161 | * <p> If this is null, source file's video track will be passed through and |
| 1162 | * copied to the destination file. |
| 1163 | */ |
| 1164 | private @Nullable MediaFormat mVideoTrackFormat = null; |
| 1165 | |
| 1166 | /** |
| 1167 | * Desired output audio format of the destination file. |
| 1168 | * <p> If this is null, source file's audio track will be passed through and copied |
| 1169 | * to the destination file. |
| 1170 | */ |
| 1171 | private @Nullable MediaFormat mAudioTrackFormat = null; |
| 1172 | |
| 1173 | /** |
| 1174 | * Creates a builder for building {@link VideoTranscodingRequest}s. |
| 1175 | * |
| 1176 | * <p> Client could only specify the settings that matters to them, e.g. codec format or |
| 1177 | * bitrate. And by default, transcoding will preserve the original video's settings |
| 1178 | * (bitrate, framerate, resolution) if not provided. |
| 1179 | * <p>Note that some settings may silently fail to apply if the device does not support |
| 1180 | * them. |
| 1181 | * @param sourceUri Content uri for the source media file. |
| 1182 | * @param destinationUri Content uri for the destination media file. |
| 1183 | * @param videoFormat MediaFormat containing the settings that client wants override in |
| 1184 | * the original video's video track. |
| 1185 | * @throws IllegalArgumentException if videoFormat is invalid. |
| 1186 | */ |
| 1187 | public Builder(@NonNull Uri sourceUri, @NonNull Uri destinationUri, |
| 1188 | @NonNull MediaFormat videoFormat) { |
| 1189 | super(TRANSCODING_TYPE_VIDEO, sourceUri, destinationUri); |
| 1190 | setVideoTrackFormat(videoFormat); |
| 1191 | } |
| 1192 | |
| 1193 | @Override |
| 1194 | @NonNull |
| 1195 | public Builder setClientUid(int uid) { |
| 1196 | super.setClientUid(uid); |
| 1197 | return self(); |
| 1198 | } |
| 1199 | |
| 1200 | @Override |
| 1201 | @NonNull |
| 1202 | public Builder setClientPid(int pid) { |
| 1203 | super.setClientPid(pid); |
| 1204 | return self(); |
| 1205 | } |
| 1206 | |
| 1207 | @Override |
| 1208 | @NonNull |
| 1209 | public Builder setSourceFileDescriptor(@NonNull ParcelFileDescriptor fd) { |
| 1210 | super.setSourceFileDescriptor(fd); |
| 1211 | return self(); |
| 1212 | } |
| 1213 | |
| 1214 | @Override |
| 1215 | @NonNull |
| 1216 | public Builder setDestinationFileDescriptor(@NonNull ParcelFileDescriptor fd) { |
| 1217 | super.setDestinationFileDescriptor(fd); |
| 1218 | return self(); |
| 1219 | } |
| 1220 | |
| 1221 | private void setVideoTrackFormat(@NonNull MediaFormat videoFormat) { |
| 1222 | if (videoFormat == null) { |
| 1223 | throw new IllegalArgumentException("videoFormat must not be null"); |
| 1224 | } |
| 1225 | |
| 1226 | // Check if the MediaFormat is for video by looking at the MIME type. |
| 1227 | String mime = videoFormat.containsKey(MediaFormat.KEY_MIME) |
| 1228 | ? videoFormat.getString(MediaFormat.KEY_MIME) : null; |
| 1229 | if (mime == null || !mime.startsWith("video/")) { |
| 1230 | throw new IllegalArgumentException("Invalid video format: wrong mime type"); |
| 1231 | } |
| 1232 | |
| 1233 | mVideoTrackFormat = videoFormat; |
| 1234 | } |
| 1235 | |
| 1236 | /** |
| 1237 | * @return a new {@link TranscodingRequest} instance successfully initialized |
| 1238 | * with all the parameters set on this <code>Builder</code>. |
| 1239 | * @throws UnsupportedOperationException if the parameters set on the |
| 1240 | * <code>Builder</code> were incompatible, or |
| 1241 | * if they are not supported by the |
| 1242 | * device. |
| 1243 | */ |
| 1244 | @NonNull |
| 1245 | public VideoTranscodingRequest build() { |
| 1246 | return new VideoTranscodingRequest(this); |
| 1247 | } |
| 1248 | |
| 1249 | @Override |
| 1250 | VideoTranscodingRequest.Builder self() { |
| 1251 | return this; |
| 1252 | } |
| 1253 | } |
| 1254 | } |
| 1255 | |
| 1256 | /** |
| 1257 | * Handle to an enqueued transcoding operation. An instance of this class represents a single |
| 1258 | * enqueued transcoding operation. The caller can use that instance to query the status or |
| 1259 | * progress, and to get the result once the operation has completed. |
| 1260 | */ |
| 1261 | public static final class TranscodingSession { |
| 1262 | /** The session is enqueued but not yet running. */ |
| 1263 | public static final int STATUS_PENDING = 1; |
| 1264 | /** The session is currently running. */ |
| 1265 | public static final int STATUS_RUNNING = 2; |
| 1266 | /** The session is finished. */ |
| 1267 | public static final int STATUS_FINISHED = 3; |
| 1268 | /** The session is paused. */ |
| 1269 | public static final int STATUS_PAUSED = 4; |
| 1270 | |
| 1271 | /** @hide */ |
| 1272 | @IntDef(prefix = { "STATUS_" }, value = { |
| 1273 | STATUS_PENDING, |
| 1274 | STATUS_RUNNING, |
| 1275 | STATUS_FINISHED, |
| 1276 | STATUS_PAUSED, |
| 1277 | }) |
| 1278 | @Retention(RetentionPolicy.SOURCE) |
| 1279 | public @interface Status {} |
| 1280 | |
| 1281 | /** The session does not have a result yet. */ |
| 1282 | public static final int RESULT_NONE = 1; |
| 1283 | /** The session completed successfully. */ |
| 1284 | public static final int RESULT_SUCCESS = 2; |
| 1285 | /** The session encountered an error while running. */ |
| 1286 | public static final int RESULT_ERROR = 3; |
| 1287 | /** The session was canceled by the caller. */ |
| 1288 | public static final int RESULT_CANCELED = 4; |
| 1289 | |
| 1290 | /** @hide */ |
| 1291 | @IntDef(prefix = { "RESULT_" }, value = { |
| 1292 | RESULT_NONE, |
| 1293 | RESULT_SUCCESS, |
| 1294 | RESULT_ERROR, |
| 1295 | RESULT_CANCELED, |
| 1296 | }) |
| 1297 | @Retention(RetentionPolicy.SOURCE) |
| 1298 | public @interface Result {} |
| 1299 | |
| 1300 | |
| 1301 | // The error code exposed here should be in sync with: |
| 1302 | // frameworks/av/media/libmediatranscoding/aidl/android/media/TranscodingErrorCode.aidl |
| 1303 | /** @hide */ |
| 1304 | @IntDef(prefix = { "TRANSCODING_SESSION_ERROR_" }, value = { |
| 1305 | ERROR_NONE, |
| 1306 | ERROR_DROPPED_BY_SERVICE, |
| 1307 | ERROR_SERVICE_DIED}) |
| 1308 | @Retention(RetentionPolicy.SOURCE) |
| 1309 | public @interface TranscodingSessionErrorCode{} |
| 1310 | /** |
| 1311 | * Constant indicating that no error occurred. |
| 1312 | */ |
| 1313 | public static final int ERROR_NONE = 0; |
| 1314 | |
| 1315 | /** |
| 1316 | * Constant indicating that the session is dropped by Transcoding service due to hitting |
| 1317 | * the limit, e.g. too many back to back transcoding happen in a short time frame. |
| 1318 | */ |
| 1319 | public static final int ERROR_DROPPED_BY_SERVICE = 1; |
| 1320 | |
| 1321 | /** |
| 1322 | * Constant indicating the backing transcoding service is died. Client should enqueue the |
| 1323 | * the request again. |
| 1324 | */ |
| 1325 | public static final int ERROR_SERVICE_DIED = 2; |
| 1326 | |
| 1327 | /** Listener that gets notified when the progress changes. */ |
| 1328 | @FunctionalInterface |
| 1329 | public interface OnProgressUpdateListener { |
| 1330 | /** |
| 1331 | * Called when the progress changes. The progress is in percentage between 0 and 1, |
| 1332 | * where 0 means the session has not yet started and 100 means that it has finished. |
| 1333 | * |
| 1334 | * @param session The session associated with the progress. |
| 1335 | * @param progress The new progress ranging from 0 ~ 100 inclusive. |
| 1336 | */ |
| 1337 | void onProgressUpdate(@NonNull TranscodingSession session, |
| 1338 | @IntRange(from = 0, to = 100) int progress); |
| 1339 | } |
| 1340 | |
| 1341 | private final MediaTranscodingManager mManager; |
| 1342 | private Executor mListenerExecutor; |
| 1343 | private OnTranscodingFinishedListener mListener; |
| 1344 | private int mSessionId = -1; |
| 1345 | // Lock for internal state. |
| 1346 | private final Object mLock = new Object(); |
| 1347 | @GuardedBy("mLock") |
| 1348 | private Executor mProgressUpdateExecutor = null; |
| 1349 | @GuardedBy("mLock") |
| 1350 | private OnProgressUpdateListener mProgressUpdateListener = null; |
| 1351 | @GuardedBy("mLock") |
| 1352 | private int mProgress = 0; |
| 1353 | @GuardedBy("mLock") |
| 1354 | private int mProgressUpdateInterval = 0; |
| 1355 | @GuardedBy("mLock") |
| 1356 | private @Status int mStatus = STATUS_PENDING; |
| 1357 | @GuardedBy("mLock") |
| 1358 | private @Result int mResult = RESULT_NONE; |
| 1359 | @GuardedBy("mLock") |
| 1360 | private @TranscodingSessionErrorCode int mErrorCode = ERROR_NONE; |
| 1361 | @GuardedBy("mLock") |
| 1362 | private boolean mHasRetried = false; |
| 1363 | // The original request that associated with this session. |
| 1364 | private final TranscodingRequest mRequest; |
| 1365 | |
| 1366 | private TranscodingSession( |
| 1367 | @NonNull MediaTranscodingManager manager, |
| 1368 | @NonNull TranscodingRequest request, |
| 1369 | @NonNull TranscodingSessionParcel parcel, |
| 1370 | @NonNull @CallbackExecutor Executor executor, |
| 1371 | @NonNull OnTranscodingFinishedListener listener) { |
| 1372 | Objects.requireNonNull(manager, "manager must not be null"); |
| 1373 | Objects.requireNonNull(parcel, "parcel must not be null"); |
| 1374 | Objects.requireNonNull(executor, "listenerExecutor must not be null"); |
| 1375 | Objects.requireNonNull(listener, "listener must not be null"); |
| 1376 | mManager = manager; |
| 1377 | mSessionId = parcel.sessionId; |
| 1378 | mListenerExecutor = executor; |
| 1379 | mListener = listener; |
| 1380 | mRequest = request; |
| 1381 | } |
| 1382 | |
| 1383 | /** |
| 1384 | * Set a progress listener. |
| 1385 | * @param executor The executor on which listener will be invoked. |
| 1386 | * @param listener The progress listener. |
| 1387 | */ |
| 1388 | public void setOnProgressUpdateListener( |
| 1389 | @NonNull @CallbackExecutor Executor executor, |
| 1390 | @NonNull OnProgressUpdateListener listener) { |
| 1391 | synchronized (mLock) { |
| 1392 | Objects.requireNonNull(executor, "listenerExecutor must not be null"); |
| 1393 | Objects.requireNonNull(listener, "listener must not be null"); |
| 1394 | mProgressUpdateExecutor = executor; |
| 1395 | mProgressUpdateListener = listener; |
| 1396 | } |
| 1397 | } |
| 1398 | |
| 1399 | /** Removes the progress listener if any. */ |
| 1400 | public void clearOnProgressUpdateListener() { |
| 1401 | synchronized (mLock) { |
| 1402 | mProgressUpdateExecutor = null; |
| 1403 | mProgressUpdateListener = null; |
| 1404 | } |
| 1405 | } |
| 1406 | |
| 1407 | private void updateStatusAndResult(@Status int sessionStatus, |
| 1408 | @Result int sessionResult, @TranscodingSessionErrorCode int errorCode) { |
| 1409 | synchronized (mLock) { |
| 1410 | mStatus = sessionStatus; |
| 1411 | mResult = sessionResult; |
| 1412 | mErrorCode = errorCode; |
| 1413 | } |
| 1414 | } |
| 1415 | |
| 1416 | /** |
| 1417 | * Retrieve the error code associated with the RESULT_ERROR. |
| 1418 | */ |
| 1419 | public @TranscodingSessionErrorCode int getErrorCode() { |
| 1420 | synchronized (mLock) { |
| 1421 | return mErrorCode; |
| 1422 | } |
| 1423 | } |
| 1424 | |
| 1425 | /** |
| 1426 | * Resubmit the transcoding session to the service. |
| 1427 | * Note that only the session that fails or gets cancelled could be retried and each session |
| 1428 | * could be retried only once. After that, Client need to enqueue a new request if they want |
| 1429 | * to try again. |
| 1430 | * |
| 1431 | * @return true if successfully resubmit the job to service. False otherwise. |
| 1432 | * @throws UnsupportedOperationException if the retry could not be fulfilled. |
| 1433 | * @hide |
| 1434 | */ |
| 1435 | public boolean retry() { |
| 1436 | return retryInternal(true /*setHasRetried*/); |
| 1437 | } |
| 1438 | |
| 1439 | // TODO(hkuang): Add more test for it. |
| 1440 | private boolean retryInternal(boolean setHasRetried) { |
| 1441 | synchronized (mLock) { |
| 1442 | if (mStatus == STATUS_PENDING || mStatus == STATUS_RUNNING) { |
| 1443 | throw new UnsupportedOperationException( |
| 1444 | "Failed to retry as session is in processing"); |
| 1445 | } |
| 1446 | |
| 1447 | if (mHasRetried) { |
| 1448 | throw new UnsupportedOperationException("Session has been retried already"); |
| 1449 | } |
| 1450 | |
| 1451 | // Get the client interface. |
| 1452 | ITranscodingClient client = mManager.getTranscodingClient(); |
| 1453 | if (client == null) { |
| 1454 | Log.e(TAG, "Service rebooting. Try again later"); |
| 1455 | return false; |
| 1456 | } |
| 1457 | |
| 1458 | synchronized (mManager.mPendingTranscodingSessions) { |
| 1459 | try { |
| 1460 | // Submits the request to MediaTranscoding service. |
| 1461 | TranscodingSessionParcel sessionParcel = new TranscodingSessionParcel(); |
| 1462 | if (!client.submitRequest(mRequest.writeToParcel(mManager.mContext), |
| 1463 | sessionParcel)) { |
| 1464 | mHasRetried = true; |
| 1465 | throw new UnsupportedOperationException("Failed to enqueue request"); |
| 1466 | } |
| 1467 | |
| 1468 | // Replace the old session id wit the new one. |
| 1469 | mSessionId = sessionParcel.sessionId; |
| 1470 | // Adds the new session back into pending sessions. |
| 1471 | mManager.mPendingTranscodingSessions.put(mSessionId, this); |
| 1472 | } catch (RemoteException re) { |
| 1473 | return false; |
| 1474 | } |
| 1475 | mStatus = STATUS_PENDING; |
| 1476 | mHasRetried = setHasRetried ? true : false; |
| 1477 | } |
| 1478 | } |
| 1479 | return true; |
| 1480 | } |
| 1481 | |
| 1482 | /** |
| 1483 | * Cancels the transcoding session and notify the listener. |
| 1484 | * If the session happened to finish before being canceled this call is effectively a no-op |
| 1485 | * and will not update the result in that case. |
| 1486 | */ |
| 1487 | public void cancel() { |
| 1488 | synchronized (mLock) { |
| 1489 | // Check if the session is finished already. |
| 1490 | if (mStatus != STATUS_FINISHED) { |
| 1491 | try { |
| 1492 | ITranscodingClient client = mManager.getTranscodingClient(); |
| 1493 | // The client may be gone. |
| 1494 | if (client != null) { |
| 1495 | client.cancelSession(mSessionId); |
| 1496 | } |
| 1497 | } catch (RemoteException re) { |
| 1498 | //TODO(hkuang): Find out what to do if failing to cancel the session. |
| 1499 | Log.e(TAG, "Failed to cancel the session due to exception: " + re); |
| 1500 | } |
| 1501 | mStatus = STATUS_FINISHED; |
| 1502 | mResult = RESULT_CANCELED; |
| 1503 | |
| 1504 | // Notifies client the session is canceled. |
| 1505 | mListenerExecutor.execute(() -> mListener.onTranscodingFinished(this)); |
| 1506 | } |
| 1507 | } |
| 1508 | } |
| 1509 | |
| 1510 | /** |
| 1511 | * Gets the progress of the transcoding session. The progress is between 0 and 100, where 0 |
| 1512 | * means that the session has not yet started and 100 means that it is finished. For the |
| 1513 | * cancelled session, the progress will be the last updated progress before it is cancelled. |
| 1514 | * @return The progress. |
| 1515 | */ |
| 1516 | @IntRange(from = 0, to = 100) |
| 1517 | public int getProgress() { |
| 1518 | synchronized (mLock) { |
| 1519 | return mProgress; |
| 1520 | } |
| 1521 | } |
| 1522 | |
| 1523 | /** |
| 1524 | * Gets the status of the transcoding session. |
| 1525 | * @return The status. |
| 1526 | */ |
| 1527 | public @Status int getStatus() { |
| 1528 | synchronized (mLock) { |
| 1529 | return mStatus; |
| 1530 | } |
| 1531 | } |
| 1532 | |
| 1533 | /** |
| 1534 | * Adds a client uid that is also waiting for this transcoding session. |
| 1535 | * <p> |
| 1536 | * Only privilege caller with android.permission.WRITE_MEDIA_STORAGE could add the |
| 1537 | * uid. Note that the permission check happens on the service side upon starting the |
| 1538 | * transcoding. If the client does not have the permission, the transcoding will fail. |
| 1539 | * @param uid the additional client uid to be added. |
| 1540 | * @return true if successfully added, false otherwise. |
| 1541 | */ |
| 1542 | public boolean addClientUid(int uid) { |
| 1543 | if (uid < 0) { |
| 1544 | throw new IllegalArgumentException("Invalid Uid"); |
| 1545 | } |
| 1546 | |
| 1547 | // Get the client interface. |
| 1548 | ITranscodingClient client = mManager.getTranscodingClient(); |
| 1549 | if (client == null) { |
| 1550 | Log.e(TAG, "Service is dead..."); |
| 1551 | return false; |
| 1552 | } |
| 1553 | |
| 1554 | try { |
| 1555 | if (!client.addClientUid(mSessionId, uid)) { |
| 1556 | Log.e(TAG, "Failed to add client uid"); |
| 1557 | return false; |
| 1558 | } |
| 1559 | } catch (Exception ex) { |
| 1560 | Log.e(TAG, "Failed to get client uids due to " + ex); |
| 1561 | return false; |
| 1562 | } |
| 1563 | return true; |
| 1564 | } |
| 1565 | |
| 1566 | /** |
| 1567 | * Query all the client that waiting for this transcoding session |
| 1568 | * @return a list containing all the client uids. |
| 1569 | */ |
| 1570 | @NonNull |
| 1571 | public List<Integer> getClientUids() { |
| 1572 | List<Integer> uidList = new ArrayList<Integer>(); |
| 1573 | |
| 1574 | // Get the client interface. |
| 1575 | ITranscodingClient client = mManager.getTranscodingClient(); |
| 1576 | if (client == null) { |
| 1577 | Log.e(TAG, "Service is dead..."); |
| 1578 | return uidList; |
| 1579 | } |
| 1580 | |
| 1581 | try { |
| 1582 | int[] clientUids = client.getClientUids(mSessionId); |
| 1583 | for (int i : clientUids) { |
| 1584 | uidList.add(i); |
| 1585 | } |
| 1586 | } catch (Exception ex) { |
| 1587 | Log.e(TAG, "Failed to get client uids due to " + ex); |
| 1588 | } |
| 1589 | |
| 1590 | return uidList; |
| 1591 | } |
| 1592 | |
| 1593 | /** |
| 1594 | * Gets sessionId of the transcoding session. |
| 1595 | * @return session id. |
| 1596 | */ |
| 1597 | public int getSessionId() { |
| 1598 | return mSessionId; |
| 1599 | } |
| 1600 | |
| 1601 | /** |
| 1602 | * Gets the result of the transcoding session. |
| 1603 | * @return The result. |
| 1604 | */ |
| 1605 | public @Result int getResult() { |
| 1606 | synchronized (mLock) { |
| 1607 | return mResult; |
| 1608 | } |
| 1609 | } |
| 1610 | |
| 1611 | @Override |
| 1612 | public String toString() { |
| 1613 | String result; |
| 1614 | String status; |
| 1615 | |
| 1616 | switch (mResult) { |
| 1617 | case RESULT_NONE: |
| 1618 | result = "RESULT_NONE"; |
| 1619 | break; |
| 1620 | case RESULT_SUCCESS: |
| 1621 | result = "RESULT_SUCCESS"; |
| 1622 | break; |
| 1623 | case RESULT_ERROR: |
| 1624 | result = "RESULT_ERROR(" + mErrorCode + ")"; |
| 1625 | break; |
| 1626 | case RESULT_CANCELED: |
| 1627 | result = "RESULT_CANCELED"; |
| 1628 | break; |
| 1629 | default: |
| 1630 | result = String.valueOf(mResult); |
| 1631 | break; |
| 1632 | } |
| 1633 | |
| 1634 | switch (mStatus) { |
| 1635 | case STATUS_PENDING: |
| 1636 | status = "STATUS_PENDING"; |
| 1637 | break; |
| 1638 | case STATUS_PAUSED: |
| 1639 | status = "STATUS_PAUSED"; |
| 1640 | break; |
| 1641 | case STATUS_RUNNING: |
| 1642 | status = "STATUS_RUNNING"; |
| 1643 | break; |
| 1644 | case STATUS_FINISHED: |
| 1645 | status = "STATUS_FINISHED"; |
| 1646 | break; |
| 1647 | default: |
| 1648 | status = String.valueOf(mStatus); |
| 1649 | break; |
| 1650 | } |
| 1651 | return String.format(" session: {id: %d, status: %s, result: %s, progress: %d}", |
| 1652 | mSessionId, status, result, mProgress); |
| 1653 | } |
| 1654 | |
| 1655 | private void updateProgress(int newProgress) { |
| 1656 | synchronized (mLock) { |
| 1657 | mProgress = newProgress; |
| 1658 | if (mProgressUpdateExecutor != null && mProgressUpdateListener != null) { |
| 1659 | final OnProgressUpdateListener listener = mProgressUpdateListener; |
| 1660 | mProgressUpdateExecutor.execute( |
| 1661 | () -> listener.onProgressUpdate(this, newProgress)); |
| 1662 | } |
| 1663 | } |
| 1664 | } |
| 1665 | |
| 1666 | private void updateStatus(int newStatus) { |
| 1667 | synchronized (mLock) { |
| 1668 | mStatus = newStatus; |
| 1669 | } |
| 1670 | } |
| 1671 | } |
| 1672 | |
| 1673 | private ITranscodingClient getTranscodingClient() { |
| 1674 | synchronized (mLock) { |
| 1675 | return mTranscodingClient; |
| 1676 | } |
| 1677 | } |
| 1678 | |
| 1679 | /** |
| 1680 | * Enqueues a TranscodingRequest for execution. |
| 1681 | * <p> Upon successfully accepting the request, MediaTranscodingManager will return a |
| 1682 | * {@link TranscodingSession} to the client. Client should use {@link TranscodingSession} to |
| 1683 | * track the progress and get the result. |
| 1684 | * <p> MediaTranscodingManager will return null if fails to accept the request due to service |
| 1685 | * rebooting. Client could retry again after receiving null. |
| 1686 | * |
| 1687 | * @param transcodingRequest The TranscodingRequest to enqueue. |
| 1688 | * @param listenerExecutor Executor on which the listener is notified. |
| 1689 | * @param listener Listener to get notified when the transcoding session is finished. |
| 1690 | * @return A TranscodingSession for this operation. |
| 1691 | * @throws UnsupportedOperationException if the request could not be fulfilled. |
| 1692 | */ |
| 1693 | @Nullable |
| 1694 | public TranscodingSession enqueueRequest( |
| 1695 | @NonNull TranscodingRequest transcodingRequest, |
| 1696 | @NonNull @CallbackExecutor Executor listenerExecutor, |
| 1697 | @NonNull OnTranscodingFinishedListener listener) { |
| 1698 | Log.i(TAG, "enqueueRequest called."); |
| 1699 | Objects.requireNonNull(transcodingRequest, "transcodingRequest must not be null"); |
| 1700 | Objects.requireNonNull(listenerExecutor, "listenerExecutor must not be null"); |
| 1701 | Objects.requireNonNull(listener, "listener must not be null"); |
| 1702 | |
| 1703 | // Converts the request to TranscodingRequestParcel. |
| 1704 | TranscodingRequestParcel requestParcel = transcodingRequest.writeToParcel(mContext); |
| 1705 | |
| 1706 | Log.i(TAG, "Getting transcoding request " + transcodingRequest.getSourceUri()); |
| 1707 | |
| 1708 | // Submits the request to MediaTranscoding service. |
| 1709 | try { |
| 1710 | TranscodingSessionParcel sessionParcel = new TranscodingSessionParcel(); |
| 1711 | // Synchronizes the access to mPendingTranscodingSessions to make sure the session Id is |
| 1712 | // inserted in the mPendingTranscodingSessions in the callback handler. |
| 1713 | synchronized (mPendingTranscodingSessions) { |
| 1714 | synchronized (mLock) { |
| 1715 | if (mTranscodingClient == null) { |
| 1716 | // Try to register with the service again. |
| 1717 | IMediaTranscodingService service = getService(false /*retry*/); |
| 1718 | if (service == null) { |
| 1719 | Log.w(TAG, "Service rebooting. Try again later"); |
| 1720 | return null; |
| 1721 | } |
| 1722 | mTranscodingClient = registerClient(service); |
| 1723 | // If still fails, throws an exception to tell client to try later. |
| 1724 | if (mTranscodingClient == null) { |
| 1725 | Log.w(TAG, "Service rebooting. Try again later"); |
| 1726 | return null; |
| 1727 | } |
| 1728 | } |
| 1729 | |
| 1730 | if (!mTranscodingClient.submitRequest(requestParcel, sessionParcel)) { |
| 1731 | throw new UnsupportedOperationException("Failed to enqueue request"); |
| 1732 | } |
| 1733 | } |
| 1734 | |
| 1735 | // Wraps the TranscodingSessionParcel into a TranscodingSession and returns it to |
| 1736 | // client for tracking. |
| 1737 | TranscodingSession session = new TranscodingSession(this, transcodingRequest, |
| 1738 | sessionParcel, |
| 1739 | listenerExecutor, |
| 1740 | listener); |
| 1741 | |
| 1742 | // Adds the new session into pending sessions. |
| 1743 | mPendingTranscodingSessions.put(session.getSessionId(), session); |
| 1744 | return session; |
| 1745 | } |
| 1746 | } catch (RemoteException ex) { |
| 1747 | Log.w(TAG, "Service rebooting. Try again later"); |
| 1748 | return null; |
| 1749 | } catch (ServiceSpecificException ex) { |
| 1750 | throw new UnsupportedOperationException( |
| 1751 | "Failed to submit request to Transcoding service. Error: " + ex); |
| 1752 | } |
| 1753 | } |
| 1754 | } |