Removed exporting service.

This commit is contained in:
Koen 2024-06-26 16:01:08 +02:00
parent c76ef7f19b
commit bc550ae8f5
8 changed files with 62 additions and 357 deletions

View File

@ -41,9 +41,6 @@
<service android:name=".services.DownloadService" <service android:name=".services.DownloadService"
android:enabled="true" android:enabled="true"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service android:name=".services.ExportingService"
android:enabled="true"
android:foregroundServiceType="dataSync" />
<receiver android:name=".receivers.MediaControlReceiver" /> <receiver android:name=".receivers.MediaControlReceiver" />
<receiver android:name=".receivers.AudioNoisyReceiver" /> <receiver android:name=".receivers.AudioNoisyReceiver" />

View File

@ -1,47 +1,37 @@
package com.futo.platformplayer.downloads package com.futo.platformplayer.downloads
import android.content.Context import android.content.Context
import android.net.Uri
import android.os.Environment
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.arthenica.ffmpegkit.* import com.arthenica.ffmpegkit.FFmpegKit
import com.futo.platformplayer.api.media.models.streams.sources.* import com.arthenica.ffmpegkit.LogCallback
import com.futo.platformplayer.constructs.Event1 import com.arthenica.ffmpegkit.ReturnCode
import com.arthenica.ffmpegkit.StatisticsCallback
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalSubtitleSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalVideoSource
import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName import com.futo.platformplayer.helpers.FileHelper.Companion.sanitizeFileName
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.toHumanBitrate import com.futo.platformplayer.toHumanBitrate
import kotlinx.coroutines.* import kotlinx.coroutines.Dispatchers
import java.io.* import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.OutputStream
import java.util.UUID import java.util.UUID
import java.util.concurrent.CancellationException
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
class VideoExport { class VideoExport {
var state: State = State.QUEUED;
var videoLocal: VideoLocal; var videoLocal: VideoLocal;
var videoSource: LocalVideoSource?; var videoSource: LocalVideoSource?;
var audioSource: LocalAudioSource?; var audioSource: LocalAudioSource?;
var subtitleSource: LocalSubtitleSource?; var subtitleSource: LocalSubtitleSource?;
var progress: Double = 0.0;
var isCancelled = false;
var error: String? = null;
@kotlinx.serialization.Transient
val onStateChanged = Event1<State>();
@kotlinx.serialization.Transient
val onProgressChanged = Event1<Double>();
fun changeState(newState: State) {
state = newState;
onStateChanged.emit(newState);
}
constructor(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) { constructor(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
this.videoLocal = videoLocal; this.videoLocal = videoLocal;
this.videoSource = videoSource; this.videoSource = videoSource;
@ -50,8 +40,6 @@ class VideoExport {
} }
suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope { suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope {
if(isCancelled) throw CancellationException("Export got cancelled");
val v = videoSource; val v = videoSource;
val a = audioSource; val a = audioSource;
val s = subtitleSource; val s = subtitleSource;
@ -107,7 +95,6 @@ class VideoExport {
throw Exception("Cannot export when no audio or video source is set."); throw Exception("Cannot export when no audio or video source is set.");
} }
onProgressChanged.emit(100.0);
return@coroutineScope outputFile; return@coroutineScope outputFile;
} }

View File

@ -8,7 +8,7 @@ import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.futo.platformplayer.* import com.futo.platformplayer.R
import com.futo.platformplayer.downloads.VideoDownload import com.futo.platformplayer.downloads.VideoDownload
import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.downloads.VideoLocal
import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.logging.Logger
@ -16,12 +16,13 @@ import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.states.StatePlaylists
import com.futo.platformplayer.toHumanBytesSize
import com.futo.platformplayer.views.AnyInsertedAdapterView import com.futo.platformplayer.views.AnyInsertedAdapterView
import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop import com.futo.platformplayer.views.AnyInsertedAdapterView.Companion.asAnyWithTop
import com.futo.platformplayer.views.others.ProgressBar
import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder import com.futo.platformplayer.views.adapters.viewholders.VideoDownloadViewHolder
import com.futo.platformplayer.views.items.ActiveDownloadItem import com.futo.platformplayer.views.items.ActiveDownloadItem
import com.futo.platformplayer.views.items.PlaylistDownloadItem import com.futo.platformplayer.views.items.PlaylistDownloadItem
import com.futo.platformplayer.views.others.ProgressBar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -64,16 +65,6 @@ class DownloadsFragment : MainFragment() {
} }
} }
}; };
StateDownloads.instance.onExportsChanged.subscribe(this) {
lifecycleScope.launch(Dispatchers.Main) {
try {
Logger.i(TAG, "Reloading UI for exports");
_view?.reloadUI()
} catch (e: Throwable) {
Logger.e(TAG, "Failed to reload UI for exports", e)
}
}
};
} }
override fun onPause() { override fun onPause() {
@ -81,7 +72,6 @@ class DownloadsFragment : MainFragment() {
StateDownloads.instance.onDownloadsChanged.remove(this); StateDownloads.instance.onDownloadsChanged.remove(this);
StateDownloads.instance.onDownloadedChanged.remove(this); StateDownloads.instance.onDownloadedChanged.remove(this);
StateDownloads.instance.onExportsChanged.remove(this);
} }
private class DownloadsView : LinearLayout { private class DownloadsView : LinearLayout {

View File

@ -1,236 +0,0 @@
package com.futo.platformplayer.services
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.futo.platformplayer.R
import com.futo.platformplayer.activities.MainActivity
import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.downloads.VideoExport
import com.futo.platformplayer.logging.Logger
import com.futo.platformplayer.share
import com.futo.platformplayer.states.Announcement
import com.futo.platformplayer.states.AnnouncementType
import com.futo.platformplayer.states.StateAnnouncement
import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.stores.FragmentedStorage
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.time.OffsetDateTime
import java.util.UUID
class ExportingService : Service() {
private val TAG = "ExportingService";
private val EXPORT_NOTIF_ID = 4;
private val EXPORT_NOTIF_TAG = "export";
private val EXPORT_NOTIF_CHANNEL_ID = "exportChannel";
private val EXPORT_NOTIF_CHANNEL_NAME = "Export";
//Context
private val _scope: CoroutineScope = CoroutineScope(Dispatchers.Default);
private var _notificationManager: NotificationManager? = null;
private var _notificationChannel: NotificationChannel? = null;
private val _client = ManagedHttpClient();
private var _started = false;
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Logger.i(TAG, "onStartCommand");
synchronized(this) {
if(_started)
return START_STICKY;
if(!FragmentedStorage.isInitialized) {
closeExportSession();
return START_NOT_STICKY;
}
_started = true;
}
setupNotificationRequirements();
_callOnStarted?.invoke(this);
_instance = this;
_scope.launch {
try {
doExporting();
}
catch(ex: Throwable) {
try {
StateAnnouncement.instance.registerAnnouncementSession(
Announcement(
"rootExportException",
"An root export service exception happened",
ex.message ?: "",
AnnouncementType.SESSION,
OffsetDateTime.now()
)
);
} catch(_: Throwable){}
}
};
return START_STICKY;
}
fun setupNotificationRequirements() {
_notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
_notificationChannel = NotificationChannel(EXPORT_NOTIF_CHANNEL_ID, EXPORT_NOTIF_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT).apply {
this.enableVibration(false);
this.setSound(null, null);
};
_notificationManager!!.createNotificationChannel(_notificationChannel!!);
}
override fun onCreate() {
Logger.i(TAG, "onCreate");
super.onCreate()
}
override fun onBind(p0: Intent?): IBinder? {
return null;
}
private suspend fun doExporting() {
Logger.i(TAG, "doExporting - Starting Exports");
val ignore = mutableListOf<VideoExport>();
var currentExport: VideoExport? = StateDownloads.instance.getExporting().firstOrNull();
while (currentExport != null)
{
try{
notifyExport(currentExport);
doExport(applicationContext, currentExport);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed export [${currentExport.videoLocal.name}]: ${ex.message}", ex);
currentExport.error = ex.message;
currentExport.changeState(VideoExport.State.ERROR);
ignore.add(currentExport);
//Give it a sec
Thread.sleep(500);
}
currentExport = StateDownloads.instance.getExporting().filter { !ignore.contains(it) }.firstOrNull();
}
Logger.i(TAG, "doExporting - Ending Exports");
stopService(this);
}
private suspend fun doExport(context: Context, export: VideoExport) {
Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
export.changeState(VideoExport.State.EXPORTING);
var lastNotifyTime: Long = 0L;
val file = export.export(context) { progress ->
export.progress = progress;
val currentTime = System.currentTimeMillis();
if (currentTime - lastNotifyTime > 500) {
notifyExport(export);
lastNotifyTime = currentTime;
}
}
export.changeState(VideoExport.State.COMPLETED);
Logger.i(TAG, "Export [${export.videoLocal.name}] finished");
StateDownloads.instance.removeExport(export);
notifyExport(export);
withContext(Dispatchers.Main) {
StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") {
file.share(this@ExportingService);
};
}
}
private fun notifyExport(export: VideoExport) {
val channel = _notificationChannel ?: return;
val bringUpIntent = Intent(this, MainActivity::class.java);
bringUpIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
bringUpIntent.action = "TAB";
bringUpIntent.putExtra("TAB", "Exports");
var builder = NotificationCompat.Builder(this, EXPORT_NOTIF_TAG)
.setSmallIcon(R.drawable.ic_export)
.setOngoing(true)
.setSilent(true)
.setContentIntent(PendingIntent.getActivity(this, 5, bringUpIntent, PendingIntent.FLAG_IMMUTABLE))
.setContentTitle("${export.state}: ${export.videoLocal.name}")
.setContentText(export.getExportInfo())
.setProgress(100, (export.progress * 100).toInt(), export.progress == 0.0)
.setChannelId(channel.id)
val notif = builder.build();
notif.flags = notif.flags or NotificationCompat.FLAG_ONGOING_EVENT or NotificationCompat.FLAG_NO_CLEAR;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(EXPORT_NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
} else {
startForeground(EXPORT_NOTIF_ID, notif);
}
}
fun closeExportSession() {
Logger.i(TAG, "closeExportSession");
stopForeground(STOP_FOREGROUND_REMOVE);
_notificationManager?.cancel(EXPORT_NOTIF_ID);
stopService();
_started = false;
super.stopSelf();
}
override fun onDestroy() {
Logger.i(TAG, "onDestroy");
_instance = null;
_scope.cancel("onDestroy");
super.onDestroy();
}
companion object {
private var _instance: ExportingService? = null;
private var _callOnStarted: ((ExportingService)->Unit)? = null;
@Synchronized
fun getOrCreateService(context: Context, handle: ((ExportingService)->Unit)? = null) {
if(!FragmentedStorage.isInitialized)
return;
if(_instance == null) {
_callOnStarted = handle;
val intent = Intent(context, ExportingService::class.java);
context.startForegroundService(intent);
}
else _instance?.let {
if(handle != null)
handle(it);
}
}
@Synchronized
fun getService() : ExportingService? {
return _instance;
}
@Synchronized
fun stopService(service: ExportingService? = null) {
(service ?: _instance)?.let {
if(_instance == it)
_instance = null;
it.closeExportSession();
}
}
}
}

View File

@ -445,9 +445,6 @@ class StateApp {
DownloadService.getOrCreateService(context); DownloadService.getOrCreateService(context);
} }
Logger.i(TAG, "MainApp Started: Check [Exports]");
StateDownloads.instance.checkForExportTodos();
Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]"); Logger.i(TAG, "MainApp Started: Initialize [AutoUpdate]");
val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled(); val autoUpdateEnabled = Settings.instance.autoUpdate.isAutoUpdateEnabled();
val shouldDownload = Settings.instance.autoUpdate.shouldDownload(); val shouldDownload = Settings.instance.autoUpdate.shouldDownload();

View File

@ -1,13 +1,13 @@
package com.futo.platformplayer.states package com.futo.platformplayer.states
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context
import android.os.StatFs import android.os.StatFs
import com.futo.platformplayer.R import com.futo.platformplayer.R
import com.futo.platformplayer.Settings import com.futo.platformplayer.Settings
import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.UIDialogs
import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.http.ManagedHttpClient
import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.PlatformID
import com.futo.platformplayer.api.media.exceptions.AlreadyQueuedException
import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource import com.futo.platformplayer.api.media.models.streams.sources.IVideoUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource import com.futo.platformplayer.api.media.models.streams.sources.LocalAudioSource
@ -27,10 +27,14 @@ import com.futo.platformplayer.models.DiskUsage
import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Playlist
import com.futo.platformplayer.models.PlaylistDownloaded import com.futo.platformplayer.models.PlaylistDownloaded
import com.futo.platformplayer.services.DownloadService import com.futo.platformplayer.services.DownloadService
import com.futo.platformplayer.services.ExportingService import com.futo.platformplayer.share
import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.FragmentedStorage
import com.futo.platformplayer.stores.v2.ManagedStore import com.futo.platformplayer.stores.v2.ManagedStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.util.UUID
/*** /***
* Used to maintain downloads * Used to maintain downloads
@ -50,12 +54,8 @@ class StateDownloads {
private val _downloadPlaylists = FragmentedStorage.storeJson<PlaylistDownloadDescriptor>("playlistDownloads") private val _downloadPlaylists = FragmentedStorage.storeJson<PlaylistDownloadDescriptor>("playlistDownloads")
.load(); .load();
private val _exporting = FragmentedStorage.storeJson<VideoExport>("exporting")
.load();
private lateinit var _downloadedSet: HashSet<PlatformID>; private lateinit var _downloadedSet: HashSet<PlatformID>;
val onExportsChanged = Event0();
val onDownloadsChanged = Event0(); val onDownloadsChanged = Event0();
val onDownloadedChanged = Event0(); val onDownloadedChanged = Event0();
@ -457,17 +457,6 @@ class StateDownloads {
} }
} }
try {
val currentDownloads = _downloaded.getItems().map { it.url }.toHashSet();
val exporting = _exporting.findItems { !currentDownloads.contains(it.videoLocal.url) };
for (export in exporting)
_exporting.delete(export);
}
catch(ex: Throwable) {
Logger.e(TAG, "Failed to delete dangling export:", ex);
UIDialogs.toast("Failed to delete dangling export:\n" + ex);
}
return Pair(totalDeletedCount, totalDeleted); return Pair(totalDeletedCount, totalDeleted);
} }
@ -475,64 +464,39 @@ class StateDownloads {
return _downloadsDirectory; return _downloadsDirectory;
} }
fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) {
var lastNotifyTime = -1L;
UIDialogs.showDialogProgress(context) {
//Export it.setText("Exporting content..");
fun getExporting(): List<VideoExport> { it.setProgress(0f);
return _exporting.getItems(); StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) {
} val export = VideoExport(videoLocal, videoSource, audioSource, subtitleSource);
fun checkForExportTodos() {
if(_exporting.hasItems()) {
StateApp.withContext {
ExportingService.getOrCreateService(it);
}
}
}
fun validateExport(export: VideoExport) {
if(_exporting.hasItem { it.videoLocal.url == export.videoLocal.url })
throw AlreadyQueuedException("Video [${export.videoLocal.name}] is already queued for export");
}
fun export(videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?, notify: Boolean = true) {
val shortName = if(videoLocal.name.length > 23)
videoLocal.name.substring(0, 20) + "...";
else
videoLocal.name;
val videoExport = VideoExport(videoLocal, videoSource, audioSource, subtitleSource);
try { try {
validateExport(videoExport); Logger.i(TAG, "Exporting [${export.videoLocal.name}] started");
_exporting.save(videoExport);
if(notify) { val file = export.export(context) { progress ->
UIDialogs.toast("Exporting [${shortName}]"); val now = System.currentTimeMillis();
StateApp.withContext { ExportingService.getOrCreateService(it) }; if (lastNotifyTime == -1L || now - lastNotifyTime > 100) {
onExportsChanged.emit(); it.setProgress(progress);
} lastNotifyTime = now;
}
catch (ex: AlreadyQueuedException) {
Logger.e(TAG, "File is already queued for export.", ex);
StateApp.withContext { ExportingService.getOrCreateService(it) };
}
catch(ex: Throwable) {
StateApp.withContext {
UIDialogs.showDialog(
it,
R.drawable.ic_error,
"Failed to start export due to:\n${ex.message}", null, null,
0,
UIDialogs.Action("Ok", {}, UIDialogs.ActionStyle.PRIMARY)
);
}
} }
} }
withContext(Dispatchers.Main) {
it.setProgress(100.0f)
it.dismiss()
fun removeExport(export: VideoExport) { StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") {
_exporting.delete(export); file.share(context);
export.isCancelled = true; };
onExportsChanged.emit(); }
} catch(ex: Throwable) {
Logger.e(TAG, "Failed export [${export.videoLocal.name}]: ${ex.message}", ex);
}
}
}
} }
companion object { companion object {

View File

@ -1,7 +1,6 @@
package com.futo.platformplayer.views package com.futo.platformplayer.views
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
@ -49,6 +48,7 @@ class MonetizationView : LinearLayout {
private val _taskLoadMerchandise = TaskHandler<String, List<StoreItem>>(StateApp.instance.scopeGetter, { url -> private val _taskLoadMerchandise = TaskHandler<String, List<StoreItem>>(StateApp.instance.scopeGetter, { url ->
val client = ManagedHttpClient(); val client = ManagedHttpClient();
Logger.i(TAG, "Loading https://storecache.grayjay.app/StoreData?url=$url")
val result = client.get("https://storecache.grayjay.app/StoreData?url=$url") val result = client.get("https://storecache.grayjay.app/StoreData?url=$url")
if (!result.isOk) { if (!result.isOk) {
throw Exception("Failed to retrieve store data."); throw Exception("Failed to retrieve store data.");

View File

@ -16,6 +16,8 @@ import com.futo.platformplayer.states.StateApp
import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StateDownloads
import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.states.StatePlayer
import com.futo.platformplayer.views.adapters.AnyAdapter import com.futo.platformplayer.views.adapters.AnyAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<VideoLocal>( class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<VideoLocal>(
@ -57,10 +59,14 @@ class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder<
return@changeExternalDownloadDirectory; return@changeExternalDownloadDirectory;
} }
StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull()); StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
StateDownloads.instance.export(_viewGroup.context, v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
}
}; };
} else { } else {
StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull()); StateApp.instance.scopeOrNull?.launch(Dispatchers.Main) {
StateDownloads.instance.export(_viewGroup.context, v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull());
}
} }
} }
} }