Fix Android ANR in SwapSources.

This commit is contained in:
Koen J 2025-07-28 15:44:37 +02:00
commit 4df227147c
19 changed files with 219 additions and 120 deletions

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,14 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
} }
//TODO: Is this still necessary? //TODO: Is this still necessary?
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
if(viewHolder.childViewHolder is ContentPreviewViewHolder) if(viewHolder.childViewHolder is ContentPreviewViewHolder)
(recyclerData.adapter as PreviewContentListAdapter?)?.preview(viewHolder.childViewHolder) (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)
mainFragment.lifecycleScope.launch(Dispatchers.Main) {
try {
player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0) player.setSource(videoSource, audioSource, play = true, keepSubtitles = false, resume = resumePositionMs > 0)
if (subtitleSource != null) player.swapSubtitles(mainFragment.lifecycleScope, subtitleSource) if (subtitleSource != null) player.swapSubtitles(subtitleSource)
player.seekTo(resumePositionMs) player.seekTo(resumePositionMs)
} catch (e: Throwable) {
Logger.e(TAG, "playVideo failed", e)
}
}
_lastVideoSource = videoSource _lastVideoSource = videoSource
_lastAudioSource = audioSource _lastAudioSource = audioSource

View file

@ -1925,13 +1925,28 @@ class VideoDetailView : ConstraintLayout {
else else
_player.setArtwork(null); _player.setArtwork(null);
} }
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
_player.setSource(videoSource, audioSource, _playWhenReady && playWhenReady, false, resume = resumePositionMs > 0); _player.setSource(videoSource, audioSource, _playWhenReady && playWhenReady, false, resume = resumePositionMs > 0);
if(subtitleSource != null) if(subtitleSource != null)
_player.swapSubtitles(fragment.lifecycleScope, subtitleSource); _player.swapSubtitles(subtitleSource);
_player.seekTo(resumePositionMs); _player.seekTo(resumePositionMs);
} catch (e: Throwable) {
Logger.e(TAG, "loadCurrentVideo failed", e)
} }
else }
}
else {
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
loadCurrentVideoCast(video, videoSource, audioSource, subtitleSource, resumePositionMs, Settings.instance.playback.getDefaultPlaybackSpeed().toDouble()); 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;
@ -1946,13 +1961,12 @@ 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 { try {
val plugin = if (videoSource is JSSource) videoSource.getUnderlyingPlugin() val plugin = if (videoSource is JSSource) videoSource.getUnderlyingPlugin()
else if (audioSource is JSSource) audioSource.getUnderlyingPlugin() else if (audioSource is JSSource) audioSource.getUnderlyingPlugin()
@ -1988,7 +2002,6 @@ class VideoDetailView : ConstraintLayout {
Logger.e(TAG, "loadCurrentVideoCast", e) Logger.e(TAG, "loadCurrentVideoCast", e)
} }
} }
}
//Events //Events
@androidx.annotation.OptIn(UnstableApi::class) @androidx.annotation.OptIn(UnstableApi::class)
@ -2501,11 +2514,17 @@ class VideoDetailView : ConstraintLayout {
if(_lastVideoSource == videoSource) if(_lastVideoSource == videoSource)
return; return;
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); castIfAvailable(context.contentResolver, video, videoSource, _lastAudioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true)) else if(!_player.swapSources(videoSource, _lastAudioSource, true, true, true))
_player.hideControls(false); //TODO: Disable player? _player.hideControls(false); //TODO: Disable player?
} catch (e: Throwable) {
Logger.e(TAG, "handleSelectVideoTrack failed", e)
}
}
_lastVideoSource = videoSource; _lastVideoSource = videoSource;
} }
@ -2516,11 +2535,17 @@ class VideoDetailView : ConstraintLayout {
if(_lastAudioSource == audioSource) if(_lastAudioSource == audioSource)
return; return;
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed) castIfAvailable(context.contentResolver, video, _lastVideoSource, audioSource, _lastSubtitleSource, (d.expectedCurrentTime * 1000.0).toLong(), d.speed)
else(!_player.swapSources(_lastVideoSource, audioSource, true, true, true)) else if (!_player.swapSources(_lastVideoSource, audioSource, true, true, true))
_player.hideControls(false); //TODO: Disable player? _player.hideControls(false); //TODO: Disable player?
} catch (e: Throwable) {
Logger.e(TAG, "handleSelectAudioTrack failed", e)
}
}
_lastAudioSource = audioSource; _lastAudioSource = audioSource;
} }
@ -2532,12 +2557,20 @@ class VideoDetailView : ConstraintLayout {
if(_lastSubtitleSource == subtitleSource) if(_lastSubtitleSource == subtitleSource)
toSet = null; toSet = null;
fragment.lifecycleScope.launch(Dispatchers.Main) {
try {
val d = StateCasting.instance.activeDevice; val d = StateCasting.instance.activeDevice;
if (d != null && d.connectionState == CastConnectionState.CONNECTED) if (d != null && d.connectionState == CastConnectionState.CONNECTED)
castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed); castIfAvailable(context.contentResolver, video, _lastVideoSource, _lastAudioSource, toSet, (d.expectedCurrentTime * 1000.0).toLong(), d.speed);
else else {
_player.swapSubtitles(fragment.lifecycleScope, toSet); withContext(Dispatchers.Main) {
_player.swapSubtitles(toSet);
}
}
} catch (e: Throwable) {
Logger.e(TAG, "handleSelectAudioTrack 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

@ -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,10 +365,11 @@ 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 {
val didSet = withContext(Dispatchers.IO) {
var videoSourceUsed = videoSource; var videoSourceUsed = videoSource;
var audioSourceUsed = audioSource; var audioSourceUsed = audioSource;
if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){ if(videoSource is JSDashManifestRawSource && audioSource is JSDashManifestRawAudioSource){
@ -382,31 +383,45 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
val didSetAudio = swapSourceInternal(audioSourceUsed, play, resume); val didSetAudio = swapSourceInternal(audioSourceUsed, play, resume);
if(!keepSubtitles) if(!keepSubtitles)
_lastSubtitleMediaSource = null; _lastSubtitleMediaSource = null;
if(didSetVideo && didSetAudio)
return loadSelectedSources(play, resume); return@withContext didSetVideo && didSetAudio
else
return true;
} }
fun swapSource(videoSource: IVideoSource?, resume: Boolean = true, play: Boolean = true): Boolean {
return withContext(Dispatchers.Main) {
if (didSet)
return@withContext loadSelectedSources(play, resume)
else
return@withContext true
}
}
suspend fun swapSource(videoSource: IVideoSource?, resume: Boolean = true, play: Boolean = true): Boolean {
val didSet = withContext(Dispatchers.IO) {
var videoSourceUsed = videoSource; var videoSourceUsed = videoSource;
if (videoSource is JSDashManifestRawSource && lastVideoSource is JSDashManifestMergingRawSource) if (videoSource is JSDashManifestRawSource && lastVideoSource is JSDashManifestMergingRawSource)
videoSourceUsed = JSDashManifestMergingRawSource(videoSource, (lastVideoSource as JSDashManifestMergingRawSource).audio); videoSourceUsed = JSDashManifestMergingRawSource(videoSource, (lastVideoSource as JSDashManifestMergingRawSource).audio);
val didSet = swapSourceInternal(videoSourceUsed, play, resume); return@withContext swapSourceInternal(videoSourceUsed, play, resume);
if(didSet)
return loadSelectedSources(play, resume);
else
return true;
} }
fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean { return withContext(Dispatchers.Main) {
if (didSet)
return@withContext loadSelectedSources(play, resume);
else
return@withContext true;
}
}
suspend fun swapSource(audioSource: IAudioSource?, resume: Boolean = true, play: Boolean = true): Boolean {
withContext(Dispatchers.IO) {
if (audioSource is JSDashManifestRawAudioSource && lastVideoSource is JSDashManifestMergingRawSource) if (audioSource is JSDashManifestRawAudioSource && lastVideoSource is JSDashManifestMergingRawSource)
swapSourceInternal(JSDashManifestMergingRawSource((lastVideoSource as JSDashManifestMergingRawSource).video, audioSource), play, resume); swapSourceInternal(JSDashManifestMergingRawSource((lastVideoSource as JSDashManifestMergingRawSource).video, audioSource), play, resume);
else else
swapSourceInternal(audioSource, play, resume); swapSourceInternal(audioSource, play, resume);
return loadSelectedSources(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)))