diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f52d6e5e55e..0b4589adf74 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -260,6 +260,8 @@ dependencies { // color palette for images -> colors implementation("androidx.palette:palette-ktx:1.0.0") + + implementation("com.github.recloudstream:Aria2cStream:0.0.3") } tasks.register("androidSourcesJar", Jar::class) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index 5f3162b49e7..2b5909ad360 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -5,6 +5,7 @@ import android.app.Application import android.content.Context import android.content.ContextWrapper import android.content.Intent +import android.util.Log import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity @@ -42,6 +43,7 @@ class CustomReportSender : ReportSender { // Sends all your crashes to google forms override fun send(context: Context, errorContent: CrashReportData) { println("Sending report") + //Log.i("Acra", "Sending report: ${errorContent.toMap().map { "${it.key}:${it.value}" }.joinToString()}") val url = "https://docs.google.com/forms/d/e/1FAIpQLSfO4r353BJ79TTY_-t5KWSIJT2xfqcQWY81xjAA1-1N0U2eSg/formResponse" val data = mapOf( diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a07ae2c28cd..7e6c0628cdc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -94,6 +94,7 @@ import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.download.DOWNLOAD_NAVIGATE_TO import com.lagradost.cloudstream3.ui.home.HomeViewModel import com.lagradost.cloudstream3.ui.player.BasicLink +import com.lagradost.cloudstream3.ui.player.ExtractorLinkGenerator import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.LinkGenerator import com.lagradost.cloudstream3.ui.result.LinearListLayout @@ -130,8 +131,11 @@ import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper.migrateResumeWatching import com.lagradost.cloudstream3.utils.Event +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.INFER_TYPE import com.lagradost.cloudstream3.utils.IOnBackPressed import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate +import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.changeStatusBarState import com.lagradost.cloudstream3.utils.UIHelper.checkWrite @@ -1112,16 +1116,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> // println("refocus $oldFocus -> $newFocus") try { - val r = Rect(0,0,0,0) + val r = Rect(0, 0, 0, 0) newFocus.getDrawingRect(r) val x = r.centerX() val y = r.centerY() val dx = 0 //screenWidth / 2 val dy = screenHeight / 2 - val r2 = Rect(x-dx,y-dy,x+dx,y+dy) + val r2 = Rect(x - dx, y - dy, x + dx, y + dy) newFocus.requestRectangleOnScreen(r2, false) - // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) - } catch (_ : Throwable) { } + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_: Throwable) { + } TvFocus.updateFocusView(newFocus) /*var focus = newFocus @@ -1562,6 +1567,25 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { setKey(HAS_DONE_SETUP_KEY, true) } + //val defaultDirectory = "${filesDir.path}/torrent_tmp" + //File(defaultDirectory).deleteRecursively() + /*navigate( + R.id.global_to_navigation_player, GeneratorPlayer.newInstance( + ExtractorLinkGenerator( + listOf( + ExtractorLink( + source = "", + name = "hello world", + "", + Qualities.Unknown.value, + type = INFER_TYPE + ) + ), + emptyList() + ) + ) + )*/ + // Used to check current focus for TV // main { // while (true) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 8388e58f7b1..619cf06c297 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -135,7 +135,7 @@ abstract class AbstractPlayerFragment( } } - private fun updateIsPlaying(wasPlaying : CSPlayerLoading, + open fun updateIsPlaying(wasPlaying : CSPlayerLoading, isPlaying : CSPlayerLoading) { val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying @@ -265,7 +265,9 @@ abstract class AbstractPlayerFragment( context?.getString(R.string.no_links_found_toast) + "\n" + message, Toast.LENGTH_LONG ) - activity?.popCurrentPage() + activity?.runOnUiThread { + activity?.popCurrentPage() + } } } @@ -370,12 +372,17 @@ abstract class AbstractPlayerFragment( // } //} + open fun onDownload(event : DownloadEvent) = Unit + /** This receives the events from the player, if you want to append functionality you do it here, * do note that this only receives events for UI changes, * and returning early WONT stop it from changing in eg the player time or pause status */ open fun mainCallback(event : PlayerEvent) { Log.i(TAG, "Handle event: $event") when(event) { + is DownloadEvent -> { + onDownload(event) + } is ResizedEvent -> { playerDimensionsLoaded(event.width, event.height) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 331cfb73619..9db65707dfc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.player import android.annotation.SuppressLint +import android.app.Activity import android.content.Context import android.net.Uri import android.os.Handler @@ -8,6 +9,7 @@ import android.os.Looper import android.util.Log import android.util.Rational import android.widget.FrameLayout +import androidx.core.net.toUri import androidx.media3.common.C.* import androidx.media3.common.Format import androidx.media3.common.MediaItem @@ -49,9 +51,12 @@ import androidx.preference.PreferenceManager import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.CommonActivity +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.debugAssert +import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle @@ -63,6 +68,20 @@ import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.ExtractorUri import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage +import com.lagradost.fetchbutton.aria2c.Aria2Args +import com.lagradost.fetchbutton.aria2c.Aria2Settings +import com.lagradost.fetchbutton.aria2c.Aria2Starter +import com.lagradost.fetchbutton.aria2c.BtPieceSelector +import com.lagradost.fetchbutton.aria2c.DownloadListener +import com.lagradost.fetchbutton.aria2c.DownloadStatusTell +import com.lagradost.fetchbutton.aria2c.FileAllocationType +import com.lagradost.fetchbutton.aria2c.FollowMetaLinkType +import com.lagradost.fetchbutton.aria2c.UriRequest +import com.lagradost.fetchbutton.aria2c.newUriRequest +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import java.io.File import java.lang.IllegalArgumentException import java.util.UUID @@ -88,7 +107,9 @@ class CS3IPlayer : IPlayer { private var exoPlayer: ExoPlayer? = null set(value) { // If the old value is not null then the player has not been properly released. - debugAssert({ field != null && value != null }, { "Previous player instance should be released!" }) + debugAssert( + { field != null && value != null }, + { "Previous player instance should be released!" }) field = value } @@ -104,6 +125,9 @@ class CS3IPlayer : IPlayer { private var lastMuteVolume: Float = 1.0f private var currentLink: ExtractorLink? = null + + private var currentAria2cRequestLink: ExtractorLink? = null + private var currentAria2cRequestId: Long? = null private var currentDownloadedFile: ExtractorUri? = null private var hasUsedFirstRender = false @@ -182,6 +206,333 @@ class CS3IPlayer : IPlayer { subtitleHelper.initSubtitles(subView, subHolder, style) } + private fun getPlayableFile( + data: com.lagradost.fetchbutton.aria2c.Metadata, + minimumBytes: Long + ): Uri? { + for (item in data.items) { + for (file in item.files) { + // only allow files with a length above minimumBytes + if (file.completedLength < minimumBytes) continue + // only allow video formats + if (videoFormats.none { suf -> + file.path.contains( + suf, + ignoreCase = true + ) + }) continue + + return file.path.toUri() + } + } + return null + } + + private val videoFormats = arrayOf( + ".3g2", + ".3gp", + ".amv", + ".asf", + ".avi", + ".drc", + ".flv", + ".f4v", + ".f4p", + ".f4a", + ".f4b", + ".gif", + ".gifv", + ".m4v", + ".mkv", + ".mng", + ".mov", + ".qt", + ".mp4", + ".m4p", + ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", + ".mpg", ".mpeg", ".m2v", + ".MTS", ".M2TS", ".TS", + ".mxf", + ".nsv", + ".ogv", ".ogg", + //".rm", // Made for RealPlayer + //".rmvb", // Made for RealPlayer + ".svi", + ".viv", + ".vob", + ".webm", + ".wmv", + ".yuv" + ) + + @Throws + private suspend fun awaitAria2c( + activity: Activity, + link: ExtractorLink, + requestId: Long, + ) { + val minimumBytes: Long = 30 shl 20 + var hasFileChecked = false + while (true) { + val gid = DownloadListener.sessionIdToGid[requestId] + + // request has not yet been processed, wait for it to do + if (gid == null) { + delay(1000) + continue + } + + val metadata = DownloadListener.getInfo(gid) + event( + DownloadEvent( + downloadedBytes = metadata.downloadedLength, + downloadSpeed = metadata.downloadSpeed, + totalBytes = metadata.totalLength, + connections = metadata.items.sumOf { it.connections } + ) + ) + when (metadata.status) { + // if completed/error/removed then we don't have to wait anymore + DownloadStatusTell.Complete, + DownloadStatusTell.Error, + DownloadStatusTell.Removed -> break + + // if waiting to be added, wait more + DownloadStatusTell.Waiting -> { + delay(1000) + continue + } + + DownloadStatusTell.Active -> { + //metadata.downloadedLength >= metadata.totalLength && + if (getPlayableFile( + metadata, + minimumBytes = minimumBytes + ) != null + ) break + + // as we don't want to waste the users time with torrents that is useless + // we do this to check that at a video file exists + if (!hasFileChecked && metadata.totalLength > minimumBytes) { + hasFileChecked = true + if (getPlayableFile( + metadata, + minimumBytes = -1 + ) == null + ) { + throw Exception("Download file has no video") + } + } + + println("downloaded ${metadata.downloadedLength}/${metadata.totalLength}") + delay(1000) + continue + } + + // if downloading then check if we have reached a stable file length + /*DownloadStatusTell.Active -> { + if (getPlayableFile(metadata, minimumBytes = 50 shl 20) != null) { + break + } + delay(1000) + continue + }*/ + + // unpause any pending files + DownloadStatusTell.Paused -> { + Aria2Starter.unpause(gid) + delay(1000) + continue + } + + null -> { + delay(1000) + continue + } + } + } + + val gid = DownloadListener.sessionIdToGid[requestId] + ?: throw Exception("Unable to start download") + + val metadata = DownloadListener.getInfo(gid) + + when (metadata.status) { + DownloadStatusTell.Active, DownloadStatusTell.Complete -> { + val uri = getPlayableFile(metadata, minimumBytes = minimumBytes) + ?: throw Exception("Not downloaded enough") + activity.runOnUiThread { + //Log.i(TAG, "downloaded data: $metadata") + exoPlayer?.release() + exoPlayer = null + loadOfflinePlayer( + activity, + ExtractorUri( + // we require at least 10MB to play the file + uri = uri, + name = link.name, + tvType = TvType.Torrent + ) + ) + } + } + + DownloadStatusTell.Waiting -> { + throw Exception("Download was unable to be started") + } + + DownloadStatusTell.Paused -> { + throw Exception("Download is paused") + } + + DownloadStatusTell.Error -> { + throw Exception("Download error") + } + + DownloadStatusTell.Removed -> { + throw Exception("Download removed") + } + + null -> { + throw Exception("Unexpected download error") + } + } + } + + private fun releaseAria2c() = pauseAllAria2c() + private fun pauseAllAria2c() { + for ((_, gid) in DownloadListener.sessionIdToGid) { + Aria2Starter.pause(gid) + } + } + + + @Throws + private suspend fun playAria2c(activity: Activity, link: ExtractorLink) { + // ephemeral id based on url to make it unique + val requestId = link.url.hashCode().toLong() + currentAria2cRequestId = requestId + currentAria2cRequestLink = link + + val uriReq = UriRequest( + id = requestId, + uris = listOf(link.url), + args = Aria2Args( + headers = link.headers, + referer = link.referer, + /** torrent specifics to make it possible to stream */ + seedRatio = 0.0f, + seedTimeMin = 0.0f, + btPieceSelector = BtPieceSelector.Inorder, + followTorrent = FollowMetaLinkType.Mem, + fileAllocation = FileAllocationType.None, + btPrioritizePiece = "head=30M,tail=30M", + /** Best trackers to make it faster */ + btTracker = listOf( + "udp://tracker.opentrackr.org:1337/announce", + "https://tracker2.ctix.cn/announce", + "https://tracker1.520.jp:443/announce", + "udp://opentracker.i2p.rocks:6969/announce", + "udp://open.tracker.cl:1337/announce", + "udp://open.demonii.com:1337/announce", + "http://tracker.openbittorrent.com:80/announce", + "udp://tracker.openbittorrent.com:6969/announce", + "udp://open.stealth.si:80/announce", + "udp://exodus.desync.com:6969/announce", + "udp://tracker-udp.gbitt.info:80/announce", + "udp://explodie.org:6969/announce", + "https://tracker.gbitt.info:443/announce", + "http://tracker.gbitt.info:80/announce", + "udp://uploads.gamecoast.net:6969/announce", + "udp://tracker1.bt.moack.co.kr:80/announce", + "udp://tracker.tiny-vps.com:6969/announce", + "udp://tracker.theoks.net:6969/announce", + "udp://tracker.dump.cl:6969/announce", + "udp://tracker.bittor.pw:1337/announce", + "https://tracker1.520.jp:443/announce", + "udp://opentracker.i2p.rocks:6969/announce", + "udp://open.tracker.cl:1337/announce", + "udp://open.demonii.com:1337/announce", + "http://tracker.openbittorrent.com:80/announce", + "udp://tracker.openbittorrent.com:6969/announce", + "udp://open.stealth.si:80/announce", + "udp://exodus.desync.com:6969/announce", + "udp://tracker-udp.gbitt.info:80/announce", + "udp://explodie.org:6969/announce", + "https://tracker.gbitt.info:443/announce", + "http://tracker.gbitt.info:80/announce", + "udp://uploads.gamecoast.net:6969/announce", + "udp://tracker1.bt.moack.co.kr:80/announce", + "udp://tracker.tiny-vps.com:6969/announce", + "udp://tracker.theoks.net:6969/announce", + "udp://tracker.dump.cl:6969/announce", + "udp://tracker.bittor.pw:1337/announce" + ) + ) + ) + + val metadata = + DownloadListener.sessionIdToGid[requestId]?.let { gid -> DownloadListener.getInfo(gid) } + + when (metadata?.status) { + DownloadStatusTell.Removed, DownloadStatusTell.Error, null -> { + Aria2Starter.download(uriReq) + } + + else -> Unit + } + + try { + saveData() + awaitAria2c(activity, link, requestId) + } catch (t: Throwable) { + // if we detect any download error then we delete it as we don't want any useless background tasks + Aria2Starter.delete(DownloadListener.sessionIdToGid[requestId], requestId) + throw t + } + } + + private fun loadAria2c(link: ExtractorLink) { + val act = CommonActivity.activity + if (act == null) { + event(ErrorEvent(IllegalArgumentException("No activity"))) + return + } + + CoroutineScope(Dispatchers.IO).launch { + try { + val defaultDirectory = "${act.cacheDir.path}/torrent_tmp" + + // start the client if not active, lazy init + if (Aria2Starter.client == null) { + Aria2Starter.start( + activity = act, + Aria2Settings( + UUID.randomUUID().toString(), + 4337, + defaultDirectory, + ) + ) + // remove all the cache + //File(defaultDirectory).deleteRecursively() + } + playAria2c(act, link) + } catch (t: Throwable) { + event(ErrorEvent(t)) + } + } + } + + /** hijacks the torrent links and downloads them with aria2c instead to be played */ + private fun loadOnlinePlayer(context: Context, link: ExtractorLink) { + when (link.type) { + ExtractorLinkType.TORRENT, ExtractorLinkType.MAGNET -> loadAria2c(link) + else -> { + loadOnlinePlayerReal(context, link) + } + } + } + override fun loadPlayer( context: Context, sameEpisode: Boolean, @@ -212,6 +563,7 @@ class CS3IPlayer : IPlayer { if (link != null) { loadOnlinePlayer(context, link) } else if (data != null) { + currentAria2cRequestId = null loadOfflinePlayer(context, data) } } @@ -470,6 +822,7 @@ class CS3IPlayer : IPlayer { currentTextRenderer = null exoPlayer = null + releaseAria2c() //simpleCache = null } @@ -871,8 +1224,20 @@ class CS3IPlayer : IPlayer { CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source) CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source) - CSPlayerEvent.NextEpisode -> event(EpisodeSeekEvent(offset = 1, source = source)) - CSPlayerEvent.PrevEpisode -> event(EpisodeSeekEvent(offset = -1, source = source)) + CSPlayerEvent.NextEpisode -> event( + EpisodeSeekEvent( + offset = 1, + source = source + ) + ) + + CSPlayerEvent.PrevEpisode -> event( + EpisodeSeekEvent( + offset = -1, + source = source + ) + ) + CSPlayerEvent.SkipCurrentChapter -> { //val dur = this@CS3IPlayer.getDuration() ?: return@apply getCurrentTimestamp()?.let { lastTimeStamp -> @@ -1019,10 +1384,66 @@ class CS3IPlayer : IPlayer { } override fun onPlayerError(error: PlaybackException) { - // If the Network fails then ignore the exception if the duration is set. - // This is to switch mirrors automatically if the stream has not been fetched, but - // allow playing the buffer without internet as then the duration is fetched. + val aria2cRequestId = currentAria2cRequestId + val loadedLink = currentAria2cRequestLink when { + // if we are loading an torrent, then we will get these errors, in that case + // we just treat it as buffering + aria2cRequestId != null && loadedLink != null && + (error.errorCode == PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE + || error.errorCode == PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED + || error.errorCode == PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED + || error.errorCode == PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED + || error.errorCode == PlaybackException.ERROR_CODE_IO_UNSPECIFIED + ) -> { + val position = exoPlayer?.currentPosition ?: 0L + val gid = DownloadListener.sessionIdToGid[aria2cRequestId] + Log.i(TAG, "Aria2 error $error error ${error.errorCode}") + if(gid == null) { + event(ErrorEvent(error)) + super.onPlayerError(error) + return + } + event( + StatusEvent( + wasPlaying = CSPlayerLoading.IsPlaying, + isPlaying = CSPlayerLoading.IsBuffering + ) + ) + CoroutineScope(Dispatchers.IO).launchSafe { + for (i in 0..5) { + val metadata = DownloadListener.getInfo(gid) + event( + DownloadEvent( + downloadedBytes = metadata.downloadedLength, + downloadSpeed = metadata.downloadSpeed, + totalBytes = metadata.totalLength, + connections = metadata.items.sumOf { it.connections } + ) + ) + + delay(1000) + } + + // CommonActivity.activity?.runOnUiThread { + // exoPlayer?.prepare() + // } + + // shitty solution to release it every time, however we will get timeout otherwise + CommonActivity.activity?.let { act -> + try { + playbackPosition = position + awaitAria2c(act, loadedLink, aria2cRequestId) + } catch (t : Throwable) { + event(ErrorEvent(t)) + } + } + } + } + + // If the Network fails then ignore the exception if the duration is set. + // This is to switch mirrors automatically if the stream has not been fetched, but + // allow playing the buffer without internet as then the duration is fetched. error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED && exoPlayer?.duration != TIME_UNSET -> { exoPlayer?.prepare() @@ -1249,10 +1670,11 @@ class CS3IPlayer : IPlayer { } @SuppressLint("UnsafeOptInUsageError") - private fun loadOnlinePlayer(context: Context, link: ExtractorLink) { + private fun loadOnlinePlayerReal(context: Context, link: ExtractorLink) { Log.i(TAG, "loadOnlinePlayer $link") try { currentLink = link + currentAria2cRequestId = null if (ignoreSSL) { // Disables ssl check diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index b2542ffaf23..52ebcb16ad4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -7,6 +7,7 @@ import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.os.Bundle +import android.text.format.Formatter import android.util.Log import android.view.LayoutInflater import android.view.View @@ -92,6 +93,60 @@ class GeneratorPlayer : FullScreenPlayer() { private var binding: FragmentPlayerBinding? = null + override fun playerDimensionsLoaded(width: Int, height: Int) { + setPlayerDimen(width to height) + showDownloadProgress(null) + } + + override fun playerError(exception: Throwable) { + Log.i(TAG, "playerError = $currentSelectedLink") + showDownloadProgress(null) + super.playerError(exception) + } + + override fun onDownload(event: DownloadEvent) { + super.onDownload(event) + showDownloadProgress(event) + } + + /*override fun updateIsPlaying(wasPlaying: CSPlayerLoading, isPlaying: CSPlayerLoading) { + super.updateIsPlaying(wasPlaying, isPlaying) + if(isPlaying == CSPlayerLoading.IsPlaying) + activity?.runOnUiThread { + + } + }*/ + + private fun showDownloadProgress(event: DownloadEvent?) { + activity?.runOnUiThread { + if(event == null) { + binding?.downloadHeader?.isVisible = false + return@runOnUiThread + } + binding?.downloadHeader?.isVisible = true + binding?.downloadedProgress?.apply { + val indeterminate = event.totalBytes <= 0 || event.downloadedBytes <= 0 + isIndeterminate = indeterminate + if (!indeterminate) { + max = (event.totalBytes / 1000).toInt() + progress = (event.downloadedBytes / 1000).toInt() + } + } + binding?.downloadedProgressText.setText( + txt( + R.string.download_size_format, + Formatter.formatShortFileSize(context, event.downloadedBytes), + Formatter.formatShortFileSize(context, event.totalBytes) + ) + ) + val downloadSpeed = Formatter.formatShortFileSize(context, event.downloadSpeed) + binding?.downloadedProgressSpeedText?.text = event.connections?.let { connections -> + "%s/s - %d Connections".format(downloadSpeed, connections) + } ?: downloadSpeed + } + } + + private fun startLoading() { player.release() currentSelectedSubtitles = null @@ -883,10 +938,6 @@ class GeneratorPlayer : FullScreenPlayer() { } - override fun playerError(exception: Throwable) { - Log.i(TAG, "playerError = $currentSelectedLink") - super.playerError(exception) - } private fun noLinksFound() { showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT) @@ -945,7 +996,7 @@ class GeneratorPlayer : FullScreenPlayer() { var maxEpisodeSet: Int? = null var hasRequestedStamps: Boolean = false - override fun playerPositionChanged(position: Long, duration : Long) { + override fun playerPositionChanged(position: Long, duration: Long) { // Don't save livestream data if ((currentMeta as? ResultEpisode)?.tvType?.isLiveStream() == true) return @@ -1208,10 +1259,6 @@ class GeneratorPlayer : FullScreenPlayer() { } } - override fun playerDimensionsLoaded(width: Int, height : Int) { - setPlayerDimen(width to height) - } - private fun unwrapBundle(savedInstanceState: Bundle?) { Log.i(TAG, "unwrapBundle = $savedInstanceState") savedInstanceState?.let { bundle -> @@ -1358,6 +1405,11 @@ class GeneratorPlayer : FullScreenPlayer() { activity?.popCurrentPage() } + binding?.playerLoadingGoBack2?.setOnClickListener { + player.release() + activity?.popCurrentPage() + } + observe(viewModel.currentStamps) { stamps -> player.addTimeStamps(stamps) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt index af74cb57028..007dec268d7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IGenerator.kt @@ -18,7 +18,10 @@ fun LoadType.toSet() : Set { LoadType.InApp -> setOf( ExtractorLinkType.VIDEO, ExtractorLinkType.DASH, - ExtractorLinkType.M3U8 + ExtractorLinkType.M3U8, + // testing + ExtractorLinkType.TORRENT, + ExtractorLinkType.MAGNET, ) LoadType.Browser -> setOf( ExtractorLinkType.VIDEO, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index ec006234eef..ad722a27591 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -72,9 +72,20 @@ data class PositionEvent( val durationMs: Long, ) : PlayerEvent() { /** how many ms (+-) we have skipped */ - val seekMs : Long get() = toMs - fromMs + val seekMs: Long get() = toMs - fromMs } +/** Used for torrent to pre-download a video before playing it */ +data class DownloadEvent( + val downloadedBytes: Long, + val totalBytes: Long, + /** bytes / sec */ + val downloadSpeed: Long, + val connections: Int?, + + override val source: PlayerEventSource = PlayerEventSource.Player +) : PlayerEvent() + /** player error when rendering or misc, used to display toast or log */ data class ErrorEvent( val error: Throwable, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 5edff7a1431..6f5f90e07c0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -278,9 +278,9 @@ enum class ExtractorLinkType { } private fun inferTypeFromUrl(url: String): ExtractorLinkType { - val path = normalSafeApiCall { URL(url).path } + val path = try { URL(url).path } catch (_ : Throwable) { null } return when { - path?.endsWith(".m3u8") == true -> ExtractorLinkType.M3U8 + path?.endsWith(".m3u8") == true || path?.endsWith(".m3u") == true -> ExtractorLinkType.M3U8 path?.endsWith(".mpd") == true -> ExtractorLinkType.DASH path?.endsWith(".torrent") == true -> ExtractorLinkType.TORRENT url.startsWith("magnet:") -> ExtractorLinkType.MAGNET diff --git a/app/src/main/res/layout/fragment_player.xml b/app/src/main/res/layout/fragment_player.xml index a620b6aee84..67c3146d925 100644 --- a/app/src/main/res/layout/fragment_player.xml +++ b/app/src/main/res/layout/fragment_player.xml @@ -29,6 +29,78 @@ app:layout_constraintTop_toTopOf="parent" app:show_timeout="0" /> + + + + + + + + + + + + + + + + + + + + + + app:layout_constraintTop_toTopOf="parent" + tools:visibility="gone"> - + + + + --> \ No newline at end of file