diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt index e1b643a4..daaba3f5 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_V8.kt @@ -109,11 +109,29 @@ inline fun V8Value.expectV8Variant(config: IV8PluginConfig, contextN else return this.expectOrThrow(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(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(config, contextName).value.toDouble() as T + }; V8ValueObject::class -> this.expectOrThrow(config, contextName) as T V8ValueArray::class -> this.expectOrThrow(config, contextName) as T; Boolean::class -> this.expectOrThrow(config, contextName).value as T; - Float::class -> this.expectOrThrow(config, contextName).value.toFloat() as T; - Double::class -> this.expectOrThrow(config, contextName).value as T; HashMap::class -> this.expectOrThrow(config, contextName).let { V8ObjectToHashMap(it) } as T; Map::class -> this.expectOrThrow(config, contextName).let { V8ObjectToHashMap(it) } as T; List::class -> this.expectOrThrow(config, contextName).let { V8ArrayToStringList(it) } as T; diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 7e1bbdaf..d287da63 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -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; } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/chapters/IChapter.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/chapters/IChapter.kt index 5909947f..1c887e18 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/chapters/IChapter.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/chapters/IChapter.kt @@ -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) { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChapter.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChapter.kt index 0eac1c65..2ae3edb1 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChapter.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChapter.kt @@ -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(config,"name", context); val type = ChapterType.fromInt(obj.getOrDefault(config, "type", context, ChapterType.NORMAL.value) ?: ChapterType.NORMAL.value); - val timeStart = obj.getOrThrow(config, "timeStart", context); - val timeEnd = obj.getOrThrow(config, "timeEnd", context); + val timeStart = obj.getOrThrow(config, "timeStart", context); + val timeEnd = obj.getOrThrow(config, "timeEnd", context); return JSChapter(name, timeStart, timeEnd, type); } diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 716f2ea7..591df973 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -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()); } } } diff --git a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt index 1b577311..7936d920 100644 --- a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt @@ -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); } }; } diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt index ba3240ff..f537232c 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayer.kt @@ -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?) { diff --git a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt index 8163dc17..c58b0908 100644 --- a/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt +++ b/app/src/main/java/com/futo/platformplayer/views/video/FutoVideoPlayerBase.kt @@ -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) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0acaf866..2abc464b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -360,6 +360,8 @@ Restore a previous automatic backup Resume After Preview Review the current and past changelogs + Chapter Update FPS + Change accuracy of chapter updating, higher might cost more performance Set Automatic Backup Shortly after opening the app, start fetching subscriptions Show FAQ @@ -779,6 +781,12 @@ Resume After 10s Always Resume + + 24 + 30 + 60 + 120 + English Spanish diff --git a/app/src/unstable/assets/sources/youtube b/app/src/unstable/assets/sources/youtube index 4f89b407..8f10daba 160000 --- a/app/src/unstable/assets/sources/youtube +++ b/app/src/unstable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit 4f89b4072f4473ff0ffac1711023dffd20f0a868 +Subproject commit 8f10daba1ef9cbcd99f3c640d86808f8c94aa84a