Fix player position performance issue (#606)

This commit is contained in:
Piotr Rogowski 2023-08-01 15:44:00 +02:00 committed by GitHub
parent affdb57ecd
commit 3e0834f83c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 296 additions and 300 deletions

View File

@ -5,6 +5,7 @@
"danceability", "danceability",
"instrumentalness", "instrumentalness",
"Mpris", "Mpris",
"riverpod",
"speechiness", "speechiness",
"Spotube", "Spotube",
"winget" "winget"

View File

@ -48,7 +48,6 @@ class PlayerControls extends HookConsumerWidget {
final playing = final playing =
useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying; useStream(audioPlayer.playingStream).data ?? audioPlayer.isPlaying;
final buffering = useStream(audioPlayer.bufferingStream).data ?? true;
final theme = Theme.of(context); final theme = Theme.of(context);
final isDominantColorDark = ThemeData.estimateBrightnessForColor( final isDominantColorDark = ThemeData.estimateBrightnessForColor(
@ -89,215 +88,208 @@ class PlayerControls extends HookConsumerWidget {
iconSize: compact ? 18 : 24, iconSize: compact ? 18 : 24,
); );
return RepaintBoundary( return GestureDetector(
child: GestureDetector( behavior: HitTestBehavior.translucent,
behavior: HitTestBehavior.translucent, onTap: () {
onTap: () { if (focusNode.canRequestFocus) {
if (focusNode.canRequestFocus) { focusNode.requestFocus();
focusNode.requestFocus(); }
} },
}, child: FocusableActionDetector(
child: FocusableActionDetector( focusNode: focusNode,
focusNode: focusNode, shortcuts: shortcuts,
shortcuts: shortcuts, actions: actions,
actions: actions, child: Container(
child: Container( constraints: const BoxConstraints(maxWidth: 600),
constraints: const BoxConstraints(maxWidth: 600), child: Column(
child: Column( children: [
children: [ if (!compact)
if (!compact) HookBuilder(
HookBuilder( builder: (context) {
builder: (context) { final (
final ( :bufferProgress,
:bufferProgress, :duration,
:duration, :position,
:position, :progressStatic
:progressStatic ) = useProgress(ref);
) = useProgress(ref);
final totalMinutes = PrimitiveUtils.zeroPadNumStr( final totalMinutes = PrimitiveUtils.zeroPadNumStr(
duration.inMinutes.remainder(60), duration.inMinutes.remainder(60),
); );
final totalSeconds = PrimitiveUtils.zeroPadNumStr( final totalSeconds = PrimitiveUtils.zeroPadNumStr(
duration.inSeconds.remainder(60), duration.inSeconds.remainder(60),
); );
final currentMinutes = PrimitiveUtils.zeroPadNumStr( final currentMinutes = PrimitiveUtils.zeroPadNumStr(
position.inMinutes.remainder(60), position.inMinutes.remainder(60),
); );
final currentSeconds = PrimitiveUtils.zeroPadNumStr( final currentSeconds = PrimitiveUtils.zeroPadNumStr(
position.inSeconds.remainder(60), position.inSeconds.remainder(60),
); );
final progress = useState<num>( final progress = useState<num>(
useMemoized(() => progressStatic, []), useMemoized(() => progressStatic, []),
); );
useEffect(() { useEffect(() {
progress.value = progressStatic; progress.value = progressStatic;
return null; return null;
}, [progressStatic]); }, [progressStatic]);
return Column( return Column(
children: [ children: [
Tooltip( Tooltip(
message: context.l10n.slide_to_seek, message: context.l10n.slide_to_seek,
child: Slider( child: Slider(
// cannot divide by zero // cannot divide by zero
// there's an edge case for value being bigger // there's an edge case for value being bigger
// than total duration. Keeping it resolved // than total duration. Keeping it resolved
value: progress.value.toDouble(), value: progress.value.toDouble(),
secondaryTrackValue: bufferProgress, secondaryTrackValue: bufferProgress,
onChanged: onChanged: playlist.isFetching == true
playlist.isFetching == true || buffering ? null
? null : (v) {
: (v) { progress.value = v;
progress.value = v; },
}, onChangeEnd: (value) async {
onChangeEnd: (value) async { await audioPlayer.seek(
await audioPlayer.seek( Duration(
Duration( seconds: (value * duration.inSeconds).toInt(),
seconds: ),
(value * duration.inSeconds).toInt(), );
), },
); activeColor: sliderColor,
}, secondaryActiveColor: sliderColor.withOpacity(0.2),
activeColor: sliderColor, inactiveColor: sliderColor.withOpacity(0.15),
secondaryActiveColor: ),
sliderColor.withOpacity(0.2), ),
inactiveColor: sliderColor.withOpacity(0.15), Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
),
child: DefaultTextStyle(
style: theme.textTheme.bodySmall!.copyWith(
color: palette?.dominantColor?.bodyTextColor,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("$currentMinutes:$currentSeconds"),
Text("$totalMinutes:$totalSeconds"),
],
), ),
), ),
Padding( ),
padding: const EdgeInsets.symmetric( ],
horizontal: 8.0, );
), },
child: DefaultTextStyle(
style: theme.textTheme.bodySmall!.copyWith(
color: palette?.dominantColor?.bodyTextColor,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text("$currentMinutes:$currentSeconds"),
Text("$totalMinutes:$totalSeconds"),
],
),
),
),
],
);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
StreamBuilder<bool>(
stream: audioPlayer.shuffledStream,
builder: (context, snapshot) {
final shuffled = snapshot.data ?? false;
return IconButton(
tooltip: shuffled
? context.l10n.unshuffle_playlist
: context.l10n.shuffle_playlist,
icon: const Icon(SpotubeIcons.shuffle),
style: shuffled ? activeButtonStyle : buttonStyle,
onPressed: playlist.isFetching == true || buffering
? null
: () {
if (shuffled) {
audioPlayer.setShuffle(false);
} else {
audioPlayer.setShuffle(true);
}
},
);
}),
IconButton(
tooltip: context.l10n.previous_track,
icon: const Icon(SpotubeIcons.skipBack),
style: buttonStyle,
onPressed: playlist.isFetching == true || buffering
? null
: playlistNotifier.previous,
),
IconButton(
tooltip: playing
? context.l10n.pause_playback
: context.l10n.resume_playback,
icon: playlist.isFetching == true
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: accentColor?.titleTextColor ??
theme.colorScheme.onPrimary,
),
)
: Icon(
playing ? SpotubeIcons.pause : SpotubeIcons.play,
),
style: resumePauseStyle,
onPressed: playlist.isFetching == true
? null
: Actions.handler<PlayPauseIntent>(
context,
PlayPauseIntent(ref),
),
),
IconButton(
tooltip: context.l10n.next_track,
icon: const Icon(SpotubeIcons.skipForward),
style: buttonStyle,
onPressed: playlist.isFetching == true || buffering
? null
: playlistNotifier.next,
),
StreamBuilder<PlaybackLoopMode>(
stream: audioPlayer.loopModeStream,
builder: (context, snapshot) {
final loopMode =
snapshot.data ?? PlaybackLoopMode.none;
return IconButton(
tooltip: loopMode == PlaybackLoopMode.one
? context.l10n.loop_track
: loopMode == PlaybackLoopMode.all
? context.l10n.repeat_playlist
: null,
icon: Icon(
loopMode == PlaybackLoopMode.one
? SpotubeIcons.repeatOne
: SpotubeIcons.repeat,
),
style: loopMode == PlaybackLoopMode.one ||
loopMode == PlaybackLoopMode.all
? activeButtonStyle
: buttonStyle,
onPressed: playlist.isFetching == true || buffering
? null
: () async {
switch (await audioPlayer.loopMode) {
case PlaybackLoopMode.all:
audioPlayer
.setLoopMode(PlaybackLoopMode.one);
break;
case PlaybackLoopMode.one:
audioPlayer
.setLoopMode(PlaybackLoopMode.none);
break;
case PlaybackLoopMode.none:
audioPlayer
.setLoopMode(PlaybackLoopMode.all);
break;
}
},
);
}),
],
), ),
const SizedBox(height: 5) Row(
], mainAxisAlignment: MainAxisAlignment.spaceEvenly,
), children: [
StreamBuilder<bool>(
stream: audioPlayer.shuffledStream,
builder: (context, snapshot) {
final shuffled = snapshot.data ?? false;
return IconButton(
tooltip: shuffled
? context.l10n.unshuffle_playlist
: context.l10n.shuffle_playlist,
icon: const Icon(SpotubeIcons.shuffle),
style: shuffled ? activeButtonStyle : buttonStyle,
onPressed: playlist.isFetching == true
? null
: () {
if (shuffled) {
audioPlayer.setShuffle(false);
} else {
audioPlayer.setShuffle(true);
}
},
);
}),
IconButton(
tooltip: context.l10n.previous_track,
icon: const Icon(SpotubeIcons.skipBack),
style: buttonStyle,
onPressed: playlist.isFetching == true
? null
: playlistNotifier.previous,
),
IconButton(
tooltip: playing
? context.l10n.pause_playback
: context.l10n.resume_playback,
icon: playlist.isFetching == true
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
color: accentColor?.titleTextColor ??
theme.colorScheme.onPrimary,
),
)
: Icon(
playing ? SpotubeIcons.pause : SpotubeIcons.play,
),
style: resumePauseStyle,
onPressed: playlist.isFetching == true
? null
: Actions.handler<PlayPauseIntent>(
context,
PlayPauseIntent(ref),
),
),
IconButton(
tooltip: context.l10n.next_track,
icon: const Icon(SpotubeIcons.skipForward),
style: buttonStyle,
onPressed: playlist.isFetching == true
? null
: playlistNotifier.next,
),
StreamBuilder<PlaybackLoopMode>(
stream: audioPlayer.loopModeStream,
builder: (context, snapshot) {
final loopMode = snapshot.data ?? PlaybackLoopMode.none;
return IconButton(
tooltip: loopMode == PlaybackLoopMode.one
? context.l10n.loop_track
: loopMode == PlaybackLoopMode.all
? context.l10n.repeat_playlist
: null,
icon: Icon(
loopMode == PlaybackLoopMode.one
? SpotubeIcons.repeatOne
: SpotubeIcons.repeat,
),
style: loopMode == PlaybackLoopMode.one ||
loopMode == PlaybackLoopMode.all
? activeButtonStyle
: buttonStyle,
onPressed: playlist.isFetching == true
? null
: () async {
switch (await audioPlayer.loopMode) {
case PlaybackLoopMode.all:
audioPlayer
.setLoopMode(PlaybackLoopMode.one);
break;
case PlaybackLoopMode.one:
audioPlayer
.setLoopMode(PlaybackLoopMode.none);
break;
case PlaybackLoopMode.none:
audioPlayer
.setLoopMode(PlaybackLoopMode.all);
break;
}
},
);
}),
],
),
const SizedBox(height: 5)
],
), ),
), ),
), ),

View File

@ -62,99 +62,95 @@ class PlayerOverlay extends HookConsumerWidget {
child: AnimatedOpacity( child: AnimatedOpacity(
duration: const Duration(milliseconds: 250), duration: const Duration(milliseconds: 250),
opacity: canShow ? 1 : 0, opacity: canShow ? 1 : 0,
child: RepaintBoundary( child: Material(
child: Material( type: MaterialType.transparency,
type: MaterialType.transparency, child: Column(
child: Column( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ HookBuilder(
HookBuilder( builder: (context) {
builder: (context) { final progress = useProgress(ref);
final progress = useProgress(ref); // animated
// animated return TweenAnimationBuilder<double>(
return TweenAnimationBuilder<double>( duration: const Duration(milliseconds: 250),
duration: const Duration(milliseconds: 250), tween: Tween<double>(
tween: Tween<double>( begin: 0,
begin: 0, end: progress.progressStatic,
end: progress.progressStatic, ),
), builder: (context, value, child) {
builder: (context, value, child) { return LinearProgressIndicator(
return LinearProgressIndicator( value: value,
value: value, minHeight: 2,
minHeight: 2, backgroundColor: Colors.transparent,
backgroundColor: Colors.transparent, valueColor: AlwaysStoppedAnimation(
valueColor: AlwaysStoppedAnimation( theme.colorScheme.primary,
theme.colorScheme.primary, ),
), );
); },
}, );
); },
}, ),
), Expanded(
Expanded( child: Row(
child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ Expanded(
Expanded( child: MouseRegion(
child: MouseRegion( cursor: SystemMouseCursors.click,
cursor: SystemMouseCursors.click, child: GestureDetector(
child: GestureDetector( onTap: () =>
onTap: () => GoRouter.of(context).push("/player"),
GoRouter.of(context).push("/player"), child: PlayerTrackDetails(
child: PlayerTrackDetails( albumArt: albumArt,
albumArt: albumArt, color: textColor,
color: textColor,
),
), ),
), ),
), ),
Row( ),
children: [ Row(
IconButton( children: [
icon: Icon( IconButton(
SpotubeIcons.skipBack, icon: Icon(
color: textColor, SpotubeIcons.skipBack,
), color: textColor,
onPressed: playlistNotifier.previous,
), ),
Consumer( onPressed: playlistNotifier.previous,
builder: (context, ref, _) { ),
return IconButton( Consumer(
icon: playlist.isFetching builder: (context, ref, _) {
? const SizedBox( return IconButton(
height: 20, icon: playlist.isFetching
width: 20, ? const SizedBox(
child: height: 20,
CircularProgressIndicator(), width: 20,
) child: CircularProgressIndicator(),
: Icon( )
playing : Icon(
? SpotubeIcons.pause playing
: SpotubeIcons.play, ? SpotubeIcons.pause
color: textColor, : SpotubeIcons.play,
), color: textColor,
onPressed: ),
Actions.handler<PlayPauseIntent>( onPressed: Actions.handler<PlayPauseIntent>(
context, context,
PlayPauseIntent(ref), PlayPauseIntent(ref),
), ),
); );
}, },
),
IconButton(
icon: Icon(
SpotubeIcons.skipForward,
color: textColor,
), ),
IconButton( onPressed: playlistNotifier.next,
icon: Icon( ),
SpotubeIcons.skipForward, ],
color: textColor, ),
), ],
onPressed: playlistNotifier.next,
),
],
),
],
),
), ),
], ),
), ],
), ),
), ),
), ),

View File

@ -1,6 +1,5 @@
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
({ ({
@ -9,21 +8,29 @@ import 'package:spotube/services/audio_player/audio_player.dart';
Duration duration, Duration duration,
double bufferProgress double bufferProgress
}) useProgress(WidgetRef ref) { }) useProgress(WidgetRef ref) {
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final bufferProgress = final bufferProgress =
useStream(audioPlayer.bufferedPositionStream).data?.inSeconds ?? 0; useStream(audioPlayer.bufferedPositionStream).data?.inSeconds ?? 0;
Duration audioPlayerDuration = Duration.zero;
Duration audioPlayerPosition = Duration.zero;
// Duration future is needed for getting the duration of the song // Duration future is needed for getting the duration of the song
// as stream can be null when no event occurs (Mostly needed for android) // as stream can be null when no event occurs (Mostly needed for android)
final durationFuture = useFuture(audioPlayer.duration); audioPlayer.duration.then((value) {
final duration = useStream(audioPlayer.durationStream).data ?? if (value != null) {
durationFuture.data ?? audioPlayerDuration = value;
Duration.zero; }
});
final positionFuture = useFuture(audioPlayer.position); audioPlayer.position.then((value) {
final position = useState<Duration>(positionFuture.data ?? Duration.zero); if (value != null) {
audioPlayerPosition = value;
}
});
final position = useState<Duration>(audioPlayerPosition);
final duration =
useStream(audioPlayer.durationStream).data ?? audioPlayerDuration;
final sliderMax = duration.inSeconds; final sliderMax = duration.inSeconds;
final sliderValue = position.value.inSeconds; final sliderValue = position.value.inSeconds;
@ -38,7 +45,7 @@ import 'package:spotube/services/audio_player/audio_player.dart';
lastPosition = event; lastPosition = event;
position.value = event; position.value = event;
}).cancel; }).cancel;
}, []); }, [audioPlayerPosition, audioPlayerDuration]);
return ( return (
progressStatic: progressStatic: