-
Notifications
You must be signed in to change notification settings - Fork 835
Fix livestreams #2627
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
CranberrySoup
wants to merge
1
commit into
recloudstream:master
Choose a base branch
from
CranberrySoup:fix_live
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Fix livestreams #2627
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LiveHelper.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| package com.lagradost.cloudstream3.ui.player.live | ||
|
|
||
| import androidx.annotation.OptIn | ||
| import androidx.media3.common.Player | ||
| import androidx.media3.common.Timeline | ||
| import androidx.media3.common.util.UnstableApi | ||
| import com.lagradost.cloudstream3.mvvm.debugWarning | ||
| import java.util.WeakHashMap | ||
|
|
||
| object LiveHelper { | ||
| private val liveManagers = WeakHashMap<Player, LiveManager>() | ||
| private val listeners = WeakHashMap<Player, Player.Listener>() | ||
|
|
||
| @OptIn(UnstableApi::class) | ||
|
|
||
| fun registerPlayer(player: Player?) { | ||
| if (player == null) { | ||
| debugWarning { "LiveHelper registerPlayer called with null player!" } | ||
| return | ||
| } | ||
|
|
||
| // Prevent duplicates | ||
| if (liveManagers.contains(player) || listeners.contains(player)) { | ||
| return | ||
| } | ||
|
|
||
| val liveManager = LiveManager(player) | ||
| val listener = object : Player.Listener { | ||
| override fun onTimelineChanged(timeline: Timeline, reason: Int) { | ||
| val window = Timeline.Window() | ||
| timeline.getWindow(player.currentMediaItemIndex, window) | ||
| if (window.isDynamic) { | ||
| liveManager.submitLivestreamChunk(LivestreamChunk(window.durationMs)) | ||
| } | ||
| super.onTimelineChanged(timeline, reason) | ||
| } | ||
|
|
||
| override fun onPositionDiscontinuity( | ||
| oldPosition: Player.PositionInfo, | ||
| newPosition: Player.PositionInfo, | ||
| reason: Int | ||
| ) { | ||
| super.onPositionDiscontinuity(oldPosition, newPosition, reason) | ||
| val timeAheadOfLive = liveManager.getTimeAheadOfLive(newPosition.positionMs) | ||
|
|
||
| // Seek back to the optimal live spot | ||
| if (timeAheadOfLive > 100) { | ||
| player.seekTo(newPosition.positionMs - timeAheadOfLive) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| synchronized(liveManagers) { | ||
| liveManagers[player] = liveManager | ||
| } | ||
| synchronized(listeners) { | ||
| player.addListener(listener) | ||
| listeners[player] = listener | ||
| } | ||
| } | ||
|
|
||
| fun unregisterPlayer(player: Player?) { | ||
| if (player == null) { | ||
| debugWarning { "LiveHelper unregisterPlayer called with null player!" } | ||
| return | ||
| } | ||
| // Prevent duplicates | ||
| if (!liveManagers.contains(player) && !listeners.contains(player)) { | ||
| return | ||
| } | ||
|
|
||
| synchronized(liveManagers) { | ||
| liveManagers.remove(player) | ||
| } | ||
| synchronized(listeners) { | ||
| listeners[player]?.let { | ||
| player.removeListener(it) | ||
| } | ||
| listeners.remove(player) | ||
| } | ||
| } | ||
|
|
||
| fun getLiveManager(player: Player?) = liveManagers[player] | ||
| } | ||
97 changes: 97 additions & 0 deletions
97
app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LiveManager.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package com.lagradost.cloudstream3.ui.player.live | ||
|
|
||
| import androidx.media3.common.C | ||
| import androidx.media3.common.Player | ||
| import java.lang.ref.WeakReference | ||
|
|
||
| // How much margin from the live point is still considered "live" | ||
| const val LIVE_MARGIN = 6_000L | ||
|
|
||
| // How many ms should we be behind the real live point? | ||
| // Too low, and we cannot pre-buffer | ||
| // Too high, and we are no longer live | ||
| const val PREFERRED_LIVE_OFFSET = 5_000L | ||
|
|
||
| // An extra offset from the optimal calculated timestamp | ||
| // This is to account for chunk updates not always being the same size | ||
| const val CHUNK_VARIANCE = 3000L | ||
|
|
||
| // A livestream chunk from the player, the time we get it and the duration can be used to calculate | ||
| // the expected live timestamp. | ||
| class LivestreamChunk( | ||
| durationMs: Long, val receiveTimeMs: Long = System.currentTimeMillis() | ||
| ) { | ||
| // We want to be PREFERRED_LIVE_OFFSET ms after the latest update, but we cannot be ahead of the middle point. | ||
| // If we are ahead of the middle point we will reach the end before the new chunk is expected to be released. | ||
| val targetPosition = maxOf(0,minOf( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be calculated using an average of several chunks. It would increase accuracy by a bit but increase complexity. |
||
| durationMs - PREFERRED_LIVE_OFFSET, | ||
| durationMs / 2 - CHUNK_VARIANCE | ||
| )) | ||
|
|
||
| fun isPositionLive(position: Long): Boolean { | ||
| val currentTime = System.currentTimeMillis() | ||
| val livePosition = targetPosition + (currentTime - receiveTimeMs) | ||
| val withinLive = position + LIVE_MARGIN > livePosition - PREFERRED_LIVE_OFFSET | ||
| // println("Position: $position, livePosition: ${livePosition}, Behind live: ${livePosition-position}, within live: $withinLive") | ||
| return withinLive | ||
| } | ||
|
|
||
| fun getTimeAheadOfLive(position: Long): Long { | ||
| val currentTime = System.currentTimeMillis() | ||
| val livePosition = targetPosition + (currentTime - receiveTimeMs) | ||
| // println("Ahead of live: ${position-livePosition}") | ||
| return position - livePosition | ||
| } | ||
| } | ||
|
|
||
| // There are two types of livestreams we need to manage | ||
| // 1. A livestream with no history, a continually sliding window. | ||
| // This livestream has no currentLiveOffset, which means we need to calculate | ||
| // the real live point based on when we receive the latest update and the size of that update. | ||
| // 2. A livestream with history. | ||
| // This livestream has a currentLiveOffset and therefore requires no calculation to get the live point. | ||
| // currentLiveOffset can however be inaccurate, and we need to be able to fall back to manual calculations. | ||
| class LiveManager { | ||
| private var _currentPlayer: WeakReference<Player>? = null | ||
| val currentPlayer: Player? get() = _currentPlayer?.get() | ||
|
|
||
| constructor(player: Player?) { | ||
| _currentPlayer = WeakReference(player) | ||
| } | ||
|
|
||
| private var lastLivestreamChunk: LivestreamChunk? = null | ||
|
|
||
| fun submitLivestreamChunk(chunk: LivestreamChunk) { | ||
| lastLivestreamChunk = chunk | ||
| } | ||
|
|
||
| /** Returns how much a position is ahead of the calculated live window. Returns 0 if not ahead of live window */ | ||
| fun getTimeAheadOfLive(position: Long): Long { | ||
| val player = currentPlayer ?: return 0 | ||
| if (!player.isCurrentMediaItemDynamic || player.duration == C.TIME_UNSET) return 0 | ||
|
|
||
| // If the currentLiveOffset is wrong we fall back to manual calculations | ||
| val ahead = if (player.currentLiveOffset != C.TIME_UNSET && player.currentLiveOffset < player.duration) { | ||
| val relativeOffset = player.currentLiveOffset - player.currentPosition + position | ||
| PREFERRED_LIVE_OFFSET - relativeOffset | ||
| } else { | ||
| lastLivestreamChunk?.getTimeAheadOfLive(position) ?: 0 | ||
| } | ||
|
|
||
| // Ensure min of 0 | ||
| return maxOf(0, ahead) | ||
| } | ||
|
|
||
| /** Check if the stream is currently at the expected live edge, with margins */ | ||
| fun isAtLiveEdge(): Boolean { | ||
| val player = currentPlayer ?: return false | ||
| if (!player.isCurrentMediaItemDynamic || player.duration == C.TIME_UNSET) return false | ||
|
|
||
| // If the currentLiveOffset is wrong we fall back to manual calculations | ||
| return if (player.currentLiveOffset != C.TIME_UNSET && player.currentLiveOffset < player.duration) { | ||
| player.currentLiveOffset < LIVE_MARGIN + PREFERRED_LIVE_OFFSET | ||
| } else { | ||
| lastLivestreamChunk?.isPositionLive(player.currentPosition) == true | ||
| } | ||
| } | ||
| } | ||
38 changes: 38 additions & 0 deletions
38
app/src/main/java/com/lagradost/cloudstream3/ui/player/live/LivePreviewTimeBar.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package com.lagradost.cloudstream3.ui.player.live | ||
|
|
||
| import android.content.Context | ||
| import android.util.AttributeSet | ||
| import androidx.annotation.OptIn | ||
| import androidx.media3.common.Player | ||
| import androidx.media3.common.util.UnstableApi | ||
| import androidx.media3.ui.PlayerControlView | ||
| import androidx.media3.ui.PlayerView | ||
| import androidx.media3.ui.R | ||
| import com.github.rubensousa.previewseekbar.media3.PreviewTimeBar | ||
| import java.lang.ref.WeakReference | ||
|
|
||
|
|
||
| @OptIn(UnstableApi::class) | ||
| class LivePreviewTimeBar(val ctx: Context, attrs: AttributeSet) : PreviewTimeBar(ctx, attrs) { | ||
|
|
||
| private var _currentPlayerView: WeakReference<PlayerView>? = null | ||
| val currentPlayer: Player? get() = _currentPlayerView?.get()?.player | ||
|
|
||
| fun registerPlayerView(player: PlayerView?) { | ||
| _currentPlayerView = WeakReference(player) | ||
| val controller = | ||
| _currentPlayerView?.get()?.findViewById<PlayerControlView>(R.id.exo_controller) | ||
|
|
||
| controller?.setProgressUpdateListener { position, bufferedPosition -> | ||
| currentPlayer?.let { player -> | ||
| if (isAtLiveEdge()) { | ||
| setPosition(player.duration) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fun isAtLiveEdge(): Boolean { | ||
| return LiveHelper.getLiveManager(currentPlayer)?.isAtLiveEdge() == true | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would make more sense as a Pair, to make them synced.