improved vertical video detection and auto full screen for vertical videos

This commit is contained in:
Kai 2024-10-22 13:01:02 -05:00
commit cc0c400b28
No known key found for this signature in database
2 changed files with 126 additions and 52 deletions

View file

@ -96,7 +96,7 @@ class VideoDetailFragment : MainFragment {
detectWindowSize() detectWindowSize()
val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: true val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: false
if ( if (
isSmallWindow isSmallWindow
@ -117,13 +117,29 @@ class VideoDetailFragment : MainFragment {
} }
private fun onStateChanged(state: State) { private fun onStateChanged(state: State) {
if (isSmallWindow && state == State.MAXIMIZED && !isFullscreen && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { if (
isSmallWindow
&& state == State.MAXIMIZED
&& !isFullscreen
&& resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
) {
_viewDetail?.setFullscreen(true) _viewDetail?.setFullscreen(true)
} }
updateOrientation() updateOrientation()
} }
private fun onVideoChanged(videoWidth : Int, videoHeight: Int) {
if (
isSmallWindow
&& state == State.MAXIMIZED
&& !isFullscreen
&& videoHeight > videoWidth
) {
_viewDetail?.setFullscreen(true)
}
}
@SuppressLint("SourceLockedOrientationActivity") @SuppressLint("SourceLockedOrientationActivity")
fun updateOrientation() { fun updateOrientation() {
val a = activity ?: return val a = activity ?: return
@ -133,7 +149,7 @@ class VideoDetailFragment : MainFragment {
val isReversePortraitAllowed = Settings.instance.playback.reversePortrait val isReversePortraitAllowed = Settings.instance.playback.reversePortrait
val rotationLock = StatePlayer.instance.rotationLock val rotationLock = StatePlayer.instance.rotationLock
val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: true val isLandscapeVideo: Boolean = _viewDetail?.isLandscapeVideo() ?: false
// For small windows if the device isn't landscape right now and full screen portrait isn't allowed then we should force landscape // For small windows if the device isn't landscape right now and full screen portrait isn't allowed then we should force landscape
if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT && !rotationLock && isLandscapeVideo) { if (isSmallWindow && isFullscreen && !isFullScreenPortraitAllowed && resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT && !rotationLock && isLandscapeVideo) {
@ -247,6 +263,7 @@ class VideoDetailFragment : MainFragment {
_viewDetail = _view!!.findViewById<VideoDetailView>(R.id.fragview_videodetail).also { _viewDetail = _view!!.findViewById<VideoDetailView>(R.id.fragview_videodetail).also {
it.applyFragment(this); it.applyFragment(this);
it.onFullscreenChanged.subscribe(::onFullscreenChanged); it.onFullscreenChanged.subscribe(::onFullscreenChanged);
it.onVideoChanged.subscribe(::onVideoChanged)
it.onMinimize.subscribe { it.onMinimize.subscribe {
isMinimizing = true isMinimizing = true
_view!!.transitionToStart(); _view!!.transitionToStart();

View file

@ -80,6 +80,7 @@ import com.futo.platformplayer.casting.CastConnectionState
import com.futo.platformplayer.casting.StateCasting 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.Event2
import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.constructs.TaskHandler
import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.dp import com.futo.platformplayer.dp
@ -300,6 +301,7 @@ class VideoDetailView : ConstraintLayout {
val onFullscreenChanged = Event1<Boolean>(); val onFullscreenChanged = Event1<Boolean>();
val onEnterPictureInPicture = Event0(); val onEnterPictureInPicture = Event0();
val onPlayChanged = Event1<Boolean>(); val onPlayChanged = Event1<Boolean>();
val onVideoChanged = Event2<Int, Int>()
var allowBackground : Boolean = false var allowBackground : Boolean = false
private set; private set;
@ -1205,6 +1207,7 @@ class VideoDetailView : ConstraintLayout {
switchContentView(_container_content_main); switchContentView(_container_content_main);
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) { fun setVideoDetails(videoDetail: IPlatformVideoDetails, newVideo: Boolean = false) {
Logger.i(TAG, "setVideoDetails (${videoDetail.name})") Logger.i(TAG, "setVideoDetails (${videoDetail.name})")
@ -1213,7 +1216,7 @@ class VideoDetailView : ConstraintLayout {
_autoplayVideo = null _autoplayVideo = null
Logger.i(TAG, "Autoplay video cleared (setVideoDetails)") Logger.i(TAG, "Autoplay video cleared (setVideoDetails)")
if(newVideo && this.video?.url == videoDetail.url) if (newVideo && this.video?.url == videoDetail.url)
return; return;
if (newVideo) { if (newVideo) {
@ -1222,8 +1225,13 @@ class VideoDetailView : ConstraintLayout {
_lastSubtitleSource = null; _lastSubtitleSource = null;
} }
if(videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now()) if (videoDetail.datetime != null && videoDetail.datetime!! > OffsetDateTime.now())
UIDialogs.toast(context, context.getString(R.string.planned_in) + " ${videoDetail.datetime?.toHumanNowDiffString(true)}") UIDialogs.toast(
context,
context.getString(R.string.planned_in) + " ${
videoDetail.datetime?.toHumanNowDiffString(true)
}"
)
if (!videoDetail.isLive) { if (!videoDetail.isLive) {
_player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed()); _player.setPlaybackRate(Settings.instance.playback.getDefaultPlaybackSpeed());
@ -1232,26 +1240,25 @@ class VideoDetailView : ConstraintLayout {
val videoLocal: VideoLocal?; val videoLocal: VideoLocal?;
val video: IPlatformVideoDetails?; val video: IPlatformVideoDetails?;
if(videoDetail is VideoLocal) { if (videoDetail is VideoLocal) {
videoLocal = videoDetail; videoLocal = videoDetail;
video = videoDetail; video = videoDetail;
this.video = video; this.video = video;
val videoTask = StatePlatform.instance.getContentDetails(videoDetail.url); val videoTask = StatePlatform.instance.getContentDetails(videoDetail.url);
videoTask.invokeOnCompletion { ex -> videoTask.invokeOnCompletion { ex ->
if(ex != null) { if (ex != null) {
Logger.e(TAG, "Failed to fetch live video for offline video", ex); Logger.e(TAG, "Failed to fetch live video for offline video", ex);
return@invokeOnCompletion; return@invokeOnCompletion;
} }
val result = videoTask.getCompleted(); val result = videoTask.getCompleted();
if(this.video == videoDetail && result is IPlatformVideoDetails) { if (this.video == videoDetail && result is IPlatformVideoDetails) {
this.video = result; this.video = result;
fragment.lifecycleScope.launch(Dispatchers.Main) { fragment.lifecycleScope.launch(Dispatchers.Main) {
updateQualitySourcesOverlay(result, videoLocal); updateQualitySourcesOverlay(result, videoLocal);
} }
} }
}; };
} } else { //TODO: Update cached video if it exists with video
else { //TODO: Update cached video if it exists with video
videoLocal = StateDownloads.instance.getCachedVideo(videoDetail.id); videoLocal = StateDownloads.instance.getCachedVideo(videoDetail.id);
video = videoDetail; video = videoDetail;
} }
@ -1259,7 +1266,9 @@ class VideoDetailView : ConstraintLayout {
this.video = video; this.video = video;
cleanupPlaybackTracker(); cleanupPlaybackTracker();
if(video is JSVideoDetails) { onVideoChanged.emit(video.video.videoSources[0].width, video.video.videoSources[0].height)
if (video is JSVideoDetails) {
val me = this; val me = this;
fragment.lifecycleScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.IO) {
try { try {
@ -1267,8 +1276,7 @@ class VideoDetailView : ConstraintLayout {
val chapters = null ?: StatePlatform.instance.getContentChapters(video.url); val chapters = null ?: StatePlatform.instance.getContentChapters(video.url);
_player.setChapters(chapters); _player.setChapters(chapters);
_cast.setChapters(chapters); _cast.setChapters(chapters);
} } catch (ex: Throwable) {
catch(ex: Throwable) {
Logger.e(TAG, "Failed to get chapters", ex); Logger.e(TAG, "Failed to get chapters", ex);
_player.setChapters(null); _player.setChapters(null);
_cast.setChapters(null); _cast.setChapters(null);
@ -1278,7 +1286,7 @@ class VideoDetailView : ConstraintLayout {
}*/ }*/
} }
try { try {
if(!StateApp.instance.privateMode) { if (!StateApp.instance.privateMode) {
val stopwatch = com.futo.platformplayer.debug.Stopwatch() val stopwatch = com.futo.platformplayer.debug.Stopwatch()
var tracker = video.getPlaybackTracker() var tracker = video.getPlaybackTracker()
Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms") Logger.i(TAG, "video.getPlaybackTracker took ${stopwatch.elapsedMs}ms")
@ -1294,17 +1302,22 @@ class VideoDetailView : ConstraintLayout {
if (me.video == video) if (me.video == video)
me._playbackTracker = tracker; me._playbackTracker = tracker;
} } else if (me.video == video)
else if(me.video == video)
me._playbackTracker = null; me._playbackTracker = null;
} } catch (ex: Throwable) {
catch(ex: Throwable) {
Logger.e(TAG, "Playback tracker failed", ex); Logger.e(TAG, "Playback tracker failed", ex);
if(me.video?.isLive == true) withContext(Dispatchers.Main) { if (me.video?.isLive == true) withContext(Dispatchers.Main) {
UIDialogs.toast(context, context.getString(R.string.failed_to_get_playback_tracker)); UIDialogs.toast(
context,
context.getString(R.string.failed_to_get_playback_tracker)
);
}; };
else withContext(Dispatchers.Main) { else withContext(Dispatchers.Main) {
UIDialogs.showGeneralErrorDialog(context, context.getString(R.string.failed_to_get_playback_tracker), ex); UIDialogs.showGeneralErrorDialog(
context,
context.getString(R.string.failed_to_get_playback_tracker),
ex
);
} }
} }
}; };
@ -1321,8 +1334,11 @@ class VideoDetailView : ConstraintLayout {
if (Settings.instance.comments.recommendationsDefault && !Settings.instance.comments.hideRecommendations) { if (Settings.instance.comments.recommendationsDefault && !Settings.instance.comments.hideRecommendations) {
setTabIndex(2, true) setTabIndex(2, true)
} else { } else {
when(Settings.instance.comments.defaultCommentSection) { when (Settings.instance.comments.defaultCommentSection) {
0 -> if(Settings.instance.other.polycentricEnabled) setTabIndex(0, true) else setTabIndex(1, true); 0 -> if (Settings.instance.other.polycentricEnabled) setTabIndex(
0,
true
) else setTabIndex(1, true);
1 -> setTabIndex(1, true); 1 -> setTabIndex(1, true);
2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true) 2 -> setTabIndex(StateMeta.instance.getLastCommentSection(), true)
} }
@ -1332,9 +1348,16 @@ class VideoDetailView : ConstraintLayout {
//UI //UI
_title.text = video.name; _title.text = video.name;
_channelName.text = video.author.name; _channelName.text = video.author.name;
if(video.author.subscribers != null) { if (video.author.subscribers != null) {
_channelMeta.text = if((video.author.subscribers ?: 0) > 0) video.author.subscribers!!.toHumanNumber() + " " + context.getString(R.string.subscribers) else ""; _channelMeta.text = if ((video.author.subscribers
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_5 * -1).toInt(), 0, 0); ?: 0) > 0
) video.author.subscribers!!.toHumanNumber() + " " + context.getString(R.string.subscribers) else "";
(_channelName.layoutParams as MarginLayoutParams).setMargins(
0,
(DP_5 * -1).toInt(),
0,
0
);
} else { } else {
_channelMeta.text = ""; _channelMeta.text = "";
(_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_2).toInt(), 0, 0); (_channelName.layoutParams as MarginLayoutParams).setMargins(0, (DP_2).toInt(), 0, 0);
@ -1342,7 +1365,7 @@ class VideoDetailView : ConstraintLayout {
video.author.let { video.author.let {
if(it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty()) if (it is PlatformAuthorMembershipLink && !it.membershipUrl.isNullOrEmpty())
_monetization.setPlatformMembership(video.id.pluginId, it.membershipUrl); _monetization.setPlatformMembership(video.id.pluginId, it.membershipUrl);
else else
_monetization.setPlatformMembership(null, null); _monetization.setPlatformMembership(null, null);
@ -1356,7 +1379,8 @@ class VideoDetailView : ConstraintLayout {
_creatorThumbnail.setThumbnail(video.author.thumbnail, false); _creatorThumbnail.setThumbnail(video.author.thumbnail, false);
val cachedPolycentricProfile = PolycentricCache.instance.getCachedProfile(video.author.url, true); val cachedPolycentricProfile =
PolycentricCache.instance.getCachedProfile(video.author.url, true);
if (cachedPolycentricProfile != null) { if (cachedPolycentricProfile != null) {
setPolycentricProfile(cachedPolycentricProfile, animate = false); setPolycentricProfile(cachedPolycentricProfile, animate = false);
} else { } else {
@ -1365,13 +1389,19 @@ class VideoDetailView : ConstraintLayout {
} }
_platform.setPlatformFromClientID(video.id.pluginId); _platform.setPlatformFromClientID(video.id.pluginId);
val subTitleSegments : ArrayList<String> = ArrayList(); val subTitleSegments: ArrayList<String> = ArrayList();
if(video.viewCount > 0) if (video.viewCount > 0)
subTitleSegments.add("${video.viewCount.toHumanNumber()} ${if(video.isLive) context.getString(R.string.watching_now) else context.getString(R.string.views)}"); subTitleSegments.add(
if(video.datetime != null) { "${video.viewCount.toHumanNumber()} ${
if (video.isLive) context.getString(
R.string.watching_now
) else context.getString(R.string.views)
}"
);
if (video.datetime != null) {
val diff = video.datetime?.getNowDiffSeconds() ?: 0; val diff = video.datetime?.getNowDiffSeconds() ?: 0;
val ago = video.datetime?.toHumanNowDiffString(true) val ago = video.datetime?.toHumanNowDiffString(true)
if(diff >= 0) if (diff >= 0)
subTitleSegments.add("${ago} ago"); subTitleSegments.add("${ago} ago");
else else
subTitleSegments.add("available in ${ago}"); subTitleSegments.add("available in ${ago}");
@ -1384,20 +1414,27 @@ class VideoDetailView : ConstraintLayout {
fragment.lifecycleScope.launch(Dispatchers.IO) { fragment.lifecycleScope.launch(Dispatchers.IO) {
try { try {
val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null, val queryReferencesResponse = ApiMethods.getQueryReferences(
PolycentricCache.SERVER, ref, null, null,
arrayListOf( arrayListOf(
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue( Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
ByteString.copyFrom(Opinion.like.data)).build(), .setFromType(ContentType.OPINION.value).setValue(
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType(ContentType.OPINION.value).setValue( ByteString.copyFrom(Opinion.like.data)
ByteString.copyFrom(Opinion.dislike.data)).build() ).build(),
Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder()
.setFromType(ContentType.OPINION.value).setValue(
ByteString.copyFrom(Opinion.dislike.data)
).build()
), ),
extraByteReferences = listOfNotNull(extraBytesRef) extraByteReferences = listOfNotNull(extraBytesRef)
); );
val likes = queryReferencesResponse.countsList[0]; val likes = queryReferencesResponse.countsList[0];
val dislikes = queryReferencesResponse.countsList[1]; val dislikes = queryReferencesResponse.countsList[1];
val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/; val hasLiked =
val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/; StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/;
val hasDisliked =
StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/;
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_rating.visibility = View.VISIBLE; _rating.visibility = View.VISIBLE;
@ -1421,7 +1458,11 @@ class VideoDetailView : ConstraintLayout {
} }
} }
StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked) StatePolycentric.instance.updateLikeMap(
ref,
args.hasLiked,
args.hasDisliked
)
}; };
} }
} catch (e: Throwable) { } catch (e: Throwable) {
@ -1443,6 +1484,7 @@ class VideoDetailView : ConstraintLayout {
_textDislikes.visibility = View.VISIBLE; _textDislikes.visibility = View.VISIBLE;
_textDislikes.text = r.dislikes.toHumanNumber(); _textDislikes.text = r.dislikes.toHumanNumber();
} }
is RatingLikes -> { is RatingLikes -> {
val r = video.rating as RatingLikes; val r = video.rating as RatingLikes;
_layoutRating.visibility = View.VISIBLE; _layoutRating.visibility = View.VISIBLE;
@ -1454,6 +1496,7 @@ class VideoDetailView : ConstraintLayout {
_imageDislikeIcon.visibility = View.GONE; _imageDislikeIcon.visibility = View.GONE;
_textDislikes.visibility = View.GONE; _textDislikes.visibility = View.GONE;
} }
else -> { else -> {
_layoutRating.visibility = View.GONE; _layoutRating.visibility = View.GONE;
} }
@ -1465,6 +1508,7 @@ class VideoDetailView : ConstraintLayout {
setLoading(false); setLoading(false);
//Set Mediasource //Set Mediasource
val toResume = _videoResumePositionMilliseconds; val toResume = _videoResumePositionMilliseconds;
@ -1481,9 +1525,22 @@ class VideoDetailView : ConstraintLayout {
val historyItem = getHistoryIndex(videoDetail) ?: return@launch; val historyItem = getHistoryIndex(videoDetail) ?: return@launch;
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
_historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong(), null, true); _historicalPosition = StateHistory.instance.updateHistoryPosition(
Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds"); video,
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) { historyItem,
false,
(toResume.toFloat() / 1000.0f).toLong(),
null,
true
);
Logger.i(
TAG,
"Historical position: $_historicalPosition, last position: $lastPositionMilliseconds"
);
if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(
_historicalPosition - lastPositionMilliseconds / 1000
) > 5.0
) {
_layoutResume.visibility = View.VISIBLE; _layoutResume.visibility = View.VISIBLE;
_textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}"; _textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}";
@ -1509,10 +1566,10 @@ class VideoDetailView : ConstraintLayout {
_liveChat?.stop(); _liveChat?.stop();
_liveChat = null; _liveChat = null;
if(video.isLive && video.live != null) { if (video.isLive && video.live != null) {
loadLiveChat(video); loadLiveChat(video);
} }
if(video.isLive && video.live == null && !video.video.videoSources.any()) if (video.isLive && video.live == null && !video.video.videoSources.any())
startLiveTry(video); startLiveTry(video);
@ -2241,14 +2298,14 @@ class VideoDetailView : ConstraintLayout {
} }
} }
fun isLandscapeVideo(): Boolean { fun isLandscapeVideo(): Boolean? {
var videoSourceWidth = _player.exoPlayer?.player?.videoSize?.width var videoSourceWidth = _player.exoPlayer?.player?.videoSize?.width
var videoSourceHeight = _player.exoPlayer?.player?.videoSize?.height var videoSourceHeight = _player.exoPlayer?.player?.videoSize?.height
return if (videoSourceHeight != null && videoSourceWidth != null) { return if (videoSourceWidth == null || videoSourceHeight == null || videoSourceWidth == 0 || videoSourceHeight == 0){
videoSourceWidth > videoSourceHeight null
} else { } else{
true videoSourceWidth >= videoSourceHeight
} }
} }