diff --git a/.idea/runConfigurations/production.xml b/.idea/runConfigurations/production.xml
index 1c5c774..e2470cc 100644
--- a/.idea/runConfigurations/production.xml
+++ b/.idea/runConfigurations/production.xml
@@ -1,7 +1,8 @@
+
-
+
\ No newline at end of file
diff --git a/.idea/runConfigurations/staging.xml b/.idea/runConfigurations/staging.xml
index f979a68..a443142 100644
--- a/.idea/runConfigurations/staging.xml
+++ b/.idea/runConfigurations/staging.xml
@@ -1,7 +1,8 @@
+
-
+
\ No newline at end of file
diff --git a/lib/src/core/resources/limited_list.dart b/lib/src/core/resources/limited_list.dart
new file mode 100644
index 0000000..719343b
--- /dev/null
+++ b/lib/src/core/resources/limited_list.dart
@@ -0,0 +1,72 @@
+// Copyright 2024 BBK Development. All rights reserved.
+// Use of this source code is governed by a GPL-style license that can be found
+// in the LICENSE file.
+
+import 'dart:collection';
+
+import 'package:equatable/equatable.dart';
+
+/// {@template limited_list}
+/// A limited list that can only contain a certain number of items. When a new
+/// item is added and the list is full, the oldest item is removed.
+/// {@endtemplate}
+final class LimitedList extends Equatable with ListBase {
+ /// {@macro limited_list}
+ LimitedList({required this.maxLength}) : _items = [];
+
+ /// The maximum number of items that the list can contain.
+ final int maxLength;
+
+ /// The internal list that contains the items.
+ final List _items;
+
+ /// The number of items in the list.
+ @override
+ int get length => _items.length;
+
+ /// Assigns a new length to the list. The new length must not exceed the
+ /// current length.
+ ///
+ /// Throws a [RangeError] if the new length exceeds [maxLength].
+ @override
+ set length(int newLength) {
+ if (newLength > maxLength) {
+ throw RangeError('New length exceeds maxLength');
+ }
+ _items.length = newLength;
+ }
+
+ /// Adds an item to the list. If the list is full, the oldest item is removed.
+ /// Returns the removed item if the list is full, otherwise returns `null`.
+ @override
+ T? add(T item) {
+ _items.add(item);
+ if (_items.length > maxLength) {
+ return _items.removeAt(0);
+ }
+ return null;
+ }
+
+ /// Throws an [UnsupportedError] because [LimitedList] does not support adding
+ /// multiple items at once.
+ @override
+ void addAll(Iterable iterable) {
+ throw UnsupportedError('LimitedList does not support addAll');
+ }
+
+ /// Returns the item at the given [index].
+ @override
+ T operator [](int index) => _items[index];
+
+ /// Sets the item at the given [index] to the given [value].
+ @override
+ void operator []=(int index, T value) {
+ _items[index] = value;
+ }
+
+ @override
+ List get props => [
+ maxLength,
+ _items,
+ ];
+}
diff --git a/lib/src/core/resources/resources.dart b/lib/src/core/resources/resources.dart
index 71a3b18..51450f6 100644
--- a/lib/src/core/resources/resources.dart
+++ b/lib/src/core/resources/resources.dart
@@ -6,4 +6,5 @@ export 'data_model.dart';
export 'data_state.dart';
export 'domain_entity.dart';
export 'failure.dart';
+export 'limited_list.dart';
export 'use_case.dart';
diff --git a/lib/src/features/merge/presentation/cubits/merge_page_cubit/merge_page_cubit.dart b/lib/src/features/merge/presentation/cubits/merge_page_cubit/merge_page_cubit.dart
index 4f7db4d..6b7b77d 100644
--- a/lib/src/features/merge/presentation/cubits/merge_page_cubit/merge_page_cubit.dart
+++ b/lib/src/features/merge/presentation/cubits/merge_page_cubit/merge_page_cubit.dart
@@ -2,155 +2,26 @@
// Use of this source code is governed by a GPL-style license that can be found
// in the LICENSE file.
+import 'dart:async';
import 'dart:developer';
-import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:video_player_service/video_player_service.dart';
+import 'package:vmerge/bootstrap.dart';
+import 'package:vmerge/src/core/core.dart';
import 'package:vmerge/src/features/merge/merge.dart';
final class MergePageCubit extends Cubit {
- MergePageCubit({
- required VideoPlayerService firstVideoPlayerService,
- required VideoPlayerService secondVideoPlayerService,
- required VideoPlayerService thirdVideoPlayerService,
- required VideoPlayerService fourthVideoPlayerService,
- }) : _firstVideoPlayerService = firstVideoPlayerService,
- _secondVideoPlayerService = secondVideoPlayerService,
- _thirdVideoPlayerService = thirdVideoPlayerService,
- _fourthVideoPlayerService = fourthVideoPlayerService,
+ MergePageCubit()
+ : _videoPlayerServices = LimitedList(maxLength: 2),
super(const MergePageInitial());
- final VideoPlayerService _firstVideoPlayerService;
- final VideoPlayerService _secondVideoPlayerService;
- final VideoPlayerService _thirdVideoPlayerService;
- final VideoPlayerService _fourthVideoPlayerService;
-
- VoidCallback get _firstVideoPlayerListener => () {
- switch (state) {
- case final MergePageLoaded state:
- if (_firstVideoPlayerService.position.inSeconds !=
- _firstVideoPlayerService.duration.inSeconds) return;
-
- _firstVideoPlayerService
- ..seekTo(Duration.zero)
- ..pause();
-
- emit(
- state.copyWith(
- activeVideoIndex: ActiveVideoIndex.two,
- videoPlayerController: _secondVideoPlayerService.controller,
- videoWidth: _secondVideoPlayerService.width,
- videoHeight: _secondVideoPlayerService.height,
- isVideoPlaying: true,
- ),
- );
-
- _secondVideoPlayerService.play();
- default:
- return;
- }
- };
-
- VoidCallback get _secondVideoPlayerListener => () {
- switch (state) {
- case final MergePageLoaded state:
- if (_secondVideoPlayerService.position.inSeconds !=
- _secondVideoPlayerService.duration.inSeconds) return;
-
- _secondVideoPlayerService
- ..seekTo(Duration.zero)
- ..pause();
-
- emit(
- state.copyWith(
- activeVideoIndex: _thirdVideoPlayerService.isReady
- ? ActiveVideoIndex.three
- : ActiveVideoIndex.one,
- videoPlayerController: _thirdVideoPlayerService.isReady
- ? _thirdVideoPlayerService.controller
- : _firstVideoPlayerService.controller,
- videoHeight: _thirdVideoPlayerService.isReady
- ? _thirdVideoPlayerService.height
- : _firstVideoPlayerService.height,
- videoWidth: _thirdVideoPlayerService.isReady
- ? _thirdVideoPlayerService.width
- : _firstVideoPlayerService.width,
- isVideoPlaying: _thirdVideoPlayerService.isReady,
- ),
- );
-
- if (_thirdVideoPlayerService.isReady) {
- _thirdVideoPlayerService.play();
- }
- default:
- return;
- }
- };
-
- VoidCallback get _thirdVideoPlayerListener => () {
- switch (state) {
- case final MergePageLoaded state:
- if (_thirdVideoPlayerService.position.inSeconds !=
- _thirdVideoPlayerService.duration.inSeconds) return;
-
- _thirdVideoPlayerService
- ..seekTo(Duration.zero)
- ..pause();
-
- emit(
- state.copyWith(
- activeVideoIndex: _fourthVideoPlayerService.isReady
- ? ActiveVideoIndex.four
- : ActiveVideoIndex.one,
- videoPlayerController: _fourthVideoPlayerService.isReady
- ? _fourthVideoPlayerService.controller
- : _firstVideoPlayerService.controller,
- videoHeight: _fourthVideoPlayerService.isReady
- ? _fourthVideoPlayerService.height
- : _firstVideoPlayerService.height,
- videoWidth: _fourthVideoPlayerService.isReady
- ? _fourthVideoPlayerService.width
- : _firstVideoPlayerService.width,
- isVideoPlaying: _fourthVideoPlayerService.isReady,
- ),
- );
-
- if (_fourthVideoPlayerService.isReady) {
- _fourthVideoPlayerService.play();
- }
- default:
- return;
- }
- };
-
- VoidCallback get _fourthVideoPlayerListener => () {
- switch (state) {
- case final MergePageLoaded state:
- if (_fourthVideoPlayerService.position.inSeconds !=
- _fourthVideoPlayerService.duration.inSeconds) return;
-
- _fourthVideoPlayerService
- ..seekTo(Duration.zero)
- ..pause();
-
- emit(
- state.copyWith(
- activeVideoIndex: ActiveVideoIndex.one,
- videoPlayerController: _firstVideoPlayerService.controller,
- videoHeight: _firstVideoPlayerService.height,
- videoWidth: _firstVideoPlayerService.width,
- isVideoPlaying: false,
- ),
- );
- default:
- return;
- }
- };
+ final LimitedList _videoPlayerServices;
Future loadVideoMetadata(
List metadatas, {
required bool isSoundOn,
+ required double playbackSpeed,
}) async {
if (metadatas.length < 2) {
emit(
@@ -166,47 +37,39 @@ final class MergePageCubit extends Cubit {
final volume = isSoundOn ? 1.0 : 0.0;
try {
+ for (var i = 0; i < _videoPlayerServices.maxLength; i++) {
+ _videoPlayerServices.add(getIt());
+ }
+ // It is important to load the videos in parallel to avoid any delay.
await Future.wait([
- _firstVideoPlayerService.loadFile(
- metadatas[0].file!,
- volume: volume,
- ),
- _secondVideoPlayerService.loadFile(
- metadatas[1].file!,
- volume: volume,
- ),
- if (metadatas.length > 2)
- _thirdVideoPlayerService.loadFile(
- metadatas[2].file!,
- volume: volume,
- ),
- if (metadatas.length > 3)
- _fourthVideoPlayerService.loadFile(
- metadatas[3].file!,
+ // To reduce loading time, it is necessary to load only the first two
+ // videos. If there are more than two videos, the rest will be loaded
+ // when the first two videos are played.
+ for (var i = 0; i < _videoPlayerServices.maxLength; i++)
+ _videoPlayerServices[i].loadFile(
+ metadatas[i].file!,
volume: volume,
),
]);
- if (metadatas.length > 1 && _firstVideoPlayerService.controller == null ||
- _secondVideoPlayerService.controller == null) {
- throw const LoadVideoException();
- }
- if (metadatas.length > 2 && _thirdVideoPlayerService.controller == null) {
- throw const LoadVideoException();
- }
- if (metadatas.length > 3 &&
- _fourthVideoPlayerService.controller == null) {
- throw const LoadVideoException();
+ final isEveryVideoPlayerServiceReady =
+ _videoPlayerServices.every((service) => service.isReady);
+ if (!isEveryVideoPlayerServiceReady) throw const LoadVideoException();
+
+ for (final videoPlayerService in _videoPlayerServices) {
+ unawaited(videoPlayerService.setPlaybackSpeed(playbackSpeed));
}
+ _videoPlayerServices.first.addListener(_videoPlayerListener);
+
emit(
MergePageLoaded(
videoMetadatas: metadatas,
- activeVideoIndex: ActiveVideoIndex.one,
- videoPlayerController: _firstVideoPlayerService.controller!,
- videoHeight: _firstVideoPlayerService.height,
- videoWidth: _firstVideoPlayerService.width,
- isVideoPlaying: _firstVideoPlayerService.isPlaying,
+ activeVideoIndex: 0,
+ videoPlayerController: _videoPlayerServices.first.controller!,
+ videoHeight: _videoPlayerServices.first.height,
+ videoWidth: _videoPlayerServices.first.width,
+ isVideoPlaying: _videoPlayerServices.first.isPlaying,
),
);
} on LoadVideoException catch (error, stackTrace) {
@@ -229,20 +92,8 @@ final class MergePageCubit extends Cubit {
Future playVideo() async {
switch (state) {
case final MergePageLoaded state:
- _addVideoPlayerListeners();
-
try {
- switch (state.activeVideoIndex) {
- case ActiveVideoIndex.one:
- await _firstVideoPlayerService.play();
- case ActiveVideoIndex.two:
- await _secondVideoPlayerService.play();
- case ActiveVideoIndex.three:
- await _thirdVideoPlayerService.play();
- case ActiveVideoIndex.four:
- await _fourthVideoPlayerService.play();
- }
-
+ await _videoPlayerServices.first.play();
emit(state.copyWith(isVideoPlaying: true));
} on PlayVideoException catch (error, stackTrace) {
log(
@@ -251,9 +102,6 @@ final class MergePageCubit extends Cubit {
error: error,
stackTrace: stackTrace,
);
-
- _removeVideoPlayerListeners();
-
emit(
MergePageError(
errorType: MergePageErrorType.playVideoException,
@@ -272,19 +120,8 @@ final class MergePageCubit extends Cubit {
Future stopVideo() async {
switch (state) {
case final MergePageLoaded state:
- _removeVideoPlayerListeners();
-
try {
- switch (state.activeVideoIndex) {
- case ActiveVideoIndex.one:
- await _firstVideoPlayerService.pause();
- case ActiveVideoIndex.two:
- await _secondVideoPlayerService.pause();
- case ActiveVideoIndex.three:
- await _thirdVideoPlayerService.pause();
- case ActiveVideoIndex.four:
- await _fourthVideoPlayerService.pause();
- }
+ await _videoPlayerServices.first.pause();
emit(state.copyWith(isVideoPlaying: false));
} on PauseVideoException catch (error, stackTrace) {
log(
@@ -313,25 +150,10 @@ final class MergePageCubit extends Cubit {
) async {
switch (state) {
case final MergePageLoaded state:
- emit(
- state.copyWith(
- activeVideoIndex: ActiveVideoIndex.one,
- videoPlayerController: _firstVideoPlayerService.controller,
- videoHeight: _firstVideoPlayerService.height,
- videoWidth: _firstVideoPlayerService.width,
- ),
- );
-
try {
await Future.wait([
- _firstVideoPlayerService.setPlaybackSpeed(speed.value),
- _secondVideoPlayerService.setPlaybackSpeed(speed.value),
- _thirdVideoPlayerService.setPlaybackSpeed(speed.value),
- _fourthVideoPlayerService.setPlaybackSpeed(speed.value),
- _firstVideoPlayerService.seekTo(Duration.zero),
- _secondVideoPlayerService.seekTo(Duration.zero),
- _thirdVideoPlayerService.seekTo(Duration.zero),
- _fourthVideoPlayerService.seekTo(Duration.zero),
+ for (final videoPlayerService in _videoPlayerServices)
+ videoPlayerService.setPlaybackSpeed(speed.value),
]);
} on SetVideoPlaybackSpeedException catch (error, stackTrace) {
log(
@@ -349,22 +171,6 @@ final class MergePageCubit extends Cubit {
);
// Restores last success state.
emit(state);
- } on SeekVideoPositionException catch (error, stackTrace) {
- log(
- 'Could not reset the video!',
- name: '$MergePageCubit',
- error: error,
- stackTrace: stackTrace,
- );
- emit(
- MergePageError(
- errorType: MergePageErrorType.seekVideoPositionException,
- error: error,
- stackTrace: stackTrace,
- ),
- );
- // Restores last success state.
- emit(state);
}
default:
return;
@@ -379,10 +185,8 @@ final class MergePageCubit extends Cubit {
final volume = isSoundOn ? 1.0 : 0.0;
try {
await Future.wait([
- _firstVideoPlayerService.setVolume(volume),
- _secondVideoPlayerService.setVolume(volume),
- _thirdVideoPlayerService.setVolume(volume),
- _fourthVideoPlayerService.setVolume(volume),
+ for (final videoPlayerService in _videoPlayerServices)
+ videoPlayerService.setVolume(volume),
]);
} on SetVolumeException catch (error, stackTrace) {
log(
@@ -406,27 +210,60 @@ final class MergePageCubit extends Cubit {
}
}
- void _addVideoPlayerListeners() {
- _firstVideoPlayerService.addListener(_firstVideoPlayerListener);
- _secondVideoPlayerService.addListener(_secondVideoPlayerListener);
- _thirdVideoPlayerService.addListener(_thirdVideoPlayerListener);
- _fourthVideoPlayerService.addListener(_fourthVideoPlayerListener);
- }
+ void _videoPlayerListener() {
+ switch (state) {
+ case final MergePageLoaded state:
+ if (_videoPlayerServices.first.position !=
+ _videoPlayerServices.first.duration) return;
+
+ final oldVideoPlayerService =
+ _videoPlayerServices.add(getIt());
+
+ final activeVideoIndex =
+ (state.activeVideoIndex + 1) % state.videoMetadatas.length;
+
+ emit(
+ state.copyWith(
+ activeVideoIndex: activeVideoIndex,
+ videoPlayerController: _videoPlayerServices.first.controller,
+ videoWidth: _videoPlayerServices.first.width,
+ videoHeight: _videoPlayerServices.first.height,
+ isVideoPlaying: activeVideoIndex != 0,
+ ),
+ );
- void _removeVideoPlayerListeners() {
- _firstVideoPlayerService.removeListener(_firstVideoPlayerListener);
- _secondVideoPlayerService.removeListener(_secondVideoPlayerListener);
- _thirdVideoPlayerService.removeListener(_thirdVideoPlayerListener);
- _fourthVideoPlayerService.removeListener(_fourthVideoPlayerListener);
+ // Add the listener to the current video player service.
+ _videoPlayerServices.first.addListener(_videoPlayerListener);
+
+ // Dispose the old video player service.
+ oldVideoPlayerService?.removeListener(_videoPlayerListener);
+ oldVideoPlayerService?.dispose();
+
+ // Load the next video.
+ final nextVideoIndex =
+ (activeVideoIndex + 1) % state.videoMetadatas.length;
+ _videoPlayerServices.last.loadFile(
+ state.videoMetadatas[nextVideoIndex].file!,
+ // TODO(all): Add volume getter to the VideoPlayerService.
+ volume: state.videoPlayerController.value.volume,
+ );
+ _videoPlayerServices.last
+ .setPlaybackSpeed(_videoPlayerServices.first.playbackSpeed);
+
+ // Play the current video.
+ if (activeVideoIndex != 0) playVideo();
+ default:
+ return;
+ }
}
@override
Future close() {
- _removeVideoPlayerListeners();
- _firstVideoPlayerService.dispose();
- _secondVideoPlayerService.dispose();
- _thirdVideoPlayerService.dispose();
- _fourthVideoPlayerService.dispose();
+ for (final videoPlayerService in _videoPlayerServices) {
+ videoPlayerService
+ ..removeListener(_videoPlayerListener)
+ ..dispose();
+ }
return super.close();
}
}
diff --git a/lib/src/features/merge/presentation/cubits/merge_page_cubit/merge_page_state.dart b/lib/src/features/merge/presentation/cubits/merge_page_cubit/merge_page_state.dart
index 37fd5d8..47263da 100644
--- a/lib/src/features/merge/presentation/cubits/merge_page_cubit/merge_page_state.dart
+++ b/lib/src/features/merge/presentation/cubits/merge_page_cubit/merge_page_state.dart
@@ -35,7 +35,7 @@ final class MergePageLoaded extends MergePageState {
});
final List videoMetadatas;
- final ActiveVideoIndex activeVideoIndex;
+ final int activeVideoIndex;
final VideoPlayerController videoPlayerController;
final double videoWidth;
final double videoHeight;
@@ -43,7 +43,7 @@ final class MergePageLoaded extends MergePageState {
MergePageLoaded copyWith({
List? videoMetadatas,
- ActiveVideoIndex? activeVideoIndex,
+ int? activeVideoIndex,
VideoPlayerController? videoPlayerController,
double? videoWidth,
double? videoHeight,
@@ -90,13 +90,6 @@ final class MergePageError extends MergePageState {
];
}
-enum ActiveVideoIndex {
- one,
- two,
- three,
- four,
-}
-
enum MergePageErrorType {
insufficientVideoException,
loadVideoException,
diff --git a/lib/src/features/merge/presentation/pages/merge_page.dart b/lib/src/features/merge/presentation/pages/merge_page.dart
index 4b7f2eb..ae1ff4b 100644
--- a/lib/src/features/merge/presentation/pages/merge_page.dart
+++ b/lib/src/features/merge/presentation/pages/merge_page.dart
@@ -14,7 +14,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:in_app_review_service/in_app_review_service.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:video_player/video_player.dart';
-import 'package:video_player_service/video_player_service.dart';
import 'package:vmerge/bootstrap.dart';
import 'package:vmerge/src/components/components.dart';
import 'package:vmerge/src/core/core.dart';
@@ -41,12 +40,7 @@ class MergePage extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider(
- create: (_) => MergePageCubit(
- firstVideoPlayerService: getIt(),
- secondVideoPlayerService: getIt(),
- thirdVideoPlayerService: getIt(),
- fourthVideoPlayerService: getIt(),
- ),
+ create: (_) => MergePageCubit(),
),
BlocProvider(
create: (_) => SettingsBottomSheetCubit(
@@ -107,6 +101,7 @@ class _MergeViewState extends State<_MergeView> with TickerProviderStateMixin {
await context.read().loadVideoMetadata(
videoMetadatas,
isSoundOn: settingsBottomSheetState.isAudioOn,
+ playbackSpeed: settingsBottomSheetState.playbackSpeed.value,
);
});
}