Commit fa593118 authored by Shakti Sahu's avatar Shakti Sahu Committed by Commit Bot

Video Player : Fixed media observer triggering logic

This CL cleans up the media session observer logic for triggering video player UI and metrics update.

Change-Id: Ib6dcb8173206f7c0f0a5d53ddfbb5bf17654b893
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2465093
Commit-Queue: Shakti Sahu <shaktisahu@chromium.org>
Reviewed-by: default avatarMin Qin <qinmin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#820040}
parent 259e147f
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
package org.chromium.chrome.browser.video_tutorials; package org.chromium.chrome.browser.video_tutorials;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.browser.video_tutorials.PlaybackStateObserver.WatchStateInfo.State;
import org.chromium.content_public.browser.MediaSession; import org.chromium.content_public.browser.MediaSession;
import org.chromium.content_public.browser.MediaSessionObserver; import org.chromium.content_public.browser.MediaSessionObserver;
import org.chromium.content_public.browser.WebContents; import org.chromium.content_public.browser.WebContents;
...@@ -14,6 +16,41 @@ import org.chromium.services.media_session.MediaPosition; ...@@ -14,6 +16,41 @@ import org.chromium.services.media_session.MediaPosition;
* events. * events.
*/ */
public class PlaybackStateObserver extends MediaSessionObserver { public class PlaybackStateObserver extends MediaSessionObserver {
/**
* A ratio used for collecting metrics used to determine whether a video was sufficiently
* watched by the user.
*/
private static final float WATCH_COMPLETION_RATIO_THRESHOLD = 0.5f;
/** Contains playback info about currently playing media. */
public static class WatchStateInfo {
/** Contains various states during the media playback. */
public enum State {
INITIAL,
PLAYING,
PAUSED,
ENDED,
ERROR,
}
/** The current state. */
public State state;
/** The duration of the video. */
public long videoLength;
/** The current position of the video. */
public long currentPosition;
/**
* Whether the video has been watched up to a certain point so that it can be considered as
* completed.
*/
public boolean videoWatched() {
return currentPosition > videoLength * WATCH_COMPLETION_RATIO_THRESHOLD;
}
}
/** /**
* Interface to be notified of playback state updates. * Interface to be notified of playback state updates.
*/ */
...@@ -26,38 +63,104 @@ public class PlaybackStateObserver extends MediaSessionObserver { ...@@ -26,38 +63,104 @@ public class PlaybackStateObserver extends MediaSessionObserver {
/** Called when the player has completed playing the video. */ /** Called when the player has completed playing the video. */
void onEnded(); void onEnded();
/** Called when an error has occurred. */
void onError();
} }
private final Observer mObserver; private final Supplier<Observer> mObserver;
private MediaPosition mMediaPosition; private long mLastUpdateTime;
private WatchStateInfo mWatchStateInfo = new WatchStateInfo();
private MediaPosition mLastPosition;
private boolean mIsControllable;
private boolean mIsSuspended;
/** Constructor. */ /** Constructor. */
public PlaybackStateObserver(WebContents webContents, Observer observer) { public PlaybackStateObserver(WebContents webContents, Supplier<Observer> observer) {
super(MediaSession.fromWebContents(webContents)); super(MediaSession.fromWebContents(webContents));
mObserver = observer; mObserver = observer;
} }
/**
* Called to get the current media playback info, such as duration, current progress, playback
* state etc.
* @return The current watch state info.
*/
public WatchStateInfo getWatchStateInfo() {
return mWatchStateInfo;
}
/** Reset internal state. */
public void reset() {
mLastPosition = null;
mIsControllable = false;
mIsSuspended = false;
mLastUpdateTime = 0;
mWatchStateInfo = new WatchStateInfo();
}
@Override @Override
public void mediaSessionPositionChanged(MediaPosition position) { public void mediaSessionPositionChanged(MediaPosition position) {
if (position == null) return; updateState(position);
mMediaPosition = position; mWatchStateInfo.currentPosition =
computeCurrentTime(position == null ? mLastPosition : position, mLastUpdateTime);
mLastUpdateTime = System.currentTimeMillis();
mLastPosition = position;
} }
@Override @Override
public void mediaSessionStateChanged(boolean isControllable, boolean isSuspended) { public void mediaSessionStateChanged(boolean isControllable, boolean isSuspended) {
boolean playerEnded = !isControllable && isSuspended && mMediaPosition != null mIsControllable = isControllable;
&& mMediaPosition.getPosition() > 0.5 * mMediaPosition.getDuration(); mIsSuspended = isSuspended;
boolean playerPaused = isControllable && isSuspended; }
boolean isPlaying = isControllable && !isSuspended;
// TODO(shaktisahu): Fix these signals and logic in another CL. private void updateState(MediaPosition newPosition) {
if (isPlaying) { State nextState = mWatchStateInfo.state;
mObserver.onPlay(); if (mIsControllable) {
} nextState = mIsSuspended ? State.PAUSED : State.PLAYING;
if (playerPaused) { } else if (newPosition == null) {
mObserver.onPause(); // TODO(shaktisahu): Determine error state.
if (mLastPosition == null) {
nextState = State.INITIAL;
} else if (mLastPosition.getDuration()
== computeCurrentTime(mLastPosition, mLastUpdateTime)) {
nextState = State.ENDED;
}
} }
if (playerEnded) {
mObserver.onEnded(); updateObservers(nextState);
}
private void updateObservers(State nextState) {
if (nextState == mWatchStateInfo.state) return;
mWatchStateInfo.state = nextState;
switch (nextState) {
case INITIAL:
break;
case PLAYING:
mObserver.get().onPlay();
break;
case PAUSED:
mObserver.get().onPause();
break;
case ENDED:
mObserver.get().onEnded();
break;
case ERROR:
mObserver.get().onError();
break;
default:
assert false : "Unknown media playback state";
} }
} }
private static long computeCurrentTime(MediaPosition mediaPosition, long lastUpdateTime) {
if (mediaPosition == null) return 0;
long elapsedTime = System.currentTimeMillis() - lastUpdateTime;
long updatedPosition = (long) (mediaPosition.getPosition()
+ (elapsedTime * mediaPosition.getPlaybackRate()));
updatedPosition = Math.min(updatedPosition, mediaPosition.getDuration());
return updatedPosition;
}
} }
...@@ -18,14 +18,16 @@ import java.lang.annotation.RetentionPolicy; ...@@ -18,14 +18,16 @@ import java.lang.annotation.RetentionPolicy;
public class VideoTutorialMetrics { public class VideoTutorialMetrics {
// Please treat this list as append only and keep it in sync with // Please treat this list as append only and keep it in sync with
// VideoTutorials.WatchState in enums.xml. // VideoTutorials.WatchState in enums.xml.
@IntDef({WatchState.STARTED, WatchState.COMPLETED, WatchState.PAUSED, WatchState.RESUMED}) @IntDef({WatchState.STARTED, WatchState.COMPLETED, WatchState.PAUSED, WatchState.RESUMED,
WatchState.WATCHED})
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
public @interface WatchState { public @interface WatchState {
int STARTED = 0; int STARTED = 0;
int COMPLETED = 1; int COMPLETED = 1;
int PAUSED = 2; int PAUSED = 2;
int RESUMED = 3; int RESUMED = 3;
int NUM_ENTRIES = 4; int WATCHED = 4;
int NUM_ENTRIES = 5;
} }
// Please treat this list as append only and keep it in sync with // Please treat this list as append only and keep it in sync with
...@@ -60,7 +62,7 @@ public class VideoTutorialMetrics { ...@@ -60,7 +62,7 @@ public class VideoTutorialMetrics {
String histogramSuffix = histogramSuffixFromFeatureType(feature); String histogramSuffix = histogramSuffixFromFeatureType(feature);
RecordHistogram.recordEnumeratedHistogram( RecordHistogram.recordEnumeratedHistogram(
"VideoTutorials." + histogramSuffix + ".Player.Progress", state, "VideoTutorials." + histogramSuffix + ".Player.Progress", state,
FeatureType.MAX_VALUE); WatchState.NUM_ENTRIES);
} }
private static String histogramSuffixFromFeatureType(@FeatureType int feature) { private static String histogramSuffixFromFeatureType(@FeatureType int feature) {
......
...@@ -61,7 +61,8 @@ public class VideoPlayerCoordinatorImpl implements VideoPlayerCoordinator { ...@@ -61,7 +61,8 @@ public class VideoPlayerCoordinatorImpl implements VideoPlayerCoordinator {
new LanguagePickerCoordinator(mView.getView().findViewById(R.id.language_picker), new LanguagePickerCoordinator(mView.getView().findViewById(R.id.language_picker),
mVideoTutorialService, languageInfoProvider); mVideoTutorialService, languageInfoProvider);
mMediator = new VideoPlayerMediator(mContext, mModel, videoTutorialService, mLanguagePicker, mMediator = new VideoPlayerMediator(mContext, mModel, videoTutorialService, mLanguagePicker,
languageInfoProvider, mWebContents, tryNowCallback, closeCallback); languageInfoProvider, mWebContents, mMediaSessionObserver, tryNowCallback,
closeCallback);
PropertyModelChangeProcessor.create(mModel, mView, new VideoPlayerViewBinder()); PropertyModelChangeProcessor.create(mModel, mView, new VideoPlayerViewBinder());
} }
...@@ -94,7 +95,8 @@ public class VideoPlayerCoordinatorImpl implements VideoPlayerCoordinator { ...@@ -94,7 +95,8 @@ public class VideoPlayerCoordinatorImpl implements VideoPlayerCoordinator {
mWebContents = pair.first; mWebContents = pair.first;
ContentView webContentView = pair.second; ContentView webContentView = pair.second;
mWebContentsDelegate = new WebContentsDelegateAndroid(); mWebContentsDelegate = new WebContentsDelegateAndroid();
mMediaSessionObserver = new PlaybackStateObserver(mWebContents, mMediator); mMediaSessionObserver =
new PlaybackStateObserver(mWebContents, () -> { return mMediator; });
ThinWebView thinWebView = ThinWebViewFactory.create(mContext, new ThinWebViewConstraints()); ThinWebView thinWebView = ThinWebViewFactory.create(mContext, new ThinWebViewConstraints());
thinWebView.attachWebContents(mWebContents, webContentView, mWebContentsDelegate); thinWebView.attachWebContents(mWebContents, webContentView, mWebContentsDelegate);
......
...@@ -36,13 +36,15 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer { ...@@ -36,13 +36,15 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer {
private final Callback<Tutorial> mTryNowCallback; private final Callback<Tutorial> mTryNowCallback;
private final Runnable mCloseCallback; private final Runnable mCloseCallback;
private final LanguageInfoProvider mLanguageInfoProvider; private final LanguageInfoProvider mLanguageInfoProvider;
private final PlaybackStateObserver mPlaybackStateObserver;
private long mVideoStartTime; private long mVideoStartTime;
/** Constructor. */ /** Constructor. */
public VideoPlayerMediator(Context context, PropertyModel model, public VideoPlayerMediator(Context context, PropertyModel model,
VideoTutorialService videoTutorialService, LanguagePickerCoordinator languagePicker, VideoTutorialService videoTutorialService, LanguagePickerCoordinator languagePicker,
LanguageInfoProvider languageInfoProvider, WebContents webContents, LanguageInfoProvider languageInfoProvider, WebContents webContents,
Callback<Tutorial> tryNowCallback, Runnable closeCallback) { PlaybackStateObserver playbackStateObserver, Callback<Tutorial> tryNowCallback,
Runnable closeCallback) {
mContext = context; mContext = context;
mModel = model; mModel = model;
mVideoTutorialService = videoTutorialService; mVideoTutorialService = videoTutorialService;
...@@ -51,6 +53,7 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer { ...@@ -51,6 +53,7 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer {
mWebContents = webContents; mWebContents = webContents;
mTryNowCallback = tryNowCallback; mTryNowCallback = tryNowCallback;
mCloseCallback = closeCallback; mCloseCallback = closeCallback;
mPlaybackStateObserver = playbackStateObserver;
mModel.set(VideoPlayerProperties.SHOW_LOADING_SCREEN, false); mModel.set(VideoPlayerProperties.SHOW_LOADING_SCREEN, false);
mModel.set(VideoPlayerProperties.SHOW_LANGUAGE_PICKER, false); mModel.set(VideoPlayerProperties.SHOW_LANGUAGE_PICKER, false);
...@@ -62,6 +65,13 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer { ...@@ -62,6 +65,13 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer {
mModel.set(VideoPlayerProperties.CALLBACK_CLOSE, this::close); mModel.set(VideoPlayerProperties.CALLBACK_CLOSE, this::close);
} }
/** Called when the player is getting destroyed. */
public void destroy() {
if (mPlaybackStateObserver.getWatchStateInfo().videoWatched()) {
VideoTutorialMetrics.recordWatchStateUpdate(mTutorial.featureType, WatchState.WATCHED);
}
}
boolean handleBackPressed() { boolean handleBackPressed() {
boolean isShowingLanguagePicker = mModel.get(VideoPlayerProperties.SHOW_LANGUAGE_PICKER); boolean isShowingLanguagePicker = mModel.get(VideoPlayerProperties.SHOW_LANGUAGE_PICKER);
boolean isShowingLoadingScreen = mModel.get(VideoPlayerProperties.SHOW_LOADING_SCREEN); boolean isShowingLoadingScreen = mModel.get(VideoPlayerProperties.SHOW_LOADING_SCREEN);
...@@ -72,7 +82,7 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer { ...@@ -72,7 +82,7 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer {
mTutorial.featureType, UserAction.BACK_PRESS_WHEN_SHOWING_VIDEO_PLAYER); mTutorial.featureType, UserAction.BACK_PRESS_WHEN_SHOWING_VIDEO_PLAYER);
} }
return true; return false;
} }
/** /**
...@@ -125,6 +135,11 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer { ...@@ -125,6 +135,11 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer {
updateChangeLanguageButtonText(); updateChangeLanguageButtonText();
} }
@Override
public void onError() {
// TODO(shaktisahu): Determine UI for error state.
}
private void changeLanguage() { private void changeLanguage() {
mModel.set(VideoPlayerProperties.SHOW_LANGUAGE_PICKER, true); mModel.set(VideoPlayerProperties.SHOW_LANGUAGE_PICKER, true);
mLanguagePicker.showLanguagePicker(this::onLanguageSelected, () -> {} /* closeCallback */); mLanguagePicker.showLanguagePicker(this::onLanguageSelected, () -> {} /* closeCallback */);
...@@ -162,6 +177,7 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer { ...@@ -162,6 +177,7 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer {
} }
private void startVideo(Tutorial tutorial) { private void startVideo(Tutorial tutorial) {
mPlaybackStateObserver.reset();
VideoTutorialMetrics.recordWatchStateUpdate(mTutorial.featureType, WatchState.STARTED); VideoTutorialMetrics.recordWatchStateUpdate(mTutorial.featureType, WatchState.STARTED);
mVideoStartTime = System.currentTimeMillis(); mVideoStartTime = System.currentTimeMillis();
mTutorial = tutorial; mTutorial = tutorial;
...@@ -180,6 +196,9 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer { ...@@ -180,6 +196,9 @@ class VideoPlayerMediator implements PlaybackStateObserver.Observer {
} }
private void onWatchNextClicked() { private void onWatchNextClicked() {
if (mPlaybackStateObserver.getWatchStateInfo().videoWatched()) {
VideoTutorialMetrics.recordWatchStateUpdate(mTutorial.featureType, WatchState.WATCHED);
}
VideoTutorialMetrics.recordUserAction(mTutorial.featureType, UserAction.WATCH_NEXT_VIDEO); VideoTutorialMetrics.recordUserAction(mTutorial.featureType, UserAction.WATCH_NEXT_VIDEO);
VideoTutorialUtils.getNextTutorial(mVideoTutorialService, mTutorial, this::startVideo); VideoTutorialUtils.getNextTutorial(mVideoTutorialService, mTutorial, this::startVideo);
} }
......
...@@ -24,6 +24,7 @@ import org.chromium.base.Callback; ...@@ -24,6 +24,7 @@ import org.chromium.base.Callback;
import org.chromium.base.metrics.test.ShadowRecordHistogram; import org.chromium.base.metrics.test.ShadowRecordHistogram;
import org.chromium.base.test.BaseRobolectricTestRunner; import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.chrome.browser.video_tutorials.LanguageInfoProvider; import org.chromium.chrome.browser.video_tutorials.LanguageInfoProvider;
import org.chromium.chrome.browser.video_tutorials.PlaybackStateObserver;
import org.chromium.chrome.browser.video_tutorials.Tutorial; import org.chromium.chrome.browser.video_tutorials.Tutorial;
import org.chromium.chrome.browser.video_tutorials.VideoTutorialUtils; import org.chromium.chrome.browser.video_tutorials.VideoTutorialUtils;
import org.chromium.chrome.browser.video_tutorials.languages.LanguagePickerCoordinator; import org.chromium.chrome.browser.video_tutorials.languages.LanguagePickerCoordinator;
...@@ -63,6 +64,8 @@ public class VideoPlayerMediatorUnitTest { ...@@ -63,6 +64,8 @@ public class VideoPlayerMediatorUnitTest {
Callback<Tutorial> mTryNowCallback; Callback<Tutorial> mTryNowCallback;
@Mock @Mock
private LanguageInfoProvider mLanguageProvider; private LanguageInfoProvider mLanguageProvider;
@Mock
PlaybackStateObserver mPlaybackStateObserver;
@Before @Before
public void setUp() { public void setUp() {
...@@ -76,7 +79,8 @@ public class VideoPlayerMediatorUnitTest { ...@@ -76,7 +79,8 @@ public class VideoPlayerMediatorUnitTest {
mTestVideoTutorialService = new TestVideoTutorialService(); mTestVideoTutorialService = new TestVideoTutorialService();
mMediator = new VideoPlayerMediator(mContext, mModel, mTestVideoTutorialService, mMediator = new VideoPlayerMediator(mContext, mModel, mTestVideoTutorialService,
mLanguagePicker, mLanguageProvider, mWebContents, mTryNowCallback, mCloseCallback); mLanguagePicker, mLanguageProvider, mWebContents, mPlaybackStateObserver,
mTryNowCallback, mCloseCallback);
} }
@Test @Test
......
...@@ -74773,6 +74773,7 @@ Full version information for the fingerprint enum values: ...@@ -74773,6 +74773,7 @@ Full version information for the fingerprint enum values:
<int value="1" label="Completed"/> <int value="1" label="Completed"/>
<int value="2" label="Paused"/> <int value="2" label="Paused"/>
<int value="3" label="Resumed"/> <int value="3" label="Resumed"/>
<int value="4" label="Watched"/>
</enum> </enum>
<enum name="ViewFileType"> <enum name="ViewFileType">
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment