一堆乱七八糟的修改

播放页面增加桌面版
This commit is contained in:
rang 2025-11-28 17:05:35 +08:00
parent aee1fbde88
commit 3ba35b31b8
116 changed files with 1238 additions and 2592 deletions

View file

@ -1,240 +1,239 @@
/// a wrapper around the audioplayers package to manage the audio player instance
///
/// this is needed as audiobook can be a list of audio files instead of a single file
library;
// my_audio_handler.dart
import 'dart:io';
import 'package:audio_service/audio_service.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.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';
import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/settings/models/app_settings.dart';
import 'package:vaani/shared/extensions/model_conversions.dart';
final _logger = Logger('AudiobookPlayer');
import 'package:vaani/features/player/core/player_status.dart' as core;
import 'package:vaani/features/player/providers/player_status_provider.dart';
import 'package:vaani/shared/extensions/chapter.dart';
// add a small offset so the display does not show the previous chapter for a split second
final offset = Duration(milliseconds: 10);
/// time into the current chapter to determine if we should go to the previous chapter or the start of the current chapter
final doNotSeekBackIfLessThan = Duration(seconds: 5);
class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
final AudioPlayer _player = AudioPlayer();
// final List<AudioSource> _playlist = [];
final Ref ref;
/// returns the sum of the duration of all the previous tracks before the [index]
Duration sumOfTracks(BookExpanded book, int? index) {
_logger.fine('Calculating sum of tracks for index: $index');
// return 0 if index is less than 0
if (index == null || index < 0) {
_logger.warning('Index is null or less than 0, returning 0');
return Duration.zero;
}
final total = book.tracks.sublist(0, index).fold<Duration>(
Duration.zero,
(previousValue, element) => previousValue + element.duration,
);
_logger.fine('Sum of tracks for index: $index is $total');
return total;
}
PlaybackSessionExpanded? _session;
/// will manage the audio player instance
class AudiobookPlayer extends AudioPlayer {
// constructor which takes in the BookExpanded object
AudiobookPlayer(this.token, this.baseUrl) : super() {
// set the source of the player to the first track in the book
_logger.config('Setting up audiobook player');
// playerStateStream.listen((playerState) {
// if (playerState.processingState == ProcessingState.completed) {
// Future.microtask(seekToNext);
// }
// });
final _currentChapterObject = BehaviorSubject<BookChapter?>.seeded(null);
AbsAudioHandler(this.ref) {
_setupAudioPlayer();
}
/// the [BookExpanded] being played
BookExpanded? _book;
void _setupAudioPlayer() {
final statusNotifier = ref.read(playerStatusProvider.notifier);
// /// the [BookExpanded] trying to be played
// BookExpanded? _intended_book;
/// the [BookExpanded] being played
///
/// to set the book, use [setSourceAudiobook]
BookExpanded? get book => _book;
/// the authentication token to access the [AudioTrack.contentUrl]
final String token;
/// the base url for the audio files
final Uri baseUrl;
// the current index of the audio file in the [book]
// int _currentIndex = 0;
// available audio tracks
int? get availableTracks => _book?.tracks.length;
/// sets the current [AudioTrack] as the source of the player
Future<void> setSourceAudiobook(
BookExpanded? book, {
bool preload = true,
int? initialIndex,
Duration? initialPosition,
List<Uri>? downloadedUris,
Uri? artworkUri,
}) async {
_logger.finer(
'Initial position: $initialPosition, Downloaded URIs: $downloadedUris',
);
final appSettings = loadOrCreateAppSettings();
if (book == null) {
_book = null;
_logger.info('Book is null, stopping player');
return stop();
}
if (_book == book) {
_logger.info('Book is the same, doing nothing');
return;
}
_logger.info('Setting source for book: $book');
_logger.fine('Stopping player');
await stop();
_book = book;
// some calculations to set the initial index and position
// initialPosition is of the entire book not just the current track
// hence first we need to calculate the current track which will be used to set the initial position
// then we set the initial index to the current track index and position as the remaining duration from the position
// after subtracting the duration of all the previous tracks
// initialPosition ;
final trackToPlay =
_book!.findTrackAtTime(initialPosition ?? Duration.zero);
final initialIndex = book.tracks.indexOf(trackToPlay);
final initialPositionInTrack = initialPosition != null
? initialPosition - trackToPlay.startOffset
: null;
_logger.finer('Setting audioSource');
final playlist = book.tracks.map((track) {
final retrievedUri =
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token);
// _logger.fine(
// 'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}',
// );
return AudioSource.uri(
retrievedUri,
// tag: MediaItem(
// // Specify a unique ID for each media item:
// id: book.libraryItemId + track.index.toString(),
// // Metadata to display in the notification:
// title: appSettings.notificationSettings.primaryTitle
// .formatNotificationTitle(book),
// album: appSettings.notificationSettings.secondaryTitle
// .formatNotificationTitle(book),
// artUri: artworkUri ??
// Uri.parse(
// '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800',
// ),
// ),
);
}).toList();
await setAudioSources(
playlist,
preload: preload,
initialIndex: initialIndex,
initialPosition: initialPositionInTrack,
).catchError((error) {
_logger.shout('Error in setting audio source: $error');
return null;
//
_player.playbackEventStream.map(_transformEvent).pipe(playbackState);
_player.playerStateStream.listen((event) {
if (event.playing) {
statusNotifier.setPlayStatusVerify(core.PlayStatus.playing);
} else {
statusNotifier.setPlayStatusVerify(core.PlayStatus.paused);
}
});
_player.positionStream.distinct().listen((position) {
final chapter = _session?.findChapterAtTime(positionInBook);
if (chapter != currentChapter) {
_currentChapterObject.sink.add(chapter);
}
});
}
/// toggles the player between play and pause
Future<void> togglePlayPause() {
// check if book is set
if (_book == null) {
_logger.warning('No book is set, not toggling play/pause');
//
Future<void> setSourceAudiobook(
PlaybackSessionExpanded playbackSession, {
required Uri baseUrl,
required String token,
List<Uri>? downloadedUris,
}) async {
_session = playbackSession;
//
List<AudioSource> audioSources = [];
for (final track in playbackSession.audioTracks) {
audioSources.add(
AudioSource.uri(
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
),
);
}
// TODO refactor this to cover all the states
return switch (playerState) {
PlayerState(playing: var isPlaying) => isPlaying ? pause() : play(),
};
playMediaItem(
MediaItem(
id: playbackSession.libraryItemId,
album: playbackSession.mediaMetadata.title,
title: playbackSession.displayTitle,
displaySubtitle: playbackSession.mediaType == MediaType.book
? (playbackSession.mediaMetadata as BookMetadata).subtitle
: null,
duration: playbackSession.duration,
artUri: Uri.parse(
'$baseUrl/api/items/${playbackSession.libraryItemId}/cover?token=$token',
),
),
);
final track = playbackSession.findTrackAtTime(playbackSession.currentTime);
final index = playbackSession.audioTracks.indexOf(track);
await _player.setAudioSources(
audioSources,
initialIndex: index,
initialPosition: playbackSession.currentTime - track.startOffset,
);
_player.seek(playbackSession.currentTime - track.startOffset, index: index);
await play();
//
// if (initialPosition != null) {
// await seekInBook(initialPosition);
// }
}
/// need to override getDuration and getCurrentPosition to return according to the book instead of the current track
/// this is because the book can be a list of audio files and the player is only aware of the current track
/// so we need to calculate the duration and current position based on the book
Future<void> seekInBook(Duration globalPosition) async {
if (_book == null) {
_logger.warning('No book is set, not seeking');
return;
}
//
final track = _book!.findTrackAtTime(globalPosition);
final index = _book!.tracks.indexOf(track);
Duration positionInTrack = globalPosition - track.startOffset;
if (positionInTrack <= Duration.zero) {
positionInTrack = offset;
}
//
if (index != currentIndex) {
await seek(positionInTrack, index: index);
}
await seek(positionInTrack);
}
// //
// void _onTrackChanged(int trackIndex) {
// if (_book == null) return;
// //
// // print('切换到音轨: ${_book!.tracks[trackIndex].title}');
// }
//
Future<void> skipToChapter(int chapterId, {Duration? position}) async {
if (_book == null) return;
Future<void> skipToChapter(int chapterId) async {
if (_session == null) return;
final chapter = _book!.chapters.firstWhere(
final chapter = _session!.chapters.firstWhere(
(ch) => ch.id == chapterId,
orElse: () => throw Exception('Chapter not found'),
);
if (position != null) {
print('章节开头: ${chapter.start}');
print('章节开头: ${chapter.start + position}');
await seekInBook(chapter.start + position);
return;
}
await seekInBook(chapter.start + offset);
}
PlaybackSessionExpanded? get session => _session;
//
AudioTrack? get currentTrack {
if (_session == null || _player.currentIndex == null) {
return null;
}
return _session!.audioTracks[_player.currentIndex!];
}
//
BookChapter? get currentChapter {
return _currentChapterObject.value;
}
Duration get position => _player.position;
Duration get positionInChapter {
return _player.position +
(currentTrack?.startOffset ?? Duration.zero) -
(currentChapter?.start ?? Duration.zero);
}
Duration get positionInBook {
return _player.position + (currentTrack?.startOffset ?? Duration.zero);
}
Duration get bufferedPositionInBook {
return _player.bufferedPosition +
(currentTrack?.startOffset ?? Duration.zero);
}
Duration? get chapterDuration => currentChapter?.duration;
Stream<PlayerState> get playerStateStream => _player.playerStateStream;
Stream<Duration> get positionStream => _player.positionStream;
Stream<Duration> get positionStreamInBook {
return _player.positionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> get slowPositionStreamInBook {
final superPositionStream = _player.createPositionStream(
steps: 100,
minPeriod: const Duration(milliseconds: 500),
maxPeriod: const Duration(seconds: 1),
);
return superPositionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> get bufferedPositionStreamInBook {
return _player.bufferedPositionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> get positionStreamInChapter {
return _player.positionStream.distinct().map((position) {
return position +
(currentTrack?.startOffset ?? Duration.zero) -
(currentChapter?.start ?? Duration.zero);
});
}
Stream<BookChapter?> get chapterStream => _currentChapterObject.stream;
Future<void> togglePlayPause() async {
// check if book is set
if (_session == null) {
return Future.value();
}
_player.playerState.playing ? await pause() : await play();
}
//
@override
Future<void> seekToNext() async {
if (_book == null) {
Future<void> play() async {
await _player.play();
}
@override
Future<void> pause() async {
await _player.pause();
}
// /
@override
Future<void> skipToNext() async {
if (_session == null) {
// 退
return super.seekToNext();
return _player.seekToNext();
}
final chapter = currentChapter;
if (chapter == null) {
// 退
return super.seekToNext();
return _player.seekToNext();
}
final currentIndex = _book!.chapters.indexOf(chapter);
if (currentIndex < _book!.chapters.length - 1) {
final chapterIndex = _session!.chapters.indexOf(chapter);
if (chapterIndex < _session!.chapters.length - 1) {
//
final nextChapter = _book!.chapters[currentIndex + 1];
final nextChapter = _session!.chapters[chapterIndex + 1];
await skipToChapter(nextChapter.id);
}
}
@override
Future<void> seekToPrevious() async {
if (_book == null) {
return super.seekToPrevious();
}
Future<void> skipToPrevious() async {
final chapter = currentChapter;
if (chapter == null) {
return super.seekToPrevious();
if (_session == null || chapter == null) {
return _player.seekToPrevious();
}
final currentIndex = _book!.chapters.indexOf(chapter);
final currentIndex = _session!.chapters.indexOf(chapter);
if (currentIndex > 0) {
//
final prevChapter = _book!.chapters[currentIndex - 1];
final prevChapter = _session!.chapters[currentIndex - 1];
await skipToChapter(prevChapter.id);
} else {
//
@ -242,84 +241,77 @@ class AudiobookPlayer extends AudioPlayer {
}
}
/// a convenience method to get position in the book instead of the current track position
Duration get positionInBook {
if (_book == null || currentIndex == null) {
return Duration.zero;
@override
Future<void> seek(Duration position) async {
// position 使
//
final track = currentTrack;
Duration startOffset = Duration.zero;
if (track != null) {
startOffset = track.startOffset;
}
return position + _book!.tracks[currentIndex!].startOffset;
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset;
await seekInBook(startOffset + position);
}
/// a convenience method to get the buffered position in the book instead of the current track position
Duration get bufferedPositionInBook {
if (_book == null || currentIndex == null) {
return Duration.zero;
Future<void> setVolume(double volume) async {
await _player.setVolume(volume);
}
@override
Future<void> setSpeed(double speed) async {
await _player.setSpeed(speed);
}
//
Future<void> seekInBook(Duration globalPosition) async {
if (_session == null) return;
//
final track = _session!.findTrackAtTime(globalPosition);
final index = _session!.audioTracks.indexOf(track);
Duration positionInTrack = globalPosition - track.startOffset;
if (positionInTrack < Duration.zero) {
positionInTrack = Duration.zero;
}
return bufferedPosition + _book!.tracks[currentIndex!].startOffset;
// return bufferedPosition + _book!.tracks[sequenceState!.currentIndex].startOffset;
//
await _player.seek(positionInTrack, index: index);
}
//
Stream<Duration> get positionStreamInChapter {
return super.positionStream.map((position) {
if (_book == null || currentIndex == null) {
return Duration.zero;
}
final globalPosition =
position + _book!.tracks[currentIndex!].startOffset;
final chapter = _book!.findChapterAtTime(globalPosition);
return globalPosition - chapter.start;
});
}
/// streams to override to suit the book instead of the current track
// - positionStream
// - bufferedPositionStream
Stream<Duration> get positionStreamInBook {
// return the positionInBook stream
return super.positionStream.map((position) {
if (_book == null || currentIndex == null) {
return Duration.zero;
}
return position + _book!.tracks[currentIndex!].startOffset;
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset;
});
}
Stream<Duration> get bufferedPositionStreamInBook {
return super.bufferedPositionStream.map((position) {
if (_book == null || currentIndex == null) {
return Duration.zero;
}
return position + _book!.tracks[currentIndex!].startOffset;
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset;
});
}
/// a convenience getter for slow position stream
Stream<Duration> get slowPositionStreamInBook {
final superPositionStream = createPositionStream(
steps: 100,
minPeriod: const Duration(milliseconds: 500),
maxPeriod: const Duration(seconds: 1),
AudioPlayer get player => _player;
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,
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] ??
AudioProcessingState.idle,
playing: _player.playing,
updatePosition: _player.position,
bufferedPosition: event.bufferedPosition,
speed: _player.speed,
queueIndex: event.currentIndex,
captioningEnabled: false,
);
// now we need to map the position to the book instead of the current track
return superPositionStream.map((position) {
if (_book == null || currentIndex == null) {
return Duration.zero;
}
return position + _book!.tracks[currentIndex!].startOffset;
// return position + _book!.tracks[sequenceState!.currentIndex].startOffset;
});
}
/// get current chapter
BookChapter? get currentChapter {
if (_book == null) {
return null;
}
return _book!.findChapterAtTime(positionInBook);
}
}
@ -340,46 +332,7 @@ Uri _getUri(
Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token');
}
extension FormatNotificationTitle on String {
String formatNotificationTitle(BookExpanded book) {
return replaceAllMapped(
RegExp(r'\$(\w+)'),
(match) {
final type = match.group(1);
return NotificationTitleType.values
.firstWhere((element) => element.name == type)
.extractFrom(book) ??
match.group(0) ??
'';
},
);
}
}
extension NotificationTitleUtils on NotificationTitleType {
String? extractFrom(BookExpanded book) {
var bookMetadataExpanded = book.metadata.asBookMetadataExpanded;
switch (this) {
case NotificationTitleType.bookTitle:
return bookMetadataExpanded.title;
case NotificationTitleType.chapterTitle:
// TODO: implement chapter title; depends on https://github.com/Dr-Blank/Vaani/issues/2
return bookMetadataExpanded.title;
case NotificationTitleType.author:
return bookMetadataExpanded.authorName;
case NotificationTitleType.narrator:
return bookMetadataExpanded.narratorName;
case NotificationTitleType.series:
return bookMetadataExpanded.seriesName;
case NotificationTitleType.subtitle:
return bookMetadataExpanded.subtitle;
case NotificationTitleType.year:
return bookMetadataExpanded.publishedYear;
}
}
}
extension BookExpandedExtension on BookExpanded {
extension PlaybackSessionExpandedExtension on PlaybackSessionExpanded {
BookChapter findChapterAtTime(Duration position) {
return chapters.firstWhere(
(element) {
@ -390,16 +343,23 @@ extension BookExpandedExtension on BookExpanded {
}
AudioTrack findTrackAtTime(Duration position) {
return tracks.firstWhere(
return audioTracks.firstWhere(
(element) {
return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset;
},
orElse: () => tracks.first,
orElse: () => audioTracks.first,
);
}
int findTrackIndexAtTime(Duration position) {
return audioTracks.indexWhere((element) {
return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset;
});
}
Duration getTrackStartOffset(int index) {
return tracks[index].startOffset;
return audioTracks[index].startOffset;
}
}

View file

@ -1,365 +0,0 @@
// my_audio_handler.dart
import 'dart:io';
import 'package:audio_service/audio_service.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:rxdart/rxdart.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/features/player/core/player_status.dart' as core;
import 'package:vaani/features/player/providers/player_status_provider.dart';
import 'package:vaani/shared/extensions/chapter.dart';
// add a small offset so the display does not show the previous chapter for a split second
final offset = Duration(milliseconds: 10);
class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
final AudioPlayer _player = AudioPlayer();
// final List<AudioSource> _playlist = [];
final Ref ref;
PlaybackSessionExpanded? _session;
final _currentChapterObject = BehaviorSubject<BookChapter?>.seeded(null);
AbsAudioHandler(this.ref) {
_setupAudioPlayer();
}
void _setupAudioPlayer() {
final statusNotifier = ref.read(playerStatusProvider.notifier);
//
_player.playbackEventStream.map(_transformEvent).pipe(playbackState);
_player.playerStateStream.listen((event) {
if (event.playing) {
statusNotifier.setPlayStatusVerify(core.PlayStatus.playing);
} else {
statusNotifier.setPlayStatusVerify(core.PlayStatus.paused);
}
});
_player.positionStream.distinct().listen((position) {
final chapter = _session?.findChapterAtTime(positionInBook);
if (chapter != currentChapter) {
_currentChapterObject.sink.add(chapter);
}
});
}
//
Future<void> setSourceAudiobook(
PlaybackSessionExpanded playbackSession, {
required Uri baseUrl,
required String token,
List<Uri>? downloadedUris,
}) async {
_session = playbackSession;
//
List<AudioSource> audioSources = [];
for (final track in playbackSession.audioTracks) {
audioSources.add(
AudioSource.uri(
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
),
);
}
playMediaItem(
MediaItem(
id: playbackSession.libraryItemId,
album: playbackSession.mediaMetadata.title,
title: playbackSession.displayTitle,
displaySubtitle: playbackSession.mediaType == MediaType.book
? (playbackSession.mediaMetadata as BookMetadata).subtitle
: null,
duration: playbackSession.duration,
artUri: Uri.parse(
'$baseUrl/api/items/${playbackSession.libraryItemId}/cover?token=$token',
),
),
);
final track = playbackSession.findTrackAtTime(playbackSession.currentTime);
final index = playbackSession.audioTracks.indexOf(track);
await _player.setAudioSources(
audioSources,
initialIndex: index,
initialPosition: playbackSession.currentTime - track.startOffset,
);
_player.seek(playbackSession.currentTime - track.startOffset, index: index);
await play();
//
// if (initialPosition != null) {
// await seekInBook(initialPosition);
// }
}
// //
// void _onTrackChanged(int trackIndex) {
// if (_book == null) return;
// //
// // print('切换到音轨: ${_book!.tracks[trackIndex].title}');
// }
//
Future<void> skipToChapter(int chapterId) async {
if (_session == null) return;
final chapter = _session!.chapters.firstWhere(
(ch) => ch.id == chapterId,
orElse: () => throw Exception('Chapter not found'),
);
await seekInBook(chapter.start + offset);
}
PlaybackSessionExpanded? get session => _session;
//
AudioTrack? get currentTrack {
if (_session == null || _player.currentIndex == null) {
return null;
}
return _session!.audioTracks[_player.currentIndex!];
}
//
BookChapter? get currentChapter {
return _currentChapterObject.value;
}
Duration get position => _player.position;
Duration get positionInChapter {
return _player.position +
(currentTrack?.startOffset ?? Duration.zero) -
(currentChapter?.start ?? Duration.zero);
}
Duration get positionInBook {
return _player.position + (currentTrack?.startOffset ?? Duration.zero);
}
Duration get bufferedPositionInBook {
return _player.bufferedPosition +
(currentTrack?.startOffset ?? Duration.zero);
}
Duration? get chapterDuration => currentChapter?.duration;
Stream<PlayerState> get playerStateStream => _player.playerStateStream;
Stream<Duration> get positionStream => _player.positionStream;
Stream<Duration> get positionStreamInBook {
return _player.positionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> get slowPositionStreamInBook {
final superPositionStream = _player.createPositionStream(
steps: 100,
minPeriod: const Duration(milliseconds: 500),
maxPeriod: const Duration(seconds: 1),
);
return superPositionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> get bufferedPositionStreamInBook {
return _player.bufferedPositionStream.map((position) {
return position + (currentTrack?.startOffset ?? Duration.zero);
});
}
Stream<Duration> get positionStreamInChapter {
return _player.positionStream.distinct().map((position) {
return position +
(currentTrack?.startOffset ?? Duration.zero) -
(currentChapter?.start ?? Duration.zero);
});
}
Stream<BookChapter?> get chapterStream => _currentChapterObject.stream;
Future<void> togglePlayPause() async {
// check if book is set
if (_session == null) {
return Future.value();
}
_player.playerState.playing ? await pause() : await play();
}
//
@override
Future<void> play() async {
await _player.play();
}
@override
Future<void> pause() async {
await _player.pause();
}
// /
@override
Future<void> skipToNext() async {
if (_session == null) {
// 退
return _player.seekToNext();
}
final chapter = currentChapter;
if (chapter == null) {
// 退
return _player.seekToNext();
}
final chapterIndex = _session!.chapters.indexOf(chapter);
if (chapterIndex < _session!.chapters.length - 1) {
//
final nextChapter = _session!.chapters[chapterIndex + 1];
await skipToChapter(nextChapter.id);
}
}
@override
Future<void> skipToPrevious() async {
final chapter = currentChapter;
if (_session == null || chapter == null) {
return _player.seekToPrevious();
}
final currentIndex = _session!.chapters.indexOf(chapter);
if (currentIndex > 0) {
//
final prevChapter = _session!.chapters[currentIndex - 1];
await skipToChapter(prevChapter.id);
} else {
//
await seekInBook(Duration.zero);
}
}
@override
Future<void> seek(Duration position) async {
// position 使
//
final track = currentTrack;
Duration startOffset = Duration.zero;
if (track != null) {
startOffset = track.startOffset;
}
await seekInBook(startOffset + position);
}
Future<void> setVolume(double volume) async {
await _player.setVolume(volume);
}
@override
Future<void> setSpeed(double speed) async {
await _player.setSpeed(speed);
}
//
Future<void> seekInBook(Duration globalPosition) async {
if (_session == null) return;
//
final track = _session!.findTrackAtTime(globalPosition);
final index = _session!.audioTracks.indexOf(track);
Duration positionInTrack = globalPosition - track.startOffset;
if (positionInTrack < Duration.zero) {
positionInTrack = Duration.zero;
}
//
await _player.seek(positionInTrack, index: index);
}
AudioPlayer get player => _player;
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,
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] ??
AudioProcessingState.idle,
playing: _player.playing,
updatePosition: _player.position,
bufferedPosition: event.bufferedPosition,
speed: _player.speed,
queueIndex: event.currentIndex,
captioningEnabled: false,
);
}
}
Uri _getUri(
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');
}
extension PlaybackSessionExpandedExtension on PlaybackSessionExpanded {
BookChapter findChapterAtTime(Duration position) {
return chapters.firstWhere(
(element) {
return element.start <= position && element.end >= position + offset;
},
orElse: () => chapters.first,
);
}
AudioTrack findTrackAtTime(Duration position) {
return audioTracks.firstWhere(
(element) {
return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset;
},
orElse: () => audioTracks.first,
);
}
int findTrackIndexAtTime(Duration position) {
return audioTracks.indexWhere((element) {
return element.startOffset <= position &&
element.startOffset + element.duration >= position + offset;
});
}
Duration getTrackStartOffset(int index) {
return audioTracks[index].startOffset;
}
}

View file

@ -1,62 +0,0 @@
// 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/settings/app_settings_provider.dart';
// import 'package:vaani/settings/models/app_settings.dart';
// Future<void> configurePlayer() async {
// // for playing audio on windows, linux
// JustAudioMediaKit.ensureInitialized(windows: false);
// // for configuring how this app will interact with other audio apps
// final session = await AudioSession.instance;
// await session.configure(const AudioSessionConfiguration.speech());
// final appSettings = loadOrCreateAppSettings();
// // for playing audio in the background
// await JustAudioBackground.init(
// androidNotificationChannelId: 'com.vaani.bg_demo.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,
// },
// );
// },
// );
// }