mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-09 07:28:14 -07: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
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue