Skip to content

Commit

Permalink
added torrent support
Browse files Browse the repository at this point in the history
  • Loading branch information
LagradOst committed Oct 28, 2024
1 parent 7194391 commit 9aa3b0e
Show file tree
Hide file tree
Showing 13 changed files with 1,003 additions and 113 deletions.
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ dependencies {
^ Don't Bump Jackson above 2.13.1 , Crashes on Android TV's and FireSticks that have Min API
Level 25 or Less. */

// torrent support
implementation("com.github.recloudstream:Aria2cStream:0.0.3")

// Downloading & Networking
implementation("androidx.work:work-runtime:2.9.0")
implementation("androidx.work:work-runtime-ktx:2.9.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ import com.lagradost.cloudstream3.actions.VideoClickActionHolder
import com.lagradost.cloudstream3.databinding.ToastBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.PlayerEventType
import com.lagradost.cloudstream3.ui.player.Torrent
import com.lagradost.cloudstream3.utils.UiText
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
import com.lagradost.cloudstream3.utils.AppContextUtils.isRtl
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.hasPIPPermission
Expand Down Expand Up @@ -203,6 +205,7 @@ object CommonActivity {

fun init(act: Activity) {
setActivityInstance(act)
ioSafe { Torrent.deleteAllOldFiles() }

val componentActivity = activity as? ComponentActivity ?: return

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,12 +384,20 @@ 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")
// we don't want to spam DownloadEvent
if(event !is DownloadEvent) {
Log.i(TAG, "Handle event: $event")
}
when (event) {
is DownloadEvent -> {
onDownload(event)
}
is ResizedEvent -> {
playerDimensionsLoaded(event.width, event.height)
}
Expand Down
187 changes: 177 additions & 10 deletions app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package com.lagradost.cloudstream3.ui.player

import android.annotation.SuppressLint
import android.content.Context
import android.content.DialogInterface
import android.graphics.Bitmap
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.util.Rational
import android.widget.FrameLayout
import androidx.annotation.MainThread
import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog
import androidx.media3.common.C.TIME_UNSET
import androidx.media3.common.C.TRACK_TYPE_AUDIO
import androidx.media3.common.C.TRACK_TYPE_TEXT
Expand Down Expand Up @@ -56,21 +59,30 @@ 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.ErrorLoadingException
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.USER_AGENT
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.mvvm.debugAssert
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
import com.lagradost.cloudstream3.utils.DrmExtractorLink
import com.lagradost.cloudstream3.utils.EpisodeSkip
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
import com.lagradost.cloudstream3.utils.ExtractorLinkType
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
import com.lagradost.fetchbutton.aria2c.Aria2Starter
import com.lagradost.fetchbutton.aria2c.DownloadListener
import com.lagradost.fetchbutton.aria2c.DownloadStatusTell
import kotlinx.coroutines.delay
import java.io.File
import java.util.UUID
import javax.net.ssl.HttpsURLConnection
Expand Down Expand Up @@ -244,6 +256,7 @@ class CS3IPlayer : IPlayer {
gen.clear(sameEpisode)
}
}

loadOnlinePlayer(context, link)
} else if (data != null) {
(imageGenerator as? PreviewGenerator)?.let { gen ->
Expand Down Expand Up @@ -530,7 +543,7 @@ class CS3IPlayer : IPlayer {

private fun releasePlayer(saveTime: Boolean = true) {
Log.i(TAG, "releasePlayer")

eventLooperIndex += 1
if (saveTime)
updatedTime()

Expand Down Expand Up @@ -567,6 +580,8 @@ class CS3IPlayer : IPlayer {

override fun release() {
imageGenerator.release()
Torrent.release()
Torrent.deleteAllOldFiles()
releasePlayer()
}

Expand Down Expand Up @@ -950,6 +965,35 @@ class CS3IPlayer : IPlayer {
}
}

// we want to push metadata when loading torrents, so we just set up a looper that loops until
// the index changes, this way only 1 looper is active at a time, and modifying eventLooperIndex
// will kill any active loopers
private var eventLooperIndex = 0
private fun torrentEventLooper() = ioSafe {
eventLooperIndex += 1
val currentIndex = eventLooperIndex
while (currentIndex == eventLooperIndex) {
DownloadListener.sessionIdToGid[activeTorrentRequest?.requestId]?.let { gid ->
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) {
DownloadStatusTell.Waiting -> delay(500)
DownloadStatusTell.Paused -> delay(1000)
DownloadStatusTell.Error, DownloadStatusTell.Removed, DownloadStatusTell.Complete -> return@ioSafe
null, DownloadStatusTell.Active -> Unit
}
}
delay(100)
}
}

private fun loadExo(
context: Context,
mediaSlices: List<MediaItemSlice>,
Expand Down Expand Up @@ -993,6 +1037,13 @@ class CS3IPlayer : IPlayer {
isPlaying = exo.isPlaying
}

// we want to avoid an empty exoplayer from sending events
// this is because we need PlayerAttachedEvent to be called to render the UI
// but don't really want the rest like Player.STATE_ENDED calling next episode
if(mediaSlices.isEmpty() && subSources.isEmpty()) {
return
}

exoPlayer?.addListener(object : Player.Listener {
override fun onTracksChanged(tracks: Tracks) {
normalSafeApiCall {
Expand Down Expand Up @@ -1079,6 +1130,50 @@ class CS3IPlayer : IPlayer {
}

override fun onPlayerError(error: PlaybackException) {
val request = activeTorrentRequest

// if we are loading an torrent, then we will get these errors, in that case
// we just treat it as buffering
if (request != null &&
(error.errorCode == PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE
|| error.errorCode == PlaybackException.ERROR_CODE_IO_UNSPECIFIED
|| 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_AUDIO_TRACK_INIT_FAILED
|| error.errorCode == PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND)
) {
val gid = DownloadListener.sessionIdToGid[request.requestId]
if (gid == null) {
event(ErrorEvent(error))
super.onPlayerError(error)
return
}

event(
StatusEvent(
wasPlaying = CSPlayerLoading.IsPlaying,
isPlaying = CSPlayerLoading.IsBuffering
)
)

// we have manually deleted the file without notifying the Aria2 instance
if (error.errorCode == PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND) {
Aria2Starter.delete(gid, request.requestId)
loadOnlinePlayer(context, request.request)
return
}

// give information when buffering, and after a set timeout we run again
Handler(Looper.myLooper() ?: Looper.getMainLooper()).postDelayed({
// if we have released the player while it is waiting, then do nothing
if(exoPlayer == null) return@postDelayed
playbackPosition = exoPlayer?.currentPosition ?: 0L
loadOnlinePlayer(context, request.request, retry = true)
}, 3000)
return
}

// 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.
Expand Down Expand Up @@ -1308,10 +1403,90 @@ class CS3IPlayer : IPlayer {
return exoPlayer != null
}

var activeTorrentRequest: TorrentRequest? = null
@MainThread
private fun loadTorrent(context: Context, link: ExtractorLink) {
ioSafe {
// we check exoPlayer a lot here, and that is because we don't want to load exo after
// the user has left the player, in the case that the user click back when this is
// happening
try {
if(exoPlayer == null) return@ioSafe
val request = Torrent.loadTorrent(link, eventHandler)
if(exoPlayer == null) return@ioSafe
activeTorrentRequest = request
runOnMainThread {
if(exoPlayer == null) return@runOnMainThread
releasePlayer()
loadOfflinePlayer(context, request.data)
torrentEventLooper()
}
} catch (t: Throwable) {
event(ErrorEvent(t))
}
}
}
@SuppressLint("UnsafeOptInUsageError")
private fun loadOnlinePlayer(context: Context, link: ExtractorLink) {
@MainThread
private fun loadOnlinePlayer(context: Context, link: ExtractorLink, retry : Boolean = false) {
Log.i(TAG, "loadOnlinePlayer $link")
try {
activeTorrentRequest = null
val mime = when (link.type) {
ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4
ExtractorLinkType.TORRENT, ExtractorLinkType.MAGNET -> {
// load the initial UI, we require an exoPlayer to be alive
if(!retry) {
// this causes a *bug* that restarts all torrents from 0
// but I would call this a feature
releasePlayer()
loadExo(context, listOf(), listOf(),null)
}
event(
StatusEvent(
wasPlaying = CSPlayerLoading.IsPlaying,
isPlaying = CSPlayerLoading.IsBuffering
)
)

if(Torrent.hasAcceptedTorrentForThisSession == true) {
loadTorrent(context,link)
return
}

val builder: AlertDialog.Builder = AlertDialog.Builder(context)

val dialogClickListener =
DialogInterface.OnClickListener { _, which ->
when (which) {
DialogInterface.BUTTON_POSITIVE -> {
Torrent.hasAcceptedTorrentForThisSession = true
loadTorrent(context, link)
}

DialogInterface.BUTTON_NEGATIVE -> {
Torrent.hasAcceptedTorrentForThisSession = false
event(ErrorEvent(ErrorLoadingException("Not accepted torrent")))
}
}
}

builder.setTitle(R.string.play_torrent_button)
.setMessage(R.string.torrent_info)
// Ensure that the user will not accidentally start a torrent session.
.setCancelable(false).setOnCancelListener {
event(ErrorEvent(ErrorLoadingException("Not accepted torrent")))
}
.setPositiveButton(R.string.ok, dialogClickListener)
.setNegativeButton(R.string.go_back, dialogClickListener)
.show().setDefaultFocus()

return
}
}

currentLink = link

if (ignoreSSL) {
Expand All @@ -1325,14 +1500,6 @@ class CS3IPlayer : IPlayer {
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory)
}

val mime = when (link.type) {
ExtractorLinkType.M3U8 -> MimeTypes.APPLICATION_M3U8
ExtractorLinkType.DASH -> MimeTypes.APPLICATION_MPD
ExtractorLinkType.VIDEO -> MimeTypes.VIDEO_MP4
ExtractorLinkType.TORRENT -> throw IllegalArgumentException("No torrent support")
ExtractorLinkType.MAGNET -> throw IllegalArgumentException("No magnet support")
}


val mediaItems = when (link) {
is ExtractorLinkPlayList -> link.playlist.map {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
playerFfwdHolder.startAnimation(fadeAnimation)
playerRewHolder.startAnimation(fadeAnimation)
playerPausePlay.startAnimation(fadeAnimation)
downloadBothHeader.startAnimation(fadeAnimation)

/*if (isBuffering) {
player_pause_play?.isVisible = false
Expand Down Expand Up @@ -730,6 +731,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
playerPausePlay.startAnimation(fadeAnimation)
playerFfwdHolder.startAnimation(fadeAnimation)
playerRewHolder.startAnimation(fadeAnimation)
downloadBothHeader.startAnimation(fadeAnimation)

//if (hasEpisodes)
// player_episodes_button?.startAnimation(fadeAnimation)
Expand Down Expand Up @@ -1311,7 +1313,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
// if nothing has loaded these buttons should not be visible
playerBinding?.apply {
playerSkipEpisode.isVisible = false
playerGoForward.isVisible = false
playerGoForwardRoot.isVisible = false
playerTracksBtt.isVisible = false
playerSkipOp.isVisible = false
shadowOverlay.isVisible = false
Expand Down Expand Up @@ -1520,7 +1522,8 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
mapOf(
playerGoBack to playerGoBackText,
playerRestart to playerRestartText,
playerGoForward to playerGoForwardText
playerGoForward to playerGoForwardText,
downloadHeaderToggle to downloadHeaderToggleText,
).forEach { (button, text) ->
button.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
Expand Down
Loading

0 comments on commit 9aa3b0e

Please sign in to comment.