didvan-app/lib/services/media/voice.dart

157 lines
5.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}