rip out media_kit, use better_player and audioplayers
This also let us rip out cocoapods stuff. Needs testing on Windows, MacOS, and ideally, iOS.
This commit is contained in:
parent
161d39322c
commit
37a2c150f3
4 changed files with 375 additions and 604 deletions
|
|
@ -4,7 +4,6 @@ import "package:dynamic_color/dynamic_color.dart";
|
|||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:material_emoji_picker/material_emoji_picker.dart";
|
||||
import "package:media_kit/media_kit.dart";
|
||||
import "package:nexus/controllers/active_account.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/controllers/client_state.dart";
|
||||
|
|
@ -65,7 +64,6 @@ void showError(Object error, [StackTrace? stackTrace]) {
|
|||
|
||||
void main(List<String> args) async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
MediaKit.ensureInitialized();
|
||||
|
||||
LicenseRegistry.addLicense(() => .fromIterable(fontLicenses));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:audioplayers/audioplayers.dart" as audioplayers;
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:media_kit/media_kit.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/helpers/extensions/save_attachment.dart";
|
||||
import "package:nexus/models/info/audio.dart";
|
||||
|
|
@ -18,9 +18,7 @@ class const AudioPlayer(
|
|||
}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final player = useMemoized(
|
||||
() => Player(configuration: .new(bufferSize: 128 * 1024 * 1024)),
|
||||
);
|
||||
final player = useMemoized(audioplayers.AudioPlayer.new);
|
||||
final loaded = useState(false);
|
||||
|
||||
useEffect(() {
|
||||
|
|
@ -29,19 +27,39 @@ class const AudioPlayer(
|
|||
.read(ClientController.provider.notifier)
|
||||
.downloadMedia(.new(mxc: uri, encrypted: encrypted));
|
||||
if (!context.mounted) return;
|
||||
await player.open(Media(audio.path), play: false);
|
||||
// Keeps the audio loaded after it finishes, so it can be replayed
|
||||
await player.setReleaseMode(.stop);
|
||||
await player.setSource(audioplayers.DeviceFileSource(audio.path));
|
||||
if (context.mounted) loaded.value = true;
|
||||
});
|
||||
|
||||
return player.dispose;
|
||||
}, []);
|
||||
|
||||
final state = useStream(
|
||||
player.onPlayerStateChanged,
|
||||
initialData: player.state,
|
||||
).requireData;
|
||||
final position = useStream(
|
||||
player.onPositionChanged,
|
||||
initialData: Duration.zero,
|
||||
).requireData;
|
||||
final duration = useStream(
|
||||
player.onDurationChanged,
|
||||
initialData: Duration.zero,
|
||||
).requireData;
|
||||
final playing = state == .playing;
|
||||
|
||||
return Card(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
child: Padding(
|
||||
padding: .symmetric(horizontal: 8),
|
||||
child: PlayerControls(
|
||||
player,
|
||||
playing: playing,
|
||||
position: position,
|
||||
duration: duration,
|
||||
onPlayPause: playing ? player.pause : player.resume,
|
||||
onSeek: player.seek,
|
||||
loading: !loaded.value,
|
||||
size: info?.size,
|
||||
actions: .new([
|
||||
|
|
|
|||
|
|
@ -1,39 +1,25 @@
|
|||
import "dart:math";
|
||||
|
||||
import "package:fast_immutable_collections/fast_immutable_collections.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:media_kit/media_kit.dart";
|
||||
import "package:nexus/helpers/extensions/size_to_string.dart";
|
||||
|
||||
/// Play/pause, position and seeking controls for a media_kit [Player].
|
||||
class const PlayerControls(
|
||||
final Player player, {
|
||||
/// Play/pause, position and seeking controls, for any player.
|
||||
class const PlayerControls({
|
||||
required final bool playing,
|
||||
required final Duration position,
|
||||
required final Duration duration,
|
||||
required final VoidCallback onPlayPause,
|
||||
required final ValueChanged<Duration> onSeek,
|
||||
final bool loading = false,
|
||||
final bool buffering = false,
|
||||
final Color? color,
|
||||
final int? size,
|
||||
final IList<Widget> actions = const .empty(),
|
||||
super.key,
|
||||
}) extends HookWidget {
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final playing = useStream(
|
||||
player.stream.playing,
|
||||
initialData: player.state.playing,
|
||||
).requireData;
|
||||
final buffering = useStream(
|
||||
player.stream.buffering,
|
||||
initialData: player.state.buffering,
|
||||
).requireData;
|
||||
final position = useStream(
|
||||
player.stream.position,
|
||||
initialData: player.state.position,
|
||||
).requireData;
|
||||
final duration = useStream(
|
||||
player.stream.duration,
|
||||
initialData: player.state.duration,
|
||||
).requireData;
|
||||
|
||||
String format(Duration duration) {
|
||||
final minutes = duration.inMinutes
|
||||
.remainder(60)
|
||||
|
|
@ -61,7 +47,7 @@ class const PlayerControls(
|
|||
child: CircularProgressIndicator(padding: .all(12), color: color),
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: player.playOrPause,
|
||||
onPressed: onPlayPause,
|
||||
color: color,
|
||||
tooltip: playing ? "Pause" : "Play",
|
||||
icon: Icon(playing ? Icons.pause_circle : Icons.play_circle),
|
||||
|
|
@ -73,7 +59,7 @@ class const PlayerControls(
|
|||
value: min(position.inMilliseconds.toDouble(), max),
|
||||
onChanged: loading
|
||||
? null
|
||||
: (value) => player.seek(.new(milliseconds: value.toInt())),
|
||||
: (value) => onSeek(.new(milliseconds: value.toInt())),
|
||||
);
|
||||
|
||||
return LayoutBuilder(
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import "dart:async";
|
||||
|
||||
import "package:better_player/better_player.dart";
|
||||
import "package:material_ui/material_ui.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:nexus/controllers/client.dart";
|
||||
import "package:nexus/helpers/extensions/save_attachment.dart";
|
||||
import "package:nexus/models/info/video.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:media_kit/media_kit.dart";
|
||||
import "package:media_kit_video/media_kit_video.dart";
|
||||
import "package:nexus/widgets/players/player_controls.dart";
|
||||
|
||||
class const VideoPlayer(
|
||||
|
|
@ -19,11 +18,118 @@ class const VideoPlayer(
|
|||
}) extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final player = useMemoized(
|
||||
() => Player(configuration: .new(bufferSize: 128 * 1024 * 1024)),
|
||||
);
|
||||
final controller = useMemoized(() => VideoController(player));
|
||||
final aspectRatio = switch (info) {
|
||||
VideoInfo(:final width?, :final height?) when width > 0 && height > 0 =>
|
||||
width / height,
|
||||
_ => 16 / 9,
|
||||
};
|
||||
|
||||
final loaded = useState(false);
|
||||
final controller = useMemoized(
|
||||
() => BetterPlayerController(
|
||||
PlayerConfiguration(
|
||||
aspectRatio: aspectRatio,
|
||||
fit: .contain,
|
||||
controlsConfiguration: PlayerControlsConfiguration(
|
||||
playerTheme: .custom,
|
||||
customControlsBuilder: (controller, _) => HookBuilder(
|
||||
builder: (context) {
|
||||
final isLoaded = useValueListenable(loaded);
|
||||
final value = useState(controller.videoPlayerValue);
|
||||
useEffect(() {
|
||||
void listener() => value.value = controller.videoPlayerValue;
|
||||
controller.addVideoListener(listener);
|
||||
return () => controller.removeVideoListener(listener);
|
||||
}, [controller]);
|
||||
|
||||
final playing = value.value?.isPlaying ?? false;
|
||||
|
||||
// better_player doesn't use material_ui, so this Material is needed.
|
||||
return Material(
|
||||
type: .transparency,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: .opaque,
|
||||
onTap: isLoaded
|
||||
? playing
|
||||
? controller.pause
|
||||
: controller.play
|
||||
: null,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: IconButton(
|
||||
onPressed: () => context.saveAttachment(
|
||||
ref,
|
||||
uri,
|
||||
encrypted: encrypted,
|
||||
filename: filename,
|
||||
),
|
||||
color: Colors.white,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.black38,
|
||||
),
|
||||
tooltip: "Download",
|
||||
icon: Icon(Icons.download),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: .bottomCenter,
|
||||
end: .topCenter,
|
||||
colors: [Colors.black54, Colors.transparent],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const .only(left: 4, right: 4, top: 16),
|
||||
child: PlayerControls(
|
||||
playing: playing,
|
||||
buffering: value.value?.isBuffering ?? false,
|
||||
position: value.value?.position ?? .zero,
|
||||
duration: value.value?.duration ?? .zero,
|
||||
onPlayPause: playing
|
||||
? controller.pause
|
||||
: controller.play,
|
||||
onSeek: controller.seekTo,
|
||||
loading: !isLoaded,
|
||||
size: info?.size,
|
||||
color: Colors.white,
|
||||
actions: .new([
|
||||
IconButton(
|
||||
onPressed: controller.toggleFullScreen,
|
||||
color: Colors.white,
|
||||
tooltip: controller.isFullScreen
|
||||
? "Exit fullscreen"
|
||||
: "Fullscreen",
|
||||
icon: Icon(
|
||||
controller.isFullScreen
|
||||
? Icons.fullscreen_exit
|
||||
: Icons.fullscreen,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
scheduleMicrotask(() async {
|
||||
|
|
@ -31,109 +137,20 @@ class const VideoPlayer(
|
|||
.read(ClientController.provider.notifier)
|
||||
.downloadMedia(.new(mxc: uri, encrypted: encrypted));
|
||||
if (!context.mounted) return;
|
||||
await player.open(Media(video.path), play: false);
|
||||
await controller.setupDataSource(.file(video.path));
|
||||
if (context.mounted) loaded.value = true;
|
||||
});
|
||||
|
||||
return player.dispose;
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
final controls = useCallback(
|
||||
(VideoState _) =>
|
||||
// Until media_kit moves to material_ui, this Material is needed.
|
||||
Material(
|
||||
type: .transparency,
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: loaded,
|
||||
builder: (_, loaded, _) => Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: .opaque,
|
||||
onTap: loaded ? player.playOrPause : null,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: IconButton(
|
||||
onPressed: () => context.saveAttachment(
|
||||
ref,
|
||||
uri,
|
||||
encrypted: encrypted,
|
||||
filename: filename,
|
||||
),
|
||||
color: Colors.white,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.black38,
|
||||
),
|
||||
tooltip: "Download",
|
||||
icon: Icon(Icons.download),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: DecoratedBox(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: .bottomCenter,
|
||||
end: .topCenter,
|
||||
colors: [Colors.black54, Colors.transparent],
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const .only(left: 4, right: 4, top: 16),
|
||||
child: PlayerControls(
|
||||
player,
|
||||
loading: !loaded,
|
||||
size: info?.size,
|
||||
color: Colors.white,
|
||||
actions: .new([
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final fullscreen = isFullscreen(context);
|
||||
|
||||
return IconButton(
|
||||
onPressed: () => toggleFullscreen(context),
|
||||
color: Colors.white,
|
||||
tooltip: fullscreen
|
||||
? "Exit fullscreen"
|
||||
: "Fullscreen",
|
||||
icon: Icon(
|
||||
fullscreen
|
||||
? Icons.fullscreen_exit
|
||||
: Icons.fullscreen,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
[uri, encrypted, filename, info],
|
||||
);
|
||||
|
||||
final aspectRatio = switch (info) {
|
||||
VideoInfo(:final width?, :final height?) when width > 0 && height > 0 =>
|
||||
width / height,
|
||||
_ => 16 / 9,
|
||||
};
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: const .new(maxHeight: 300),
|
||||
child: AspectRatio(
|
||||
aspectRatio: aspectRatio,
|
||||
child: ClipRRect(
|
||||
borderRadius: .circular(12),
|
||||
child: Video(controller: controller, controls: controls),
|
||||
child: BetterPlayer(controller: controller),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue