torrent testing

This commit is contained in:
LagradOst 2023-09-14 18:46:34 +02:00
parent 2bed79b1f1
commit d394f0e1d0
8 changed files with 510 additions and 54 deletions

View file

@ -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,26 @@ 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",
url = "",
"",
Qualities.Unknown.value,
type = INFER_TYPE
)
),
emptyList()
)
)
)*/
// Used to check current focus for TV
// main {
// while (true) {

View file

@ -370,12 +370,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)
}

View file

@ -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,6 +51,8 @@ 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
@ -63,6 +67,15 @@ 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.Aria2Settings
import com.lagradost.fetchbutton.aria2c.Aria2Starter
import com.lagradost.fetchbutton.aria2c.DownloadListener
import com.lagradost.fetchbutton.aria2c.DownloadStatusTell
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 +101,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
}
@ -182,6 +197,273 @@ 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,
) {
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 -> {
if (metadata.downloadedLength >= metadata.totalLength && getPlayableFile(
metadata,
minimumBytes = 10 shl 20
) != 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 > (10 shl 20)) {
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 -> break
}
}
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 = 10 shl 20)
?: throw Exception("Not downloaded enough")
activity.runOnUiThread {
//Log.i(TAG, "downloaded data: $metadata")
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()
val uriReq = newUriRequest(
id = requestId,
uri = link.url,
fileName = null,
seed = false
)
val metadata =
DownloadListener.sessionIdToGid[requestId]?.let { gid -> DownloadListener.getInfo(gid) }
when (metadata?.status) {
DownloadStatusTell.Removed, DownloadStatusTell.Error, null -> {
Aria2Starter.download(uriReq)
}
else -> Unit
}
try {
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,
@ -470,6 +752,7 @@ class CS3IPlayer : IPlayer {
currentTextRenderer = null
exoPlayer = null
releaseAria2c()
//simpleCache = null
}
@ -871,8 +1154,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 ->
@ -1249,7 +1544,7 @@ 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

View file

@ -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,52 @@ 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)
}
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 +930,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 +988,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 +1251,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 +1397,11 @@ class GeneratorPlayer : FullScreenPlayer() {
activity?.popCurrentPage()
}
binding?.playerLoadingGoBack2?.setOnClickListener {
player.release()
activity?.popCurrentPage()
}
observe(viewModel.currentStamps) { stamps ->
player.addTimeStamps(stamps)
}

View file

@ -18,7 +18,10 @@ fun LoadType.toSet() : Set<ExtractorLinkType> {
LoadType.InApp -> setOf(
ExtractorLinkType.VIDEO,
ExtractorLinkType.DASH,
ExtractorLinkType.M3U8
ExtractorLinkType.M3U8,
// testing
ExtractorLinkType.TORRENT,
ExtractorLinkType.MAGNET,
)
LoadType.Browser -> setOf(
ExtractorLinkType.VIDEO,

View file

@ -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,

View file

@ -280,7 +280,7 @@ enum class ExtractorLinkType {
private fun inferTypeFromUrl(url: String): ExtractorLinkType {
val path = normalSafeApiCall { URL(url).path }
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

View file

@ -29,6 +29,78 @@
app:layout_constraintTop_toTopOf="parent"
app:show_timeout="0" />
<FrameLayout
android:id="@+id/download_header"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:backgroundTint="@android:color/black">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<TextView
android:id="@+id/downloaded_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
tools:text="10MB / 20MB" />
<TextView
android:id="@+id/downloaded_progress_speed_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/white"
tools:text="10MB/s - 12 seeders" />
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/downloaded_progress"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="200dp"
android:layout_height="15dp"
android:layout_gravity="center"
android:layout_marginBottom="-6.5dp"
android:indeterminate="false"
android:indeterminateTint="?attr/colorPrimary"
android:progressBackgroundTint="?attr/colorPrimary"
android:progressTint="?attr/colorPrimary"
tools:progress="20" />
</LinearLayout>
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:contentDescription="@string/go_back_img_des"
android:src="@drawable/ic_baseline_arrow_back_24"
app:tint="@android:color/white" />
<ImageView
android:id="@+id/player_loading_go_back2"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_gravity="center"
android:background="@drawable/video_tap_button_always_white"
android:clickable="true"
android:contentDescription="@string/go_back_img_des"
android:focusable="true" />
</FrameLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/player_loading_overlay"
android:layout_width="match_parent"
@ -38,7 +110,8 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
tools:visibility="gone">
<com.google.android.material.button.MaterialButton
android:id="@+id/overlay_loading_skip_button"
@ -93,39 +166,39 @@
</FrameLayout>
</FrameLayout>
<!-- <FrameLayout
android:id="@+id/player_torrent_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<!-- <FrameLayout
android:id="@+id/player_torrent_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/video_torrent_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:gravity="start"
android:textColor="@color/white"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="78% at 18kb/s" />
<TextView
android:id="@+id/video_torrent_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:gravity="start"
android:textColor="@color/white"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="78% at 18kb/s" />
<TextView
android:id="@+id/video_torrent_seeders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:gravity="start"
android:textColor="@color/white"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/player_video_title"
tools:text="17 seeders" />
</FrameLayout>-->
<TextView
android:id="@+id/video_torrent_seeders"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:gravity="start"
android:textColor="@color/white"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@+id/player_video_title"
tools:text="17 seeders" />
</FrameLayout>-->
</androidx.constraintlayout.widget.ConstraintLayout>