Add web audio player implementation with enhanced stream handling and error management

This commit is contained in:
Leon Bösche
2026-01-17 02:40:58 +01:00
parent d9a651b375
commit 8a1660b781
2 changed files with 189 additions and 29 deletions

View File

@@ -1,8 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'dart:async';
import '../theme/app_theme.dart';
import '../theme/modern_glass_button.dart';
import 'package:just_audio/just_audio.dart';
// Conditional imports for audio
import 'package:just_audio/just_audio.dart'
if (dart.library.html) 'web_audio_player.dart';
class AudioPlayerBar extends StatefulWidget {
final String fileName;
@@ -28,6 +33,10 @@ class _AudioPlayerBarState extends State<AudioPlayerBar>
Duration _position = Duration.zero;
bool _isPlaying = false;
bool _isLoading = true;
StreamSubscription? _positionSubscription;
StreamSubscription? _durationSubscription;
StreamSubscription? _playingSubscription;
StreamSubscription? _errorSubscription;
@override
void initState() {
@@ -43,6 +52,10 @@ class _AudioPlayerBarState extends State<AudioPlayerBar>
@override
void dispose() {
_iconController.dispose();
_positionSubscription?.cancel();
_durationSubscription?.cancel();
_playingSubscription?.cancel();
_errorSubscription?.cancel();
_audioPlayer.dispose();
super.dispose();
}
@@ -54,43 +67,95 @@ class _AudioPlayerBarState extends State<AudioPlayerBar>
print('[AudioPlayerBar] Loading audio URL: ${widget.fileUrl}');
try {
await _audioPlayer.setUrl(widget.fileUrl);
// Wait until duration is available
_audioPlayer.durationStream.firstWhere((d) => d != null).then((d) async {
setState(() {
_duration = d ?? Duration.zero;
_isLoading = false;
if (kIsWeb) {
// Web implementation
_durationSubscription = _audioPlayer.durationStream.listen((d) {
if (d != null) {
setState(() {
_duration = d;
_isLoading = false;
});
}
});
try {
await _audioPlayer.play(); // Start playback automatically
if (_audioPlayer.playerState.playing) {
_positionSubscription = _audioPlayer.positionStream.listen((pos) {
setState(() {
_position = pos;
});
});
_playingSubscription = _audioPlayer.playingStream.listen((playing) {
setState(() {
_isPlaying = playing;
});
if (playing) {
if (mounted) _iconController.forward();
} else {
setState(() {
_errorMsg = 'Audio could not be played.';
});
// ignore: avoid_print
print('[AudioPlayerBar] ERROR: Audio could not be played.');
if (mounted) _iconController.reverse();
}
} catch (e, st) {
});
_errorSubscription = _audioPlayer.errorStream.listen((error) {
setState(() {
_errorMsg =
'Audio playback error: ' +
(e is Exception ? e.toString() : 'Unknown error');
_errorMsg = error.toString();
_isLoading = false;
});
// ignore: avoid_print
print('[AudioPlayerBar] Playback error: $e\n$st');
}
});
_audioPlayer.positionStream.listen((pos) {
setState(() {
_position = pos;
print('[AudioPlayerBar] Web audio error: $error');
});
});
_audioPlayer.playerStateStream.listen((state) {
setState(() {
_isPlaying = state.playing;
// Auto-play for web
await _audioPlayer.play();
} else {
// Mobile implementation (just_audio)
_audioPlayer.durationStream.firstWhere((d) => d != null).then((
d,
) async {
setState(() {
_duration = d ?? Duration.zero;
_isLoading = false;
});
try {
await _audioPlayer.play(); // Start playback automatically
// For mobile, check playing state after a short delay
Future.delayed(const Duration(milliseconds: 100), () {
if (_audioPlayer.playerState.playing) {
if (mounted) _iconController.forward();
} else {
setState(() {
_errorMsg = 'Audio could not be played.';
});
// ignore: avoid_print
print('[AudioPlayerBar] ERROR: Audio could not be played.');
}
});
} catch (e, st) {
setState(() {
_errorMsg =
'Audio playback error: ' +
(e is Exception ? e.toString() : 'Unknown error');
});
// ignore: avoid_print
print('[AudioPlayerBar] Playback error: $e\n$st');
}
});
});
_audioPlayer.positionStream.listen((pos) {
setState(() {
_position = pos;
});
});
_audioPlayer.playerStateStream.listen((state) {
setState(() {
_isPlaying = state.playing;
});
if (state.playing) {
if (mounted) _iconController.forward();
} else {
if (mounted) _iconController.reverse();
}
});
}
} catch (e, st) {
setState(() {
_isLoading = false;
@@ -122,6 +187,9 @@ class _AudioPlayerBarState extends State<AudioPlayerBar>
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: 48,
width:
MediaQuery.of(context).size.width *
0.65, // Reduce width by 35% (100% - 35% = 65%)
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: AppTheme.glassDecoration.copyWith(
borderRadius: BorderRadius.circular(16),

View File

@@ -0,0 +1,92 @@
import 'package:web/web.dart' as web;
import 'dart:async';
class AudioPlayer {
web.HTMLAudioElement? _audioElement;
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();
Stream<Duration> get positionStream => _positionController.stream;
Stream<Duration> get durationStream => _durationController.stream;
Stream<bool> get playingStream => _playingController.stream;
Stream<String> get errorStream => _errorController.stream;
Future<void> setUrl(String url) async {
try {
_audioElement = web.HTMLAudioElement();
_audioElement!.src = url;
_audioElement!.crossOrigin = 'anonymous'; // Handle CORS
// Set up event listeners
_audioElement!.onLoadedMetadata.listen((_) {
_durationController.add(
Duration(milliseconds: (_audioElement!.duration * 1000).toInt()),
);
});
_audioElement!.onTimeUpdate.listen((_) {
_positionController.add(
Duration(milliseconds: (_audioElement!.currentTime * 1000).toInt()),
);
});
_audioElement!.onPlay.listen((_) {
_playingController.add(true);
});
_audioElement!.onPause.listen((_) {
_playingController.add(false);
});
_audioElement!.onEnded.listen((_) {
_playingController.add(false);
});
_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() {
_audioElement?.pause();
_audioElement = null;
_positionController.close();
_durationController.close();
_playingController.close();
_errorController.close();
}
}