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*"
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
paths:
|
||||
- pubspec.yaml
|
||||
- .github/workflows/build.yml
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
|
@ -228,6 +235,7 @@ jobs:
|
|||
build-linux-flatpak:
|
||||
name: "Flatpak"
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [fetch-info, build-linux]
|
||||
container:
|
||||
image: bilelmoussaoui/flatpak-github-actions:gnome-46
|
||||
|
|
|
|||
|
|
@ -1125,5 +1125,13 @@
|
|||
"stop": "Stop",
|
||||
"resumeVideo": "Resume 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:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
|
@ -67,7 +66,6 @@ Future<Map<String, dynamic>> loadConfig() async {
|
|||
void main() async {
|
||||
_setupLogging();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
MediaKit.ensureInitialized();
|
||||
|
||||
if (kIsWeb) {
|
||||
html.document.onContextMenu.listen((event) => event.preventDefault());
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ class TrickPlayModel with _$TrickPlayModel {
|
|||
int get imagesPerTile => tileWidth * tileHeight;
|
||||
|
||||
String? getTile(Duration position) {
|
||||
final int currentIndex = (position.inMilliseconds ~/ interval.inMilliseconds).clamp(0, thumbnailCount - 1);
|
||||
final int indexOfTile = (currentIndex ~/ imagesPerTile).clamp(0, (images.length - 1));
|
||||
final int currentIndex = (position.inMilliseconds ~/ interval.inMilliseconds).clamp(0, thumbnailCount);
|
||||
final int indexOfTile = (currentIndex ~/ imagesPerTile).clamp(0, images.length);
|
||||
return images.elementAtOrNull(indexOfTile);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
|
|
@ -13,9 +14,7 @@ import 'package:fladder/providers/api_provider.dart';
|
|||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:fladder/util/duration_extensions.dart';
|
||||
import 'package:fladder/util/list_extensions.dart';
|
||||
import 'package:fladder/wrappers/media_control_wrapper.dart'
|
||||
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:fladder/wrappers/media_control_wrapper.dart';
|
||||
|
||||
class DirectPlaybackModel implements PlaybackModel {
|
||||
DirectPlaybackModel({
|
||||
|
|
@ -67,22 +66,8 @@ class DirectPlaybackModel implements PlaybackModel {
|
|||
|
||||
@override
|
||||
Future<DirectPlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
|
||||
final wantedSubtitle =
|
||||
model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex);
|
||||
if (wantedSubtitle == null) return this;
|
||||
if (wantedSubtitle.index == SubStreamModel.no().index) {
|
||||
await player.setSubtitleTrack(SubtitleTrack.no());
|
||||
} else {
|
||||
final subTracks = player.subTracks.getRange(2, player.subTracks.length).toList();
|
||||
final index = subStreams.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id);
|
||||
final subTrack = subTracks.elementAtOrNull(index);
|
||||
if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) {
|
||||
await player.setSubtitleTrack(SubtitleTrack.uri(wantedSubtitle.url!));
|
||||
} else if (subTrack != null) {
|
||||
await player.setSubtitleTrack(subTrack);
|
||||
}
|
||||
}
|
||||
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: wantedSubtitle.index));
|
||||
final newIndex = await player.setSubtitleTrack(model, this);
|
||||
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: newIndex));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -90,19 +75,8 @@ class DirectPlaybackModel implements PlaybackModel {
|
|||
|
||||
@override
|
||||
Future<DirectPlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
|
||||
final wantedAudioStream =
|
||||
model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex);
|
||||
if (wantedAudioStream == null) return this;
|
||||
if (wantedAudioStream.index == AudioStreamModel.no().index) {
|
||||
await player.setAudioTrack(AudioTrack.no());
|
||||
} else {
|
||||
final audioTracks = player.audioTracks.getRange(2, player.audioTracks.length).toList();
|
||||
final audioTrack = audioTracks.elementAtOrNull(audioStreams.indexOf(wantedAudioStream) - 1);
|
||||
if (audioTrack != null) {
|
||||
await player.setAudioTrack(audioTrack);
|
||||
}
|
||||
}
|
||||
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: wantedAudioStream.index));
|
||||
final newIndex = await player.setAudioTrack(model, this);
|
||||
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: newIndex));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
|
|
@ -13,9 +13,7 @@ import 'package:fladder/models/syncing/sync_item.dart';
|
|||
import 'package:fladder/providers/sync_provider.dart';
|
||||
import 'package:fladder/util/duration_extensions.dart';
|
||||
import 'package:fladder/util/list_extensions.dart';
|
||||
import 'package:fladder/wrappers/media_control_wrapper.dart'
|
||||
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:fladder/wrappers/media_control_wrapper.dart';
|
||||
|
||||
class OfflinePlaybackModel implements PlaybackModel {
|
||||
OfflinePlaybackModel({
|
||||
|
|
@ -66,22 +64,8 @@ class OfflinePlaybackModel implements PlaybackModel {
|
|||
|
||||
@override
|
||||
Future<OfflinePlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
|
||||
final wantedSubtitle =
|
||||
model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex);
|
||||
if (wantedSubtitle == null) return this;
|
||||
if (wantedSubtitle.index == SubStreamModel.no().index) {
|
||||
await player.setSubtitleTrack(SubtitleTrack.no());
|
||||
} else {
|
||||
final subTracks = player.subTracks.getRange(2, player.subTracks.length).toList();
|
||||
final index = subStreams.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id);
|
||||
final subTrack = subTracks.elementAtOrNull(index);
|
||||
if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) {
|
||||
await player.setSubtitleTrack(SubtitleTrack.uri(wantedSubtitle.url!));
|
||||
} else if (subTrack != null) {
|
||||
await player.setSubtitleTrack(subTrack);
|
||||
}
|
||||
}
|
||||
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: wantedSubtitle.index));
|
||||
final newIndex = await player.setSubtitleTrack(model, this);
|
||||
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: newIndex));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -89,19 +73,8 @@ class OfflinePlaybackModel implements PlaybackModel {
|
|||
|
||||
@override
|
||||
Future<OfflinePlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
|
||||
final wantedAudioStream =
|
||||
model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex);
|
||||
if (wantedAudioStream == null) return this;
|
||||
if (wantedAudioStream.index == AudioStreamModel.no().index) {
|
||||
await player.setAudioTrack(AudioTrack.no());
|
||||
} else {
|
||||
final audioTracks = player.audioTracks.getRange(2, player.audioTracks.length).toList();
|
||||
final audioTrack = audioTracks.elementAtOrNull(audioStreams.indexOf(wantedAudioStream) - 1);
|
||||
if (audioTrack != null) {
|
||||
await player.setAudioTrack(audioTrack);
|
||||
}
|
||||
}
|
||||
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: wantedAudioStream.index));
|
||||
final newIndex = await player.setAudioTrack(model, this);
|
||||
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: newIndex));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import 'dart:developer';
|
|||
import 'package:chopper/chopper.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
|
|
@ -27,10 +26,23 @@ import 'package:fladder/providers/sync_provider.dart';
|
|||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:fladder/util/duration_extensions.dart';
|
||||
import 'package:fladder/wrappers/media_control_wrapper.dart'
|
||||
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
|
||||
import 'package:fladder/wrappers/media_control_wrapper.dart';
|
||||
|
||||
class Media {
|
||||
final String url;
|
||||
|
||||
const Media({
|
||||
required this.url,
|
||||
});
|
||||
}
|
||||
|
||||
extension PlaybackModelExtension on PlaybackModel? {
|
||||
SubStreamModel? get defaultSubStream =>
|
||||
this?.subStreams?.firstWhereOrNull((element) => element.index == this?.mediaStreams?.defaultSubStreamIndex);
|
||||
|
||||
AudioStreamModel? get defaultAudioStream =>
|
||||
this?.audioStreams?.firstWhereOrNull((element) => element.index == this?.mediaStreams?.defaultAudioStreamIndex);
|
||||
|
||||
String? get label => switch (this) {
|
||||
DirectPlaybackModel _ => PlaybackType.directStream.name,
|
||||
TranscodePlaybackModel _ => PlaybackType.transcode.name,
|
||||
|
|
@ -119,7 +131,7 @@ class PlaybackModelHelper {
|
|||
syncedItem: syncedItem,
|
||||
trickPlay: syncedItem.trickPlayModel,
|
||||
mediaSegments: syncedItem.mediaSegments,
|
||||
media: Media(syncedItem.videoFile.path),
|
||||
media: Media(url: syncedItem.videoFile.path),
|
||||
queue: itemQueue.whereNotNull().toList(),
|
||||
syncedQueue: children,
|
||||
mediaStreams: item.streamModel ?? syncedItemModel.streamModel,
|
||||
|
|
@ -170,7 +182,7 @@ class PlaybackModelHelper {
|
|||
subtitleStreamIndex: streamModel?.defaultSubStreamIndex,
|
||||
enableTranscoding: true,
|
||||
autoOpenLiveStream: true,
|
||||
deviceProfile: defaultProfile,
|
||||
deviceProfile: ref.read(videoProfileProvider),
|
||||
userId: userId,
|
||||
mediaSourceId: firstItemToPlay.id,
|
||||
),
|
||||
|
|
@ -218,7 +230,7 @@ class PlaybackModelHelper {
|
|||
chapters: chapters,
|
||||
playbackInfo: playbackInfo,
|
||||
trickPlay: trickPlay,
|
||||
media: Media('${ref.read(userProvider)?.server ?? ""}/Videos/${mediaSource.id}/stream?$params'),
|
||||
media: Media(url: '${ref.read(userProvider)?.server ?? ""}/Videos/${mediaSource.id}/stream?$params'),
|
||||
mediaStreams: mediaStreamsWithUrls,
|
||||
);
|
||||
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
|
||||
|
|
@ -229,7 +241,7 @@ class PlaybackModelHelper {
|
|||
chapters: chapters,
|
||||
trickPlay: trickPlay,
|
||||
playbackInfo: playbackInfo,
|
||||
media: Media("${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
|
||||
media: Media(url: "${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
|
||||
mediaStreams: mediaStreamsWithUrls,
|
||||
);
|
||||
}
|
||||
|
|
@ -300,7 +312,7 @@ class PlaybackModelHelper {
|
|||
subtitleStreamIndex: subIndex,
|
||||
enableTranscoding: true,
|
||||
autoOpenLiveStream: true,
|
||||
deviceProfile: defaultProfile,
|
||||
deviceProfile: ref.read(videoProfileProvider),
|
||||
userId: userId,
|
||||
mediaSourceId: item.id,
|
||||
),
|
||||
|
|
@ -347,7 +359,7 @@ class PlaybackModelHelper {
|
|||
chapters: playbackModel.chapters,
|
||||
playbackInfo: playbackInfo,
|
||||
trickPlay: playbackModel.trickPlay,
|
||||
media: Media(directPlay),
|
||||
media: Media(url: directPlay),
|
||||
mediaStreams: mediaStreamsWithUrls,
|
||||
);
|
||||
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
|
||||
|
|
@ -358,7 +370,7 @@ class PlaybackModelHelper {
|
|||
chapters: playbackModel.chapters,
|
||||
playbackInfo: playbackInfo,
|
||||
trickPlay: playbackModel.trickPlay,
|
||||
media: Media("${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
|
||||
media: Media(url: "${ref.read(userProvider)?.server ?? ""}${mediaSource.transcodingUrl ?? ""}"),
|
||||
mediaStreams: mediaStreamsWithUrls,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/item_base_model.dart';
|
||||
|
|
@ -13,9 +14,7 @@ import 'package:fladder/providers/api_provider.dart';
|
|||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:fladder/util/duration_extensions.dart';
|
||||
import 'package:fladder/util/list_extensions.dart';
|
||||
import 'package:fladder/wrappers/media_control_wrapper.dart'
|
||||
if (dart.library.html) 'package:fladder/wrappers/media_control_wrapper_web.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:fladder/wrappers/media_control_wrapper.dart';
|
||||
|
||||
class TranscodePlaybackModel implements PlaybackModel {
|
||||
TranscodePlaybackModel({
|
||||
|
|
@ -67,22 +66,8 @@ class TranscodePlaybackModel implements PlaybackModel {
|
|||
|
||||
@override
|
||||
Future<TranscodePlaybackModel> setSubtitle(SubStreamModel? model, MediaControlsWrapper player) async {
|
||||
final wantedSubtitle =
|
||||
model ?? subStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultSubStreamIndex);
|
||||
if (wantedSubtitle == null) return this;
|
||||
if (wantedSubtitle.index == SubStreamModel.no().index) {
|
||||
await player.setSubtitleTrack(SubtitleTrack.no());
|
||||
} else {
|
||||
final subTracks = player.subTracks.getRange(2, player.subTracks.length).toList();
|
||||
final index = subStreams.sublist(1).indexWhere((element) => element.id == wantedSubtitle.id);
|
||||
final subTrack = subTracks.elementAtOrNull(index);
|
||||
if (wantedSubtitle.isExternal && wantedSubtitle.url != null && subTrack == null) {
|
||||
await player.setSubtitleTrack(SubtitleTrack.uri(wantedSubtitle.url!));
|
||||
} else if (subTrack != null) {
|
||||
await player.setSubtitleTrack(subTrack);
|
||||
}
|
||||
}
|
||||
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: wantedSubtitle.index));
|
||||
final newIndex = await player.setSubtitleTrack(model, this);
|
||||
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultSubStreamIndex: newIndex));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -90,19 +75,8 @@ class TranscodePlaybackModel implements PlaybackModel {
|
|||
|
||||
@override
|
||||
Future<TranscodePlaybackModel>? setAudio(AudioStreamModel? model, MediaControlsWrapper player) async {
|
||||
final wantedAudioStream =
|
||||
model ?? audioStreams.firstWhereOrNull((element) => element.index == mediaStreams?.defaultAudioStreamIndex);
|
||||
if (wantedAudioStream == null) return this;
|
||||
if (wantedAudioStream.index == AudioStreamModel.no().index) {
|
||||
await player.setAudioTrack(AudioTrack.no());
|
||||
} else {
|
||||
final audioTracks = player.audioTracks.getRange(2, player.audioTracks.length).toList();
|
||||
final audioTrack = audioTracks.elementAtOrNull(audioStreams.indexOf(wantedAudioStream) - 1);
|
||||
if (audioTrack != null) {
|
||||
await player.setAudioTrack(audioTrack);
|
||||
}
|
||||
}
|
||||
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: wantedAudioStream.index));
|
||||
final newIndex = await player.setAudioTrack(model, this);
|
||||
return copyWith(mediaStreams: () => mediaStreams?.copyWith(defaultAudioStreamIndex: newIndex));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
|
|||
@Default(false) bool fillScreen,
|
||||
@Default(true) bool hardwareAccel,
|
||||
@Default(false) bool useLibass,
|
||||
PlayerOptions? playerOptions,
|
||||
@Default(100) double internalVolume,
|
||||
Set<DeviceOrientation>? allowedOrientations,
|
||||
@Default(AutoNextType.smart) AutoNextType nextVideoType,
|
||||
|
|
@ -33,8 +34,10 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
|
|||
|
||||
factory VideoPlayerSettingsModel.fromJson(Map<String, dynamic> json) => _$VideoPlayerSettingsModelFromJson(json);
|
||||
|
||||
PlayerOptions get wantedPlayer => playerOptions ?? PlayerOptions.platformDefaults;
|
||||
|
||||
bool playerSame(VideoPlayerSettingsModel other) {
|
||||
return other.hardwareAccel == hardwareAccel && other.useLibass == useLibass;
|
||||
return other.hardwareAccel == hardwareAccel && other.useLibass == useLibass && other.wantedPlayer == wantedPlayer;
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -48,6 +51,7 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
|
|||
other.hardwareAccel == hardwareAccel &&
|
||||
other.useLibass == useLibass &&
|
||||
other.internalVolume == internalVolume &&
|
||||
other.playerOptions == playerOptions &&
|
||||
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 {
|
||||
off,
|
||||
smart,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ mixin _$VideoPlayerSettingsModel {
|
|||
bool get fillScreen => throw _privateConstructorUsedError;
|
||||
bool get hardwareAccel => throw _privateConstructorUsedError;
|
||||
bool get useLibass => throw _privateConstructorUsedError;
|
||||
PlayerOptions? get playerOptions => throw _privateConstructorUsedError;
|
||||
double get internalVolume => throw _privateConstructorUsedError;
|
||||
Set<DeviceOrientation>? get allowedOrientations =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
|
@ -54,6 +55,7 @@ abstract class $VideoPlayerSettingsModelCopyWith<$Res> {
|
|||
bool fillScreen,
|
||||
bool hardwareAccel,
|
||||
bool useLibass,
|
||||
PlayerOptions? playerOptions,
|
||||
double internalVolume,
|
||||
Set<DeviceOrientation>? allowedOrientations,
|
||||
AutoNextType nextVideoType,
|
||||
|
|
@ -81,6 +83,7 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res,
|
|||
Object? fillScreen = null,
|
||||
Object? hardwareAccel = null,
|
||||
Object? useLibass = null,
|
||||
Object? playerOptions = freezed,
|
||||
Object? internalVolume = null,
|
||||
Object? allowedOrientations = freezed,
|
||||
Object? nextVideoType = null,
|
||||
|
|
@ -107,6 +110,10 @@ class _$VideoPlayerSettingsModelCopyWithImpl<$Res,
|
|||
? _value.useLibass
|
||||
: useLibass // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
playerOptions: freezed == playerOptions
|
||||
? _value.playerOptions
|
||||
: playerOptions // ignore: cast_nullable_to_non_nullable
|
||||
as PlayerOptions?,
|
||||
internalVolume: null == internalVolume
|
||||
? _value.internalVolume
|
||||
: internalVolume // ignore: cast_nullable_to_non_nullable
|
||||
|
|
@ -142,6 +149,7 @@ abstract class _$$VideoPlayerSettingsModelImplCopyWith<$Res>
|
|||
bool fillScreen,
|
||||
bool hardwareAccel,
|
||||
bool useLibass,
|
||||
PlayerOptions? playerOptions,
|
||||
double internalVolume,
|
||||
Set<DeviceOrientation>? allowedOrientations,
|
||||
AutoNextType nextVideoType,
|
||||
|
|
@ -168,6 +176,7 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res>
|
|||
Object? fillScreen = null,
|
||||
Object? hardwareAccel = null,
|
||||
Object? useLibass = null,
|
||||
Object? playerOptions = freezed,
|
||||
Object? internalVolume = null,
|
||||
Object? allowedOrientations = freezed,
|
||||
Object? nextVideoType = null,
|
||||
|
|
@ -194,6 +203,10 @@ class __$$VideoPlayerSettingsModelImplCopyWithImpl<$Res>
|
|||
? _value.useLibass
|
||||
: useLibass // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
playerOptions: freezed == playerOptions
|
||||
? _value.playerOptions
|
||||
: playerOptions // ignore: cast_nullable_to_non_nullable
|
||||
as PlayerOptions?,
|
||||
internalVolume: null == internalVolume
|
||||
? _value.internalVolume
|
||||
: internalVolume // ignore: cast_nullable_to_non_nullable
|
||||
|
|
@ -224,6 +237,7 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
|
|||
this.fillScreen = false,
|
||||
this.hardwareAccel = true,
|
||||
this.useLibass = false,
|
||||
this.playerOptions,
|
||||
this.internalVolume = 100,
|
||||
final Set<DeviceOrientation>? allowedOrientations,
|
||||
this.nextVideoType = AutoNextType.smart,
|
||||
|
|
@ -249,6 +263,8 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
|
|||
@JsonKey()
|
||||
final bool useLibass;
|
||||
@override
|
||||
final PlayerOptions? playerOptions;
|
||||
@override
|
||||
@JsonKey()
|
||||
final double internalVolume;
|
||||
final Set<DeviceOrientation>? _allowedOrientations;
|
||||
|
|
@ -270,7 +286,7 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
|
|||
|
||||
@override
|
||||
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
|
||||
|
|
@ -283,6 +299,7 @@ class _$VideoPlayerSettingsModelImpl extends _VideoPlayerSettingsModel
|
|||
..add(DiagnosticsProperty('fillScreen', fillScreen))
|
||||
..add(DiagnosticsProperty('hardwareAccel', hardwareAccel))
|
||||
..add(DiagnosticsProperty('useLibass', useLibass))
|
||||
..add(DiagnosticsProperty('playerOptions', playerOptions))
|
||||
..add(DiagnosticsProperty('internalVolume', internalVolume))
|
||||
..add(DiagnosticsProperty('allowedOrientations', allowedOrientations))
|
||||
..add(DiagnosticsProperty('nextVideoType', nextVideoType))
|
||||
|
|
@ -313,6 +330,7 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel {
|
|||
final bool fillScreen,
|
||||
final bool hardwareAccel,
|
||||
final bool useLibass,
|
||||
final PlayerOptions? playerOptions,
|
||||
final double internalVolume,
|
||||
final Set<DeviceOrientation>? allowedOrientations,
|
||||
final AutoNextType nextVideoType,
|
||||
|
|
@ -333,6 +351,8 @@ abstract class _VideoPlayerSettingsModel extends VideoPlayerSettingsModel {
|
|||
@override
|
||||
bool get useLibass;
|
||||
@override
|
||||
PlayerOptions? get playerOptions;
|
||||
@override
|
||||
double get internalVolume;
|
||||
@override
|
||||
Set<DeviceOrientation>? get allowedOrientations;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ _$VideoPlayerSettingsModelImpl _$$VideoPlayerSettingsModelImplFromJson(
|
|||
fillScreen: json['fillScreen'] as bool? ?? false,
|
||||
hardwareAccel: json['hardwareAccel'] as bool? ?? true,
|
||||
useLibass: json['useLibass'] as bool? ?? false,
|
||||
playerOptions:
|
||||
$enumDecodeNullable(_$PlayerOptionsEnumMap, json['playerOptions']),
|
||||
internalVolume: (json['internalVolume'] as num?)?.toDouble() ?? 100,
|
||||
allowedOrientations: (json['allowedOrientations'] as List<dynamic>?)
|
||||
?.map((e) => $enumDecode(_$DeviceOrientationEnumMap, e))
|
||||
|
|
@ -33,6 +35,7 @@ Map<String, dynamic> _$$VideoPlayerSettingsModelImplToJson(
|
|||
'fillScreen': instance.fillScreen,
|
||||
'hardwareAccel': instance.hardwareAccel,
|
||||
'useLibass': instance.useLibass,
|
||||
'playerOptions': _$PlayerOptionsEnumMap[instance.playerOptions],
|
||||
'internalVolume': instance.internalVolume,
|
||||
'allowedOrientations': instance.allowedOrientations
|
||||
?.map((e) => _$DeviceOrientationEnumMap[e]!)
|
||||
|
|
@ -51,6 +54,11 @@ const _$BoxFitEnumMap = {
|
|||
BoxFit.scaleDown: 'scaleDown',
|
||||
};
|
||||
|
||||
const _$PlayerOptionsEnumMap = {
|
||||
PlayerOptions.libMDK: 'libMDK',
|
||||
PlayerOptions.libMPV: 'libMPV',
|
||||
};
|
||||
|
||||
const _$DeviceOrientationEnumMap = {
|
||||
DeviceOrientation.portraitUp: 'portraitUp',
|
||||
DeviceOrientation.landscapeLeft: 'landscapeLeft',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:ficonsax/ficonsax.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
|
||||
import 'package:fladder/models/items/chapters_model.dart';
|
||||
|
|
@ -73,8 +72,6 @@ class VideoPlayback {
|
|||
ItemStreamModel? currentItem,
|
||||
SyncedItem? currentSyncedItem,
|
||||
VideoStream? currentStream,
|
||||
Map<AudioStreamModel, AudioTrack>? audioMappings,
|
||||
Map<SubStreamModel, SubtitleTrack>? subMappings,
|
||||
}) {
|
||||
return VideoPlayback(
|
||||
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';
|
||||
|
||||
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
|
||||
: DeviceProfile(
|
||||
maxStreamingBitrate: 120000000,
|
||||
maxStaticBitrate: 120000000,
|
||||
musicStreamingTranscodingBitrate: 384000,
|
||||
directPlayProfiles: [
|
||||
DirectPlayProfile(
|
||||
const DirectPlayProfile(
|
||||
type: DlnaProfileType.video,
|
||||
),
|
||||
DirectPlayProfile(
|
||||
const DirectPlayProfile(
|
||||
type: DlnaProfileType.audio,
|
||||
)
|
||||
],
|
||||
transcodingProfiles: [
|
||||
TranscodingProfile(
|
||||
const TranscodingProfile(
|
||||
audioCodec: 'aac,mp3,mp2',
|
||||
container: 'ts',
|
||||
maxAudioChannels: '2',
|
||||
|
|
@ -28,8 +36,10 @@ const DeviceProfile defaultProfile = kIsWeb
|
|||
],
|
||||
containerProfiles: [],
|
||||
subtitleProfiles: [
|
||||
SubtitleProfile(format: 'vtt', method: SubtitleDeliveryMethod.$external),
|
||||
SubtitleProfile(format: 'ass', method: SubtitleDeliveryMethod.$external),
|
||||
SubtitleProfile(format: 'ssa', method: SubtitleDeliveryMethod.$external),
|
||||
const SubtitleProfile(format: 'vtt', method: SubtitleDeliveryMethod.$external),
|
||||
const SubtitleProfile(format: 'ass', 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,
|
||||
fields: [ItemFields.overview],
|
||||
);
|
||||
newState = newState?.copyWith(
|
||||
episodes: EpisodeModel.episodesFromDto(episodes.body?.items, ref)
|
||||
.where((element) => element.season == newState?.season)
|
||||
.toList());
|
||||
newState = newState?.copyWith(episodes: EpisodeModel.episodesFromDto(episodes.body?.items, ref).toList());
|
||||
state = newState;
|
||||
return season;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -420,11 +420,11 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
|
||||
final playbackResponse = await api.itemsItemIdPlaybackInfoPost(
|
||||
itemId: syncItem.id,
|
||||
body: const PlaybackInfoDto(
|
||||
body: PlaybackInfoDto(
|
||||
enableDirectPlay: true,
|
||||
enableDirectStream: true,
|
||||
enableTranscoding: false,
|
||||
deviceProfile: defaultProfile,
|
||||
deviceProfile: ref.read(videoProfileProvider),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
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/playback/playback_model.dart';
|
||||
import 'package:fladder/providers/settings/video_player_settings_provider.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';
|
||||
import 'package:fladder/wrappers/media_control_wrapper.dart';
|
||||
|
||||
final mediaPlaybackProvider = StateProvider<MediaPlaybackModel>((ref) => MediaPlaybackModel());
|
||||
|
||||
|
|
@ -30,45 +26,37 @@ class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
|
|||
|
||||
late final mediaState = ref.read(mediaPlaybackProvider.notifier);
|
||||
|
||||
bool initMediaControls = false;
|
||||
MediaPlaybackModel get playbackState => ref.read(mediaPlaybackProvider);
|
||||
|
||||
void init() async {
|
||||
state.player?.dispose();
|
||||
if (!initMediaControls && !kDebugMode) {
|
||||
state.init();
|
||||
initMediaControls = true;
|
||||
}
|
||||
await state.dispose();
|
||||
await state.init();
|
||||
|
||||
for (final s in subscriptions) {
|
||||
s.cancel();
|
||||
}
|
||||
final player = state.setup();
|
||||
if (player.platform is NativePlayer) {
|
||||
await (player.platform as dynamic).setProperty(
|
||||
'force-seekable',
|
||||
'yes',
|
||||
);
|
||||
|
||||
final subscription = state.stateStream?.listen((value) {
|
||||
mediaState.update((state) => state.copyWith(buffering: value.buffering));
|
||||
mediaState.update((state) => state.copyWith(buffer: value.buffer));
|
||||
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 {
|
||||
final player = state.player;
|
||||
if (player == null) return;
|
||||
if (!state.hasPlayer) return;
|
||||
mediaState.update((state) => state.copyWith(playing: event));
|
||||
}
|
||||
|
||||
Future<void> updatePosition(Duration event) async {
|
||||
final player = state.player;
|
||||
if (player == null) return;
|
||||
if (!player.state.playing) return;
|
||||
if (!state.hasPlayer) return;
|
||||
if (playbackState.playing == false) return;
|
||||
|
||||
final position = event;
|
||||
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) {
|
||||
mediaState.update((value) => value.copyWith(
|
||||
position: event,
|
||||
playing: player.state.playing,
|
||||
duration: player.state.duration,
|
||||
playing: playbackState.playing,
|
||||
duration: playbackState.duration,
|
||||
lastPosition: position,
|
||||
));
|
||||
ref.read(playBackModel)?.updatePlaybackPosition(position, player.state.playing, ref);
|
||||
ref.read(playBackModel)?.updatePlaybackPosition(position, playbackState.playing, ref);
|
||||
} else {
|
||||
mediaState.update((value) => value.copyWith(
|
||||
position: event,
|
||||
playing: player.state.playing,
|
||||
duration: player.state.duration,
|
||||
playing: playbackState.playing,
|
||||
duration: playbackState.duration,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -100,22 +88,24 @@ class VideoPlayerNotifier extends StateNotifier<MediaControlsWrapper> {
|
|||
PlaybackModel? newPlaybackModel = model;
|
||||
|
||||
if (media != null) {
|
||||
await state.open(media.url, false);
|
||||
await state.setVolume(ref.read(videoPlayerSettingsProvider).volume);
|
||||
await state.open(media, play: false);
|
||||
state.player?.stream.buffering.takeWhile((event) => event == true).listen(
|
||||
state.stateStream?.takeWhile((event) => event.buffering == true).listen(
|
||||
null,
|
||||
onDone: () async {
|
||||
final start = startPosition ?? await model.startDuration();
|
||||
if (start != null) {
|
||||
await state.seek(start);
|
||||
}
|
||||
newPlaybackModel = await newPlaybackModel?.setAudio(null, state);
|
||||
newPlaybackModel = await newPlaybackModel?.setSubtitle(null, state);
|
||||
ref.read(playBackModel.notifier).update((state) => newPlaybackModel);
|
||||
await state.setAudioTrack(null, model);
|
||||
await state.setSubtitleTrack(null, model);
|
||||
state.play();
|
||||
ref.read(playBackModel.notifier).update((state) => newPlaybackModel);
|
||||
},
|
||||
);
|
||||
|
||||
ref.read(playBackModel.notifier).update((state) => model);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ class MediaStreamInformation extends ConsumerWidget {
|
|||
_StreamOptionSelect(
|
||||
label: Text(context.localized.audio),
|
||||
current: mediaStream.currentAudioStream?.displayTitle ?? "",
|
||||
itemBuilder: (context) => mediaStream.audioStreams
|
||||
itemBuilder: (context) => [AudioStreamModel.no(), ...mediaStream.audioStreams]
|
||||
.map(
|
||||
(e) => PopupMenuItem(
|
||||
value: e,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import 'package:fladder/models/account_model.dart';
|
|||
import 'package:fladder/providers/auth_provider.dart';
|
||||
import 'package:fladder/screens/shared/flat_button.dart';
|
||||
import 'package:fladder/screens/shared/user_icon.dart';
|
||||
import 'package:fladder/util/adaptive_layout.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
|
||||
class LoginUserGrid extends ConsumerWidget {
|
||||
|
|
@ -37,79 +36,87 @@ class LoginUserGrid extends ConsumerWidget {
|
|||
itemCount: users.length,
|
||||
itemBuilder: (context, index) {
|
||||
final user = users[index];
|
||||
return _CardHolder(
|
||||
return FlatButton(
|
||||
key: Key(user.id),
|
||||
content: Stack(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(
|
||||
child: UserIcon(
|
||||
labelStyle: Theme.of(context).textTheme.headlineMedium,
|
||||
user: user,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
onTap: () => editMode ? onLongPress?.call(user) : onPressed?.call(user),
|
||||
onLongPress: () => onLongPress?.call(user),
|
||||
child: _CardHolder(
|
||||
content: Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
user.authMethod.icon,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
user.name,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
)),
|
||||
],
|
||||
),
|
||||
if (user.credentials.serverName.isNotEmpty)
|
||||
Opacity(
|
||||
opacity: 0.75,
|
||||
child: Row(
|
||||
child: UserIcon(
|
||||
labelStyle: Theme.of(context).textTheme.headlineMedium,
|
||||
user: user,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Icon(
|
||||
IconsaxBold.driver_2,
|
||||
size: 14,
|
||||
Icon(
|
||||
user.authMethod.icon,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
user.credentials.serverName,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
user.name,
|
||||
maxLines: 2,
|
||||
softWrap: true,
|
||||
)),
|
||||
],
|
||||
),
|
||||
)
|
||||
].addInBetween(const SizedBox(width: 4, height: 4)),
|
||||
),
|
||||
if (editMode)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Card(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
IconsaxBold.edit_2,
|
||||
size: 14,
|
||||
if (user.credentials.serverName.isNotEmpty)
|
||||
Opacity(
|
||||
opacity: 0.75,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Icon(
|
||||
IconsaxBold.driver_2,
|
||||
size: 14,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
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 {
|
||||
final Widget content;
|
||||
final Function() onTap;
|
||||
final Function() onLongPress;
|
||||
|
||||
const _CardHolder({
|
||||
required this.content,
|
||||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -137,14 +140,7 @@ class _CardHolder extends StatelessWidget {
|
|||
margin: EdgeInsets.zero,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150),
|
||||
child: FlatButton(
|
||||
onTap: onTap,
|
||||
onLongPress: AdaptiveLayout.of(context).isDesktop ? onLongPress : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
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_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:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/util/duration_extensions.dart';
|
||||
import 'package:ficonsax/ficonsax.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.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 {
|
||||
final PhotoModel video;
|
||||
final bool showOverlay;
|
||||
|
|
@ -26,10 +30,10 @@ class SimpleVideoPlayer extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with WindowListener, WidgetsBindingObserver {
|
||||
final Player player = Player(
|
||||
configuration: const PlayerConfiguration(title: "nl.jknaapen.fladder", libass: true),
|
||||
);
|
||||
late VideoController controller = VideoController(player);
|
||||
late final player = switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) {
|
||||
PlayerOptions.libMDK => LibMDK(),
|
||||
PlayerOptions.libMPV => LibMPV(),
|
||||
};
|
||||
late String videoUrl = "";
|
||||
|
||||
bool playing = false;
|
||||
|
|
@ -61,9 +65,9 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
|||
super.initState();
|
||||
windowManager.addListener(this);
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
playing = player.state.playing;
|
||||
position = player.state.position;
|
||||
duration = player.state.duration;
|
||||
playing = player.lastState.playing;
|
||||
position = player.lastState.position;
|
||||
duration = player.lastState.duration;
|
||||
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';
|
||||
|
||||
subscriptions.addAll(
|
||||
[
|
||||
player.stream.playing.listen((event) {
|
||||
setState(() {
|
||||
playing = event;
|
||||
});
|
||||
if (playing) {
|
||||
WakelockPlus.enable();
|
||||
} else {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
}),
|
||||
player.stream.position.listen((event) {
|
||||
setState(() {
|
||||
position = event;
|
||||
});
|
||||
}),
|
||||
player.stream.completed.listen((event) {
|
||||
if (event) {
|
||||
_restartVideo();
|
||||
}
|
||||
}),
|
||||
player.stream.duration.listen((event) {
|
||||
setState(() {
|
||||
duration = event;
|
||||
});
|
||||
}),
|
||||
],
|
||||
);
|
||||
subscriptions.add(player.stateStream.listen((event) {
|
||||
setState(() {
|
||||
playing = event.playing;
|
||||
position = event.position;
|
||||
duration = event.duration;
|
||||
});
|
||||
if (playing) {
|
||||
WakelockPlus.enable();
|
||||
} else {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
}));
|
||||
await player.open(videoUrl, !ref.watch(photoViewSettingsProvider).autoPlay);
|
||||
await player.loop(ref.watch(photoViewSettingsProvider.select((value) => value.repeat)));
|
||||
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
|
||||
|
|
@ -142,6 +124,9 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
|||
.textTheme
|
||||
.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold, shadows: [const Shadow(blurRadius: 2)]);
|
||||
ref.listen(photoViewSettingsProvider.select((value) => value.repeat), (previous, next) {
|
||||
player.loop(next);
|
||||
});
|
||||
ref.listen(
|
||||
photoViewSettingsProvider.select((value) => value.mute),
|
||||
(previous, next) {
|
||||
|
|
@ -165,13 +150,17 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
|||
//Fixes small overlay problems with thumbnail
|
||||
Transform.scale(
|
||||
scaleY: 1.004,
|
||||
child: Video(
|
||||
fit: BoxFit.contain,
|
||||
fill: const Color.fromARGB(0, 123, 62, 62),
|
||||
controller: controller,
|
||||
controls: NoVideoControls,
|
||||
wakelock: false,
|
||||
child: player.videoWidget(
|
||||
UniqueKey(),
|
||||
BoxFit.contain,
|
||||
),
|
||||
// child: Video(
|
||||
// fit: BoxFit.contain,
|
||||
// fill: const Color.fromARGB(0, 123, 62, 62),
|
||||
// controller: controller,
|
||||
// controls: NoVideoControls,
|
||||
// wakelock: false,
|
||||
// ),
|
||||
),
|
||||
IgnorePointer(
|
||||
ignoring: !widget.showOverlay,
|
||||
|
|
@ -211,7 +200,7 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
|||
}
|
||||
},
|
||||
onChangeStart: (value) {
|
||||
wasPlaying = player.state.playing;
|
||||
wasPlaying = player.lastState.playing;
|
||||
player.pause();
|
||||
},
|
||||
onChanged: (e) {
|
||||
|
|
@ -239,7 +228,7 @@ class _SimpleVideoPlayerState extends ConsumerState<SimpleVideoPlayer> with Wind
|
|||
player.playOrPause();
|
||||
},
|
||||
icon: Icon(
|
||||
player.state.playing ? IconsaxBold.pause_circle : IconsaxBold.play_circle,
|
||||
player.lastState.playing ? IconsaxBold.pause_circle : IconsaxBold.play_circle,
|
||||
shadows: [
|
||||
BoxShadow(blurRadius: 16, spreadRadius: 2, color: Colors.black.withOpacity(0.15))
|
||||
],
|
||||
|
|
|
|||
|
|
@ -82,34 +82,90 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
),
|
||||
const Divider(),
|
||||
SettingsLabelDivider(label: context.localized.advanced),
|
||||
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) ...[
|
||||
if (PlayerOptions.available.length != 1)
|
||||
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,
|
||||
label: Text(context.localized.playerSettingsBackendTitle),
|
||||
subLabel: Text(context.localized.playerSettingsBackendDesc),
|
||||
trailing: Builder(builder: (context) {
|
||||
final wantedPlayer = ref.watch(videoPlayerSettingsProvider.select((value) => value.wantedPlayer));
|
||||
final currentPlayer = ref.watch(videoPlayerSettingsProvider.select((value) => value.playerOptions));
|
||||
return EnumBox(
|
||||
current: currentPlayer == null
|
||||
? "${context.localized.defaultLabel} (${PlayerOptions.platformDefaults.label(context)})"
|
||||
: wantedPlayer.label(context),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: null,
|
||||
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(
|
||||
label: Text(context.localized.settingsAutoNextTitle),
|
||||
subLabel: Text(context.localized.settingsAutoNextDesc),
|
||||
|
|
@ -138,20 +194,6 @@ class _PlayerSettingsPageState extends ConsumerState<PlayerSettingsPage> {
|
|||
_ => 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)
|
||||
SettingsListTile(
|
||||
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/providers/settings/subtitle_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/localization_helper.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 {
|
||||
const SubtitleEditor({super.key});
|
||||
|
|
|
|||
|
|
@ -14,35 +14,43 @@ class FlatButton extends ConsumerWidget {
|
|||
final Color? splashColor;
|
||||
final double elevation;
|
||||
final Clip clipBehavior;
|
||||
const FlatButton(
|
||||
{this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onDoubleTap,
|
||||
this.onSecondaryTapDown,
|
||||
this.borderRadiusGeometry,
|
||||
this.splashColor,
|
||||
this.elevation = 0,
|
||||
this.clipBehavior = Clip.none,
|
||||
super.key});
|
||||
const FlatButton({
|
||||
this.child,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onDoubleTap,
|
||||
this.onSecondaryTapDown,
|
||||
this.borderRadiusGeometry,
|
||||
this.splashColor,
|
||||
this.elevation = 0,
|
||||
this.clipBehavior = Clip.none,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
clipBehavior: clipBehavior,
|
||||
borderRadius: borderRadiusGeometry ?? FladderTheme.defaultShape.borderRadius,
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
onDoubleTap: onDoubleTap,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
borderRadius: borderRadiusGeometry ?? BorderRadius.circular(10),
|
||||
splashColor: splashColor ?? Theme.of(context).colorScheme.primary.withOpacity(0.5),
|
||||
splashFactory: InkSparkle.splashFactory,
|
||||
child: child ?? Container(),
|
||||
),
|
||||
return Stack(
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
child ?? Container(),
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
clipBehavior: clipBehavior,
|
||||
borderRadius: borderRadiusGeometry ?? FladderTheme.defaultShape.borderRadius,
|
||||
elevation: 0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
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/providers/session_info_provider.dart';
|
||||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:fladder/util/list_padding.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
|
||||
Future<void> showVideoPlaybackInformation(BuildContext context) {
|
||||
return showDialog(
|
||||
|
|
@ -19,6 +22,7 @@ class _VideoPlaybackInformation extends ConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final playbackModel = ref.watch(playBackModel);
|
||||
final sessionInfo = ref.watch(sessionInfoProvider);
|
||||
final backend = ref.read(videoPlayerProvider.select((value) => value.backend));
|
||||
return Dialog(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
|
|
@ -27,47 +31,81 @@ class _VideoPlaybackInformation extends ConsumerWidget {
|
|||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(),
|
||||
...[
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const Text('type: '), Text(playbackModel.label ?? "")],
|
||||
),
|
||||
if (sessionInfo.transCodeInfo != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text("Transcoding", style: Theme.of(context).textTheme.titleMedium),
|
||||
if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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)} %")
|
||||
Text("Playback information", style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4),
|
||||
child: Opacity(
|
||||
opacity: 0.8,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const Text('type: '), Text(playbackModel.label ?? "")],
|
||||
),
|
||||
if (sessionInfo.transCodeInfo != null) ...[
|
||||
Text("Transcoding", style: Theme.of(context).textTheme.titleMedium),
|
||||
if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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())
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (sessionInfo.transCodeInfo?.container != null)
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const Text('container: '), Text(sessionInfo.transCodeInfo!.container.toString())],
|
||||
),
|
||||
],
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [const Text('resolution: '), Text(playbackModel?.item.streamModel?.resolutionText ?? "")],
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('resolution: '),
|
||||
Text(playbackModel?.item.streamModel?.resolutionText ?? "")
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
|
||||
import 'package:fladder/providers/video_player_provider.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 {
|
||||
final Duration position;
|
||||
final Player player;
|
||||
const ChapterButton({super.key, required this.position, required this.player});
|
||||
const ChapterButton({super.key, required this.position});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
|
@ -22,9 +20,9 @@ class ChapterButton extends ConsumerWidget {
|
|||
context,
|
||||
chapters: currentChapters,
|
||||
currentPosition: position,
|
||||
onChapterTapped: (chapter) => player.seek(
|
||||
chapter.startPosition,
|
||||
),
|
||||
onChapterTapped: (chapter) => ref.read(videoPlayerProvider).seek(
|
||||
chapter.startPosition,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
|
|
|
|||
|
|
@ -447,13 +447,13 @@ class _SimpleControls extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
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));
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton.filledTonal(
|
||||
onPressed: () => player?.playOrPause(),
|
||||
onPressed: () => player.playOrPause(),
|
||||
icon: Icon(isPlaying ? IconsaxBold.pause : IconsaxBold.play),
|
||||
),
|
||||
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/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/user_provider.dart';
|
||||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
|
|
@ -375,15 +376,16 @@ Future<void> showSubSelection(BuildContext context) {
|
|||
children: [
|
||||
Text(context.localized.subtitle),
|
||||
const Spacer(),
|
||||
IconButton.outlined(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
showSubtitleControls(
|
||||
context: context,
|
||||
label: context.localized.subtitleConfiguration,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.display_settings_rounded))
|
||||
if (player.backend == PlayerOptions.libMPV)
|
||||
IconButton.outlined(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
showSubtitleControls(
|
||||
context: context,
|
||||
label: context.localized.subtitleConfiguration,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.display_settings_rounded))
|
||||
],
|
||||
),
|
||||
children: playbackModel?.subStreams?.mapIndexed(
|
||||
|
|
@ -459,7 +461,7 @@ Future<void> showPlaybackSpeed(BuildContext context) {
|
|||
return StatefulBuilder(builder: (context, setState) {
|
||||
return Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final player = ref.watch(videoPlayerProvider.select((value) => value.player));
|
||||
final player = ref.watch(videoPlayerProvider);
|
||||
final lastSpeed = ref.watch(playbackRateProvider);
|
||||
return SimpleDialog(
|
||||
contentPadding: const EdgeInsets.only(top: 8, bottom: 24),
|
||||
|
|
@ -484,7 +486,7 @@ Future<void> showPlaybackSpeed(BuildContext context) {
|
|||
divisions: 39,
|
||||
onChanged: (value) {
|
||||
ref.read(playbackRateProvider.notifier).state = value;
|
||||
player?.setRate(value);
|
||||
player.setSpeed(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ class _ChapterProgressSliderState extends ConsumerState<VideoProgressBar> {
|
|||
setState(() {
|
||||
onHoverStart = true;
|
||||
});
|
||||
widget.wasPlayingChanged.call(player.player?.state.playing ?? false);
|
||||
widget.wasPlayingChanged.call(player.lastState?.playing ?? false);
|
||||
player.pause();
|
||||
},
|
||||
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_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
|
||||
import 'package:fladder/models/media_playback_model.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 padding = MediaQuery.of(context).padding;
|
||||
|
||||
final playerController = ref.watch(videoPlayerProvider.select((value) => value.controller));
|
||||
final playerController = ref.watch(videoPlayerProvider.select((value) => value));
|
||||
|
||||
ref.listen(
|
||||
videoPlayerSettingsProvider.select((value) => value.allowedOrientations),
|
||||
|
|
@ -103,24 +102,15 @@ class _VideoPlayerState extends ConsumerState<VideoPlayer> with WidgetsBindingOb
|
|||
lastScale = 0.0;
|
||||
},
|
||||
child: VideoPlayerNextWrapper(
|
||||
video: playerController != null
|
||||
? Padding(
|
||||
padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right),
|
||||
child: OrientationBuilder(builder: (context, orientation) {
|
||||
return Video(
|
||||
key: Key(orientation.toString()),
|
||||
controller: playerController,
|
||||
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(),
|
||||
video: Padding(
|
||||
padding: fillScreen ? EdgeInsets.zero : EdgeInsets.only(left: padding.left, right: padding.right),
|
||||
child: playerController.videoWidget(
|
||||
const Key("VideoPlayer"),
|
||||
fillScreen
|
||||
? (MediaQuery.of(context).orientation == Orientation.portrait ? videoFit : BoxFit.cover)
|
||||
: videoFit,
|
||||
),
|
||||
),
|
||||
controls: const DesktopControls(),
|
||||
overlays: [
|
||||
if (errorPlaying) const _VideoErrorWidget(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.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_seek_indicator.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/util/adaptive_layout.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/localization_helper.dart';
|
||||
import 'package:fladder/util/string_extensions.dart';
|
||||
import 'package:fladder/widgets/shared/full_screen_button.dart'
|
||||
if (dart.library.html) 'package:fladder/widgets/shared/full_screen_button_web.dart';
|
||||
import 'package:fladder/widgets/shared/full_screen_button.dart';
|
||||
|
||||
class DesktopControls extends ConsumerStatefulWidget {
|
||||
const DesktopControls({super.key});
|
||||
|
|
@ -115,7 +112,8 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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(
|
||||
autoFocus: false,
|
||||
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,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (player != null)
|
||||
VideoSubtitles(
|
||||
key: const Key('subtitles'),
|
||||
controller: player,
|
||||
overLayed: showOverlay,
|
||||
),
|
||||
if (subtitleWidget != null) subtitleWidget,
|
||||
if (AdaptiveLayout.of(context).isDesktop)
|
||||
Consumer(builder: (context, ref, child) {
|
||||
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))),
|
||||
const Spacer(),
|
||||
if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer &&
|
||||
ref.read(videoPlayerProvider).player != null) ...{
|
||||
ref.read(videoPlayerProvider).hasPlayer) ...{
|
||||
// OpenQueueButton(x),
|
||||
// ChapterButton(
|
||||
// position: position,
|
||||
|
|
@ -471,7 +464,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
|
|||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
},
|
||||
].addPadding(const EdgeInsets.symmetric(horizontal: 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) {
|
||||
PhotoAlbumModel album => album.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(
|
||||
|
|
|
|||
|
|
@ -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:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'package:fladder/models/media_playback_model.dart';
|
||||
|
|
@ -103,12 +102,11 @@ class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
|||
children: [
|
||||
Hero(
|
||||
tag: videoPlayerHeroTag,
|
||||
child: Video(
|
||||
controller: player.controller!,
|
||||
fit: BoxFit.fitHeight,
|
||||
controls: NoVideoControls,
|
||||
wakelock: playbackInfo.playing,
|
||||
),
|
||||
child: player.videoWidget(
|
||||
UniqueKey(),
|
||||
BoxFit.fitHeight,
|
||||
) ??
|
||||
const SizedBox.shrink(),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Tooltip(
|
||||
|
|
@ -169,8 +167,7 @@ class _CurrentlyPlayingBarState extends ConsumerState<FloatingPlayerBar> {
|
|||
if (constraints.maxWidth > 500) ...{
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
final volume = player.player?.state.volume == 0 ? 100.0 : 0.0;
|
||||
ref.read(videoPlayerSettingsProvider.notifier).setVolume(volume);
|
||||
final volume = player.lastState?.volume == 0 ? 100.0 : 0.0;
|
||||
player.setVolume(volume);
|
||||
},
|
||||
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 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:collection/collection.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:smtc_windows/smtc_windows.dart';
|
||||
import 'package:smtc_windows/smtc_windows.dart' if (dart.library.html) 'package:fladder/stubs/web/smtc_web.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.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/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:fladder/wrappers/players/base_player.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});
|
||||
|
||||
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;
|
||||
|
||||
List<StreamSubscription> subscriptions = [];
|
||||
SMTCWindows? smtc;
|
||||
|
||||
@override
|
||||
bool initMediaControls = false;
|
||||
|
||||
Future<void> init() async {
|
||||
await AudioService.init(
|
||||
builder: () => this,
|
||||
config: audioServiceConfig,
|
||||
);
|
||||
if (!initMediaControls && !kDebugMode) {
|
||||
await AudioService.init(
|
||||
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
|
||||
Player setup() => setPlayer(_initPlayer());
|
||||
Future<void> dispose() async => _player?.dispose();
|
||||
|
||||
Player _initPlayer() {
|
||||
Future<void> setup(BasePlayer newPlayer) async {
|
||||
_player = newPlayer;
|
||||
await newPlayer.init(ref);
|
||||
_initPlayer();
|
||||
}
|
||||
|
||||
void _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: !kIsWeb &&
|
||||
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> open(String url, bool play) async => _player?.open(url, play);
|
||||
|
||||
void _subscribePlayer() {
|
||||
if (Platform.isWindows) {
|
||||
if (Platform.isWindows && !kIsWeb) {
|
||||
smtc = SMTCWindows(
|
||||
config: const SMTCConfig(
|
||||
fastForwardEnabled: true,
|
||||
|
|
@ -113,43 +135,33 @@ class MediaControlsWrapper extends MediaPlayback implements MediaControlBase {
|
|||
}
|
||||
}
|
||||
|
||||
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,
|
||||
));
|
||||
smtc?.setPosition(position);
|
||||
}),
|
||||
player?.stream.playing.listen((playing) {
|
||||
if (playing) {
|
||||
WakelockPlus.enable();
|
||||
} else {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
playing: playing,
|
||||
));
|
||||
smtc?.setPlaybackStatus(playing ? PlaybackStatus.Playing : PlaybackStatus.Paused);
|
||||
}),
|
||||
].whereNotNull());
|
||||
subscriptions.add(_player!.stateStream.listen((value) {
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
bufferedPosition: value.buffer,
|
||||
));
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
processingState: value.buffering ? AudioProcessingState.buffering : AudioProcessingState.ready,
|
||||
));
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
updatePosition: value.position,
|
||||
));
|
||||
smtc?.setPosition(value.position);
|
||||
if (value.playing) {
|
||||
WakelockPlus.enable();
|
||||
} else {
|
||||
WakelockPlus.disable();
|
||||
}
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
playing: value.playing,
|
||||
));
|
||||
smtc?.setPlaybackStatus(value.playing ? PlaybackStatus.Playing : PlaybackStatus.Paused);
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() async {
|
||||
if (!ref.read(clientSettingsProvider).enableMediaKeys) {
|
||||
await player?.play();
|
||||
return super.play();
|
||||
}
|
||||
_player?.play();
|
||||
if (!ref.read(clientSettingsProvider).enableMediaKeys) return;
|
||||
|
||||
final playBackItem = ref.read(playBackModel.select((value) => value?.item));
|
||||
final currentPosition = await ref.read(playBackModel.select((value) => value?.startDuration()));
|
||||
|
|
@ -182,7 +194,6 @@ class MediaControlsWrapper extends MediaPlayback implements MediaControlBase {
|
|||
processingState: AudioProcessingState.ready,
|
||||
));
|
||||
|
||||
await player?.play();
|
||||
return super.play();
|
||||
}
|
||||
|
||||
|
|
@ -211,9 +222,11 @@ class MediaControlsWrapper extends MediaPlayback implements MediaControlBase {
|
|||
@override
|
||||
Future<void> stop() async {
|
||||
WakelockPlus.disable();
|
||||
final position = player?.state.position;
|
||||
final totalDuration = player?.state.duration;
|
||||
await player?.stop();
|
||||
final position = _player?.lastState.position;
|
||||
final totalDuration = _player?.lastState.duration;
|
||||
super.stop();
|
||||
_player?.stop();
|
||||
|
||||
ref.read(playBackModel)?.playbackStopped(position ?? Duration.zero, totalDuration, ref);
|
||||
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(position: Duration.zero));
|
||||
smtc?.setPlaybackStatus(PlaybackStatus.Stopped);
|
||||
|
|
@ -229,16 +242,37 @@ class MediaControlsWrapper extends MediaPlayback implements MediaControlBase {
|
|||
return super.stop();
|
||||
}
|
||||
|
||||
@override
|
||||
void playOrPause() async {
|
||||
await player?.playOrPause();
|
||||
Future<void> playOrPause() async {
|
||||
await _player?.playOrPause();
|
||||
playbackState.add(playbackState.value.copyWith(
|
||||
playing: player?.state.playing ?? false,
|
||||
playing: _player?.lastState.playing ?? false,
|
||||
controls: [MediaControl.play],
|
||||
));
|
||||
final playerState = player;
|
||||
final playerState = _player;
|
||||
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 <dynamic_color/dynamic_color_plugin.h>
|
||||
#include <fvp/fvp_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_video/media_kit_video_plugin.h>
|
||||
|
|
@ -22,6 +23,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||
g_autoptr(FlPluginRegistrar) dynamic_color_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin");
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin");
|
||||
isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_drop
|
||||
dynamic_color
|
||||
fvp
|
||||
isar_flutter_libs
|
||||
media_kit_libs_linux
|
||||
media_kit_video
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import audio_service
|
|||
import audio_session
|
||||
import desktop_drop
|
||||
import dynamic_color
|
||||
import fvp
|
||||
import isar_flutter_libs
|
||||
import just_audio
|
||||
import local_auth_darwin
|
||||
|
|
@ -32,6 +33,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||
DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin"))
|
||||
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
|
||||
FvpPlugin.register(with: registry.registrar(forPlugin: "FvpPlugin"))
|
||||
IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin"))
|
||||
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
||||
FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin"))
|
||||
|
|
|
|||
10
pubspec.lock
10
pubspec.lock
|
|
@ -746,6 +746,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -2016,7 +2024,7 @@ packages:
|
|||
source: hosted
|
||||
version: "2.1.4"
|
||||
video_player:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player
|
||||
sha256: "4a8c3492d734f7c39c2588a3206707a05ee80cef52e8c7f3b2078d430c84bc17"
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ dependencies:
|
|||
media_kit_video: ^1.2.4 # For video rendering.
|
||||
media_kit_libs_video: ^1.0.4 # Native video dependencies.
|
||||
audio_service: ^0.18.12
|
||||
fvp: ^0.28.0
|
||||
video_player: ^2.9.2
|
||||
|
||||
# UI Components
|
||||
dynamic_color: ^1.7.0
|
||||
|
|
@ -158,6 +160,7 @@ flutter:
|
|||
- icons/
|
||||
- assets/fonts/
|
||||
- config/
|
||||
- assets/mp-font.ttf
|
||||
|
||||
fonts:
|
||||
- family: Rubik
|
||||
|
|
@ -173,7 +176,3 @@ flutter:
|
|||
style: normal
|
||||
- asset: assets/fonts/opensans/OpenSans-Italic.ttf
|
||||
style: italic
|
||||
|
||||
- family: mp-font
|
||||
fonts:
|
||||
- asset: assets/fonts/mp-font.ttf
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#include <desktop_drop/desktop_drop_plugin.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 <local_auth_windows/local_auth_plugin.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"));
|
||||
DynamicColorPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
|
||||
FvpPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FvpPluginCApi"));
|
||||
IsarFlutterLibsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin"));
|
||||
LocalAuthPluginRegisterWithRegistrar(
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
desktop_drop
|
||||
dynamic_color
|
||||
fvp
|
||||
isar_flutter_libs
|
||||
local_auth_windows
|
||||
media_kit_libs_windows_video
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue