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

View file

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

View file

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

View file

@ -83,7 +83,7 @@ class ConnectCastingDialog(context: Context?) : AlertDialog(context) {
_buttonClose.setOnClickListener { dismiss(); };
_buttonAdd.setOnClickListener {
UIDialogs.showCastingAddDialog(context);
UIDialogs.showCastingAddDialog(context, ownerActivity);
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() {

View file

@ -209,7 +209,7 @@ class ChannelContentsFragment(private val subType: String? = null) : Fragment(),
_searchView = searchView
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.onUrlClicked.subscribe(this@ChannelContentsFragment.onUrlClicked::emit);
this.onContentClicked.subscribe { content, num ->

View file

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

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.util.Log
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
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.platforms.js.models.JSWeb
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.states.StateMeta
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.SlideUpMenuOverlay
import com.futo.platformplayer.withTimestamp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.math.floor
import kotlin.math.max
@ -59,7 +64,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
player.modifyState("ThumbnailPlayer") { state -> state.muted = true };
_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);
}
}
@ -246,8 +251,15 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
}
//TODO: Is this still necessary?
if(viewHolder.childViewHolder is ContentPreviewViewHolder)
(recyclerData.adapter as PreviewContentListAdapter?)?.preview(viewHolder.childViewHolder)
if(viewHolder.childViewHolder is ContentPreviewViewHolder) {
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() {

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.IPlatformVideoDetails
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.Event1
import com.futo.platformplayer.constructs.Event3
@ -563,7 +565,13 @@ class ShortView : FrameLayout {
var toSet: ISubtitleSource? = subtitleSource
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
}
@ -852,9 +860,16 @@ class ShortView : FrameLayout {
}
})
else player.setArtwork(null)
player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0)
if (subtitleSource != null) player.swapSubtitles(mainFragment.lifecycleScope, subtitleSource)
player.seekTo(resumePositionMs)
mainFragment.lifecycleScope.launch(Dispatchers.Main) {
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
_lastAudioSource = audioSource

View file

@ -454,6 +454,29 @@ class VideoDetailView : ConstraintLayout {
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 {
_container_content_support.setPolycentricProfile(_polycentricProfile);
switchContentView(_container_content_support);
@ -478,11 +501,6 @@ class VideoDetailView : ConstraintLayout {
_player.attachPlayer();
_container_content_liveChat.onRaidNow.subscribe {
StatePlayer.instance.clearQueue();
fragment.navigate<VideoDetailFragment>(it.targetUrl);
};
StateApp.instance.preventPictureInPicture.subscribe(this) {
Logger.i(TAG, "StateApp.instance.preventPictureInPicture.subscribe preventPictureInPicture = true");
preventPictureInPicture = true;
@ -1969,13 +1987,28 @@ class VideoDetailView : ConstraintLayout {
else
_player.setArtwork(null);
}
_player.setSource(videoSource, audioSource, _playWhenReady && playWhenReady, false, resume = resumePositionMs > 0);
if(subtitleSource != null)
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource);
_player.seekTo(resumePositionMs);
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
_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
loadCurrentVideoCast(video, videoSource, audioSource, subtitleSource, resumePositionMs, Settings.instance.playback.getDefaultPlaybackSpeed().toDouble());
else {
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;
_lastAudioSource = audioSource;
@ -1990,47 +2023,45 @@ class VideoDetailView : ConstraintLayout {
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)")
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?) {
fragment.lifecycleScope.launch(Dispatchers.IO) {
private suspend fun castIfAvailable(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: ISubtitleSource?, resumePositionMs: Long, speed: Double?) {
try {
val plugin = if (videoSource is JSSource) videoSource.getUnderlyingPlugin()
else if (audioSource is JSSource) audioSource.getUnderlyingPlugin()
else null
val startId = plugin?.getUnderlyingPlugin()?.runtimeId
try {
val plugin = if (videoSource is JSSource) videoSource.getUnderlyingPlugin()
else if (audioSource is JSSource) audioSource.getUnderlyingPlugin()
else null
val castingSucceeded = StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
_cast.setLoading(it)
}, onLoadingEstimate = {
_cast.setLoading(it)
})
val startId = plugin?.getUnderlyingPlugin()?.runtimeId
try {
val castingSucceeded = StateCasting.instance.castIfAvailable(contentResolver, video, videoSource, audioSource, subtitleSource, resumePositionMs, speed, onLoading = {
_cast.setLoading(it)
}, onLoadingEstimate = {
_cast.setLoading(it)
})
if (castingSucceeded) {
withContext(Dispatchers.Main) {
_cast.setVideoDetails(video, resumePositionMs / 1000);
setCastEnabled(true);
}
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) {
Logger.e(TAG, "loadCurrentVideoCast", e)
} 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) {
Logger.e(TAG, "loadCurrentVideoCast", e)
}
}
@ -2545,11 +2576,17 @@ class VideoDetailView : ConstraintLayout {
if(_lastVideoSource == videoSource)
return;
val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
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?
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
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;
}
@ -2560,11 +2597,17 @@ class VideoDetailView : ConstraintLayout {
if(_lastAudioSource == audioSource)
return;
val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed)
else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
_player.hideControls(false); //TODO: Disable player?
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
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;
}
@ -2576,12 +2619,18 @@ class VideoDetailView : ConstraintLayout {
if(_lastSubtitleSource == subtitleSource)
toSet = null;
val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
else
_player.swapSubtitles(fragment.lifecycleScope, toSet);
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED)
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;
}

View file

@ -10,7 +10,7 @@ abstract class ContentPreviewViewHolder(itemView: View) : ViewHolder(itemView) {
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 pausePreview();
abstract fun resumePreview();

View file

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

View file

@ -29,7 +29,7 @@ class PreviewChannelViewHolder : ContentPreviewViewHolder {
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 pausePreview() = Unit;
override fun resumePreview() = Unit;

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
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.IPlatformContent
@ -15,6 +16,8 @@ import com.futo.platformplayer.constructs.Event1
import com.futo.platformplayer.constructs.Event2
import com.futo.platformplayer.constructs.TaskHandler
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.states.StateApp
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.EmptyPreviewViewHolder
import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okhttp3.internal.platform.Platform
class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewViewHolder> {
@ -33,6 +39,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
private val _feedStyle : FeedStyle;
private var _paused: Boolean = false;
private val _shouldShowTimeBar: Boolean
private val _scope: CoroutineScope
val onUrlClicked = Event1<String>();
val onContentUrlClicked = Event2<String, ContentType>();
@ -43,15 +50,9 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
val onAddToWatchLaterClicked = Event1<IPlatformContent>();
val onLongPress = Event1<IPlatformContent>();
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) }
private var _taskLoadContent: TaskHandler<Pair<ContentPreviewViewHolder, IPlatformContent>, Pair<ContentPreviewViewHolder, IPlatformContentDetails>>
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(),
viewsToAppend: ArrayList<View> = arrayListOf(), shouldShowTimeBar: Boolean = true) : super(context, viewsToPrepend, viewsToAppend) {
@ -60,6 +61,24 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
this._initialPlay = initialPlay;
this._exoPlayer = exoPlayer;
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;
@ -132,12 +151,18 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
_initialPlay = false;
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");
if (viewHolder == _previewingViewHolder)
return
@ -175,7 +200,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
onAddToWatchLaterClicked.clear();
}
private fun previewContentDetails(viewHolder: ContentPreviewViewHolder, videoDetails: IPlatformContentDetails?) {
private suspend fun previewContentDetails(viewHolder: ContentPreviewViewHolder, videoDetails: IPlatformContentDetails?) {
_previewingViewHolder?.stopPreview();
viewHolder.preview(videoDetails, _paused);
_previewingViewHolder = viewHolder;

View file

@ -25,7 +25,7 @@ class PreviewLockedViewHolder : ContentPreviewViewHolder {
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 pausePreview() { }
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) {
super.preview(video, paused);
} else if(_content is IPlatformVideoDetails) _contentNested?.let {

View file

@ -40,7 +40,7 @@ class PreviewNestedVideoViewHolder : ContentPreviewViewHolder {
view.bind(content);
}
override fun preview(details: IPlatformContentDetails?, paused: Boolean) {
override suspend fun preview(details: IPlatformContentDetails?, paused: Boolean) {
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 pausePreview() { }
override fun resumePreview() { }

View file

@ -28,7 +28,7 @@ class PreviewPlaylistViewHolder : ContentPreviewViewHolder {
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 pausePreview() = Unit;
override fun resumePreview() = Unit;

View file

@ -28,7 +28,7 @@ class PreviewPostViewHolder : ContentPreviewViewHolder {
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 pausePreview() {};
override fun resumePreview() {};

View file

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

View file

@ -42,7 +42,7 @@ class PreviewVideoViewHolder : ContentPreviewViewHolder {
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 pausePreview() = view.pausePreview();
override fun resumePreview() = view.resumePreview();

View file

@ -2,11 +2,14 @@ package com.futo.platformplayer.views.overlays
import android.animation.LayoutTransition
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.PointF
import android.net.Uri
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.View
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Button
@ -19,6 +22,7 @@ import androidx.recyclerview.widget.LinearSmoothScroller
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.futo.platformplayer.R
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.media.LiveChatManager
import com.futo.platformplayer.api.media.models.live.ILiveChatWindowDescriptor
import com.futo.platformplayer.api.media.models.live.ILiveEventChatMessage
@ -41,6 +45,7 @@ import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import toAndroidColor
import androidx.core.net.toUri
class LiveChatOverlay : LinearLayout {
@ -92,6 +97,7 @@ class LiveChatOverlay : LinearLayout {
val onRaidNow = Event1<LiveEventRaid>();
val onRaidPrevent = Event1<LiveEventRaid>();
val onUrlClick = Event1<Uri>()
private val _argJsonSerializer = Json;
@ -116,6 +122,18 @@ class LiveChatOverlay : LinearLayout {
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);

View file

@ -126,7 +126,7 @@ class FutoThumbnailPlayer : FutoVideoPlayerBase {
_evMuteChanged.add(callback);
}
fun setPreview(video: IPlatformVideoDetails) {
suspend fun setPreview(video: IPlatformVideoDetails) {
if (video.live != null) {
setSource(video.live, null,true, false);
} 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)) } };
}
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);
}
fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean {
var videoSourceUsed = videoSource;
var audioSourceUsed = audioSource;
if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){
videoSource.getUnderlyingPlugin()?.busy {
videoSourceUsed = JSDashManifestMergingRawSource(videoSource, audioSource);
audioSourceUsed = null;
suspend fun swapSources(videoSource: IVideoSource?, audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true, keepSubtitles: Boolean = false): Boolean {
val didSet = withContext(Dispatchers.IO) {
var videoSourceUsed = videoSource;
var audioSourceUsed = audioSource;
if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){
videoSource.getUnderlyingPlugin()?.busy {
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);
val didSetAudio = swapSourceInternal(audioSourceUsed, play, resume);
if(!keepSubtitles)
_lastSubtitleMediaSource = null;
if(didSetVideo && didSetAudio)
return loadSelectedSources(play, resume);
else
return true;
return withContext(Dispatchers.Main) {
if (didSet)
return@withContext loadSelectedSources(play, resume)
else
return@withContext true
}
}
fun swapSource(videoSource: IVideoSource?, resume: Boolean = true, play: Boolean = true): Boolean {
var videoSourceUsed = videoSource;
if(videoSource is JSDashManifestRawSource && lastVideoSource is JSDashManifestMergingRawSource)
videoSourceUsed = JSDashManifestMergingRawSource(videoSource, (lastVideoSource as JSDashManifestMergingRawSource).audio);
val didSet = swapSourceInternal(videoSourceUsed, play, resume);
if(didSet)
return loadSelectedSources(play, resume);
else
return true;
suspend fun swapSource(videoSource: IVideoSource?, resume: Boolean = true, play: Boolean = true): Boolean {
val didSet = withContext(Dispatchers.IO) {
var videoSourceUsed = videoSource;
if (videoSource is JSDashManifestRawSource && lastVideoSource is JSDashManifestMergingRawSource)
videoSourceUsed = JSDashManifestMergingRawSource(videoSource, (lastVideoSource as JSDashManifestMergingRawSource).audio);
return@withContext swapSourceInternal(videoSourceUsed, play, resume);
}
return withContext(Dispatchers.Main) {
if (didSet)
return@withContext loadSelectedSources(play, resume);
else
return@withContext true;
}
}
fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean {
if(audioSource is JSDashManifestRawAudioSource && lastVideoSource is JSDashManifestMergingRawSource)
swapSourceInternal(JSDashManifestMergingRawSource((lastVideoSource as JSDashManifestMergingRawSource).video, audioSource), play, resume);
else
swapSourceInternal(audioSource, play, resume);
return loadSelectedSources(play, resume);
suspend fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean {
withContext(Dispatchers.IO) {
if (audioSource is JSDashManifestRawAudioSource && lastVideoSource is JSDashManifestMergingRawSource)
swapSourceInternal(JSDashManifestMergingRawSource((lastVideoSource as JSDashManifestMergingRawSource).video, audioSource), play, resume);
else
swapSourceInternal(audioSource, play, resume);
}
return withContext(Dispatchers.Main) {
return@withContext loadSelectedSources(play, resume);
}
}
@OptIn(UnstableApi::class)
fun swapSubtitles(scope: CoroutineScope, subtitles: ISubtitleSource?) {
suspend fun swapSubtitles(subtitles: ISubtitleSource?) {
if(subtitles == null)
clearSubtitles();
else {
@ -420,9 +435,9 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
C.TIME_UNSET);
loadSelectedSources(true, true);
} else {
scope.launch(Dispatchers.IO) {
withContext(Dispatchers.IO) {
try {
val subUri = subtitles.getSubtitlesURI() ?: return@launch;
val subUri = subtitles.getSubtitlesURI() ?: return@withContext;
withContext(Dispatchers.Main) {
try {
_lastSubtitleMediaSource = SingleSampleMediaSource.Factory(DefaultDataSource.Factory(context, DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)))