diff --git a/app/src/main/java/com/futo/platformplayer/api/media/LiveChatManager.kt b/app/src/main/java/com/futo/platformplayer/api/media/LiveChatManager.kt index ab903057..da7302fb 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/LiveChatManager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/LiveChatManager.kt @@ -11,6 +11,7 @@ import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent import com.futo.platformplayer.api.media.models.live.LiveEventComment import com.futo.platformplayer.api.media.models.live.LiveEventEmojis import com.futo.platformplayer.api.media.platforms.js.models.JSLiveEventPager +import com.futo.platformplayer.api.media.platforms.js.models.JSVODEventPager import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.constructs.BatchedTaskHandler import com.futo.platformplayer.logging.Logger @@ -26,12 +27,17 @@ class LiveChatManager { private val _emojiCache: EmojiCache = EmojiCache(); private val _pager: IPager?; + private var _position: Long = 0; + private var _eventsPosition: Long = 0; + private val _history: ArrayList = arrayListOf(); private var _startCounter = 0; private val _followers: HashMap) -> Unit> = hashMapOf(); + val isVOD get() = _pager is JSVODEventPager; + var viewCount: Long = 0 private set; @@ -39,8 +45,24 @@ class LiveChatManager { _scope = scope; _pager = pager; viewCount = initialViewCount; - handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n"))); - handleEvents(pager.getResults()); + if(pager is JSVODEventPager) + handleEvents(listOf(LiveEventComment("SYSTEM", null, "VOD chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n"))); + else + handleEvents(listOf(LiveEventComment("SYSTEM", null, "Live chat is still under construction. While it is mostly functional, the experience still needs to be improved.\n"))); + + if(pager is JSVODEventPager) { + var replayResults = pager.getResults().filter { it.time > _eventsPosition || it is LiveEventEmojis }; + //TODO: Remove this once dripfeed is done properly + replayResults = replayResults.filter{ it.time < _eventsPosition + 1500 || it is LiveEventEmojis }; + if(replayResults.size > 0) { + _eventsPosition = replayResults.maxOf { it.time }; + Logger.i(TAG, "VOD Events last event: " + _eventsPosition); + } + else + _eventsPosition = _eventsPosition + 1500; + } + else + handleEvents(pager.getResults()); } fun start() { @@ -52,6 +74,10 @@ class LiveChatManager { _startCounter++; } + fun setVideoPosition(ms: Long) { + _position = ms; + } + fun getHistory(): List { synchronized(_history) { return _history.toList(); @@ -85,13 +111,36 @@ class LiveChatManager { try { while(_startCounter == counter) { var nextInterval = 1000L; + if(_pager is JSVODEventPager && _eventsPosition > _position) { + delay(500); + continue; + } + try { if(_pager == null || !_pager.hasMorePages()) return@launch; - _pager.nextPage(); - val newEvents = _pager.getResults(); + val newEvents = if(_pager is JSVODEventPager) { + val requestPosition = _position; + _pager.nextPage(requestPosition.toInt()); + var replayResults = _pager.getResults().filter { it.time > requestPosition || it is LiveEventEmojis }; + //TODO: Remove this once dripfeed is done properly + replayResults = replayResults.filter{ it.time < requestPosition + 1500 || it is LiveEventEmojis }; + if(replayResults.size > 0) { + _eventsPosition = replayResults.maxOf { it.time }; + Logger.i(TAG, "VOD Events last event: " + _eventsPosition); + } + else + _eventsPosition = requestPosition + _pager.nextRequest.coerceAtLeast(800).toLong(); + replayResults; + } + else { + _pager.nextPage(); + _pager.getResults(); + } if(_pager is JSLiveEventPager) nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong(); + else if(_pager is JSVODEventPager) + nextInterval = _pager.nextRequest.coerceAtLeast(800).toLong(); if(newEvents.size > 0) Logger.i(TAG, "New Live Events (${newEvents.size}) [${newEvents.map { it.type.name }.joinToString(", ")}]"); diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/IPlatformLiveEvent.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/IPlatformLiveEvent.kt index 19b4bbb9..8c7ca14c 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/IPlatformLiveEvent.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/IPlatformLiveEvent.kt @@ -7,6 +7,7 @@ import com.futo.platformplayer.getOrThrow interface IPlatformLiveEvent { val type : LiveEventType; + var time: Long; companion object { diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventComment.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventComment.kt index 8b9883ef..33818eb0 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventComment.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventComment.kt @@ -18,12 +18,15 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage { val colorName: String?; val badges: List; - constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List? = null) { + override var time: Long = -1; + + constructor(name: String, thumbnail: String?, message: String, colorName: String? = null, badges: List? = null, time: Long = -1) { this.name = name; this.message = message; this.thumbnail = thumbnail; this.colorName = colorName; this.badges = badges ?: listOf(); + this.time = time; } companion object { @@ -39,7 +42,8 @@ class LiveEventComment: IPlatformLiveEvent, ILiveEventChatMessage { obj.getOrThrow(config, "name", contextName), obj.getOrThrow(config, "thumbnail", contextName, true), obj.getOrThrow(config, "message", contextName), - colorName, badges); + colorName, badges, + obj.getOrDefault(config, "time", contextName, -1) ?: -1); } } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventDonation.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventDonation.kt index f8cbafe6..49044906 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventDonation.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventDonation.kt @@ -21,6 +21,8 @@ class LiveEventDonation: IPlatformLiveEvent, ILiveEventChatMessage { var expire: Int = 6000; + override var time: Long = -1; + constructor(name: String, thumbnail: String?, message: String, amount: String, expire: Int = 6000, colorDonation: String? = null) { this.name = name; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventEmojis.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventEmojis.kt index ebd75b44..f4141174 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventEmojis.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventEmojis.kt @@ -10,6 +10,8 @@ class LiveEventEmojis: IPlatformLiveEvent { val emojis: HashMap; + override var time: Long = -1; + constructor(emojis: HashMap) { this.emojis = emojis; } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventRaid.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventRaid.kt index 6663852d..6f7740a0 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventRaid.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventRaid.kt @@ -14,6 +14,8 @@ class LiveEventRaid: IPlatformLiveEvent { val targetUrl: String; val isOutgoing: Boolean; + override var time: Long = -1; + constructor(name: String, url: String, thumbnail: String, isOutgoing: Boolean) { this.targetName = name; this.targetUrl = url; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventViewCount.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventViewCount.kt index 5e48e984..d69a6ccb 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventViewCount.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/live/LiveEventViewCount.kt @@ -10,6 +10,8 @@ class LiveEventViewCount: IPlatformLiveEvent { val viewCount: Int; + override var time: Long = -1; + constructor(viewCount: Int) { this.viewCount = viewCount; } diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt index 731d0e51..d4aecc16 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSPager.kt @@ -19,8 +19,8 @@ abstract class JSPager : IPager { protected var pager: V8ValueObject; private var _lastResults: List? = null; - private var _resultChanged: Boolean = true; - private var _hasMorePages: Boolean = false; + protected var _resultChanged: Boolean = true; + protected var _hasMorePages: Boolean = false; //private var _morePagesWasFalse: Boolean = false; val isAvailable get() = plugin.getUnderlyingPlugin()._runtime?.let { !it.isClosed && !it.isDead } ?: false; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVODEventPager.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVODEventPager.kt new file mode 100644 index 00000000..5361a2a4 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVODEventPager.kt @@ -0,0 +1,44 @@ +package com.futo.platformplayer.api.media.platforms.js.models + +import com.caoccao.javet.values.V8Value +import com.caoccao.javet.values.reference.V8ValueObject +import com.futo.platformplayer.api.media.models.live.IPlatformLiveEvent +import com.futo.platformplayer.api.media.platforms.js.JSClient +import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig +import com.futo.platformplayer.api.media.structures.IPlatformLiveEventPager +import com.futo.platformplayer.getOrDefault +import com.futo.platformplayer.getOrThrow +import com.futo.platformplayer.invokeV8 +import com.futo.platformplayer.warnIfMainThread +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +class JSVODEventPager : JSPager, IPlatformLiveEventPager { + override var nextRequest: Int; + + constructor(config: SourcePluginConfig, plugin: JSClient, pager: V8ValueObject) : super(config, plugin, pager) { + nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); + } + + fun nextPage(ms: Int) = plugin.isBusyWith("JSLiveEventPager.nextPage") { + warnIfMainThread("VODEventPager.nextPage"); + + val pluginV8 = plugin.getUnderlyingPlugin(); + pluginV8.busy { + val newPager: V8Value = pluginV8.catchScriptErrors("[${plugin.config.name}] JSPager", "pager.nextPage(...)") { + pager.invokeV8("nextPage", ms); + }; + if(newPager is V8ValueObject) + pager = newPager; + _hasMorePages = pager.getOrDefault(config, "hasMore", "Pager", false) ?: false; + _resultChanged = true; + } + nextRequest = pager.getOrThrow(config, "nextRequest", "LiveEventPager"); + } + + override fun nextPage() = nextPage(0); + + override fun convertResult(obj: V8ValueObject): IPlatformLiveEvent { + return IPlatformLiveEvent.fromV8(config, obj, "LiveEventPager"); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt index abea9550..4aca63aa 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/platforms/js/models/JSVideoDetails.kt @@ -7,6 +7,7 @@ import com.caoccao.javet.values.reference.V8ValueObject import com.futo.platformplayer.api.media.IPlatformClient 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.live.IPlatformLiveEvent import com.futo.platformplayer.api.media.models.playback.IPlaybackTracker import com.futo.platformplayer.api.media.models.ratings.IRating import com.futo.platformplayer.api.media.models.ratings.RatingLikes @@ -26,12 +27,15 @@ import com.futo.platformplayer.getOrThrow import com.futo.platformplayer.getOrThrowNullable import com.futo.platformplayer.invokeV8 import com.futo.platformplayer.states.StateDeveloper +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json class JSVideoDetails : JSVideo, IPlatformVideoDetails { private val _plugin: JSClient; private val _hasGetComments: Boolean; private val _hasGetContentRecommendations: Boolean; private val _hasGetPlaybackTracker: Boolean; + private val _hasGetVODEvents: Boolean; //Details override val description : String; @@ -47,7 +51,6 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { override val subtitles: List; - constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin.config, obj) { val contextName = "VideoDetails"; _plugin = plugin; @@ -72,6 +75,7 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { _hasGetComments = _content.has("getComments"); _hasGetPlaybackTracker = _content.has("getPlaybackTracker"); _hasGetContentRecommendations = _content.has("getContentRecommendations"); + _hasGetVODEvents = _content.has("getVODEvents"); } override fun getPlaybackTracker(): IPlaybackTracker? { @@ -138,4 +142,15 @@ class JSVideoDetails : JSVideo, IPlatformVideoDetails { return@busy JSCommentPager(_pluginConfig, client, commentPager); } } + + fun hasVODEvents(): Boolean{ + return _hasGetVODEvents; + } + fun getVODEvents(url: String): IPager? = _plugin.busy { + if(!_hasGetVODEvents) + return@busy null; + + return@busy JSVODEventPager(_plugin.config, _plugin, + _content.invokeV8("getVODEvents", arrayOf())); + } } \ 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 3dbde51b..97392221 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 @@ -1777,12 +1777,19 @@ class VideoDetailView : ConstraintLayout { _liveChat?.stop(); _liveChat = null; + var gotLive = false; if (video.isLive && video.live != null) { loadLiveChat(video); + gotLive = true; } - if (video.isLive && video.live == null && !video.video.videoSources.any()) + if (video.isLive && video.live == null && !video.video.videoSources.any()) { startLiveTry(video); - + gotLive = true; + } + if(!gotLive && video is JSVideoDetails && video.hasVODEvents()) { + Logger.i(TAG, "Loading VOD chat"); + loadVODChat(video); + } _player.updateNextPrevious(); updateMoreButtons(); @@ -1806,6 +1813,43 @@ class VideoDetailView : ConstraintLayout { _taskLoadRecommendations.run(videoDetail.url) } } + fun loadVODChat(video: IPlatformVideoDetails) { + _liveChat?.stop(); + _container_content_liveChat.cancel(); + _liveChat = null; + if(video !is JSVideoDetails) + return; + fragment.lifecycleScope.launch(Dispatchers.IO) { + try { + var livePager: IPager?; + try { + //TODO: Create video.getLiveEvents shortcut/optimalization + livePager = video.getVODEvents(video.url); + } catch (ex: Throwable) { + Logger.e(TAG, "Failed to obtain VODEvents pager", ex); + livePager = null; + } + val liveChat = livePager?.let { + val liveChatManager = LiveChatManager(fragment.lifecycleScope, livePager, video.viewCount); + liveChatManager.start(); + return@let liveChatManager; + } + _liveChat = liveChat; + + fragment.lifecycleScope.launch(Dispatchers.Main) { + try { + _container_content_liveChat.load(fragment.lifecycleScope, liveChat, null, if(liveChat != null) video.viewCount else null); + switchContentView(_container_content_liveChat); + } catch (e: Throwable) { + Logger.e(TAG, "Failed to switch content view to vod chat.", e); + } + } + } + catch(ex: Throwable) { + Logger.e(TAG, "Failed to load vod chat", ex); + } + } + } fun loadLiveChat(video: IPlatformVideoDetails) { _liveChat?.stop(); _container_content_liveChat.cancel(); @@ -2962,6 +3006,8 @@ class VideoDetailView : ConstraintLayout { private fun setLastPositionMilliseconds(positionMilliseconds: Long, updateHistory: Boolean) { lastPositionMilliseconds = positionMilliseconds; + _liveChat?.setVideoPosition(lastPositionMilliseconds); + val v = video ?: return; val currentTime = System.currentTimeMillis(); if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) { diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt index a2ed19ef..e7f143be 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/LiveChatOverlay.kt @@ -202,6 +202,8 @@ class LiveChatOverlay : LinearLayout { if(viewerCount != null) _textViewers.text = viewerCount.toHumanNumber() + " " + context.getString(R.string.viewers); + else if(manager != null && manager.isVOD) + _textViewers.text = manager.viewCount.toHumanNumber() + " past viewers"; else if(manager != null) _textViewers.text = manager.viewCount.toHumanNumber() + " " + context.getString(R.string.viewers); else