From c754d95476e76ad0e649f99dd05d9795f64f0776 Mon Sep 17 00:00:00 2001 From: Bulent Baris Kilic Date: Mon, 22 Jul 2024 21:59:57 +0200 Subject: [PATCH 1/3] feat: create LimitedList for handling list with constant length --- lib/src/core/resources/limited_list.dart | 72 ++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 lib/src/core/resources/limited_list.dart 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, + ]; +} From 5cae9576e36bd4295cebb8b4907589ecd0b8e5a0 Mon Sep 17 00:00:00 2001 From: Bulent Baris Kilic Date: Mon, 22 Jul 2024 22:04:57 +0200 Subject: [PATCH 2/3] fix: load only two videos at the same time to resolve OutOfMemoryError issue --- lib/src/core/resources/resources.dart | 1 + .../merge_page_cubit/merge_page_cubit.dart | 333 +++++------------- .../merge_page_cubit/merge_page_state.dart | 11 +- .../merge/presentation/pages/merge_page.dart | 9 +- 4 files changed, 90 insertions(+), 264 deletions(-) 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 6c819dd..0f32df0 100644 --- a/lib/src/features/merge/presentation/pages/merge_page.dart +++ b/lib/src/features/merge/presentation/pages/merge_page.dart @@ -13,7 +13,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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'; @@ -40,12 +39,7 @@ class MergePage extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => MergePageCubit( - firstVideoPlayerService: getIt(), - secondVideoPlayerService: getIt(), - thirdVideoPlayerService: getIt(), - fourthVideoPlayerService: getIt(), - ), + create: (_) => MergePageCubit(), ), BlocProvider( create: (_) => SettingsBottomSheetCubit( @@ -103,6 +97,7 @@ class _MergeViewState extends State<_MergeView> with TickerProviderStateMixin { await context.read().loadVideoMetadata( videoMetadatas, isSoundOn: settingsBottomSheetState.isAudioOn, + playbackSpeed: settingsBottomSheetState.playbackSpeed.value, ); }); } From a62d626b6697511d4403b99dea09d421562bda6b Mon Sep 17 00:00:00 2001 From: Bulent Baris Kilic Date: Mon, 22 Jul 2024 22:05:25 +0200 Subject: [PATCH 3/3] build: update run configs --- .idea/runConfigurations/production.xml | 3 ++- .idea/runConfigurations/staging.xml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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