From c2d245e8b46dc55a5763e747f8d3ba969c5d6d19 Mon Sep 17 00:00:00 2001 From: reduplicated <110570621+reduplicated@users.noreply.github.com> Date: Sat, 8 Oct 2022 22:29:17 +0200 Subject: [PATCH] mpv --- app/src/main/AndroidManifest.xml | 1 + .../lagradost/cloudstream3/CommonActivity.kt | 27 +++- .../lagradost/cloudstream3/MainActivity.kt | 97 +++++++++------ .../cloudstream3/ui/result/EpisodeAdapter.kt | 3 + .../ui/result/ResultViewModel2.kt | 116 ++++++++++-------- app/src/main/res/values/array.xml | 2 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 158 insertions(+), 89 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 043195f5..216a5f21 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,6 +24,7 @@ + diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 8f22c01a..32df314f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -10,16 +10,22 @@ import android.util.Log import android.view.* import android.widget.TextView import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.MainThread import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.SearchView import androidx.preference.PreferenceManager import com.google.android.gms.cast.framework.CastSession +import com.lagradost.cloudstream3.AcraApplication.Companion.getKey +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.PlayerEventType +import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.UiText import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission @@ -34,6 +40,7 @@ object CommonActivity { return (this as MainActivity?)?.mSessionManager?.currentCastSession } + var canEnterPipMode: Boolean = false var canShowPipMode: Boolean = false var isInPIPMode: Boolean = false @@ -117,7 +124,7 @@ object CommonActivity { setLocale(this, localeCode) } - fun init(act: Activity?) { + fun init(act: ComponentActivity?) { if (act == null) return //https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission //https://developer.android.com/guide/topics/ui/picture-in-picture @@ -129,6 +136,22 @@ object CommonActivity { act.updateLocale() act.updateTv() NewPipe.init(DownloaderTestImpl.getInstance()) + + for (resumeApp in resumeApps) { + resumeApp.launcher = + act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + val resultCode = result.resultCode + val data = result.data + if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) { + val pos = data.getLongExtra(resumeApp.position, -1L) + val dur = data.getLongExtra(resumeApp.duration, -1L) + if (dur > 0L && pos > 0L) + DataStoreHelper.setViewPos(getKey(resumeApp.lastId), pos, dur) + removeKey(resumeApp.lastId) + ResultFragment.updateUI() + } + } + } } private fun Activity.enterPIPMode() { @@ -167,7 +190,7 @@ object CommonActivity { "Amoled" -> R.style.AmoledMode "AmoledLight" -> R.style.AmoledModeLight "Monet" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) - R.style.MonetMode else R.style.AppTheme + R.style.MonetMode else R.style.AppTheme else -> R.style.AppTheme } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index de01ef9f..4c193f42 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -10,7 +10,7 @@ import android.view.KeyEvent import android.view.Menu import android.view.MenuItem import android.view.WindowManager -import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher import androidx.annotation.IdRes import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity @@ -35,6 +35,8 @@ import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.initAll import com.lagradost.cloudstream3.APIHolder.updateHasTrailers +import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.loadThemes import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent @@ -53,7 +55,6 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStri import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.inAppAuths import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO -import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings @@ -67,10 +68,8 @@ import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.DataStore.getKey -import com.lagradost.cloudstream3.utils.DataStore.removeKey import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching -import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.IOnBackPressed import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate @@ -95,18 +94,65 @@ import java.nio.charset.Charset import kotlin.reflect.KClass +//https://github.com/videolan/vlc-android/blob/3706c4be2da6800b3d26344fc04fab03ffa4b860/application/vlc-android/src/org/videolan/vlc/gui/video/VideoPlayerActivity.kt#L1898 +//https://wiki.videolan.org/Android_Player_Intents/ + +//https://github.com/mpv-android/mpv-android/blob/0eb3cdc6f1632636b9c30d52ec50e4b017661980/app/src/main/java/is/xyz/mpv/MPVActivity.kt#L904 +//https://mpv-android.github.io/mpv-android/intent.html + +// https://www.webvideocaster.com/integrations + +//https://github.com/jellyfin/jellyfin-android/blob/6cbf0edf84a3da82347c8d59b5d5590749da81a9/app/src/main/java/org/jellyfin/mobile/bridge/ExternalPlayer.kt#L225 + const val VLC_PACKAGE = "org.videolan.vlc" -const val VLC_INTENT_ACTION_RESULT = "org.videolan.vlc.player.result" -val VLC_COMPONENT: ComponentName = - ComponentName(VLC_PACKAGE, "org.videolan.vlc.gui.video.VideoPlayerActivity") -const val VLC_REQUEST_CODE = 42 - -const val VLC_EXTRA_POSITION_OUT = "extra_position" -const val VLC_EXTRA_DURATION_OUT = "extra_duration" -const val VLC_LAST_ID_KEY = "vlc_last_open_id" - +const val MPV_PACKAGE = "is.xyz.mpv" const val WEB_VIDEO_CAST_PACKAGE = "com.instantbits.cast.webvideo" +val VLC_COMPONENT = ComponentName(VLC_PACKAGE, "$VLC_PACKAGE.gui.video.VideoPlayerActivity") +val MPV_COMPONENT = ComponentName(MPV_PACKAGE, "$MPV_PACKAGE.MPVActivity") + +//TODO REFACTOR AF +data class ResultResume( + val packageString: String, + val action: String = Intent.ACTION_VIEW, + val position: String? = null, + val duration: String? = null, + var launcher: ActivityResultLauncher? = null, +) { + val lastId get() = "${packageString}_last_open_id" + suspend fun launch(id: Int?, callback: suspend Intent.() -> Unit) { + val intent = Intent(action) + + if (id != null) + setKey(lastId, id) + else + removeKey(lastId) + + intent.setPackage(packageString) + callback.invoke(intent) + launcher?.launch(intent) + } +} + +val VLC = ResultResume( + VLC_PACKAGE, + "org.videolan.vlc.player.result", + "extra_position", + "extra_duration", +) + +val MPV = ResultResume( + MPV_PACKAGE, + //"is.xyz.mpv.MPVActivity.result", // resume not working :pensive: + position = "position", + duration = "duration", +) + +val WEB_VIDEO = ResultResume(WEB_VIDEO_CAST_PACKAGE) + +val resumeApps = arrayOf( + VLC, MPV, WEB_VIDEO +) // Short name for requests client to make it nicer to use @@ -373,31 +419,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == VLC_REQUEST_CODE) { - if (resultCode == RESULT_OK && data != null) { - val pos: Long = - data.getLongExtra( - VLC_EXTRA_POSITION_OUT, - -1 - ) //Last position in media when player exited - val dur: Long = - data.getLongExtra( - VLC_EXTRA_DURATION_OUT, - -1 - ) //Last position in media when player exited - val id = getKey(VLC_LAST_ID_KEY) - println("SET KEY $id at $pos / $dur") - if (dur > 0 && pos > 0) { - setViewPos(id, pos, dur) - } - removeKey(VLC_LAST_ID_KEY) - ResultFragment.updateUI() - } - } - super.onActivityResult(requestCode, resultCode, data) - } - override fun onDestroy() { val broadcastIntent = Intent() broadcastIntent.action = "restart_service" diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 1997edbf..e9fbd5f9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -56,6 +56,7 @@ const val ACTION_DOWNLOAD_EPISODE_SUBTITLE = 13 const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14 const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16 +const val ACTION_PLAY_EPISODE_IN_MPV = 17 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) @@ -71,12 +72,14 @@ class EpisodeAdapter( * See array.xml/player_pref_values **/ fun getPlayerAction(context: Context): Int { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) return when (settingsManager.getInt(context.getString(R.string.player_pref_key), 1)) { 1 -> ACTION_PLAY_EPISODE_IN_PLAYER 2 -> ACTION_PLAY_EPISODE_IN_VLC_PLAYER 3 -> ACTION_PLAY_EPISODE_IN_BROWSER 4 -> ACTION_PLAY_EPISODE_IN_WEB_VIDEO + 5 -> ACTION_PLAY_EPISODE_IN_MPV else -> ACTION_PLAY_EPISODE_IN_PLAYER } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 9bf378eb..906b652d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getId import com.lagradost.cloudstream3.APIHolder.unixTime +import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.getCastSession import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.LoadResponse.Companion.addTrailer @@ -43,7 +44,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.Coroutines.ioWorkSafe import com.lagradost.cloudstream3.utils.Coroutines.main -import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultEpisode import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultSeason @@ -613,7 +613,7 @@ class ResultViewModel2 : ViewModel() { val src = "$DOWNLOAD_NAVIGATE_TO/$parentId" // url ?: return@let // SET VISUAL KEYS - AcraApplication.setKey( + setKey( DOWNLOAD_HEADER_CACHE, parentId.toString(), VideoDownloadHelper.DownloadHeaderCached( @@ -627,7 +627,7 @@ class ResultViewModel2 : ViewModel() { ) ) - AcraApplication.setKey( + setKey( DataStore.getFolderName( DOWNLOAD_EPISODE_CACHE, parentId.toString() @@ -956,12 +956,16 @@ class ResultViewModel2 : ViewModel() { private fun launchActivity( activity: Activity?, - work: suspend (CoroutineScope.(Activity) -> Unit) + resumeApp: ResultResume, + id: Int? = null, + work: suspend (Intent.(Activity) -> Unit) ): Job? { val act = activity ?: return null return CoroutineScope(Dispatchers.IO).launch { try { - work(act) + resumeApp.launch(id) { + work(act) + } } catch (t: Throwable) { logError(t) main { @@ -981,14 +985,12 @@ class ResultViewModel2 : ViewModel() { title: String?, posterUrl: String?, subtitles: List - ) = launchActivity(activity) { act -> - val shareVideo = Intent(Intent.ACTION_VIEW) + ) = launchActivity(activity, WEB_VIDEO) { + setDataAndType(Uri.parse(link.url), "video/*") - shareVideo.setDataAndType(Uri.parse(link.url), "video/*") - shareVideo.setPackage(WEB_VIDEO_CAST_PACKAGE) - shareVideo.putExtra("subs", subtitles.map { it.url.toUri() }.toTypedArray()) - title?.let { shareVideo.putExtra("title", title) } - posterUrl?.let { shareVideo.putExtra("poster", posterUrl) } + putExtra("subs", subtitles.map { it.url.toUri() }.toTypedArray()) + title?.let { putExtra("title", title) } + posterUrl?.let { putExtra("poster", posterUrl) } val headers = Bundle().apply { if (link.referer.isNotBlank()) putString("Referer", link.referer) @@ -997,10 +999,27 @@ class ResultViewModel2 : ViewModel() { putString(key, value) } } - shareVideo.putExtra("android.media.intent.extra.HTTP_HEADERS", headers) - shareVideo.putExtra("secure_uri", true) + putExtra("android.media.intent.extra.HTTP_HEADERS", headers) + putExtra("secure_uri", true) + } - act.startActivity(shareVideo) + private fun playWithMpv( + activity: Activity?, + id: Int, + link: ExtractorLink, + subtitles: List, + resume: Boolean = true, + ) = launchActivity(activity, MPV, id) { + putExtra("subs", subtitles.map { it.url.toUri() }.toTypedArray()) + putExtra("subs.name", subtitles.map { it.name }.toTypedArray()) + putExtra("subs.filename", subtitles.map { it.name }.toTypedArray()) + setDataAndType(Uri.parse(link.url), "video/*") + component = MPV_COMPONENT + putExtra("secure_uri", true) + putExtra("return_result", true) + val position = getViewPos(id)?.position + if (resume && position != null) + putExtra("position", position.toInt()) } // https://wiki.videolan.org/Android_Player_Intents/ @@ -1011,18 +1030,16 @@ class ResultViewModel2 : ViewModel() { resume: Boolean = true, // if it is only a single link then resume works correctly singleFile: Boolean? = null - ) = launchActivity(activity) { act -> - val vlcIntent = Intent(VLC_INTENT_ACTION_RESULT) + ) = launchActivity(activity, VLC, id) { act -> + addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - vlcIntent.setPackage(VLC_PACKAGE) - vlcIntent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - vlcIntent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION) - vlcIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - vlcIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) val outputDir = act.cacheDir if (singleFile ?: (data.links.size == 1)) { - vlcIntent.setDataAndType(data.links.first().url.toUri(), "video/*") + setDataAndType(data.links.first().url.toUri(), "video/*") } else { val outputFile = File.createTempFile("mirrorlist", ".m3u8", outputDir) @@ -1037,7 +1054,7 @@ class ResultViewModel2 : ViewModel() { } outputFile.writeText(text) - vlcIntent.setDataAndType( + setDataAndType( FileProvider.getUriForFile( act, act.applicationContext.packageName + ".provider", @@ -1051,33 +1068,14 @@ class ResultViewModel2 : ViewModel() { } else { 1L } - vlcIntent.putExtra("from_start", !resume) - vlcIntent.putExtra("position", position) - //vlcIntent.putExtra("subtitles_location", data.subs.first().url) - /*for (s in data.subs) { - if (s.origin == SubtitleOrigin.URL) { - try { - val txt = app.get(s.url, s.headers).text - val subtitleFile = File.createTempFile("subtitle1", ".srt", outputDir) - subtitleFile.writeText(txt) - println("Subtitles::::::${subtitleFile.path}") - vlcIntent.putExtra("subtitles_location", FileProvider.getUriForFile( - act, - act.applicationContext.packageName + ".provider", - subtitleFile - )) - break - } catch (t : Throwable) { - logError(t) - } - } - }*/ - vlcIntent.component = VLC_COMPONENT - act.setKey(VLC_LAST_ID_KEY, id) - act.startActivityForResult(vlcIntent, VLC_REQUEST_CODE) + component = VLC_COMPONENT + + putExtra("from_start", !resume) + putExtra("position", position) } + fun handleAction(activity: Activity?, click: EpisodeClickEvent) = viewModelScope.launchSafe { handleEpisodeClickEvent(activity, click) @@ -1098,6 +1096,11 @@ class ResultViewModel2 : ViewModel() { WEB_VIDEO_CAST_PACKAGE, R.string.player_settings_play_in_web, ACTION_PLAY_EPISODE_IN_WEB_VIDEO + ), + ExternalApp( + MPV_PACKAGE, + R.string.player_settings_play_in_mpv, + ACTION_PLAY_EPISODE_IN_MPV ) ) @@ -1329,6 +1332,21 @@ class ResultViewModel2 : ViewModel() { result.subs ) } + ACTION_PLAY_EPISODE_IN_MPV -> acquireSingleLink( + click.data, + isCasting = true, + txt( + R.string.episode_action_play_in_format, + txt(R.string.player_settings_play_in_mpv) + ) + ) { (result, index) -> + playWithMpv( + activity, + click.data.id, + result.links[index], + result.subs + ) + } ACTION_PLAY_EPISODE_IN_PLAYER -> { val data = currentResponse?.syncData?.toList() ?: emptyList() val list = @@ -2107,7 +2125,7 @@ class ResultViewModel2 : ViewModel() { preferStartEpisode = getResultEpisode(mainId) preferStartSeason = getResultSeason(mainId) - AcraApplication.setKey( + setKey( DOWNLOAD_HEADER_CACHE, mainId.toString(), VideoDownloadHelper.DownloadHeaderCached( diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index e4c8eb78..3554beb2 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -36,6 +36,7 @@ @string/player_settings_play_in_app @string/player_settings_play_in_vlc + @string/player_settings_play_in_mpv @string/player_settings_play_in_web @string/player_settings_play_in_browser @@ -43,6 +44,7 @@ 1 2 + 5 4 3 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7940eca9..924e5eb9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -633,6 +633,7 @@ Preferred video player Internal player VLC + MPV Web Video Cast Browser App not found