mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-09 09:39:51 +00:00
Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay
This commit is contained in:
commit
0ffaec6bc2
26 changed files with 227 additions and 53 deletions
|
@ -1,6 +1,11 @@
|
||||||
package com.futo.platformplayer
|
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.states.StatePlatform
|
||||||
|
import com.futo.platformplayer.views.adapters.CommentViewHolder
|
||||||
|
import com.futo.polycentric.core.ProcessHandle
|
||||||
import userpackage.Protocol
|
import userpackage.Protocol
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
@ -40,3 +45,20 @@ fun Protocol.Claim.resolveChannelUrl(): String? {
|
||||||
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
fun Protocol.Claim.resolveChannelUrls(): List<String> {
|
||||||
return StatePlatform.instance.resolveChannelUrlsByClaimTemplates(this.claimType.toInt(), this.claimFieldsList.associate { Pair(it.key.toInt(), it.value) })
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,10 @@
|
||||||
package com.futo.platformplayer
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
import java.net.URLEncoder
|
||||||
|
|
||||||
//Syntax sugaring
|
//Syntax sugaring
|
||||||
inline fun <reified T> Any.assume(): T?{
|
inline fun <reified T> Any.assume(): T?{
|
||||||
if(this is T)
|
if(this is T)
|
||||||
|
@ -17,3 +22,24 @@ inline fun <reified T, R> Any.assume(cb: (T) -> R): R? {
|
||||||
fun String?.yesNoToBoolean(): Boolean {
|
fun String?.yesNoToBoolean(): Boolean {
|
||||||
return this?.uppercase() == "YES"
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
var logging = Logging();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Logging {
|
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();
|
var announcementSettings = AnnouncementSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class AnnouncementSettings {
|
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();
|
var notifications = NotificationSettings();
|
||||||
@Serializable
|
@Serializable
|
||||||
class NotificationSettings {
|
class NotificationSettings {
|
||||||
|
@ -490,7 +510,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
var plannedContentNotification: Boolean = true;
|
var plannedContentNotification: Boolean = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.plugins, FieldForm.GROUP, -1, 12)
|
@FormField(R.string.plugins, FieldForm.GROUP, -1, 13)
|
||||||
@Transient
|
@Transient
|
||||||
var plugins = Plugins();
|
var plugins = Plugins();
|
||||||
@Serializable
|
@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();
|
var storage = Storage();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Storage {
|
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();
|
var autoUpdate = AutoUpdate();
|
||||||
@Serializable
|
@Serializable
|
||||||
class AutoUpdate {
|
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();
|
var backup = Backup();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Backup {
|
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();
|
var payment = Payment();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Payment {
|
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();
|
var other = Other();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Other {
|
class Other {
|
||||||
|
@ -722,7 +742,7 @@ class Settings : FragmentedStorageFileJson() {
|
||||||
var bypassRotationPrevention: Boolean = false;
|
var bypassRotationPrevention: Boolean = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FormField(R.string.info, FieldForm.GROUP, -1, 18)
|
@FormField(R.string.info, FieldForm.GROUP, -1, 19)
|
||||||
var info = Info();
|
var info = Info();
|
||||||
@Serializable
|
@Serializable
|
||||||
class Info {
|
class Info {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import android.content.res.Configuration
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.preference.PreferenceManager
|
import android.preference.PreferenceManager
|
||||||
|
import android.util.Log
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
|
@ -884,15 +885,20 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||||
|
|
||||||
if((fragment?.isOverlay ?: false) && fragBeforeOverlay != null) {
|
if((fragment?.isOverlay ?: false) && fragBeforeOverlay != null) {
|
||||||
navigate(fragBeforeOverlay!!, null, false, true);
|
navigate(fragBeforeOverlay!!, null, false, true);
|
||||||
|
} else {
|
||||||
}
|
|
||||||
else {
|
|
||||||
val last = _queue.lastOrNull();
|
val last = _queue.lastOrNull();
|
||||||
if (last != null) {
|
if (last != null) {
|
||||||
_queue.remove(last);
|
_queue.remove(last);
|
||||||
navigate(last.first, last.second, false, true);
|
navigate(last.first, last.second, false, true);
|
||||||
} else
|
} else {
|
||||||
finish();
|
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();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
import com.futo.platformplayer.setNavigationBarColorAndIcons
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
|
@ -82,7 +83,7 @@ class PolycentricCreateProfileActivity : AppCompatActivity() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Started backfill");
|
Logger.i(TAG, "Started backfill");
|
||||||
processHandle.fullyBackfillServers();
|
processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
Logger.i(TAG, "Finished backfill");
|
Logger.i(TAG, "Finished backfill");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
Logger.e(TAG, getString(R.string.failed_to_fully_backfill_servers), e);
|
||||||
|
|
|
@ -19,6 +19,7 @@ import com.futo.platformplayer.R
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.dialogs.CommentDialog
|
import com.futo.platformplayer.dialogs.CommentDialog
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
|
@ -194,7 +195,7 @@ class PolycentricProfileActivity : AppCompatActivity() {
|
||||||
if (hasChanges) {
|
if (hasChanges) {
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Started backfill");
|
Logger.i(TAG, "Started backfill");
|
||||||
processHandle.fullyBackfillServers();
|
processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
Logger.i(TAG, "Finished backfill");
|
Logger.i(TAG, "Finished backfill");
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.changes_have_been_saved));
|
UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.changes_have_been_saved));
|
||||||
|
|
|
@ -361,13 +361,13 @@ class StateCasting {
|
||||||
else if (audioSource is IAudioUrlSource)
|
else if (audioSource is IAudioUrlSource)
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble());
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.getAudioUrl(), resumePosition, video.duration.toDouble());
|
||||||
else if(videoSource is IHLSManifestSource) {
|
else if(videoSource is IHLSManifestSource) {
|
||||||
if (ad is ChromecastCastingDevice) {
|
if (ad is ChromecastCastingDevice && video.isLive) {
|
||||||
castHlsIndirect(video, videoSource.url, resumePosition);
|
castHlsIndirect(video, videoSource.url, resumePosition);
|
||||||
} else {
|
} else {
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble());
|
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", videoSource.container, videoSource.url, resumePosition, video.duration.toDouble());
|
||||||
}
|
}
|
||||||
} else if(audioSource is IHLSManifestAudioSource) {
|
} else if(audioSource is IHLSManifestAudioSource) {
|
||||||
if (ad is ChromecastCastingDevice) {
|
if (ad is ChromecastCastingDevice && video.isLive) {
|
||||||
castHlsIndirect(video, audioSource.url, resumePosition);
|
castHlsIndirect(video, audioSource.url, resumePosition);
|
||||||
} else {
|
} else {
|
||||||
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", audioSource.container, audioSource.url, resumePosition, video.duration.toDouble());
|
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 masterPlaylist = HLS.downloadAndParseMasterPlaylist(_client, sourceUrl)
|
||||||
val newVariantPlaylistRefs = arrayListOf<HLS.VariantPlaylistReference>()
|
val newVariantPlaylistRefs = arrayListOf<HLS.VariantPlaylistReference>()
|
||||||
val newMediaRenditions = arrayListOf<HLS.MediaRendition>()
|
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) {
|
for (variantPlaylistRef in masterPlaylist.variantPlaylistsRefs) {
|
||||||
val playlistId = UUID.randomUUID();
|
val playlistId = UUID.randomUUID();
|
||||||
|
@ -606,15 +606,17 @@ class StateCasting {
|
||||||
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
val newPlaylistPath = "/hls-playlist-${playlistId}"
|
||||||
val newPlaylistUrl = url + newPlaylistPath;
|
val newPlaylistUrl = url + newPlaylistPath;
|
||||||
|
|
||||||
_castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
if (mediaRendition.uri != null) {
|
||||||
val vpHeaders = vpContext.headers.clone()
|
_castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
|
||||||
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
val vpHeaders = vpContext.headers.clone()
|
||||||
|
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
|
||||||
|
|
||||||
val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, mediaRendition.uri)
|
val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, mediaRendition.uri)
|
||||||
val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist)
|
val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist)
|
||||||
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
|
||||||
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
|
||||||
}.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castHlsIndirectVariant")
|
}.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castHlsIndirectVariant")
|
||||||
|
}
|
||||||
|
|
||||||
newMediaRenditions.add(HLS.MediaRendition(
|
newMediaRenditions.add(HLS.MediaRendition(
|
||||||
mediaRendition.type,
|
mediaRendition.type,
|
||||||
|
@ -637,12 +639,16 @@ class StateCasting {
|
||||||
return listOf(hlsUrl);
|
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>()
|
val newSegments = arrayListOf<HLS.Segment>()
|
||||||
|
|
||||||
variantPlaylist.segments.forEachIndexed { index, segment ->
|
if (proxySegments) {
|
||||||
val sequenceNumber = variantPlaylist.mediaSequence + index.toLong()
|
variantPlaylist.segments.forEachIndexed { index, segment ->
|
||||||
newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber))
|
val sequenceNumber = variantPlaylist.mediaSequence + index.toLong()
|
||||||
|
newSegments.add(proxySegment(url, playlistId, segment, sequenceNumber))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
newSegments.addAll(variantPlaylist.segments)
|
||||||
}
|
}
|
||||||
|
|
||||||
return HLS.VariantPlaylist(
|
return HLS.VariantPlaylist(
|
||||||
|
|
|
@ -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.api.media.models.ratings.RatingLikeDislikes
|
||||||
import com.futo.platformplayer.constructs.Event1
|
import com.futo.platformplayer.constructs.Event1
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.selectBestImage
|
import com.futo.platformplayer.selectBestImage
|
||||||
import com.futo.platformplayer.states.StateApp
|
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) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Started backfill");
|
Logger.i(TAG, "Started backfill");
|
||||||
processHandle.fullyBackfillServers()
|
processHandle.fullyBackfillServersAnnounceExceptions()
|
||||||
Logger.i(TAG, "Finished backfill");
|
Logger.i(TAG, "Finished backfill");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to backfill servers.", e);
|
Logger.e(TAG, "Failed to backfill servers.", e);
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.states.StatePlatform
|
import com.futo.platformplayer.states.StatePlatform
|
||||||
import com.futo.platformplayer.R
|
import com.futo.platformplayer.R
|
||||||
|
import com.futo.platformplayer.Settings
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||||
|
@ -151,7 +152,7 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||||
|
|
||||||
_recyclerResults = view.findViewById(R.id.recycler_videos);
|
_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.onContentUrlClicked.subscribe(this@ChannelContentsFragment.onContentUrlClicked::emit);
|
||||||
this.onUrlClicked.subscribe(this@ChannelContentsFragment.onUrlClicked::emit);
|
this.onUrlClicked.subscribe(this@ChannelContentsFragment.onUrlClicked::emit);
|
||||||
this.onContentClicked.subscribe(this@ChannelContentsFragment.onContentClicked::emit);
|
this.onContentClicked.subscribe(this@ChannelContentsFragment.onContentClicked::emit);
|
||||||
|
|
|
@ -37,6 +37,7 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||||
override val visibleThreshold: Int get() = if (feedStyle == FeedStyle.PREVIEW) { 5 } else { 10 };
|
override val visibleThreshold: Int get() = if (feedStyle == FeedStyle.PREVIEW) { 5 } else { 10 };
|
||||||
protected lateinit var headerView: LinearLayout;
|
protected lateinit var headerView: LinearLayout;
|
||||||
private var _videoOptionsOverlay: SlideUpMenuOverlay? = null;
|
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) {
|
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;
|
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);
|
attachAdapterEvents(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,7 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||||
private var _channelUrl: String? = null;
|
private var _channelUrl: String? = null;
|
||||||
|
|
||||||
private val _taskSearch: TaskHandler<String, IPager<IPlatformContent>>;
|
private val _taskSearch: TaskHandler<String, IPager<IPlatformContent>>;
|
||||||
|
override val shouldShowTimeBar: Boolean get() = Settings.instance.timeBars.search
|
||||||
|
|
||||||
constructor(fragment: ContentSearchResultsFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
constructor(fragment: ContentSearchResultsFragment, inflater: LayoutInflater) : super(fragment, inflater) {
|
||||||
_taskSearch = TaskHandler<String, IPager<IPlatformContent>>({fragment.lifecycleScope}, { query ->
|
_taskSearch = TaskHandler<String, IPager<IPlatformContent>>({fragment.lifecycleScope}, { query ->
|
||||||
|
|
|
@ -95,6 +95,7 @@ class HomeFragment : MainFragment() {
|
||||||
private var _announcementsView: AnnouncementView;
|
private var _announcementsView: AnnouncementView;
|
||||||
|
|
||||||
private val _taskGetPager: TaskHandler<Boolean, IPager<IPlatformContent>>;
|
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) {
|
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 {
|
_announcementsView = AnnouncementView(context, null).apply {
|
||||||
|
|
|
@ -31,6 +31,7 @@ import com.futo.platformplayer.api.media.models.ratings.RatingLikes
|
||||||
import com.futo.platformplayer.constructs.TaskHandler
|
import com.futo.platformplayer.constructs.TaskHandler
|
||||||
import com.futo.platformplayer.dp
|
import com.futo.platformplayer.dp
|
||||||
import com.futo.platformplayer.fixHtmlWhitespace
|
import com.futo.platformplayer.fixHtmlWhitespace
|
||||||
|
import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions
|
||||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||||
import com.futo.platformplayer.logging.Logger
|
import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
|
@ -363,7 +364,7 @@ class PostDetailFragment : MainFragment {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Started backfill");
|
Logger.i(TAG, "Started backfill");
|
||||||
args.processHandle.fullyBackfillServers();
|
args.processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
Logger.i(TAG, "Finished backfill");
|
Logger.i(TAG, "Finished backfill");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to backfill servers", e)
|
Logger.e(TAG, "Failed to backfill servers", e)
|
||||||
|
|
|
@ -93,6 +93,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
class SubscriptionsFeedView : ContentFeedView<SubscriptionsFeedFragment> {
|
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) {
|
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()");
|
Logger.i(TAG, "SubscriptionsFeedFragment constructor()");
|
||||||
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total ->
|
StateSubscriptions.instance.onGlobalSubscriptionsUpdateProgress.subscribe(this) { progress, total ->
|
||||||
|
|
|
@ -1181,7 +1181,7 @@ class VideoDetailView : ConstraintLayout {
|
||||||
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
fragment.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Started backfill");
|
Logger.i(TAG, "Started backfill");
|
||||||
args.processHandle.fullyBackfillServers();
|
args.processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
Logger.i(TAG, "Finished backfill");
|
Logger.i(TAG, "Finished backfill");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to backfill servers", e)
|
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("audio", _lastAudioSource);
|
||||||
_overlay_quality_selector?.selectOption("subtitles", _lastSubtitleSource);
|
_overlay_quality_selector?.selectOption("subtitles", _lastSubtitleSource);
|
||||||
_overlay_quality_selector?.show();
|
_overlay_quality_selector?.show();
|
||||||
|
_slideUpOverlay = _overlay_quality_selector;
|
||||||
}
|
}
|
||||||
|
|
||||||
fun prevVideo() {
|
fun prevVideo() {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.futo.platformplayer.parsers
|
package com.futo.platformplayer.parsers
|
||||||
|
|
||||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||||
|
import com.futo.platformplayer.toURIRobust
|
||||||
import com.futo.platformplayer.yesNoToBoolean
|
import com.futo.platformplayer.yesNoToBoolean
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
|
@ -14,10 +15,11 @@ class HLS {
|
||||||
|
|
||||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||||
?: throw Exception("Master playlist content is empty")
|
?: throw Exception("Master playlist content is empty")
|
||||||
val baseUrl = URI(sourceUrl).resolve("./").toString()
|
val baseUrl = sourceUrl.toURIRobust()!!.resolve("./").toString()
|
||||||
|
|
||||||
val variantPlaylists = mutableListOf<VariantPlaylistReference>()
|
val variantPlaylists = mutableListOf<VariantPlaylistReference>()
|
||||||
val mediaRenditions = mutableListOf<MediaRendition>()
|
val mediaRenditions = mutableListOf<MediaRendition>()
|
||||||
|
val sessionDataList = mutableListOf<SessionData>()
|
||||||
var independentSegments = false
|
var independentSegments = false
|
||||||
|
|
||||||
masterPlaylistContent.lines().forEachIndexed { index, line ->
|
masterPlaylistContent.lines().forEachIndexed { index, line ->
|
||||||
|
@ -37,10 +39,15 @@ class HLS {
|
||||||
line == "#EXT-X-INDEPENDENT-SEGMENTS" -> {
|
line == "#EXT-X-INDEPENDENT-SEGMENTS" -> {
|
||||||
independentSegments = true
|
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 {
|
fun downloadAndParseVariantPlaylist(client: ManagedHttpClient, sourceUrl: String): VariantPlaylist {
|
||||||
|
@ -86,7 +93,7 @@ class HLS {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resolveUrl(baseUrl: String, url: String): String {
|
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 {
|
private fun parseMediaRendition(client: ManagedHttpClient, line: String, baseUrl: String): MediaRendition {
|
||||||
val attributes = parseAttributes(line)
|
val attributes = parseAttributes(line)
|
||||||
val uri = attributes["URI"]!!
|
val uri = attributes["URI"]?.let { resolveUrl(baseUrl, it) }
|
||||||
val url = resolveUrl(baseUrl, uri)
|
|
||||||
return MediaRendition(
|
return MediaRendition(
|
||||||
type = attributes["TYPE"],
|
type = attributes["TYPE"],
|
||||||
uri = url,
|
uri = uri,
|
||||||
groupID = attributes["GROUP-ID"],
|
groupID = attributes["GROUP-ID"],
|
||||||
language = attributes["LANGUAGE"],
|
language = attributes["LANGUAGE"],
|
||||||
name = attributes["NAME"],
|
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> {
|
private fun parseAttributes(content: String): Map<String, String> {
|
||||||
val attributes = mutableMapOf<String, String>()
|
val attributes = mutableMapOf<String, String>()
|
||||||
val attributePairs = content.substringAfter(":").splitToSequence(',')
|
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(
|
data class StreamInfo(
|
||||||
val bandwidth: Int?,
|
val bandwidth: Int?,
|
||||||
val resolution: String?,
|
val resolution: String?,
|
||||||
|
@ -170,7 +197,7 @@ class HLS {
|
||||||
|
|
||||||
data class MediaRendition(
|
data class MediaRendition(
|
||||||
val type: String?,
|
val type: String?,
|
||||||
val uri: String,
|
val uri: String?,
|
||||||
val groupID: String?,
|
val groupID: String?,
|
||||||
val language: String?,
|
val language: String?,
|
||||||
val name: String?,
|
val name: String?,
|
||||||
|
@ -194,9 +221,11 @@ class HLS {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
data class MasterPlaylist(
|
data class MasterPlaylist(
|
||||||
val variantPlaylistsRefs: List<VariantPlaylistReference>,
|
val variantPlaylistsRefs: List<VariantPlaylistReference>,
|
||||||
val mediaRenditions: List<MediaRendition>,
|
val mediaRenditions: List<MediaRendition>,
|
||||||
|
val sessionDataList: List<SessionData>,
|
||||||
val independentSegments: Boolean
|
val independentSegments: Boolean
|
||||||
) {
|
) {
|
||||||
fun buildM3U8(): String {
|
fun buildM3U8(): String {
|
||||||
|
@ -214,6 +243,10 @@ class HLS {
|
||||||
builder.append(variant.toM3U8Line())
|
builder.append(variant.toM3U8Line())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sessionDataList.forEach { data ->
|
||||||
|
builder.append(data.toM3U8Line())
|
||||||
|
}
|
||||||
|
|
||||||
return builder.toString()
|
return builder.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ class CommentViewHolder : ViewHolder {
|
||||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
Logger.i(TAG, "Started backfill");
|
Logger.i(TAG, "Started backfill");
|
||||||
args.processHandle.fullyBackfillServers();
|
args.processHandle.fullyBackfillServersAnnounceExceptions();
|
||||||
Logger.i(TAG, "Finished backfill");
|
Logger.i(TAG, "Finished backfill");
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.e(TAG, "Failed to backfill servers.", e)
|
Logger.e(TAG, "Failed to backfill servers.", e)
|
||||||
|
|
|
@ -29,6 +29,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||||
private val _exoPlayer: PlayerManager?;
|
private val _exoPlayer: PlayerManager?;
|
||||||
private val _feedStyle : FeedStyle;
|
private val _feedStyle : FeedStyle;
|
||||||
private var _paused: Boolean = false;
|
private var _paused: Boolean = false;
|
||||||
|
private val _shouldShowTimeBar: Boolean
|
||||||
|
|
||||||
val onUrlClicked = Event1<String>();
|
val onUrlClicked = Event1<String>();
|
||||||
val onContentUrlClicked = Event2<String, ContentType>();
|
val onContentUrlClicked = Event2<String, ContentType>();
|
||||||
|
@ -48,12 +49,13 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||||
|
|
||||||
constructor(context: Context, feedStyle : FeedStyle, dataSet: ArrayList<IPlatformContent>, exoPlayer: PlayerManager? = null,
|
constructor(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()) : super(context, viewsToPrepend, viewsToAppend) {
|
viewsToAppend: ArrayList<View> = arrayListOf(), shouldShowTimeBar: Boolean = true) : super(context, viewsToPrepend, viewsToAppend) {
|
||||||
|
|
||||||
this._feedStyle = feedStyle;
|
this._feedStyle = feedStyle;
|
||||||
this._dataSet = dataSet;
|
this._dataSet = dataSet;
|
||||||
this._initialPlay = initialPlay;
|
this._initialPlay = initialPlay;
|
||||||
this._exoPlayer = exoPlayer;
|
this._exoPlayer = exoPlayer;
|
||||||
|
this._shouldShowTimeBar = shouldShowTimeBar
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChildCount(): Int = _dataSet.size;
|
override fun getChildCount(): Int = _dataSet.size;
|
||||||
|
@ -97,7 +99,7 @@ class PreviewContentListAdapter : InsertedViewAdapterWithLoader<ContentPreviewVi
|
||||||
};
|
};
|
||||||
private fun createPlaceholderViewHolder(viewGroup: ViewGroup): PreviewPlaceholderViewHolder
|
private fun createPlaceholderViewHolder(viewGroup: ViewGroup): PreviewPlaceholderViewHolder
|
||||||
= PreviewPlaceholderViewHolder(viewGroup, _feedStyle);
|
= 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.onVideoClicked.subscribe(this@PreviewContentListAdapter.onContentClicked::emit);
|
||||||
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
|
this.onChannelClicked.subscribe(this@PreviewContentListAdapter.onChannelClicked::emit);
|
||||||
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
|
this.onAddToClicked.subscribe(this@PreviewContentListAdapter.onAddToClicked::emit);
|
||||||
|
|
|
@ -27,9 +27,11 @@ import com.futo.platformplayer.logging.Logger
|
||||||
import com.futo.platformplayer.polycentric.PolycentricCache
|
import com.futo.platformplayer.polycentric.PolycentricCache
|
||||||
import com.futo.platformplayer.states.StateApp
|
import com.futo.platformplayer.states.StateApp
|
||||||
import com.futo.platformplayer.states.StateDownloads
|
import com.futo.platformplayer.states.StateDownloads
|
||||||
|
import com.futo.platformplayer.states.StatePlaylists
|
||||||
import com.futo.platformplayer.video.PlayerManager
|
import com.futo.platformplayer.video.PlayerManager
|
||||||
import com.futo.platformplayer.views.others.CreatorThumbnail
|
import com.futo.platformplayer.views.others.CreatorThumbnail
|
||||||
import com.futo.platformplayer.views.FeedStyle
|
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.platform.PlatformIndicator
|
||||||
import com.futo.platformplayer.views.video.FutoThumbnailPlayer
|
import com.futo.platformplayer.views.video.FutoThumbnailPlayer
|
||||||
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
import com.futo.polycentric.core.toURLInfoSystemLinkUrl
|
||||||
|
@ -67,6 +69,8 @@ open class PreviewVideoView : LinearLayout {
|
||||||
Logger.w(TAG, "Failed to load profile.", it);
|
Logger.w(TAG, "Failed to load profile.", it);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private val _timeBar: ProgressBar;
|
||||||
|
|
||||||
val onVideoClicked = Event2<IPlatformVideo, Long>();
|
val onVideoClicked = Event2<IPlatformVideo, Long>();
|
||||||
val onLongPress = Event1<IPlatformVideo>();
|
val onLongPress = Event1<IPlatformVideo>();
|
||||||
val onChannelClicked = Event1<PlatformAuthorLink>();
|
val onChannelClicked = Event1<PlatformAuthorLink>();
|
||||||
|
@ -77,10 +81,12 @@ open class PreviewVideoView : LinearLayout {
|
||||||
private set
|
private set
|
||||||
|
|
||||||
val content: IPlatformContent? get() = currentVideo;
|
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);
|
inflate(feedStyle);
|
||||||
_feedStyle = feedStyle;
|
_feedStyle = feedStyle;
|
||||||
|
this.shouldShowTimeBar = shouldShowTimeBar
|
||||||
val playerContainer = findViewById<FrameLayout>(R.id.player_container);
|
val playerContainer = findViewById<FrameLayout>(R.id.player_container);
|
||||||
|
|
||||||
val displayMetrics = Resources.getSystem().displayMetrics;
|
val displayMetrics = Resources.getSystem().displayMetrics;
|
||||||
|
@ -117,6 +123,7 @@ open class PreviewVideoView : LinearLayout {
|
||||||
_button_add_to = findViewById(R.id.button_add_to);
|
_button_add_to = findViewById(R.id.button_add_to);
|
||||||
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
_imageNeopassChannel = findViewById(R.id.image_neopass_channel);
|
||||||
_layoutDownloaded = findViewById(R.id.layout_downloaded);
|
_layoutDownloaded = findViewById(R.id.layout_downloaded);
|
||||||
|
_timeBar = findViewById(R.id.time_bar)
|
||||||
|
|
||||||
this._exoPlayer = exoPlayer
|
this._exoPlayer = exoPlayer
|
||||||
|
|
||||||
|
@ -235,13 +242,23 @@ open class PreviewVideoView : LinearLayout {
|
||||||
_containerLive.visibility = GONE;
|
_containerLive.visibility = GONE;
|
||||||
_containerDuration.visibility = VISIBLE;
|
_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 {
|
else {
|
||||||
currentVideo = null;
|
currentVideo = null;
|
||||||
_imageVideo.setImageResource(0);
|
_imageVideo.setImageResource(0);
|
||||||
_containerDuration.visibility = GONE;
|
_containerDuration.visibility = GONE;
|
||||||
_containerLive.visibility = GONE;
|
_containerLive.visibility = GONE;
|
||||||
|
_timeBar.visibility = GONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
_textVideoMetadata.text = metadata + timeMeta;
|
_textVideoMetadata.text = metadata + timeMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,8 +27,8 @@ class PreviewVideoViewHolder : ContentPreviewViewHolder {
|
||||||
|
|
||||||
private val view: PreviewVideoView get() = itemView as PreviewVideoView;
|
private val view: PreviewVideoView get() = itemView as PreviewVideoView;
|
||||||
|
|
||||||
constructor(viewGroup: ViewGroup, feedStyle : FeedStyle, exoPlayer: PlayerManager? = null): super(
|
constructor(viewGroup: ViewGroup, feedStyle : FeedStyle, exoPlayer: PlayerManager? = null, shouldShowTimeBar: Boolean = true): super(
|
||||||
PreviewVideoView(viewGroup.context, feedStyle, exoPlayer)
|
PreviewVideoView(viewGroup.context, feedStyle, exoPlayer, shouldShowTimeBar)
|
||||||
) {
|
) {
|
||||||
view.onVideoClicked.subscribe(onVideoClicked::emit);
|
view.onVideoClicked.subscribe(onVideoClicked::emit);
|
||||||
view.onChannelClicked.subscribe(onChannelClicked::emit);
|
view.onChannelClicked.subscribe(onChannelClicked::emit);
|
||||||
|
|
|
@ -32,6 +32,20 @@
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
tools:srcCompat="@drawable/placeholder_video_thumbnail" />
|
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
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
|
@ -117,6 +117,20 @@
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
android:layout_marginStart="4dp"
|
android:layout_marginStart="4dp"
|
||||||
android:layout_marginBottom="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>
|
</RelativeLayout>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
|
@ -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="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="add_to_queue">Add to queue</string>
|
||||||
<string name="general">General</string>
|
<string name="general">General</string>
|
||||||
|
<string name="channel">Channel</string>
|
||||||
<string name="home">Home</string>
|
<string name="home">Home</string>
|
||||||
<string name="recommendations">Recommendations</string>
|
<string name="recommendations">Recommendations</string>
|
||||||
<string name="more">More</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_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="clears_in_app_browser_cookies">Clears in-app browser cookies</string>
|
||||||
<string name="configure_browsing_behavior">Configure browsing behavior</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_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_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>
|
<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
|
Loading…
Add table
Add a link
Reference in a new issue