From 3d99856f80ca23ce4e10bb3efcf7cefc65ff7337 Mon Sep 17 00:00:00 2001 From: Lajos Molnar Date: Thu, 15 Aug 2013 17:05:05 -0700 Subject: Add MediaTimeProvider to MediaPlayer Change-Id: Ie56331ef4eb4bdffa606598f241edb1cb2c2e2dc Signed-off-by: Lajos Molnar Bug: 10326117 --- media/java/android/media/MediaPlayer.java | 383 +++++++++++++++++++++++++++++- 1 file changed, 382 insertions(+), 1 deletion(-) (limited to 'media') diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java index 1b9bdaf..bcb1cbd 100644 --- a/media/java/android/media/MediaPlayer.java +++ b/media/java/android/media/MediaPlayer.java @@ -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 activatedListeners = + new Vector(); + 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; + } + } } -- cgit v1.1