mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
preview seekbar m3u8
This commit is contained in:
parent
1d90858f64
commit
08060314ad
4 changed files with 298 additions and 27 deletions
|
@ -225,20 +225,15 @@ class CS3IPlayer : IPlayer {
|
||||||
releasePlayer()
|
releasePlayer()
|
||||||
if (link != null) {
|
if (link != null) {
|
||||||
// only video support atm
|
// only video support atm
|
||||||
if (link.type == ExtractorLinkType.VIDEO && preview) {
|
if (preview) {
|
||||||
val headers = if (link.referer.isBlank()) {
|
imageGenerator.load(link, sameEpisode)
|
||||||
link.headers
|
|
||||||
} else {
|
|
||||||
mapOf("referer" to link.referer) + link.headers
|
|
||||||
}
|
|
||||||
imageGenerator.load(sameEpisode, link.url, headers)
|
|
||||||
} else {
|
} else {
|
||||||
imageGenerator.clear(sameEpisode)
|
imageGenerator.clear(sameEpisode)
|
||||||
}
|
}
|
||||||
loadOnlinePlayer(context, link)
|
loadOnlinePlayer(context, link)
|
||||||
} else if (data != null) {
|
} else if (data != null) {
|
||||||
if (preview) {
|
if (preview) {
|
||||||
imageGenerator.load(sameEpisode, context, data.uri)
|
imageGenerator.load(context, data, sameEpisode)
|
||||||
} else {
|
} else {
|
||||||
imageGenerator.clear(sameEpisode)
|
imageGenerator.clear(sameEpisode)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,18 @@ import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||||
|
import com.lagradost.cloudstream3.utils.ExtractorUri
|
||||||
|
import com.lagradost.cloudstream3.utils.M3u8Helper
|
||||||
|
import com.lagradost.cloudstream3.utils.M3u8Helper2
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.ceil
|
import kotlin.math.ceil
|
||||||
import kotlin.math.log2
|
import kotlin.math.log2
|
||||||
|
@ -17,7 +25,248 @@ import kotlin.math.log2
|
||||||
const val MAX_LOD = 6
|
const val MAX_LOD = 6
|
||||||
const val MIN_LOD = 3
|
const val MIN_LOD = 3
|
||||||
|
|
||||||
class PreviewGenerator {
|
interface IPreviewGenerator {
|
||||||
|
fun hasPreview(): Boolean
|
||||||
|
fun getPreviewImage(fraction: Float): Bitmap?
|
||||||
|
fun clear(keepCache: Boolean = false)
|
||||||
|
fun release()
|
||||||
|
}
|
||||||
|
|
||||||
|
class PreviewGenerator : IPreviewGenerator {
|
||||||
|
private var currentGenerator: IPreviewGenerator = NoPreviewGenerator()
|
||||||
|
override fun hasPreview(): Boolean {
|
||||||
|
return currentGenerator.hasPreview()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPreviewImage(fraction: Float): Bitmap? {
|
||||||
|
return try {
|
||||||
|
currentGenerator.getPreviewImage(fraction)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
logError(t)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear(keepCache: Boolean) {
|
||||||
|
currentGenerator.clear(keepCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun release() {
|
||||||
|
currentGenerator.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(link: ExtractorLink, keepCache: Boolean) {
|
||||||
|
val gen = currentGenerator
|
||||||
|
when (link.type) {
|
||||||
|
ExtractorLinkType.M3U8 -> {
|
||||||
|
if (gen is M3u8PreviewGenerator) {
|
||||||
|
gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders())
|
||||||
|
} else {
|
||||||
|
currentGenerator.release()
|
||||||
|
currentGenerator = M3u8PreviewGenerator().apply {
|
||||||
|
load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtractorLinkType.VIDEO -> {
|
||||||
|
if (gen is Mp4PreviewGenerator) {
|
||||||
|
gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders())
|
||||||
|
} else {
|
||||||
|
currentGenerator.release()
|
||||||
|
currentGenerator = Mp4PreviewGenerator().apply {
|
||||||
|
load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
currentGenerator.clear(keepCache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun load(context: Context, link: ExtractorUri, keepCache: Boolean) {
|
||||||
|
val gen = currentGenerator
|
||||||
|
if (gen is Mp4PreviewGenerator) {
|
||||||
|
gen.load(keepCache = keepCache, context = context, uri = link.uri)
|
||||||
|
} else {
|
||||||
|
currentGenerator.release()
|
||||||
|
currentGenerator = Mp4PreviewGenerator().apply {
|
||||||
|
load(keepCache = keepCache, context = context, uri = link.uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoPreviewGenerator : IPreviewGenerator {
|
||||||
|
override fun hasPreview(): Boolean = false
|
||||||
|
override fun getPreviewImage(fraction: Float): Bitmap? = null
|
||||||
|
override fun clear(keepCache: Boolean) = Unit
|
||||||
|
override fun release() = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
class M3u8PreviewGenerator : IPreviewGenerator {
|
||||||
|
// generated images 1:1 to idx of hsl
|
||||||
|
private var images: Array<Bitmap?> = arrayOf()
|
||||||
|
|
||||||
|
private val TAG = "PreviewImgM3u8"
|
||||||
|
|
||||||
|
// prefixSum[i] = sum(hsl.ts[0..i].time)
|
||||||
|
// where [0] = 0, [1] = hsl.ts[0].time aka time at start of segment, do [b] - [a] for range a,b
|
||||||
|
private var prefixSum: Array<Double> = arrayOf()
|
||||||
|
|
||||||
|
// how many images has been generated
|
||||||
|
private var loadedImages: Int = 0
|
||||||
|
|
||||||
|
// how many images we can generate in total, == hsl.size ?: 0
|
||||||
|
private var totalImages: Int = 0
|
||||||
|
|
||||||
|
override fun hasPreview(): Boolean {
|
||||||
|
return totalImages > 0 && loadedImages >= minOf(totalImages, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPreviewImage(fraction: Float): Bitmap? {
|
||||||
|
var bestIdx = -1
|
||||||
|
var bestDiff = Double.MAX_VALUE
|
||||||
|
synchronized(images) {
|
||||||
|
// just find the best one in a for loop, we don't care about bin searching rn
|
||||||
|
for (i in 0..images.size) {
|
||||||
|
val diff = prefixSum[i].minus(fraction).absoluteValue
|
||||||
|
if (diff > bestDiff) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if (images[i] != null) {
|
||||||
|
bestIdx = i
|
||||||
|
bestDiff = diff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return images.getOrNull(bestIdx)
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
val targetIndex = prefixSum.binarySearch(target)
|
||||||
|
var ret = images[targetIndex]
|
||||||
|
if (ret != null) {
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
for (i in 0..images.size) {
|
||||||
|
ret = images.getOrNull(i+targetIndex) ?:
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun clear(keepCache: Boolean) {
|
||||||
|
synchronized(images) {
|
||||||
|
currentJob?.cancel()
|
||||||
|
images = arrayOf()
|
||||||
|
prefixSum = arrayOf()
|
||||||
|
loadedImages = 0
|
||||||
|
totalImages = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun release() {
|
||||||
|
clear()
|
||||||
|
images = arrayOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var currentJob: Job? = null
|
||||||
|
fun load(keepCache: Boolean, url: String, headers: Map<String, String>) {
|
||||||
|
clear(keepCache)
|
||||||
|
currentJob?.cancel()
|
||||||
|
currentJob = ioSafe {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
Log.i(TAG, "Loading with url = $url headers = $headers")
|
||||||
|
//tmpFile =
|
||||||
|
// File.createTempFile("video", ".ts", context.cacheDir).apply {
|
||||||
|
// deleteOnExit()
|
||||||
|
// }
|
||||||
|
val retriever = MediaMetadataRetriever()
|
||||||
|
val hsl = M3u8Helper2.hslLazy(
|
||||||
|
listOf(
|
||||||
|
M3u8Helper.M3u8Stream(
|
||||||
|
streamUrl = url,
|
||||||
|
headers = headers
|
||||||
|
)
|
||||||
|
),
|
||||||
|
selectBest = false
|
||||||
|
)
|
||||||
|
|
||||||
|
// no support for encryption atm
|
||||||
|
if (hsl.isEncrypted) {
|
||||||
|
totalImages = 0
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
// total duration of the entire m3u8 in seconds
|
||||||
|
val duration = hsl.allTsLinks.sumOf { it.time ?: 0.0 }
|
||||||
|
val durationInv = 1.0 / duration
|
||||||
|
|
||||||
|
// if the total duration is less then 10s then something is very wrong or
|
||||||
|
// too short playback to matter
|
||||||
|
if (duration <= 10.0) {
|
||||||
|
totalImages = 0
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
|
||||||
|
totalImages = hsl.allTsLinks.size
|
||||||
|
|
||||||
|
// we cant init directly as it is no guarantee of in order
|
||||||
|
prefixSum = Array(hsl.allTsLinks.size + 1) { 0.0 }
|
||||||
|
var runningSum = 0.0
|
||||||
|
for (i in hsl.allTsLinks.indices) {
|
||||||
|
runningSum += (hsl.allTsLinks[i].time ?: 0.0)
|
||||||
|
prefixSum[i + 1] = runningSum * durationInv
|
||||||
|
}
|
||||||
|
synchronized(images) {
|
||||||
|
images = Array(hsl.size) { null }
|
||||||
|
loadedImages = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val maxLod = ceil(log2(duration)).toInt().coerceIn(MIN_LOD, MAX_LOD)
|
||||||
|
val count = hsl.allTsLinks.size
|
||||||
|
for (l in 1..maxLod) {
|
||||||
|
val items = (1 shl (l - 1))
|
||||||
|
for (i in 0 until items) {
|
||||||
|
val index = (count.div(1 shl l) + (i * count) / items).coerceIn(0, hsl.size)
|
||||||
|
if (synchronized(images) { images[index] } != null) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Generating preview for $index")
|
||||||
|
|
||||||
|
val ts = hsl.allTsLinks[index]
|
||||||
|
try {
|
||||||
|
retriever.setDataSource(ts.url, hsl.headers)
|
||||||
|
if (!isActive) {
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
val frame = retriever.getFrameAtTime(0)
|
||||||
|
if (!isActive) {
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
|
synchronized(images) {
|
||||||
|
images[index] = frame
|
||||||
|
loadedImages += 1
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
logError(t)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
val buffer = hsl.resolveLinkSafe(index) ?: continue
|
||||||
|
tmpFile?.writeBytes(buffer)
|
||||||
|
val buff = FileOutputStream(tmpFile)
|
||||||
|
retriever.setDataSource(buff.fd)
|
||||||
|
val frame = retriever.getFrameAtTime(0L)*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Mp4PreviewGenerator : IPreviewGenerator {
|
||||||
// lod = level of detail where the number indicates how many ones there is
|
// lod = level of detail where the number indicates how many ones there is
|
||||||
// 2^(lod-1) = images
|
// 2^(lod-1) = images
|
||||||
private var loadedLod = 0
|
private var loadedLod = 0
|
||||||
|
@ -26,15 +275,15 @@ class PreviewGenerator {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasPreview(): Boolean {
|
override fun hasPreview(): Boolean {
|
||||||
synchronized(images) {
|
synchronized(images) {
|
||||||
return loadedLod >= MIN_LOD
|
return loadedLod >= MIN_LOD
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val TAG = "PreviewImg"
|
val TAG = "PreviewImgMp4"
|
||||||
|
|
||||||
fun getPreviewImage(fraction: Float): Bitmap? {
|
override fun getPreviewImage(fraction: Float): Bitmap? {
|
||||||
synchronized(images) {
|
synchronized(images) {
|
||||||
if (loadedLod < MIN_LOD) {
|
if (loadedLod < MIN_LOD) {
|
||||||
Log.i(TAG, "Requesting preview for $fraction but $loadedLod < $MIN_LOD")
|
Log.i(TAG, "Requesting preview for $fraction but $loadedLod < $MIN_LOD")
|
||||||
|
@ -70,7 +319,7 @@ class PreviewGenerator {
|
||||||
// also check out https://github.com/wseemann/FFmpegMediaMetadataRetriever
|
// also check out https://github.com/wseemann/FFmpegMediaMetadataRetriever
|
||||||
private val retriever: MediaMetadataRetriever = MediaMetadataRetriever()
|
private val retriever: MediaMetadataRetriever = MediaMetadataRetriever()
|
||||||
|
|
||||||
fun clear(keepCache: Boolean = false) {
|
override fun clear(keepCache: Boolean) {
|
||||||
if (keepCache) return
|
if (keepCache) return
|
||||||
synchronized(images) {
|
synchronized(images) {
|
||||||
loadedLod = 0
|
loadedLod = 0
|
||||||
|
@ -100,7 +349,7 @@ class PreviewGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun release() {
|
override fun release() {
|
||||||
currentJob?.cancel()
|
currentJob?.cancel()
|
||||||
clear(false)
|
clear(false)
|
||||||
}
|
}
|
||||||
|
@ -135,7 +384,7 @@ class PreviewGenerator {
|
||||||
if (!scope.isActive) return
|
if (!scope.isActive) return
|
||||||
synchronized(images) {
|
synchronized(images) {
|
||||||
images[idx] = img
|
images[idx] = img
|
||||||
loadedImages = maxOf(loadedImages,idx)
|
loadedImages = maxOf(loadedImages, idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -378,6 +378,15 @@ open class ExtractorLink constructor(
|
||||||
val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8
|
val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8
|
||||||
val isDash : Boolean get() = type == ExtractorLinkType.DASH
|
val isDash : Boolean get() = type == ExtractorLinkType.DASH
|
||||||
|
|
||||||
|
fun getAllHeaders() : Map<String, String> {
|
||||||
|
if (referer.isBlank()) {
|
||||||
|
return headers
|
||||||
|
} else if (headers.keys.none { it.equals("referer", ignoreCase = true) }) {
|
||||||
|
return headers + mapOf("referer" to referer)
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
source: String,
|
source: String,
|
||||||
name: String,
|
name: String,
|
||||||
|
|
|
@ -71,7 +71,7 @@ object M3u8Helper2 {
|
||||||
private val QUALITY_REGEX =
|
private val QUALITY_REGEX =
|
||||||
Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""")
|
Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""")
|
||||||
private val TS_EXTENSION_REGEX =
|
private val TS_EXTENSION_REGEX =
|
||||||
Regex("""#EXTINF:.*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways
|
Regex("""#EXTINF:(([0-9]*[.])?[0-9]+|).*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways
|
||||||
//Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts
|
//Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts
|
||||||
|
|
||||||
private fun absoluteExtensionDetermination(url: String): String? {
|
private fun absoluteExtensionDetermination(url: String): String? {
|
||||||
|
@ -122,6 +122,15 @@ object M3u8Helper2 {
|
||||||
return result.lastOrNull()
|
return result.lastOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun selectWorst(qualities: List<M3u8Helper.M3u8Stream>): M3u8Helper.M3u8Stream? {
|
||||||
|
val result = qualities.sortedBy {
|
||||||
|
if (it.quality != null && it.quality <= 1080) it.quality else 0
|
||||||
|
}.filter {
|
||||||
|
listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl))
|
||||||
|
}
|
||||||
|
return result.firstOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getParentLink(uri: String): String {
|
private fun getParentLink(uri: String): String {
|
||||||
val split = uri.split("/").toMutableList()
|
val split = uri.split("/").toMutableList()
|
||||||
split.removeLast()
|
split.removeLast()
|
||||||
|
@ -173,14 +182,20 @@ object M3u8Helper2 {
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class TsLink(
|
||||||
|
val url : String,
|
||||||
|
val time : Double?,
|
||||||
|
)
|
||||||
|
|
||||||
data class LazyHlsDownloadData(
|
data class LazyHlsDownloadData(
|
||||||
private val encryptionData: ByteArray,
|
private val encryptionData: ByteArray,
|
||||||
private val encryptionIv: ByteArray,
|
private val encryptionIv: ByteArray,
|
||||||
private val isEncrypted: Boolean,
|
val isEncrypted: Boolean,
|
||||||
private val allTsLinks: List<String>,
|
val allTsLinks: List<TsLink>,
|
||||||
private val relativeUrl: String,
|
val relativeUrl: String,
|
||||||
private val headers: Map<String, String>,
|
val headers: Map<String, String>,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val size get() = allTsLinks.size
|
val size get() = allTsLinks.size
|
||||||
|
|
||||||
suspend fun resolveLinkWhileSafe(
|
suspend fun resolveLinkWhileSafe(
|
||||||
|
@ -228,9 +243,9 @@ object M3u8Helper2 {
|
||||||
@Throws
|
@Throws
|
||||||
suspend fun resolveLink(index: Int): ByteArray {
|
suspend fun resolveLink(index: Int): ByteArray {
|
||||||
if (index < 0 || index >= size) throw IllegalArgumentException("index must be in the bounds of the ts")
|
if (index < 0 || index >= size) throw IllegalArgumentException("index must be in the bounds of the ts")
|
||||||
val url = allTsLinks[index]
|
val ts = allTsLinks[index]
|
||||||
|
|
||||||
val tsResponse = app.get(url, headers = headers, verify = false)
|
val tsResponse = app.get(ts.url, headers = headers, verify = false)
|
||||||
val tsData = tsResponse.body.bytes()
|
val tsData = tsResponse.body.bytes()
|
||||||
if (tsData.isEmpty()) throw ErrorLoadingException("no data")
|
if (tsData.isEmpty()) throw ErrorLoadingException("no data")
|
||||||
|
|
||||||
|
@ -244,15 +259,16 @@ object M3u8Helper2 {
|
||||||
|
|
||||||
@Throws
|
@Throws
|
||||||
suspend fun hslLazy(
|
suspend fun hslLazy(
|
||||||
qualities: List<M3u8Helper.M3u8Stream>
|
qualities: List<M3u8Helper.M3u8Stream>, selectBest : Boolean = true
|
||||||
): LazyHlsDownloadData {
|
): LazyHlsDownloadData {
|
||||||
if (qualities.isEmpty()) throw IllegalArgumentException("qualities must be non empty")
|
if (qualities.isEmpty()) throw IllegalArgumentException("qualities must be non empty")
|
||||||
val selected = selectBest(qualities) ?: qualities.first()
|
val selected = if(selectBest) { selectBest(qualities) } else { selectWorst(qualities) } ?: qualities.first()
|
||||||
val headers = selected.headers
|
val headers = selected.headers
|
||||||
val streams = qualities.map { m3u8Generation(it, false) }.flatten()
|
val streams = qualities.map { m3u8Generation(it, false) }.flatten()
|
||||||
// this selects the best quality of the qualities offered,
|
// this selects the best quality of the qualities offered,
|
||||||
// due to the recursive nature of m3u8, we only go 2 depth
|
// due to the recursive nature of m3u8, we only go 2 depth
|
||||||
val secondSelection = selectBest(streams.ifEmpty { listOf(selected) })
|
val innerStreams = streams.ifEmpty { listOf(selected) }
|
||||||
|
val secondSelection = if(selectBest) { selectBest(innerStreams) } else { selectWorst(innerStreams) }
|
||||||
?: throw IllegalArgumentException("qualities has no streams")
|
?: throw IllegalArgumentException("qualities has no streams")
|
||||||
|
|
||||||
val m3u8Response =
|
val m3u8Response =
|
||||||
|
@ -285,12 +301,14 @@ object M3u8Helper2 {
|
||||||
}
|
}
|
||||||
val relativeUrl = getParentLink(secondSelection.streamUrl)
|
val relativeUrl = getParentLink(secondSelection.streamUrl)
|
||||||
val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts ->
|
val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts ->
|
||||||
val value = ts.groupValues[1]
|
val time = ts.groupValues[1]
|
||||||
if (isNotCompleteUrl(value)) {
|
val value = ts.groupValues[3]
|
||||||
|
val url = if (isNotCompleteUrl(value)) {
|
||||||
"$relativeUrl/${value}"
|
"$relativeUrl/${value}"
|
||||||
} else {
|
} else {
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
TsLink(url = url, time = time.toDoubleOrNull())
|
||||||
}.toList()
|
}.toList()
|
||||||
if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty")
|
if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty")
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue