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.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
@ -79,12 +80,12 @@ abstract class AbstractPlayerFragment(
var isBuffering = true
protected open var hasPipModeSupport = true
var playerPausePlayHolderHolder : FrameLayout? = null
var playerPausePlay : ImageView? = null
var playerBuffering : ProgressBar? = null
var playerView : PlayerView? = null
var piphide : FrameLayout? = null
var subtitleHolder : FrameLayout? = null
var playerPausePlayHolderHolder: FrameLayout? = null
var playerPausePlay: ImageView? = null
var playerBuffering: ProgressBar? = null
var playerView: PlayerView? = null
var piphide: FrameLayout? = null
var subtitleHolder: FrameLayout? = null
@LayoutRes
protected open var layout: Int = R.layout.fragment_player
@ -97,11 +98,11 @@ abstract class AbstractPlayerFragment(
throw NotImplementedError()
}
open fun playerPositionChanged(position: Long, duration : Long) {
open fun playerPositionChanged(position: Long, duration: Long) {
throw NotImplementedError()
}
open fun playerDimensionsLoaded(width: Int, height : Int) {
open fun playerDimensionsLoaded(width: Int, height: Int) {
throw NotImplementedError()
}
@ -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()
)
}
}
}
@ -375,49 +382,61 @@ abstract class AbstractPlayerFragment(
/** 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,
* 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")
when(event) {
when (event) {
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) {
when (event.offset) {
-1 -> prevEpisode()
1 -> nextEpisode()
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
}
@ -457,10 +477,10 @@ abstract class AbstractPlayerFragment(
if (player is CS3IPlayer) {
// preview bar
val progressBar : PreviewTimeBar? = playerView?.findViewById(R.id.exo_progress)
val previewImageView : ImageView? = playerView?.findViewById(R.id.previewImageView)
val previewFrameLayout : FrameLayout? = playerView?.findViewById(R.id.previewFrameLayout)
if(progressBar != null && previewImageView != null && previewFrameLayout != null) {
val progressBar: PreviewTimeBar? = playerView?.findViewById(R.id.exo_progress)
val previewImageView: ImageView? = playerView?.findViewById(R.id.previewImageView)
val previewFrameLayout: FrameLayout? = playerView?.findViewById(R.id.previewFrameLayout)
if (progressBar != null && previewImageView != null && previewFrameLayout != null) {
var resume = false
progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener {
override fun onScrubStart(previewBar: PreviewBar?) {
@ -495,19 +515,34 @@ 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 {
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))
}
})
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
)
)
}
})
SubtitlesFragment.applyStyleEvent += ::onSubStyleChanged

View file

@ -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
@ -205,7 +205,7 @@ class CS3IPlayer : IPlayer {
subtitles: Set<SubtitleData>,
subtitle: SubtitleData?,
autoPlay: Boolean?,
preview : Boolean,
preview: Boolean,
) {
Log.i(TAG, "loadPlayer")
if (sameEpisode) {
@ -224,24 +224,30 @@ class CS3IPlayer : IPlayer {
// release the current exoplayer and cache
releasePlayer()
if (link != null) {
// only video support atm
if (preview) {
imageGenerator.load(link, sameEpisode)
} else {
imageGenerator.clear(sameEpisode)
(imageGenerator as? PreviewGenerator)?.let { gen ->
if (preview) {
gen.load(link, sameEpisode)
} else {
gen.clear(sameEpisode)
}
}
loadOnlinePlayer(context, link)
} else if (data != null) {
if (preview) {
imageGenerator.load(context, data, sameEpisode)
} else {
imageGenerator.clear(sameEpisode)
(imageGenerator as? PreviewGenerator)?.let { gen ->
if (preview) {
gen.load(context, data, sameEpisode)
} else {
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
}
}

View file

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