mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
444 lines
14 KiB
Dart
444 lines
14 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:collection/collection.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
|
|
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
|
|
import 'package:fladder/providers/user_provider.dart';
|
|
import 'package:fladder/util/localization_helper.dart';
|
|
import 'package:fladder/util/video_properties.dart';
|
|
|
|
class MediaStreamsModel {
|
|
final int? versionStreamIndex;
|
|
final int? defaultAudioStreamIndex;
|
|
final int? defaultSubStreamIndex;
|
|
final List<VersionStreamModel> versionStreams;
|
|
MediaStreamsModel({
|
|
this.versionStreamIndex,
|
|
this.defaultAudioStreamIndex,
|
|
this.defaultSubStreamIndex,
|
|
required this.versionStreams,
|
|
});
|
|
|
|
VersionStreamModel? get currentVersionStream => versionStreams.elementAtOrNull(versionStreamIndex ?? 0);
|
|
|
|
List<VideoStreamModel> get videoStreams => currentVersionStream?.videoStreams ?? [];
|
|
List<AudioStreamModel> get audioStreams => currentVersionStream?.audioStreams ?? [];
|
|
List<SubStreamModel> get subStreams => currentVersionStream?.subStreams ?? [];
|
|
|
|
bool get isNull {
|
|
return defaultAudioStreamIndex == null ||
|
|
defaultSubStreamIndex == null ||
|
|
audioStreams.isEmpty ||
|
|
subStreams.isEmpty;
|
|
}
|
|
|
|
bool get isNotEmpty {
|
|
return audioStreams.isNotEmpty && subStreams.isNotEmpty;
|
|
}
|
|
|
|
AudioStreamModel? get currentAudioStream {
|
|
if (defaultAudioStreamIndex == -1 || defaultAudioStreamIndex == null) {
|
|
return AudioStreamModel.no();
|
|
}
|
|
return audioStreams.firstWhereOrNull((element) => element.index == defaultAudioStreamIndex) ??
|
|
audioStreams.firstOrNull;
|
|
}
|
|
|
|
SubStreamModel? get currentSubStream {
|
|
if (defaultSubStreamIndex == -1 || defaultSubStreamIndex == null) {
|
|
return SubStreamModel.no();
|
|
}
|
|
return subStreams.firstWhereOrNull((element) => element.index == defaultSubStreamIndex) ?? subStreams.firstOrNull;
|
|
}
|
|
|
|
DisplayProfile? get displayProfile {
|
|
return DisplayProfile.fromVideoStreams(videoStreams);
|
|
}
|
|
|
|
Resolution? get resolution {
|
|
return Resolution.fromVideoStream(videoStreams.firstOrNull);
|
|
}
|
|
|
|
String? get resolutionText {
|
|
final stream = videoStreams.firstOrNull;
|
|
if (stream == null) return null;
|
|
return "${stream.width}x${stream.height}";
|
|
}
|
|
|
|
Widget? audioIcon(
|
|
BuildContext context,
|
|
Function()? onTap,
|
|
) {
|
|
final audioStream = audioStreams.firstWhereOrNull((element) => element.isDefault) ?? audioStreams.firstOrNull;
|
|
if (audioStream == null) return null;
|
|
return DefaultVideoInformationBox(
|
|
onTap: onTap,
|
|
child: Text(
|
|
audioStream.title,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget subtitleIcon(
|
|
BuildContext context,
|
|
Function()? onTap,
|
|
) {
|
|
return DefaultVideoInformationBox(
|
|
onTap: onTap,
|
|
child: Icon(
|
|
subStreams.isNotEmpty ? Icons.subtitles_rounded : Icons.subtitles_off_outlined,
|
|
),
|
|
);
|
|
}
|
|
|
|
static MediaStreamsModel fromMediaStreamsList(
|
|
List<dto.MediaSourceInfo>? mediaSource,
|
|
Ref ref,
|
|
) {
|
|
return MediaStreamsModel(
|
|
defaultAudioStreamIndex: mediaSource?.firstOrNull?.defaultAudioStreamIndex,
|
|
defaultSubStreamIndex: mediaSource?.firstOrNull?.defaultSubtitleStreamIndex,
|
|
versionStreams: mediaSource?.mapIndexed(
|
|
(index, element) {
|
|
final streams = element.mediaStreams ?? [];
|
|
return VersionStreamModel(
|
|
name: element.name ?? "",
|
|
index: index,
|
|
id: element.id,
|
|
defaultAudioStreamIndex: element.defaultAudioStreamIndex,
|
|
defaultSubStreamIndex: element.defaultSubtitleStreamIndex,
|
|
videoStreams: streams
|
|
.where((element) => element.type == dto.MediaStreamType.video)
|
|
.map(
|
|
(e) => VideoStreamModel.fromMediaStream(e),
|
|
)
|
|
.sortByExternal(),
|
|
audioStreams: streams
|
|
.where((element) => element.type == dto.MediaStreamType.audio)
|
|
.map(
|
|
(e) => AudioStreamModel.fromMediaStream(e),
|
|
)
|
|
.sortByExternal(),
|
|
subStreams: streams
|
|
.where((element) => element.type == dto.MediaStreamType.subtitle)
|
|
.map(
|
|
(sub) => SubStreamModel.fromMediaStream(sub, ref),
|
|
)
|
|
.sortByExternal());
|
|
},
|
|
).toList() ??
|
|
[]);
|
|
}
|
|
|
|
MediaStreamsModel copyWith({
|
|
int? versionStreamIndex,
|
|
int? defaultAudioStreamIndex,
|
|
int? defaultSubStreamIndex,
|
|
List<VersionStreamModel>? versionStreams,
|
|
}) {
|
|
final streamIndexChanged = versionStreamIndex != this.versionStreamIndex && versionStreamIndex != null;
|
|
final currentVersionStreams = versionStreams ?? this.versionStreams;
|
|
return MediaStreamsModel(
|
|
versionStreamIndex: versionStreamIndex ?? this.versionStreamIndex,
|
|
defaultAudioStreamIndex: streamIndexChanged
|
|
? currentVersionStreams.elementAtOrNull(versionStreamIndex)?.defaultAudioStreamIndex
|
|
: defaultAudioStreamIndex ?? this.defaultAudioStreamIndex,
|
|
defaultSubStreamIndex: streamIndexChanged
|
|
? currentVersionStreams.elementAtOrNull(versionStreamIndex)?.defaultSubStreamIndex
|
|
: defaultSubStreamIndex ?? this.defaultSubStreamIndex,
|
|
versionStreams: versionStreams ?? this.versionStreams,
|
|
);
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return 'MediaStreamsModel(defaultAudioStreamIndex: $defaultAudioStreamIndex, defaultSubStreamIndex: $defaultSubStreamIndex, videoStreams: $videoStreams, audioStreams: $audioStreams, subStreams: $subStreams)';
|
|
}
|
|
}
|
|
|
|
class StreamModel {
|
|
final String name;
|
|
final String codec;
|
|
final bool isDefault;
|
|
final bool isExternal;
|
|
final int index;
|
|
StreamModel({
|
|
required this.name,
|
|
required this.codec,
|
|
required this.isDefault,
|
|
required this.isExternal,
|
|
required this.index,
|
|
});
|
|
}
|
|
|
|
class AudioAndSubStreamModel extends StreamModel {
|
|
final String language;
|
|
final String displayTitle;
|
|
AudioAndSubStreamModel({
|
|
required this.displayTitle,
|
|
required super.name,
|
|
required super.codec,
|
|
required super.isDefault,
|
|
required super.isExternal,
|
|
required super.index,
|
|
required this.language,
|
|
});
|
|
}
|
|
|
|
class VersionStreamModel {
|
|
final String name;
|
|
final int index;
|
|
final String? id;
|
|
final int? defaultAudioStreamIndex;
|
|
final int? defaultSubStreamIndex;
|
|
final List<VideoStreamModel> videoStreams;
|
|
final List<AudioStreamModel> audioStreams;
|
|
final List<SubStreamModel> subStreams;
|
|
|
|
VersionStreamModel({
|
|
required this.name,
|
|
required this.index,
|
|
this.id,
|
|
required this.defaultAudioStreamIndex,
|
|
required this.defaultSubStreamIndex,
|
|
required this.videoStreams,
|
|
required this.audioStreams,
|
|
required this.subStreams,
|
|
});
|
|
}
|
|
|
|
class VideoStreamModel extends StreamModel {
|
|
final int width;
|
|
final int height;
|
|
final int? bitRate;
|
|
final double frameRate;
|
|
final String? videoDoViTitle;
|
|
final VideoRangeType? videoRangeType;
|
|
VideoStreamModel({
|
|
required super.name,
|
|
required super.codec,
|
|
required super.isDefault,
|
|
required super.isExternal,
|
|
required super.index,
|
|
required this.videoDoViTitle,
|
|
required this.videoRangeType,
|
|
required this.bitRate,
|
|
required this.width,
|
|
required this.height,
|
|
required this.frameRate,
|
|
});
|
|
|
|
factory VideoStreamModel.fromMediaStream(dto.MediaStream stream) {
|
|
return VideoStreamModel(
|
|
name: stream.title ?? "",
|
|
isDefault: stream.isDefault ?? false,
|
|
codec: stream.codec ?? "",
|
|
videoDoViTitle: stream.videoDoViTitle,
|
|
bitRate: stream.bitRate,
|
|
videoRangeType: stream.videoRangeType,
|
|
width: stream.width ?? 0,
|
|
height: stream.height ?? 0,
|
|
frameRate: stream.realFrameRate ?? 24,
|
|
isExternal: stream.isExternal ?? false,
|
|
index: stream.index ?? -1,
|
|
);
|
|
}
|
|
String get prettyName {
|
|
return "${Resolution.fromVideoStream(this)?.value} - ${DisplayProfile.fromVideoStream(this).value} - (${codec.toUpperCase()})";
|
|
}
|
|
|
|
@override
|
|
String toString() {
|
|
return 'VideoStreamModel(width: $width, height: $height, frameRate: $frameRate, videoDoViTitle: $videoDoViTitle, videoRangeType: $videoRangeType)';
|
|
}
|
|
}
|
|
|
|
//Instead of using sortBy(a.isExternal etc..) this one seems to be more consistent for some reason
|
|
extension SortByExternalExtension<T extends StreamModel> on Iterable<T> {
|
|
List<T> sortByExternal() {
|
|
return [...where((element) => !element.isExternal), ...where((element) => element.isExternal)];
|
|
}
|
|
}
|
|
|
|
class AudioStreamModel extends AudioAndSubStreamModel {
|
|
final String channelLayout;
|
|
|
|
AudioStreamModel({
|
|
required super.displayTitle,
|
|
required super.name,
|
|
required super.codec,
|
|
required super.isDefault,
|
|
required super.isExternal,
|
|
required super.index,
|
|
required super.language,
|
|
required this.channelLayout,
|
|
});
|
|
|
|
factory AudioStreamModel.fromMediaStream(dto.MediaStream stream) {
|
|
return AudioStreamModel(
|
|
displayTitle: stream.displayTitle ?? "",
|
|
name: stream.title ?? "",
|
|
isDefault: stream.isDefault ?? false,
|
|
codec: stream.codec ?? "",
|
|
language: stream.language ?? "Unknown",
|
|
channelLayout: stream.channelLayout ?? "",
|
|
isExternal: stream.isExternal ?? false,
|
|
index: stream.index ?? -1,
|
|
);
|
|
}
|
|
|
|
String label(BuildContext context) {
|
|
if (index == -1) {
|
|
return context.localized.off;
|
|
} else {
|
|
return displayTitle;
|
|
}
|
|
}
|
|
|
|
String get title =>
|
|
[name, language, codec, channelLayout].nonNulls.where((element) => element.isNotEmpty).join(' - ');
|
|
|
|
AudioStreamModel.no({
|
|
super.name = 'Off',
|
|
super.displayTitle = 'Off',
|
|
super.language = '',
|
|
super.codec = '',
|
|
this.channelLayout = '',
|
|
super.isDefault = false,
|
|
super.isExternal = false,
|
|
super.index = -1,
|
|
});
|
|
}
|
|
|
|
class SubStreamModel extends AudioAndSubStreamModel {
|
|
String id;
|
|
String title;
|
|
String? url;
|
|
bool supportsExternalStream;
|
|
SubStreamModel({
|
|
required super.name,
|
|
required this.id,
|
|
required this.title,
|
|
required super.displayTitle,
|
|
required super.language,
|
|
this.url,
|
|
required super.codec,
|
|
required super.isDefault,
|
|
required super.isExternal,
|
|
required super.index,
|
|
this.supportsExternalStream = false,
|
|
});
|
|
|
|
SubStreamModel.no({
|
|
super.name = 'Off',
|
|
this.id = 'Off',
|
|
this.title = 'Off',
|
|
super.displayTitle = 'Off',
|
|
super.language = '',
|
|
this.url = '',
|
|
super.codec = '',
|
|
super.isDefault = false,
|
|
super.isExternal = false,
|
|
super.index = -1,
|
|
this.supportsExternalStream = false,
|
|
});
|
|
|
|
String label(BuildContext context) {
|
|
if (index == -1) {
|
|
return context.localized.off;
|
|
} else {
|
|
return displayTitle;
|
|
}
|
|
}
|
|
|
|
factory SubStreamModel.fromMediaStream(dto.MediaStream stream, Ref ref) {
|
|
return SubStreamModel(
|
|
name: stream.title ?? "",
|
|
title: stream.title ?? "",
|
|
displayTitle: stream.displayTitle ?? "",
|
|
language: stream.language ?? "Unknown",
|
|
isDefault: stream.isDefault ?? false,
|
|
codec: stream.codec ?? "",
|
|
id: stream.hashCode.toString(),
|
|
supportsExternalStream: stream.supportsExternalStream ?? false,
|
|
url: stream.deliveryUrl != null
|
|
? "${ref.read(userProvider)?.server ?? ""}${stream.deliveryUrl}}".replaceAll(".vtt", ".srt")
|
|
: null,
|
|
isExternal: stream.isExternal ?? false,
|
|
index: stream.index ?? -1,
|
|
);
|
|
}
|
|
|
|
SubStreamModel copyWith({
|
|
String? name,
|
|
String? id,
|
|
String? title,
|
|
String? displayTitle,
|
|
String? language,
|
|
ValueGetter<String?>? url,
|
|
String? codec,
|
|
bool? isDefault,
|
|
bool? isExternal,
|
|
int? index,
|
|
bool? supportsExternalStream,
|
|
}) {
|
|
return SubStreamModel(
|
|
name: name ?? this.name,
|
|
id: id ?? this.id,
|
|
title: title ?? this.title,
|
|
displayTitle: displayTitle ?? this.displayTitle,
|
|
language: language ?? this.language,
|
|
url: url != null ? url() : this.url,
|
|
supportsExternalStream: supportsExternalStream ?? this.supportsExternalStream,
|
|
codec: codec ?? this.codec,
|
|
isDefault: isDefault ?? this.isDefault,
|
|
isExternal: isExternal ?? this.isExternal,
|
|
index: index ?? this.index,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toMap() {
|
|
return {
|
|
'name': name,
|
|
'id': id,
|
|
'title': title,
|
|
'displayTitle': displayTitle,
|
|
'language': language,
|
|
'url': url,
|
|
'supportsExternalStream': supportsExternalStream,
|
|
'codec': codec,
|
|
'isExternal': isExternal,
|
|
'isDefault': isDefault,
|
|
'index': index,
|
|
};
|
|
}
|
|
|
|
factory SubStreamModel.fromMap(Map<String, dynamic> map) {
|
|
return SubStreamModel(
|
|
name: map['name'] ?? '',
|
|
id: map['id'] ?? '',
|
|
title: map['title'] ?? '',
|
|
displayTitle: map['displayTitle'] ?? '',
|
|
language: map['language'] ?? '',
|
|
url: map['url'],
|
|
supportsExternalStream: map['supportsExternalStream'] ?? false,
|
|
codec: map['codec'] ?? '',
|
|
isDefault: map['isDefault'] ?? false,
|
|
isExternal: map['isExternal'] ?? false,
|
|
index: map['index'] ?? -1,
|
|
);
|
|
}
|
|
|
|
String toJson() => json.encode(toMap());
|
|
|
|
factory SubStreamModel.fromJson(String source) => SubStreamModel.fromMap(json.decode(source));
|
|
|
|
@override
|
|
String toString() {
|
|
return 'SubFile(title: $title, displayTitle: $displayTitle, language: $language, url: $url, isExternal: $isExternal)';
|
|
}
|
|
}
|