Fix issues with attempting to download sources that are not supported (including mixed playlists)

This commit is contained in:
Kelvin 2023-10-13 18:00:01 +02:00
parent 67e29999ef
commit 8bb1ff87c0
10 changed files with 113 additions and 26 deletions

View file

@ -0,0 +1,15 @@
package com.futo.platformplayer
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails
import com.futo.platformplayer.helpers.VideoHelper
fun IPlatformVideoDetails.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
fun IVideoSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
fun IAudioSource.isDownloadable(): Boolean = VideoHelper.isDownloadable(this);
fun IVideoSourceDescriptor.hasAnySource(): Boolean = this.videoSources.any() || (this is VideoUnMuxedSourceDescriptor && this.audioSources.any());

View file

@ -4,7 +4,6 @@ import android.content.ContentResolver
import android.view.View
import android.view.ViewGroup
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.http.server.handlers.HttpConstantHandler
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
@ -29,7 +28,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class UISlideOverlays {
companion object {
@ -68,6 +66,12 @@ class UISlideOverlays {
return null;
}
if(!VideoHelper.isDownloadable(video)) {
Logger.i(TAG, "Attempted to open downloads without valid sources for [${video.name}]: ${video.url}");
UIDialogs.toast( "No downloadable sources (yet)");
return null;
}
items.add(SlideUpMenuGroup(container.context, "Video", videoSources,
listOf(listOf(SlideUpMenuItem(container.context, R.drawable.ic_movie, "None", "Audio Only", "none", {
selectedVideo = null;
@ -76,7 +80,7 @@ class UISlideOverlays {
menu?.setOk("Download");
}, false)) +
videoSources
.filter { it is IVideoUrlSource }
.filter { it.isDownloadable() }
.map {
SlideUpMenuItem(container.context, R.drawable.ic_movie, it.name, "${it.width}x${it.height}", it, {
selectedVideo = it as IVideoUrlSource;
@ -88,14 +92,14 @@ class UISlideOverlays {
));
if(Settings.instance.downloads.getDefaultVideoQualityPixels() > 0 && videoSources.size > 0)
selectedVideo = VideoHelper.selectBestVideoSource(videoSources.filter { it is IVideoUrlSource }.asIterable(),
selectedVideo = VideoHelper.selectBestVideoSource(videoSources.filter { it.isDownloadable() }.asIterable(),
Settings.instance.downloads.getDefaultVideoQualityPixels(),
FutoVideoPlayerBase.PREFERED_VIDEO_CONTAINERS) as IVideoUrlSource;
audioSources?.let { audioSources ->
items.add(SlideUpMenuGroup(container.context, "Audio", audioSources, audioSources
.filter { it is IAudioUrlSource }
.filter { VideoHelper.isDownloadable(it) }
.map {
SlideUpMenuItem(container.context, R.drawable.ic_music, it.name, "${it.bitrate}", it, {
selectedAudio = it as IAudioUrlSource;
@ -111,7 +115,7 @@ class UISlideOverlays {
menu?.selectOption(asources, preferredAudioSource);
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it is IAudioUrlSource }.asIterable(),
selectedAudio = VideoHelper.selectBestAudioSource(audioSources.filter { it.isDownloadable() }.asIterable(),
FutoVideoPlayerBase.PREFERED_AUDIO_CONTAINERS,
Settings.instance.playback.getPrimaryLanguage(container.context),
if(Settings.instance.downloads.isHighBitrateDefault()) 9999999 else 1) as IAudioUrlSource?;

View file

@ -9,7 +9,7 @@ import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
import com.futo.platformplayer.orNull
class JSHLSManifestAudioSource : IAudioUrlSource, IHLSManifestAudioSource, JSSource {
class JSHLSManifestAudioSource : IHLSManifestAudioSource, JSSource {
override val container : String get() = "application/vnd.apple.mpegurl";
override val codec: String = "HLS";
override val name : String;
@ -31,9 +31,6 @@ class JSHLSManifestAudioSource : IAudioUrlSource, IHLSManifestAudioSource, JSSou
priority = obj.getOrNull(config, "priority", contextName) ?: false;
}
override fun getAudioUrl(): String {
return url;
}
companion object {
fun fromV8HLSNullable(config: IV8PluginConfig, obj: V8Value?) : JSHLSManifestAudioSource? = obj.orNull { fromV8HLS(config, it as V8ValueObject) };

View file

@ -7,7 +7,7 @@ import com.futo.platformplayer.engine.IV8PluginConfig
import com.futo.platformplayer.getOrNull
import com.futo.platformplayer.getOrThrow
class JSHLSManifestSource : IVideoUrlSource, IHLSManifestSource, JSSource {
class JSHLSManifestSource : IHLSManifestSource, JSSource {
override val width : Int = 0;
override val height : Int = 0;
override val container : String get() = "application/vnd.apple.mpegurl";
@ -28,8 +28,4 @@ class JSHLSManifestSource : IVideoUrlSource, IHLSManifestSource, JSSource {
priority = obj.getOrNull(config, "priority", contextName) ?: false;
}
override fun getVideoUrl(): String {
return url;
}
}

View file

@ -1,8 +1,19 @@
package com.futo.platformplayer.downloads
import com.futo.platformplayer.api.media.models.video.IPlatformVideo
@kotlinx.serialization.Serializable
data class PlaylistDownloadDescriptor(
val id: String,
val targetPxCount: Long?,
val targetBitrate: Long?
);
) {
var preventDownload: MutableList<String> = arrayListOf();
fun getPreventDownloadList(): List<String> = synchronized(preventDownload){ preventDownload };
fun shouldDownload(video: IPlatformVideo): Boolean {
synchronized(preventDownload) {
return !preventDownload.contains(video.url);
}
}
}

View file

@ -13,8 +13,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.constructs.Event1
import com.futo.platformplayer.exceptions.DownloadException
import com.futo.platformplayer.hasAnySource
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
import com.futo.platformplayer.helpers.VideoHelper
import com.futo.platformplayer.isDownloadable
import com.futo.platformplayer.serializers.OffsetDateTimeNullableSerializer
import com.futo.platformplayer.serializers.OffsetDateTimeSerializer
import com.futo.platformplayer.toHumanBitrate
@ -147,27 +150,37 @@ class VideoDownload {
if(original !is IPlatformVideoDetails)
throw IllegalStateException("Original content is not media?");
if(original.video.hasAnySource() && !original.isDownloadable()) {
Logger.i(TAG, "Attempted to download unsupported video [${original.name}]:${original.url}");
throw DownloadException("Unsupported video for downloading", false);
}
videoDetails = SerializedPlatformVideoDetails.fromVideo(original, if (subtitleSource != null) listOf(subtitleSource!!) else listOf());
if(videoSource == null && targetPixelCount != null) {
val vsource = VideoHelper.selectBestVideoSource(videoDetails!!.video, targetPixelCount!!.toInt(), arrayOf())
?: throw IllegalStateException("Could not find a valid video source for video");
if(vsource is IVideoUrlSource)
videoSource = VideoUrlSource.fromUrlSource(vsource);
else
throw IllegalStateException("Download video source is not a url source");
// ?: throw IllegalStateException("Could not find a valid video source for video");
if(vsource != null) {
if (vsource is IVideoUrlSource)
videoSource = VideoUrlSource.fromUrlSource(vsource);
else
throw DownloadException("Video source is not supported for downloading (yet)", false);
}
}
if(audioSource == null && targetBitrate != null) {
val asource = VideoHelper.selectBestAudioSource(videoDetails!!.video, arrayOf(), null, targetPixelCount)
?: if(videoSource != null ) null
else throw IllegalStateException("Could not find a valid audio source for video");
else throw DownloadException("Could not find a valid video or audio source for download")
if(asource == null)
audioSource = null;
else if(asource is IAudioUrlSource)
audioSource = AudioUrlSource.fromUrlSource(asource);
else
throw IllegalStateException("Download audio source is not a url source");
throw DownloadException("Audio source is not supported for downloading (yet)", false);
}
if(videoSource == null && audioSource == null)
throw DownloadException("No valid sources found for video/audio");
}
}
suspend fun download(client: ManagedHttpClient, onProgress: ((Double) -> Unit)? = null) = coroutineScope {

View file

@ -0,0 +1,12 @@
package com.futo.platformplayer.exceptions
class DownloadException : Throwable {
val isRetryable: Boolean;
constructor(innerException: Throwable, retryable: Boolean = true): super(innerException) {
isRetryable = retryable;
}
constructor(msg: String, retryable: Boolean = true): super(msg) {
isRetryable = retryable;
}
}

View file

@ -4,7 +4,10 @@ import android.net.Uri
import com.futo.platformplayer.api.media.models.streams.IVideoSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.VideoUnMuxedSourceDescriptor
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
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.JSVideoUrlRangeSource
import com.futo.platformplayer.logging.Logger
@ -17,6 +20,12 @@ import com.google.android.exoplayer2.upstream.ResolvingDataSource
class VideoHelper {
companion object {
fun isDownloadable(detail: IPlatformVideoDetails) =
(detail.video.videoSources.any { isDownloadable(it) }) ||
(if (detail is VideoUnMuxedSourceDescriptor) detail.audioSources.any { isDownloadable(it) } else false);
fun isDownloadable(source: IVideoSource) = source is IVideoUrlSource;
fun isDownloadable(source: IAudioSource) = source is IAudioUrlSource;
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)

View file

@ -12,6 +12,7 @@ import com.futo.platformplayer.*
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.exceptions.DownloadException
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.Announcement
import com.futo.platformplayer.states.AnnouncementType
@ -134,6 +135,22 @@ class DownloadService : Service() {
Logger.w(TAG, "Video had no video or videodetail, removing download");
StateDownloads.instance.removeDownload(currentVideo);
}
else if(ex is DownloadException && !ex.isRetryable) {
Logger.w(TAG, "Video had exception that should not be retried");
StateDownloads.instance.removeDownload(currentVideo);
//Ensure impossible downloads are not retried for playlists
if(currentVideo.video != null && currentVideo.groupID != null && currentVideo.groupType == VideoDownload.GROUP_PLAYLIST) {
StateDownloads.instance.getPlaylistDownload(currentVideo.groupID!!)?.let {
synchronized(it.preventDownload) {
if(currentVideo?.video?.url != null && !it.preventDownload.contains(currentVideo!!.video!!.url)) {
it.preventDownload.add(currentVideo!!.video!!.url);
StateDownloads.instance.savePlaylistDownload(it);
Logger.w(TAG, "Preventing further download attempts in playlist [${it.id}] for [${currentVideo?.name}]:${currentVideo?.video?.url}");
}
}
}
}
}
else
Logger.e(TAG, "Failed download [${currentVideo.name}]: ${ex.message}", ex);
currentVideo.error = ex.message;

View file

@ -108,6 +108,11 @@ class StateDownloads {
fun getPlaylistDownload(playlistId: String): PlaylistDownloadDescriptor? {
return _downloadPlaylists.findItem { it.id == playlistId };
}
fun savePlaylistDownload(playlistDownload: PlaylistDownloadDescriptor) {
synchronized(playlistDownload.preventDownload) {
_downloadPlaylists.save(playlistDownload);
}
}
fun deleteCachedPlaylist(id: String) {
val pdl = getPlaylistDownload(id);
if(pdl != null)
@ -157,12 +162,15 @@ class StateDownloads {
val playlistsDownloaded = getCachedPlaylists();
for(playlist in playlistsDownloaded) {
val playlistDownload = getPlaylistDownload(playlist.playlist.id) ?: continue;
if(playlist.playlist.videos.any{ getCachedVideo(it.id) == null }) {
Logger.i(TAG, "Found new videos on playlist [${playlist.playlist.name}]");
val toIgnore = playlistDownload.getPreventDownloadList();
val missingVideoCount = playlist.playlist.videos.count { !toIgnore.contains(it.url) && getCachedVideo(it.id) == null };
if(missingVideoCount > 0) {
Logger.i(TAG, "Found new videos (${missingVideoCount}) on playlist [${playlist.playlist.name}] to download");
continueDownload(playlistDownload, playlist.playlist);
hasChanged = true;
}
else
Logger.v(TAG, "Offline playlist [${playlist.playlist.name}] is up to date");
}
return hasChanged;
}
@ -171,6 +179,11 @@ class StateDownloads {
var hasNew = false;
for(item in playlist.videos) {
val existing = getCachedVideo(item.id);
if(!playlistDownload.shouldDownload(item)) {
Logger.i(TAG, "Not downloading for playlist [${playlistDownload.id}] Video [${item.name}]:${item.url}")
continue;
}
if(existing == null) {
val ongoingDownload = getDownloading().find { it.id.value == item.id.value && it.id.value != null };
if(ongoingDownload != null) {