293 lines
8.9 KiB
Dart
293 lines
8.9 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:audio_service/audio_service.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:video_player/video_player.dart';
|
|
|
|
import 'audio_file_track.dart';
|
|
import 'audio_playback_controller.dart';
|
|
import 'voice_audio_controller.dart';
|
|
|
|
// #***! что играет сейчас, голосовое кружок или музыка
|
|
enum PlaybackKind { voice, videoNote, audioFile }
|
|
|
|
// #***! играющее голосовое плюс сообщение откуда оно
|
|
class VoiceTrack {
|
|
const VoiceTrack({
|
|
required this.cacheName,
|
|
required this.chatId,
|
|
required this.messageId,
|
|
required this.senderId,
|
|
required this.isMe,
|
|
required this.time,
|
|
required this.audio,
|
|
});
|
|
|
|
final String cacheName;
|
|
final int chatId;
|
|
final String messageId;
|
|
final int senderId;
|
|
final bool isMe;
|
|
final int time;
|
|
final VoiceAudioController audio;
|
|
}
|
|
|
|
// #***! играющий кружок
|
|
class VideoNoteTrack {
|
|
const VideoNoteTrack({
|
|
required this.cacheName,
|
|
required this.chatId,
|
|
required this.messageId,
|
|
required this.senderId,
|
|
required this.isMe,
|
|
required this.time,
|
|
required this.controller,
|
|
required this.preview,
|
|
});
|
|
|
|
final String cacheName;
|
|
final int chatId;
|
|
final String messageId;
|
|
final int senderId;
|
|
final bool isMe;
|
|
final int time;
|
|
final VideoPlayerController controller;
|
|
final Uint8List? preview;
|
|
}
|
|
|
|
// #***! единый диспетчер, одновременно играет только что то одно
|
|
class MediaPlayback {
|
|
MediaPlayback._();
|
|
|
|
static final MediaPlayback instance = MediaPlayback._();
|
|
|
|
// #***! скорости которые перебирает кнопка
|
|
static const List<double> speeds = [1.0, 1.5, 2.0];
|
|
|
|
// #***! primary говорит юишке какую плашку рисовать
|
|
final ValueNotifier<PlaybackKind?> primary = ValueNotifier(null);
|
|
final ValueNotifier<AudioFileTrack?> audioFile = ValueNotifier(null);
|
|
bool _audioCompletionListenerAttached = false;
|
|
|
|
// #***! ушли из чата, играет дальше но плашка меняется
|
|
final ValueNotifier<int?> visibleChatId = ValueNotifier(null);
|
|
|
|
void enterChat(int chatId) => visibleChatId.value = chatId;
|
|
|
|
void leaveChat(int chatId) {
|
|
if (visibleChatId.value == chatId) visibleChatId.value = null;
|
|
}
|
|
|
|
final ValueNotifier<VoiceTrack?> voice = ValueNotifier(null);
|
|
final ValueNotifier<double> voiceSpeed = ValueNotifier(speeds.first);
|
|
|
|
// #***! держатели не дают освободить контроллер пока его рисует виджет,
|
|
// считаем их по головам: один и тот же контроллер могут рисовать сразу
|
|
// два бабла, отпускать его можно только когда ушёл последний
|
|
final Map<VoiceAudioController, int> _heldVoice = {};
|
|
|
|
VoiceAudioController acquireVoice({
|
|
required String cacheName,
|
|
required Future<String?> Function() resolveUrl,
|
|
required Duration fallbackDuration,
|
|
}) {
|
|
final active = voice.value;
|
|
if (active != null && active.cacheName == cacheName) {
|
|
_retain(_heldVoice, active.audio);
|
|
return active.audio;
|
|
}
|
|
final created = VoiceAudioController(
|
|
cacheName: cacheName,
|
|
resolveUrl: resolveUrl,
|
|
fallbackDuration: fallbackDuration,
|
|
);
|
|
_retain(_heldVoice, created);
|
|
return created;
|
|
}
|
|
|
|
void releaseVoice(VoiceAudioController audio) {
|
|
if (_releasedByLastHolder(_heldVoice, audio)) _disposeVoiceIfIdle(audio);
|
|
}
|
|
|
|
static void _retain<T>(Map<T, int> holders, T value) =>
|
|
holders.update(value, (count) => count + 1, ifAbsent: () => 1);
|
|
|
|
static bool _releasedByLastHolder<T>(Map<T, int> holders, T value) {
|
|
final left = (holders[value] ?? 0) - 1;
|
|
if (left > 0) {
|
|
holders[value] = left;
|
|
return false;
|
|
}
|
|
holders.remove(value);
|
|
return true;
|
|
}
|
|
|
|
// #***! включили голосовое, кружок и музыка гаснут
|
|
void activateVoice(VoiceTrack track) {
|
|
_clearAudioFile();
|
|
_clearVideoNote();
|
|
final previous = voice.value;
|
|
if (previous != null && previous.audio != track.audio) {
|
|
previous.audio.pause();
|
|
voice.value = null;
|
|
_disposeVoiceIfIdle(previous.audio);
|
|
}
|
|
voice.value = track;
|
|
primary.value = PlaybackKind.voice;
|
|
track.audio.setSpeed(voiceSpeed.value);
|
|
}
|
|
|
|
// #***! перебор скорости по кругу
|
|
void cycleVoiceSpeed() {
|
|
final next = speeds[(speeds.indexOf(voiceSpeed.value) + 1) % speeds.length];
|
|
voiceSpeed.value = next;
|
|
voice.value?.audio.setSpeed(next);
|
|
}
|
|
|
|
void closeVoice() {
|
|
if (!_clearVoice()) return;
|
|
primary.value = videoNote.value == null ? null : PlaybackKind.videoNote;
|
|
}
|
|
|
|
bool _clearVoice() {
|
|
final track = voice.value;
|
|
if (track == null) return false;
|
|
voice.value = null;
|
|
track.audio.stopAndReset();
|
|
_disposeVoiceIfIdle(track.audio);
|
|
return true;
|
|
}
|
|
|
|
// #***! освобождаем только когда никто не держит и он не активен
|
|
void _disposeVoiceIfIdle(VoiceAudioController audio) {
|
|
if (_heldVoice.containsKey(audio)) return;
|
|
if (voice.value?.audio == audio) return;
|
|
audio.dispose();
|
|
}
|
|
|
|
// #***! то же для кружков
|
|
final ValueNotifier<VideoNoteTrack?> videoNote = ValueNotifier(null);
|
|
final ValueNotifier<double> videoNoteSpeed = ValueNotifier(speeds.first);
|
|
|
|
final Map<VideoPlayerController, int> _heldNotes = {};
|
|
|
|
VideoPlayerController? liveVideoNote(String cacheName) {
|
|
final active = videoNote.value;
|
|
if (active == null || active.cacheName != cacheName) return null;
|
|
_retain(_heldNotes, active.controller);
|
|
return active.controller;
|
|
}
|
|
|
|
void holdVideoNote(VideoPlayerController controller) =>
|
|
_retain(_heldNotes, controller);
|
|
|
|
bool isActiveVideoNote(VideoPlayerController controller) =>
|
|
videoNote.value?.controller == controller;
|
|
|
|
void releaseVideoNote(VideoPlayerController controller) {
|
|
if (_releasedByLastHolder(_heldNotes, controller)) {
|
|
_disposeNoteIfIdle(controller);
|
|
}
|
|
}
|
|
|
|
void activateVideoNote(VideoNoteTrack track) {
|
|
_clearAudioFile();
|
|
_clearVoice();
|
|
final previous = videoNote.value;
|
|
if (previous != null && previous.controller != track.controller) {
|
|
previous.controller.pause();
|
|
videoNote.value = null;
|
|
_disposeNoteIfIdle(previous.controller);
|
|
}
|
|
videoNote.value = track;
|
|
primary.value = PlaybackKind.videoNote;
|
|
track.controller.setPlaybackSpeed(videoNoteSpeed.value);
|
|
}
|
|
|
|
void cycleVideoNoteSpeed() {
|
|
final index = speeds.indexOf(videoNoteSpeed.value);
|
|
final next = speeds[(index + 1) % speeds.length];
|
|
videoNoteSpeed.value = next;
|
|
videoNote.value?.controller.setPlaybackSpeed(next);
|
|
}
|
|
|
|
void closeVideoNote() {
|
|
if (!_clearVideoNote()) return;
|
|
primary.value = voice.value == null ? null : PlaybackKind.voice;
|
|
}
|
|
|
|
bool _clearVideoNote() {
|
|
final track = videoNote.value;
|
|
if (track == null) return false;
|
|
track.controller.pause();
|
|
track.controller.seekTo(Duration.zero);
|
|
videoNote.value = null;
|
|
_disposeNoteIfIdle(track.controller);
|
|
return true;
|
|
}
|
|
|
|
void _disposeNoteIfIdle(VideoPlayerController controller) {
|
|
if (_heldNotes.containsKey(controller)) return;
|
|
if (videoNote.value?.controller == controller) return;
|
|
controller.dispose();
|
|
}
|
|
|
|
Future<void> activateAudioFile(
|
|
AudioFileTrack track, {
|
|
required String notificationChannelName,
|
|
}) async {
|
|
final audio = await AudioPlaybackController.ensureInitialized(
|
|
notificationChannelName,
|
|
);
|
|
_attachAudioCompletionListener(audio);
|
|
final current = audioFile.value;
|
|
if (current?.cacheName == track.cacheName) {
|
|
await audio.toggle();
|
|
return;
|
|
}
|
|
_clearVoice();
|
|
_clearVideoNote();
|
|
audioFile.value = track;
|
|
primary.value = PlaybackKind.audioFile;
|
|
try {
|
|
await audio.playTrack(track);
|
|
} catch (_) {
|
|
audioFile.value = null;
|
|
primary.value = null;
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
void closeAudioFile() {
|
|
if (!_clearAudioFile()) return;
|
|
primary.value = voice.value != null
|
|
? PlaybackKind.voice
|
|
: videoNote.value != null
|
|
? PlaybackKind.videoNote
|
|
: null;
|
|
}
|
|
|
|
bool _clearAudioFile() {
|
|
if (audioFile.value == null) return false;
|
|
audioFile.value = null;
|
|
if (AudioPlaybackController.isInitialized) {
|
|
unawaited(AudioPlaybackController.instance.stop());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void _attachAudioCompletionListener(AudioPlaybackController audio) {
|
|
if (_audioCompletionListenerAttached) return;
|
|
_audioCompletionListenerAttached = true;
|
|
audio.processingState.addListener(_onAudioProcessingStateChanged);
|
|
}
|
|
|
|
void _onAudioProcessingStateChanged() {
|
|
if (AudioPlaybackController.instance.processingState.value !=
|
|
AudioProcessingState.completed) {
|
|
return;
|
|
}
|
|
closeAudioFile();
|
|
}
|
|
}
|