142 lines
4.3 KiB
Dart
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();
|
|
}
|
|
}
|