Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Kelvin 2023-11-22 16:05:39 +01:00
commit 0ffaec6bc2
26 changed files with 227 additions and 53 deletions

View file

@ -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<String> {
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)
}
}

View file

@ -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 <reified T> Any.assume(): T?{
if(this is T)
@ -16,4 +21,25 @@ inline fun <reified T, R> 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)
}
}

View file

@ -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 {

View file

@ -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();
})
}
}
}
}

View file

@ -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);

View file

@ -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));

View file

@ -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<HLS.VariantPlaylistReference>()
val newMediaRenditions = arrayListOf<HLS.MediaRendition>()
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<HLS.Segment>()
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(

View file

@ -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);

View file

@ -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);

View file

@ -37,6 +37,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
override val visibleThreshold: Int get() = if (feedStyle == FeedStyle.PREVIEW) { 5 } else { 10 };
protected lateinit var headerView: LinearLayout;
private var _videoOptionsOverlay: SlideUpMenuOverlay? = null;
protected open val shouldShowTimeBar: Boolean get() = true
constructor(fragment: TFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
@ -57,7 +58,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
};
headerView = v;
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(v)).apply {
return PreviewContentListAdapter(context, feedStyle, dataset, player, _previewsEnabled, arrayListOf(v), arrayListOf(), shouldShowTimeBar).apply {
attachAdapterEvents(this);
}
}

View file

@ -84,6 +84,7 @@ class ContentSearchResultsFragment : MainFragment() {
private var _channelUrl: String? = null;
private val _taskSearch: TaskHandler<String, IPager<IPlatformContent>>;
override val shouldShowTimeBar: Boolean get() = Settings.instance.timeBars.search
constructor(fragment: ContentSearchResultsFragment, inflater: LayoutInflater) : super(fragment, inflater) {
_taskSearch = TaskHandler<String, IPager<IPlatformContent>>({fragment.lifecycleScope}, { query ->

View file

@ -95,6 +95,7 @@ class HomeFragment : MainFragment() {
private var _announcementsView: AnnouncementView;
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
override val shouldShowTimeBar: Boolean get() = Settings.instance.timeBars.home
constructor(fragment: HomeFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
_announcementsView = AnnouncementView(context, null).apply {

View file

@ -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)

View file

@ -93,6 +93,8 @@ class SubscriptionsFeedFragment : MainFragment() {
@SuppressLint("ViewConstructor")
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
override val shouldShowTimeBar: Boolean get() = Settings.instance.timeBars.subscriptions
constructor(fragment: SubscriptionsFeedFragment, inflater: LayoutInflater, cachedRecyclerData: RecyclerData<InsertedViewAdapterWithLoader<ContentPreviewViewHolder>, LinearLayoutManager, IPager<IPlatformContent>, IPlatformContent, IPlatformContent, InsertedViewHolder<ContentPreviewViewHolder>>? = null) : super(fragment, inflater, cachedRecyclerData) {
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total ->

View file

@ -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() {

View file

@ -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<VariantPlaylistReference>()
val mediaRenditions = mutableListOf<MediaRendition>()
val sessionDataList = mutableListOf<SessionData>()
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<String, String> {
val attributes = mutableMapOf<String, String>()
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<VariantPlaylistReference>,
val mediaRenditions: List<MediaRendition>,
val sessionDataList: List<SessionData>,
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()
}
}

View file

@ -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)

View file

@ -29,6 +29,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
private val _exoPlayer: PlayerManager?;
private val _feedStyle : FeedStyle;
private var _paused: Boolean = false;
private val _shouldShowTimeBar: Boolean
val onUrlClicked = Event1<String>();
val onContentUrlClicked = Event2<String, ContentType>();
@ -48,12 +49,13 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
constructor(context: Context, feedStyle : FeedStyle, dataSet: ArrayList<IPlatformContent>, exoPlayer: PlayerManager? = null,
initialPlay: Boolean = false, viewsToPrepend: ArrayList<View> = arrayListOf(),
viewsToAppend: ArrayList<View> = arrayListOf()) : super(context, viewsToPrepend, viewsToAppend) {
viewsToAppend: ArrayList<View> = 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<ContentPreviewVi
};
private fun createPlaceholderViewHolder(viewGroup: ViewGroup): PreviewPlaceholderViewHolder
= PreviewPlaceholderViewHolder(viewGroup, _feedStyle);
private fun createVideoPreviewViewHolder(viewGroup: ViewGroup): PreviewVideoViewHolder = PreviewVideoViewHolder(viewGroup, _feedStyle, _exoPlayer).apply {
private fun createVideoPreviewViewHolder(viewGroup: ViewGroup): PreviewVideoViewHolder = PreviewVideoViewHolder(viewGroup, _feedStyle, _exoPlayer, _shouldShowTimeBar).apply {
this.onVideoClicked.subscribe(this@PreviewContentListAdapter.onContentClicked::emit);
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);

View file

@ -27,9 +27,11 @@ import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.polycentric.PolycentricCache
import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.video.PlayerManager
import com.futo.platformplayer.views.others.CreatorThumbnail
import com.futo.platformplayer.views.FeedStyle
import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.views.platform.PlatformIndicator
import com.futo.platformplayer.views.video.FutoThumbnailPlayer
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
@ -67,6 +69,8 @@ open class PreviewVideoView : LinearLayout {
Logger.w(TAG, "Failed to load profile.", it);
};
private val _timeBar: ProgressBar;
val onVideoClicked = Event2<IPlatformVideo, Long>();
val onLongPress = Event1<IPlatformVideo>();
val onChannelClicked = Event1<PlatformAuthorLink>();
@ -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<FrameLayout>(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;
}

View file

@ -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);

View file

@ -32,6 +32,20 @@
android:scaleType="centerCrop"
tools:srcCompat="@drawable/placeholder_video_thumbnail" />
<com.futo.platformplayer.views.others.ProgressBar
android:id="@+id/time_bar"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_gravity="bottom"
android:layout_marginBottom="6dp"
app:progress="60%"
app:inactiveColor="#55EEEEEE"
app:radiusBottomLeft="0dp"
app:radiusBottomRight="0dp"
app:radiusTopLeft="0dp"
app:radiusTopRight="0dp"
android:visibility="visible"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

View file

@ -117,6 +117,20 @@
android:layout_gravity="end"
android:layout_marginStart="4dp"
android:layout_marginBottom="4dp" />
<com.futo.platformplayer.views.others.ProgressBar
android:id="@+id/time_bar"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_alignParentStart="true"
android:layout_alignParentBottom="true"
app:progress="60%"
app:inactiveColor="#55EEEEEE"
app:radiusBottomLeft="4dp"
app:radiusBottomRight="4dp"
app:radiusTopLeft="0dp"
app:radiusTopRight="0dp"
android:visibility="visible"/>
</RelativeLayout>
</FrameLayout>

View file

@ -8,6 +8,7 @@
<string name="lorem_ipsum" translatable="false">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.</string>
<string name="add_to_queue">Add to queue</string>
<string name="general">General</string>
<string name="channel">Channel</string>
<string name="home">Home</string>
<string name="recommendations">Recommendations</string>
<string name="more">More</string>
@ -299,6 +300,8 @@
<string name="clears_cookies_when_you_log_out">Clears cookies when you log out</string>
<string name="clears_in_app_browser_cookies">Clears in-app browser cookies</string>
<string name="configure_browsing_behavior">Configure browsing behavior</string>
<string name="time_bar">Time bar</string>
<string name="configure_if_historical_time_bar_should_be_shown">Configure if historical time bars should be shown</string>
<string name="configure_casting">Configure casting</string>
<string name="configure_daily_backup_in_case_of_catastrophic_failure">Configure daily backup in case of catastrophic failure</string>
<string name="configure_downloading_of_videos">Configure downloading of videos</string>

@ -1 +1 @@
Subproject commit a8bc4ff91301ef70f8fbabf181e78bbed828156d
Subproject commit 6ea204605d4a27867702d7b024237506904d53c7

@ -1 +1 @@
Subproject commit a8bc4ff91301ef70f8fbabf181e78bbed828156d
Subproject commit 6ea204605d4a27867702d7b024237506904d53c7

@ -1 +1 @@
Subproject commit 7de4d54c25f087a2bc76a2704e575a6f9441987b
Subproject commit 839e4c4a4f5ed6cb6f68047f88b26c5831e6e703