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