feature: Added LibMDK video player backend (#162)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2024-11-22 18:53:31 +01:00 committed by GitHub
parent 6e32018183
commit da354437e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 1499 additions and 1006 deletions

View file

@ -1,6 +1,7 @@
import 'package:flutter/widgets.dart';
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
@ -13,9 +14,7 @@ import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/util/duration_extensions.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart'
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
import 'package:flutter/widgets.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart';
class DirectPlaybackModel implements PlaybackModel {
DirectPlaybackModel({
@ -67,22 +66,8 @@ class DirectPlaybackModel implements PlaybackModel {
@override
Future<DirectPlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
final wantedSubtitle =
model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex);
if (wantedSubtitle == null) return this;
if (wantedSubtitle.index == SubStreamModel.no().index) {
await player.setSubtitleTrack(SubtitleTrack.no());
} else {
final subTracks = player.subTracks.getRange(2, player.subTracks.length).toList();
final index = subStreams.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id);
final subTrack = subTracks.elementAtOrNull(index);
if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) {
await player.setSubtitleTrack(SubtitleTrack.uri(wantedSubtitle.url!));
} else if (subTrack != null) {
await player.setSubtitleTrack(subTrack);
}
}
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: wantedSubtitle.index));
final newIndex = await player.setSubtitleTrack(model, this);
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: newIndex));
}
@override
@ -90,19 +75,8 @@ class DirectPlaybackModel implements PlaybackModel {
@override
Future<DirectPlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
final wantedAudioStream =
model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex);
if (wantedAudioStream == null) return this;
if (wantedAudioStream.index == AudioStreamModel.no().index) {
await player.setAudioTrack(AudioTrack.no());
} else {
final audioTracks = player.audioTracks.getRange(2, player.audioTracks.length).toList();
final audioTrack = audioTracks.elementAtOrNull(audioStreams.indexOf(wantedAudioStream) - 1);
if (audioTrack != null) {
await player.setAudioTrack(audioTrack);
}
}
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: wantedAudioStream.index));
final newIndex = await player.setAudioTrack(model, this);
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: newIndex));
}
@override

View file

@ -1,6 +1,6 @@
import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
@ -13,9 +13,7 @@ import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/util/duration_extensions.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart'
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
import 'package:flutter/widgets.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart';
class OfflinePlaybackModel implements PlaybackModel {
OfflinePlaybackModel({
@ -66,22 +64,8 @@ class OfflinePlaybackModel implements PlaybackModel {
@override
Future<OfflinePlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
final wantedSubtitle =
model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex);
if (wantedSubtitle == null) return this;
if (wantedSubtitle.index == SubStreamModel.no().index) {
await player.setSubtitleTrack(SubtitleTrack.no());
} else {
final subTracks = player.subTracks.getRange(2, player.subTracks.length).toList();
final index = subStreams.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id);
final subTrack = subTracks.elementAtOrNull(index);
if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) {
await player.setSubtitleTrack(SubtitleTrack.uri(wantedSubtitle.url!));
} else if (subTrack != null) {
await player.setSubtitleTrack(subTrack);
}
}
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: wantedSubtitle.index));
final newIndex = await player.setSubtitleTrack(model, this);
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: newIndex));
}
@override
@ -89,19 +73,8 @@ class OfflinePlaybackModel implements PlaybackModel {
@override
Future<OfflinePlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
final wantedAudioStream =
model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex);
if (wantedAudioStream == null) return this;
if (wantedAudioStream.index == AudioStreamModel.no().index) {
await player.setAudioTrack(AudioTrack.no());
} else {
final audioTracks = player.audioTracks.getRange(2, player.audioTracks.length).toList();
final audioTrack = audioTracks.elementAtOrNull(audioStreams.indexOf(wantedAudioStream) - 1);
if (audioTrack != null) {
await player.setAudioTrack(audioTrack);
}
}
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: wantedAudioStream.index));
final newIndex = await player.setAudioTrack(model, this);
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: newIndex));
}
@override

View file

@ -3,7 +3,6 @@ import 'dart:developer';
import 'package:chopper/chopper.dart';
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
@ -27,10 +26,23 @@ 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/duration_extensions.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart'
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart';
class Media {
final String url;
const Media({
required this.url,
});
}
extension PlaybackModelExtension on PlaybackModel? {
SubStreamModel? get defaultSubStream =>
this?.subStreams?.firstWhereOrNull((element) => element.index == this?.mediaStreams?.defaultSubStreamIndex);
AudioStreamModel? get defaultAudioStream =>
this?.audioStreams?.firstWhereOrNull((element) => element.index == this?.mediaStreams?.defaultAudioStreamIndex);
String? get label => switch (this) {
DirectPlaybackModel _ => PlaybackType.directStream.name,
TranscodePlaybackModel _ => PlaybackType.transcode.name,
@ -119,7 +131,7 @@ class PlaybackModelHelper {
syncedItem: syncedItem,
trickPlay: syncedItem.trickPlayModel,
mediaSegments: syncedItem.mediaSegments,
media: Media(syncedItem.videoFile.path),
media: Media(url: syncedItem.videoFile.path),
queue: itemQueue.whereNotNull().toList(),
syncedQueue: children,
mediaStreams: item.streamModel ?? syncedItemModel.streamModel,
@ -170,7 +182,7 @@ class PlaybackModelHelper {
subtitleStreamIndex: streamModel?.defaultSubStreamIndex,
enableTranscoding: true,
autoOpenLiveStream: true,
deviceProfile: defaultProfile,
deviceProfile: ref.read(videoProfileProvider),
userId: userId,
mediaSourceId: firstItemToPlay.id,
),
@ -218,7 +230,7 @@ class PlaybackModelHelper {
chapters: chapters,
playbackInfo: playbackInfo,
trickPlay: trickPlay,
media: Media('${ref.read(userProvider)?.server ?? ""}/Videos/${mediaSource.id}/stream?$params'),
media: Media(url: '${ref.read(userProvider)?.server ?? ""}/Videos/${mediaSource.id}/stream?$params'),
mediaStreams: mediaStreamsWithUrls,
);
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
@ -229,7 +241,7 @@ class PlaybackModelHelper {
chapters: chapters,
trickPlay: trickPlay,
playbackInfo: playbackInfo,
media: Media("${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
media: Media(url: "${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
mediaStreams: mediaStreamsWithUrls,
);
}
@ -300,7 +312,7 @@ class PlaybackModelHelper {
subtitleStreamIndex: subIndex,
enableTranscoding: true,
autoOpenLiveStream: true,
deviceProfile: defaultProfile,
deviceProfile: ref.read(videoProfileProvider),
userId: userId,
mediaSourceId: item.id,
),
@ -347,7 +359,7 @@ class PlaybackModelHelper {
chapters: playbackModel.chapters,
playbackInfo: playbackInfo,
trickPlay: playbackModel.trickPlay,
media: Media(directPlay),
media: Media(url: directPlay),
mediaStreams: mediaStreamsWithUrls,
);
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
@ -358,7 +370,7 @@ class PlaybackModelHelper {
chapters: playbackModel.chapters,
playbackInfo: playbackInfo,
trickPlay: playbackModel.trickPlay,
media: Media("${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
media: Media(url: "${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
mediaStreams: mediaStreamsWithUrls,
);
}

View file

@ -1,6 +1,7 @@
import 'package:flutter/widgets.dart';
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
@ -13,9 +14,7 @@ import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
import 'package:fladder/util/duration_extensions.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart'
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
import 'package:flutter/widgets.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart';
class TranscodePlaybackModel implements PlaybackModel {
TranscodePlaybackModel({
@ -67,22 +66,8 @@ class TranscodePlaybackModel implements PlaybackModel {
@override
Future<TranscodePlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
final wantedSubtitle =
model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex);
if (wantedSubtitle == null) return this;
if (wantedSubtitle.index == SubStreamModel.no().index) {
await player.setSubtitleTrack(SubtitleTrack.no());
} else {
final subTracks = player.subTracks.getRange(2, player.subTracks.length).toList();
final index = subStreams.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id);
final subTrack = subTracks.elementAtOrNull(index);
if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) {
await player.setSubtitleTrack(SubtitleTrack.uri(wantedSubtitle.url!));
} else if (subTrack != null) {
await player.setSubtitleTrack(subTrack);
}
}
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: wantedSubtitle.index));
final newIndex = await player.setSubtitleTrack(model, this);
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: newIndex));
}
@override
@ -90,19 +75,8 @@ class TranscodePlaybackModel implements PlaybackModel {
@override
Future<TranscodePlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
final wantedAudioStream =
model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex);
if (wantedAudioStream == null) return this;
if (wantedAudioStream.index == AudioStreamModel.no().index) {
await player.setAudioTrack(AudioTrack.no());
} else {
final audioTracks = player.audioTracks.getRange(2, player.audioTracks.length).toList();
final audioTrack = audioTracks.elementAtOrNull(audioStreams.indexOf(wantedAudioStream) - 1);
if (audioTrack != null) {
await player.setAudioTrack(audioTrack);
}
}
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: wantedAudioStream.index));
final newIndex = await player.setAudioTrack(model, this);
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: newIndex));
}
@override