mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-29 11:03:01 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2697107f76 |
+1
-1
@@ -83,7 +83,7 @@
|
||||
path = app/src/stable/assets/sources/dailymotion
|
||||
url = ../plugins/dailymotion.git
|
||||
[submodule "app/src/stable/assets/sources/apple-podcast"]
|
||||
path = app/src/stable/assets/sources/apple-podcasts
|
||||
path = app/src/stable/assets/sources/apple-podcast
|
||||
url = ../plugins/apple-podcasts.git
|
||||
[submodule "app/src/unstable/assets/sources/apple-podcasts"]
|
||||
path = app/src/unstable/assets/sources/apple-podcasts
|
||||
|
||||
@@ -263,10 +263,6 @@ class PlatformVideoDetails extends PlatformVideo {
|
||||
this.rating = obj.rating ?? null; //IRating
|
||||
this.subtitles = obj.subtitles ?? [];
|
||||
this.isShort = !!obj.isShort ?? false;
|
||||
|
||||
if (obj.getContentRecommendations) {
|
||||
this.getContentRecommendations = obj.getContentRecommendations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,20 +368,16 @@ class VideoUrlSource {
|
||||
this.url = obj.url;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
|
||||
// deprecated api conversion
|
||||
if(obj.licenseUri)
|
||||
this.drmLicenseUri = obj.licenseUri;
|
||||
|
||||
if(obj.drmLicenseUri)
|
||||
this.drmLicenseUri = obj.drmLicenseUri;
|
||||
if(obj.getLicenseRequestExecutor)
|
||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||
}
|
||||
}
|
||||
class VideoUrlWidevineSource extends VideoUrlSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
this.plugin_type = "VideoUrlWidevineSource";
|
||||
|
||||
this.licenseUri = obj.licenseUri;
|
||||
if(obj.getLicenseRequestExecutor)
|
||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||
}
|
||||
}
|
||||
class VideoUrlRangeSource extends VideoUrlSource {
|
||||
@@ -413,6 +405,16 @@ class AudioUrlSource {
|
||||
this.language = obj.language ?? Language.UNKNOWN;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
}
|
||||
}
|
||||
class AudioUrlWidevineSource extends AudioUrlSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
this.plugin_type = "AudioUrlWidevineSource";
|
||||
|
||||
this.licenseUri = obj.licenseUri;
|
||||
if(obj.getLicenseRequestExecutor)
|
||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||
|
||||
// deprecated api conversion
|
||||
if(obj.bearerToken) {
|
||||
@@ -430,19 +432,6 @@ class AudioUrlSource {
|
||||
}
|
||||
}
|
||||
}
|
||||
// deprecated api conversion
|
||||
if(obj.licenseUri)
|
||||
this.drmLicenseUri = obj.licenseUri;
|
||||
|
||||
if(obj.drmLicenseUri)
|
||||
this.drmLicenseUri = obj.drmLicenseUri;
|
||||
if(obj.getLicenseRequestExecutor)
|
||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||
}
|
||||
}
|
||||
class AudioUrlWidevineSource extends AudioUrlSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
}
|
||||
}
|
||||
class AudioUrlRangeSource extends AudioUrlSource {
|
||||
@@ -483,22 +472,16 @@ class DashSource {
|
||||
this.language = obj.language;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
if(obj.getRequestExecutor)
|
||||
this.getRequestExecutor = obj.getRequestExecutor;
|
||||
|
||||
// deprecated api conversion
|
||||
if(obj.licenseUri)
|
||||
this.drmLicenseUri = obj.licenseUri;
|
||||
|
||||
if(obj.drmLicenseUri)
|
||||
this.drmLicenseUri = obj.drmLicenseUri;
|
||||
if(obj.getLicenseRequestExecutor)
|
||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||
}
|
||||
}
|
||||
class DashWidevineSource extends DashSource {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
this.plugin_type = "DashWidevineSource";
|
||||
|
||||
this.licenseUri = obj.licenseUri;
|
||||
if(obj.getLicenseRequestExecutor)
|
||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||
}
|
||||
}
|
||||
class DashManifestRawSource {
|
||||
@@ -531,11 +514,6 @@ class DashManifestRawAudioSource {
|
||||
this.manifest = obj.manifest ?? null;
|
||||
if(obj.requestModifier)
|
||||
this.requestModifier = obj.requestModifier;
|
||||
|
||||
if(obj.drmLicenseUri)
|
||||
this.drmLicenseUri = obj.drmLicenseUri;
|
||||
if(obj.getLicenseRequestExecutor)
|
||||
this.getLicenseRequestExecutor = obj.getLicenseRequestExecutor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -644,9 +644,6 @@ class Settings : FragmentedStorageFileJson() {
|
||||
@Serializable
|
||||
class Plugins {
|
||||
|
||||
@FormField(R.string.check_disabled_plugin_updates, FieldForm.TOGGLE, R.string.check_disabled_plugin_updates_description, -1)
|
||||
var checkDisabledPluginsForUpdates: Boolean = false;
|
||||
|
||||
@FormField(R.string.clear_cookies_on_logout, FieldForm.TOGGLE, R.string.clears_cookies_when_you_log_out, 0)
|
||||
var clearCookiesOnLogout: Boolean = true;
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ 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.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
|
||||
@@ -79,36 +82,6 @@ class UISlideOverlays {
|
||||
return menu;
|
||||
}
|
||||
|
||||
fun showQueueOptionsOverlay(context: Context, container: ViewGroup) {
|
||||
UISlideOverlays.showOverlay(container, "Queue options", null, {
|
||||
|
||||
}, SlideUpMenuItem(context, R.drawable.ic_playlist, "Save as playlist", "", "Creates a new playlist with queue as videos", null, {
|
||||
val nameInput = SlideUpMenuTextInput(container.context, container.context.getString(R.string.name));
|
||||
val addPlaylistOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.create_new_playlist), container.context.getString(R.string.ok), false, nameInput);
|
||||
|
||||
addPlaylistOverlay.onOK.subscribe {
|
||||
val text = nameInput.text.trim()
|
||||
if (text.isBlank()) {
|
||||
return@subscribe;
|
||||
}
|
||||
|
||||
addPlaylistOverlay.hide();
|
||||
nameInput.deactivate();
|
||||
nameInput.clear();
|
||||
StatePlayer.instance.saveQueueAsPlaylist(text);
|
||||
UIDialogs.appToast("Playlist [${text}] created");
|
||||
};
|
||||
|
||||
addPlaylistOverlay.onCancel.subscribe {
|
||||
nameInput.deactivate();
|
||||
nameInput.clear();
|
||||
};
|
||||
|
||||
addPlaylistOverlay.show();
|
||||
nameInput.activate();
|
||||
}, false));
|
||||
}
|
||||
|
||||
fun showSubscriptionOptionsOverlay(subscription: Subscription, container: ViewGroup): SlideUpMenuOverlay {
|
||||
val items = arrayListOf<View>();
|
||||
|
||||
@@ -299,12 +272,17 @@ class UISlideOverlays {
|
||||
|
||||
}
|
||||
|
||||
fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay {
|
||||
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 masterPlaylistContent = masterPlaylistResponse.body?.string()
|
||||
@@ -365,9 +343,7 @@ class UISlideOverlays {
|
||||
call = {
|
||||
selectedVideoVariant = it
|
||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||
if (audioButtons.isEmpty()){
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
}
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
},
|
||||
invokeParent = false
|
||||
))
|
||||
@@ -387,7 +363,7 @@ class UISlideOverlays {
|
||||
|
||||
slideUpMenuOverlay.onOK.subscribe {
|
||||
//TODO: Fix SubtitleRawSource issue
|
||||
StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null);
|
||||
StateDownloads.instance.download(video, selectedVideoVariant, selectedAudioVariant, null, if (source is JSSource) source.hasRequestModifier else false);
|
||||
slideUpMenuOverlay.hide()
|
||||
}
|
||||
|
||||
@@ -449,7 +425,7 @@ class UISlideOverlays {
|
||||
}
|
||||
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
||||
listOf((if (audioSources != null) listOf(SlideUpMenuItem(
|
||||
listOf(listOf(SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
container.context.getString(R.string.none),
|
||||
@@ -462,7 +438,7 @@ class UISlideOverlays {
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
},
|
||||
invokeParent = false
|
||||
)) else listOf()) +
|
||||
)) +
|
||||
videoSources
|
||||
.filter { it.isDownloadable() }
|
||||
.map {
|
||||
@@ -507,7 +483,7 @@ class UISlideOverlays {
|
||||
)
|
||||
}
|
||||
|
||||
is IHLSManifestSource -> {
|
||||
is JSHLSManifestSource -> {
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
@@ -581,7 +557,7 @@ class UISlideOverlays {
|
||||
);
|
||||
}
|
||||
|
||||
is IHLSManifestAudioSource -> {
|
||||
is JSHLSManifestAudioSource -> {
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
@@ -646,13 +622,13 @@ class UISlideOverlays {
|
||||
|
||||
menu.onOK.subscribe {
|
||||
val sv = selectedVideo
|
||||
if (sv is IHLSManifestSource) {
|
||||
if (sv is JSHLSManifestSource) {
|
||||
showHlsPicker(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
|
||||
}
|
||||
@@ -939,7 +915,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,
|
||||
@@ -1075,9 +1051,8 @@ class UISlideOverlays {
|
||||
StatePlayer.TYPE_WATCHLATER,
|
||||
"${watchLater.size} " + container.context.getString(R.string.videos),
|
||||
tag = "watch later",
|
||||
call = {
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true))
|
||||
UIDialogs.appToast("Added to watch later", false);
|
||||
call = { StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(video), true);
|
||||
UIDialogs.appToast("Added to watch later", false);
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
+5
-2
@@ -2,6 +2,7 @@ package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
import android.net.Uri
|
||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSSource
|
||||
|
||||
class HLSVariantVideoUrlSource(
|
||||
override val name: String,
|
||||
@@ -12,7 +13,8 @@ class HLSVariantVideoUrlSource(
|
||||
override val bitrate: Int?,
|
||||
override val duration: Long,
|
||||
override val priority: Boolean,
|
||||
val url: String
|
||||
val url: String,
|
||||
val jsSource: JSSource? = null,
|
||||
) : IVideoUrlSource {
|
||||
override fun getVideoUrl(): String {
|
||||
return url
|
||||
@@ -27,7 +29,8 @@ class HLSVariantAudioUrlSource(
|
||||
override val language: String,
|
||||
override val duration: Long?,
|
||||
override val priority: Boolean,
|
||||
val url: String
|
||||
val url: String,
|
||||
val jsSource: JSSource? = null,
|
||||
) : IAudioUrlSource {
|
||||
override fun getAudioUrl(): String {
|
||||
return url
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
interface IAudioUrlWidevineSource : IAudioUrlSource, IWidevineSource
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
interface IDashManifestWidevineSource : IWidevineSource {
|
||||
val url: String
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
interface IVideoUrlWidevineSource : IVideoUrlSource, IWidevineSource
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.futo.platformplayer.api.media.models.streams.sources
|
||||
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
|
||||
interface IWidevineSource {
|
||||
val licenseUri: String
|
||||
val hasLicenseRequestExecutor: Boolean
|
||||
fun getLicenseRequestExecutor(): JSRequestExecutor?
|
||||
}
|
||||
+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
|
||||
);
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
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.IAudioUrlWidevineSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class JSAudioUrlWidevineSource : JSAudioUrlSource, IAudioUrlWidevineSource {
|
||||
override val licenseUri: String
|
||||
override val hasLicenseRequestExecutor: Boolean
|
||||
|
||||
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
|
||||
val contextName = "JSAudioUrlWidevineSource"
|
||||
val config = plugin.config
|
||||
|
||||
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||
}
|
||||
|
||||
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||
return null
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return null
|
||||
|
||||
return JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val url = getAudioUrl()
|
||||
return "(name=$name, container=$container, bitrate=$bitrate, codec=$codec, url=$url, language=$language, duration=$duration, hasLicenseRequestExecutor=${hasLicenseRequestExecutor}, licenseUri=$licenseUri)"
|
||||
}
|
||||
}
|
||||
+10
-23
@@ -2,17 +2,20 @@ 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.IAudioSource
|
||||
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.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
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.getOrDefault
|
||||
import com.futo.platformplayer.getOrNull
|
||||
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;
|
||||
@@ -26,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;
|
||||
@@ -50,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 {
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
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.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestWidevineSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrNull
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class JSDashManifestWidevineSource : IVideoUrlSource, IDashManifestSource,
|
||||
IDashManifestWidevineSource, JSSource {
|
||||
override val width: Int = 0
|
||||
override val height: Int = 0
|
||||
override val container: String = "application/dash+xml"
|
||||
override val codec: String = "Dash"
|
||||
override val name: String
|
||||
override val bitrate: Int? = null
|
||||
override val url: String
|
||||
override val duration: Long
|
||||
|
||||
override var priority: Boolean = false
|
||||
|
||||
override val licenseUri: String
|
||||
override val hasLicenseRequestExecutor: Boolean
|
||||
|
||||
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(TYPE_DASH, plugin, obj) {
|
||||
val contextName = "DashWidevineSource"
|
||||
val config = plugin.config
|
||||
name = _obj.getOrThrow(config, "name", contextName)
|
||||
url = _obj.getOrThrow(config, "url", contextName)
|
||||
duration = _obj.getOrThrow(config, "duration", contextName)
|
||||
|
||||
priority = obj.getOrNull(config, "priority", contextName) ?: false
|
||||
|
||||
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||
}
|
||||
|
||||
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||
return null
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSDashManifestWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return null
|
||||
|
||||
return JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
override fun getVideoUrl(): String {
|
||||
return url
|
||||
}
|
||||
}
|
||||
+9
-20
@@ -1,5 +1,7 @@
|
||||
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
|
||||
@@ -13,9 +15,9 @@ import com.futo.platformplayer.api.media.platforms.js.models.JSRequestModifier
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrDefault
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.orNull
|
||||
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||
|
||||
abstract class JSSource {
|
||||
protected val _plugin: JSClient;
|
||||
@@ -28,9 +30,6 @@ abstract class JSSource {
|
||||
val hasRequestExecutor: Boolean;
|
||||
private val _requestExecutor: JSRequest?;
|
||||
|
||||
val drmLicenseUri: String?
|
||||
val hasLicenseRequestExecutor: Boolean
|
||||
|
||||
val requiresCustomDatasource: Boolean get() {
|
||||
return hasRequestModifier || hasRequestExecutor;
|
||||
}
|
||||
@@ -52,9 +51,6 @@ abstract class JSSource {
|
||||
JSRequest(plugin, it, null, null, true);
|
||||
}
|
||||
hasRequestExecutor = _requestExecutor != null || obj.has("getRequestExecutor");
|
||||
|
||||
drmLicenseUri = _obj.getOrDefault(_config, "drmLicenseUri", "JSSource.drmLicenseUri", null)
|
||||
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||
}
|
||||
|
||||
fun getRequestModifier(): IRequestModifier? {
|
||||
@@ -88,19 +84,6 @@ abstract class JSSource {
|
||||
|
||||
return JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||
return null
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return null
|
||||
|
||||
return JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
fun getUnderlyingPlugin(): JSClient? {
|
||||
return _plugin;
|
||||
@@ -115,17 +98,22 @@ abstract class JSSource {
|
||||
const val TYPE_AUDIO_WITH_METADATA = "AudioUrlRangeSource";
|
||||
const val TYPE_VIDEO_WITH_METADATA = "VideoUrlRangeSource";
|
||||
const val TYPE_DASH = "DashSource";
|
||||
const val TYPE_DASH_WIDEVINE = "DashWidevineSource";
|
||||
const val TYPE_DASH_RAW = "DashRawSource";
|
||||
const val TYPE_DASH_RAW_AUDIO = "DashRawAudioSource";
|
||||
const val TYPE_HLS = "HLSSource";
|
||||
const val TYPE_AUDIOURL_WIDEVINE = "AudioUrlWidevineSource"
|
||||
const val TYPE_VIDEOURL_WIDEVINE = "VideoUrlWidevineSource"
|
||||
|
||||
fun fromV8VideoNullable(plugin: JSClient, obj: V8Value?) : IVideoSource? = obj.orNull { fromV8Video(plugin, it as V8ValueObject) };
|
||||
fun fromV8Video(plugin: JSClient, obj: V8ValueObject) : IVideoSource? {
|
||||
val type = obj.getString("plugin_type");
|
||||
return when(type) {
|
||||
TYPE_VIDEOURL -> JSVideoUrlSource(plugin, obj);
|
||||
TYPE_VIDEOURL_WIDEVINE -> JSVideoUrlWidevineSource(plugin, obj);
|
||||
TYPE_VIDEO_WITH_METADATA -> JSVideoUrlRangeSource(plugin, obj);
|
||||
TYPE_HLS -> fromV8HLS(plugin, obj);
|
||||
TYPE_DASH_WIDEVINE -> JSDashManifestWidevineSource(plugin, obj)
|
||||
TYPE_DASH -> fromV8Dash(plugin, obj);
|
||||
TYPE_DASH_RAW -> fromV8DashRaw(plugin, obj);
|
||||
else -> {
|
||||
@@ -147,6 +135,7 @@ abstract class JSSource {
|
||||
TYPE_HLS -> JSHLSManifestAudioSource.fromV8HLS(plugin, obj);
|
||||
TYPE_AUDIOURL -> JSAudioUrlSource(plugin, obj);
|
||||
TYPE_DASH_RAW_AUDIO -> fromV8DashRawAudio(plugin, obj);
|
||||
TYPE_AUDIOURL_WIDEVINE -> JSAudioUrlWidevineSource(plugin, obj);
|
||||
TYPE_AUDIO_WITH_METADATA -> JSAudioUrlRangeSource(plugin, obj);
|
||||
else -> {
|
||||
Logger.w("JSSource", "Unknown audio type ${type}");
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
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.IVideoUrlWidevineSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.JSRequestExecutor
|
||||
import com.futo.platformplayer.engine.V8Plugin
|
||||
import com.futo.platformplayer.getOrThrow
|
||||
|
||||
class JSVideoUrlWidevineSource : JSVideoUrlSource, IVideoUrlWidevineSource {
|
||||
override val licenseUri: String
|
||||
override val hasLicenseRequestExecutor: Boolean
|
||||
|
||||
@Suppress("ConvertSecondaryConstructorToPrimary")
|
||||
constructor(plugin: JSClient, obj: V8ValueObject) : super(plugin, obj) {
|
||||
val contextName = "JSAudioUrlWidevineSource"
|
||||
val config = plugin.config
|
||||
|
||||
licenseUri = _obj.getOrThrow(config, "licenseUri", contextName)
|
||||
hasLicenseRequestExecutor = obj.has("getLicenseRequestExecutor")
|
||||
}
|
||||
|
||||
override fun getLicenseRequestExecutor(): JSRequestExecutor? {
|
||||
if (!hasLicenseRequestExecutor || _obj.isClosed)
|
||||
return null
|
||||
|
||||
val result = V8Plugin.catchScriptErrors<Any>(_config, "[${_config.name}] JSAudioUrlWidevineSource", "obj.getLicenseRequestExecutor()") {
|
||||
_obj.invoke("getLicenseRequestExecutor", arrayOf<Any>())
|
||||
}
|
||||
|
||||
if (result !is V8ValueObject)
|
||||
return null
|
||||
|
||||
return JSRequestExecutor(_plugin, result)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val url = getVideoUrl()
|
||||
return "(width=$width, height=$height, container=$container, codec=$codec, name=$name, bitrate=$bitrate, duration=$duration, url=$url, hasLicenseRequestExecutor=$hasLicenseRequestExecutor, licenseUri=$licenseUri)"
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -10,11 +10,10 @@ 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.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
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||
@@ -28,12 +27,11 @@ 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.JSDashManifestRawAudioSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSDashManifestRawSource
|
||||
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
|
||||
@@ -44,9 +42,9 @@ 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 +57,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Transient
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
@@ -69,8 +68,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 {
|
||||
@@ -93,10 +94,10 @@ class VideoDownload {
|
||||
var audioSource: AudioUrlSource?;
|
||||
@Contextual
|
||||
@Transient
|
||||
val videoSourceToUse: IVideoSource? get () = if(requiresLiveVideoSource) videoSourceLive as IVideoSource? else videoSource as IVideoSource?;
|
||||
val videoSourceToUse: IVideoSource? get () = if (videoSource?.container == "application/vnd.apple.mpegurl") videoSource else if(requiresLiveVideoSource) videoSourceLive as IVideoSource? else videoSource as IVideoSource?;
|
||||
@Contextual
|
||||
@Transient
|
||||
val audioSourceToUse: IAudioSource? get () = if(requiresLiveAudioSource) audioSourceLive as IAudioSource? else audioSource as IAudioSource?;
|
||||
val audioSourceToUse: IAudioSource? get () = if (audioSource?.container == "application/vnd.apple.mpegurl") audioSource else if(requiresLiveAudioSource) audioSourceLive as IAudioSource? else audioSource as IAudioSource?;
|
||||
|
||||
var requireVideoSource: Boolean = false;
|
||||
var requireAudioSource: Boolean = false;
|
||||
@@ -131,6 +132,9 @@ class VideoDownload {
|
||||
var hasVideoRequestExecutor: Boolean = false;
|
||||
var hasAudioRequestExecutor: Boolean = false;
|
||||
|
||||
var hasVideoRequestModifier: Boolean = false;
|
||||
var hasAudioRequestModifier: Boolean = false;
|
||||
|
||||
var progress: Double = 0.0;
|
||||
var isCancelled = false;
|
||||
|
||||
@@ -141,17 +145,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;
|
||||
@@ -186,7 +184,7 @@ class VideoDownload {
|
||||
this.requireAudioSource = targetBitrate != null; //TODO: May not be a valid check.. can only be determined after live fetch?
|
||||
this.requiredCheck = optionalSources;
|
||||
}
|
||||
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
||||
constructor(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?, hasHLSRequestModifier: Boolean = false) {
|
||||
this.video = SerializedPlatformVideo.fromVideo(video);
|
||||
this.videoDetails = SerializedPlatformVideoDetails.fromVideo(video, if (subtitleSource != null) listOf(subtitleSource) else listOf());
|
||||
this.videoSource = if(videoSource is IVideoUrlSource) VideoUrlSource.fromUrlSource(videoSource) else null;
|
||||
@@ -197,8 +195,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 || hasHLSRequestModifier
|
||||
this.hasAudioRequestModifier = audioSource is JSSource && audioSource.hasRequestModifier || hasHLSRequestModifier
|
||||
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || this.hasVideoRequestModifier || (videoSource is JSDashManifestRawSource && videoSource.hasGenerate);
|
||||
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || this.hasAudioRequestModifier || (audioSource is JSDashManifestRawAudioSource && audioSource.hasGenerate);
|
||||
this.targetVideoName = videoSource?.name;
|
||||
this.targetAudioName = audioSource?.name;
|
||||
this.targetPixelCount = if(videoSource != null) (videoSource.width * videoSource.height).toLong() else null;
|
||||
@@ -241,13 +241,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");
|
||||
@@ -293,9 +291,14 @@ class VideoDownload {
|
||||
if(videoSource == null && targetPixelCount != null) {
|
||||
val videoSources = arrayListOf<IVideoSource>()
|
||||
for (source in original.video.videoSources) {
|
||||
if (source is IHLSManifestSource) {
|
||||
if (source is JSHLSManifestSource) {
|
||||
try {
|
||||
val playlistResponse = client.get(source.url)
|
||||
val playlistResponse = if (source.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) {
|
||||
@@ -328,6 +331,10 @@ class VideoDownload {
|
||||
if(original.video.videoSources.size == 0)
|
||||
requireVideoSource = false;
|
||||
}
|
||||
else if (vsource is HLSVariantVideoUrlSource && vsource.container == "application/vnd.apple.mpegurl") {
|
||||
videoSource = VideoUrlSource.fromUrlSource(vsource)
|
||||
videoSourceLive = vsource.jsSource!!
|
||||
}
|
||||
else if(vsource is IVideoUrlSource)
|
||||
videoSource = VideoUrlSource.fromUrlSource(vsource)
|
||||
else if(vsource is JSSource && requiresLiveVideoSource)
|
||||
@@ -341,9 +348,14 @@ class VideoDownload {
|
||||
val video = original.video
|
||||
if (video is VideoUnMuxedSourceDescriptor) {
|
||||
for (source in video.audioSources) {
|
||||
if (source is IHLSManifestAudioSource) {
|
||||
if (source is JSHLSManifestAudioSource) {
|
||||
try {
|
||||
val playlistResponse = client.get(source.url)
|
||||
val playlistResponse = if (source.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) {
|
||||
@@ -358,6 +370,26 @@ class VideoDownload {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (source in video.videoSources) {
|
||||
if (source is JSHLSManifestSource) {
|
||||
try {
|
||||
val playlistResponse = if (source.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))
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.i(TAG, "Failed to get HLS audio sources", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var asource: IAudioSource? = null;
|
||||
if(targetAudioName != null) {
|
||||
@@ -375,8 +407,8 @@ class VideoDownload {
|
||||
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);
|
||||
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || asource.hasRequestExecutor;
|
||||
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
||||
}
|
||||
|
||||
if(asource == null) {
|
||||
@@ -384,6 +416,10 @@ class VideoDownload {
|
||||
if(!original.video.isUnMuxed || original.video.videoSources.size == 0)
|
||||
requireVideoSource = false;
|
||||
}
|
||||
else if (asource is HLSVariantAudioUrlSource && asource.container == "application/vnd.apple.mpegurl") {
|
||||
audioSource = AudioUrlSource.fromUrlSource(asource)
|
||||
audioSourceLive = asource.jsSource!!
|
||||
}
|
||||
else if(asource is IAudioUrlSource)
|
||||
audioSource = AudioUrlSource.fromUrlSource(asource)
|
||||
else if(asource is JSSource && requiresLiveAudioSource)
|
||||
@@ -418,13 +454,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,9 +502,9 @@ class VideoDownload {
|
||||
}
|
||||
}
|
||||
|
||||
if(actualVideoSource is IVideoUrlSource)
|
||||
if(videoSource is IVideoUrlSource)
|
||||
videoFileSize = when (videoSource!!.container) {
|
||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Video", client, if (actualVideoSource is JSSource) actualVideoSource else null, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||
else -> downloadFileSource("Video", client, videoSource!!.getVideoUrl(), File(downloadDir, videoFileName!!), progressCallback)
|
||||
}
|
||||
else if(actualVideoSource is JSDashManifestRawSource) {
|
||||
@@ -508,9 +542,9 @@ class VideoDownload {
|
||||
}
|
||||
}
|
||||
|
||||
if(actualAudioSource is IAudioUrlSource)
|
||||
if(audioSource is IAudioUrlSource)
|
||||
audioFileSize = when (audioSource!!.container) {
|
||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||
"application/vnd.apple.mpegurl" -> downloadHlsSource(context, "Audio", client, if (audioSourceLive is JSSource) audioSourceLive else null, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||
else -> downloadFileSource("Audio", client, audioSource!!.getAudioUrl(), File(downloadDir, audioFileName!!), progressCallback)
|
||||
}
|
||||
else if(actualAudioSource is JSDashManifestRawAudioSource) {
|
||||
@@ -564,7 +598,15 @@ class VideoDownload {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadHlsSource(context: Context, name: String, client: ManagedHttpClient, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
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?, hlsUrl: String, targetFile: File, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
if(targetFile.exists())
|
||||
targetFile.delete();
|
||||
|
||||
@@ -572,13 +614,33 @@ class VideoDownload {
|
||||
|
||||
val segmentFiles = arrayListOf<File>()
|
||||
try {
|
||||
val response = client.get(hlsUrl)
|
||||
val response = if (source is JSSource && source.hasRequestModifier) {
|
||||
val request = source.getRequestModifier()!!.modifyRequest(hlsUrl, mapOf())
|
||||
client.get(request.url!!, request.headers.toMutableMap())
|
||||
} else {
|
||||
client.get(hlsUrl)
|
||||
}
|
||||
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 decryptionInfo: DecryptionInfo? = if (variantPlaylist.decryptionInfo != null) {
|
||||
val keyResponse = if (source is JSSource && 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 +652,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)
|
||||
@@ -630,10 +692,8 @@ class VideoDownload {
|
||||
|
||||
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}\""
|
||||
val cmd =
|
||||
"-i \"concat:${segmentFiles.joinToString("|")}\" -c copy \"${targetFile.absolutePath}\""
|
||||
|
||||
val statisticsCallback = StatisticsCallback { _ ->
|
||||
//TODO: Show progress?
|
||||
@@ -643,7 +703,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 +710,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 +829,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 +856,31 @@ class VideoDownload {
|
||||
}
|
||||
return sourceLength!!;
|
||||
}
|
||||
private fun downloadSource_Sequential(client: ManagedHttpClient, fileStream: FileOutputStream, url: String, onProgress: (Long, Long, Long) -> Unit): Long {
|
||||
|
||||
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 +900,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 +912,7 @@ class VideoDownload {
|
||||
if (read < 0)
|
||||
break;
|
||||
|
||||
fileStream.write(buffer, 0, read);
|
||||
segmentBuffer.write(buffer, 0, read);
|
||||
|
||||
totalRead += read;
|
||||
|
||||
@@ -854,6 +938,14 @@ 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;
|
||||
}
|
||||
@@ -1072,8 +1164,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)
|
||||
@@ -1154,7 +1246,7 @@ class VideoDownload {
|
||||
else if (container.contains("video/x-matroska"))
|
||||
return "mkv";
|
||||
else
|
||||
return "video";//throw IllegalStateException("Unknown container: " + container)
|
||||
return "video";
|
||||
}
|
||||
|
||||
fun audioContainerToExtension(container: String): String {
|
||||
@@ -1165,11 +1257,11 @@ class VideoDownload {
|
||||
else if (container.contains("audio/mp3"))
|
||||
return "mp3";
|
||||
else if (container.contains("audio/webm"))
|
||||
return "webm";
|
||||
return "webma";
|
||||
else if (container == "application/vnd.apple.mpegurl")
|
||||
return "mp4a";
|
||||
return "mp4";
|
||||
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)
|
||||
|
||||
+2
-2
@@ -238,8 +238,8 @@ class ChannelFragment : MainFragment() {
|
||||
}
|
||||
adapter.onAddToWatchLaterClicked.subscribe { content ->
|
||||
if (content is IPlatformVideo) {
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true))
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(content), true)
|
||||
UIDialogs.toast("Added to watch later\n[${content.name}]")
|
||||
}
|
||||
}
|
||||
adapter.onUrlClicked.subscribe { url ->
|
||||
|
||||
+2
-2
@@ -82,8 +82,8 @@ abstract class ContentFeedView<TFragment> : FeedView<TFragment, IPlatformContent
|
||||
};
|
||||
adapter.onAddToWatchLaterClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
}
|
||||
};
|
||||
adapter.onLongPress.subscribe(this) {
|
||||
|
||||
+1
-8
@@ -10,7 +10,6 @@ import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.Spinner
|
||||
import android.widget.TextView
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
@@ -27,7 +26,6 @@ class CreatorsFragment : MainFragment() {
|
||||
private var _overlayContainer: FrameLayout? = null;
|
||||
private var _containerSearch: FrameLayout? = null;
|
||||
private var _editSearch: EditText? = null;
|
||||
private var _textMeta: TextView? = null;
|
||||
private var _buttonClearSearch: ImageButton? = null
|
||||
|
||||
override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
@@ -36,7 +34,6 @@ class CreatorsFragment : MainFragment() {
|
||||
val editSearch: EditText = view.findViewById(R.id.edit_search);
|
||||
val buttonClearSearch: ImageButton = view.findViewById(R.id.button_clear_search)
|
||||
_editSearch = editSearch
|
||||
_textMeta = view.findViewById(R.id.text_meta);
|
||||
_buttonClearSearch = buttonClearSearch
|
||||
buttonClearSearch.setOnClickListener {
|
||||
editSearch.text.clear()
|
||||
@@ -44,11 +41,7 @@ class CreatorsFragment : MainFragment() {
|
||||
_buttonClearSearch?.visibility = View.INVISIBLE;
|
||||
}
|
||||
|
||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription)) { subs ->
|
||||
_textMeta?.let {
|
||||
it.text = "${subs.size} creator${if(subs.size > 1) "s" else ""}";
|
||||
}
|
||||
};
|
||||
val adapter = SubscriptionAdapter(inflater, getString(R.string.confirm_delete_subscription));
|
||||
adapter.onClick.subscribe { platformUser -> navigate<ChannelFragment>(platformUser) };
|
||||
adapter.onSettings.subscribe { sub -> _overlayContainer?.let { UISlideOverlays.showSubscriptionOptionsOverlay(sub, it) } }
|
||||
|
||||
|
||||
+1
-2
@@ -22,7 +22,6 @@ import com.futo.platformplayer.states.StateDownloads
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.states.StatePlaylists
|
||||
import com.futo.platformplayer.toHumanBytesSize
|
||||
import com.futo.platformplayer.toHumanDuration
|
||||
import com.futo.platformplayer.views.AnyInsertedAdapterView
|
||||
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
|
||||
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
|
||||
@@ -216,7 +215,7 @@ class DownloadsFragment : MainFragment() {
|
||||
_listDownloadedHeader.visibility = GONE;
|
||||
} else {
|
||||
_listDownloadedHeader.visibility = VISIBLE;
|
||||
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()}${if(downloaded.size > 0) ", ${downloaded.sumOf { it.duration }.toHumanDuration(false)}" else ""})";
|
||||
_listDownloadedMeta.text = "(${downloaded.size} ${context.getString(R.string.videos).lowercase()})";
|
||||
}
|
||||
|
||||
lastDownloads = downloaded;
|
||||
|
||||
-10
@@ -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;
|
||||
@@ -185,7 +176,6 @@ class PlaylistFragment : MainFragment() {
|
||||
setVideos(parameter.videos, true)
|
||||
setMetadata(parameter.videos.size, parameter.videos.sumOf { it.duration })
|
||||
setButtonDownloadVisible(true)
|
||||
setButtonExportVisible(false)
|
||||
setButtonEditVisible(true)
|
||||
|
||||
if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) {
|
||||
|
||||
+1
-1
@@ -556,7 +556,7 @@ class SourceDetailFragment : MainFragment() {
|
||||
Logger.i(TAG, "Downloaded source config ($sourceUrl):\n${configJson}");
|
||||
|
||||
val config = SourcePluginConfig.fromJson(configJson);
|
||||
if (config.version <= c.version) {
|
||||
if (config.version <= c.version && config.name != "Youtube") {
|
||||
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;
|
||||
|
||||
+3
-25
@@ -31,13 +31,11 @@ import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.datasource.HttpDataSource
|
||||
import androidx.media3.exoplayer.analytics.AnalyticsListener
|
||||
import androidx.media3.ui.PlayerControlView
|
||||
import androidx.media3.ui.TimeBar
|
||||
import com.bumptech.glide.Glide
|
||||
@@ -434,21 +432,6 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
_player.attachPlayer();
|
||||
|
||||
_player.exoPlayer?.player?.addAnalyticsListener(object : AnalyticsListener {
|
||||
override fun onDrmSessionManagerError(
|
||||
eventTime: AnalyticsListener.EventTime, error: Exception
|
||||
) {
|
||||
super.onDrmSessionManagerError(eventTime, error)
|
||||
UIDialogs.showDialog(context, R.drawable.ic_lock, context.getString(R.string.drm_not_supported), context.getString(R.string.open_and_play_in_mobile_browser), null, 1, UIDialogs.Action(context.getString(R.string.open_in_browser), {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(video?.url))
|
||||
val options =
|
||||
android.app.ActivityOptions.makeCustomAnimation(context, android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
startActivity(context, intent, options.toBundle())
|
||||
}, UIDialogs.ActionStyle.NONE), UIDialogs.Action(context.getString(R.string.close), {}, UIDialogs.ActionStyle.PRIMARY)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
_container_content_liveChat.onRaidNow.subscribe {
|
||||
StatePlayer.instance.clearQueue();
|
||||
fragment.navigate<VideoDetailFragment>(it.targetUrl);
|
||||
@@ -899,7 +882,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_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);
|
||||
@@ -2612,13 +2595,8 @@ class VideoDetailView : ConstraintLayout {
|
||||
|
||||
onAddToWatchLaterClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
if(StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true))
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
}
|
||||
}
|
||||
onAddToQueueClicked.subscribe(this) {
|
||||
if(it is IPlatformVideo) {
|
||||
StatePlayer.instance.addToQueue(it);
|
||||
StatePlaylists.instance.addToWatchLater(SerializedPlatformVideo.fromVideo(it), true);
|
||||
UIDialogs.toast("Added to watch later\n[${it.name}]");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
-10
@@ -34,7 +34,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 +54,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 +68,6 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
buttonShuffle.setOnClickListener { onShuffleClick(); };
|
||||
|
||||
_buttonEdit.setOnClickListener { onEditClick(); };
|
||||
setButtonExportVisible(false);
|
||||
setButtonDownloadVisible(canEdit());
|
||||
|
||||
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
|
||||
@@ -112,7 +108,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 +117,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 +125,6 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
}
|
||||
}
|
||||
else {
|
||||
setButtonExportVisible(false);
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download);
|
||||
_buttonDownload.setOnClickListener {
|
||||
onDownload();
|
||||
@@ -178,9 +171,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;
|
||||
|
||||
@@ -17,11 +17,11 @@ import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudi
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
|
||||
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.IWidevineSource
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSAudioUrlRangeSource
|
||||
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.JSSource
|
||||
import com.futo.platformplayer.api.media.platforms.js.models.sources.JSVideoUrlRangeSource
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.others.Language
|
||||
@@ -45,8 +45,10 @@ class VideoHelper {
|
||||
|
||||
return false
|
||||
}
|
||||
fun isDownloadable(source: IVideoSource) = (source is IVideoUrlSource || source is IHLSManifestSource || source is JSDashManifestRawSource) && !(source is JSSource && source.drmLicenseUri != null)
|
||||
fun isDownloadable(source: IAudioSource) = (source is IAudioUrlSource || source is IHLSManifestAudioSource || source is JSDashManifestRawAudioSource) && !(source is JSSource && source.drmLicenseUri != null)
|
||||
|
||||
fun isDownloadable(source: IVideoSource) = (source is IVideoUrlSource || source is IHLSManifestSource || source is JSDashManifestRawSource) && 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);
|
||||
fun selectBestVideoSource(sources: Iterable<IVideoSource>, desiredPixelCount : Int, prefContainers : Array<String>) : IVideoSource? {
|
||||
val targetVideo = if(desiredPixelCount > 0) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantSubtit
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
||||
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.platforms.js.models.sources.JSSource
|
||||
import com.futo.platformplayer.toYesNo
|
||||
import com.futo.platformplayer.yesNoToBoolean
|
||||
import java.net.URI
|
||||
@@ -61,7 +62,28 @@ 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,14 +108,14 @@ 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> {
|
||||
fun parseAndGetVideoSources(source: JSSource, content: String, url: String): List<HLSVariantVideoUrlSource> {
|
||||
val masterPlaylist: MasterPlaylist
|
||||
try {
|
||||
masterPlaylist = parseMasterPlaylist(content, url)
|
||||
return masterPlaylist.getVideoSources()
|
||||
return masterPlaylist.getVideoSources(source)
|
||||
} catch (e: Throwable) {
|
||||
if (content.lines().any { it.startsWith("#EXTINF:") }) {
|
||||
return if (source is IHLSManifestSource) {
|
||||
@@ -109,11 +131,11 @@ class HLS {
|
||||
}
|
||||
}
|
||||
|
||||
fun parseAndGetAudioSources(source: Any, content: String, url: String): List<HLSVariantAudioUrlSource> {
|
||||
fun parseAndGetAudioSources(source: JSSource, content: String, url: String): List<HLSVariantAudioUrlSource> {
|
||||
val masterPlaylist: MasterPlaylist
|
||||
try {
|
||||
masterPlaylist = parseMasterPlaylist(content, url)
|
||||
return masterPlaylist.getAudioSources()
|
||||
return masterPlaylist.getAudioSources(source)
|
||||
} catch (e: Throwable) {
|
||||
if (content.lines().any { it.startsWith("#EXTINF:") }) {
|
||||
return if (source is IHLSManifestSource) {
|
||||
@@ -317,7 +339,7 @@ class HLS {
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
fun getVideoSources(): List<HLSVariantVideoUrlSource> {
|
||||
fun getVideoSources(source: JSSource? = null): List<HLSVariantVideoUrlSource> {
|
||||
return variantPlaylistsRefs.map {
|
||||
var width: Int? = null
|
||||
var height: Int? = null
|
||||
@@ -328,11 +350,11 @@ class HLS {
|
||||
}
|
||||
|
||||
val suffix = listOf(it.streamInfo.video, it.streamInfo.codecs).mapNotNull { x -> x?.ifEmpty { null } }.joinToString(", ")
|
||||
HLSVariantVideoUrlSource(suffix, width ?: 0, height ?: 0, "application/vnd.apple.mpegurl", it.streamInfo.codecs ?: "", it.streamInfo.bandwidth, 0, false, it.url)
|
||||
HLSVariantVideoUrlSource(suffix, width ?: 0, height ?: 0, "application/vnd.apple.mpegurl", it.streamInfo.codecs ?: "", it.streamInfo.bandwidth, 0, false, it.url, source)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAudioSources(): List<HLSVariantAudioUrlSource> {
|
||||
fun getAudioSources(source: JSSource? = null): List<HLSVariantAudioUrlSource> {
|
||||
return mediaRenditions.mapNotNull {
|
||||
if (it.uri == null) {
|
||||
return@mapNotNull null
|
||||
@@ -340,7 +362,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, "application/vnd.apple.mpegurl", "", it.language ?: "", null, false, it.uri, source)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -368,6 +390,11 @@ class HLS {
|
||||
}
|
||||
}
|
||||
|
||||
data class DecryptionInfo(
|
||||
val keyUrl: String,
|
||||
val iv: String
|
||||
)
|
||||
|
||||
data class VariantPlaylist(
|
||||
val version: Int?,
|
||||
val targetDuration: Int?,
|
||||
@@ -376,7 +403,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
|
||||
@@ -48,17 +46,6 @@ class StateDownloads {
|
||||
private val _downloadsStat = StatFs(_downloadsDirectory.absolutePath);
|
||||
|
||||
private val _downloaded = FragmentedStorage.storeJson<VideoLocal>("downloaded")
|
||||
.withOnModified({
|
||||
synchronized(_downloadedSet) {
|
||||
if(!_downloadedSet.contains(it.id))
|
||||
_downloadedSet.add(it.id);
|
||||
}
|
||||
}, {
|
||||
synchronized(_downloadedSet) {
|
||||
if(_downloadedSet.contains(it.id))
|
||||
_downloadedSet.remove(it.id);
|
||||
}
|
||||
})
|
||||
.load()
|
||||
.apply { afterLoadingDownloaded(this) };
|
||||
private val _downloading = FragmentedStorage.storeJson<VideoDownload>("downloading")
|
||||
@@ -98,6 +85,9 @@ class StateDownloads {
|
||||
Logger.i("StateDownloads", "Deleting local video ${id.value}");
|
||||
val downloaded = getCachedVideo(id);
|
||||
if(downloaded != null) {
|
||||
synchronized(_downloadedSet) {
|
||||
_downloadedSet.remove(id);
|
||||
}
|
||||
_downloaded.delete(downloaded);
|
||||
}
|
||||
onDownloadedChanged.emit();
|
||||
@@ -271,6 +261,9 @@ class StateDownloads {
|
||||
if(existing.groupID == null) {
|
||||
existing.groupID = VideoDownload.GROUP_WATCHLATER;
|
||||
existing.groupType = VideoDownload.GROUP_WATCHLATER;
|
||||
synchronized(_downloadedSet) {
|
||||
_downloadedSet.add(existing.id);
|
||||
}
|
||||
_downloaded.save(existing);
|
||||
}
|
||||
}
|
||||
@@ -313,6 +306,9 @@ class StateDownloads {
|
||||
if(existing.groupID == null) {
|
||||
existing.groupID = playlist.id;
|
||||
existing.groupType = VideoDownload.GROUP_PLAYLIST;
|
||||
synchronized(_downloadedSet) {
|
||||
_downloadedSet.add(existing.id);
|
||||
}
|
||||
_downloaded.save(existing);
|
||||
}
|
||||
}
|
||||
@@ -340,8 +336,8 @@ class StateDownloads {
|
||||
fun download(video: IPlatformVideo, targetPixelcount: Long?, targetBitrate: Long?) {
|
||||
download(VideoDownload(video, targetPixelcount, targetBitrate));
|
||||
}
|
||||
fun download(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?) {
|
||||
download(VideoDownload(video, videoSource, audioSource, subtitleSource));
|
||||
fun download(video: IPlatformVideoDetails, videoSource: IVideoSource?, audioSource: IAudioSource?, subtitleSource: SubtitleRawSource?, hasRequestModifier: Boolean = false) {
|
||||
download(VideoDownload(video, videoSource, audioSource, subtitleSource, hasRequestModifier));
|
||||
}
|
||||
|
||||
private fun download(videoState: VideoDownload, notify: Boolean = true) {
|
||||
@@ -470,65 +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 playlist = StatePlaylists.instance.getPlaylist(playlistId);
|
||||
var localVideos = StateDownloads.instance.getDownloadedVideosPlaylist(playlistId);
|
||||
if(playlist != null) {
|
||||
val missing = playlist.videos
|
||||
.filter { vid -> !localVideos.any { it.id.value == null || it.id.value == vid.id.value } }
|
||||
.map { getCachedVideo(it.id) }
|
||||
.filterNotNull();
|
||||
if(missing.size > 0)
|
||||
localVideos = localVideos + missing;
|
||||
};
|
||||
|
||||
var lastNotifyTime = -1L;
|
||||
|
||||
UIDialogs.showDialogProgress(context) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
it.setText("Exporting videos..");
|
||||
var i = 0;
|
||||
var success = 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);
|
||||
success++;
|
||||
} 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 (${success} videos${if(i < success) ", ${i} errors" else ""})");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
|
||||
var lastNotifyTime = -1L;
|
||||
|
||||
@@ -540,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)
|
||||
|
||||
@@ -13,7 +13,6 @@ import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
|
||||
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
|
||||
import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.constructs.Event1
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
@@ -131,12 +130,6 @@ class StatePlayer {
|
||||
closeMediaSession();
|
||||
}
|
||||
|
||||
fun saveQueueAsPlaylist(name: String){
|
||||
val videos = _queue.toList();
|
||||
val playlist = Playlist(name, videos.map { SerializedPlatformVideo.fromVideo(it) });
|
||||
StatePlaylists.instance.createOrUpdatePlaylist(playlist);
|
||||
}
|
||||
|
||||
//Notifications
|
||||
fun hasMediaSession() : Boolean {
|
||||
return MediaPlaybackService.getService() != null;
|
||||
|
||||
@@ -177,11 +177,8 @@ class StatePlaylists {
|
||||
StateDownloads.instance.checkForOutdatedPlaylistVideos(VideoDownload.GROUP_WATCHLATER);
|
||||
}
|
||||
}
|
||||
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1): Boolean {
|
||||
var wasNew = false;
|
||||
fun addToWatchLater(video: SerializedPlatformVideo, isUserInteraction: Boolean = false, orderPosition: Int = -1) {
|
||||
synchronized(_watchlistStore) {
|
||||
if(!_watchlistStore.hasItem { it.url == video.url })
|
||||
wasNew = true;
|
||||
_watchlistStore.saveAsync(video);
|
||||
if(orderPosition == -1)
|
||||
_watchlistOrderStore.set(*(listOf(video.url) + _watchlistOrderStore.values) .toTypedArray());
|
||||
@@ -201,7 +198,6 @@ class StatePlaylists {
|
||||
}
|
||||
|
||||
StateDownloads.instance.checkForOutdatedPlaylists();
|
||||
return wasNew;
|
||||
}
|
||||
|
||||
fun getLastPlayedPlaylist() : Playlist? {
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.futo.platformplayer.states
|
||||
|
||||
import android.content.Context
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.Settings
|
||||
import com.futo.platformplayer.UIDialogs
|
||||
import com.futo.platformplayer.activities.LoginActivity
|
||||
import com.futo.platformplayer.api.http.ManagedHttpClient
|
||||
@@ -102,8 +101,6 @@ class StatePlugins {
|
||||
if (availableClient !is JSClient) {
|
||||
continue
|
||||
}
|
||||
if(!Settings.instance.plugins.checkDisabledPluginsForUpdates && !StatePlatform.instance.isClientEnabled(availableClient.id))
|
||||
continue;
|
||||
|
||||
val newConfig = checkForUpdates(availableClient.config);
|
||||
if (newConfig != null) {
|
||||
|
||||
@@ -33,9 +33,6 @@ class ManagedStore<T>{
|
||||
|
||||
val className: String? get() = _class.classifier?.assume<KClass<*>>()?.simpleName;
|
||||
|
||||
private var _onModificationCreate: ((T) -> Unit)? = null;
|
||||
private var _onModificationDelete: ((T) -> Unit)? = null;
|
||||
|
||||
val name: String;
|
||||
|
||||
constructor(name: String, dir: File, clazz: KType, serializer: StoreSerializer<T>, niceName: String? = null) {
|
||||
@@ -65,12 +62,6 @@ class ManagedStore<T>{
|
||||
return this;
|
||||
}
|
||||
|
||||
fun withOnModified(created: (T)->Unit, deleted: (T)->Unit): ManagedStore<T> {
|
||||
_onModificationCreate = created;
|
||||
_onModificationDelete = deleted;
|
||||
return this;
|
||||
}
|
||||
|
||||
fun load(): ManagedStore<T> {
|
||||
synchronized(_files) {
|
||||
_files.clear();
|
||||
@@ -274,7 +265,6 @@ class ManagedStore<T>{
|
||||
file = saveNew(obj);
|
||||
if(_reconstructStore != null && (_reconstructStore!!.backupOnCreate || withReconstruction))
|
||||
saveReconstruction(file, obj);
|
||||
_onModificationCreate?.invoke(obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,7 +300,6 @@ class ManagedStore<T>{
|
||||
_files.remove(item);
|
||||
Logger.v(TAG, "Deleting file ${logName(file.id)}");
|
||||
file.delete();
|
||||
_onModificationDelete?.invoke(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
||||
private lateinit var _sortedDataset: List<Subscription>;
|
||||
private val _inflater: LayoutInflater;
|
||||
private val _confirmationMessage: String;
|
||||
private val _onDatasetChanged: ((List<Subscription>)->Unit)?;
|
||||
|
||||
var onClick = Event1<Subscription>();
|
||||
var onSettings = Event1<Subscription>();
|
||||
@@ -31,10 +30,9 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
||||
updateDataset();
|
||||
}
|
||||
|
||||
constructor(inflater: LayoutInflater, confirmationMessage: String, onDatasetChanged: ((List<Subscription>)->Unit)? = null) : super() {
|
||||
constructor(inflater: LayoutInflater, confirmationMessage: String) : super() {
|
||||
_inflater = inflater;
|
||||
_confirmationMessage = confirmationMessage;
|
||||
_onDatasetChanged = onDatasetChanged;
|
||||
|
||||
StateSubscriptions.instance.onSubscriptionsChanged.subscribe { _, _ -> if(Looper.myLooper() != Looper.getMainLooper())
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { updateDataset() }
|
||||
@@ -80,8 +78,6 @@ class SubscriptionAdapter : RecyclerView.Adapter<SubscriptionViewHolder> {
|
||||
.filter { (queryLower.isNullOrBlank() || it.channel.name.lowercase().contains(queryLower)) }
|
||||
.toList();
|
||||
|
||||
_onDatasetChanged?.invoke(_sortedDataset);
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ open class PreviewVideoView : LinearLayout {
|
||||
|
||||
fun hideAddTo() {
|
||||
_button_add_to.visibility = View.GONE
|
||||
//_button_add_to_queue.visibility = View.GONE
|
||||
_button_add_to_queue.visibility = View.GONE
|
||||
}
|
||||
|
||||
protected open fun inflate(feedStyle: FeedStyle) {
|
||||
|
||||
@@ -2,26 +2,16 @@ package com.futo.platformplayer.views.overlays
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import com.futo.platformplayer.states.StatePlayer
|
||||
import com.futo.platformplayer.R
|
||||
import com.futo.platformplayer.UISlideOverlays
|
||||
import com.futo.platformplayer.constructs.Event0
|
||||
import com.futo.platformplayer.views.lists.VideoListEditorView
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay
|
||||
import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuTextInput
|
||||
|
||||
class QueueEditorOverlay : LinearLayout {
|
||||
|
||||
private val _topbar : OverlayTopbar;
|
||||
private val _editor : VideoListEditorView;
|
||||
private val _btnSettings: ImageView;
|
||||
|
||||
private val _overlayContainer: FrameLayout;
|
||||
|
||||
|
||||
val onClose = Event0();
|
||||
|
||||
@@ -29,9 +19,6 @@ class QueueEditorOverlay : LinearLayout {
|
||||
inflate(context, R.layout.overlay_queue, this)
|
||||
_topbar = findViewById(R.id.topbar);
|
||||
_editor = findViewById(R.id.editor);
|
||||
_btnSettings = findViewById(R.id.button_settings);
|
||||
_overlayContainer = findViewById(R.id.overlay_container_queue);
|
||||
|
||||
|
||||
_topbar.onClose.subscribe(this, onClose::emit);
|
||||
_editor.onVideoOrderChanged.subscribe { StatePlayer.instance.setQueueWithExisting(it) }
|
||||
@@ -41,10 +28,6 @@ class QueueEditorOverlay : LinearLayout {
|
||||
}
|
||||
_editor.onVideoClicked.subscribe { v -> StatePlayer.instance.setQueuePosition(v) }
|
||||
|
||||
_btnSettings.setOnClickListener {
|
||||
handleSettings();
|
||||
}
|
||||
|
||||
_topbar.setInfo(context.getString(R.string.queue), "");
|
||||
}
|
||||
|
||||
@@ -57,8 +40,4 @@ class QueueEditorOverlay : LinearLayout {
|
||||
fun cleanup() {
|
||||
_topbar.onClose.remove(this);
|
||||
}
|
||||
|
||||
fun handleSettings() {
|
||||
UISlideOverlays.showQueueOptionsOverlay(context, _overlayContainer);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.futo.platformplayer.views.video
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaDrm
|
||||
import android.net.Uri
|
||||
import android.util.AttributeSet
|
||||
import android.widget.RelativeLayout
|
||||
@@ -23,7 +22,6 @@ import androidx.media3.exoplayer.dash.DashMediaSource
|
||||
import androidx.media3.exoplayer.dash.manifest.DashManifestParser
|
||||
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager
|
||||
import androidx.media3.exoplayer.drm.HttpMediaDrmCallback
|
||||
import androidx.media3.exoplayer.drm.MediaDrmCallback
|
||||
import androidx.media3.exoplayer.hls.HlsMediaSource
|
||||
import androidx.media3.exoplayer.source.MediaSource
|
||||
import androidx.media3.exoplayer.source.MergingMediaSource
|
||||
@@ -35,11 +33,14 @@ import com.futo.platformplayer.api.media.models.chapters.IChapter
|
||||
import com.futo.platformplayer.api.media.models.streams.VideoMuxedSourceDescriptor
|
||||
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.IAudioUrlWidevineSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestWidevineSource
|
||||
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
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlWidevineSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
|
||||
import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource
|
||||
@@ -56,8 +57,8 @@ import com.futo.platformplayer.helpers.VideoHelper
|
||||
import com.futo.platformplayer.logging.Logger
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.video.PlayerManager
|
||||
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||
import com.futo.platformplayer.views.video.datasources.PluginMediaDrmCallback
|
||||
import com.futo.platformplayer.views.video.datasources.JSHttpDataSource
|
||||
import getHttpDataSourceFactory
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -419,9 +420,11 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
val didSet = when(videoSource) {
|
||||
is LocalVideoSource -> { swapVideoSourceLocal(videoSource); true; }
|
||||
is JSVideoUrlRangeSource -> { swapVideoSourceUrlRange(videoSource); true; }
|
||||
is IDashManifestWidevineSource -> { swapVideoSourceDashWidevine(videoSource); true }
|
||||
is IDashManifestSource -> { swapVideoSourceDash(videoSource); true;}
|
||||
is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume)
|
||||
is JSDashManifestRawSource -> swapVideoSourceDashRaw(videoSource, play, resume);
|
||||
is IHLSManifestSource -> { swapVideoSourceHLS(videoSource); true; }
|
||||
is IVideoUrlWidevineSource -> { swapVideoSourceUrlWidevine(videoSource); true; }
|
||||
is IVideoUrlSource -> { swapVideoSourceUrl(videoSource); true; }
|
||||
null -> { _lastVideoMediaSource = null; true;}
|
||||
else -> throw IllegalArgumentException("Unsupported video source [${videoSource.javaClass.simpleName}]");
|
||||
@@ -434,7 +437,8 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
is LocalAudioSource -> {swapAudioSourceLocal(audioSource); true; }
|
||||
is JSAudioUrlRangeSource -> { swapAudioSourceUrlRange(audioSource); true; }
|
||||
is JSHLSManifestAudioSource -> { swapAudioSourceHLS(audioSource); true; }
|
||||
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume)
|
||||
is JSDashManifestRawAudioSource -> swapAudioSourceDashRaw(audioSource, play, resume);
|
||||
is IAudioUrlWidevineSource -> { swapAudioSourceUrlWidevine(audioSource); true; }
|
||||
is IAudioUrlSource -> { swapAudioSourceUrl(audioSource); true; }
|
||||
null -> { _lastAudioMediaSource = null; true; }
|
||||
else -> throw IllegalArgumentException("Unsupported video source [${audioSource.javaClass.simpleName}]");
|
||||
@@ -474,50 +478,70 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
}
|
||||
else throw IllegalArgumentException("source without itag data...");
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceUrl(videoSource: IVideoUrlSource) {
|
||||
Logger.i(TAG, "Loading VideoSource [Url]");
|
||||
val dataSource = if (videoSource is JSSource && videoSource.requiresCustomDatasource)
|
||||
val dataSource = if(videoSource is JSSource && videoSource.requiresCustomDatasource)
|
||||
videoSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||
|
||||
val drmCallback = if (videoSource is JSSource) getDrmCallback(videoSource, dataSource) else null
|
||||
|
||||
val factory = ProgressiveMediaSource.Factory(dataSource)
|
||||
if (drmCallback != null) {
|
||||
factory.setDrmSessionManagerProvider {
|
||||
DefaultDrmSessionManager.Builder()
|
||||
.setMultiSession(true)
|
||||
.build(drmCallback)
|
||||
}
|
||||
}
|
||||
|
||||
_lastVideoMediaSource = factory
|
||||
_lastVideoMediaSource = ProgressiveMediaSource.Factory(dataSource)
|
||||
.createMediaSource(MediaItem.fromUri(videoSource.getVideoUrl()));
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceUrlWidevine(videoSource: IVideoUrlWidevineSource) {
|
||||
Logger.i(TAG, "Loading VideoSource [UrlWidevine]");
|
||||
val dataSource = if(videoSource is JSSource && videoSource.requiresCustomDatasource)
|
||||
videoSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
|
||||
|
||||
val baseCallback = HttpMediaDrmCallback(videoSource.licenseUri, dataSource)
|
||||
|
||||
val callback = if (videoSource.hasLicenseRequestExecutor) {
|
||||
PluginMediaDrmCallback(baseCallback, videoSource.getLicenseRequestExecutor()!!, videoSource.licenseUri)
|
||||
} else {
|
||||
baseCallback
|
||||
}
|
||||
|
||||
_lastVideoMediaSource = ProgressiveMediaSource.Factory(dataSource)
|
||||
.setDrmSessionManagerProvider {
|
||||
DefaultDrmSessionManager.Builder()
|
||||
.setMultiSession(true)
|
||||
.build(callback)
|
||||
}
|
||||
.createMediaSource(
|
||||
MediaItem.fromUri(videoSource.getVideoUrl())
|
||||
)
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceDash(videoSource: IDashManifestSource) {
|
||||
Logger.i(TAG, "Loading VideoSource [Dash]");
|
||||
val dataSource = if (videoSource is JSSource && (videoSource.requiresCustomDatasource))
|
||||
val dataSource = if(videoSource is JSSource && (videoSource.requiresCustomDatasource))
|
||||
videoSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
|
||||
.createMediaSource(MediaItem.fromUri(videoSource.url))
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceDashWidevine(videoSource: IDashManifestWidevineSource) {
|
||||
Logger.i(TAG, "Loading VideoSource [DashWidevine]")
|
||||
val dataSource =
|
||||
if (videoSource is JSSource && (videoSource.requiresCustomDatasource)) videoSource.getHttpDataSourceFactory()
|
||||
else DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
|
||||
|
||||
val drmCallback =
|
||||
if (videoSource is JSSource) getDrmCallback(videoSource, dataSource) else null
|
||||
val baseCallback = HttpMediaDrmCallback(videoSource.licenseUri, dataSource)
|
||||
|
||||
val factory = DashMediaSource.Factory(dataSource)
|
||||
if (drmCallback != null) {
|
||||
factory.setDrmSessionManagerProvider {
|
||||
DefaultDrmSessionManager.Builder().setMultiSession(true).build(drmCallback)
|
||||
}
|
||||
val callback = if (videoSource.hasLicenseRequestExecutor) {
|
||||
PluginMediaDrmCallback(baseCallback, videoSource.getLicenseRequestExecutor()!!, videoSource.licenseUri)
|
||||
} else {
|
||||
baseCallback
|
||||
}
|
||||
|
||||
_lastVideoMediaSource =
|
||||
factory.createMediaSource(MediaItem.fromUri(videoSource.url))
|
||||
_lastVideoMediaSource = DashMediaSource.Factory(dataSource).setDrmSessionManagerProvider {
|
||||
DefaultDrmSessionManager.Builder().setMultiSession(true).build(callback)
|
||||
}.createMediaSource(MediaItem.fromUri(videoSource.url))
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapVideoSourceDashRaw(videoSource: JSDashManifestRawSource, play: Boolean, resume: Boolean): Boolean {
|
||||
@@ -612,28 +636,15 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
}
|
||||
else throw IllegalArgumentException("source without itag data...")
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapAudioSourceUrl(audioSource: IAudioUrlSource) {
|
||||
Logger.i(TAG, "Loading AudioSource [Url]");
|
||||
val dataSource = if (audioSource is JSSource && audioSource.requiresCustomDatasource)
|
||||
val dataSource = if(audioSource is JSSource && audioSource.requiresCustomDatasource)
|
||||
audioSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||
|
||||
val drmCallback =
|
||||
if (audioSource is JSSource) getDrmCallback(audioSource, dataSource) else null
|
||||
|
||||
val factory = ProgressiveMediaSource.Factory(dataSource)
|
||||
if (drmCallback != null) {
|
||||
factory.setDrmSessionManagerProvider {
|
||||
DefaultDrmSessionManager.Builder()
|
||||
.setMultiSession(true)
|
||||
.build(drmCallback)
|
||||
}
|
||||
}
|
||||
_lastAudioMediaSource =
|
||||
factory.createMediaSource(MediaItem.fromUri(audioSource.getAudioUrl()));
|
||||
_lastAudioMediaSource = ProgressiveMediaSource.Factory(dataSource)
|
||||
.createMediaSource(MediaItem.fromUri(audioSource.getAudioUrl()));
|
||||
}
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapAudioSourceHLS(audioSource: IHLSManifestAudioSource) {
|
||||
@@ -649,46 +660,26 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun swapAudioSourceDashRaw(audioSource: JSDashManifestRawAudioSource, play: Boolean, resume: Boolean): Boolean {
|
||||
Logger.i(TAG, "Loading AudioSource [DashRaw]");
|
||||
val dataSource = if (audioSource.requiresCustomDatasource)
|
||||
val dataSource = if(audioSource is JSSource && (audioSource.requiresCustomDatasource))
|
||||
audioSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT);
|
||||
|
||||
val drmCallback = getDrmCallback(audioSource, dataSource)
|
||||
|
||||
if (audioSource.hasGenerate) {
|
||||
if(audioSource.hasGenerate) {
|
||||
findViewTreeLifecycleOwner()?.lifecycle?.coroutineScope?.launch(Dispatchers.IO) {
|
||||
val generated = audioSource.generate();
|
||||
if (generated != null) {
|
||||
if(generated != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
val factory = DashMediaSource.Factory(dataSource)
|
||||
if (drmCallback != null) {
|
||||
factory.setDrmSessionManagerProvider {
|
||||
DefaultDrmSessionManager.Builder().setMultiSession(true)
|
||||
.build(drmCallback)
|
||||
}
|
||||
}
|
||||
_lastVideoMediaSource = factory
|
||||
.createMediaSource(
|
||||
DashManifestParser().parse(
|
||||
Uri.parse(audioSource.url),
|
||||
ByteArrayInputStream(generated.toByteArray() ?: ByteArray(0))
|
||||
)
|
||||
);
|
||||
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
|
||||
.createMediaSource(DashManifestParser().parse(Uri.parse(audioSource.url),
|
||||
ByteArrayInputStream(generated?.toByteArray() ?: ByteArray(0))));
|
||||
loadSelectedSources(play, resume);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
val factory = DashMediaSource.Factory(dataSource)
|
||||
if (drmCallback != null) {
|
||||
factory.setDrmSessionManagerProvider {
|
||||
DefaultDrmSessionManager.Builder().setMultiSession(true)
|
||||
.build(drmCallback)
|
||||
}
|
||||
}
|
||||
_lastVideoMediaSource = factory
|
||||
}
|
||||
else {
|
||||
_lastVideoMediaSource = DashMediaSource.Factory(dataSource)
|
||||
.createMediaSource(
|
||||
DashManifestParser().parse(
|
||||
Uri.parse(audioSource.url),
|
||||
@@ -700,23 +691,33 @@ abstract class FutoVideoPlayerBase : RelativeLayout {
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun getDrmCallback(source: JSSource, dataSource: HttpDataSource.Factory): MediaDrmCallback? {
|
||||
return if (source.drmLicenseUri != null) {
|
||||
if (!MediaDrm.isCryptoSchemeSupported(C.WIDEVINE_UUID)) {
|
||||
throw IllegalArgumentException("Device does not support Widevine")
|
||||
}
|
||||
private fun swapAudioSourceUrlWidevine(audioSource: IAudioUrlWidevineSource) {
|
||||
Logger.i(TAG, "Loading AudioSource [UrlWidevine]")
|
||||
val dataSource = if (audioSource is JSSource && audioSource.requiresCustomDatasource)
|
||||
audioSource.getHttpDataSourceFactory()
|
||||
else
|
||||
DefaultHttpDataSource.Factory().setUserAgent(DEFAULT_USER_AGENT)
|
||||
|
||||
val delegateCallback =
|
||||
HttpMediaDrmCallback(source.drmLicenseUri, dataSource)
|
||||
val baseCallback = HttpMediaDrmCallback(audioSource.licenseUri, dataSource)
|
||||
|
||||
if (source.hasLicenseRequestExecutor) {
|
||||
PluginMediaDrmCallback(delegateCallback, source.getLicenseRequestExecutor()!!, source.drmLicenseUri)
|
||||
} else {
|
||||
delegateCallback
|
||||
val callback = if (audioSource.hasLicenseRequestExecutor) {
|
||||
PluginMediaDrmCallback(baseCallback, audioSource.getLicenseRequestExecutor()!!, audioSource.licenseUri)
|
||||
} else {
|
||||
baseCallback
|
||||
}
|
||||
|
||||
_lastAudioMediaSource = ProgressiveMediaSource.Factory(dataSource)
|
||||
.setDrmSessionManagerProvider {
|
||||
DefaultDrmSessionManager.Builder()
|
||||
.setMultiSession(true)
|
||||
.build(callback)
|
||||
}
|
||||
} else null
|
||||
.createMediaSource(
|
||||
MediaItem.fromUri(audioSource.getAudioUrl())
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//Prefered source selection
|
||||
fun getPreferredVideoSource(video: IPlatformVideoDetails, targetPixels: Int = -1): IVideoSource? {
|
||||
val usePreview = false;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="110dp"
|
||||
android:layout_height="100dp"
|
||||
android:minHeight="0dp"
|
||||
app:layout_scrollFlags="scroll"
|
||||
app:contentInsetStart="0dp"
|
||||
@@ -77,16 +77,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="20dp" />
|
||||
|
||||
</LinearLayout>
|
||||
<TextView
|
||||
android:id="@+id/text_meta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="9dp"
|
||||
android:textAlignment="center"
|
||||
android:textColor="#333333"
|
||||
android:text="0 creators" />
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -21,20 +21,5 @@
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/topbar"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
<ImageView
|
||||
android:id="@+id/button_settings"
|
||||
android:background="@drawable/background_pill"
|
||||
android:padding="5dp"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:srcCompat="@drawable/ic_settings" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/overlay_container_queue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -36,7 +36,6 @@
|
||||
<string name="preferred_quality_description">Default quality for watching a video</string>
|
||||
<string name="update">Update</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="open_in_browser">Open In Browser</string>
|
||||
<string name="never">Never</string>
|
||||
<string name="import_options">Select any of the following available import options.</string>
|
||||
<string name="there_is_an_update_available_do_you_wish_to_update">There is an update available, do you wish to update?</string>
|
||||
@@ -244,8 +243,6 @@
|
||||
<string name="locked_content_description">This content is locked</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
<string name="tap_to_open_in_browser">Tap to open in browser</string>
|
||||
<string name="drm_not_supported">This device does not support the DRM required to play this content</string>
|
||||
<string name="open_and_play_in_mobile_browser">Please try playing this content in a mobile browser on this device. If it plays open a bug report and share your device model and OS version.</string>
|
||||
<string name="missing_plugin">Missing Plugin</string>
|
||||
<string name="viewers_are_raiding">Viewers are raiding</string>
|
||||
<string name="go_now">Go now</string>
|
||||
@@ -289,8 +286,6 @@
|
||||
<string name="also_removes_any_data_related_plugin_like_login_or_settings">Also removes any data related plugin like login or settings</string>
|
||||
<string name="announcement">Announcement</string>
|
||||
<string name="notifications">Notifications</string>
|
||||
<string name="check_disabled_plugin_updates">Check disabled plugins for updates</string>
|
||||
<string name="check_disabled_plugin_updates_description">Check disabled plugins for updates</string>
|
||||
<string name="planned_content_notifications">Planned Content Notifications</string>
|
||||
<string name="planned_content_notifications_description">Schedules discovered planned content as notifications, resulting in more accurate notifications for this content.</string>
|
||||
<string name="attempt_to_utilize_byte_ranges">Attempt to utilize byte ranges</string>
|
||||
|
||||
+1
Submodule app/src/stable/assets/sources/apple-podcast added at f79c7141bc
Submodule app/src/stable/assets/sources/apple-podcasts deleted from 090104c7fa
Submodule app/src/stable/assets/sources/bitchute updated: 7f869aa4b1...8d7c0e2527
Submodule app/src/stable/assets/sources/peertube updated: 20fd03d984...cfabdc97ab
Submodule app/src/stable/assets/sources/rumble updated: b9e6259f4e...670cbc043e
Submodule app/src/stable/assets/sources/youtube updated: 8f8774a782...15d3391a5d
@@ -12,8 +12,7 @@
|
||||
"cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json",
|
||||
"4e365633-6d3f-4267-8941-fdc36631d813": "sources/spotify/build/SpotifyConfig.json",
|
||||
"9c87e8db-e75d-48f4-afe5-2d203d4b95c5": "sources/dailymotion/build/DailymotionConfig.json",
|
||||
"e8b1ad5f-0c6d-497d-a5fa-0a785a16d902": "sources/bitchute/BitchuteConfig.json",
|
||||
"89ae4889-0420-4d16-ad6c-19c776b28f99": "sources/apple-podcasts/ApplePodcastsConfig.json"
|
||||
"e8b1ad5f-0c6d-497d-a5fa-0a785a16d902": "sources/bitchute/BitchuteConfig.json"
|
||||
},
|
||||
"SOURCES_EMBEDDED_DEFAULT": [
|
||||
"35ae969a-a7db-11ed-afa1-0242ac120002"
|
||||
|
||||
Submodule app/src/unstable/assets/sources/apple-podcasts updated: 090104c7fa...f79c7141bc
Submodule app/src/unstable/assets/sources/bitchute updated: 7f869aa4b1...8d7c0e2527
Submodule app/src/unstable/assets/sources/peertube updated: 20fd03d984...cfabdc97ab
Submodule app/src/unstable/assets/sources/rumble updated: b9e6259f4e...670cbc043e
Submodule app/src/unstable/assets/sources/youtube updated: 8f8774a782...2c816009f7
@@ -12,8 +12,7 @@
|
||||
"cf8ea74d-ad9b-489e-a083-539b6aa8648c": "sources/bilibili/build/BiliBiliConfig.json",
|
||||
"4e365633-6d3f-4267-8941-fdc36631d813": "sources/spotify/build/SpotifyConfig.json",
|
||||
"9c87e8db-e75d-48f4-afe5-2d203d4b95c5": "sources/dailymotion/build/DailymotionConfig.json",
|
||||
"e8b1ad5f-0c6d-497d-a5fa-0a785a16d902": "sources/bitchute/BitchuteConfig.json",
|
||||
"89ae4889-0420-4d16-ad6c-19c776b28f99": "sources/apple-podcasts/ApplePodcastsConfig.json"
|
||||
"e8b1ad5f-0c6d-497d-a5fa-0a785a16d902": "sources/bitchute/BitchuteConfig.json"
|
||||
},
|
||||
"SOURCES_EMBEDDED_DEFAULT": [
|
||||
"35ae969a-a7db-11ed-afa1-0242ac120002"
|
||||
|
||||
Reference in New Issue
Block a user