Correctly save stream progress at the end of a video

This commit is contained in:
Stypox 2021-06-07 09:35:40 +02:00
parent e58feadba9
commit 0113ad5e14
No known key found for this signature in database
GPG key ID: 4BDF1B40A49FDD23
13 changed files with 74 additions and 92 deletions

View file

@ -21,7 +21,7 @@ import static org.schabi.newpipe.database.stream.StreamStatisticsEntry.STREAM_WA
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao @Dao
@ -80,7 +80,7 @@ public abstract class StreamHistoryDAO implements HistoryDAO<StreamHistoryEntity
+ " LEFT JOIN " + " LEFT JOIN "
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
+ STREAM_PROGRESS_TIME + STREAM_PROGRESS_MILLIS
+ " FROM " + STREAM_STATE_TABLE + " )" + " FROM " + STREAM_STATE_TABLE + " )"
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS) + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS)
public abstract Flowable<List<StreamStatisticsEntry>> getStatistics(); public abstract Flowable<List<StreamStatisticsEntry>> getStatistics();

View file

@ -12,8 +12,8 @@ data class PlaylistStreamEntry(
@Embedded @Embedded
val streamEntity: StreamEntity, val streamEntity: StreamEntity,
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME, defaultValue = "0") @ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS, defaultValue = "0")
val progressTime: Long, val progressMillis: Long,
@ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID) @ColumnInfo(name = PlaylistStreamEntity.JOIN_STREAM_ID)
val streamId: Long, val streamId: Long,

View file

@ -25,7 +25,7 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PL
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@Dao @Dao
@ -64,7 +64,7 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
+ " LEFT JOIN " + " LEFT JOIN "
+ "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", " + "(SELECT " + JOIN_STREAM_ID + " AS " + JOIN_STREAM_ID_ALIAS + ", "
+ STREAM_PROGRESS_TIME + STREAM_PROGRESS_MILLIS
+ " FROM " + STREAM_STATE_TABLE + " )" + " FROM " + STREAM_STATE_TABLE + " )"
+ " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS + " ON " + STREAM_ID + " = " + JOIN_STREAM_ID_ALIAS

View file

@ -5,7 +5,7 @@ import androidx.room.Embedded
import org.schabi.newpipe.database.LocalItem import org.schabi.newpipe.database.LocalItem
import org.schabi.newpipe.database.history.model.StreamHistoryEntity import org.schabi.newpipe.database.history.model.StreamHistoryEntity
import org.schabi.newpipe.database.stream.model.StreamEntity import org.schabi.newpipe.database.stream.model.StreamEntity
import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_TIME import org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -13,8 +13,8 @@ class StreamStatisticsEntry(
@Embedded @Embedded
val streamEntity: StreamEntity, val streamEntity: StreamEntity,
@ColumnInfo(name = STREAM_PROGRESS_TIME, defaultValue = "0") @ColumnInfo(name = STREAM_PROGRESS_MILLIS, defaultValue = "0")
val progressTime: Long, val progressMillis: Long,
@ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID) @ColumnInfo(name = StreamHistoryEntity.JOIN_STREAM_ID)
val streamId: Long, val streamId: Long,

View file

@ -9,6 +9,6 @@ data class StreamWithState(
@Embedded @Embedded
val stream: StreamEntity, val stream: StreamEntity,
@ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_TIME) @ColumnInfo(name = StreamStateEntity.STREAM_PROGRESS_MILLIS)
val stateProgressTime: Long? val stateProgressMillis: Long?
) )

View file

@ -23,7 +23,7 @@ public class StreamStateEntity {
// This additional field is required for the SQL query because 'stream_id' is used // This additional field is required for the SQL query because 'stream_id' is used
// for some other joins already // for some other joins already
public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias"; public static final String JOIN_STREAM_ID_ALIAS = "stream_id_alias";
public static final String STREAM_PROGRESS_TIME = "progress_time"; public static final String STREAM_PROGRESS_MILLIS = "progress_time";
/** /**
* Playback state will not be saved, if playback time is less than this threshold. * Playback state will not be saved, if playback time is less than this threshold.
@ -39,12 +39,12 @@ public class StreamStateEntity {
@ColumnInfo(name = JOIN_STREAM_ID) @ColumnInfo(name = JOIN_STREAM_ID)
private long streamUid; private long streamUid;
@ColumnInfo(name = STREAM_PROGRESS_TIME) @ColumnInfo(name = STREAM_PROGRESS_MILLIS)
private long progressTime; private long progressMillis;
public StreamStateEntity(final long streamUid, final long progressTime) { public StreamStateEntity(final long streamUid, final long progressMillis) {
this.streamUid = streamUid; this.streamUid = streamUid;
this.progressTime = progressTime; this.progressMillis = progressMillis;
} }
public long getStreamUid() { public long getStreamUid() {
@ -55,12 +55,12 @@ public class StreamStateEntity {
this.streamUid = streamUid; this.streamUid = streamUid;
} }
public long getProgressTime() { public long getProgressMillis() {
return progressTime; return progressMillis;
} }
public void setProgressTime(final long progressTime) { public void setProgressMillis(final long progressMillis) {
this.progressTime = progressTime; this.progressMillis = progressMillis;
} }
/** /**
@ -69,7 +69,7 @@ public class StreamStateEntity {
* @return whether this stream state entity should be saved or not * @return whether this stream state entity should be saved or not
*/ */
public boolean isValid() { public boolean isValid() {
return progressTime > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS; return progressMillis > PLAYBACK_SAVE_THRESHOLD_START_MILLISECONDS;
} }
/** /**
@ -82,15 +82,15 @@ public class StreamStateEntity {
* @return whether the stream is finished or not * @return whether the stream is finished or not
*/ */
public boolean isFinished(final long durationInSeconds) { public boolean isFinished(final long durationInSeconds) {
return progressTime >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS return progressMillis >= durationInSeconds * 1000 - PLAYBACK_FINISHED_END_MILLISECONDS
&& progressTime >= durationInSeconds * 1000 * 3 / 4; && progressMillis >= durationInSeconds * 1000 * 3 / 4;
} }
@Override @Override
public boolean equals(@Nullable final Object obj) { public boolean equals(@Nullable final Object obj) {
if (obj instanceof StreamStateEntity) { if (obj instanceof StreamStateEntity) {
return ((StreamStateEntity) obj).streamUid == streamUid return ((StreamStateEntity) obj).streamUid == streamUid
&& ((StreamStateEntity) obj).progressTime == progressTime; && ((StreamStateEntity) obj).progressMillis == progressMillis;
} else { } else {
return false; return false;
} }

View file

@ -1669,7 +1669,7 @@ public final class VideoDetailFragment
.onErrorComplete() .onErrorComplete()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(state -> { .subscribe(state -> {
showPlaybackProgress(state.getProgressTime(), info.getDuration() * 1000); showPlaybackProgress(state.getProgressMillis(), info.getDuration() * 1000);
animate(binding.positionView, true, 500); animate(binding.positionView, true, 500);
animate(binding.detailPositionView, true, 500); animate(binding.detailPositionView, true, 500);
}, e -> { }, e -> {

View file

@ -66,7 +66,7 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemProgressView.setVisibility(View.VISIBLE); itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getDuration()); itemProgressView.setMax((int) item.getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(state2.getProgressTime())); .toSeconds(state2.getProgressMillis()));
} else { } else {
itemProgressView.setVisibility(View.GONE); itemProgressView.setVisibility(View.GONE);
} }
@ -121,10 +121,10 @@ public class StreamMiniInfoItemHolder extends InfoItemHolder {
itemProgressView.setMax((int) item.getDuration()); itemProgressView.setMax((int) item.getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) { if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime())); .toSeconds(state.getProgressMillis()));
} else { } else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(state.getProgressTime())); .toSeconds(state.getProgressMillis()));
ViewUtils.animate(itemProgressView, true, 500); ViewUtils.animate(itemProgressView, true, 500);
} }
} else if (itemProgressView.getVisibility() == View.VISIBLE) { } else if (itemProgressView.getVisibility() == View.VISIBLE) {

View file

@ -29,7 +29,7 @@ data class StreamItem(
} }
private val stream: StreamEntity = streamWithState.stream private val stream: StreamEntity = streamWithState.stream
private val stateProgressTime: Long? = streamWithState.stateProgressTime private val stateProgressTime: Long? = streamWithState.stateProgressMillis
override fun getId(): Long = stream.uid override fun getId(): Long = stream.uid

View file

@ -228,14 +228,12 @@ public class HistoryRecordManager {
.subscribeOn(Schedulers.io()); .subscribeOn(Schedulers.io());
} }
public Completable saveStreamState(@NonNull final StreamInfo info, final long progressTime) { public Completable saveStreamState(@NonNull final StreamInfo info, final long progressMillis) {
return Completable.fromAction(() -> database.runInTransaction(() -> { return Completable.fromAction(() -> database.runInTransaction(() -> {
final long streamId = streamTable.upsert(new StreamEntity(info)); final long streamId = streamTable.upsert(new StreamEntity(info));
final StreamStateEntity state = new StreamStateEntity(streamId, progressTime); final StreamStateEntity state = new StreamStateEntity(streamId, progressMillis);
if (state.isValid()) { if (state.isValid()) {
streamStateTable.upsert(state); streamStateTable.upsert(state);
} else {
streamStateTable.deleteState(streamId);
} }
})).subscribeOn(Schedulers.io()); })).subscribeOn(Schedulers.io());
} }

View file

@ -68,11 +68,11 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
R.color.duration_background_color)); R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE); itemDurationView.setVisibility(View.VISIBLE);
if (item.getProgressTime() > 0) { if (item.getProgressMillis() > 0) {
itemProgressView.setVisibility(View.VISIBLE); itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setMax((int) item.getStreamEntity().getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime())); .toSeconds(item.getProgressMillis()));
} else { } else {
itemProgressView.setVisibility(View.GONE); itemProgressView.setVisibility(View.GONE);
} }
@ -109,14 +109,14 @@ public class LocalPlaylistStreamItemHolder extends LocalItemHolder {
} }
final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem; final PlaylistStreamEntry item = (PlaylistStreamEntry) localItem;
if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) { if (item.getProgressMillis() > 0 && item.getStreamEntity().getDuration() > 0) {
itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setMax((int) item.getStreamEntity().getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) { if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime())); .toSeconds(item.getProgressMillis()));
} else { } else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime())); .toSeconds(item.getProgressMillis()));
ViewUtils.animate(itemProgressView, true, 500); ViewUtils.animate(itemProgressView, true, 500);
} }
} else if (itemProgressView.getVisibility() == View.VISIBLE) { } else if (itemProgressView.getVisibility() == View.VISIBLE) {

View file

@ -96,11 +96,11 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
R.color.duration_background_color)); R.color.duration_background_color));
itemDurationView.setVisibility(View.VISIBLE); itemDurationView.setVisibility(View.VISIBLE);
if (item.getProgressTime() > 0) { if (item.getProgressMillis() > 0) {
itemProgressView.setVisibility(View.VISIBLE); itemProgressView.setVisibility(View.VISIBLE);
itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setMax((int) item.getStreamEntity().getDuration());
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime())); .toSeconds(item.getProgressMillis()));
} else { } else {
itemProgressView.setVisibility(View.GONE); itemProgressView.setVisibility(View.GONE);
} }
@ -140,14 +140,14 @@ public class LocalStatisticStreamItemHolder extends LocalItemHolder {
} }
final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem; final StreamStatisticsEntry item = (StreamStatisticsEntry) localItem;
if (item.getProgressTime() > 0 && item.getStreamEntity().getDuration() > 0) { if (item.getProgressMillis() > 0 && item.getStreamEntity().getDuration() > 0) {
itemProgressView.setMax((int) item.getStreamEntity().getDuration()); itemProgressView.setMax((int) item.getStreamEntity().getDuration());
if (itemProgressView.getVisibility() == View.VISIBLE) { if (itemProgressView.getVisibility() == View.VISIBLE) {
itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS itemProgressView.setProgressAnimated((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime())); .toSeconds(item.getProgressMillis()));
} else { } else {
itemProgressView.setProgress((int) TimeUnit.MILLISECONDS itemProgressView.setProgress((int) TimeUnit.MILLISECONDS
.toSeconds(item.getProgressTime())); .toSeconds(item.getProgressMillis()));
ViewUtils.animate(itemProgressView, true, 500); ViewUtils.animate(itemProgressView, true, 500);
} }
} else if (itemProgressView.getVisibility() == View.VISIBLE) { } else if (itemProgressView.getVisibility() == View.VISIBLE) {

View file

@ -674,7 +674,7 @@ public final class Player implements
if (!state.isFinished(newQueue.getItem().getDuration())) { if (!state.isFinished(newQueue.getItem().getDuration())) {
// resume playback only if the stream was not played to the end // resume playback only if the stream was not played to the end
newQueue.setRecovery(newQueue.getIndex(), newQueue.setRecovery(newQueue.getIndex(),
state.getProgressTime()); state.getProgressMillis());
} }
initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch, initPlayback(newQueue, repeatMode, playbackSpeed, playbackPitch,
playbackSkipSilence, playWhenReady, isMuted); playbackSkipSilence, playWhenReady, isMuted);
@ -1939,9 +1939,7 @@ public final class Player implements
break; break;
case com.google.android.exoplayer2.Player.STATE_ENDED: // 4 case com.google.android.exoplayer2.Player.STATE_ENDED: // 4
changeState(STATE_COMPLETED); changeState(STATE_COMPLETED);
if (currentMetadata != null) { saveStreamProgressStateCompleted();
resetStreamProgressState(currentMetadata.getMetadata());
}
isPrepared = false; isPrepared = false;
break; break;
} }
@ -2402,7 +2400,7 @@ public final class Player implements
case DISCONTINUITY_REASON_SEEK_ADJUSTMENT: case DISCONTINUITY_REASON_SEEK_ADJUSTMENT:
case DISCONTINUITY_REASON_INTERNAL: case DISCONTINUITY_REASON_INTERNAL:
if (playQueue.getIndex() != newWindowIndex) { if (playQueue.getIndex() != newWindowIndex) {
resetStreamProgressState(playQueue.getItem()); saveStreamProgressStateCompleted(); // current stream has ended
playQueue.setIndex(newWindowIndex); playQueue.setIndex(newWindowIndex);
} }
break; break;
@ -2793,61 +2791,47 @@ public final class Player implements
} }
} }
private void saveStreamProgressState(final StreamInfo info, final long progress) { private void saveStreamProgressState(final long progressMillis) {
if (info == null) { if (currentMetadata == null
|| !prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
return; return;
} }
if (DEBUG) { if (DEBUG) {
Log.d(TAG, "saveStreamProgressState() called"); Log.d(TAG, "saveStreamProgressState() called with: progressMillis=" + progressMillis
+ ", currentMetadata=[" + currentMetadata.getMetadata().getName() + "]");
} }
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) {
final Disposable stateSaver = recordManager.saveStreamState(info, progress)
.observeOn(AndroidSchedulers.mainThread())
.doOnError((e) -> {
if (DEBUG) {
e.printStackTrace();
}
})
.onErrorComplete()
.subscribe();
databaseUpdateDisposable.add(stateSaver);
}
}
private void resetStreamProgressState(final PlayQueueItem queueItem) { databaseUpdateDisposable
if (queueItem == null) { .add(recordManager.saveStreamState(currentMetadata.getMetadata(), progressMillis)
return; .observeOn(AndroidSchedulers.mainThread())
} .doOnError((e) -> {
if (prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true)) { if (DEBUG) {
final Disposable stateSaver = queueItem.getStream() e.printStackTrace();
.flatMapCompletable(info -> recordManager.saveStreamState(info, 0)) }
.observeOn(AndroidSchedulers.mainThread()) })
.doOnError((e) -> { .onErrorComplete()
if (DEBUG) { .subscribe());
e.printStackTrace();
}
})
.onErrorComplete()
.subscribe();
databaseUpdateDisposable.add(stateSaver);
}
}
private void resetStreamProgressState(final StreamInfo info) {
saveStreamProgressState(info, 0);
} }
public void saveStreamProgressState() { public void saveStreamProgressState() {
if (exoPlayerIsNull() || currentMetadata == null) { if (exoPlayerIsNull() || currentMetadata == null || playQueue == null
|| playQueue.getIndex() != simpleExoPlayer.getCurrentWindowIndex()) {
// Make sure play queue and current window index are equal, to prevent saving state for
// the wrong stream on discontinuity (e.g. when the stream just changed but the
// playQueue index and currentMetadata still haven't updated)
return; return;
} }
final StreamInfo currentInfo = currentMetadata.getMetadata(); // Save current position. It will help to restore this position once a user
if (playQueue != null) { // wants to play prev or next stream from the queue
// Save current position. It will help to restore this position once a user playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition());
// wants to play prev or next stream from the queue saveStreamProgressState(simpleExoPlayer.getCurrentPosition());
playQueue.setRecovery(playQueue.getIndex(), simpleExoPlayer.getContentPosition()); }
public void saveStreamProgressStateCompleted() {
if (currentMetadata != null) {
// current stream has ended, so the progress is its duration (+1 to overcome rounding)
saveStreamProgressState((currentMetadata.getMetadata().getDuration() + 1) * 1000);
} }
saveStreamProgressState(currentInfo, simpleExoPlayer.getCurrentPosition());
} }
//endregion //endregion