Chapter accuracy now sub-second

This commit is contained in:
Kelvin 2023-11-15 20:02:28 +01:00
parent 17b9853bb6
commit 640c2cbed0
10 changed files with 133 additions and 32 deletions

View file

@ -109,11 +109,29 @@ inline fun <reified T> V8Value.expectV8Variant(config: IV8PluginConfig, contextN
else
return this.expectOrThrow<V8ValueLong>(config, contextName).value.toLong() as T
};
Float::class -> {
if(this is V8ValueDouble)
return this.value.toFloat() as T;
else if(this is V8ValueInteger)
return this.value.toFloat() as T;
else if(this is V8ValueLong)
return this.value.toFloat() as T;
else
return this.expectOrThrow<V8ValueDouble>(config, contextName).value.toDouble() as T
};
Double::class -> {
if(this is V8ValueDouble)
return this.value.toDouble() as T;
else if(this is V8ValueInteger)
return this.value.toDouble() as T;
else if(this is V8ValueLong)
return this.value.toDouble() as T;
else
return this.expectOrThrow<V8ValueDouble>(config, contextName).value.toDouble() as T
};
V8ValueObject::class -> this.expectOrThrow<V8ValueObject>(config, contextName) as T
V8ValueArray::class -> this.expectOrThrow<V8ValueArray>(config, contextName) as T;
Boolean::class -> this.expectOrThrow<V8ValueBoolean>(config, contextName).value as T;
Float::class -> this.expectOrThrow<V8ValueDouble>(config, contextName).value.toFloat() as T;
Double::class -> this.expectOrThrow<V8ValueDouble>(config, contextName).value as T;
HashMap::class -> this.expectOrThrow<V8ValueObject>(config, contextName).let { V8ObjectToHashMap(it) } as T;
Map::class -> this.expectOrThrow<V8ValueObject>(config, contextName).let { V8ObjectToHashMap(it) } as T;
List::class -> this.expectOrThrow<V8ValueArray>(config, contextName).let { V8ArrayToStringList(it) } as T;

View file

@ -337,12 +337,26 @@ class Settings : FragmentedStorageFileJson() {
return false;
}
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 8)
@FormField(R.string.chapter_update_fps_title, FieldForm.DROPDOWN, R.string.chapter_update_fps_description, 8)
@DropdownFieldOptionsId(R.array.chapter_fps)
var chapterUpdateFPS: Int = 0;
fun getChapterUpdateFrames(): Int {
return when(chapterUpdateFPS) {
0 -> 24
1 -> 30
2 -> 60
3 -> 120
else -> 1
};
}
@FormField(R.string.live_chat_webview, FieldForm.TOGGLE, R.string.use_the_live_chat_web_window_when_available_over_native_implementation, 9)
var useLiveChatWindow: Boolean = true;
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 8)
@FormField(R.string.background_switch_audio, FieldForm.TOGGLE, R.string.background_switch_audio_description, 10)
var backgroundSwitchToAudio: Boolean = true;
}

View file

@ -6,8 +6,8 @@ import com.futo.platformplayer.api.media.models.contents.ContentType
interface IChapter {
val name: String;
val type: ChapterType;
val timeStart: Int;
val timeEnd: Int;
val timeStart: Double;
val timeEnd: Double;
}
enum class ChapterType(val value: Int) {

View file

@ -12,10 +12,10 @@ import com.futo.platformplayer.getOrThrow
class JSChapter : IChapter {
override val name: String;
override val type: ChapterType;
override val timeStart: Int;
override val timeEnd: Int;
override val timeStart: Double;
override val timeEnd: Double;
constructor(name: String, timeStart: Int, timeEnd: Int, type: ChapterType = ChapterType.NORMAL) {
constructor(name: String, timeStart: Double, timeEnd: Double, type: ChapterType = ChapterType.NORMAL) {
this.name = name;
this.timeStart = timeStart;
this.timeEnd = timeEnd;
@ -29,8 +29,8 @@ class JSChapter : IChapter {
val name = obj.getOrThrow<String>(config,"name", context);
val type = ChapterType.fromInt(obj.getOrDefault<Int>(config, "type", context, ChapterType.NORMAL.value) ?: ChapterType.NORMAL.value);
val timeStart = obj.getOrThrow<Int>(config, "timeStart", context);
val timeEnd = obj.getOrThrow<Int>(config, "timeEnd", context);
val timeStart = obj.getOrThrow<Double>(config, "timeStart", context);
val timeEnd = obj.getOrThrow<Double>(config, "timeEnd", context);
return JSChapter(name, timeStart, timeEnd, type);
}

View file

@ -419,7 +419,7 @@ class VideoDetailView : ConstraintLayout {
_layoutSkip.visibility = VISIBLE;
}
else if(chapter?.type == ChapterType.SKIP) {
_player.seekTo(chapter.timeEnd.toLong() * 1000);
_player.seekTo((chapter.timeEnd * 1000).toLong());
UIDialogs.toast(context, "Skipped chapter [${chapter.name}]", false);
}
}
@ -615,7 +615,7 @@ class VideoDetailView : ConstraintLayout {
_layoutSkip.setOnClickListener {
val currentChapter = _player.getCurrentChapter(_player.position);
if(currentChapter?.type == ChapterType.SKIPPABLE) {
_player.seekTo(currentChapter.timeEnd.toLong() * 1000);
_player.seekTo((currentChapter.timeEnd * 1000).toLong());
}
}
}

View file

@ -210,7 +210,8 @@ class GestureControlView : LinearLayout {
hideControls();
} catch (e: Throwable) {
Logger.e(TAG, "Failed to hide controls", e);
if(e !is CancellationException)
Logger.e(TAG, "Failed to hide controls", e);
}
};
}

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.content.res.Resources
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.os.Handler
import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue
@ -16,6 +17,7 @@ import android.widget.RelativeLayout
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.setMargins
import androidx.lifecycle.LifecycleOwner
import com.futo.platformplayer.*
import com.futo.platformplayer.api.media.models.chapters.IChapter
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
@ -35,6 +37,10 @@ import com.google.android.exoplayer2.ui.PlayerControlView
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.ui.TimeBar
import com.google.android.exoplayer2.video.VideoSize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlin.math.abs
@ -91,6 +97,10 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
var isFitMode : Boolean = false
private set;
private var _isScrubbing = false;
private val _currentChapterLoopLock = Object();
private var _currentChapterLoopActive = false;
private var _currentChapterLoopId: Int = 0;
private var _currentChapter: IChapter? = null;
@ -186,18 +196,30 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
if (!attrShowMinimize)
_control_minimize.visibility = View.GONE;
var lastScrubPosition = 0L;
_time_bar_listener = object : TimeBar.OnScrubListener {
override fun onScrubStart(timeBar: TimeBar, position: Long) {
_isScrubbing = true;
Logger.i(TAG, "Scrubbing started");
gestureControl.restartHideJob();
}
override fun onScrubMove(timeBar: TimeBar, position: Long) {
gestureControl.restartHideJob();
updateCurrentChapter(position);
val playerPosition = position;
val scrubDelta = abs(lastScrubPosition - position);
lastScrubPosition = position;
if(scrubDelta > 1000 || Math.abs(position - playerPosition) > 500)
_currentChapterUpdateExecuter.execute {
updateCurrentChapter(position);
}
}
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
_isScrubbing = false;
Logger.i(TAG, "Scrubbing stopped");
gestureControl.restartHideJob();
}
};
@ -239,15 +261,16 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
UIDialogs.showCastingDialog(context);
};
var lastPos = 0L;
videoControls.setProgressUpdateListener { position, bufferedPosition ->
onTimeBarChanged.emit(position, bufferedPosition);
val delta = position - lastPos;
if(delta > 1000 || delta < 0) {
lastPos = position;
updateCurrentChapter();
}
if(!_currentChapterLoopActive)
synchronized(_currentChapterLoopLock) {
if(!_currentChapterLoopActive) {
_currentChapterLoopActive = true;
updateChaptersLoop(++_currentChapterLoopId);
}
}
}
if(!isInEditMode) {
@ -255,24 +278,58 @@ class FutoVideoPlayer : FutoVideoPlayerBase {
}
}
private val _currentChapterUpdateInterval: Long = 1000L / Settings.instance.playback.getChapterUpdateFrames();
private var _currentChapterUpdateLastPos = 0L;
private val _currentChapterUpdateExecuter = Executors.newSingleThreadScheduledExecutor();
private fun updateChaptersLoop(loopId: Int) {
if(_currentChapterLoopId == loopId) {
_currentChapterLoopActive = true;
_currentChapterUpdateExecuter.schedule({
try {
if(!_isScrubbing) {
var pos: Long = runBlocking(Dispatchers.Main) { position; };
val delta = pos - _currentChapterUpdateLastPos;
if(delta > _currentChapterUpdateInterval || delta < 0) {
_currentChapterUpdateLastPos = pos;
if(updateCurrentChapter(pos))
Logger.i(TAG, "Updated chapter to [${_currentChapter?.name}] with speed ${delta}ms (${pos - (_currentChapter?.timeStart?.times(1000)?.toLong() ?: 0)}ms late [${_currentChapter?.timeStart}s])");
}
}
if(playingCached)
updateChaptersLoop(loopId);
else
_currentChapterLoopActive = false;
}
catch(ex: Throwable) {
_currentChapterLoopActive = false;
}
}, _currentChapterUpdateInterval, TimeUnit.MILLISECONDS);
}
else
_currentChapterLoopActive = false;
}
fun attachPlayer() {
exoPlayer?.attach(_videoView, PLAYER_STATE_NAME);
}
fun updateCurrentChapter(pos: Long? = null) {
val chaptPos = pos ?: position;
fun updateCurrentChapter(chaptPos: Long, isScrub: Boolean = false): Boolean {
val currentChapter = getCurrentChapter(chaptPos);
if(_currentChapter != currentChapter) {
_currentChapter = currentChapter;
if (currentChapter != null) {
_control_chapter.text = "" + currentChapter.name;
_control_chapter_fullscreen.text = "" + currentChapter.name;
} else {
_control_chapter.text = "";
_control_chapter_fullscreen.text = "";
runBlocking(Dispatchers.Main) {
if (currentChapter != null) {
_control_chapter.text = "" + currentChapter.name;
_control_chapter_fullscreen.text = "" + currentChapter.name;
} else {
_control_chapter.text = "";
_control_chapter_fullscreen.text = "";
}
onChapterChanged.emit(currentChapter, isScrub);
}
onChapterChanged.emit(currentChapter, pos != null);
return true;
}
return false;
}
fun setArtwork(drawable: Drawable?) {

View file

@ -60,6 +60,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
private set;
val exoPlayerStateName: String;
protected var playingCached: Boolean = false;
val playing: Boolean get() = exoPlayer?.player?.playWhenReady ?: false;
val position: Long get() = exoPlayer?.player?.currentPosition ?: 0;
val duration: Long get() = exoPlayer?.player?.duration ?: 0;
@ -103,6 +104,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
onPlayChanged.emit(playWhenReady);
playingCached = playWhenReady;
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
@ -167,6 +169,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
}
fun seekTo(ms: Long) {
Logger.i(TAG, "Seeking to [${ms}ms]");
exoPlayer?.player?.seekTo(ms);
}
fun seekToEnd(ms: Long = 0) {
@ -218,7 +221,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return _chapters?.let { it.toList() } ?: listOf();
}
fun getCurrentChapter(pos: Long): IChapter? {
return _chapters?.let { chaps -> chaps.find { pos / 1000 > it.timeStart && pos / 1000 < it.timeEnd } };
return _chapters?.let { chaps -> chaps.find { pos.toDouble() / 1000 > it.timeStart && pos.toDouble() / 1000 < it.timeEnd } };
}
fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false) {

View file

@ -360,6 +360,8 @@
<string name="restore_a_previous_automatic_backup">Restore a previous automatic backup</string>
<string name="resume_after_preview">Resume After Preview</string>
<string name="review_the_current_and_past_changelogs">Review the current and past changelogs</string>
<string name="chapter_update_fps_title">Chapter Update FPS</string>
<string name="chapter_update_fps_description">Change accuracy of chapter updating, higher might cost more performance</string>
<string name="set_automatic_backup">Set Automatic Backup</string>
<string name="shortly_after_opening_the_app_start_fetching_subscriptions">Shortly after opening the app, start fetching subscriptions</string>
<string name="show_faq">Show FAQ</string>
@ -779,6 +781,12 @@
<item>Resume After 10s</item>
<item>Always Resume</item>
</string-array>
<string-array name="chapter_fps">
<item>24</item>
<item>30</item>
<item>60</item>
<item>120</item>
</string-array>
<string-array name="audio_languages">
<item>English</item>
<item>Spanish</item>

@ -1 +1 @@
Subproject commit 4f89b4072f4473ff0ffac1711023dffd20f0a868
Subproject commit 8f10daba1ef9cbcd99f3c640d86808f8c94aa84a