summaryrefslogtreecommitdiffstats
path: root/media
diff options
context:
space:
mode:
authorLajos Molnar <lajos@google.com>2013-08-15 17:05:05 -0700
committerLajos Molnar <lajos@google.com>2013-09-03 19:23:25 -0700
commit3d99856f80ca23ce4e10bb3efcf7cefc65ff7337 (patch)
tree967773bdd8cdb4112b72a33d5c8b7e26bd647d57 /media
parent6416729c4732be6bcdbc5f303752a9d1885f86d0 (diff)
downloadframeworks_base-3d99856f80ca23ce4e10bb3efcf7cefc65ff7337.zip
frameworks_base-3d99856f80ca23ce4e10bb3efcf7cefc65ff7337.tar.gz
frameworks_base-3d99856f80ca23ce4e10bb3efcf7cefc65ff7337.tar.bz2
Add MediaTimeProvider to MediaPlayer
Change-Id: Ie56331ef4eb4bdffa606598f241edb1cb2c2e2dc Signed-off-by: Lajos Molnar <lajos@google.com> Bug: 10326117
Diffstat (limited to 'media')
-rw-r--r--media/java/android/media/MediaPlayer.java383
1 files changed, 382 insertions, 1 deletions
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<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;
+ }
+ }
}