mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-26 22:42:17 +00:00
parent
03f8d89655
commit
0c0c616bed
1 changed files with 173 additions and 41 deletions
|
|
@ -30,6 +30,7 @@ import org.jetbrains.compose.resources.getString
|
|||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.PlaybackException
|
||||
|
|
@ -119,6 +120,43 @@ actual fun PlatformPlayerSurface(
|
|||
var fallbackStartPositionMs by remember(playerSourceKey) { mutableStateOf<Long?>(null) }
|
||||
val effectiveDecoderPriority = decoderPriorityOverride ?: playerSettings.decoderPriority
|
||||
|
||||
val extractorsFactory = remember {
|
||||
DefaultExtractorsFactory()
|
||||
.setTsExtractorFlags(DefaultTsPayloadReaderFactory.FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS)
|
||||
.setTsExtractorTimestampSearchBytes(1500 * TsExtractor.TS_PACKET_SIZE)
|
||||
}
|
||||
val dataSourceFactory = remember(
|
||||
context,
|
||||
sanitizedSourceHeaders,
|
||||
sanitizedSourceResponseHeaders,
|
||||
useYoutubeChunkedPlayback,
|
||||
) {
|
||||
PlatformPlaybackDataSourceFactory.create(
|
||||
context = context,
|
||||
defaultRequestHeaders = sanitizedSourceHeaders,
|
||||
defaultResponseHeaders = sanitizedSourceResponseHeaders,
|
||||
useYoutubeChunkedPlayback = useYoutubeChunkedPlayback,
|
||||
)
|
||||
}
|
||||
|
||||
fun ExoPlayer.setPlaybackMediaItem(videoMediaItem: MediaItem, startPositionMs: Long? = null) {
|
||||
if (!sourceAudioUrl.isNullOrBlank()) {
|
||||
val mediaSourceFactory = DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory)
|
||||
val videoSource = mediaSourceFactory.createMediaSource(videoMediaItem)
|
||||
val audioSource = mediaSourceFactory.createMediaSource(MediaItem.fromUri(sourceAudioUrl))
|
||||
val mergedSource = MergingMediaSource(videoSource, audioSource)
|
||||
if (startPositionMs != null) {
|
||||
setMediaSource(mergedSource, startPositionMs.coerceAtLeast(0L))
|
||||
} else {
|
||||
setMediaSource(mergedSource)
|
||||
}
|
||||
} else if (startPositionMs != null) {
|
||||
setMediaItem(videoMediaItem, startPositionMs.coerceAtLeast(0L))
|
||||
} else {
|
||||
setMediaItem(videoMediaItem)
|
||||
}
|
||||
}
|
||||
|
||||
val exoPlayer = remember(
|
||||
sourceUrl,
|
||||
sourceAudioUrl,
|
||||
|
|
@ -158,17 +196,6 @@ actual fun PlatformPlayerSurface(
|
|||
)
|
||||
.build()
|
||||
|
||||
val extractorsFactory = DefaultExtractorsFactory()
|
||||
.setTsExtractorFlags(DefaultTsPayloadReaderFactory.FLAG_ENABLE_HDMV_DTS_AUDIO_STREAMS)
|
||||
.setTsExtractorTimestampSearchBytes(1500 * TsExtractor.TS_PACKET_SIZE)
|
||||
|
||||
val dataSourceFactory = PlatformPlaybackDataSourceFactory.create(
|
||||
context = context,
|
||||
defaultRequestHeaders = sanitizedSourceHeaders,
|
||||
defaultResponseHeaders = sanitizedSourceResponseHeaders,
|
||||
useYoutubeChunkedPlayback = useYoutubeChunkedPlayback,
|
||||
)
|
||||
|
||||
val player = if (useLibass) {
|
||||
ExoPlayer.Builder(context)
|
||||
.setTrackSelector(trackSelector)
|
||||
|
|
@ -195,21 +222,17 @@ actual fun PlatformPlayerSurface(
|
|||
}
|
||||
|
||||
player.apply {
|
||||
if (!sourceAudioUrl.isNullOrBlank()) {
|
||||
val msf = DefaultMediaSourceFactory(dataSourceFactory, extractorsFactory)
|
||||
val videoSource = msf.createMediaSource(MediaItem.fromUri(sourceUrl))
|
||||
val audioSource = msf.createMediaSource(MediaItem.fromUri(sourceAudioUrl))
|
||||
setMediaSource(MergingMediaSource(videoSource, audioSource))
|
||||
} else {
|
||||
setMediaItem(MediaItem.fromUri(sourceUrl))
|
||||
}
|
||||
fallbackStartPositionMs?.let { seekTo(it.coerceAtLeast(0L)) }
|
||||
prepare()
|
||||
this.playWhenReady = playWhenReady
|
||||
}
|
||||
setPlaybackMediaItem(
|
||||
videoMediaItem = MediaItem.fromUri(sourceUrl),
|
||||
startPositionMs = fallbackStartPositionMs,
|
||||
)
|
||||
prepare()
|
||||
this.playWhenReady = playWhenReady
|
||||
}
|
||||
}
|
||||
|
||||
val pendingSubtitleTrackIndex = remember { mutableListOf<Int>() }
|
||||
val pendingAudioTrackSelection = remember { mutableListOf<TrackSelectionSnapshot>() }
|
||||
var playerViewRef by remember { mutableStateOf<PlayerView?>(null) }
|
||||
var currentSubtitleStyle by remember { mutableStateOf(SubtitleStyleState.DEFAULT) }
|
||||
var subtitleSelectionJob by remember { mutableStateOf<Job?>(null) }
|
||||
|
|
@ -218,6 +241,13 @@ actual fun PlatformPlayerSurface(
|
|||
playerViewRef?.keepScreenOn = exoPlayer.shouldKeepPlayerScreenOn()
|
||||
}
|
||||
|
||||
fun preserveAudioSelectionForReload(reason: String) {
|
||||
pendingAudioTrackSelection.clear()
|
||||
val selection = exoPlayer.captureSelectedTrack(C.TRACK_TYPE_AUDIO) ?: return
|
||||
pendingAudioTrackSelection.add(selection)
|
||||
Log.d(TAG, "$reason: preserving audio track index=${selection.index} id=${selection.id}")
|
||||
}
|
||||
|
||||
DisposableEffect(exoPlayer) {
|
||||
PlayerPictureInPictureManager.registerPausePlaybackCallback {
|
||||
exoPlayer.pause()
|
||||
|
|
@ -274,6 +304,13 @@ actual fun PlatformPlayerSurface(
|
|||
override fun onTracksChanged(tracks: androidx.media3.common.Tracks) {
|
||||
Log.d(TAG, "onTracksChanged: ${tracks.groups.size} groups total")
|
||||
exoPlayer.logCurrentTracks("onTracksChanged")
|
||||
pendingAudioTrackSelection.firstOrNull()?.let { selection ->
|
||||
if (tracks.groups.any { it.type == C.TRACK_TYPE_AUDIO }) {
|
||||
pendingAudioTrackSelection.clear()
|
||||
val restored = exoPlayer.restoreTrackSelection(selection)
|
||||
Log.d(TAG, "onTracksChanged: restored pending audio selection=$restored")
|
||||
}
|
||||
}
|
||||
if (pendingSubtitleTrackIndex.isNotEmpty() && tracks.groups.isNotEmpty()) {
|
||||
val idx = pendingSubtitleTrackIndex.removeAt(0)
|
||||
Log.d(TAG, "onTracksChanged: applying pending subtitle selection index=$idx")
|
||||
|
|
@ -401,6 +438,7 @@ actual fun PlatformPlayerSurface(
|
|||
Log.e(TAG, "setSubtitleUri: currentMediaItem is null, aborting")
|
||||
return@launch
|
||||
}
|
||||
preserveAudioSelectionForReload("setSubtitleUri")
|
||||
val resolvedMime = withContext(Dispatchers.IO) {
|
||||
resolveSubtitleMimeType(url)
|
||||
}
|
||||
|
|
@ -426,7 +464,7 @@ actual fun PlatformPlayerSurface(
|
|||
.setPreferredTextRoleFlags(C.ROLE_FLAG_SUBTITLE)
|
||||
.build()
|
||||
Log.d(TAG, "setSubtitleUri: track params set before prepare, textDisabled=${exoPlayer.trackSelectionParameters.disabledTrackTypes.contains(C.TRACK_TYPE_TEXT)}")
|
||||
exoPlayer.setMediaItem(newMediaItem, currentPosition)
|
||||
exoPlayer.setPlaybackMediaItem(newMediaItem, currentPosition)
|
||||
exoPlayer.prepare()
|
||||
exoPlayer.playWhenReady = wasPlaying
|
||||
Log.d(TAG, "setSubtitleUri: prepare() called, waiting for STATE_READY")
|
||||
|
|
@ -440,10 +478,11 @@ actual fun PlatformPlayerSurface(
|
|||
val currentPosition = exoPlayer.currentPosition
|
||||
val wasPlaying = exoPlayer.isPlaying
|
||||
val currentMediaItem = exoPlayer.currentMediaItem ?: return
|
||||
preserveAudioSelectionForReload("clearExternalSubtitle")
|
||||
val newMediaItem = currentMediaItem.buildUpon()
|
||||
.setSubtitleConfigurations(emptyList())
|
||||
.build()
|
||||
exoPlayer.setMediaItem(newMediaItem, currentPosition)
|
||||
exoPlayer.setPlaybackMediaItem(newMediaItem, currentPosition)
|
||||
exoPlayer.prepare()
|
||||
exoPlayer.playWhenReady = wasPlaying
|
||||
Log.d(TAG, "clearExternalSubtitle: done, position=$currentPosition")
|
||||
|
|
@ -458,10 +497,11 @@ actual fun PlatformPlayerSurface(
|
|||
val currentPosition = exoPlayer.currentPosition
|
||||
val wasPlaying = exoPlayer.isPlaying
|
||||
val currentMediaItem = exoPlayer.currentMediaItem ?: return
|
||||
preserveAudioSelectionForReload("clearExternalSubtitleAndSelect")
|
||||
val newMediaItem = currentMediaItem.buildUpon()
|
||||
.setSubtitleConfigurations(emptyList())
|
||||
.build()
|
||||
exoPlayer.setMediaItem(newMediaItem, currentPosition)
|
||||
exoPlayer.setPlaybackMediaItem(newMediaItem, currentPosition)
|
||||
exoPlayer.prepare()
|
||||
exoPlayer.playWhenReady = wasPlaying
|
||||
Log.d(TAG, "clearExternalSubtitleAndSelect: done, pending=$trackIndex position=$currentPosition")
|
||||
|
|
@ -544,6 +584,86 @@ private fun ExoPlayer.shouldKeepPlayerScreenOn(): Boolean =
|
|||
playWhenReady &&
|
||||
playbackState in setOf(Player.STATE_BUFFERING, Player.STATE_READY)
|
||||
|
||||
private data class TrackSelectionSnapshot(
|
||||
val trackType: Int,
|
||||
val index: Int,
|
||||
val id: String?,
|
||||
val language: String?,
|
||||
val label: String?,
|
||||
val sampleMimeType: String?,
|
||||
val codecs: String?,
|
||||
val channelCount: Int,
|
||||
val roleFlags: Int,
|
||||
)
|
||||
|
||||
private fun ExoPlayer.captureSelectedTrack(trackType: Int): TrackSelectionSnapshot? {
|
||||
var idx = 0
|
||||
for (group in currentTracks.groups) {
|
||||
if (group.type != trackType) continue
|
||||
if (group.isSelected) {
|
||||
val format = group.mediaTrackGroup.getFormat(0)
|
||||
return TrackSelectionSnapshot(
|
||||
trackType = trackType,
|
||||
index = idx,
|
||||
id = format.id,
|
||||
language = format.language,
|
||||
label = format.label,
|
||||
sampleMimeType = format.sampleMimeType,
|
||||
codecs = format.codecs,
|
||||
channelCount = format.channelCount,
|
||||
roleFlags = format.roleFlags,
|
||||
)
|
||||
}
|
||||
idx++
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun ExoPlayer.restoreTrackSelection(selection: TrackSelectionSnapshot): Boolean {
|
||||
selection.id?.takeIf { it.isNotBlank() }?.let { id ->
|
||||
val restored = selectTrackByPredicate(selection.trackType, "id=$id") { _, format ->
|
||||
format.id == id
|
||||
}
|
||||
if (restored) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
selection.label?.takeIf { it.isNotBlank() }?.let { label ->
|
||||
val restored = selectTrackByPredicate(selection.trackType, "label=$label") { _, format ->
|
||||
format.label.equals(label, ignoreCase = true) &&
|
||||
(selection.language.isNullOrBlank() ||
|
||||
format.language.equals(selection.language, ignoreCase = true))
|
||||
}
|
||||
if (restored) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
val technicalMatchIndexes = mutableListOf<Int>()
|
||||
var idx = 0
|
||||
for (group in currentTracks.groups) {
|
||||
if (group.type != selection.trackType) continue
|
||||
val format = group.mediaTrackGroup.getFormat(0)
|
||||
if (
|
||||
!selection.language.isNullOrBlank() &&
|
||||
format.language.equals(selection.language, ignoreCase = true) &&
|
||||
format.sampleMimeType == selection.sampleMimeType &&
|
||||
format.codecs == selection.codecs &&
|
||||
format.channelCount == selection.channelCount &&
|
||||
format.roleFlags == selection.roleFlags
|
||||
) {
|
||||
technicalMatchIndexes.add(idx)
|
||||
}
|
||||
idx++
|
||||
}
|
||||
if (technicalMatchIndexes.size == 1) {
|
||||
return selectTrackByIndex(selection.trackType, technicalMatchIndexes.first())
|
||||
}
|
||||
|
||||
return selectTrackByIndex(selection.trackType, selection.index)
|
||||
}
|
||||
|
||||
private fun PlaybackException.isDecoderFailure(): Boolean =
|
||||
errorCode in setOf(
|
||||
PlaybackException.ERROR_CODE_DECODER_INIT_FAILED,
|
||||
|
|
@ -694,27 +814,39 @@ private fun ExoPlayer.extractSubtitleTracks(context: Context): List<SubtitleTrac
|
|||
return tracks
|
||||
}
|
||||
|
||||
private fun ExoPlayer.selectTrackByIndex(trackType: Int, targetIndex: Int) {
|
||||
private fun ExoPlayer.selectTrackByIndex(trackType: Int, targetIndex: Int): Boolean {
|
||||
return selectTrackByPredicate(trackType, "index=$targetIndex") { idx, _ ->
|
||||
idx == targetIndex
|
||||
}
|
||||
}
|
||||
|
||||
private fun ExoPlayer.selectTrackByPredicate(
|
||||
trackType: Int,
|
||||
targetDescription: String,
|
||||
predicate: (index: Int, format: Format) -> Boolean,
|
||||
): Boolean {
|
||||
val typeName = if (trackType == C.TRACK_TYPE_AUDIO) "AUDIO" else "TEXT"
|
||||
Log.d(TAG, "selectTrackByIndex: type=$typeName targetIndex=$targetIndex")
|
||||
Log.d(TAG, "selectTrack: type=$typeName target=$targetDescription")
|
||||
var idx = 0
|
||||
for (group in currentTracks.groups) {
|
||||
if (group.type != trackType) continue
|
||||
if (idx == targetIndex) {
|
||||
val format = group.mediaTrackGroup.getFormat(0)
|
||||
Log.d(TAG, "selectTrackByIndex: found group at idx=$idx, format.id=${format.id}, lang=${format.language}, label=${format.label}")
|
||||
trackSelectionParameters = trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setOverrideForType(
|
||||
TrackSelectionOverride(group.mediaTrackGroup, listOf(0))
|
||||
)
|
||||
.build()
|
||||
Log.d(TAG, "selectTrackByIndex: override applied")
|
||||
return
|
||||
val format = group.mediaTrackGroup.getFormat(0)
|
||||
if (!predicate(idx, format)) {
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
idx++
|
||||
Log.d(TAG, "selectTrack: found group at idx=$idx, format.id=${format.id}, lang=${format.language}, label=${format.label}")
|
||||
trackSelectionParameters = trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setOverrideForType(
|
||||
TrackSelectionOverride(group.mediaTrackGroup, listOf(0))
|
||||
)
|
||||
.build()
|
||||
Log.d(TAG, "selectTrack: override applied")
|
||||
return true
|
||||
}
|
||||
Log.w(TAG, "selectTrackByIndex: no group found for type=$typeName at index=$targetIndex (total groups scanned=$idx)")
|
||||
Log.w(TAG, "selectTrack: no group found for type=$typeName target=$targetDescription (total groups scanned=$idx)")
|
||||
return false
|
||||
}
|
||||
|
||||
private fun ExoPlayer.logCurrentTracks(context: String) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue