mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-27 02:05:20 +02:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f31abac409 | |||
| 8c879c68d9 | |||
| 0190bbffdd |
File diff suppressed because one or more lines are too long
@@ -11,8 +11,7 @@ let Type = {
|
||||
Streams: "STREAMS",
|
||||
Mixed: "MIXED",
|
||||
Live: "LIVE",
|
||||
Subscriptions: "SUBSCRIPTIONS",
|
||||
Shorts: "SHORTS"
|
||||
Subscriptions: "SUBSCRIPTIONS"
|
||||
},
|
||||
Order: {
|
||||
Chronological: "CHRONOLOGICAL"
|
||||
@@ -245,7 +244,6 @@ class PlatformVideo extends PlatformContent {
|
||||
this.viewCount = obj.viewCount ?? -1; //Long
|
||||
|
||||
this.isLive = obj.isLive ?? false; //Boolean
|
||||
this.isShort = !!obj.isShort ?? false;
|
||||
}
|
||||
}
|
||||
class PlatformVideoDetails extends PlatformVideo {
|
||||
@@ -262,7 +260,6 @@ class PlatformVideoDetails extends PlatformVideo {
|
||||
|
||||
this.rating = obj.rating ?? null; //IRating
|
||||
this.subtitles = obj.subtitles ?? [];
|
||||
this.isShort = !!obj.isShort ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -226,25 +226,6 @@ fun Long.toHumanTime(isMs: Boolean): String {
|
||||
else
|
||||
return "${prefix}${minsStr}:${secsStr}"
|
||||
}
|
||||
fun Long.toHumanDuration(isMs: Boolean): String {
|
||||
var scaler = 1;
|
||||
if(isMs)
|
||||
scaler = 1000;
|
||||
val v = Math.abs(this);
|
||||
val hours = Math.max(v/(secondsInHour*scaler), 0);
|
||||
val mins = Math.max((v % (secondsInHour*scaler)) / (secondsInMinute * scaler), 0);
|
||||
val minsStr = mins.toString();
|
||||
val seconds = Math.max(((v % (secondsInHour*scaler)) % (secondsInMinute * scaler))/scaler, 0);
|
||||
val secsStr = seconds.toString().padStart(2, '0');
|
||||
val prefix = if (this < 0) { "-" } else { "" };
|
||||
|
||||
return listOf(
|
||||
if(hours > 0) "${hours}h" else null,
|
||||
if(mins > 0) "${mins}m" else null ,
|
||||
if(seconds > 0) "${seconds}s" else null
|
||||
).filterNotNull().joinToString(" ");
|
||||
}
|
||||
|
||||
|
||||
//TODO: Determine if below stuff should have its own proper class, seems a bit too complex for a utility method
|
||||
fun String.fixHtmlWhitespace(): Spanned {
|
||||
|
||||
@@ -6,7 +6,6 @@ import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.net.Inet4Address
|
||||
import java.net.Inet6Address
|
||||
import java.net.InetAddress
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Socket
|
||||
|
||||
@@ -33,10 +33,10 @@ fun Boolean?.toYesNo(): String {
|
||||
fun InetAddress?.toUrlAddress(): String {
|
||||
return when (this) {
|
||||
is Inet6Address -> {
|
||||
"[${hostAddress}]"
|
||||
"[${toString()}]"
|
||||
}
|
||||
is Inet4Address -> {
|
||||
hostAddress
|
||||
toString()
|
||||
}
|
||||
else -> {
|
||||
throw Exception("Invalid address type")
|
||||
|
||||
@@ -254,9 +254,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@FormField(R.string.progress_bar, FieldForm.TOGGLE, R.string.progress_bar_description, 6)
|
||||
var progressBar: Boolean = true;
|
||||
|
||||
@FormField(R.string.hide_hidden_from_search, FieldForm.TOGGLE, R.string.hide_hidden_from_search_description, 7)
|
||||
var hidefromSearch: Boolean = false;
|
||||
|
||||
|
||||
fun getSearchFeedStyle(): FeedStyle {
|
||||
if(searchFeedStyle == 0)
|
||||
@@ -864,13 +861,11 @@ class Settings : FragmentedStorageFileJson() {
|
||||
class Other {
|
||||
@FormField(R.string.playlist_delete_confirmation, FieldForm.TOGGLE, R.string.playlist_delete_confirmation_description, 2)
|
||||
var playlistDeleteConfirmation: Boolean = true;
|
||||
@FormField(R.string.playlist_allow_dups, FieldForm.TOGGLE, R.string.playlist_allow_dups_description, 3)
|
||||
var playlistAllowDups: Boolean = true;
|
||||
|
||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 4)
|
||||
@FormField(R.string.enable_polycentric, FieldForm.TOGGLE, R.string.can_be_disabled_when_you_are_experiencing_issues, 3)
|
||||
var polycentricEnabled: Boolean = true;
|
||||
|
||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 5)
|
||||
@FormField(R.string.polycentric_local_cache, FieldForm.TOGGLE, R.string.polycentric_local_cache_description, 4)
|
||||
var polycentricLocalCache: Boolean = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -368,8 +368,8 @@ class UIDialogs {
|
||||
}
|
||||
}
|
||||
|
||||
fun showChangelogDialog(context: Context, lastVersion: Int, changelogs: Map<Int, String>? = null) {
|
||||
val dialog = ChangelogDialog(context, changelogs);
|
||||
fun showChangelogDialog(context: Context, lastVersion: Int) {
|
||||
val dialog = ChangelogDialog(context);
|
||||
registerDialogOpened(dialog);
|
||||
dialog.setOnDismissListener { registerDialogClosed(dialog) };
|
||||
dialog.show();
|
||||
|
||||
@@ -4,8 +4,13 @@ import android.app.NotificationManager
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.activities.MainActivity
|
||||
import com.futo.platformplayer.activities.SettingsActivity
|
||||
@@ -13,10 +18,13 @@ import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.DashManifestAudioSourceDelegate
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.DashManifestSourceDelegate
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
@@ -28,6 +36,10 @@ import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.downloads.VideoLocal
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.SubscriptionGroupFragment
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
@@ -36,6 +48,7 @@ import com.futo.platformplayer.models.ImageVariable
|
||||
import com.futo.platformplayer.models.Playlist
|
||||
import com.futo.platformplayer.models.Subscription
|
||||
import com.futo.platformplayer.models.SubscriptionGroup
|
||||
import com.futo.platformplayer.others.Language
|
||||
import com.futo.platformplayer.parsers.HLS
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
@@ -63,6 +76,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.ByteArrayInputStream
|
||||
|
||||
class UISlideOverlays {
|
||||
companion object {
|
||||
@@ -269,14 +283,116 @@ class UISlideOverlays {
|
||||
|
||||
}
|
||||
|
||||
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
|
||||
@OptIn(UnstableApi::class)
|
||||
fun showDashPicker(video: IPlatformVideoDetails, source: JSDashManifestSource, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
|
||||
val items = arrayListOf<View>(LoaderView(container.context))
|
||||
val slideUpMenuOverlay =
|
||||
SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val manifestResponse = ManagedHttpClient().get(sourceUrl)
|
||||
check(manifestResponse.isOk) { "Failed to get DASH manifest: ${manifestResponse.code}" }
|
||||
|
||||
val manifestContent = manifestResponse.body?.string()
|
||||
?: throw Exception("Manifest content is empty")
|
||||
|
||||
val videoButtons = arrayListOf<SlideUpMenuItem>()
|
||||
val audioButtons = arrayListOf<SlideUpMenuItem>()
|
||||
//TODO: Implement subtitles
|
||||
//val subtitleButtons = arrayListOf<SlideUpMenuItem>()
|
||||
|
||||
var selectedVideoVariant: IDashManifestSource? = null
|
||||
var selectedAudioVariant: IAudioSource? = null
|
||||
//TODO: Implement subtitles
|
||||
//var selectedSubtitleVariant: HLSVariantSubtitleUrlSource? = null
|
||||
|
||||
val manifestStream = ByteArrayInputStream(manifestContent.toByteArray())
|
||||
val playlist = DashManifestParser().parse(Uri.parse(sourceUrl), manifestStream)
|
||||
|
||||
playlist.getPeriod(0).adaptationSets.filter { it.type == C.TRACK_TYPE_AUDIO }
|
||||
.flatMap { it.representations }.forEach {
|
||||
audioButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.format.containerMimeType
|
||||
?: "", listOf(it.format.language, it.format.codecs).mapNotNull { x -> x?.ifEmpty { null } }
|
||||
.joinToString(", "), it.format.codecs, tag = it, call = {
|
||||
selectedAudioVariant = DashManifestAudioSourceDelegate(
|
||||
source, it.format.language
|
||||
?: Language.UNKNOWN, it.format.bitrate, it.format.containerMimeType!!
|
||||
)
|
||||
|
||||
slideUpMenuOverlay.selectOption(audioButtons, it)
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
}, invokeParent = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/*masterPlaylist.getSubtitleSources().forEach { it ->
|
||||
subtitleButtons.add(SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, listOf(it.format).mapNotNull { x -> x.ifEmpty { null } }.joinToString(", "), it, {
|
||||
selectedSubtitleVariant = it
|
||||
slideUpMenuOverlay.selectOption(subtitleButtons, it)
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
}, false))
|
||||
}*/
|
||||
|
||||
playlist.getPeriod(0).adaptationSets.filter { it.type == C.TRACK_TYPE_VIDEO }
|
||||
.flatMap { it.representations }.forEach {
|
||||
videoButtons.add(
|
||||
SlideUpMenuItem(
|
||||
container.context, R.drawable.ic_movie, it.format.containerMimeType
|
||||
?: "", "${it.format.width}x${it.format.height}", it.format.codecs, tag = it, call = {
|
||||
selectedVideoVariant =
|
||||
DashManifestSourceDelegate(source, it.format.width, it.format.height, it.format.containerMimeType!!)
|
||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||
if (audioButtons.isEmpty()){
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
}
|
||||
}, invokeParent = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val newItems = arrayListOf<View>()
|
||||
if (videoButtons.isNotEmpty()) {
|
||||
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoButtons, videoButtons))
|
||||
}
|
||||
if (audioButtons.isNotEmpty()) {
|
||||
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.audio), audioButtons, audioButtons))
|
||||
}
|
||||
//TODO: Implement subtitles
|
||||
/*if (subtitleButtons.isNotEmpty()) {
|
||||
newItems.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.subtitles), subtitleButtons, subtitleButtons))
|
||||
}*/
|
||||
|
||||
slideUpMenuOverlay.onOK.subscribe {
|
||||
//TODO: Fix SubtitleRawSource issue
|
||||
StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null)
|
||||
slideUpMenuOverlay.hide()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
slideUpMenuOverlay.setItems(newItems)
|
||||
}
|
||||
}
|
||||
|
||||
return slideUpMenuOverlay.apply { show() }
|
||||
}
|
||||
|
||||
fun showHlsPicker(video: IPlatformVideoDetails, source: JSSource, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
|
||||
val items = arrayListOf<View>(LoaderView(container.context))
|
||||
val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items)
|
||||
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
val masterPlaylistResponse = ManagedHttpClient().get(sourceUrl)
|
||||
|
||||
val masterPlaylistResponse = if (source.hasRequestModifier) {
|
||||
val request = source.getRequestModifier()!!.modifyRequest(sourceUrl, mapOf())
|
||||
ManagedHttpClient().get(request.url!!, request.headers.toMutableMap())
|
||||
} else {
|
||||
ManagedHttpClient().get(sourceUrl)
|
||||
}
|
||||
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||
|
||||
val resolvedSourceUrl = masterPlaylistResponse.url
|
||||
|
||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||
?: throw Exception("Master playlist content is empty")
|
||||
|
||||
@@ -285,14 +401,14 @@ class UISlideOverlays {
|
||||
//TODO: Implement subtitles
|
||||
//val subtitleButtons = arrayListOf<SlideUpMenuItem>()
|
||||
|
||||
var selectedVideoVariant: HLSVariantVideoUrlSource? = null
|
||||
var selectedAudioVariant: HLSVariantAudioUrlSource? = null
|
||||
var selectedVideoVariant: IHLSManifestSource? = null
|
||||
var selectedAudioVariant: IAudioSource? = null
|
||||
//TODO: Implement subtitles
|
||||
//var selectedSubtitleVariant: HLSVariantSubtitleUrlSource? = null
|
||||
|
||||
val masterPlaylist: HLS.MasterPlaylist
|
||||
try {
|
||||
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
||||
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, masterPlaylistResponse.url, source is IHLSManifestAudioSource)
|
||||
|
||||
masterPlaylist.getAudioSources().forEach { it ->
|
||||
|
||||
@@ -306,7 +422,19 @@ class UISlideOverlays {
|
||||
(prefix + it.codec).trim(),
|
||||
tag = it,
|
||||
call = {
|
||||
selectedAudioVariant = it
|
||||
if (source is JSHLSManifestAudioSource) {
|
||||
source.setPreferredBitrate(it.bitrate)
|
||||
source.setPreferredLanguage(it.language)
|
||||
source.setPreferredContainer(it.container)
|
||||
selectedAudioVariant = source
|
||||
} else if (source is JSHLSManifestSource) {
|
||||
source.setPreferredBitrate(it.bitrate)
|
||||
source.setPreferredLanguage(it.language)
|
||||
selectedAudioVariant = source
|
||||
} else {
|
||||
throw Exception("Expected HLS Source")
|
||||
}
|
||||
|
||||
slideUpMenuOverlay.selectOption(audioButtons, it)
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
},
|
||||
@@ -333,7 +461,12 @@ class UISlideOverlays {
|
||||
(prefix + it.codec).trim(),
|
||||
tag = it,
|
||||
call = {
|
||||
selectedVideoVariant = it
|
||||
if (source !is JSHLSManifestSource){
|
||||
throw Exception("Expected HLS Source")
|
||||
}
|
||||
source.setPreferredWidth(it.width)
|
||||
source.setPreferredHeight(it.height)
|
||||
selectedVideoVariant = source
|
||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||
if (audioButtons.isEmpty()){
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
@@ -368,11 +501,11 @@ class UISlideOverlays {
|
||||
if (masterPlaylistContent.lines().any { it.startsWith("#EXTINF:") }) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (source is IHLSManifestSource) {
|
||||
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, sourceUrl), null, null)
|
||||
StateDownloads.instance.download(video, HLSVariantVideoUrlSource("variant", 0, 0, "application/vnd.apple.mpegurl", "", null, 0, false, resolvedSourceUrl), null, null)
|
||||
UIDialogs.toast(container.context, "Variant video HLS playlist download started")
|
||||
slideUpMenuOverlay.hide()
|
||||
} else if (source is IHLSManifestAudioSource) {
|
||||
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, sourceUrl), null)
|
||||
StateDownloads.instance.download(video, null, HLSVariantAudioUrlSource("variant", 0, "application/vnd.apple.mpegurl", "", "", null, false, resolvedSourceUrl), null)
|
||||
UIDialogs.toast(container.context, "Variant audio HLS playlist download started")
|
||||
slideUpMenuOverlay.hide()
|
||||
} else {
|
||||
@@ -437,26 +570,6 @@ class UISlideOverlays {
|
||||
.filter { it.isDownloadable() }
|
||||
.map {
|
||||
when (it) {
|
||||
is IVideoUrlSource -> {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
it.name,
|
||||
"${it.width}x${it.height}",
|
||||
(prefix + it.codec).trim(),
|
||||
tag = it,
|
||||
call = {
|
||||
selectedVideo = it
|
||||
menu?.selectOption(videoSources, it);
|
||||
if(selectedAudio != null || !requiresAudio)
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
},
|
||||
invokeParent = false
|
||||
)
|
||||
}
|
||||
|
||||
is JSDashManifestRawSource -> {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
@@ -477,7 +590,15 @@ class UISlideOverlays {
|
||||
)
|
||||
}
|
||||
|
||||
is IHLSManifestSource -> {
|
||||
is JSDashManifestSource -> {
|
||||
SlideUpMenuItem(
|
||||
container.context, R.drawable.ic_movie, it.name, "DASH", tag = it, call = {
|
||||
showDashPicker(video, it, it.url, container)
|
||||
}, invokeParent = false
|
||||
)
|
||||
}
|
||||
|
||||
is JSHLSManifestSource -> {
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
@@ -491,6 +612,26 @@ class UISlideOverlays {
|
||||
)
|
||||
}
|
||||
|
||||
is IVideoUrlSource -> {
|
||||
val estSize = VideoHelper.estimateSourceSize(it);
|
||||
val prefix = if(estSize > 0) "±" + estSize.toHumanBytesSize() + " " else "";
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
it.name,
|
||||
"${it.width}x${it.height}",
|
||||
(prefix + it.codec).trim(),
|
||||
tag = it,
|
||||
call = {
|
||||
selectedVideo = it
|
||||
menu?.selectOption(videoSources, it);
|
||||
if(selectedAudio != null || !requiresAudio)
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
},
|
||||
invokeParent = false
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Logger.w(TAG, "Unhandled source type for UISlideOverlay download items");
|
||||
null;//throw Exception("Unhandled source type")
|
||||
@@ -551,7 +692,7 @@ class UISlideOverlays {
|
||||
);
|
||||
}
|
||||
|
||||
is IHLSManifestAudioSource -> {
|
||||
is JSHLSManifestAudioSource -> {
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
@@ -616,13 +757,18 @@ class UISlideOverlays {
|
||||
|
||||
menu.onOK.subscribe {
|
||||
val sv = selectedVideo
|
||||
if (sv is IHLSManifestSource) {
|
||||
if (sv is JSHLSManifestSource) {
|
||||
showHlsPicker(video, sv, sv.url, container)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
if (sv is JSDashManifestSource) {
|
||||
showDashPicker(video, sv, sv.url, container)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
val sa = selectedAudio
|
||||
if (sa is IHLSManifestAudioSource) {
|
||||
if (sa is JSHLSManifestAudioSource) {
|
||||
showHlsPicker(video, sa, sa.url, container)
|
||||
return@subscribe
|
||||
}
|
||||
@@ -897,8 +1043,7 @@ class UISlideOverlays {
|
||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||
tag = "",
|
||||
call = {
|
||||
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
||||
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
}))
|
||||
);
|
||||
@@ -909,7 +1054,7 @@ class UISlideOverlays {
|
||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||
(listOf(
|
||||
if(!isLimited && !video.isLive)
|
||||
if(!isLimited)
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_download,
|
||||
@@ -994,8 +1139,7 @@ class UISlideOverlays {
|
||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||
tag = "",
|
||||
call = {
|
||||
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
||||
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
}));
|
||||
}
|
||||
@@ -1022,8 +1166,7 @@ class UISlideOverlays {
|
||||
"${lastUpdated.videos.size} " + container.context.getString(R.string.videos),
|
||||
tag = "",
|
||||
call = {
|
||||
if(StatePlaylists.instance.addToPlaylist(lastUpdated.id, video))
|
||||
UIDialogs.appToast("Added to playlist [${lastUpdated?.name}]", false);
|
||||
StatePlaylists.instance.addToPlaylist(lastUpdated.id, video);
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
}))
|
||||
);
|
||||
@@ -1045,9 +1188,7 @@ class UISlideOverlays {
|
||||
StatePlayer.TYPE_WATCHLATER,
|
||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||
tag = "watch later",
|
||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true);
|
||||
UIDialogs.appToast("Added to watch later", false);
|
||||
}),
|
||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true); }),
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1074,8 +1215,7 @@ class UISlideOverlays {
|
||||
"${playlist.videos.size} " + container.context.getString(R.string.videos),
|
||||
tag = "",
|
||||
call = {
|
||||
if(StatePlaylists.instance.addToPlaylist(playlist.id, video))
|
||||
UIDialogs.appToast("Added to playlist [${playlist.name}]", false);
|
||||
StatePlaylists.instance.addToPlaylist(playlist.id, video);
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1281,7 +1281,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher {
|
||||
if (toast.long)
|
||||
delay(5000);
|
||||
else
|
||||
delay(2500);
|
||||
delay(3000);
|
||||
}
|
||||
Logger.i(TAG, "Ending appToast loop");
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ class HttpFileHandler(method: String, path: String, private val contentType: Str
|
||||
Logger.v(TAG, "Sent bytes $current-${current + bytesToSend}, totalBytesSent=$totalBytesSent")
|
||||
|
||||
current += bytesToSend.toLong()
|
||||
if (current > end) {
|
||||
if (current >= end) {
|
||||
Logger.i(TAG, "Expected amount of bytes sent")
|
||||
break
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ class ResultCapabilities(
|
||||
const val TYPE_POSTS = "POSTS";
|
||||
const val TYPE_MIXED = "MIXED";
|
||||
const val TYPE_SUBSCRIPTIONS = "SUBSCRIPTIONS";
|
||||
const val TYPE_SHORTS = "SHORTS";
|
||||
|
||||
const val ORDER_CHONOLOGICAL = "CHRONOLOGICAL";
|
||||
|
||||
|
||||
+10
-7
@@ -1,18 +1,21 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
import com.futo.platformplayer.others.Language
|
||||
|
||||
class HLSManifestSource : IVideoSource, IHLSManifestSource {
|
||||
override val width : Int = 0;
|
||||
override val height : Int = 0;
|
||||
override val container : String = "HLS";
|
||||
override val width: Int = 0;
|
||||
override val height: Int = 0;
|
||||
override val container: String = "HLS";
|
||||
override val codec: String = "HLS";
|
||||
override val name : String = "HLS";
|
||||
override val bitrate : Int? = null;
|
||||
override val url : String;
|
||||
override val name: String = "HLS";
|
||||
override val bitrate: Int = 0;
|
||||
override val url: String;
|
||||
override val duration: Long = 0;
|
||||
override val language: String = Language.UNKNOWN;
|
||||
|
||||
override var priority: Boolean = false;
|
||||
|
||||
constructor(url : String) {
|
||||
constructor(url: String) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
+25
-1
@@ -1,5 +1,29 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.IUnderlyingObject
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestSource
|
||||
|
||||
interface IDashManifestSource : IVideoSource {
|
||||
val url : String;
|
||||
val url: String
|
||||
}
|
||||
|
||||
interface DashWrapper {
|
||||
val source: IDashManifestSource
|
||||
}
|
||||
|
||||
class DashManifestAudioSourceDelegate(
|
||||
override val source: JSDashManifestSource, override val language: String, override val bitrate: Int, override val container: String
|
||||
) : IDashManifestSource by source, IAudioSource, DashWrapper, IUnderlyingObject {
|
||||
override fun getUnderlyingObject(): V8ValueObject? {
|
||||
return source.getUnderlyingObject()
|
||||
}
|
||||
}
|
||||
|
||||
class DashManifestSourceDelegate(
|
||||
override val source: JSDashManifestSource, override val width: Int, override val height: Int, override val container: String
|
||||
) : IDashManifestSource by source, DashWrapper, IUnderlyingObject {
|
||||
override fun getUnderlyingObject(): V8ValueObject? {
|
||||
return source.getUnderlyingObject()
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
interface IHLSManifestSource : IVideoSource {
|
||||
val url : String;
|
||||
interface IHLSManifestSource : IVideoSource, IAudioSource {
|
||||
val url : String
|
||||
}
|
||||
interface IHLSManifestAudioSource : IAudioSource {
|
||||
val url : String;
|
||||
val url : String
|
||||
}
|
||||
+2
-2
@@ -33,13 +33,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
|
||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource {
|
||||
return LocalAudioSource(
|
||||
source.name,
|
||||
path,
|
||||
fileSize,
|
||||
source.bitrate,
|
||||
overrideContainer ?: source.container,
|
||||
source.container,
|
||||
source.codec,
|
||||
source.language
|
||||
);
|
||||
|
||||
+2
-2
@@ -35,7 +35,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
|
||||
fun fromSource(source: IVideoSource, path: String, fileSize: Long): LocalVideoSource {
|
||||
return LocalVideoSource(
|
||||
source.name,
|
||||
path,
|
||||
@@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
||||
source.width,
|
||||
source.height,
|
||||
source.duration,
|
||||
overrideContainer ?: source.container,
|
||||
source.container,
|
||||
source.codec,
|
||||
source.bitrate?:0
|
||||
);
|
||||
|
||||
@@ -13,6 +13,4 @@ interface IPlatformVideo : IPlatformContent {
|
||||
val viewCount: Long;
|
||||
|
||||
val isLive : Boolean;
|
||||
|
||||
val isShort: Boolean;
|
||||
}
|
||||
-1
@@ -25,7 +25,6 @@ open class SerializedPlatformVideo(
|
||||
|
||||
override val duration: Long,
|
||||
override val viewCount: Long,
|
||||
override val isShort: Boolean = false
|
||||
) : IPlatformVideo, SerializedPlatformContent {
|
||||
override val contentType: ContentType = ContentType.MEDIA;
|
||||
|
||||
|
||||
+1
-2
@@ -38,8 +38,7 @@ open class SerializedPlatformVideoDetails(
|
||||
override val video: ISerializedVideoSourceDescriptor,
|
||||
override val preview: ISerializedVideoSourceDescriptor?,
|
||||
|
||||
override val subtitles: List<SubtitleRawSource> = listOf(),
|
||||
override val isShort: Boolean = false
|
||||
override val subtitles: List<SubtitleRawSource> = listOf()
|
||||
) : IPlatformVideo, IPlatformVideoDetails {
|
||||
final override val contentType: ContentType get() = ContentType.MEDIA;
|
||||
|
||||
|
||||
+1
-20
@@ -33,7 +33,6 @@ class SourcePluginConfig(
|
||||
override val allowEval: Boolean = false,
|
||||
override val allowUrls: List<String> = listOf(),
|
||||
override val packages: List<String> = listOf(),
|
||||
override val packagesOptional: List<String> = listOf(),
|
||||
|
||||
val settings: List<Setting> = listOf(),
|
||||
|
||||
@@ -53,7 +52,6 @@ class SourcePluginConfig(
|
||||
var allowAllHttpHeaderAccess: Boolean = false,
|
||||
var maxDownloadParallelism: Int = 0,
|
||||
var reduceFunctionsInLimitedVersion: Boolean = false,
|
||||
var changelog: HashMap<String, List<String>>? = null
|
||||
) : IV8PluginConfig {
|
||||
|
||||
val absoluteIconUrl: String? get() = resolveAbsoluteUrl(iconUrl, sourceUrl);
|
||||
@@ -103,10 +101,6 @@ class SourcePluginConfig(
|
||||
if(!packages.contains(pack))
|
||||
return false;
|
||||
}
|
||||
for(pack in newConfig.packagesOptional) {
|
||||
if(!packagesOptional.contains(pack))
|
||||
return false;
|
||||
}
|
||||
//Developer Submit Url should be same or empty
|
||||
if(!newConfig.developerSubmitUrl.isNullOrEmpty() && developerSubmitUrl != newConfig.developerSubmitUrl)
|
||||
return false;
|
||||
@@ -135,7 +129,7 @@ class SourcePluginConfig(
|
||||
|
||||
val currentlyInstalledPlugin = StatePlugins.instance.getPlugin(id);
|
||||
if (currentlyInstalledPlugin != null) {
|
||||
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey && !currentlyInstalledPlugin.config.scriptPublicKey.isNullOrEmpty()) {
|
||||
if (currentlyInstalledPlugin.config.scriptPublicKey != scriptPublicKey) {
|
||||
list.add(Pair(
|
||||
"Different Author",
|
||||
"This plugin was signed by a different author. Please ensure that this is correct and that the plugin was not provided by a malicious actor."));
|
||||
@@ -184,19 +178,6 @@ class SourcePluginConfig(
|
||||
return _allowUrlsLower.any { it == host || (it.length > 0 && it[0] == '.' && host.matchesDomain(it)) };
|
||||
}
|
||||
|
||||
fun getChangelogString(version: String): String?{
|
||||
if(changelog == null || !changelog!!.containsKey(version))
|
||||
return null;
|
||||
val changelog = changelog!![version]!!;
|
||||
if(changelog.size > 1) {
|
||||
return "Changelog (${version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
|
||||
}
|
||||
else if(changelog.size == 1) {
|
||||
return "Changelog (${version})\n" + changelog[0].trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromJson(json: String, sourceUrl: String? = null): SourcePluginConfig {
|
||||
val obj = Serializer.json.decodeFromString<SourcePluginConfig>(json);
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.futo.platformplayer.api.media.models.Thumbnails
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||
@@ -18,7 +17,6 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||
final override val viewCount: Long;
|
||||
|
||||
final override val isLive: Boolean;
|
||||
final override val isShort: Boolean;
|
||||
|
||||
constructor(config: SourcePluginConfig, obj: V8ValueObject): super(config, obj) {
|
||||
val contextName = "PlatformVideo";
|
||||
@@ -28,6 +26,5 @@ open class JSVideo : JSContent, IPlatformVideo, IPluginSourced {
|
||||
duration = _content.getOrThrow<Int>(config, "duration", contextName).toLong();
|
||||
viewCount = _content.getOrThrow(config, "viewCount", contextName);
|
||||
isLive = _content.getOrThrow(config, "isLive", contextName);
|
||||
isShort = _content.getOrDefault(config, "isShort", contextName, false) ?: false;
|
||||
}
|
||||
}
|
||||
+4
-22
@@ -4,8 +4,6 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
@@ -16,8 +14,8 @@ import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.others.Language
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||
override val container : String;
|
||||
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource {
|
||||
override val container : String = "application/dash+xml";
|
||||
override val name : String;
|
||||
override val codec: String;
|
||||
override val bitrate: Int;
|
||||
@@ -31,14 +29,11 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||
|
||||
override val hasGenerate: Boolean;
|
||||
|
||||
override var streamMetaData: StreamMetaData? = null;
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||
val contextName = "DashRawSource";
|
||||
val config = plugin.config;
|
||||
name = _obj.getOrThrow(config, "name", contextName);
|
||||
url = _obj.getOrThrow(config, "url", contextName);
|
||||
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||
manifest = _obj.getOrThrow(config, "manifest", contextName);
|
||||
codec = _obj.getOrDefault(config, "codec", contextName, "") ?: "";
|
||||
bitrate = _obj.getOrDefault(config, "bitrate", contextName, 0) ?: 0;
|
||||
@@ -55,28 +50,15 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||
throw IllegalStateException("Source object already closed");
|
||||
|
||||
val plugin = _plugin.getUnderlyingPlugin();
|
||||
|
||||
var result: String? = null;
|
||||
if(_plugin is DevJSClient)
|
||||
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||
_obj.invokeString("generate");
|
||||
}
|
||||
}
|
||||
else
|
||||
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||
_obj.invokeString("generate");
|
||||
}
|
||||
|
||||
if(result != null){
|
||||
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+6
-28
@@ -6,8 +6,6 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
@@ -22,8 +20,8 @@ interface IJSDashManifestRawSource {
|
||||
var manifest: String?;
|
||||
fun generate(): String?;
|
||||
}
|
||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||
override val container : String;
|
||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource {
|
||||
override val container : String = "application/dash+xml";
|
||||
override val name : String;
|
||||
override val width: Int;
|
||||
override val height: Int;
|
||||
@@ -38,14 +36,11 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
||||
override val hasGenerate: Boolean;
|
||||
val canMerge: Boolean;
|
||||
|
||||
override var streamMetaData: StreamMetaData? = null;
|
||||
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH_RAW, plugin, obj) {
|
||||
val contextName = "DashRawSource";
|
||||
val config = plugin.config;
|
||||
name = _obj.getOrThrow(config, "name", contextName);
|
||||
url = _obj.getOrThrow(config, "url", contextName);
|
||||
container = _obj.getOrDefault<String>(config, "container", contextName, null) ?: "application/dash+xml";
|
||||
manifest = _obj.getOrDefault<String>(config, "manifest", contextName, null);
|
||||
width = _obj.getOrDefault(config, "width", contextName, 0) ?: 0;
|
||||
height = _obj.getOrDefault(config, "height", contextName, 0) ?: 0;
|
||||
@@ -62,30 +57,17 @@ open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSo
|
||||
return manifest;
|
||||
if(_obj.isClosed)
|
||||
throw IllegalStateException("Source object already closed");
|
||||
|
||||
var result: String? = null;
|
||||
if(_plugin is DevJSClient) {
|
||||
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||
_obj.invokeString("generate");
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
result = _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||
_obj.invokeString("generate");
|
||||
});
|
||||
|
||||
if(result != null){
|
||||
val initStart = _obj.getOrDefault<Int>(_config, "initStart", "JSDashManifestRawSource", null) ?: 0;
|
||||
val initEnd = _obj.getOrDefault<Int>(_config, "initEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||
val indexStart = _obj.getOrDefault<Int>(_config, "indexStart", "JSDashManifestRawSource", null) ?: 0;
|
||||
val indexEnd = _obj.getOrDefault<Int>(_config, "indexEnd", "JSDashManifestRawSource", null) ?: 0;
|
||||
if(initEnd > 0 && indexStart > 0 && indexEnd > 0) {
|
||||
streamMetaData = StreamMetaData(initStart, initEnd, indexStart, indexEnd);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,16 +100,12 @@ class JSDashManifestMergingRawSource(
|
||||
if(videoDash == null) return null;
|
||||
|
||||
//TODO: Temporary simple solution..make more reliable version
|
||||
|
||||
var result: String? = null;
|
||||
val audioAdaptationSet = adaptationSetRegex.find(audioDash!!);
|
||||
if(audioAdaptationSet != null) {
|
||||
result = videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||
}
|
||||
else
|
||||
result = videoDash;
|
||||
|
||||
return result;
|
||||
return videoDash;
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
-2
@@ -4,8 +4,6 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
|
||||
+14
-6
@@ -2,23 +2,20 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.orNull
|
||||
|
||||
class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||
override val container : String get() = "application/vnd.apple.mpegurl";
|
||||
override var container : String = "application/vnd.apple.mpegurl";
|
||||
override val codec: String = "HLS";
|
||||
override val name : String;
|
||||
override val bitrate : Int = 0;
|
||||
override var bitrate : Int = 0;
|
||||
override val url : String;
|
||||
override val duration: Long;
|
||||
override val language: String;
|
||||
override var language: String;
|
||||
|
||||
override var priority: Boolean = false;
|
||||
|
||||
@@ -34,6 +31,17 @@ class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
|
||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||
}
|
||||
|
||||
fun setPreferredBitrate(bitrate: Int) {
|
||||
this@JSHLSManifestAudioSource.bitrate = bitrate;
|
||||
}
|
||||
|
||||
fun setPreferredLanguage(language: String) {
|
||||
this@JSHLSManifestAudioSource.language = language;
|
||||
}
|
||||
|
||||
fun setPreferredContainer(container: String) {
|
||||
this@JSHLSManifestAudioSource.container = container;
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromV8HLSNullable(plugin: JSClient, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(plugin, it as V8ValueObject) };
|
||||
|
||||
+21
-6
@@ -2,22 +2,21 @@ package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.others.Language
|
||||
|
||||
class JSHLSManifestSource : IHLSManifestSource, JSSource {
|
||||
override val width : Int = 0;
|
||||
override val height : Int = 0;
|
||||
override var width : Int = 0;
|
||||
override var height : Int = 0;
|
||||
override val container : String get() = "application/vnd.apple.mpegurl";
|
||||
override val codec: String = "HLS";
|
||||
override val name : String;
|
||||
override val bitrate : Int? = null;
|
||||
override var bitrate : Int = 0;
|
||||
override val url : String;
|
||||
override val duration: Long;
|
||||
override var language: String = Language.UNKNOWN
|
||||
|
||||
override var priority: Boolean = false;
|
||||
|
||||
@@ -31,4 +30,20 @@ class JSHLSManifestSource : IHLSManifestSource, JSSource {
|
||||
|
||||
priority = obj.getOrNull(config, "priority", contextName) ?: false;
|
||||
}
|
||||
|
||||
fun setPreferredWidth(width: Int) {
|
||||
this@JSHLSManifestSource.width = width
|
||||
}
|
||||
|
||||
fun setPreferredHeight(height: Int) {
|
||||
this@JSHLSManifestSource.height = height
|
||||
}
|
||||
|
||||
fun setPreferredBitrate(bitrate: Int) {
|
||||
this@JSHLSManifestSource.bitrate = bitrate;
|
||||
}
|
||||
|
||||
fun setPreferredLanguage(language: String) {
|
||||
this@JSHLSManifestSource.language = language;
|
||||
}
|
||||
}
|
||||
+6
-5
@@ -1,7 +1,5 @@
|
||||
package com.futo.platformplayer.api.media.platforms.js.models.sources
|
||||
|
||||
import androidx.media3.datasource.DefaultHttpDataSource
|
||||
import androidx.media3.datasource.HttpDataSource
|
||||
import com.caoccao.javet.values.V8Value
|
||||
import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.modifier.AdhocRequestModifier
|
||||
@@ -17,9 +15,12 @@ import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.orNull
|
||||
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||
|
||||
abstract class JSSource {
|
||||
interface IUnderlyingObject {
|
||||
fun getUnderlyingObject(): V8ValueObject?
|
||||
}
|
||||
|
||||
abstract class JSSource : IUnderlyingObject {
|
||||
protected val _plugin: JSClient;
|
||||
protected val _config: IV8PluginConfig;
|
||||
protected val _obj: V8ValueObject;
|
||||
@@ -88,7 +89,7 @@ abstract class JSSource {
|
||||
fun getUnderlyingPlugin(): JSClient? {
|
||||
return _plugin;
|
||||
}
|
||||
fun getUnderlyingObject(): V8ValueObject? {
|
||||
override fun getUnderlyingObject(): V8ValueObject? {
|
||||
return _obj;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ class StateCasting {
|
||||
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
||||
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
||||
|
||||
private val _castServer = ManagedHttpServer();
|
||||
private val _castServer = ManagedHttpServer(9999);
|
||||
private var _started = false;
|
||||
|
||||
var devices: HashMap<String, CastingDevice> = hashMapOf();
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
package com.futo.platformplayer.dialogs
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.PendingIntent.*
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.os.Bundle
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.receivers.InstallReceiver
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.constructs.TaskHandler
|
||||
import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StateUpdate
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
class ChangelogDialog(context: Context?, val changelogs: Map<Int, String>? = null) : AlertDialog(context) {
|
||||
class ChangelogDialog(context: Context?) : AlertDialog(context) {
|
||||
companion object {
|
||||
private val TAG = "ChangelogDialog";
|
||||
}
|
||||
@@ -35,11 +48,7 @@ class ChangelogDialog(context: Context?, val changelogs: Map<Int, String>? = nul
|
||||
private var _maxVersion: Int = 0;
|
||||
private var _managedHttpClient = ManagedHttpClient();
|
||||
|
||||
private val _taskDownloadChangelog = TaskHandler<Int, String?>(StateApp.instance.scopeGetter, { version -> if(changelogs == null)
|
||||
StateUpdate.instance.downloadChangelog(_managedHttpClient, version)
|
||||
else
|
||||
changelogs[version]
|
||||
})
|
||||
private val _taskDownloadChangelog = TaskHandler<Int, String?>(StateApp.instance.scopeGetter, { version -> StateUpdate.instance.downloadChangelog(_managedHttpClient, version) })
|
||||
.success { setChangelog(it); }
|
||||
.exception<Throwable> {
|
||||
Logger.w(TAG, "Failed to load changelog.", it);
|
||||
@@ -88,7 +97,7 @@ class ChangelogDialog(context: Context?, val changelogs: Map<Int, String>? = nul
|
||||
setVersion(version);
|
||||
|
||||
val currentVersion = BuildConfig.VERSION_CODE;
|
||||
_buttonUpdate.visibility = if (currentVersion == _maxVersion || changelogs != null) View.GONE else View.VISIBLE;
|
||||
_buttonUpdate.visibility = if (currentVersion == _maxVersion) View.GONE else View.VISIBLE;
|
||||
}
|
||||
|
||||
private fun setVersion(version: Int) {
|
||||
|
||||
@@ -54,7 +54,6 @@ class PluginUpdateDialog : AlertDialog {
|
||||
private lateinit var _buttonInstall: LinearLayout;
|
||||
|
||||
private lateinit var _textPlugin: TextView;
|
||||
private lateinit var _textChangelog: TextView;
|
||||
private lateinit var _textProgres: TextView;
|
||||
private lateinit var _textError: TextView;
|
||||
private lateinit var _textResult: TextView;
|
||||
@@ -95,7 +94,6 @@ class PluginUpdateDialog : AlertDialog {
|
||||
_buttonInstall = findViewById(R.id.button_install);
|
||||
|
||||
_textPlugin = findViewById(R.id.text_plugin);
|
||||
_textChangelog = findViewById(R.id.text_changelog);
|
||||
_textProgres = findViewById(R.id.text_progress);
|
||||
_textError = findViewById(R.id.text_error);
|
||||
_textResult = findViewById(R.id.text_result);
|
||||
@@ -112,27 +110,6 @@ class PluginUpdateDialog : AlertDialog {
|
||||
_updateSpinner = findViewById(R.id.update_spinner);
|
||||
_iconPlugin = findViewById(R.id.icon_plugin);
|
||||
|
||||
try {
|
||||
var changelogVersion = _newConfig.version.toString();
|
||||
if (_newConfig.changelog != null && _newConfig.changelog?.containsKey(changelogVersion) == true) {
|
||||
_textChangelog.movementMethod = ScrollingMovementMethod();
|
||||
val changelog = _newConfig.changelog!![changelogVersion]!!;
|
||||
if(changelog.size > 1) {
|
||||
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog.map { " - " + it.trim() }.joinToString("\n");
|
||||
}
|
||||
else if(changelog.size == 1) {
|
||||
_textChangelog.text = "Changelog (${_newConfig.version})\n" + changelog[0].trim();
|
||||
}
|
||||
else
|
||||
_textChangelog.visibility = View.GONE;
|
||||
} else
|
||||
_textChangelog.visibility = View.GONE;
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
_textChangelog.visibility = View.GONE;
|
||||
Logger.e(TAG, "Invalid changelog? ", ex);
|
||||
}
|
||||
|
||||
_buttonCancel1.setOnClickListener {
|
||||
dismiss();
|
||||
};
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package com.futo.platformplayer.downloads
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
|
||||
import com.arthenica.ffmpegkit.FFmpegKit
|
||||
import com.arthenica.ffmpegkit.ReturnCode
|
||||
import com.arthenica.ffmpegkit.StatisticsCallback
|
||||
@@ -10,6 +15,8 @@ import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.AudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.DashManifestAudioSourceDelegate
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.DashManifestSourceDelegate
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
@@ -28,25 +35,27 @@ import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.SourcePluginConfig
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSVideo
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.IJSDashManifestRawSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.IUnderlyingObject
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.exceptions.DownloadException
|
||||
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
|
||||
import com.futo.platformplayer.helpers.VideoHelper
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.others.Language
|
||||
import com.futo.platformplayer.parsers.HLS
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.states.StatePlugins
|
||||
import com.futo.platformplayer.toHumanBitrate
|
||||
import com.futo.platformplayer.toHumanBytesSpeed
|
||||
import com.futo.polycentric.core.hexStringToByteArray
|
||||
import hasAnySource
|
||||
import isDownloadable
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -59,6 +68,8 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Transient
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
@@ -69,8 +80,10 @@ import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.ForkJoinTask
|
||||
import java.util.concurrent.ThreadLocalRandom
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.time.times
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
class VideoDownload {
|
||||
@@ -119,18 +132,21 @@ class VideoDownload {
|
||||
var requiresLiveVideoSource: Boolean = false;
|
||||
@Contextual
|
||||
@kotlinx.serialization.Transient
|
||||
var videoSourceLive: JSSource? = null;
|
||||
var videoSourceLive: IUnderlyingObject? = null;
|
||||
val isLiveVideoSourceValid get() = videoSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
|
||||
|
||||
var requiresLiveAudioSource: Boolean = false;
|
||||
@Contextual
|
||||
@kotlinx.serialization.Transient
|
||||
var audioSourceLive: JSSource? = null;
|
||||
var audioSourceLive: IUnderlyingObject? = null;
|
||||
val isLiveAudioSourceValid get() = audioSourceLive?.getUnderlyingObject()?.isClosed?.let { !it } ?: false;
|
||||
|
||||
var hasVideoRequestExecutor: Boolean = false;
|
||||
var hasAudioRequestExecutor: Boolean = false;
|
||||
|
||||
private var hasVideoRequestModifier: Boolean = false
|
||||
private var hasAudioRequestModifier: Boolean = false
|
||||
|
||||
var progress: Double = 0.0;
|
||||
var isCancelled = false;
|
||||
|
||||
@@ -141,17 +157,11 @@ class VideoDownload {
|
||||
var error: String? = null;
|
||||
|
||||
var videoFilePath: String? = null;
|
||||
var videoFileNameBase: String? = null;
|
||||
var videoFileNameExt: String? = null;
|
||||
val videoFileName: String? get() = if(videoFileNameBase.isNullOrEmpty()) null else videoFileNameBase + (if(!videoFileNameExt.isNullOrEmpty()) "." + videoFileNameExt else "");
|
||||
var videoOverrideContainer: String? = null;
|
||||
var videoFileName: String? = null;
|
||||
var videoFileSize: Long? = null;
|
||||
|
||||
var audioFilePath: String? = null;
|
||||
var audioFileNameBase: String? = null;
|
||||
var audioFileNameExt: String? = null;
|
||||
val audioFileName: String? get() = if(audioFileNameBase.isNullOrEmpty()) null else audioFileNameBase + (if(!audioFileNameExt.isNullOrEmpty()) "." + audioFileNameExt else "");
|
||||
var audioOverrideContainer: String? = null;
|
||||
var audioFileName: String? = null;
|
||||
var audioFileSize: Long? = null;
|
||||
|
||||
var subtitleFilePath: String? = null;
|
||||
@@ -197,8 +207,10 @@ class VideoDownload {
|
||||
this.prepareTime = OffsetDateTime.now();
|
||||
this.hasVideoRequestExecutor = videoSource is JSSource && videoSource.hasRequestExecutor;
|
||||
this.hasAudioRequestExecutor = audioSource is JSSource && audioSource.hasRequestExecutor;
|
||||
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (videoSource is JSDashManifestRawSource && videoSource.hasGenerate);
|
||||
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (audioSource is JSDashManifestRawAudioSource && audioSource.hasGenerate);
|
||||
this.hasVideoRequestModifier = videoSource is JSSource && videoSource.hasRequestModifier
|
||||
this.hasAudioRequestModifier = audioSource is JSSource && audioSource.hasRequestModifier
|
||||
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || this.hasVideoRequestModifier || videoSource !is IVideoUrlSource
|
||||
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || this.hasAudioRequestModifier || audioSource !is IAudioUrlSource
|
||||
this.targetVideoName = videoSource?.name;
|
||||
this.targetAudioName = audioSource?.name;
|
||||
this.targetPixelCount = if(videoSource != null) (videoSource.width * videoSource.height).toLong() else null;
|
||||
@@ -233,6 +245,7 @@ class VideoDownload {
|
||||
return items.joinToString(" • ");
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
suspend fun prepare(client: ManagedHttpClient) {
|
||||
Logger.i(TAG, "VideoDownload Prepare [${name}]");
|
||||
|
||||
@@ -241,13 +254,11 @@ class VideoDownload {
|
||||
videoDetails = null;
|
||||
videoSource = null;
|
||||
videoSourceLive = null;
|
||||
videoOverrideContainer = null;
|
||||
}
|
||||
if(requiresLiveAudioSource && !isLiveAudioSourceValid) {
|
||||
videoDetails = null;
|
||||
audioSource = null;
|
||||
videoSourceLive = null;
|
||||
audioOverrideContainer = null;
|
||||
}
|
||||
if(video == null && videoDetails == null)
|
||||
throw IllegalStateException("Missing information for download to complete");
|
||||
@@ -290,21 +301,57 @@ class VideoDownload {
|
||||
}
|
||||
|
||||
videoDetails = SerializedPlatformVideoDetails.fromVideo(original, if (subtitleSource != null) listOf(subtitleSource!!) else listOf());
|
||||
if(videoSource == null && targetPixelCount != null) {
|
||||
if (videoSource == null && targetPixelCount != null) {
|
||||
val videoSources = arrayListOf<IVideoSource>()
|
||||
for (source in original.video.videoSources) {
|
||||
if (source is IHLSManifestSource) {
|
||||
try {
|
||||
val playlistResponse = client.get(source.url)
|
||||
val playlistResponse = if ((source as JSSource).hasRequestModifier) {
|
||||
val request =
|
||||
source.getRequestModifier()!!.modifyRequest(source.url, mapOf())
|
||||
client.get(request.url!!, request.headers.toMutableMap())
|
||||
} else {
|
||||
client.get(source.url)
|
||||
}
|
||||
if (playlistResponse.isOk) {
|
||||
val playlistContent = playlistResponse.body?.string()
|
||||
if (playlistContent != null) {
|
||||
videoSources.addAll(HLS.parseAndGetVideoSources(source, playlistContent, source.url))
|
||||
val variantSources =
|
||||
HLS.parseAndGetVideoSources(source, playlistContent, source.url)
|
||||
val target =
|
||||
VideoHelper.selectBestVideoSource(variantSources, targetPixelCount!!.toInt(), arrayOf())
|
||||
if (target != null) {
|
||||
(source as JSHLSManifestSource).setPreferredWidth(target.width)
|
||||
source.setPreferredHeight(target.height)
|
||||
videoSources.add(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.i(TAG, "Failed to get HLS video sources", e)
|
||||
}
|
||||
} else if (source is JSDashManifestSource) {
|
||||
val masterPlaylistResponse = ManagedHttpClient().get(source.url)
|
||||
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||
|
||||
val hlsManifestUrl = masterPlaylistResponse.url
|
||||
|
||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||
?: throw Exception("Master playlist content is empty")
|
||||
|
||||
val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray())
|
||||
|
||||
val playlist =
|
||||
DashManifestParser().parse(Uri.parse(hlsManifestUrl), inputStream)
|
||||
|
||||
val period = playlist.getPeriod(0)
|
||||
|
||||
val representation =
|
||||
period.adaptationSets.filter { it.type == C.TRACK_TYPE_VIDEO }
|
||||
.flatMap { it.representations }.filter {
|
||||
(it.format.width * it.format.height).toLong() == targetPixelCount
|
||||
}[0]
|
||||
videoSources.add(DashManifestSourceDelegate(source, representation.format.width, representation.format.height, representation.format.containerMimeType!!))
|
||||
} else {
|
||||
videoSources.add(source)
|
||||
}
|
||||
@@ -318,10 +365,6 @@ class VideoDownload {
|
||||
if(vsource == null)
|
||||
vsource = VideoHelper.selectBestVideoSource(videoSources, targetPixelCount!!.toInt(), arrayOf())
|
||||
// ?: throw IllegalStateException("Could not find a valid video source for video");
|
||||
if(vsource is JSSource) {
|
||||
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || vsource.hasRequestExecutor;
|
||||
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (vsource is JSDashManifestRawSource && vsource.hasGenerate);
|
||||
}
|
||||
|
||||
if(vsource == null) {
|
||||
videoSource = null;
|
||||
@@ -332,22 +375,40 @@ class VideoDownload {
|
||||
videoSource = VideoUrlSource.fromUrlSource(vsource)
|
||||
else if(vsource is JSSource && requiresLiveVideoSource)
|
||||
videoSourceLive = vsource;
|
||||
else if (vsource is DashManifestSourceDelegate)
|
||||
videoSourceLive = vsource
|
||||
else
|
||||
throw DownloadException("Video source is not supported for downloading (yet) [" + vsource?.javaClass?.name + "]", false);
|
||||
}
|
||||
|
||||
if(audioSource == null && targetBitrate != null) {
|
||||
if (audioSource == null && targetBitrate != null) {
|
||||
var audioSources = mutableListOf<IAudioSource>()
|
||||
val video = original.video
|
||||
if (video is VideoUnMuxedSourceDescriptor) {
|
||||
for (source in video.audioSources) {
|
||||
if (source is IHLSManifestAudioSource) {
|
||||
try {
|
||||
val playlistResponse = client.get(source.url)
|
||||
val playlistResponse =
|
||||
if ((source as JSSource).hasRequestModifier) {
|
||||
val request = source.getRequestModifier()!!
|
||||
.modifyRequest(source.url, mapOf())
|
||||
client.get(request.url!!, request.headers.toMutableMap())
|
||||
} else {
|
||||
client.get(source.url)
|
||||
}
|
||||
if (playlistResponse.isOk) {
|
||||
val playlistContent = playlistResponse.body?.string()
|
||||
if (playlistContent != null) {
|
||||
audioSources.addAll(HLS.parseAndGetAudioSources(source, playlistContent, source.url))
|
||||
val variantSources =
|
||||
HLS.parseAndGetAudioSources(source, playlistContent, source.url, true)
|
||||
val target =
|
||||
VideoHelper.selectBestAudioSource(variantSources, arrayOf(), null, targetBitrate)
|
||||
if (target != null) {
|
||||
(source as JSHLSManifestAudioSource).setPreferredBitrate(target.bitrate)
|
||||
source.setPreferredLanguage(target.language)
|
||||
source.setPreferredContainer(target.container)
|
||||
audioSources.add(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
@@ -358,6 +419,62 @@ class VideoDownload {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (source in video.videoSources) {
|
||||
if (source is IHLSManifestSource) {
|
||||
try {
|
||||
val playlistResponse = if ((source as JSSource).hasRequestModifier) {
|
||||
val request =
|
||||
source.getRequestModifier()!!.modifyRequest(source.url, mapOf())
|
||||
client.get(request.url!!, request.headers.toMutableMap())
|
||||
} else {
|
||||
client.get(source.url)
|
||||
}
|
||||
if (playlistResponse.isOk) {
|
||||
val playlistContent = playlistResponse.body?.string()
|
||||
if (playlistContent != null) {
|
||||
val variantSources =
|
||||
HLS.parseAndGetAudioSources(source, playlistContent, source.url, true)
|
||||
val target =
|
||||
VideoHelper.selectBestAudioSource(variantSources, arrayOf(), null, targetBitrate)
|
||||
if (target != null) {
|
||||
(source as JSHLSManifestSource).setPreferredBitrate(target.bitrate)
|
||||
source.setPreferredLanguage(target.language)
|
||||
audioSources.add(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.i(TAG, "Failed to get HLS audio sources", e)
|
||||
}
|
||||
} else if (source is JSDashManifestSource) {
|
||||
val masterPlaylistResponse = ManagedHttpClient().get(source.url)
|
||||
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||
|
||||
val hlsManifestUrl = masterPlaylistResponse.url
|
||||
|
||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||
?: throw Exception("Master playlist content is empty")
|
||||
|
||||
val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray())
|
||||
|
||||
val playlist =
|
||||
DashManifestParser().parse(Uri.parse(hlsManifestUrl), inputStream)
|
||||
|
||||
val period = playlist.getPeriod(0)
|
||||
|
||||
val representation =
|
||||
period.adaptationSets.filter { it.type == C.TRACK_TYPE_AUDIO }
|
||||
.flatMap { it.representations }.filter {
|
||||
it.format.bitrate.toLong() == targetBitrate
|
||||
}[0]
|
||||
audioSources.add(
|
||||
DashManifestAudioSourceDelegate(
|
||||
source, representation.format.language
|
||||
?: Language.UNKNOWN, representation.format.bitrate, representation.format.containerMimeType!!
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var asource: IAudioSource? = null;
|
||||
if(targetAudioName != null) {
|
||||
@@ -373,12 +490,6 @@ class VideoDownload {
|
||||
asource = VideoHelper.selectBestAudioSource(audioSources, arrayOf(), null, targetBitrate)
|
||||
?: if(videoSource != null ) null
|
||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||
|
||||
if(asource is JSSource) {
|
||||
this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor;
|
||||
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
||||
}
|
||||
|
||||
if(asource == null) {
|
||||
audioSource = null;
|
||||
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
||||
@@ -388,6 +499,8 @@ class VideoDownload {
|
||||
audioSource = AudioUrlSource.fromUrlSource(asource)
|
||||
else if(asource is JSSource && requiresLiveAudioSource)
|
||||
audioSourceLive = asource;
|
||||
else if (asource is DashManifestAudioSourceDelegate)
|
||||
audioSourceLive = asource
|
||||
else
|
||||
throw DownloadException("Audio source is not supported for downloading (yet) [" + asource?.javaClass?.name + "]", false);
|
||||
}
|
||||
@@ -418,13 +531,11 @@ class VideoDownload {
|
||||
else audioSource;
|
||||
|
||||
if(actualVideoSource != null) {
|
||||
videoFileNameBase = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}]".sanitizeFileName();
|
||||
videoFileNameExt = videoContainerToExtension(actualVideoSource!!.container);
|
||||
videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName();
|
||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||
}
|
||||
if(actualAudioSource != null) {
|
||||
audioFileNameBase = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}]".sanitizeFileName();
|
||||
audioFileNameExt = audioContainerToExtension(actualAudioSource!!.container);
|
||||
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName();
|
||||
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||
}
|
||||
if(subtitleSource != null) {
|
||||
@@ -468,16 +579,23 @@ class VideoDownload {
|
||||
}
|
||||
}
|
||||
|
||||
if(actualVideoSource is IVideoUrlSource)
|
||||
videoFileSize = when (videoSource!!.container) {
|
||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||
else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||
videoFileSize = when (actualVideoSource) {
|
||||
is IVideoUrlSource -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||
is JSDashManifestRawSource -> {
|
||||
downloadDashFileSource("Video", client, actualVideoSource, File(downloadDir, videoFileName!!), progressCallback)
|
||||
}
|
||||
else if(actualVideoSource is JSDashManifestRawSource) {
|
||||
videoFileSize = downloadDashFileSource("Video", client, actualVideoSource, File(downloadDir, videoFileName!!), progressCallback);
|
||||
|
||||
is JSHLSManifestSource -> {
|
||||
downloadHlsSource(context, "Video", client, actualVideoSource, false, actualVideoSource.url, File(downloadDir, videoFileName!!), progressCallback)
|
||||
}
|
||||
|
||||
is DashManifestSourceDelegate -> {
|
||||
downloadDashSource(context, "Video", client, actualVideoSource.source, actualVideoSource.url, File(downloadDir, videoFileName!!), progressCallback)
|
||||
}
|
||||
|
||||
else -> throw NotImplementedError("NotImplemented video download: " + actualVideoSource.javaClass.name)
|
||||
}
|
||||
else throw NotImplementedError("NotImplemented video download: " + actualVideoSource.javaClass.name);
|
||||
});
|
||||
})
|
||||
}
|
||||
if(actualAudioSource != null) {
|
||||
sourcesToDownload.add(async {
|
||||
@@ -508,16 +626,27 @@ class VideoDownload {
|
||||
}
|
||||
}
|
||||
|
||||
if(actualAudioSource is IAudioUrlSource)
|
||||
audioFileSize = when (audioSource!!.container) {
|
||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||
else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||
audioFileSize = when (actualAudioSource) {
|
||||
is IVideoUrlSource -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||
is JSDashManifestRawAudioSource -> {
|
||||
downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback)
|
||||
}
|
||||
else if(actualAudioSource is JSDashManifestRawAudioSource) {
|
||||
audioFileSize = downloadDashFileSource("Audio", client, actualAudioSource, File(downloadDir, audioFileName!!), progressCallback);
|
||||
|
||||
is JSHLSManifestAudioSource -> {
|
||||
downloadHlsSource(context, "Audio", client, actualAudioSource, false, actualAudioSource.url, File(downloadDir, audioFileName!!), progressCallback)
|
||||
}
|
||||
|
||||
is JSHLSManifestSource -> {
|
||||
downloadHlsSource(context, "Audio", client, actualAudioSource, true, actualAudioSource.url, File(downloadDir, audioFileName!!), progressCallback)
|
||||
}
|
||||
|
||||
is DashManifestAudioSourceDelegate -> {
|
||||
downloadDashSource(context, "Audio", client, actualAudioSource.source, actualAudioSource.url, File(downloadDir, audioFileName!!), progressCallback)
|
||||
}
|
||||
|
||||
else -> throw NotImplementedError("NotImplemented audio download: " + actualAudioSource.javaClass.name)
|
||||
}
|
||||
else throw NotImplementedError("NotImplemented audio download: " + actualAudioSource.javaClass.name);
|
||||
});
|
||||
})
|
||||
}
|
||||
if (subtitleSource != null) {
|
||||
sourcesToDownload.add(async {
|
||||
@@ -564,7 +693,108 @@ class VideoDownload {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
@OptIn(UnstableApi::class)
|
||||
private suspend fun downloadDashSource(context: Context, name: String, client: ManagedHttpClient, source: JSSource?, hlsManifestUrl2: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
if (targetFile.exists()) targetFile.delete()
|
||||
|
||||
var downloadedTotalLength = 0L
|
||||
|
||||
val segmentFiles = arrayListOf<File>()
|
||||
try {
|
||||
val manifestResponse = ManagedHttpClient().get(hlsManifestUrl2)
|
||||
check(manifestResponse.isOk) { "Failed to get DASH manifest: ${manifestResponse.code}" }
|
||||
|
||||
val resolvedUrl = manifestResponse.url
|
||||
|
||||
val manifestContent = manifestResponse.body?.string()
|
||||
?: throw Exception("Manifest content is empty")
|
||||
|
||||
val inputStream = ByteArrayInputStream(manifestContent.toByteArray())
|
||||
val playlist = DashManifestParser().parse(Uri.parse(resolvedUrl), inputStream)
|
||||
|
||||
val period = playlist.getPeriod(0)
|
||||
|
||||
val representation = when (name) {
|
||||
"Audio" -> {
|
||||
period.adaptationSets.filter { it.type == C.TRACK_TYPE_AUDIO }
|
||||
.flatMap { it.representations }.filter {
|
||||
it.format.bitrate.toLong() == targetBitrate
|
||||
}[0]
|
||||
}
|
||||
"Video" -> {
|
||||
period.adaptationSets.filter { it.type == C.TRACK_TYPE_VIDEO }
|
||||
.flatMap { it.representations }.filter {
|
||||
(it.format.width * it.format.height).toLong() == targetPixelCount
|
||||
}[0]
|
||||
}
|
||||
else -> {
|
||||
throw Exception("Unknown type")
|
||||
}
|
||||
}
|
||||
|
||||
val segmentIndex = representation.index
|
||||
|
||||
if (segmentIndex != null) {
|
||||
val baseUrl = representation.baseUrls[0]
|
||||
val count = segmentIndex.getSegmentCount(C.TIME_UNSET)
|
||||
for (index in 0 until count) {
|
||||
val segmentUrl = if (index != 0L) segmentIndex.getSegmentUrl(index)
|
||||
.resolveUriString(baseUrl.url)
|
||||
else {
|
||||
val init = representation.initializationUri ?: continue
|
||||
init.resolveUriString(baseUrl.url)
|
||||
}
|
||||
Logger.i(TAG, "Download '$name' segment $index Sequential")
|
||||
val segmentFile = File(context.cacheDir, "segment-${UUID.randomUUID()}")
|
||||
val outputStream = segmentFile.outputStream()
|
||||
outputStream.use { os ->
|
||||
segmentFiles.add(segmentFile)
|
||||
|
||||
val segmentLength =
|
||||
downloadSource_Sequential(client, os, segmentUrl, null) { segmentLength, totalRead, lastSpeed ->
|
||||
val averageSegmentLength =
|
||||
if (index == 0L) segmentLength else downloadedTotalLength / index
|
||||
val expectedTotalLength =
|
||||
averageSegmentLength * (count - 1) + segmentLength
|
||||
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
||||
}
|
||||
|
||||
downloadedTotalLength += segmentLength
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println("No segment index available for representation: ${representation.format.id}")
|
||||
}
|
||||
|
||||
Logger.i(TAG, "Combining segments into $targetFile")
|
||||
combineSegments(context, segmentFiles, targetFile)
|
||||
|
||||
Logger.i(TAG, "$name downloadSource Finished")
|
||||
} catch (ioex: IOException) {
|
||||
if (targetFile.exists()) targetFile.delete()
|
||||
if (ioex.message?.contains("ENOSPC") == true
|
||||
) throw Exception("Not enough space on device", ioex)
|
||||
else throw ioex
|
||||
} catch (ex: Throwable) {
|
||||
if (targetFile.exists()) targetFile.delete()
|
||||
throw ex
|
||||
} finally {
|
||||
for (segmentFile in segmentFiles) {
|
||||
segmentFile.delete()
|
||||
}
|
||||
}
|
||||
return downloadedTotalLength
|
||||
}
|
||||
|
||||
private fun decryptSegment(encryptedSegment: ByteArray, key: ByteArray, iv: ByteArray): ByteArray {
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
val secretKey = SecretKeySpec(key, "AES")
|
||||
val ivSpec = IvParameterSpec(iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec)
|
||||
return cipher.doFinal(encryptedSegment)
|
||||
}
|
||||
|
||||
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, source: JSSource?, audio: Boolean, hlsManifestUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
if(targetFile.exists())
|
||||
targetFile.delete();
|
||||
|
||||
@@ -572,13 +802,68 @@ class VideoDownload {
|
||||
|
||||
val segmentFiles = arrayListOf<File>()
|
||||
try {
|
||||
val response = client.get(hlsUrl)
|
||||
val masterPlaylistResponse = ManagedHttpClient().get(hlsManifestUrl)
|
||||
check(masterPlaylistResponse.isOk) { "Failed to get master playlist: ${masterPlaylistResponse.code}" }
|
||||
|
||||
val resolvedSourceUrl = masterPlaylistResponse.url
|
||||
|
||||
val masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||
?: throw Exception("Master playlist content is empty")
|
||||
|
||||
val variantUrl = if (source is JSHLSManifestAudioSource){
|
||||
val audioTracks = HLS.parseAndGetAudioSources(source, masterPlaylistContent, resolvedSourceUrl, true)
|
||||
|
||||
val variant = VideoHelper.selectBestAudioSource(audioTracks, arrayOf(), source.language, targetBitrate)
|
||||
if (variant !is IAudioUrlSource){
|
||||
throw Exception("Variant is not an audio source")
|
||||
}
|
||||
variant.getAudioUrl()
|
||||
}else if (audio && source is JSHLSManifestSource){
|
||||
val audioTracks = HLS.parseAndGetAudioSources(source, masterPlaylistContent, resolvedSourceUrl, true)
|
||||
|
||||
val variant = VideoHelper.selectBestAudioSource(audioTracks, arrayOf(), source.language, targetBitrate)
|
||||
if (variant !is IAudioUrlSource){
|
||||
throw Exception("Variant is not an audio source")
|
||||
}
|
||||
variant.getAudioUrl()
|
||||
}else if (source is JSHLSManifestSource) {
|
||||
val variants = HLS.parseAndGetVideoSources(source, masterPlaylistContent, resolvedSourceUrl)
|
||||
|
||||
val variant = VideoHelper.selectBestVideoSource(variants, targetPixelCount!!.toInt(), arrayOf())
|
||||
if (variant !is IVideoUrlSource){
|
||||
throw Exception("Variant is not a video source")
|
||||
}
|
||||
variant.getVideoUrl()
|
||||
} else {
|
||||
throw Exception("Source is not a HLS manifest")
|
||||
}
|
||||
|
||||
val response = if (source.hasRequestModifier) {
|
||||
val request = source.getRequestModifier()!!.modifyRequest(variantUrl, mapOf())
|
||||
client.get(request.url!!, request.headers.toMutableMap())
|
||||
} else {
|
||||
client.get(variantUrl)
|
||||
}
|
||||
check(response.isOk) { "Failed to get variant playlist: ${response.code}" }
|
||||
|
||||
val vpContent = response.body?.string()
|
||||
?: throw Exception("Variant playlist content is empty")
|
||||
|
||||
val variantPlaylist = HLS.parseVariantPlaylist(vpContent, hlsUrl)
|
||||
val variantPlaylist = HLS.parseVariantPlaylist(vpContent, variantUrl)
|
||||
val decryptionInfo: DecryptionInfo? = if (variantPlaylist.decryptionInfo != null) {
|
||||
val keyResponse = if (source.hasRequestModifier) {
|
||||
val request = source.getRequestModifier()!!.modifyRequest(variantPlaylist.decryptionInfo.keyUrl, mapOf())
|
||||
client.get(request.url!!, request.headers.toMutableMap())
|
||||
} else {
|
||||
client.get(variantPlaylist.decryptionInfo.keyUrl)
|
||||
}
|
||||
check(keyResponse.isOk) { "HLS request failed for decryption key: ${keyResponse.code}" }
|
||||
|
||||
DecryptionInfo(keyResponse.body!!.bytes(), variantPlaylist.decryptionInfo.iv.hexStringToByteArray())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
variantPlaylist.segments.forEachIndexed { index, segment ->
|
||||
if (segment !is HLS.MediaSegment) {
|
||||
return@forEachIndexed
|
||||
@@ -590,7 +875,7 @@ class VideoDownload {
|
||||
try {
|
||||
segmentFiles.add(segmentFile)
|
||||
|
||||
val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri) { segmentLength, totalRead, lastSpeed ->
|
||||
val segmentLength = downloadSource_Sequential(client, outputStream, segment.uri, if (index == 0) null else decryptionInfo) { segmentLength, totalRead, lastSpeed ->
|
||||
val averageSegmentLength = if (index == 0) segmentLength else downloadedTotalLength / index
|
||||
val expectedTotalLength = averageSegmentLength * (variantPlaylist.segments.size - 1) + segmentLength
|
||||
onProgress(expectedTotalLength, downloadedTotalLength + totalRead, lastSpeed)
|
||||
@@ -628,12 +913,11 @@ class VideoDownload {
|
||||
return downloadedTotalLength;
|
||||
}
|
||||
|
||||
private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val fileList = File(context.cacheDir, "fileList-${UUID.randomUUID()}.txt")
|
||||
fileList.writeText(segmentFiles.joinToString("\n") { "file '${it.absolutePath}'" })
|
||||
|
||||
val cmd = "-f concat -safe 0 -i \"${fileList.absolutePath}\" -c copy \"${targetFile.absolutePath}\""
|
||||
private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) =
|
||||
withContext(Dispatchers.IO) {
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val cmd =
|
||||
"-i \"concat:${segmentFiles.joinToString("|")}\" -c copy \"${targetFile.absolutePath}\""
|
||||
|
||||
val statisticsCallback = StatisticsCallback { _ ->
|
||||
//TODO: Show progress?
|
||||
@@ -643,7 +927,6 @@ class VideoDownload {
|
||||
val session = FFmpegKit.executeAsync(cmd,
|
||||
{ session ->
|
||||
if (ReturnCode.isSuccess(session.returnCode)) {
|
||||
fileList.delete()
|
||||
continuation.resumeWith(Result.success(Unit))
|
||||
} else {
|
||||
val errorMessage = if (ReturnCode.isCancel(session.returnCode)) {
|
||||
@@ -651,7 +934,6 @@ class VideoDownload {
|
||||
} else {
|
||||
"Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}"
|
||||
}
|
||||
fileList.delete()
|
||||
continuation.resumeWithException(RuntimeException(errorMessage))
|
||||
}
|
||||
},
|
||||
@@ -771,7 +1053,7 @@ class VideoDownload {
|
||||
else {
|
||||
Logger.i(TAG, "Download $name Sequential");
|
||||
try {
|
||||
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, onProgress);
|
||||
sourceLength = downloadSource_Sequential(client, fileStream, videoUrl, null, onProgress);
|
||||
} catch (e: Throwable) {
|
||||
Logger.w(TAG, "Failed to download sequentially (url = $videoUrl)")
|
||||
throw e
|
||||
@@ -798,7 +1080,31 @@ class VideoDownload {
|
||||
}
|
||||
return sourceLength!!;
|
||||
}
|
||||
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
// methods are auto generated
|
||||
data class DecryptionInfo(
|
||||
val key: ByteArray,
|
||||
val iv: ByteArray
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as DecryptionInfo
|
||||
|
||||
if (!key.contentEquals(other.key)) return false
|
||||
if (!iv.contentEquals(other.iv)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = key.contentHashCode()
|
||||
result = 31 * result + iv.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, decryptionInfo: DecryptionInfo?, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
val progressRate: Int = 4096 * 5;
|
||||
var lastProgressCount: Int = 0;
|
||||
val speedRate: Int = 4096 * 5;
|
||||
@@ -818,6 +1124,8 @@ class VideoDownload {
|
||||
val sourceLength = result.body.contentLength();
|
||||
val sourceStream = result.body.byteStream();
|
||||
|
||||
val segmentBuffer = ByteArrayOutputStream()
|
||||
|
||||
var totalRead: Long = 0;
|
||||
try {
|
||||
var read: Int;
|
||||
@@ -828,7 +1136,7 @@ class VideoDownload {
|
||||
if (read < 0)
|
||||
break;
|
||||
|
||||
fileStream.write(buffer, 0, read);
|
||||
segmentBuffer.write(buffer, 0, read);
|
||||
|
||||
totalRead += read;
|
||||
|
||||
@@ -854,6 +1162,13 @@ class VideoDownload {
|
||||
result.body.close()
|
||||
}
|
||||
|
||||
if(decryptionInfo != null){
|
||||
val decryptedData = decryptSegment(segmentBuffer.toByteArray(), decryptionInfo.key, decryptionInfo.iv)
|
||||
fileStream.write(decryptedData)
|
||||
}else {
|
||||
fileStream.write(segmentBuffer.toByteArray())
|
||||
}
|
||||
|
||||
onProgress(sourceLength, totalRead, 0);
|
||||
return sourceLength;
|
||||
}
|
||||
@@ -1045,7 +1360,7 @@ class VideoDownload {
|
||||
val expectedFile = File(videoFilePath!!);
|
||||
if(!expectedFile.exists())
|
||||
throw IllegalStateException("Video file missing after download");
|
||||
if (videoSource?.container != "application/vnd.apple.mpegurl") {
|
||||
if (videoSourceLive !is IHLSManifestSource && videoSourceLive !is IDashManifestSource) {
|
||||
if (expectedFile.length() != videoFileSize)
|
||||
throw IllegalStateException("Expected size [${videoFileSize}], but found ${expectedFile.length()}");
|
||||
}
|
||||
@@ -1056,7 +1371,7 @@ class VideoDownload {
|
||||
val expectedFile = File(audioFilePath!!);
|
||||
if(!expectedFile.exists())
|
||||
throw IllegalStateException("Audio file missing after download");
|
||||
if (audioSource?.container != "application/vnd.apple.mpegurl") {
|
||||
if (audioSourceLive !is IHLSManifestAudioSource && audioSourceLive !is IHLSManifestSource && audioSourceLive !is IDashManifestSource) {
|
||||
if (expectedFile.length() != audioFileSize)
|
||||
throw IllegalStateException("Expected size [${audioFileSize}], but found ${expectedFile.length()}");
|
||||
}
|
||||
@@ -1072,8 +1387,8 @@ class VideoDownload {
|
||||
fun complete() {
|
||||
Logger.i(TAG, "VideoDownload Complete [${name}]");
|
||||
val existing = StateDownloads.instance.getCachedVideo(id);
|
||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0, videoOverrideContainer) };
|
||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0, audioOverrideContainer) };
|
||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0) };
|
||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) };
|
||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||
|
||||
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||
@@ -1102,7 +1417,7 @@ class VideoDownload {
|
||||
StateDownloads.instance.updateCachedVideo(existing);
|
||||
}
|
||||
else {
|
||||
val newVideo = VideoLocal(videoDetails!!, OffsetDateTime.now());
|
||||
val newVideo = VideoLocal(videoDetails!!);
|
||||
if(localVideoSource != null)
|
||||
newVideo.videoSource.add(localVideoSource);
|
||||
if(localAudioSource != null)
|
||||
@@ -1141,7 +1456,7 @@ class VideoDownload {
|
||||
val REGEX_DASH_CUE = Regex("<S .*?t=\"([0-9]*?)\".*?d=\"([0-9]*?)\".*?\\/>", RegexOption.DOT_MATCHES_ALL);
|
||||
|
||||
fun videoContainerToExtension(container: String): String? {
|
||||
if (container.contains("video/mp4") || container == "application/vnd.apple.mpegurl")
|
||||
if (container.contains("video/mp4"))
|
||||
return "mp4";
|
||||
else if (container.contains("application/x-mpegURL"))
|
||||
return "m3u8";
|
||||
@@ -1153,23 +1468,28 @@ class VideoDownload {
|
||||
return "webm";
|
||||
else if (container.contains("video/x-matroska"))
|
||||
return "mkv";
|
||||
else if (container.contains("video/mp2t"))
|
||||
return "m2ts"
|
||||
else if (container == "application/vnd.apple.mpegurl")
|
||||
return "mp4"
|
||||
else
|
||||
return "video";//throw IllegalStateException("Unknown container: " + container)
|
||||
return "video";
|
||||
}
|
||||
|
||||
fun audioContainerToExtension(container: String): String {
|
||||
if (container.contains("audio/mp4"))
|
||||
return "mp4a";
|
||||
return "m4a";
|
||||
else if (container.contains("audio/mpeg"))
|
||||
return "mpga";
|
||||
return "mp3";
|
||||
// return "mpga";
|
||||
else if (container.contains("audio/mp3"))
|
||||
return "mp3";
|
||||
else if (container.contains("audio/webm"))
|
||||
return "webm";
|
||||
else if (container == "application/vnd.apple.mpegurl")
|
||||
return "mp4a";
|
||||
return "m4a"
|
||||
else if (container.contains("audio/webm"))
|
||||
return "webma";
|
||||
else
|
||||
return "audio";// throw IllegalStateException("Unknown container: " + container)
|
||||
return "audio";
|
||||
}
|
||||
|
||||
fun subtitleContainerToExtension(container: String?): String {
|
||||
|
||||
@@ -39,7 +39,7 @@ class VideoExport {
|
||||
this.subtitleSource = subtitleSource;
|
||||
}
|
||||
|
||||
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null, documentRoot: DocumentFile? = null): DocumentFile = coroutineScope {
|
||||
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
|
||||
val v = videoSource;
|
||||
val a = audioSource;
|
||||
val s = subtitleSource;
|
||||
@@ -50,7 +50,7 @@ class VideoExport {
|
||||
if (s != null) sourceCount++;
|
||||
|
||||
val outputFile: DocumentFile?;
|
||||
val downloadRoot = documentRoot ?: StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
||||
val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
||||
if (sourceCount > 1) {
|
||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container);
|
||||
val f = downloadRoot.createFile("video/mp4", outputFileName)
|
||||
@@ -69,7 +69,7 @@ class VideoExport {
|
||||
outputFile = f;
|
||||
} else if (v != null) {
|
||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container);
|
||||
val f = downloadRoot.createFile(v.container, outputFileName)
|
||||
val f = downloadRoot.createFile(if (v.container == "application/vnd.apple.mpegurl") "video/mp2t" else v.container, outputFileName)
|
||||
?: throw Exception("Failed to create file in external directory.");
|
||||
|
||||
Logger.i(TAG, "Copying video.");
|
||||
@@ -81,7 +81,7 @@ class VideoExport {
|
||||
outputFile = f;
|
||||
} else if (a != null) {
|
||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container);
|
||||
val f = downloadRoot.createFile(a.container, outputFileName)
|
||||
val f = downloadRoot.createFile(if (a.container == "application/vnd.apple.mpegurl") "audio/mp3" else a.container, outputFileName)
|
||||
?: throw Exception("Failed to create file in external directory.");
|
||||
|
||||
Logger.i(TAG, "Copying audio.");
|
||||
|
||||
@@ -23,7 +23,6 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
|
||||
import com.futo.platformplayer.stores.v2.IStoreItem
|
||||
import java.io.File
|
||||
import java.time.OffsetDateTime
|
||||
@@ -71,21 +70,14 @@ class VideoLocal: IPlatformVideoDetails, IStoreItem {
|
||||
|
||||
override val isLive: Boolean get() = videoSerialized.isLive;
|
||||
|
||||
override val isShort: Boolean get() = videoSerialized.isShort;
|
||||
|
||||
//TODO: Offline subtitles
|
||||
override val subtitles: List<ISubtitleSource> = listOf();
|
||||
|
||||
@kotlinx.serialization.Serializable(with = OffsetDateTimeNullableSerializer::class)
|
||||
var downloadDate: OffsetDateTime? = null;
|
||||
|
||||
constructor(video: SerializedPlatformVideoDetails, downloadDate: OffsetDateTime? = null) {
|
||||
constructor(video: SerializedPlatformVideoDetails) {
|
||||
this.videoSerialized = video;
|
||||
this.downloadDate = downloadDate;
|
||||
}
|
||||
constructor(video: IPlatformVideoDetails, subtitleSources: List<SubtitleRawSource>) {
|
||||
this.videoSerialized = SerializedPlatformVideoDetails.fromVideo(video, subtitleSources);
|
||||
downloadDate = OffsetDateTime.now();
|
||||
}
|
||||
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment>? = null;
|
||||
|
||||
@@ -32,7 +32,6 @@ import com.futo.platformplayer.engine.internal.V8Converter
|
||||
import com.futo.platformplayer.engine.packages.PackageBridge
|
||||
import com.futo.platformplayer.engine.packages.PackageDOMParser
|
||||
import com.futo.platformplayer.engine.packages.PackageHttp
|
||||
import com.futo.platformplayer.engine.packages.PackageJSDOM
|
||||
import com.futo.platformplayer.engine.packages.PackageUtilities
|
||||
import com.futo.platformplayer.engine.packages.V8Package
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
@@ -95,11 +94,7 @@ class V8Plugin {
|
||||
withDependency(PackageBridge(this, config));
|
||||
|
||||
for(pack in config.packages)
|
||||
withDependency(getPackage(pack)!!);
|
||||
for(pack in config.packagesOptional)
|
||||
getPackage(pack, true)?.let {
|
||||
withDependency(it);
|
||||
}
|
||||
withDependency(getPackage(pack));
|
||||
}
|
||||
|
||||
fun changeAllowDevSubmit(isAllowed: Boolean) {
|
||||
@@ -259,14 +254,13 @@ class V8Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPackage(packageName: String, allowNull: Boolean = false): V8Package? {
|
||||
private fun getPackage(packageName: String): V8Package {
|
||||
//TODO: Auto get all package types?
|
||||
return when(packageName) {
|
||||
"DOMParser" -> PackageDOMParser(this)
|
||||
"Http" -> PackageHttp(this, config)
|
||||
"Utilities" -> PackageUtilities(this, config)
|
||||
"JSDOM" -> PackageJSDOM(this, config)
|
||||
else -> if(allowNull) null else throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
||||
else -> throw ScriptCompilationException(config, "Unknown package [${packageName}] required for plugin ${config.name}");
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ interface IV8PluginConfig {
|
||||
val allowEval: Boolean;
|
||||
val allowUrls: List<String>;
|
||||
val packages: List<String>;
|
||||
val packagesOptional: List<String>;
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
@@ -14,20 +13,17 @@ class V8PluginConfig : IV8PluginConfig {
|
||||
override val allowEval: Boolean;
|
||||
override val allowUrls: List<String>;
|
||||
override val packages: List<String>;
|
||||
override val packagesOptional: List<String>;
|
||||
|
||||
constructor() {
|
||||
name = "Unknown";
|
||||
allowEval = false;
|
||||
allowUrls = listOf();
|
||||
packages = listOf();
|
||||
packagesOptional = listOf();
|
||||
}
|
||||
constructor(name: String, allowEval: Boolean, allowUrls: List<String>, packages: List<String> = listOf(), packagesOptional: List<String> = listOf()) {
|
||||
constructor(name: String, allowEval: Boolean, allowUrls: List<String>, packages: List<String> = listOf()) {
|
||||
this.name = name;
|
||||
this.allowEval = allowEval;
|
||||
this.allowUrls = allowUrls;
|
||||
this.packages = packages;
|
||||
this.packagesOptional = packagesOptional;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package com.futo.platformplayer.engine.packages
|
||||
|
||||
import android.media.MediaCodec
|
||||
import android.media.MediaCodecList
|
||||
import com.caoccao.javet.annotations.V8Function
|
||||
import com.caoccao.javet.annotations.V8Property
|
||||
import com.caoccao.javet.utils.JavetResourceUtils
|
||||
@@ -189,44 +187,7 @@ class PackageBridge : V8Package {
|
||||
return false;
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun getHardwareCodecs(): List<String>{
|
||||
return getSupportedHardwareMediaCodecs();
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PackageBridge";
|
||||
|
||||
private var _mediaCodecList: MutableList<String> = mutableListOf();
|
||||
private var _mediaCodecListHardware: MutableList<String> = mutableListOf();
|
||||
|
||||
fun getSupportedMediaCodecs(): List<String>{
|
||||
synchronized(_mediaCodecList) {
|
||||
if(_mediaCodecList.size <= 0)
|
||||
updateMediaCodecList();
|
||||
return _mediaCodecList;
|
||||
}
|
||||
}
|
||||
fun getSupportedHardwareMediaCodecs(): List<String>{
|
||||
synchronized(_mediaCodecList) {
|
||||
if(_mediaCodecList.size <= 0)
|
||||
updateMediaCodecList();
|
||||
return _mediaCodecListHardware;
|
||||
}
|
||||
}
|
||||
private fun updateMediaCodecList() {
|
||||
_mediaCodecList.clear();
|
||||
_mediaCodecListHardware.clear();
|
||||
for(codec in MediaCodecList(MediaCodecList.ALL_CODECS).codecInfos) {
|
||||
if(!codec.isEncoder) {
|
||||
_mediaCodecList.add(codec.canonicalName);
|
||||
if (codec.isHardwareAccelerated)
|
||||
_mediaCodecListHardware.add(codec.canonicalName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -21,13 +21,9 @@ import com.futo.platformplayer.engine.internal.IV8Convertable
|
||||
import com.futo.platformplayer.engine.internal.V8BindObject
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.net.SocketTimeoutException
|
||||
import java.util.concurrent.ForkJoinPool
|
||||
import java.util.concurrent.ForkJoinTask
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.streams.asSequence
|
||||
|
||||
class PackageHttp: V8Package {
|
||||
@@ -46,9 +42,6 @@ class PackageHttp: V8Package {
|
||||
override val name: String get() = "Http";
|
||||
override val variableName: String get() = "http";
|
||||
|
||||
private var _batchPoolLock: Any = Any();
|
||||
private var _batchPool: ForkJoinPool? = null;
|
||||
|
||||
|
||||
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
||||
_config = config;
|
||||
@@ -58,37 +51,6 @@ class PackageHttp: V8Package {
|
||||
_packageClientAuth = PackageHttpClient(this, _clientAuth);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Automatically adjusting threadpool dedicated per PackageHttp for batch requests.
|
||||
*/
|
||||
private fun <T, R> autoParallelPool(data: List<T>, parallelism: Int, handle: (T)->R): List<Pair<R?, Throwable?>> {
|
||||
synchronized(_batchPoolLock) {
|
||||
val threadsToUse = if (parallelism <= 0) data.size else Math.min(parallelism, data.size);
|
||||
if(_batchPool == null)
|
||||
_batchPool = ForkJoinPool(threadsToUse);
|
||||
var pool = _batchPool ?: return listOf();
|
||||
if(pool.poolSize < threadsToUse) { //Resize pool
|
||||
pool.shutdown();
|
||||
_batchPool = ForkJoinPool(threadsToUse);
|
||||
pool = _batchPool ?: return listOf();
|
||||
}
|
||||
|
||||
val resultTasks = mutableListOf<ForkJoinTask<Pair<R?, Throwable?>>>();
|
||||
for(item in data){
|
||||
resultTasks.add(pool.submit<Pair<R?, Throwable?>> {
|
||||
try {
|
||||
return@submit Pair<R?, Throwable?>(handle(item), null);
|
||||
}
|
||||
catch(ex: Throwable) {
|
||||
return@submit Pair<R?, Throwable?>(null, ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
return resultTasks.map { it.join() };
|
||||
}
|
||||
}
|
||||
|
||||
@V8Function
|
||||
fun newClient(withAuth: Boolean): PackageHttpClient {
|
||||
val httpClient = if(withAuth) _clientAuth.clone() else _client.clone();
|
||||
@@ -214,6 +176,8 @@ class PackageHttp: V8Package {
|
||||
obj.set("url", url);
|
||||
obj.set("code", code);
|
||||
if(body != null) {
|
||||
val buffer = runtime.createV8ValueArrayBuffer(body.size);
|
||||
buffer.fromBytes(body);
|
||||
obj.set("body", body);
|
||||
}
|
||||
obj.set("headers", headers);
|
||||
@@ -272,19 +236,16 @@ class PackageHttp: V8Package {
|
||||
//Finalizer
|
||||
@V8Function
|
||||
fun execute(): List<IBridgeHttpResponse?> {
|
||||
return _package.autoParallelPool(_reqs, -1) {
|
||||
return _reqs.parallelStream().map {
|
||||
if(it.second.method == "DUMMY")
|
||||
return@autoParallelPool null;
|
||||
return@map null;
|
||||
if(it.second.body != null)
|
||||
return@autoParallelPool it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
|
||||
return@map it.first.requestWithBody(it.second.method, it.second.url, it.second.body!!, it.second.headers, it.second.respType);
|
||||
else
|
||||
return@autoParallelPool it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
|
||||
}.map {
|
||||
if(it.second != null)
|
||||
throw it.second!!;
|
||||
else
|
||||
return@map it.first;
|
||||
}.toList();
|
||||
return@map it.first.request(it.second.method, it.second.url, it.second.headers, it.second.respType);
|
||||
}
|
||||
.asSequence()
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,8 +439,11 @@ class PackageHttp: V8Package {
|
||||
else {
|
||||
headers?.forEach { (header, values) ->
|
||||
val lowerCaseHeader = header.lowercase()
|
||||
if(lowerCaseHeader == "set-cookie" && !values.any { it.lowercase().contains("httponly") })
|
||||
result[lowerCaseHeader] = values;
|
||||
if(lowerCaseHeader == "set-cookie") {
|
||||
result[lowerCaseHeader] = values.filter{
|
||||
!it.lowercase().contains("httponly")
|
||||
};
|
||||
}
|
||||
else
|
||||
result[lowerCaseHeader] = values;
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.futo.platformplayer.engine.packages
|
||||
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
|
||||
|
||||
class PackageJSDOM : V8Package {
|
||||
@Transient
|
||||
private val _config: IV8PluginConfig;
|
||||
|
||||
override val name: String get() = "JSDOM";
|
||||
override val variableName: String get() = "packageJSDOM";
|
||||
|
||||
constructor(plugin: V8Plugin, config: IV8PluginConfig): super(plugin) {
|
||||
_config = config;
|
||||
plugin.withDependency(StateApp.instance.contextOrNull ?: return, "scripts/JSDOM.js");
|
||||
}
|
||||
|
||||
}
|
||||
+5
-8
@@ -43,7 +43,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.max
|
||||
|
||||
class ChannelContentsFragment(private val subType: String? = null) : Fragment(), IChannelTabFragment {
|
||||
class ChannelContentsFragment : Fragment(), IChannelTabFragment {
|
||||
private var _recyclerResults: RecyclerView? = null;
|
||||
private var _glmVideo: GridLayoutManager? = null;
|
||||
private var _loading = false;
|
||||
@@ -73,12 +73,9 @@ class ChannelContentsFragment(private val subType: String? = null) : Fragment(),
|
||||
if (lastPolycentricProfile != null)
|
||||
pager= StatePolycentric.instance.getChannelContent(lifecycleScope, lastPolycentricProfile);
|
||||
|
||||
if(pager == null) {
|
||||
if(subType != null)
|
||||
pager = StatePlatform.instance.getChannelContent(channel.url, subType);
|
||||
else
|
||||
pager = StatePlatform.instance.getChannelContent(channel.url);
|
||||
}
|
||||
if(pager == null)
|
||||
pager = StatePlatform.instance.getChannelContent(channel.url);
|
||||
|
||||
return pager;
|
||||
}
|
||||
|
||||
@@ -370,6 +367,6 @@ class ChannelContentsFragment(private val subType: String? = null) : Fragment(),
|
||||
|
||||
companion object {
|
||||
val TAG = "VideoListFragment";
|
||||
fun newInstance(subType: String? = null) = ChannelContentsFragment(subType).apply { }
|
||||
fun newInstance() = ChannelContentsFragment().apply { }
|
||||
}
|
||||
}
|
||||
+2
-14
@@ -25,7 +25,6 @@ import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.channels.SerializedChannel
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
@@ -458,12 +457,6 @@ class ChannelFragment : MainFragment() {
|
||||
|
||||
_fragment.topBar?.assume<NavigationTopBarFragment>()?.setMenuItems(buttons)
|
||||
}
|
||||
if(plugin != null && plugin.capabilities.hasGetChannelCapabilities) {
|
||||
if(plugin.getChannelCapabilities()?.types?.contains(ResultCapabilities.TYPE_SHORTS) ?: false &&
|
||||
!(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(ChannelTab.SHORTS.ordinal.toLong())) {
|
||||
(_viewPager.adapter as ChannelViewPagerAdapter).insert(1, ChannelTab.SHORTS);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,13 +469,8 @@ class ChannelFragment : MainFragment() {
|
||||
R.string.subscribers
|
||||
).lowercase() else ""
|
||||
|
||||
var supportsPlaylists = false;
|
||||
try {
|
||||
supportsPlaylists = StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists
|
||||
} catch (ex: Throwable) {
|
||||
//Ignore error
|
||||
Logger.e(TAG, "Failed to check if supports playlists", ex);
|
||||
}
|
||||
val supportsPlaylists =
|
||||
StatePlatform.instance.getChannelClient(channel.url).capabilities.hasGetChannelPlaylists
|
||||
val playlistPosition = 1
|
||||
if (supportsPlaylists && !(_viewPager.adapter as ChannelViewPagerAdapter).containsItem(
|
||||
ChannelTab.PLAYLISTS.ordinal.toLong()
|
||||
|
||||
-7
@@ -17,7 +17,6 @@ import com.futo.platformplayer.engine.exceptions.ScriptCaptchaRequiredException
|
||||
import com.futo.platformplayer.fragment.mainactivity.topbar.SearchTopBarFragment
|
||||
import com.futo.platformplayer.isHttpUrl
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateMeta
|
||||
import com.futo.platformplayer.states.StatePlatform
|
||||
import com.futo.platformplayer.views.FeedStyle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -223,12 +222,6 @@ class ContentSearchResultsFragment : MainFragment() {
|
||||
setSortByOptions(null);
|
||||
}
|
||||
|
||||
override fun filterResults(results: List<IPlatformContent>): List<IPlatformContent> {
|
||||
if(Settings.instance.search.hidefromSearch)
|
||||
return super.filterResults(results.filter { !StateMeta.instance.isVideoHidden(it.url) && !StateMeta.instance.isCreatorHidden(it.author.url) });
|
||||
return super.filterResults(results)
|
||||
}
|
||||
|
||||
override fun reload() {
|
||||
loadResults();
|
||||
}
|
||||
|
||||
+2
-58
@@ -4,13 +4,8 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.futo.platformplayer.R
|
||||
@@ -30,7 +25,6 @@ import com.futo.platformplayer.views.items.PlaylistDownloadItem
|
||||
import com.futo.platformplayer.views.others.ProgressBar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class DownloadsFragment : MainFragment() {
|
||||
private val TAG = "DownloadsFragment";
|
||||
@@ -98,12 +92,8 @@ class DownloadsFragment : MainFragment() {
|
||||
|
||||
private val _listDownloadedHeader: LinearLayout;
|
||||
private val _listDownloadedMeta: TextView;
|
||||
private val _listDownloadSearch: EditText;
|
||||
private val _listDownloaded: AnyInsertedAdapterView<VideoLocal, VideoDownloadViewHolder>;
|
||||
|
||||
private var lastDownloads: List<VideoLocal>? = null;
|
||||
private var ordering: String? = "nameAsc";
|
||||
|
||||
constructor(frag: DownloadsFragment, inflater: LayoutInflater): super(frag.requireContext()) {
|
||||
inflater.inflate(R.layout.fragment_downloads, this);
|
||||
_frag = frag;
|
||||
@@ -114,7 +104,6 @@ class DownloadsFragment : MainFragment() {
|
||||
|
||||
_listActiveDownloadsContainer = findViewById(R.id.downloads_active_downloads_container);
|
||||
_listActiveDownloadsMeta = findViewById(R.id.downloads_active_downloads_meta);
|
||||
_listDownloadSearch = findViewById(R.id.downloads_search);
|
||||
_listActiveDownloads = findViewById(R.id.downloads_active_downloads_list);
|
||||
|
||||
_listPlaylistsContainer = findViewById(R.id.downloads_playlist_container);
|
||||
@@ -124,30 +113,6 @@ class DownloadsFragment : MainFragment() {
|
||||
_listDownloadedHeader = findViewById(R.id.downloads_videos_header);
|
||||
_listDownloadedMeta = findViewById(R.id.downloads_videos_meta);
|
||||
|
||||
_listDownloadSearch.addTextChangedListener {
|
||||
updateContentFilters();
|
||||
}
|
||||
val spinnerSortBy: Spinner = findViewById(R.id.spinner_sortby);
|
||||
spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.downloads_sortby_array)).also {
|
||||
it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple);
|
||||
};
|
||||
spinnerSortBy.setSelection(0);
|
||||
spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) {
|
||||
when(pos) {
|
||||
0 -> ordering = "nameAsc"
|
||||
1 -> ordering = "nameDesc"
|
||||
2 -> ordering = "downloadDateAsc"
|
||||
3 -> ordering = "downloadDateDesc"
|
||||
4 -> ordering = "releasedAsc"
|
||||
5 -> ordering = "releasedDesc"
|
||||
else -> ordering = null
|
||||
}
|
||||
updateContentFilters()
|
||||
}
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) = Unit
|
||||
};
|
||||
|
||||
_listDownloaded = findViewById<RecyclerView>(R.id.list_downloaded)
|
||||
.asAnyWithTop(findViewById(R.id.downloads_top)) {
|
||||
it.onClick.subscribe {
|
||||
@@ -160,6 +125,7 @@ class DownloadsFragment : MainFragment() {
|
||||
reloadUI();
|
||||
}
|
||||
|
||||
|
||||
fun reloadUI() {
|
||||
val usage = StateDownloads.instance.getTotalUsage(true);
|
||||
_usageUsed.text = "${usage.usage.toHumanBytesSize()} " + context.getString(R.string.used);
|
||||
@@ -218,29 +184,7 @@ class DownloadsFragment : MainFragment() {
|
||||
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()})";
|
||||
}
|
||||
|
||||
lastDownloads = downloaded;
|
||||
_listDownloaded.setData(filterDownloads(downloaded));
|
||||
}
|
||||
fun updateContentFilters(){
|
||||
val toFilter = lastDownloads ?: return;
|
||||
_listDownloaded.setData(filterDownloads(toFilter));
|
||||
}
|
||||
fun filterDownloads(vids: List<VideoLocal>): List<VideoLocal>{
|
||||
var vidsToReturn = vids;
|
||||
if(!_listDownloadSearch.text.isNullOrEmpty())
|
||||
vidsToReturn = vids.filter { it.name.contains(_listDownloadSearch.text, true) };
|
||||
if(!ordering.isNullOrEmpty()) {
|
||||
vidsToReturn = when(ordering){
|
||||
"downloadDateAsc" -> vidsToReturn.sortedBy { it.downloadDate ?: OffsetDateTime.MAX };
|
||||
"downloadDateDesc" -> vidsToReturn.sortedByDescending { it.downloadDate ?: OffsetDateTime.MIN };
|
||||
"nameAsc" -> vidsToReturn.sortedBy { it.name.lowercase() }
|
||||
"nameDesc" -> vidsToReturn.sortedByDescending { it.name.lowercase() }
|
||||
"releasedAsc" -> vidsToReturn.sortedBy { it.datetime ?: OffsetDateTime.MAX }
|
||||
"releasedDesc" -> vidsToReturn.sortedByDescending { it.datetime ?: OffsetDateTime.MIN }
|
||||
else -> vidsToReturn
|
||||
}
|
||||
}
|
||||
return vidsToReturn;
|
||||
_listDownloaded.setData(downloaded);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-15
@@ -8,7 +8,6 @@ import android.view.ViewGroup
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.futo.platformplayer.*
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
@@ -79,14 +78,6 @@ class PlaylistFragment : MainFragment() {
|
||||
val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name));
|
||||
val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput);
|
||||
|
||||
_buttonExport.setOnClickListener {
|
||||
_playlist?.let {
|
||||
val context = StateApp.instance.contextOrNull ?: return@let;
|
||||
if(context is IWithResultLauncher)
|
||||
StateDownloads.instance.exportPlaylist(context, it.id);
|
||||
}
|
||||
};
|
||||
|
||||
_buttonDownload.visibility = View.VISIBLE;
|
||||
editPlaylistOverlay.onOK.subscribe {
|
||||
val text = nameInput.text;
|
||||
@@ -155,7 +146,7 @@ class PlaylistFragment : MainFragment() {
|
||||
setName(it.name);
|
||||
//TODO: Implement support for pagination
|
||||
setVideos(it.videos, false);
|
||||
setMetadata(it.videos.size, it.videos.sumOf { it.duration });
|
||||
setVideoCount(it.videos.size);
|
||||
setLoading(false);
|
||||
}
|
||||
.exception<Throwable> {
|
||||
@@ -183,9 +174,8 @@ class PlaylistFragment : MainFragment() {
|
||||
if (parameter != null) {
|
||||
setName(parameter.name)
|
||||
setVideos(parameter.videos, true)
|
||||
setMetadata(parameter.videos.size, parameter.videos.sumOf { it.duration })
|
||||
setVideoCount(parameter.videos.size)
|
||||
setButtonDownloadVisible(true)
|
||||
setButtonExportVisible(false)
|
||||
setButtonEditVisible(true)
|
||||
|
||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
||||
@@ -197,7 +187,7 @@ class PlaylistFragment : MainFragment() {
|
||||
} else {
|
||||
setName(null)
|
||||
setVideos(null, false)
|
||||
setMetadata(-1, -1);
|
||||
setVideoCount(-1)
|
||||
setButtonDownloadVisible(false)
|
||||
setButtonEditVisible(false)
|
||||
}
|
||||
@@ -205,7 +195,7 @@ class PlaylistFragment : MainFragment() {
|
||||
_playlist = null
|
||||
_url = parameter.url
|
||||
|
||||
setMetadata(parameter.videoCount, -1);
|
||||
setVideoCount(parameter.videoCount)
|
||||
setName(parameter.name)
|
||||
setVideos(null, false)
|
||||
setButtonDownloadVisible(false)
|
||||
@@ -218,7 +208,7 @@ class PlaylistFragment : MainFragment() {
|
||||
|
||||
setName(null)
|
||||
setVideos(null, false)
|
||||
setMetadata(-1, -1);
|
||||
setVideoCount(-1)
|
||||
setButtonDownloadVisible(false)
|
||||
setButtonEditVisible(false)
|
||||
|
||||
|
||||
+2
-14
@@ -237,19 +237,7 @@ class SourceDetailFragment : MainFragment() {
|
||||
BigButtonGroup(c, context.getString(R.string.update),
|
||||
BigButton(c, context.getString(R.string.check_for_updates), context.getString(R.string.checks_for_new_versions_of_the_source), R.drawable.ic_update) {
|
||||
checkForUpdatesSource();
|
||||
},
|
||||
if(config.changelog?.any() == true)
|
||||
BigButton(c, context.getString(R.string.changelog), context.getString(R.string.changelog_plugin_description), R.drawable.ic_list) {
|
||||
UIDialogs.showChangelogDialog(context, config.version, config.changelog!!.filterKeys { it.toIntOrNull() != null }
|
||||
.mapKeys { it.key.toInt() }
|
||||
.mapValues { config.getChangelogString(it.key.toString()) ?: "" });
|
||||
}.apply {
|
||||
this.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
|
||||
setMargins(0, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5f, resources.displayMetrics).toInt(), 0, 0);
|
||||
};
|
||||
}
|
||||
else
|
||||
null
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -556,7 +544,7 @@ class SourceDetailFragment : MainFragment() {
|
||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||
|
||||
val config = SourcePluginConfig.fromJson(configJson);
|
||||
if (config.version <= c.version && config.name != "Youtube") {
|
||||
if (config.version <= c.version) {
|
||||
Logger.i(TAG, "Plugin is up to date.");
|
||||
withContext(Dispatchers.Main) { UIDialogs.toast(context.getString(R.string.plugin_is_fully_up_to_date)); };
|
||||
return@launch;
|
||||
|
||||
+2
-4
@@ -204,10 +204,8 @@ class SubscriptionsFeedFragment : MainFragment() {
|
||||
val feed = StateSubscriptions.instance.getFeed(group?.id);
|
||||
|
||||
val currentExs = feed?.exceptions ?: listOf();
|
||||
if(currentExs != _lastExceptions && currentExs.any()) {
|
||||
handleExceptions(currentExs)
|
||||
feed?.exceptions = listOf()
|
||||
}
|
||||
if(currentExs != _lastExceptions && currentExs.any())
|
||||
handleExceptions(currentExs);
|
||||
|
||||
return@TaskHandler resp;
|
||||
})
|
||||
|
||||
-1
@@ -151,7 +151,6 @@ class TutorialFragment : MainFragment() {
|
||||
override val rating: IRating = RatingLikes(-1)
|
||||
override val viewCount: Long = -1
|
||||
override val video: IVideoSourceDescriptor = TutorialVideoSourceDescriptor(videoUrl, duration, width, height)
|
||||
override val isShort: Boolean = false;
|
||||
override fun getComments(client: IPlatformClient): IPager<IPlatformComment> {
|
||||
return EmptyPager()
|
||||
}
|
||||
|
||||
+2
-34
@@ -11,14 +11,12 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.OrientationEventListener
|
||||
import android.view.Surface
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
import android.view.WindowManager
|
||||
import androidx.constraintlayout.motion.widget.MotionLayout
|
||||
import androidx.core.view.ViewCompat.getDisplay
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.futo.platformplayer.R
|
||||
@@ -39,7 +37,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
import kotlin.math.min
|
||||
|
||||
//region Fragment
|
||||
@UnstableApi
|
||||
@@ -207,37 +205,7 @@ class VideoDetailFragment() : MainFragment() {
|
||||
} else if (rotationLock) {
|
||||
_portraitOrientationListener?.disableListener()
|
||||
_landscapeOrientationListener?.disableListener()
|
||||
val display = getDisplay(_viewDetail!!)
|
||||
val rotation = display!!.rotation
|
||||
val orientation = resources.configuration.orientation
|
||||
|
||||
a.requestedOrientation = when (orientation) {
|
||||
Configuration.ORIENTATION_PORTRAIT -> {
|
||||
if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) {
|
||||
if (rotation == Surface.ROTATION_0) {
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
} else {
|
||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
||||
}
|
||||
} else {
|
||||
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
}
|
||||
|
||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||
if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
|
||||
if (rotation == Surface.ROTATION_90) {
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
} else {
|
||||
ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE
|
||||
}
|
||||
} else {
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
}
|
||||
|
||||
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
a.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
} else {
|
||||
_portraitOrientationListener?.disableListener()
|
||||
_landscapeOrientationListener?.disableListener()
|
||||
|
||||
+27
-54
@@ -171,6 +171,7 @@ import kotlinx.coroutines.withContext
|
||||
import userpackage.Protocol
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
@UnstableApi
|
||||
@@ -869,20 +870,22 @@ class VideoDetailView : ConstraintLayout {
|
||||
}
|
||||
_slideUpOverlay?.hide();
|
||||
} else null,
|
||||
if (!isLimitedVersion) RoundButton(context, R.drawable.ic_screen_share, if (allowBackground) context.getString(R.string.background_revert) else context.getString(R.string.background), TAG_BACKGROUND) {
|
||||
if (!allowBackground) {
|
||||
_player.switchToAudioMode();
|
||||
allowBackground = true;
|
||||
it.text.text = resources.getString(R.string.background_revert);
|
||||
} else {
|
||||
_player.switchToVideoMode();
|
||||
allowBackground = false;
|
||||
it.text.text = resources.getString(R.string.background);
|
||||
if(!isLimitedVersion)
|
||||
RoundButton(context, R.drawable.ic_screen_share, context.getString(R.string.background), TAG_BACKGROUND) {
|
||||
if(!allowBackground) {
|
||||
_player.switchToAudioMode();
|
||||
allowBackground = true;
|
||||
it.text.text = resources.getString(R.string.background_revert);
|
||||
}
|
||||
else {
|
||||
_player.switchToVideoMode();
|
||||
allowBackground = false;
|
||||
it.text.text = resources.getString(R.string.background);
|
||||
}
|
||||
_slideUpOverlay?.hide();
|
||||
}
|
||||
_slideUpOverlay?.hide();
|
||||
}
|
||||
else null,
|
||||
if(!isLimitedVersion && !(video?.isLive ?: false))
|
||||
if(!isLimitedVersion)
|
||||
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
||||
video?.let {
|
||||
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
||||
@@ -1898,45 +1901,13 @@ class VideoDetailView : ConstraintLayout {
|
||||
return super.onInterceptTouchEvent(ev);
|
||||
}
|
||||
|
||||
|
||||
//Actions
|
||||
private fun showVideoSettings() {
|
||||
Logger.i(TAG, "showVideoSettings")
|
||||
_overlay_quality_selector?.selectOption("video", _lastVideoSource);
|
||||
_overlay_quality_selector?.selectOption("audio", _lastAudioSource);
|
||||
_overlay_quality_selector?.selectOption("subtitles", _lastSubtitleSource);
|
||||
|
||||
if (_lastVideoSource is IDashManifestSource || _lastVideoSource is IHLSManifestSource) {
|
||||
|
||||
val videoTracks =
|
||||
_player.exoPlayer?.player?.currentTracks?.groups?.firstOrNull { it.mediaTrackGroup.type == C.TRACK_TYPE_VIDEO }
|
||||
|
||||
var selectedQuality: Format? = null
|
||||
|
||||
if (videoTracks != null) {
|
||||
for (i in 0 until videoTracks.mediaTrackGroup.length) {
|
||||
if (videoTracks.mediaTrackGroup.getFormat(i).height == _player.targetTrackVideoHeight) {
|
||||
selectedQuality = videoTracks.mediaTrackGroup.getFormat(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var videoMenuGroup: SlideUpMenuGroup? = null
|
||||
for (view in _overlay_quality_selector!!.groupItems) {
|
||||
if (view is SlideUpMenuGroup && view.groupTag == "video") {
|
||||
videoMenuGroup = view
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedQuality != null) {
|
||||
videoMenuGroup?.getItem("auto")?.setSubText("")
|
||||
_overlay_quality_selector?.selectOption("video", selectedQuality)
|
||||
} else {
|
||||
videoMenuGroup?.getItem("auto")
|
||||
?.setSubText("${_player.exoPlayer?.player?.videoFormat?.width}x${_player.exoPlayer?.player?.videoFormat?.height}")
|
||||
_overlay_quality_selector?.selectOption("video", "auto")
|
||||
}
|
||||
}
|
||||
|
||||
val currentPlaybackRate = (if (_isCasting) StateCasting.instance.activeDevice?.speed else _player.getPlaybackRate()) ?: 1.0
|
||||
_overlay_quality_selector?.groupItems?.firstOrNull { it is SlideUpMenuButtonList && it.id == "playback_rate" }?.let {
|
||||
(it as SlideUpMenuButtonList).setSelected(currentPlaybackRate.toString())
|
||||
@@ -2110,15 +2081,17 @@ class VideoDetailView : ConstraintLayout {
|
||||
call = { handleSelectSubtitleTrack(it) })
|
||||
}.toList().toTypedArray())
|
||||
else null,
|
||||
if (liveStreamVideoFormats?.isEmpty() == false) SlideUpMenuGroup(
|
||||
this.context, context.getString(R.string.stream_video), "video", (listOf(
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_movie, "Auto", tag = "auto", call = { _player.selectVideoTrack(-1) })
|
||||
) + (liveStreamVideoFormats.map {
|
||||
SlideUpMenuItem(this.context, R.drawable.ic_movie, it.label
|
||||
?: it.containerMimeType
|
||||
?: it.bitrate.toString(), "${it.width}x${it.height}", tag = it, call = { _player.selectVideoTrack(it.height) });
|
||||
}))
|
||||
)
|
||||
if(liveStreamVideoFormats?.isEmpty() == false)
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.stream_video), "video",
|
||||
*liveStreamVideoFormats
|
||||
.map {
|
||||
SlideUpMenuItem(this.context,
|
||||
R.drawable.ic_movie,
|
||||
it.label ?: it.containerMimeType ?: it.bitrate.toString(),
|
||||
"${it.width}x${it.height}",
|
||||
tag = it,
|
||||
call = { _player.selectVideoTrack(it.height) });
|
||||
}.toList().toTypedArray())
|
||||
else null,
|
||||
if(liveStreamAudioFormats?.isEmpty() == false)
|
||||
SlideUpMenuGroup(this.context, context.getString(R.string.stream_audio), "audio",
|
||||
|
||||
+2
-20
@@ -20,8 +20,6 @@ import com.futo.platformplayer.downloads.VideoDownload
|
||||
import com.futo.platformplayer.images.GlideHelper.Companion.crossfade
|
||||
import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.toHumanDuration
|
||||
import com.futo.platformplayer.toHumanTime
|
||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||
|
||||
abstract class VideoListEditorView : LinearLayout {
|
||||
@@ -34,7 +32,6 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
protected var overlayContainer: FrameLayout
|
||||
private set;
|
||||
protected var _buttonDownload: ImageButton;
|
||||
protected var _buttonExport: ImageButton;
|
||||
private var _buttonShare: ImageButton;
|
||||
private var _buttonEdit: ImageButton;
|
||||
|
||||
@@ -55,8 +52,6 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
_buttonEdit = findViewById(R.id.button_edit);
|
||||
_buttonDownload = findViewById(R.id.button_download);
|
||||
_buttonDownload.visibility = View.GONE;
|
||||
_buttonExport = findViewById(R.id.button_export);
|
||||
_buttonExport.visibility = View.GONE;
|
||||
|
||||
_buttonShare = findViewById(R.id.button_share);
|
||||
val onShare = _onShare;
|
||||
@@ -71,7 +66,6 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
buttonShuffle.setOnClickListener { onShuffleClick(); };
|
||||
|
||||
_buttonEdit.setOnClickListener { onEditClick(); };
|
||||
setButtonExportVisible(false);
|
||||
setButtonDownloadVisible(canEdit());
|
||||
|
||||
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
|
||||
@@ -112,7 +106,6 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
_buttonDownload.setBackgroundResource(R.drawable.background_button_round);
|
||||
|
||||
if(isDownloading) {
|
||||
setButtonExportVisible(false);
|
||||
_buttonDownload.setImageResource(R.drawable.ic_loader_animated);
|
||||
_buttonDownload.drawable.assume<Animatable, Unit> { it.start() };
|
||||
_buttonDownload.setOnClickListener {
|
||||
@@ -122,7 +115,6 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
}
|
||||
}
|
||||
else if(isDownloaded) {
|
||||
setButtonExportVisible(true)
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download_off);
|
||||
_buttonDownload.setOnClickListener {
|
||||
UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), {
|
||||
@@ -131,7 +123,6 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
}
|
||||
}
|
||||
else {
|
||||
setButtonExportVisible(false);
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download);
|
||||
_buttonDownload.setOnClickListener {
|
||||
onDownload();
|
||||
@@ -145,14 +136,8 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
_textName.text = name ?: "";
|
||||
}
|
||||
|
||||
protected fun setMetadata(videoCount: Int = -1, duration: Long = -1) {
|
||||
val parts = mutableListOf<String>()
|
||||
if(videoCount >= 0)
|
||||
parts.add("${videoCount} " + context.getString(R.string.videos));
|
||||
if(duration > 0)
|
||||
parts.add("${duration.toHumanDuration(false)} ");
|
||||
|
||||
_textMetadata.text = parts.joinToString(" • ");
|
||||
protected fun setVideoCount(videoCount: Int = -1) {
|
||||
_textMetadata.text = if (videoCount == -1) "" else "${videoCount} " + context.getString(R.string.videos);
|
||||
}
|
||||
|
||||
protected fun setVideos(videos: List<IPlatformVideo>?, canEdit: Boolean) {
|
||||
@@ -178,9 +163,6 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
protected fun setButtonDownloadVisible(isVisible: Boolean) {
|
||||
_buttonDownload.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||
}
|
||||
protected fun setButtonExportVisible(isVisible: Boolean) {
|
||||
_buttonExport.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||
}
|
||||
|
||||
protected fun setButtonEditVisible(isVisible: Boolean) {
|
||||
_buttonEdit.visibility = if (isVisible) View.VISIBLE else View.GONE;
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
|
||||
@@ -46,7 +47,7 @@ class VideoHelper {
|
||||
return false
|
||||
}
|
||||
|
||||
fun isDownloadable(source: IVideoSource) = (source is IVideoUrlSource || source is IHLSManifestSource || source is JSDashManifestRawSource) && source !is IWidevineSource
|
||||
fun isDownloadable(source: IVideoSource) = (source is IVideoUrlSource || source is IHLSManifestSource || source is JSDashManifestRawSource || source is IDashManifestSource) && source !is IWidevineSource
|
||||
fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource || source is JSDashManifestRawAudioSource) && source !is IWidevineSource
|
||||
|
||||
fun selectBestVideoSource(desc: IVideoSourceDescriptor, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? = selectBestVideoSource(desc.videoSources.toList(), desiredPixelCount, prefContainers);
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package com.futo.platformplayer.parsers
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistParserFactory
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist
|
||||
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantSubtitleUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
||||
@@ -7,13 +13,20 @@ import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudi
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
import com.futo.platformplayer.toYesNo
|
||||
import com.futo.platformplayer.yesNoToBoolean
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.net.URI
|
||||
import java.net.URLConnection
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class HLS {
|
||||
companion object {
|
||||
fun parseMasterPlaylist(masterPlaylistContent: String, sourceUrl: String): MasterPlaylist {
|
||||
@OptIn(UnstableApi::class)
|
||||
fun parseMasterPlaylist(masterPlaylistContent: String, sourceUrl: String, isAudioSource: Boolean? = null): MasterPlaylist {
|
||||
val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray())
|
||||
val playlist = DefaultHlsPlaylistParserFactory().createPlaylistParser()
|
||||
.parse(Uri.parse(sourceUrl), inputStream)
|
||||
|
||||
val baseUrl = URI(sourceUrl).resolve("./").toString()
|
||||
|
||||
val variantPlaylists = mutableListOf<VariantPlaylistReference>()
|
||||
@@ -21,27 +34,38 @@ class HLS {
|
||||
val sessionDataList = mutableListOf<SessionData>()
|
||||
var independentSegments = false
|
||||
|
||||
masterPlaylistContent.lines().forEachIndexed { index, line ->
|
||||
when {
|
||||
line.startsWith("#EXT-X-STREAM-INF") -> {
|
||||
val nextLine = masterPlaylistContent.lines().getOrNull(index + 1)
|
||||
?: throw Exception("Expected URI following #EXT-X-STREAM-INF, found none")
|
||||
val url = resolveUrl(baseUrl, nextLine)
|
||||
if (playlist is HlsMediaPlaylist) {
|
||||
independentSegments = playlist.hasIndependentSegments
|
||||
if (isAudioSource == true) {
|
||||
val firstSegmentUrlFile =
|
||||
Uri.parse(playlist.segments[0].url).buildUpon().clearQuery().fragment(null)
|
||||
.build().toString()
|
||||
mediaRenditions.add(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null, URLConnection.guessContentTypeFromName(firstSegmentUrlFile)))
|
||||
} else {
|
||||
variantPlaylists.add(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null)))
|
||||
}
|
||||
} else if (playlist is HlsMultivariantPlaylist) {
|
||||
masterPlaylistContent.lines().forEachIndexed { index, line ->
|
||||
when {
|
||||
line.startsWith("#EXT-X-STREAM-INF") -> {
|
||||
val nextLine = masterPlaylistContent.lines().getOrNull(index + 1)
|
||||
?: throw Exception("Expected URI following #EXT-X-STREAM-INF, found none")
|
||||
val url = resolveUrl(baseUrl, nextLine)
|
||||
variantPlaylists.add(VariantPlaylistReference(url, parseStreamInfo(line)))
|
||||
}
|
||||
|
||||
variantPlaylists.add(VariantPlaylistReference(url, parseStreamInfo(line)))
|
||||
}
|
||||
line.startsWith("#EXT-X-MEDIA") -> {
|
||||
mediaRenditions.add(parseMediaRendition(line, baseUrl))
|
||||
}
|
||||
|
||||
line.startsWith("#EXT-X-MEDIA") -> {
|
||||
mediaRenditions.add(parseMediaRendition(line, baseUrl))
|
||||
}
|
||||
line == "#EXT-X-INDEPENDENT-SEGMENTS" -> {
|
||||
independentSegments = true
|
||||
}
|
||||
|
||||
line == "#EXT-X-INDEPENDENT-SEGMENTS" -> {
|
||||
independentSegments = true
|
||||
}
|
||||
|
||||
line.startsWith("#EXT-X-SESSION-DATA") -> {
|
||||
val sessionData = parseSessionData(line)
|
||||
sessionDataList.add(sessionData)
|
||||
line.startsWith("#EXT-X-SESSION-DATA") -> {
|
||||
val sessionData = parseSessionData(line)
|
||||
sessionDataList.add(sessionData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +85,26 @@ class HLS {
|
||||
val playlistType = lines.find { it.startsWith("#EXT-X-PLAYLIST-TYPE:") }?.substringAfter(":")
|
||||
val streamInfo = lines.find { it.startsWith("#EXT-X-STREAM-INF:") }?.let { parseStreamInfo(it) }
|
||||
|
||||
val keyInfo =
|
||||
lines.find { it.startsWith("#EXT-X-KEY:") }?.substringAfter(":")?.split(",")
|
||||
|
||||
val key = keyInfo?.find { it.startsWith("URI=") }?.substringAfter("=")?.trim('"')
|
||||
val iv =
|
||||
keyInfo?.find { it.startsWith("IV=") }?.substringAfter("=")?.substringAfter("x")
|
||||
|
||||
val decryptionInfo: DecryptionInfo? = key?.let { k ->
|
||||
iv?.let { i ->
|
||||
DecryptionInfo(k, i)
|
||||
}
|
||||
}
|
||||
|
||||
val initSegment =
|
||||
lines.find { it.startsWith("#EXT-X-MAP:") }?.substringAfter(":")?.split(",")?.get(0)
|
||||
?.substringAfter("=")?.trim('"')
|
||||
val segments = mutableListOf<Segment>()
|
||||
if (initSegment != null) {
|
||||
segments.add(MediaSegment(0.0, resolveUrl(sourceUrl, initSegment)))
|
||||
}
|
||||
var currentSegment: MediaSegment? = null
|
||||
lines.forEach { line ->
|
||||
when {
|
||||
@@ -86,7 +129,7 @@ class HLS {
|
||||
}
|
||||
}
|
||||
|
||||
return VariantPlaylist(version, targetDuration, mediaSequence, discontinuitySequence, programDateTime, playlistType, streamInfo, segments)
|
||||
return VariantPlaylist(version, targetDuration, mediaSequence, discontinuitySequence, programDateTime, playlistType, streamInfo, segments, decryptionInfo)
|
||||
}
|
||||
|
||||
fun parseAndGetVideoSources(source: Any, content: String, url: String): List<HLSVariantVideoUrlSource> {
|
||||
@@ -109,10 +152,10 @@ class HLS {
|
||||
}
|
||||
}
|
||||
|
||||
fun parseAndGetAudioSources(source: Any, content: String, url: String): List<HLSVariantAudioUrlSource> {
|
||||
fun parseAndGetAudioSources(source: Any, content: String, url: String, isAudioSource: Boolean? = null): List<HLSVariantAudioUrlSource> {
|
||||
val masterPlaylist: MasterPlaylist
|
||||
try {
|
||||
masterPlaylist = parseMasterPlaylist(content, url)
|
||||
masterPlaylist = parseMasterPlaylist(content, url, isAudioSource)
|
||||
return masterPlaylist.getAudioSources()
|
||||
} catch (e: Throwable) {
|
||||
if (content.lines().any { it.startsWith("#EXTINF:") }) {
|
||||
@@ -203,10 +246,10 @@ class HLS {
|
||||
private val _quoteList = listOf("GROUP-ID", "NAME", "URI", "CODECS", "AUDIO", "VIDEO")
|
||||
private fun shouldQuote(key: String, value: String?): Boolean {
|
||||
if (value == null)
|
||||
return false;
|
||||
return false
|
||||
|
||||
if (value.contains(','))
|
||||
return true;
|
||||
return true
|
||||
|
||||
return _quoteList.contains(key)
|
||||
}
|
||||
@@ -270,7 +313,8 @@ class HLS {
|
||||
val name: String?,
|
||||
val isDefault: Boolean?,
|
||||
val isAutoSelect: Boolean?,
|
||||
val isForced: Boolean?
|
||||
val isForced: Boolean?,
|
||||
val container: String? = null
|
||||
) {
|
||||
fun toM3U8Line(): String = buildString {
|
||||
append("#EXT-X-MEDIA:")
|
||||
@@ -340,7 +384,7 @@ class HLS {
|
||||
|
||||
val suffix = listOf(it.language, it.groupID).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
||||
return@mapNotNull when (it.type) {
|
||||
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri)
|
||||
"AUDIO" -> HLSVariantAudioUrlSource(it.name?.ifEmpty { "Audio (${suffix})" } ?: "Audio (${suffix})", 0, it.container?: "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -368,6 +412,11 @@ class HLS {
|
||||
}
|
||||
}
|
||||
|
||||
data class DecryptionInfo(
|
||||
val keyUrl: String,
|
||||
val iv: String
|
||||
)
|
||||
|
||||
data class VariantPlaylist(
|
||||
val version: Int?,
|
||||
val targetDuration: Int?,
|
||||
@@ -376,7 +425,8 @@ class HLS {
|
||||
val programDateTime: ZonedDateTime?,
|
||||
val playlistType: String?,
|
||||
val streamInfo: StreamInfo?,
|
||||
val segments: List<Segment>
|
||||
val segments: List<Segment>,
|
||||
val decryptionInfo: DecryptionInfo? = null
|
||||
) {
|
||||
fun buildM3U8(): String = buildString {
|
||||
append("#EXTM3U\n")
|
||||
|
||||
@@ -3,11 +3,9 @@ package com.futo.platformplayer.states
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.os.StatFs
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.IWithResultLauncher
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
import com.futo.platformplayer.api.media.PlatformID
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
@@ -468,54 +466,6 @@ class StateDownloads {
|
||||
return _downloadsDirectory;
|
||||
}
|
||||
|
||||
fun exportPlaylist(context: Context, playlistId: String) {
|
||||
if(context is IWithResultLauncher)
|
||||
StateApp.instance.requestDirectoryAccess(context, "Export Playlist", "To export playlist to directory", null) {
|
||||
if (it == null)
|
||||
return@requestDirectoryAccess;
|
||||
|
||||
val root = DocumentFile.fromTreeUri(context, it!!);
|
||||
|
||||
val localVideos = StateDownloads.instance.getDownloadedVideosPlaylist(playlistId)
|
||||
|
||||
var lastNotifyTime = -1L;
|
||||
|
||||
UIDialogs.showDialogProgress(context) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
it.setText("Exporting videos..");
|
||||
var i = 0;
|
||||
for (video in localVideos) {
|
||||
withContext(Dispatchers.Main) {
|
||||
it.setText("Exporting videos...(${i}/${localVideos.size})");
|
||||
//it.setProgress(i.toDouble() / localVideos.size);
|
||||
}
|
||||
|
||||
try {
|
||||
val export = VideoExport(video, video.videoSource.firstOrNull(), video.audioSource.firstOrNull(), video.subtitlesSources.firstOrNull());
|
||||
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
|
||||
|
||||
val file = export.export(context, { progress ->
|
||||
val now = System.currentTimeMillis();
|
||||
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
||||
it.setProgress(progress);
|
||||
lastNotifyTime = now;
|
||||
}
|
||||
}, root);
|
||||
} catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed export [${video.name}]: ${ex.message}", ex);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
it.setProgress(1f);
|
||||
it.dismiss();
|
||||
UIDialogs.appToast("Finished exporting playlist");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
|
||||
var lastNotifyTime = -1L;
|
||||
|
||||
@@ -527,13 +477,13 @@ class StateDownloads {
|
||||
try {
|
||||
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
|
||||
|
||||
val file = export.export(context, { progress ->
|
||||
val file = export.export(context) { progress ->
|
||||
val now = System.currentTimeMillis();
|
||||
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
||||
it.setProgress(progress);
|
||||
lastNotifyTime = now;
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
it.setProgress(100.0f)
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.net.Uri
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
@@ -307,19 +306,14 @@ class StatePlaylists {
|
||||
broadcastSyncPlaylist(playlist);
|
||||
}
|
||||
}
|
||||
fun addToPlaylist(id: String, video: IPlatformVideo): Boolean {
|
||||
fun addToPlaylist(id: String, video: IPlatformVideo) {
|
||||
synchronized(playlistStore) {
|
||||
val playlist = getPlaylist(id) ?: return false;
|
||||
if(!Settings.instance.other.playlistAllowDups && playlist.videos.any { it.url == video.url })
|
||||
return false;
|
||||
|
||||
|
||||
val playlist = getPlaylist(id) ?: return;
|
||||
playlist.videos.add(SerializedPlatformVideo.fromVideo(video));
|
||||
playlist.dateUpdate = OffsetDateTime.now();
|
||||
playlistStore.saveAsync(playlist, true);
|
||||
|
||||
broadcastSyncPlaylist(playlist);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class ToastView : LinearLayout {
|
||||
translationY = 20.dp(context.resources).toFloat();
|
||||
animate()
|
||||
.alpha(1f)
|
||||
.setDuration(300)
|
||||
.setDuration(700)
|
||||
.translationY(0f)
|
||||
.start();
|
||||
}
|
||||
|
||||
+1
-15
@@ -5,7 +5,6 @@ import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.futo.platformplayer.api.media.models.PlatformAuthorLink
|
||||
import com.futo.platformplayer.api.media.models.ResultCapabilities
|
||||
import com.futo.platformplayer.api.media.models.channels.IPlatformChannel
|
||||
import com.futo.platformplayer.api.media.models.contents.ContentType
|
||||
import com.futo.platformplayer.api.media.models.contents.IPlatformContent
|
||||
@@ -22,7 +21,7 @@ import com.google.android.material.tabs.TabLayout
|
||||
|
||||
|
||||
enum class ChannelTab {
|
||||
VIDEOS, SHORTS, CHANNELS, PLAYLISTS, SUPPORT, ABOUT
|
||||
VIDEOS, CHANNELS, PLAYLISTS, SUPPORT, ABOUT
|
||||
}
|
||||
|
||||
class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
|
||||
@@ -92,19 +91,6 @@ class ChannelViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifec
|
||||
}
|
||||
}
|
||||
|
||||
ChannelTab.SHORTS -> {
|
||||
fragment = ChannelContentsFragment.newInstance(ResultCapabilities.TYPE_SHORTS).apply {
|
||||
onContentClicked.subscribe(this@ChannelViewPagerAdapter.onContentClicked::emit)
|
||||
onContentUrlClicked.subscribe(this@ChannelViewPagerAdapter.onContentUrlClicked::emit)
|
||||
onUrlClicked.subscribe(this@ChannelViewPagerAdapter.onUrlClicked::emit)
|
||||
onChannelClicked.subscribe(this@ChannelViewPagerAdapter.onChannelClicked::emit)
|
||||
onAddToClicked.subscribe(this@ChannelViewPagerAdapter.onAddToClicked::emit)
|
||||
onAddToQueueClicked.subscribe(this@ChannelViewPagerAdapter.onAddToQueueClicked::emit)
|
||||
onAddToWatchLaterClicked.subscribe(this@ChannelViewPagerAdapter.onAddToWatchLaterClicked::emit)
|
||||
onLongPress.subscribe(this@ChannelViewPagerAdapter.onLongPress::emit)
|
||||
}
|
||||
}
|
||||
|
||||
ChannelTab.CHANNELS -> {
|
||||
fragment = ChannelListFragment.newInstance()
|
||||
.apply { onClickChannel.subscribe(onChannelClicked::emit) }
|
||||
|
||||
@@ -755,6 +755,10 @@ class GestureControlView : LinearLayout {
|
||||
}
|
||||
}
|
||||
|
||||
if (!Settings.instance.gestureControls.useSystemBrightness) {
|
||||
_brightnessFactor = 1.0f;
|
||||
}
|
||||
|
||||
if (Settings.instance.gestureControls.useSystemVolume) {
|
||||
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
|
||||
|
||||
@@ -61,15 +61,6 @@ class SlideUpMenuGroup : LinearLayout {
|
||||
return didSelect;
|
||||
}
|
||||
|
||||
fun getItem(tag: Any?): SlideUpMenuItem? {
|
||||
for(item in items) {
|
||||
if(item.itemTag == tag){
|
||||
return item
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun addItems(items: List<SlideUpMenuItem>) {
|
||||
for (item in items) {
|
||||
item.setParentClickListener { parentClickListener?.invoke() }
|
||||
|
||||
@@ -82,10 +82,6 @@ class SlideUpMenuItem : ConstraintLayout {
|
||||
return isSelected;
|
||||
}
|
||||
|
||||
fun setSubText(subText: String) {
|
||||
_subtext.text = subText
|
||||
}
|
||||
|
||||
fun setParentClickListener(listener: (()->Unit)?) {
|
||||
_parentClickListener = listener;
|
||||
}
|
||||
|
||||
@@ -110,10 +110,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
private var _didCallSourceChange = false;
|
||||
private var _lastState: Int = -1;
|
||||
|
||||
|
||||
var targetTrackVideoHeight = -1
|
||||
private set
|
||||
private var _targetTrackAudioBitrate = -1
|
||||
private var _targetTrackVideoHeight = -1;
|
||||
private var _targetTrackAudioBitrate = -1;
|
||||
|
||||
private var _toResume = false;
|
||||
|
||||
@@ -280,7 +278,7 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
|
||||
//TODO: Temporary solution, Implement custom track selector without using constraints
|
||||
fun selectVideoTrack(height: Int) {
|
||||
targetTrackVideoHeight = height;
|
||||
_targetTrackVideoHeight = height;
|
||||
updateTrackSelector();
|
||||
}
|
||||
fun selectAudioTrack(bitrate: Int) {
|
||||
@@ -290,22 +288,16 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun updateTrackSelector() {
|
||||
var builder = DefaultTrackSelector.Parameters.Builder(context);
|
||||
if(targetTrackVideoHeight > 0) {
|
||||
if(_targetTrackVideoHeight > 0) {
|
||||
builder = builder
|
||||
.setMinVideoSize(0, targetTrackVideoHeight - 10)
|
||||
.setMaxVideoSize(9999, targetTrackVideoHeight + 10);
|
||||
.setMinVideoSize(0, _targetTrackVideoHeight - 10)
|
||||
.setMaxVideoSize(9999, _targetTrackVideoHeight + 10);
|
||||
}
|
||||
|
||||
if(_targetTrackAudioBitrate > 0) {
|
||||
builder = builder.setMaxAudioBitrate(_targetTrackAudioBitrate);
|
||||
}
|
||||
|
||||
builder = if (isAudioMode) {
|
||||
builder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true)
|
||||
} else {
|
||||
builder.setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, false)
|
||||
}
|
||||
|
||||
val trackSelector = exoPlayer?.player?.trackSelector;
|
||||
if(trackSelector != null) {
|
||||
trackSelector.parameters = builder.build();
|
||||
@@ -745,7 +737,6 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
val sourceAudio = _lastAudioMediaSource;
|
||||
val sourceSubs = _lastSubtitleMediaSource;
|
||||
|
||||
updateTrackSelector()
|
||||
|
||||
beforeSourceChanged();
|
||||
|
||||
|
||||
@@ -93,26 +93,6 @@
|
||||
android:layout_marginStart="30dp"
|
||||
android:layout_marginEnd="30dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_changelog"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:textAlignment="textStart"
|
||||
android:text="Changelog"
|
||||
android:textSize="9.5sp"
|
||||
android:textColor="@color/white"
|
||||
android:fontFamily="monospace"
|
||||
android:scrollbars="vertical"
|
||||
|
||||
android:background="@color/black"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp"
|
||||
android:paddingTop="5dp"
|
||||
android:paddingBottom="5dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginEnd="10dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<!--Playlists-->
|
||||
<LinearLayout
|
||||
android:id="@+id/downloads_playlist_container"
|
||||
@@ -162,37 +163,6 @@
|
||||
android:textColor="#ACACAC"
|
||||
tools:text="(12 videos)" />
|
||||
</LinearLayout>
|
||||
<EditText
|
||||
android:id="@+id/downloads_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@drawable/background_button_round"
|
||||
android:hint="Seach.." />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14dp"
|
||||
android:textColor="@color/gray_ac"
|
||||
android:fontFamily="@font/inter_light"
|
||||
android:text="@string/sort_by"
|
||||
android:paddingStart="20dp" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinner_sortby"
|
||||
android:layout_width="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
@@ -54,22 +54,6 @@
|
||||
|
||||
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/button_export"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:contentDescription="@string/cd_button_download"
|
||||
android:background="@drawable/background_button_round"
|
||||
android:gravity="center"
|
||||
android:layout_marginStart="10dp"
|
||||
android:orientation="horizontal"
|
||||
app:srcCompat="@drawable/ic_export"
|
||||
app:layout_constraintRight_toLeftOf="@id/button_share"
|
||||
app:layout_constraintTop_toTopOf="@id/button_share"
|
||||
app:tint="@color/white"
|
||||
android:padding="8dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:scaleType="fitCenter" />
|
||||
<ImageButton
|
||||
android:id="@+id/button_share"
|
||||
android:layout_width="40dp"
|
||||
|
||||
@@ -13,8 +13,6 @@
|
||||
<string name="home">Home</string>
|
||||
<string name="progress_bar">Progress Bar</string>
|
||||
<string name="progress_bar_description">If a historical progress bar should be shown</string>
|
||||
<string name="hide_hidden_from_search">Hide hidden from home in search</string>
|
||||
<string name="hide_hidden_from_search_description">Hide videos and creators hidden from home also in search results</string>
|
||||
<string name="recommendations">Recommendations</string>
|
||||
<string name="more">More</string>
|
||||
<string name="playlists">Playlists</string>
|
||||
@@ -197,7 +195,6 @@
|
||||
<string name="connected_to">Connected to</string>
|
||||
<string name="volume">Volume</string>
|
||||
<string name="changelog">Changelog</string>
|
||||
<string name="changelog_plugin_description">Shows available changelogs for current and past versions</string>
|
||||
<string name="some_example_changelog">Some example changelog.</string>
|
||||
<string name="previous">Previous</string>
|
||||
<string name="next">Next</string>
|
||||
@@ -401,8 +398,8 @@
|
||||
<string name="prefer_webm_audio_description">If player should prefer Webm codecs (opus) over mp4 codecs (AAC), may result in worse compatibility.</string>
|
||||
<string name="allow_under_cutout">Allow video under cutout</string>
|
||||
<string name="allow_under_cutout_description">Allow video to go underneath the screen cutout in full screen.\nMay require restart</string>
|
||||
<string name="autoplay">Autoplay next video by default</string>
|
||||
<string name="autoplay_description">Autoplay next video will be enabled by default whenever you watch a video</string>
|
||||
<string name="autoplay">Enable autoplay by default</string>
|
||||
<string name="autoplay_description">Autoplay will be enabled by default whenever you watch a video</string>
|
||||
<string name="allow_full_screen_portrait">Allow full-screen portrait when watching horizontal videos</string>
|
||||
<string name="delete_watchlist_on_finish">Delete from WatchLater when watched</string>
|
||||
<string name="delete_watchlist_on_finish_description">After you leave a video that you mostly watched, it will be removed from watch later.</string>
|
||||
@@ -427,8 +424,6 @@
|
||||
<string name="bypass_rotation_prevention">Bypass Rotation Prevention</string>
|
||||
<string name="playlist_delete_confirmation">Playlist Delete Confirmation</string>
|
||||
<string name="playlist_delete_confirmation_description">Show confirmation dialog when deleting media from a playlist</string>
|
||||
<string name="playlist_allow_dups">Allow duplicate playlist videos</string>
|
||||
<string name="playlist_allow_dups_description">Allow adding duplicate videos to playlists</string>
|
||||
<string name="enable_polycentric">Enable Polycentric</string>
|
||||
<string name="polycentric_local_cache">Enable Polycentric Local Caching</string>
|
||||
<string name="polycentric_local_cache_description">Caches polycentric results on-device to reduce load times, changing requires app reboot</string>
|
||||
@@ -958,14 +953,6 @@
|
||||
<item>Watchtime Ascending</item>
|
||||
<item>Watchtime Descending</item>
|
||||
</string-array>
|
||||
<string-array name="downloads_sortby_array">
|
||||
<item>Name (Ascending)</item>
|
||||
<item>Name (Descending)</item>
|
||||
<item>Download Date (Oldest)</item>
|
||||
<item>Download Date (Newest)</item>
|
||||
<item>Release Date (Oldest)</item>
|
||||
<item>Release Date (Newest)</item>
|
||||
</string-array>
|
||||
<string-array name="feed_style">
|
||||
<item>Preview</item>
|
||||
<item>List</item>
|
||||
|
||||
Submodule app/src/stable/assets/sources/dailymotion updated: d00c7ff8e5...6dc9ba2b61
Submodule app/src/stable/assets/sources/odysee updated: 04b4d8ed31...34dc142f81
Submodule app/src/stable/assets/sources/rumble updated: 670cbc043e...6811ff4b41
Submodule app/src/stable/assets/sources/soundcloud updated: a72aeb85d0...90bceac198
Submodule app/src/stable/assets/sources/twitch updated: 1b2833cdf2...c3ee73a3e5
Submodule app/src/stable/assets/sources/youtube updated: 15d3391a5d...f5afc782a9
Submodule app/src/unstable/assets/sources/dailymotion updated: d00c7ff8e5...6dc9ba2b61
Submodule app/src/unstable/assets/sources/odysee updated: 04b4d8ed31...34dc142f81
Submodule app/src/unstable/assets/sources/rumble updated: 670cbc043e...6811ff4b41
Submodule app/src/unstable/assets/sources/soundcloud updated: a72aeb85d0...90bceac198
Submodule app/src/unstable/assets/sources/twitch updated: 1b2833cdf2...c3ee73a3e5
Submodule app/src/unstable/assets/sources/youtube updated: 2c816009f7...f5afc782a9
Reference in New Issue
Block a user