feat: Added playbackinformation to native player ui

This commit is contained in:
PartyDonut 2025-10-17 10:32:06 +02:00
parent 37496f87e3
commit 25304d0a5b
14 changed files with 266 additions and 50 deletions

View file

@ -80,6 +80,18 @@ class FlutterError (
val details: Any? = null val details: Any? = null
) : Throwable() ) : 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) { enum class MediaSegmentType(val raw: Int) {
COMMERCIAL(0), COMMERCIAL(0),
PREVIEW(1), PREVIEW(1),
@ -137,6 +149,37 @@ data class SimpleItemModel (
override fun hashCode(): Int = toList().hashCode() 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. */ /** Generated class from Pigeon that represents data sent in messages. */
data class PlayableData ( data class PlayableData (
val currentItem: SimpleItemModel, val currentItem: SimpleItemModel,
@ -151,6 +194,7 @@ data class PlayableData (
val segments: List<MediaSegment>, val segments: List<MediaSegment>,
val previousVideo: SimpleItemModel? = null, val previousVideo: SimpleItemModel? = null,
val nextVideo: SimpleItemModel? = null, val nextVideo: SimpleItemModel? = null,
val mediaInfo: MediaInfo,
val url: String val url: String
) )
{ {
@ -168,8 +212,9 @@ data class PlayableData (
val segments = pigeonVar_list[9] as List<MediaSegment> val segments = pigeonVar_list[9] as List<MediaSegment>
val previousVideo = pigeonVar_list[10] as SimpleItemModel? val previousVideo = pigeonVar_list[10] as SimpleItemModel?
val nextVideo = pigeonVar_list[11] as SimpleItemModel? val nextVideo = pigeonVar_list[11] as SimpleItemModel?
val url = pigeonVar_list[12] as String val mediaInfo = pigeonVar_list[12] as MediaInfo
return PlayableData(currentItem, description, startPosition, defaultAudioTrack, audioTracks, defaultSubtrack, subtitleTracks, trickPlayModel, chapters, segments, previousVideo, nextVideo, url) 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?> { fun toList(): List<Any?> {
@ -186,6 +231,7 @@ data class PlayableData (
segments, segments,
previousVideo, previousVideo,
nextVideo, nextVideo,
mediaInfo,
url, url,
) )
} }
@ -482,50 +528,60 @@ private open class VideoPlayerHelperPigeonCodec : StandardMessageCodec() {
return when (type) { return when (type) {
129.toByte() -> { 129.toByte() -> {
return (readValue(buffer) as Long?)?.let { return (readValue(buffer) as Long?)?.let {
MediaSegmentType.ofRaw(it.toInt()) PlaybackType.ofRaw(it.toInt())
} }
} }
130.toByte() -> { 130.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { return (readValue(buffer) as Long?)?.let {
SimpleItemModel.fromList(it) MediaSegmentType.ofRaw(it.toInt())
} }
} }
131.toByte() -> { 131.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { return (readValue(buffer) as? List<Any?>)?.let {
PlayableData.fromList(it) SimpleItemModel.fromList(it)
} }
} }
132.toByte() -> { 132.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { return (readValue(buffer) as? List<Any?>)?.let {
MediaSegment.fromList(it) MediaInfo.fromList(it)
} }
} }
133.toByte() -> { 133.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { return (readValue(buffer) as? List<Any?>)?.let {
AudioTrack.fromList(it) PlayableData.fromList(it)
} }
} }
134.toByte() -> { 134.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { return (readValue(buffer) as? List<Any?>)?.let {
SubtitleTrack.fromList(it) MediaSegment.fromList(it)
} }
} }
135.toByte() -> { 135.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { return (readValue(buffer) as? List<Any?>)?.let {
Chapter.fromList(it) AudioTrack.fromList(it)
} }
} }
136.toByte() -> { 136.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { return (readValue(buffer) as? List<Any?>)?.let {
TrickPlayModel.fromList(it) SubtitleTrack.fromList(it)
} }
} }
137.toByte() -> { 137.toByte() -> {
return (readValue(buffer) as? List<Any?>)?.let { return (readValue(buffer) as? List<Any?>)?.let {
StartResult.fromList(it) Chapter.fromList(it)
} }
} }
138.toByte() -> { 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 { return (readValue(buffer) as? List<Any?>)?.let {
PlaybackState.fromList(it) PlaybackState.fromList(it)
} }
@ -535,46 +591,54 @@ private open class VideoPlayerHelperPigeonCodec : StandardMessageCodec() {
} }
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) { when (value) {
is MediaSegmentType -> { is PlaybackType -> {
stream.write(129) stream.write(129)
writeValue(stream, value.raw) writeValue(stream, value.raw)
} }
is SimpleItemModel -> { is MediaSegmentType -> {
stream.write(130) stream.write(130)
writeValue(stream, value.toList()) writeValue(stream, value.raw)
} }
is PlayableData -> { is SimpleItemModel -> {
stream.write(131) stream.write(131)
writeValue(stream, value.toList()) writeValue(stream, value.toList())
} }
is MediaSegment -> { is MediaInfo -> {
stream.write(132) stream.write(132)
writeValue(stream, value.toList()) writeValue(stream, value.toList())
} }
is AudioTrack -> { is PlayableData -> {
stream.write(133) stream.write(133)
writeValue(stream, value.toList()) writeValue(stream, value.toList())
} }
is SubtitleTrack -> { is MediaSegment -> {
stream.write(134) stream.write(134)
writeValue(stream, value.toList()) writeValue(stream, value.toList())
} }
is Chapter -> { is AudioTrack -> {
stream.write(135) stream.write(135)
writeValue(stream, value.toList()) writeValue(stream, value.toList())
} }
is TrickPlayModel -> { is SubtitleTrack -> {
stream.write(136) stream.write(136)
writeValue(stream, value.toList()) writeValue(stream, value.toList())
} }
is StartResult -> { is Chapter -> {
stream.write(137) stream.write(137)
writeValue(stream, value.toList()) writeValue(stream, value.toList())
} }
is PlaybackState -> { is TrickPlayModel -> {
stream.write(138) stream.write(138)
writeValue(stream, value.toList()) 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) else -> super.writeValue(stream, value)
} }
} }

View file

@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth 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.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -65,6 +67,7 @@ import androidx.compose.ui.util.fastCoerceIn
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import nl.jknaapen.fladder.objects.VideoPlayerObject import nl.jknaapen.fladder.objects.VideoPlayerObject
import nl.jknaapen.fladder.utility.capitalize
import nl.jknaapen.fladder.utility.formatTime import nl.jknaapen.fladder.utility.formatTime
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -117,7 +120,7 @@ internal fun ProgressBar(
trickPlayModel = playbackData?.trickPlayModel trickPlayModel = playbackData?.trickPlayModel
) )
Row( Row(
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
val progressBarTopLabel = listOf( val progressBarTopLabel = listOf(
playableData?.currentItem?.subTitle, 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( Row(
horizontalArrangement = Arrangement.spacedBy( 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 @Composable

View file

@ -476,11 +476,15 @@ internal fun RowScope.RightButtons(
showAudioDialog: MutableState<Boolean>, showAudioDialog: MutableState<Boolean>,
showSubDialog: MutableState<Boolean> showSubDialog: MutableState<Boolean>
) { ) {
val hasSubtitles by VideoPlayerObject.hasSubtracks.collectAsState(false)
val hasAudioTracks by VideoPlayerObject.hasAudioTracks.collectAsState(false)
Row( Row(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.End) horizontalArrangement = Arrangement.spacedBy(12.dp, alignment = Alignment.End)
) { ) {
CustomButton( CustomButton(
enabled = hasAudioTracks,
onClick = { onClick = {
showAudioDialog.value = true showAudioDialog.value = true
}, },
@ -491,6 +495,7 @@ internal fun RowScope.RightButtons(
) )
} }
CustomButton( CustomButton(
enabled = hasSubtitles,
onClick = { onClick = {
showSubDialog.value = true showSubDialog.value = true
}, },

View file

@ -31,6 +31,8 @@ fun AudioPicker(
val audioTracks by VideoPlayerObject.audioTracks.collectAsState(listOf()) val audioTracks by VideoPlayerObject.audioTracks.collectAsState(listOf())
val internalAudioTracks by VideoPlayerObject.exoAudioTracks val internalAudioTracks by VideoPlayerObject.exoAudioTracks
if (internalAudioTracks.isEmpty()) return
val focusOffTrack = remember { FocusRequester() } val focusOffTrack = remember { FocusRequester() }
val focusRequesters = remember(internalAudioTracks) { val focusRequesters = remember(internalAudioTracks) {
internalAudioTracks.associateWith { FocusRequester() } internalAudioTracks.associateWith { FocusRequester() }

View file

@ -32,6 +32,8 @@ fun SubtitlePicker(
val subTitles by VideoPlayerObject.subtitleTracks.collectAsState(listOf()) val subTitles by VideoPlayerObject.subtitleTracks.collectAsState(listOf())
val internalSubTracks by VideoPlayerObject.exoSubTracks val internalSubTracks by VideoPlayerObject.exoSubTracks
if (internalSubTracks.isEmpty()) return
val focusOffTrack = remember { FocusRequester() } val focusOffTrack = remember { FocusRequester() }
val focusRequesters = remember(internalSubTracks) { val focusRequesters = remember(internalSubTracks) {

View file

@ -72,6 +72,10 @@ object VideoPlayerObject {
val subtitleTracks = implementation.playbackData.map { it?.subtitleTracks ?: listOf() } val subtitleTracks = implementation.playbackData.map { it?.subtitleTracks ?: listOf() }
val audioTracks = implementation.playbackData.map { it?.audioTracks ?: 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) { fun setPlaybackState(state: PlaybackState) {
_currentState.value = state _currentState.value = state
videoPlayerListener?.onPlaybackStateChanged( videoPlayerListener?.onPlaybackStateChanged(

View file

@ -14,7 +14,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip 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.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
@ -115,11 +114,9 @@ fun Modifier.visible(
alpha = alphaAnimated alpha = alphaAnimated
} }
.then( .then(
if (!visible) { if (alphaAnimated == 0f) {
//Collapse composable to disable input blocking
Modifier Modifier
.size(0.dp) .size(0.dp)
.clipToBounds()
} else { } else {
Modifier Modifier
} }

View file

@ -2,7 +2,9 @@ package nl.jknaapen.fladder.utility
import AudioTrack import AudioTrack
import Chapter import Chapter
import MediaInfo
import PlayableData import PlayableData
import PlaybackType
import SimpleItemModel import SimpleItemModel
import SubtitleTrack import SubtitleTrack
import kotlin.time.Duration.Companion.seconds 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" 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(), 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", url = "https://github.com/ietf-wg-cellar/matroska-test-files/raw/refs/heads/master/test_files/test5.mkv",
) )

View file

@ -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 = "")

View file

@ -69,6 +69,8 @@ class MediaStreamsModel {
return "${stream.width}x${stream.height}"; return "${stream.width}x${stream.height}";
} }
String? get mediaInfoTag => '${displayProfile?.value} ${resolution?.value}';
Widget? audioIcon( Widget? audioIcon(
BuildContext context, BuildContext context,
Function()? onTap, Function()? onTap,

View file

@ -443,7 +443,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text( child: Text(
'${item.streamModel?.displayProfile?.value} ${item.streamModel?.resolution?.value}', item.streamModel?.mediaInfoTag ?? "",
), ),
), ),
), ),

View file

@ -39,6 +39,12 @@ bool _deepEquals(Object? a, Object? b) {
} }
enum PlaybackType {
direct,
transcoded,
offline,
}
enum MediaSegmentType { enum MediaSegmentType {
commercial, commercial,
preview, preview,
@ -113,6 +119,52 @@ class SimpleItemModel {
; ;
} }
class MediaInfo {
MediaInfo({
required this.playbackType,
required this.videoInformation,
});
PlaybackType playbackType;
String videoInformation;
List<Object?> _toList() {
return <Object?>[
playbackType,
videoInformation,
];
}
Object encode() {
return _toList(); }
static MediaInfo decode(Object result) {
result as List<Object?>;
return MediaInfo(
playbackType: result[0]! as PlaybackType,
videoInformation: result[1]! as String,
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! MediaInfo || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(encode(), other.encode());
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => Object.hashAll(_toList())
;
}
class PlayableData { class PlayableData {
PlayableData({ PlayableData({
required this.currentItem, required this.currentItem,
@ -127,6 +179,7 @@ class PlayableData {
required this.segments, required this.segments,
this.previousVideo, this.previousVideo,
this.nextVideo, this.nextVideo,
required this.mediaInfo,
required this.url, required this.url,
}); });
@ -154,6 +207,8 @@ class PlayableData {
SimpleItemModel? nextVideo; SimpleItemModel? nextVideo;
MediaInfo mediaInfo;
String url; String url;
List<Object?> _toList() { List<Object?> _toList() {
@ -170,6 +225,7 @@ class PlayableData {
segments, segments,
previousVideo, previousVideo,
nextVideo, nextVideo,
mediaInfo,
url, url,
]; ];
} }
@ -192,7 +248,8 @@ class PlayableData {
segments: (result[9] as List<Object?>?)!.cast<MediaSegment>(), segments: (result[9] as List<Object?>?)!.cast<MediaSegment>(),
previousVideo: result[10] as SimpleItemModel?, previousVideo: result[10] as SimpleItemModel?,
nextVideo: result[11] as SimpleItemModel?, nextVideo: result[11] as SimpleItemModel?,
url: result[12]! as String, mediaInfo: result[12]! as MediaInfo,
url: result[13]! as String,
); );
} }
@ -644,36 +701,42 @@ class _PigeonCodec extends StandardMessageCodec {
if (value is int) { if (value is int) {
buffer.putUint8(4); buffer.putUint8(4);
buffer.putInt64(value); buffer.putInt64(value);
} else if (value is MediaSegmentType) { } else if (value is PlaybackType) {
buffer.putUint8(129); buffer.putUint8(129);
writeValue(buffer, value.index); writeValue(buffer, value.index);
} else if (value is SimpleItemModel) { } else if (value is MediaSegmentType) {
buffer.putUint8(130); buffer.putUint8(130);
writeValue(buffer, value.encode()); writeValue(buffer, value.index);
} else if (value is PlayableData) { } else if (value is SimpleItemModel) {
buffer.putUint8(131); buffer.putUint8(131);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else if (value is MediaSegment) { } else if (value is MediaInfo) {
buffer.putUint8(132); buffer.putUint8(132);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else if (value is AudioTrack) { } else if (value is PlayableData) {
buffer.putUint8(133); buffer.putUint8(133);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else if (value is SubtitleTrack) { } else if (value is MediaSegment) {
buffer.putUint8(134); buffer.putUint8(134);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else if (value is Chapter) { } else if (value is AudioTrack) {
buffer.putUint8(135); buffer.putUint8(135);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else if (value is TrickPlayModel) { } else if (value is SubtitleTrack) {
buffer.putUint8(136); buffer.putUint8(136);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else if (value is StartResult) { } else if (value is Chapter) {
buffer.putUint8(137); buffer.putUint8(137);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else if (value is PlaybackState) { } else if (value is TrickPlayModel) {
buffer.putUint8(138); buffer.putUint8(138);
writeValue(buffer, value.encode()); writeValue(buffer, value.encode());
} else if (value is StartResult) {
buffer.putUint8(139);
writeValue(buffer, value.encode());
} else if (value is PlaybackState) {
buffer.putUint8(140);
writeValue(buffer, value.encode());
} else { } else {
super.writeValue(buffer, value); super.writeValue(buffer, value);
} }
@ -684,24 +747,29 @@ class _PigeonCodec extends StandardMessageCodec {
switch (type) { switch (type) {
case 129: case 129:
final int? value = readValue(buffer) as int?; final int? value = readValue(buffer) as int?;
return value == null ? null : MediaSegmentType.values[value]; return value == null ? null : PlaybackType.values[value];
case 130: case 130:
return SimpleItemModel.decode(readValue(buffer)!); final int? value = readValue(buffer) as int?;
return value == null ? null : MediaSegmentType.values[value];
case 131: case 131:
return PlayableData.decode(readValue(buffer)!); return SimpleItemModel.decode(readValue(buffer)!);
case 132: case 132:
return MediaSegment.decode(readValue(buffer)!); return MediaInfo.decode(readValue(buffer)!);
case 133: case 133:
return AudioTrack.decode(readValue(buffer)!); return PlayableData.decode(readValue(buffer)!);
case 134: case 134:
return SubtitleTrack.decode(readValue(buffer)!); return MediaSegment.decode(readValue(buffer)!);
case 135: case 135:
return Chapter.decode(readValue(buffer)!); return AudioTrack.decode(readValue(buffer)!);
case 136: case 136:
return TrickPlayModel.decode(readValue(buffer)!); return SubtitleTrack.decode(readValue(buffer)!);
case 137: case 137:
return StartResult.decode(readValue(buffer)!); return Chapter.decode(readValue(buffer)!);
case 138: case 138:
return TrickPlayModel.decode(readValue(buffer)!);
case 139:
return StartResult.decode(readValue(buffer)!);
case 140:
return PlaybackState.decode(readValue(buffer)!); return PlaybackState.decode(readValue(buffer)!);
default: default:
return super.readValueOfType(type, buffer); return super.readValueOfType(type, buffer);

View file

@ -3,7 +3,10 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/playback/direct_playback_model.dart';
import 'package:fladder/models/playback/offline_playback_model.dart';
import 'package:fladder/models/playback/playback_model.dart'; import 'package:fladder/models/playback/playback_model.dart';
import 'package:fladder/models/playback/transcode_playback_model.dart';
import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/models/settings/video_player_settings.dart';
import 'package:fladder/src/video_player_helper.g.dart'; import 'package:fladder/src/video_player_helper.g.dart';
import 'package:fladder/wrappers/players/base_player.dart'; import 'package:fladder/wrappers/players/base_player.dart';
@ -157,6 +160,15 @@ class NativePlayer extends BasePlayer implements VideoPlayerListenerCallback {
?.map((e) => Chapter(name: e.name, url: e.imageUrl, time: e.startPosition.inMilliseconds)) ?.map((e) => Chapter(name: e.name, url: e.imageUrl, time: e.startPosition.inMilliseconds))
.toList() ?? .toList() ??
[], [],
mediaInfo: MediaInfo(
playbackType: switch (model) {
DirectPlaybackModel() => PlaybackType.direct,
OfflinePlaybackModel() => PlaybackType.offline,
TranscodePlaybackModel() => PlaybackType.transcoded,
_ => PlaybackType.direct,
},
videoInformation: model.item.streamModel?.mediaInfoTag ?? " ",
),
url: model.media?.url ?? "", url: model.media?.url ?? "",
); );
player.sendPlayableModel(playableData); player.sendPlayableModel(playableData);

View file

@ -27,6 +27,22 @@ class SimpleItemModel {
}); });
} }
enum PlaybackType {
direct,
transcoded,
offline,
}
class MediaInfo {
final PlaybackType playbackType;
final String videoInformation;
const MediaInfo({
required this.playbackType,
required this.videoInformation,
});
}
class PlayableData { class PlayableData {
final SimpleItemModel currentItem; final SimpleItemModel currentItem;
final String description; final String description;
@ -40,6 +56,7 @@ class PlayableData {
final List<MediaSegment> segments; final List<MediaSegment> segments;
final SimpleItemModel? previousVideo; final SimpleItemModel? previousVideo;
final SimpleItemModel? nextVideo; final SimpleItemModel? nextVideo;
final MediaInfo mediaInfo;
final String url; final String url;
PlayableData({ PlayableData({
@ -55,6 +72,7 @@ class PlayableData {
this.segments = const [], this.segments = const [],
this.previousVideo, this.previousVideo,
this.nextVideo, this.nextVideo,
required this.mediaInfo,
required this.url, required this.url,
}); });
} }