Add MediaTimeProvider to MediaPlayer

Change-Id: Ie56331ef4eb4bdffa606598f241edb1cb2c2e2dc
Signed-off-by: Lajos Molnar <lajos@google.com>
Bug: 10326117
This commit is contained in:
Lajos Molnar
2013-08-15 17:05:05 -07:00
parent 6416729c47
commit 3d99856f80

View File

@@ -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;
}
}
}