mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-08 23:18:16 -07:00
feat: Android TV support (#503)
Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
parent
7ab8c015b9
commit
c299492d6d
168 changed files with 12019 additions and 3073 deletions
|
|
@ -14,17 +14,20 @@ import 'package:fladder/models/items/media_streams_model.dart';
|
|||
import 'package:fladder/models/media_playback_model.dart';
|
||||
import 'package:fladder/models/playback/playback_model.dart';
|
||||
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||
import 'package:fladder/providers/arguments_provider.dart';
|
||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
|
||||
import 'package:fladder/providers/video_player_provider.dart';
|
||||
import 'package:fladder/src/video_player_helper.g.dart' hide PlaybackState;
|
||||
import 'package:fladder/util/localization_helper.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/native_player.dart';
|
||||
import 'package:fladder/wrappers/players/player_states.dart';
|
||||
|
||||
class MediaControlsWrapper extends BaseAudioHandler {
|
||||
class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerControlsCallback {
|
||||
MediaControlsWrapper({required this.ref});
|
||||
|
||||
BasePlayer? _player;
|
||||
|
|
@ -49,11 +52,12 @@ class MediaControlsWrapper extends BaseAudioHandler {
|
|||
List<StreamSubscription> subscriptions = [];
|
||||
SMTCWindows? smtc;
|
||||
|
||||
bool initMediaControls = false;
|
||||
bool initializedWrapper = false;
|
||||
|
||||
Future<void> init() async {
|
||||
if (!initMediaControls) {
|
||||
initMediaControls = true;
|
||||
if (!initializedWrapper) {
|
||||
initializedWrapper = true;
|
||||
VideoPlayerControlsCallback.setUp(this);
|
||||
await AudioService.init(
|
||||
builder: () => this,
|
||||
config: const AudioServiceConfig(
|
||||
|
|
@ -69,10 +73,13 @@ class MediaControlsWrapper extends BaseAudioHandler {
|
|||
);
|
||||
}
|
||||
|
||||
final player = switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) {
|
||||
PlayerOptions.libMDK => LibMDK(),
|
||||
PlayerOptions.libMPV => LibMPV(),
|
||||
};
|
||||
final player = ref.read(argumentsStateProvider).leanBackMode
|
||||
? NativePlayer()
|
||||
: switch (ref.read(videoPlayerSettingsProvider.select((value) => value.wantedPlayer))) {
|
||||
PlayerOptions.libMDK => LibMDK(),
|
||||
PlayerOptions.libMPV => LibMPV(),
|
||||
PlayerOptions.nativePlayer => NativePlayer(),
|
||||
};
|
||||
|
||||
setup(player);
|
||||
}
|
||||
|
|
@ -93,7 +100,15 @@ class MediaControlsWrapper extends BaseAudioHandler {
|
|||
_subscribePlayer();
|
||||
}
|
||||
|
||||
Future<void> open(String url, bool play) async => _player?.open(url, play);
|
||||
Future<void> loadVideo(PlaybackModel model, Duration startPosition, bool play) async {
|
||||
if (_player is NativePlayer) {
|
||||
final context = ref.read(localizationContextProvider);
|
||||
await (_player as NativePlayer).sendPlaybackDataToNative(context, model, startPosition);
|
||||
}
|
||||
return _player?.loadVideo(model.media?.url ?? "", play);
|
||||
}
|
||||
|
||||
Future<void> openPlayer(BuildContext context) async => _player?.open(context);
|
||||
|
||||
void _subscribePlayer() {
|
||||
if (Platform.isWindows && !kIsWeb) {
|
||||
|
|
@ -313,4 +328,46 @@ class MediaControlsWrapper extends BaseAudioHandler {
|
|||
_player?.setSpeed(speed);
|
||||
return super.setSpeed(speed);
|
||||
}
|
||||
|
||||
//Native player calls
|
||||
//
|
||||
//
|
||||
@override
|
||||
void loadNextVideo() async {
|
||||
final nextVideo = ref.read(playBackModel.select((value) => value?.nextVideo));
|
||||
final buffering = ref.read(mediaPlaybackProvider.select((value) => value.buffering));
|
||||
if (nextVideo != null && !buffering) ref.read(playbackModelHelper).loadNewVideo(nextVideo);
|
||||
}
|
||||
|
||||
@override
|
||||
void loadPreviousVideo() async {
|
||||
final previousVideo = ref.read(playBackModel.select((value) => value?.previousVideo));
|
||||
final buffering = ref.read(mediaPlaybackProvider.select((value) => value.buffering));
|
||||
if (previousVideo != null && !buffering) ref.read(playbackModelHelper).loadNewVideo(previousVideo);
|
||||
}
|
||||
|
||||
@override
|
||||
void onStop() => stop();
|
||||
|
||||
@override
|
||||
void swapAudioTrack(int value) async {
|
||||
final playbackModel = ref.read(playBackModel);
|
||||
final newModel = await playbackModel?.setAudio(
|
||||
playbackModel.audioStreams?.firstWhere((element) => element.index == value), this);
|
||||
ref.read(playBackModel.notifier).update((state) => newModel);
|
||||
if (newModel != null) {
|
||||
await ref.read(playbackModelHelper).shouldReload(newModel);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void swapSubtitleTrack(int value) async {
|
||||
final playbackModel = ref.read(playBackModel);
|
||||
final newModel = await playbackModel?.setSubtitle(
|
||||
playbackModel.subStreams?.firstWhere((element) => element.index == value), this);
|
||||
ref.read(playBackModel.notifier).update((state) => newModel);
|
||||
if (newModel != null) {
|
||||
await ref.read(playbackModelHelper).shouldReload(newModel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ abstract class BasePlayer {
|
|||
GlobalKey? controlsKey,
|
||||
});
|
||||
Future<void> dispose();
|
||||
Future<void> open(String url, bool play);
|
||||
Future<void> open(BuildContext context);
|
||||
Future<void> loadVideo(String url, bool play);
|
||||
Future<void> seek(Duration position);
|
||||
Future<void> play();
|
||||
Future<void> setVolume(double volume);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ 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/models/settings/video_player_settings.dart';
|
||||
import 'package:fladder/screens/video_player/video_player.dart' as video_screen;
|
||||
import 'package:fladder/wrappers/players/base_player.dart';
|
||||
import 'package:fladder/wrappers/players/player_states.dart';
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ class LibMDK extends BasePlayer {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> open(String url, bool play) async {
|
||||
Future<void> loadVideo(String url, bool play) async {
|
||||
if (_controller != null) {
|
||||
_controller?.dispose();
|
||||
}
|
||||
|
|
@ -95,6 +96,13 @@ class LibMDK extends BasePlayer {
|
|||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> open(BuildContext context) async => Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const video_screen.VideoPlayer(),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> pause() async => _controller?.pause();
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import 'package:fladder/models/playback/playback_model.dart';
|
|||
import 'package:fladder/models/settings/subtitle_settings_model.dart';
|
||||
import 'package:fladder/models/settings/video_player_settings.dart';
|
||||
import 'package:fladder/providers/settings/subtitle_settings_provider.dart';
|
||||
import 'package:fladder/screens/video_player/video_player.dart' as video_screen;
|
||||
import 'package:fladder/util/subtitle_position_calculator.dart';
|
||||
import 'package:fladder/wrappers/players/base_player.dart';
|
||||
import 'package:fladder/wrappers/players/player_states.dart';
|
||||
|
|
@ -82,11 +83,18 @@ class LibMPV extends BasePlayer {
|
|||
}
|
||||
|
||||
@override
|
||||
Future<void> open(String url, bool play) async {
|
||||
Future<void> loadVideo(String url, bool play) async {
|
||||
await _player?.open(mpv.Media(url), play: play);
|
||||
return setState(lastState.update(buffering: true));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> open(BuildContext context) async => Navigator.of(context, rootNavigator: true).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const video_screen.VideoPlayer(),
|
||||
),
|
||||
);
|
||||
|
||||
List<mpv.SubtitleTrack> get subTracks => _player?.state.tracks.subtitle ?? [];
|
||||
mpv.SubtitleTrack get subtitleTrack => _player?.state.track.subtitle ?? mpv.SubtitleTrack.no();
|
||||
|
||||
|
|
|
|||
167
lib/wrappers/players/native_player.dart
Normal file
167
lib/wrappers/players/native_player.dart
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.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/src/video_player_helper.g.dart';
|
||||
import 'package:fladder/wrappers/players/base_player.dart';
|
||||
import 'package:fladder/wrappers/players/player_states.dart';
|
||||
|
||||
class NativePlayer extends BasePlayer implements VideoPlayerListenerCallback {
|
||||
final player = VideoPlayerApi();
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
return NativeVideoActivity().disposeActivity();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> init(VideoPlayerSettingsModel settings) async => VideoPlayerListenerCallback.setUp(this);
|
||||
|
||||
@override
|
||||
Future<void> loop(bool loop) {
|
||||
return player.setLooping(loop);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> loadVideo(String url, bool play) async => player.open(url, play);
|
||||
|
||||
@override
|
||||
Future<void> open(BuildContext newContext) async => NativeVideoActivity().launchActivity();
|
||||
|
||||
@override
|
||||
Future<void> pause() {
|
||||
return player.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() => player.play();
|
||||
|
||||
@override
|
||||
Future<void> playOrPause() async {
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seek(Duration position) {
|
||||
return player.seekTo(position.inMilliseconds);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> setAudioTrack(AudioStreamModel? model, PlaybackModel playbackModel) async {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setSpeed(double speed) async {}
|
||||
|
||||
@override
|
||||
Future<int> setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setVolume(double volume) async {
|
||||
return player.setVolume(volume);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
return player.stop();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? subtitles(bool showOverlay, {GlobalKey<State<StatefulWidget>>? controlsKey}) => null;
|
||||
|
||||
@override
|
||||
Widget? videoWidget(Key key, BoxFit fit) => null;
|
||||
|
||||
@override
|
||||
void onPlaybackStateChanged(PlaybackState state) {
|
||||
lastState = lastState.update(
|
||||
playing: state.playing,
|
||||
position: Duration(milliseconds: state.position),
|
||||
buffer: Duration(milliseconds: state.buffered),
|
||||
buffering: state.buffering,
|
||||
);
|
||||
_stateController.add(lastState);
|
||||
}
|
||||
|
||||
final StreamController<PlayerState> _stateController = StreamController.broadcast();
|
||||
|
||||
@override
|
||||
Stream<PlayerState> get stateStream => _stateController.stream;
|
||||
|
||||
Future<void> sendPlaybackDataToNative(
|
||||
BuildContext? context,
|
||||
PlaybackModel model,
|
||||
Duration startPosition,
|
||||
) async {
|
||||
final playableData = PlayableData(
|
||||
id: model.item.id,
|
||||
title: model.item.title,
|
||||
subTitle: context != null ? model.item.label(context) : "",
|
||||
logoUrl: model.item.getPosters?.logo?.path,
|
||||
startPosition: startPosition.inMilliseconds,
|
||||
description: model.item.overview.summary,
|
||||
defaultAudioTrack: model.mediaStreams?.defaultAudioStreamIndex ?? 1,
|
||||
nextVideo: model.nextVideo?.name,
|
||||
previousVideo: model.previousVideo?.name,
|
||||
audioTracks: model.audioStreams
|
||||
?.map(
|
||||
(audio) => AudioTrack(
|
||||
name: audio.displayTitle,
|
||||
languageCode: audio.language,
|
||||
codec: audio.codec,
|
||||
index: audio.index,
|
||||
external: false,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
[],
|
||||
defaultSubtrack: model.mediaStreams?.defaultSubStreamIndex ?? 1,
|
||||
subtitleTracks: model.subStreams
|
||||
?.map(
|
||||
(sub) => SubtitleTrack(
|
||||
name: sub.displayTitle,
|
||||
languageCode: sub.language,
|
||||
codec: sub.codec,
|
||||
index: sub.index,
|
||||
external: sub.isExternal,
|
||||
url: sub.url,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
[],
|
||||
segments: model.mediaSegments?.segments
|
||||
.map(
|
||||
(e) => MediaSegment(
|
||||
type: MediaSegmentType.values.firstWhere((element) => element.name == e.type.name),
|
||||
name: context != null ? e.type.label(context) : e.type.name,
|
||||
start: e.start.inMilliseconds,
|
||||
end: e.end.inMilliseconds,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
[],
|
||||
trickPlayModel: model.trickPlay != null
|
||||
? TrickPlayModel(
|
||||
width: model.trickPlay!.width,
|
||||
height: model.trickPlay!.height,
|
||||
tileWidth: model.trickPlay!.tileWidth,
|
||||
tileHeight: model.trickPlay!.tileHeight,
|
||||
thumbnailCount: model.trickPlay!.thumbnailCount,
|
||||
interval: model.trickPlay!.interval.inMilliseconds,
|
||||
images: model.trickPlay?.images ?? [])
|
||||
: null,
|
||||
chapters: model.chapters
|
||||
?.map((e) => Chapter(name: e.name, url: e.imageUrl, time: e.startPosition.inMilliseconds))
|
||||
.toList() ??
|
||||
[],
|
||||
url: model.media?.url ?? "",
|
||||
);
|
||||
player.sendPlayableModel(playableData);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue