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()
|
||||
if (link != null) {
|
||||
// only video support atm
|
||||
if (link.type == ExtractorLinkType.VIDEO && preview) {
|
||||
val headers = if (link.referer.isBlank()) {
|
||||
link.headers
|
||||
} else {
|
||||
mapOf("referer" to link.referer) + link.headers
|
||||
}
|
||||
imageGenerator.load(sameEpisode, link.url, headers)
|
||||
if (preview) {
|
||||
imageGenerator.load(link, sameEpisode)
|
||||
} else {
|
||||
imageGenerator.clear(sameEpisode)
|
||||
}
|
||||
loadOnlinePlayer(context, link)
|
||||
} else if (data != null) {
|
||||
if (preview) {
|
||||
imageGenerator.load(sameEpisode, context, data.uri)
|
||||
imageGenerator.load(context, data, sameEpisode)
|
||||
} else {
|
||||
imageGenerator.clear(sameEpisode)
|
||||
}
|
||||
|
|
|
@ -6,10 +6,18 @@ import android.media.MediaMetadataRetriever
|
|||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
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.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.log2
|
||||
|
@ -17,7 +25,248 @@ import kotlin.math.log2
|
|||
const val MAX_LOD = 6
|
||||
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
|
||||
// 2^(lod-1) = images
|
||||
private var loadedLod = 0
|
||||
|
@ -26,15 +275,15 @@ class PreviewGenerator {
|
|||
null
|
||||
}
|
||||
|
||||
fun hasPreview(): Boolean {
|
||||
override fun hasPreview(): Boolean {
|
||||
synchronized(images) {
|
||||
return loadedLod >= MIN_LOD
|
||||
}
|
||||
}
|
||||
|
||||
val TAG = "PreviewImg"
|
||||
val TAG = "PreviewImgMp4"
|
||||
|
||||
fun getPreviewImage(fraction: Float): Bitmap? {
|
||||
override fun getPreviewImage(fraction: Float): Bitmap? {
|
||||
synchronized(images) {
|
||||
if (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
|
||||
private val retriever: MediaMetadataRetriever = MediaMetadataRetriever()
|
||||
|
||||
fun clear(keepCache: Boolean = false) {
|
||||
override fun clear(keepCache: Boolean) {
|
||||
if (keepCache) return
|
||||
synchronized(images) {
|
||||
loadedLod = 0
|
||||
|
@ -100,7 +349,7 @@ class PreviewGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
fun release() {
|
||||
override fun release() {
|
||||
currentJob?.cancel()
|
||||
clear(false)
|
||||
}
|
||||
|
@ -135,7 +384,7 @@ class PreviewGenerator {
|
|||
if (!scope.isActive) return
|
||||
synchronized(images) {
|
||||
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 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(
|
||||
source: String,
|
||||
name: String,
|
||||
|
|
|
@ -71,7 +71,7 @@ object M3u8Helper2 {
|
|||
private val QUALITY_REGEX =
|
||||
Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""")
|
||||
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
|
||||
|
||||
private fun absoluteExtensionDetermination(url: String): String? {
|
||||
|
@ -122,6 +122,15 @@ object M3u8Helper2 {
|
|||
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 {
|
||||
val split = uri.split("/").toMutableList()
|
||||
split.removeLast()
|
||||
|
@ -173,14 +182,20 @@ object M3u8Helper2 {
|
|||
return list
|
||||
}
|
||||
|
||||
data class TsLink(
|
||||
val url : String,
|
||||
val time : Double?,
|
||||
)
|
||||
|
||||
data class LazyHlsDownloadData(
|
||||
private val encryptionData: ByteArray,
|
||||
private val encryptionIv: ByteArray,
|
||||
private val isEncrypted: Boolean,
|
||||
private val allTsLinks: List<String>,
|
||||
private val relativeUrl: String,
|
||||
private val headers: Map<String, String>,
|
||||
val isEncrypted: Boolean,
|
||||
val allTsLinks: List<TsLink>,
|
||||
val relativeUrl: String,
|
||||
val headers: Map<String, String>,
|
||||
) {
|
||||
|
||||
val size get() = allTsLinks.size
|
||||
|
||||
suspend fun resolveLinkWhileSafe(
|
||||
|
@ -228,9 +243,9 @@ object M3u8Helper2 {
|
|||
@Throws
|
||||
suspend fun resolveLink(index: Int): ByteArray {
|
||||
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()
|
||||
if (tsData.isEmpty()) throw ErrorLoadingException("no data")
|
||||
|
||||
|
@ -244,15 +259,16 @@ object M3u8Helper2 {
|
|||
|
||||
@Throws
|
||||
suspend fun hslLazy(
|
||||
qualities: List<M3u8Helper.M3u8Stream>
|
||||
qualities: List<M3u8Helper.M3u8Stream>, selectBest : Boolean = true
|
||||
): LazyHlsDownloadData {
|
||||
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 streams = qualities.map { m3u8Generation(it, false) }.flatten()
|
||||
// this selects the best quality of the qualities offered,
|
||||
// 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")
|
||||
|
||||
val m3u8Response =
|
||||
|
@ -285,12 +301,14 @@ object M3u8Helper2 {
|
|||
}
|
||||
val relativeUrl = getParentLink(secondSelection.streamUrl)
|
||||
val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts ->
|
||||
val value = ts.groupValues[1]
|
||||
if (isNotCompleteUrl(value)) {
|
||||
val time = ts.groupValues[1]
|
||||
val value = ts.groupValues[3]
|
||||
val url = if (isNotCompleteUrl(value)) {
|
||||
"$relativeUrl/${value}"
|
||||
} else {
|
||||
value
|
||||
}
|
||||
TsLink(url = url, time = time.toDoubleOrNull())
|
||||
}.toList()
|
||||
if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty")
|
||||
|
||||
|
|
Loading…
Reference in a new issue