blob: 3b3e119ed377f9090065acaa45e3c670198d6520 [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 androidx.media;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.annotation.TargetApi;
import android.graphics.SurfaceTexture;
import android.media.AudioAttributes;
import android.media.DeniedByServerException;
import android.media.MediaDataSource;
import android.media.MediaDrm;
import android.media.MediaFormat;
import android.media.MediaPlayer;
import android.media.MediaTimestamp;
import android.media.PlaybackParams;
import android.media.ResourceBusyException;
import android.media.SyncParams;
import android.media.TimedMetaData;
import android.media.UnsupportedSchemeException;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import android.view.Surface;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
import java.io.IOException;
import java.nio.ByteOrder;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @hide
*/
@TargetApi(Build.VERSION_CODES.P)
@RestrictTo(LIBRARY_GROUP)
public final class MediaPlayer2Impl extends MediaPlayer2 {
private static final String TAG = "MediaPlayer2Impl";
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;
private static ArrayMap<Integer, Integer> sInfoEventMap;
private static ArrayMap<Integer, Integer> sErrorEventMap;
static {
sInfoEventMap = new ArrayMap<>();
sInfoEventMap.put(MediaPlayer.MEDIA_INFO_UNKNOWN, MEDIA_INFO_UNKNOWN);
sInfoEventMap.put(2 /*MediaPlayer.MEDIA_INFO_STARTED_AS_NEXT*/, MEDIA_INFO_STARTED_AS_NEXT);
sInfoEventMap.put(
MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START, MEDIA_INFO_VIDEO_RENDERING_START);
sInfoEventMap.put(
MediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING, MEDIA_INFO_VIDEO_TRACK_LAGGING);
sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BUFFERING_START, MEDIA_INFO_BUFFERING_START);
sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BUFFERING_END, MEDIA_INFO_BUFFERING_END);
sInfoEventMap.put(MediaPlayer.MEDIA_INFO_BAD_INTERLEAVING, MEDIA_INFO_BAD_INTERLEAVING);
sInfoEventMap.put(MediaPlayer.MEDIA_INFO_NOT_SEEKABLE, MEDIA_INFO_NOT_SEEKABLE);
sInfoEventMap.put(MediaPlayer.MEDIA_INFO_METADATA_UPDATE, MEDIA_INFO_METADATA_UPDATE);
sInfoEventMap.put(MediaPlayer.MEDIA_INFO_AUDIO_NOT_PLAYING, MEDIA_INFO_AUDIO_NOT_PLAYING);
sInfoEventMap.put(MediaPlayer.MEDIA_INFO_VIDEO_NOT_PLAYING, MEDIA_INFO_VIDEO_NOT_PLAYING);
sInfoEventMap.put(
MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, MEDIA_INFO_UNSUPPORTED_SUBTITLE);
sInfoEventMap.put(MediaPlayer.MEDIA_INFO_SUBTITLE_TIMED_OUT, MEDIA_INFO_SUBTITLE_TIMED_OUT);
sErrorEventMap = new ArrayMap<>();
sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNKNOWN);
sErrorEventMap.put(
MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK,
MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK);
sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_IO, MEDIA_ERROR_IO);
sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_MALFORMED, MEDIA_ERROR_MALFORMED);
sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_UNSUPPORTED, MEDIA_ERROR_UNSUPPORTED);
sErrorEventMap.put(MediaPlayer.MEDIA_ERROR_TIMED_OUT, MEDIA_ERROR_TIMED_OUT);
}
private MediaPlayer mPlayer; // MediaPlayer is thread-safe.
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;
private HandlerThread mHandlerThread;
private final Handler mTaskHandler;
private final Object mTaskLock = new Object();
@GuardedBy("mTaskLock")
private final ArrayDeque<Task> mPendingTasks = new ArrayDeque<>();
@GuardedBy("mTaskLock")
private Task mCurrentTask;
private final Object mLock = new Object();
//--- guarded by |mLock| start
@PlayerState private int mPlayerState;
@BuffState private int mBufferingState;
private AudioAttributesCompat mAudioAttributes;
private ArrayList<Pair<Executor, MediaPlayer2EventCallback>> mMp2EventCallbackRecords =
new ArrayList<>();
private ArrayMap<PlayerEventCallback, Executor> mPlayerEventCallbackMap =
new ArrayMap<>();
private ArrayList<Pair<Executor, DrmEventCallback>> mDrmEventCallbackRecords =
new ArrayList<>();
//--- guarded by |mLock| end
/**
* 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() {
mHandlerThread = new HandlerThread("MediaPlayer2TaskThread");
mHandlerThread.start();
Looper looper = mHandlerThread.getLooper();
mTaskHandler = new Handler(looper);
mPlayer = new MediaPlayer();
mPlayerState = PLAYER_STATE_IDLE;
mBufferingState = BUFFERING_STATE_UNKNOWN;
setUpListeners();
}
/**
* 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() {
mPlayer.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() {
mPlayer.start();
setPlayerState(PLAYER_STATE_PLAYING);
}
});
}
/**
* 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() throws IOException {
mPlayer.prepareAsync();
setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
}
});
}
/**
* 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() {
mPlayer.pause();
setPlayerState(PLAYER_STATE_PAUSED);
}
});
}
/**
* 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 long getCurrentPosition() {
return mPlayer.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 long getDuration() {
return mPlayer.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() {
synchronized (mLock) {
return mPlayerState;
}
}
/**
* 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() {
synchronized (mLock) {
return mBufferingState;
}
}
/**
* 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 final AudioAttributesCompat attributes) {
addTask(new Task(CALL_COMPLETED_SET_AUDIO_ATTRIBUTES, false) {
@Override
void process() {
AudioAttributes attr;
synchronized (mLock) {
mAudioAttributes = attributes;
attr = (AudioAttributes) mAudioAttributes.unwrap();
}
mPlayer.setAudioAttributes(attr);
}
});
}
@Override
public @NonNull AudioAttributesCompat getAudioAttributes() {
synchronized (mLock) {
return mAudioAttributes;
}
}
/**
* 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 final 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 final 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;
}
/* FIXME : define and handle state.
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 final 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;
}
/* FIXME : define and handle state.
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(final boolean loop) {
addTask(new Task(CALL_COMPLETED_LOOP_CURRENT, false) {
@Override
void process() {
mPlayer.setLooping(loop);
}
});
}
/**
* 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(final float speed) {
addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_SPEED, false) {
@Override
void process() {
setPlaybackParamsInternal(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(final float volume) {
addTask(new Task(CALL_COMPLETED_SET_PLAYER_VOLUME, false) {
@Override
void process() {
mVolume = volume;
mPlayer.setVolume(volume, volume);
}
});
}
/**
* 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) {
if (cb == null) {
throw new IllegalArgumentException("Illegal null PlayerEventCallback");
}
if (e == null) {
throw new IllegalArgumentException(
"Illegal null Executor for the PlayerEventCallback");
}
synchronized (mLock) {
mPlayerEventCallbackMap.put(cb, e);
}
}
/**
* Removes a previously registered callback for player events
* @param cb the callback to remove
*/
@Override
public void unregisterPlayerEventCallback(@NonNull PlayerEventCallback cb) {
if (cb == null) {
throw new IllegalArgumentException("Illegal null PlayerEventCallback");
}
synchronized (mLock) {
mPlayerEventCallbackMap.remove(cb);
}
}
@Override
public void notifyWhenCommandLabelReached(final Object label) {
addTask(new Task(CALL_COMPLETED_NOTIFY_WHEN_COMMAND_LABEL_REACHED, false) {
@Override
void process() {
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
cb.onCommandLabelReached(MediaPlayer2Impl.this, label);
}
});
}
});
}
/**
* Sets the {@link Surface} to be used as the sink for the video portion of
* the media. 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(final Surface surface) {
addTask(new Task(CALL_COMPLETED_SET_SURFACE, false) {
@Override
void process() {
mPlayer.setSurface(surface);
}
});
}
/**
* Discards all pending commands.
*/
@Override
public void clearPendingCommands() {
// TODO: implement this.
}
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.removeFirst();
mCurrentTask = task;
mTaskHandler.post(task);
}
}
private void handleDataSource(boolean isCurrent, @NonNull final DataSourceDesc dsd, long srcId)
throws IOException {
Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
// TODO: handle the case isCurrent is false.
switch (dsd.getType()) {
case DataSourceDesc.TYPE_CALLBACK:
mPlayer.setDataSource(new MediaDataSource() {
Media2DataSource mDataSource = dsd.getMedia2DataSource();
@Override
public int readAt(long position, byte[] buffer, int offset, int size)
throws IOException {
return mDataSource.readAt(position, buffer, offset, size);
}
@Override
public long getSize() throws IOException {
return mDataSource.getSize();
}
@Override
public void close() throws IOException {
mDataSource.close();
}
});
break;
case DataSourceDesc.TYPE_FD:
mPlayer.setDataSource(
dsd.getFileDescriptor(),
dsd.getFileDescriptorOffset(),
dsd.getFileDescriptorLength());
break;
case DataSourceDesc.TYPE_URI:
mPlayer.setDataSource(
dsd.getUriContext(),
dsd.getUri(),
dsd.getUriHeaders(),
dsd.getUriCookies());
break;
default:
break;
}
}
/**
* 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 int getVideoWidth() {
return mPlayer.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 int getVideoHeight() {
return mPlayer.getVideoHeight();
}
@Override
public PersistableBundle getMetrics() {
return mPlayer.getMetrics();
}
/**
* 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 final PlaybackParams params) {
addTask(new Task(CALL_COMPLETED_SET_PLAYBACK_PARAMS, false) {
@Override
void process() {
setPlaybackParamsInternal(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 PlaybackParams getPlaybackParams() {
return mPlayer.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 final SyncParams params) {
addTask(new Task(CALL_COMPLETED_SET_SYNC_PARAMS, false) {
@Override
void process() {
mPlayer.setSyncParams(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 SyncParams getSyncParams() {
return mPlayer.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 final int mode) {
addTask(new Task(CALL_COMPLETED_SEEK_TO, true) {
@Override
void process() {
mPlayer.seekTo(msec, 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() {
return mPlayer.getTimestamp();
}
/**
* 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() {
mPlayer.reset();
setPlayerState(PLAYER_STATE_IDLE);
setBufferingState(BUFFERING_STATE_UNKNOWN);
/* FIXME: reset other internal variables. */
}
/**
* 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(final int sessionId) {
addTask(new Task(CALL_COMPLETED_SET_AUDIO_SESSION_ID, false) {
@Override
void process() {
mPlayer.setAudioSessionId(sessionId);
}
});
}
@Override
public int getAudioSessionId() {
return mPlayer.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(final int effectId) {
addTask(new Task(CALL_COMPLETED_ATTACH_AUX_EFFECT, false) {
@Override
void process() {
mPlayer.attachAuxEffect(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(final float level) {
addTask(new Task(CALL_COMPLETED_SET_AUX_EFFECT_SEND_LEVEL, false) {
@Override
void process() {
mPlayer.setAuxEffectSendLevel(level);
}
});
}
/**
* Class for MediaPlayer2 to return each audio/video/subtitle track's metadata.
*
* @see MediaPlayer2#getTrackInfo
*/
public static final class TrackInfoImpl extends TrackInfo {
final int mTrackType;
final MediaFormat mFormat;
/**
* 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;
}
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());
}
}
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];
}
};
};
/**
* 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() {
MediaPlayer.TrackInfo[] list = mPlayer.getTrackInfo();
List<TrackInfo> trackList = new ArrayList<>();
for (MediaPlayer.TrackInfo info : list) {
trackList.add(new TrackInfoImpl(info.getTrackType(), info.getFormat()));
}
return trackList;
}
/**
* 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) {
return mPlayer.getSelectedTrack(trackType);
}
/**
* 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 MediaPlayer2#getTrackInfo
*/
@Override
public void selectTrack(final int index) {
addTask(new Task(CALL_COMPLETED_SELECT_TRACK, false) {
@Override
void process() {
mPlayer.selectTrack(index);
}
});
}
/**
* 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 MediaPlayer2#getTrackInfo
*/
@Override
public void deselectTrack(final int index) {
addTask(new Task(CALL_COMPLETED_DESELECT_TRACK, false) {
@Override
void process() {
mPlayer.deselectTrack(index);
}
});
}
/**
* 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 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 (mLock) {
mMp2EventCallbackRecords.add(new Pair(executor, eventCallback));
}
}
/**
* Clears the {@link MediaPlayer2EventCallback}.
*/
@Override
public void clearMediaPlayer2EventCallback() {
synchronized (mLock) {
mMp2EventCallbackRecords.clear();
}
}
// 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(final OnDrmConfigHelper listener) {
mPlayer.setOnDrmConfigHelper(new MediaPlayer.OnDrmConfigHelper() {
@Override
public void onDrmConfig(MediaPlayer mp) {
/** FIXME: pass the right DSD. */
listener.onDrmConfig(MediaPlayer2Impl.this, null);
}
});
}
/**
* 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 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 (mLock) {
mDrmEventCallbackRecords.add(new Pair(executor, eventCallback));
}
}
/**
* Clears the {@link DrmEventCallback}.
*/
@Override
public void clearDrmEventCallback() {
synchronized (mLock) {
mDrmEventCallbackRecords.clear();
}
}
/**
* Retrieves the DRM Info associated with the current source
*
* @throws IllegalStateException if called before prepare()
*/
@Override
public DrmInfo getDrmInfo() {
MediaPlayer.DrmInfo info = mPlayer.getDrmInfo();
return info == null ? null : new DrmInfoImpl(info.getPssh(), info.getSupportedSchemes());
}
/**
* 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 {
try {
mPlayer.prepareDrm(uuid);
} catch (MediaPlayer.ProvisioningNetworkErrorException e) {
throw new ProvisioningNetworkErrorException(e.getMessage());
} catch (MediaPlayer.ProvisioningServerErrorException e) {
throw new ProvisioningServerErrorException(e.getMessage());
}
}
/**
* 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 {
try {
mPlayer.releaseDrm();
} catch (MediaPlayer.NoDrmSchemeException e) {
throw new NoDrmSchemeException(e.getMessage());
}
}
});
}
/**
* 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, int keyType,
@Nullable Map<String, String> optionalParameters)
throws NoDrmSchemeException {
try {
return mPlayer.getKeyRequest(keySetId, initData, mimeType, keyType, optionalParameters);
} catch (MediaPlayer.NoDrmSchemeException e) {
throw new NoDrmSchemeException(e.getMessage());
}
}
/**
* 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 {
try {
return mPlayer.provideKeyResponse(keySetId, response);
} catch (MediaPlayer.NoDrmSchemeException e) {
throw new NoDrmSchemeException(e.getMessage());
}
}
/**
* 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 final byte[] keySetId)
throws NoDrmSchemeException {
addTask(new Task(CALL_COMPLETED_RESTORE_DRM_KEYS, false) {
@Override
void process() throws NoDrmSchemeException {
try {
mPlayer.restoreKeys(keySetId);
} catch (MediaPlayer.NoDrmSchemeException e) {
throw new NoDrmSchemeException(e.getMessage());
}
}
});
}
/**
* 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 String propertyName)
throws NoDrmSchemeException {
try {
return mPlayer.getDrmPropertyString(propertyName);
} catch (MediaPlayer.NoDrmSchemeException e) {
throw new NoDrmSchemeException(e.getMessage());
}
}
/**
* 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 String propertyName,
@NonNull String value)
throws NoDrmSchemeException {
try {
mPlayer.setDrmPropertyString(propertyName, value);
} catch (MediaPlayer.NoDrmSchemeException e) {
throw new NoDrmSchemeException(e.getMessage());
}
}
private void setPlaybackParamsInternal(final PlaybackParams params) {
PlaybackParams current = mPlayer.getPlaybackParams();
mPlayer.setPlaybackParams(params);
if (Math.abs(current.getSpeed() - params.getSpeed()) > 0.0001f) {
notifyPlayerEvent(new PlayerEventNotifier() {
@Override
public void notify(PlayerEventCallback cb) {
cb.onPlaybackSpeedChanged(MediaPlayer2Impl.this, params.getSpeed());
}
});
}
}
private void setPlayerState(@PlayerState final int state) {
synchronized (mLock) {
if (mPlayerState == state) {
return;
}
mPlayerState = state;
}
notifyPlayerEvent(new PlayerEventNotifier() {
@Override
public void notify(PlayerEventCallback cb) {
cb.onPlayerStateChanged(MediaPlayer2Impl.this, state);
}
});
}
private void setBufferingState(@BuffState final int state) {
synchronized (mLock) {
if (mBufferingState == state) {
return;
}
mBufferingState = state;
}
notifyPlayerEvent(new PlayerEventNotifier() {
@Override
public void notify(PlayerEventCallback cb) {
cb.onBufferingStateChanged(MediaPlayer2Impl.this, mCurrentDSD, state);
}
});
}
private void notifyMediaPlayer2Event(final Mp2EventNotifier notifier) {
List<Pair<Executor, MediaPlayer2EventCallback>> records;
synchronized (mLock) {
records = new ArrayList<>(mMp2EventCallbackRecords);
}
for (final Pair<Executor, MediaPlayer2EventCallback> record : records) {
record.first.execute(new Runnable() {
@Override
public void run() {
notifier.notify(record.second);
}
});
}
}
private void notifyPlayerEvent(final PlayerEventNotifier notifier) {
ArrayMap<PlayerEventCallback, Executor> map;
synchronized (mLock) {
map = new ArrayMap<>(mPlayerEventCallbackMap);
}
final int callbackCount = map.size();
for (int i = 0; i < callbackCount; i++) {
final Executor executor = map.valueAt(i);
final PlayerEventCallback cb = map.keyAt(i);
executor.execute(new Runnable() {
@Override
public void run() {
notifier.notify(cb);
}
});
}
}
private interface Mp2EventNotifier {
void notify(MediaPlayer2EventCallback callback);
}
private interface PlayerEventNotifier {
void notify(PlayerEventCallback callback);
}
private void setUpListeners() {
mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
setPlayerState(PLAYER_STATE_PAUSED);
setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback callback) {
callback.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PREPARED, 0);
}
});
notifyPlayerEvent(new PlayerEventNotifier() {
@Override
public void notify(PlayerEventCallback cb) {
cb.onMediaPrepared(MediaPlayer2Impl.this, mCurrentDSD);
}
});
synchronized (mTaskLock) {
if (mCurrentTask != null
&& mCurrentTask.mMediaCallType == CALL_COMPLETED_PREPARE
&& mCurrentTask.mDSD == mCurrentDSD
&& mCurrentTask.mNeedToWaitForEventToComplete) {
mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
mCurrentTask = null;
processPendingTask_l();
}
}
}
});
mPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, final int width, final int height) {
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
cb.onVideoSizeChanged(MediaPlayer2Impl.this, mCurrentDSD, width, height);
}
});
}
});
mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
@Override
public boolean onInfo(MediaPlayer mp, int what, int extra) {
switch (what) {
case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START:
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
MEDIA_INFO_VIDEO_RENDERING_START, 0);
}
});
break;
case MediaPlayer.MEDIA_INFO_BUFFERING_START:
setBufferingState(BUFFERING_STATE_BUFFERING_AND_STARVED);
break;
case MediaPlayer.MEDIA_INFO_BUFFERING_END:
setBufferingState(BUFFERING_STATE_BUFFERING_AND_PLAYABLE);
break;
}
return false;
}
});
mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
setPlayerState(PLAYER_STATE_PAUSED);
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, MEDIA_INFO_PLAYBACK_COMPLETE,
0);
}
});
}
});
mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, final int what, final int extra) {
setPlayerState(PLAYER_STATE_ERROR);
setBufferingState(BUFFERING_STATE_UNKNOWN);
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
int w = sErrorEventMap.getOrDefault(what, MEDIA_ERROR_UNKNOWN);
cb.onError(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
}
});
return true;
}
});
mPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
@Override
public void onSeekComplete(MediaPlayer mp) {
synchronized (mTaskLock) {
if (mCurrentTask != null
&& mCurrentTask.mMediaCallType == CALL_COMPLETED_SEEK_TO
&& mCurrentTask.mNeedToWaitForEventToComplete) {
mCurrentTask.sendCompleteNotification(CALL_STATUS_NO_ERROR);
mCurrentTask = null;
processPendingTask_l();
}
}
final long seekPos = getCurrentPosition();
notifyPlayerEvent(new PlayerEventNotifier() {
@Override
public void notify(PlayerEventCallback cb) {
// TODO: The actual seeked position might be different from the
// requested position. Clarify which one is expected here.
cb.onSeekCompleted(MediaPlayer2Impl.this, seekPos);
}
});
}
});
mPlayer.setOnTimedMetaDataAvailableListener(
new MediaPlayer.OnTimedMetaDataAvailableListener() {
@Override
public void onTimedMetaDataAvailable(MediaPlayer mp, final TimedMetaData data) {
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
cb.onTimedMetaDataAvailable(
MediaPlayer2Impl.this, mCurrentDSD, data);
}
});
}
});
mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() {
@Override
public boolean onInfo(MediaPlayer mp, final int what, final int extra) {
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
int w = sInfoEventMap.getOrDefault(what, MEDIA_INFO_UNKNOWN);
cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD, w, extra);
}
});
return true;
}
});
mPlayer.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() {
@Override
public void onBufferingUpdate(MediaPlayer mp, final int percent) {
if (percent >= 100) {
setBufferingState(BUFFERING_STATE_BUFFERING_COMPLETE);
}
mBufferedPercentageCurrent.set(percent);
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
cb.onInfo(MediaPlayer2Impl.this, mCurrentDSD,
MEDIA_INFO_BUFFERING_UPDATE, percent);
}
});
}
});
}
/**
* Encapsulates the DRM properties of the source.
*/
public static final class DrmInfoImpl extends DrmInfo {
private Map<UUID, byte[]> mMapPssh;
private UUID[] mSupportedSchemes;
/**
* Returns the PSSH info of the data source for each supported DRM scheme.
*/
@Override
public Map<UUID, byte[]> getPssh() {
return mMapPssh;
}
/**
* 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(mSupportedSchemes);
}
private DrmInfoImpl(Map<UUID, byte[]> pssh, UUID[] supportedSchemes) {
mMapPssh = pssh;
mSupportedSchemes = 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));
mMapPssh = parsePSSH(pssh, psshsize);
Log.v(TAG, "DrmInfoImpl() PSSH: " + mMapPssh);
int supportedDRMsCount = parcel.readInt();
mSupportedSchemes = new UUID[supportedDRMsCount];
for (int i = 0; i < supportedDRMsCount; i++) {
byte[] uuid = new byte[16];
parcel.readByteArray(uuid);
mSupportedSchemes[i] = bytesToUUID(uuid);
Log.v(TAG, "DrmInfoImpl() supportedScheme[" + i + "]: "
+ mSupportedSchemes[i]);
}
Log.v(TAG, "DrmInfoImpl() Parcel psshsize: " + psshsize
+ " supportedDRMsCount: " + supportedDRMsCount);
}
private DrmInfoImpl makeCopy() {
return new DrmInfoImpl(this.mMapPssh, this.mSupportedSchemes);
}
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 uuidSize = 16;
final int dataLenSize = 4;
int len = psshsize;
int numentries = 0;
int i = 0;
while (len > 0) {
if (len < uuidSize) {
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 + uuidSize);
UUID uuid = bytesToUUID(subset);
i += uuidSize;
len -= uuidSize;
// 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 + dataLenSize);
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 += dataLenSize;
len -= dataLenSize;
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 abstract class Task implements Runnable {
private final int mMediaCallType;
private final boolean mNeedToWaitForEventToComplete;
private DataSourceDesc mDSD;
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;
}
if (!mNeedToWaitForEventToComplete || status != CALL_STATUS_NO_ERROR) {
sendCompleteNotification(status);
synchronized (mTaskLock) {
mCurrentTask = null;
processPendingTask_l();
}
}
}
private void sendCompleteNotification(final 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;
}
notifyMediaPlayer2Event(new Mp2EventNotifier() {
@Override
public void notify(MediaPlayer2EventCallback cb) {
cb.onCallCompleted(
MediaPlayer2Impl.this, mDSD, mMediaCallType, status);
}
});
}
};
}