This commit is contained in:
rang 2026-01-05 17:29:24 +08:00
parent 178f3fbdb1
commit 634ffaed8c
27 changed files with 648 additions and 1012 deletions

View file

@ -139,6 +139,7 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final book = ref.watch(currentBookProvider);
final player = ref.watch(absPlayerProvider);
final libraryItem = ref.watch(libraryItemProvider(id)).valueOrNull;
if (libraryItem == null) {
@ -146,13 +147,13 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
}
final mediaProgress = libraryItem.userMediaProgress;
if (mediaProgress == null && player.book?.libraryItemId != libraryItem.id) {
if (mediaProgress == null && book?.libraryItemId != libraryItem.id) {
return const SizedBox.shrink();
}
double progress;
Duration remainingTime;
if (player.book?.libraryItemId == libraryItem.id) {
if (book?.libraryItemId == libraryItem.id) {
// final positionStream = useStream(player.slowPositionStream);
progress = (player.positionInBook).inSeconds /
libraryItem.media.asBookExpanded.duration.inSeconds;

View file

@ -4,7 +4,6 @@ import 'package:logging/logging.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/features/player/core/abs_audio_player.dart';
import 'package:vaani/shared/extensions/obfuscation.dart';
import 'package:vaani/shared/utils/error_response.dart';
final _logger = Logger('PlaybackReporter');

View file

@ -1,3 +0,0 @@
import 'package:just_audio/just_audio.dart';
class AudiobookPlayer extends AudioPlayer {}

View file

@ -1,143 +0,0 @@
import 'package:audio_service/audio_service.dart';
import 'package:vaani/features/player/core/abs_audio_player.dart';
// audio_service
class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
final AbsAudioPlayer _player;
AbsAudioHandler(AbsAudioPlayer player) : _player = player {
player.mediaItemStream.listen((item) {
mediaItem.add(item);
});
// _player.playbackEventStream.map(_transformEvent).pipe(playbackState);
playbackState.add(
playbackState.value.copyWith(
controls: [
MediaControl.skipToPrevious,
// if (player.state.playing) MediaControl.pause else MediaControl.play,
// MediaControl.rewind,
// MediaControl.fastForward,
MediaControl.skipToNext,
MediaControl.stop,
],
systemActions: {
MediaAction.play,
MediaAction.pause,
MediaAction.seek,
MediaAction.seekForward,
MediaAction.seekBackward,
},
),
);
// 1. /
player.playerStateStream.listen((playerState) {
playbackState.add(
playbackState.value.copyWith(
playing: playerState.playing,
// playing processingState
processingState: const {
AbsProcessingState.idle: AudioProcessingState.idle,
AbsProcessingState.loading: AudioProcessingState.loading,
AbsProcessingState.buffering: AudioProcessingState.buffering,
AbsProcessingState.ready: AudioProcessingState.ready,
AbsProcessingState.completed: AudioProcessingState.completed,
}[playerState.processingState] ??
AudioProcessingState.idle,
),
);
});
// 2.
player.positionStream.listen((Duration position) {
playbackState.add(
playbackState.value.copyWith(
updatePosition: position,
),
);
});
// 3.
// player.stream.duration.listen((Duration? duration) {
// // mediaItem duration
// final currentItem = mediaItem.value;
// if (currentItem != null && duration != null) {
// mediaItem.add(currentItem.copyWith(duration: duration));
// }
// });
// player.stream.completed.listen((bool playing) {
// print('播放完成');
// });
}
//
@override
Future<void> play() async {
await _player.play();
}
@override
Future<void> pause() async {
await _player.pause();
}
@override
Future<void> skipToNext() async {
await _player.next();
}
@override
Future<void> skipToPrevious() async {
await _player.previous();
}
@override
Future<void> seek(Duration position) async {
await _player.seek(position);
}
@override
Future<void> setSpeed(double speed) async {
await _player.setSpeed(speed);
}
Future<void> setVolume(double volume) async {
await _player.setVolume(volume);
}
// PlaybackState _transformEvent(PlaybackEvent event) {
// return PlaybackState(
// controls: [
// if (kIsWeb || !Platform.isAndroid) MediaControl.skipToPrevious,
// MediaControl.rewind,
// if (_player.playing) MediaControl.pause else MediaControl.play,
// MediaControl.stop,
// MediaControl.fastForward,
// if (kIsWeb || !Platform.isAndroid) MediaControl.skipToNext
// ],
// systemActions: {
// if (kIsWeb || !Platform.isAndroid) MediaAction.skipToPrevious,
// MediaAction.rewind,
// if (!(_settingsProvider?['lockSeekingNotification'] ?? false))
// MediaAction.seek,
// MediaAction.fastForward,
// MediaAction.stop,
// MediaAction.setSpeed,
// if (kIsWeb || !Platform.isAndroid) MediaAction.skipToNext
// },
// androidCompactActionIndices: const [1, 2, 3],
// processingState: const {
// ProcessingState.idle: AudioProcessingState.idle,
// ProcessingState.loading: AudioProcessingState.loading,
// ProcessingState.buffering: AudioProcessingState.buffering,
// ProcessingState.ready: AudioProcessingState.ready,
// ProcessingState.completed: AudioProcessingState.completed,
// }[_player.processingState]!,
// playing: _player.playing,
// updatePosition: position,
// bufferedPosition: _player.bufferedPosition,
// speed: _player.speed,
// queueIndex: event.currentIndex,
// captioningEnabled: false,
// );
// }
}

View file

@ -1,8 +1,9 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:collection/collection.dart';
import 'package:just_audio/just_audio.dart';
import 'package:just_audio_background/just_audio_background.dart';
import 'package:logging/logging.dart';
import 'package:rxdart/rxdart.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
@ -16,20 +17,32 @@ final offset = Duration(milliseconds: 10);
final _logger = Logger('AbsAudioPlayer');
///
abstract class AbsAudioPlayer {
final _mediaItemController = BehaviorSubject<MediaItem?>.seeded(null);
final playerStateSubject =
BehaviorSubject.seeded(AbsPlayerState(false, AbsProcessingState.idle));
class AbsAudioPlayer {
late final AudioPlayer _player;
AbsAudioPlayer(AudioPlayer player) : _player = player {
_player.positionStream.listen((position) {
final chapter = currentChapter;
if (positionInBook <= (chapter?.start ?? Duration.zero) ||
positionInBook >= (chapter?.end ?? Duration.zero)) {
final chapter = book?.findChapterAtTime(positionInBook);
if (chapter != currentChapter) {
// print('当前章节时长: ${currentChapter?.duration}');
// print('切换章节时长: ${chapter?.duration}');
// print('当前播放音轨时长: ${_player.duration}');
chapterStreamController.add(chapter);
}
}
});
}
final _bookStreamController = BehaviorSubject<BookExpanded?>.seeded(null);
final chapterStreamController = BehaviorSubject<BookChapter?>.seeded(null);
BookExpanded? get book => _bookStreamController.nvalue;
AudioTrack? get currentTrack => book?.tracks[currentIndex];
BookChapter? get currentChapter => chapterStreamController.nvalue;
AbsPlayerState get playerState => playerStateSubject.value;
Stream<MediaItem?> get mediaItemStream => _mediaItemController.stream;
Stream<AbsPlayerState> get playerStateStream => playerStateSubject.stream;
PlayerState get playerState => _player.playerState;
Stream<PlayerState> get playerStateStream => _player.playerStateStream;
//
Future<void> load(
@ -59,43 +72,77 @@ abstract class AbsAudioPlayer {
.formatNotificationTitle(book);
chapterStreamController
.add(book.findChapterAtTime(initialPosition ?? Duration.zero));
final item = MediaItem(
id: book.libraryItemId,
title: title,
artist: artist,
duration: currentChapter?.duration ?? book.duration,
artUri: Uri.parse(
'$baseUrl/api/items/${book.libraryItemId}/cover?token=$token',
),
);
_mediaItemController.sink.add(item);
final playlist = book.tracks
.map(
(track) => (
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
track.duration
// final item = MediaItem(
// id: book.libraryItemId,
// title: title,
// artist: artist,
// duration: currentChapter?.duration ?? book.duration,
// artUri: Uri.parse(
// '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token',
// ),
// );
mediaItem(track) => MediaItem(
id: book.libraryItemId + track.index.toString(),
title: title,
artist: artist,
duration: currentChapter?.duration ?? book.duration,
artUri: Uri.parse(
'$baseUrl/api/items/${book.libraryItemId}/cover?token=$token',
),
)
.toList();
await setPlayList(
playlist,
index: indexTrack,
position: positionInTrack,
start: start,
end: end,
);
);
List<AudioSource> audioSources = start != null && start > Duration.zero ||
end != null && end > Duration.zero
? book.tracks
.map(
(track) => ClippingAudioSource(
child: AudioSource.uri(
_getUri(
track,
downloadedUris,
baseUrl: baseUrl,
token: token,
),
),
start: start,
end: end == null ? null : track.duration - end,
tag: mediaItem(track),
),
)
.toList()
: book.tracks
.map(
(track) => AudioSource.uri(
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
tag: mediaItem(track),
),
)
.toList();
await _player
.setAudioSources(
audioSources,
preload: true,
initialIndex: indexTrack,
initialPosition: positionInTrack,
)
.catchError((error) {
_logger.shout('Error in setting audio source: $error');
return null;
});
}
Future<void> setPlayList(
List<(Uri, Duration)> playlist, {
int? index,
Duration? position,
Duration? start,
Duration? end,
});
Future<void> play();
Future<void> pause();
Future<void> playOrPause();
Future<void> play() async {
await _player.play();
}
Future<void> pause() async {
await _player.pause();
}
Future<void> playOrPause() async {
_player.playing ? await _player.pause() : await _player.play();
}
//
Future<void> next() async {
@ -126,7 +173,19 @@ abstract class AbsAudioPlayer {
}
}
Future<void> seek(Duration position, {int? index});
Future<void> seek(Duration position, {int? index}) async {
await _player.seek(_addClippingStart(_player.position, add: false),
index: index);
}
Future<void> setSpeed(double speed) async {
await _player.setSpeed(speed);
}
Future<void> setVolume(double volume) async {
await _player.setVolume(volume);
}
Future<void> seekInBook(Duration position) async {
if (book == null) return;
//
@ -140,8 +199,6 @@ abstract class AbsAudioPlayer {
await seek(positionInTrack, index: index);
}
Future<void> setSpeed(double speed);
Future<void> setVolume(double volume);
Future<void> switchChapter(int chapterId) async {
if (book == null) return;
@ -153,15 +210,18 @@ abstract class AbsAudioPlayer {
}
bool get playing => playerState.playing;
Stream<bool> get playingStream;
Stream<bool> get playingStream => _player.playingStream;
Stream<BookExpanded?> get bookStream => _bookStreamController.stream;
Stream<BookChapter?> get chapterStream => chapterStreamController.stream;
int get currentIndex;
double get speed;
int get currentIndex => _player.currentIndex ?? 0;
double get speed => _player.speed;
Duration get position;
Stream<Duration> get positionStream;
Duration get position => _addClippingStart(_player.position);
Stream<Duration> get positionStream =>
_player.positionStream.where((_) => _player.playing).map((position) {
return _addClippingStart(position);
});
Duration get positionInChapter => getPositionInChapter(position);
Duration getPositionInChapter(position) {
@ -183,8 +243,8 @@ abstract class AbsAudioPlayer {
return positionInBook;
});
Duration get bufferedPosition;
Stream<Duration> get bufferedPositionStream;
Duration get bufferedPosition => _player.bufferedPosition;
Stream<Duration> get bufferedPositionStream => _player.bufferedPositionStream;
Duration get bufferedPositionInBook =>
bufferedPosition +
(book?.tracks[currentIndex].startOffset ?? Duration.zero);
@ -193,70 +253,26 @@ abstract class AbsAudioPlayer {
return bufferedPositionInBook;
});
Duration _addClippingStart(Duration position, {bool add = true}) {
if (_player.sequenceState.currentSource != null &&
_player.sequenceState.currentSource is ClippingAudioSource) {
final currentSource =
_player.sequenceState.currentSource as ClippingAudioSource;
if (currentSource.start != null) {
return add
? position + currentSource.start!
: position - currentSource.start!;
}
}
return position;
}
dispose() {
_mediaItemController.close();
playerStateSubject.close();
_bookStreamController.close();
chapterStreamController.close();
}
}
/// Enumerates the different processing states of a player.
enum AbsProcessingState {
/// The player has not loaded an [AudioSource].
idle,
/// The player is loading an [AudioSource].
loading,
/// The player is buffering audio and unable to play.
buffering,
/// The player is has enough audio buffered and is able to play.
ready,
/// The player has reached the end of the audio.
completed,
}
/// Encapsulates the playing and processing states. These two states vary
/// orthogonally, and so if [processingState] is [ProcessingState.buffering],
/// you can check [playing] to determine whether the buffering occurred while
/// the player was playing or while the player was paused.
class AbsPlayerState {
/// Whether the player will play when [processingState] is
/// [ProcessingState.ready].
final bool playing;
/// The current processing state of the player.
final AbsProcessingState processingState;
AbsPlayerState(this.playing, this.processingState);
@override
String toString() => 'playing=$playing,processingState=$processingState';
@override
int get hashCode => Object.hash(playing, processingState);
@override
bool operator ==(Object other) =>
other.runtimeType == runtimeType &&
other is PlayerState &&
other.playing == playing &&
other.processingState == processingState;
AbsPlayerState copyWith({
bool? playing,
AbsProcessingState? processingState,
}) {
return AbsPlayerState(
playing ?? this.playing,
processingState ?? this.processingState,
);
}
}
Uri _getUri(
AudioTrack track,
List<Uri>? downloadedUris, {
@ -280,6 +296,38 @@ extension _ValueStreamExtension<T> on ValueStream<T> {
T? get nvalue => hasValue ? value : null;
}
extension BookExpandedExtension on BookExpanded {
BookChapter findChapterAtTime(Duration position) {
return chapters.firstWhere(
(element) {
return element.start <= position && element.end >= position + offset;
},
orElse: () => chapters.first,
);
}
AudioTrack findTrackAtTime(Duration position) {
return tracks.firstWhere(
(element) {
return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset;
},
orElse: () => tracks.first,
);
}
int findTrackIndexAtTime(Duration position) {
return tracks.indexWhere((element) {
return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset;
});
}
Duration getTrackStartOffset(int index) {
return tracks[index].startOffset;
}
}
extension FormatNotificationTitle on String {
String formatNotificationTitle(BookExpanded book) {
return replaceAllMapped(
@ -318,49 +366,3 @@ extension NotificationTitleUtils on NotificationTitleType {
}
}
}
extension BookExpandedExtension on BookExpanded {
BookChapter findChapterAtTime(Duration position) {
return chapters.firstWhere(
(element) {
return element.start <= position && element.end >= position + offset;
},
orElse: () => chapters.first,
);
}
AudioTrack findTrackAtTime(Duration position) {
return tracks.firstWhere(
(element) {
return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset;
},
orElse: () => tracks.first,
);
}
int findTrackIndexAtTime(Duration position) {
return tracks.indexWhere((element) {
return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset;
});
}
Duration getTrackStartOffset(int index) {
return tracks[index].startOffset;
}
}
class AudioMetadata {
final String album;
final String title;
final String artist;
final String artwork;
AudioMetadata({
required this.album,
required this.title,
required this.artist,
required this.artwork,
});
}

View file

@ -1,108 +0,0 @@
import 'dart:async';
import 'package:media_kit/media_kit.dart' hide PlayerState;
import 'package:vaani/features/player/core/abs_audio_player.dart';
/// mpv全平台 (media_kit)
class AbsMpvAudioPlayer extends AbsAudioPlayer {
late Player player;
AbsMpvAudioPlayer() {
MediaKit.ensureInitialized();
player = Player();
player.stream.playing.listen((playing) {
final state = playerState;
playerStateSubject.add(
state.copyWith(
playing: playing,
processingState: playing
? state.processingState == AbsProcessingState.idle
? AbsProcessingState.ready
: state.processingState
: player.state.buffering
? AbsProcessingState.buffering
: player.state.completed
? AbsProcessingState.completed
: AbsProcessingState.ready,
),
);
});
}
@override
Duration get bufferedPosition => player.state.buffer;
@override
Stream<Duration> get bufferedPositionStream => player.stream.buffer;
@override
int get currentIndex => player.state.playlist.index;
@override
Future<void> pause() async {
await player.pause();
}
@override
Future<void> play() async {
await player.play();
}
@override
Future<void> playOrPause() async {
await player.playOrPause();
}
@override
Stream<bool> get playingStream => player.stream.playing;
@override
Duration get position => player.state.position;
@override
Stream<Duration> get positionStream => player.stream.position;
@override
Future<void> seek(Duration position, {int? index}) async {
if (index != null) {
final playing = this.playing;
await player.jump(index);
if (!playing) await player.pause();
}
await player.seek(position);
}
@override
Future<void> setPlayList(
List<(Uri, Duration)> playlist, {
int? index,
Duration? position,
Duration? start,
Duration? end,
}) async {
await player.open(
Playlist(
playlist.map((uri) => Media(uri.$1.toString())).toList(),
index: index ?? 0,
),
play: false,
);
// open方法加载完成
// ignore: unnecessary_null_comparison
await player.stream.duration.firstWhere((d) => d != null);
if (position != null) {
await player.seek(position);
}
}
@override
Future<void> setSpeed(double speed) async {
await player.setRate(speed);
}
@override
Future<void> setVolume(double volume) async {
await player.setVolume(volume * 100);
}
@override
double get speed => player.state.rate;
}

View file

@ -1,159 +0,0 @@
import 'package:just_audio/just_audio.dart';
import 'package:just_audio_media_kit/just_audio_media_kit.dart';
import 'package:logging/logging.dart';
import 'package:vaani/features/player/core/abs_audio_player.dart';
final _logger = Logger('AbsPlatformAudioPlayer');
/// ios,macos,android (just_audio)
class AbsPlatformAudioPlayer extends AbsAudioPlayer {
late final AudioPlayer _player;
AbsPlatformAudioPlayer() {
//
// prefetch-playlist=yes
JustAudioMediaKit.prefetchPlaylist = true;
// merge-files=yes
// cache=yes
// cache-pause-wait=60
JustAudioMediaKit.ensureInitialized();
_player = AudioPlayer();
_player.playerStateStream.listen((state) {
playerStateSubject.add(
playerState.copyWith(
playing: state.playing,
processingState: {
ProcessingState.idle: AbsProcessingState.idle,
ProcessingState.buffering: AbsProcessingState.buffering,
ProcessingState.completed: AbsProcessingState.completed,
ProcessingState.loading: AbsProcessingState.loading,
ProcessingState.ready: AbsProcessingState.ready,
}[state.processingState],
),
);
});
positionStream.listen((position) {
final chapter = currentChapter;
if (positionInBook <= (chapter?.start ?? Duration.zero) ||
positionInBook >= (chapter?.end ?? Duration.zero)) {
final chapter = book?.findChapterAtTime(positionInBook);
if (chapter != currentChapter) {
// print('当前章节时长: ${currentChapter?.duration}');
// print('切换章节时长: ${chapter?.duration}');
// print('当前播放音轨时长: ${_player.duration}');
chapterStreamController.add(chapter);
}
}
});
}
@override
Duration get bufferedPosition => _player.bufferedPosition;
@override
Stream<Duration> get bufferedPositionStream => _player.bufferedPositionStream
.where(
(_) => _player.playerState.processingState == ProcessingState.buffering,
)
.asBroadcastStream();
@override
int get currentIndex => _player.currentIndex ?? 0;
@override
Future<void> pause() async {
await _player.pause();
}
@override
Future<void> play() async {
await _player.play();
}
@override
Future<void> playOrPause() async {
_player.playing ? await _player.pause() : await _player.play();
}
@override
Stream<bool> get playingStream => _player.playingStream;
@override
Duration get position => _addClippingStart(_player.position);
Duration _addClippingStart(Duration position, {bool add = true}) {
if (_player.sequenceState.currentSource != null &&
_player.sequenceState.currentSource is ClippingAudioSource) {
final currentSource =
_player.sequenceState.currentSource as ClippingAudioSource;
if (currentSource.start != null) {
return add
? position + currentSource.start!
: position - currentSource.start!;
}
}
return position;
}
@override
Stream<Duration> get positionStream =>
_player.positionStream.where((_) => _player.playing).map((position) {
return _addClippingStart(position);
});
@override
Future<void> seek(Duration position, {int? index}) async {
await _player.seek(_addClippingStart(position, add: false), index: index);
}
@override
Future<void> setPlayList(
List<(Uri, Duration)> playlist, {
int? index,
Duration? position,
Duration? start,
Duration? end,
}) async {
List<AudioSource> audioSources = start != null && start > Duration.zero ||
end != null && end > Duration.zero
? playlist
.map(
(item) => ClippingAudioSource(
child: AudioSource.uri(item.$1),
start: start,
end: end == null ? null : item.$2 - end,
),
)
.toList()
: playlist.map((item) => AudioSource.uri(item.$1)).toList();
await _player
.setAudioSources(
audioSources,
preload: true,
initialIndex: index,
initialPosition: position,
)
.catchError((error) {
_logger.shout('Error in setting audio source: $error');
return null;
});
}
@override
Future<void> setSpeed(double speed) async {
await _player.setSpeed(speed);
}
@override
Future<void> setVolume(double volume) async {
await _player.setVolume(volume);
}
@override
double get speed => _player.speed;
@override
void dispose() {
super.dispose();
_player.dispose();
}
}

View file

@ -1,62 +1,69 @@
// import 'package:audio_service/audio_service.dart';
// import 'package:audio_session/audio_session.dart';
// import 'package:just_audio_background/just_audio_background.dart'
// show JustAudioBackground, NotificationConfig;
// import 'package:just_audio_media_kit/just_audio_media_kit.dart'
// show JustAudioMediaKit;
// import 'package:vaani/features/settings/app_settings_provider.dart';
// import 'package:vaani/features/settings/models/app_settings.dart';
import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
import 'package:just_audio_background/just_audio_background.dart'
show JustAudioBackground, NotificationConfig;
import 'package:just_audio_media_kit/just_audio_media_kit.dart'
show JustAudioMediaKit;
import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/features/settings/models/app_settings.dart';
// Future<void> configurePlayer() async {
// // for playing audio on windows, linux
// JustAudioMediaKit.ensureInitialized();
Future<void> configurePlayer() async {
// for playing audio on windows, linux
JustAudioMediaKit.ensureInitialized();
// // for configuring how this app will interact with other audio apps
// final session = await AudioSession.instance;
// await session.configure(const AudioSessionConfiguration.speech());
//
// prefetch-playlist=yes
// JustAudioMediaKit.prefetchPlaylist = true;
// merge-files=yes
// cache=yes
// cache-pause-wait=60
// final appSettings = loadOrCreateAppSettings();
// for configuring how this app will interact with other audio apps
final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration.speech());
// // for playing audio in the background
// await JustAudioBackground.init(
// androidNotificationChannelId: 'dr.blank.vaani.channel.audio',
// androidNotificationChannelName: 'Audio playback',
// androidNotificationOngoing: false,
// androidStopForegroundOnPause: false,
// androidNotificationChannelDescription: 'Audio playback in the background',
// androidNotificationIcon: 'drawable/ic_stat_logo',
// rewindInterval: appSettings.notificationSettings.rewindInterval,
// fastForwardInterval: appSettings.notificationSettings.fastForwardInterval,
// androidShowNotificationBadge: false,
// notificationConfigBuilder: (state) {
// final controls = [
// if (appSettings.notificationSettings.mediaControls
// .contains(NotificationMediaControl.skipToPreviousChapter) &&
// state.hasPrevious)
// MediaControl.skipToPrevious,
// if (appSettings.notificationSettings.mediaControls
// .contains(NotificationMediaControl.rewind))
// MediaControl.rewind,
// if (state.playing) MediaControl.pause else MediaControl.play,
// if (appSettings.notificationSettings.mediaControls
// .contains(NotificationMediaControl.fastForward))
// MediaControl.fastForward,
// if (appSettings.notificationSettings.mediaControls
// .contains(NotificationMediaControl.skipToNextChapter) &&
// state.hasNext)
// MediaControl.skipToNext,
// if (appSettings.notificationSettings.mediaControls
// .contains(NotificationMediaControl.stop))
// MediaControl.stop,
// ];
// return NotificationConfig(
// controls: controls,
// systemActions: const {
// MediaAction.seek,
// MediaAction.seekForward,
// MediaAction.seekBackward,
// },
// );
// },
// );
// }
final appSettings = loadOrCreateAppSettings();
// for playing audio in the background
await JustAudioBackground.init(
androidNotificationChannelId: 'dr.blank.vaani.channel.audio',
androidNotificationChannelName: 'Audio playback',
androidNotificationOngoing: false,
androidStopForegroundOnPause: false,
androidNotificationChannelDescription: 'Audio playback in the background',
androidNotificationIcon: 'drawable/ic_stat_logo',
rewindInterval: appSettings.notificationSettings.rewindInterval,
fastForwardInterval: appSettings.notificationSettings.fastForwardInterval,
androidShowNotificationBadge: false,
notificationConfigBuilder: (state) {
final controls = [
if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.skipToPreviousChapter) &&
state.hasPrevious)
MediaControl.skipToPrevious,
if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.rewind))
MediaControl.rewind,
if (state.playing) MediaControl.pause else MediaControl.play,
if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.fastForward))
MediaControl.fastForward,
if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.skipToNextChapter) &&
state.hasNext)
MediaControl.skipToNext,
if (appSettings.notificationSettings.mediaControls
.contains(NotificationMediaControl.stop))
MediaControl.stop,
];
return NotificationConfig(
controls: controls,
systemActions: const {
MediaAction.seek,
MediaAction.seekForward,
MediaAction.seekBackward,
},
);
},
);
}

View file

@ -1,9 +1,6 @@
import 'package:audio_service/audio_service.dart';
import 'package:audio_session/audio_session.dart';
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:logging/logging.dart';
import 'package:just_audio/just_audio.dart' as audio;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' as api;
import 'package:vaani/api/api_provider.dart';
@ -12,55 +9,52 @@ import 'package:vaani/db/available_boxes.dart';
import 'package:vaani/db/cache/cache_key.dart';
import 'package:vaani/features/downloads/providers/download_manager.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
import 'package:vaani/features/player/core/abs_audio_handler.dart';
import 'package:vaani/features/player/core/abs_audio_player.dart' as core;
import 'package:vaani/features/player/core/abs_audio_player_platform.dart';
import 'package:vaani/features/player/core/abs_audio_player.dart'
show AbsAudioPlayer;
import 'package:vaani/features/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/box.dart';
import 'package:vaani/shared/extensions/model_conversions.dart';
part 'abs_provider.g.dart';
final _logger = Logger('AbsPlayerProvider');
///
@Riverpod(keepAlive: true)
Future<AudioHandler> configurePlayer(Ref ref) async {
final player = ref.read(absPlayerProvider);
// for playing audio on windows, linux
// @Riverpod(keepAlive: true)
// Future<AudioHandler> configurePlayer(Ref ref) async {
// final player = ref.read(absPlayerProvider);
// // for playing audio on windows, linux
// for configuring how this app will interact with other audio apps
final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration.speech());
// // for configuring how this app will interact with other audio apps
// final session = await AudioSession.instance;
// await session.configure(const AudioSessionConfiguration.speech());
final audioService = await AudioService.init(
builder: () => AbsAudioHandler(player),
config: const AudioServiceConfig(
androidNotificationChannelId: 'dr.blank.vaani.channel.audio',
androidNotificationChannelName: 'ABSPlayback',
androidNotificationChannelDescription:
'Needed to control audio from lock screen',
androidNotificationOngoing: false,
androidStopForegroundOnPause: false,
androidNotificationIcon: 'drawable/ic_stat_logo',
preloadArtwork: true,
// fastForwardInterval: Duration(seconds: 20),
// rewindInterval: Duration(seconds: 20),
),
);
// final audioService = await AudioService.init(
// builder: () => AbsAudioHandler(player),
// config: const AudioServiceConfig(
// androidNotificationChannelId: 'dr.blank.vaani.channel.audio',
// androidNotificationChannelName: 'ABSPlayback',
// androidNotificationChannelDescription:
// 'Needed to control audio from lock screen',
// androidNotificationOngoing: false,
// androidStopForegroundOnPause: false,
// androidNotificationIcon: 'drawable/ic_stat_logo',
// preloadArtwork: true,
// // fastForwardInterval: Duration(seconds: 20),
// // rewindInterval: Duration(seconds: 20),
// ),
// );
_logger.finer('created simple player');
return audioService;
}
// _logger.finer('created simple player');
// return audioService;
// }
// just_audio
@Riverpod(keepAlive: true)
core.AbsAudioPlayer audioPlayer(Ref ref) {
final player = AbsPlatformAudioPlayer();
// final player = AbsMpvAudioPlayer();
ref.onDispose(player.dispose);
return player;
}
// @Riverpod(keepAlive: true)
// core.AbsAudioPlayer audioPlayer(Ref ref) {
// final player = AbsPlatformAudioPlayer();
// // final player = AbsMpvAudioPlayer();
// ref.onDispose(player.dispose);
// return player;
// }
//
@riverpod
@ -69,129 +63,20 @@ bool playerActive(Ref ref) {
}
@Riverpod(keepAlive: true)
AudioPlayer simpleAudioPlayer(Ref ref) {
final player = AudioPlayer();
audio.AudioPlayer simpleAudioPlayer(Ref ref) {
final player = audio.AudioPlayer();
ref.onDispose(player.dispose);
return player;
}
@Riverpod(keepAlive: true)
class AbsAudioPlayer extends _$AbsAudioPlayer {
@override
AudioPlayer build() {
final audioPlayer = ref.watch(simpleAudioPlayerProvider);
return audioPlayer;
}
final offset = Duration(milliseconds: 10);
Future<void> load(
api.BookExpanded book, {
Duration? initialPosition,
bool play = true,
}) async {
final currentTrack = book.findTrackAtTime(initialPosition ?? Duration.zero);
final indexTrack = book.tracks.indexOf(currentTrack);
final positionInTrack = initialPosition != null
? initialPosition - currentTrack.startOffset
: null;
final api = ref.read(authenticatedApiProvider);
final downloadManager = ref.read(simpleDownloadManagerProvider);
print(downloadManager.basePath);
final libItem =
await ref.read(libraryItemProvider(book.libraryItemId).future);
final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
final bookSettings = ref.read(bookSettingsProvider(book.libraryItemId));
var bookPlayerSettings = bookSettings.playerSettings;
final start = bookSettings.playerSettings.skipChapterStart;
final end = bookSettings.playerSettings.skipChapterEnd;
final appPlayerSettings = ref.read(appSettingsProvider).playerSettings;
final configurePlayerForEveryBook =
appPlayerSettings.configurePlayerForEveryBook;
List<AudioSource> audioSources =
start > Duration.zero || end > Duration.zero
? book.tracks
.map(
(track) => ClippingAudioSource(
child: AudioSource.uri(
_getUri(
track,
downloadedUris,
baseUrl: api.baseUrl,
token: api.token!,
),
),
start: start,
end: end > Duration.zero ? null : track.duration - end,
),
)
.toList()
: book.tracks
.map(
(track) => AudioSource.uri(
_getUri(
track,
downloadedUris,
baseUrl: api.baseUrl,
token: api.token!,
),
),
)
.toList();
await state
.setAudioSources(
audioSources,
preload: true,
initialIndex: indexTrack,
initialPosition: positionInTrack,
)
.catchError((error) {
_logger.shout('Error in setting audio source: $error');
return null;
});
// set the volume
await state.setVolume(
configurePlayerForEveryBook
? bookPlayerSettings.preferredDefaultVolume ??
appPlayerSettings.preferredDefaultVolume
: appPlayerSettings.preferredDefaultVolume,
);
// set the speed
await state.setSpeed(
configurePlayerForEveryBook
? bookPlayerSettings.preferredDefaultSpeed ??
appPlayerSettings.preferredDefaultSpeed
: appPlayerSettings.preferredDefaultSpeed,
);
if (play) await state.play();
}
Uri _getUri(
api.AudioTrack track,
List<Uri>? downloadedUris, {
required Uri baseUrl,
required String token,
}) {
// check if the track is in the downloadedUris
final uri = downloadedUris?.firstWhereOrNull(
(element) {
return element.pathSegments.last == track.metadata?.filename;
},
);
return uri ??
Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token');
}
}
/// riverpod状态
@Riverpod(keepAlive: true)
class AbsPlayer extends _$AbsPlayer {
@override
core.AbsAudioPlayer build() {
final audioPlayer = ref.watch(audioPlayerProvider);
return audioPlayer;
AbsAudioPlayer build() {
final audioPlayer = ref.watch(simpleAudioPlayerProvider);
return AbsAudioPlayer(audioPlayer);
}
Future<void> load(
@ -246,10 +131,71 @@ class AbsPlayer extends _$AbsPlayer {
}
}
/// riverpod状态
// @Riverpod(keepAlive: true)
// class AbsPlayer extends _$AbsPlayer {
// @override
// core.AbsAudioPlayer build() {
// final audioPlayer = ref.watch(audioPlayerProvider);
// return audioPlayer;
// }
// Future<void> load(
// api.BookExpanded book, {
// Duration? initialPosition,
// bool play = true,
// }) async {
// if (state.book == book || state.book?.libraryItemId == book.libraryItemId) {
// state.playOrPause();
// return;
// }
// final api = ref.read(authenticatedApiProvider);
// final downloadManager = ref.read(simpleDownloadManagerProvider);
// print(downloadManager.basePath);
// final libItem =
// await ref.read(libraryItemProvider(book.libraryItemId).future);
// final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
// final bookSettings = ref.read(bookSettingsProvider(book.libraryItemId));
// var bookPlayerSettings = bookSettings.playerSettings;
// var appPlayerSettings = ref.read(appSettingsProvider).playerSettings;
// var configurePlayerForEveryBook =
// appPlayerSettings.configurePlayerForEveryBook;
// await state.load(
// book,
// baseUrl: api.baseUrl,
// token: api.token!,
// initialPosition: initialPosition,
// downloadedUris: downloadedUris,
// start: bookSettings.playerSettings.skipChapterStart,
// end: bookSettings.playerSettings.skipChapterEnd,
// );
// // set the volume
// await state.setVolume(
// configurePlayerForEveryBook
// ? bookPlayerSettings.preferredDefaultVolume ??
// appPlayerSettings.preferredDefaultVolume
// : appPlayerSettings.preferredDefaultVolume,
// );
// // set the speed
// await state.setSpeed(
// configurePlayerForEveryBook
// ? bookPlayerSettings.preferredDefaultSpeed ??
// appPlayerSettings.preferredDefaultSpeed
// : appPlayerSettings.preferredDefaultSpeed,
// );
// if (play) await state.play();
// }
// }
@riverpod
class PlayerState extends _$PlayerState {
@override
core.AbsPlayerState build() {
audio.PlayerState build() {
final player = ref.read(absPlayerProvider);
player.playerStateStream.listen((playerState) {
if (playerState != state) {
@ -260,10 +206,10 @@ class PlayerState extends _$PlayerState {
}
bool isLoading(String itemId) {
final player = ref.read(absPlayerProvider);
return player.book?.libraryItemId == itemId &&
final book = ref.read(currentBookProvider);
return book?.libraryItemId == itemId &&
!state.playing &&
state.processingState == core.AbsProcessingState.loading;
state.processingState == audio.ProcessingState.loading;
}
bool isPlaying() {
@ -307,7 +253,7 @@ class CurrentBook extends _$CurrentBook {
Future<void> update(String libraryItemId, {bool play = true}) async {
if (state?.libraryItemId == libraryItemId) {
ref.read(audioPlayerProvider).playOrPause();
ref.read(absPlayerProvider).playOrPause();
return;
}
final book = await ref.read(libraryItemProvider(libraryItemId).future);

View file

@ -6,44 +6,11 @@ part of 'abs_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$configurePlayerHash() => r'7ac63b6c3a34c56f42be55bc7a4856dabaae1583';
String _$playerActiveHash() => r'86831758035aa69d74f42ebde0a19bf7ef830910';
///
///
/// Copied from [configurePlayer].
@ProviderFor(configurePlayer)
final configurePlayerProvider = FutureProvider<AudioHandler>.internal(
configurePlayer,
name: r'configurePlayerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$configurePlayerHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef ConfigurePlayerRef = FutureProviderRef<AudioHandler>;
String _$audioPlayerHash() => r'156f85effafdcd287db88e455e8f4f4d33c41a0e';
/// See also [audioPlayer].
@ProviderFor(audioPlayer)
final audioPlayerProvider = Provider<core.AbsAudioPlayer>.internal(
audioPlayer,
name: r'audioPlayerProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$audioPlayerHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef AudioPlayerRef = ProviderRef<core.AbsAudioPlayer>;
String _$playerActiveHash() => r'86831758035aa69d74f42ebde0a19bf7ef830910';
/// See also [playerActive].
/// Copied from [playerActive].
@ProviderFor(playerActive)
final playerActiveProvider = AutoDisposeProvider<bool>.internal(
playerActive,
@ -57,11 +24,11 @@ final playerActiveProvider = AutoDisposeProvider<bool>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef PlayerActiveRef = AutoDisposeProviderRef<bool>;
String _$simpleAudioPlayerHash() => r'4da667e3b7047003edd594f8a76700afb963aceb';
String _$simpleAudioPlayerHash() => r'99d84a750cf605ad036603320925f0ba7253930b';
/// See also [simpleAudioPlayer].
@ProviderFor(simpleAudioPlayer)
final simpleAudioPlayerProvider = Provider<AudioPlayer>.internal(
final simpleAudioPlayerProvider = Provider<audio.AudioPlayer>.internal(
simpleAudioPlayer,
name: r'simpleAudioPlayerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@ -73,7 +40,7 @@ final simpleAudioPlayerProvider = Provider<AudioPlayer>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef SimpleAudioPlayerRef = ProviderRef<AudioPlayer>;
typedef SimpleAudioPlayerRef = ProviderRef<audio.AudioPlayer>;
String _$currentTimeHash() => r'3e7f99dbf48242a5fa0a4239a0f696535d0b4ac9';
/// Copied from Dart SDK
@ -242,30 +209,11 @@ final positionChapterProvider = AutoDisposeStreamProvider<Duration>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef PositionChapterRef = AutoDisposeStreamProviderRef<Duration>;
String _$absAudioPlayerHash() => r'f595b5033eed9f4a4aa07c297c4a176955e6aab1';
String _$absPlayerHash() => r'370f576d3d3a2196d1a93f2046005c1a3298d994';
/// See also [AbsAudioPlayer].
@ProviderFor(AbsAudioPlayer)
final absAudioPlayerProvider =
NotifierProvider<AbsAudioPlayer, AudioPlayer>.internal(
AbsAudioPlayer.new,
name: r'absAudioPlayerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$absAudioPlayerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AbsAudioPlayer = Notifier<AudioPlayer>;
String _$absPlayerHash() => r'e682fea03793a0370cb143602980d5c1e37396c7';
/// riverpod状态
///
/// Copied from [AbsPlayer].
/// See also [AbsPlayer].
@ProviderFor(AbsPlayer)
final absPlayerProvider =
NotifierProvider<AbsPlayer, core.AbsAudioPlayer>.internal(
final absPlayerProvider = NotifierProvider<AbsPlayer, AbsAudioPlayer>.internal(
AbsPlayer.new,
name: r'absPlayerProvider',
debugGetCreateSourceHash:
@ -274,13 +222,15 @@ final absPlayerProvider =
allTransitiveDependencies: null,
);
typedef _$AbsPlayer = Notifier<core.AbsAudioPlayer>;
String _$playerStateHash() => r'f195d2d13bcee0f91b862e669ab3549667d8dd2d';
typedef _$AbsPlayer = Notifier<AbsAudioPlayer>;
String _$playerStateHash() => r'eb79bd816714f721da1c4226d4447de5dc55fc5c';
/// See also [PlayerState].
/// riverpod状态
///
/// Copied from [PlayerState].
@ProviderFor(PlayerState)
final playerStateProvider =
AutoDisposeNotifierProvider<PlayerState, core.AbsPlayerState>.internal(
AutoDisposeNotifierProvider<PlayerState, audio.PlayerState>.internal(
PlayerState.new,
name: r'playerStateProvider',
debugGetCreateSourceHash:
@ -289,8 +239,8 @@ final playerStateProvider =
allTransitiveDependencies: null,
);
typedef _$PlayerState = AutoDisposeNotifier<core.AbsPlayerState>;
String _$currentBookHash() => r'714d7701508b6186598e13bc38c57c3fe644ae90';
typedef _$PlayerState = AutoDisposeNotifier<audio.PlayerState>;
String _$currentBookHash() => r'85de9041d356e214761b65bd1b7b74321d5a9221';
/// See also [CurrentBook].
@ProviderFor(CurrentBook)

View file

@ -62,17 +62,17 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
// add a shadow to the image elevation hovering effect
child: PlayerExpandedImage(imageSize),
),
_buildControls(imageSize),
SizedBox(
width: imageSize,
child: Padding(
padding: EdgeInsets.only(
left: AppElementSizes.paddingRegular,
right: AppElementSizes.paddingRegular,
),
child: const AudiobookChapterProgressBar(),
),
),
// _buildControls(imageSize),
// SizedBox(
// width: imageSize,
// child: Padding(
// padding: EdgeInsets.only(
// left: AppElementSizes.paddingRegular,
// right: AppElementSizes.paddingRegular,
// ),
// child: const AudiobookChapterProgressBar(),
// ),
// ),
_buildSettings(imageSize),
],
),
@ -113,7 +113,37 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
),
),
),
Hero(tag: 'player_hero', child: const PlayerMinimizedControls()),
SizedBox(
height: playerMinimizedHeight,
child: _buildBottom(),
),
],
);
}
Widget _buildBottom() {
return Row(
children: [
SizedBox(
width: 180,
child: Row(
children: [
const AudiobookPlayerSeekChapterButton(isForward: false),
// play/pause button
const AudiobookPlayerPlayPauseButton(),
const AudiobookPlayerSeekChapterButton(isForward: true),
],
),
),
Expanded(
child: Padding(
padding: EdgeInsets.only(
left: AppElementSizes.paddingRegular,
right: AppElementSizes.paddingRegular,
),
child: const AudiobookChapterProgressBar(),
),
),
],
);
}
@ -128,7 +158,7 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
const AudiobookPlayerSeekChapterButton(isForward: false),
// buttonSkipBackwards
const AudiobookPlayerSeekButton(isForward: false),
AudiobookPlayerPlayPauseButton(),
const AudiobookPlayerPlayPauseButton(),
// // buttonSkipForwards
const AudiobookPlayerSeekButton(isForward: true),
// // next chapter
@ -144,6 +174,8 @@ class PlayerExpandedDesktop extends HookConsumerWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const AudiobookPlayerSeekButton(isForward: false),
const AudiobookPlayerSeekButton(isForward: true),
// speed control
const PlayerSpeedAdjustButton(),
const Spacer(),

View file

@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/abs_provider.dart';
import 'package:vaani/features/player/view/widgets/audiobook_player_seek_chapter_button.dart';
import 'package:vaani/features/player/view/widgets/player_player_pause_button.dart';
import 'package:vaani/router/router.dart';
import 'package:vaani/shared/extensions/chapter.dart';
@ -22,6 +23,9 @@ class PlayerMinimized extends HookConsumerWidget {
if (currentBook == null) {
return SizedBox.shrink();
}
final size = MediaQuery.of(context).size;
//
final isVertical = size.height > size.width;
return GestureDetector(
child: Container(
height: playerMinimizedHeight,
@ -29,8 +33,10 @@ class PlayerMinimized extends HookConsumerWidget {
child: Stack(
alignment: Alignment.topCenter,
children: [
Hero(tag: 'player_hero', child: const PlayerMinimizedControls()),
PlayerMinimizedProgress(),
isVertical
? const PlayerMinimizedControls()
: const PlayerMinimizedControlsDesktop(),
const PlayerMinimizedProgress(),
],
),
),
@ -130,10 +136,7 @@ class PlayerMinimizedControls extends HookConsumerWidget {
),
// play/pause button
Padding(
padding: const EdgeInsets.only(right: 8),
child: AudiobookPlayerPlayPauseButton(),
),
const AudiobookPlayerPlayPauseButton(),
],
),
);
@ -162,3 +165,99 @@ class PlayerMinimizedProgress extends HookConsumerWidget {
);
}
}
class PlayerMinimizedControlsDesktop extends HookConsumerWidget {
const PlayerMinimizedControlsDesktop({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentBook = ref.watch(currentBookProvider);
final currentChapter = ref.watch(currentChapterProvider);
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (GoRouterState.of(context).topRoute?.name != Routes.player.name) {
context.pushNamed(Routes.player.name);
} else {
context.pop();
}
},
child: Row(
children: [
SizedBox(
width: 180,
child: Row(
children: [
const AudiobookPlayerSeekChapterButton(isForward: false),
// play/pause button
const AudiobookPlayerPlayPauseButton(),
const AudiobookPlayerSeekChapterButton(isForward: true),
],
),
),
// image
Padding(
padding: EdgeInsets.all(AppElementSizes.paddingSmall),
child: GestureDetector(
onTap: () {
// navigate to item page
if (currentBook != null) {
context.pushNamed(
Routes.libraryItem.name,
pathParameters: {
Routes.libraryItem.pathParamName!:
currentBook.libraryItemId,
},
);
}
},
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: playerMinimizedHeight,
),
child: BookCoverWidget(),
),
),
),
// author and title of the book
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: AppElementSizes.paddingRegular,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
// AutoScrollText(
Text(
'${currentBook?.metadata.title ?? ''} - ${currentChapter?.title ?? ''}',
maxLines: 1, overflow: TextOverflow.ellipsis,
// velocity:
// const Velocity(pixelsPerSecond: Offset(16, 0)),
style: Theme.of(context).textTheme.bodyLarge,
),
Text(
currentBook?.metadata.asBookMetadataExpanded.authorName ??
'',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.7),
),
),
],
),
),
),
],
),
);
}
}

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:vaani/features/player/providers/abs_provider.dart'
hide PlayerState;
import 'package:vaani/features/player/core/abs_audio_player.dart';
class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
const AudiobookPlayerPlayPauseButton({
@ -21,12 +21,12 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
);
}
Widget _getIcon(AbsPlayerState playerState, BuildContext context) {
Widget _getIcon(PlayerState playerState, BuildContext context) {
if (playerState.playing) {
return Icon(size: iconSize, Icons.pause);
} else {
switch (playerState.processingState) {
case AbsProcessingState.loading || AbsProcessingState.buffering:
case ProcessingState.loading || ProcessingState.buffering:
return CircularProgressIndicator();
default:
return Icon(size: iconSize, Icons.play_arrow);
@ -34,13 +34,13 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget {
}
}
void _actionButtonPressed(AbsPlayerState playerState, WidgetRef ref) async {
void _actionButtonPressed(PlayerState playerState, WidgetRef ref) async {
final player = ref.read(absPlayerProvider);
if (playerState.playing) {
await player.pause();
} else {
switch (playerState.processingState) {
case AbsProcessingState.completed:
case ProcessingState.completed:
await player.seekInBook(const Duration(seconds: 0));
await player.play();
default:

View file

@ -12,6 +12,7 @@ class AudiobookChapterProgressBar extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final book = ref.watch(currentBookProvider);
final player = ref.watch(absPlayerProvider);
final currentChapter = ref.watch(currentChapterProvider);
final position = useStream(
@ -36,7 +37,7 @@ class AudiobookChapterProgressBar extends HookConsumerWidget {
final progress =
currentChapterProgress ?? position.data ?? const Duration(seconds: 0);
final total = currentChapter == null
? player.book?.duration ?? const Duration(seconds: 0)
? book?.duration ?? const Duration(seconds: 0)
: currentChapter.end - currentChapter.start;
return ProgressBar(
progress: progress,
@ -65,6 +66,7 @@ class AudiobookProgressBar extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final book = ref.watch(currentBookProvider);
final player = ref.read(absPlayerProvider);
final position = useStream(
player.positionInBookStream,
@ -75,7 +77,7 @@ class AudiobookProgressBar extends HookConsumerWidget {
height: AppElementSizes.barHeightLarge,
child: LinearProgressIndicator(
value: (position.data ?? const Duration(seconds: 0)).inSeconds /
(player.book?.duration ?? const Duration(seconds: 0)).inSeconds,
(book?.duration ?? const Duration(seconds: 0)).inSeconds,
borderRadius: BorderRadiusGeometry.all(Radius.circular(10)),
),
);

View file

@ -9,7 +9,6 @@ import 'package:vaani/features/settings/app_settings_provider.dart'
import 'package:vaani/features/settings/models/app_settings.dart';
import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'
show sleepTimerProvider;
import 'package:vaani/features/player/core/abs_audio_player.dart';
import 'package:vibration/vibration.dart';
import 'shake_detector.dart' as core;
@ -33,22 +32,22 @@ class ShakeDetector extends _$ShakeDetector {
}
// if no book is loaded, shake detection should not be enabled
final book = ref.watch(currentBookProvider);
final player = ref.watch(absPlayerProvider);
player.playerStateStream.listen((event) {
if (event.processingState == AbsProcessingState.idle && wasPlayerLoaded) {
if (event.processingState == ProcessingState.idle && wasPlayerLoaded) {
_logger.config('Player is now not loaded, invalidating');
wasPlayerLoaded = false;
ref.invalidateSelf();
}
if (event.processingState != AbsProcessingState.idle &&
!wasPlayerLoaded) {
if (event.processingState != ProcessingState.idle && !wasPlayerLoaded) {
_logger.config('Player is now loaded, invalidating');
wasPlayerLoaded = true;
ref.invalidateSelf();
}
});
if (player.book == null) {
if (book == null) {
_logger.config('No book is loaded, disabling shake detection');
wasPlayerLoaded = false;
return null;
@ -89,8 +88,10 @@ class ShakeDetector extends _$ShakeDetector {
ShakeAction shakeAction, {
required Ref ref,
}) {
final book = ref.read(currentBookProvider);
final player = ref.read(absPlayerProvider);
if (player.book == null && shakeAction.isPlaybackManagementEnabled) {
if (book == null && shakeAction.isPlaybackManagementEnabled) {
_logger.warning('No book is loaded');
return false;
}
@ -122,7 +123,7 @@ class ShakeDetector extends _$ShakeDetector {
return true;
case ShakeAction.playPause:
_logger.fine('Toggling play/pause');
player.playOrPause();
player.playing ? player.pause() : player.play();
return true;
default:
return false;

View file

@ -6,7 +6,7 @@ part of 'shake_detector_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$shakeDetectorHash() => r'8e65e89d59a9cf9492fd5f3eb309eb3a37cf1c6d';
String _$shakeDetectorHash() => r'c2e6b6b2edf3a40a7a8f5a274f881911be68a5a0';
/// See also [ShakeDetector].
@ProviderFor(ShakeDetector)

View file

@ -1,55 +1,55 @@
import 'dart:async';
// import 'dart:async';
import 'package:vaani/features/player/core/abs_audio_player.dart';
import 'package:vaani/shared/extensions/chapter.dart';
import 'package:vaani/shared/utils/throttler.dart';
// import 'package:vaani/features/player/core/abs_audio_player.dart';
// import 'package:vaani/shared/extensions/chapter.dart';
// import 'package:vaani/shared/utils/throttler.dart';
class SkipStartEnd {
final Duration start;
final Duration end;
final AbsAudioPlayer player;
// class SkipStartEnd {
// final Duration start;
// final Duration end;
// final AbsAudioPlayer player;
final List<StreamSubscription> _subscriptions = [];
final throttlerStart = Throttler(delay: Duration(seconds: 3));
final throttlerEnd = Throttler(delay: Duration(seconds: 3));
// final List<StreamSubscription> _subscriptions = [];
// final throttlerStart = Throttler(delay: Duration(seconds: 3));
// final throttlerEnd = Throttler(delay: Duration(seconds: 3));
SkipStartEnd({
required this.start,
required this.end,
required this.player,
}) {
if (start > Duration.zero) {
_subscriptions.add(
player.chapterStream.listen((chapter) async {
if (chapter != null &&
player.positionInChapter < Duration(seconds: 1)) {
player.seekInBook(chapter.start + start);
}
}),
);
}
if (end > Duration.zero) {
_subscriptions.add(
player.positionInChapterStream.listen((positionChapter) {
if (end >
(player.currentChapter?.duration ?? Duration.zero) -
positionChapter) {
Future.microtask(
() => throttlerEnd.call(() => player.next()),
);
}
}),
);
}
}
// SkipStartEnd({
// required this.start,
// required this.end,
// required this.player,
// }) {
// if (start > Duration.zero) {
// _subscriptions.add(
// player.chapterStream.listen((chapter) async {
// if (chapter != null &&
// player.positionInChapter < Duration(seconds: 1)) {
// player.seekInBook(chapter.start + start);
// }
// }),
// );
// }
// if (end > Duration.zero) {
// _subscriptions.add(
// player.positionInChapterStream.listen((positionChapter) {
// if (end >
// (player.currentChapter?.duration ?? Duration.zero) -
// positionChapter) {
// Future.microtask(
// () => throttlerEnd.call(() => player.next()),
// );
// }
// }),
// );
// }
// }
/// dispose the timer
void dispose() {
for (var sub in _subscriptions) {
sub.cancel();
}
throttlerStart.dispose();
throttlerEnd.dispose();
// _playbackController.close();
}
}
// /// dispose the timer
// void dispose() {
// for (var sub in _subscriptions) {
// sub.cancel();
// }
// throttlerStart.dispose();
// throttlerEnd.dispose();
// // _playbackController.close();
// }
// }

View file

@ -1,34 +1,34 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
import 'package:vaani/features/player/providers/abs_provider.dart';
import 'package:vaani/features/skip_start_end/core/skip_start_end.dart' as core;
// import 'package:riverpod_annotation/riverpod_annotation.dart';
// import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
// import 'package:vaani/features/player/providers/abs_provider.dart';
// import 'package:vaani/features/skip_start_end/core/skip_start_end.dart' as core;
part 'skip_start_end_provider.g.dart';
// part 'skip_start_end_provider.g.dart';
@riverpod
class SkipStartEnd extends _$SkipStartEnd {
@override
core.SkipStartEnd? build() {
final currentBook = ref.watch(currentBookProvider);
final bookId = currentBook?.libraryItemId;
if (currentBook == null || bookId == null) {
return null;
}
// @riverpod
// class SkipStartEnd extends _$SkipStartEnd {
// @override
// core.SkipStartEnd? build() {
// final currentBook = ref.watch(currentBookProvider);
// final bookId = currentBook?.libraryItemId;
// if (currentBook == null || bookId == null) {
// return null;
// }
final player = ref.read(absPlayerProvider);
final bookSettings = ref.watch(bookSettingsProvider(bookId));
final start = bookSettings.playerSettings.skipChapterStart;
final end = bookSettings.playerSettings.skipChapterEnd;
if (start < Duration.zero && end < Duration.zero) {
return null;
}
// final player = ref.read(absPlayerProvider);
// final bookSettings = ref.watch(bookSettingsProvider(bookId));
// final start = bookSettings.playerSettings.skipChapterStart;
// final end = bookSettings.playerSettings.skipChapterEnd;
// if (start < Duration.zero && end < Duration.zero) {
// return null;
// }
final skipStartEnd = core.SkipStartEnd(
start: start,
end: end,
player: player,
);
ref.onDispose(skipStartEnd.dispose);
return skipStartEnd;
}
}
// final skipStartEnd = core.SkipStartEnd(
// start: start,
// end: end,
// player: player,
// );
// ref.onDispose(skipStartEnd.dispose);
// return skipStartEnd;
// }
// }

View file

@ -1,25 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'skip_start_end_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$skipStartEndHash() => r'45572f40a098f081181e8b8bf9e4913e6e649cdc';
/// See also [SkipStartEnd].
@ProviderFor(SkipStartEnd)
final skipStartEndProvider =
AutoDisposeNotifierProvider<SkipStartEnd, core.SkipStartEnd?>.internal(
SkipStartEnd.new,
name: r'skipStartEndProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$skipStartEndHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SkipStartEnd = AutoDisposeNotifier<core.SkipStartEnd?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -1,8 +1,8 @@
import 'dart:async';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:just_audio/just_audio.dart';
import 'package:logging/logging.dart';
import 'package:vaani/features/player/core/abs_audio_player.dart';
/// this timer pauses the music player after a certain duration
///
@ -32,7 +32,7 @@ class SleepTimer {
}
/// The player to be paused
final AbsAudioPlayer player;
final AudioPlayer player;
/// The timer that will pause the player
Timer? timer;
@ -50,8 +50,8 @@ class SleepTimer {
SleepTimer({required duration, required this.player}) : _duration = duration {
_subscriptions.add(
player.playerStateStream.listen((event) {
if (event.processingState == AbsProcessingState.completed ||
event.processingState == AbsProcessingState.idle) {
if (event.processingState == ProcessingState.completed ||
event.processingState == ProcessingState.idle) {
clearCountDownTimer();
}
}),

View file

@ -26,7 +26,7 @@ class SleepTimer extends _$SleepTimer {
var sleepTimer = core.SleepTimer(
duration: sleepTimerSettings.defaultDuration,
player: ref.watch(absPlayerProvider),
player: ref.watch(simpleAudioPlayerProvider),
);
ref.onDispose(sleepTimer.dispose);
return sleepTimer;
@ -45,7 +45,7 @@ class SleepTimer extends _$SleepTimer {
} else {
final timer = core.SleepTimer(
duration: resultingDuration,
player: ref.watch(absPlayerProvider),
player: ref.watch(simpleAudioPlayerProvider),
);
ref.onDispose(timer.dispose);
state = timer;

View file

@ -6,7 +6,7 @@ part of 'sleep_timer_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$sleepTimerHash() => r'417759e07a45e69af93bd9a1c78ac859d9abcf4b';
String _$sleepTimerHash() => r'7cac4509d8bd40c4d418c295d5b37c66492e7de9';
/// See also [SleepTimer].
@ProviderFor(SleepTimer)