Add web audio player implementation with enhanced stream handling and error management
This commit is contained in:
@@ -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),
|
||||
|
||||
92
b0esche_cloud/lib/widgets/web_audio_player.dart
Normal file
92
b0esche_cloud/lib/widgets/web_audio_player.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user