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

View File

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

View File

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

View File

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