Basic chapter system working

This commit is contained in:
Kelvin 2023-10-25 20:38:57 +02:00
parent 1d18c13817
commit d8aecd325b
14 changed files with 202 additions and 2 deletions

View file

@ -31,6 +31,12 @@ let Type = {
RAW: 0,
HTML: 1,
MARKUP: 2
},
Chapter: {
NORMAL: 0,
SKIPPABLE: 5,
SKIP: 6
}
};

View file

@ -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<IChapter> = _client.getContentChapters(url);
override fun getPlaybackTracker(url: String): IPlaybackTracker? = _client.getPlaybackTracker(url);
override fun isChannelUrl(url: String): Boolean = _client.isChannelUrl(url);

View file

@ -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<IChapter>;
/**
* Gets the playback tracker for a piece of content
*/

View file

@ -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
) {
}

View file

@ -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;
}
}
}

View file

@ -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<IChapter> = 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)")

View file

@ -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<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);
return JSChapter(name, timeStart, timeEnd, type);
}
fun fromV8(config: IV8PluginConfig, arr: V8ValueArray): List<IChapter> {
return arr.keys.mapNotNull {
val obj = arr.get<V8ValueObject>(it);
return@mapNotNull fromV8(config, obj);
};
}
}
}

View file

@ -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()

View file

@ -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<IChapter>? {
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) {

View file

@ -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<FutoVideoPlayer>();
val onVideoSettings = Event1<FutoVideoPlayer>();
@ -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) {

View file

@ -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<IChapter>? = null;
var exoPlayer: PlayerManager? = null
private set;
val exoPlayerStateName: String;
@ -208,6 +211,16 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
}
}
fun setChapters(chapters: List<IChapter>?) {
_chapters = chapters;
}
fun getChapters(): List<IChapter> {
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);
}

View file

@ -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"/>
<TextView
android:id="@+id/text_chapter_current"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="#FFFFFF"
android:layout_marginTop="-2dp"
android:paddingRight="10dp"
android:textSize="11sp"
android:gravity="left"
app:layout_constraintLeft_toRightOf="@id/exo_duration"
app:layout_constraintTop_toTopOf="@id/exo_duration"
app:layout_constraintBottom_toBottomOf="@id/exo_duration"
app:layout_constraintRight_toLeftOf="@id/exo_fullscreen"
android:ellipsize="end"
android:maxLines="1"
android:text="">
</TextView>
<com.google.android.exoplayer2.ui.SubtitleView
android:id="@id/exo_subtitles"
android:layout_width="match_parent"

View file

@ -159,6 +159,26 @@
app:layout_constraintTop_toTopOf="@id/exo_position"
app:layout_constraintBottom_toBottomOf="@id/exo_position"/>
<TextView
android:id="@+id/text_chapter_current"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textColor="#FFFFFF"
android:paddingRight="10dp"
android:layout_marginTop="-2dp"
android:textSize="11sp"
android:gravity="left"
app:layout_constraintLeft_toRightOf="@id/exo_duration"
app:layout_constraintTop_toTopOf="@id/exo_duration"
app:layout_constraintBottom_toBottomOf="@id/exo_duration"
app:layout_constraintRight_toLeftOf="@id/exo_fullscreen"
android:ellipsize="end"
android:maxLines="1"
android:text="">
</TextView>
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="match_parent"

@ -1 +1 @@
Subproject commit a1d432865ef87cc4860be998a02ba95f60ccfcd8
Subproject commit 5011bfcddb084007b938e6276b11f63e940006eb