Add MediaTimeProvider to MediaPlayer
Change-Id: Ie56331ef4eb4bdffa606598f241edb1cb2c2e2dc Signed-off-by: Lajos Molnar <lajos@google.com> Bug: 10326117
This commit is contained in:
@@ -39,6 +39,8 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.MediaTimeProvider;
|
||||
import android.media.MediaTimeProvider.OnMediaTimeListener;
|
||||
import android.media.SubtitleData;
|
||||
|
||||
import java.io.File;
|
||||
@@ -48,6 +50,7 @@ import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Vector;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
@@ -590,6 +593,8 @@ public class MediaPlayer
|
||||
mEventHandler = null;
|
||||
}
|
||||
|
||||
mTimeProvider = new TimeProvider(this);
|
||||
|
||||
/* Native setup requires a weak reference to our object.
|
||||
* It's easier to create it here than in C++.
|
||||
*/
|
||||
@@ -1337,6 +1342,8 @@ public class MediaPlayer
|
||||
mOnInfoListener = null;
|
||||
mOnVideoSizeChangedListener = null;
|
||||
mOnTimedTextListener = null;
|
||||
mTimeProvider.close();
|
||||
mTimeProvider = null;
|
||||
mOnSubtitleDataListener = null;
|
||||
_release();
|
||||
}
|
||||
@@ -1914,11 +1921,21 @@ public class MediaPlayer
|
||||
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_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 TimeProvider mTimeProvider;
|
||||
|
||||
/** @hide */
|
||||
public MediaTimeProvider getMediaTimeProvider() {
|
||||
return mTimeProvider;
|
||||
}
|
||||
|
||||
private class EventHandler extends Handler
|
||||
{
|
||||
private MediaPlayer mMediaPlayer;
|
||||
@@ -1946,14 +1963,31 @@ public class MediaPlayer
|
||||
stayAwake(false);
|
||||
return;
|
||||
|
||||
case MEDIA_STOPPED:
|
||||
if (mTimeProvider != null) {
|
||||
mTimeProvider.onStopped();
|
||||
}
|
||||
break;
|
||||
|
||||
case MEDIA_STARTED:
|
||||
case MEDIA_PAUSED:
|
||||
if (mTimeProvider != null) {
|
||||
mTimeProvider.onPaused(msg.what == MEDIA_PAUSED);
|
||||
}
|
||||
break;
|
||||
|
||||
case MEDIA_BUFFERING_UPDATE:
|
||||
if (mOnBufferingUpdateListener != null)
|
||||
mOnBufferingUpdateListener.onBufferingUpdate(mMediaPlayer, msg.arg1);
|
||||
return;
|
||||
|
||||
case MEDIA_SEEK_COMPLETE:
|
||||
if (mOnSeekCompleteListener != null)
|
||||
if (mOnSeekCompleteListener != null) {
|
||||
mOnSeekCompleteListener.onSeekComplete(mMediaPlayer);
|
||||
}
|
||||
if (mTimeProvider != null) {
|
||||
mTimeProvider.onSeekComplete(mMediaPlayer);
|
||||
}
|
||||
return;
|
||||
|
||||
case MEDIA_SET_VIDEO_SIZE:
|
||||
@@ -2496,4 +2530,351 @@ public class MediaPlayer
|
||||
}
|
||||
|
||||
private native void updateProxyConfig(ProxyProperties props);
|
||||
|
||||
/** @hide */
|
||||
static class TimeProvider implements MediaPlayer.OnSeekCompleteListener,
|
||||
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 MediaPlayer mPlayer;
|
||||
private boolean mPaused = true;
|
||||
private boolean mStopped = true;
|
||||
private long mLastReportedTime;
|
||||
private long mTimeAdjustment;
|
||||
// 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 long mLastNanoTime;
|
||||
private Handler mEventHandler;
|
||||
private boolean mRefresh = false;
|
||||
private boolean mPausing = false;
|
||||
private static final int NOTIFY = 1;
|
||||
private static final int NOTIFY_TIME = 0;
|
||||
private static final int REFRESH_AND_NOTIFY_TIME = 1;
|
||||
private static final int NOTIFY_STOP = 2;
|
||||
private static final int NOTIFY_SEEK = 3;
|
||||
|
||||
/** @hide */
|
||||
public boolean DEBUG = false;
|
||||
|
||||
public TimeProvider(MediaPlayer mp) {
|
||||
mPlayer = mp;
|
||||
try {
|
||||
getCurrentTimeUs(true, false);
|
||||
} catch (IllegalStateException e) {
|
||||
// we assume starting position
|
||||
mRefresh = true;
|
||||
}
|
||||
mEventHandler = new EventHandler();
|
||||
mListeners = new MediaTimeProvider.OnMediaTimeListener[0];
|
||||
mTimes = new long[0];
|
||||
mLastTimeUs = 0;
|
||||
mTimeAdjustment = 0;
|
||||
}
|
||||
|
||||
private void scheduleNotification(int type, long delayUs) {
|
||||
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);
|
||||
}
|
||||
|
||||
/** @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;
|
||||
scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
|
||||
} else {
|
||||
mPausing = paused; // special handling if player disappeared
|
||||
scheduleNotification(REFRESH_AND_NOTIFY_TIME, 0 /* delay */);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public void onStopped() {
|
||||
synchronized(this) {
|
||||
if (DEBUG) Log.d(TAG, "onStopped");
|
||||
mPaused = true;
|
||||
mStopped = true;
|
||||
scheduleNotification(NOTIFY_STOP, 0 /* delay */);
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
@Override
|
||||
public void onSeekComplete(MediaPlayer mp) {
|
||||
synchronized(this) {
|
||||
mStopped = false;
|
||||
scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public void onNewPlayer() {
|
||||
if (mRefresh) {
|
||||
synchronized(this) {
|
||||
scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void notifySeek() {
|
||||
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 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) {
|
||||
scheduleNotification(NOTIFY_STOP, 0 /* delay */);
|
||||
} else {
|
||||
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 (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);
|
||||
scheduleNotification(NOTIFY_TIME, nextTimeUs - nowUs);
|
||||
} else {
|
||||
mEventHandler.removeMessages(NOTIFY);
|
||||
// no more callbacks
|
||||
}
|
||||
|
||||
for (MediaTimeProvider.OnMediaTimeListener listener: activatedListeners) {
|
||||
listener.onTimedEvent(nowUs);
|
||||
}
|
||||
}
|
||||
|
||||
private long getEstimatedTime(long nanoTime, boolean monotonic) {
|
||||
if (mPaused) {
|
||||
mLastReportedTime = mLastTimeUs + mTimeAdjustment;
|
||||
} else {
|
||||
long timeSinceRead = (nanoTime - mLastNanoTime) / 1000;
|
||||
mLastReportedTime = mLastTimeUs + timeSinceRead;
|
||||
if (mTimeAdjustment > 0) {
|
||||
long adjustment =
|
||||
mTimeAdjustment - timeSinceRead / TIME_ADJUSTMENT_RATE;
|
||||
if (adjustment <= 0) {
|
||||
mTimeAdjustment = 0;
|
||||
} else {
|
||||
mLastReportedTime += adjustment;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mLastReportedTime;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
long nanoTime = System.nanoTime();
|
||||
if (refreshTime ||
|
||||
nanoTime >= mLastNanoTime + MAX_NS_WITHOUT_POSITION_CHECK) {
|
||||
try {
|
||||
mLastTimeUs = mPlayer.getCurrentPosition() * 1000;
|
||||
mPaused = !mPlayer.isPlaying();
|
||||
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;
|
||||
getEstimatedTime(nanoTime, monotonic);
|
||||
mPaused = true;
|
||||
if (DEBUG) Log.d(TAG, "illegal state, but pausing: estimating at " + mLastReportedTime);
|
||||
return mLastReportedTime;
|
||||
}
|
||||
// TODO get time when prepared
|
||||
throw e;
|
||||
}
|
||||
mLastNanoTime = nanoTime;
|
||||
if (monotonic && mLastTimeUs < mLastReportedTime) {
|
||||
/* have to adjust time */
|
||||
mTimeAdjustment = mLastReportedTime - mLastTimeUs;
|
||||
} else {
|
||||
mTimeAdjustment = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return getEstimatedTime(nanoTime, monotonic);
|
||||
}
|
||||
}
|
||||
|
||||
private class EventHandler extends Handler {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
if (msg.what == NOTIFY) {
|
||||
switch (msg.arg1) {
|
||||
case NOTIFY_TIME:
|
||||
notifyTimedEvent(false /* refreshTime */);
|
||||
break;
|
||||
case REFRESH_AND_NOTIFY_TIME:
|
||||
notifyTimedEvent(true /* refreshTime */);
|
||||
break;
|
||||
case NOTIFY_STOP:
|
||||
notifyStop();
|
||||
break;
|
||||
case NOTIFY_SEEK:
|
||||
notifySeek();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @hide */
|
||||
public Handler getHandler() {
|
||||
return mEventHandler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user