Skip to content

Instantly share code, notes, and snippets.

@mgp
Created June 12, 2015 05:08
Show Gist options
  • Save mgp/a7df4286dc5fcc188e01 to your computer and use it in GitHub Desktop.
Save mgp/a7df4286dc5fcc188e01 to your computer and use it in GitHub Desktop.
VUPU
package org.khanacademy.android.ui.videos;
import org.khanacademy.android.logging.Logger;
import org.khanacademy.android.ui.videos.VideoViewActivity.VideoPlayer;
import org.khanacademy.core.progress.ProgressUpdater;
import org.khanacademy.core.progress.models.VideoUserProgress;
import org.khanacademy.core.topictree.identifiers.ContentItemIdentifier;
import org.khanacademy.core.util.ObservableUtils;
import com.google.common.base.Preconditions;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import android.util.Log;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* Monitors the playback of a video and updates its progress.
*/
public final class VideoUserProgressUpdater {
/**
* Monitors the last second of the video that the user has watched, and the total number of
* seconds of the video that the user has watched.
*/
private static final class PlaybackTimeMonitor {
private long mLastPlaybackTimeMillis;
private long mTotalPlaybackTimeMillis;
/**
* Creates a {@link PlaybackTimeMonitor} instance and immediately begins monitoring the
* playback of the video.
*/
static PlaybackTimeMonitor createAndBeginMonitoring(final VideoPlayer videoPlayer) {
PlaybackTimeMonitor monitor = new PlaybackTimeMonitor();
monitor.beginMonitoringPlayback(videoPlayer);
return monitor;
}
private PlaybackTimeMonitor() {
mLastPlaybackTimeMillis = 0;
mTotalPlaybackTimeMillis = 0;
}
/**
* Begins monitoring the playback of the video.
*/
private void beginMonitoringPlayback(final VideoPlayer videoPlayer) {
videoPlayer.getVideoPlayingObservable()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(isPlaying -> {
if (isPlaying) {
mLastPlaybackTimeMillis = videoPlayer.getCurrentTime();
}
})
.switchMap(isPlaying -> {
// Update the number of seconds watched every second whenever the video
// stops, and whenever the video stops playback.
if (isPlaying) {
return Observable.timer(1, 1, TimeUnit.SECONDS);
} else {
return Observable.just(null).concatWith(Observable.never());
}
})
.map(o -> videoPlayer.getCurrentTime())
.distinctUntilChanged()
.subscribe(currentPlaybackTimeMillis -> {
final long playbackTimeMillis =
currentPlaybackTimeMillis - mLastPlaybackTimeMillis;
if (playbackTimeMillis > 0) {
mTotalPlaybackTimeMillis += playbackTimeMillis;
}
mLastPlaybackTimeMillis = currentPlaybackTimeMillis;
});
}
/**
* @return the last second of the video that the user watched
*/
long getLastPlaybackTimeSeconds() {
return mLastPlaybackTimeMillis / 1000;
}
/**
* @return the total number of seconds of the video that the user has watched
*/
long getSecondsWatched() {
return mTotalPlaybackTimeMillis / 1000;
}
}
private final ContentItemIdentifier mVideoContentItemId;
private final String mYouTubeId;
private final long mVideoDuration;
private final long mPrevTotalSecondsWatched;
private final PlaybackTimeMonitor mPlaybackTimeMonitor;
/**
* Creates a {@link VideoUserProgressUpdater} instance and immediately begins updating the
* progress of the video as appropriate.
*
* Updating ends when the {@link VideoPlayer#getCurrentTimeObservable()} observable completes.
*/
public static VideoUserProgressUpdater createAndBeginUpdating(
final ContentItemIdentifier videoContentItemId,
final String youTubeId,
final long videoDuration,
final long prevTotalSecondsWatched,
final ProgressUpdater progressUpdater,
final VideoPlayer videoPlayer) {
final PlaybackTimeMonitor monitor =
PlaybackTimeMonitor.createAndBeginMonitoring(videoPlayer);
final VideoUserProgressUpdater updater = new VideoUserProgressUpdater(
videoContentItemId, youTubeId, videoDuration, prevTotalSecondsWatched, monitor);
updater.beginUpdatingProgress(progressUpdater, videoPlayer.getVideoPlayingObservable());
return updater;
}
private VideoUserProgressUpdater(
final ContentItemIdentifier videoContentItemId,
final String youTubeId,
final long videoDuration,
final long prevTotalSecondsWatched,
final PlaybackTimeMonitor playbackTimeMonitor) {
Preconditions.checkArgument(videoDuration >= 0,
"Parameter videoDuration is negative: " + videoDuration);
Preconditions.checkArgument(prevTotalSecondsWatched >= 0,
"Parameter prevTotalSecondsWatched is negative: " + prevTotalSecondsWatched);
mVideoContentItemId = Preconditions.checkNotNull(videoContentItemId);
mYouTubeId = Preconditions.checkNotNull(youTubeId);
mVideoDuration = videoDuration;
mPrevTotalSecondsWatched = prevTotalSecondsWatched;
mPlaybackTimeMonitor = Preconditions.checkNotNull(playbackTimeMonitor);
}
private VideoUserProgress createVideoUserProgress() {
final long lastSecondWatched =
mPlaybackTimeMonitor.getLastPlaybackTimeSeconds();
final long totalSecondsWatched =
mPrevTotalSecondsWatched + mPlaybackTimeMonitor.getSecondsWatched();
final boolean completed = totalSecondsWatched >= mVideoDuration;
return VideoUserProgress.create(
mVideoContentItemId,
completed,
lastSecondWatched,
totalSecondsWatched,
new Date()
);
}
/**
* Begins updating the progress of the video.
*/
void beginUpdatingProgress(final ProgressUpdater progressUpdater,
final Observable<Boolean> playbackObserver) {
// An observable that, after a delay, emits a value whenever playback occurs, and then
// immediately completes.
final Observable<Boolean> cachedPlaybackObserver =
ObservableUtils.cache(playbackObserver, 1);
final Observable<Void> delayedPlayingObservable = Observable
.just(null)
.delay(8, TimeUnit.SECONDS)
.switchMap(o -> cachedPlaybackObserver
.filter(isPlaying -> isPlaying)
.<Void>map(isPlaying -> null)
.take(1)
)
.doOnSubscribe(() -> Log.d("VUPU", "delayedPlaybackObservable subscribing"))
.doOnNext(aVoid -> Log.d("VUPU", "delayedPlaybackObservable emitting"))
.doOnCompleted(() -> Log.d("VUPU", "delayedPlaybackObservable completing"));
// An observable that, after a delay, emits a value whenever the video is paused or
// stopped, and then immediately completes.
final Observable<Void> delayedPlaybackStoppedObservable = Observable
.just(null)
.delay(4, TimeUnit.SECONDS)
.switchMap(o -> playbackObserver
.filter(isPlaying -> !isPlaying)
.<Void>map(playbackState -> null)
.take(1)
)
.doOnSubscribe(() -> Log.d("VUPU", "delayedPlaybackStoppedObservable subscribing"))
.doOnNext(aVoid -> Log.d("VUPU", "delayedPlaybackStoppedObservable emitting"))
.doOnCompleted(() -> Log.d("VUPU", "delayedPlaybackStoppedObservable completing"));
// An observable that, upon subscription, waits for an event that requires setting the
// progress, and then sets the progress, and then completes.
final Observable<Void> setProgressObservable = Observable
.merge(delayedPlayingObservable, delayedPlaybackStoppedObservable)
.switchMap(aVoid -> Observable.defer(() -> {
final VideoUserProgress videoUserProgress = createVideoUserProgress();
return progressUpdater.setVideoProgress(mYouTubeId, videoUserProgress);
}))
.doOnSubscribe(() -> Log.d("VUPU", "setProgressObservable subscribing"))
.doOnNext(aVoid -> Log.d("VUPU", "setProgressObservable emitting"))
.doOnCompleted(() -> Log.d("VUPU", "setProgressObservable completing"));
// Once the user begins playing the video, repeatedly wait for an event that requires
// setting the progress, and then set the progress.
final Observable<Void> continuousSetProgressObservable = playbackObserver
.filter(isPlaying -> isPlaying)
.<Void>map(isPlaying -> null)
.take(1)
.switchMap(aVoid -> setProgressObservable.repeat())
.doOnSubscribe(() -> Log.d("VUPU", "continuousSetProgressObservable subscribing"))
.doOnNext(aVoid -> Log.d("VUPU", "continuousSetProgressObservable emitting"))
.doOnCompleted(() -> Log.d("VUPU", "continuousSetProgressObservable completing"));
ObservableUtils.performOperation(continuousSetProgressObservable,
() -> { /* will complete only when playback observer completes */ },
throwable -> Logger.e(
VideoUserProgressUpdater.class.getSimpleName(),
throwable,
"Could not update video progress"
));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment