diff --git a/app/src/main/assets/scripts/source.js b/app/src/main/assets/scripts/source.js index fcdb7804..b836ba3d 100644 --- a/app/src/main/assets/scripts/source.js +++ b/app/src/main/assets/scripts/source.js @@ -31,6 +31,12 @@ let Type = { RAW: 0, HTML: 1, MARKUP: 2 + }, + Chapter: { + NORMAL: 0, + + SKIPPABLE: 5, + SKIP: 6 } }; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/CachedPlatformClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/CachedPlatformClient.kt index 66a86f8b..ad567a99 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/CachedPlatformClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/CachedPlatformClient.kt @@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media import androidx.collection.LruCache import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.channels.IPlatformChannel +import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails @@ -49,6 +50,7 @@ class CachedPlatformClient : IPlatformClient { return result; } + override fun getContentChapters(url: String): List = _client.getContentChapters(url); override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url); override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/IPlatformClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/IPlatformClient.kt index 0178cb5a..009e0c14 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/IPlatformClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/IPlatformClient.kt @@ -3,6 +3,7 @@ package com.futo.platformplayer.api.media import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.channels.IPlatformChannel +import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails @@ -100,6 +101,8 @@ interface IPlatformClient { */ fun getContentDetails(url: String): IPlatformContentDetails; + fun getContentChapters(url: String): List; + /** * Gets the playback tracker for a piece of content */ diff --git a/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientCapabilities.kt b/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientCapabilities.kt index 4b15791e..84edbf94 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientCapabilities.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/PlatformClientCapabilities.kt @@ -15,7 +15,8 @@ data class PlatformClientCapabilities( val hasGetSearchCapabilities: Boolean = false, val hasGetChannelCapabilities: Boolean = false, val hasGetLiveEvents: Boolean = false, - val hasGetLiveChatWindow: Boolean = false + val hasGetLiveChatWindow: Boolean = false, + val hasGetContentChapters: Boolean = false ) { } \ No newline at end of file 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 new file mode 100644 index 00000000..5909947f --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/chapters/IChapter.kt @@ -0,0 +1,31 @@ +package com.futo.platformplayer.api.media.models.chapters + +import com.futo.platformplayer.api.media.exceptions.UnknownPlatformException +import com.futo.platformplayer.api.media.models.contents.ContentType + +interface IChapter { + val name: String; + val type: ChapterType; + val timeStart: Int; + val timeEnd: Int; +} + +enum class ChapterType(val value: Int) { + NORMAL(0), + + SKIPPABLE(5), + SKIP(6); + + + + + companion object { + fun fromInt(value: Int): ChapterType + { + val result = ChapterType.values().firstOrNull { it.value == value }; + if(result == null) + throw UnknownPlatformException(value.toString()); + return result; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt index 0103eb68..eff3e0f1 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/JSClient.kt @@ -15,6 +15,7 @@ import com.futo.platformplayer.api.media.PlatformClientCapabilities import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.channels.IPlatformChannel +import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails @@ -181,6 +182,7 @@ open class JSClient : IPlatformClient { hasGetChannelCapabilities = plugin.executeBoolean("!!source.getChannelCapabilities") ?: false, hasGetLiveEvents = plugin.executeBoolean("!!source.getLiveEvents") ?: false, hasGetLiveChatWindow = plugin.executeBoolean("!!source.getLiveChatWindow") ?: false, + hasGetContentChapters = plugin.executeBoolean("!!source.getContentChapters") ?: false, ); try { @@ -414,6 +416,17 @@ open class JSClient : IPlatformClient { plugin.executeTyped("source.getContentDetails(${Json.encodeToString(url)})")); } + @JSOptional //getContentChapters = function(url, initialData) + @JSDocs(15, "source.getContentChapters(url)", "Gets chapters for content details") + @JSDocsParameter("url", "A content url (this platform)") + override fun getContentChapters(url: String): List = isBusyWith { + if(!capabilities.hasGetContentChapters) + return@isBusyWith listOf(); + ensureEnabled(); + return@isBusyWith JSChapter.fromV8(config, + plugin.executeTyped("source.getContentChapters(${Json.encodeToString(url)})")); + } + @JSOptional @JSDocs(15, "source.getPlaybackTracker(url)", "Gets a playback tracker for given content url") @JSDocsParameter("url", "A content url (this platform)") 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 new file mode 100644 index 00000000..0eac1c65 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSChapter.kt @@ -0,0 +1,45 @@ +package com.futo.platformplayer.api.media.platforms.js.models + +import com.caoccao.javet.values.reference.V8ValueArray +import com.caoccao.javet.values.reference.V8ValueObject +import com.futo.platformplayer.api.media.models.chapters.ChapterType +import com.futo.platformplayer.api.media.models.chapters.IChapter +import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.engine.IV8PluginConfig +import com.futo.platformplayer.getOrDefault +import com.futo.platformplayer.getOrThrow + +class JSChapter : IChapter { + override val name: String; + override val type: ChapterType; + override val timeStart: Int; + override val timeEnd: Int; + + constructor(name: String, timeStart: Int, timeEnd: Int, type: ChapterType = ChapterType.NORMAL) { + this.name = name; + this.timeStart = timeStart; + this.timeEnd = timeEnd; + this.type = type; + } + + + companion object { + fun fromV8(config: IV8PluginConfig, obj: V8ValueObject): IChapter { + val context = "Chapter"; + + 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); + + return JSChapter(name, timeStart, timeEnd, type); + } + + fun fromV8(config: IV8PluginConfig, arr: V8ValueArray): List { + return arr.keys.mapNotNull { + val obj = arr.get(it); + return@mapNotNull fromV8(config, obj); + }; + } + } +} \ No newline at end of file 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 9b578127..1bb98f2e 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 @@ -949,6 +949,17 @@ class VideoDetailView : ConstraintLayout { if(video is JSVideoDetails) { val me = this; fragment.lifecycleScope.launch(Dispatchers.IO) { + try { + //TODO: Implement video.getContentChapters() + val chapters = null ?: StatePlatform.instance.getContentChapters(video.url); + _player.setChapters(chapters); + } + catch(ex: Throwable) { + Logger.e(TAG, "Failed to get chapters", ex); + withContext(Dispatchers.Main) { + UIDialogs.toast(context, "Failed to get chapters\n" + ex.message); + } + } try { val stopwatch = com.futo.platformplayer.debug.Stopwatch() var tracker = video.getPlaybackTracker() diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt index 1f1b38c3..88dcaf91 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePlatform.kt @@ -14,6 +14,7 @@ import com.futo.platformplayer.api.media.models.FilterGroup import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.ResultCapabilities import com.futo.platformplayer.api.media.models.channels.IPlatformChannel +import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContentDetails @@ -615,6 +616,14 @@ class StatePlatform { } } + fun getContentChapters(url: String): List? { + val baseClient = getContentClientOrNull(url) ?: return null; + if (baseClient !is JSClient) { + return baseClient.getContentChapters(url); + } + val client = _trackerClientPool.getClientPooled(baseClient, 1); + return client.getContentChapters(url); + } fun getPlaybackTracker(url: String): IPlaybackTracker? { val baseClient = getContentClientOrNull(url) ?: return null; if (baseClient !is JSClient) { 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 76cf609d..f5574285 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 @@ -17,6 +17,7 @@ import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.setMargins import com.futo.platformplayer.* +import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource import com.futo.platformplayer.constructs.Event0 @@ -63,6 +64,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase { private val _control_rotate_lock: ImageButton; private val _control_cast: ImageButton; private val _control_play: ImageButton; + private val _control_chapter: TextView; private val _time_bar: TimeBar; private val _control_fullscreen_fullscreen: ImageButton; @@ -72,6 +74,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase { private val _control_play_fullscreen: ImageButton; private val _time_bar_fullscreen: TimeBar; private val _overlay_brightness: FrameLayout; + private val _control_chapter_fullscreen: TextView; private val _title_fullscreen: TextView; private val _author_fullscreen: TextView; @@ -87,6 +90,9 @@ class FutoVideoPlayer : FutoVideoPlayerBase { var isFitMode : Boolean = false private set; + private var _currentChapter: IChapter? = null; + + //Events val onMinimize = Event1(); val onVideoSettings = Event1(); @@ -112,6 +118,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase { _control_cast = videoControls.findViewById(R.id.exo_cast); _control_play = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_play); _time_bar = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress); + _control_chapter = videoControls.findViewById(R.id.text_chapter_current); _videoControls_fullscreen = findViewById(R.id.video_player_controller_fullscreen); _control_fullscreen_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_fullscreen); @@ -119,6 +126,7 @@ class FutoVideoPlayer : FutoVideoPlayerBase { _control_videosettings_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_settings); _control_rotate_lock_fullscreen = _videoControls_fullscreen.findViewById(R.id.exo_rotate_lock); _control_play_fullscreen = videoControls.findViewById(com.google.android.exoplayer2.ui.R.id.exo_play); + _control_chapter_fullscreen = _videoControls_fullscreen.findViewById(R.id.text_chapter_current); _time_bar_fullscreen = _videoControls_fullscreen.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress); _overlay_brightness = findViewById(R.id.overlay_brightness); @@ -218,8 +226,25 @@ class FutoVideoPlayer : FutoVideoPlayerBase { updateRotateLock(); }; + var lastPos = 0L; videoControls.setProgressUpdateListener { position, bufferedPosition -> onTimeBarChanged.emit(position, bufferedPosition); + + val delta = position - lastPos; + if(delta > 1000 || delta < 0) { + lastPos = position; + val currentChapter = getCurrentChapter(position) + 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 = ""; + } + } + } } if(!isInEditMode) { 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 5c4d6394..8163dc17 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 @@ -4,6 +4,7 @@ import android.content.Context import android.net.Uri import android.util.AttributeSet import android.widget.RelativeLayout +import com.futo.platformplayer.api.media.models.chapters.IChapter import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor import com.futo.platformplayer.helpers.VideoHelper @@ -53,6 +54,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout { private var _shouldPlaybackRestartOnConnectivity: Boolean = false; private val _referenceObject = Object(); + private var _chapters: List? = null; + var exoPlayer: PlayerManager? = null private set; val exoPlayerStateName: String; @@ -208,6 +211,16 @@ abstract class FutoVideoPlayerBase : RelativeLayout { } } + fun setChapters(chapters: List?) { + _chapters = chapters; + } + fun getChapters(): List { + 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 } }; + } + fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false) { swapSources(videoSource, audioSource,false, play, keepSubtitles); } diff --git a/app/src/main/res/layout/video_player_ui.xml b/app/src/main/res/layout/video_player_ui.xml index 78e83b84..7a318f12 100644 --- a/app/src/main/res/layout/video_player_ui.xml +++ b/app/src/main/res/layout/video_player_ui.xml @@ -137,6 +137,27 @@ app:layout_constraintLeft_toRightOf="@id/text_divider" app:layout_constraintTop_toTopOf="@id/exo_position" app:layout_constraintBottom_toBottomOf="@id/exo_position"/> + + + + + + + + + + +