mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-18 16:31:20 +00:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay
This commit is contained in:
commit
b5da0d4462
24 changed files with 270 additions and 135 deletions
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() { }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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() { }
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {};
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue