diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Polycentric.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Polycentric.kt index 77a73917..7737df09 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Polycentric.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Polycentric.kt @@ -1,6 +1,11 @@ package com.futo.platformplayer +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.AnnouncementType +import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StatePlatform +import com.futo.platformplayer.views.adapters.CommentViewHolder +import com.futo.polycentric.core.ProcessHandle import userpackage.Protocol import kotlin.math.abs import kotlin.math.min @@ -39,4 +44,21 @@ fun Protocol.Claim.resolveChannelUrl(): String? { fun Protocol.Claim.resolveChannelUrls(): List { return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) }) +} + +suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() { + val exceptions = fullyBackfillServers() + for (pair in exceptions) { + val server = pair.key + val exception = pair.value + + StateAnnouncement.instance.registerAnnouncement( + "backfill-failed", + "Backfill failed", + "Failed to backfill server $server. $exception", + AnnouncementType.SESSION_RECURRING + ); + + Logger.e("Backfill", "Failed to backfill server $server.", exception) + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Syntax.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Syntax.kt index 0b79de90..6ce03b70 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Syntax.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Syntax.kt @@ -1,5 +1,10 @@ package com.futo.platformplayer +import android.net.Uri +import java.net.URI +import java.net.URISyntaxException +import java.net.URLEncoder + //Syntax sugaring inline fun Any.assume(): T?{ if(this is T) @@ -16,4 +21,25 @@ inline fun Any.assume(cb: (T) -> R): R? { fun String?.yesNoToBoolean(): Boolean { return this?.uppercase() == "YES" +} + +fun String?.toURIRobust(): URI? { + if (this == null) { + return null + } + + try { + return URI(this) + } catch (e: URISyntaxException) { + val parts = this.split("\\?".toRegex(), 2) + if (parts.size < 2) { + return null + } + + val beforeQuery = parts[0] + val query = parts[1] + val encodedQuery = URLEncoder.encode(query, "UTF-8") + val rebuiltUrl = "$beforeQuery?$encodedQuery" + return URI(rebuiltUrl) + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index fc4b1fb7..9ef64a86 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -446,8 +446,28 @@ class Settings : FragmentedStorageFileJson() { }*/ } + @FormField(R.string.time_bar, "group", R.string.configure_if_historical_time_bar_should_be_shown, 8) + var timeBars = TimeBars(); + @Serializable + class TimeBars { + @FormField(R.string.home, FieldForm.TOGGLE, -1, 0) + @Serializable(with = FlexibleBooleanSerializer::class) + var home: Boolean = true; - @FormField(R.string.logging, FieldForm.GROUP, -1, 8) + @FormField(R.string.subscriptions, FieldForm.TOGGLE, -1, 1) + @Serializable(with = FlexibleBooleanSerializer::class) + var subscriptions: Boolean = true; + + @FormField(R.string.search, FieldForm.TOGGLE, -1, 2) + @Serializable(with = FlexibleBooleanSerializer::class) + var search: Boolean = true; + + @FormField(R.string.channel, FieldForm.TOGGLE, -1, 3) + @Serializable(with = FlexibleBooleanSerializer::class) + var channel: Boolean = true; + } + + @FormField(R.string.logging, FieldForm.GROUP, -1, 9) var logging = Logging(); @Serializable class Logging { @@ -471,7 +491,7 @@ class Settings : FragmentedStorageFileJson() { } } - @FormField(R.string.announcement, FieldForm.GROUP, -1, 10) + @FormField(R.string.announcement, FieldForm.GROUP, -1, 11) var announcementSettings = AnnouncementSettings(); @Serializable class AnnouncementSettings { @@ -482,7 +502,7 @@ class Settings : FragmentedStorageFileJson() { } } - @FormField(R.string.notifications, FieldForm.GROUP, -1, 11) + @FormField(R.string.notifications, FieldForm.GROUP, -1, 12) var notifications = NotificationSettings(); @Serializable class NotificationSettings { @@ -490,7 +510,7 @@ class Settings : FragmentedStorageFileJson() { var plannedContentNotification: Boolean = true; } - @FormField(R.string.plugins, FieldForm.GROUP, -1, 12) + @FormField(R.string.plugins, FieldForm.GROUP, -1, 13) @Transient var plugins = Plugins(); @Serializable @@ -527,7 +547,7 @@ class Settings : FragmentedStorageFileJson() { } - @FormField(R.string.external_storage, FieldForm.GROUP, -1, 13) + @FormField(R.string.external_storage, FieldForm.GROUP, -1, 14) var storage = Storage(); @Serializable class Storage { @@ -561,7 +581,7 @@ class Settings : FragmentedStorageFileJson() { } - @FormField(R.string.auto_update, "group", R.string.configure_the_auto_updater, 14) + @FormField(R.string.auto_update, "group", R.string.configure_the_auto_updater, 15) var autoUpdate = AutoUpdate(); @Serializable class AutoUpdate { @@ -643,7 +663,7 @@ class Settings : FragmentedStorageFileJson() { } } - @FormField(R.string.backup, FieldForm.GROUP, -1, 15) + @FormField(R.string.backup, FieldForm.GROUP, -1, 16) var backup = Backup(); @Serializable class Backup { @@ -696,7 +716,7 @@ class Settings : FragmentedStorageFileJson() { }*/ } - @FormField(R.string.payment, FieldForm.GROUP, -1, 16) + @FormField(R.string.payment, FieldForm.GROUP, -1, 17) var payment = Payment(); @Serializable class Payment { @@ -713,7 +733,7 @@ class Settings : FragmentedStorageFileJson() { } } - @FormField(R.string.other, FieldForm.GROUP, -1, 17) + @FormField(R.string.other, FieldForm.GROUP, -1, 18) var other = Other(); @Serializable class Other { @@ -722,7 +742,7 @@ class Settings : FragmentedStorageFileJson() { var bypassRotationPrevention: Boolean = false; } - @FormField(R.string.info, FieldForm.GROUP, -1, 18) + @FormField(R.string.info, FieldForm.GROUP, -1, 19) var info = Info(); @Serializable class Info { diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index 0f280a79..c1d849ef 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -8,6 +8,7 @@ import android.content.res.Configuration import android.net.Uri import android.os.Bundle import android.preference.PreferenceManager +import android.util.Log import android.util.TypedValue import android.view.View import android.widget.FrameLayout @@ -884,15 +885,20 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { if((fragment?.isOverlay ?: false) && fragBeforeOverlay != null) { navigate(fragBeforeOverlay!!, null, false, true); - - } - else { + } else { val last = _queue.lastOrNull(); if (last != null) { _queue.remove(last); navigate(last.first, last.second, false, true); - } else - finish(); + } else { + if (_fragVideoDetail.state == VideoDetailFragment.State.CLOSED) { + finish(); + } else { + UIDialogs.showConfirmationDialog(this, "There is a video playing, are you sure you want to exit the app?", { + finish(); + }) + } + } } } diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricCreateProfileActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricCreateProfileActivity.kt index 2129eb75..32b4f42a 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricCreateProfileActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricCreateProfileActivity.kt @@ -10,6 +10,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.R import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.states.StateApp @@ -82,7 +83,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() { try { Logger.i(TAG, "Started backfill"); - processHandle.fullyBackfillServers(); + processHandle.fullyBackfillServersAnnounceExceptions(); Logger.i(TAG, "Finished backfill"); } catch (e: Throwable) { Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e); diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt index 4c03d06d..b207da44 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt @@ -19,6 +19,7 @@ import com.futo.platformplayer.R import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.dialogs.CommentDialog import com.futo.platformplayer.dp +import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.selectBestImage @@ -194,7 +195,7 @@ class PolycentricProfileActivity : AppCompatActivity() { if (hasChanges) { try { Logger.i(TAG, "Started backfill"); - processHandle.fullyBackfillServers(); + processHandle.fullyBackfillServersAnnounceExceptions(); Logger.i(TAG, "Finished backfill"); withContext(Dispatchers.Main) { UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.changes_have_been_saved)); diff --git a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt index 34b3da87..ad136bdb 100644 --- a/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt +++ b/app/src/main/java/com/futo/platformplayer/casting/StateCasting.kt @@ -361,13 +361,13 @@ class StateCasting { else if (audioSource is IAudioUrlSource) ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble()); else if(videoSource is IHLSManifestSource) { - if (ad is ChromecastCastingDevice) { + if (ad is ChromecastCastingDevice && video.isLive) { castHlsIndirect(video, videoSource.url, resumePosition); } else { ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble()); } } else if(audioSource is IHLSManifestAudioSource) { - if (ad is ChromecastCastingDevice) { + if (ad is ChromecastCastingDevice && video.isLive) { castHlsIndirect(video, audioSource.url, resumePosition); } else { ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble()); @@ -578,7 +578,7 @@ class StateCasting { val masterPlaylist = HLS.downloadAndParseMasterPlaylist(_client, sourceUrl) val newVariantPlaylistRefs = arrayListOf() val newMediaRenditions = arrayListOf() - val newMasterPlaylist = HLS.MasterPlaylist(newVariantPlaylistRefs, newMediaRenditions, masterPlaylist.independentSegments) + val newMasterPlaylist = HLS.MasterPlaylist(newVariantPlaylistRefs, newMediaRenditions, masterPlaylist.sessionDataList, masterPlaylist.independentSegments) for (variantPlaylistRef in masterPlaylist.variantPlaylistsRefs) { val playlistId = UUID.randomUUID(); @@ -606,15 +606,17 @@ class StateCasting { val newPlaylistPath = "/hls-playlist-${playlistId}" val newPlaylistUrl = url + newPlaylistPath; - _castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext -> - val vpHeaders = vpContext.headers.clone() - vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; + if (mediaRendition.uri != null) { + _castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext -> + val vpHeaders = vpContext.headers.clone() + vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; - val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, mediaRendition.uri) - val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist) - val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() - vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); - }.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castHlsIndirectVariant") + val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, mediaRendition.uri) + val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist) + val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() + vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); + }.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castHlsIndirectVariant") + } newMediaRenditions.add(HLS.MediaRendition( mediaRendition.type, @@ -637,12 +639,16 @@ class StateCasting { return listOf(hlsUrl); } - private fun proxyVariantPlaylist(url: String, playlistId: UUID, variantPlaylist: HLS.VariantPlaylist): HLS.VariantPlaylist { + private fun proxyVariantPlaylist(url: String, playlistId: UUID, variantPlaylist: HLS.VariantPlaylist, proxySegments: Boolean = true): HLS.VariantPlaylist { val newSegments = arrayListOf() - variantPlaylist.segments.forEachIndexed { index, segment -> - val sequenceNumber = variantPlaylist.mediaSequence + index.toLong() - newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber)) + if (proxySegments) { + variantPlaylist.segments.forEachIndexed { index, segment -> + val sequenceNumber = variantPlaylist.mediaSequence + index.toLong() + newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber)) + } + } else { + newSegments.addAll(variantPlaylist.segments) } return HLS.VariantPlaylist( diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt index 2c78b694..584c8465 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt @@ -20,6 +20,7 @@ import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComm import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.dp +import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.states.StateApp @@ -97,7 +98,7 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { try { Logger.i(TAG, "Started backfill"); - processHandle.fullyBackfillServers() + processHandle.fullyBackfillServersAnnounceExceptions() Logger.i(TAG, "Finished backfill"); } catch (e: Throwable) { Logger.e(TAG, "Failed to backfill servers.", e); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt index 0d92eec8..9ac89b82 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt @@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.R +import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.channels.IPlatformChannel @@ -151,7 +152,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment { _recyclerResults = view.findViewById(R.id.recycler_videos); - _adapterResults = PreviewContentListAdapter(view.context, FeedStyle.THUMBNAIL, _results).apply { + _adapterResults = PreviewContentListAdapter(view.context, FeedStyle.THUMBNAIL, _results, null, Settings.instance.timeBars.channel).apply { this.onContentUrlClicked.subscribe(this@ChannelContentsFragment.onContentUrlClicked::emit); this.onUrlClicked.subscribe(this@ChannelContentsFragment.onUrlClicked::emit); this.onContentClicked.subscribe(this@ChannelContentsFragment.onContentClicked::emit); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt index 686c54e5..2986eb20 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ContentFeedView.kt @@ -37,6 +37,7 @@ abstract class ContentFeedView : FeedView, LinearLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null) : super(fragment, inflater, cachedRecyclerData) { @@ -57,7 +58,7 @@ abstract class ContentFeedView : FeedView>; + override val shouldShowTimeBar: Boolean get() = Settings.instance.timeBars.search constructor(fragment: ContentSearchResultsFragment, inflater: LayoutInflater) : super(fragment, inflater) { _taskSearch = TaskHandler>({fragment.lifecycleScope}, { query -> diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt index ebf5d56e..ee1944e9 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HomeFragment.kt @@ -95,6 +95,7 @@ class HomeFragment : MainFragment() { private var _announcementsView: AnnouncementView; private val _taskGetPager: TaskHandler>; + override val shouldShowTimeBar: Boolean get() = Settings.instance.timeBars.home constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, LinearLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null) : super(fragment, inflater, cachedRecyclerData) { _announcementsView = AnnouncementView(context, null).apply { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt index 4e8d8bca..b35cd912 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt @@ -31,6 +31,7 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikes import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.dp import com.futo.platformplayer.fixHtmlWhitespace +import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.polycentric.PolycentricCache @@ -363,7 +364,7 @@ class PostDetailFragment : MainFragment { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { try { Logger.i(TAG, "Started backfill"); - args.processHandle.fullyBackfillServers(); + args.processHandle.fullyBackfillServersAnnounceExceptions(); Logger.i(TAG, "Finished backfill"); } catch (e: Throwable) { Logger.e(TAG, "Failed to backfill servers", e) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt index adeb0390..7317cb95 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/SubscriptionsFeedFragment.kt @@ -93,6 +93,8 @@ class SubscriptionsFeedFragment : MainFragment() { @SuppressLint("ViewConstructor") class SubscriptionsFeedView : ContentFeedView { + override val shouldShowTimeBar: Boolean get() = Settings.instance.timeBars.subscriptions + constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData, LinearLayoutManager, IPager, IPlatformContent, IPlatformContent, InsertedViewHolder>? = null) : super(fragment, inflater, cachedRecyclerData) { Logger.i(TAG, "SubscriptionsFeedFragment constructor()"); StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total -> diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 5a366550..d529062d 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -1181,7 +1181,7 @@ class VideoDetailView : ConstraintLayout { fragment.lifecycleScope.launch(Dispatchers.IO) { try { Logger.i(TAG, "Started backfill"); - args.processHandle.fullyBackfillServers(); + args.processHandle.fullyBackfillServersAnnounceExceptions(); Logger.i(TAG, "Finished backfill"); } catch (e: Throwable) { Logger.e(TAG, "Failed to backfill servers", e) @@ -1486,6 +1486,7 @@ class VideoDetailView : ConstraintLayout { _overlay_quality_selector?.selectOption("audio", _lastAudioSource); _overlay_quality_selector?.selectOption("subtitles", _lastSubtitleSource); _overlay_quality_selector?.show(); + _slideUpOverlay = _overlay_quality_selector; } fun prevVideo() { diff --git a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt index c3fa6245..6f37e815 100644 --- a/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt +++ b/app/src/main/java/com/futo/platformplayer/parsers/HLS.kt @@ -1,6 +1,7 @@ package com.futo.platformplayer.parsers import com.futo.platformplayer.api.http.ManagedHttpClient +import com.futo.platformplayer.toURIRobust import com.futo.platformplayer.yesNoToBoolean import java.net.URI import java.time.ZonedDateTime @@ -14,10 +15,11 @@ class HLS { val masterPlaylistContent = masterPlaylistResponse.body?.string() ?: throw Exception("Master playlist content is empty") - val baseUrl = URI(sourceUrl).resolve("./").toString() + val baseUrl = sourceUrl.toURIRobust()!!.resolve("./").toString() val variantPlaylists = mutableListOf() val mediaRenditions = mutableListOf() + val sessionDataList = mutableListOf() var independentSegments = false masterPlaylistContent.lines().forEachIndexed { index, line -> @@ -37,10 +39,15 @@ class HLS { line == "#EXT-X-INDEPENDENT-SEGMENTS" -> { independentSegments = true } + + line.startsWith("#EXT-X-SESSION-DATA") -> { + val sessionData = parseSessionData(line) + sessionDataList.add(sessionData) + } } } - return MasterPlaylist(variantPlaylists, mediaRenditions, independentSegments) + return MasterPlaylist(variantPlaylists, mediaRenditions, sessionDataList, independentSegments) } fun downloadAndParseVariantPlaylist(client: ManagedHttpClient, sourceUrl: String): VariantPlaylist { @@ -86,7 +93,7 @@ class HLS { } private fun resolveUrl(baseUrl: String, url: String): String { - return if (URI(url).isAbsolute) url else baseUrl + url + return if (url.toURIRobust()!!.isAbsolute) url else baseUrl + url } @@ -105,11 +112,10 @@ class HLS { private fun parseMediaRendition(client: ManagedHttpClient, line: String, baseUrl: String): MediaRendition { val attributes = parseAttributes(line) - val uri = attributes["URI"]!! - val url = resolveUrl(baseUrl, uri) + val uri = attributes["URI"]?.let { resolveUrl(baseUrl, it) } return MediaRendition( type = attributes["TYPE"], - uri = url, + uri = uri, groupID = attributes["GROUP-ID"], language = attributes["LANGUAGE"], name = attributes["NAME"], @@ -119,6 +125,13 @@ class HLS { ) } + private fun parseSessionData(line: String): SessionData { + val attributes = parseAttributes(line) + val dataId = attributes["DATA-ID"]!! + val value = attributes["VALUE"]!! + return SessionData(dataId, value) + } + private fun parseAttributes(content: String): Map { val attributes = mutableMapOf() val attributePairs = content.substringAfter(":").splitToSequence(',') @@ -158,6 +171,20 @@ class HLS { } } + data class SessionData( + val dataId: String, + val value: String + ) { + fun toM3U8Line(): String = buildString { + append("#EXT-X-SESSION-DATA:") + appendAttributes(this, + "DATA-ID" to dataId, + "VALUE" to value + ) + append("\n") + } + } + data class StreamInfo( val bandwidth: Int?, val resolution: String?, @@ -170,7 +197,7 @@ class HLS { data class MediaRendition( val type: String?, - val uri: String, + val uri: String?, val groupID: String?, val language: String?, val name: String?, @@ -194,9 +221,11 @@ class HLS { } } + data class MasterPlaylist( val variantPlaylistsRefs: List, val mediaRenditions: List, + val sessionDataList: List, val independentSegments: Boolean ) { fun buildM3U8(): String { @@ -214,6 +243,10 @@ class HLS { builder.append(variant.toM3U8Line()) } + sessionDataList.forEach { data -> + builder.append(data.toM3U8Line()) + } + return builder.toString() } } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt index 65aa180e..79660207 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt @@ -75,7 +75,7 @@ class CommentViewHolder : ViewHolder { StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { try { Logger.i(TAG, "Started backfill"); - args.processHandle.fullyBackfillServers(); + args.processHandle.fullyBackfillServersAnnounceExceptions(); Logger.i(TAG, "Finished backfill"); } catch (e: Throwable) { Logger.e(TAG, "Failed to backfill servers.", e) diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewContentListAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewContentListAdapter.kt index 503a26c2..112fab64 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewContentListAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewContentListAdapter.kt @@ -29,6 +29,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader(); val onContentUrlClicked = Event2(); @@ -48,12 +49,13 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader, exoPlayer: PlayerManager? = null, initialPlay: Boolean = false, viewsToPrepend: ArrayList = arrayListOf(), - viewsToAppend: ArrayList = arrayListOf()) : super(context, viewsToPrepend, viewsToAppend) { + viewsToAppend: ArrayList = arrayListOf(), shouldShowTimeBar: Boolean = true) : super(context, viewsToPrepend, viewsToAppend) { this._feedStyle = feedStyle; this._dataSet = dataSet; this._initialPlay = initialPlay; this._exoPlayer = exoPlayer; + this._shouldShowTimeBar = shouldShowTimeBar } override fun getChildCount(): Int = _dataSet.size; @@ -97,7 +99,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader(); val onLongPress = Event1(); val onChannelClicked = Event1(); @@ -77,10 +81,12 @@ open class PreviewVideoView : LinearLayout { private set val content: IPlatformContent? get() = currentVideo; + val shouldShowTimeBar: Boolean - constructor(context: Context, feedStyle : FeedStyle, exoPlayer: PlayerManager? = null) : super(context) { + constructor(context: Context, feedStyle : FeedStyle, exoPlayer: PlayerManager? = null, shouldShowTimeBar: Boolean = true) : super(context) { inflate(feedStyle); _feedStyle = feedStyle; + this.shouldShowTimeBar = shouldShowTimeBar val playerContainer = findViewById(R.id.player_container); val displayMetrics = Resources.getSystem().displayMetrics; @@ -117,6 +123,7 @@ open class PreviewVideoView : LinearLayout { _button_add_to = findViewById(R.id.button_add_to); _imageNeopassChannel = findViewById(R.id.image_neopass_channel); _layoutDownloaded = findViewById(R.id.layout_downloaded); + _timeBar = findViewById(R.id.time_bar) this._exoPlayer = exoPlayer @@ -235,13 +242,23 @@ open class PreviewVideoView : LinearLayout { _containerLive.visibility = GONE; _containerDuration.visibility = VISIBLE; } + + if (shouldShowTimeBar) { + val historyPosition = StatePlaylists.instance.getHistoryPosition(video.url) + _timeBar.visibility = if (historyPosition > 0) VISIBLE else GONE + _timeBar.progress = historyPosition.toFloat() / video.duration.toFloat() + } else { + _timeBar.visibility = GONE + } } else { currentVideo = null; _imageVideo.setImageResource(0); _containerDuration.visibility = GONE; _containerLive.visibility = GONE; + _timeBar.visibility = GONE; } + _textVideoMetadata.text = metadata + timeMeta; } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoViewHolder.kt index e3793ff4..8f998365 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoViewHolder.kt @@ -27,8 +27,8 @@ class PreviewVideoViewHolder : ContentPreviewViewHolder { private val view: PreviewVideoView get() = itemView as PreviewVideoView; - constructor(viewGroup: ViewGroup, feedStyle : FeedStyle, exoPlayer: PlayerManager? = null): super( - PreviewVideoView(viewGroup.context, feedStyle, exoPlayer) + constructor(viewGroup: ViewGroup, feedStyle : FeedStyle, exoPlayer: PlayerManager? = null, shouldShowTimeBar: Boolean = true): super( + PreviewVideoView(viewGroup.context, feedStyle, exoPlayer, shouldShowTimeBar) ) { view.onVideoClicked.subscribe(onVideoClicked::emit); view.onChannelClicked.subscribe(onChannelClicked::emit); diff --git a/app/src/main/res/layout/list_video_preview.xml b/app/src/main/res/layout/list_video_preview.xml index 0f067668..ac93a071 100644 --- a/app/src/main/res/layout/list_video_preview.xml +++ b/app/src/main/res/layout/list_video_preview.xml @@ -32,6 +32,20 @@ android:scaleType="centerCrop" tools:srcCompat="@drawable/placeholder_video_thumbnail" /> + + diff --git a/app/src/main/res/layout/list_video_thumbnail.xml b/app/src/main/res/layout/list_video_thumbnail.xml index 33115dcb..bc69e9da 100644 --- a/app/src/main/res/layout/list_video_thumbnail.xml +++ b/app/src/main/res/layout/list_video_thumbnail.xml @@ -117,6 +117,20 @@ android:layout_gravity="end" android:layout_marginStart="4dp" android:layout_marginBottom="4dp" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c6514a13..dda12fc9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Add to queue General + Channel Home Recommendations More @@ -299,6 +300,8 @@ Clears cookies when you log out Clears in-app browser cookies Configure browsing behavior + Time bar + Configure if historical time bars should be shown Configure casting Configure daily backup in case of catastrophic failure Configure downloading of videos diff --git a/app/src/stable/assets/sources/odysee b/app/src/stable/assets/sources/odysee index a8bc4ff9..6ea20460 160000 --- a/app/src/stable/assets/sources/odysee +++ b/app/src/stable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit a8bc4ff91301ef70f8fbabf181e78bbed828156d +Subproject commit 6ea204605d4a27867702d7b024237506904d53c7 diff --git a/app/src/unstable/assets/sources/odysee b/app/src/unstable/assets/sources/odysee index a8bc4ff9..6ea20460 160000 --- a/app/src/unstable/assets/sources/odysee +++ b/app/src/unstable/assets/sources/odysee @@ -1 +1 @@ -Subproject commit a8bc4ff91301ef70f8fbabf181e78bbed828156d +Subproject commit 6ea204605d4a27867702d7b024237506904d53c7 diff --git a/dep/polycentricandroid b/dep/polycentricandroid index 7de4d54c..839e4c4a 160000 --- a/dep/polycentricandroid +++ b/dep/polycentricandroid @@ -1 +1 @@ -Subproject commit 7de4d54c25f087a2bc76a2704e575a6f9441987b +Subproject commit 839e4c4a4f5ed6cb6f68047f88b26c5831e6e703