@@ -2,14 +2,17 @@ package com.lagradost.cloudstream3.ui.player
2
2
3
3
import android.annotation.SuppressLint
4
4
import android.content.Context
5
+ import android.content.DialogInterface
5
6
import android.graphics.Bitmap
6
7
import android.net.Uri
7
8
import android.os.Handler
8
9
import android.os.Looper
9
10
import android.util.Log
10
11
import android.util.Rational
11
12
import android.widget.FrameLayout
13
+ import androidx.annotation.MainThread
12
14
import androidx.annotation.OptIn
15
+ import androidx.appcompat.app.AlertDialog
13
16
import androidx.media3.common.C.TIME_UNSET
14
17
import androidx.media3.common.C.TRACK_TYPE_AUDIO
15
18
import androidx.media3.common.C.TRACK_TYPE_TEXT
@@ -56,21 +59,30 @@ import androidx.preference.PreferenceManager
56
59
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
57
60
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
58
61
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
62
+ import com.lagradost.cloudstream3.ErrorLoadingException
59
63
import com.lagradost.cloudstream3.MainActivity.Companion.deleteFileOnExit
64
+ import com.lagradost.cloudstream3.R
60
65
import com.lagradost.cloudstream3.USER_AGENT
61
66
import com.lagradost.cloudstream3.app
62
67
import com.lagradost.cloudstream3.mvvm.debugAssert
63
68
import com.lagradost.cloudstream3.mvvm.logError
64
69
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
65
70
import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle
66
71
import com.lagradost.cloudstream3.utils.AppContextUtils.isUsingMobileData
72
+ import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
73
+ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
74
+ import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
67
75
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
68
76
import com.lagradost.cloudstream3.utils.DrmExtractorLink
69
77
import com.lagradost.cloudstream3.utils.EpisodeSkip
70
78
import com.lagradost.cloudstream3.utils.ExtractorLink
71
79
import com.lagradost.cloudstream3.utils.ExtractorLinkPlayList
72
80
import com.lagradost.cloudstream3.utils.ExtractorLinkType
73
81
import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTwoLettersToLanguage
82
+ import com.lagradost.fetchbutton.aria2c.Aria2Starter
83
+ import com.lagradost.fetchbutton.aria2c.DownloadListener
84
+ import com.lagradost.fetchbutton.aria2c.DownloadStatusTell
85
+ import kotlinx.coroutines.delay
74
86
import java.io.File
75
87
import java.util.UUID
76
88
import javax.net.ssl.HttpsURLConnection
@@ -244,6 +256,7 @@ class CS3IPlayer : IPlayer {
244
256
gen.clear(sameEpisode)
245
257
}
246
258
}
259
+
247
260
loadOnlinePlayer(context, link)
248
261
} else if (data != null ) {
249
262
(imageGenerator as ? PreviewGenerator )?.let { gen ->
@@ -530,7 +543,7 @@ class CS3IPlayer : IPlayer {
530
543
531
544
private fun releasePlayer (saveTime : Boolean = true) {
532
545
Log .i(TAG , " releasePlayer" )
533
-
546
+ eventLooperIndex + = 1
534
547
if (saveTime)
535
548
updatedTime()
536
549
@@ -567,6 +580,8 @@ class CS3IPlayer : IPlayer {
567
580
568
581
override fun release () {
569
582
imageGenerator.release()
583
+ Torrent .release()
584
+ Torrent .deleteAllOldFiles()
570
585
releasePlayer()
571
586
}
572
587
@@ -950,6 +965,35 @@ class CS3IPlayer : IPlayer {
950
965
}
951
966
}
952
967
968
+ // we want to push metadata when loading torrents, so we just set up a looper that loops until
969
+ // the index changes, this way only 1 looper is active at a time, and modifying eventLooperIndex
970
+ // will kill any active loopers
971
+ private var eventLooperIndex = 0
972
+ private fun torrentEventLooper () = ioSafe {
973
+ eventLooperIndex + = 1
974
+ val currentIndex = eventLooperIndex
975
+ while (currentIndex == eventLooperIndex) {
976
+ DownloadListener .sessionIdToGid[activeTorrentRequest?.requestId]?.let { gid ->
977
+ val metadata = DownloadListener .getInfo(gid)
978
+ event(
979
+ DownloadEvent (
980
+ downloadedBytes = metadata.downloadedLength,
981
+ downloadSpeed = metadata.downloadSpeed,
982
+ totalBytes = metadata.totalLength,
983
+ connections = metadata.items.sumOf { it.connections }
984
+ )
985
+ )
986
+ when (metadata.status) {
987
+ DownloadStatusTell .Waiting -> delay(500 )
988
+ DownloadStatusTell .Paused -> delay(1000 )
989
+ DownloadStatusTell .Error , DownloadStatusTell .Removed , DownloadStatusTell .Complete -> return @ioSafe
990
+ null , DownloadStatusTell .Active -> Unit
991
+ }
992
+ }
993
+ delay(100 )
994
+ }
995
+ }
996
+
953
997
private fun loadExo (
954
998
context : Context ,
955
999
mediaSlices : List <MediaItemSlice >,
@@ -993,6 +1037,13 @@ class CS3IPlayer : IPlayer {
993
1037
isPlaying = exo.isPlaying
994
1038
}
995
1039
1040
+ // we want to avoid an empty exoplayer from sending events
1041
+ // this is because we need PlayerAttachedEvent to be called to render the UI
1042
+ // but don't really want the rest like Player.STATE_ENDED calling next episode
1043
+ if (mediaSlices.isEmpty() && subSources.isEmpty()) {
1044
+ return
1045
+ }
1046
+
996
1047
exoPlayer?.addListener(object : Player .Listener {
997
1048
override fun onTracksChanged (tracks : Tracks ) {
998
1049
normalSafeApiCall {
@@ -1079,6 +1130,50 @@ class CS3IPlayer : IPlayer {
1079
1130
}
1080
1131
1081
1132
override fun onPlayerError (error : PlaybackException ) {
1133
+ val request = activeTorrentRequest
1134
+
1135
+ // if we are loading an torrent, then we will get these errors, in that case
1136
+ // we just treat it as buffering
1137
+ if (request != null &&
1138
+ (error.errorCode == PlaybackException .ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE
1139
+ || error.errorCode == PlaybackException .ERROR_CODE_IO_UNSPECIFIED
1140
+ || error.errorCode == PlaybackException .ERROR_CODE_PARSING_MANIFEST_MALFORMED
1141
+ || error.errorCode == PlaybackException .ERROR_CODE_PARSING_CONTAINER_MALFORMED
1142
+ || error.errorCode == PlaybackException .ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED
1143
+ || error.errorCode == PlaybackException .ERROR_CODE_AUDIO_TRACK_INIT_FAILED
1144
+ || error.errorCode == PlaybackException .ERROR_CODE_IO_FILE_NOT_FOUND )
1145
+ ) {
1146
+ val gid = DownloadListener .sessionIdToGid[request.requestId]
1147
+ if (gid == null ) {
1148
+ event(ErrorEvent (error))
1149
+ super .onPlayerError(error)
1150
+ return
1151
+ }
1152
+
1153
+ event(
1154
+ StatusEvent (
1155
+ wasPlaying = CSPlayerLoading .IsPlaying ,
1156
+ isPlaying = CSPlayerLoading .IsBuffering
1157
+ )
1158
+ )
1159
+
1160
+ // we have manually deleted the file without notifying the Aria2 instance
1161
+ if (error.errorCode == PlaybackException .ERROR_CODE_IO_FILE_NOT_FOUND ) {
1162
+ Aria2Starter .delete(gid, request.requestId)
1163
+ loadOnlinePlayer(context, request.request)
1164
+ return
1165
+ }
1166
+
1167
+ // give information when buffering, and after a set timeout we run again
1168
+ Handler (Looper .myLooper() ? : Looper .getMainLooper()).postDelayed({
1169
+ // if we have released the player while it is waiting, then do nothing
1170
+ if (exoPlayer == null ) return @postDelayed
1171
+ playbackPosition = exoPlayer?.currentPosition ? : 0L
1172
+ loadOnlinePlayer(context, request.request, retry = true )
1173
+ }, 3000 )
1174
+ return
1175
+ }
1176
+
1082
1177
// If the Network fails then ignore the exception if the duration is set.
1083
1178
// This is to switch mirrors automatically if the stream has not been fetched, but
1084
1179
// allow playing the buffer without internet as then the duration is fetched.
@@ -1308,10 +1403,90 @@ class CS3IPlayer : IPlayer {
1308
1403
return exoPlayer != null
1309
1404
}
1310
1405
1406
+ var activeTorrentRequest: TorrentRequest ? = null
1407
+ @MainThread
1408
+ private fun loadTorrent (context : Context , link : ExtractorLink ) {
1409
+ ioSafe {
1410
+ // we check exoPlayer a lot here, and that is because we don't want to load exo after
1411
+ // the user has left the player, in the case that the user click back when this is
1412
+ // happening
1413
+ try {
1414
+ if (exoPlayer == null ) return @ioSafe
1415
+ val request = Torrent .loadTorrent(link, eventHandler)
1416
+ if (exoPlayer == null ) return @ioSafe
1417
+ activeTorrentRequest = request
1418
+ runOnMainThread {
1419
+ if (exoPlayer == null ) return @runOnMainThread
1420
+ releasePlayer()
1421
+ loadOfflinePlayer(context, request.data)
1422
+ torrentEventLooper()
1423
+ }
1424
+ } catch (t: Throwable ) {
1425
+ event(ErrorEvent (t))
1426
+ }
1427
+ }
1428
+ }
1311
1429
@SuppressLint(" UnsafeOptInUsageError" )
1312
- private fun loadOnlinePlayer (context : Context , link : ExtractorLink ) {
1430
+ @MainThread
1431
+ private fun loadOnlinePlayer (context : Context , link : ExtractorLink , retry : Boolean = false) {
1313
1432
Log .i(TAG , " loadOnlinePlayer $link " )
1314
1433
try {
1434
+ activeTorrentRequest = null
1435
+ val mime = when (link.type) {
1436
+ ExtractorLinkType .M3U8 -> MimeTypes .APPLICATION_M3U8
1437
+ ExtractorLinkType .DASH -> MimeTypes .APPLICATION_MPD
1438
+ ExtractorLinkType .VIDEO -> MimeTypes .VIDEO_MP4
1439
+ ExtractorLinkType .TORRENT , ExtractorLinkType .MAGNET -> {
1440
+ // load the initial UI, we require an exoPlayer to be alive
1441
+ if (! retry) {
1442
+ // this causes a *bug* that restarts all torrents from 0
1443
+ // but I would call this a feature
1444
+ releasePlayer()
1445
+ loadExo(context, listOf (), listOf (),null )
1446
+ }
1447
+ event(
1448
+ StatusEvent (
1449
+ wasPlaying = CSPlayerLoading .IsPlaying ,
1450
+ isPlaying = CSPlayerLoading .IsBuffering
1451
+ )
1452
+ )
1453
+
1454
+ if (Torrent .hasAcceptedTorrentForThisSession == true ) {
1455
+ loadTorrent(context,link)
1456
+ return
1457
+ }
1458
+
1459
+ val builder: AlertDialog .Builder = AlertDialog .Builder (context)
1460
+
1461
+ val dialogClickListener =
1462
+ DialogInterface .OnClickListener { _, which ->
1463
+ when (which) {
1464
+ DialogInterface .BUTTON_POSITIVE -> {
1465
+ Torrent .hasAcceptedTorrentForThisSession = true
1466
+ loadTorrent(context, link)
1467
+ }
1468
+
1469
+ DialogInterface .BUTTON_NEGATIVE -> {
1470
+ Torrent .hasAcceptedTorrentForThisSession = false
1471
+ event(ErrorEvent (ErrorLoadingException (" Not accepted torrent" )))
1472
+ }
1473
+ }
1474
+ }
1475
+
1476
+ builder.setTitle(R .string.play_torrent_button)
1477
+ .setMessage(R .string.torrent_info)
1478
+ // Ensure that the user will not accidentally start a torrent session.
1479
+ .setCancelable(false ).setOnCancelListener {
1480
+ event(ErrorEvent (ErrorLoadingException (" Not accepted torrent" )))
1481
+ }
1482
+ .setPositiveButton(R .string.ok, dialogClickListener)
1483
+ .setNegativeButton(R .string.go_back, dialogClickListener)
1484
+ .show().setDefaultFocus()
1485
+
1486
+ return
1487
+ }
1488
+ }
1489
+
1315
1490
currentLink = link
1316
1491
1317
1492
if (ignoreSSL) {
@@ -1325,14 +1500,6 @@ class CS3IPlayer : IPlayer {
1325
1500
HttpsURLConnection .setDefaultSSLSocketFactory(sslContext.socketFactory)
1326
1501
}
1327
1502
1328
- val mime = when (link.type) {
1329
- ExtractorLinkType .M3U8 -> MimeTypes .APPLICATION_M3U8
1330
- ExtractorLinkType .DASH -> MimeTypes .APPLICATION_MPD
1331
- ExtractorLinkType .VIDEO -> MimeTypes .VIDEO_MP4
1332
- ExtractorLinkType .TORRENT -> throw IllegalArgumentException (" No torrent support" )
1333
- ExtractorLinkType .MAGNET -> throw IllegalArgumentException (" No magnet support" )
1334
- }
1335
-
1336
1503
1337
1504
val mediaItems = when (link) {
1338
1505
is ExtractorLinkPlayList -> link.playlist.map {
0 commit comments