242 lines
7.1 KiB
Dart
242 lines
7.1 KiB
Dart
import 'package:audioplayers/audioplayers.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import '../models/spotify_track.dart';
|
|
|
|
class AudioService extends ChangeNotifier {
|
|
static final AudioService _instance = AudioService._internal();
|
|
factory AudioService() => _instance;
|
|
AudioService._internal() {
|
|
_initializePlayer();
|
|
}
|
|
|
|
final AudioPlayer _audioPlayer = AudioPlayer();
|
|
|
|
// Current track state
|
|
SpotifyTrack? _currentTrack;
|
|
bool _isPlaying = false;
|
|
bool _isLoading = false;
|
|
Duration _currentPosition = Duration.zero;
|
|
Duration _totalDuration = Duration.zero;
|
|
|
|
// Getters
|
|
SpotifyTrack? get currentTrack => _currentTrack;
|
|
bool get isPlaying => _isPlaying;
|
|
bool get isLoading => _isLoading;
|
|
Duration get currentPosition => _currentPosition;
|
|
Duration get totalDuration => _totalDuration;
|
|
double get progress => _totalDuration.inMilliseconds > 0
|
|
? _currentPosition.inMilliseconds / _totalDuration.inMilliseconds
|
|
: 0.0;
|
|
|
|
// Free audio sources for demo purposes
|
|
// Using royalty-free music from reliable sources
|
|
final Map<String, String> _demoAudioUrls = {
|
|
// Peaceful, lido-appropriate tracks
|
|
'pop1': 'https://www.bensound.com/bensound-music/bensound-relaxing.mp3',
|
|
'pop2': 'https://www.bensound.com/bensound-music/bensound-sunny.mp3',
|
|
'pop3': 'https://www.bensound.com/bensound-music/bensound-jazzcomedy.mp3',
|
|
'pop4': 'https://www.bensound.com/bensound-music/bensound-acousticbreeze.mp3',
|
|
'1': 'https://www.bensound.com/bensound-music/bensound-creativeminds.mp3',
|
|
'2': 'https://www.bensound.com/bensound-music/bensound-happyrock.mp3',
|
|
'3': 'https://www.bensound.com/bensound-music/bensound-ukulele.mp3',
|
|
'4': 'https://www.bensound.com/bensound-music/bensound-summer.mp3',
|
|
'5': 'https://www.bensound.com/bensound-music/bensound-happiness.mp3',
|
|
};
|
|
|
|
void _initializePlayer() {
|
|
// Listen to player state changes
|
|
_audioPlayer.onPlayerStateChanged.listen((PlayerState state) {
|
|
_isPlaying = state == PlayerState.playing;
|
|
_isLoading = state == PlayerState.stopped && _currentTrack != null;
|
|
notifyListeners();
|
|
});
|
|
|
|
// Listen to position changes
|
|
_audioPlayer.onPositionChanged.listen((Duration position) {
|
|
_currentPosition = position;
|
|
notifyListeners();
|
|
});
|
|
|
|
// Listen to duration changes
|
|
_audioPlayer.onDurationChanged.listen((Duration duration) {
|
|
_totalDuration = duration;
|
|
notifyListeners();
|
|
});
|
|
|
|
// Listen for track completion
|
|
_audioPlayer.onPlayerComplete.listen((_) {
|
|
_onTrackComplete();
|
|
});
|
|
}
|
|
|
|
Future<void> playTrack(SpotifyTrack track) async {
|
|
try {
|
|
_isLoading = true;
|
|
_currentTrack = track;
|
|
notifyListeners();
|
|
|
|
// Check if we have a demo URL for this track
|
|
String? audioUrl = _demoAudioUrls[track.id];
|
|
|
|
if (audioUrl != null) {
|
|
// Play the demo audio
|
|
await _audioPlayer.play(UrlSource(audioUrl));
|
|
print('Playing audio for: ${track.name} by ${track.artistNames}');
|
|
} else {
|
|
// For tracks without demo URLs, simulate playback
|
|
print('Simulating playback for: ${track.name} by ${track.artistNames}');
|
|
_simulateTrackPlayback(track);
|
|
}
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
} catch (e) {
|
|
print('Error playing track: $e');
|
|
_isLoading = false;
|
|
// Fallback to simulation
|
|
_simulateTrackPlayback(track);
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
void _simulateTrackPlayback(SpotifyTrack track) {
|
|
// Set simulated duration
|
|
_totalDuration = Duration(milliseconds: track.durationMs);
|
|
_currentPosition = Duration.zero;
|
|
_isPlaying = true;
|
|
|
|
// Simulate playback progress
|
|
_startSimulatedProgress();
|
|
}
|
|
|
|
void _startSimulatedProgress() {
|
|
if (_isPlaying && _currentTrack != null) {
|
|
Future.delayed(const Duration(seconds: 1), () {
|
|
if (_isPlaying && _currentTrack != null) {
|
|
_currentPosition = _currentPosition + const Duration(seconds: 1);
|
|
|
|
if (_currentPosition >= _totalDuration) {
|
|
_onTrackComplete();
|
|
} else {
|
|
notifyListeners();
|
|
_startSimulatedProgress();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> togglePlayPause() async {
|
|
try {
|
|
if (_isPlaying) {
|
|
await _audioPlayer.pause();
|
|
} else {
|
|
if (_currentTrack != null) {
|
|
// Check if we have a real audio URL
|
|
String? audioUrl = _demoAudioUrls[_currentTrack!.id];
|
|
if (audioUrl != null) {
|
|
await _audioPlayer.resume();
|
|
} else {
|
|
// Resume simulation
|
|
_isPlaying = true;
|
|
_startSimulatedProgress();
|
|
}
|
|
}
|
|
}
|
|
notifyListeners();
|
|
} catch (e) {
|
|
print('Error toggling play/pause: $e');
|
|
// Fallback to simulation toggle
|
|
_isPlaying = !_isPlaying;
|
|
if (_isPlaying) {
|
|
_startSimulatedProgress();
|
|
}
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
Future<void> stop() async {
|
|
try {
|
|
await _audioPlayer.stop();
|
|
} catch (e) {
|
|
print('Error stopping audio: $e');
|
|
}
|
|
|
|
_isPlaying = false;
|
|
_currentPosition = Duration.zero;
|
|
_currentTrack = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> seekTo(Duration position) async {
|
|
try {
|
|
// Check if we have a real audio URL
|
|
if (_currentTrack != null && _demoAudioUrls.containsKey(_currentTrack!.id)) {
|
|
await _audioPlayer.seek(position);
|
|
} else {
|
|
// Simulate seeking
|
|
_currentPosition = position;
|
|
notifyListeners();
|
|
}
|
|
} catch (e) {
|
|
print('Error seeking: $e');
|
|
// Fallback to simulation
|
|
_currentPosition = position;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
void _onTrackComplete() {
|
|
_isPlaying = false;
|
|
_currentPosition = Duration.zero;
|
|
notifyListeners();
|
|
|
|
// Notify that track is complete (for queue management)
|
|
onTrackComplete?.call();
|
|
}
|
|
|
|
// Callback for when a track completes
|
|
Function()? onTrackComplete;
|
|
|
|
@override
|
|
void dispose() {
|
|
_audioPlayer.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// Get formatted time strings
|
|
String get currentPositionString => _formatDuration(_currentPosition);
|
|
String get totalDurationString => _formatDuration(_totalDuration);
|
|
|
|
String _formatDuration(Duration duration) {
|
|
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
|
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
|
|
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
|
|
return '$twoDigitMinutes:$twoDigitSeconds';
|
|
}
|
|
|
|
// Add better demo audio URLs (using royalty-free sources)
|
|
void addDemoAudioUrl(String trackId, String audioUrl) {
|
|
_demoAudioUrls[trackId] = audioUrl;
|
|
}
|
|
|
|
// Add local asset support
|
|
Future<void> playAsset(SpotifyTrack track, String assetPath) async {
|
|
try {
|
|
_isLoading = true;
|
|
_currentTrack = track;
|
|
notifyListeners();
|
|
|
|
await _audioPlayer.play(AssetSource(assetPath));
|
|
print('Playing asset: $assetPath for ${track.name}');
|
|
|
|
_isLoading = false;
|
|
notifyListeners();
|
|
} catch (e) {
|
|
print('Error playing asset: $e');
|
|
_isLoading = false;
|
|
_simulateTrackPlayback(track);
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|