mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
feat: Added playbackinformation to native player ui
This commit is contained in:
parent
37496f87e3
commit
25304d0a5b
14 changed files with 266 additions and 50 deletions
|
|
@ -80,6 +80,18 @@ class FlutterError (
|
|||
val details: Any? = null
|
||||
) : Throwable()
|
||||
|
||||
enum class PlaybackType(val raw: Int) {
|
||||
DIRECT(0),
|
||||
TRANSCODED(1),
|
||||
OFFLINE(2);
|
||||
|
||||
companion object {
|
||||
fun ofRaw(raw: Int): PlaybackType? {
|
||||
return values().firstOrNull { it.raw == raw }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class MediaSegmentType(val raw: Int) {
|
||||
COMMERCIAL(0),
|
||||
PREVIEW(1),
|
||||
|
|
@ -137,6 +149,37 @@ data class SimpleItemModel (
|
|||
override fun hashCode(): Int = toList().hashCode()
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class MediaInfo (
|
||||
val playbackType: PlaybackType,
|
||||
val videoInformation: String
|
||||
)
|
||||
{
|
||||
companion object {
|
||||
fun fromList(pigeonVar_list: List<Any?>): MediaInfo {
|
||||
val playbackType = pigeonVar_list[0] as PlaybackType
|
||||
val videoInformation = pigeonVar_list[1] as String
|
||||
return MediaInfo(playbackType, videoInformation)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
return listOf(
|
||||
playbackType,
|
||||
videoInformation,
|
||||
)
|
||||
}
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is MediaInfo) {
|
||||
return false
|
||||
}
|
||||
if (this === other) {
|
||||
return true
|
||||
}
|
||||
return VideoPlayerHelperPigeonUtils.deepEquals(toList(), other.toList()) }
|
||||
|
||||
override fun hashCode(): Int = toList().hashCode()
|
||||
}
|
||||
|
||||
/** Generated class from Pigeon that represents data sent in messages. */
|
||||
data class PlayableData (
|
||||
val currentItem: SimpleItemModel,
|
||||
|
|
@ -151,6 +194,7 @@ data class PlayableData (
|
|||
val segments: List<MediaSegment>,
|
||||
val previousVideo: SimpleItemModel? = null,
|
||||
val nextVideo: SimpleItemModel? = null,
|
||||
val mediaInfo: MediaInfo,
|
||||
val url: String
|
||||
)
|
||||
{
|
||||
|
|
@ -168,8 +212,9 @@ data class PlayableData (
|
|||
val segments = pigeonVar_list[9] as List<MediaSegment>
|
||||
val previousVideo = pigeonVar_list[10] as SimpleItemModel?
|
||||
val nextVideo = pigeonVar_list[11] as SimpleItemModel?
|
||||
val url = pigeonVar_list[12] as String
|
||||
return PlayableData(currentItem, description, startPosition, defaultAudioTrack, audioTracks, defaultSubtrack, subtitleTracks, trickPlayModel, chapters, segments, previousVideo, nextVideo, url)
|
||||
val mediaInfo = pigeonVar_list[12] as MediaInfo
|
||||
val url = pigeonVar_list[13] as String
|
||||
return PlayableData(currentItem, description, startPosition, defaultAudioTrack, audioTracks, defaultSubtrack, subtitleTracks, trickPlayModel, chapters, segments, previousVideo, nextVideo, mediaInfo, url)
|
||||
}
|
||||
}
|
||||
fun toList(): List<Any?> {
|
||||
|
|
@ -186,6 +231,7 @@ data class PlayableData (
|
|||
segments,
|
||||
previousVideo,
|
||||
nextVideo,
|
||||
mediaInfo,
|
||||
url,
|
||||
)
|
||||
}
|
||||
|
|
@ -482,50 +528,60 @@ private open class VideoPlayerHelperPigeonCodec : StandardMessageCodec() {
|
|||
return when (type) {
|
||||
129.toByte() -> {
|
||||
return (readValue(buffer) as Long?)?.let {
|
||||
MediaSegmentType.ofRaw(it.toInt())
|
||||
PlaybackType.ofRaw(it.toInt())
|
||||
}
|
||||
}
|
||||
130.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
SimpleItemModel.fromList(it)
|
||||
return (readValue(buffer) as Long?)?.let {
|
||||
MediaSegmentType.ofRaw(it.toInt())
|
||||
}
|
||||
}
|
||||
131.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
PlayableData.fromList(it)
|
||||
SimpleItemModel.fromList(it)
|
||||
}
|
||||
}
|
||||
132.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
MediaSegment.fromList(it)
|
||||
MediaInfo.fromList(it)
|
||||
}
|
||||
}
|
||||
133.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
AudioTrack.fromList(it)
|
||||
PlayableData.fromList(it)
|
||||
}
|
||||
}
|
||||
134.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
SubtitleTrack.fromList(it)
|
||||
MediaSegment.fromList(it)
|
||||
}
|
||||
}
|
||||
135.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
Chapter.fromList(it)
|
||||
AudioTrack.fromList(it)
|
||||
}
|
||||
}
|
||||
136.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
TrickPlayModel.fromList(it)
|
||||
SubtitleTrack.fromList(it)
|
||||
}
|
||||
}
|
||||
137.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
StartResult.fromList(it)
|
||||
Chapter.fromList(it)
|
||||
}
|
||||
}
|
||||
138.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
TrickPlayModel.fromList(it)
|
||||
}
|
||||
}
|
||||
139.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
StartResult.fromList(it)
|
||||
}
|
||||
}
|
||||
140.toByte() -> {
|
||||
return (readValue(buffer) as? List<Any?>)?.let {
|
||||
PlaybackState.fromList(it)
|
||||
}
|
||||
|
|
@ -535,46 +591,54 @@ private open class VideoPlayerHelperPigeonCodec : StandardMessageCodec() {
|
|||
}
|
||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||
when (value) {
|
||||
is MediaSegmentType -> {
|
||||
is PlaybackType -> {
|
||||
stream.write(129)
|
||||
writeValue(stream, value.raw)
|
||||
}
|
||||
is SimpleItemModel -> {
|
||||
is MediaSegmentType -> {
|
||||
stream.write(130)
|
||||
writeValue(stream, value.toList())
|
||||
writeValue(stream, value.raw)
|
||||
}
|
||||
is PlayableData -> {
|
||||
is SimpleItemModel -> {
|
||||
stream.write(131)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is MediaSegment -> {
|
||||
is MediaInfo -> {
|
||||
stream.write(132)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is AudioTrack -> {
|
||||
is PlayableData -> {
|
||||
stream.write(133)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is SubtitleTrack -> {
|
||||
is MediaSegment -> {
|
||||
stream.write(134)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is Chapter -> {
|
||||
is AudioTrack -> {
|
||||
stream.write(135)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is TrickPlayModel -> {
|
||||
is SubtitleTrack -> {
|
||||
stream.write(136)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is StartResult -> {
|
||||
is Chapter -> {
|
||||
stream.write(137)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is PlaybackState -> {
|
||||
is TrickPlayModel -> {
|
||||
stream.write(138)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is StartResult -> {
|
||||
stream.write(139)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
is PlaybackState -> {
|
||||
stream.write(140)
|
||||
writeValue(stream, value.toList())
|
||||
}
|
||||
else -> super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
|
@ -22,6 +23,7 @@ import androidx.compose.foundation.layout.height
|
|||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentSize
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -65,6 +67,7 @@ import androidx.compose.ui.util.fastCoerceIn
|
|||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import kotlinx.coroutines.delay
|
||||
import nl.jknaapen.fladder.objects.VideoPlayerObject
|
||||
import nl.jknaapen.fladder.utility.capitalize
|
||||
import nl.jknaapen.fladder.utility.formatTime
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
|
@ -117,7 +120,7 @@ internal fun ProgressBar(
|
|||
trickPlayModel = playbackData?.trickPlayModel
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val progressBarTopLabel = listOf(
|
||||
playableData?.currentItem?.subTitle,
|
||||
|
|
@ -134,6 +137,11 @@ internal fun ProgressBar(
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
Videolabel(playableData?.mediaInfo?.playbackType?.name?.capitalize)
|
||||
Videolabel(playableData?.mediaInfo?.videoInformation)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
|
|
@ -177,7 +185,30 @@ internal fun ProgressBar(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Videolabel(value: String?) {
|
||||
if (value.isNullOrBlank()) return
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.wrapContentSize()
|
||||
.padding(horizontal = 6.dp, vertical = 4.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
value,
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -476,11 +476,15 @@ internal fun RowScope.RightButtons(
|
|||
showAudioDialog: MutableState<Boolean>,
|
||||
showSubDialog: MutableState<Boolean>
|
||||
) {
|
||||
val hasSubtitles by VideoPlayerObject.hasSubtracks.collectAsState(false)
|
||||
val hasAudioTracks by VideoPlayerObject.hasAudioTracks.collectAsState(false)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.End)
|
||||
) {
|
||||
CustomButton(
|
||||
enabled = hasAudioTracks,
|
||||
onClick = {
|
||||
showAudioDialog.value = true
|
||||
},
|
||||
|
|
@ -491,6 +495,7 @@ internal fun RowScope.RightButtons(
|
|||
)
|
||||
}
|
||||
CustomButton(
|
||||
enabled = hasSubtitles,
|
||||
onClick = {
|
||||
showSubDialog.value = true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ fun AudioPicker(
|
|||
val audioTracks by VideoPlayerObject.audioTracks.collectAsState(listOf())
|
||||
val internalAudioTracks by VideoPlayerObject.exoAudioTracks
|
||||
|
||||
if (internalAudioTracks.isEmpty()) return
|
||||
|
||||
val focusOffTrack = remember { FocusRequester() }
|
||||
val focusRequesters = remember(internalAudioTracks) {
|
||||
internalAudioTracks.associateWith { FocusRequester() }
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ fun SubtitlePicker(
|
|||
val subTitles by VideoPlayerObject.subtitleTracks.collectAsState(listOf())
|
||||
val internalSubTracks by VideoPlayerObject.exoSubTracks
|
||||
|
||||
if (internalSubTracks.isEmpty()) return
|
||||
|
||||
val focusOffTrack = remember { FocusRequester() }
|
||||
|
||||
val focusRequesters = remember(internalSubTracks) {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,10 @@ object VideoPlayerObject {
|
|||
val subtitleTracks = implementation.playbackData.map { it?.subtitleTracks ?: listOf() }
|
||||
val audioTracks = implementation.playbackData.map { it?.audioTracks ?: listOf() }
|
||||
|
||||
val hasSubtracks = subtitleTracks.map { it.isNotEmpty() && exoSubTracks.value.isNotEmpty() }
|
||||
val hasAudioTracks = audioTracks.map { it.isNotEmpty() && exoAudioTracks.value.isNotEmpty() }
|
||||
|
||||
|
||||
fun setPlaybackState(state: PlaybackState) {
|
||||
_currentState.value = state
|
||||
videoPlayerListener?.onPlaybackStateChanged(
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
|
|
@ -115,11 +114,9 @@ fun Modifier.visible(
|
|||
alpha = alphaAnimated
|
||||
}
|
||||
.then(
|
||||
if (!visible) {
|
||||
//Collapse composable to disable input blocking
|
||||
if (alphaAnimated == 0f) {
|
||||
Modifier
|
||||
.size(0.dp)
|
||||
.clipToBounds()
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package nl.jknaapen.fladder.utility
|
|||
|
||||
import AudioTrack
|
||||
import Chapter
|
||||
import MediaInfo
|
||||
import PlayableData
|
||||
import PlaybackType
|
||||
import SimpleItemModel
|
||||
import SubtitleTrack
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
|
@ -99,5 +101,9 @@ val testPlaybackData = PlayableData(
|
|||
primaryPoster = "https://external-content.duckduckgo.com/iu/?u=https%3A%2F%2Fi.ytimg.com%2Fvi%2Faqz-KE-bpKQ%2Fmaxresdefault.jpg&f=1&nofb=1&ipt=4e375598bf8cc78e681ee62de9111dea32b85972ae756e40a1eddac01aa79f80"
|
||||
),
|
||||
segments = listOf(),
|
||||
mediaInfo = MediaInfo(
|
||||
videoInformation = "SDR HD",
|
||||
playbackType = PlaybackType.DIRECT,
|
||||
),
|
||||
url = "https://github.com/ietf-wg-cellar/matroska-test-files/raw/refs/heads/master/test_files/test5.mkv",
|
||||
)
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package nl.jknaapen.fladder.utility
|
||||
|
||||
val String.capitalize: String
|
||||
get() = this.mapIndexed { index, char -> if (index == 0) char.uppercase() else char.lowercase() }
|
||||
.joinToString(separator = "")
|
||||
Loading…
Add table
Add a link
Reference in a new issue