157 lines
5.8 KiB
Dart
157 lines
5.8 KiB
Dart
import 'package:didvan/services/network/request.dart';
|
||
import 'package:didvan/services/network/request_helper.dart';
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:just_audio/just_audio.dart';
|
||
import 'package:audio_session/audio_session.dart';
|
||
|
||
class MyJABytesSource extends StreamAudioSource {
|
||
final Uint8List _buffer;
|
||
|
||
MyJABytesSource(this._buffer) : super(tag: 'MyAudioSource');
|
||
|
||
@override
|
||
Future<StreamAudioResponse> request([int? start, int? end]) async {
|
||
// Returning the stream audio response with the parameters
|
||
return StreamAudioResponse(
|
||
sourceLength: _buffer.length,
|
||
contentLength: (end ?? _buffer.length) - (start ?? 0),
|
||
offset: start ?? 0,
|
||
stream: Stream.fromIterable([_buffer.sublist(start ?? 0, end)]),
|
||
contentType: 'audio/wav',
|
||
);
|
||
}
|
||
}
|
||
|
||
class VoiceService {
|
||
static final audioPlayer = AudioPlayer();
|
||
static String? src;
|
||
static bool _isAudioSessionConfigured = false;
|
||
|
||
static Future<void> _configureAudioSession() async {
|
||
// ⚠️ همیشه reconfigure میکنیم تا اگر ماژول دیگری session را روی
|
||
// voiceCall ست کرده، روی media برگردد. در غیر این صورت صدای TTS از
|
||
// earpiece (نه speaker) پخش میشود.
|
||
try {
|
||
final session = await AudioSession.instance;
|
||
await session.configure(AudioSessionConfiguration(
|
||
// iOS: playback + spokenAudio روتر را به speaker میفرستد.
|
||
avAudioSessionCategory: AVAudioSessionCategory.playback,
|
||
avAudioSessionMode: AVAudioSessionMode.spokenAudio,
|
||
avAudioSessionCategoryOptions:
|
||
AVAudioSessionCategoryOptions.defaultToSpeaker,
|
||
// Android: usage=media + contentType=music → routed to MEDIA
|
||
// speaker (نه earpiece/call).
|
||
androidAudioAttributes: const AndroidAudioAttributes(
|
||
contentType: AndroidAudioContentType.music,
|
||
flags: AndroidAudioFlags.none,
|
||
usage: AndroidAudioUsage.media,
|
||
),
|
||
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
|
||
));
|
||
_isAudioSessionConfigured = true;
|
||
debugPrint('✅ VoiceService AudioSession configured for MEDIA playback');
|
||
} catch (e) {
|
||
debugPrint('❌ Error configuring VoiceService AudioSession: $e');
|
||
}
|
||
}
|
||
|
||
static Future<Duration?> getDuration(
|
||
{required String src, Uint8List? bytes}) async {
|
||
final ap = AudioPlayer();
|
||
Duration? duration;
|
||
try {
|
||
// اگر لینک با http شروع شود، مستقیم استفاده میشود (یک لقمه استراتژی)
|
||
if (src.startsWith('http')) {
|
||
duration = await ap.setUrl(src);
|
||
} else if (src.startsWith('/uploads')) {
|
||
final source =
|
||
'${RequestHelper.baseUrl + src}?accessToken=${RequestService.token}';
|
||
// if (kIsWeb) {
|
||
// final response =
|
||
// await http.get(Uri.parse(source.replaceAll('%3A', ':')));
|
||
// final bytes = response.bodyBytes;
|
||
// final blob = html.Blob([bytes]);
|
||
// final blobUrl = html.Url.createObjectUrlFromBlob(blob);
|
||
// duration = await ap.setAudioSource(
|
||
// AudioSource.uri(Uri.parse(blobUrl)),
|
||
// );
|
||
// } else {
|
||
duration = await ap.setUrl(source);
|
||
// }
|
||
} else if (src.startsWith('blob:') || src == '') {
|
||
duration = await ap.setUrl(src);
|
||
} else {
|
||
duration = await ap.setFilePath(src);
|
||
}
|
||
} catch (e) {
|
||
if (kDebugMode) {
|
||
print('Error setting audio source: $e');
|
||
}
|
||
} finally {
|
||
await ap.dispose();
|
||
}
|
||
|
||
await ap.load();
|
||
|
||
return duration;
|
||
}
|
||
|
||
static Future<void> voiceHelper(
|
||
{required String src,
|
||
final Duration? duration,
|
||
final Function()? startTimer,
|
||
final Function()? stopTimer}) async {
|
||
try {
|
||
// تنظیم AudioSession برای پخش از MEDIA
|
||
await _configureAudioSession();
|
||
|
||
if (VoiceService.src == src) {
|
||
if (audioPlayer.playing) {
|
||
await audioPlayer.pause();
|
||
stopTimer?.call();
|
||
} else {
|
||
await audioPlayer.play();
|
||
startTimer?.call();
|
||
}
|
||
return;
|
||
}
|
||
// ⚠️ stop باید await شود تا قبل از setUrl/setFilePath بهدرستی
|
||
// pipeline پاک شود؛ در غیر این صورت setUrl میتواند fail کند.
|
||
await audioPlayer.stop();
|
||
VoiceService.src = src;
|
||
debugPrint('🔊 voiceHelper src=$src');
|
||
// اگر لینک با http شروع شود، مستقیم استفاده میشود (یک لقمه استراتژی)
|
||
if (src.startsWith('http')) {
|
||
// برای URL های خارجی (مثل kie.ai)، با setAudioSource استفاده
|
||
// میکنیم تا redirect های HTTP بهتر هندل شوند. headers User-Agent
|
||
// را اضافه میکنیم چون بعضی CDN ها بدون UA کلاینت را reject میکنند.
|
||
await audioPlayer.setAudioSource(
|
||
AudioSource.uri(Uri.parse(src), headers: const {
|
||
'User-Agent': 'Mozilla/5.0 (Linux; Android) didvan-app',
|
||
}),
|
||
);
|
||
} else if (src.startsWith('/uploads')) {
|
||
final source =
|
||
'${RequestHelper.baseUrl + src}?accessToken=${RequestService.token}';
|
||
await audioPlayer.setUrl(source.replaceAll('%3A', ''));
|
||
} else if (src.startsWith('blob:') || src == '') {
|
||
await audioPlayer.setUrl(src);
|
||
} else {
|
||
await audioPlayer.setFilePath(src);
|
||
}
|
||
await audioPlayer.play();
|
||
|
||
startTimer?.call();
|
||
} catch (e, st) {
|
||
debugPrint('❌ voiceHelper failed for src=$src: $e');
|
||
debugPrint(st.toString());
|
||
resetVoicePlayer();
|
||
}
|
||
}
|
||
|
||
static Future<void> resetVoicePlayer() async {
|
||
src = null;
|
||
await audioPlayer.stop();
|
||
}
|
||
}
|