mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 13:38:13 -08:00
feature: Added LibMDK video player backend (#162)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
6e32018183
commit
da354437e3
53 changed files with 1499 additions and 1006 deletions
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
|
|
@ -6,6 +6,13 @@ on:
|
||||||
- "v*"
|
- "v*"
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- pubspec.yaml
|
||||||
|
- .github/workflows/build.yml
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
@ -228,6 +235,7 @@ jobs:
|
||||||
build-linux-flatpak:
|
build-linux-flatpak:
|
||||||
name: "Flatpak"
|
name: "Flatpak"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
needs: [fetch-info, build-linux]
|
needs: [fetch-info, build-linux]
|
||||||
container:
|
container:
|
||||||
image: bilelmoussaoui/flatpak-github-actions:gnome-46
|
image: bilelmoussaoui/flatpak-github-actions:gnome-46
|
||||||
|
|
|
||||||
|
|
@ -1125,5 +1125,13 @@
|
||||||
"stop": "Stop",
|
"stop": "Stop",
|
||||||
"resumeVideo": "Resume video",
|
"resumeVideo": "Resume video",
|
||||||
"closeVideo": "Close video",
|
"closeVideo": "Close video",
|
||||||
"playNextVideo": "Play next video"
|
"playNextVideo": "Play next video",
|
||||||
|
"playerSettingsBackendTitle": "Video player Backend",
|
||||||
|
"playerSettingsBackendDesc": "Choose your preferred media player for optimal playback experience",
|
||||||
|
"defaultLabel": "Default",
|
||||||
|
"@defaultLabel": {
|
||||||
|
"description": "To indicate a default value, default video player backend"
|
||||||
|
},
|
||||||
|
"noVideoPlayerOptions": "The selected backend has no options",
|
||||||
|
"mdkExperimental": "MDK is still in a experimental stage"
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,6 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
@ -67,7 +66,6 @@ Future<Map<String, dynamic>> loadConfig() async {
|
||||||
void main() async {
|
void main() async {
|
||||||
_setupLogging();
|
_setupLogging();
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
MediaKit.ensureInitialized();
|
|
||||||
|
|
||||||
if (kIsWeb) {
|
if (kIsWeb) {
|
||||||
html.document.onContextMenu.listen((event) => event.preventDefault());
|
html.document.onContextMenu.listen((event) => event.preventDefault());
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@ class TrickPlayModel with _$TrickPlayModel {
|
||||||
int get imagesPerTile => tileWidth * tileHeight;
|
int get imagesPerTile => tileWidth * tileHeight;
|
||||||
|
|
||||||
String? getTile(Duration position) {
|
String? getTile(Duration position) {
|
||||||
final int currentIndex = (position.inMilliseconds ~/ interval.inMilliseconds).clamp(0, thumbnailCount - 1);
|
final int currentIndex = (position.inMilliseconds ~/ interval.inMilliseconds).clamp(0, thumbnailCount);
|
||||||
final int indexOfTile = (currentIndex ~/ imagesPerTile).clamp(0, (images.length - 1));
|
final int indexOfTile = (currentIndex ~/ imagesPerTile).clamp(0, images.length);
|
||||||
return images.elementAtOrNull(indexOfTile);
|
return images.elementAtOrNull(indexOfTile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/jellyfin/jellyfin_open_api.swagger.dart';
|
||||||
import 'package:fladder/models/item_base_model.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/providers/video_player_provider.dart';
|
||||||
import 'package:fladder/util/duration_extensions.dart';
|
import 'package:fladder/util/duration_extensions.dart';
|
||||||
import 'package:fladder/util/list_extensions.dart';
|
import 'package:fladder/util/list_extensions.dart';
|
||||||
import 'package:fladder/wrappers/media_control_wrapper.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';
|
|
||||||
|
|
||||||
class DirectPlaybackModel implements PlaybackModel {
|
class DirectPlaybackModel implements PlaybackModel {
|
||||||
DirectPlaybackModel({
|
DirectPlaybackModel({
|
||||||
|
|
@ -67,22 +66,8 @@ class DirectPlaybackModel implements PlaybackModel {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<DirectPlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
|
Future<DirectPlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
|
||||||
final wantedSubtitle =
|
final newIndex = await player.setSubtitleTrack(model, this);
|
||||||
model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex);
|
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: newIndex));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -90,19 +75,8 @@ class DirectPlaybackModel implements PlaybackModel {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<DirectPlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
|
Future<DirectPlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
|
||||||
final wantedAudioStream =
|
final newIndex = await player.setAudioTrack(model, this);
|
||||||
model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex);
|
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: newIndex));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:collection/collection.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/jellyfin/jellyfin_open_api.swagger.dart';
|
||||||
import 'package:fladder/models/item_base_model.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/providers/sync_provider.dart';
|
||||||
import 'package:fladder/util/duration_extensions.dart';
|
import 'package:fladder/util/duration_extensions.dart';
|
||||||
import 'package:fladder/util/list_extensions.dart';
|
import 'package:fladder/util/list_extensions.dart';
|
||||||
import 'package:fladder/wrappers/media_control_wrapper.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';
|
|
||||||
|
|
||||||
class OfflinePlaybackModel implements PlaybackModel {
|
class OfflinePlaybackModel implements PlaybackModel {
|
||||||
OfflinePlaybackModel({
|
OfflinePlaybackModel({
|
||||||
|
|
@ -66,22 +64,8 @@ class OfflinePlaybackModel implements PlaybackModel {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<OfflinePlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
|
Future<OfflinePlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
|
||||||
final wantedSubtitle =
|
final newIndex = await player.setSubtitleTrack(model, this);
|
||||||
model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex);
|
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: newIndex));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -89,19 +73,8 @@ class OfflinePlaybackModel implements PlaybackModel {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<OfflinePlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
|
Future<OfflinePlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
|
||||||
final wantedAudioStream =
|
final newIndex = await player.setAudioTrack(model, this);
|
||||||
model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex);
|
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: newIndex));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ import 'dart:developer';
|
||||||
import 'package:chopper/chopper.dart';
|
import 'package:chopper/chopper.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/jellyfin/jellyfin_open_api.swagger.dart';
|
||||||
import 'package:fladder/models/item_base_model.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/user_provider.dart';
|
||||||
import 'package:fladder/providers/video_player_provider.dart';
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
import 'package:fladder/util/duration_extensions.dart';
|
import 'package:fladder/util/duration_extensions.dart';
|
||||||
import 'package:fladder/wrappers/media_control_wrapper.dart'
|
import 'package:fladder/wrappers/media_control_wrapper.dart';
|
||||||
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
|
|
||||||
|
class Media {
|
||||||
|
final String url;
|
||||||
|
|
||||||
|
const Media({
|
||||||
|
required this.url,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
extension PlaybackModelExtension on PlaybackModel? {
|
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) {
|
String? get label => switch (this) {
|
||||||
DirectPlaybackModel _ => PlaybackType.directStream.name,
|
DirectPlaybackModel _ => PlaybackType.directStream.name,
|
||||||
TranscodePlaybackModel _ => PlaybackType.transcode.name,
|
TranscodePlaybackModel _ => PlaybackType.transcode.name,
|
||||||
|
|
@ -119,7 +131,7 @@ class PlaybackModelHelper {
|
||||||
syncedItem: syncedItem,
|
syncedItem: syncedItem,
|
||||||
trickPlay: syncedItem.trickPlayModel,
|
trickPlay: syncedItem.trickPlayModel,
|
||||||
mediaSegments: syncedItem.mediaSegments,
|
mediaSegments: syncedItem.mediaSegments,
|
||||||
media: Media(syncedItem.videoFile.path),
|
media: Media(url: syncedItem.videoFile.path),
|
||||||
queue: itemQueue.whereNotNull().toList(),
|
queue: itemQueue.whereNotNull().toList(),
|
||||||
syncedQueue: children,
|
syncedQueue: children,
|
||||||
mediaStreams: item.streamModel ?? syncedItemModel.streamModel,
|
mediaStreams: item.streamModel ?? syncedItemModel.streamModel,
|
||||||
|
|
@ -170,7 +182,7 @@ class PlaybackModelHelper {
|
||||||
subtitleStreamIndex: streamModel?.defaultSubStreamIndex,
|
subtitleStreamIndex: streamModel?.defaultSubStreamIndex,
|
||||||
enableTranscoding: true,
|
enableTranscoding: true,
|
||||||
autoOpenLiveStream: true,
|
autoOpenLiveStream: true,
|
||||||
deviceProfile: defaultProfile,
|
deviceProfile: ref.read(videoProfileProvider),
|
||||||
userId: userId,
|
userId: userId,
|
||||||
mediaSourceId: firstItemToPlay.id,
|
mediaSourceId: firstItemToPlay.id,
|
||||||
),
|
),
|
||||||
|
|
@ -218,7 +230,7 @@ class PlaybackModelHelper {
|
||||||
chapters: chapters,
|
chapters: chapters,
|
||||||
playbackInfo: playbackInfo,
|
playbackInfo: playbackInfo,
|
||||||
trickPlay: trickPlay,
|
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,
|
mediaStreams: mediaStreamsWithUrls,
|
||||||
);
|
);
|
||||||
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
|
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
|
||||||
|
|
@ -229,7 +241,7 @@ class PlaybackModelHelper {
|
||||||
chapters: chapters,
|
chapters: chapters,
|
||||||
trickPlay: trickPlay,
|
trickPlay: trickPlay,
|
||||||
playbackInfo: playbackInfo,
|
playbackInfo: playbackInfo,
|
||||||
media: Media("${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
|
media: Media(url: "${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
|
||||||
mediaStreams: mediaStreamsWithUrls,
|
mediaStreams: mediaStreamsWithUrls,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -300,7 +312,7 @@ class PlaybackModelHelper {
|
||||||
subtitleStreamIndex: subIndex,
|
subtitleStreamIndex: subIndex,
|
||||||
enableTranscoding: true,
|
enableTranscoding: true,
|
||||||
autoOpenLiveStream: true,
|
autoOpenLiveStream: true,
|
||||||
deviceProfile: defaultProfile,
|
deviceProfile: ref.read(videoProfileProvider),
|
||||||
userId: userId,
|
userId: userId,
|
||||||
mediaSourceId: item.id,
|
mediaSourceId: item.id,
|
||||||
),
|
),
|
||||||
|
|
@ -347,7 +359,7 @@ class PlaybackModelHelper {
|
||||||
chapters: playbackModel.chapters,
|
chapters: playbackModel.chapters,
|
||||||
playbackInfo: playbackInfo,
|
playbackInfo: playbackInfo,
|
||||||
trickPlay: playbackModel.trickPlay,
|
trickPlay: playbackModel.trickPlay,
|
||||||
media: Media(directPlay),
|
media: Media(url: directPlay),
|
||||||
mediaStreams: mediaStreamsWithUrls,
|
mediaStreams: mediaStreamsWithUrls,
|
||||||
);
|
);
|
||||||
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
|
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
|
||||||
|
|
@ -358,7 +370,7 @@ class PlaybackModelHelper {
|
||||||
chapters: playbackModel.chapters,
|
chapters: playbackModel.chapters,
|
||||||
playbackInfo: playbackInfo,
|
playbackInfo: playbackInfo,
|
||||||
trickPlay: playbackModel.trickPlay,
|
trickPlay: playbackModel.trickPlay,
|
||||||
media: Media("${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
|
media: Media(url: "${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
|
||||||
mediaStreams: mediaStreamsWithUrls,
|
mediaStreams: mediaStreamsWithUrls,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/jellyfin/jellyfin_open_api.swagger.dart';
|
||||||
import 'package:fladder/models/item_base_model.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/providers/video_player_provider.dart';
|
||||||
import 'package:fladder/util/duration_extensions.dart';
|
import 'package:fladder/util/duration_extensions.dart';
|
||||||
import 'package:fladder/util/list_extensions.dart';
|
import 'package:fladder/util/list_extensions.dart';
|
||||||
import 'package:fladder/wrappers/media_control_wrapper.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';
|
|
||||||
|
|
||||||
class TranscodePlaybackModel implements PlaybackModel {
|
class TranscodePlaybackModel implements PlaybackModel {
|
||||||
TranscodePlaybackModel({
|
TranscodePlaybackModel({
|
||||||
|
|
@ -67,22 +66,8 @@ class TranscodePlaybackModel implements PlaybackModel {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TranscodePlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
|
Future<TranscodePlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
|
||||||
final wantedSubtitle =
|
final newIndex = await player.setSubtitleTrack(model, this);
|
||||||
model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex);
|
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: newIndex));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -90,19 +75,8 @@ class TranscodePlaybackModel implements PlaybackModel {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TranscodePlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
|
Future<TranscodePlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
|
||||||
final wantedAudioStream =
|
final newIndex = await player.setAudioTrack(model, this);
|
||||||
model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex);
|
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: newIndex));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
|
||||||
@Default(false) bool fillScreen,
|
@Default(false) bool fillScreen,
|
||||||
@Default(true) bool hardwareAccel,
|
@Default(true) bool hardwareAccel,
|
||||||
@Default(false) bool useLibass,
|
@Default(false) bool useLibass,
|
||||||
|
PlayerOptions? playerOptions,
|
||||||
@Default(100) double internalVolume,
|
@Default(100) double internalVolume,
|
||||||
Set<DeviceOrientation>? allowedOrientations,
|
Set<DeviceOrientation>? allowedOrientations,
|
||||||
@Default(AutoNextType.smart) AutoNextType nextVideoType,
|
@Default(AutoNextType.smart) AutoNextType nextVideoType,
|
||||||
|
|
@ -33,8 +34,10 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
|
||||||
|
|
||||||
factory VideoPlayerSettingsModel.fromJson(Map<String, dynamic> json) => _$VideoPlayerSettingsModelFromJson(json);
|
factory VideoPlayerSettingsModel.fromJson(Map<String, dynamic> json) => _$VideoPlayerSettingsModelFromJson(json);
|
||||||
|
|
||||||
|
PlayerOptions get wantedPlayer => playerOptions ?? PlayerOptions.platformDefaults;
|
||||||
|
|
||||||
bool playerSame(VideoPlayerSettingsModel other) {
|
bool playerSame(VideoPlayerSettingsModel other) {
|
||||||
return other.hardwareAccel == hardwareAccel && other.useLibass == useLibass;
|
return other.hardwareAccel == hardwareAccel && other.useLibass == useLibass && other.wantedPlayer == wantedPlayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -48,6 +51,7 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
|
||||||
other.hardwareAccel == hardwareAccel &&
|
other.hardwareAccel == hardwareAccel &&
|
||||||
other.useLibass == useLibass &&
|
other.useLibass == useLibass &&
|
||||||
other.internalVolume == internalVolume &&
|
other.internalVolume == internalVolume &&
|
||||||
|
other.playerOptions == playerOptions &&
|
||||||
other.audioDevice == audioDevice;
|
other.audioDevice == audioDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,6 +67,27 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum PlayerOptions {
|
||||||
|
libMDK,
|
||||||
|
libMPV;
|
||||||
|
|
||||||
|
const PlayerOptions();
|
||||||
|
|
||||||
|
static Iterable<PlayerOptions> get available => kIsWeb ? {PlayerOptions.libMPV} : PlayerOptions.values;
|
||||||
|
|
||||||
|
static PlayerOptions get platformDefaults {
|
||||||
|
if (kIsWeb) return PlayerOptions.libMPV;
|
||||||
|
return switch (defaultTargetPlatform) {
|
||||||
|
_ => PlayerOptions.libMPV,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String label(BuildContext context) => switch (this) {
|
||||||
|
PlayerOptions.libMDK => "MDK",
|
||||||
|
PlayerOptions.libMPV => "MPV",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
enum AutoNextType {
|
enum AutoNextType {
|
||||||
off,
|
off,
|
||||||
smart,
|
smart,
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ mixin _$VideoPlayerSettingsModel {
|
||||||
bool get fillScreen => throw _privateConstructorUsedError;
|
bool get fillScreen => throw _privateConstructorUsedError;
|
||||||
bool get hardwareAccel => throw _privateConstructorUsedError;
|
bool get hardwareAccel => throw _privateConstructorUsedError;
|
||||||
bool get useLibass => throw _privateConstructorUsedError;
|
bool get useLibass => throw _privateConstructorUsedError;
|
||||||
|
PlayerOptions? get playerOptions => throw _privateConstructorUsedError;
|
||||||
double get internalVolume => throw _privateConstructorUsedError;
|
double get internalVolume => throw _privateConstructorUsedError;
|
||||||
Set<DeviceOrientation>? get allowedOrientations =>
|
Set<DeviceOrientation>? get allowedOrientations =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
|
|
@ -54,6 +55,7 @@ abstract class $VideoPlayerSettingsModelCopyWith<$Res> {
|
||||||
bool fillScreen,
|
bool fillScreen,
|
||||||
bool hardwareAccel,
|
bool hardwareAccel,
|
||||||
bool useLibass,
|
bool useLibass,
|
||||||
|
PlayerOptions? playerOptions,
|
||||||
double internalVolume,
|
double internalVolume,
|
||||||
Set<DeviceOrientation>? allowedOrientations,
|
Set<DeviceOrientation>? allowedOrientations,
|
||||||
AutoNextType nextVideoType,
|
AutoNextType nextVideoType,
|
||||||
|
|
@ -81,6 +83,7 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res,
|
||||||
Object? fillScreen = null,
|
Object? fillScreen = null,
|
||||||
Object? hardwareAccel = null,
|
Object? hardwareAccel = null,
|
||||||
Object? useLibass = null,
|
Object? useLibass = null,
|
||||||
|
Object? playerOptions = freezed,
|
||||||
Object? internalVolume = null,
|
Object? internalVolume = null,
|
||||||
Object? allowedOrientations = freezed,
|
Object? allowedOrientations = freezed,
|
||||||
Object? nextVideoType = null,
|
Object? nextVideoType = null,
|
||||||
|
|
@ -107,6 +110,10 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res,
|
||||||
? _value.useLibass
|
? _value.useLibass
|
||||||
: useLibass // ignore: cast_nullable_to_non_nullable
|
: useLibass // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
|
playerOptions: freezed == playerOptions
|
||||||
|
? _value.playerOptions
|
||||||
|
: playerOptions // ignore: cast_nullable_to_non_nullable
|
||||||
|
as PlayerOptions?,
|
||||||
internalVolume: null == internalVolume
|
internalVolume: null == internalVolume
|
||||||
? _value.internalVolume
|
? _value.internalVolume
|
||||||
: internalVolume // ignore: cast_nullable_to_non_nullable
|
: internalVolume // ignore: cast_nullable_to_non_nullable
|
||||||
|
|
@ -142,6 +149,7 @@ abstract class _$$VideoPlayerSettingsModelImplCopyWith<$Res>
|
||||||
bool fillScreen,
|
bool fillScreen,
|
||||||
bool hardwareAccel,
|
bool hardwareAccel,
|
||||||
bool useLibass,
|
bool useLibass,
|
||||||
|
PlayerOptions? playerOptions,
|
||||||
double internalVolume,
|
double internalVolume,
|
||||||
Set<DeviceOrientation>? allowedOrientations,
|
Set<DeviceOrientation>? allowedOrientations,
|
||||||
AutoNextType nextVideoType,
|
AutoNextType nextVideoType,
|
||||||
|
|
@ -168,6 +176,7 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res>
|
||||||
Object? fillScreen = null,
|
Object? fillScreen = null,
|
||||||
Object? hardwareAccel = null,
|
Object? hardwareAccel = null,
|
||||||
Object? useLibass = null,
|
Object? useLibass = null,
|
||||||
|
Object? playerOptions = freezed,
|
||||||
Object? internalVolume = null,
|
Object? internalVolume = null,
|
||||||
Object? allowedOrientations = freezed,
|
Object? allowedOrientations = freezed,
|
||||||
Object? nextVideoType = null,
|
Object? nextVideoType = null,
|
||||||
|
|
@ -194,6 +203,10 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res>
|
||||||
? _value.useLibass
|
? _value.useLibass
|
||||||
: useLibass // ignore: cast_nullable_to_non_nullable
|
: useLibass // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
|
playerOptions: freezed == playerOptions
|
||||||
|
? _value.playerOptions
|
||||||
|
: playerOptions // ignore: cast_nullable_to_non_nullable
|
||||||
|
as PlayerOptions?,
|
||||||
internalVolume: null == internalVolume
|
internalVolume: null == internalVolume
|
||||||
? _value.internalVolume
|
? _value.internalVolume
|
||||||
: internalVolume // ignore: cast_nullable_to_non_nullable
|
: internalVolume // ignore: cast_nullable_to_non_nullable
|
||||||
|
|
@ -224,6 +237,7 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
|
||||||
this.fillScreen = false,
|
this.fillScreen = false,
|
||||||
this.hardwareAccel = true,
|
this.hardwareAccel = true,
|
||||||
this.useLibass = false,
|
this.useLibass = false,
|
||||||
|
this.playerOptions,
|
||||||
this.internalVolume = 100,
|
this.internalVolume = 100,
|
||||||
final Set<DeviceOrientation>? allowedOrientations,
|
final Set<DeviceOrientation>? allowedOrientations,
|
||||||
this.nextVideoType = AutoNextType.smart,
|
this.nextVideoType = AutoNextType.smart,
|
||||||
|
|
@ -249,6 +263,8 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final bool useLibass;
|
final bool useLibass;
|
||||||
@override
|
@override
|
||||||
|
final PlayerOptions? playerOptions;
|
||||||
|
@override
|
||||||
@JsonKey()
|
@JsonKey()
|
||||||
final double internalVolume;
|
final double internalVolume;
|
||||||
final Set<DeviceOrientation>? _allowedOrientations;
|
final Set<DeviceOrientation>? _allowedOrientations;
|
||||||
|
|
@ -270,7 +286,7 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||||
return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, audioDevice: $audioDevice)';
|
return 'VideoPlayerSettingsModel(screenBrightness: $screenBrightness, videoFit: $videoFit, fillScreen: $fillScreen, hardwareAccel: $hardwareAccel, useLibass: $useLibass, playerOptions: $playerOptions, internalVolume: $internalVolume, allowedOrientations: $allowedOrientations, nextVideoType: $nextVideoType, audioDevice: $audioDevice)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -283,6 +299,7 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
|
||||||
..add(DiagnosticsProperty('fillScreen', fillScreen))
|
..add(DiagnosticsProperty('fillScreen', fillScreen))
|
||||||
..add(DiagnosticsProperty('hardwareAccel', hardwareAccel))
|
..add(DiagnosticsProperty('hardwareAccel', hardwareAccel))
|
||||||
..add(DiagnosticsProperty('useLibass', useLibass))
|
..add(DiagnosticsProperty('useLibass', useLibass))
|
||||||
|
..add(DiagnosticsProperty('playerOptions', playerOptions))
|
||||||
..add(DiagnosticsProperty('internalVolume', internalVolume))
|
..add(DiagnosticsProperty('internalVolume', internalVolume))
|
||||||
..add(DiagnosticsProperty('allowedOrientations', allowedOrientations))
|
..add(DiagnosticsProperty('allowedOrientations', allowedOrientations))
|
||||||
..add(DiagnosticsProperty('nextVideoType', nextVideoType))
|
..add(DiagnosticsProperty('nextVideoType', nextVideoType))
|
||||||
|
|
@ -313,6 +330,7 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel {
|
||||||
final bool fillScreen,
|
final bool fillScreen,
|
||||||
final bool hardwareAccel,
|
final bool hardwareAccel,
|
||||||
final bool useLibass,
|
final bool useLibass,
|
||||||
|
final PlayerOptions? playerOptions,
|
||||||
final double internalVolume,
|
final double internalVolume,
|
||||||
final Set<DeviceOrientation>? allowedOrientations,
|
final Set<DeviceOrientation>? allowedOrientations,
|
||||||
final AutoNextType nextVideoType,
|
final AutoNextType nextVideoType,
|
||||||
|
|
@ -333,6 +351,8 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel {
|
||||||
@override
|
@override
|
||||||
bool get useLibass;
|
bool get useLibass;
|
||||||
@override
|
@override
|
||||||
|
PlayerOptions? get playerOptions;
|
||||||
|
@override
|
||||||
double get internalVolume;
|
double get internalVolume;
|
||||||
@override
|
@override
|
||||||
Set<DeviceOrientation>? get allowedOrientations;
|
Set<DeviceOrientation>? get allowedOrientations;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ _$VideoPlayerSettingsModelImpl _$$VideoPlayerSettingsModelImplFromJson(
|
||||||
fillScreen: json['fillScreen'] as bool? ?? false,
|
fillScreen: json['fillScreen'] as bool? ?? false,
|
||||||
hardwareAccel: json['hardwareAccel'] as bool? ?? true,
|
hardwareAccel: json['hardwareAccel'] as bool? ?? true,
|
||||||
useLibass: json['useLibass'] as bool? ?? false,
|
useLibass: json['useLibass'] as bool? ?? false,
|
||||||
|
playerOptions:
|
||||||
|
$enumDecodeNullable(_$PlayerOptionsEnumMap, json['playerOptions']),
|
||||||
internalVolume: (json['internalVolume'] as num?)?.toDouble() ?? 100,
|
internalVolume: (json['internalVolume'] as num?)?.toDouble() ?? 100,
|
||||||
allowedOrientations: (json['allowedOrientations'] as List<dynamic>?)
|
allowedOrientations: (json['allowedOrientations'] as List<dynamic>?)
|
||||||
?.map((e) => $enumDecode(_$DeviceOrientationEnumMap, e))
|
?.map((e) => $enumDecode(_$DeviceOrientationEnumMap, e))
|
||||||
|
|
@ -33,6 +35,7 @@ Map<String, dynamic> _$$VideoPlayerSettingsModelImplToJson(
|
||||||
'fillScreen': instance.fillScreen,
|
'fillScreen': instance.fillScreen,
|
||||||
'hardwareAccel': instance.hardwareAccel,
|
'hardwareAccel': instance.hardwareAccel,
|
||||||
'useLibass': instance.useLibass,
|
'useLibass': instance.useLibass,
|
||||||
|
'playerOptions': _$PlayerOptionsEnumMap[instance.playerOptions],
|
||||||
'internalVolume': instance.internalVolume,
|
'internalVolume': instance.internalVolume,
|
||||||
'allowedOrientations': instance.allowedOrientations
|
'allowedOrientations': instance.allowedOrientations
|
||||||
?.map((e) => _$DeviceOrientationEnumMap[e]!)
|
?.map((e) => _$DeviceOrientationEnumMap[e]!)
|
||||||
|
|
@ -51,6 +54,11 @@ const _$BoxFitEnumMap = {
|
||||||
BoxFit.scaleDown: 'scaleDown',
|
BoxFit.scaleDown: 'scaleDown',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const _$PlayerOptionsEnumMap = {
|
||||||
|
PlayerOptions.libMDK: 'libMDK',
|
||||||
|
PlayerOptions.libMPV: 'libMPV',
|
||||||
|
};
|
||||||
|
|
||||||
const _$DeviceOrientationEnumMap = {
|
const _$DeviceOrientationEnumMap = {
|
||||||
DeviceOrientation.portraitUp: 'portraitUp',
|
DeviceOrientation.portraitUp: 'portraitUp',
|
||||||
DeviceOrientation.landscapeLeft: 'landscapeLeft',
|
DeviceOrientation.landscapeLeft: 'landscapeLeft',
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:ficonsax/ficonsax.dart';
|
import 'package:ficonsax/ficonsax.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/jellyfin/jellyfin_open_api.swagger.dart';
|
||||||
import 'package:fladder/models/items/chapters_model.dart';
|
import 'package:fladder/models/items/chapters_model.dart';
|
||||||
|
|
@ -73,8 +72,6 @@ class VideoPlayback {
|
||||||
ItemStreamModel? currentItem,
|
ItemStreamModel? currentItem,
|
||||||
SyncedItem? currentSyncedItem,
|
SyncedItem? currentSyncedItem,
|
||||||
VideoStream? currentStream,
|
VideoStream? currentStream,
|
||||||
Map<AudioStreamModel, AudioTrack>? audioMappings,
|
|
||||||
Map<SubStreamModel, SubtitleTrack>? subMappings,
|
|
||||||
}) {
|
}) {
|
||||||
return VideoPlayback(
|
return VideoPlayback(
|
||||||
queue: queue ?? this.queue,
|
queue: queue ?? this.queue,
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,31 @@
|
||||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
|
||||||
import 'package:fladder/profiles/web_profile.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
const DeviceProfile defaultProfile = kIsWeb
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||||
|
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||||
|
import 'package:fladder/profiles/web_profile.dart';
|
||||||
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
|
|
||||||
|
final videoProfileProvider = StateProvider.autoDispose<DeviceProfile>((ref) =>
|
||||||
|
defaultProfile(ref.read(videoPlayerProvider.select((value) => value.backend)) ?? PlayerOptions.platformDefaults));
|
||||||
|
|
||||||
|
DeviceProfile defaultProfile(PlayerOptions player) => kIsWeb
|
||||||
? webProfile
|
? webProfile
|
||||||
: DeviceProfile(
|
: DeviceProfile(
|
||||||
maxStreamingBitrate: 120000000,
|
maxStreamingBitrate: 120000000,
|
||||||
maxStaticBitrate: 120000000,
|
maxStaticBitrate: 120000000,
|
||||||
musicStreamingTranscodingBitrate: 384000,
|
musicStreamingTranscodingBitrate: 384000,
|
||||||
directPlayProfiles: [
|
directPlayProfiles: [
|
||||||
DirectPlayProfile(
|
const DirectPlayProfile(
|
||||||
type: DlnaProfileType.video,
|
type: DlnaProfileType.video,
|
||||||
),
|
),
|
||||||
DirectPlayProfile(
|
const DirectPlayProfile(
|
||||||
type: DlnaProfileType.audio,
|
type: DlnaProfileType.audio,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
transcodingProfiles: [
|
transcodingProfiles: [
|
||||||
TranscodingProfile(
|
const TranscodingProfile(
|
||||||
audioCodec: 'aac,mp3,mp2',
|
audioCodec: 'aac,mp3,mp2',
|
||||||
container: 'ts',
|
container: 'ts',
|
||||||
maxAudioChannels: '2',
|
maxAudioChannels: '2',
|
||||||
|
|
@ -28,8 +36,10 @@ const DeviceProfile defaultProfile = kIsWeb
|
||||||
],
|
],
|
||||||
containerProfiles: [],
|
containerProfiles: [],
|
||||||
subtitleProfiles: [
|
subtitleProfiles: [
|
||||||
SubtitleProfile(format: 'vtt', method: SubtitleDeliveryMethod.$external),
|
const SubtitleProfile(format: 'vtt', method: SubtitleDeliveryMethod.$external),
|
||||||
SubtitleProfile(format: 'ass', method: SubtitleDeliveryMethod.$external),
|
const SubtitleProfile(format: 'ass', method: SubtitleDeliveryMethod.$external),
|
||||||
SubtitleProfile(format: 'ssa', method: SubtitleDeliveryMethod.$external),
|
const SubtitleProfile(format: 'ssa', method: SubtitleDeliveryMethod.$external),
|
||||||
|
if (player == PlayerOptions.libMDK)
|
||||||
|
const SubtitleProfile(format: 'pgssub', method: SubtitleDeliveryMethod.$external),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,7 @@ class SeasonDetailsNotifier extends StateNotifier<SeasonModel?> {
|
||||||
season: newState?.season,
|
season: newState?.season,
|
||||||
fields: [ItemFields.overview],
|
fields: [ItemFields.overview],
|
||||||
);
|
);
|
||||||
newState = newState?.copyWith(
|
newState = newState?.copyWith(episodes: EpisodeModel.episodesFromDto(episodes.body?.items, ref).toList());
|
||||||
episodes: EpisodeModel.episodesFromDto(episodes.body?.items, ref)
|
|
||||||
.where((element) => element.season == newState?.season)
|
|
||||||
.toList());
|
|
||||||
state = newState;
|
state = newState;
|
||||||
return season;
|
return season;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -420,11 +420,11 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
||||||
|
|
||||||
final playbackResponse = await api.itemsItemIdPlaybackInfoPost(
|
final playbackResponse = await api.itemsItemIdPlaybackInfoPost(
|
||||||
itemId: syncItem.id,
|
itemId: syncItem.id,
|
||||||
body: const PlaybackInfoDto(
|
body: PlaybackInfoDto(
|
||||||
enableDirectPlay: true,
|
enableDirectPlay: true,
|
||||||
enableDirectStream: true,
|
enableDirectStream: true,
|
||||||
enableTranscoding: false,
|
enableTranscoding: false,
|
||||||
deviceProfile: defaultProfile,
|
deviceProfile: ref.read(videoProfileProvider),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/media_playback_model.dart';
|
import 'package:fladder/models/media_playback_model.dart';
|
||||||
import 'package:fladder/models/playback/playback_model.dart';
|
import 'package:fladder/models/playback/playback_model.dart';
|
||||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
import 'package:fladder/wrappers/media_control_wrapper.dart'
|
import 'package:fladder/wrappers/media_control_wrapper.dart';
|
||||||
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
|
|
||||||
final mediaPlaybackProvider = StateProvider<MediaPlaybackModel>((ref) => MediaPlaybackModel());
|
final mediaPlaybackProvider = StateProvider<MediaPlaybackModel>((ref) => MediaPlaybackModel());
|
||||||
|
|
||||||
|
|
@ -30,45 +26,37 @@ class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
|
||||||
|
|
||||||
late final mediaState = ref.read(mediaPlaybackProvider.notifier);
|
late final mediaState = ref.read(mediaPlaybackProvider.notifier);
|
||||||
|
|
||||||
bool initMediaControls = false;
|
MediaPlaybackModel get playbackState => ref.read(mediaPlaybackProvider);
|
||||||
|
|
||||||
void init() async {
|
void init() async {
|
||||||
state.player?.dispose();
|
await state.dispose();
|
||||||
if (!initMediaControls && !kDebugMode) {
|
await state.init();
|
||||||
state.init();
|
|
||||||
initMediaControls = true;
|
|
||||||
}
|
|
||||||
for (final s in subscriptions) {
|
for (final s in subscriptions) {
|
||||||
s.cancel();
|
s.cancel();
|
||||||
}
|
}
|
||||||
final player = state.setup();
|
|
||||||
if (player.platform is NativePlayer) {
|
final subscription = state.stateStream?.listen((value) {
|
||||||
await (player.platform as dynamic).setProperty(
|
mediaState.update((state) => state.copyWith(buffering: value.buffering));
|
||||||
'force-seekable',
|
mediaState.update((state) => state.copyWith(buffer: value.buffer));
|
||||||
'yes',
|
updatePlaying(value.playing);
|
||||||
);
|
updatePosition(value.position);
|
||||||
|
mediaState.update((state) => state.copyWith(duration: value.duration));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (subscription != null) {
|
||||||
|
subscriptions.add(subscription);
|
||||||
}
|
}
|
||||||
subscriptions.addAll(
|
|
||||||
[
|
|
||||||
player.stream.buffering.listen((event) => mediaState.update((state) => state.copyWith(buffering: event))),
|
|
||||||
player.stream.buffer.listen((event) => mediaState.update((state) => state.copyWith(buffer: event))),
|
|
||||||
player.stream.playing.listen((event) => updatePlaying(event)),
|
|
||||||
player.stream.position.listen((event) => updatePosition(event)),
|
|
||||||
player.stream.duration.listen((event) => mediaState.update((state) => state.copyWith(duration: event))),
|
|
||||||
].whereNotNull(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updatePlaying(bool event) async {
|
Future<void> updatePlaying(bool event) async {
|
||||||
final player = state.player;
|
if (!state.hasPlayer) return;
|
||||||
if (player == null) return;
|
|
||||||
mediaState.update((state) => state.copyWith(playing: event));
|
mediaState.update((state) => state.copyWith(playing: event));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updatePosition(Duration event) async {
|
Future<void> updatePosition(Duration event) async {
|
||||||
final player = state.player;
|
if (!state.hasPlayer) return;
|
||||||
if (player == null) return;
|
if (playbackState.playing == false) return;
|
||||||
if (!player.state.playing) return;
|
|
||||||
|
|
||||||
final position = event;
|
final position = event;
|
||||||
final lastPosition = ref.read(mediaPlaybackProvider.select((value) => value.lastPosition));
|
final lastPosition = ref.read(mediaPlaybackProvider.select((value) => value.lastPosition));
|
||||||
|
|
@ -77,16 +65,16 @@ class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
|
||||||
if (diff > const Duration(seconds: 1, milliseconds: 500).inMilliseconds) {
|
if (diff > const Duration(seconds: 1, milliseconds: 500).inMilliseconds) {
|
||||||
mediaState.update((value) => value.copyWith(
|
mediaState.update((value) => value.copyWith(
|
||||||
position: event,
|
position: event,
|
||||||
playing: player.state.playing,
|
playing: playbackState.playing,
|
||||||
duration: player.state.duration,
|
duration: playbackState.duration,
|
||||||
lastPosition: position,
|
lastPosition: position,
|
||||||
));
|
));
|
||||||
ref.read(playBackModel)?.updatePlaybackPosition(position, player.state.playing, ref);
|
ref.read(playBackModel)?.updatePlaybackPosition(position, playbackState.playing, ref);
|
||||||
} else {
|
} else {
|
||||||
mediaState.update((value) => value.copyWith(
|
mediaState.update((value) => value.copyWith(
|
||||||
position: event,
|
position: event,
|
||||||
playing: player.state.playing,
|
playing: playbackState.playing,
|
||||||
duration: player.state.duration,
|
duration: playbackState.duration,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -100,22 +88,24 @@ class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
|
||||||
PlaybackModel? newPlaybackModel = model;
|
PlaybackModel? newPlaybackModel = model;
|
||||||
|
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
|
await state.open(media.url, false);
|
||||||
await state.setVolume(ref.read(videoPlayerSettingsProvider).volume);
|
await state.setVolume(ref.read(videoPlayerSettingsProvider).volume);
|
||||||
await state.open(media, play: false);
|
state.stateStream?.takeWhile((event) => event.buffering == true).listen(
|
||||||
state.player?.stream.buffering.takeWhile((event) => event == true).listen(
|
|
||||||
null,
|
null,
|
||||||
onDone: () async {
|
onDone: () async {
|
||||||
final start = startPosition ?? await model.startDuration();
|
final start = startPosition ?? await model.startDuration();
|
||||||
if (start != null) {
|
if (start != null) {
|
||||||
await state.seek(start);
|
await state.seek(start);
|
||||||
}
|
}
|
||||||
newPlaybackModel = await newPlaybackModel?.setAudio(null, state);
|
await state.setAudioTrack(null, model);
|
||||||
newPlaybackModel = await newPlaybackModel?.setSubtitle(null, state);
|
await state.setSubtitleTrack(null, model);
|
||||||
ref.read(playBackModel.notifier).update((state) => newPlaybackModel);
|
|
||||||
state.play();
|
state.play();
|
||||||
|
ref.read(playBackModel.notifier).update((state) => newPlaybackModel);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
ref.read(playBackModel.notifier).update((state) => model);
|
ref.read(playBackModel.notifier).update((state) => model);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ class MediaStreamInformation extends ConsumerWidget {
|
||||||
_StreamOptionSelect(
|
_StreamOptionSelect(
|
||||||
label: Text(context.localized.audio),
|
label: Text(context.localized.audio),
|
||||||
current: mediaStream.currentAudioStream?.displayTitle ?? "",
|
current: mediaStream.currentAudioStream?.displayTitle ?? "",
|
||||||
itemBuilder: (context) => mediaStream.audioStreams
|
itemBuilder: (context) => [AudioStreamModel.no(), ...mediaStream.audioStreams]
|
||||||
.map(
|
.map(
|
||||||
(e) => PopupMenuItem(
|
(e) => PopupMenuItem(
|
||||||
value: e,
|
value: e,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import 'package:fladder/models/account_model.dart';
|
||||||
import 'package:fladder/providers/auth_provider.dart';
|
import 'package:fladder/providers/auth_provider.dart';
|
||||||
import 'package:fladder/screens/shared/flat_button.dart';
|
import 'package:fladder/screens/shared/flat_button.dart';
|
||||||
import 'package:fladder/screens/shared/user_icon.dart';
|
import 'package:fladder/screens/shared/user_icon.dart';
|
||||||
import 'package:fladder/util/adaptive_layout.dart';
|
|
||||||
import 'package:fladder/util/list_padding.dart';
|
import 'package:fladder/util/list_padding.dart';
|
||||||
|
|
||||||
class LoginUserGrid extends ConsumerWidget {
|
class LoginUserGrid extends ConsumerWidget {
|
||||||
|
|
@ -37,79 +36,87 @@ class LoginUserGrid extends ConsumerWidget {
|
||||||
itemCount: users.length,
|
itemCount: users.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final user = users[index];
|
final user = users[index];
|
||||||
return _CardHolder(
|
return FlatButton(
|
||||||
key: Key(user.id),
|
key: Key(user.id),
|
||||||
content: Stack(
|
onTap: () => editMode ? onLongPress?.call(user) : onPressed?.call(user),
|
||||||
children: [
|
onLongPress: () => onLongPress?.call(user),
|
||||||
Column(
|
child: _CardHolder(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
content: Stack(
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
Padding(
|
||||||
Flexible(
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: UserIcon(
|
child: Column(
|
||||||
labelStyle: Theme.of(context).textTheme.headlineMedium,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
user: user,
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
mainAxisSize: MainAxisSize.max,
|
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
|
||||||
user.authMethod.icon,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: UserIcon(
|
||||||
user.name,
|
labelStyle: Theme.of(context).textTheme.headlineMedium,
|
||||||
maxLines: 2,
|
user: user,
|
||||||
softWrap: true,
|
),
|
||||||
)),
|
),
|
||||||
],
|
Row(
|
||||||
),
|
|
||||||
if (user.credentials.serverName.isNotEmpty)
|
|
||||||
Opacity(
|
|
||||||
opacity: 0.75,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
Icon(
|
||||||
IconsaxBold.driver_2,
|
user.authMethod.icon,
|
||||||
size: 14,
|
size: 18,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
user.credentials.serverName,
|
user.name,
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
softWrap: true,
|
softWrap: true,
|
||||||
),
|
)),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
if (user.credentials.serverName.isNotEmpty)
|
||||||
].addInBetween(const SizedBox(width: 4, height: 4)),
|
Opacity(
|
||||||
),
|
opacity: 0.75,
|
||||||
if (editMode)
|
child: Row(
|
||||||
Align(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
alignment: Alignment.topRight,
|
mainAxisSize: MainAxisSize.max,
|
||||||
child: Card(
|
children: [
|
||||||
color: Theme.of(context).colorScheme.errorContainer,
|
const Icon(
|
||||||
child: const Padding(
|
IconsaxBold.driver_2,
|
||||||
padding: EdgeInsets.all(8.0),
|
size: 14,
|
||||||
child: Icon(
|
),
|
||||||
IconsaxBold.edit_2,
|
const SizedBox(width: 4),
|
||||||
size: 14,
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
user.credentials.serverName,
|
||||||
|
maxLines: 2,
|
||||||
|
softWrap: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
].addInBetween(const SizedBox(width: 4, height: 4)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (editMode)
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topRight,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Card(
|
||||||
|
color: Theme.of(context).colorScheme.errorContainer,
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: Icon(
|
||||||
|
IconsaxBold.edit_2,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
onTap: () => editMode ? onLongPress?.call(user) : onPressed?.call(user),
|
|
||||||
onLongPress: () => onLongPress?.call(user),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -118,13 +125,9 @@ class LoginUserGrid extends ConsumerWidget {
|
||||||
|
|
||||||
class _CardHolder extends StatelessWidget {
|
class _CardHolder extends StatelessWidget {
|
||||||
final Widget content;
|
final Widget content;
|
||||||
final Function() onTap;
|
|
||||||
final Function() onLongPress;
|
|
||||||
|
|
||||||
const _CardHolder({
|
const _CardHolder({
|
||||||
required this.content,
|
required this.content,
|
||||||
required this.onTap,
|
|
||||||
required this.onLongPress,
|
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -137,14 +140,7 @@ class _CardHolder extends StatelessWidget {
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150),
|
constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150),
|
||||||
child: FlatButton(
|
child: content,
|
||||||
onTap: onTap,
|
|
||||||
onLongPress: AdaptiveLayout.of(context).isDesktop ? onLongPress : null,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: content,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,24 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:ficonsax/ficonsax.dart';
|
|
||||||
import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
|
|
||||||
import 'package:fladder/util/fladder_image.dart';
|
|
||||||
import 'package:fladder/widgets/shared/fladder_slider.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
|
||||||
|
|
||||||
import 'package:fladder/models/items/photos_model.dart';
|
import 'package:ficonsax/ficonsax.dart';
|
||||||
import 'package:fladder/providers/user_provider.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:fladder/util/duration_extensions.dart';
|
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/items/photos_model.dart';
|
||||||
|
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||||
|
import 'package:fladder/providers/settings/photo_view_settings_provider.dart';
|
||||||
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
|
import 'package:fladder/providers/user_provider.dart';
|
||||||
|
import 'package:fladder/util/duration_extensions.dart';
|
||||||
|
import 'package:fladder/util/fladder_image.dart';
|
||||||
|
import 'package:fladder/widgets/shared/fladder_slider.dart';
|
||||||
|
import 'package:fladder/wrappers/players/lib_mdk.dart'
|
||||||
|
if (dart.library.html) 'package:fladder/stubs/web/lib_mdk_web.dart';
|
||||||
|
import 'package:fladder/wrappers/players/lib_mpv.dart';
|
||||||
|
|
||||||
class SimpleVideoPlayer extends ConsumerStatefulWidget {
|
class SimpleVideoPlayer extends ConsumerStatefulWidget {
|
||||||
final PhotoModel video;
|
final PhotoModel video;
|
||||||
final bool showOverlay;
|
final bool showOverlay;
|
||||||
|
|
@ -26,10 +30,10 @@ class SimpleVideoPlayer extends ConsumerStatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with WindowListener, WidgetsBindingObserver {
|
class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with WindowListener, WidgetsBindingObserver {
|
||||||
final Player player = Player(
|
late final player = switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) {
|
||||||
configuration: const PlayerConfiguration(title: "nl.jknaapen.fladder", libass: true),
|
PlayerOptions.libMDK => LibMDK(),
|
||||||
);
|
PlayerOptions.libMPV => LibMPV(),
|
||||||
late VideoController controller = VideoController(player);
|
};
|
||||||
late String videoUrl = "";
|
late String videoUrl = "";
|
||||||
|
|
||||||
bool playing = false;
|
bool playing = false;
|
||||||
|
|
@ -61,9 +65,9 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
||||||
super.initState();
|
super.initState();
|
||||||
windowManager.addListener(this);
|
windowManager.addListener(this);
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
playing = player.state.playing;
|
playing = player.lastState.playing;
|
||||||
position = player.state.position;
|
position = player.lastState.position;
|
||||||
duration = player.state.duration;
|
duration = player.lastState.duration;
|
||||||
Future.microtask(() async => {_init()});
|
Future.microtask(() async => {_init()});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,43 +88,21 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
||||||
|
|
||||||
videoUrl = '${ref.read(userProvider)?.server ?? ""}/Videos/${widget.video.id}/stream?$params';
|
videoUrl = '${ref.read(userProvider)?.server ?? ""}/Videos/${widget.video.id}/stream?$params';
|
||||||
|
|
||||||
subscriptions.addAll(
|
subscriptions.add(player.stateStream.listen((event) {
|
||||||
[
|
setState(() {
|
||||||
player.stream.playing.listen((event) {
|
playing = event.playing;
|
||||||
setState(() {
|
position = event.position;
|
||||||
playing = event;
|
duration = event.duration;
|
||||||
});
|
});
|
||||||
if (playing) {
|
if (playing) {
|
||||||
WakelockPlus.enable();
|
WakelockPlus.enable();
|
||||||
} else {
|
} else {
|
||||||
WakelockPlus.disable();
|
WakelockPlus.disable();
|
||||||
}
|
}
|
||||||
}),
|
}));
|
||||||
player.stream.position.listen((event) {
|
await player.open(videoUrl, !ref.watch(photoViewSettingsProvider).autoPlay);
|
||||||
setState(() {
|
await player.loop(ref.watch(photoViewSettingsProvider.select((value) => value.repeat)));
|
||||||
position = event;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
player.stream.completed.listen((event) {
|
|
||||||
if (event) {
|
|
||||||
_restartVideo();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
player.stream.duration.listen((event) {
|
|
||||||
setState(() {
|
|
||||||
duration = event;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
await player.setVolume(ref.watch(photoViewSettingsProvider.select((value) => value.mute)) ? 0 : 100);
|
await player.setVolume(ref.watch(photoViewSettingsProvider.select((value) => value.mute)) ? 0 : 100);
|
||||||
await player.open(Media(videoUrl), play: !ref.watch(photoViewSettingsProvider).autoPlay);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _restartVideo() {
|
|
||||||
if (ref.read(photoViewSettingsProvider.select((value) => value.repeat))) {
|
|
||||||
player.play();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -142,6 +124,9 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
||||||
.textTheme
|
.textTheme
|
||||||
.titleMedium
|
.titleMedium
|
||||||
?.copyWith(fontWeight: FontWeight.bold, shadows: [const Shadow(blurRadius: 2)]);
|
?.copyWith(fontWeight: FontWeight.bold, shadows: [const Shadow(blurRadius: 2)]);
|
||||||
|
ref.listen(photoViewSettingsProvider.select((value) => value.repeat), (previous, next) {
|
||||||
|
player.loop(next);
|
||||||
|
});
|
||||||
ref.listen(
|
ref.listen(
|
||||||
photoViewSettingsProvider.select((value) => value.mute),
|
photoViewSettingsProvider.select((value) => value.mute),
|
||||||
(previous, next) {
|
(previous, next) {
|
||||||
|
|
@ -165,13 +150,17 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
||||||
//Fixes small overlay problems with thumbnail
|
//Fixes small overlay problems with thumbnail
|
||||||
Transform.scale(
|
Transform.scale(
|
||||||
scaleY: 1.004,
|
scaleY: 1.004,
|
||||||
child: Video(
|
child: player.videoWidget(
|
||||||
fit: BoxFit.contain,
|
UniqueKey(),
|
||||||
fill: const Color.fromARGB(0, 123, 62, 62),
|
BoxFit.contain,
|
||||||
controller: controller,
|
|
||||||
controls: NoVideoControls,
|
|
||||||
wakelock: false,
|
|
||||||
),
|
),
|
||||||
|
// child: Video(
|
||||||
|
// fit: BoxFit.contain,
|
||||||
|
// fill: const Color.fromARGB(0, 123, 62, 62),
|
||||||
|
// controller: controller,
|
||||||
|
// controls: NoVideoControls,
|
||||||
|
// wakelock: false,
|
||||||
|
// ),
|
||||||
),
|
),
|
||||||
IgnorePointer(
|
IgnorePointer(
|
||||||
ignoring: !widget.showOverlay,
|
ignoring: !widget.showOverlay,
|
||||||
|
|
@ -211,7 +200,7 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onChangeStart: (value) {
|
onChangeStart: (value) {
|
||||||
wasPlaying = player.state.playing;
|
wasPlaying = player.lastState.playing;
|
||||||
player.pause();
|
player.pause();
|
||||||
},
|
},
|
||||||
onChanged: (e) {
|
onChanged: (e) {
|
||||||
|
|
@ -239,7 +228,7 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
||||||
player.playOrPause();
|
player.playOrPause();
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
player.state.playing ? IconsaxBold.pause_circle : IconsaxBold.play_circle,
|
player.lastState.playing ? IconsaxBold.pause_circle : IconsaxBold.play_circle,
|
||||||
shadows: [
|
shadows: [
|
||||||
BoxShadow(blurRadius: 16, spreadRadius: 2, color: Colors.black.withOpacity(0.15))
|
BoxShadow(blurRadius: 16, spreadRadius: 2, color: Colors.black.withOpacity(0.15))
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -82,34 +82,90 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
SettingsLabelDivider(label: context.localized.advanced),
|
SettingsLabelDivider(label: context.localized.advanced),
|
||||||
SettingsListTile(
|
if (PlayerOptions.available.length != 1)
|
||||||
label: Text(context.localized.settingsPlayerVideoHWAccelTitle),
|
|
||||||
subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc),
|
|
||||||
onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel),
|
|
||||||
trailing: Switch(
|
|
||||||
value: videoSettings.hardwareAccel,
|
|
||||||
onChanged: (value) => provider.setHardwareAccel(value),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (!kIsWeb) ...[
|
|
||||||
SettingsListTile(
|
SettingsListTile(
|
||||||
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
|
label: Text(context.localized.playerSettingsBackendTitle),
|
||||||
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
|
subLabel: Text(context.localized.playerSettingsBackendDesc),
|
||||||
onTap: () => provider.setUseLibass(!videoSettings.useLibass),
|
trailing: Builder(builder: (context) {
|
||||||
trailing: Switch(
|
final wantedPlayer = ref.watch(videoPlayerSettingsProvider.select((value) => value.wantedPlayer));
|
||||||
value: videoSettings.useLibass,
|
final currentPlayer = ref.watch(videoPlayerSettingsProvider.select((value) => value.playerOptions));
|
||||||
onChanged: (value) => provider.setUseLibass(value),
|
return EnumBox(
|
||||||
),
|
current: currentPlayer == null
|
||||||
),
|
? "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"
|
||||||
AnimatedFadeSize(
|
: wantedPlayer.label(context),
|
||||||
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
|
itemBuilder: (context) => [
|
||||||
? SettingsMessageBox(
|
PopupMenuItem(
|
||||||
context.localized.settingsPlayerMobileWarning,
|
value: null,
|
||||||
messageType: MessageType.warning,
|
child:
|
||||||
|
Text("${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"),
|
||||||
|
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||||
|
ref.read(videoPlayerSettingsProvider).copyWith(playerOptions: null),
|
||||||
|
),
|
||||||
|
...PlayerOptions.available.map(
|
||||||
|
(entry) => PopupMenuItem(
|
||||||
|
value: entry,
|
||||||
|
child: Text(entry.label(context)),
|
||||||
|
onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state =
|
||||||
|
ref.read(videoPlayerSettingsProvider).copyWith(playerOptions: entry),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
: Container(),
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
],
|
AnimatedFadeSize(
|
||||||
|
child: switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) {
|
||||||
|
PlayerOptions.libMPV => Column(
|
||||||
|
children: [
|
||||||
|
SettingsListTile(
|
||||||
|
label: Text(context.localized.settingsPlayerVideoHWAccelTitle),
|
||||||
|
subLabel: Text(context.localized.settingsPlayerVideoHWAccelDesc),
|
||||||
|
onTap: () => provider.setHardwareAccel(!videoSettings.hardwareAccel),
|
||||||
|
trailing: Switch(
|
||||||
|
value: videoSettings.hardwareAccel,
|
||||||
|
onChanged: (value) => provider.setHardwareAccel(value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!kIsWeb) ...[
|
||||||
|
SettingsListTile(
|
||||||
|
label: Text(context.localized.settingsPlayerNativeLibassAccelTitle),
|
||||||
|
subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc),
|
||||||
|
onTap: () => provider.setUseLibass(!videoSettings.useLibass),
|
||||||
|
trailing: Switch(
|
||||||
|
value: videoSettings.useLibass,
|
||||||
|
onChanged: (value) => provider.setUseLibass(value),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedFadeSize(
|
||||||
|
child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid
|
||||||
|
? SettingsMessageBox(
|
||||||
|
context.localized.settingsPlayerMobileWarning,
|
||||||
|
messageType: MessageType.warning,
|
||||||
|
)
|
||||||
|
: Container(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
SettingsListTile(
|
||||||
|
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
||||||
|
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
|
||||||
|
onTap: videoSettings.useLibass
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
useSafeArea: false,
|
||||||
|
builder: (context) => const SubtitleEditor(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
_ => SettingsMessageBox(
|
||||||
|
messageType: MessageType.info,
|
||||||
|
"${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}")
|
||||||
|
},
|
||||||
|
),
|
||||||
SettingsListTile(
|
SettingsListTile(
|
||||||
label: Text(context.localized.settingsAutoNextTitle),
|
label: Text(context.localized.settingsAutoNextTitle),
|
||||||
subLabel: Text(context.localized.settingsAutoNextDesc),
|
subLabel: Text(context.localized.settingsAutoNextDesc),
|
||||||
|
|
@ -138,20 +194,6 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
||||||
_ => const SizedBox.shrink(),
|
_ => const SizedBox.shrink(),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
SettingsListTile(
|
|
||||||
label: Text(context.localized.settingsPlayerCustomSubtitlesTitle),
|
|
||||||
subLabel: Text(context.localized.settingsPlayerCustomSubtitlesDesc),
|
|
||||||
onTap: videoSettings.useLibass
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierDismissible: false,
|
|
||||||
useSafeArea: false,
|
|
||||||
builder: (context) => const SubtitleEditor(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
|
if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb)
|
||||||
SettingsListTile(
|
SettingsListTile(
|
||||||
label: Text(context.localized.playerSettingsOrientationTitle),
|
label: Text(context.localized.playerSettingsOrientationTitle),
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/settings/subtitle_settings_model.dart';
|
import 'package:fladder/models/settings/subtitle_settings_model.dart';
|
||||||
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
||||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
|
|
@ -5,10 +10,6 @@ import 'package:fladder/screens/video_player/components/video_subtitle_controls.
|
||||||
import 'package:fladder/util/adaptive_layout.dart';
|
import 'package:fladder/util/adaptive_layout.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
import 'package:fladder/widgets/navigation_scaffold/components/fladder_appbar.dart';
|
import 'package:fladder/widgets/navigation_scaffold/components/fladder_appbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
// ignore: depend_on_referenced_packages
|
|
||||||
|
|
||||||
class SubtitleEditor extends ConsumerStatefulWidget {
|
class SubtitleEditor extends ConsumerStatefulWidget {
|
||||||
const SubtitleEditor({super.key});
|
const SubtitleEditor({super.key});
|
||||||
|
|
|
||||||
|
|
@ -14,35 +14,43 @@ class FlatButton extends ConsumerWidget {
|
||||||
final Color? splashColor;
|
final Color? splashColor;
|
||||||
final double elevation;
|
final double elevation;
|
||||||
final Clip clipBehavior;
|
final Clip clipBehavior;
|
||||||
const FlatButton(
|
const FlatButton({
|
||||||
{this.child,
|
this.child,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.onLongPress,
|
this.onLongPress,
|
||||||
this.onDoubleTap,
|
this.onDoubleTap,
|
||||||
this.onSecondaryTapDown,
|
this.onSecondaryTapDown,
|
||||||
this.borderRadiusGeometry,
|
this.borderRadiusGeometry,
|
||||||
this.splashColor,
|
this.splashColor,
|
||||||
this.elevation = 0,
|
this.elevation = 0,
|
||||||
this.clipBehavior = Clip.none,
|
this.clipBehavior = Clip.none,
|
||||||
super.key});
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Material(
|
return Stack(
|
||||||
color: Colors.transparent,
|
fit: StackFit.passthrough,
|
||||||
clipBehavior: clipBehavior,
|
children: [
|
||||||
borderRadius: borderRadiusGeometry ?? FladderTheme.defaultShape.borderRadius,
|
child ?? Container(),
|
||||||
elevation: 0,
|
Positioned.fill(
|
||||||
child: InkWell(
|
child: Material(
|
||||||
onTap: onTap,
|
color: Colors.transparent,
|
||||||
onLongPress: onLongPress,
|
clipBehavior: clipBehavior,
|
||||||
onDoubleTap: onDoubleTap,
|
borderRadius: borderRadiusGeometry ?? FladderTheme.defaultShape.borderRadius,
|
||||||
onSecondaryTapDown: onSecondaryTapDown,
|
elevation: 0,
|
||||||
borderRadius: borderRadiusGeometry ?? BorderRadius.circular(10),
|
child: InkWell(
|
||||||
splashColor: splashColor ?? Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
onTap: onTap,
|
||||||
splashFactory: InkSparkle.splashFactory,
|
onLongPress: onLongPress,
|
||||||
child: child ?? Container(),
|
onDoubleTap: onDoubleTap,
|
||||||
),
|
onSecondaryTapDown: onSecondaryTapDown,
|
||||||
|
borderRadius: borderRadiusGeometry ?? BorderRadius.circular(10),
|
||||||
|
splashColor: splashColor ?? Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||||
|
splashFactory: InkSparkle.splashFactory,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/playback/playback_model.dart';
|
import 'package:fladder/models/playback/playback_model.dart';
|
||||||
import 'package:fladder/providers/session_info_provider.dart';
|
import 'package:fladder/providers/session_info_provider.dart';
|
||||||
import 'package:fladder/providers/video_player_provider.dart';
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
import 'package:fladder/util/list_padding.dart';
|
import 'package:fladder/util/list_padding.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
|
|
||||||
Future<void> showVideoPlaybackInformation(BuildContext context) {
|
Future<void> showVideoPlaybackInformation(BuildContext context) {
|
||||||
return showDialog(
|
return showDialog(
|
||||||
|
|
@ -19,6 +22,7 @@ class _VideoPlaybackInformation extends ConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final playbackModel = ref.watch(playBackModel);
|
final playbackModel = ref.watch(playBackModel);
|
||||||
final sessionInfo = ref.watch(sessionInfoProvider);
|
final sessionInfo = ref.watch(sessionInfoProvider);
|
||||||
|
final backend = ref.read(videoPlayerProvider.select((value) => value.backend));
|
||||||
return Dialog(
|
return Dialog(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12.0),
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
|
@ -27,47 +31,81 @@ class _VideoPlaybackInformation extends ConsumerWidget {
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text("Playback information", style: Theme.of(context).textTheme.titleMedium),
|
Text("Player info", style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4),
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.80,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [const Text('backend: '), Text(backend?.label(context) ?? context.localized.unknown)],
|
||||||
|
)
|
||||||
|
].addPadding(const EdgeInsets.symmetric(vertical: 3)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
...[
|
Text("Playback information", style: Theme.of(context).textTheme.titleMedium),
|
||||||
Row(
|
const SizedBox(height: 4),
|
||||||
mainAxisSize: MainAxisSize.min,
|
Padding(
|
||||||
children: [const Text('type: '), Text(playbackModel.label ?? "")],
|
padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4),
|
||||||
),
|
child: Opacity(
|
||||||
if (sessionInfo.transCodeInfo != null) ...[
|
opacity: 0.8,
|
||||||
const SizedBox(height: 6),
|
child: Column(
|
||||||
Text("Transcoding", style: Theme.of(context).textTheme.titleMedium),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true)
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [const Text('reason: '), Text(sessionInfo.transCodeInfo?.transcodeReasons.toString() ?? "")],
|
children: [const Text('type: '), Text(playbackModel.label ?? "")],
|
||||||
),
|
),
|
||||||
if (sessionInfo.transCodeInfo?.completionPercentage != null)
|
if (sessionInfo.transCodeInfo != null) ...[
|
||||||
Row(
|
Text("Transcoding", style: Theme.of(context).textTheme.titleMedium),
|
||||||
mainAxisSize: MainAxisSize.min,
|
if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true)
|
||||||
children: [
|
Row(
|
||||||
const Text('transcode progress: '),
|
mainAxisSize: MainAxisSize.min,
|
||||||
Text("${sessionInfo.transCodeInfo?.completionPercentage?.toStringAsFixed(2)} %")
|
children: [
|
||||||
|
const Text('reason: '),
|
||||||
|
Text(sessionInfo.transCodeInfo?.transcodeReasons.toString() ?? "")
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (sessionInfo.transCodeInfo?.completionPercentage != null)
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text('transcode progress: '),
|
||||||
|
Text("${sessionInfo.transCodeInfo?.completionPercentage?.toStringAsFixed(2)} %")
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (sessionInfo.transCodeInfo?.container != null)
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text('container: '),
|
||||||
|
Text(sessionInfo.transCodeInfo!.container.toString())
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
Row(
|
||||||
if (sessionInfo.transCodeInfo?.container != null)
|
mainAxisSize: MainAxisSize.min,
|
||||||
Row(
|
children: [
|
||||||
mainAxisSize: MainAxisSize.min,
|
const Text('resolution: '),
|
||||||
children: [const Text('container: '), Text(sessionInfo.transCodeInfo!.container.toString())],
|
Text(playbackModel?.item.streamModel?.resolutionText ?? "")
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [const Text('resolution: '), Text(playbackModel?.item.streamModel?.resolutionText ?? "")],
|
children: [
|
||||||
|
const Text('container: '),
|
||||||
|
Text(playbackModel?.playbackInfo?.mediaSources?.firstOrNull?.container ?? "")
|
||||||
|
],
|
||||||
|
)
|
||||||
|
].addPadding(const EdgeInsets.symmetric(vertical: 3)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Row(
|
),
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Text('container: '),
|
|
||||||
Text(playbackModel?.playbackInfo?.mediaSources?.firstOrNull?.container ?? "")
|
|
||||||
],
|
|
||||||
),
|
|
||||||
].addPadding(const EdgeInsets.symmetric(vertical: 3))
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
|
|
||||||
import 'package:fladder/providers/video_player_provider.dart';
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
import 'package:fladder/screens/video_player/components/video_player_chapters.dart';
|
import 'package:fladder/screens/video_player/components/video_player_chapters.dart';
|
||||||
|
|
@ -9,8 +8,7 @@ import 'package:fladder/screens/video_player/components/video_player_queue.dart'
|
||||||
|
|
||||||
class ChapterButton extends ConsumerWidget {
|
class ChapterButton extends ConsumerWidget {
|
||||||
final Duration position;
|
final Duration position;
|
||||||
final Player player;
|
const ChapterButton({super.key, required this.position});
|
||||||
const ChapterButton({super.key, required this.position, required this.player});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -22,9 +20,9 @@ class ChapterButton extends ConsumerWidget {
|
||||||
context,
|
context,
|
||||||
chapters: currentChapters,
|
chapters: currentChapters,
|
||||||
currentPosition: position,
|
currentPosition: position,
|
||||||
onChapterTapped: (chapter) => player.seek(
|
onChapterTapped: (chapter) => ref.read(videoPlayerProvider).seek(
|
||||||
chapter.startPosition,
|
chapter.startPosition,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
|
|
|
||||||
|
|
@ -447,13 +447,13 @@ class _SimpleControls extends ConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final player = ref.watch(videoPlayerProvider.select((value) => value.controller?.player));
|
final player = ref.watch(videoPlayerProvider);
|
||||||
final isPlaying = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
|
final isPlaying = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
IconButton.filledTonal(
|
IconButton.filledTonal(
|
||||||
onPressed: () => player?.playOrPause(),
|
onPressed: () => player.playOrPause(),
|
||||||
icon: Icon(isPlaying ? IconsaxBold.pause : IconsaxBold.play),
|
icon: Icon(isPlaying ? IconsaxBold.pause : IconsaxBold.play),
|
||||||
),
|
),
|
||||||
if (skip != null)
|
if (skip != null)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import 'package:fladder/models/playback/direct_playback_model.dart';
|
||||||
import 'package:fladder/models/playback/offline_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/playback/transcode_playback_model.dart';
|
||||||
|
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
import 'package:fladder/providers/user_provider.dart';
|
import 'package:fladder/providers/user_provider.dart';
|
||||||
import 'package:fladder/providers/video_player_provider.dart';
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
|
|
@ -375,15 +376,16 @@ Future<void> showSubSelection(BuildContext context) {
|
||||||
children: [
|
children: [
|
||||||
Text(context.localized.subtitle),
|
Text(context.localized.subtitle),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton.outlined(
|
if (player.backend == PlayerOptions.libMPV)
|
||||||
onPressed: () {
|
IconButton.outlined(
|
||||||
Navigator.pop(context);
|
onPressed: () {
|
||||||
showSubtitleControls(
|
Navigator.pop(context);
|
||||||
context: context,
|
showSubtitleControls(
|
||||||
label: context.localized.subtitleConfiguration,
|
context: context,
|
||||||
);
|
label: context.localized.subtitleConfiguration,
|
||||||
},
|
);
|
||||||
icon: const Icon(Icons.display_settings_rounded))
|
},
|
||||||
|
icon: const Icon(Icons.display_settings_rounded))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
children: playbackModel?.subStreams?.mapIndexed(
|
children: playbackModel?.subStreams?.mapIndexed(
|
||||||
|
|
@ -459,7 +461,7 @@ Future<void> showPlaybackSpeed(BuildContext context) {
|
||||||
return StatefulBuilder(builder: (context, setState) {
|
return StatefulBuilder(builder: (context, setState) {
|
||||||
return Consumer(
|
return Consumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final player = ref.watch(videoPlayerProvider.select((value) => value.player));
|
final player = ref.watch(videoPlayerProvider);
|
||||||
final lastSpeed = ref.watch(playbackRateProvider);
|
final lastSpeed = ref.watch(playbackRateProvider);
|
||||||
return SimpleDialog(
|
return SimpleDialog(
|
||||||
contentPadding: const EdgeInsets.only(top: 8, bottom: 24),
|
contentPadding: const EdgeInsets.only(top: 8, bottom: 24),
|
||||||
|
|
@ -484,7 +486,7 @@ Future<void> showPlaybackSpeed(BuildContext context) {
|
||||||
divisions: 39,
|
divisions: 39,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
ref.read(playbackRateProvider.notifier).state = value;
|
ref.read(playbackRateProvider.notifier).state = value;
|
||||||
player?.setRate(value);
|
player.setSpeed(value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ class _ChapterProgressSliderState extends ConsumerState<VideoProgressBar> {
|
||||||
setState(() {
|
setState(() {
|
||||||
onHoverStart = true;
|
onHoverStart = true;
|
||||||
});
|
});
|
||||||
widget.wasPlayingChanged.call(player.player?.state.playing ?? false);
|
widget.wasPlayingChanged.call(player.lastState?.playing ?? false);
|
||||||
player.pause();
|
player.pause();
|
||||||
},
|
},
|
||||||
onChanged: (e) {
|
onChanged: (e) {
|
||||||
|
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
|
||||||
|
|
||||||
import 'package:fladder/models/settings/subtitle_settings_model.dart';
|
|
||||||
|
|
||||||
class VideoSubtitles extends ConsumerStatefulWidget {
|
|
||||||
final VideoController controller;
|
|
||||||
final bool overLayed;
|
|
||||||
const VideoSubtitles({
|
|
||||||
required this.controller,
|
|
||||||
this.overLayed = false,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<ConsumerStatefulWidget> createState() => _VideoSubtitlesState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VideoSubtitlesState extends ConsumerState<VideoSubtitles> {
|
|
||||||
late List<String> subtitle = widget.controller.player.state.subtitle;
|
|
||||||
StreamSubscription<List<String>>? subscription;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
subscription = widget.controller.player.stream.subtitle.listen((value) {
|
|
||||||
setState(() {
|
|
||||||
subtitle = value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
super.initState();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
subscription?.cancel();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final settings = ref.watch(subtitleSettingsProvider);
|
|
||||||
final padding = MediaQuery.of(context).padding;
|
|
||||||
final text = [
|
|
||||||
for (final line in subtitle)
|
|
||||||
if (line.trim().isNotEmpty) line.trim(),
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
if (widget.controller.player.platform?.configuration.libass ?? false) {
|
|
||||||
return const IgnorePointer(child: SizedBox());
|
|
||||||
} else {
|
|
||||||
return SubtitleText(
|
|
||||||
subModel: settings,
|
|
||||||
padding: padding,
|
|
||||||
offset: (widget.overLayed ? 0.5 : settings.verticalOffset),
|
|
||||||
text: text,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
|
||||||
|
|
||||||
import 'package:fladder/models/media_playback_model.dart';
|
import 'package:fladder/models/media_playback_model.dart';
|
||||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
|
|
@ -73,7 +72,7 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
|
||||||
final videoFit = ref.watch(videoPlayerSettingsProvider.select((value) => value.videoFit));
|
final videoFit = ref.watch(videoPlayerSettingsProvider.select((value) => value.videoFit));
|
||||||
final padding = MediaQuery.of(context).padding;
|
final padding = MediaQuery.of(context).padding;
|
||||||
|
|
||||||
final playerController = ref.watch(videoPlayerProvider.select((value) => value.controller));
|
final playerController = ref.watch(videoPlayerProvider.select((value) => value));
|
||||||
|
|
||||||
ref.listen(
|
ref.listen(
|
||||||
videoPlayerSettingsProvider.select((value) => value.allowedOrientations),
|
videoPlayerSettingsProvider.select((value) => value.allowedOrientations),
|
||||||
|
|
@ -103,24 +102,15 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
|
||||||
lastScale = 0.0;
|
lastScale = 0.0;
|
||||||
},
|
},
|
||||||
child: VideoPlayerNextWrapper(
|
child: VideoPlayerNextWrapper(
|
||||||
video: playerController != null
|
video: Padding(
|
||||||
? Padding(
|
padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right),
|
||||||
padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right),
|
child: playerController.videoWidget(
|
||||||
child: OrientationBuilder(builder: (context, orientation) {
|
const Key("VideoPlayer"),
|
||||||
return Video(
|
fillScreen
|
||||||
key: Key(orientation.toString()),
|
? (MediaQuery.of(context).orientation == Orientation.portrait ? videoFit : BoxFit.cover)
|
||||||
controller: playerController,
|
: videoFit,
|
||||||
fill: Colors.transparent,
|
),
|
||||||
wakelock: true,
|
),
|
||||||
fit: fillScreen
|
|
||||||
? (MediaQuery.of(context).orientation == Orientation.portrait ? videoFit : BoxFit.cover)
|
|
||||||
: videoFit,
|
|
||||||
subtitleViewConfiguration: const SubtitleViewConfiguration(visible: false),
|
|
||||||
controls: NoVideoControls,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
controls: const DesktopControls(),
|
controls: const DesktopControls(),
|
||||||
overlays: [
|
overlays: [
|
||||||
if (errorPlaying) const _VideoErrorWidget(),
|
if (errorPlaying) const _VideoErrorWidget(),
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
@ -23,7 +22,6 @@ import 'package:fladder/screens/video_player/components/video_player_controls_ex
|
||||||
import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart';
|
import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart';
|
||||||
import 'package:fladder/screens/video_player/components/video_player_seek_indicator.dart';
|
import 'package:fladder/screens/video_player/components/video_player_seek_indicator.dart';
|
||||||
import 'package:fladder/screens/video_player/components/video_progress_bar.dart';
|
import 'package:fladder/screens/video_player/components/video_progress_bar.dart';
|
||||||
import 'package:fladder/screens/video_player/components/video_subtitles.dart';
|
|
||||||
import 'package:fladder/screens/video_player/components/video_volume_slider.dart';
|
import 'package:fladder/screens/video_player/components/video_volume_slider.dart';
|
||||||
import 'package:fladder/util/adaptive_layout.dart';
|
import 'package:fladder/util/adaptive_layout.dart';
|
||||||
import 'package:fladder/util/duration_extensions.dart';
|
import 'package:fladder/util/duration_extensions.dart';
|
||||||
|
|
@ -31,8 +29,7 @@ import 'package:fladder/util/input_handler.dart';
|
||||||
import 'package:fladder/util/list_padding.dart';
|
import 'package:fladder/util/list_padding.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
import 'package:fladder/util/string_extensions.dart';
|
import 'package:fladder/util/string_extensions.dart';
|
||||||
import 'package:fladder/widgets/shared/full_screen_button.dart'
|
import 'package:fladder/widgets/shared/full_screen_button.dart';
|
||||||
if (dart.library.html) 'package:fladder/widgets/shared/full_screen_button_web.dart';
|
|
||||||
|
|
||||||
class DesktopControls extends ConsumerStatefulWidget {
|
class DesktopControls extends ConsumerStatefulWidget {
|
||||||
const DesktopControls({super.key});
|
const DesktopControls({super.key});
|
||||||
|
|
@ -115,7 +112,8 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final mediaSegments = ref.watch(playBackModel.select((value) => value?.mediaSegments));
|
final mediaSegments = ref.watch(playBackModel.select((value) => value?.mediaSegments));
|
||||||
final player = ref.watch(videoPlayerProvider.select((value) => value.controller));
|
final player = ref.watch(videoPlayerProvider);
|
||||||
|
final subtitleWidget = player.subtitleWidget(showOverlay);
|
||||||
return InputHandler(
|
return InputHandler(
|
||||||
autoFocus: false,
|
autoFocus: false,
|
||||||
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
|
onKeyEvent: (node, event) => _onKey(event) ? KeyEventResult.handled : KeyEventResult.ignored,
|
||||||
|
|
@ -135,12 +133,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
onHover: AdaptiveLayout.of(context).isDesktop ? (event) => toggleOverlay(value: true) : null,
|
onHover: AdaptiveLayout.of(context).isDesktop ? (event) => toggleOverlay(value: true) : null,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
if (player != null)
|
if (subtitleWidget != null) subtitleWidget,
|
||||||
VideoSubtitles(
|
|
||||||
key: const Key('subtitles'),
|
|
||||||
controller: player,
|
|
||||||
overLayed: showOverlay,
|
|
||||||
),
|
|
||||||
if (AdaptiveLayout.of(context).isDesktop)
|
if (AdaptiveLayout.of(context).isDesktop)
|
||||||
Consumer(builder: (context, ref, child) {
|
Consumer(builder: (context, ref, child) {
|
||||||
final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
|
final playing = ref.watch(mediaPlaybackProvider.select((value) => value.playing));
|
||||||
|
|
@ -385,7 +378,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
onPressed: () => closePlayer(), icon: const Icon(IconsaxOutline.close_square))),
|
onPressed: () => closePlayer(), icon: const Icon(IconsaxOutline.close_square))),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer &&
|
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer &&
|
||||||
ref.read(videoPlayerProvider).player != null) ...{
|
ref.read(videoPlayerProvider).hasPlayer) ...{
|
||||||
// OpenQueueButton(x),
|
// OpenQueueButton(x),
|
||||||
// ChapterButton(
|
// ChapterButton(
|
||||||
// position: position,
|
// position: position,
|
||||||
|
|
@ -471,7 +464,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}
|
},
|
||||||
].addPadding(const EdgeInsets.symmetric(horizontal: 4)),
|
].addPadding(const EdgeInsets.symmetric(horizontal: 4)),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|
|
||||||
75
lib/stubs/web/lib_mdk_web.dart
Normal file
75
lib/stubs/web/lib_mdk_web.dart
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/items/media_streams_model.dart';
|
||||||
|
import 'package:fladder/models/playback/playback_model.dart';
|
||||||
|
import 'package:fladder/wrappers/players/base_player.dart';
|
||||||
|
import 'package:fladder/wrappers/players/player_states.dart';
|
||||||
|
|
||||||
|
// This is a stub class that provides the same method signatures as the original
|
||||||
|
// implementation, ensuring web builds compile without requiring changes elsewhere.
|
||||||
|
class LibMDK extends BasePlayer {
|
||||||
|
final StreamController<PlayerState> _stateController = StreamController.broadcast();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<PlayerState> get stateStream => _stateController.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> init(Ref ref) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> open(String url, bool play) async {}
|
||||||
|
|
||||||
|
void setState(PlayerState state) {}
|
||||||
|
|
||||||
|
void updateState() {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> pause() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> play() async {}
|
||||||
|
@override
|
||||||
|
Future<void> playOrPause() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> seek(Duration position) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> setAudioTrack(AudioStreamModel? model, PlaybackModel playbackModel) async {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setSpeed(double speed) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> stop() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? videoWidget(
|
||||||
|
Key key,
|
||||||
|
BoxFit fit,
|
||||||
|
) =>
|
||||||
|
null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? subtitles(bool showOverlay) => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setVolume(double volume) async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> loop(bool loop) async {}
|
||||||
|
}
|
||||||
195
lib/stubs/web/smtc_web.dart
Normal file
195
lib/stubs/web/smtc_web.dart
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
// ignore_for_file: implementation_imports, constant_identifier_names
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
// This is a stub class that provides the same method signatures as the original
|
||||||
|
// implementation, ensuring web builds compile without requiring changes elsewhere.
|
||||||
|
class SMTCWindows {
|
||||||
|
SMTCWindows({
|
||||||
|
SMTCConfig? config,
|
||||||
|
PlaybackTimeline? timeline,
|
||||||
|
MusicMetadata? metadata,
|
||||||
|
PlaybackStatus? status,
|
||||||
|
bool? shuffleEnabled,
|
||||||
|
RepeatMode? repeatMode,
|
||||||
|
bool? enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
get buttonPressStream => null;
|
||||||
|
|
||||||
|
Future<void> updateConfig(SMTCConfig config) async {}
|
||||||
|
|
||||||
|
Future<void> updateTimeline(PlaybackTimeline timeline) async {}
|
||||||
|
|
||||||
|
Future<void> updateMetadata(MusicMetadata metadata) async {}
|
||||||
|
|
||||||
|
Future<void> clearMetadata() async {}
|
||||||
|
|
||||||
|
Future<void> dispose() async {}
|
||||||
|
|
||||||
|
Future<void> disableSmtc() async {}
|
||||||
|
|
||||||
|
Future<void> enableSmtc() async {}
|
||||||
|
|
||||||
|
Future<void> setPlaybackStatus(PlaybackStatus status) async {}
|
||||||
|
|
||||||
|
Future<void> setIsPlayEnabled(bool enabled) async {}
|
||||||
|
|
||||||
|
Future<void> setIsPauseEnabled(bool enabled) async {}
|
||||||
|
|
||||||
|
Future<void> setIsStopEnabled(bool enabled) async {}
|
||||||
|
|
||||||
|
Future<void> setIsNextEnabled(bool enabled) async {}
|
||||||
|
|
||||||
|
Future<void> setIsPrevEnabled(bool enabled) async {}
|
||||||
|
|
||||||
|
Future<void> setIsFastForwardEnabled(bool enabled) async {}
|
||||||
|
|
||||||
|
Future<void> setIsRewindEnabled(bool enabled) async {}
|
||||||
|
|
||||||
|
Future<void> setTimeline(PlaybackTimeline timeline) async {}
|
||||||
|
|
||||||
|
Future<void> setTitle(String title) async {}
|
||||||
|
|
||||||
|
Future<void> setArtist(String artist) async {}
|
||||||
|
|
||||||
|
Future<void> setAlbum(String album) async {}
|
||||||
|
|
||||||
|
Future<void> setAlbumArtist(String albumArtist) async {}
|
||||||
|
|
||||||
|
Future<void> setThumbnail(String thumbnail) async {}
|
||||||
|
|
||||||
|
Future<void> setPosition(Duration position) async {}
|
||||||
|
|
||||||
|
Future<void> setStartTime(Duration startTime) async {}
|
||||||
|
|
||||||
|
Future<void> setEndTime(Duration endTime) async {}
|
||||||
|
|
||||||
|
Future<void> setMaxSeekTime(Duration maxSeekTime) async {}
|
||||||
|
|
||||||
|
Future<void> setMinSeekTime(Duration minSeekTime) async {}
|
||||||
|
|
||||||
|
Future<void> setShuffleEnabled(bool enabled) async {}
|
||||||
|
|
||||||
|
Future<void> setRepeatMode(RepeatMode repeatMode) async {}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MusicMetadata {
|
||||||
|
final String? title;
|
||||||
|
final String? artist;
|
||||||
|
final String? album;
|
||||||
|
final String? albumArtist;
|
||||||
|
final String? thumbnail;
|
||||||
|
|
||||||
|
const MusicMetadata({
|
||||||
|
this.title,
|
||||||
|
this.artist,
|
||||||
|
this.album,
|
||||||
|
this.albumArtist,
|
||||||
|
this.thumbnail,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PlaybackStatus {
|
||||||
|
Closed,
|
||||||
|
Changing,
|
||||||
|
Stopped,
|
||||||
|
Playing,
|
||||||
|
Paused,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PressedButton {
|
||||||
|
play,
|
||||||
|
pause,
|
||||||
|
next,
|
||||||
|
previous,
|
||||||
|
fastForward,
|
||||||
|
rewind,
|
||||||
|
stop,
|
||||||
|
record,
|
||||||
|
channelUp,
|
||||||
|
channelDown;
|
||||||
|
|
||||||
|
static PressedButton fromString(String button) {
|
||||||
|
switch (button) {
|
||||||
|
case 'play':
|
||||||
|
return PressedButton.play;
|
||||||
|
case 'pause':
|
||||||
|
return PressedButton.pause;
|
||||||
|
case 'next':
|
||||||
|
return PressedButton.next;
|
||||||
|
case 'previous':
|
||||||
|
return PressedButton.previous;
|
||||||
|
case 'fast_forward':
|
||||||
|
return PressedButton.fastForward;
|
||||||
|
case 'rewind':
|
||||||
|
return PressedButton.rewind;
|
||||||
|
case 'stop':
|
||||||
|
return PressedButton.stop;
|
||||||
|
case 'record':
|
||||||
|
return PressedButton.record;
|
||||||
|
case 'channel_up':
|
||||||
|
return PressedButton.channelUp;
|
||||||
|
case 'channel_down':
|
||||||
|
return PressedButton.channelDown;
|
||||||
|
default:
|
||||||
|
throw Exception('Unknown button: $button');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlaybackTimeline {
|
||||||
|
final int startTimeMs;
|
||||||
|
final int endTimeMs;
|
||||||
|
final int positionMs;
|
||||||
|
final int? minSeekTimeMs;
|
||||||
|
final int? maxSeekTimeMs;
|
||||||
|
|
||||||
|
const PlaybackTimeline({
|
||||||
|
required this.startTimeMs,
|
||||||
|
required this.endTimeMs,
|
||||||
|
required this.positionMs,
|
||||||
|
this.minSeekTimeMs,
|
||||||
|
this.maxSeekTimeMs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SMTCConfig {
|
||||||
|
final bool playEnabled;
|
||||||
|
final bool pauseEnabled;
|
||||||
|
final bool stopEnabled;
|
||||||
|
final bool nextEnabled;
|
||||||
|
final bool prevEnabled;
|
||||||
|
final bool fastForwardEnabled;
|
||||||
|
final bool rewindEnabled;
|
||||||
|
|
||||||
|
const SMTCConfig({
|
||||||
|
required this.playEnabled,
|
||||||
|
required this.pauseEnabled,
|
||||||
|
required this.stopEnabled,
|
||||||
|
required this.nextEnabled,
|
||||||
|
required this.prevEnabled,
|
||||||
|
required this.fastForwardEnabled,
|
||||||
|
required this.rewindEnabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RepeatMode {
|
||||||
|
none,
|
||||||
|
track,
|
||||||
|
list;
|
||||||
|
|
||||||
|
static RepeatMode fromString(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'none':
|
||||||
|
return none;
|
||||||
|
case 'track':
|
||||||
|
return track;
|
||||||
|
case 'list':
|
||||||
|
return list;
|
||||||
|
default:
|
||||||
|
throw Exception('Unknown repeat mode: $value');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get asString => toString().split('.').last;
|
||||||
|
}
|
||||||
|
|
@ -199,7 +199,7 @@ extension ItemBaseModelExtensions on ItemBaseModel? {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
PhotoAlbumModel album => album.play(context, ref),
|
PhotoAlbumModel album => album.play(context, ref),
|
||||||
BookModel book => book.play(context, ref),
|
BookModel book => book.play(context, ref),
|
||||||
_ => _default(context, this, ref, startPosition: startPosition),
|
_ => _default(context, this, ref, startPosition: startPosition, showPlaybackOption: showPlaybackOption),
|
||||||
};
|
};
|
||||||
|
|
||||||
Future<void> _default(
|
Future<void> _default(
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fladder/models/items/media_streams_model.dart';
|
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
import 'dart:io' show Platform;
|
|
||||||
|
|
||||||
extension PlayerExtensions on Player {
|
|
||||||
Future<void> addSubtitles(List<SubStreamModel> subtitles) async {
|
|
||||||
final separator = Platform.isWindows ? ";" : ":";
|
|
||||||
await (platform as NativePlayer).setProperty(
|
|
||||||
"sub-files",
|
|
||||||
subtitles
|
|
||||||
.mapIndexed((index, e) => "${Platform.isWindows ? e.url : e.url?.replaceFirst(":", "\\:")}@${e.displayTitle}")
|
|
||||||
.join(separator),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import 'package:fladder/models/items/media_streams_model.dart';
|
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
|
|
||||||
extension PlayerExtensions on Player {
|
|
||||||
Future<void> addSubtitles(List<SubStreamModel> subtitles) async {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
import 'package:validators/validators.dart';
|
|
||||||
import 'string_extensions.dart';
|
|
||||||
|
|
||||||
extension SubtitleExtension on SubtitleTrack {
|
|
||||||
String get cleanName {
|
|
||||||
final names = {
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
};
|
|
||||||
return names
|
|
||||||
.where((element) => element != null)
|
|
||||||
.map((e) {
|
|
||||||
if (e == null) return e;
|
|
||||||
if (isNumeric(e)) return '';
|
|
||||||
if (e == "no") {
|
|
||||||
return "Off";
|
|
||||||
}
|
|
||||||
return e.capitalize();
|
|
||||||
})
|
|
||||||
.where((element) => element != null && element.isNotEmpty)
|
|
||||||
.join(" - ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AudioTrackExtension on AudioTrack {
|
|
||||||
String get cleanName {
|
|
||||||
final names = {
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
};
|
|
||||||
return names
|
|
||||||
.where((element) => element != null)
|
|
||||||
.map((e) {
|
|
||||||
if (e == null) return e;
|
|
||||||
if (isNumeric(e)) return '';
|
|
||||||
if (e == "no") {
|
|
||||||
return "Off";
|
|
||||||
}
|
|
||||||
return e.capitalize();
|
|
||||||
})
|
|
||||||
.where((element) => element != null && element.isNotEmpty)
|
|
||||||
.join(" - ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,6 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:ficonsax/ficonsax.dart';
|
import 'package:ficonsax/ficonsax.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/media_playback_model.dart';
|
import 'package:fladder/models/media_playback_model.dart';
|
||||||
|
|
@ -103,12 +102,11 @@ class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
||||||
children: [
|
children: [
|
||||||
Hero(
|
Hero(
|
||||||
tag: videoPlayerHeroTag,
|
tag: videoPlayerHeroTag,
|
||||||
child: Video(
|
child: player.videoWidget(
|
||||||
controller: player.controller!,
|
UniqueKey(),
|
||||||
fit: BoxFit.fitHeight,
|
BoxFit.fitHeight,
|
||||||
controls: NoVideoControls,
|
) ??
|
||||||
wakelock: playbackInfo.playing,
|
const SizedBox.shrink(),
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
|
|
@ -169,8 +167,7 @@ class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
||||||
if (constraints.maxWidth > 500) ...{
|
if (constraints.maxWidth > 500) ...{
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final volume = player.player?.state.volume == 0 ? 100.0 : 0.0;
|
final volume = player.lastState?.volume == 0 ? 100.0 : 0.0;
|
||||||
ref.read(videoPlayerSettingsProvider.notifier).setVolume(volume);
|
|
||||||
player.setVolume(volume);
|
player.setVolume(volume);
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
|
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
import 'package:audio_service/audio_service.dart';
|
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
|
|
||||||
AudioServiceConfig get audioServiceConfig => const AudioServiceConfig(
|
|
||||||
androidNotificationChannelId: 'nl.jknaapen.fladder.channel.playback',
|
|
||||||
androidNotificationChannelName: 'Video playback',
|
|
||||||
androidNotificationOngoing: true,
|
|
||||||
androidStopForegroundOnPause: true,
|
|
||||||
rewindInterval: Duration(seconds: 10),
|
|
||||||
fastForwardInterval: Duration(seconds: 15),
|
|
||||||
androidNotificationChannelDescription: "Playback",
|
|
||||||
androidShowNotificationBadge: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
abstract class MediaControlBase {
|
|
||||||
Future<void> init() {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Player setup() {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> seek(Duration position) {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> play() {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fastForward() {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> rewind() {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setSpeed(double speed) {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pause() {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> stop() {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
void playOrPause() {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setSubtitleTrack(SubtitleTrack subtitleTrack) {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setAudioTrack(AudioTrack subtitleTrack) {
|
|
||||||
throw UnimplementedError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,76 +2,98 @@ import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:smtc_windows/smtc_windows.dart' if (dart.library.html) 'package:fladder/stubs/web/smtc_web.dart';
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
|
||||||
import 'package:smtc_windows/smtc_windows.dart';
|
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
|
|
||||||
import 'package:fladder/models/item_base_model.dart';
|
import 'package:fladder/models/item_base_model.dart';
|
||||||
|
import 'package:fladder/models/items/media_streams_model.dart';
|
||||||
|
import 'package:fladder/models/playback/playback_model.dart';
|
||||||
|
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
import 'package:fladder/providers/video_player_provider.dart';
|
import 'package:fladder/providers/video_player_provider.dart';
|
||||||
import 'package:fladder/wrappers/media_control_base.dart';
|
import 'package:fladder/wrappers/players/base_player.dart';
|
||||||
import 'package:fladder/wrappers/media_wrapper_interface.dart';
|
import 'package:fladder/wrappers/players/lib_mdk.dart'
|
||||||
|
if (dart.library.html) 'package:fladder/stubs/web/lib_mdk_web.dart';
|
||||||
|
import 'package:fladder/wrappers/players/lib_mpv.dart';
|
||||||
|
import 'package:fladder/wrappers/players/player_states.dart';
|
||||||
|
|
||||||
class MediaControlsWrapper extends MediaPlayback implements MediaControlBase {
|
class MediaControlsWrapper extends BaseAudioHandler {
|
||||||
MediaControlsWrapper({required this.ref});
|
MediaControlsWrapper({required this.ref});
|
||||||
|
|
||||||
|
BasePlayer? _player;
|
||||||
|
|
||||||
|
bool get hasPlayer => _player != null;
|
||||||
|
|
||||||
|
PlayerOptions? get backend => switch (_player) {
|
||||||
|
LibMPV _ => PlayerOptions.libMPV,
|
||||||
|
LibMDK _ => PlayerOptions.libMDK,
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
Stream<PlayerState>? get stateStream => _player?.stateStream;
|
||||||
|
PlayerState? get lastState => _player?.lastState;
|
||||||
|
|
||||||
|
Widget? subtitleWidget(bool showOverlay) => _player?.subtitles(showOverlay);
|
||||||
|
Widget? videoWidget(Key key, BoxFit fit) => _player?.videoWidget(key, fit);
|
||||||
|
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
|
||||||
List<StreamSubscription> subscriptions = [];
|
List<StreamSubscription> subscriptions = [];
|
||||||
SMTCWindows? smtc;
|
SMTCWindows? smtc;
|
||||||
|
|
||||||
@override
|
bool initMediaControls = false;
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
await AudioService.init(
|
if (!initMediaControls && !kDebugMode) {
|
||||||
builder: () => this,
|
await AudioService.init(
|
||||||
config: audioServiceConfig,
|
builder: () => this,
|
||||||
);
|
config: const AudioServiceConfig(
|
||||||
|
androidNotificationChannelId: 'nl.jknaapen.fladder.channel.playback',
|
||||||
|
androidNotificationChannelName: 'Video playback',
|
||||||
|
androidNotificationOngoing: true,
|
||||||
|
androidStopForegroundOnPause: true,
|
||||||
|
rewindInterval: Duration(seconds: 10),
|
||||||
|
fastForwardInterval: Duration(seconds: 15),
|
||||||
|
androidNotificationChannelDescription: "Playback",
|
||||||
|
androidShowNotificationBadge: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
initMediaControls = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final player = switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) {
|
||||||
|
PlayerOptions.libMDK => LibMDK(),
|
||||||
|
PlayerOptions.libMPV => LibMPV(),
|
||||||
|
};
|
||||||
|
|
||||||
|
setup(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> dispose() async => _player?.dispose();
|
||||||
Player setup() => setPlayer(_initPlayer());
|
|
||||||
|
|
||||||
Player _initPlayer() {
|
Future<void> setup(BasePlayer newPlayer) async {
|
||||||
|
_player = newPlayer;
|
||||||
|
await newPlayer.init(ref);
|
||||||
|
_initPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _initPlayer() {
|
||||||
for (var element in subscriptions) {
|
for (var element in subscriptions) {
|
||||||
element.cancel();
|
element.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
stop();
|
stop();
|
||||||
|
|
||||||
player?.dispose();
|
|
||||||
|
|
||||||
final newPlayer = Player(
|
|
||||||
configuration: PlayerConfiguration(
|
|
||||||
title: "nl.jknaapen.fladder",
|
|
||||||
bufferSize: 64 * 1024 * 1024,
|
|
||||||
libassAndroidFont: 'assets/fonts/mp-font.ttf',
|
|
||||||
libass: !kIsWeb &&
|
|
||||||
ref.read(
|
|
||||||
videoPlayerSettingsProvider.select((value) => value.useLibass),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setPlayer(newPlayer);
|
|
||||||
setController(VideoController(
|
|
||||||
newPlayer,
|
|
||||||
configuration: VideoControllerConfiguration(
|
|
||||||
enableHardwareAcceleration: ref.read(
|
|
||||||
videoPlayerSettingsProvider.select((value) => value.hardwareAccel),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
_subscribePlayer();
|
_subscribePlayer();
|
||||||
return newPlayer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> open(String url, bool play) async => _player?.open(url, play);
|
||||||
|
|
||||||
void _subscribePlayer() {
|
void _subscribePlayer() {
|
||||||
if (Platform.isWindows) {
|
if (Platform.isWindows && !kIsWeb) {
|
||||||
smtc = SMTCWindows(
|
smtc = SMTCWindows(
|
||||||
config: const SMTCConfig(
|
config: const SMTCConfig(
|
||||||
fastForwardEnabled: true,
|
fastForwardEnabled: true,
|
||||||
|
|
@ -113,43 +135,33 @@ class MediaControlsWrapper extends MediaPlayback implements MediaControlBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions.addAll([
|
subscriptions.add(_player!.stateStream.listen((value) {
|
||||||
player?.stream.buffer.listen((buffer) {
|
playbackState.add(playbackState.value.copyWith(
|
||||||
playbackState.add(playbackState.value.copyWith(
|
bufferedPosition: value.buffer,
|
||||||
bufferedPosition: buffer,
|
));
|
||||||
));
|
playbackState.add(playbackState.value.copyWith(
|
||||||
}),
|
processingState: value.buffering ? AudioProcessingState.buffering : AudioProcessingState.ready,
|
||||||
player?.stream.buffering.listen((buffering) {
|
));
|
||||||
playbackState.add(playbackState.value.copyWith(
|
playbackState.add(playbackState.value.copyWith(
|
||||||
processingState: buffering ? AudioProcessingState.buffering : AudioProcessingState.ready,
|
updatePosition: value.position,
|
||||||
));
|
));
|
||||||
}),
|
smtc?.setPosition(value.position);
|
||||||
player?.stream.position.listen((position) {
|
if (value.playing) {
|
||||||
playbackState.add(playbackState.value.copyWith(
|
WakelockPlus.enable();
|
||||||
updatePosition: position,
|
} else {
|
||||||
));
|
WakelockPlus.disable();
|
||||||
smtc?.setPosition(position);
|
}
|
||||||
}),
|
playbackState.add(playbackState.value.copyWith(
|
||||||
player?.stream.playing.listen((playing) {
|
playing: value.playing,
|
||||||
if (playing) {
|
));
|
||||||
WakelockPlus.enable();
|
smtc?.setPlaybackStatus(value.playing ? PlaybackStatus.Playing : PlaybackStatus.Paused);
|
||||||
} else {
|
}));
|
||||||
WakelockPlus.disable();
|
|
||||||
}
|
|
||||||
playbackState.add(playbackState.value.copyWith(
|
|
||||||
playing: playing,
|
|
||||||
));
|
|
||||||
smtc?.setPlaybackStatus(playing ? PlaybackStatus.Playing : PlaybackStatus.Paused);
|
|
||||||
}),
|
|
||||||
].whereNotNull());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> play() async {
|
Future<void> play() async {
|
||||||
if (!ref.read(clientSettingsProvider).enableMediaKeys) {
|
_player?.play();
|
||||||
await player?.play();
|
if (!ref.read(clientSettingsProvider).enableMediaKeys) return;
|
||||||
return super.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
final playBackItem = ref.read(playBackModel.select((value) => value?.item));
|
final playBackItem = ref.read(playBackModel.select((value) => value?.item));
|
||||||
final currentPosition = await ref.read(playBackModel.select((value) => value?.startDuration()));
|
final currentPosition = await ref.read(playBackModel.select((value) => value?.startDuration()));
|
||||||
|
|
@ -182,7 +194,6 @@ class MediaControlsWrapper extends MediaPlayback implements MediaControlBase {
|
||||||
processingState: AudioProcessingState.ready,
|
processingState: AudioProcessingState.ready,
|
||||||
));
|
));
|
||||||
|
|
||||||
await player?.play();
|
|
||||||
return super.play();
|
return super.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,9 +222,11 @@ class MediaControlsWrapper extends MediaPlayback implements MediaControlBase {
|
||||||
@override
|
@override
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
WakelockPlus.disable();
|
WakelockPlus.disable();
|
||||||
final position = player?.state.position;
|
final position = _player?.lastState.position;
|
||||||
final totalDuration = player?.state.duration;
|
final totalDuration = _player?.lastState.duration;
|
||||||
await player?.stop();
|
super.stop();
|
||||||
|
_player?.stop();
|
||||||
|
|
||||||
ref.read(playBackModel)?.playbackStopped(position ?? Duration.zero, totalDuration, ref);
|
ref.read(playBackModel)?.playbackStopped(position ?? Duration.zero, totalDuration, ref);
|
||||||
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(position: Duration.zero));
|
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(position: Duration.zero));
|
||||||
smtc?.setPlaybackStatus(PlaybackStatus.Stopped);
|
smtc?.setPlaybackStatus(PlaybackStatus.Stopped);
|
||||||
|
|
@ -229,16 +242,37 @@ class MediaControlsWrapper extends MediaPlayback implements MediaControlBase {
|
||||||
return super.stop();
|
return super.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> playOrPause() async {
|
||||||
void playOrPause() async {
|
await _player?.playOrPause();
|
||||||
await player?.playOrPause();
|
|
||||||
playbackState.add(playbackState.value.copyWith(
|
playbackState.add(playbackState.value.copyWith(
|
||||||
playing: player?.state.playing ?? false,
|
playing: _player?.lastState.playing ?? false,
|
||||||
controls: [MediaControl.play],
|
controls: [MediaControl.play],
|
||||||
));
|
));
|
||||||
final playerState = player;
|
final playerState = _player;
|
||||||
if (playerState != null) {
|
if (playerState != null) {
|
||||||
ref.read(playBackModel)?.updatePlaybackPosition(playerState.state.position, playerState.state.playing, ref);
|
ref
|
||||||
|
.read(playBackModel)
|
||||||
|
?.updatePlaybackPosition(playerState.lastState.position, playerState.lastState.playing, ref);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> setAudioTrack(AudioStreamModel? model, PlaybackModel playbackModel) async =>
|
||||||
|
await _player?.setAudioTrack(model, playbackModel) ?? -1;
|
||||||
|
|
||||||
|
Future<int> setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async =>
|
||||||
|
await _player?.setSubtitleTrack(model, playbackModel) ?? -1;
|
||||||
|
|
||||||
|
Future<void> setVolume(double speed) async => _player?.setVolume(speed);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> seek(Duration position) {
|
||||||
|
_player?.seek(position);
|
||||||
|
return super.seek(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setSpeed(double speed) {
|
||||||
|
_player?.setSpeed(speed);
|
||||||
|
return super.setSpeed(speed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:audio_service/audio_service.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
|
||||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
|
||||||
import 'package:fladder/providers/video_player_provider.dart';
|
|
||||||
import 'package:fladder/wrappers/media_control_base.dart';
|
|
||||||
import 'package:fladder/wrappers/media_wrapper_interface.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
|
||||||
|
|
||||||
class MediaControlsWrapper extends MediaPlayback implements MediaControlBase {
|
|
||||||
MediaControlsWrapper({required this.ref});
|
|
||||||
|
|
||||||
final Ref ref;
|
|
||||||
|
|
||||||
List<StreamSubscription> subscriptions = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> init() async {
|
|
||||||
await AudioService.init(
|
|
||||||
builder: () => this,
|
|
||||||
config: audioServiceConfig,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Player setup() => setPlayer(_initPlayer());
|
|
||||||
|
|
||||||
Player _initPlayer() {
|
|
||||||
for (var element in subscriptions) {
|
|
||||||
element.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
stop();
|
|
||||||
|
|
||||||
player?.dispose();
|
|
||||||
|
|
||||||
final newPlayer = Player(
|
|
||||||
configuration: PlayerConfiguration(
|
|
||||||
title: "nl.jknaapen.fladder",
|
|
||||||
bufferSize: 64 * 1024 * 1024,
|
|
||||||
libassAndroidFont: 'assets/fonts/mp-font.ttf',
|
|
||||||
libass: ref.read(
|
|
||||||
videoPlayerSettingsProvider.select((value) => value.useLibass),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setPlayer(newPlayer);
|
|
||||||
setController(VideoController(
|
|
||||||
newPlayer,
|
|
||||||
configuration: VideoControllerConfiguration(
|
|
||||||
enableHardwareAcceleration: ref.read(
|
|
||||||
videoPlayerSettingsProvider.select((value) => value.hardwareAccel),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
_subscribePlayer();
|
|
||||||
return newPlayer;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _subscribePlayer() async {
|
|
||||||
subscriptions.addAll([
|
|
||||||
player?.stream.buffer.listen((buffer) {
|
|
||||||
playbackState.add(playbackState.value.copyWith(
|
|
||||||
bufferedPosition: buffer,
|
|
||||||
));
|
|
||||||
}),
|
|
||||||
player?.stream.buffering.listen((buffering) {
|
|
||||||
playbackState.add(playbackState.value.copyWith(
|
|
||||||
processingState: buffering ? AudioProcessingState.buffering : AudioProcessingState.ready,
|
|
||||||
));
|
|
||||||
}),
|
|
||||||
player?.stream.position.listen((position) {
|
|
||||||
playbackState.add(playbackState.value.copyWith(
|
|
||||||
updatePosition: position,
|
|
||||||
));
|
|
||||||
}),
|
|
||||||
player?.stream.playing.listen((playing) {
|
|
||||||
playbackState.add(playbackState.value.copyWith(
|
|
||||||
playing: playing,
|
|
||||||
));
|
|
||||||
}),
|
|
||||||
].whereNotNull());
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> seek(Duration position) async => player?.seek(position);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> play() async {
|
|
||||||
if (!ref.read(clientSettingsProvider).enableMediaKeys) {
|
|
||||||
await player?.play();
|
|
||||||
return super.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
final playBackItem = ref.read(playBackModel.select((value) => value?.item));
|
|
||||||
if (playBackItem == null) return;
|
|
||||||
|
|
||||||
final poster = playBackItem.images?.firstOrNull;
|
|
||||||
|
|
||||||
//Everything else setup
|
|
||||||
mediaItem.add(MediaItem(
|
|
||||||
id: playBackItem.id,
|
|
||||||
title: playBackItem.title,
|
|
||||||
artist: playBackItem.subText,
|
|
||||||
rating: Rating.newHeartRating(playBackItem.userData.isFavourite),
|
|
||||||
duration: playBackItem.overview.runTime ?? const Duration(seconds: 0),
|
|
||||||
artUri: poster != null ? Uri.parse(poster.path) : null,
|
|
||||||
));
|
|
||||||
playbackState.add(playbackState.value.copyWith(
|
|
||||||
playing: true,
|
|
||||||
controls: [
|
|
||||||
MediaControl.pause,
|
|
||||||
MediaControl.stop,
|
|
||||||
],
|
|
||||||
systemActions: const {
|
|
||||||
MediaAction.seek,
|
|
||||||
MediaAction.fastForward,
|
|
||||||
MediaAction.setSpeed,
|
|
||||||
MediaAction.rewind,
|
|
||||||
},
|
|
||||||
processingState: AudioProcessingState.ready,
|
|
||||||
));
|
|
||||||
|
|
||||||
await player?.play();
|
|
||||||
return super.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> pause() async {
|
|
||||||
playbackState.add(playbackState.value.copyWith(
|
|
||||||
playing: false,
|
|
||||||
controls: [MediaControl.play],
|
|
||||||
));
|
|
||||||
await player?.pause();
|
|
||||||
return super.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> stop() async {
|
|
||||||
WakelockPlus.disable();
|
|
||||||
final position = player?.state.position;
|
|
||||||
final totalDuration = player?.state.duration;
|
|
||||||
await player?.stop();
|
|
||||||
ref.read(playBackModel)?.playbackStopped(position ?? Duration.zero, totalDuration, ref);
|
|
||||||
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(position: Duration.zero));
|
|
||||||
|
|
||||||
playbackState.add(
|
|
||||||
playbackState.value.copyWith(
|
|
||||||
playing: false,
|
|
||||||
processingState: AudioProcessingState.completed,
|
|
||||||
controls: [],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return super.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void playOrPause() {
|
|
||||||
player?.playOrPause();
|
|
||||||
playbackState.add(playbackState.value.copyWith(
|
|
||||||
playing: player?.state.playing ?? false,
|
|
||||||
controls: [MediaControl.play],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
import 'package:audio_service/audio_service.dart';
|
|
||||||
import 'package:media_kit/media_kit.dart';
|
|
||||||
import 'package:media_kit_video/media_kit_video.dart';
|
|
||||||
|
|
||||||
class MediaPlayback extends BaseAudioHandler {
|
|
||||||
Player? _player;
|
|
||||||
VideoController? _controller;
|
|
||||||
Player? get player => _player;
|
|
||||||
VideoController? get controller => _controller;
|
|
||||||
|
|
||||||
Player setPlayer(Player player) => _player = player;
|
|
||||||
VideoController setController(VideoController player) => _controller = player;
|
|
||||||
|
|
||||||
Future<void> setVolume(double volume) async => _player?.setVolume(volume);
|
|
||||||
|
|
||||||
Future<void> setSubtitleTrack(SubtitleTrack track) async => _player?.setSubtitleTrack(track);
|
|
||||||
List<SubtitleTrack> get subTracks => _player?.state.tracks.subtitle ?? [];
|
|
||||||
SubtitleTrack get subtitleTrack => _player?.state.track.subtitle ?? SubtitleTrack.no();
|
|
||||||
|
|
||||||
Future<void> setAudioTrack(AudioTrack track) async => _player?.setAudioTrack(track);
|
|
||||||
List<AudioTrack> get audioTracks => _player?.state.tracks.audio ?? [];
|
|
||||||
AudioTrack get audioTrack => _player?.state.track.audio ?? AudioTrack.no();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> seek(Duration position) async => player?.seek(position);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> play() async {
|
|
||||||
await player?.play();
|
|
||||||
return super.play();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> open(
|
|
||||||
Playable playable, {
|
|
||||||
bool play = true,
|
|
||||||
}) async {
|
|
||||||
return player?.open(playable, play: play);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> fastForward() async {
|
|
||||||
if (player != null) {
|
|
||||||
await player!.seek(player!.state.position + const Duration(seconds: 30));
|
|
||||||
}
|
|
||||||
return super.fastForward();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> rewind() async {
|
|
||||||
if (player != null) {
|
|
||||||
await player?.seek(player!.state.position - const Duration(seconds: 10));
|
|
||||||
}
|
|
||||||
return super.rewind();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> setSpeed(double speed) async {
|
|
||||||
await player?.setRate(speed);
|
|
||||||
return super.setSpeed(speed);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> pause() async {
|
|
||||||
playbackState.add(playbackState.value.copyWith(
|
|
||||||
playing: false,
|
|
||||||
controls: [MediaControl.play],
|
|
||||||
));
|
|
||||||
await player?.pause();
|
|
||||||
return super.pause();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
50
lib/wrappers/players/base_player.dart
Normal file
50
lib/wrappers/players/base_player.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/items/media_streams_model.dart';
|
||||||
|
import 'package:fladder/models/playback/playback_model.dart';
|
||||||
|
import 'package:fladder/wrappers/players/player_states.dart';
|
||||||
|
|
||||||
|
const libassFallbackFont = "assets/mp-font.ttf";
|
||||||
|
|
||||||
|
abstract class BasePlayer {
|
||||||
|
Stream<PlayerState> get stateStream;
|
||||||
|
PlayerState lastState = PlayerState();
|
||||||
|
|
||||||
|
Future<void> init(Ref ref);
|
||||||
|
Widget? videoWidget(
|
||||||
|
Key key,
|
||||||
|
BoxFit fit,
|
||||||
|
);
|
||||||
|
Widget? subtitles(
|
||||||
|
bool showOverlay,
|
||||||
|
);
|
||||||
|
Future<void> dispose();
|
||||||
|
Future<void> open(String url, bool play);
|
||||||
|
Future<void> seek(Duration position);
|
||||||
|
Future<void> play();
|
||||||
|
Future<void> setVolume(double volume);
|
||||||
|
Future<void> setSpeed(double speed);
|
||||||
|
Future<void> pause();
|
||||||
|
Future<void> stop();
|
||||||
|
Future<void> playOrPause();
|
||||||
|
Future<void> loop(bool loop);
|
||||||
|
Future<int> setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel);
|
||||||
|
Future<int> setAudioTrack(AudioStreamModel? model, PlaybackModel playbackModel);
|
||||||
|
|
||||||
|
Uri? isValidUrl(String input) {
|
||||||
|
try {
|
||||||
|
final uri = Uri.tryParse(input);
|
||||||
|
if (uri != null && uri.isAbsolute && (uri.scheme == 'http' || uri.scheme == 'https')) {
|
||||||
|
return uri;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
183
lib/wrappers/players/lib_mdk.dart
Normal file
183
lib/wrappers/players/lib_mdk.dart
Normal file
|
|
@ -0,0 +1,183 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:fvp/fvp.dart' as fvp;
|
||||||
|
import 'package:fvp/mdk.dart';
|
||||||
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/items/media_streams_model.dart';
|
||||||
|
import 'package:fladder/models/playback/playback_model.dart';
|
||||||
|
import 'package:fladder/wrappers/players/base_player.dart';
|
||||||
|
import 'package:fladder/wrappers/players/player_states.dart';
|
||||||
|
|
||||||
|
class LibMDK extends BasePlayer {
|
||||||
|
VideoPlayerController? _controller;
|
||||||
|
late final player = Player();
|
||||||
|
|
||||||
|
bool externalSubEnabled = false;
|
||||||
|
|
||||||
|
final StreamController<PlayerState> _stateController = StreamController.broadcast();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<PlayerState> get stateStream => _stateController.stream;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> init(Ref ref) async {
|
||||||
|
dispose();
|
||||||
|
fvp.registerWith(options: {
|
||||||
|
'global': {'log': 'off'},
|
||||||
|
'subtitleFontFile': libassFallbackFont,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
_controller?.dispose();
|
||||||
|
_controller = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> open(String url, bool play) async {
|
||||||
|
final validUrl = isValidUrl(url);
|
||||||
|
if (validUrl != null) {
|
||||||
|
_controller = VideoPlayerController.networkUrl(validUrl);
|
||||||
|
} else {
|
||||||
|
_controller = VideoPlayerController.file(File(url));
|
||||||
|
}
|
||||||
|
await _controller?.initialize();
|
||||||
|
|
||||||
|
_controller?.addListener(() => updateState());
|
||||||
|
|
||||||
|
if (play) {
|
||||||
|
await _controller?.play();
|
||||||
|
}
|
||||||
|
return setState(lastState.update(
|
||||||
|
buffering: true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setState(PlayerState state) {
|
||||||
|
lastState = state;
|
||||||
|
_stateController.add(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateState() {
|
||||||
|
setState(lastState.update(
|
||||||
|
playing: _controller?.value.isPlaying ?? false,
|
||||||
|
completed: _controller?.value.isCompleted ?? false,
|
||||||
|
position: _controller?.value.position ?? Duration.zero,
|
||||||
|
duration: _controller?.value.duration ?? Duration.zero,
|
||||||
|
volume: (_controller?.value.volume ?? 1.0) * 100,
|
||||||
|
rate: _controller?.value.playbackSpeed ?? 1.0,
|
||||||
|
buffering: _controller?.value.isBuffering ?? true,
|
||||||
|
buffer: _controller?.value.buffered.last.end ?? Duration.zero,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> pause() async => _controller?.pause();
|
||||||
|
@override
|
||||||
|
Future<void> play() async => _controller?.play();
|
||||||
|
@override
|
||||||
|
Future<void> playOrPause() async => lastState.playing ? _controller?.pause() : _controller?.play();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> seek(Duration position) async => _controller?.seekTo(position);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> setAudioTrack(AudioStreamModel? model, PlaybackModel playbackModel) async {
|
||||||
|
final wantedAudioStream = model ?? playbackModel.defaultAudioStream;
|
||||||
|
if (wantedAudioStream == AudioStreamModel.no() || wantedAudioStream == null) {
|
||||||
|
_controller?.setAudioTracks([-1]);
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
final indexOf = playbackModel.audioStreams?.indexOf(wantedAudioStream);
|
||||||
|
if (indexOf != null) {
|
||||||
|
_controller?.setAudioTracks([indexOf - 1]);
|
||||||
|
}
|
||||||
|
return wantedAudioStream.index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setSpeed(double speed) async => _controller?.setPlaybackSpeed(speed);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async {
|
||||||
|
final wantedSubtitle = model ?? playbackModel.defaultSubStream;
|
||||||
|
if (wantedSubtitle == SubStreamModel.no()) {
|
||||||
|
externalSubEnabled = false;
|
||||||
|
_controller?.setSubtitleTracks([-1]);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (wantedSubtitle != null) {
|
||||||
|
if (wantedSubtitle.isExternal && wantedSubtitle.url != null) {
|
||||||
|
externalSubEnabled = true;
|
||||||
|
_controller?.setExternalSubtitle(wantedSubtitle.url!);
|
||||||
|
return wantedSubtitle.index;
|
||||||
|
} else {
|
||||||
|
if (externalSubEnabled) {
|
||||||
|
externalSubEnabled = false;
|
||||||
|
_controller?.setExternalSubtitle("");
|
||||||
|
}
|
||||||
|
final indexOf = playbackModel.subStreams?.indexOf(wantedSubtitle);
|
||||||
|
if (indexOf != null) {
|
||||||
|
_controller?.setSubtitleTracks([indexOf - 1]);
|
||||||
|
}
|
||||||
|
return wantedSubtitle.index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> stop() async => _controller?.dispose();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? videoWidget(
|
||||||
|
Key key,
|
||||||
|
BoxFit fit,
|
||||||
|
) =>
|
||||||
|
_controller == null
|
||||||
|
? null
|
||||||
|
: Container(
|
||||||
|
key: key,
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) => Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
FittedBox(
|
||||||
|
fit: fit,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: ValueListenableBuilder<VideoPlayerValue>(
|
||||||
|
valueListenable: _controller!,
|
||||||
|
builder: (context, value, child) {
|
||||||
|
final aspectRatio = value.isInitialized ? value.aspectRatio : 1.77;
|
||||||
|
return SizedBox(
|
||||||
|
width: constraints.maxWidth,
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: aspectRatio,
|
||||||
|
child: VideoPlayer(_controller!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? subtitles(bool showOverlay) => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setVolume(double volume) async => _controller?.setVolume(volume / 100);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> loop(bool loop) async => _controller?.setLooping(loop);
|
||||||
|
}
|
||||||
251
lib/wrappers/players/lib_mpv.dart
Normal file
251
lib/wrappers/players/lib_mpv.dart
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart' as mpv;
|
||||||
|
import 'package:media_kit_video/media_kit_video.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/items/media_streams_model.dart';
|
||||||
|
import 'package:fladder/models/playback/playback_model.dart';
|
||||||
|
import 'package:fladder/models/settings/subtitle_settings_model.dart';
|
||||||
|
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
||||||
|
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||||
|
import 'package:fladder/wrappers/players/base_player.dart';
|
||||||
|
import 'package:fladder/wrappers/players/player_states.dart';
|
||||||
|
|
||||||
|
class LibMPV extends BasePlayer {
|
||||||
|
mpv.Player? _player;
|
||||||
|
VideoController? _controller;
|
||||||
|
|
||||||
|
final StreamController<PlayerState> _stateController = StreamController.broadcast();
|
||||||
|
@override
|
||||||
|
Stream<PlayerState> get stateStream => _stateController.stream;
|
||||||
|
|
||||||
|
StreamSubscription<bool>? _onCompleted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> init(Ref ref) async {
|
||||||
|
dispose();
|
||||||
|
|
||||||
|
mpv.MediaKit.ensureInitialized();
|
||||||
|
|
||||||
|
_player = mpv.Player(
|
||||||
|
configuration: mpv.PlayerConfiguration(
|
||||||
|
title: "nl.jknaapen.fladder",
|
||||||
|
libassAndroidFont: libassFallbackFont,
|
||||||
|
libass: !kIsWeb &&
|
||||||
|
ref.read(
|
||||||
|
videoPlayerSettingsProvider.select((value) => value.useLibass),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_player != null) {
|
||||||
|
_controller = VideoController(
|
||||||
|
_player!,
|
||||||
|
configuration: VideoControllerConfiguration(
|
||||||
|
enableHardwareAcceleration: ref.read(
|
||||||
|
videoPlayerSettingsProvider.select((value) => value.hardwareAccel),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
_player!.stream.playing.listen((value) => setState(lastState.update(playing: value)));
|
||||||
|
_player!.stream.buffering.listen((value) => setState(lastState.update(buffering: value)));
|
||||||
|
_player!.stream.position.listen((value) => setState(lastState.update(position: value)));
|
||||||
|
_player!.stream.duration.listen((value) => setState(lastState.update(duration: value)));
|
||||||
|
_player!.stream.volume.listen((value) => setState(lastState.update(volume: value)));
|
||||||
|
_player!.stream.rate.listen((value) => setState(lastState.update(rate: value)));
|
||||||
|
_player!.stream.buffer.listen((value) => setState(lastState.update(buffer: value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_player?.platform is mpv.NativePlayer) {
|
||||||
|
await (_player?.platform as dynamic).setProperty(
|
||||||
|
'force-seekable',
|
||||||
|
'yes',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {
|
||||||
|
_onCompleted?.cancel();
|
||||||
|
_onCompleted = null;
|
||||||
|
_player?.stop();
|
||||||
|
_player?.dispose();
|
||||||
|
_player = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setState(PlayerState state) {
|
||||||
|
lastState = state;
|
||||||
|
_stateController.add(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> open(String url, bool play) async {
|
||||||
|
await _player?.open(mpv.Media(url), play: play);
|
||||||
|
return setState(lastState.update(buffering: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<mpv.SubtitleTrack> get subTracks => _player?.state.tracks.subtitle ?? [];
|
||||||
|
mpv.SubtitleTrack get subtitleTrack => _player?.state.track.subtitle ?? mpv.SubtitleTrack.no();
|
||||||
|
|
||||||
|
List<mpv.AudioTrack> get audioTracks => _player?.state.tracks.audio ?? [];
|
||||||
|
mpv.AudioTrack get audioTrack => _player?.state.track.audio ?? mpv.AudioTrack.no();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> pause() async => _player?.pause();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> play() async => _player?.play();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> playOrPause() async => _player?.playOrPause();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> seek(Duration position) async => _player?.seek(position);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> setAudioTrack(AudioStreamModel? model, PlaybackModel playbackModel) async {
|
||||||
|
final wantedAudioStream = model ?? playbackModel.defaultAudioStream;
|
||||||
|
if (wantedAudioStream == null) return -1;
|
||||||
|
if (wantedAudioStream.index == AudioStreamModel.no().index) {
|
||||||
|
await _player?.setAudioTrack(mpv.AudioTrack.no());
|
||||||
|
} else {
|
||||||
|
final internalTracks = audioTracks.getRange(2, audioTracks.length).toList();
|
||||||
|
final audioTrack =
|
||||||
|
internalTracks.elementAtOrNull((playbackModel.audioStreams?.indexOf(wantedAudioStream) ?? -1) - 1);
|
||||||
|
if (audioTrack != null) {
|
||||||
|
await _player?.setAudioTrack(audioTrack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wantedAudioStream.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setSpeed(double speed) async => _player?.setRate(speed);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async {
|
||||||
|
if (_player == null) return -1;
|
||||||
|
final wantedSubtitle = model ?? playbackModel.defaultSubStream;
|
||||||
|
if (wantedSubtitle == null) return -1;
|
||||||
|
if (wantedSubtitle.index == SubStreamModel.no().index) {
|
||||||
|
await _player?.setSubtitleTrack(mpv.SubtitleTrack.no());
|
||||||
|
} else {
|
||||||
|
final internalTrack = subTracks.getRange(2, subTracks.length).toList();
|
||||||
|
final index = playbackModel.subStreams?.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id);
|
||||||
|
final subTrack = internalTrack.elementAtOrNull(index ?? -1);
|
||||||
|
if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) {
|
||||||
|
await _player?.setSubtitleTrack(mpv.SubtitleTrack.uri(wantedSubtitle.url!));
|
||||||
|
} else if (subTrack != null) {
|
||||||
|
await _player?.setSubtitleTrack(subTrack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wantedSubtitle.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> stop() async => _player?.stop();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? videoWidget(
|
||||||
|
Key key,
|
||||||
|
BoxFit fit,
|
||||||
|
) =>
|
||||||
|
_controller == null
|
||||||
|
? null
|
||||||
|
: Video(
|
||||||
|
key: key,
|
||||||
|
controller: _controller!,
|
||||||
|
wakelock: true,
|
||||||
|
fill: Colors.transparent,
|
||||||
|
fit: fit,
|
||||||
|
subtitleViewConfiguration: const SubtitleViewConfiguration(visible: false),
|
||||||
|
controls: NoVideoControls,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget? subtitles(
|
||||||
|
bool showOverlay,
|
||||||
|
) =>
|
||||||
|
_controller != null
|
||||||
|
? _VideoSubtitles(
|
||||||
|
controller: _controller!,
|
||||||
|
showOverlay: showOverlay,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setVolume(double volume) async => _player?.setVolume(volume);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> loop(bool loop) async {
|
||||||
|
if (loop && _onCompleted == null) {
|
||||||
|
_onCompleted = _player?.stream.completed.listen((completed) {
|
||||||
|
if (completed) {
|
||||||
|
_player?.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_onCompleted?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoSubtitles extends ConsumerStatefulWidget {
|
||||||
|
final VideoController controller;
|
||||||
|
final bool showOverlay;
|
||||||
|
const _VideoSubtitles({
|
||||||
|
required this.controller,
|
||||||
|
this.showOverlay = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ConsumerStatefulWidget> createState() => _VideoSubtitlesState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _VideoSubtitlesState extends ConsumerState<_VideoSubtitles> {
|
||||||
|
late List<String> subtitle = widget.controller.player.state.subtitle;
|
||||||
|
StreamSubscription<List<String>>? subscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
subscription = widget.controller.player.stream.subtitle.listen((value) {
|
||||||
|
setState(() {
|
||||||
|
subtitle = value;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
subscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final settings = ref.watch(subtitleSettingsProvider);
|
||||||
|
final padding = MediaQuery.of(context).padding;
|
||||||
|
final text = [
|
||||||
|
for (final line in subtitle)
|
||||||
|
if (line.trim().isNotEmpty) line.trim(),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
if (widget.controller.player.platform?.configuration.libass ?? false) {
|
||||||
|
return const IgnorePointer(child: SizedBox.shrink());
|
||||||
|
} else {
|
||||||
|
return SubtitleText(
|
||||||
|
subModel: settings,
|
||||||
|
padding: padding,
|
||||||
|
offset: (widget.showOverlay ? 0.5 : settings.verticalOffset),
|
||||||
|
text: text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
lib/wrappers/players/player_states.dart
Normal file
74
lib/wrappers/players/player_states.dart
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
class PlayerState {
|
||||||
|
bool playing;
|
||||||
|
bool completed;
|
||||||
|
Duration position;
|
||||||
|
Duration duration;
|
||||||
|
double volume;
|
||||||
|
double rate;
|
||||||
|
bool buffering;
|
||||||
|
Duration buffer;
|
||||||
|
|
||||||
|
PlayerState({
|
||||||
|
this.playing = false,
|
||||||
|
this.completed = false,
|
||||||
|
this.position = Duration.zero,
|
||||||
|
this.duration = Duration.zero,
|
||||||
|
this.volume = 100,
|
||||||
|
this.rate = 1.0,
|
||||||
|
this.buffering = true,
|
||||||
|
this.buffer = Duration.zero,
|
||||||
|
});
|
||||||
|
|
||||||
|
PlayerState update({
|
||||||
|
bool? playing,
|
||||||
|
bool? completed,
|
||||||
|
bool? buffering,
|
||||||
|
Duration? position,
|
||||||
|
Duration? duration,
|
||||||
|
double? volume,
|
||||||
|
double? rate,
|
||||||
|
Duration? buffer,
|
||||||
|
}) {
|
||||||
|
if (playing != null) this.playing = playing;
|
||||||
|
if (completed != null) this.completed = completed;
|
||||||
|
if (buffering != null) this.buffering = buffering;
|
||||||
|
if (position != null) this.position = position;
|
||||||
|
if (duration != null) this.duration = duration;
|
||||||
|
if (volume != null) this.volume = volume;
|
||||||
|
if (rate != null) this.rate = rate;
|
||||||
|
if (buffer != null) this.buffer = buffer;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayerStream {
|
||||||
|
final Stream<bool> playing;
|
||||||
|
final Stream<bool> completed;
|
||||||
|
final Stream<Duration> position;
|
||||||
|
final Stream<Duration> duration;
|
||||||
|
final Stream<double> volume;
|
||||||
|
final Stream<double> rate;
|
||||||
|
final Stream<bool> buffering;
|
||||||
|
final Stream<Duration> buffer;
|
||||||
|
|
||||||
|
const PlayerStream(
|
||||||
|
this.playing,
|
||||||
|
this.completed,
|
||||||
|
this.position,
|
||||||
|
this.duration,
|
||||||
|
this.volume,
|
||||||
|
this.rate,
|
||||||
|
this.buffering,
|
||||||
|
this.buffer,
|
||||||
|
);
|
||||||
|
|
||||||
|
void bindToState(PlayerState state) {
|
||||||
|
playing.listen((value) => state.update(playing: value));
|
||||||
|
buffering.listen((value) => state.update(buffering: value));
|
||||||
|
position.listen((value) => state.update(position: value));
|
||||||
|
duration.listen((value) => state.update(duration: value));
|
||||||
|
volume.listen((value) => state.update(volume: value));
|
||||||
|
rate.listen((value) => state.update(rate: value));
|
||||||
|
buffer.listen((value) => state.update(buffer: value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
#include <desktop_drop/desktop_drop_plugin.h>
|
#include <desktop_drop/desktop_drop_plugin.h>
|
||||||
#include <dynamic_color/dynamic_color_plugin.h>
|
#include <dynamic_color/dynamic_color_plugin.h>
|
||||||
|
#include <fvp/fvp_plugin.h>
|
||||||
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
||||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||||
#include <media_kit_video/media_kit_video_plugin.h>
|
#include <media_kit_video/media_kit_video_plugin.h>
|
||||||
|
|
@ -22,6 +23,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
|
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
|
||||||
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
|
dynamic_color_plugin_register_with_registrar(dynamic_color_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) fvp_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FvpPlugin");
|
||||||
|
fvp_plugin_register_with_registrar(fvp_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar =
|
g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin");
|
||||||
isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar);
|
isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
desktop_drop
|
desktop_drop
|
||||||
dynamic_color
|
dynamic_color
|
||||||
|
fvp
|
||||||
isar_flutter_libs
|
isar_flutter_libs
|
||||||
media_kit_libs_linux
|
media_kit_libs_linux
|
||||||
media_kit_video
|
media_kit_video
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import audio_service
|
||||||
import audio_session
|
import audio_session
|
||||||
import desktop_drop
|
import desktop_drop
|
||||||
import dynamic_color
|
import dynamic_color
|
||||||
|
import fvp
|
||||||
import isar_flutter_libs
|
import isar_flutter_libs
|
||||||
import just_audio
|
import just_audio
|
||||||
import local_auth_darwin
|
import local_auth_darwin
|
||||||
|
|
@ -32,6 +33,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||||
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
|
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
|
||||||
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
||||||
|
FvpPlugin.register(with: registry.registrar(forPlugin: "FvpPlugin"))
|
||||||
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
|
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
|
||||||
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
||||||
FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin"))
|
FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin"))
|
||||||
|
|
|
||||||
10
pubspec.lock
10
pubspec.lock
|
|
@ -746,6 +746,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.0"
|
version: "4.0.0"
|
||||||
|
fvp:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: fvp
|
||||||
|
sha256: "3dd245cac5dfba36311cbf5834d8f275ba1f3e49a5cdcb4a98e01cb41e9a21d8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.28.0"
|
||||||
fwfh_cached_network_image:
|
fwfh_cached_network_image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -2016,7 +2024,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
video_player:
|
video_player:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: video_player
|
name: video_player
|
||||||
sha256: "4a8c3492d734f7c39c2588a3206707a05ee80cef52e8c7f3b2078d430c84bc17"
|
sha256: "4a8c3492d734f7c39c2588a3206707a05ee80cef52e8c7f3b2078d430c84bc17"
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,8 @@ dependencies:
|
||||||
media_kit_video: ^1.2.4 # For video rendering.
|
media_kit_video: ^1.2.4 # For video rendering.
|
||||||
media_kit_libs_video: ^1.0.4 # Native video dependencies.
|
media_kit_libs_video: ^1.0.4 # Native video dependencies.
|
||||||
audio_service: ^0.18.12
|
audio_service: ^0.18.12
|
||||||
|
fvp: ^0.28.0
|
||||||
|
video_player: ^2.9.2
|
||||||
|
|
||||||
# UI Components
|
# UI Components
|
||||||
dynamic_color: ^1.7.0
|
dynamic_color: ^1.7.0
|
||||||
|
|
@ -158,6 +160,7 @@ flutter:
|
||||||
- icons/
|
- icons/
|
||||||
- assets/fonts/
|
- assets/fonts/
|
||||||
- config/
|
- config/
|
||||||
|
- assets/mp-font.ttf
|
||||||
|
|
||||||
fonts:
|
fonts:
|
||||||
- family: Rubik
|
- family: Rubik
|
||||||
|
|
@ -173,7 +176,3 @@ flutter:
|
||||||
style: normal
|
style: normal
|
||||||
- asset: assets/fonts/opensans/OpenSans-Italic.ttf
|
- asset: assets/fonts/opensans/OpenSans-Italic.ttf
|
||||||
style: italic
|
style: italic
|
||||||
|
|
||||||
- family: mp-font
|
|
||||||
fonts:
|
|
||||||
- asset: assets/fonts/mp-font.ttf
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
#include <desktop_drop/desktop_drop_plugin.h>
|
#include <desktop_drop/desktop_drop_plugin.h>
|
||||||
#include <dynamic_color/dynamic_color_plugin_c_api.h>
|
#include <dynamic_color/dynamic_color_plugin_c_api.h>
|
||||||
|
#include <fvp/fvp_plugin_c_api.h>
|
||||||
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
|
||||||
#include <local_auth_windows/local_auth_plugin.h>
|
#include <local_auth_windows/local_auth_plugin.h>
|
||||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||||
|
|
@ -24,6 +25,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
|
registry->GetRegistrarForPlugin("DesktopDropPlugin"));
|
||||||
DynamicColorPluginCApiRegisterWithRegistrar(
|
DynamicColorPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
|
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
|
||||||
|
FvpPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FvpPluginCApi"));
|
||||||
IsarFlutterLibsPluginRegisterWithRegistrar(
|
IsarFlutterLibsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
|
registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
|
||||||
LocalAuthPluginRegisterWithRegistrar(
|
LocalAuthPluginRegisterWithRegistrar(
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
desktop_drop
|
desktop_drop
|
||||||
dynamic_color
|
dynamic_color
|
||||||
|
fvp
|
||||||
isar_flutter_libs
|
isar_flutter_libs
|
||||||
local_auth_windows
|
local_auth_windows
|
||||||
media_kit_libs_windows_video
|
media_kit_libs_windows_video
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue