Files
b0esche_cloud/b0esche_cloud/lib/widgets/web_audio_player.dart
2026-01-25 01:37:22 +01:00

142 lines
4.3 KiB
Dart

import 'package:web/web.dart' as web;
import 'dart:async';
import 'dart:typed_data';
import '../services/api_client.dart';
import '../injection.dart';
class AudioPlayer {
web.HTMLAudioElement? _audioElement;
String? _blobUrl;
final StreamController<Duration> _positionController =
StreamController<Duration>.broadcast();
final StreamController<Duration> _durationController =
StreamController<Duration>.broadcast();
final StreamController<bool> _playingController =
StreamController<bool>.broadcast();
final StreamController<String> _errorController =
StreamController<String>.broadcast();
// Store subscriptions for cleanup
StreamSubscription? _durationSubscription;
StreamSubscription? _positionSubscription;
StreamSubscription? _playSubscription;
StreamSubscription? _pauseSubscription;
StreamSubscription? _endedSubscription;
StreamSubscription? _errorSubscription;
Stream<Duration> get positionStream => _positionController.stream;
Stream<Duration> get durationStream => _durationController.stream;
Stream<bool> get playingStream => _playingController.stream;
Stream<String> get errorStream => _errorController.stream;
void _disposeSubscriptions() {
_durationSubscription?.cancel();
_positionSubscription?.cancel();
_playSubscription?.cancel();
_pauseSubscription?.cancel();
_endedSubscription?.cancel();
_errorSubscription?.cancel();
_durationSubscription = null;
_positionSubscription = null;
_playSubscription = null;
_pauseSubscription = null;
_endedSubscription = null;
_errorSubscription = null;
}
Future<void> setUrl(String url, {String? mimeType}) async {
// Clean up any existing subscriptions
_disposeSubscriptions();
try {
final apiClient = getIt<ApiClient>();
final path = url.replaceFirst(apiClient.baseUrl, '');
final bytes = await apiClient.getBytes(path);
final blob = web.Blob(
[Uint8List.fromList(bytes)] as dynamic,
web.BlobPropertyBag(type: mimeType ?? 'audio/mpeg'),
);
final blobUrl = web.URL.createObjectURL(blob);
_audioElement = web.HTMLAudioElement();
_audioElement!.src = blobUrl;
_audioElement!.crossOrigin = 'anonymous'; // Handle CORS
// Set up event listeners and store subscriptions
_durationSubscription = _audioElement!.onLoadedMetadata.listen((_) {
if (_audioElement != null) {
_durationController.add(
Duration(milliseconds: (_audioElement!.duration * 1000).toInt()),
);
}
});
_positionSubscription = _audioElement!.onTimeUpdate.listen((_) {
if (_audioElement != null) {
_positionController.add(
Duration(milliseconds: (_audioElement!.currentTime * 1000).toInt()),
);
}
});
_playSubscription = _audioElement!.onPlay.listen((_) {
_playingController.add(true);
});
_pauseSubscription = _audioElement!.onPause.listen((_) {
_playingController.add(false);
});
_endedSubscription = _audioElement!.onEnded.listen((_) {
_playingController.add(false);
});
_errorSubscription = _audioElement!.onError.listen((_) {
_errorController.add('Failed to load audio');
});
// Load the audio
_audioElement!.load();
} catch (e) {
_errorController.add('Error initializing audio: $e');
}
}
Future<void> play() async {
try {
if (_audioElement != null) {
// The play() method returns a JSPromise, but we can call it without await
// since we're not depending on the promise resolution for our logic
_audioElement!.play();
}
} catch (e) {
_errorController.add('Error playing audio: $e');
}
}
Future<void> pause() async {
_audioElement?.pause();
}
Future<void> seek(Duration position) async {
if (_audioElement != null) {
_audioElement!.currentTime = position.inMilliseconds / 1000;
}
}
void dispose() {
_disposeSubscriptions();
_audioElement?.pause();
if (_blobUrl != null) {
web.URL.revokeObjectURL(_blobUrl!);
_blobUrl = null;
}
_audioElement = null;
_positionController.close();
_durationController.close();
_playingController.close();
_errorController.close();
}
}