mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Chapter accuracy now sub-second
This commit is contained in:
parent
17b9853bb6
commit
640c2cbed0
10 changed files with 133 additions and 32 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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?) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
Loading…
Add table
Reference in a new issue