Skip to content
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

Fix LiveTV #500

Merged
merged 5 commits into from
Aug 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 42 additions & 28 deletions app/src/main/java/org/jellyfin/mobile/player/PlaybackMenus.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.view.View
import android.widget.ImageButton
import android.widget.PopupMenu
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.core.view.forEach
import androidx.core.view.isVisible
import org.jellyfin.mobile.R
Expand Down Expand Up @@ -91,42 +92,52 @@ class PlaybackMenus(
updateSubtitlesButton()

val playMethod = context.getString(R.string.playback_info_play_method, mediaSource.playMethod)
val videoTracksInfo = mediaSource.videoStreams.run {
joinToString(
"\n",
"${fragment.getString(R.string.playback_info_video_streams)}:\n",
limit = MAX_VIDEO_STREAMS_DISPLAY,
truncated = fragment.getString(R.string.playback_info_and_x_more, size - MAX_VIDEO_STREAMS_DISPLAY)
) { stream ->
val bitrate = stream.bitRate ?: 0

@Suppress("MagicNumber")
val bitrateString = when {
bitrate > 1_000_000 -> "%.2f Mbps".format(Locale.getDefault(), bitrate.toDouble() / 1_000_000)
bitrate > 1_000 -> "%.2f Kbps".format(Locale.getDefault(), bitrate.toDouble() / 1_000)
else -> "%d Kbps".format(bitrate / 1000)
val videoTracksInfo = buildMediaStreamsInfo(
mediaStreams = mediaSource.videoStreams,
prefix = R.string.playback_info_video_streams,
maxStreams = MAX_VIDEO_STREAMS_DISPLAY,
streamSuffix = { stream ->
val bitrate = stream.bitRate
when {
bitrate == null -> ""
bitrate > BITRATE_MEGA_BIT -> " (%.2f Mbps)".format(Locale.getDefault(), bitrate.toDouble() / BITRATE_MEGA_BIT)
bitrate > BITRATE_KILO_BIT -> " (%.2f Kbps)".format(Locale.getDefault(), bitrate.toDouble() / BITRATE_KILO_BIT)
else -> " (%d bps)".format(bitrate)
}
"- ${stream.displayTitle} ($bitrateString)"
}
}
val audioTracksInfo = mediaSource.audioStreams.run {
joinToString(
"\n",
"${fragment.getString(R.string.playback_info_audio_streams)}:\n",
limit = MAX_AUDIO_STREAMS_DISPLAY,
truncated = fragment.getString(R.string.playback_info_and_x_more, size - MAX_AUDIO_STREAMS_DISPLAY)
) { stream ->
val languageString = stream.language?.let { lang -> " ($lang)" }.orEmpty()
"- ${stream.displayTitle}$languageString"
}
}
},
)
val audioTracksInfo = buildMediaStreamsInfo(
mediaStreams = mediaSource.audioStreams,
prefix = R.string.playback_info_audio_streams,
maxStreams = MAX_AUDIO_STREAMS_DISPLAY,
streamSuffix = { stream ->
stream.language?.let { lang -> " ($lang)" }.orEmpty()
},
)

playbackInfo.text = listOf(
playMethod,
videoTracksInfo,
audioTracksInfo,
).joinToString("\n\n")
}

private fun buildMediaStreamsInfo(
mediaStreams: List<MediaStream>,
@StringRes prefix: Int,
maxStreams: Int,
streamSuffix: (MediaStream) -> String,
): String = mediaStreams.joinToString(
"\n",
"${fragment.getString(prefix)}:\n",
limit = maxStreams,
truncated = fragment.getString(R.string.playback_info_and_x_more, mediaStreams.size - maxStreams)
) { stream ->
val title = stream.displayTitle?.takeUnless(String::isEmpty) ?: fragment.getString(R.string.playback_info_stream_unknown_title)
val suffix = streamSuffix(stream)
"- $title$suffix"
}

private fun createSubtitlesMenu() = PopupMenu(context, subtitlesButton).apply {
setOnMenuItemClickListener { clickedItem ->
val selected = clickedItem.itemId
Expand Down Expand Up @@ -216,6 +227,9 @@ class PlaybackMenus(
private const val MAX_VIDEO_STREAMS_DISPLAY = 3
private const val MAX_AUDIO_STREAMS_DISPLAY = 5

private const val BITRATE_MEGA_BIT = 1_000_000
private const val BITRATE_KILO_BIT = 1_000

private const val SPEED_MENU_STEP_SIZE = 0.25f
private const val SPEED_MENU_STEP_MIN = 2 // → 0.5x
private const val SPEED_MENU_STEP_MAX = 8 // → 2x
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.jellyfin.mobile.player.PlayerViewModel
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.operations.VideosApi
import org.jellyfin.sdk.model.api.DeviceProfile
import org.jellyfin.sdk.model.api.MediaProtocol
import org.jellyfin.sdk.model.api.MediaStream
import org.jellyfin.sdk.model.api.PlayMethod
import org.koin.core.component.KoinComponent
Expand Down Expand Up @@ -135,17 +136,26 @@ class MediaQueueManager(
@CheckResult
private fun createVideoMediaSource(source: JellyfinMediaSource): MediaSource {
val sourceInfo = source.sourceInfo
val builder = MediaItem.Builder().setMediaId(source.itemId.toString())
return when (source.playMethod) {
val (url, factory) = when (source.playMethod) {
PlayMethod.DIRECT_PLAY -> {
val url = videosApi.getVideoStreamUrl(
itemId = source.itemId,
static = true,
mediaSourceId = source.id,
)
when (sourceInfo.protocol) {
MediaProtocol.FILE -> {
val url = videosApi.getVideoStreamUrl(
itemId = source.itemId,
static = true,
mediaSourceId = source.id,
)

url to get<ProgressiveMediaSource.Factory>()
}
MediaProtocol.HTTP -> {
val url = requireNotNull(sourceInfo.path)
val factory = get<HlsMediaSource.Factory>().setAllowChunklessPreparation(true)

val mediaItem = builder.setUri(url).build()
get<ProgressiveMediaSource.Factory>().createMediaSource(mediaItem)
url to factory
}
else -> throw IllegalArgumentException("Unsupported protocol ${sourceInfo.protocol}")
}
}
PlayMethod.DIRECT_STREAM -> {
val container = requireNotNull(sourceInfo.container) { "Missing direct stream container" }
Expand All @@ -155,20 +165,25 @@ class MediaQueueManager(
mediaSourceId = source.id,
)

val mediaItem = builder.setUri(url).build()
get<ProgressiveMediaSource.Factory>().createMediaSource(mediaItem)
url to get<ProgressiveMediaSource.Factory>()
}
PlayMethod.TRANSCODE -> {
val transcodingPath = requireNotNull(sourceInfo.transcodingUrl) { "Missing transcode URL" }
val protocol = sourceInfo.transcodingSubProtocol
require(protocol == "hls") { "Unsupported transcode protocol '$protocol'" }
val transcodingUrl = apiClient.createUrl(transcodingPath)
val mediaItem = builder.setUri(transcodingUrl).build()
get<HlsMediaSource.Factory>()
.setAllowChunklessPreparation(true)
.createMediaSource(mediaItem)
val factory = get<HlsMediaSource.Factory>().setAllowChunklessPreparation(true)

transcodingUrl to factory
}
}

val mediaItem = MediaItem.Builder()
.setMediaId(source.itemId.toString())
.setUri(url)
.build()

return factory.createMediaSource(mediaItem)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ class MediaSourceResolver(
),
)

response.mediaSources?.find { source ->
source.id?.toUUIDOrNull() == itemId
response.mediaSources?.let { sources ->
sources.find { source -> source.id?.toUUIDOrNull() == itemId } ?: sources.firstOrNull()
} ?: return Result.failure(PlayerException.UnsupportedContent())
} catch (e: ApiClientException) {
Timber.e(e, "Failed to load media source $itemId")
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<string name="playback_info_transcoding">Transcoding: %b</string>
<string name="playback_info_video_streams">Video streams</string>
<string name="playback_info_audio_streams">Audio streams</string>
<string name="playback_info_stream_unknown_title">Unknown title</string>
<string name="playback_info_and_x_more">…and %d more</string>

<string name="external_player_invalid_play_method">Invalid play method.</string>
Expand Down