[READ ONLY MIRROR] Open Source TikTok alternative built on AT Protocol github.com/sprksocial/client
flutter atproto video dart
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482import 'dart:async';
import 'package:flutter_test/flutter_test.dart';import 'package:pro_image_editor/pro_image_editor.dart';import 'package:spark/src/features/sound/controllers/audio_audition_controller.dart';import 'package:spark/src/features/sound/controllers/audio_audition_playback.dart';
void main() { test( 'owns the picker, range, and commit phases as one transaction', () async { final playback = _FakeAudioPlayback(); final waveform = Completer<List<double>>(); final commits = <AudioAuditionResult>[]; final controller = AudioAuditionController( playback, (_) => waveform.future, commits.add, (_, _, _) {}, ); addTearDown(controller.dispose); final hostSpan = _span(2, 12); final track = _track('selected');
await controller.beginPicker(previousTrack: null, hostSpan: hostSpan); expect(controller.state, isA<AudioPickerAuditionState>()); expect(await controller.selectPickerTrack(track), isTrue); expect(controller.confirmPicker(), isTrue); expect(controller.rangeState?.draft.audioStartTime, Duration.zero); expect( controller.rangeState?.draft.audioEndTime, const Duration(seconds: 10), ); await Future<void>.delayed(Duration.zero); expect(playback.preparedSpans, [hostSpan]); expect(playback.startCount, 1);
waveform.complete([0.2, 0.8]); await Future<void>.delayed(Duration.zero); expect(controller.rangeState?.waveform, [0.2, 0.8]);
expect(controller.finish(const Duration(seconds: 4)), isTrue);
expect(commits.single.track.audioStartTime, const Duration(seconds: 4)); expect(commits.single.waveform, [0.2, 0.8]); expect(controller.state, isNull); }, );
test('does not replace an active audition session', () async { final playback = _FakeAudioPlayback(); final waveforms = <String, Completer<List<double>>>{}; final controller = AudioAuditionController( playback, (track) => (waveforms[track.id] = Completer<List<double>>()).future, (_) {}, (_, _, _) {}, ); addTearDown(controller.dispose); final span = _span(0, 10);
expect( controller.beginAdjustment( track: _track('first'), hostSpan: span, waveform: const [], ), isTrue, ); expect( controller.beginAdjustment( track: _track('second'), hostSpan: span, waveform: const [], ), isFalse, );
expect(controller.rangeState?.draft.id, 'first'); expect(waveforms, contains('first')); expect(waveforms, isNot(contains('second')));
waveforms['first']!.complete([0.1]); await Future<void>.delayed(Duration.zero); expect(controller.rangeState?.waveform, [0.1]); });
test('does not start a second picker while the first is pausing', () async { final pause = Completer<void>(); final playback = _FakeAudioPlayback(onPause: () => pause.future); final controller = AudioAuditionController( playback, (_) async => [0.5], (_) {}, (_, _, _) {}, ); addTearDown(controller.dispose); final first = controller.beginPicker( previousTrack: null, hostSpan: _span(0, 10), );
expect( await controller.beginPicker(previousTrack: null, hostSpan: _span(1, 11)), isFalse, ); pause.complete(); expect(await first, isTrue); expect(controller.state?.hostSpan, _span(0, 10)); });
test('keeps cancellation restoring until the prior track is ready', () async { final restore = Completer<void>(); final playback = _FakeAudioPlayback(onRestore: () => restore.future); final controller = AudioAuditionController( playback, (_) async => [0.5], (_) {}, (_, _, _) {}, ); addTearDown(controller.dispose); final previousTrack = _track('previous'); final hostSpan = _span(0, 12); controller.beginAdjustment( track: previousTrack, hostSpan: hostSpan, waveform: const [0.5], );
final cancellation = controller.cancel(); expect(controller.state, isA<AudioAuditionRestoringState>()); expect(controller.state?.blocksHostInteraction, isTrue);
restore.complete(); await cancellation; expect(controller.state, isNull); expect(playback.restoredTracks, [same(previousTrack)]); });
test( 'stale preview preparation cannot start playback after cancel', () async { final prepare = Completer<void>(); final playback = _FakeAudioPlayback(onPrepare: () => prepare.future); final controller = AudioAuditionController( playback, (_) async => [0.5], (_) {}, (_, _, _) {}, ); addTearDown(controller.dispose); controller.beginAdjustment( track: _track('selected'), hostSpan: _span(0, 10), waveform: const [0.5], ); await Future<void>.delayed(Duration.zero);
final cancellation = controller.cancel(); prepare.complete(); await cancellation; await Future<void>.delayed(Duration.zero);
expect(playback.startCount, 0); expect(controller.state, isNull); }, );
test('waveform completion does not cancel current preparation', () async { final prepare = Completer<void>(); final waveform = Completer<List<double>>(); final playback = _FakeAudioPlayback(onPrepare: () => prepare.future); final controller = AudioAuditionController( playback, (_) => waveform.future, (_) {}, (_, _, _) {}, ); addTearDown(controller.dispose); controller.beginAdjustment( track: _track('selected'), hostSpan: _span(0, 10), waveform: const [], ); await Future<void>.delayed(Duration.zero);
waveform.complete([0.4]); await Future<void>.delayed(Duration.zero); prepare.complete(); await Future<void>.delayed(Duration.zero);
expect(playback.startCount, 1); expect(controller.rangeState?.waveform, [0.4]); });
test('waveform completion keeps in-flight playback current', () async { final play = Completer<void>(); final waveform = Completer<List<double>>(); final playback = _FakeAudioPlayback(onPlay: () => play.future); final controller = AudioAuditionController( playback, (_) => waveform.future, (_) {}, (_, _, _) {}, ); addTearDown(controller.dispose); controller.beginAdjustment( track: _track('selected'), hostSpan: _span(0, 10), waveform: const [], ); await Future<void>.delayed(Duration.zero); waveform.complete([0.4]); await Future<void>.delayed(Duration.zero); play.complete(); await Future<void>.delayed(Duration.zero);
expect(playback.playWasCurrent, isTrue); });
test('preview failure invalidates the failed transport request', () async { final playback = _FakeAudioPlayback( onPlay: () async => throw StateError('play failed'), ); final controller = AudioAuditionController( playback, (_) async => [0.5], (_) {}, (_, _, _) {}, ); addTearDown(controller.dispose);
controller.beginAdjustment( track: _track('selected'), hostSpan: _span(0, 10), waveform: const [0.5], ); await Future<void>.delayed(Duration.zero); await Future<void>.delayed(Duration.zero);
expect(controller.rangeState?.isScrubbing, isTrue); expect(playback.lastPlayIsCurrent?.call(), isFalse); });
test('picker phase ignores host playback snapshots', () async { final playback = _FakeAudioPlayback(); final controller = AudioAuditionController( playback, (_) async => [0.5], (_) {}, (_, _, _) {}, ); addTearDown(controller.dispose); await controller.beginPicker(previousTrack: null, hostSpan: _span(0, 10));
expect( controller.handlePlaybackSnapshot( const AudioAuditionPlaybackSnapshot( position: Duration.zero, isPlaying: false, isCompleted: false, ), ), isNull, ); });
test('host snapshot updates progress and exposes the active range', () async { final playback = _FakeAudioPlayback(); final controller = AudioAuditionController( playback, (_) async => [0.5], (_) {}, (_, _, _) {}, ); addTearDown(controller.dispose); controller.beginAdjustment( track: _track('selected'), hostSpan: _span(2, 10), waveform: const [0.5], ); await Future<void>.delayed(Duration.zero); const snapshot = AudioAuditionPlaybackSnapshot( position: Duration(seconds: 6), isPlaying: true, isCompleted: false, );
expect( controller.handlePlaybackSnapshot(snapshot), same(controller.rangeState), );
expect(controller.playbackProgress.value, 0.5); });
test('range preview waits for an asynchronous scrub pause', () async { final pause = Completer<void>(); final playback = _FakeAudioPlayback(onPause: () => pause.future); final controller = AudioAuditionController( playback, (_) async => [0.5], (_) {}, (_, _, _) {}, ); addTearDown(controller.dispose); controller.beginAdjustment( track: _track('selected'), hostSpan: _span(0, 10), waveform: const [0.5], ); await Future<void>.delayed(Duration.zero); expect(playback.startCount, 1);
final pauseFuture = controller.pauseForScrub(); final previewFuture = controller.previewRange(const Duration(seconds: 5)); await Future<void>.delayed(Duration.zero); expect(playback.startCount, 1);
pause.complete(); await Future.wait([pauseFuture, previewFuture]); await Future<void>.delayed(Duration.zero);
expect(playback.startCount, 2); expect( controller.rangeState?.draft.audioStartTime, const Duration(seconds: 5), ); });
test('only the latest overlapping scrub can restart playback', () async { final pauses = [Completer<void>(), Completer<void>()]; var pauseIndex = 0; final playback = _FakeAudioPlayback( onPause: () => pauses[pauseIndex++].future, ); final controller = AudioAuditionController( playback, (_) async => [0.5], (_) {}, (_, _, _) {}, ); addTearDown(controller.dispose); controller.beginAdjustment( track: _track('selected'), hostSpan: _span(0, 10), waveform: const [0.5], ); await Future<void>.delayed(Duration.zero); expect(playback.startCount, 1);
final firstPause = controller.pauseForScrub(); final firstPreview = controller.previewRange(const Duration(seconds: 3)); final secondPause = controller.pauseForScrub(); final secondPreview = controller.previewRange(const Duration(seconds: 5));
pauses.last.complete(); await Future<void>.delayed(Duration.zero); expect(playback.startCount, 1);
pauses.first.complete(); await Future.wait([firstPause, firstPreview, secondPause, secondPreview]); await Future<void>.delayed(Duration.zero);
expect(playback.startCount, 2); expect( controller.rangeState?.draft.audioStartTime, const Duration(seconds: 5), ); });
test('range boundary restarts without exposing a stale range', () async { final playback = _FakeAudioPlayback(); final controller = AudioAuditionController( playback, (_) async => [0.5], (_) {}, (_, _, _) {}, ); addTearDown(controller.dispose); controller.beginAdjustment( track: _track('selected'), hostSpan: _span(2, 10), waveform: const [0.5], ); await Future<void>.delayed(Duration.zero); expect( controller.handlePlaybackSnapshot( const AudioAuditionPlaybackSnapshot( position: Duration(seconds: 10), isPlaying: false, isCompleted: true, ), ), isNull, ); await Future<void>.delayed(Duration.zero);
expect(playback.preparedSpans, hasLength(2)); });}
class _FakeAudioPlayback implements AudioAuditionPlayback { _FakeAudioPlayback({ this.onPause, this.onPrepare, this.onPlay, this.onRestore, });
final Future<void> Function()? onPause; final Future<void> Function()? onPrepare; final Future<void> Function()? onPlay; final Future<void> Function()? onRestore; final preparedSpans = <TrimDurationSpan>[]; final restoredTracks = <AudioTrack?>[]; var startCount = 0; bool? playWasCurrent; bool Function()? lastPlayIsCurrent;
@override Future<void> pausePreview() async { await onPause?.call(); }
@override Future<void> previewCandidate( AudioTrack track, TrimDurationSpan hostSpan, { required bool Function() isCurrent, }) async {}
@override Future<void> prepareRangePreview( AudioTrack track, TrimDurationSpan playbackSpan, { required bool Function() isCurrent, }) async { preparedSpans.add(playbackSpan); await onPrepare?.call(); }
@override Future<void> startRangePreview( AudioTrack track, TrimDurationSpan playbackSpan, { required bool Function() isCurrent, }) async { startCount++; lastPlayIsCurrent = isCurrent; await onPlay?.call(); playWasCurrent = isCurrent(); }
@override Future<void> restorePrevious( AudioTrack? track, TrimDurationSpan hostSpan, { required bool Function() isCurrent, }) async { restoredTracks.add(track); await onRestore?.call(); }}
AudioTrack _track(String id) { return AudioTrack( id: id, title: 'Sound $id', subtitle: 'artist.sprk.so', duration: const Duration(seconds: 30), audio: EditorAudio(networkUrl: 'https://example.com/$id.mp3'), );}
TrimDurationSpan _span(int startSeconds, int endSeconds) { return TrimDurationSpan( start: Duration(seconds: startSeconds), end: Duration(seconds: endSeconds), );}