feature: Video quality options (#234)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2025-02-23 13:29:59 +01:00 committed by GitHub
parent 957ad6c991
commit 935d6fe176
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 644 additions and 232 deletions

View file

@ -22,11 +22,15 @@ import 'package:fladder/models/video_stream_model.dart';
import 'package:fladder/profiles/default_profile.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/service_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/util/bitrate_helper.dart';
import 'package:fladder/util/duration_extensions.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:fladder/util/map_bool_helper.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart';
class Media {
@ -52,15 +56,17 @@ extension PlaybackModelExtension on PlaybackModel? {
};
}
abstract class PlaybackModel {
final ItemBaseModel item = throw UnimplementedError();
final Media? media = throw UnimplementedError();
final List<ItemBaseModel> queue = const [];
final MediaSegmentsModel? mediaSegments = null;
final PlaybackInfoResponse? playbackInfo = throw UnimplementedError();
class PlaybackModel {
final ItemBaseModel item;
final Media? media;
final List<ItemBaseModel> queue;
final MediaSegmentsModel? mediaSegments;
final PlaybackInfoResponse? playbackInfo;
List<Chapter>? get chapters;
TrickPlayModel? get trickPlay;
Map<Bitrate, bool> bitRateOptions;
List<Chapter>? chapters = [];
TrickPlayModel? trickPlay;
Future<PlaybackModel?> updatePlaybackPosition(Duration position, bool isPlaying, Ref ref) =>
throw UnimplementedError();
@ -68,21 +74,32 @@ abstract class PlaybackModel {
Future<PlaybackModel?> playbackStopped(Duration position, Duration? totalDuration, Ref ref) =>
throw UnimplementedError();
final MediaStreamsModel? mediaStreams = throw UnimplementedError();
List<SubStreamModel>? get subStreams;
List<AudioStreamModel>? get audioStreams;
final MediaStreamsModel? mediaStreams;
List<SubStreamModel>? get subStreams => throw UnimplementedError();
List<AudioStreamModel>? get audioStreams => throw UnimplementedError();
Future<Duration>? startDuration() async => item.userData.playBackPosition;
Future<PlaybackModel>? setSubtitle(SubStreamModel? model, MediaControlsWrapper player) {
return null;
}
Future<PlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) => null;
Future<PlaybackModel>? setSubtitle(SubStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError();
Future<PlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) => throw UnimplementedError();
Future<PlaybackModel>? setQualityOption(Map<Bitrate, bool> map) => throw UnimplementedError();
ItemBaseModel? get nextVideo => throw UnimplementedError();
ItemBaseModel? get previousVideo => throw UnimplementedError();
ItemBaseModel? get nextVideo => queue.nextOrNull(item);
ItemBaseModel? get previousVideo => queue.previousOrNull(item);
PlaybackModel copyWith();
PlaybackModel copyWith() => throw UnimplementedError();
PlaybackModel({
required this.playbackInfo,
this.mediaStreams,
required this.item,
required this.media,
this.queue = const [],
this.bitRateOptions = const {},
this.mediaSegments,
this.chapters,
this.trickPlay,
});
}
final playbackModelHelper = Provider<PlaybackModelHelper>((ref) {
@ -149,8 +166,13 @@ class PlaybackModelHelper {
}
}
Future<PlaybackModel?> createServerPlaybackModel(ItemBaseModel? item, PlaybackType? type,
{PlaybackModel? oldModel, List<ItemBaseModel>? libraryQueue, Duration? startPosition}) async {
Future<PlaybackModel?> createServerPlaybackModel(
ItemBaseModel? item,
PlaybackType? type, {
PlaybackModel? oldModel,
List<ItemBaseModel>? libraryQueue,
Duration? startPosition,
}) async {
try {
if (item == null) return null;
final userId = ref.read(userProvider)?.id;
@ -165,18 +187,18 @@ class PlaybackModelHelper {
final fullItem = await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id);
Map<Bitrate, bool> qualityOptions = getVideoQualityOptions(
VideoQualitySettings(
maxBitRate: ref.read(videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate)),
videoBitRate: firstItemToPlay.streamModel?.videoStreams.first.bitRate ?? 0,
videoCodec: firstItemToPlay.streamModel?.videoStreams.first.codec,
),
);
final streamModel = firstItemToPlay.streamModel;
Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
final Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
itemId: firstItemToPlay.id,
enableDirectPlay: type != PlaybackType.transcode,
enableDirectStream: type != PlaybackType.transcode,
enableTranscoding: true,
autoOpenLiveStream: true,
startTimeTicks: startPosition?.toRuntimeTicks,
audioStreamIndex: streamModel?.defaultAudioStreamIndex,
subtitleStreamIndex: streamModel?.defaultSubStreamIndex,
mediaSourceId: firstItemToPlay.id,
body: PlaybackInfoDto(
startTimeTicks: startPosition?.toRuntimeTicks,
audioStreamIndex: streamModel?.defaultAudioStreamIndex,
@ -185,6 +207,9 @@ class PlaybackModelHelper {
autoOpenLiveStream: true,
deviceProfile: ref.read(videoProfileProvider),
userId: userId,
enableDirectPlay: type != PlaybackType.transcode,
enableDirectStream: type != PlaybackType.transcode,
maxStreamingBitrate: qualityOptions.enabledFirst.keys.firstOrNull?.bitRate,
mediaSourceId: firstItemToPlay.id,
),
);
@ -234,10 +259,9 @@ class PlaybackModelHelper {
chapters: chapters,
playbackInfo: playbackInfo,
trickPlay: trickPlay,
media: Media(
url: mediaPath ?? playbackUrl,
),
media: Media(url: mediaPath ?? playbackUrl),
mediaStreams: mediaStreamsWithUrls,
bitRateOptions: qualityOptions,
);
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
return TranscodePlaybackModel(
@ -309,22 +333,17 @@ class PlaybackModelHelper {
Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
itemId: item.id,
enableDirectPlay: true,
enableDirectStream: true,
enableTranscoding: true,
autoOpenLiveStream: true,
startTimeTicks: currentPosition.toRuntimeTicks,
audioStreamIndex: audioIndex,
subtitleStreamIndex: subIndex,
mediaSourceId: item.id,
body: PlaybackInfoDto(
startTimeTicks: currentPosition.toRuntimeTicks,
audioStreamIndex: audioIndex,
enableDirectPlay: true,
enableDirectStream: true,
subtitleStreamIndex: subIndex,
enableTranscoding: true,
autoOpenLiveStream: true,
deviceProfile: ref.read(videoProfileProvider),
userId: userId,
maxStreamingBitrate: playbackModel.bitRateOptions.enabledFirst.entries.firstOrNull?.key.bitRate,
mediaSourceId: item.id,
),
);
@ -363,6 +382,8 @@ class PlaybackModelHelper {
final directPlay = '${ref.read(userProvider)?.server ?? ""}/Videos/${mediaSource.id}/stream?$params';
final mediaPath = isValidVideoUrl(mediaSource.path ?? "");
newModel = DirectPlaybackModel(
item: playbackModel.item,
queue: playbackModel.queue,
@ -370,8 +391,9 @@ class PlaybackModelHelper {
chapters: playbackModel.chapters,
playbackInfo: playbackInfo,
trickPlay: playbackModel.trickPlay,
media: Media(url: directPlay),
media: Media(url: mediaPath ?? directPlay),
mediaStreams: mediaStreamsWithUrls,
bitRateOptions: playbackModel.bitRateOptions,
);
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
newModel = TranscodePlaybackModel(
@ -383,6 +405,7 @@ class PlaybackModelHelper {
trickPlay: playbackModel.trickPlay,
media: Media(url: "${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
mediaStreams: mediaStreamsWithUrls,
bitRateOptions: playbackModel.bitRateOptions,
);
}
if (newModel == null) return;