Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Kelvin 2025-07-30 18:22:27 +02:00
commit b5da0d4462
24 changed files with 270 additions and 135 deletions

View file

@ -424,7 +424,7 @@ class UIDialogs {
} }
fun showCastingDialog(context: Context) { fun showCastingDialog(context: Context, ownerActivity: Activity? = null) {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null) { if (d != null) {
val dialog = ConnectedCastingDialog(context); val dialog = ConnectedCastingDialog(context);
@ -432,6 +432,7 @@ class UIDialogs {
dialog.setOwnerActivity(context) dialog.setOwnerActivity(context)
} }
registerDialogOpened(dialog); registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
} else { } else {
@ -444,21 +445,24 @@ class UIDialogs {
if (c is Activity) { if (c is Activity) {
dialog.setOwnerActivity(c); dialog.setOwnerActivity(c);
} }
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
} }
} }
fun showCastingTutorialDialog(context: Context) { fun showCastingTutorialDialog(context: Context, ownerActivity: Activity? = null) {
val dialog = CastingHelpDialog(context); val dialog = CastingHelpDialog(context);
registerDialogOpened(dialog); registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
} }
fun showCastingAddDialog(context: Context) { fun showCastingAddDialog(context: Context, ownerActivity: Activity? = null) {
val dialog = CastingAddDialog(context); val dialog = CastingAddDialog(context);
registerDialogOpened(dialog); registerDialogOpened(dialog);
ownerActivity?.let { dialog.setOwnerActivity(it) }
dialog.setOnDismissListener { registerDialogClosed(dialog) }; dialog.setOnDismissListener { registerDialogClosed(dialog) };
dialog.show(); dialog.show();
} }

View file

@ -106,7 +106,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
}; };
_buttonTutorial.setOnClickListener { _buttonTutorial.setOnClickListener {
UIDialogs.showCastingTutorialDialog(context) UIDialogs.showCastingTutorialDialog(context, ownerActivity)
dismiss() dismiss()
} }
} }
@ -130,7 +130,7 @@ class CastingAddDialog(context: Context?) : AlertDialog(context) {
private fun performDismiss(shouldShowCastingDialog: Boolean = true) { private fun performDismiss(shouldShowCastingDialog: Boolean = true) {
if (shouldShowCastingDialog) { if (shouldShowCastingDialog) {
UIDialogs.showCastingDialog(context); UIDialogs.showCastingDialog(context, ownerActivity);
} }
dismiss(); dismiss();

View file

@ -53,7 +53,7 @@ class CastingHelpDialog(context: Context?) : AlertDialog(context) {
findViewById<BigButton>(R.id.button_close).onClick.subscribe { findViewById<BigButton>(R.id.button_close).onClick.subscribe {
dismiss() dismiss()
UIDialogs.showCastingAddDialog(context) UIDialogs.showCastingAddDialog(context, ownerActivity)
} }
} }

View file

@ -83,7 +83,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
_buttonClose.setOnClickListener { dismiss(); }; _buttonClose.setOnClickListener { dismiss(); };
_buttonAdd.setOnClickListener { _buttonAdd.setOnClickListener {
UIDialogs.showCastingAddDialog(context); UIDialogs.showCastingAddDialog(context, ownerActivity);
dismiss(); dismiss();
}; };
@ -139,9 +139,6 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
} }
} }
} }
_textNoDevicesFound.visibility = if (_devices.isEmpty()) View.VISIBLE else View.GONE;
_recyclerDevices.visibility = if (_devices.isNotEmpty()) View.VISIBLE else View.GONE;
} }
override fun dismiss() { override fun dismiss() {

View file

@ -209,7 +209,7 @@ class ChannelContentsFragment(private val subType: String? = null) : Fragment(),
_searchView = searchView _searchView = searchView
updateSearchViewVisibility() updateSearchViewVisibility()
_adapterResults = PreviewContentListAdapter(view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar, viewsToPrepend = arrayListOf(searchView)).apply { _adapterResults = PreviewContentListAdapter(lifecycleScope, view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar, viewsToPrepend = arrayListOf(searchView)).apply {
this.onContentUrlClicked.subscribe(this@ChannelContentsFragment.onContentUrlClicked::emit); this.onContentUrlClicked.subscribe(this@ChannelContentsFragment.onContentUrlClicked::emit);
this.onUrlClicked.subscribe(this@ChannelContentsFragment.onUrlClicked::emit); this.onUrlClicked.subscribe(this@ChannelContentsFragment.onUrlClicked::emit);
this.onContentClicked.subscribe { content, num -> this.onContentClicked.subscribe { content, num ->

View file

@ -148,7 +148,7 @@ class ChannelPlaylistsFragment : Fragment(), IChannelTabFragment {
_recyclerResults = view.findViewById(R.id.recycler_videos) _recyclerResults = view.findViewById(R.id.recycler_videos)
_adapterResults = PreviewContentListAdapter( _adapterResults = PreviewContentListAdapter(
view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar lifecycleScope, view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.channel.progressBar
).apply { ).apply {
this.onContentUrlClicked.subscribe(this@ChannelPlaylistsFragment.onContentUrlClicked::emit) this.onContentUrlClicked.subscribe(this@ChannelPlaylistsFragment.onContentUrlClicked::emit)
this.onUrlClicked.subscribe(this@ChannelPlaylistsFragment.onUrlClicked::emit) this.onUrlClicked.subscribe(this@ChannelPlaylistsFragment.onUrlClicked::emit)

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.util.Log import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.R import com.futo.platformplayer.R
@ -19,6 +20,7 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
import com.futo.platformplayer.api.media.platforms.js.models.JSWeb import com.futo.platformplayer.api.media.platforms.js.models.JSWeb
import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.api.media.structures.IPager
import com.futo.platformplayer.fragment.mainactivity.main.ShortView.Companion
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateMeta import com.futo.platformplayer.states.StateMeta
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
@ -34,6 +36,9 @@ import com.futo.platformplayer.views.adapters.feedtypes.PreviewVideoViewHolder
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
import com.futo.platformplayer.withTimestamp import com.futo.platformplayer.withTimestamp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.floor import kotlin.math.floor
import kotlin.math.max import kotlin.math.max
@ -59,7 +64,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
player.modifyState("ThumbnailPlayer") { state -> state.muted = true }; player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
_exoPlayer = player; _exoPlayer = player;
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply { return PreviewContentListAdapter(fragment.lifecycleScope, context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(), arrayListOf(), shouldShowTimeBar).apply {
attachAdapterEvents(this); attachAdapterEvents(this);
} }
} }
@ -246,8 +251,15 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
} }
//TODO: Is this still necessary? //TODO: Is this still necessary?
if(viewHolder.childViewHolder is ContentPreviewViewHolder) if(viewHolder.childViewHolder is ContentPreviewViewHolder) {
(recyclerData.adapter as PreviewContentListAdapter?)?.preview(viewHolder.childViewHolder) fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
(recyclerData.adapter as PreviewContentListAdapter?)?.preview(viewHolder.childViewHolder)
} catch (e: Throwable) {
Logger.e(TAG, "playPreview failed", e)
}
}
}
} }
private fun stopVideo() { private fun stopVideo() {

View file

@ -54,6 +54,8 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideo
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.StateCasting
import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event0
import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event3 import com.futo.platformplayer.constructs.Event3
@ -563,7 +565,13 @@ class ShortView : FrameLayout {
var toSet: ISubtitleSource? = subtitleSource var toSet: ISubtitleSource? = subtitleSource
if (_lastSubtitleSource == subtitleSource) toSet = null if (_lastSubtitleSource == subtitleSource) toSet = null
player.swapSubtitles(mainFragment.lifecycleScope, toSet) mainFragment.lifecycleScope.launch(Dispatchers.Main) {
try {
player.swapSubtitles(toSet)
} catch (e: Throwable) {
Logger.e(TAG, "handleSelectSubtitleTrack failed", e)
}
}
_lastSubtitleSource = toSet _lastSubtitleSource = toSet
} }
@ -852,9 +860,16 @@ class ShortView : FrameLayout {
} }
}) })
else player.setArtwork(null) else player.setArtwork(null)
player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0)
if (subtitleSource != null) player.swapSubtitles(mainFragment.lifecycleScope, subtitleSource) mainFragment.lifecycleScope.launch(Dispatchers.Main) {
player.seekTo(resumePositionMs) try {
player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0)
if (subtitleSource != null) player.swapSubtitles(subtitleSource)
player.seekTo(resumePositionMs)
} catch (e: Throwable) {
Logger.e(TAG, "playVideo failed", e)
}
}
_lastVideoSource = videoSource _lastVideoSource = videoSource
_lastAudioSource = audioSource _lastAudioSource = audioSource

View file

@ -454,6 +454,29 @@ class VideoDetailView : ConstraintLayout {
fragment.navigate<VideoDetailFragment>(it.targetUrl); fragment.navigate<VideoDetailFragment>(it.targetUrl);
}; };
_container_content_liveChat.onUrlClick.subscribe { uri ->
val c = context
if (c is MainActivity) {
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
if (!c.handleUrl(uri.toString())) {
Intent(Intent.ACTION_VIEW, uri).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
context.startActivity(this)
}
}
} catch (e: Throwable) {
Log.e(TAG, "Failed to handle live chat URL")
}
}
} else {
Intent(Intent.ACTION_VIEW, uri).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
context.startActivity(this)
}
}
}
_monetization.onSupportTap.subscribe { _monetization.onSupportTap.subscribe {
_container_content_support.setPolycentricProfile(_polycentricProfile); _container_content_support.setPolycentricProfile(_polycentricProfile);
switchContentView(_container_content_support); switchContentView(_container_content_support);
@ -478,11 +501,6 @@ class VideoDetailView : ConstraintLayout {
_player.attachPlayer(); _player.attachPlayer();
_container_content_liveChat.onRaidNow.subscribe {
StatePlayer.instance.clearQueue();
fragment.navigate<VideoDetailFragment>(it.targetUrl);
};
StateApp.instance.preventPictureInPicture.subscribe(this) { StateApp.instance.preventPictureInPicture.subscribe(this) {
Logger.i(TAG, "StateApp.instance.preventPictureInPicture.subscribe preventPictureInPicture = true"); Logger.i(TAG, "StateApp.instance.preventPictureInPicture.subscribe preventPictureInPicture = true");
preventPictureInPicture = true; preventPictureInPicture = true;
@ -1969,13 +1987,28 @@ class VideoDetailView : ConstraintLayout {
else else
_player.setArtwork(null); _player.setArtwork(null);
} }
_player.setSource(videoSource, audioSource, _playWhenReady && playWhenReady, false, resume = resumePositionMs > 0);
if(subtitleSource != null) fragment.lifecycleScope.launch(Dispatchers.Main) {
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource); try {
_player.seekTo(resumePositionMs); _player.setSource(videoSource, audioSource, _playWhenReady && playWhenReady, false, resume = resumePositionMs > 0);
if(subtitleSource != null)
_player.swapSubtitles(subtitleSource);
_player.seekTo(resumePositionMs);
} catch (e: Throwable) {
Logger.e(TAG, "loadCurrentVideo failed", e)
}
}
} }
else else {
loadCurrentVideoCast(video, videoSource, audioSource, subtitleSource, resumePositionMs, Settings.instance.playback.getDefaultPlaybackSpeed().toDouble()); fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
loadCurrentVideoCast(video, videoSource, audioSource, subtitleSource, resumePositionMs, Settings.instance.playback.getDefaultPlaybackSpeed().toDouble());
} catch (e: Throwable) {
Logger.e(TAG, "loadCurrentVideo failed (casting)", e)
}
}
}
_lastVideoSource = videoSource; _lastVideoSource = videoSource;
_lastAudioSource = audioSource; _lastAudioSource = audioSource;
@ -1990,47 +2023,45 @@ class VideoDetailView : ConstraintLayout {
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_load_media), ex); UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_load_media), ex);
} }
} }
private fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) { private suspend fun loadCurrentVideoCast(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)") Logger.i(TAG, "loadCurrentVideoCast(video=$video, videoSource=$videoSource, audioSource=$audioSource, resumePositionMs=$resumePositionMs)")
castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed) castIfAvailable(context.contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed)
} }
private fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) { private suspend fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
fragment.lifecycleScope.launch(Dispatchers.IO) { try {
val plugin = if (videoSource is JSSource) videoSource.getUnderlyingPlugin()
else if (audioSource is JSSource) audioSource.getUnderlyingPlugin()
else null
val startId = plugin?.getUnderlyingPlugin()?.runtimeId
try { try {
val plugin = if (videoSource is JSSource) videoSource.getUnderlyingPlugin() val castingSucceeded = StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
else if (audioSource is JSSource) audioSource.getUnderlyingPlugin() _cast.setLoading(it)
else null }, onLoadingEstimate = {
_cast.setLoading(it)
})
val startId = plugin?.getUnderlyingPlugin()?.runtimeId if (castingSucceeded) {
try { withContext(Dispatchers.Main) {
val castingSucceeded = StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = { _cast.setVideoDetails(video, resumePositionMs / 1000);
_cast.setLoading(it) setCastEnabled(true);
}, onLoadingEstimate = {
_cast.setLoading(it)
})
if (castingSucceeded) {
withContext(Dispatchers.Main) {
_cast.setVideoDetails(video, resumePositionMs / 1000);
setCastEnabled(true);
}
} }
} catch (e: ScriptReloadRequiredException) {
Log.i(TAG, "Reload required exception", e)
if (plugin == null)
throw e
if (startId != -1 && plugin.getUnderlyingPlugin().runtimeId != startId)
throw e
StatePlatform.instance.handleReloadRequired(e, {
fetchVideo()
});
} }
} catch (e: Throwable) { } catch (e: ScriptReloadRequiredException) {
Logger.e(TAG, "loadCurrentVideoCast", e) Log.i(TAG, "Reload required exception", e)
if (plugin == null)
throw e
if (startId != -1 && plugin.getUnderlyingPlugin().runtimeId != startId)
throw e
StatePlatform.instance.handleReloadRequired(e, {
fetchVideo()
});
} }
} catch (e: Throwable) {
Logger.e(TAG, "loadCurrentVideoCast", e)
} }
} }
@ -2545,11 +2576,17 @@ class VideoDetailView : ConstraintLayout {
if(_lastVideoSource == videoSource) if(_lastVideoSource == videoSource)
return; return;
val d = StateCasting.instance.activeDevice; fragment.lifecycleScope.launch(Dispatchers.Main) {
if (d != null && d.connectionState == CastConnectionState.CONNECTED) try {
castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); val d = StateCasting.instance.activeDevice;
else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true)) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
_player.hideControls(false); //TODO: Disable player? castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true))
_player.hideControls(false); //TODO: Disable player?
} catch (e: Throwable) {
Logger.e(TAG, "handleSelectVideoTrack failed", e)
}
}
_lastVideoSource = videoSource; _lastVideoSource = videoSource;
} }
@ -2560,11 +2597,17 @@ class VideoDetailView : ConstraintLayout {
if(_lastAudioSource == audioSource) if(_lastAudioSource == audioSource)
return; return;
val d = StateCasting.instance.activeDevice; fragment.lifecycleScope.launch(Dispatchers.Main) {
if (d != null && d.connectionState == CastConnectionState.CONNECTED) try {
castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed) val d = StateCasting.instance.activeDevice;
else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true)) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
_player.hideControls(false); //TODO: Disable player? castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed)
else if (!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
_player.hideControls(false); //TODO: Disable player?
} catch (e: Throwable) {
Logger.e(TAG, "handleSelectAudioTrack failed", e)
}
}
_lastAudioSource = audioSource; _lastAudioSource = audioSource;
} }
@ -2576,12 +2619,18 @@ class VideoDetailView : ConstraintLayout {
if(_lastSubtitleSource == subtitleSource) if(_lastSubtitleSource == subtitleSource)
toSet = null; toSet = null;
val d = StateCasting.instance.activeDevice; fragment.lifecycleScope.launch(Dispatchers.Main) {
if (d != null && d.connectionState == CastConnectionState.CONNECTED) try {
castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); val d = StateCasting.instance.activeDevice;
else if (d != null && d.connectionState == CastConnectionState.CONNECTED)
_player.swapSubtitles(fragment.lifecycleScope, toSet); castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
else {
_player.swapSubtitles(toSet);
}
} catch (e: Throwable) {
Logger.e(TAG, "handleSelectSubtitleTrack failed", e)
}
}
_lastSubtitleSource = toSet; _lastSubtitleSource = toSet;
} }

View file

@ -10,7 +10,7 @@ abstract class ContentPreviewViewHolder(itemView: View) : ViewHolder(itemView) {
abstract fun bind(content: IPlatformContent); abstract fun bind(content: IPlatformContent);
abstract fun preview(details: IPlatformContentDetails?, paused: Boolean); abstract suspend fun preview(details: IPlatformContentDetails?, paused: Boolean);
abstract fun stopPreview(); abstract fun stopPreview();
abstract fun pausePreview(); abstract fun pausePreview();
abstract fun resumePreview(); abstract fun resumePreview();

View file

@ -11,7 +11,7 @@ class EmptyPreviewViewHolder(viewGroup: ViewGroup) : ContentPreviewViewHolder(Vi
override fun bind(content: IPlatformContent) {} override fun bind(content: IPlatformContent) {}
override fun preview(details: IPlatformContentDetails?, paused: Boolean) {} override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) {}
override fun stopPreview() {} override fun stopPreview() {}

View file

@ -29,7 +29,7 @@ class PreviewChannelViewHolder : ContentPreviewViewHolder {
override fun bind(content: IPlatformContent) = view.bind(content); override fun bind(content: IPlatformContent) = view.bind(content);
override fun preview(details: IPlatformContentDetails?, paused: Boolean) = Unit; override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) = Unit;
override fun stopPreview() = Unit; override fun stopPreview() = Unit;
override fun pausePreview() = Unit; override fun pausePreview() = Unit;
override fun resumePreview() = Unit; override fun resumePreview() = Unit;

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorLink
import com.futo.platformplayer.api.media.models.contents.ContentType import com.futo.platformplayer.api.media.models.contents.ContentType
import com.futo.platformplayer.api.media.models.contents.IPlatformContent import com.futo.platformplayer.api.media.models.contents.IPlatformContent
@ -15,6 +16,8 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2 import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.debug.Stopwatch import com.futo.platformplayer.debug.Stopwatch
import com.futo.platformplayer.fragment.mainactivity.main.ShortView
import com.futo.platformplayer.fragment.mainactivity.main.ShortView.Companion
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePlatform
@ -23,6 +26,9 @@ import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder import com.futo.platformplayer.views.adapters.ContentPreviewViewHolder
import com.futo.platformplayer.views.adapters.EmptyPreviewViewHolder import com.futo.platformplayer.views.adapters.EmptyPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.internal.platform.Platform import okhttp3.internal.platform.Platform
class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewViewHolder> { class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewViewHolder> {
@ -33,6 +39,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
private val _feedStyle : FeedStyle; private val _feedStyle : FeedStyle;
private var _paused: Boolean = false; private var _paused: Boolean = false;
private val _shouldShowTimeBar: Boolean private val _shouldShowTimeBar: Boolean
private val _scope: CoroutineScope
val onUrlClicked = Event1<String>(); val onUrlClicked = Event1<String>();
val onContentUrlClicked = Event2<String, ContentType>(); val onContentUrlClicked = Event2<String, ContentType>();
@ -43,15 +50,9 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
val onAddToWatchLaterClicked = Event1<IPlatformContent>(); val onAddToWatchLaterClicked = Event1<IPlatformContent>();
val onLongPress = Event1<IPlatformContent>(); val onLongPress = Event1<IPlatformContent>();
private var _taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>( private var _taskLoadContent: TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>
StateApp.instance.scopeGetter, { (viewHolder, video) ->
val stopwatch = Stopwatch()
val contentDetails = StatePlatform.instance.getContentDetails(video.url).await();
stopwatch.logAndNext(TAG, "Retrieving video detail (IO thread)")
return@TaskHandler Pair(viewHolder, contentDetails)
}).exception<Throwable> { Logger.e(TAG, "Failed to retrieve preview content.", it) }.success { previewContentDetails(it.first, it.second) }
constructor(context: Context, feedStyle : FeedStyle, dataSet: ArrayList<IPlatformContent>, exoPlayer: PlayerManager? = null, constructor(scope: CoroutineScope, context: Context, feedStyle : FeedStyle, dataSet: ArrayList<IPlatformContent>, exoPlayer: PlayerManager? = null,
initialPlay: Boolean = false, viewsToPrepend: ArrayList<View> = arrayListOf(), initialPlay: Boolean = false, viewsToPrepend: ArrayList<View> = arrayListOf(),
viewsToAppend: ArrayList<View> = arrayListOf(), shouldShowTimeBar: Boolean = true) : super(context, viewsToPrepend, viewsToAppend) { viewsToAppend: ArrayList<View> = arrayListOf(), shouldShowTimeBar: Boolean = true) : super(context, viewsToPrepend, viewsToAppend) {
@ -60,6 +61,24 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
this._initialPlay = initialPlay; this._initialPlay = initialPlay;
this._exoPlayer = exoPlayer; this._exoPlayer = exoPlayer;
this._shouldShowTimeBar = shouldShowTimeBar this._shouldShowTimeBar = shouldShowTimeBar
this._scope = scope
_taskLoadContent = TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>(
{ scope }, { (viewHolder, video) ->
val stopwatch = Stopwatch()
val contentDetails = StatePlatform.instance.getContentDetails(video.url).await();
stopwatch.logAndNext(TAG, "Retrieving video detail (IO thread)")
return@TaskHandler Pair(viewHolder, contentDetails)
}).exception<Throwable> { Logger.e(TAG, "Failed to retrieve preview content.", it) }.success {
_scope.launch(Dispatchers.Main) {
try {
previewContentDetails(it.first, it.second)
} catch (e: Throwable) {
Logger.e(TAG, "bindChild preview failed", e)
}
}
}
} }
override fun getChildCount(): Int = _dataSet.size; override fun getChildCount(): Int = _dataSet.size;
@ -132,12 +151,18 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
_initialPlay = false; _initialPlay = false;
if (_feedStyle != FeedStyle.THUMBNAIL) { if (_feedStyle != FeedStyle.THUMBNAIL) {
preview(holder); _scope.launch(Dispatchers.Main) {
try {
preview(holder)
} catch (e: Throwable) {
Logger.e(TAG, "bindChild preview failed", e)
}
}
} }
} }
} }
fun preview(viewHolder: ContentPreviewViewHolder) { suspend fun preview(viewHolder: ContentPreviewViewHolder) {
Log.v(TAG, "previewing content"); Log.v(TAG, "previewing content");
if (viewHolder == _previewingViewHolder) if (viewHolder == _previewingViewHolder)
return return
@ -175,7 +200,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
onAddToWatchLaterClicked.clear(); onAddToWatchLaterClicked.clear();
} }
private fun previewContentDetails(viewHolder: ContentPreviewViewHolder, videoDetails: IPlatformContentDetails?) { private suspend fun previewContentDetails(viewHolder: ContentPreviewViewHolder, videoDetails: IPlatformContentDetails?) {
_previewingViewHolder?.stopPreview(); _previewingViewHolder?.stopPreview();
viewHolder.preview(videoDetails, _paused); viewHolder.preview(videoDetails, _paused);
_previewingViewHolder = viewHolder; _previewingViewHolder = viewHolder;

View file

@ -25,7 +25,7 @@ class PreviewLockedViewHolder : ContentPreviewViewHolder {
override fun bind(content: IPlatformContent) = view.bind(content); override fun bind(content: IPlatformContent) = view.bind(content);
override fun preview(details: IPlatformContentDetails?, paused: Boolean) { } override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) { }
override fun stopPreview() { } override fun stopPreview() { }
override fun pausePreview() { } override fun pausePreview() { }
override fun resumePreview() { } override fun resumePreview() { }

View file

@ -185,7 +185,7 @@ class PreviewNestedVideoView : PreviewVideoView {
} }
} }
override fun preview(video: IPlatformContentDetails?, paused: Boolean) { override suspend fun preview(video: IPlatformContentDetails?, paused: Boolean) {
if(video != null) { if(video != null) {
super.preview(video, paused); super.preview(video, paused);
} else if(_content is IPlatformVideoDetails) _contentNested?.let { } else if(_content is IPlatformVideoDetails) _contentNested?.let {

View file

@ -40,7 +40,7 @@ class PreviewNestedVideoViewHolder : ContentPreviewViewHolder {
view.bind(content); view.bind(content);
} }
override fun preview(details: IPlatformContentDetails?, paused: Boolean) { override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) {
view.preview(details, paused); view.preview(details, paused);
} }

View file

@ -58,7 +58,7 @@ class PreviewPlaceholderViewHolder : ContentPreviewViewHolder {
} }
} }
override fun preview(details: IPlatformContentDetails?, paused: Boolean) { } override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) { }
override fun stopPreview() { } override fun stopPreview() { }
override fun pausePreview() { } override fun pausePreview() { }
override fun resumePreview() { } override fun resumePreview() { }

View file

@ -28,7 +28,7 @@ class PreviewPlaylistViewHolder : ContentPreviewViewHolder {
override fun bind(content: IPlatformContent) = view.bind(content); override fun bind(content: IPlatformContent) = view.bind(content);
override fun preview(details: IPlatformContentDetails?, paused: Boolean) = Unit; override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) = Unit;
override fun stopPreview() = Unit; override fun stopPreview() = Unit;
override fun pausePreview() = Unit; override fun pausePreview() = Unit;
override fun resumePreview() = Unit; override fun resumePreview() = Unit;

View file

@ -28,7 +28,7 @@ class PreviewPostViewHolder : ContentPreviewViewHolder {
override fun bind(content: IPlatformContent) = view.bind(content); override fun bind(content: IPlatformContent) = view.bind(content);
override fun preview(details: IPlatformContentDetails?, paused: Boolean) {}; override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) {};
override fun stopPreview() {}; override fun stopPreview() {};
override fun pausePreview() {}; override fun pausePreview() {};
override fun resumePreview() {}; override fun resumePreview() {};

View file

@ -248,7 +248,7 @@ open class PreviewVideoView : LinearLayout {
_textVideoMetadata.text = metadata + timeMeta; _textVideoMetadata.text = metadata + timeMeta;
} }
open fun preview(video: IPlatformContentDetails?, paused: Boolean) { open suspend fun preview(video: IPlatformContentDetails?, paused: Boolean) {
if(video == null) if(video == null)
return; return;
Logger.i(TAG, "Previewing"); Logger.i(TAG, "Previewing");

View file

@ -42,7 +42,7 @@ class PreviewVideoViewHolder : ContentPreviewViewHolder {
override fun bind(content: IPlatformContent) = view.bind(content); override fun bind(content: IPlatformContent) = view.bind(content);
override fun preview(details: IPlatformContentDetails?, paused: Boolean) = view.preview(details, paused); override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) = view.preview(details, paused);
override fun stopPreview() = view.stopPreview(); override fun stopPreview() = view.stopPreview();
override fun pausePreview() = view.pausePreview(); override fun pausePreview() = view.pausePreview();
override fun resumePreview() = view.resumePreview(); override fun resumePreview() = view.resumePreview();

View file

@ -2,11 +2,14 @@ package com.futo.platformplayer.views.overlays
import android.animation.LayoutTransition import android.animation.LayoutTransition
import android.content.Context import android.content.Context
import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.graphics.PointF import android.graphics.PointF
import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.View import android.view.View
import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import android.widget.Button import android.widget.Button
@ -19,6 +22,7 @@ import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.LiveChatManager import com.futo.platformplayer.api.media.LiveChatManager
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
import com.futo.platformplayer.api.media.models.live.ILiveEventChatMessage import com.futo.platformplayer.api.media.models.live.ILiveEventChatMessage
@ -41,6 +45,7 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import toAndroidColor import toAndroidColor
import androidx.core.net.toUri
class LiveChatOverlay : LinearLayout { class LiveChatOverlay : LinearLayout {
@ -92,6 +97,7 @@ class LiveChatOverlay : LinearLayout {
val onRaidNow = Event1<LiveEventRaid>(); val onRaidNow = Event1<LiveEventRaid>();
val onRaidPrevent = Event1<LiveEventRaid>(); val onRaidPrevent = Event1<LiveEventRaid>();
val onUrlClick = Event1<Uri>()
private val _argJsonSerializer = Json; private val _argJsonSerializer = Json;
@ -116,6 +122,18 @@ class LiveChatOverlay : LinearLayout {
view?.evaluateJavascript("setInterval(()=>{" + toRemoveJSInterval + "}, 1000)") {}; view?.evaluateJavascript("setInterval(()=>{" + toRemoveJSInterval + "}, 1000)") {};
}; };
} }
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
onUrlClick.emit(request.url)
return true
}
// API < 24
@Suppress("DEPRECATION")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
onUrlClick.emit(url.toUri())
return true
}
}; };
_chatContainer = findViewById(R.id.chatContainer); _chatContainer = findViewById(R.id.chatContainer);

View file

@ -126,7 +126,7 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
_evMuteChanged.add(callback); _evMuteChanged.add(callback);
} }
fun setPreview(video: IPlatformVideoDetails) { suspend fun setPreview(video: IPlatformVideoDetails) {
if (video.live != null) { if (video.live != null) {
setSource(video.live, null,true, false); setSource(video.live, null,true, false);
} else { } else {

View file

@ -365,48 +365,63 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
return _chapters?.let { chaps -> chaps.find { pos.toDouble() / 1000 > it.timeStart && pos.toDouble() / 1000 < it.timeEnd && (toIgnore.isEmpty() || !toIgnore.contains(it)) } }; return _chapters?.let { chaps -> chaps.find { pos.toDouble() / 1000 > it.timeStart && pos.toDouble() / 1000 < it.timeEnd && (toIgnore.isEmpty() || !toIgnore.contains(it)) } };
} }
fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false, resume: Boolean = false) { suspend fun setSource(videoSource: IVideoSource?, audioSource: IAudioSource? = null, play: Boolean = false, keepSubtitles: Boolean = false, resume: Boolean = false) {
swapSources(videoSource, audioSource,resume, play, keepSubtitles); swapSources(videoSource, audioSource,resume, play, keepSubtitles);
} }
fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean { suspend fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean {
var videoSourceUsed = videoSource; val didSet = withContext(Dispatchers.IO) {
var audioSourceUsed = audioSource; var videoSourceUsed = videoSource;
if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){ var audioSourceUsed = audioSource;
videoSource.getUnderlyingPlugin()?.busy { if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){
videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource); videoSource.getUnderlyingPlugin()?.busy {
audioSourceUsed = null; videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource);
audioSourceUsed = null;
}
} }
val didSetVideo = swapSourceInternal(videoSourceUsed, play, resume);
val didSetAudio = swapSourceInternal(audioSourceUsed, play, resume);
if(!keepSubtitles)
_lastSubtitleMediaSource = null;
return@withContext didSetVideo && didSetAudio
} }
val didSetVideo = swapSourceInternal(videoSourceUsed, play, resume); return withContext(Dispatchers.Main) {
val didSetAudio = swapSourceInternal(audioSourceUsed, play, resume); if (didSet)
if(!keepSubtitles) return@withContext loadSelectedSources(play, resume)
_lastSubtitleMediaSource = null; else
if(didSetVideo && didSetAudio) return@withContext true
return loadSelectedSources(play, resume); }
else
return true;
} }
fun swapSource(videoSource: IVideoSource?, resume: Boolean = true, play: Boolean = true): Boolean { suspend fun swapSource(videoSource: IVideoSource?, resume: Boolean = true, play: Boolean = true): Boolean {
var videoSourceUsed = videoSource; val didSet = withContext(Dispatchers.IO) {
if(videoSource is JSDashManifestRawSource && lastVideoSource is JSDashManifestMergingRawSource) var videoSourceUsed = videoSource;
videoSourceUsed = JSDashManifestMergingRawSource(videoSource, (lastVideoSource as JSDashManifestMergingRawSource).audio); if (videoSource is JSDashManifestRawSource && lastVideoSource is JSDashManifestMergingRawSource)
val didSet = swapSourceInternal(videoSourceUsed, play, resume); videoSourceUsed = JSDashManifestMergingRawSource(videoSource, (lastVideoSource as JSDashManifestMergingRawSource).audio);
if(didSet) return@withContext swapSourceInternal(videoSourceUsed, play, resume);
return loadSelectedSources(play, resume); }
else return withContext(Dispatchers.Main) {
return true; if (didSet)
return@withContext loadSelectedSources(play, resume);
else
return@withContext true;
}
} }
fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean { suspend fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean {
if(audioSource is JSDashManifestRawAudioSource && lastVideoSource is JSDashManifestMergingRawSource) withContext(Dispatchers.IO) {
swapSourceInternal(JSDashManifestMergingRawSource((lastVideoSource as JSDashManifestMergingRawSource).video, audioSource), play, resume); if (audioSource is JSDashManifestRawAudioSource && lastVideoSource is JSDashManifestMergingRawSource)
else swapSourceInternal(JSDashManifestMergingRawSource((lastVideoSource as JSDashManifestMergingRawSource).video, audioSource), play, resume);
swapSourceInternal(audioSource, play, resume); else
return loadSelectedSources(play, resume); swapSourceInternal(audioSource, play, resume);
}
return withContext(Dispatchers.Main) {
return@withContext loadSelectedSources(play, resume);
}
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
fun swapSubtitles(scope: CoroutineScope, subtitles: ISubtitleSource?) { suspend fun swapSubtitles(subtitles: ISubtitleSource?) {
if(subtitles == null) if(subtitles == null)
clearSubtitles(); clearSubtitles();
else { else {
@ -420,9 +435,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
C.TIME_UNSET); C.TIME_UNSET);
loadSelectedSources(true, true); loadSelectedSources(true, true);
} else { } else {
scope.launch(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
val subUri = subtitles.getSubtitlesURI() ?: return@launch; val subUri = subtitles.getSubtitlesURI() ?: return@withContext;
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
try { try {
_lastSubtitleMediaSource = SingleSampleMediaSource.Factory(DefaultDataSource.Factory(context, DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT))) _lastSubtitleMediaSource = SingleSampleMediaSource.Factory(DefaultDataSource.Factory(context, DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)))