Vaani/lib/features/player/providers/session_provider.dart

189 lines
6 KiB
Dart
Raw Normal View History

2025-11-19 17:43:04 +08:00
import 'package:audio_service/audio_service.dart';
import 'package:http/http.dart' as http;
import 'package:just_audio_media_kit/just_audio_media_kit.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' as core;
import 'package:vaani/api/api_provider.dart';
import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/features/downloads/providers/download_manager.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
2025-11-22 15:54:29 +08:00
import 'package:vaani/features/playback_reporting/core/playback_reporter_session.dart'
as core;
2025-11-19 17:43:04 +08:00
import 'package:vaani/features/player/core/audiobook_player_session.dart';
2025-11-22 15:54:29 +08:00
import 'package:vaani/features/player/providers/player_status_provider.dart';
2025-11-19 17:43:04 +08:00
import 'package:vaani/globals.dart';
import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/obfuscation.dart';
2025-11-23 01:25:57 +08:00
import 'package:vaani/shared/utils/utils.dart';
2025-11-19 17:43:04 +08:00
part 'session_provider.g.dart';
2025-11-22 15:54:29 +08:00
@Riverpod(keepAlive: true)
Future<AbsAudioHandler> audioHandlerInit(Ref ref) async {
2025-11-23 01:25:57 +08:00
if (Utils.isWindows() || Utils.isLinux()) {
// JustAudioMediaKit.ensureInitialized(windows: false);
JustAudioMediaKit.ensureInitialized();
}
2025-11-22 15:54:29 +08:00
final audioService = await AudioService.init(
builder: () => AbsAudioHandler(ref),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.vaani.rang.channel.audio',
androidNotificationChannelName: 'ABSPlayback',
androidNotificationChannelDescription:
'Needed to control audio from lock screen',
androidNotificationOngoing: false,
androidStopForegroundOnPause: false,
androidNotificationIcon: 'drawable/ic_stat_logo',
preloadArtwork: true,
),
);
return audioService;
}
@Riverpod(keepAlive: true)
class Player extends _$Player {
@override
AbsAudioHandler build() {
return ref.watch(audioHandlerInitProvider).requireValue;
}
}
@Riverpod(keepAlive: true)
class Session extends _$Session {
@override
core.PlaybackSessionExpanded? build() {
return null;
2025-11-19 17:43:04 +08:00
}
Future<void> load(String id, String? episodeId) async {
2025-11-22 15:54:29 +08:00
final audioService = ref.read(playerProvider);
await audioService.pause();
ref.read(playerStatusProvider.notifier).setLoading(id);
2025-11-19 17:43:04 +08:00
final api = ref.read(authenticatedApiProvider);
final playBack = await api.items.play(
libraryItemId: id,
parameters: core.PlayItemReqParams(
deviceInfo: core.DeviceInfoReqParams(
clientVersion: appVersion,
manufacturer: deviceManufacturer,
model: deviceModel,
sdkVersion: deviceSdkVersion,
clientName: appName,
deviceName: deviceName,
),
forceDirectPlay: false,
forceTranscode: false,
supportedMimeTypes: [
"audio/flac",
"audio/mpeg",
"audio/mp4",
"audio/ogg",
"audio/aac",
"audio/webm",
],
),
responseErrorHandler: _responseErrorHandler,
) as core.PlaybackSessionExpanded;
2025-11-22 15:54:29 +08:00
state = playBack;
2025-11-19 17:43:04 +08:00
final downloadManager = ref.read(simpleDownloadManagerProvider);
final libItem =
await ref.read(libraryItemProvider(playBack.libraryItemId).future);
final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
var bookPlayerSettings =
ref.read(bookSettingsProvider(playBack.libraryItemId)).playerSettings;
var appPlayerSettings = ref.read(appSettingsProvider).playerSettings;
var configurePlayerForEveryBook =
appPlayerSettings.configurePlayerForEveryBook;
await Future.wait([
2025-11-22 15:54:29 +08:00
audioService.setSourceAudiobook(
2025-11-19 17:43:04 +08:00
playBack,
baseUrl: api.baseUrl,
token: api.token!,
downloadedUris: downloadedUris,
),
// set the volume
2025-11-22 15:54:29 +08:00
audioService.setVolume(
2025-11-19 17:43:04 +08:00
configurePlayerForEveryBook
? bookPlayerSettings.preferredDefaultVolume ??
appPlayerSettings.preferredDefaultVolume
: appPlayerSettings.preferredDefaultVolume,
),
// set the speed
2025-11-22 15:54:29 +08:00
audioService.setSpeed(
2025-11-19 17:43:04 +08:00
configurePlayerForEveryBook
? bookPlayerSettings.preferredDefaultSpeed ??
appPlayerSettings.preferredDefaultSpeed
: appPlayerSettings.preferredDefaultSpeed,
),
]);
}
void _responseErrorHandler(http.Response response, [error]) {
if (response.statusCode != 200) {
appLogger.severe('Error with api: ${response.obfuscate()}, $error');
throw PlaybackSyncError(
'Error syncing position: ${response.body}, $error',
);
}
}
}
@Riverpod(keepAlive: true)
2025-11-22 15:54:29 +08:00
class CurrentChapter extends _$CurrentChapter {
2025-11-19 17:43:04 +08:00
@override
2025-11-22 15:54:29 +08:00
core.BookChapter? build() {
final player = ref.watch(playerProvider);
player.chapterStream.distinct().listen((chapter) {
update(chapter);
});
return player.currentChapter;
2025-11-19 17:43:04 +08:00
}
2025-11-22 15:54:29 +08:00
void update(core.BookChapter? chapter) {
if (state != chapter) {
state = chapter;
}
2025-11-19 17:43:04 +08:00
}
}
@Riverpod(keepAlive: true)
2025-11-22 15:54:29 +08:00
class PlaybackReporter extends _$PlaybackReporter {
2025-11-19 17:43:04 +08:00
@override
2025-11-22 15:54:29 +08:00
Future<core.PlaybackReporter?> build() async {
final session = ref.watch(sessionProvider);
if (session == null) {
return null;
}
final playerSettings = ref.watch(appSettingsProvider).playerSettings;
final player = ref.watch(playerProvider);
final api = ref.watch(authenticatedApiProvider);
final reporter = core.PlaybackReporter(
player,
api,
reportingInterval: playerSettings.playbackReportInterval,
markCompleteWhenTimeLeft: playerSettings.markCompleteWhenTimeLeft,
minimumPositionForReporting: playerSettings.minimumPositionForReporting,
session: session,
);
ref.onDispose(reporter.dispose);
return reporter;
2025-11-19 17:43:04 +08:00
}
}
class PlaybackSyncError implements Exception {
String message;
PlaybackSyncError([this.message = 'Error syncing playback']);
@override
String toString() {
return 'PlaybackSyncError: $message';
}
}