| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.media; |
| |
| import android.annotation.CallbackExecutor; |
| import android.annotation.IntDef; |
| import android.annotation.IntRange; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.SystemApi; |
| import android.annotation.TestApi; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.res.AssetFileDescriptor; |
| import android.net.Uri; |
| import android.os.ParcelFileDescriptor; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.system.Os; |
| import android.util.Log; |
| |
| import com.android.internal.annotations.GuardedBy; |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.io.FileNotFoundException; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.concurrent.Executor; |
| import java.util.concurrent.ExecutorService; |
| import java.util.concurrent.Executors; |
| |
| /** |
| MediaTranscodeManager provides an interface to the system's media transcoding service and can be |
| used to transcode media files, e.g. transcoding a video from HEVC to AVC. |
| |
| <h3>Transcoding Types</h3> |
| <h4>Video Transcoding</h4> |
| When transcoding a video file, the video file could be of any of the following types: |
| <ul> |
| <li> Video file with single video track. </li> |
| <li> Video file with multiple video track. </li> |
| <li> Video file with multiple video tracks and audio tracks. </li> |
| <li> Video file with video/audio tracks and metadata track. Note that metadata track will be passed |
| through only if it could be recognized by {@link MediaExtractor}. |
| TODO(hkuang): Finalize the metadata track behavior. </li> |
| </ul> |
| <p class=note> |
| Note that currently only support transcoding video file in mp4 format. |
| |
| <h3>Transcoding Request</h3> |
| <p> |
| To transcode a media file, first create a {@link TranscodingRequest} through its builder class |
| {@link TranscodingRequest.Builder}. Transcode requests are then enqueue to the manager through |
| {@link MediaTranscodeManager#enqueueRequest( |
| TranscodingRequest, Executor,OnTranscodingFinishedListener)} |
| TranscodeRequest are processed based on client process's priority and request priority. When a |
| transcode operation is completed the caller is notified via its |
| {@link OnTranscodingFinishedListener}. |
| In the meantime the caller may use the returned TranscodingJob object to cancel or check the status |
| of a specific transcode operation. |
| <p> |
| Here is an example where <code>Builder</code> is used to specify all parameters |
| |
| <pre class=prettyprint> |
| TranscodingRequest request = |
| new TranscodingRequest.Builder() |
| .setSourceUri(srcUri) |
| .setDestinationUri(dstUri) |
| .setType(MediaTranscodeManager.TRANSCODING_TYPE_VIDEO) |
| .setPriority(REALTIME) |
| .setVideoTrackFormat(videoFormat) |
| .build(); |
| }</pre> |
| |
| TODO(hkuang): Add architecture diagram showing the transcoding service and api. |
| TODO(hkuang): Add sample code when API is settled. |
| TODO(hkuang): Clarify whether multiple video tracks is supported or not. |
| TODO(hkuang): Clarify whether image/audio transcoding is supported or not. |
| TODO(hkuang): Clarify what will happen if there is unrecognized track in the source. |
| TODO(hkuang): Clarify whether supports scaling. |
| TODO(hkuang): Clarify whether supports framerate conversion. |
| @hide |
| */ |
| @TestApi |
| @SystemApi |
| public final class MediaTranscodeManager { |
| private static final String TAG = "MediaTranscodeManager"; |
| |
| private static final String MEDIA_TRANSCODING_SERVICE = "media.transcoding"; |
| |
| /** Maximum number of retry to connect to the service. */ |
| private static final int CONNECT_SERVICE_RETRY_COUNT = 100; |
| |
| /** Interval between trying to reconnect to the service. */ |
| private static final int INTERVAL_CONNECT_SERVICE_RETRY_MS = 40; |
| |
| /** |
| * Default transcoding type. |
| * @hide |
| */ |
| public static final int TRANSCODING_TYPE_UNKNOWN = 0; |
| |
| /** |
| * TRANSCODING_TYPE_VIDEO indicates that client wants to perform transcoding on a video file. |
| * <p>Note that currently only support transcoding video file in mp4 format. |
| */ |
| public static final int TRANSCODING_TYPE_VIDEO = 1; |
| |
| /** |
| * TRANSCODING_TYPE_IMAGE indicates that client wants to perform transcoding on an image file. |
| * @hide |
| */ |
| public static final int TRANSCODING_TYPE_IMAGE = 2; |
| |
| /** @hide */ |
| @IntDef(prefix = {"TRANSCODING_TYPE_"}, value = { |
| TRANSCODING_TYPE_UNKNOWN, |
| TRANSCODING_TYPE_VIDEO, |
| TRANSCODING_TYPE_IMAGE, |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface TranscodingType {} |
| |
| /** |
| * Default value. |
| * @hide |
| */ |
| public static final int PRIORITY_UNKNOWN = 0; |
| /** |
| * PRIORITY_REALTIME indicates that the transcoding request is time-critical and that the client |
| * wants the transcoding result as soon as possible. |
| * <p> Set PRIORITY_REALTIME only if the transcoding is time-critical as it will involve |
| * performance penalty due to resource reallocation to prioritize the jobs with higher priority. |
| * TODO(hkuang): Add more description of this when priority is finalized. |
| */ |
| public static final int PRIORITY_REALTIME = 1; |
| |
| /** |
| * PRIORITY_OFFLINE indicates the transcoding is not time-critical and the client does not need |
| * the transcoding result as soon as possible. |
| * <p>Jobs with PRIORITY_OFFLINE will be scheduled behind PRIORITY_REALTIME. Always set to |
| * PRIORITY_OFFLINE if client does not need the result as soon as possible and could accept |
| * delay of the transcoding result. |
| * @hide |
| * TODO(hkuang): Add more description of this when priority is finalized. |
| */ |
| public static final int PRIORITY_OFFLINE = 2; |
| |
| /** @hide */ |
| @IntDef(prefix = {"PRIORITY_"}, value = { |
| PRIORITY_UNKNOWN, |
| PRIORITY_REALTIME, |
| PRIORITY_OFFLINE, |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface TranscodingPriority {} |
| |
| /** |
| * Listener that gets notified when a transcoding operation has finished. |
| * This listener gets notified regardless of how the operation finished. It is up to the |
| * listener implementation to check the result and take appropriate action. |
| */ |
| @FunctionalInterface |
| public interface OnTranscodingFinishedListener { |
| /** |
| * Called when the transcoding operation has finished. The receiver may use the |
| * TranscodingJob to check the result, i.e. whether the operation succeeded, was canceled or |
| * if an error occurred. |
| * |
| * @param transcodingJob The TranscodingJob instance for the finished transcoding operation. |
| */ |
| void onTranscodingFinished(@NonNull TranscodingJob transcodingJob); |
| } |
| |
| private final Context mContext; |
| private ContentResolver mContentResolver; |
| private final String mPackageName; |
| private final int mPid; |
| private final int mUid; |
| private final ExecutorService mExecutor = Executors.newSingleThreadExecutor(); |
| private final HashMap<Integer, TranscodingJob> mPendingTranscodingJobs = new HashMap(); |
| private final Object mLock = new Object(); |
| @GuardedBy("mLock") |
| @NonNull private ITranscodingClient mTranscodingClient = null; |
| private static MediaTranscodeManager sMediaTranscodeManager; |
| |
| private void handleTranscodingFinished(int jobId, TranscodingResultParcel result) { |
| synchronized (mPendingTranscodingJobs) { |
| // Gets the job associated with the jobId and removes it from |
| // mPendingTranscodingJobs. |
| final TranscodingJob job = mPendingTranscodingJobs.remove(jobId); |
| |
| if (job == null) { |
| // This should not happen in reality. |
| Log.e(TAG, "Job " + jobId + " is not in PendingJobs"); |
| return; |
| } |
| |
| // Updates the job status and result. |
| job.updateStatusAndResult(TranscodingJob.STATUS_FINISHED, |
| TranscodingJob.RESULT_SUCCESS); |
| |
| // Notifies client the job is done. |
| if (job.mListener != null && job.mListenerExecutor != null) { |
| job.mListenerExecutor.execute(() -> job.mListener.onTranscodingFinished(job)); |
| } |
| } |
| } |
| |
| private void handleTranscodingFailed(int jobId, int errorCode) { |
| synchronized (mPendingTranscodingJobs) { |
| // Gets the job associated with the jobId and removes it from |
| // mPendingTranscodingJobs. |
| final TranscodingJob job = mPendingTranscodingJobs.remove(jobId); |
| |
| if (job == null) { |
| // This should not happen in reality. |
| Log.e(TAG, "Job " + jobId + " is not in PendingJobs"); |
| return; |
| } |
| |
| // Updates the job status and result. |
| job.updateStatusAndResult(TranscodingJob.STATUS_FINISHED, |
| TranscodingJob.RESULT_ERROR); |
| |
| // Notifies client the job failed. |
| if (job.mListener != null && job.mListenerExecutor != null) { |
| job.mListenerExecutor.execute(() -> job.mListener.onTranscodingFinished(job)); |
| } |
| } |
| } |
| |
| private void handleTranscodingProgressUpdate(int jobId, int newProgress) { |
| synchronized (mPendingTranscodingJobs) { |
| // Gets the job associated with the jobId. |
| final TranscodingJob job = mPendingTranscodingJobs.get(jobId); |
| |
| if (job == null) { |
| // This should not happen in reality. |
| Log.e(TAG, "Job " + jobId + " is not in PendingJobs"); |
| return; |
| } |
| |
| // Updates the job progress. |
| job.updateProgress(newProgress); |
| |
| // Notifies client the progress update. |
| if (job.mProgressUpdateExecutor != null && job.mProgressUpdateListener != null) { |
| job.mProgressUpdateExecutor.execute( |
| () -> job.mProgressUpdateListener.onProgressUpdate(job, newProgress)); |
| } |
| } |
| } |
| |
| private static IMediaTranscodingService getService(boolean retry) { |
| int retryCount = !retry ? 1 : CONNECT_SERVICE_RETRY_COUNT; |
| Log.i(TAG, "get service with rety " + retryCount); |
| for (int count = 1; count <= retryCount; count++) { |
| Log.d(TAG, "Trying to connect to service. Try count: " + count); |
| IMediaTranscodingService service = IMediaTranscodingService.Stub.asInterface( |
| ServiceManager.getService(MEDIA_TRANSCODING_SERVICE)); |
| if (service != null) { |
| return service; |
| } |
| try { |
| // Sleep a bit before retry. |
| Thread.sleep(INTERVAL_CONNECT_SERVICE_RETRY_MS); |
| } catch (InterruptedException ie) { |
| /* ignore */ |
| } |
| } |
| |
| throw new UnsupportedOperationException("Failed to connect to MediaTranscoding service"); |
| } |
| |
| /* |
| * Handle client binder died event. |
| * Upon receiving a binder died event of the client, we will do the following: |
| * 1) For the job that is running, notify the client that the job is failed with error code, |
| * so client could choose to retry the job or not. |
| * TODO(hkuang): Add a new error code to signal service died error. |
| * 2) For the jobs that is still pending or paused, we will resubmit the job internally once |
| * we successfully reconnect to the service and register a new client. |
| * 3) When trying to connect to the service and register a new client. The service may need time |
| * to reboot or never boot up again. So we will retry for a number of times. If we still |
| * could not connect, we will notify client job failure for the pending and paused jobs. |
| */ |
| private void onClientDied() { |
| synchronized (mLock) { |
| mTranscodingClient = null; |
| } |
| |
| // Delegates the job notification and retry to the executor as it may take some time. |
| mExecutor.execute(() -> { |
| // List to track the jobs that we want to retry. |
| List<TranscodingJob> retryJobs = new ArrayList<TranscodingJob>(); |
| |
| // First notify the client of job failure for all the running jobs. |
| synchronized (mPendingTranscodingJobs) { |
| for (Map.Entry<Integer, TranscodingJob> entry : |
| mPendingTranscodingJobs.entrySet()) { |
| TranscodingJob job = entry.getValue(); |
| |
| if (job.getStatus() == TranscodingJob.STATUS_RUNNING) { |
| job.updateStatusAndResult(TranscodingJob.STATUS_FINISHED, |
| TranscodingJob.RESULT_ERROR); |
| |
| // Remove the job from pending jobs. |
| mPendingTranscodingJobs.remove(entry.getKey()); |
| |
| if (job.mListener != null && job.mListenerExecutor != null) { |
| Log.i(TAG, "Notify client job failed"); |
| job.mListenerExecutor.execute( |
| () -> job.mListener.onTranscodingFinished(job)); |
| } |
| } else if (job.getStatus() == TranscodingJob.STATUS_PENDING |
| || job.getStatus() == TranscodingJob.STATUS_PAUSED) { |
| // Add the job to retryJobs to handle them later. |
| retryJobs.add(job); |
| } |
| } |
| } |
| |
| // Try to register with the service once it boots up. |
| IMediaTranscodingService service = getService(true /*retry*/); |
| boolean haveTranscodingClient = false; |
| if (service != null) { |
| synchronized (mLock) { |
| mTranscodingClient = registerClient(service); |
| if (mTranscodingClient != null) { |
| haveTranscodingClient = true; |
| } |
| } |
| } |
| |
| for (TranscodingJob job : retryJobs) { |
| // Notify the job failure if we fails to connect to the service or fail |
| // to retry the job. |
| if (!haveTranscodingClient || !job.retry()) { |
| // TODO(hkuang): Return correct error code to the client. |
| handleTranscodingFailed(job.getJobId(), 0 /*unused */); |
| } |
| } |
| }); |
| } |
| |
| private void updateStatus(int jobId, int status) { |
| synchronized (mPendingTranscodingJobs) { |
| final TranscodingJob job = mPendingTranscodingJobs.get(jobId); |
| |
| if (job == null) { |
| // This should not happen in reality. |
| Log.e(TAG, "Job " + jobId + " is not in PendingJobs"); |
| return; |
| } |
| |
| // Updates the job status. |
| job.updateStatus(status); |
| } |
| } |
| |
| // Just forwards all the events to the event handler. |
| private ITranscodingClientCallback mTranscodingClientCallback = |
| new ITranscodingClientCallback.Stub() { |
| // TODO(hkuang): Add more unit test to test difference file open mode. |
| @Override |
| public ParcelFileDescriptor openFileDescriptor(String fileUri, String mode) |
| throws RemoteException { |
| if (!mode.equals("r") && !mode.equals("w") && !mode.equals("rw")) { |
| Log.e(TAG, "Unsupport mode: " + mode); |
| return null; |
| } |
| |
| Uri uri = Uri.parse(fileUri); |
| try { |
| AssetFileDescriptor afd = mContentResolver.openAssetFileDescriptor(uri, |
| mode); |
| if (afd != null) { |
| return afd.getParcelFileDescriptor(); |
| } |
| } catch (FileNotFoundException e) { |
| Log.w(TAG, "Cannot find content uri: " + uri, e); |
| } catch (SecurityException e) { |
| Log.w(TAG, "Cannot open content uri: " + uri, e); |
| } catch (Exception e) { |
| Log.w(TAG, "Unknown content uri: " + uri, e); |
| } |
| return null; |
| } |
| |
| @Override |
| public void onTranscodingStarted(int jobId) throws RemoteException { |
| updateStatus(jobId, TranscodingJob.STATUS_RUNNING); |
| } |
| |
| @Override |
| public void onTranscodingPaused(int jobId) throws RemoteException { |
| updateStatus(jobId, TranscodingJob.STATUS_PAUSED); |
| } |
| |
| @Override |
| public void onTranscodingResumed(int jobId) throws RemoteException { |
| updateStatus(jobId, TranscodingJob.STATUS_RUNNING); |
| } |
| |
| @Override |
| public void onTranscodingFinished(int jobId, TranscodingResultParcel result) |
| throws RemoteException { |
| handleTranscodingFinished(jobId, result); |
| } |
| |
| @Override |
| public void onTranscodingFailed(int jobId, int errorCode) throws RemoteException { |
| handleTranscodingFailed(jobId, errorCode); |
| } |
| |
| @Override |
| public void onAwaitNumberOfJobsChanged(int jobId, int oldAwaitNumber, |
| int newAwaitNumber) throws RemoteException { |
| //TODO(hkuang): Implement this. |
| } |
| |
| @Override |
| public void onProgressUpdate(int jobId, int newProgress) throws RemoteException { |
| handleTranscodingProgressUpdate(jobId, newProgress); |
| } |
| }; |
| |
| private ITranscodingClient registerClient(IMediaTranscodingService service) |
| throws UnsupportedOperationException { |
| synchronized (mLock) { |
| try { |
| // Registers the client with MediaTranscoding service. |
| mTranscodingClient = service.registerClient( |
| mTranscodingClientCallback, |
| mPackageName, |
| mPackageName, |
| IMediaTranscodingService.USE_CALLING_UID, |
| IMediaTranscodingService.USE_CALLING_PID); |
| |
| if (mTranscodingClient != null) { |
| mTranscodingClient.asBinder().linkToDeath(() -> onClientDied(), /* flags */ 0); |
| } |
| return mTranscodingClient; |
| } catch (RemoteException re) { |
| Log.e(TAG, "Failed to register new client due to exception " + re); |
| mTranscodingClient = null; |
| } |
| } |
| throw new UnsupportedOperationException("Failed to register new client"); |
| } |
| |
| /** |
| * @hide |
| */ |
| public MediaTranscodeManager(@NonNull Context context) { |
| mContext = context; |
| mContentResolver = mContext.getContentResolver(); |
| mPackageName = mContext.getPackageName(); |
| mPid = Os.getuid(); |
| mUid = Os.getpid(); |
| IMediaTranscodingService service = getService(false /*retry*/); |
| mTranscodingClient = registerClient(service); |
| } |
| |
| public static final class TranscodingRequest { |
| /** Uri of the source media file. */ |
| private @NonNull Uri mSourceUri; |
| |
| /** Uri of the destination media file. */ |
| private @NonNull Uri mDestinationUri; |
| |
| /** Type of the transcoding. */ |
| private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN; |
| |
| /** Priority of the transcoding. */ |
| private @TranscodingPriority int mPriority = PRIORITY_UNKNOWN; |
| |
| /** |
| * Desired output video format of the destination file. |
| * <p> If this is null, source file's video track will be passed through and copied to the |
| * destination file. |
| * <p> |
| */ |
| private @Nullable MediaFormat mVideoTrackFormat = null; |
| |
| /** |
| * Desired output audio format of the destination file. |
| * <p> If this is null, source file's audio track will be passed through and copied to the |
| * destination file. |
| * @hide |
| */ |
| private @Nullable MediaFormat mAudioTrackFormat = null; |
| |
| /** |
| * Desired image format for the destination file. |
| * <p> If this is null, source file's image track will be passed through and copied to the |
| * destination file. |
| * @hide |
| */ |
| private @Nullable MediaFormat mImageFormat = null; |
| |
| @VisibleForTesting |
| private TranscodingTestConfig mTestConfig = null; |
| |
| private TranscodingRequest(Builder b) { |
| mSourceUri = b.mSourceUri; |
| mDestinationUri = b.mDestinationUri; |
| mPriority = b.mPriority; |
| mType = b.mType; |
| mVideoTrackFormat = b.mVideoTrackFormat; |
| mAudioTrackFormat = b.mAudioTrackFormat; |
| mImageFormat = b.mImageFormat; |
| mTestConfig = b.mTestConfig; |
| } |
| |
| /** Return the type of the transcoding. */ |
| @TranscodingType |
| public int getType() { |
| return mType; |
| } |
| |
| /** Return source uri of the transcoding. */ |
| @NonNull |
| public Uri getSourceUri() { |
| return mSourceUri; |
| } |
| |
| /** Return destination uri of the transcoding. */ |
| @NonNull |
| public Uri getDestinationUri() { |
| return mDestinationUri; |
| } |
| |
| /** Return priority of the transcoding. */ |
| @TranscodingPriority |
| public int getPriority() { |
| return mPriority; |
| } |
| |
| /** |
| * Return the video track format of the transcoding. |
| * This will be null is the transcoding is not for video transcoding. |
| */ |
| @Nullable |
| public MediaFormat getVideoTrackFormat() { |
| return mVideoTrackFormat; |
| } |
| |
| /** |
| * Return TestConfig of the transcoding. |
| * @hide |
| */ |
| @Nullable |
| public TranscodingTestConfig getTestConfig() { |
| return mTestConfig; |
| } |
| |
| /* Writes the TranscodingRequest to a parcel. */ |
| private TranscodingRequestParcel writeToParcel() { |
| TranscodingRequestParcel parcel = new TranscodingRequestParcel(); |
| // TODO(hkuang): Implement all the fields here to pass to service. |
| parcel.priority = mPriority; |
| parcel.transcodingType = mType; |
| parcel.sourceFilePath = mSourceUri.toString(); |
| parcel.destinationFilePath = mDestinationUri.toString(); |
| parcel.requestedVideoTrackFormat = convertToVideoTrackFormat(mVideoTrackFormat); |
| if (mTestConfig != null) { |
| parcel.isForTesting = true; |
| parcel.testConfig = mTestConfig; |
| } |
| return parcel; |
| } |
| |
| /* Converts the MediaFormat to TranscodingVideoTrackFormat. */ |
| private static TranscodingVideoTrackFormat convertToVideoTrackFormat(MediaFormat format) { |
| if (format == null) { |
| throw new IllegalArgumentException("Invalid MediaFormat"); |
| } |
| |
| TranscodingVideoTrackFormat trackFormat = new TranscodingVideoTrackFormat(); |
| |
| if (format.containsKey(MediaFormat.KEY_MIME)) { |
| String mime = format.getString(MediaFormat.KEY_MIME); |
| if (MediaFormat.MIMETYPE_VIDEO_AVC.equals(mime)) { |
| trackFormat.codecType = TranscodingVideoCodecType.kAvc; |
| } else if (MediaFormat.MIMETYPE_VIDEO_HEVC.equals(mime)) { |
| trackFormat.codecType = TranscodingVideoCodecType.kHevc; |
| } else { |
| throw new UnsupportedOperationException("Only support transcode to avc/hevc"); |
| } |
| } |
| |
| if (format.containsKey(MediaFormat.KEY_BIT_RATE)) { |
| int bitrateBps = format.getInteger(MediaFormat.KEY_BIT_RATE); |
| if (bitrateBps <= 0) { |
| throw new IllegalArgumentException("Bitrate must be larger than 0"); |
| } |
| trackFormat.bitrateBps = bitrateBps; |
| } |
| |
| if (format.containsKey(MediaFormat.KEY_WIDTH) && format.containsKey( |
| MediaFormat.KEY_HEIGHT)) { |
| int width = format.getInteger(MediaFormat.KEY_WIDTH); |
| int height = format.getInteger(MediaFormat.KEY_HEIGHT); |
| if (width <= 0 || height <= 0) { |
| throw new IllegalArgumentException("Width and height must be larger than 0"); |
| } |
| // TODO(hkuang): Validate the aspect ratio after adding scaling. |
| trackFormat.width = width; |
| trackFormat.height = height; |
| } |
| |
| if (format.containsKey(MediaFormat.KEY_PROFILE)) { |
| int profile = format.getInteger(MediaFormat.KEY_PROFILE); |
| if (profile <= 0) { |
| throw new IllegalArgumentException("Invalid codec profile"); |
| } |
| // TODO(hkuang): Validate the profile according to codec type. |
| trackFormat.profile = profile; |
| } |
| |
| if (format.containsKey(MediaFormat.KEY_LEVEL)) { |
| int level = format.getInteger(MediaFormat.KEY_LEVEL); |
| if (level <= 0) { |
| throw new IllegalArgumentException("Invalid codec level"); |
| } |
| // TODO(hkuang): Validate the level according to codec type. |
| trackFormat.level = level; |
| } |
| |
| return trackFormat; |
| } |
| |
| /** |
| * Builder class for {@link TranscodingRequest} objects. |
| * Use this class to configure and create a <code>TranscodingRequest</code> instance. |
| */ |
| public static final class Builder { |
| private @NonNull Uri mSourceUri; |
| private @NonNull Uri mDestinationUri; |
| private @TranscodingType int mType = TRANSCODING_TYPE_UNKNOWN; |
| private @TranscodingPriority int mPriority = PRIORITY_UNKNOWN; |
| private @Nullable MediaFormat mVideoTrackFormat; |
| private @Nullable MediaFormat mAudioTrackFormat; |
| private @Nullable MediaFormat mImageFormat; |
| private TranscodingTestConfig mTestConfig; |
| |
| /** |
| * Specifies the uri of source media file. |
| * |
| * @param sourceUri Content uri for the source media file. |
| * @return The same builder instance. |
| * @throws IllegalArgumentException if Uri is null or empty. |
| */ |
| // TODO(hkuang): Add documentation on how the app could generate the correct Uri. |
| @NonNull |
| public Builder setSourceUri(@NonNull Uri sourceUri) { |
| if (sourceUri == null || Uri.EMPTY.equals(sourceUri)) { |
| throw new IllegalArgumentException( |
| "You must specify a non-empty source Uri."); |
| } |
| mSourceUri = sourceUri; |
| return this; |
| } |
| |
| /** |
| * Specifies the uri of the destination media file. |
| * |
| * @param destinationUri Content uri for the destination media file. |
| * @return The same builder instance. |
| * @throws IllegalArgumentException if Uri is null or empty. |
| */ |
| @NonNull |
| public Builder setDestinationUri(@NonNull Uri destinationUri) { |
| if (destinationUri == null || Uri.EMPTY.equals(destinationUri)) { |
| throw new IllegalArgumentException( |
| "You must specify a non-empty destination Uri."); |
| } |
| mDestinationUri = destinationUri; |
| return this; |
| } |
| |
| /** |
| * Specifies the priority of the transcoding. |
| * |
| * @param priority Must be one of the {@code PRIORITY_*} |
| * @return The same builder instance. |
| * @throws IllegalArgumentException if flags is invalid. |
| */ |
| @NonNull |
| public Builder setPriority(@TranscodingPriority int priority) { |
| if (priority != PRIORITY_OFFLINE && priority != PRIORITY_REALTIME) { |
| throw new IllegalArgumentException("Invalid priority: " + priority); |
| } |
| mPriority = priority; |
| return this; |
| } |
| |
| /** |
| * Specifies the type of transcoding. |
| * <p> Clients must provide the source and destination that corresponds to the |
| * transcoding type. |
| * |
| * @param type Must be one of the {@code TRANSCODING_TYPE_*} |
| * @return The same builder instance. |
| * @throws IllegalArgumentException if flags is invalid. |
| */ |
| @NonNull |
| public Builder setType(@TranscodingType int type) { |
| if (type != TRANSCODING_TYPE_VIDEO && type != TRANSCODING_TYPE_IMAGE) { |
| throw new IllegalArgumentException("Invalid transcoding type"); |
| } |
| mType = type; |
| return this; |
| } |
| |
| /** |
| * Specifies the desired video track format in the destination media file. |
| * <p>Client could only specify the settings that matters to them, e.g. codec format or |
| * bitrate. And by default, transcoding will preserve the original video's |
| * settings(bitrate, framerate, resolution) if not provided. |
| * <p>Note that some settings may silently fail to apply if the device does not |
| * support them. |
| * TODO(hkuang): Add MediaTranscodeUtil to help client generate transcoding setting. |
| * TODO(hkuang): Add MediaTranscodeUtil to check if the setting is valid. |
| * |
| * @param videoFormat MediaFormat containing the settings that client wants override in |
| * the original video's video track. |
| * @return The same builder instance. |
| * @throws IllegalArgumentException if videoFormat is invalid. |
| */ |
| @NonNull |
| public Builder setVideoTrackFormat(@NonNull MediaFormat videoFormat) { |
| if (videoFormat == null) { |
| throw new IllegalArgumentException("videoFormat must not be null"); |
| } |
| |
| // Check if the MediaFormat is for video by looking at the MIME type. |
| String mime = videoFormat.containsKey(MediaFormat.KEY_MIME) |
| ? videoFormat.getString(MediaFormat.KEY_MIME) : null; |
| if (mime == null || !mime.startsWith("video/")) { |
| throw new IllegalArgumentException("Invalid video format: wrong mime type"); |
| } |
| |
| mVideoTrackFormat = videoFormat; |
| return this; |
| } |
| |
| /** |
| * Sets the delay in processing this request. |
| * @param config test config. |
| * @return The same builder instance. |
| * @hide |
| */ |
| @VisibleForTesting |
| @NonNull |
| public Builder setTestConfig(@NonNull TranscodingTestConfig config) { |
| mTestConfig = config; |
| return this; |
| } |
| |
| /** |
| * @return a new {@link TranscodingRequest} instance successfully initialized with all |
| * the parameters set on this <code>Builder</code>. |
| * @throws UnsupportedOperationException if the parameters set on the |
| * <code>Builder</code> were incompatible, or if they are not supported by the |
| * device. |
| */ |
| @NonNull |
| public TranscodingRequest build() { |
| if (mSourceUri == null) { |
| throw new UnsupportedOperationException("Source URI must not be null"); |
| } |
| |
| if (mDestinationUri == null) { |
| throw new UnsupportedOperationException("Destination URI must not be null"); |
| } |
| |
| if (mPriority == PRIORITY_UNKNOWN) { |
| throw new UnsupportedOperationException("Must specify transcoding priority"); |
| } |
| |
| // Only support video transcoding now. |
| if (mType != TRANSCODING_TYPE_VIDEO) { |
| throw new UnsupportedOperationException("Only supports video transcoding now"); |
| } |
| |
| // Must provide video track format for video transcoding. |
| if (mType == TRANSCODING_TYPE_VIDEO && mVideoTrackFormat == null) { |
| throw new UnsupportedOperationException( |
| "Must provide video track format for video transcoding"); |
| } |
| |
| return new TranscodingRequest(this); |
| } |
| } |
| } |
| |
| /** |
| * Handle to an enqueued transcoding operation. An instance of this class represents a single |
| * enqueued transcoding operation. The caller can use that instance to query the status or |
| * progress, and to get the result once the operation has completed. |
| */ |
| public static final class TranscodingJob { |
| /** The job is enqueued but not yet running. */ |
| public static final int STATUS_PENDING = 1; |
| /** The job is currently running. */ |
| public static final int STATUS_RUNNING = 2; |
| /** The job is finished. */ |
| public static final int STATUS_FINISHED = 3; |
| /** The job is paused. */ |
| public static final int STATUS_PAUSED = 4; |
| |
| /** @hide */ |
| @IntDef(prefix = { "STATUS_" }, value = { |
| STATUS_PENDING, |
| STATUS_RUNNING, |
| STATUS_FINISHED, |
| STATUS_PAUSED, |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface Status {} |
| |
| /** The job does not have a result yet. */ |
| public static final int RESULT_NONE = 1; |
| /** The job completed successfully. */ |
| public static final int RESULT_SUCCESS = 2; |
| /** The job encountered an error while running. */ |
| public static final int RESULT_ERROR = 3; |
| /** The job was canceled by the caller. */ |
| public static final int RESULT_CANCELED = 4; |
| |
| /** @hide */ |
| @IntDef(prefix = { "RESULT_" }, value = { |
| RESULT_NONE, |
| RESULT_SUCCESS, |
| RESULT_ERROR, |
| RESULT_CANCELED, |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface Result {} |
| |
| /** Listener that gets notified when the progress changes. */ |
| @FunctionalInterface |
| public interface OnProgressUpdateListener { |
| /** |
| * Called when the progress changes. The progress is in percentage between 0 and 1, |
| * where 0 means that the job has not yet started and 100 means that it has finished. |
| * |
| * @param job The job associated with the progress. |
| * @param progress The new progress ranging from 0 ~ 100 inclusive. |
| */ |
| void onProgressUpdate(@NonNull TranscodingJob job, |
| @IntRange(from = 0, to = 100) int progress); |
| } |
| |
| private final ITranscodingClient mJobOwner; |
| private final Executor mListenerExecutor; |
| private final OnTranscodingFinishedListener mListener; |
| private int mJobId = -1; |
| // Lock for internal state. |
| private final Object mLock = new Object(); |
| @GuardedBy("mLock") |
| private Executor mProgressUpdateExecutor = null; |
| @GuardedBy("mLock") |
| private OnProgressUpdateListener mProgressUpdateListener = null; |
| @GuardedBy("mLock") |
| private int mProgress = 0; |
| @GuardedBy("mLock") |
| private int mProgressUpdateInterval = 0; |
| @GuardedBy("mLock") |
| private @Status int mStatus = STATUS_PENDING; |
| @GuardedBy("mLock") |
| private @Result int mResult = RESULT_NONE; |
| |
| private TranscodingJob( |
| @NonNull ITranscodingClient jobOwner, |
| @NonNull TranscodingJobParcel parcel, |
| @NonNull @CallbackExecutor Executor executor, |
| @NonNull OnTranscodingFinishedListener listener) { |
| Objects.requireNonNull(jobOwner, "JobOwner must not be null"); |
| Objects.requireNonNull(parcel, "TranscodingJobParcel must not be null"); |
| Objects.requireNonNull(executor, "listenerExecutor must not be null"); |
| Objects.requireNonNull(listener, "listener must not be null"); |
| mJobOwner = jobOwner; |
| mJobId = parcel.jobId; |
| mListenerExecutor = executor; |
| mListener = listener; |
| } |
| |
| /** |
| * Set a progress listener. |
| * @param executor The executor on which listener will be invoked. |
| * @param listener The progress listener. |
| */ |
| public void setOnProgressUpdateListener( |
| @NonNull @CallbackExecutor Executor executor, |
| @Nullable OnProgressUpdateListener listener) { |
| setOnProgressUpdateListener( |
| 0 /* minProgressUpdateInterval */, |
| executor, listener); |
| } |
| |
| /** |
| * Set a progress listener with specified progress update interval. |
| * @param minProgressUpdateInterval The minimum interval between each progress update. |
| * @param executor The executor on which listener will be invoked. |
| * @param listener The progress listener. |
| */ |
| public void setOnProgressUpdateListener( |
| int minProgressUpdateInterval, |
| @NonNull @CallbackExecutor Executor executor, |
| @Nullable OnProgressUpdateListener listener) { |
| synchronized (mLock) { |
| Objects.requireNonNull(executor, "listenerExecutor must not be null"); |
| Objects.requireNonNull(listener, "listener must not be null"); |
| mProgressUpdateExecutor = executor; |
| mProgressUpdateListener = listener; |
| } |
| } |
| |
| private void updateStatusAndResult(@Status int jobStatus, |
| @Result int jobResult) { |
| synchronized (mLock) { |
| mStatus = jobStatus; |
| mResult = jobResult; |
| } |
| } |
| |
| /** |
| * Resubmit the transcoding job to the service. |
| * |
| * @return true if successfully resubmit the job to the service. False otherwise. |
| */ |
| public boolean retry() { |
| synchronized (mLock) { |
| // TODO(hkuang): Implement this. |
| } |
| return true; |
| } |
| |
| /** |
| * Cancels the transcoding job and notify the listener. |
| * If the job happened to finish before being canceled this call is effectively a no-op and |
| * will not update the result in that case. |
| */ |
| public void cancel() { |
| synchronized (mLock) { |
| // Check if the job is finished already. |
| if (mStatus != STATUS_FINISHED) { |
| try { |
| mJobOwner.cancelJob(mJobId); |
| } catch (RemoteException re) { |
| //TODO(hkuang): Find out what to do if failing to cancel the job. |
| Log.e(TAG, "Failed to cancel the job due to exception: " + re); |
| } |
| mStatus = STATUS_FINISHED; |
| mResult = RESULT_CANCELED; |
| |
| // Notifies client the job is canceled. |
| mListenerExecutor.execute(() -> mListener.onTranscodingFinished(this)); |
| } |
| } |
| } |
| |
| /** |
| * Gets the progress of the transcoding job. The progress is between 0 and 100, where 0 |
| * means that the job has not yet started and 100 means that it is finished. For the |
| * cancelled job, the progress will be the last updated progress before it is cancelled. |
| * @return The progress. |
| */ |
| @IntRange(from = 0, to = 100) |
| public int getProgress() { |
| synchronized (mLock) { |
| return mProgress; |
| } |
| } |
| |
| /** |
| * Gets the status of the transcoding job. |
| * @return The status. |
| */ |
| public @Status int getStatus() { |
| synchronized (mLock) { |
| return mStatus; |
| } |
| } |
| |
| /** |
| * Gets jobId of the transcoding job. |
| * @return job id. |
| */ |
| public int getJobId() { |
| return mJobId; |
| } |
| |
| /** |
| * Gets the result of the transcoding job. |
| * @return The result. |
| */ |
| public @Result int getResult() { |
| synchronized (mLock) { |
| return mResult; |
| } |
| } |
| |
| private void updateProgress(int newProgress) { |
| synchronized (mLock) { |
| mProgress = newProgress; |
| } |
| } |
| |
| private void updateStatus(int newStatus) { |
| synchronized (mLock) { |
| mStatus = newStatus; |
| } |
| } |
| } |
| |
| /** |
| * Enqueues a TranscodingRequest for execution. |
| * <p> Upon successfully accepting the request, MediaTranscodeManager will return a |
| * {@link TranscodingJob} to the client. Client should use {@link TranscodingJob} to track the |
| * progress and get the result. |
| * |
| * @param transcodingRequest The TranscodingRequest to enqueue. |
| * @param listenerExecutor Executor on which the listener is notified. |
| * @param listener Listener to get notified when the transcoding job is finished. |
| * @return A TranscodingJob for this operation. |
| * @throws FileNotFoundException if the source Uri or destination Uri could not be opened. |
| * @throws UnsupportedOperationException if the request could not be fulfilled. |
| */ |
| @NonNull |
| public TranscodingJob enqueueRequest( |
| @NonNull TranscodingRequest transcodingRequest, |
| @NonNull @CallbackExecutor Executor listenerExecutor, |
| @NonNull OnTranscodingFinishedListener listener) |
| throws FileNotFoundException { |
| Log.i(TAG, "enqueueRequest called."); |
| Objects.requireNonNull(transcodingRequest, "transcodingRequest must not be null"); |
| Objects.requireNonNull(listenerExecutor, "listenerExecutor must not be null"); |
| Objects.requireNonNull(listener, "listener must not be null"); |
| |
| // Converts the request to TranscodingRequestParcel. |
| TranscodingRequestParcel requestParcel = transcodingRequest.writeToParcel(); |
| |
| // Submits the request to MediaTranscoding service. |
| try { |
| TranscodingJobParcel jobParcel = new TranscodingJobParcel(); |
| // Synchronizes the access to mPendingTranscodingJobs to make sure the job Id is |
| // inserted in the mPendingTranscodingJobs in the callback handler. |
| synchronized (mPendingTranscodingJobs) { |
| synchronized (mLock) { |
| if (mTranscodingClient == null) { |
| // TODO(hkuang): Handle the case if client is temporarily unavailable. |
| } |
| |
| if (!mTranscodingClient.submitRequest(requestParcel, jobParcel)) { |
| throw new UnsupportedOperationException("Failed to enqueue request"); |
| } |
| } |
| |
| // Wraps the TranscodingJobParcel into a TranscodingJob and returns it to client for |
| // tracking. |
| TranscodingJob job = new TranscodingJob(mTranscodingClient, jobParcel, |
| listenerExecutor, |
| listener); |
| |
| // Adds the new job into pending jobs. |
| mPendingTranscodingJobs.put(job.getJobId(), job); |
| return job; |
| } |
| } catch (RemoteException re) { |
| throw new UnsupportedOperationException( |
| "Failed to submit request to Transcoding service"); |
| } |
| } |
| } |