blob: 56423fda0404b4b8d3b417131bc31a8519e57580 [file] [log] [blame]
/*
* Copyright 2018 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.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.graphics.SurfaceTexture;
import android.media.SubtitleController.Anchor;
import android.media.SubtitleTrack.RenderingWidget;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.os.PowerManager;
import android.os.Process;
import android.os.SystemProperties;
import android.provider.Settings;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.widget.VideoView;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;
import dalvik.system.CloseGuard;
import libcore.io.IoBridge;
import libcore.io.Streams;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.UUID;
import java.util.Vector;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @hide
*/
public final class MediaPlayer2Impl extends MediaPlayer2 {
static {
System.loadLibrary("media2_jni");
native_init();
}
private final static String TAG = "MediaPlayer2Impl";
private long mNativeContext; // accessed by native methods
private long mNativeSurfaceTexture; // accessed by native methods
private int mListenerContext; // accessed by native methods
private SurfaceHolder mSurfaceHolder;
private EventHandler mEventHandler;
private PowerManager.WakeLock mWakeLock = null;
private boolean mScreenOnWhilePlaying;
private boolean mStayAwake;
private int mStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE;
private final CloseGuard mGuard = CloseGuard.get();
private final Object mSrcLock = new Object();
//--- guarded by |mSrcLock| start
private long mSrcIdGenerator = 0;
private DataSourceDesc mCurrentDSD;
private long mCurrentSrcId = mSrcIdGenerator++;
private List<DataSourceDesc> mNextDSDs;
private long mNextSrcId = mSrcIdGenerator++;
private int mNextSourceState = NEXT_SOURCE_STATE_INIT;
private boolean mNextSourcePlayPending = false;
//--- guarded by |mSrcLock| end
private AtomicInteger mBufferedPercentageCurrent = new AtomicInteger(0);
private AtomicInteger mBufferedPercentageNext = new AtomicInteger(0);
private volatile float mVolume = 1.0f;
// Modular DRM
private final Object mDrmLock = new Object();
//--- guarded by |mDrmLock| start
private UUID mDrmUUID;
private DrmInfoImpl mDrmInfoImpl;
private MediaDrm mDrmObj;
private byte[] mDrmSessionId;
private boolean mDrmInfoResolved;
private boolean mActiveDrmScheme;
private boolean mDrmConfigAllowed;
private boolean mDrmProvisioningInProgress;
private boolean mPrepareDrmInProgress;
private ProvisioningThread mDrmProvisioningThread;
//--- guarded by |mDrmLock| end
private HandlerThread mHandlerThread;
private final Handler mTaskHandler;
private final Object mTaskLock = new Object();
@GuardedBy("mTaskLock")
private final List<Task> mPendingTasks = new LinkedList<>();
@GuardedBy("mTaskLock")
private Task mCurrentTask;
/**
* Default constructor.
* <p>When done with the MediaPlayer2Impl, you should call {@link #close()},
* to free the resources. If not released, too many MediaPlayer2Impl instances may
* result in an exception.</p>
*/
public MediaPlayer2Impl() {
Looper looper;
if ((looper = Looper.myLooper()) != null) {
mEventHandler = new EventHandler(this, looper);
} else if ((looper = Looper.getMainLooper()) != null) {
mEventHandler = new EventHandler(this, looper);
} else {
mEventHandler = null;
}
mHandlerThread = new HandlerThread("MediaPlayer2TaskThread");
mHandlerThread.start();
looper = mHandlerThread.getLooper();
mTaskHandler = new Handler(looper);
mTimeProvider = new TimeProvider(this);
mOpenSubtitleSources = new Vector<InputStream>();
mGuard.open("close");
/* Native setup requires a weak reference to our object.
* It's easier to create it here than in C++.
*/
native_setup(new WeakReference<MediaPlayer2Impl>(this));
}
/**
* Releases the resources held by this {@code MediaPlayer2} object.
*
* It is considered good practice to call this method when you're
* done using the MediaPlayer2. In particular, whenever an Activity
* of an application is paused (its onPause() method is called),
* or stopped (its onStop() method is called), this method should be
* invoked to release the MediaPlayer2 object, unless the application
* has a special need to keep the object around. In addition to
* unnecessary resources (such as memory and instances of codecs)
* being held, failure to call this method immediately if a
* MediaPlayer2 object is no longer needed may also lead to
* continuous battery consumption for mobile devices, and playback
* failure for other applications if no multiple instances of the
* same codec are supported on a device. Even if multiple instances
* of the same codec are supported, some performance degradation
* may be expected when unnecessary multiple instances are used
* at the same time.
*
* {@code close()} may be safely called after a prior {@code close()}.
* This class implements the Java {@code AutoCloseable} interface and
* may be used with try-with-resources.
*/
@Override
public void close() {
synchronized (mGuard) {
release();
}
}
/**
* Starts or resumes playback. If playback had previously been paused,
* playback will continue from where it was paused. If playback had
* been stopped, or never started before, playback will start at the
* beginning.
*
* @throws IllegalStateException if it is called in an invalid state
*/
@Override
public void play() {
addTask(new Task(CALL_COMPLETED_PLAY, false) {
@Override
void process() {
stayAwake(true);
_start();
}
});
}
private native void _start() throws IllegalStateException;
/**
* Prepares the player for playback, asynchronously.
*
* After setting the datasource and the display surface, you need to either
* call prepare(). For streams, you should call prepare(),
* which returns immediately, rather than blocking until enough data has been
* buffered.
*
* @throws IllegalStateException if it is called in an invalid state
*/
@Override
public void prepare() {
addTask(new Task(CALL_COMPLETED_PREPARE, true) {
@Override
void process() {
_prepare();
}
});
}
public native void _prepare();
/**
* Pauses playback. Call play() to resume.
*
* @throws IllegalStateException if the internal player engine has not been
* initialized.
*/
@Override
public void pause() {
addTask(new Task(CALL_COMPLETED_PAUSE, false) {
@Override
void process() {
stayAwake(false);
_pause();
}
});
}
private native void _pause() throws IllegalStateException;
/**
* Tries to play next data source if applicable.
*
* @throws IllegalStateException if it is called in an invalid state
*/
@Override
public void skipToNext() {
addTask(new Task(CALL_COMPLETED_SKIP_TO_NEXT, false) {
@Override
void process() {
// TODO: switch to next data source and play
}
});
}
/**
* Gets the current playback position.
*
* @return the current position in milliseconds
*/
@Override
public native long getCurrentPosition();
/**
* Gets the duration of the file.
*
* @return the duration in milliseconds, if no duration is available
* (for example, if streaming live content), -1 is returned.
*/
@Override
public native long getDuration();
/**
* Gets the current buffered media source position received through progressive downloading.
* The received buffering percentage indicates how much of the content has been buffered
* or played. For example a buffering update of 80 percent when half the content
* has already been played indicates that the next 30 percent of the
* content to play has been buffered.
*
* @return the current buffered media source position in milliseconds
*/
@Override
public long getBufferedPosition() {
// Use cached buffered percent for now.
return getDuration() * mBufferedPercentageCurrent.get() / 100;
}
@Override
public @PlayerState int getPlayerState() {
int mediaplayer2State = getMediaPlayer2State();
int playerState;
switch (mediaplayer2State) {
case MEDIAPLAYER2_STATE_IDLE:
playerState = PLAYER_STATE_IDLE;
break;
case MEDIAPLAYER2_STATE_PREPARED:
case MEDIAPLAYER2_STATE_PAUSED:
playerState = PLAYER_STATE_PAUSED;
break;
case MEDIAPLAYER2_STATE_PLAYING:
playerState = PLAYER_STATE_PLAYING;
break;
case MEDIAPLAYER2_STATE_ERROR:
default:
playerState = PLAYER_STATE_ERROR;
break;
}
return playerState;
}
/**
* Gets the current buffering state of the player.
* During buffering, see {@link #getBufferedPosition()} for the quantifying the amount already
* buffered.
*/
@Override
public @BuffState int getBufferingState() {
// TODO: use cached state or call native function.
return BUFFERING_STATE_UNKNOWN;
}
/**
* Sets the audio attributes for this MediaPlayer2.
* See {@link AudioAttributes} for how to build and configure an instance of this class.
* You must call this method before {@link #prepare()} in order
* for the audio attributes to become effective thereafter.
* @param attributes a non-null set of audio attributes
* @throws IllegalArgumentException if the attributes are null or invalid.
*/
@Override
public void setAudioAttributes(@NonNull AudioAttributes attributes) {
addTask(new Task(CALL_COMPLETED_SET_AUDIO_ATTRIBUTES, false) {
@Override
void process() {
if (attributes == null) {
final String msg = "Cannot set AudioAttributes to null";
throw new IllegalArgumentException(msg);
}
Parcel pattributes = Parcel.obtain();
attributes.writeToParcel(pattributes, AudioAttributes.FLATTEN_TAGS);
setParameter(KEY_PARAMETER_AUDIO_ATTRIBUTES, pattributes);
pattributes.recycle();
}
});
}
@Override
public @NonNull AudioAttributes getAudioAttributes() {
Parcel pattributes = getParameter(KEY_PARAMETER_AUDIO_ATTRIBUTES);
AudioAttributes attributes = AudioAttributes.CREATOR.createFromParcel(pattributes);
pattributes.recycle();
return attributes;
}
/**
* Sets the data source as described by a DataSourceDesc.
*
* @param dsd the descriptor of data source you want to play
* @throws IllegalStateException if it is called in an invalid state
* @throws NullPointerException if dsd is null
*/
@Override
public void setDataSource(@NonNull DataSourceDesc dsd) {
addTask(new Task(CALL_COMPLETED_SET_DATA_SOURCE, false) {
@Override
void process() {
Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
// TODO: setDataSource could update exist data source
synchronized (mSrcLock) {
mCurrentDSD = dsd;
mCurrentSrcId = mSrcIdGenerator++;
try {
handleDataSource(true /* isCurrent */, dsd, mCurrentSrcId);
} catch (IOException e) {
}
}
}
});
}
/**
* Sets a single data source as described by a DataSourceDesc which will be played
* after current data source is finished.
*
* @param dsd the descriptor of data source you want to play after current one
* @throws IllegalStateException if it is called in an invalid state
* @throws NullPointerException if dsd is null
*/
@Override
public void setNextDataSource(@NonNull DataSourceDesc dsd) {
addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCE, false) {
@Override
void process() {
Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
synchronized (mSrcLock) {
mNextDSDs = new ArrayList<DataSourceDesc>(1);
mNextDSDs.add(dsd);
mNextSrcId = mSrcIdGenerator++;
mNextSourceState = NEXT_SOURCE_STATE_INIT;
mNextSourcePlayPending = false;
}
int state = getMediaPlayer2State();
if (state != MEDIAPLAYER2_STATE_IDLE) {
synchronized (mSrcLock) {
prepareNextDataSource_l();
}
}
}
});
}
/**
* Sets a list of data sources to be played sequentially after current data source is done.
*
* @param dsds the list of data sources you want to play after current one
* @throws IllegalStateException if it is called in an invalid state
* @throws IllegalArgumentException if dsds is null or empty, or contains null DataSourceDesc
*/
@Override
public void setNextDataSources(@NonNull List<DataSourceDesc> dsds) {
addTask(new Task(CALL_COMPLETED_SET_NEXT_DATA_SOURCES, false) {
@Override
void process() {
if (dsds == null || dsds.size() == 0) {
throw new IllegalArgumentException("data source list cannot be null or empty.");
}
for (DataSourceDesc dsd : dsds) {
if (dsd == null) {
throw new IllegalArgumentException(
"DataSourceDesc in the source list cannot be null.");
}
}
synchronized (mSrcLock) {
mNextDSDs = new ArrayList(dsds);
mNextSrcId = mSrcIdGenerator++;
mNextSourceState = NEXT_SOURCE_STATE_INIT;
mNextSourcePlayPending = false;
}
int state = getMediaPlayer2State();
if (state != MEDIAPLAYER2_STATE_IDLE) {
synchronized (mSrcLock) {
prepareNextDataSource_l();
}
}
}
});
}
@Override
public @NonNull DataSourceDesc getCurrentDataSource() {
synchronized (mSrcLock) {
return mCurrentDSD;
}
}
/**
* Configures the player to loop on the current data source.
* @param loop true if the current data source is meant to loop.
*/
@Override
public void loopCurrent(boolean loop) {
addTask(new Task(CALL_COMPLETED_LOOP_CURRENT, false) {
@Override
void process() {
// TODO: set the looping mode, send notification
setLooping(loop);
}
});
}
private native void setLooping(boolean looping);
/**
* Sets the playback speed.
* A value of 1.0f is the default playback value.
* A negative value indicates reverse playback, check {@link #isReversePlaybackSupported()}
* before using negative values.<br>
* After changing the playback speed, it is recommended to query the actual speed supported
* by the player, see {@link #getPlaybackSpeed()}.
* @param speed the desired playback speed
*/
@Override
public void setPlaybackSpeed(float speed) {
addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_SPEED, false) {
@Override
void process() {
_setPlaybackParams(getPlaybackParams().setSpeed(speed));
}
});
}
/**
* Returns the actual playback speed to be used by the player when playing.
* Note that it may differ from the speed set in {@link #setPlaybackSpeed(float)}.
* @return the actual playback speed
*/
@Override
public float getPlaybackSpeed() {
return getPlaybackParams().getSpeed();
}
/**
* Indicates whether reverse playback is supported.
* Reverse playback is indicated by negative playback speeds, see
* {@link #setPlaybackSpeed(float)}.
* @return true if reverse playback is supported.
*/
@Override
public boolean isReversePlaybackSupported() {
return false;
}
/**
* Sets the volume of the audio of the media to play, expressed as a linear multiplier
* on the audio samples.
* Note that this volume is specific to the player, and is separate from stream volume
* used across the platform.<br>
* A value of 0.0f indicates muting, a value of 1.0f is the nominal unattenuated and unamplified
* gain. See {@link #getMaxPlayerVolume()} for the volume range supported by this player.
* @param volume a value between 0.0f and {@link #getMaxPlayerVolume()}.
*/
@Override
public void setPlayerVolume(float volume) {
addTask(new Task(CALL_COMPLETED_SET_PLAYER_VOLUME, false) {
@Override
void process() {
mVolume = volume;
_setVolume(volume, volume);
}
});
}
private native void _setVolume(float leftVolume, float rightVolume);
/**
* Returns the current volume of this player to this player.
* Note that it does not take into account the associated stream volume.
* @return the player volume.
*/
@Override
public float getPlayerVolume() {
return mVolume;
}
/**
* @return the maximum volume that can be used in {@link #setPlayerVolume(float)}.
*/
@Override
public float getMaxPlayerVolume() {
return 1.0f;
}
/**
* Adds a callback to be notified of events for this player.
* @param e the {@link Executor} to be used for the events.
* @param cb the callback to receive the events.
*/
@Override
public void registerPlayerEventCallback(@NonNull Executor e,
@NonNull PlayerEventCallback cb) {
}
/**
* Removes a previously registered callback for player events
* @param cb the callback to remove
*/
@Override
public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb) {
}
private static final int NEXT_SOURCE_STATE_ERROR = -1;
private static final int NEXT_SOURCE_STATE_INIT = 0;
private static final int NEXT_SOURCE_STATE_PREPARING = 1;
private static final int NEXT_SOURCE_STATE_PREPARED = 2;
/*
* Update the MediaPlayer2Impl SurfaceTexture.
* Call after setting a new display surface.
*/
private native void _setVideoSurface(Surface surface);
/* Do not change these values (starting with INVOKE_ID) without updating
* their counterparts in include/media/mediaplayer2.h!
*/
private static final int INVOKE_ID_GET_TRACK_INFO = 1;
private static final int INVOKE_ID_ADD_EXTERNAL_SOURCE = 2;
private static final int INVOKE_ID_ADD_EXTERNAL_SOURCE_FD = 3;
private static final int INVOKE_ID_SELECT_TRACK = 4;
private static final int INVOKE_ID_DESELECT_TRACK = 5;
private static final int INVOKE_ID_SET_VIDEO_SCALE_MODE = 6;
private static final int INVOKE_ID_GET_SELECTED_TRACK = 7;
/**
* Create a request parcel which can be routed to the native media
* player using {@link #invoke(Parcel, Parcel)}. The Parcel
* returned has the proper InterfaceToken set. The caller should
* not overwrite that token, i.e it can only append data to the
* Parcel.
*
* @return A parcel suitable to hold a request for the native
* player.
* {@hide}
*/
@Override
public Parcel newRequest() {
Parcel parcel = Parcel.obtain();
return parcel;
}
/**
* Invoke a generic method on the native player using opaque
* parcels for the request and reply. Both payloads' format is a
* convention between the java caller and the native player.
* Must be called after setDataSource or setPlaylist to make sure a native player
* exists. On failure, a RuntimeException is thrown.
*
* @param request Parcel with the data for the extension. The
* caller must use {@link #newRequest()} to get one.
*
* @param reply Output parcel with the data returned by the
* native player.
* {@hide}
*/
@Override
public void invoke(Parcel request, Parcel reply) {
int retcode = native_invoke(request, reply);
reply.setDataPosition(0);
if (retcode != 0) {
throw new RuntimeException("failure code: " + retcode);
}
}
@Override
public void notifyWhenCommandLabelReached(Object label) {
addTask(new Task(CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED, false) {
@Override
void process() {
synchronized (mEventCbLock) {
for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
cb.first.execute(() -> cb.second.onCommandLabelReached(
MediaPlayer2Impl.this, label));
}
}
}
});
}
/**
* Sets the {@link SurfaceHolder} to use for displaying the video
* portion of the media.
*
* Either a surface holder or surface must be set if a display or video sink
* is needed. Not calling this method or {@link #setSurface(Surface)}
* when playing back a video will result in only the audio track being played.
* A null surface holder or surface will result in only the audio track being
* played.
*
* @param sh the SurfaceHolder to use for video display
* @throws IllegalStateException if the internal player engine has not been
* initialized or has been released.
* @hide
*/
@Override
public void setDisplay(SurfaceHolder sh) {
mSurfaceHolder = sh;
Surface surface;
if (sh != null) {
surface = sh.getSurface();
} else {
surface = null;
}
_setVideoSurface(surface);
updateSurfaceScreenOn();
}
/**
* Sets the {@link Surface} to be used as the sink for the video portion of
* the media. This is similar to {@link #setDisplay(SurfaceHolder)}, but
* does not support {@link #setScreenOnWhilePlaying(boolean)}. Setting a
* Surface will un-set any Surface or SurfaceHolder that was previously set.
* A null surface will result in only the audio track being played.
*
* If the Surface sends frames to a {@link SurfaceTexture}, the timestamps
* returned from {@link SurfaceTexture#getTimestamp()} will have an
* unspecified zero point. These timestamps cannot be directly compared
* between different media sources, different instances of the same media
* source, or multiple runs of the same program. The timestamp is normally
* monotonically increasing and is unaffected by time-of-day adjustments,
* but it is reset when the position is set.
*
* @param surface The {@link Surface} to be used for the video portion of
* the media.
* @throws IllegalStateException if the internal player engine has not been
* initialized or has been released.
*/
@Override
public void setSurface(Surface surface) {
addTask(new Task(CALL_COMPLETED_SET_SURFACE, false) {
@Override
void process() {
if (mScreenOnWhilePlaying && surface != null) {
Log.w(TAG, "setScreenOnWhilePlaying(true) is ineffective for Surface");
}
mSurfaceHolder = null;
_setVideoSurface(surface);
updateSurfaceScreenOn();
}
});
}
/**
* Sets video scaling mode. To make the target video scaling mode
* effective during playback, this method must be called after
* data source is set. If not called, the default video
* scaling mode is {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT}.
*
* <p> The supported video scaling modes are:
* <ul>
* <li> {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT}
* <li> {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}
* </ul>
*
* @param mode target video scaling mode. Must be one of the supported
* video scaling modes; otherwise, IllegalArgumentException will be thrown.
*
* @see MediaPlayer2#VIDEO_SCALING_MODE_SCALE_TO_FIT
* @see MediaPlayer2#VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
* @hide
*/
@Override
public void setVideoScalingMode(int mode) {
addTask(new Task(CALL_COMPLETED_SET_VIDEO_SCALING_MODE, false) {
@Override
void process() {
if (!isVideoScalingModeSupported(mode)) {
final String msg = "Scaling mode " + mode + " is not supported";
throw new IllegalArgumentException(msg);
}
Parcel request = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
request.writeInt(INVOKE_ID_SET_VIDEO_SCALE_MODE);
request.writeInt(mode);
invoke(request, reply);
} finally {
request.recycle();
reply.recycle();
}
}
});
}
/**
* Discards all pending commands.
*/
@Override
public void clearPendingCommands() {
}
private void addTask(Task task) {
synchronized (mTaskLock) {
mPendingTasks.add(task);
processPendingTask_l();
}
}
@GuardedBy("mTaskLock")
private void processPendingTask_l() {
if (mCurrentTask != null) {
return;
}
if (!mPendingTasks.isEmpty()) {
Task task = mPendingTasks.remove(0);
mCurrentTask = task;
mTaskHandler.post(task);
}
}
private void handleDataSource(boolean isCurrent, @NonNull DataSourceDesc dsd, long srcId)
throws IOException {
Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
switch (dsd.getType()) {
case DataSourceDesc.TYPE_CALLBACK:
handleDataSource(isCurrent,
srcId,
dsd.getMedia2DataSource());
break;
case DataSourceDesc.TYPE_FD:
handleDataSource(isCurrent,
srcId,
dsd.getFileDescriptor(),
dsd.getFileDescriptorOffset(),
dsd.getFileDescriptorLength());
break;
case DataSourceDesc.TYPE_URI:
handleDataSource(isCurrent,
srcId,
dsd.getUriContext(),
dsd.getUri(),
dsd.getUriHeaders(),
dsd.getUriCookies());
break;
default:
break;
}
}
/**
* To provide cookies for the subsequent HTTP requests, you can install your own default cookie
* handler and use other variants of setDataSource APIs instead. Alternatively, you can use
* this API to pass the cookies as a list of HttpCookie. If the app has not installed
* a CookieHandler already, this API creates a CookieManager and populates its CookieStore with
* the provided cookies. If the app has installed its own handler already, this API requires the
* handler to be of CookieManager type such that the API can update the manager’s CookieStore.
*
* <p><strong>Note</strong> that the cross domain redirection is allowed by default,
* but that can be changed with key/value pairs through the headers parameter with
* "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value to
* disallow or allow cross domain redirection.
*
* @throws IllegalArgumentException if cookies are provided and the installed handler is not
* a CookieManager
* @throws IllegalStateException if it is called in an invalid state
* @throws NullPointerException if context or uri is null
* @throws IOException if uri has a file scheme and an I/O error occurs
*/
private void handleDataSource(
boolean isCurrent, long srcId,
@NonNull Context context, @NonNull Uri uri,
@Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies)
throws IOException {
// The context and URI usually belong to the calling user. Get a resolver for that user
// and strip out the userId from the URI if present.
final ContentResolver resolver = context.getContentResolver();
final String scheme = uri.getScheme();
final String authority = ContentProvider.getAuthorityWithoutUserId(uri.getAuthority());
if (ContentResolver.SCHEME_FILE.equals(scheme)) {
handleDataSource(isCurrent, srcId, uri.getPath(), null, null);
return;
}
if (ContentResolver.SCHEME_CONTENT.equals(scheme)
&& Settings.AUTHORITY.equals(authority)) {
// Try cached ringtone first since the actual provider may not be
// encryption aware, or it may be stored on CE media storage
final int type = RingtoneManager.getDefaultType(uri);
final Uri cacheUri = RingtoneManager.getCacheForType(type, context.getUserId());
final Uri actualUri = RingtoneManager.getActualDefaultRingtoneUri(context, type);
if (attemptDataSource(isCurrent, srcId, resolver, cacheUri)) {
return;
}
if (attemptDataSource(isCurrent, srcId, resolver, actualUri)) {
return;
}
handleDataSource(isCurrent, srcId, uri.toString(), headers, cookies);
} else {
// Try requested Uri locally first, or fallback to media server
if (attemptDataSource(isCurrent, srcId, resolver, uri)) {
return;
}
handleDataSource(isCurrent, srcId, uri.toString(), headers, cookies);
}
}
private boolean attemptDataSource(
boolean isCurrent, long srcId, ContentResolver resolver, Uri uri) {
try (AssetFileDescriptor afd = resolver.openAssetFileDescriptor(uri, "r")) {
if (afd.getDeclaredLength() < 0) {
handleDataSource(isCurrent,
srcId,
afd.getFileDescriptor(),
0,
DataSourceDesc.LONG_MAX);
} else {
handleDataSource(isCurrent,
srcId,
afd.getFileDescriptor(),
afd.getStartOffset(),
afd.getDeclaredLength());
}
return true;
} catch (NullPointerException | SecurityException | IOException ex) {
Log.w(TAG, "Couldn't open " + uri + ": " + ex);
return false;
}
}
private void handleDataSource(
boolean isCurrent, long srcId,
String path, Map<String, String> headers, List<HttpCookie> cookies)
throws IOException {
String[] keys = null;
String[] values = null;
if (headers != null) {
keys = new String[headers.size()];
values = new String[headers.size()];
int i = 0;
for (Map.Entry<String, String> entry: headers.entrySet()) {
keys[i] = entry.getKey();
values[i] = entry.getValue();
++i;
}
}
handleDataSource(isCurrent, srcId, path, keys, values, cookies);
}
private void handleDataSource(boolean isCurrent, long srcId,
String path, String[] keys, String[] values, List<HttpCookie> cookies)
throws IOException {
final Uri uri = Uri.parse(path);
final String scheme = uri.getScheme();
if ("file".equals(scheme)) {
path = uri.getPath();
} else if (scheme != null) {
// handle non-file sources
nativeHandleDataSourceUrl(
isCurrent,
srcId,
Media2HTTPService.createHTTPService(path, cookies),
path,
keys,
values);
return;
}
final File file = new File(path);
if (file.exists()) {
FileInputStream is = new FileInputStream(file);
FileDescriptor fd = is.getFD();
handleDataSource(isCurrent, srcId, fd, 0, DataSourceDesc.LONG_MAX);
is.close();
} else {
throw new IOException("handleDataSource failed.");
}
}
private native void nativeHandleDataSourceUrl(
boolean isCurrent, long srcId,
Media2HTTPService httpService, String path, String[] keys, String[] values)
throws IOException;
/**
* Sets the data source (FileDescriptor) to use. The FileDescriptor must be
* seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility
* to close the file descriptor. It is safe to do so as soon as this call returns.
*
* @throws IllegalStateException if it is called in an invalid state
* @throws IllegalArgumentException if fd is not a valid FileDescriptor
* @throws IOException if fd can not be read
*/
private void handleDataSource(
boolean isCurrent, long srcId,
FileDescriptor fd, long offset, long length) throws IOException {
nativeHandleDataSourceFD(isCurrent, srcId, fd, offset, length);
}
private native void nativeHandleDataSourceFD(boolean isCurrent, long srcId,
FileDescriptor fd, long offset, long length) throws IOException;
/**
* @throws IllegalStateException if it is called in an invalid state
* @throws IllegalArgumentException if dataSource is not a valid Media2DataSource
*/
private void handleDataSource(boolean isCurrent, long srcId, Media2DataSource dataSource) {
nativeHandleDataSourceCallback(isCurrent, srcId, dataSource);
}
private native void nativeHandleDataSourceCallback(
boolean isCurrent, long srcId, Media2DataSource dataSource);
// This function shall be called with |mSrcLock| acquired.
private void prepareNextDataSource_l() {
if (mNextDSDs == null || mNextDSDs.isEmpty()
|| mNextSourceState != NEXT_SOURCE_STATE_INIT) {
// There is no next source or it's in preparing or prepared state.
return;
}
try {
mNextSourceState = NEXT_SOURCE_STATE_PREPARING;
handleDataSource(false /* isCurrent */, mNextDSDs.get(0), mNextSrcId);
} catch (Exception e) {
Message msg2 = mEventHandler.obtainMessage(
MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, null);
final long nextSrcId = mNextSrcId;
mEventHandler.post(new Runnable() {
@Override
public void run() {
mEventHandler.handleMessage(msg2, nextSrcId);
}
});
}
}
// This function shall be called with |mSrcLock| acquired.
private void playNextDataSource_l() {
if (mNextDSDs == null || mNextDSDs.isEmpty()) {
return;
}
if (mNextSourceState == NEXT_SOURCE_STATE_PREPARED) {
// Switch to next source only when it's in prepared state.
mCurrentDSD = mNextDSDs.get(0);
mCurrentSrcId = mNextSrcId;
mBufferedPercentageCurrent.set(mBufferedPercentageNext.get());
mNextDSDs.remove(0);
mNextSrcId = mSrcIdGenerator++; // make it different from mCurrentSrcId
mBufferedPercentageNext.set(0);
mNextSourceState = NEXT_SOURCE_STATE_INIT;
mNextSourcePlayPending = false;
long srcId = mCurrentSrcId;
try {
nativePlayNextDataSource(srcId);
} catch (Exception e) {
Message msg2 = mEventHandler.obtainMessage(
MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, null);
mEventHandler.post(new Runnable() {
@Override
public void run() {
mEventHandler.handleMessage(msg2, srcId);
}
});
}
// Wait for MEDIA2_INFO_STARTED_AS_NEXT to prepare next source.
} else {
if (mNextSourceState == NEXT_SOURCE_STATE_INIT) {
prepareNextDataSource_l();
}
mNextSourcePlayPending = true;
}
}
private native void nativePlayNextDataSource(long srcId);
private int getAudioStreamType() {
if (mStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
mStreamType = _getAudioStreamType();
}
return mStreamType;
}
private native int _getAudioStreamType() throws IllegalStateException;
/**
* Stops playback after playback has been started or paused.
*
* @throws IllegalStateException if the internal player engine has not been
* initialized.
* #hide
*/
@Override
public void stop() {
stayAwake(false);
_stop();
}
private native void _stop() throws IllegalStateException;
//--------------------------------------------------------------------------
// Explicit Routing
//--------------------
private AudioDeviceInfo mPreferredDevice = null;
/**
* Specifies an audio device (via an {@link AudioDeviceInfo} object) to route
* the output from this MediaPlayer2.
* @param deviceInfo The {@link AudioDeviceInfo} specifying the audio sink or source.
* If deviceInfo is null, default routing is restored.
* @return true if succesful, false if the specified {@link AudioDeviceInfo} is non-null and
* does not correspond to a valid audio device.
*/
@Override
public boolean setPreferredDevice(AudioDeviceInfo deviceInfo) {
if (deviceInfo != null && !deviceInfo.isSink()) {
return false;
}
int preferredDeviceId = deviceInfo != null ? deviceInfo.getId() : 0;
boolean status = native_setOutputDevice(preferredDeviceId);
if (status == true) {
synchronized (this) {
mPreferredDevice = deviceInfo;
}
}
return status;
}
/**
* Returns the selected output specified by {@link #setPreferredDevice}. Note that this
* is not guaranteed to correspond to the actual device being used for playback.
*/
@Override
public AudioDeviceInfo getPreferredDevice() {
synchronized (this) {
return mPreferredDevice;
}
}
/**
* Returns an {@link AudioDeviceInfo} identifying the current routing of this MediaPlayer2
* Note: The query is only valid if the MediaPlayer2 is currently playing.
* If the player is not playing, the returned device can be null or correspond to previously
* selected device when the player was last active.
*/
@Override
public AudioDeviceInfo getRoutedDevice() {
int deviceId = native_getRoutedDeviceId();
if (deviceId == 0) {
return null;
}
AudioDeviceInfo[] devices =
AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS);
for (int i = 0; i < devices.length; i++) {
if (devices[i].getId() == deviceId) {
return devices[i];
}
}
return null;
}
/*
* Call BEFORE adding a routing callback handler or AFTER removing a routing callback handler.
*/
@GuardedBy("mRoutingChangeListeners")
private void enableNativeRoutingCallbacksLocked(boolean enabled) {
if (mRoutingChangeListeners.size() == 0) {
native_enableDeviceCallback(enabled);
}
}
/**
* The list of AudioRouting.OnRoutingChangedListener interfaces added (with
* {@link #addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, Handler)}
* by an app to receive (re)routing notifications.
*/
@GuardedBy("mRoutingChangeListeners")
private ArrayMap<AudioRouting.OnRoutingChangedListener,
NativeRoutingEventHandlerDelegate> mRoutingChangeListeners = new ArrayMap<>();
/**
* Adds an {@link AudioRouting.OnRoutingChangedListener} to receive notifications of routing
* changes on this MediaPlayer2.
* @param listener The {@link AudioRouting.OnRoutingChangedListener} interface to receive
* notifications of rerouting events.
* @param handler Specifies the {@link Handler} object for the thread on which to execute
* the callback. If <code>null</code>, the handler on the main looper will be used.
*/
@Override
public void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener,
Handler handler) {
synchronized (mRoutingChangeListeners) {
if (listener != null && !mRoutingChangeListeners.containsKey(listener)) {
enableNativeRoutingCallbacksLocked(true);
mRoutingChangeListeners.put(
listener, new NativeRoutingEventHandlerDelegate(this, listener,
handler != null ? handler : mEventHandler));
}
}
}
/**
* Removes an {@link AudioRouting.OnRoutingChangedListener} which has been previously added
* to receive rerouting notifications.
* @param listener The previously added {@link AudioRouting.OnRoutingChangedListener} interface
* to remove.
*/
@Override
public void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener) {
synchronized (mRoutingChangeListeners) {
if (mRoutingChangeListeners.containsKey(listener)) {
mRoutingChangeListeners.remove(listener);
enableNativeRoutingCallbacksLocked(false);
}
}
}
private native final boolean native_setOutputDevice(int deviceId);
private native final int native_getRoutedDeviceId();
private native final void native_enableDeviceCallback(boolean enabled);
/**
* Set the low-level power management behavior for this MediaPlayer2. This
* can be used when the MediaPlayer2 is not playing through a SurfaceHolder
* set with {@link #setDisplay(SurfaceHolder)} and thus can use the
* high-level {@link #setScreenOnWhilePlaying(boolean)} feature.
*
* <p>This function has the MediaPlayer2 access the low-level power manager
* service to control the device's power usage while playing is occurring.
* The parameter is a combination of {@link android.os.PowerManager} wake flags.
* Use of this method requires {@link android.Manifest.permission#WAKE_LOCK}
* permission.
* By default, no attempt is made to keep the device awake during playback.
*
* @param context the Context to use
* @param mode the power/wake mode to set
* @see android.os.PowerManager
* @hide
*/
@Override
public void setWakeMode(Context context, int mode) {
boolean washeld = false;
/* Disable persistant wakelocks in media player based on property */
if (SystemProperties.getBoolean("audio.offload.ignore_setawake", false) == true) {
Log.w(TAG, "IGNORING setWakeMode " + mode);
return;
}
if (mWakeLock != null) {
if (mWakeLock.isHeld()) {
washeld = true;
mWakeLock.release();
}
mWakeLock = null;
}
PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(mode|PowerManager.ON_AFTER_RELEASE, MediaPlayer2Impl.class.getName());
mWakeLock.setReferenceCounted(false);
if (washeld) {
mWakeLock.acquire();
}
}
/**
* Control whether we should use the attached SurfaceHolder to keep the
* screen on while video playback is occurring. This is the preferred
* method over {@link #setWakeMode} where possible, since it doesn't
* require that the application have permission for low-level wake lock
* access.
*
* @param screenOn Supply true to keep the screen on, false to allow it
* to turn off.
* @hide
*/
@Override
public void setScreenOnWhilePlaying(boolean screenOn) {
if (mScreenOnWhilePlaying != screenOn) {
if (screenOn && mSurfaceHolder == null) {
Log.w(TAG, "setScreenOnWhilePlaying(true) is ineffective without a SurfaceHolder");
}
mScreenOnWhilePlaying = screenOn;
updateSurfaceScreenOn();
}
}
private void stayAwake(boolean awake) {
if (mWakeLock != null) {
if (awake && !mWakeLock.isHeld()) {
mWakeLock.acquire();
} else if (!awake && mWakeLock.isHeld()) {
mWakeLock.release();
}
}
mStayAwake = awake;
updateSurfaceScreenOn();
}
private void updateSurfaceScreenOn() {
if (mSurfaceHolder != null) {
mSurfaceHolder.setKeepScreenOn(mScreenOnWhilePlaying && mStayAwake);
}
}
/**
* Returns the width of the video.
*
* @return the width of the video, or 0 if there is no video,
* no display surface was set, or the width has not been determined
* yet. The {@code MediaPlayer2EventCallback} can be registered via
* {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
* notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the width
* is available.
*/
@Override
public native int getVideoWidth();
/**
* Returns the height of the video.
*
* @return the height of the video, or 0 if there is no video,
* no display surface was set, or the height has not been determined
* yet. The {@code MediaPlayer2EventCallback} can be registered via
* {@link #setMediaPlayer2EventCallback(Executor, MediaPlayer2EventCallback)} to provide a
* notification {@code MediaPlayer2EventCallback.onVideoSizeChanged} when the height
* is available.
*/
@Override
public native int getVideoHeight();
/**
* Return Metrics data about the current player.
*
* @return a {@link PersistableBundle} containing the set of attributes and values
* available for the media being handled by this instance of MediaPlayer2
* The attributes are descibed in {@link MetricsConstants}.
*
* Additional vendor-specific fields may also be present in
* the return value.
*/
@Override
public PersistableBundle getMetrics() {
PersistableBundle bundle = native_getMetrics();
return bundle;
}
private native PersistableBundle native_getMetrics();
/**
* Checks whether the MediaPlayer2 is playing.
*
* @return true if currently playing, false otherwise
* @throws IllegalStateException if the internal player engine has not been
* initialized or has been released.
* @hide
*/
@Override
public native boolean isPlaying();
@Override
public @MediaPlayer2State int getMediaPlayer2State() {
return native_getMediaPlayer2State();
}
private native int native_getMediaPlayer2State();
/**
* Gets the current buffering management params used by the source component.
* Calling it only after {@code setDataSource} has been called.
* Each type of data source might have different set of default params.
*
* @return the current buffering management params used by the source component.
* @throws IllegalStateException if the internal player engine has not been
* initialized, or {@code setDataSource} has not been called.
* @hide
*/
@Override
@NonNull
public native BufferingParams getBufferingParams();
/**
* Sets buffering management params.
* The object sets its internal BufferingParams to the input, except that the input is
* invalid or not supported.
* Call it only after {@code setDataSource} has been called.
* The input is a hint to MediaPlayer2.
*
* @param params the buffering management params.
*
* @throws IllegalStateException if the internal player engine has not been
* initialized or has been released, or {@code setDataSource} has not been called.
* @throws IllegalArgumentException if params is invalid or not supported.
* @hide
*/
@Override
public void setBufferingParams(@NonNull BufferingParams params) {
addTask(new Task(CALL_COMPLETED_SET_BUFFERING_PARAMS, false) {
@Override
void process() {
Preconditions.checkNotNull(params, "the BufferingParams cannot be null");
_setBufferingParams(params);
}
});
}
private native void _setBufferingParams(@NonNull BufferingParams params);
/**
* Sets playback rate and audio mode.
*
* @param rate the ratio between desired playback rate and normal one.
* @param audioMode audio playback mode. Must be one of the supported
* audio modes.
*
* @throws IllegalStateException if the internal player engine has not been
* initialized.
* @throws IllegalArgumentException if audioMode is not supported.
*
* @hide
*/
@Override
@NonNull
public PlaybackParams easyPlaybackParams(float rate, @PlaybackRateAudioMode int audioMode) {
PlaybackParams params = new PlaybackParams();
params.allowDefaults();
switch (audioMode) {
case PLAYBACK_RATE_AUDIO_MODE_DEFAULT:
params.setSpeed(rate).setPitch(1.0f);
break;
case PLAYBACK_RATE_AUDIO_MODE_STRETCH:
params.setSpeed(rate).setPitch(1.0f)
.setAudioFallbackMode(params.AUDIO_FALLBACK_MODE_FAIL);
break;
case PLAYBACK_RATE_AUDIO_MODE_RESAMPLE:
params.setSpeed(rate).setPitch(rate);
break;
default:
final String msg = "Audio playback mode " + audioMode + " is not supported";
throw new IllegalArgumentException(msg);
}
return params;
}
/**
* Sets playback rate using {@link PlaybackParams}. The object sets its internal
* PlaybackParams to the input, except that the object remembers previous speed
* when input speed is zero. This allows the object to resume at previous speed
* when play() is called. Calling it before the object is prepared does not change
* the object state. After the object is prepared, calling it with zero speed is
* equivalent to calling pause(). After the object is prepared, calling it with
* non-zero speed is equivalent to calling play().
*
* @param params the playback params.
*
* @throws IllegalStateException if the internal player engine has not been
* initialized or has been released.
* @throws IllegalArgumentException if params is not supported.
*/
@Override
public void setPlaybackParams(@NonNull PlaybackParams params) {
addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_PARAMS, false) {
@Override
void process() {
Preconditions.checkNotNull(params, "the PlaybackParams cannot be null");
_setPlaybackParams(params);
}
});
}
private native void _setPlaybackParams(@NonNull PlaybackParams params);
/**
* Gets the playback params, containing the current playback rate.
*
* @return the playback params.
* @throws IllegalStateException if the internal player engine has not been
* initialized.
*/
@Override
@NonNull
public native PlaybackParams getPlaybackParams();
/**
* Sets A/V sync mode.
*
* @param params the A/V sync params to apply
*
* @throws IllegalStateException if the internal player engine has not been
* initialized.
* @throws IllegalArgumentException if params are not supported.
*/
@Override
public void setSyncParams(@NonNull SyncParams params) {
addTask(new Task(CALL_COMPLETED_SET_SYNC_PARAMS, false) {
@Override
void process() {
Preconditions.checkNotNull(params, "the SyncParams cannot be null");
_setSyncParams(params);
}
});
}
private native void _setSyncParams(@NonNull SyncParams params);
/**
* Gets the A/V sync mode.
*
* @return the A/V sync params
*
* @throws IllegalStateException if the internal player engine has not been
* initialized.
*/
@Override
@NonNull
public native SyncParams getSyncParams();
/**
* Moves the media to specified time position by considering the given mode.
* <p>
* When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user.
* There is at most one active seekTo processed at any time. If there is a to-be-completed
* seekTo, new seekTo requests will be queued in such a way that only the last request
* is kept. When current seekTo is completed, the queued request will be processed if
* that request is different from just-finished seekTo operation, i.e., the requested
* position or mode is different.
*
* @param msec the offset in milliseconds from the start to seek to.
* When seeking to the given time position, there is no guarantee that the data source
* has a frame located at the position. When this happens, a frame nearby will be rendered.
* If msec is negative, time position zero will be used.
* If msec is larger than duration, duration will be used.
* @param mode the mode indicating where exactly to seek to.
* Use {@link #SEEK_PREVIOUS_SYNC} if one wants to seek to a sync frame
* that has a timestamp earlier than or the same as msec. Use
* {@link #SEEK_NEXT_SYNC} if one wants to seek to a sync frame
* that has a timestamp later than or the same as msec. Use
* {@link #SEEK_CLOSEST_SYNC} if one wants to seek to a sync frame
* that has a timestamp closest to or the same as msec. Use
* {@link #SEEK_CLOSEST} if one wants to seek to a frame that may
* or may not be a sync frame but is closest to or the same as msec.
* {@link #SEEK_CLOSEST} often has larger performance overhead compared
* to the other options if there is no sync frame located at msec.
* @throws IllegalStateException if the internal player engine has not been
* initialized
* @throws IllegalArgumentException if the mode is invalid.
*/
@Override
public void seekTo(final long msec, @SeekMode int mode) {
addTask(new Task(CALL_COMPLETED_SEEK_TO, true) {
@Override
void process() {
if (mode < SEEK_PREVIOUS_SYNC || mode > SEEK_CLOSEST) {
final String msg = "Illegal seek mode: " + mode;
throw new IllegalArgumentException(msg);
}
// TODO: pass long to native, instead of truncating here.
long posMs = msec;
if (posMs > Integer.MAX_VALUE) {
Log.w(TAG, "seekTo offset " + posMs + " is too large, cap to "
+ Integer.MAX_VALUE);
posMs = Integer.MAX_VALUE;
} else if (posMs < Integer.MIN_VALUE) {
Log.w(TAG, "seekTo offset " + posMs + " is too small, cap to "
+ Integer.MIN_VALUE);
posMs = Integer.MIN_VALUE;
}
_seekTo(posMs, mode);
}
});
}
private native final void _seekTo(long msec, int mode);
/**
* Get current playback position as a {@link MediaTimestamp}.
* <p>
* The MediaTimestamp represents how the media time correlates to the system time in
* a linear fashion using an anchor and a clock rate. During regular playback, the media
* time moves fairly constantly (though the anchor frame may be rebased to a current
* system time, the linear correlation stays steady). Therefore, this method does not
* need to be called often.
* <p>
* To help users get current playback position, this method always anchors the timestamp
* to the current {@link System#nanoTime system time}, so
* {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position.
*
* @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp
* is available, e.g. because the media player has not been initialized.
*
* @see MediaTimestamp
*/
@Override
@Nullable
public MediaTimestamp getTimestamp()
{
try {
// TODO: get the timestamp from native side
return new MediaTimestamp(
getCurrentPosition() * 1000L,
System.nanoTime(),
isPlaying() ? getPlaybackParams().getSpeed() : 0.f);
} catch (IllegalStateException e) {
return null;
}
}
/**
* Gets the media metadata.
*
* @param update_only controls whether the full set of available
* metadata is returned or just the set that changed since the
* last call. See {@see #METADATA_UPDATE_ONLY} and {@see
* #METADATA_ALL}.
*
* @param apply_filter if true only metadata that matches the
* filter is returned. See {@see #APPLY_METADATA_FILTER} and {@see
* #BYPASS_METADATA_FILTER}.
*
* @return The metadata, possibly empty. null if an error occured.
// FIXME: unhide.
* {@hide}
*/
@Override
public Metadata getMetadata(final boolean update_only,
final boolean apply_filter) {
Parcel reply = Parcel.obtain();
Metadata data = new Metadata();
if (!native_getMetadata(update_only, apply_filter, reply)) {
reply.recycle();
return null;
}
// Metadata takes over the parcel, don't recycle it unless
// there is an error.
if (!data.parse(reply)) {
reply.recycle();
return null;
}
return data;
}
/**
* Set a filter for the metadata update notification and update
* retrieval. The caller provides 2 set of metadata keys, allowed
* and blocked. The blocked set always takes precedence over the
* allowed one.
* Metadata.MATCH_ALL and Metadata.MATCH_NONE are 2 sets available as
* shorthands to allow/block all or no metadata.
*
* By default, there is no filter set.
*
* @param allow Is the set of metadata the client is interested
* in receiving new notifications for.
* @param block Is the set of metadata the client is not interested
* in receiving new notifications for.
* @return The call status code.
*
// FIXME: unhide.
* {@hide}
*/
@Override
public int setMetadataFilter(Set<Integer> allow, Set<Integer> block) {
// Do our serialization manually instead of calling
// Parcel.writeArray since the sets are made of the same type
// we avoid paying the price of calling writeValue (used by
// writeArray) which burns an extra int per element to encode
// the type.
Parcel request = newRequest();
// The parcel starts already with an interface token. There
// are 2 filters. Each one starts with a 4bytes number to
// store the len followed by a number of int (4 bytes as well)
// representing the metadata type.
int capacity = request.dataSize() + 4 * (1 + allow.size() + 1 + block.size());
if (request.dataCapacity() < capacity) {
request.setDataCapacity(capacity);
}
request.writeInt(allow.size());
for(Integer t: allow) {
request.writeInt(t);
}
request.writeInt(block.size());
for(Integer t: block) {
request.writeInt(t);
}
return native_setMetadataFilter(request);
}
/**
* Resets the MediaPlayer2 to its uninitialized state. After calling
* this method, you will have to initialize it again by setting the
* data source and calling prepare().
*/
@Override
public void reset() {
mSelectedSubtitleTrackIndex = -1;
synchronized(mOpenSubtitleSources) {
for (final InputStream is: mOpenSubtitleSources) {
try {
is.close();
} catch (IOException e) {
}
}
mOpenSubtitleSources.clear();
}
if (mSubtitleController != null) {
mSubtitleController.reset();
}
if (mTimeProvider != null) {
mTimeProvider.close();
mTimeProvider = null;
}
synchronized (mEventCbLock) {
mEventCallbackRecords.clear();
}
synchronized (mDrmEventCbLock) {
mDrmEventCallbackRecords.clear();
}
stayAwake(false);
_reset();
// make sure none of the listeners get called anymore
if (mEventHandler != null) {
mEventHandler.removeCallbacksAndMessages(null);
}
synchronized (mIndexTrackPairs) {
mIndexTrackPairs.clear();
mInbandTrackIndices.clear();
};
resetDrmState();
}
private native void _reset();
/**
* Set up a timer for {@link #TimeProvider}. {@link #TimeProvider} will be
* notified when the presentation time reaches (becomes greater than or equal to)
* the value specified.
*
* @param mediaTimeUs presentation time to get timed event callback at
* @hide
*/
@Override
public void notifyAt(long mediaTimeUs) {
_notifyAt(mediaTimeUs);
}
private native void _notifyAt(long mediaTimeUs);
// Keep KEY_PARAMETER_* in sync with include/media/mediaplayer2.h
private final static int KEY_PARAMETER_AUDIO_ATTRIBUTES = 1400;
/**
* Sets the parameter indicated by key.
* @param key key indicates the parameter to be set.
* @param value value of the parameter to be set.
* @return true if the parameter is set successfully, false otherwise
*/
private native boolean setParameter(int key, Parcel value);
private native Parcel getParameter(int key);
/**
* Checks whether the MediaPlayer2 is looping or non-looping.
*
* @return true if the MediaPlayer2 is currently looping, false otherwise
* @hide
*/
@Override
public native boolean isLooping();
/**
* Sets the audio session ID.
*
* @param sessionId the audio session ID.
* The audio session ID is a system wide unique identifier for the audio stream played by
* this MediaPlayer2 instance.
* The primary use of the audio session ID is to associate audio effects to a particular
* instance of MediaPlayer2: if an audio session ID is provided when creating an audio effect,
* this effect will be applied only to the audio content of media players within the same
* audio session and not to the output mix.
* When created, a MediaPlayer2 instance automatically generates its own audio session ID.
* However, it is possible to force this player to be part of an already existing audio session
* by calling this method.
* This method must be called before one of the overloaded <code> setDataSource </code> methods.
* @throws IllegalStateException if it is called in an invalid state
* @throws IllegalArgumentException if the sessionId is invalid.
*/
@Override
public void setAudioSessionId(int sessionId) {
addTask(new Task(CALL_COMPLETED_SET_AUDIO_SESSION_ID, false) {
@Override
void process() {
_setAudioSessionId(sessionId);
}
});
}
private native void _setAudioSessionId(int sessionId);
/**
* Returns the audio session ID.
*
* @return the audio session ID. {@see #setAudioSessionId(int)}
* Note that the audio session ID is 0 only if a problem occured when the MediaPlayer2 was contructed.
*/
@Override
public native int getAudioSessionId();
/**
* Attaches an auxiliary effect to the player. A typical auxiliary effect is a reverberation
* effect which can be applied on any sound source that directs a certain amount of its
* energy to this effect. This amount is defined by setAuxEffectSendLevel().
* See {@link #setAuxEffectSendLevel(float)}.
* <p>After creating an auxiliary effect (e.g.
* {@link android.media.audiofx.EnvironmentalReverb}), retrieve its ID with
* {@link android.media.audiofx.AudioEffect#getId()} and use it when calling this method
* to attach the player to the effect.
* <p>To detach the effect from the player, call this method with a null effect id.
* <p>This method must be called after one of the overloaded <code> setDataSource </code>
* methods.
* @param effectId system wide unique id of the effect to attach
*/
@Override
public void attachAuxEffect(int effectId) {
addTask(new Task(CALL_COMPLETED_ATTACH_AUX_EFFECT, false) {
@Override
void process() {
_attachAuxEffect(effectId);
}
});
}
private native void _attachAuxEffect(int effectId);
/**
* Sets the send level of the player to the attached auxiliary effect.
* See {@link #attachAuxEffect(int)}. The level value range is 0 to 1.0.
* <p>By default the send level is 0, so even if an effect is attached to the player
* this method must be called for the effect to be applied.
* <p>Note that the passed level value is a raw scalar. UI controls should be scaled
* logarithmically: the gain applied by audio framework ranges from -72dB to 0dB,
* so an appropriate conversion from linear UI input x to level is:
* x == 0 -> level = 0
* 0 < x <= R -> level = 10^(72*(x-R)/20/R)
* @param level send level scalar
*/
@Override
public void setAuxEffectSendLevel(float level) {
addTask(new Task(CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL, false) {
@Override
void process() {
_setAuxEffectSendLevel(level);
}
});
}
private native void _setAuxEffectSendLevel(float level);
/*
* @param request Parcel destinated to the media player.
* @param reply[out] Parcel that will contain the reply.
* @return The status code.
*/
private native final int native_invoke(Parcel request, Parcel reply);
/*
* @param update_only If true fetch only the set of metadata that have
* changed since the last invocation of getMetadata.
* The set is built using the unfiltered
* notifications the native player sent to the
* MediaPlayer2Manager during that period of
* time. If false, all the metadatas are considered.
* @param apply_filter If true, once the metadata set has been built based on
* the value update_only, the current filter is applied.
* @param reply[out] On return contains the serialized
* metadata. Valid only if the call was successful.
* @return The status code.
*/
private native final boolean native_getMetadata(boolean update_only,
boolean apply_filter,
Parcel reply);
/*
* @param request Parcel with the 2 serialized lists of allowed
* metadata types followed by the one to be
* dropped. Each list starts with an integer
* indicating the number of metadata type elements.
* @return The status code.
*/
private native final int native_setMetadataFilter(Parcel request);
private static native final void native_init();
private native final void native_setup(Object mediaplayer2_this);
private native final void native_finalize();
private static native final void native_stream_event_onTearDown(
long nativeCallbackPtr, long userDataPtr);
private static native final void native_stream_event_onStreamPresentationEnd(
long nativeCallbackPtr, long userDataPtr);
private static native final void native_stream_event_onStreamDataRequest(
long jAudioTrackPtr, long nativeCallbackPtr, long userDataPtr);
/**
* Class for MediaPlayer2 to return each audio/video/subtitle track's metadata.
*
* @see android.media.MediaPlayer2#getTrackInfo
*/
public static final class TrackInfoImpl extends TrackInfo {
/**
* Gets the track type.
* @return TrackType which indicates if the track is video, audio, timed text.
*/
@Override
public int getTrackType() {
return mTrackType;
}
/**
* Gets the language code of the track.
* @return a language code in either way of ISO-639-1 or ISO-639-2.
* When the language is unknown or could not be determined,
* ISO-639-2 language code, "und", is returned.
*/
@Override
public String getLanguage() {
String language = mFormat.getString(MediaFormat.KEY_LANGUAGE);
return language == null ? "und" : language;
}
/**
* Gets the {@link MediaFormat} of the track. If the format is
* unknown or could not be determined, null is returned.
*/
@Override
public MediaFormat getFormat() {
if (mTrackType == MEDIA_TRACK_TYPE_TIMEDTEXT
|| mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
return mFormat;
}
return null;
}
final int mTrackType;
final MediaFormat mFormat;
TrackInfoImpl(Parcel in) {
mTrackType = in.readInt();
// TODO: parcel in the full MediaFormat; currently we are using createSubtitleFormat
// even for audio/video tracks, meaning we only set the mime and language.
String mime = in.readString();
String language = in.readString();
mFormat = MediaFormat.createSubtitleFormat(mime, language);
if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
mFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, in.readInt());
mFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, in.readInt());
mFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, in.readInt());
}
}
/** @hide */
TrackInfoImpl(int type, MediaFormat format) {
mTrackType = type;
mFormat = format;
}
/**
* Flatten this object in to a Parcel.
*
* @param dest The Parcel in which the object should be written.
* @param flags Additional flags about how the object should be written.
* May be 0 or {@link android.os.Parcelable#PARCELABLE_WRITE_RETURN_VALUE}.
*/
/* package private */ void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mTrackType);
dest.writeString(getLanguage());
if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
dest.writeString(mFormat.getString(MediaFormat.KEY_MIME));
dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_AUTOSELECT));
dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_DEFAULT));
dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE));
}
}
@Override
public String toString() {
StringBuilder out = new StringBuilder(128);
out.append(getClass().getName());
out.append('{');
switch (mTrackType) {
case MEDIA_TRACK_TYPE_VIDEO:
out.append("VIDEO");
break;
case MEDIA_TRACK_TYPE_AUDIO:
out.append("AUDIO");
break;
case MEDIA_TRACK_TYPE_TIMEDTEXT:
out.append("TIMEDTEXT");
break;
case MEDIA_TRACK_TYPE_SUBTITLE:
out.append("SUBTITLE");
break;
default:
out.append("UNKNOWN");
break;
}
out.append(", " + mFormat.toString());
out.append("}");
return out.toString();
}
/**
* Used to read a TrackInfoImpl from a Parcel.
*/
/* package private */ static final Parcelable.Creator<TrackInfoImpl> CREATOR
= new Parcelable.Creator<TrackInfoImpl>() {
@Override
public TrackInfoImpl createFromParcel(Parcel in) {
return new TrackInfoImpl(in);
}
@Override
public TrackInfoImpl[] newArray(int size) {
return new TrackInfoImpl[size];
}
};
};
// We would like domain specific classes with more informative names than the `first` and `second`
// in generic Pair, but we would also like to avoid creating new/trivial classes. As a compromise
// we document the meanings of `first` and `second` here:
//
// Pair.first - inband track index; non-null iff representing an inband track.
// Pair.second - a SubtitleTrack registered with mSubtitleController; non-null iff representing
// an inband subtitle track or any out-of-band track (subtitle or timedtext).
private Vector<Pair<Integer, SubtitleTrack>> mIndexTrackPairs = new Vector<>();
private BitSet mInbandTrackIndices = new BitSet();
/**
* Returns a List of track information.
*
* @return List of track info. The total number of tracks is the array length.
* Must be called again if an external timed text source has been added after
* addTimedTextSource method is called.
* @throws IllegalStateException if it is called in an invalid state.
*/
@Override
public List<TrackInfo> getTrackInfo() {
TrackInfoImpl trackInfo[] = getInbandTrackInfoImpl();
// add out-of-band tracks
synchronized (mIndexTrackPairs) {
TrackInfoImpl allTrackInfo[] = new TrackInfoImpl[mIndexTrackPairs.size()];
for (int i = 0; i < allTrackInfo.length; i++) {
Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i);
if (p.first != null) {
// inband track
allTrackInfo[i] = trackInfo[p.first];
} else {
SubtitleTrack track = p.second;
allTrackInfo[i] = new TrackInfoImpl(track.getTrackType(), track.getFormat());
}
}
return Arrays.asList(allTrackInfo);
}
}
private TrackInfoImpl[] getInbandTrackInfoImpl() throws IllegalStateException {
Parcel request = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
request.writeInt(INVOKE_ID_GET_TRACK_INFO);
invoke(request, reply);
TrackInfoImpl trackInfo[] = reply.createTypedArray(TrackInfoImpl.CREATOR);
return trackInfo;
} finally {
request.recycle();
reply.recycle();
}
}
/*
* A helper function to check if the mime type is supported by media framework.
*/
private static boolean availableMimeTypeForExternalSource(String mimeType) {
if (MEDIA_MIMETYPE_TEXT_SUBRIP.equals(mimeType)) {
return true;
}
return false;
}
private SubtitleController mSubtitleController;
/** @hide */
@Override
public void setSubtitleAnchor(
SubtitleController controller,
SubtitleController.Anchor anchor) {
// TODO: create SubtitleController in MediaPlayer2
mSubtitleController = controller;
mSubtitleController.setAnchor(anchor);
}
/**
* The private version of setSubtitleAnchor is used internally to set mSubtitleController if
* necessary when clients don't provide their own SubtitleControllers using the public version
* {@link #setSubtitleAnchor(SubtitleController, Anchor)} (e.g. {@link VideoView} provides one).
*/
private synchronized void setSubtitleAnchor() {
if ((mSubtitleController == null) && (ActivityThread.currentApplication() != null)) {
final HandlerThread thread = new HandlerThread("SetSubtitleAnchorThread");
thread.start();
Handler handler = new Handler(thread.getLooper());
handler.post(new Runnable() {
@Override
public void run() {
Context context = ActivityThread.currentApplication();
mSubtitleController = new SubtitleController(context, mTimeProvider, MediaPlayer2Impl.this);
mSubtitleController.setAnchor(new Anchor() {
@Override
public void setSubtitleWidget(RenderingWidget subtitleWidget) {
}
@Override
public Looper getSubtitleLooper() {
return Looper.getMainLooper();
}
});
thread.getLooper().quitSafely();
}
});
try {
thread.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Log.w(TAG, "failed to join SetSubtitleAnchorThread");
}
}
}
private int mSelectedSubtitleTrackIndex = -1;
private Vector<InputStream> mOpenSubtitleSources;
private OnSubtitleDataListener mSubtitleDataListener = new OnSubtitleDataListener() {
@Override
public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) {
int index = data.getTrackIndex();
synchronized (mIndexTrackPairs) {
for (Pair<Integer, SubtitleTrack> p : mIndexTrackPairs) {
if (p.first != null && p.first == index && p.second != null) {
// inband subtitle track that owns data
SubtitleTrack track = p.second;
track.onData(data);
}
}
}
}
};
/** @hide */
@Override
public void onSubtitleTrackSelected(SubtitleTrack track) {
if (mSelectedSubtitleTrackIndex >= 0) {
try {
selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, false);
} catch (IllegalStateException e) {
}
mSelectedSubtitleTrackIndex = -1;
}
setOnSubtitleDataListener(null);
if (track == null) {
return;
}
synchronized (mIndexTrackPairs) {
for (Pair<Integer, SubtitleTrack> p : mIndexTrackPairs) {
if (p.first != null && p.second == track) {
// inband subtitle track that is selected
mSelectedSubtitleTrackIndex = p.first;
break;
}
}
}
if (mSelectedSubtitleTrackIndex >= 0) {
try {
selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, true);
} catch (IllegalStateException e) {
}
setOnSubtitleDataListener(mSubtitleDataListener);
}
// no need to select out-of-band tracks
}
/** @hide */
@Override
public void addSubtitleSource(InputStream is, MediaFormat format)
throws IllegalStateException
{
final InputStream fIs = is;
final MediaFormat fFormat = format;
if (is != null) {
// Ensure all input streams are closed. It is also a handy
// way to implement timeouts in the future.
synchronized(mOpenSubtitleSources) {
mOpenSubtitleSources.add(is);
}
} else {
Log.w(TAG, "addSubtitleSource called with null InputStream");
}
getMediaTimeProvider();
// process each subtitle in its own thread
final HandlerThread thread = new HandlerThread("SubtitleReadThread",
Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
thread.start();
Handler handler = new Handler(thread.getLooper());
handler.post(new Runnable() {
private int addTrack() {
if (fIs == null || mSubtitleController == null) {
return MEDIA_INFO_UNSUPPORTED_SUBTITLE;
}
SubtitleTrack track = mSubtitleController.addTrack(fFormat);
if (track == null) {
return MEDIA_INFO_UNSUPPORTED_SUBTITLE;
}
// TODO: do the conversion in the subtitle track
Scanner scanner = new Scanner(fIs, "UTF-8");
String contents = scanner.useDelimiter("\\A").next();
synchronized(mOpenSubtitleSources) {
mOpenSubtitleSources.remove(fIs);
}
scanner.close();
synchronized (mIndexTrackPairs) {
mIndexTrackPairs.add(Pair.<Integer, SubtitleTrack>create(null, track));
}
Handler h = mTimeProvider.mEventHandler;
int what = TimeProvider.NOTIFY;
int arg1 = TimeProvider.NOTIFY_TRACK_DATA;
Pair<SubtitleTrack, byte[]> trackData = Pair.create(track, contents.getBytes());
Message m = h.obtainMessage(what, arg1, 0, trackData);
h.sendMessage(m);
return MEDIA_INFO_EXTERNAL_METADATA_UPDATE;
}
public void run() {
int res = addTrack();
if (mEventHandler != null) {
Message m = mEventHandler.obtainMessage(MEDIA_INFO, res, 0, null);
mEventHandler.sendMessage(m);
}
thread.getLooper().quitSafely();
}
});
}
private void scanInternalSubtitleTracks() {
setSubtitleAnchor();
populateInbandTracks();
if (mSubtitleController != null) {
mSubtitleController.selectDefaultTrack();
}
}
private void populateInbandTracks() {
TrackInfoImpl[] tracks = getInbandTrackInfoImpl();
synchronized (mIndexTrackPairs) {
for (int i = 0; i < tracks.length; i++) {
if (mInbandTrackIndices.get(i)) {
continue;
} else {
mInbandTrackIndices.set(i);
}
// newly appeared inband track
if (tracks[i].getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
SubtitleTrack track = mSubtitleController.addTrack(
tracks[i].getFormat());
mIndexTrackPairs.add(Pair.create(i, track));
} else {
mIndexTrackPairs.add(Pair.<Integer, SubtitleTrack>create(i, null));
}
}
}
}
/* TODO: Limit the total number of external timed text source to a reasonable number.
*/
/**
* Adds an external timed text source file.
*
* Currently supported format is SubRip with the file extension .srt, case insensitive.
* Note that a single external timed text source may contain multiple tracks in it.
* One can find the total number of available tracks using {@link #getTrackInfo()} to see what
* additional tracks become available after this method call.
*
* @param path The file path of external timed text source file.
* @param mimeType The mime type of the file. Must be one of the mime types listed above.
* @throws IOException if the file cannot be accessed or is corrupted.
* @throws IllegalArgumentException if the mimeType is not supported.
* @throws IllegalStateException if called in an invalid state.
* @hide
*/
@Override
public void addTimedTextSource(String path, String mimeType)
throws IOException {
if (!availableMimeTypeForExternalSource(mimeType)) {
final String msg = "Illegal mimeType for timed text source: " + mimeType;
throw new IllegalArgumentException(msg);
}
File file = new File(path);
if (file.exists()) {
FileInputStream is = new FileInputStream(file);
FileDescriptor fd = is.getFD();
addTimedTextSource(fd, mimeType);
is.close();
} else {
// We do not support the case where the path is not a file.
throw new IOException(path);
}
}
/**
* Adds an external timed text source file (Uri).
*
* Currently supported format is SubRip with the file extension .srt, case insensitive.
* Note that a single external timed text source may contain multiple tracks in it.
* One can find the total number of available tracks using {@link #getTrackInfo()} to see what
* additional tracks become available after this method call.
*
* @param context the Context to use when resolving the Uri
* @param uri the Content URI of the data you want to play
* @param mimeType The mime type of the file. Must be one of the mime types listed above.
* @throws IOException if the file cannot be accessed or is corrupted.
* @throws IllegalArgumentException if the mimeType is not supported.
* @throws IllegalStateException if called in an invalid state.
* @hide
*/
@Override
public void addTimedTextSource(Context context, Uri uri, String mimeType)
throws IOException {
String scheme = uri.getScheme();
if(scheme == null || scheme.equals("file")) {
addTimedTextSource(uri.getPath(), mimeType);
return;
}
AssetFileDescriptor fd = null;
try {
ContentResolver resolver = context.getContentResolver();
fd = resolver.openAssetFileDescriptor(uri, "r");
if (fd == null) {
return;
}
addTimedTextSource(fd.getFileDescriptor(), mimeType);
return;
} catch (SecurityException ex) {
} catch (IOException ex) {
} finally {
if (fd != null) {
fd.close();
}
}
}
/**
* Adds an external timed text source file (FileDescriptor).
*
* It is the caller's responsibility to close the file descriptor.
* It is safe to do so as soon as this call returns.
*
* Currently supported format is SubRip. Note that a single external timed text source may
* contain multiple tracks in it. One can find the total number of available tracks
* using {@link #getTrackInfo()} to see what additional tracks become available
* after this method call.
*
* @param fd the FileDescriptor for the file you want to play
* @param mimeType The mime type of the file. Must be one of the mime types listed above.
* @throws IllegalArgumentException if the mimeType is not supported.
* @throws IllegalStateException if called in an invalid state.
* @hide
*/
@Override
public void addTimedTextSource(FileDescriptor fd, String mimeType) {
// intentionally less than LONG_MAX
addTimedTextSource(fd, 0, 0x7ffffffffffffffL, mimeType);
}
/**
* Adds an external timed text file (FileDescriptor).
*
* It is the caller's responsibility to close the file descriptor.
* It is safe to do so as soon as this call returns.
*
* Currently supported format is SubRip. Note that a single external timed text source may
* contain multiple tracks in it. One can find the total number of available tracks
* using {@link #getTrackInfo()} to see what additional tracks become available
* after this method call.
*
* @param fd the FileDescriptor for the file you want to play
* @param offset the offset into the file where the data to be played starts, in bytes
* @param length the length in bytes of the data to be played
* @param mime The mime type of the file. Must be one of the mime types listed above.
* @throws IllegalArgumentException if the mimeType is not supported.
* @throws IllegalStateException if called in an invalid state.
* @hide
*/
@Override
public void addTimedTextSource(FileDescriptor fd, long offset, long length, String mime) {
if (!availableMimeTypeForExternalSource(mime)) {
throw new IllegalArgumentException("Illegal mimeType for timed text source: " + mime);
}
final FileDescriptor dupedFd;
try {
dupedFd = Os.dup(fd);
} catch (ErrnoException ex) {
Log.e(TAG, ex.getMessage(), ex);
throw new RuntimeException(ex);
}
final MediaFormat fFormat = new MediaFormat();
fFormat.setString(MediaFormat.KEY_MIME, mime);
fFormat.setInteger(MediaFormat.KEY_IS_TIMED_TEXT, 1);
// A MediaPlayer2 created by a VideoView should already have its mSubtitleController set.
if (mSubtitleController == null) {
setSubtitleAnchor();
}
if (!mSubtitleController.hasRendererFor(fFormat)) {
// test and add not atomic
Context context = ActivityThread.currentApplication();
mSubtitleController.registerRenderer(new SRTRenderer(context, mEventHandler));
}
final SubtitleTrack track = mSubtitleController.addTrack(fFormat);
synchronized (mIndexTrackPairs) {
mIndexTrackPairs.add(Pair.<Integer, SubtitleTrack>create(null, track));
}
getMediaTimeProvider();
final long offset2 = offset;
final long length2 = length;
final HandlerThread thread = new HandlerThread(
"TimedTextReadThread",
Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
thread.start();
Handler handler = new Handler(thread.getLooper());
handler.post(new Runnable() {
private int addTrack() {
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
Os.lseek(dupedFd, offset2, OsConstants.SEEK_SET);
byte[] buffer = new byte[4096];
for (long total = 0; total < length2;) {
int bytesToRead = (int) Math.min(buffer.length, length2 - total);
int bytes = IoBridge.read(dupedFd, buffer, 0, bytesToRead);
if (bytes < 0) {
break;
} else {
bos.write(buffer, 0, bytes);
total += bytes;
}
}
Handler h = mTimeProvider.mEventHandler;
int what = TimeProvider.NOTIFY;
int arg1 = TimeProvider.NOTIFY_TRACK_DATA;
Pair<SubtitleTrack, byte[]> trackData = Pair.create(track, bos.toByteArray());
Message m = h.obtainMessage(what, arg1, 0, trackData);
h.sendMessage(m);
return MEDIA_INFO_EXTERNAL_METADATA_UPDATE;
} catch (Exception e) {
Log.e(TAG, e.getMessage(), e);
return MEDIA_INFO_TIMED_TEXT_ERROR;
} finally {
try {
Os.close(dupedFd);
} catch (ErrnoException e) {
Log.e(TAG, e.getMessage(), e);
}
}
}
public void run() {
int res = addTrack();
if (mEventHandler != null) {
Message m = mEventHandler.obtainMessage(MEDIA_INFO, res, 0, null);
mEventHandler.sendMessage(m);
}
thread.getLooper().quitSafely();
}
});
}
/**
* Returns the index of the audio, video, or subtitle track currently selected for playback,
* The return value is an index into the array returned by {@link #getTrackInfo()}, and can
* be used in calls to {@link #selectTrack(int)} or {@link #deselectTrack(int)}.
*
* @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO},
* {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, or
* {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}
* @return index of the audio, video, or subtitle track currently selected for playback;
* a negative integer is returned when there is no selected track for {@code trackType} or
* when {@code trackType} is not one of audio, video, or subtitle.
* @throws IllegalStateException if called after {@link #close()}
*
* @see #getTrackInfo()
* @see #selectTrack(int)
* @see #deselectTrack(int)
*/
@Override
public int getSelectedTrack(int trackType) {
if (mSubtitleController != null
&& (trackType == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE
|| trackType == TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT)) {
SubtitleTrack subtitleTrack = mSubtitleController.getSelectedTrack();
if (subtitleTrack != null) {
synchronized (mIndexTrackPairs) {
for (int i = 0; i < mIndexTrackPairs.size(); i++) {
Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i);
if (p.second == subtitleTrack && subtitleTrack.getTrackType() == trackType) {
return i;
}
}
}
}
}
Parcel request = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
request.writeInt(INVOKE_ID_GET_SELECTED_TRACK);
request.writeInt(trackType);
invoke(request, reply);
int inbandTrackIndex = reply.readInt();
synchronized (mIndexTrackPairs) {
for (int i = 0; i < mIndexTrackPairs.size(); i++) {
Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i);
if (p.first != null && p.first == inbandTrackIndex) {
return i;
}
}
}
return -1;
} finally {
request.recycle();
reply.recycle();
}
}
/**
* Selects a track.
* <p>
* If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception.
* If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately.
* If a MediaPlayer2 is not in Started state, it just marks the track to be played.
* </p>
* <p>
* In any valid state, if it is called multiple times on the same type of track (ie. Video,
* Audio, Timed Text), the most recent one will be chosen.
* </p>
* <p>
* The first audio and video tracks are selected by default if available, even though
* this method is not called. However, no timed text track will be selected until
* this function is called.
* </p>
* <p>
* Currently, only timed text tracks or audio tracks can be selected via this method.
* In addition, the support for selecting an audio track at runtime is pretty limited
* in that an audio track can only be selected in the <em>Prepared</em> state.
* </p>
* @param index the index of the track to be selected. The valid range of the index
* is 0..total number of track - 1. The total number of tracks as well as the type of
* each individual track can be found by calling {@link #getTrackInfo()} method.
* @throws IllegalStateException if called in an invalid state.
*
* @see android.media.MediaPlayer2#getTrackInfo
*/
@Override
public void selectTrack(int index) {
addTask(new Task(CALL_COMPLETED_SELECT_TRACK, false) {
@Override
void process() {
selectOrDeselectTrack(index, true /* select */);
}
});
}
/**
* Deselect a track.
* <p>
* Currently, the track must be a timed text track and no audio or video tracks can be
* deselected. If the timed text track identified by index has not been
* selected before, it throws an exception.
* </p>
* @param index the index of the track to be deselected. The valid range of the index
* is 0..total number of tracks - 1. The total number of tracks as well as the type of
* each individual track can be found by calling {@link #getTrackInfo()} method.
* @throws IllegalStateException if called in an invalid state.
*
* @see android.media.MediaPlayer2#getTrackInfo
*/
@Override
public void deselectTrack(int index) {
addTask(new Task(CALL_COMPLETED_DESELECT_TRACK, false) {
@Override
void process() {
selectOrDeselectTrack(index, false /* select */);
}
});
}
private void selectOrDeselectTrack(int index, boolean select)
throws IllegalStateException {
// handle subtitle track through subtitle controller
populateInbandTracks();
Pair<Integer,SubtitleTrack> p = null;
try {
p = mIndexTrackPairs.get(index);
} catch (ArrayIndexOutOfBoundsException e) {
// ignore bad index
return;
}
SubtitleTrack track = p.second;
if (track == null) {
// inband (de)select
selectOrDeselectInbandTrack(p.first, select);
return;
}
if (mSubtitleController == null) {
return;
}
if (!select) {
// out-of-band deselect
if (mSubtitleController.getSelectedTrack() == track) {
mSubtitleController.selectTrack(null);
} else {
Log.w(TAG, "trying to deselect track that was not selected");
}
return;
}
// out-of-band select
if (track.getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) {
int ttIndex = getSelectedTrack(TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT);
synchronized (mIndexTrackPairs) {
if (ttIndex >= 0 && ttIndex < mIndexTrackPairs.size()) {
Pair<Integer,SubtitleTrack> p2 = mIndexTrackPairs.get(ttIndex);
if (p2.first != null && p2.second == null) {
// deselect inband counterpart
selectOrDeselectInbandTrack(p2.first, false);
}
}
}
}
mSubtitleController.selectTrack(track);
}
private void selectOrDeselectInbandTrack(int index, boolean select)
throws IllegalStateException {
Parcel request = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
request.writeInt(select? INVOKE_ID_SELECT_TRACK: INVOKE_ID_DESELECT_TRACK);
request.writeInt(index);
invoke(request, reply);
} finally {
request.recycle();
reply.recycle();
}
}
// Have to declare protected for finalize() since it is protected
// in the base class Object.
@Override
protected void finalize() throws Throwable {
if (mGuard != null) {
mGuard.warnIfOpen();
}
close();
native_finalize();
}
private void release() {
stayAwake(false);
updateSurfaceScreenOn();
synchronized (mEventCbLock) {
mEventCallbackRecords.clear();
}
if (mHandlerThread != null) {
mHandlerThread.quitSafely();
mHandlerThread = null;
}
if (mTimeProvider != null) {
mTimeProvider.close();
mTimeProvider = null;
}
mOnSubtitleDataListener = null;
// Modular DRM clean up
mOnDrmConfigHelper = null;
synchronized (mDrmEventCbLock) {
mDrmEventCallbackRecords.clear();
}
resetDrmState();
_release();
}
private native void _release();
/* Do not change these values without updating their counterparts
* in include/media/mediaplayer2.h!
*/
private static final int MEDIA_NOP = 0; // interface test message
private static final int MEDIA_PREPARED = 1;
private static final int MEDIA_PLAYBACK_COMPLETE = 2;
private static final int MEDIA_BUFFERING_UPDATE = 3;
private static final int MEDIA_SEEK_COMPLETE = 4;
private static final int MEDIA_SET_VIDEO_SIZE = 5;
private static final int MEDIA_STARTED = 6;
private static final int MEDIA_PAUSED = 7;
private static final int MEDIA_STOPPED = 8;
private static final int MEDIA_SKIPPED = 9;
private static final int MEDIA_NOTIFY_TIME = 98;
private static final int MEDIA_TIMED_TEXT = 99;
private static final int MEDIA_ERROR = 100;
private static final int MEDIA_INFO = 200;
private static final int MEDIA_SUBTITLE_DATA = 201;
private static final int MEDIA_META_DATA = 202;
private static final int MEDIA_DRM_INFO = 210;
private static final int MEDIA_AUDIO_ROUTING_CHANGED = 10000;
private TimeProvider mTimeProvider;
/** @hide */
@Override
public MediaTimeProvider getMediaTimeProvider() {
if (mTimeProvider == null) {
mTimeProvider = new TimeProvider(this);
}
return mTimeProvider;
}
private class EventHandler extends Handler {
private MediaPlayer2Impl mMediaPlayer;
public EventHandler(MediaPlayer2Impl mp, Looper looper) {
super(looper);
mMediaPlayer = mp;
}
@Override
public void handleMessage(Message msg) {
handleMessage(msg, 0);
}
public void handleMessage(Message msg, long srcId) {
if (mMediaPlayer.mNativeContext == 0) {
Log.w(TAG, "mediaplayer2 went away with unhandled events");
return;
}
final int what = msg.arg1;
final int extra = msg.arg2;
switch(msg.what) {
case MEDIA_PREPARED:
{
try {
scanInternalSubtitleTracks();
} catch (RuntimeException e) {
// send error message instead of crashing;
// send error message instead of inlining a call to onError
// to avoid code duplication.
Message msg2 = obtainMessage(
MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, null);
sendMessage(msg2);
}
final DataSourceDesc dsd;
synchronized (mSrcLock) {
Log.i(TAG, "MEDIA_PREPARED: srcId=" + srcId
+ ", currentSrcId=" + mCurrentSrcId + ", nextSrcId=" + mNextSrcId);
if (srcId == mCurrentSrcId) {
dsd = mCurrentDSD;
prepareNextDataSource_l();
} else if (mNextDSDs != null && !mNextDSDs.isEmpty()
&& srcId == mNextSrcId) {
dsd = mNextDSDs.get(0);
mNextSourceState = NEXT_SOURCE_STATE_PREPARED;
if (mNextSourcePlayPending) {
playNextDataSource_l();
}
} else {
dsd = null;
}
}
if (dsd != null) {
synchronized (mEventCbLock) {
for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
cb.first.execute(() -> cb.second.onInfo(
mMediaPlayer, dsd, MEDIA_INFO_PREPARED, 0));
}
}
}
synchronized (mTaskLock) {
if (mCurrentTask != null
&& mCurrentTask.mMediaCallType == CALL_COMPLETED_PREPARE
&& mCurrentTask.mDSD == dsd
&& mCurrentTask.mNeedToWaitForEventToComplete) {
mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
mCurrentTask = null;
processPendingTask_l();
}
}
return;
}
case MEDIA_DRM_INFO:
{
if (msg.obj == null) {
Log.w(TAG, "MEDIA_DRM_INFO msg.obj=NULL");
} else if (msg.obj instanceof Parcel) {
// The parcel was parsed already in postEventFromNative
final DrmInfoImpl drmInfo;
synchronized (mDrmLock) {
if (mDrmInfoImpl != null) {
drmInfo = mDrmInfoImpl.makeCopy();
} else {
drmInfo = null;
}
}
// notifying the client outside the lock
if (drmInfo != null) {
synchronized (mEventCbLock) {
for (Pair<Executor, DrmEventCallback> cb : mDrmEventCallbackRecords) {
cb.first.execute(() -> cb.second.onDrmInfo(
mMediaPlayer, mCurrentDSD, drmInfo));
}
}
}
} else {
Log.w(TAG, "MEDIA_DRM_INFO msg.obj of unexpected type " + msg.obj);
}
return;
}
case MEDIA_PLAYBACK_COMPLETE:
{
final DataSourceDesc dsd = mCurrentDSD;
synchronized (mSrcLock) {
if (srcId == mCurrentSrcId) {
Log.i(TAG, "MEDIA_PLAYBACK_COMPLETE: srcId=" + srcId
+ ", currentSrcId=" + mCurrentSrcId + ", nextSrcId=" + mNextSrcId);
playNextDataSource_l();
}
}
synchronized (mEventCbLock) {
for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
cb.first.execute(() -> cb.second.onInfo(
mMediaPlayer, dsd, MEDIA_INFO_PLAYBACK_COMPLETE, 0));
}
}
stayAwake(false);
return;
}
case MEDIA_STOPPED:
{
TimeProvider timeProvider = mTimeProvider;
if (timeProvider != null) {
timeProvider.onStopped();
}
break;
}
case MEDIA_STARTED:
case MEDIA_PAUSED:
{
TimeProvider timeProvider = mTimeProvider;
if (timeProvider != null) {
timeProvider.onPaused(msg.what == MEDIA_PAUSED);
}
break;
}
case MEDIA_BUFFERING_UPDATE:
{
final int percent = msg.arg1;
synchronized (mEventCbLock) {
if (srcId == mCurrentSrcId) {
mBufferedPercentageCurrent.set(percent);
for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
cb.first.execute(() -> cb.second.onInfo(
mMediaPlayer, mCurrentDSD, MEDIA_INFO_BUFFERING_UPDATE,
percent));
}
} else if (srcId == mNextSrcId && !mNextDSDs.isEmpty()) {
mBufferedPercentageNext.set(percent);
DataSourceDesc nextDSD = mNextDSDs.get(0);
for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
cb.first.execute(() -> cb.second.onInfo(
mMediaPlayer, nextDSD, MEDIA_INFO_BUFFERING_UPDATE,
percent));
}
}
}
return;
}
case MEDIA_SEEK_COMPLETE:
{
synchronized (mTaskLock) {
if (mCurrentTask != null
&& mCurrentTask.mMediaCallType == CALL_COMPLETED_SEEK_TO
&& mCurrentTask.mNeedToWaitForEventToComplete) {
mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
mCurrentTask = null;
processPendingTask_l();
}
}
}
// fall through
case MEDIA_SKIPPED:
{
TimeProvider timeProvider = mTimeProvider;
if (timeProvider != null) {
timeProvider.onSeekComplete(mMediaPlayer);
}
return;
}
case MEDIA_SET_VIDEO_SIZE:
{
final int width = msg.arg1;
final int height = msg.arg2;
synchronized (mEventCbLock) {
for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
cb.first.execute(() -> cb.second.onVideoSizeChanged(
mMediaPlayer, mCurrentDSD, width, height));
}
}
return;
}
case MEDIA_ERROR:
{
Log.e(TAG, "Error (" + msg.arg1 + "," + msg.arg2 + ")");
synchronized (mEventCbLock) {
for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
cb.first.execute(() -> cb.second.onError(
mMediaPlayer, mCurrentDSD, what, extra));
cb.first.execute(() -> cb.second.onInfo(
mMediaPlayer, mCurrentDSD, MEDIA_INFO_PLAYBACK_COMPLETE, 0));
}
}
stayAwake(false);
return;
}
case MEDIA_INFO:
{
switch (msg.arg1) {
case MEDIA_INFO_STARTED_AS_NEXT:
if (srcId == mCurrentSrcId) {
prepareNextDataSource_l();
}
break;
case MEDIA_INFO_VIDEO_TRACK_LAGGING:
Log.i(TAG, "Info (" + msg.arg1 + "," + msg.arg2 + ")");
break;
case MEDIA_INFO_METADATA_UPDATE:
try {
scanInternalSubtitleTracks();
} catch (RuntimeException e) {
Message msg2 = obtainMessage(
MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED,
null);
sendMessage(msg2);
}
// fall through
case MEDIA_INFO_EXTERNAL_METADATA_UPDATE:
msg.arg1 = MEDIA_INFO_METADATA_UPDATE;
// update default track selection
if (mSubtitleController != null) {
mSubtitleController.selectDefaultTrack();
}
break;
case MEDIA_INFO_BUFFERING_START:
case MEDIA_INFO_BUFFERING_END:
TimeProvider timeProvider = mTimeProvider;
if (timeProvider != null) {
timeProvider.onBuffering(msg.arg1 == MEDIA_INFO_BUFFERING_START);
}
break;
}
synchronized (mEventCbLock) {
for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
cb.first.execute(() -> cb.second.onInfo(
mMediaPlayer, mCurrentDSD, what, extra));
}
}
// No real default action so far.
return;
}
case MEDIA_NOTIFY_TIME:
{
TimeProvider timeProvider = mTimeProvider;
if (timeProvider != null) {
timeProvider.onNotifyTime();
}
return;
}
case MEDIA_TIMED_TEXT:
{
final TimedText text;
if (msg.obj instanceof Parcel) {
Parcel parcel = (Parcel)msg.obj;
text = new TimedText(parcel);
parcel.recycle();
} else {
text = null;
}
synchronized (mEventCbLock) {
for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
cb.first.execute(() -> cb.second.onTimedText(mMediaPlayer, mCurrentDSD, text));
}
}
return;
}
case MEDIA_SUBTITLE_DATA:
{
OnSubtitleDataListener onSubtitleDataListener = mOnSubtitleDataListener;
if (onSubtitleDataListener == null) {
return;
}
if (msg.obj instanceof Parcel) {
Parcel parcel = (Parcel) msg.obj;
SubtitleData data = new SubtitleData(parcel);
parcel.recycle();
onSubtitleDataListener.onSubtitleData(mMediaPlayer, data);
}
return;
}
case MEDIA_META_DATA:
{
final TimedMetaData data;
if (msg.obj instanceof Parcel) {
Parcel parcel = (Parcel) msg.obj;
data = TimedMetaData.createTimedMetaDataFromParcel(parcel);
parcel.recycle();
} else {
data = null;
}
synchronized (mEventCbLock) {
for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
cb.first.execute(() -> cb.second.onTimedMetaDataAvailable(
mMediaPlayer, mCurrentDSD, data));
}
}
return;
}
case MEDIA_NOP: // interface test message - ignore
{
break;
}
case MEDIA_AUDIO_ROUTING_CHANGED:
{
AudioManager.resetAudioPortGeneration();
synchronized (mRoutingChangeListeners) {
for (NativeRoutingEventHandlerDelegate delegate
: mRoutingChangeListeners.values()) {
delegate.notifyClient();
}
}
return;
}
default:
{
Log.e(TAG, "Unknown message type " + msg.what);
return;
}
}
}
}
/*
* Called from native code when an interesting event happens. This method
* just uses the EventHandler system to post the event back to the main app thread.
* We use a weak reference to the original MediaPlayer2 object so that the native
* code is safe from the object disappearing from underneath it. (This is
* the cookie passed to native_setup().)
*/
private static void postEventFromNative(Object mediaplayer2_ref, long srcId,
int what, int arg1, int arg2, Object obj)
{
final MediaPlayer2Impl mp = (MediaPlayer2Impl)((WeakReference)mediaplayer2_ref).get();
if (mp == null) {
return;
}
switch (what) {
case MEDIA_INFO:
if (arg1 == MEDIA_INFO_STARTED_AS_NEXT) {
new Thread(new Runnable() {
@Override
public void run() {
// this acquires the wakelock if needed, and sets the client side state
mp.play();
}
}).start();
Thread.yield();
}
break;
case MEDIA_DRM_INFO:
// We need to derive mDrmInfoImpl before prepare() returns so processing it here
// before the notification is sent to EventHandler below. EventHandler runs in the
// notification looper so its handleMessage might process the event after prepare()
// has returned.
Log.v(TAG, "postEventFromNative MEDIA_DRM_INFO");
if (obj instanceof Parcel) {
Parcel parcel = (Parcel)obj;
DrmInfoImpl drmInfo = new DrmInfoImpl(parcel);
synchronized (mp.mDrmLock) {
mp.mDrmInfoImpl = drmInfo;
}
} else {
Log.w(TAG, "MEDIA_DRM_INFO msg.obj of unexpected type " + obj);
}
break;
case MEDIA_PREPARED:
// By this time, we've learned about DrmInfo's presence or absence. This is meant
// mainly for prepare() use case. For prepare(), this still can run to a race
// condition b/c MediaPlayerNative releases the prepare() lock before calling notify
// so we also set mDrmInfoResolved in prepare().
synchronized (mp.mDrmLock) {
mp.mDrmInfoResolved = true;
}
break;
}
if (mp.mEventHandler != null) {
Message m = mp.mEventHandler.obtainMessage(what, arg1, arg2, obj);
mp.mEventHandler.post(new Runnable() {
@Override
public void run() {
mp.mEventHandler.handleMessage(m, srcId);
}
});
}
}
private final Object mEventCbLock = new Object();
private ArrayList<Pair<Executor, MediaPlayer2EventCallback> > mEventCallbackRecords
= new ArrayList<Pair<Executor, MediaPlayer2EventCallback> >();
/**
* Register a callback to be invoked when the media source is ready
* for playback.
*
* @param eventCallback the callback that will be run
* @param executor the executor through which the callback should be invoked
*/
@Override
public void setMediaPlayer2EventCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull MediaPlayer2EventCallback eventCallback) {
if (eventCallback == null) {
throw new IllegalArgumentException("Illegal null MediaPlayer2EventCallback");
}
if (executor == null) {
throw new IllegalArgumentException(
"Illegal null Executor for the MediaPlayer2EventCallback");
}
synchronized (mEventCbLock) {
mEventCallbackRecords.add(new Pair(executor, eventCallback));
}
}
/**
* Clears the {@link MediaPlayer2EventCallback}.
*/
@Override
public void clearMediaPlayer2EventCallback() {
synchronized (mEventCbLock) {
mEventCallbackRecords.clear();
}
}
/**
* Register a callback to be invoked when a track has data available.
*
* @param listener the callback that will be run
*
* @hide
*/
@Override
public void setOnSubtitleDataListener(OnSubtitleDataListener listener) {
mOnSubtitleDataListener = listener;
}
private OnSubtitleDataListener mOnSubtitleDataListener;
// Modular DRM begin
/**
* Register a callback to be invoked for configuration of the DRM object before
* the session is created.
* The callback will be invoked synchronously during the execution
* of {@link #prepareDrm(UUID uuid)}.
*
* @param listener the callback that will be run
*/
@Override
public void setOnDrmConfigHelper(OnDrmConfigHelper listener)
{
synchronized (mDrmLock) {
mOnDrmConfigHelper = listener;
} // synchronized
}
private OnDrmConfigHelper mOnDrmConfigHelper;
private final Object mDrmEventCbLock = new Object();
private ArrayList<Pair<Executor, DrmEventCallback> > mDrmEventCallbackRecords
= new ArrayList<Pair<Executor, DrmEventCallback> >();
/**
* Register a callback to be invoked when the media source is ready
* for playback.
*
* @param eventCallback the callback that will be run
* @param executor the executor through which the callback should be invoked
*/
@Override
public void setDrmEventCallback(@NonNull @CallbackExecutor Executor executor,
@NonNull DrmEventCallback eventCallback) {
if (eventCallback == null) {
throw new IllegalArgumentException("Illegal null MediaPlayer2EventCallback");
}
if (executor == null) {
throw new IllegalArgumentException(
"Illegal null Executor for the MediaPlayer2EventCallback");
}
synchronized (mDrmEventCbLock) {
mDrmEventCallbackRecords.add(new Pair(executor, eventCallback));
}
}
/**
* Clears the {@link DrmEventCallback}.
*/
@Override
public void clearDrmEventCallback() {
synchronized (mDrmEventCbLock) {
mDrmEventCallbackRecords.clear();
}
}
/**
* Retrieves the DRM Info associated with the current source
*
* @throws IllegalStateException if called before prepare()
*/
@Override
public DrmInfo getDrmInfo() {
DrmInfoImpl drmInfo = null;
// there is not much point if the app calls getDrmInfo within an OnDrmInfoListenet;
// regardless below returns drmInfo anyway instead of raising an exception
synchronized (mDrmLock) {
if (!mDrmInfoResolved && mDrmInfoImpl == null) {
final String msg = "The Player has not been prepared yet";
Log.v(TAG, msg);
throw new IllegalStateException(msg);
}
if (mDrmInfoImpl != null) {
drmInfo = mDrmInfoImpl.makeCopy();
}
} // synchronized
return drmInfo;
}
/**
* Prepares the DRM for the current source
* <p>
* If {@code OnDrmConfigHelper} is registered, it will be called during
* preparation to allow configuration of the DRM properties before opening the
* DRM session. Note that the callback is called synchronously in the thread that called
* {@code prepareDrm}. It should be used only for a series of {@code getDrmPropertyString}
* and {@code setDrmPropertyString} calls and refrain from any lengthy operation.
* <p>
* If the device has not been provisioned before, this call also provisions the device
* which involves accessing the provisioning server and can take a variable time to
* complete depending on the network connectivity.
* If {@code OnDrmPreparedListener} is registered, prepareDrm() runs in non-blocking
* mode by launching the provisioning in the background and returning. The listener
* will be called when provisioning and preparation has finished. If a
* {@code OnDrmPreparedListener} is not registered, prepareDrm() waits till provisioning
* and preparation has finished, i.e., runs in blocking mode.
* <p>
* If {@code OnDrmPreparedListener} is registered, it is called to indicate the DRM
* session being ready. The application should not make any assumption about its call
* sequence (e.g., before or after prepareDrm returns), or the thread context that will
* execute the listener (unless the listener is registered with a handler thread).
* <p>
*
* @param uuid The UUID of the crypto scheme. If not known beforehand, it can be retrieved
* from the source through {@code getDrmInfo} or registering a {@code onDrmInfoListener}.
*
* @throws IllegalStateException if called before prepare(), or the DRM was
* prepared already
* @throws UnsupportedSchemeException if the crypto scheme is not supported
* @throws ResourceBusyException if required DRM resources are in use
* @throws ProvisioningNetworkErrorException if provisioning is required but failed due to a
* network error
* @throws ProvisioningServerErrorException if provisioning is required but failed due to
* the request denied by the provisioning server
*/
@Override
public void prepareDrm(@NonNull UUID uuid)
throws UnsupportedSchemeException, ResourceBusyException,
ProvisioningNetworkErrorException, ProvisioningServerErrorException
{
Log.v(TAG, "prepareDrm: uuid: " + uuid + " mOnDrmConfigHelper: " + mOnDrmConfigHelper);
boolean allDoneWithoutProvisioning = false;
synchronized (mDrmLock) {
// only allowing if tied to a protected source; might relax for releasing offline keys
if (mDrmInfoImpl == null) {
final String msg = "prepareDrm(): Wrong usage: The player must be prepared and " +
"DRM info be retrieved before this call.";
Log.e(TAG, msg);
throw new IllegalStateException(msg);
}
if (mActiveDrmScheme) {
final String msg = "prepareDrm(): Wrong usage: There is already " +
"an active DRM scheme with " + mDrmUUID;
Log.e(TAG, msg);
throw new IllegalStateException(msg);
}
if (mPrepareDrmInProgress) {
final String msg = "prepareDrm(): Wrong usage: There is already " +
"a pending prepareDrm call.";
Log.e(TAG, msg);
throw new IllegalStateException(msg);
}
if (mDrmProvisioningInProgress) {
final String msg = "prepareDrm(): Unexpectd: Provisioning is already in progress.";
Log.e(TAG, msg);
throw new IllegalStateException(msg);
}
// shouldn't need this; just for safeguard
cleanDrmObj();
mPrepareDrmInProgress = true;
try {
// only creating the DRM object to allow pre-openSession configuration
prepareDrm_createDrmStep(uuid);
} catch (Exception e) {
Log.w(TAG, "prepareDrm(): Exception ", e);
mPrepareDrmInProgress = false;
throw e;
}
mDrmConfigAllowed = true;
} // synchronized
// call the callback outside the lock
if (mOnDrmConfigHelper != null) {
mOnDrmConfigHelper.onDrmConfig(this, mCurrentDSD);
}
synchronized (mDrmLock) {
mDrmConfigAllowed = false;
boolean earlyExit = false;
try {
prepareDrm_openSessionStep(uuid);
mDrmUUID = uuid;
mActiveDrmScheme = true;
allDoneWithoutProvisioning = true;
} catch (IllegalStateException e) {
final String msg = "prepareDrm(): Wrong usage: The player must be " +
"in the prepared state to call prepareDrm().";
Log.e(TAG, msg);
earlyExit = true;
throw new IllegalStateException(msg);
} catch (NotProvisionedException e) {
Log.w(TAG, "prepareDrm: NotProvisionedException");
// handle provisioning internally; it'll reset mPrepareDrmInProgress
int result = HandleProvisioninig(uuid);
// if blocking mode, we're already done;
// if non-blocking mode, we attempted to launch background provisioning
if (result != PREPARE_DRM_STATUS_SUCCESS) {
earlyExit = true;
String msg;
switch (result) {
case PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR:
msg = "prepareDrm: Provisioning was required but failed " +
"due to a network error.";
Log.e(TAG, msg);
throw new ProvisioningNetworkErrorExceptionImpl(msg);
case PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR:
msg = "prepareDrm: Provisioning was required but the request " +
"was denied by the server.";
Log.e(TAG, msg);
throw new ProvisioningServerErrorExceptionImpl(msg);
case PREPARE_DRM_STATUS_PREPARATION_ERROR:
default: // default for safeguard
msg = "prepareDrm: Post-provisioning preparation failed.";
Log.e(TAG, msg);
throw new IllegalStateException(msg);
}
}
// nothing else to do;
// if blocking or non-blocking, HandleProvisioninig does the re-attempt & cleanup
} catch (Exception e) {
Log.e(TAG, "prepareDrm: Exception " + e);
earlyExit = true;
throw e;
} finally {
if (!mDrmProvisioningInProgress) {// if early exit other than provisioning exception
mPrepareDrmInProgress = false;
}
if (earlyExit) { // cleaning up object if didn't succeed
cleanDrmObj();
}
} // finally
} // synchronized
// if finished successfully without provisioning, call the callback outside the lock
if (allDoneWithoutProvisioning) {
synchronized (mDrmEventCbLock) {
for (Pair<Executor, DrmEventCallback> cb : mDrmEventCallbackRecords) {
cb.first.execute(() -> cb.second.onDrmPrepared(
this, mCurrentDSD, PREPARE_DRM_STATUS_SUCCESS));
}
}
}
}
private native void _releaseDrm();
/**
* Releases the DRM session
* <p>
* The player has to have an active DRM session and be in stopped, or prepared
* state before this call is made.
* A {@code reset()} call will release the DRM session implicitly.
*
* @throws NoDrmSchemeException if there is no active DRM session to release
*/
@Override
public void releaseDrm()
throws NoDrmSchemeException
{
addTask(new Task(CALL_COMPLETED_RELEASE_DRM, false) {
@Override
void process() throws NoDrmSchemeException {
synchronized (mDrmLock) {
Log.v(TAG, "releaseDrm:");
if (!mActiveDrmScheme) {
Log.e(TAG, "releaseDrm(): No active DRM scheme to release.");
throw new NoDrmSchemeExceptionImpl(
"releaseDrm: No active DRM scheme to release.");
}
try {
// we don't have the player's state in this layer. The below call raises
// exception if we're in a non-stopped/prepared state.
// for cleaning native/mediaserver crypto object
_releaseDrm();
// for cleaning client-side MediaDrm object; only called if above has succeeded
cleanDrmObj();
mActiveDrmScheme = false;
} catch (IllegalStateException e) {
Log.w(TAG, "releaseDrm: Exception ", e);
throw new IllegalStateException(
"releaseDrm: The player is not in a valid state.");
} catch (Exception e) {
Log.e(TAG, "releaseDrm: Exception ", e);
}
} // synchronized
}
});
}
/**
* A key request/response exchange occurs between the app and a license server
* to obtain or release keys used to decrypt encrypted content.
* <p>
* getDrmKeyRequest() is used to obtain an opaque key request byte array that is
* delivered to the license server. The opaque key request byte array is returned
* in KeyRequest.data. The recommended URL to deliver the key request to is
* returned in KeyRequest.defaultUrl.
* <p>
* After the app has received the key request response from the server,
* it should deliver to the response to the DRM engine plugin using the method
* {@link #provideDrmKeyResponse}.
*
* @param keySetId is the key-set identifier of the offline keys being released when keyType is
* {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when
* keyType is {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}.
*
* @param initData is the container-specific initialization data when the keyType is
* {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. Its meaning is
* interpreted based on the mime type provided in the mimeType parameter. It could
* contain, for example, the content ID, key ID or other data obtained from the content
* metadata that is required in generating the key request.
* When the keyType is {@link MediaDrm#KEY_TYPE_RELEASE}, it should be set to null.
*
* @param mimeType identifies the mime type of the content
*
* @param keyType specifies the type of the request. The request may be to acquire
* keys for streaming, {@link MediaDrm#KEY_TYPE_STREAMING}, or for offline content
* {@link MediaDrm#KEY_TYPE_OFFLINE}, or to release previously acquired
* keys ({@link MediaDrm#KEY_TYPE_RELEASE}), which are identified by a keySetId.
*
* @param optionalParameters are included in the key request message to
* allow a client application to provide additional message parameters to the server.
* This may be {@code null} if no additional parameters are to be sent.
*
* @throws NoDrmSchemeException if there is no active DRM session
*/
@Override
@NonNull
public MediaDrm.KeyRequest getDrmKeyRequest(@Nullable byte[] keySetId, @Nullable byte[] initData,
@Nullable String mimeType, @MediaDrm.KeyType int keyType,
@Nullable Map<String, String> optionalParameters)
throws NoDrmSchemeException
{
Log.v(TAG, "getDrmKeyRequest: " +
" keySetId: " + keySetId + " initData:" + initData + " mimeType: " + mimeType +
" keyType: " + keyType + " optionalParameters: " + optionalParameters);
synchronized (mDrmLock) {
if (!mActiveDrmScheme) {
Log.e(TAG, "getDrmKeyRequest NoDrmSchemeException");
throw new NoDrmSchemeExceptionImpl(
"getDrmKeyRequest: Has to set a DRM scheme first.");
}
try {
byte[] scope = (keyType != MediaDrm.KEY_TYPE_RELEASE) ?
mDrmSessionId : // sessionId for KEY_TYPE_STREAMING/OFFLINE
keySetId; // keySetId for KEY_TYPE_RELEASE
HashMap<String, String> hmapOptionalParameters =
(optionalParameters != null) ?
new HashMap<String, String>(optionalParameters) :
null;
MediaDrm.KeyRequest request = mDrmObj.getKeyRequest(scope, initData, mimeType,
keyType, hmapOptionalParameters);
Log.v(TAG, "getDrmKeyRequest: --> request: " + request);
return request;
} catch (NotProvisionedException e) {
Log.w(TAG, "getDrmKeyRequest NotProvisionedException: " +
"Unexpected. Shouldn't have reached here.");
throw new IllegalStateException("getDrmKeyRequest: Unexpected provisioning error.");
} catch (Exception e) {
Log.w(TAG, "getDrmKeyRequest Exception " + e);
throw e;
}
} // synchronized
}
/**
* A key response is received from the license server by the app, then it is
* provided to the DRM engine plugin using provideDrmKeyResponse. When the
* response is for an offline key request, a key-set identifier is returned that
* can be used to later restore the keys to a new session with the method
* {@ link # restoreDrmKeys}.
* When the response is for a streaming or release request, null is returned.
*
* @param keySetId When the response is for a release request, keySetId identifies
* the saved key associated with the release request (i.e., the same keySetId
* passed to the earlier {@ link #getDrmKeyRequest} call. It MUST be null when the
* response is for either streaming or offline key requests.
*
* @param response the byte array response from the server
*
* @throws NoDrmSchemeException if there is no active DRM session
* @throws DeniedByServerException if the response indicates that the
* server rejected the request
*/
@Override
public byte[] provideDrmKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
throws NoDrmSchemeException, DeniedByServerException
{
Log.v(TAG, "provideDrmKeyResponse: keySetId: " + keySetId + " response: " + response);
synchronized (mDrmLock) {
if (!mActiveDrmScheme) {
Log.e(TAG, "getDrmKeyRequest NoDrmSchemeException");
throw new NoDrmSchemeExceptionImpl(
"getDrmKeyRequest: Has to set a DRM scheme first.");
}
try {
byte[] scope = (keySetId == null) ?
mDrmSessionId : // sessionId for KEY_TYPE_STREAMING/OFFLINE
keySetId; // keySetId for KEY_TYPE_RELEASE
byte[] keySetResult = mDrmObj.provideKeyResponse(scope, response);
Log.v(TAG, "provideDrmKeyResponse: keySetId: " + keySetId + " response: " + response
+ " --> " + keySetResult);
return keySetResult;
} catch (NotProvisionedException e) {
Log.w(TAG, "provideDrmKeyResponse NotProvisionedException: " +
"Unexpected. Shouldn't have reached here.");
throw new IllegalStateException("provideDrmKeyResponse: " +
"Unexpected provisioning error.");
} catch (Exception e) {
Log.w(TAG, "provideDrmKeyResponse Exception " + e);
throw e;
}
} // synchronized
}
/**
* Restore persisted offline keys into a new session. keySetId identifies the
* keys to load, obtained from a prior call to {@link #provideDrmKeyResponse}.
*
* @param keySetId identifies the saved key set to restore
*/
@Override
public void restoreDrmKeys(@NonNull byte[] keySetId)
throws NoDrmSchemeException
{
addTask(new Task(CALL_COMPLETED_RESTORE_DRM_KEYS, false) {
@Override
void process() throws NoDrmSchemeException {
Log.v(TAG, "restoreDrmKeys: keySetId: " + keySetId);
synchronized (mDrmLock) {
if (!mActiveDrmScheme) {
Log.w(TAG, "restoreDrmKeys NoDrmSchemeException");
throw new NoDrmSchemeExceptionImpl(
"restoreDrmKeys: Has to set a DRM scheme first.");
}
try {
mDrmObj.restoreKeys(mDrmSessionId, keySetId);
} catch (Exception e) {
Log.w(TAG, "restoreKeys Exception " + e);
throw e;
}
} // synchronized
}
});
}
/**
* Read a DRM engine plugin String property value, given the property name string.
* <p>
* @param propertyName the property name
*
* Standard fields names are:
* {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
* {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
*/
@Override
@NonNull
public String getDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName)
throws NoDrmSchemeException
{
Log.v(TAG, "getDrmPropertyString: propertyName: " + propertyName);
String value;
synchronized (mDrmLock) {
if (!mActiveDrmScheme && !mDrmConfigAllowed) {
Log.w(TAG, "getDrmPropertyString NoDrmSchemeException");
throw new NoDrmSchemeExceptionImpl(
"getDrmPropertyString: Has to prepareDrm() first.");
}
try {
value = mDrmObj.getPropertyString(propertyName);
} catch (Exception e) {
Log.w(TAG, "getDrmPropertyString Exception " + e);
throw e;
}
} // synchronized
Log.v(TAG, "getDrmPropertyString: propertyName: " + propertyName + " --> value: " + value);
return value;
}
/**
* Set a DRM engine plugin String property value.
* <p>
* @param propertyName the property name
* @param value the property value
*
* Standard fields names are:
* {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
* {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
*/
@Override
public void setDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName,
@NonNull String value)
throws NoDrmSchemeException
{
Log.v(TAG, "setDrmPropertyString: propertyName: " + propertyName + " value: " + value);
synchronized (mDrmLock) {
if ( !mActiveDrmScheme && !mDrmConfigAllowed ) {
Log.w(TAG, "setDrmPropertyString NoDrmSchemeException");
throw new NoDrmSchemeExceptionImpl(
"setDrmPropertyString: Has to prepareDrm() first.");
}
try {
mDrmObj.setPropertyString(propertyName, value);
} catch ( Exception e ) {
Log.w(TAG, "setDrmPropertyString Exception " + e);
throw e;
}
} // synchronized
}
/**
* Encapsulates the DRM properties of the source.
*/
public static final class DrmInfoImpl extends DrmInfo {
private Map<UUID, byte[]> mapPssh;
private UUID[] supportedSchemes;
/**
* Returns the PSSH info of the data source for each supported DRM scheme.
*/
@Override
public Map<UUID, byte[]> getPssh() {
return mapPssh;
}
/**
* Returns the intersection of the data source and the device DRM schemes.
* It effectively identifies the subset of the source's DRM schemes which
* are supported by the device too.
*/
@Override
public List<UUID> getSupportedSchemes() {
return Arrays.asList(supportedSchemes);
}
private DrmInfoImpl(Map<UUID, byte[]> Pssh, UUID[] SupportedSchemes) {
mapPssh = Pssh;
supportedSchemes = SupportedSchemes;
}
private DrmInfoImpl(Parcel parcel) {
Log.v(TAG, "DrmInfoImpl(" + parcel + ") size " + parcel.dataSize());
int psshsize = parcel.readInt();
byte[] pssh = new byte[psshsize];
parcel.readByteArray(pssh);
Log.v(TAG, "DrmInfoImpl() PSSH: " + arrToHex(pssh));
mapPssh = parsePSSH(pssh, psshsize);
Log.v(TAG, "DrmInfoImpl() PSSH: " + mapPssh);
int supportedDRMsCount = parcel.readInt();
supportedSchemes = new UUID[supportedDRMsCount];
for (int i = 0; i < supportedDRMsCount; i++) {
byte[] uuid = new byte[16];
parcel.readByteArray(uuid);
supportedSchemes[i] = bytesToUUID(uuid);
Log.v(TAG, "DrmInfoImpl() supportedScheme[" + i + "]: " +
supportedSchemes[i]);
}
Log.v(TAG, "DrmInfoImpl() Parcel psshsize: " + psshsize +
" supportedDRMsCount: " + supportedDRMsCount);
}
private DrmInfoImpl makeCopy() {
return new DrmInfoImpl(this.mapPssh, this.supportedSchemes);
}
private String arrToHex(byte[] bytes) {
String out = "0x";
for (int i = 0; i < bytes.length; i++) {
out += String.format("%02x", bytes[i]);
}
return out;
}
private UUID bytesToUUID(byte[] uuid) {
long msb = 0, lsb = 0;
for (int i = 0; i < 8; i++) {
msb |= ( ((long)uuid[i] & 0xff) << (8 * (7 - i)) );
lsb |= ( ((long)uuid[i+8] & 0xff) << (8 * (7 - i)) );
}
return new UUID(msb, lsb);
}
private Map<UUID, byte[]> parsePSSH(byte[] pssh, int psshsize) {
Map<UUID, byte[]> result = new HashMap<UUID, byte[]>();
final int UUID_SIZE = 16;
final int DATALEN_SIZE = 4;
int len = psshsize;
int numentries = 0;
int i = 0;
while (len > 0) {
if (len < UUID_SIZE) {
Log.w(TAG, String.format("parsePSSH: len is too short to parse " +
"UUID: (%d < 16) pssh: %d", len, psshsize));
return null;
}
byte[] subset = Arrays.copyOfRange(pssh, i, i + UUID_SIZE);
UUID uuid = bytesToUUID(subset);
i += UUID_SIZE;
len -= UUID_SIZE;
// get data length
if (len < 4) {
Log.w(TAG, String.format("parsePSSH: len is too short to parse " +
"datalen: (%d < 4) pssh: %d", len, psshsize));
return null;
}
subset = Arrays.copyOfRange(pssh, i, i+DATALEN_SIZE);
int datalen = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) ?
((subset[3] & 0xff) << 24) | ((subset[2] & 0xff) << 16) |
((subset[1] & 0xff) << 8) | (subset[0] & 0xff) :
((subset[0] & 0xff) << 24) | ((subset[1] & 0xff) << 16) |
((subset[2] & 0xff) << 8) | (subset[3] & 0xff) ;
i += DATALEN_SIZE;
len -= DATALEN_SIZE;
if (len < datalen) {
Log.w(TAG, String.format("parsePSSH: len is too short to parse " +
"data: (%d < %d) pssh: %d", len, datalen, psshsize));
return null;
}
byte[] data = Arrays.copyOfRange(pssh, i, i+datalen);
// skip the data
i += datalen;
len -= datalen;
Log.v(TAG, String.format("parsePSSH[%d]: <%s, %s> pssh: %d",
numentries, uuid, arrToHex(data), psshsize));
numentries++;
result.put(uuid, data);
}
return result;
}
}; // DrmInfoImpl
/**
* Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm().
* Extends MediaDrm.MediaDrmException
*/
public static final class NoDrmSchemeExceptionImpl extends NoDrmSchemeException {
public NoDrmSchemeExceptionImpl(String detailMessage) {
super(detailMessage);
}
}
/**
* Thrown when the device requires DRM provisioning but the provisioning attempt has
* failed due to a network error (Internet reachability, timeout, etc.).
* Extends MediaDrm.MediaDrmException
*/
public static final class ProvisioningNetworkErrorExceptionImpl
extends ProvisioningNetworkErrorException {
public ProvisioningNetworkErrorExceptionImpl(String detailMessage) {
super(detailMessage);
}
}
/**
* Thrown when the device requires DRM provisioning but the provisioning attempt has
* failed due to the provisioning server denying the request.
* Extends MediaDrm.MediaDrmException
*/
public static final class ProvisioningServerErrorExceptionImpl
extends ProvisioningServerErrorException {
public ProvisioningServerErrorExceptionImpl(String detailMessage) {
super(detailMessage);
}
}
private native void _prepareDrm(@NonNull byte[] uuid, @NonNull byte[] drmSessionId);
// Modular DRM helpers
private void prepareDrm_createDrmStep(@NonNull UUID uuid)
throws UnsupportedSchemeException {
Log.v(TAG, "prepareDrm_createDrmStep: UUID: " + uuid);
try {
mDrmObj = new MediaDrm(uuid);
Log.v(TAG, "prepareDrm_createDrmStep: Created mDrmObj=" + mDrmObj);
} catch (Exception e) { // UnsupportedSchemeException
Log.e(TAG, "prepareDrm_createDrmStep: MediaDrm failed with " + e);
throw e;
}
}
private void prepareDrm_openSessionStep(@NonNull UUID uuid)
throws NotProvisionedException, ResourceBusyException {
Log.v(TAG, "prepareDrm_openSessionStep: uuid: " + uuid);
// TODO: don't need an open session for a future specialKeyReleaseDrm mode but we should do
// it anyway so it raises provisioning error if needed. We'd rather handle provisioning
// at prepareDrm/openSession rather than getDrmKeyRequest/provideDrmKeyResponse
try {
mDrmSessionId = mDrmObj.openSession();
Log.v(TAG, "prepareDrm_openSessionStep: mDrmSessionId=" + mDrmSessionId);
// Sending it down to native/mediaserver to create the crypto object
// This call could simply fail due to bad player state, e.g., after play().
_prepareDrm(getByteArrayFromUUID(uuid), mDrmSessionId);
Log.v(TAG, "prepareDrm_openSessionStep: _prepareDrm/Crypto succeeded");
} catch (Exception e) { //ResourceBusyException, NotProvisionedException
Log.e(TAG, "prepareDrm_openSessionStep: open/crypto failed with " + e);
throw e;
}
}
// Called from the native side
@SuppressWarnings("unused")
private static boolean setAudioOutputDeviceById(AudioTrack track, int deviceId) {
if (track == null) {
return false;
}
if (deviceId == 0) {
// Use default routing.
track.setPreferredDevice(null);
return true;
}
// TODO: Unhide AudioManager.getDevicesStatic.
AudioDeviceInfo[] outputDevices =
AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS);
boolean success = false;
for (AudioDeviceInfo device : outputDevices) {
if (device.getId() == deviceId) {
track.setPreferredDevice(device);
success = true;
break;
}
}
return success;
}
// Instantiated from the native side
@SuppressWarnings("unused")
private static class StreamEventCallback extends AudioTrack.StreamEventCallback {
public long mJAudioTrackPtr;
public long mNativeCallbackPtr;
public long mUserDataPtr;
public StreamEventCallback(long jAudioTrackPtr, long nativeCallbackPtr, long userDataPtr) {
super();
mJAudioTrackPtr = jAudioTrackPtr;
mNativeCallbackPtr = nativeCallbackPtr;
mUserDataPtr = userDataPtr;
}
@Override
public void onTearDown(AudioTrack track) {
native_stream_event_onTearDown(mNativeCallbackPtr, mUserDataPtr);
}
@Override
public void onStreamPresentationEnd(AudioTrack track) {
native_stream_event_onStreamPresentationEnd(mNativeCallbackPtr, mUserDataPtr);
}
@Override
public void onStreamDataRequest(AudioTrack track) {
native_stream_event_onStreamDataRequest(
mJAudioTrackPtr, mNativeCallbackPtr, mUserDataPtr);
}
}
private class ProvisioningThread extends Thread {
public static final int TIMEOUT_MS = 60000;
private UUID uuid;
private String urlStr;
private Object drmLock;
private MediaPlayer2Impl mediaPlayer;
private int status;
private boolean finished;
public int status() {
return status;
}
public ProvisioningThread initialize(MediaDrm.ProvisionRequest request,
UUID uuid, MediaPlayer2Impl mediaPlayer) {
// lock is held by the caller
drmLock = mediaPlayer.mDrmLock;
this.mediaPlayer = mediaPlayer;
urlStr = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
this.uuid = uuid;
status = PREPARE_DRM_STATUS_PREPARATION_ERROR;
Log.v(TAG, "HandleProvisioninig: Thread is initialised url: " + urlStr);
return this;
}
public void run() {
byte[] response = null;
boolean provisioningSucceeded = false;
try {
URL url = new URL(urlStr);
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
try {
connection.setRequestMethod("POST");
connection.setDoOutput(false);
connection.setDoInput(true);
connection.setConnectTimeout(TIMEOUT_MS);
connection.setReadTimeout(TIMEOUT_MS);
connection.connect();
response = Streams.readFully(connection.getInputStream());
Log.v(TAG, "HandleProvisioninig: Thread run: response " +
response.length + " " + response);
} catch (Exception e) {
status = PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR;
Log.w(TAG, "HandleProvisioninig: Thread run: connect " + e + " url: " + url);
} finally {
connection.disconnect();
}
} catch (Exception e) {
status = PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR;
Log.w(TAG, "HandleProvisioninig: Thread run: openConnection " + e);
}
if (response != null) {
try {
mDrmObj.provideProvisionResponse(response);
Log.v(TAG, "HandleProvisioninig: Thread run: " +
"provideProvisionResponse SUCCEEDED!");
provisioningSucceeded = true;
} catch (Exception e) {
status = PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR;
Log.w(TAG, "HandleProvisioninig: Thread run: " +
"provideProvisionResponse " + e);
}
}
boolean succeeded = false;
boolean hasCallback = false;
synchronized (mDrmEventCbLock) {
hasCallback = !mDrmEventCallbackRecords.isEmpty();
}
// non-blocking mode needs the lock
if (hasCallback) {
synchronized (drmLock) {
// continuing with prepareDrm
if (provisioningSucceeded) {
succeeded = mediaPlayer.resumePrepareDrm(uuid);
status = (succeeded) ?
PREPARE_DRM_STATUS_SUCCESS :
PREPARE_DRM_STATUS_PREPARATION_ERROR;
}
mediaPlayer.mDrmProvisioningInProgress = false;
mediaPlayer.mPrepareDrmInProgress = false;
if (!succeeded) {
cleanDrmObj(); // cleaning up if it hasn't gone through while in the lock
}
} // synchronized
// calling the callback outside the lock
synchronized (mDrmEventCbLock) {
for (Pair<Executor, DrmEventCallback> cb : mDrmEventCallbackRecords) {
cb.first.execute(() -> cb.second.onDrmPrepared(
mediaPlayer, mCurrentDSD, status));
}
}
} else { // blocking mode already has the lock
// continuing with prepareDrm
if (provisioningSucceeded) {
succeeded = mediaPlayer.resumePrepareDrm(uuid);
status = (succeeded) ?
PREPARE_DRM_STATUS_SUCCESS :
PREPARE_DRM_STATUS_PREPARATION_ERROR;
}
mediaPlayer.mDrmProvisioningInProgress = false;
mediaPlayer.mPrepareDrmInProgress = false;
if (!succeeded) {
cleanDrmObj(); // cleaning up if it hasn't gone through
}
}
finished = true;
} // run()
} // ProvisioningThread
private int HandleProvisioninig(UUID uuid) {
// the lock is already held by the caller
if (mDrmProvisioningInProgress) {
Log.e(TAG, "HandleProvisioninig: Unexpected mDrmProvisioningInProgress");
return PREPARE_DRM_STATUS_PREPARATION_ERROR;
}
MediaDrm.ProvisionRequest provReq = mDrmObj.getProvisionRequest();
if (provReq == null) {
Log.e(TAG, "HandleProvisioninig: getProvisionRequest returned null.");
return PREPARE_DRM_STATUS_PREPARATION_ERROR;
}
Log.v(TAG, "HandleProvisioninig provReq " +
" data: " + provReq.getData() + " url: " + provReq.getDefaultUrl());
// networking in a background thread
mDrmProvisioningInProgress = true;
mDrmProvisioningThread = new ProvisioningThread().initialize(provReq, uuid, this);
mDrmProvisioningThread.start();
int result;
// non-blocking: this is not the final result
boolean hasCallback = false;
synchronized (mDrmEventCbLock) {
hasCallback = !mDrmEventCallbackRecords.isEmpty();
}
if (hasCallback) {
result = PREPARE_DRM_STATUS_SUCCESS;
} else {
// if blocking mode, wait till provisioning is done
try {
mDrmProvisioningThread.join();
} catch (Exception e) {
Log.w(TAG, "HandleProvisioninig: Thread.join Exception " + e);
}
result = mDrmProvisioningThread.status();
// no longer need the thread
mDrmProvisioningThread = null;
}
return result;
}
private boolean resumePrepareDrm(UUID uuid) {
Log.v(TAG, "resumePrepareDrm: uuid: " + uuid);
// mDrmLock is guaranteed to be held
boolean success = false;
try {
// resuming
prepareDrm_openSessionStep(uuid);
mDrmUUID = uuid;
mActiveDrmScheme = true;
success = true;
} catch (Exception e) {
Log.w(TAG, "HandleProvisioninig: Thread run _prepareDrm resume failed with " + e);
// mDrmObj clean up is done by the caller
}
return success;
}
private void resetDrmState() {
synchronized (mDrmLock) {
Log.v(TAG, "resetDrmState: " +
" mDrmInfoImpl=" + mDrmInfoImpl +
" mDrmProvisioningThread=" + mDrmProvisioningThread +
" mPrepareDrmInProgress=" + mPrepareDrmInProgress +
" mActiveDrmScheme=" + mActiveDrmScheme);
mDrmInfoResolved = false;
mDrmInfoImpl = null;
if (mDrmProvisioningThread != null) {
// timeout; relying on HttpUrlConnection
try {
mDrmProvisioningThread.join();
}
catch (InterruptedException e) {
Log.w(TAG, "resetDrmState: ProvThread.join Exception " + e);
}
mDrmProvisioningThread = null;
}
mPrepareDrmInProgress = false;
mActiveDrmScheme = false;
cleanDrmObj();
} // synchronized
}
private void cleanDrmObj() {
// the caller holds mDrmLock
Log.v(TAG, "cleanDrmObj: mDrmObj=" + mDrmObj + " mDrmSessionId=" + mDrmSessionId);
if (mDrmSessionId != null) {
mDrmObj.closeSession(mDrmSessionId);
mDrmSessionId = null;
}
if (mDrmObj != null) {
mDrmObj.release();
mDrmObj = null;
}
}
private static final byte[] getByteArrayFromUUID(@NonNull UUID uuid) {
long msb = uuid.getMostSignificantBits();
long lsb = uuid.getLeastSignificantBits();
byte[] uuidBytes = new byte[16];
for (int i = 0; i < 8; ++i) {
uuidBytes[i] = (byte)(msb >>> (8 * (7 - i)));
uuidBytes[8 + i] = (byte)(lsb >>> (8 * (7 - i)));
}
return uuidBytes;
}
// Modular DRM end
/*
* Test whether a given video scaling mode is supported.
*/
private boolean isVideoScalingModeSupported(int mode) {
return (mode == VIDEO_SCALING_MODE_SCALE_TO_FIT ||
mode == VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING);
}
/** @hide */
static class TimeProvider implements MediaTimeProvider {
private static final String TAG = "MTP";
private static final long MAX_NS_WITHOUT_POSITION_CHECK = 5000000000L;
private static final long MAX_EARLY_CALLBACK_US = 1000;
private static final long TIME_ADJUSTMENT_RATE = 2; /* meaning 1/2 */
private long mLastTimeUs = 0;
private MediaPlayer2Impl mPlayer;
private boolean mPaused = true;
private boolean mStopped = true;
private boolean mBuffering;
private long mLastReportedTime;
// since we are expecting only a handful listeners per stream, there is
// no need for log(N) search performance
private MediaTimeProvider.OnMediaTimeListener mListeners[];
private long mTimes[];
private EventHandler mEventHandler;
private boolean mRefresh = false;
private boolean mPausing = false;
private boolean mSeeking = false;
private static final int NOTIFY = 1;
private static final int NOTIFY_TIME = 0;
private static final int NOTIFY_STOP = 2;
private static final int NOTIFY_SEEK = 3;
private static final int NOTIFY_TRACK_DATA = 4;
private HandlerThread mHandlerThread;
/** @hide */
public boolean DEBUG = false;
public TimeProvider(MediaPlayer2Impl mp) {
mPlayer = mp;
try {
getCurrentTimeUs(true, false);
} catch (IllegalStateException e) {
// we assume starting position
mRefresh = true;
}
Looper looper;
if ((looper = Looper.myLooper()) == null &&
(looper = Looper.getMainLooper()) == null) {
// Create our own looper here in case MP was created without one
mHandlerThread = new HandlerThread("MediaPlayer2MTPEventThread",
Process.THREAD_PRIORITY_FOREGROUND);
mHandlerThread.start();
looper = mHandlerThread.getLooper();
}
mEventHandler = new EventHandler(looper);
mListeners = new MediaTimeProvider.OnMediaTimeListener[0];
mTimes = new long[0];
mLastTimeUs = 0;
}
private void scheduleNotification(int type, long delayUs) {
// ignore time notifications until seek is handled
if (mSeeking && type == NOTIFY_TIME) {
return;
}
if (DEBUG) Log.v(TAG, "scheduleNotification " + type + " in " + delayUs);
mEventHandler.removeMessages(NOTIFY);
Message msg = mEventHandler.obtainMessage(NOTIFY, type, 0);
mEventHandler.sendMessageDelayed(msg, (int) (delayUs / 1000));
}
/** @hide */
public void close() {
mEventHandler.removeMessages(NOTIFY);
if (mHandlerThread != null) {
mHandlerThread.quitSafely();
mHandlerThread = null;
}
}
/** @hide */
protected void finalize() {
if (mHandlerThread != null) {
mHandlerThread.quitSafely();
}
}
/** @hide */
public void onNotifyTime() {
synchronized (this) {
if (DEBUG) Log.d(TAG, "onNotifyTime: ");
scheduleNotification(NOTIFY_TIME, 0 /* delay */);
}
}
/** @hide */
public void onPaused(boolean paused) {
synchronized(this) {
if (DEBUG) Log.d(TAG, "onPaused: " + paused);
if (mStopped) { // handle as seek if we were stopped
mStopped = false;
mSeeking = true;
scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
} else {
mPausing = paused; // special handling if player disappeared
mSeeking = false;
scheduleNotification(NOTIFY_TIME, 0 /* delay */);
}
}
}
/** @hide */
public void onBuffering(boolean buffering) {
synchronized (this) {
if (DEBUG) Log.d(TAG, "onBuffering: " + buffering);
mBuffering = buffering;
scheduleNotification(NOTIFY_TIME, 0 /* delay */);
}
}
/** @hide */
public void onStopped() {
synchronized(this) {
if (DEBUG) Log.d(TAG, "onStopped");
mPaused = true;
mStopped = true;
mSeeking = false;
mBuffering = false;
scheduleNotification(NOTIFY_STOP, 0 /* delay */);
}
}
/** @hide */
public void onSeekComplete(MediaPlayer2Impl mp) {
synchronized(this) {
mStopped = false;
mSeeking = true;
scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
}
}
/** @hide */
public void onNewPlayer() {
if (mRefresh) {
synchronized(this) {
mStopped = false;
mSeeking = true;
mBuffering = false;
scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
}
}
}
private synchronized void notifySeek() {
mSeeking = false;
try {
long timeUs = getCurrentTimeUs(true, false);
if (DEBUG) Log.d(TAG, "onSeekComplete at " + timeUs);
for (MediaTimeProvider.OnMediaTimeListener listener: mListeners) {
if (listener == null) {
break;
}
listener.onSeek(timeUs);
}
} catch (IllegalStateException e) {
// we should not be there, but at least signal pause
if (DEBUG) Log.d(TAG, "onSeekComplete but no player");
mPausing = true; // special handling if player disappeared
notifyTimedEvent(false /* refreshTime */);
}
}
private synchronized void notifyTrackData(Pair<SubtitleTrack, byte[]> trackData) {
SubtitleTrack track = trackData.first;
byte[] data = trackData.second;
track.onData(data, true /* eos */, ~0 /* runID: keep forever */);
}
private synchronized void notifyStop() {
for (MediaTimeProvider.OnMediaTimeListener listener: mListeners) {
if (listener == null) {
break;
}
listener.onStop();
}
}
private int registerListener(MediaTimeProvider.OnMediaTimeListener listener) {
int i = 0;
for (; i < mListeners.length; i++) {
if (mListeners[i] == listener || mListeners[i] == null) {
break;
}
}
// new listener
if (i >= mListeners.length) {
MediaTimeProvider.OnMediaTimeListener[] newListeners =
new MediaTimeProvider.OnMediaTimeListener[i + 1];
long[] newTimes = new long[i + 1];
System.arraycopy(mListeners, 0, newListeners, 0, mListeners.length);
System.arraycopy(mTimes, 0, newTimes, 0, mTimes.length);
mListeners = newListeners;
mTimes = newTimes;
}
if (mListeners[i] == null) {
mListeners[i] = listener;
mTimes[i] = MediaTimeProvider.NO_TIME;
}
return i;
}
public void notifyAt(
long timeUs, MediaTimeProvider.OnMediaTimeListener listener) {
synchronized(this) {
if (DEBUG) Log.d(TAG, "notifyAt " + timeUs);
mTimes[registerListener(listener)] = timeUs;
scheduleNotification(NOTIFY_TIME, 0 /* delay */);
}
}
public void scheduleUpdate(MediaTimeProvider.OnMediaTimeListener listener) {
synchronized(this) {
if (DEBUG) Log.d(TAG, "scheduleUpdate");
int i = registerListener(listener);
if (!mStopped) {
mTimes[i] = 0;
scheduleNotification(NOTIFY_TIME, 0 /* delay */);
}
}
}
public void cancelNotifications(
MediaTimeProvider.OnMediaTimeListener listener) {
synchronized(this) {
int i = 0;
for (; i < mListeners.length; i++) {
if (mListeners[i] == listener) {
System.arraycopy(mListeners, i + 1,
mListeners, i, mListeners.length - i - 1);
System.arraycopy(mTimes, i + 1,
mTimes, i, mTimes.length - i - 1);
mListeners[mListeners.length - 1] = null;
mTimes[mTimes.length - 1] = NO_TIME;
break;
} else if (mListeners[i] == null) {
break;
}
}
scheduleNotification(NOTIFY_TIME, 0 /* delay */);
}
}
private synchronized void notifyTimedEvent(boolean refreshTime) {
// figure out next callback
long nowUs;
try {
nowUs = getCurrentTimeUs(refreshTime, true);
} catch (IllegalStateException e) {
// assume we paused until new player arrives
mRefresh = true;
mPausing = true; // this ensures that call succeeds
nowUs = getCurrentTimeUs(refreshTime, true);
}
long nextTimeUs = nowUs;
if (mSeeking) {
// skip timed-event notifications until seek is complete
return;
}
if (DEBUG) {
StringBuilder sb = new StringBuilder();
sb.append("notifyTimedEvent(").append(mLastTimeUs).append(" -> ")
.append(nowUs).append(") from {");
boolean first = true;
for (long time: mTimes) {
if (time == NO_TIME) {
continue;
}
if (!first) sb.append(", ");
sb.append(time);
first = false;
}
sb.append("}");
Log.d(TAG, sb.toString());
}
Vector<MediaTimeProvider.OnMediaTimeListener> activatedListeners =
new Vector<MediaTimeProvider.OnMediaTimeListener>();
for (int ix = 0; ix < mTimes.length; ix++) {
if (mListeners[ix] == null) {
break;
}
if (mTimes[ix] <= NO_TIME) {
// ignore, unless we were stopped
} else if (mTimes[ix] <= nowUs + MAX_EARLY_CALLBACK_US) {
activatedListeners.add(mListeners[ix]);
if (DEBUG) Log.d(TAG, "removed");
mTimes[ix] = NO_TIME;
} else if (nextTimeUs == nowUs || mTimes[ix] < nextTimeUs) {
nextTimeUs = mTimes[ix];
}
}
if (nextTimeUs > nowUs && !mPaused) {
// schedule callback at nextTimeUs
if (DEBUG) Log.d(TAG, "scheduling for " + nextTimeUs + " and " + nowUs);
mPlayer.notifyAt(nextTimeUs);
} else {
mEventHandler.removeMessages(NOTIFY);
// no more callbacks
}
for (MediaTimeProvider.OnMediaTimeListener listener: activatedListeners) {
listener.onTimedEvent(nowUs);
}
}
public long getCurrentTimeUs(boolean refreshTime, boolean monotonic)
throws IllegalStateException {
synchronized (this) {
// we always refresh the time when the paused-state changes, because
// we expect to have received the pause-change event delayed.
if (mPaused && !refreshTime) {
return mLastReportedTime;
}
try {
mLastTimeUs = mPlayer.getCurrentPosition() * 1000L;
mPaused = !mPlayer.isPlaying() || mBuffering;
if (DEBUG) Log.v(TAG, (mPaused ? "paused" : "playing") + " at " + mLastTimeUs);
} catch (IllegalStateException e) {
if (mPausing) {
// if we were pausing, get last estimated timestamp
mPausing = false;
if (!monotonic || mLastReportedTime < mLastTimeUs) {
mLastReportedTime = mLastTimeUs;
}
mPaused = true;
if (DEBUG) Log.d(TAG, "illegal state, but pausing: estimating at " + mLastReportedTime);
return mLastReportedTime;
}
// TODO get time when prepared
throw e;
}
if (monotonic && mLastTimeUs < mLastReportedTime) {
/* have to adjust time */
if (mLastReportedTime - mLastTimeUs > 1000000) {
// schedule seeked event if time jumped significantly
// TODO: do this properly by introducing an exception
mStopped = false;
mSeeking = true;
scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
}
} else {
mLastReportedTime = mLastTimeUs;
}
return mLastReportedTime;
}
}
private class EventHandler extends Handler {
public EventHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
if (msg.what == NOTIFY) {
switch (msg.arg1) {
case NOTIFY_TIME:
notifyTimedEvent(true /* refreshTime */);
break;
case NOTIFY_STOP:
notifyStop();
break;
case NOTIFY_SEEK:
notifySeek();
break;
case NOTIFY_TRACK_DATA:
notifyTrackData((Pair<SubtitleTrack, byte[]>)msg.obj);
break;
}
}
}
}
}
private abstract class Task implements Runnable {
private final int mMediaCallType;
private final boolean mNeedToWaitForEventToComplete;
private DataSourceDesc mDSD;
public Task (int mediaCallType, boolean needToWaitForEventToComplete) {
mMediaCallType = mediaCallType;
mNeedToWaitForEventToComplete = needToWaitForEventToComplete;
}
abstract void process() throws IOException, NoDrmSchemeException;
@Override
public void run() {
int status = CALL_STATUS_NO_ERROR;
try {
process();
} catch (IllegalStateException e) {
status = CALL_STATUS_INVALID_OPERATION;
} catch (IllegalArgumentException e) {
status = CALL_STATUS_BAD_VALUE;
} catch (SecurityException e) {
status = CALL_STATUS_PERMISSION_DENIED;
} catch (IOException e) {
status = CALL_STATUS_ERROR_IO;
} catch (NoDrmSchemeException e) {
status = CALL_STATUS_NO_DRM_SCHEME;
} catch (Exception e) {
status = CALL_STATUS_ERROR_UNKNOWN;
}
synchronized (mSrcLock) {
mDSD = mCurrentDSD;
}
// TODO: Make native implementations asynchronous and let them send notifications.
if (!mNeedToWaitForEventToComplete || status != CALL_STATUS_NO_ERROR) {
sendCompleteNotification(status);
synchronized (mTaskLock) {
mCurrentTask = null;
processPendingTask_l();
}
}
}
private void sendCompleteNotification(int status) {
// In {@link #notifyWhenCommandLabelReached} case, a separate callback
// {#link #onCommandLabelReached} is already called in {@code process()}.
if (mMediaCallType == CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED) {
return;
}
synchronized (mEventCbLock) {
for (Pair<Executor, MediaPlayer2EventCallback> cb : mEventCallbackRecords) {
cb.first.execute(() -> cb.second.onCallCompleted(
MediaPlayer2Impl.this, mDSD, mMediaCallType, status));
}
}
}
};
}