Skip to content

Commit

Permalink
be able to react on playback errors
Browse files Browse the repository at this point in the history
  • Loading branch information
theScrabi committed Oct 10, 2024
1 parent 7e2ceab commit 6afe6ea
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 44 deletions.
88 changes: 49 additions & 39 deletions new-player/src/main/java/net/newpipe/newplayer/NewPlayerImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.google.common.util.concurrent.MoreExecutors
Expand All @@ -54,6 +55,7 @@ import net.newpipe.newplayer.service.NewPlayerService
import net.newpipe.newplayer.utils.ActionResponse
import net.newpipe.newplayer.utils.MediaSourceBuilder
import net.newpipe.newplayer.utils.NewPlayerException
import net.newpipe.newplayer.utils.NewPlayerLoadErrorHandlingPolicy
import net.newpipe.newplayer.utils.NoResponse
import net.newpipe.newplayer.utils.SingleSelection
import net.newpipe.newplayer.utils.StreamExceptionResponse
Expand All @@ -74,12 +76,13 @@ class NewPlayerImpl(
app,
R.drawable.new_player_tiny_icon
),
val rescudeStreamFault: suspend (
val rescueStreamFault: suspend (
item: String?,
mediaItem: MediaItem?,
exception: PlaybackException
exception: Exception,
repository: MediaRepository
) -> StreamExceptionResponse
= { _, _, _ -> NoResponse() }
= { _, _, _, _ -> NoResponse() }
) : NewPlayer {
private val mutableExoPlayer = MutableStateFlow<ExoPlayer?>(null)
override val exoPlayer = mutableExoPlayer.asStateFlow()
Expand Down Expand Up @@ -179,43 +182,10 @@ class NewPlayerImpl(
.setHandleAudioBecomingNoisy(true)
.setWakeMode(if (repository.getRepoInfo().pullsDataFromNetwrok) C.WAKE_MODE_NETWORK else C.WAKE_MODE_LOCAL)
.build()

newExoPlayer.addListener(object : Player.Listener {
override fun onPlayerError(error: PlaybackException) {
newExoPlayer.pause()
launchJobAndCollectError {
val item = newExoPlayer.currentMediaItem?.mediaId?.let {
uniqueIdToIdLookup[it.toLong()]
}
val response = rescudeStreamFault(
item,
newExoPlayer.currentMediaItem,
error
)
when (response) {
is ActionResponse -> {
response.action()
}

is StreamSelectionResponse -> {
replaceCurrentStream(response.streamSelection)
}

is NoResponse -> {
try {
throw NewPlayerException(
"Playback Exception happened, but no response was send by rescueStreamFault(). You may consider to implement this function.",
error
)
} catch (e: Exception) {
mutableErrorFlow.emit(e)
}
}

else -> {
throw NewPlayerException("Unknwon exception response ${response.javaClass}")
}
}
}
onPlayBackError(error)
}

override fun onEvents(player: Player, events: Player.Events) {
Expand Down Expand Up @@ -255,6 +225,45 @@ class NewPlayerImpl(
}
}

fun onPlayBackError(exception: Exception) {
exoPlayer.value?.pause()
launchJobAndCollectError {
val item = exoPlayer.value?.currentMediaItem?.mediaId?.let {
uniqueIdToIdLookup[it.toLong()]
}
val response = rescueStreamFault(
item,
exoPlayer.value?.currentMediaItem!!,
exception,
repository
)
when (response) {
is ActionResponse -> {
response.action()
}

is StreamSelectionResponse -> {
replaceCurrentStream(response.streamSelection)
}

is NoResponse -> {
try {
throw NewPlayerException(
"Playback Exception happened, but no response was send by rescueStreamFault(). You may consider to implement this function.",
exception
)
} catch (e: Exception) {
mutableErrorFlow.emit(e)
}
}

else -> {
throw NewPlayerException("Unknwon exception response ${response.javaClass}")
}
}
}
}

init {
playerScope.launch {
currentlyPlaying.collect { playing ->
Expand Down Expand Up @@ -455,7 +464,8 @@ class NewPlayerImpl(
repository = repository,
uniqueIdToIdLookup = uniqueIdToIdLookup,
mutableErrorFlow = mutableErrorFlow,
httpDataSourceFactory = repository.getHttpDataSourceFactory(item)
httpDataSourceFactory = repository.getHttpDataSourceFactory(item),
loadErrorHandlingPolicy = NewPlayerLoadErrorHandlingPolicy(this::onPlayBackError)
)
val mediaSource = builder.buildMediaSource(streamSelection)
uniqueIdToStreamVariantSelection[mediaSource.mediaItem.mediaId.toLong()] = streamSelection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,21 @@ import androidx.media3.exoplayer.dash.DashMediaSource
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.MergingMediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
import kotlinx.coroutines.flow.MutableSharedFlow
import net.newpipe.newplayer.MediaRepository
import net.newpipe.newplayer.StreamType
import net.newpipe.newplayer.Stream
import kotlin.random.Random

internal class MediaSourceBuilder(
@OptIn(UnstableApi::class)
internal class MediaSourceBuilder
(
private val repository: MediaRepository,
private val uniqueIdToIdLookup: HashMap<Long, String>,
private val mutableErrorFlow: MutableSharedFlow<Exception>,
private val httpDataSourceFactory: HttpDataSource.Factory,
private val loadErrorHandlingPolicy: LoadErrorHandlingPolicy
) {
@OptIn(UnstableApi::class)
internal suspend fun buildMediaSource(selectedStream: StreamSelection): MediaSource {
Expand Down Expand Up @@ -88,9 +92,11 @@ internal class MediaSourceBuilder(
private fun toMediaSource(mediaItem: MediaItem, stream: Stream): MediaSource =
if (stream.streamType == StreamType.DYNAMIC)
DashMediaSource.Factory(httpDataSourceFactory)
.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
.createMediaSource(mediaItem)
else
ProgressiveMediaSource.Factory(httpDataSourceFactory)
.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy)
.createMediaSource(mediaItem)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package net.newpipe.newplayer.utils

import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
import net.newpipe.newplayer.NewPlayer

@UnstableApi
class NewPlayerLoadErrorHandlingPolicy(val exceptionHandler: (Exception) -> Unit) :
DefaultLoadErrorHandlingPolicy() {
override fun getFallbackSelectionFor(
fallbackOptions: LoadErrorHandlingPolicy.FallbackOptions,
loadErrorInfo: LoadErrorHandlingPolicy.LoadErrorInfo
): LoadErrorHandlingPolicy.FallbackSelection? {
val fallbackSelection = super.getFallbackSelectionFor(fallbackOptions, loadErrorInfo)
exceptionHandler(loadErrorInfo.exception)
return fallbackSelection
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ interface StreamSelection {
val item: String
}

internal data class SingleSelection(
data class SingleSelection(
override val item: String,
val stream: Stream
) : StreamSelection

internal data class MultiSelection(
data class MultiSelection(
override val item: String,
val streams: List<Stream>
) : StreamSelection
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ class MainActivity : AppCompatActivity() {
newPlayer.addToPlaylist("yt_test")
}

binding.buttons.addFaultyStream.setOnClickListener {
newPlayer.addToPlaylist("faulty")
}

binding.buttons.listenModeButton.setOnClickListener {
newPlayer.playBackMode.update {
PlayMode.FULLSCREEN_AUDIO
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ object NewPlayerComponent {
app = app,
repository = TestMediaRepository(app),
notificationIcon = IconCompat.createWithResource(app, R.drawable.tinny_cools),
playerActivityClass = MainActivity::class.java as Class<Activity>
playerActivityClass = MainActivity::class.java as Class<Activity>,
rescueStreamFault = ::streamErrorHandler
)
if (app is NewPlayerApp) {
app.appScope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ class TestMediaRepository(private val context: Context) : MediaRepository {
)
.build()

"faulty" -> MediaMetadata.Builder()
.setTitle(context.getString(R.string.ccc_6502_title))
.setArtist(context.getString(R.string.ccc_6502_channel))
.setArtworkUri(Uri.parse(context.getString(R.string.ccc_6502_thumbnail)))
.setDurationMs(
context.resources.getInteger(R.integer.ccc_6502_length).toLong() * 1000L
)
.build()

"imu" -> MediaMetadata.Builder()
.setTitle(context.getString(R.string.ccc_imu_title))
.setArtist(context.getString(R.string.ccc_imu_channel))
Expand Down Expand Up @@ -99,6 +108,16 @@ class TestMediaRepository(private val context: Context) : MediaRepository {
)
)

"faulty" -> listOf(
Stream(
streamUri = Uri.parse("https://kernel.org"),
mimeType = null,
streamType = StreamType.AUDIO_AND_VIDEO,
language = "Deutsch",
identifier = "576p",
)
)

"portrait" -> listOf(
Stream(
streamUri = Uri.parse(context.getString(R.string.portrait_video_example)),
Expand Down Expand Up @@ -189,7 +208,8 @@ class TestMediaRepository(private val context: Context) : MediaRepository {
"6502" -> context.getString(R.string.ccc_6502_preview_thumbnails)
"imu" -> context.getString(R.string.ccc_imu_preview_thumbnails)
"portrait" -> return
"ty_test" -> return
"yt_test" -> return
"faulty" -> return
else -> throw Exception("Unknown stream: $item")
}

Expand Down Expand Up @@ -230,6 +250,7 @@ class TestMediaRepository(private val context: Context) : MediaRepository {
"imu" -> context.getString(R.string.ccc_imu_preview_thumbnails)
"portrait" -> return null
"ty_test" -> return null
"faulty" -> return null
else -> throw Exception("Unknown stream: $item")
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package net.newpipe.newplayer.testapp

import androidx.media3.common.MediaItem
import net.newpipe.newplayer.MediaRepository
import net.newpipe.newplayer.utils.NoResponse
import net.newpipe.newplayer.utils.SingleSelection
import net.newpipe.newplayer.utils.StreamExceptionResponse
import net.newpipe.newplayer.utils.StreamSelectionResponse
import java.lang.Exception

suspend fun streamErrorHandler(
item: String?,
mediaItem: MediaItem?,
exception: Exception,
repository: MediaRepository
): StreamExceptionResponse {
return if (item == "faulty") {
StreamSelectionResponse(SingleSelection(item, repository.getStreams("6502")[0]))
} else {
NoResponse()
}
}
8 changes: 8 additions & 0 deletions test-app/src/main/res/layout/buttons.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,12 @@
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Add Yt Test Video" />

<Button
android:id="@+id/add_faulty_stream"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:backgroundTint="@color/material_dynamic_primary50"
android:text="Add Faulty Stream" />

</LinearLayout>

0 comments on commit 6afe6ea

Please sign in to comment.