更改播放音频方式

This commit is contained in:
rang 2025-11-19 17:43:04 +08:00
parent 4a02b757bc
commit eb1955e5e6
25 changed files with 2102 additions and 1250 deletions

View file

@ -5,7 +5,7 @@ library;
import 'package:collection/collection.dart';
import 'package:just_audio/just_audio.dart';
import 'package:just_audio_background/just_audio_background.dart';
// import 'package:just_audio_background/just_audio_background.dart';
import 'package:logging/logging.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
@ -124,19 +124,19 @@ class AudiobookPlayer extends AudioPlayer {
// );
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',
),
),
// 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(

View file

@ -1,33 +1,30 @@
// 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:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/features/player/providers/session_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 HookAudioHandler extends BaseAudioHandler {
class AbsAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
final AudioPlayer _player = AudioPlayer();
final List<AudioSource> _playlist = [];
// final List<AudioSource> _playlist = [];
final Ref ref;
BookExpanded? _book;
PlaybackSessionExpanded? _session;
/// the authentication token to access the [AudioTrack.contentUrl]
final String token;
/// the base url for the audio files
final Uri baseUrl;
HookAudioHandler(this.ref, {required this.token, required this.baseUrl}) {
AbsAudioHandler(this.ref) {
_setupAudioPlayer();
}
void _setupAudioPlayer() {
_player.setAudioSources(_playlist);
// //
// _player.positionStream.listen((position) {
// // _updateGlobalPosition(position);
@ -42,49 +39,58 @@ class HookAudioHandler extends BaseAudioHandler {
//
_player.playbackEventStream.map(_transformEvent).pipe(playbackState);
_player.playerStateStream.distinct().listen((event) {
ref.read(playStateProvider.notifier).setState(event);
});
}
//
Future<void> setSourceAudiobook(
BookExpanded audiobook, {
Duration? initialPosition,
PlaybackSessionExpanded playbackSession, {
required Uri baseUrl,
required String token,
List<Uri>? downloadedUris,
}) async {
_book = audiobook;
//
_playlist.clear();
_session = playbackSession;
//
for (final track in audiobook.tracks) {
final audioSource = ProgressiveAudioSource(
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
tag: MediaItem(
id: '${audiobook.libraryItemId}${track.index}',
title: track.title,
duration: track.duration,
List<AudioSource> audioSources = [];
for (final track in playbackSession.audioTracks) {
audioSources.add(
AudioSource.uri(
_getUri(track, downloadedUris, baseUrl: baseUrl, token: token),
),
);
_playlist.add(audioSource);
}
//
final mediaItems = audiobook.tracks
.map(
(track) => MediaItem(
id: '${audiobook.libraryItemId}${track.index}',
title: track.title,
duration: track.duration,
),
)
.toList();
queue.add(mediaItems);
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 seekToPosition(initialPosition);
}
// if (initialPosition != null) {
// await seekInBook(initialPosition);
// }
}
// //
@ -97,19 +103,19 @@ class HookAudioHandler extends BaseAudioHandler {
//
Future<void> skipToChapter(int chapterId) async {
if (_book == null) return;
if (_session == null) return;
final chapter = _book!.chapters.firstWhere(
final chapter = _session!.chapters.firstWhere(
(ch) => ch.id == chapterId,
orElse: () => throw Exception('Chapter not found'),
);
await seekToPosition(chapter.start + offset);
await seekInBook(chapter.start + offset);
}
Duration get positionInBook {
if (_book != null && _player.currentIndex != null) {
return _book!.tracks[_player.currentIndex!].startOffset +
if (_session != null && _player.currentIndex != null) {
return _session!.audioTracks[_player.currentIndex!].startOffset +
_player.position;
}
return Duration.zero;
@ -117,31 +123,61 @@ class HookAudioHandler extends BaseAudioHandler {
//
AudioTrack? get currentTrack {
if (_book == null) {
if (_session == null) {
return null;
}
return _book!.findTrackAtTime(positionInBook);
return _session!.findTrackAtTime(positionInBook);
}
//
BookChapter? get currentChapter {
if (_book == null) {
if (_session == null) {
return null;
}
return _book!.findChapterAtTime(positionInBook);
return _session!.findChapterAtTime(positionInBook);
}
Duration? get chapterDuration => currentChapter?.duration;
Stream<Duration> get positionStream => _player.positionStream;
Stream<Duration> get positionStreamInChapter {
return _player.positionStream.map((position) {
final currentIndex = _player.currentIndex;
if (_session == null || currentIndex == null) {
return Duration.zero;
}
final globalPosition =
position + _session!.audioTracks[currentIndex].startOffset;
final chapter = _session!.findChapterAtTime(globalPosition);
return globalPosition - chapter.start;
});
}
Future<void> togglePlayPause() {
// check if book is set
if (_session == null) {
return Future.value();
}
return switch (_player.playerState) {
PlayerState(playing: var isPlaying) => isPlaying ? pause() : play(),
};
}
//
@override
Future<void> play() => _player.play();
Future<void> play() async {
await _player.play();
}
@override
Future<void> pause() => _player.pause();
Future<void> pause() async {
await _player.pause();
}
// /
@override
Future<void> skipToNext() async {
if (_book == null) {
if (_session == null) {
// 退
return _player.seekToNext();
}
@ -150,17 +186,17 @@ class HookAudioHandler extends BaseAudioHandler {
// 退
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> skipToPrevious() async {
if (_book == null) {
if (_session == null) {
return _player.seekToPrevious();
}
@ -168,14 +204,14 @@ class HookAudioHandler extends BaseAudioHandler {
if (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 {
//
await seekToPosition(Duration.zero);
await seekInBook(Duration.zero);
}
}
@ -188,15 +224,24 @@ class HookAudioHandler extends BaseAudioHandler {
if (track != null) {
startOffset = track.startOffset;
}
await seekToPosition(startOffset + position);
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> seekToPosition(Duration globalPosition) async {
if (_book == null) return;
Future<void> seekInBook(Duration globalPosition) async {
if (_session == null) return;
//
final track = _book!.findTrackAtTime(globalPosition);
final index = _book!.tracks.indexOf(track);
final track = _session!.findTrackAtTime(globalPosition);
final index = _session!.audioTracks.indexOf(track);
Duration positionInTrack = globalPosition - track.startOffset;
if (positionInTrack <= Duration.zero) {
positionInTrack = offset;
@ -205,13 +250,26 @@ class HookAudioHandler extends BaseAudioHandler {
await _player.seek(positionInTrack, index: index);
}
AudioPlayer get player => _player;
PlaybackState _transformEvent(PlaybackEvent event) {
return PlaybackState(
controls: [
MediaControl.skipToPrevious,
if (kIsWeb || !Platform.isAndroid) MediaControl.skipToPrevious,
MediaControl.rewind,
if (_player.playing) MediaControl.pause else MediaControl.play,
MediaControl.skipToNext,
MediaControl.stop,
MediaControl.fastForward,
if (kIsWeb || !Platform.isAndroid) MediaControl.skipToNext,
],
systemActions: {
if (kIsWeb || !Platform.isAndroid) MediaAction.skipToPrevious,
MediaAction.rewind,
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,
@ -225,6 +283,7 @@ class HookAudioHandler extends BaseAudioHandler {
bufferedPosition: _player.bufferedPosition,
speed: _player.speed,
queueIndex: event.currentIndex,
captioningEnabled: false,
);
}
}
@ -246,7 +305,7 @@ Uri _getUri(
Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token');
}
extension BookExpandedExtension on BookExpanded {
extension PlaybackSessionExpandedExtension on PlaybackSessionExpanded {
BookChapter findChapterAtTime(Duration position) {
return chapters.firstWhere(
(element) {
@ -257,16 +316,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,62 +1,62 @@
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';
// 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);
// 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());
// // 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();
// 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,
},
);
},
);
}
// // 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,
// },
// );
// },
// );
// }