mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2026-05-28 10:43:00 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0630ec1d46 | |||
| 4dce8d6a80 | |||
| 3b62f999bf | |||
| 65ae8610fd | |||
| c1c2000c98 | |||
| 287c2d82a1 | |||
| 5cde1650f4 | |||
| a4b90f14ab | |||
| 4826b40136 | |||
| 62618224da | |||
| 49f15e1637 |
@@ -335,7 +335,9 @@ class UISlideOverlays {
|
||||
call = {
|
||||
selectedVideoVariant = it
|
||||
slideUpMenuOverlay.selectOption(videoButtons, it)
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
if (audioButtons.isEmpty()){
|
||||
slideUpMenuOverlay.setOk(container.context.getString(R.string.download))
|
||||
}
|
||||
},
|
||||
invokeParent = false
|
||||
))
|
||||
@@ -417,7 +419,7 @@ class UISlideOverlays {
|
||||
}
|
||||
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.video), videoSources,
|
||||
listOf(listOf(SlideUpMenuItem(
|
||||
listOf((if (audioSources != null) listOf(SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_movie,
|
||||
container.context.getString(R.string.none),
|
||||
@@ -430,7 +432,7 @@ class UISlideOverlays {
|
||||
menu?.setOk(container.context.getString(R.string.download));
|
||||
},
|
||||
invokeParent = false
|
||||
)) +
|
||||
)) else listOf()) +
|
||||
videoSources
|
||||
.filter { it.isDownloadable() }
|
||||
.map {
|
||||
@@ -907,7 +909,7 @@ class UISlideOverlays {
|
||||
val watchLater = StatePlaylists.instance.getWatchLater();
|
||||
items.add(SlideUpMenuGroup(container.context, container.context.getString(R.string.actions), "actions",
|
||||
(listOf(
|
||||
if(!isLimited)
|
||||
if(!isLimited && !video.isLive)
|
||||
SlideUpMenuItem(
|
||||
container.context,
|
||||
R.drawable.ic_download,
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
+2
-2
@@ -33,13 +33,13 @@ class LocalAudioSource : IAudioSource, IStreamMetaDataSource {
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long): LocalAudioSource {
|
||||
fun fromSource(source: IAudioSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalAudioSource {
|
||||
return LocalAudioSource(
|
||||
source.name,
|
||||
path,
|
||||
fileSize,
|
||||
source.bitrate,
|
||||
source.container,
|
||||
overrideContainer ?: 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): LocalVideoSource {
|
||||
fun fromSource(source: IVideoSource, path: String, fileSize: Long, overrideContainer: String? = null): LocalVideoSource {
|
||||
return LocalVideoSource(
|
||||
source.name,
|
||||
path,
|
||||
@@ -43,7 +43,7 @@ class LocalVideoSource : IVideoSource, IStreamMetaDataSource {
|
||||
source.width,
|
||||
source.height,
|
||||
source.duration,
|
||||
source.container,
|
||||
overrideContainer ?: source.container,
|
||||
source.codec,
|
||||
source.bitrate?:0
|
||||
);
|
||||
|
||||
+22
-4
@@ -4,6 +4,8 @@ import com.caoccao.javet.values.reference.V8ValueObject
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IDashManifestSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.other.IStreamMetaDataSource
|
||||
import com.futo.platformplayer.api.media.models.streams.sources.other.StreamMetaData
|
||||
import com.futo.platformplayer.api.media.platforms.js.DevJSClient
|
||||
import com.futo.platformplayer.api.media.platforms.js.JSClient
|
||||
import com.futo.platformplayer.engine.IV8PluginConfig
|
||||
@@ -14,8 +16,8 @@ import com.futo.platformplayer.getOrThrow
|
||||
import com.futo.platformplayer.others.Language
|
||||
import com.futo.platformplayer.states.StateDeveloper
|
||||
|
||||
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource {
|
||||
override val container : String = "application/dash+xml";
|
||||
class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||
override val container : String;
|
||||
override val name : String;
|
||||
override val codec: String;
|
||||
override val bitrate: Int;
|
||||
@@ -29,11 +31,14 @@ 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,15 +55,28 @@ class JSDashManifestRawAudioSource : JSSource, IAudioSource, IJSDashManifestRawS
|
||||
throw IllegalStateException("Source object already closed");
|
||||
|
||||
val plugin = _plugin.getUnderlyingPlugin();
|
||||
|
||||
var result: String? = null;
|
||||
if(_plugin is DevJSClient)
|
||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRaw", false) {
|
||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||
_obj.invokeString("generate");
|
||||
}
|
||||
}
|
||||
else
|
||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw", "dashManifestRaw.generate()") {
|
||||
result = _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;
|
||||
}
|
||||
}
|
||||
+28
-6
@@ -6,6 +6,8 @@ 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
|
||||
@@ -20,8 +22,8 @@ interface IJSDashManifestRawSource {
|
||||
var manifest: String?;
|
||||
fun generate(): String?;
|
||||
}
|
||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource {
|
||||
override val container : String = "application/dash+xml";
|
||||
open class JSDashManifestRawSource: JSSource, IVideoSource, IJSDashManifestRawSource, IStreamMetaDataSource {
|
||||
override val container : String;
|
||||
override val name : String;
|
||||
override val width: Int;
|
||||
override val height: Int;
|
||||
@@ -36,11 +38,14 @@ 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;
|
||||
@@ -57,17 +62,30 @@ 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) {
|
||||
return StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||
result = StateDeveloper.instance.handleDevCall(_plugin.devID, "DashManifestRawSource.generate()") {
|
||||
_plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||
_obj.invokeString("generate");
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
return _plugin.getUnderlyingPlugin().catchScriptErrors("DashManifestRaw.generate", "generate()", {
|
||||
result = _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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,12 +118,16 @@ 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) {
|
||||
return videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||
result = videoDash.replace("</AdaptationSet>", "</AdaptationSet>\n" + audioAdaptationSet.value)
|
||||
}
|
||||
else
|
||||
return videoDash;
|
||||
result = videoDash;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -64,7 +64,7 @@ class StateCasting {
|
||||
private val _scopeMain = CoroutineScope(Dispatchers.Main);
|
||||
private val _storage: CastingDeviceInfoStorage = FragmentedStorage.get();
|
||||
|
||||
private val _castServer = ManagedHttpServer(9999);
|
||||
private val _castServer = ManagedHttpServer();
|
||||
private var _started = false;
|
||||
|
||||
var devices: HashMap<String, CastingDevice> = hashMapOf();
|
||||
|
||||
@@ -141,11 +141,17 @@ class VideoDownload {
|
||||
var error: String? = null;
|
||||
|
||||
var videoFilePath: String? = null;
|
||||
var videoFileName: 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 videoFileSize: Long? = null;
|
||||
|
||||
var audioFilePath: String? = null;
|
||||
var audioFileName: 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 audioFileSize: Long? = null;
|
||||
|
||||
var subtitleFilePath: String? = null;
|
||||
@@ -235,11 +241,13 @@ 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");
|
||||
@@ -367,8 +375,8 @@ class VideoDownload {
|
||||
else throw DownloadException("Could not find a valid video or audio source for download")
|
||||
|
||||
if(asource is JSSource) {
|
||||
this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || asource.hasRequestExecutor;
|
||||
this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
||||
this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor;
|
||||
this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate);
|
||||
}
|
||||
|
||||
if(asource == null) {
|
||||
@@ -410,11 +418,13 @@ class VideoDownload {
|
||||
else audioSource;
|
||||
|
||||
if(actualVideoSource != null) {
|
||||
videoFileName = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}].${videoContainerToExtension(actualVideoSource!!.container)}".sanitizeFileName();
|
||||
videoFileNameBase = "${videoDetails!!.id.value!!} [${actualVideoSource!!.width}x${actualVideoSource!!.height}]".sanitizeFileName();
|
||||
videoFileNameExt = videoContainerToExtension(actualVideoSource!!.container);
|
||||
videoFilePath = File(downloadDir, videoFileName!!).absolutePath;
|
||||
}
|
||||
if(actualAudioSource != null) {
|
||||
audioFileName = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}].${audioContainerToExtension(actualAudioSource!!.container)}".sanitizeFileName();
|
||||
audioFileNameBase = "${videoDetails!!.id.value!!} [${actualAudioSource!!.language}-${actualAudioSource!!.bitrate}]".sanitizeFileName();
|
||||
audioFileNameExt = audioContainerToExtension(actualAudioSource!!.container);
|
||||
audioFilePath = File(downloadDir, audioFileName!!).absolutePath;
|
||||
}
|
||||
if(subtitleSource != null) {
|
||||
@@ -1062,8 +1072,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) };
|
||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0) };
|
||||
val localVideoSource = videoFilePath?.let { LocalVideoSource.fromSource(videoSourceToUse!!, it, videoFileSize ?: 0, videoOverrideContainer) };
|
||||
val localAudioSource = audioFilePath?.let { LocalAudioSource.fromSource(audioSourceToUse!!, it, audioFileSize ?: 0, audioOverrideContainer) };
|
||||
val localSubtitleSource = subtitleFilePath?.let { LocalSubtitleSource.fromSource(subtitleSource!!, it) };
|
||||
|
||||
if(localVideoSource != null && videoSourceToUse != null && videoSourceToUse is IStreamMetaDataSource)
|
||||
@@ -1144,7 +1154,7 @@ class VideoDownload {
|
||||
else if (container.contains("video/x-matroska"))
|
||||
return "mkv";
|
||||
else
|
||||
return "video";
|
||||
return "video";//throw IllegalStateException("Unknown container: " + container)
|
||||
}
|
||||
|
||||
fun audioContainerToExtension(container: String): String {
|
||||
@@ -1155,11 +1165,11 @@ class VideoDownload {
|
||||
else if (container.contains("audio/mp3"))
|
||||
return "mp3";
|
||||
else if (container.contains("audio/webm"))
|
||||
return "webma";
|
||||
return "webm";
|
||||
else if (container == "application/vnd.apple.mpegurl")
|
||||
return "mp4";
|
||||
return "mp4a";
|
||||
else
|
||||
return "audio";
|
||||
return "audio";// throw IllegalStateException("Unknown container: " + container)
|
||||
}
|
||||
|
||||
fun subtitleContainerToExtension(container: String?): String {
|
||||
|
||||
@@ -39,7 +39,7 @@ class VideoExport {
|
||||
this.subtitleSource = subtitleSource;
|
||||
}
|
||||
|
||||
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
|
||||
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null, documentRoot: DocumentFile? = 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 = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set");
|
||||
val downloadRoot = documentRoot ?: 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)
|
||||
|
||||
+10
@@ -8,6 +8,7 @@ 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
|
||||
@@ -78,6 +79,14 @@ 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;
|
||||
@@ -176,6 +185,7 @@ 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
@@ -882,7 +882,7 @@ class VideoDetailView : ConstraintLayout {
|
||||
_slideUpOverlay?.hide();
|
||||
}
|
||||
else null,
|
||||
if(!isLimitedVersion)
|
||||
if(!isLimitedVersion && !(video?.isLive ?: false))
|
||||
RoundButton(context, R.drawable.ic_download, context.getString(R.string.download), TAG_DOWNLOAD) {
|
||||
video?.let {
|
||||
_slideUpOverlay = UISlideOverlays.showDownloadVideoOverlay(it, _overlayContainer, context.contentResolver);
|
||||
|
||||
+10
@@ -34,6 +34,7 @@ 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;
|
||||
|
||||
@@ -54,6 +55,8 @@ 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;
|
||||
@@ -68,6 +71,7 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
buttonShuffle.setOnClickListener { onShuffleClick(); };
|
||||
|
||||
_buttonEdit.setOnClickListener { onEditClick(); };
|
||||
setButtonExportVisible(false);
|
||||
setButtonDownloadVisible(canEdit());
|
||||
|
||||
videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged);
|
||||
@@ -108,6 +112,7 @@ 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 {
|
||||
@@ -117,6 +122,7 @@ 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), {
|
||||
@@ -125,6 +131,7 @@ abstract class VideoListEditorView : LinearLayout {
|
||||
}
|
||||
}
|
||||
else {
|
||||
setButtonExportVisible(false);
|
||||
_buttonDownload.setImageResource(R.drawable.ic_download);
|
||||
_buttonDownload.setOnClickListener {
|
||||
onDownload();
|
||||
@@ -171,6 +178,9 @@ 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;
|
||||
|
||||
@@ -3,9 +3,11 @@ 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
|
||||
@@ -466,6 +468,54 @@ class StateDownloads {
|
||||
return _downloadsDirectory;
|
||||
}
|
||||
|
||||
fun exportPlaylist(context: Context, playlistId: String) {
|
||||
if(context is IWithResultLauncher)
|
||||
StateApp.instance.requestDirectoryAccess(context, "Export Playlist", "To export playlist to directory", null) {
|
||||
if (it == null)
|
||||
return@requestDirectoryAccess;
|
||||
|
||||
val root = DocumentFile.fromTreeUri(context, it!!);
|
||||
|
||||
val localVideos = StateDownloads.instance.getDownloadedVideosPlaylist(playlistId)
|
||||
|
||||
var lastNotifyTime = -1L;
|
||||
|
||||
UIDialogs.showDialogProgress(context) {
|
||||
StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
|
||||
it.setText("Exporting videos..");
|
||||
var i = 0;
|
||||
for (video in localVideos) {
|
||||
withContext(Dispatchers.Main) {
|
||||
it.setText("Exporting videos...(${i}/${localVideos.size})");
|
||||
//it.setProgress(i.toDouble() / localVideos.size);
|
||||
}
|
||||
|
||||
try {
|
||||
val export = VideoExport(video, video.videoSource.firstOrNull(), video.audioSource.firstOrNull(), video.subtitlesSources.firstOrNull());
|
||||
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
|
||||
|
||||
val file = export.export(context, { progress ->
|
||||
val now = System.currentTimeMillis();
|
||||
if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
|
||||
it.setProgress(progress);
|
||||
lastNotifyTime = now;
|
||||
}
|
||||
}, root);
|
||||
} catch(ex: Throwable) {
|
||||
Logger.e(TAG, "Failed export [${video.name}]: ${ex.message}", ex);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
it.setProgress(1f);
|
||||
it.dismiss();
|
||||
UIDialogs.appToast("Finished exporting playlist");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
|
||||
var lastNotifyTime = -1L;
|
||||
|
||||
@@ -477,13 +527,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)
|
||||
|
||||
@@ -54,6 +54,22 @@
|
||||
|
||||
|
||||
|
||||
<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"
|
||||
|
||||
Reference in New Issue
Block a user