Created
June 12, 2015 05:08
-
-
Save mgp/a7df4286dc5fcc188e01 to your computer and use it in GitHub Desktop.
VUPU
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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