resize images for lower mem footprint

This commit is contained in:
LagradOst 2023-10-11 18:31:46 +02:00
parent 2f2bbd7d88
commit bb6a17e23c
3 changed files with 189 additions and 49 deletions

View file

@ -37,6 +37,7 @@ import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode
import com.lagradost.cloudstream3.CommonActivity.isInPIPMode import com.lagradost.cloudstream3.CommonActivity.isInPIPMode
import com.lagradost.cloudstream3.CommonActivity.keyEventListener import com.lagradost.cloudstream3.CommonActivity.keyEventListener
import com.lagradost.cloudstream3.CommonActivity.playerEventListener import com.lagradost.cloudstream3.CommonActivity.playerEventListener
import com.lagradost.cloudstream3.CommonActivity.screenWidth
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
@ -79,12 +80,12 @@ abstract class AbstractPlayerFragment(
var isBuffering = true var isBuffering = true
protected open var hasPipModeSupport = true protected open var hasPipModeSupport = true
var playerPausePlayHolderHolder : FrameLayout? = null var playerPausePlayHolderHolder: FrameLayout? = null
var playerPausePlay : ImageView? = null var playerPausePlay: ImageView? = null
var playerBuffering : ProgressBar? = null var playerBuffering: ProgressBar? = null
var playerView : PlayerView? = null var playerView: PlayerView? = null
var piphide : FrameLayout? = null var piphide: FrameLayout? = null
var subtitleHolder : FrameLayout? = null var subtitleHolder: FrameLayout? = null
@LayoutRes @LayoutRes
protected open var layout: Int = R.layout.fragment_player protected open var layout: Int = R.layout.fragment_player
@ -97,11 +98,11 @@ abstract class AbstractPlayerFragment(
throw NotImplementedError() throw NotImplementedError()
} }
open fun playerPositionChanged(position: Long, duration : Long) { open fun playerPositionChanged(position: Long, duration: Long) {
throw NotImplementedError() throw NotImplementedError()
} }
open fun playerDimensionsLoaded(width: Int, height : Int) { open fun playerDimensionsLoaded(width: Int, height: Int) {
throw NotImplementedError() throw NotImplementedError()
} }
@ -137,8 +138,10 @@ abstract class AbstractPlayerFragment(
} }
} }
private fun updateIsPlaying(wasPlaying : CSPlayerLoading, private fun updateIsPlaying(
isPlaying : CSPlayerLoading) { wasPlaying: CSPlayerLoading,
isPlaying: CSPlayerLoading
) {
val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying val isPlayingRightNow = CSPlayerLoading.IsPlaying == isPlaying
val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying val isPausedRightNow = CSPlayerLoading.IsPaused == isPlaying
@ -186,7 +189,11 @@ abstract class AbstractPlayerFragment(
canEnterPipMode = isPlayingRightNow && hasPipModeSupport canEnterPipMode = isPlayingRightNow && hasPipModeSupport
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity?.let { act -> activity?.let { act ->
PlayerPipHelper.updatePIPModeActions(act, isPlayingRightNow, player.getAspectRatio()) PlayerPipHelper.updatePIPModeActions(
act,
isPlayingRightNow,
player.getAspectRatio()
)
} }
} }
} }
@ -375,49 +382,61 @@ abstract class AbstractPlayerFragment(
/** This receives the events from the player, if you want to append functionality you do it here, /** This receives the events from the player, if you want to append functionality you do it here,
* do note that this only receives events for UI changes, * do note that this only receives events for UI changes,
* and returning early WONT stop it from changing in eg the player time or pause status */ * and returning early WONT stop it from changing in eg the player time or pause status */
open fun mainCallback(event : PlayerEvent) { open fun mainCallback(event: PlayerEvent) {
Log.i(TAG, "Handle event: $event") Log.i(TAG, "Handle event: $event")
when(event) { when (event) {
is ResizedEvent -> { is ResizedEvent -> {
playerDimensionsLoaded(event.width, event.height) playerDimensionsLoaded(event.width, event.height)
} }
is PlayerAttachedEvent -> { is PlayerAttachedEvent -> {
playerUpdated(event.player) playerUpdated(event.player)
} }
is SubtitlesUpdatedEvent -> { is SubtitlesUpdatedEvent -> {
subtitlesChanged() subtitlesChanged()
} }
is TimestampSkippedEvent -> { is TimestampSkippedEvent -> {
onTimestampSkipped(event.timestamp) onTimestampSkipped(event.timestamp)
} }
is TimestampInvokedEvent -> { is TimestampInvokedEvent -> {
onTimestamp(event.timestamp) onTimestamp(event.timestamp)
} }
is TracksChangedEvent -> { is TracksChangedEvent -> {
onTracksInfoChanged() onTracksInfoChanged()
} }
is EmbeddedSubtitlesFetchedEvent -> { is EmbeddedSubtitlesFetchedEvent -> {
embeddedSubtitlesFetched(event.tracks) embeddedSubtitlesFetched(event.tracks)
} }
is ErrorEvent -> { is ErrorEvent -> {
playerError(event.error) playerError(event.error)
} }
is RequestAudioFocusEvent -> { is RequestAudioFocusEvent -> {
requestAudioFocus() requestAudioFocus()
} }
is EpisodeSeekEvent -> { is EpisodeSeekEvent -> {
when(event.offset) { when (event.offset) {
-1 -> prevEpisode() -1 -> prevEpisode()
1 -> nextEpisode() 1 -> nextEpisode()
else -> {} else -> {}
} }
} }
is StatusEvent -> { is StatusEvent -> {
updateIsPlaying(wasPlaying = event.wasPlaying, isPlaying = event.isPlaying) updateIsPlaying(wasPlaying = event.wasPlaying, isPlaying = event.isPlaying)
} }
is PositionEvent -> { is PositionEvent -> {
playerPositionChanged(position = event.toMs, duration = event.durationMs) playerPositionChanged(position = event.toMs, duration = event.durationMs)
} }
is VideoEndedEvent -> { is VideoEndedEvent -> {
context?.let { ctx -> context?.let { ctx ->
// Only play next episode if autoplay is on (default) // Only play next episode if autoplay is on (default)
@ -434,6 +453,7 @@ abstract class AbstractPlayerFragment(
} }
} }
} }
is PauseEvent -> Unit is PauseEvent -> Unit
is PlayEvent -> Unit is PlayEvent -> Unit
} }
@ -457,10 +477,10 @@ abstract class AbstractPlayerFragment(
if (player is CS3IPlayer) { if (player is CS3IPlayer) {
// preview bar // preview bar
val progressBar : PreviewTimeBar? = playerView?.findViewById(R.id.exo_progress) val progressBar: PreviewTimeBar? = playerView?.findViewById(R.id.exo_progress)
val previewImageView : ImageView? = playerView?.findViewById(R.id.previewImageView) val previewImageView: ImageView? = playerView?.findViewById(R.id.previewImageView)
val previewFrameLayout : FrameLayout? = playerView?.findViewById(R.id.previewFrameLayout) val previewFrameLayout: FrameLayout? = playerView?.findViewById(R.id.previewFrameLayout)
if(progressBar != null && previewImageView != null && previewFrameLayout != null) { if (progressBar != null && previewImageView != null && previewFrameLayout != null) {
var resume = false var resume = false
progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener { progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener {
override fun onScrubStart(previewBar: PreviewBar?) { override fun onScrubStart(previewBar: PreviewBar?) {
@ -495,17 +515,32 @@ abstract class AbstractPlayerFragment(
subView = playerView?.findViewById(R.id.exo_subtitles) subView = playerView?.findViewById(R.id.exo_subtitles)
subStyle = SubtitlesFragment.getCurrentSavedStyle() subStyle = SubtitlesFragment.getCurrentSavedStyle()
player.initSubtitles(subView, subtitleHolder, subStyle) 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 /** 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 */ * 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 onScrubStart(timeBar: TimeBar, position: Long) = Unit
override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit override fun onScrubMove(timeBar: TimeBar, position: Long) = Unit
override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) { override fun onScrubStop(timeBar: TimeBar, position: Long, canceled: Boolean) {
if (canceled) return if (canceled) return
val playerDuration = player.getDuration() ?: return val playerDuration = player.getDuration() ?: return
val playerPosition = player.getPosition() ?: 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
)
)
} }
}) })

View file

@ -100,7 +100,7 @@ class CS3IPlayer : IPlayer {
var simpleCacheSize = 0L var simpleCacheSize = 0L
var videoBufferMs = 0L var videoBufferMs = 0L
private val imageGenerator = PreviewGenerator() val imageGenerator = IPreviewGenerator.new()
private val seekActionTime = 30000L private val seekActionTime = 30000L
@ -205,7 +205,7 @@ class CS3IPlayer : IPlayer {
subtitles: Set<SubtitleData>, subtitles: Set<SubtitleData>,
subtitle: SubtitleData?, subtitle: SubtitleData?,
autoPlay: Boolean?, autoPlay: Boolean?,
preview : Boolean, preview: Boolean,
) { ) {
Log.i(TAG, "loadPlayer") Log.i(TAG, "loadPlayer")
if (sameEpisode) { if (sameEpisode) {
@ -224,24 +224,30 @@ class CS3IPlayer : IPlayer {
// release the current exoplayer and cache // release the current exoplayer and cache
releasePlayer() releasePlayer()
if (link != null) { if (link != null) {
// only video support atm // only video support atm
(imageGenerator as? PreviewGenerator)?.let { gen ->
if (preview) { if (preview) {
imageGenerator.load(link, sameEpisode) gen.load(link, sameEpisode)
} else { } else {
imageGenerator.clear(sameEpisode) gen.clear(sameEpisode)
}
} }
loadOnlinePlayer(context, link) loadOnlinePlayer(context, link)
} else if (data != null) { } else if (data != null) {
(imageGenerator as? PreviewGenerator)?.let { gen ->
if (preview) { if (preview) {
imageGenerator.load(context, data, sameEpisode) gen.load(context, data, sameEpisode)
} else { } else {
imageGenerator.clear(sameEpisode) gen.clear(sameEpisode)
}
} }
loadOfflinePlayer(context, data) loadOfflinePlayer(context, data)
} else { } else {
throw IllegalArgumentException("Requires link or uri") throw IllegalArgumentException("Requires link or uri")
} }
} }
override fun setActiveSubtitles(subtitles: Set<SubtitleData>) { override fun setActiveSubtitles(subtitles: Set<SubtitleData>) {
@ -537,7 +543,10 @@ class CS3IPlayer : IPlayer {
**/ **/
var preferredAudioTrackLanguage: String? = null var preferredAudioTrackLanguage: String? = null
get() { get() {
return field ?: getKey("$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", field)?.also { return field ?: getKey(
"$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY",
field
)?.also {
field = it field = it
} }
} }

View file

@ -4,9 +4,12 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.net.Uri import android.net.Uri
import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.core.graphics.scale
import com.lagradost.cloudstream3.mvvm.logError 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.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.ExtractorLinkType
@ -25,21 +28,98 @@ import kotlin.math.log2
const val MAX_LOD = 6 const val MAX_LOD = 6
const val MIN_LOD = 3 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 { interface IPreviewGenerator {
fun hasPreview(): Boolean fun hasPreview(): Boolean
fun getPreviewImage(fraction: Float): Bitmap? fun getPreviewImage(fraction: Float): Bitmap?
fun release() fun release()
var params: ImageParams
var durationMs: Long var durationMs: Long
var loadedImages: Int 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 */ /** PreviewGenerator that hides the implementation details of the sub generators that is used, used for source switch cache */
class PreviewGenerator : IPreviewGenerator { class PreviewGenerator : IPreviewGenerator {
/** the most up to date generator, will always mirror the actual source in the player */ /** the most up to date generator, will always mirror the actual source in the player */
private var currentGenerator: IPreviewGenerator = NoPreviewGenerator() private var currentGenerator: IPreviewGenerator = NoPreviewGenerator()
/** the longest generated preview of the same episode */ /** the longest generated preview of the same episode */
private var lastGenerator: IPreviewGenerator = NoPreviewGenerator() private var lastGenerator: IPreviewGenerator = NoPreviewGenerator()
/** always NoPreviewGenerator, used as a cache for nothing */ /** always NoPreviewGenerator, used as a cache for nothing */
private val dummy: IPreviewGenerator = NoPreviewGenerator() private val dummy: IPreviewGenerator = NoPreviewGenerator()
@ -76,6 +156,14 @@ class PreviewGenerator : IPreviewGenerator {
currentGenerator = NoPreviewGenerator() 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 override var durationMs: Long
get() = currentGenerator.durationMs get() = currentGenerator.durationMs
set(_) {} set(_) {}
@ -110,13 +198,13 @@ class PreviewGenerator : IPreviewGenerator {
when (link.type) { when (link.type) {
ExtractorLinkType.M3U8 -> { ExtractorLinkType.M3U8 -> {
currentGenerator = M3u8PreviewGenerator().apply { currentGenerator = M3u8PreviewGenerator(params).apply {
load(url = link.url, headers = link.getAllHeaders()) load(url = link.url, headers = link.getAllHeaders())
} }
} }
ExtractorLinkType.VIDEO -> { ExtractorLinkType.VIDEO -> {
currentGenerator = Mp4PreviewGenerator().apply { currentGenerator = Mp4PreviewGenerator(params).apply {
load(url = link.url, headers = link.getAllHeaders()) load(url = link.url, headers = link.getAllHeaders())
} }
} }
@ -129,21 +217,25 @@ class PreviewGenerator : IPreviewGenerator {
fun load(context: Context, link: ExtractorUri, keepCache: Boolean) { fun load(context: Context, link: ExtractorUri, keepCache: Boolean) {
clear(keepCache) clear(keepCache)
currentGenerator = Mp4PreviewGenerator().apply { currentGenerator = Mp4PreviewGenerator(params).apply {
load(keepCache = keepCache, context = context, uri = link.uri) load(keepCache = keepCache, context = context, uri = link.uri)
} }
} }
} }
@Suppress("UNUSED_PARAMETER")
private class NoPreviewGenerator : IPreviewGenerator { private class NoPreviewGenerator : IPreviewGenerator {
override fun hasPreview(): Boolean = false override fun hasPreview(): Boolean = false
override fun getPreviewImage(fraction: Float): Bitmap? = null override fun getPreviewImage(fraction: Float): Bitmap? = null
override fun release() = Unit override fun release() = Unit
override var params: ImageParams
get() = ImageParams(0, 0)
set(value) {}
override var durationMs: Long = 0L override var durationMs: Long = 0L
override var loadedImages: Int = 0 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 // generated images 1:1 to idx of hsl
private var images: Array<Bitmap?> = arrayOf() private var images: Array<Bitmap?> = arrayOf()
@ -194,6 +286,9 @@ private class M3u8PreviewGenerator : IPreviewGenerator {
private fun clear() { private fun clear() {
synchronized(images) { synchronized(images) {
currentJob?.cancel() currentJob?.cancel()
// for (i in images.indices) {
// images[i]?.recycle()
// }
images = arrayOf() images = arrayOf()
prefixSum = arrayOf() prefixSum = arrayOf()
loadedImages = 0 loadedImages = 0
@ -280,7 +375,7 @@ private class M3u8PreviewGenerator : IPreviewGenerator {
if (!isActive) { if (!isActive) {
return@withContext return@withContext
} }
val img = retriever.getFrameAtTime(0) val img = retriever.image(0, params)
if (!isActive) { if (!isActive) {
return@withContext 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 // 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
@ -369,6 +464,10 @@ private class Mp4PreviewGenerator : IPreviewGenerator {
synchronized(images) { synchronized(images) {
loadedLod = 0 loadedLod = 0
loadedImages = 0 loadedImages = 0
// for (i in images.indices) {
// images[i]?.recycle()
// images[i] = null
//}
images.fill(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())) val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat()))
Log.i(TAG, "Generating preview for ${fraction * 100}%") Log.i(TAG, "Generating preview for ${fraction * 100}%")
val frame = durationUs * fraction val frame = durationUs * fraction
val img = retriever.getFrameAtTime( val img = retriever.image(frame.toLong(), params);
frame.toLong(),
MediaMetadataRetriever.OPTION_CLOSEST_SYNC
)
if (!scope.isActive) return if (!scope.isActive) return
if (img == null || img.width <= 1 || img.height <= 1) continue if (img == null || img.width <= 1 || img.height <= 1) continue
synchronized(images) { synchronized(images) {