mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
resize images for lower mem footprint
This commit is contained in:
parent
2f2bbd7d88
commit
bb6a17e23c
3 changed files with 189 additions and 49 deletions
|
@ -37,6 +37,7 @@ import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode
|
|||
import com.lagradost.cloudstream3.CommonActivity.isInPIPMode
|
||||
import com.lagradost.cloudstream3.CommonActivity.keyEventListener
|
||||
import com.lagradost.cloudstream3.CommonActivity.playerEventListener
|
||||
import com.lagradost.cloudstream3.CommonActivity.screenWidth
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
|
@ -137,8 +138,10 @@ abstract class AbstractPlayerFragment(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateIsPlaying(wasPlaying : CSPlayerLoading,
|
||||
isPlaying : CSPlayerLoading) {
|
||||
private fun updateIsPlaying(
|
||||
wasPlaying: CSPlayerLoading,
|
||||
isPlaying: CSPlayerLoading
|
||||
) {
|
||||
val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying
|
||||
val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying
|
||||
|
||||
|
@ -186,7 +189,11 @@ abstract class AbstractPlayerFragment(
|
|||
canEnterPipMode = isPlayingRightNow && hasPipModeSupport
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
activity?.let { act ->
|
||||
PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow, player.getAspectRatio())
|
||||
PlayerPipHelper.updatePIPModeActions(
|
||||
act,
|
||||
isPlayingRightNow,
|
||||
player.getAspectRatio()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -381,30 +388,39 @@ abstract class AbstractPlayerFragment(
|
|||
is ResizedEvent -> {
|
||||
playerDimensionsLoaded(event.width, event.height)
|
||||
}
|
||||
|
||||
is PlayerAttachedEvent -> {
|
||||
playerUpdated(event.player)
|
||||
}
|
||||
|
||||
is SubtitlesUpdatedEvent -> {
|
||||
subtitlesChanged()
|
||||
}
|
||||
|
||||
is TimestampSkippedEvent -> {
|
||||
onTimestampSkipped(event.timestamp)
|
||||
}
|
||||
|
||||
is TimestampInvokedEvent -> {
|
||||
onTimestamp(event.timestamp)
|
||||
}
|
||||
|
||||
is TracksChangedEvent -> {
|
||||
onTracksInfoChanged()
|
||||
}
|
||||
|
||||
is EmbeddedSubtitlesFetchedEvent -> {
|
||||
embeddedSubtitlesFetched(event.tracks)
|
||||
}
|
||||
|
||||
is ErrorEvent -> {
|
||||
playerError(event.error)
|
||||
}
|
||||
|
||||
is RequestAudioFocusEvent -> {
|
||||
requestAudioFocus()
|
||||
}
|
||||
|
||||
is EpisodeSeekEvent -> {
|
||||
when (event.offset) {
|
||||
-1 -> prevEpisode()
|
||||
|
@ -412,12 +428,15 @@ abstract class AbstractPlayerFragment(
|
|||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
is StatusEvent -> {
|
||||
updateIsPlaying(wasPlaying = event.wasPlaying, isPlaying = event.isPlaying)
|
||||
}
|
||||
|
||||
is PositionEvent -> {
|
||||
playerPositionChanged(position = event.toMs, duration = event.durationMs)
|
||||
}
|
||||
|
||||
is VideoEndedEvent -> {
|
||||
context?.let { ctx ->
|
||||
// Only play next episode if autoplay is on (default)
|
||||
|
@ -434,6 +453,7 @@ abstract class AbstractPlayerFragment(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
is PauseEvent -> Unit
|
||||
is PlayEvent -> Unit
|
||||
}
|
||||
|
@ -495,17 +515,32 @@ abstract class AbstractPlayerFragment(
|
|||
subView = playerView?.findViewById(R.id.exo_subtitles)
|
||||
subStyle = SubtitlesFragment.getCurrentSavedStyle()
|
||||
player.initSubtitles(subView, subtitleHolder, subStyle)
|
||||
(player.imageGenerator as? PreviewGenerator)?.params = ImageParams.new16by9(screenWidth)
|
||||
|
||||
/*previewImageView?.doOnLayout {
|
||||
(player.imageGenerator as? PreviewGenerator)?.params = ImageParams(
|
||||
it.measuredWidth,
|
||||
it.measuredHeight
|
||||
)
|
||||
}*/
|
||||
/** this might seam a bit fucky and that is because it is, the seek event is captured twice, once by the player
|
||||
* and once by the UI even if it should only be registered once by the UI */
|
||||
playerView?.findViewById<DefaultTimeBar>(R.id.exo_progress)?.addListener(object : TimeBar.OnScrubListener {
|
||||
playerView?.findViewById<DefaultTimeBar>(R.id.exo_progress)
|
||||
?.addListener(object : TimeBar.OnScrubListener {
|
||||
override fun onScrubStart(timeBar: TimeBar, position: Long) = Unit
|
||||
override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit
|
||||
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
|
||||
if (canceled) return
|
||||
val playerDuration = player.getDuration() ?: return
|
||||
val playerPosition = player.getPosition() ?: return
|
||||
mainCallback(PositionEvent(source = PlayerEventSource.UI, durationMs = playerDuration, fromMs = playerPosition, toMs = position))
|
||||
mainCallback(
|
||||
PositionEvent(
|
||||
source = PlayerEventSource.UI,
|
||||
durationMs = playerDuration,
|
||||
fromMs = playerPosition,
|
||||
toMs = position
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ class CS3IPlayer : IPlayer {
|
|||
var simpleCacheSize = 0L
|
||||
var videoBufferMs = 0L
|
||||
|
||||
private val imageGenerator = PreviewGenerator()
|
||||
val imageGenerator = IPreviewGenerator.new()
|
||||
|
||||
private val seekActionTime = 30000L
|
||||
|
||||
|
@ -224,24 +224,30 @@ class CS3IPlayer : IPlayer {
|
|||
|
||||
// release the current exoplayer and cache
|
||||
releasePlayer()
|
||||
|
||||
if (link != null) {
|
||||
// only video support atm
|
||||
(imageGenerator as? PreviewGenerator)?.let { gen ->
|
||||
if (preview) {
|
||||
imageGenerator.load(link, sameEpisode)
|
||||
gen.load(link, sameEpisode)
|
||||
} else {
|
||||
imageGenerator.clear(sameEpisode)
|
||||
gen.clear(sameEpisode)
|
||||
}
|
||||
}
|
||||
loadOnlinePlayer(context, link)
|
||||
} else if (data != null) {
|
||||
(imageGenerator as? PreviewGenerator)?.let { gen ->
|
||||
if (preview) {
|
||||
imageGenerator.load(context, data, sameEpisode)
|
||||
gen.load(context, data, sameEpisode)
|
||||
} else {
|
||||
imageGenerator.clear(sameEpisode)
|
||||
gen.clear(sameEpisode)
|
||||
}
|
||||
}
|
||||
loadOfflinePlayer(context, data)
|
||||
} else {
|
||||
throw IllegalArgumentException("Requires link or uri")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun setActiveSubtitles(subtitles: Set<SubtitleData>) {
|
||||
|
@ -537,7 +543,10 @@ class CS3IPlayer : IPlayer {
|
|||
**/
|
||||
var preferredAudioTrackLanguage: String? = null
|
||||
get() {
|
||||
return field ?: getKey("$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", field)?.also {
|
||||
return field ?: getKey(
|
||||
"$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY",
|
||||
field
|
||||
)?.also {
|
||||
field = it
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,9 +4,12 @@ import android.content.Context
|
|||
import android.graphics.Bitmap
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.graphics.scale
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||
import com.lagradost.cloudstream3.utils.ExtractorLinkType
|
||||
|
@ -25,21 +28,98 @@ import kotlin.math.log2
|
|||
const val MAX_LOD = 6
|
||||
const val MIN_LOD = 3
|
||||
|
||||
data class ImageParams(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
) {
|
||||
companion object {
|
||||
val DEFAULT = ImageParams(200, 320)
|
||||
fun new16by9(width: Int): ImageParams {
|
||||
if (width < 100) {
|
||||
return DEFAULT
|
||||
}
|
||||
return ImageParams(
|
||||
width / 4,
|
||||
(width * 9) / (4 * 16)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
assert(width > 0 && height > 0)
|
||||
}
|
||||
}
|
||||
|
||||
interface IPreviewGenerator {
|
||||
fun hasPreview(): Boolean
|
||||
fun getPreviewImage(fraction: Float): Bitmap?
|
||||
fun release()
|
||||
|
||||
var params: ImageParams
|
||||
|
||||
var durationMs: Long
|
||||
var loadedImages: Int
|
||||
|
||||
companion object {
|
||||
fun new(): IPreviewGenerator {
|
||||
/** because TV has low ram + not show we disable this for now */
|
||||
return if (SettingsFragment.isTrueTvSettings()) {
|
||||
empty()
|
||||
} else {
|
||||
PreviewGenerator()
|
||||
}
|
||||
}
|
||||
|
||||
fun empty(): IPreviewGenerator {
|
||||
return NoPreviewGenerator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun rescale(image: Bitmap, params: ImageParams): Bitmap {
|
||||
if (image.width <= params.width && image.height <= params.height) return image
|
||||
val new = image.scale(params.width, params.height)
|
||||
// throw away the old image
|
||||
if (new != image) {
|
||||
image.recycle()
|
||||
}
|
||||
return new
|
||||
}
|
||||
|
||||
/** rescale to not take up as much memory */
|
||||
private fun MediaMetadataRetriever.image(timeUs: Long, params: ImageParams): Bitmap? {
|
||||
/*if (timeUs <= 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
try {
|
||||
val primary = this.primaryImage
|
||||
if (primary != null) {
|
||||
return rescale(primary, params)
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
}
|
||||
}*/
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
this.getScaledFrameAtTime(
|
||||
timeUs,
|
||||
MediaMetadataRetriever.OPTION_CLOSEST_SYNC,
|
||||
params.width,
|
||||
params.height
|
||||
)
|
||||
} else {
|
||||
return rescale(this.getFrameAtTime(timeUs) ?: return null, params)
|
||||
}
|
||||
}
|
||||
|
||||
/** PreviewGenerator that hides the implementation details of the sub generators that is used, used for source switch cache */
|
||||
class PreviewGenerator : IPreviewGenerator {
|
||||
|
||||
/** the most up to date generator, will always mirror the actual source in the player */
|
||||
private var currentGenerator: IPreviewGenerator = NoPreviewGenerator()
|
||||
|
||||
/** the longest generated preview of the same episode */
|
||||
private var lastGenerator: IPreviewGenerator = NoPreviewGenerator()
|
||||
|
||||
/** always NoPreviewGenerator, used as a cache for nothing */
|
||||
private val dummy: IPreviewGenerator = NoPreviewGenerator()
|
||||
|
||||
|
@ -76,6 +156,14 @@ class PreviewGenerator : IPreviewGenerator {
|
|||
currentGenerator = NoPreviewGenerator()
|
||||
}
|
||||
|
||||
override var params: ImageParams = ImageParams.DEFAULT
|
||||
set(value) {
|
||||
field = value
|
||||
lastGenerator.params = value
|
||||
backupGenerator.params = value
|
||||
currentGenerator.params = value
|
||||
}
|
||||
|
||||
override var durationMs: Long
|
||||
get() = currentGenerator.durationMs
|
||||
set(_) {}
|
||||
|
@ -110,13 +198,13 @@ class PreviewGenerator : IPreviewGenerator {
|
|||
|
||||
when (link.type) {
|
||||
ExtractorLinkType.M3U8 -> {
|
||||
currentGenerator = M3u8PreviewGenerator().apply {
|
||||
currentGenerator = M3u8PreviewGenerator(params).apply {
|
||||
load(url = link.url, headers = link.getAllHeaders())
|
||||
}
|
||||
}
|
||||
|
||||
ExtractorLinkType.VIDEO -> {
|
||||
currentGenerator = Mp4PreviewGenerator().apply {
|
||||
currentGenerator = Mp4PreviewGenerator(params).apply {
|
||||
load(url = link.url, headers = link.getAllHeaders())
|
||||
}
|
||||
}
|
||||
|
@ -129,21 +217,25 @@ class PreviewGenerator : IPreviewGenerator {
|
|||
|
||||
fun load(context: Context, link: ExtractorUri, keepCache: Boolean) {
|
||||
clear(keepCache)
|
||||
currentGenerator = Mp4PreviewGenerator().apply {
|
||||
currentGenerator = Mp4PreviewGenerator(params).apply {
|
||||
load(keepCache = keepCache, context = context, uri = link.uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
private class NoPreviewGenerator : IPreviewGenerator {
|
||||
override fun hasPreview(): Boolean = false
|
||||
override fun getPreviewImage(fraction: Float): Bitmap? = null
|
||||
override fun release() = Unit
|
||||
override var params: ImageParams
|
||||
get() = ImageParams(0, 0)
|
||||
set(value) {}
|
||||
override var durationMs: Long = 0L
|
||||
override var loadedImages: Int = 0
|
||||
}
|
||||
|
||||
private class M3u8PreviewGenerator : IPreviewGenerator {
|
||||
private class M3u8PreviewGenerator(override var params: ImageParams) : IPreviewGenerator {
|
||||
// generated images 1:1 to idx of hsl
|
||||
private var images: Array<Bitmap?> = arrayOf()
|
||||
|
||||
|
@ -194,6 +286,9 @@ private class M3u8PreviewGenerator : IPreviewGenerator {
|
|||
private fun clear() {
|
||||
synchronized(images) {
|
||||
currentJob?.cancel()
|
||||
// for (i in images.indices) {
|
||||
// images[i]?.recycle()
|
||||
// }
|
||||
images = arrayOf()
|
||||
prefixSum = arrayOf()
|
||||
loadedImages = 0
|
||||
|
@ -280,7 +375,7 @@ private class M3u8PreviewGenerator : IPreviewGenerator {
|
|||
if (!isActive) {
|
||||
return@withContext
|
||||
}
|
||||
val img = retriever.getFrameAtTime(0)
|
||||
val img = retriever.image(0, params)
|
||||
if (!isActive) {
|
||||
return@withContext
|
||||
}
|
||||
|
@ -308,7 +403,7 @@ private class M3u8PreviewGenerator : IPreviewGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
private class Mp4PreviewGenerator : IPreviewGenerator {
|
||||
private class Mp4PreviewGenerator(override var params: ImageParams) : IPreviewGenerator {
|
||||
// lod = level of detail where the number indicates how many ones there is
|
||||
// 2^(lod-1) = images
|
||||
private var loadedLod = 0
|
||||
|
@ -369,6 +464,10 @@ private class Mp4PreviewGenerator : IPreviewGenerator {
|
|||
synchronized(images) {
|
||||
loadedLod = 0
|
||||
loadedImages = 0
|
||||
// for (i in images.indices) {
|
||||
// images[i]?.recycle()
|
||||
// images[i] = null
|
||||
//}
|
||||
images.fill(null)
|
||||
}
|
||||
}
|
||||
|
@ -425,10 +524,7 @@ private class Mp4PreviewGenerator : IPreviewGenerator {
|
|||
val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat()))
|
||||
Log.i(TAG, "Generating preview for ${fraction * 100}%")
|
||||
val frame = durationUs * fraction
|
||||
val img = retriever.getFrameAtTime(
|
||||
frame.toLong(),
|
||||
MediaMetadataRetriever.OPTION_CLOSEST_SYNC
|
||||
)
|
||||
val img = retriever.image(frame.toLong(), params);
|
||||
if (!scope.isActive) return
|
||||
if (img == null || img.width <= 1 || img.height <= 1) continue
|
||||
synchronized(images) {
|
||||
|
|
Loading…
Reference in a new issue