feat/refactor: добавил индикатор при просмотре кружков/гс, сделал запись кружков нормальным членом общества
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'voice_audio_controller.dart';
|
||||
|
||||
enum PlaybackKind { voice, videoNote }
|
||||
|
||||
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];
|
||||
|
||||
final ValueNotifier<PlaybackKind?> primary = ValueNotifier(null);
|
||||
|
||||
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 Set<VoiceAudioController> _heldVoice = {};
|
||||
|
||||
VoiceAudioController acquireVoice({
|
||||
required String cacheName,
|
||||
required Future<String?> Function() resolveUrl,
|
||||
required Duration fallbackDuration,
|
||||
}) {
|
||||
final active = voice.value;
|
||||
if (active != null && active.cacheName == cacheName) {
|
||||
_heldVoice.add(active.audio);
|
||||
return active.audio;
|
||||
}
|
||||
final created = VoiceAudioController(
|
||||
cacheName: cacheName,
|
||||
resolveUrl: resolveUrl,
|
||||
fallbackDuration: fallbackDuration,
|
||||
);
|
||||
_heldVoice.add(created);
|
||||
return created;
|
||||
}
|
||||
|
||||
void releaseVoice(VoiceAudioController audio) {
|
||||
_heldVoice.remove(audio);
|
||||
_disposeVoiceIfIdle(audio);
|
||||
}
|
||||
|
||||
void activateVoice(VoiceTrack track) {
|
||||
_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.contains(audio)) return;
|
||||
if (voice.value?.audio == audio) return;
|
||||
audio.dispose();
|
||||
}
|
||||
|
||||
final ValueNotifier<VideoNoteTrack?> videoNote = ValueNotifier(null);
|
||||
final ValueNotifier<double> videoNoteSpeed = ValueNotifier(speeds.first);
|
||||
|
||||
final Set<VideoPlayerController> _heldNotes = {};
|
||||
|
||||
VideoPlayerController? liveVideoNote(String cacheName) {
|
||||
final active = videoNote.value;
|
||||
if (active == null || active.cacheName != cacheName) return null;
|
||||
_heldNotes.add(active.controller);
|
||||
return active.controller;
|
||||
}
|
||||
|
||||
void holdVideoNote(VideoPlayerController controller) =>
|
||||
_heldNotes.add(controller);
|
||||
|
||||
bool isActiveVideoNote(VideoPlayerController controller) =>
|
||||
videoNote.value?.controller == controller;
|
||||
|
||||
void releaseVideoNote(VideoPlayerController controller) {
|
||||
_heldNotes.remove(controller);
|
||||
_disposeNoteIfIdle(controller);
|
||||
}
|
||||
|
||||
void activateVideoNote(VideoNoteTrack track) {
|
||||
_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;
|
||||
videoNote.value = null;
|
||||
track.controller.pause();
|
||||
track.controller.seekTo(Duration.zero);
|
||||
_disposeNoteIfIdle(track.controller);
|
||||
return true;
|
||||
}
|
||||
|
||||
void _disposeNoteIfIdle(VideoPlayerController controller) {
|
||||
if (_heldNotes.contains(controller)) return;
|
||||
if (videoNote.value?.controller == controller) return;
|
||||
controller.dispose();
|
||||
}
|
||||
}
|
||||
@@ -13,8 +13,19 @@ class NativeVideoNoteRecorder {
|
||||
static const _channel = MethodChannel('ru.komet.app/video_note');
|
||||
|
||||
int? textureId;
|
||||
bool hasFlash = false;
|
||||
bool get isAvailable => Platform.isAndroid;
|
||||
|
||||
Future<bool> requestPermission() async {
|
||||
if (!isAvailable) return false;
|
||||
try {
|
||||
return await _channel.invokeMethod<bool>('permission') ?? false;
|
||||
} catch (e) {
|
||||
logger.w('NativeVideoNoteRecorder.requestPermission: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> init({bool front = true, int size = 480, int fps = 30}) async {
|
||||
if (!isAvailable) return false;
|
||||
try {
|
||||
@@ -24,6 +35,7 @@ class NativeVideoNoteRecorder {
|
||||
'fps': fps,
|
||||
});
|
||||
textureId = res?['textureId'] as int?;
|
||||
hasFlash = res?['hasFlash'] as bool? ?? false;
|
||||
return textureId != null;
|
||||
} catch (e) {
|
||||
logger.w('NativeVideoNoteRecorder.init: $e');
|
||||
@@ -42,6 +54,16 @@ class NativeVideoNoteRecorder {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> setTorch(bool on) async {
|
||||
if (!isAvailable || !hasFlash) return false;
|
||||
try {
|
||||
return await _channel.invokeMethod<bool>('torch', {'on': on}) ?? false;
|
||||
} catch (e) {
|
||||
logger.w('NativeVideoNoteRecorder.setTorch: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> start() async {
|
||||
if (!isAvailable) return false;
|
||||
try {
|
||||
@@ -69,5 +91,6 @@ class NativeVideoNoteRecorder {
|
||||
await _channel.invokeMethod('dispose');
|
||||
} catch (_) {}
|
||||
textureId = null;
|
||||
hasFlash = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ class VoiceAudioController {
|
||||
Timer? _ticker;
|
||||
Future<void>? _loading;
|
||||
double _sliceOffset = 0;
|
||||
double _speed = 1;
|
||||
int _startGeneration = 0;
|
||||
bool _scrubbing = false;
|
||||
bool _resumeAfterScrub = false;
|
||||
@@ -85,6 +86,7 @@ class VoiceAudioController {
|
||||
final player = _player;
|
||||
if (player != null) {
|
||||
player.play();
|
||||
_applySpeed();
|
||||
playing.value = true;
|
||||
_startTicker();
|
||||
return;
|
||||
@@ -104,6 +106,31 @@ class VoiceAudioController {
|
||||
_stopTicker();
|
||||
}
|
||||
|
||||
void setSpeed(double speed) {
|
||||
if (_disposed) return;
|
||||
_speed = speed;
|
||||
_applySpeed();
|
||||
}
|
||||
|
||||
void stopAndReset() {
|
||||
if (_disposed) return;
|
||||
pause();
|
||||
_disposePlayer();
|
||||
_finished = false;
|
||||
_sliceOffset = 0;
|
||||
position.value = 0;
|
||||
}
|
||||
|
||||
void _applySpeed() {
|
||||
final player = _player;
|
||||
if (player == null) return;
|
||||
try {
|
||||
player.setPlaybackRate(_speed);
|
||||
} catch (e) {
|
||||
logger.w('VoiceAudioController.setSpeed($cacheName): $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> seekTo(double seconds) async {
|
||||
scrubStart();
|
||||
scrubTo(seconds);
|
||||
@@ -239,6 +266,7 @@ class VoiceAudioController {
|
||||
_player = player;
|
||||
player.state.addListener(_onPlayerState);
|
||||
player.play();
|
||||
_applySpeed();
|
||||
playing.value = true;
|
||||
_startTicker();
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user