Fladder/lib/models/items/media_streams_model.dart

446 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}";
}
String? get mediaInfoTag => '${displayProfile?.value} ${resolution?.value}';
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)';
}
}