preview seekbar m3u8

This commit is contained in:
LagradOst 2023-10-03 16:50:34 +02:00
parent 1d90858f64
commit 08060314ad
4 changed files with 298 additions and 27 deletions

View File

@ -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)
} }

View File

@ -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)
} }
} }

View File

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

View File

@ -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")