From 20aabee294141df825270ad8e819101d4451d146 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Thu, 21 Sep 2023 15:40:18 -0300 Subject: [PATCH 01/29] feat: add video compression args to ffmpeg command --- .../chat/ds_video_message_bubble.controller.dart | 10 ++++++---- lib/src/utils/ds_utils.util.dart | 10 ++++++++++ lib/src/widgets/chat/audio/ds_audio_player.widget.dart | 3 ++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart index 28406a65..31f45c46 100644 --- a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart @@ -9,6 +9,7 @@ import 'package:path_provider/path_provider.dart'; import '../../models/ds_toast_props.model.dart'; import '../../services/ds_file.service.dart'; import '../../services/ds_toast.service.dart'; +import '../../utils/ds_utils.util.dart'; import '../../widgets/chat/video/ds_video_error.dialog.dart'; class DSVideoMessageBubbleController { @@ -75,7 +76,8 @@ class DSVideoMessageBubbleController { ); final session = await FFmpegKit.execute( - '-hide_banner -y -i $inputFilePath ${outputFile.path}'); + '-hide_banner -y -i $inputFilePath ${DSUtils.compressVideoArgs} ${outputFile.path}', + ); final returnCode = await session.getReturnCode(); @@ -90,10 +92,10 @@ class DSVideoMessageBubbleController { } final thumbnailPath = await getFullThumbnailPath(); - final command = - '-ss 00:00:3 -i ${outputFile.path} -frames:v 1 $thumbnailPath'; - await FFmpegKit.execute(command); + await FFmpegKit.execute( + '-ss 00:00:03 -i ${outputFile.path} -frames:v 1 $thumbnailPath', + ); thumbnail.value = thumbnailPath; } catch (_) { diff --git a/lib/src/utils/ds_utils.util.dart b/lib/src/utils/ds_utils.util.dart index 58d908f1..dd7ce937 100644 --- a/lib/src/utils/ds_utils.util.dart +++ b/lib/src/utils/ds_utils.util.dart @@ -7,6 +7,16 @@ abstract class DSUtils { static const bubbleMaxSize = 480.0; static const defaultAnimationDuration = Duration(milliseconds: 300); + static String get compressVideoArgs { + const resolution = '-vf scale=-2:480'; + const codec = '-c:v libx264'; + const preset = '-preset veryfast'; + const quality = '-crf 18'; + const audio = '-c:a copy'; + + return '$resolution $codec $preset $quality $audio'; + } + static const countriesList = [ DSCountry( code: '+55', diff --git a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart index af8e24e6..fce77d9c 100644 --- a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart +++ b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart @@ -153,7 +153,8 @@ class _DSAudioPlayerState extends State await _controller.player.setFilePath(outputFile.path); } else { final session = await FFmpegKit.execute( - '-hide_banner -y -i $inputFilePath -c:a libmp3lame -qscale:a 2 ${outputFile.path}'); + '-hide_banner -y -i $inputFilePath -c:a libmp3lame -qscale:a 2 ${outputFile.path}', + ); final returnCode = await session.getReturnCode(); From 6ec3983ab59bb7530987b9bf3520f3ff6287a971 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Fri, 22 Sep 2023 10:51:26 -0300 Subject: [PATCH 02/29] feat: add ffmpeg service class --- .../ds_video_message_bubble.controller.dart | 18 +++-- lib/src/services/ds_ffmpeg.service.dart | 72 +++++++++++++++++++ lib/src/utils/ds_utils.util.dart | 10 --- .../chat/audio/ds_audio_player.widget.dart | 12 ++-- 4 files changed, 85 insertions(+), 27 deletions(-) create mode 100644 lib/src/services/ds_ffmpeg.service.dart diff --git a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart index 5afde653..17fe60e8 100644 --- a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart @@ -1,15 +1,13 @@ import 'dart:io'; -import 'package:ffmpeg_kit_flutter_full_gpl/ffmpeg_kit.dart'; -import 'package:ffmpeg_kit_flutter_full_gpl/return_code.dart'; import 'package:file_sizes/file_sizes.dart'; import 'package:get/get.dart'; import 'package:path_provider/path_provider.dart'; import '../../models/ds_toast_props.model.dart'; +import '../../services/ds_ffmpeg.service.dart'; import '../../services/ds_file.service.dart'; import '../../services/ds_toast.service.dart'; -import '../../utils/ds_utils.util.dart'; import '../../widgets/chat/video/ds_video_error.dialog.dart'; class DSVideoMessageBubbleController { @@ -75,13 +73,12 @@ class DSVideoMessageBubbleController { httpHeaders: httpHeaders, ); - final session = await FFmpegKit.execute( - '-hide_banner -y -i "$inputFilePath" ${DSUtils.compressVideoArgs} "${outputFile.path}"', + final isSuccess = await DSFFMpegService.formatVideo( + input: inputFilePath!, + output: outputFile.path, ); - final returnCode = await session.getReturnCode(); - - if (!ReturnCode.isSuccess(returnCode)) { + if (!isSuccess) { hasError.value = true; await DSVideoErrorDialog.show( filename: fileName, @@ -93,8 +90,9 @@ class DSVideoMessageBubbleController { final thumbnailPath = await getFullThumbnailPath(); - await FFmpegKit.execute( - '-hide_banner -y -i "${outputFile.path}" -vframes 1 "$thumbnailPath"', + await DSFFMpegService.getVideoThumbnail( + input: outputFile.path, + output: thumbnailPath, ); thumbnail.value = thumbnailPath; diff --git a/lib/src/services/ds_ffmpeg.service.dart b/lib/src/services/ds_ffmpeg.service.dart new file mode 100644 index 00000000..2a8866cc --- /dev/null +++ b/lib/src/services/ds_ffmpeg.service.dart @@ -0,0 +1,72 @@ +import 'package:ffmpeg_kit_flutter_full_gpl/ffmpeg_kit.dart'; +import 'package:ffmpeg_kit_flutter_full_gpl/return_code.dart'; + +abstract class DSFFMpegService { + static String get _defaultArgs { + const hideInfoBanner = '-hide_banner'; + const answerYes = '-y'; + + return '$hideInfoBanner $answerYes'; + } + + static String get _compressVideoArgs { + const resolution = '-vf scale=-2:480'; + const codec = '-c:v libx264'; + const preset = '-preset veryfast'; + const quality = '-crf 18'; + const audio = '-c:a copy'; + + return '$resolution $codec $preset $quality $audio'; + } + + static String get _thumbnailArgs { + const getOneFrame = '-vframes 1'; + + return getOneFrame; + } + + static String get _transcodeAudioArgs { + const codec = '-c:a libmp3lame'; + const quality = '-qscale:a 2'; + + return '$codec $quality'; + } + + static Future _executeCommand({ + required final String command, + }) async { + final session = await FFmpegKit.execute( + '$_defaultArgs $command', + ); + + return ReturnCode.isSuccess( + await session.getReturnCode(), + ); + } + + static Future formatVideo({ + required final String input, + required final String output, + final bool shouldCompress = true, + }) => + _executeCommand( + command: + '-i "$input" ${shouldCompress ? '$_compressVideoArgs ' : ''}"$output"', + ); + + static Future getVideoThumbnail({ + required final String input, + required final String output, + }) => + _executeCommand( + command: '-i "$input" $_thumbnailArgs "$output"', + ); + + static Future transcodeAudio({ + required final String input, + required final String output, + }) => + _executeCommand( + command: '-i "$input" $_transcodeAudioArgs "$output"', + ); +} diff --git a/lib/src/utils/ds_utils.util.dart b/lib/src/utils/ds_utils.util.dart index dd7ce937..58d908f1 100644 --- a/lib/src/utils/ds_utils.util.dart +++ b/lib/src/utils/ds_utils.util.dart @@ -7,16 +7,6 @@ abstract class DSUtils { static const bubbleMaxSize = 480.0; static const defaultAnimationDuration = Duration(milliseconds: 300); - static String get compressVideoArgs { - const resolution = '-vf scale=-2:480'; - const codec = '-c:v libx264'; - const preset = '-preset veryfast'; - const quality = '-crf 18'; - const audio = '-c:a copy'; - - return '$resolution $codec $preset $quality $audio'; - } - static const countriesList = [ DSCountry( code: '+55', diff --git a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart index 57e8eb6f..037cdc0d 100644 --- a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart +++ b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart @@ -1,7 +1,5 @@ import 'dart:io'; -import 'package:ffmpeg_kit_flutter_full_gpl/ffmpeg_kit.dart'; -import 'package:ffmpeg_kit_flutter_full_gpl/return_code.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; @@ -10,6 +8,7 @@ import 'package:path_provider/path_provider.dart'; import '../../../controllers/chat/ds_audio_player.controller.dart'; import '../../../models/ds_toast_props.model.dart'; import '../../../services/ds_auth.service.dart'; +import '../../../services/ds_ffmpeg.service.dart'; import '../../../services/ds_file.service.dart'; import '../../../services/ds_toast.service.dart'; import '../../buttons/ds_pause_button.widget.dart'; @@ -152,13 +151,12 @@ class _DSAudioPlayerState extends State if (await outputFile.exists()) { await _controller.player.setFilePath(outputFile.path); } else { - final session = await FFmpegKit.execute( - '-hide_banner -y -i "$inputFilePath" -c:a libmp3lame -qscale:a 2 "${outputFile.path}"', + final isSuccess = await DSFFMpegService.transcodeAudio( + input: inputFilePath!, + output: outputFile.path, ); - final returnCode = await session.getReturnCode(); - - if (ReturnCode.isSuccess(returnCode)) { + if (isSuccess) { await _controller.player.setFilePath(outputFile.path); } } From ec9eb22973e5dc46dc3dc533783ed3823402a632 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Tue, 26 Sep 2023 15:55:45 -0300 Subject: [PATCH 03/29] feat: add ffmpeg service to export --- lib/blip_ds.dart | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/blip_ds.dart b/lib/blip_ds.dart index da2c6a89..2f83e778 100644 --- a/lib/blip_ds.dart +++ b/lib/blip_ds.dart @@ -23,6 +23,7 @@ export 'src/models/ds_toast_props.model.dart' show DSToastProps; export 'src/services/ds_auth.service.dart' show DSAuthService; export 'src/services/ds_bottom_sheet.service.dart' show DSBottomSheetService; export 'src/services/ds_dialog.service.dart' show DSDialogService; +export 'src/services/ds_ffmpeg.service.dart' show DSFFMpegService; export 'src/services/ds_file.service.dart' show DSFileService; export 'src/services/ds_toast.service.dart' show DSToastService; export 'src/themes/colors/ds_colors.theme.dart' show DSColors; @@ -77,10 +78,10 @@ export 'src/widgets/buttons/ds_icon_button.widget.dart' show DSIconButton; export 'src/widgets/buttons/ds_pause_button.widget.dart' show DSPauseButton; export 'src/widgets/buttons/ds_play_button.widget.dart' show DSPlayButton; export 'src/widgets/buttons/ds_primary_button.widget.dart' show DSPrimaryButton; -export 'src/widgets/buttons/ds_secondary_button.widget.dart' - show DSSecondaryButton; export 'src/widgets/buttons/ds_request_location_button.widget.dart' show DSRequestLocationButton; +export 'src/widgets/buttons/ds_secondary_button.widget.dart' + show DSSecondaryButton; export 'src/widgets/buttons/ds_send_button.widget.dart' show DSSendButton; export 'src/widgets/buttons/ds_tertiary_button.widget.dart' show DSTertiaryButton; @@ -101,6 +102,8 @@ export 'src/widgets/chat/ds_location_message_bubble.widget.dart' export 'src/widgets/chat/ds_message_bubble.widget.dart' show DSMessageBubble; export 'src/widgets/chat/ds_message_bubble_detail.widget.dart' show DSMessageBubbleDetail; +export 'src/widgets/chat/ds_request_location_bubble.widget.dart' + show DSRequestLocationBubble; export 'src/widgets/chat/ds_survey_message_bubble.widget.dart' show DSSurveyMessageBubble; export 'src/widgets/chat/ds_text_message_bubble.widget.dart' @@ -155,5 +158,3 @@ export 'src/widgets/utils/ds_group_card.widget.dart' show DSGroupCard; export 'src/widgets/utils/ds_header.widget.dart' show DSHeader; export 'src/widgets/utils/ds_progress_bar.widget.dart' show DSProgressBar; export 'src/widgets/utils/ds_user_avatar.widget.dart' show DSUserAvatar; -export 'src/widgets/chat/ds_request_location_bubble.widget.dart' - show DSRequestLocationBubble; From 4045d59fac58e871fc427b9d5b15fa5a56ea55b8 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Wed, 27 Sep 2023 09:51:49 -0300 Subject: [PATCH 04/29] feat: add merge audio method in ffmpeg service --- lib/blip_ds.dart | 2 +- .../ds_video_message_bubble.controller.dart | 4 +- lib/src/services/ds_ffmpeg.service.dart | 91 +++++++++++-------- .../chat/audio/ds_audio_player.widget.dart | 2 +- 4 files changed, 59 insertions(+), 40 deletions(-) diff --git a/lib/blip_ds.dart b/lib/blip_ds.dart index 2f83e778..1d7cf158 100644 --- a/lib/blip_ds.dart +++ b/lib/blip_ds.dart @@ -23,7 +23,7 @@ export 'src/models/ds_toast_props.model.dart' show DSToastProps; export 'src/services/ds_auth.service.dart' show DSAuthService; export 'src/services/ds_bottom_sheet.service.dart' show DSBottomSheetService; export 'src/services/ds_dialog.service.dart' show DSDialogService; -export 'src/services/ds_ffmpeg.service.dart' show DSFFMpegService; +export 'src/services/ds_ffmpeg.service.dart' show DSFFmpegService; export 'src/services/ds_file.service.dart' show DSFileService; export 'src/services/ds_toast.service.dart' show DSToastService; export 'src/themes/colors/ds_colors.theme.dart' show DSColors; diff --git a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart index 17fe60e8..ef08462a 100644 --- a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart @@ -73,7 +73,7 @@ class DSVideoMessageBubbleController { httpHeaders: httpHeaders, ); - final isSuccess = await DSFFMpegService.formatVideo( + final isSuccess = await DSFFmpegService.formatVideo( input: inputFilePath!, output: outputFile.path, ); @@ -90,7 +90,7 @@ class DSVideoMessageBubbleController { final thumbnailPath = await getFullThumbnailPath(); - await DSFFMpegService.getVideoThumbnail( + await DSFFmpegService.getVideoThumbnail( input: outputFile.path, output: thumbnailPath, ); diff --git a/lib/src/services/ds_ffmpeg.service.dart b/lib/src/services/ds_ffmpeg.service.dart index 2a8866cc..c4d9c78f 100644 --- a/lib/src/services/ds_ffmpeg.service.dart +++ b/lib/src/services/ds_ffmpeg.service.dart @@ -1,7 +1,55 @@ import 'package:ffmpeg_kit_flutter_full_gpl/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter_full_gpl/return_code.dart'; -abstract class DSFFMpegService { +abstract class DSFFmpegService { + static Future formatVideo({ + required final String input, + required final String output, + final bool shouldCompress = true, + }) => + _executeCommand( + command: + '-i "$input" ${shouldCompress ? '$_compressVideoArgs ' : ''}"$output"', + ); + + static Future getVideoThumbnail({ + required final String input, + required final String output, + }) => + _executeCommand( + command: '-i "$input" $_thumbnailArgs "$output"', + ); + + static Future transcodeAudio({ + required final String input, + required final String output, + }) => + _executeCommand( + command: '-i "$input" $_transcodeAudioArgs "$output"', + ); + + static Future mergeAudio({ + required final String firstInput, + required final String secondInput, + required final String output, + }) => + _executeCommand( + command: + '-i "$firstInput" -i "$secondInput" $_mergeAudioArgs "$output"', + ); + + static Future _executeCommand({ + required final String command, + }) async { + final session = await FFmpegKit.execute( + '$_defaultArgs $command', + ); + + return ReturnCode.isSuccess( + await session.getReturnCode(), + ); + } + static String get _defaultArgs { const hideInfoBanner = '-hide_banner'; const answerYes = '-y'; @@ -32,41 +80,12 @@ abstract class DSFFMpegService { return '$codec $quality'; } - static Future _executeCommand({ - required final String command, - }) async { - final session = await FFmpegKit.execute( - '$_defaultArgs $command', - ); + static String get _mergeAudioArgs { + const filter = '-filter_complex'; + const firstInput = '[0:0]'; + const secondInput = '[1:0]'; + const audioConcat = 'concat=n=2:v=0:a=1'; - return ReturnCode.isSuccess( - await session.getReturnCode(), - ); + return '$filter "$firstInput$secondInput$audioConcat"'; } - - static Future formatVideo({ - required final String input, - required final String output, - final bool shouldCompress = true, - }) => - _executeCommand( - command: - '-i "$input" ${shouldCompress ? '$_compressVideoArgs ' : ''}"$output"', - ); - - static Future getVideoThumbnail({ - required final String input, - required final String output, - }) => - _executeCommand( - command: '-i "$input" $_thumbnailArgs "$output"', - ); - - static Future transcodeAudio({ - required final String input, - required final String output, - }) => - _executeCommand( - command: '-i "$input" $_transcodeAudioArgs "$output"', - ); } diff --git a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart index 037cdc0d..753172f7 100644 --- a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart +++ b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart @@ -151,7 +151,7 @@ class _DSAudioPlayerState extends State if (await outputFile.exists()) { await _controller.player.setFilePath(outputFile.path); } else { - final isSuccess = await DSFFMpegService.transcodeAudio( + final isSuccess = await DSFFmpegService.transcodeAudio( input: inputFilePath!, output: outputFile.path, ); From a715442e5d289dceb175afddedd97e288b847e00 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Wed, 27 Sep 2023 14:44:04 -0300 Subject: [PATCH 05/29] feat: change video compression from 480p to 720p --- lib/src/services/ds_ffmpeg.service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/services/ds_ffmpeg.service.dart b/lib/src/services/ds_ffmpeg.service.dart index c4d9c78f..2f75c761 100644 --- a/lib/src/services/ds_ffmpeg.service.dart +++ b/lib/src/services/ds_ffmpeg.service.dart @@ -58,7 +58,7 @@ abstract class DSFFmpegService { } static String get _compressVideoArgs { - const resolution = '-vf scale=-2:480'; + const resolution = '-vf scale=-2:720'; const codec = '-c:v libx264'; const preset = '-preset veryfast'; const quality = '-crf 18'; From 83fdecb73e437582638587f6fbe962d25b772460 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Fri, 29 Sep 2023 18:07:25 -0300 Subject: [PATCH 06/29] feat: get media info in ffmpeg service --- lib/src/services/ds_ffmpeg.service.dart | 33 ++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/src/services/ds_ffmpeg.service.dart b/lib/src/services/ds_ffmpeg.service.dart index 2f75c761..2a3fce87 100644 --- a/lib/src/services/ds_ffmpeg.service.dart +++ b/lib/src/services/ds_ffmpeg.service.dart @@ -1,4 +1,6 @@ import 'package:ffmpeg_kit_flutter_full_gpl/ffmpeg_kit.dart'; +import 'package:ffmpeg_kit_flutter_full_gpl/ffprobe_kit.dart'; +import 'package:ffmpeg_kit_flutter_full_gpl/media_information.dart'; import 'package:ffmpeg_kit_flutter_full_gpl/return_code.dart'; abstract class DSFFmpegService { @@ -38,6 +40,31 @@ abstract class DSFFmpegService { '-i "$firstInput" -i "$secondInput" $_mergeAudioArgs "$output"', ); + static Future<(int?, int?)> getMediaResolution({ + required final String path, + }) async { + int? width; + int? height; + + final info = await _getMediaInfo( + path: path, + ); + + final streams = info?.getStreams(); + + if (streams?.isNotEmpty ?? false) { + width = streams!.first.getWidth(); + height = streams.first.getHeight(); + } + + return (width, height); + } + + static Future _getMediaInfo({ + required final String path, + }) async => + (await FFprobeKit.getMediaInformation(path)).getMediaInformation(); + static Future _executeCommand({ required final String command, }) async { @@ -58,10 +85,10 @@ abstract class DSFFmpegService { } static String get _compressVideoArgs { - const resolution = '-vf scale=-2:720'; + const resolution = '-vf scale=720x1280'; const codec = '-c:v libx264'; - const preset = '-preset veryfast'; - const quality = '-crf 18'; + const preset = '-preset faster'; + const quality = '-crf 23'; const audio = '-c:a copy'; return '$resolution $codec $preset $quality $audio'; From a9a495d319a20e9dd000fecf2b072d8fa308a02f Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Fri, 13 Oct 2023 15:58:26 -0300 Subject: [PATCH 07/29] fix: toast layout when message is empty --- lib/src/widgets/toast/ds_toast.widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/toast/ds_toast.widget.dart b/lib/src/widgets/toast/ds_toast.widget.dart index 93d8ccfd..e2e8ddbf 100644 --- a/lib/src/widgets/toast/ds_toast.widget.dart +++ b/lib/src/widgets/toast/ds_toast.widget.dart @@ -163,7 +163,7 @@ class _DSToastState extends State with AutomaticKeepAliveClientMixin { props.title, overflow: TextOverflow.visible, ), - if (props.message != null) + if (props.message?.isNotEmpty ?? false) DSBodyText( props.message, overflow: TextOverflow.visible, From 69d3895e83003b9ee09141061040069ce60f694e Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Tue, 17 Oct 2023 09:39:27 -0300 Subject: [PATCH 08/29] feat: use media info to decide if video should be compressed --- .../ds_file_message_bubble.controller.dart | 9 +- lib/src/models/ds_media_info.model.dart | 21 ++++ lib/src/services/ds_ffmpeg.service.dart | 102 ++++++++++++------ pubspec.yaml | 1 - 4 files changed, 100 insertions(+), 33 deletions(-) create mode 100644 lib/src/models/ds_media_info.model.dart diff --git a/lib/src/controllers/chat/ds_file_message_bubble.controller.dart b/lib/src/controllers/chat/ds_file_message_bubble.controller.dart index 188ce30e..a72dae42 100644 --- a/lib/src/controllers/chat/ds_file_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_file_message_bubble.controller.dart @@ -1,4 +1,4 @@ -import 'package:filesize/filesize.dart'; +import 'package:file_sizes/file_sizes.dart'; import 'package:get/get.dart'; import '../../services/ds_file.service.dart'; @@ -7,7 +7,12 @@ class DSFileMessageBubbleController extends GetxController { final isDownloading = RxBool(false); String getFileSize(final int size) { - return size > 0 ? filesize(size, 1) : ''; + return size > 0 + ? FileSize.getSize( + size, + precision: PrecisionValue.One, + ) + : ''; } Future openFile({ diff --git a/lib/src/models/ds_media_info.model.dart b/lib/src/models/ds_media_info.model.dart new file mode 100644 index 00000000..e9fad92d --- /dev/null +++ b/lib/src/models/ds_media_info.model.dart @@ -0,0 +1,21 @@ +class DSMediaInfo { + final int? width; + final int? height; + final int? size; + + DSMediaInfo({ + this.width, + this.height, + this.size, + }); + + bool get hasDimensions => width != null && height != null; + + bool get isWidescreen => hasDimensions && width! > height!; + + double? get aspectRatio => hasDimensions + ? isWidescreen + ? width! / height! + : height! / width! + : null; +} diff --git a/lib/src/services/ds_ffmpeg.service.dart b/lib/src/services/ds_ffmpeg.service.dart index 2a3fce87..6cb79eef 100644 --- a/lib/src/services/ds_ffmpeg.service.dart +++ b/lib/src/services/ds_ffmpeg.service.dart @@ -1,18 +1,54 @@ import 'package:ffmpeg_kit_flutter_full_gpl/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter_full_gpl/ffprobe_kit.dart'; -import 'package:ffmpeg_kit_flutter_full_gpl/media_information.dart'; import 'package:ffmpeg_kit_flutter_full_gpl/return_code.dart'; +import '../models/ds_media_info.model.dart'; + abstract class DSFFmpegService { + static const _maxSize = 10485760; // 10 MB + static const _targetResolution = 720; // 720p + static Future formatVideo({ required final String input, required final String output, final bool shouldCompress = true, - }) => - _executeCommand( - command: - '-i "$input" ${shouldCompress ? '$_compressVideoArgs ' : ''}"$output"', - ); + }) async { + final info = await _getMediaInfo( + path: input, + ); + + if (info.hasDimensions) { + final isOversized = + info.size == null || (info.size != null && info.size! > _maxSize); + + final isOverdimensioned = + info.width! > _targetResolution && info.height! > _targetResolution; + + final shouldCompress = isOversized && isOverdimensioned; + + if (shouldCompress) { + final compressedResolution = + (info.aspectRatio! * _targetResolution).ceil(); + + final compressedWidth = + info.isWidescreen ? compressedResolution : _targetResolution; + + final compressedHeight = + info.isWidescreen ? _targetResolution : compressedResolution; + + final args = _compressVideoArgs( + width: compressedWidth, + height: compressedHeight, + ); + + return _executeCommand( + command: '-i "$input" ${shouldCompress ? '$args ' : ''}"$output"', + ); + } + } + + return true; + } static Future getVideoThumbnail({ required final String input, @@ -40,31 +76,34 @@ abstract class DSFFmpegService { '-i "$firstInput" -i "$secondInput" $_mergeAudioArgs "$output"', ); - static Future<(int?, int?)> getMediaResolution({ + static Future _getMediaInfo({ required final String path, }) async { int? width; int? height; + int? size; - final info = await _getMediaInfo( - path: path, - ); + final session = await FFprobeKit.getMediaInformation(path); + final info = session.getMediaInformation(); + + if (info != null) { + size = int.tryParse(info.getSize() ?? '') ?? 0; - final streams = info?.getStreams(); + final streams = info.getStreams(); - if (streams?.isNotEmpty ?? false) { - width = streams!.first.getWidth(); - height = streams.first.getHeight(); + if (streams.isNotEmpty) { + width = streams.first.getWidth(); + height = streams.first.getHeight(); + } } - return (width, height); + return DSMediaInfo( + width: width, + height: height, + size: size, + ); } - static Future _getMediaInfo({ - required final String path, - }) async => - (await FFprobeKit.getMediaInformation(path)).getMediaInformation(); - static Future _executeCommand({ required final String command, }) async { @@ -84,16 +123,6 @@ abstract class DSFFmpegService { return '$hideInfoBanner $answerYes'; } - static String get _compressVideoArgs { - const resolution = '-vf scale=720x1280'; - const codec = '-c:v libx264'; - const preset = '-preset faster'; - const quality = '-crf 23'; - const audio = '-c:a copy'; - - return '$resolution $codec $preset $quality $audio'; - } - static String get _thumbnailArgs { const getOneFrame = '-vframes 1'; @@ -115,4 +144,17 @@ abstract class DSFFmpegService { return '$filter "$firstInput$secondInput$audioConcat"'; } + + static String _compressVideoArgs({ + required final int width, + required final int height, + }) { + final resolution = '-vf scale=${width}x$height'; + const codec = '-c:v libx264'; + const preset = '-preset faster'; + const quality = '-crf 23'; + const audio = '-c:a copy'; + + return '$resolution $codec $preset $quality $audio'; + } } diff --git a/pubspec.yaml b/pubspec.yaml index 941fa970..e430f648 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,6 @@ dependencies: rxdart: ^0.27.4 flutter_spinkit: ^5.1.0 get: ^4.6.5 - filesize: ^2.0.1 open_filex: ^4.3.2 path_provider: ^2.0.11 dio: ^5.2.1+1 From 4e971d4f0a73422a130cce9d0c505db80e39cef8 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Wed, 18 Oct 2023 19:13:48 -0300 Subject: [PATCH 09/29] feat: change video compression target resolution from 720 to 480 --- lib/src/services/ds_ffmpeg.service.dart | 102 ++++++++++++++---------- lib/src/services/ds_toast.service.dart | 13 +-- 2 files changed, 68 insertions(+), 47 deletions(-) diff --git a/lib/src/services/ds_ffmpeg.service.dart b/lib/src/services/ds_ffmpeg.service.dart index 6cb79eef..869fbfae 100644 --- a/lib/src/services/ds_ffmpeg.service.dart +++ b/lib/src/services/ds_ffmpeg.service.dart @@ -6,48 +6,24 @@ import '../models/ds_media_info.model.dart'; abstract class DSFFmpegService { static const _maxSize = 10485760; // 10 MB - static const _targetResolution = 720; // 720p + static const _targetResolution = 480; // 480p static Future formatVideo({ required final String input, required final String output, final bool shouldCompress = true, }) async { - final info = await _getMediaInfo( - path: input, - ); - - if (info.hasDimensions) { - final isOversized = - info.size == null || (info.size != null && info.size! > _maxSize); - - final isOverdimensioned = - info.width! > _targetResolution && info.height! > _targetResolution; - - final shouldCompress = isOversized && isOverdimensioned; - - if (shouldCompress) { - final compressedResolution = - (info.aspectRatio! * _targetResolution).ceil(); - - final compressedWidth = - info.isWidescreen ? compressedResolution : _targetResolution; - - final compressedHeight = - info.isWidescreen ? _targetResolution : compressedResolution; - - final args = _compressVideoArgs( - width: compressedWidth, - height: compressedHeight, - ); + final args = shouldCompress + ? await _getCompressVideoArgs( + path: input, + ) + : null; - return _executeCommand( - command: '-i "$input" ${shouldCompress ? '$args ' : ''}"$output"', - ); - } - } + final command = '-i "$input" ${args != null ? '$args ' : ''}"$output"'; - return true; + return _executeCommand( + command: command, + ); } static Future getVideoThumbnail({ @@ -76,12 +52,13 @@ abstract class DSFFmpegService { '-i "$firstInput" -i "$secondInput" $_mergeAudioArgs "$output"', ); - static Future _getMediaInfo({ + static Future getMediaInfo({ required final String path, }) async { int? width; int? height; int? size; + int? rotation; final session = await FFprobeKit.getMediaInformation(path); final info = session.getMediaInformation(); @@ -92,18 +69,58 @@ abstract class DSFFmpegService { final streams = info.getStreams(); if (streams.isNotEmpty) { - width = streams.first.getWidth(); - height = streams.first.getHeight(); + final properties = streams.first.getAllProperties(); + final List? sideData = properties?['side_data_list']; + + width = properties?['width']; + height = properties?['height']; + rotation = sideData?.first['rotation']; } } + final isFlipped = rotation != null && rotation != 0; + return DSMediaInfo( - width: width, - height: height, + width: isFlipped ? height : width, + height: isFlipped ? width : height, size: size, ); } + static Future _getCompressVideoArgs({ + required final String path, + }) async { + final info = await getMediaInfo( + path: path, + ); + + if (info.hasDimensions) { + final isOversized = + info.size == null || (info.size != null && info.size! > _maxSize); + + final isOverdimensioned = + info.width! > _targetResolution && info.height! > _targetResolution; + + if (isOversized && isOverdimensioned) { + final compressedResolution = + (info.aspectRatio! * _targetResolution).ceil(); + + final compressedWidth = + info.isWidescreen ? compressedResolution : _targetResolution; + + final compressedHeight = + info.isWidescreen ? _targetResolution : compressedResolution; + + return _compressVideoArgs( + width: compressedWidth, + height: compressedHeight, + ); + } + } + + return null; + } + static Future _executeCommand({ required final String command, }) async { @@ -150,11 +167,12 @@ abstract class DSFFmpegService { required final int height, }) { final resolution = '-vf scale=${width}x$height'; + const pixelFormat = '-pix_fmt yuv420p'; const codec = '-c:v libx264'; - const preset = '-preset faster'; + const preset = '-preset veryfast'; const quality = '-crf 23'; - const audio = '-c:a copy'; + const audio = '-c:a aac'; - return '$resolution $codec $preset $quality $audio'; + return '$resolution $pixelFormat $codec $preset $quality $audio'; } } diff --git a/lib/src/services/ds_toast.service.dart b/lib/src/services/ds_toast.service.dart index 4638b015..1f5eecc1 100644 --- a/lib/src/services/ds_toast.service.dart +++ b/lib/src/services/ds_toast.service.dart @@ -20,11 +20,14 @@ abstract class DSToastService { ); _visibleToasts.insert(0, toast); - _controller?.animateTo( - 0, - duration: DSUtils.defaultAnimationDuration, - curve: Curves.easeInOut, - ); + + if (_controller?.hasClients ?? false) { + _controller?.animateTo( + 0, + duration: DSUtils.defaultAnimationDuration, + curve: Curves.easeInOut, + ); + } if (_overlayEntry == null) { _controller = ScrollController(); From 662bca34ee75588c3669697e6eea4ab96d7f45da Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Wed, 25 Oct 2023 18:23:04 -0300 Subject: [PATCH 10/29] feat: add hardware acceleration to video compression --- lib/src/services/ds_ffmpeg.service.dart | 5 +++-- pubspec.yaml | 2 +- sample/pubspec.lock | 26 +++++++++---------------- 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/lib/src/services/ds_ffmpeg.service.dart b/lib/src/services/ds_ffmpeg.service.dart index 869fbfae..292abc99 100644 --- a/lib/src/services/ds_ffmpeg.service.dart +++ b/lib/src/services/ds_ffmpeg.service.dart @@ -19,7 +19,8 @@ abstract class DSFFmpegService { ) : null; - final command = '-i "$input" ${args != null ? '$args ' : ''}"$output"'; + final command = + '-hwaccel auto -i "$input" ${args != null ? '$args ' : ''}"$output"'; return _executeCommand( command: command, @@ -166,7 +167,7 @@ abstract class DSFFmpegService { required final int width, required final int height, }) { - final resolution = '-vf scale=${width}x$height'; + final resolution = '-s ${width}x$height'; const pixelFormat = '-pix_fmt yuv420p'; const codec = '-c:v libx264'; const preset = '-preset veryfast'; diff --git a/pubspec.yaml b/pubspec.yaml index 81be8166..ce653e68 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,7 +39,7 @@ dependencies: map_launcher: ^2.5.0+1 mime: ^1.0.4 crypto: ^3.0.3 - + dev_dependencies: flutter_test: sdk: flutter diff --git a/sample/pubspec.lock b/sample/pubspec.lock index 67d875ba..1bc07fec 100644 --- a/sample/pubspec.lock +++ b/sample/pubspec.lock @@ -21,17 +21,17 @@ packages: dependency: transitive description: name: audio_session - sha256: e4acc4e9eaa32436dfc5d7aed7f0a370f2d7bb27ee27de30d6c4f220c2a05c73 + sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad" url: "https://pub.dev" source: hosted - version: "0.1.13" + version: "0.1.16" blip_ds: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.0.81" + version: "0.0.84" boolean_selector: dependency: transitive description: @@ -208,14 +208,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.6" - filesize: - dependency: transitive - description: - name: filesize - sha256: f53df1f27ff60e466eefcd9df239e02d4722d5e2debee92a87dfd99ac66de2af - url: "https://pub.dev" - source: hosted - version: "2.0.1" flutter: dependency: "direct main" description: flutter @@ -331,26 +323,26 @@ packages: dependency: transitive description: name: just_audio - sha256: "7a5057a4d05c8f88ee968cec6fdfe1015577d5184e591d5ac15ab16d8f5ecb17" + sha256: "5ed0cd723e17dfd8cd4b0253726221e67f6546841ea4553635cf895061fc335b" url: "https://pub.dev" source: hosted - version: "0.9.31" + version: "0.9.35" just_audio_platform_interface: dependency: transitive description: name: just_audio_platform_interface - sha256: eff112d5138bea3ba544b6338b1e0537a32b5e1425e4d0dc38f732771cda7c84 + sha256: d8409da198bbc59426cd45d4c92fca522a2ec269b576ce29459d6d6fcaeb44df url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.2.1" just_audio_web: dependency: transitive description: name: just_audio_web - sha256: "89d8db6f19f3821bb6bf908c4bfb846079afb2ab575b783d781a6bf119e3abaf" + sha256: ff62f733f437b25a0ff590f0e295fa5441dcb465f1edbdb33b3dea264705bc13 url: "https://pub.dev" source: hosted - version: "0.4.7" + version: "0.4.8" libphonenumber_platform_interface: dependency: transitive description: From f62ff877874f023fb7f6e86403a86800cb79446f Mon Sep 17 00:00:00 2001 From: Andre Rossi Date: Fri, 27 Oct 2023 13:23:32 -0300 Subject: [PATCH 11/29] feat: added a progressive loading --- analysis_options.yaml | 4 ++ .../ds_image_message_bubble.controller.dart | 10 ++-- .../ds_video_message_bubble.controller.dart | 39 ++++++++------- .../video/ds_video_message_bubble.widget.dart | 47 ++++++++++++------- 4 files changed, 58 insertions(+), 42 deletions(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index a5744c1c..e0eb3931 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,8 @@ include: package:flutter_lints/flutter.yaml +linter: + rules: + - prefer_relative_imports + # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options diff --git a/lib/src/controllers/chat/ds_image_message_bubble.controller.dart b/lib/src/controllers/chat/ds_image_message_bubble.controller.dart index 38baf0f5..7cef0da9 100644 --- a/lib/src/controllers/chat/ds_image_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_image_message_bubble.controller.dart @@ -2,12 +2,13 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:blip_ds/src/utils/ds_directory_formatter.util.dart'; import 'package:crypto/crypto.dart'; import 'package:get/get.dart'; +import 'package:path/path.dart' as path_utils; import '../../services/ds_auth.service.dart'; import '../../services/ds_file.service.dart'; +import '../../utils/ds_directory_formatter.util.dart'; class DSImageMessageBubbleController extends GetxController { final maximumProgress = RxInt(0); @@ -49,14 +50,11 @@ class DSImageMessageBubbleController extends GetxController { return; } - final fileName = fullPath.split('/').last; - final path = fullPath.substring(0, fullPath.lastIndexOf('/')); - try { final savedFilePath = await DSFileService.download( url, - fileName, - path: path, + path_utils.basename(fullPath), + path: path_utils.dirname(fullPath), onReceiveProgress: _onReceiveProgress, httpHeaders: shouldAuthenticate ? DSAuthService.httpHeaders : null, ); diff --git a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart index 9a73ee87..a8660f57 100644 --- a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart @@ -3,21 +3,22 @@ import 'dart:io'; import 'package:crypto/crypto.dart'; import 'package:ffmpeg_kit_flutter_full_gpl/ffmpeg_kit.dart'; -import 'package:ffmpeg_kit_flutter_full_gpl/return_code.dart'; import 'package:file_sizes/file_sizes.dart'; import 'package:get/get.dart'; +import 'package:path/path.dart' as path_utils; import '../../models/ds_toast_props.model.dart'; import '../../services/ds_file.service.dart'; import '../../services/ds_toast.service.dart'; import '../../utils/ds_directory_formatter.util.dart'; -import '../../widgets/chat/video/ds_video_error.dialog.dart'; class DSVideoMessageBubbleController { final String url; final int mediaSize; final Map? httpHeaders; final String type; + final maximumProgress = RxInt(0); + final downloadProgress = RxInt(0); DSVideoMessageBubbleController({ required this.url, @@ -90,28 +91,17 @@ class DSVideoMessageBubbleController { if (!await outputFile.exists()) { final inputFilePath = await DSFileService.download( url, - fileName, + path_utils.basename(fullPath), + path: path_utils.dirname(fullPath), + onReceiveProgress: (current, max) { + downloadProgress.value = current; + maximumProgress.value = max; + }, httpHeaders: httpHeaders, ); - final session = await FFmpegKit.execute( - '-hide_banner -y -i "$inputFilePath" "${outputFile.path}"'); - - File(inputFilePath!).delete(); - - final returnCode = await session.getReturnCode(); - - if (!ReturnCode.isSuccess(returnCode)) { - hasError.value = true; - await DSVideoErrorDialog.show( - filename: fileName, - url: url, - httpHeaders: httpHeaders, - ); - } + _generateThumbnail(inputFilePath!); } - - _generateThumbnail(outputFile.path); } catch (_) { hasError.value = true; @@ -136,4 +126,13 @@ class DSVideoMessageBubbleController { thumbnail.value = thumbnailPath; } + + String getDownloadProgress() { + String getSize(int value) => FileSize.getSize( + value, + precision: PrecisionValue.One, + ); + + return '${getSize(downloadProgress.value)} / ${getSize(maximumProgress.value)}'; + } } diff --git a/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart b/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart index 33a3916b..0d1e4196 100644 --- a/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart +++ b/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart @@ -10,8 +10,8 @@ import '../../../models/ds_message_bubble_style.model.dart'; import '../../../services/ds_auth.service.dart'; import '../../../themes/colors/ds_colors.theme.dart'; import '../../../themes/icons/ds_icons.dart'; -import '../../animations/ds_fading_circle_loading.widget.dart'; import '../../buttons/ds_button.widget.dart'; +import '../../texts/ds_caption_small_text.widget.dart'; import '../ds_message_bubble.widget.dart'; import '../ds_show_more_text.widget.dart'; import 'ds_video_body.widget.dart'; @@ -135,21 +135,8 @@ class _DSVideoMessageBubbleState extends State size: 80.0, color: DSColors.neutralDarkRooftop, ) - : (_controller.isDownloading.value || - _controller.isLoadingThumbnail.value) - ? Center( - child: Container( - height: 50.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: foregroundColor, - ), - child: DSFadingCircleLoading( - color: backgroundLoadingColor, - size: 45.0, - ), - ), - ) + : _controller.isDownloading.value + ? _buildDownloadProgress(foregroundColor) : _controller.thumbnail.isEmpty ? Center( child: SizedBox( @@ -204,4 +191,32 @@ class _DSVideoMessageBubbleState extends State ), ); } + + Widget _buildDownloadProgress(final Color foregroundColor) { + final double percent = _controller.maximumProgress.value > 0 + ? _controller.downloadProgress.value / _controller.maximumProgress.value + : 0; + + return AnimatedOpacity( + opacity: _controller.maximumProgress.value > 0 ? 1 : 0, + duration: const Duration(milliseconds: 250), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: CircularProgressIndicator( + color: foregroundColor, + backgroundColor: Colors.grey, + value: percent, + ), + ), + DSCaptionSmallText( + _controller.getDownloadProgress(), + color: foregroundColor, + ) + ], + ), + ); + } } From 79b560cf2d9c15be1b4f8640c55bcecd80364184 Mon Sep 17 00:00:00 2001 From: Andre Rossi Date: Mon, 30 Oct 2023 13:38:33 -0300 Subject: [PATCH 12/29] chore: update new version --- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa37039a..1f1910da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.85 + +- [DSVideoMessageBubble] Added a progressive loading + ## 0.0.84 - [DSImageMessageBubble] Improved the performance of the image bubble diff --git a/pubspec.yaml b/pubspec.yaml index e4986f77..f66c5c4f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: blip_ds description: Blip Design System for Flutter. -version: 0.0.84 +version: 0.0.85 homepage: https://github.com/takenet/blip-ds-flutter#readme repository: https://github.com/takenet/blip-ds-flutter From 2f5c990d2f17fa634dfb021c8c6735d5977b0f56 Mon Sep 17 00:00:00 2001 From: Andre Rossi Date: Mon, 30 Oct 2023 15:48:26 -0300 Subject: [PATCH 13/29] fix: removed code --- .../widgets/chat/video/ds_video_message_bubble.widget.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart b/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart index 0d1e4196..f5eee8fc 100644 --- a/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart +++ b/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart @@ -97,11 +97,6 @@ class _DSVideoMessageBubbleState extends State ? DSColors.neutralDarkCity : DSColors.neutralLightSnow; - final backgroundLoadingColor = - widget.style.isLightBubbleBackground(widget.align) - ? DSColors.neutralLightSnow - : DSColors.neutralDarkCity; - final buttonBorderColor = widget.style.isLightBubbleBackground(widget.align) ? DSColors.primaryNight : DSColors.neutralLightSnow; From 7dafa5c26c9064e31d8ef576f55e8c4605ac546e Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Mon, 30 Oct 2023 20:42:11 -0300 Subject: [PATCH 14/29] feat: video compression general improvements --- .../ds_file_message_bubble.controller.dart | 4 +- .../ds_image_message_bubble.controller.dart | 16 +- .../ds_video_message_bubble.controller.dart | 47 +++--- .../ds_video_player.controller.dart | 3 - lib/src/models/ds_media_info.model.dart | 21 --- lib/src/services/ds_ffmpeg.service.dart | 154 +++++------------- lib/src/services/ds_file.service.dart | 31 ++-- .../utils/ds_directory_formatter.util.dart | 13 +- .../audio/ds_audio_message_bubble.widget.dart | 4 - .../chat/audio/ds_audio_player.widget.dart | 88 ++++++---- .../chat/ds_file_message_bubble.widget.dart | 1 - .../chat/video/ds_video_error.dialog.dart | 15 +- lib/src/widgets/utils/ds_card.widget.dart | 1 - .../widgets/utils/ds_user_avatar.widget.dart | 19 ++- pubspec.yaml | 1 + .../sample_message_bubble.showcase.dart | 2 - .../Flutter/GeneratedPluginRegistrant.swift | 2 + sample/pubspec.lock | 8 + 18 files changed, 183 insertions(+), 247 deletions(-) delete mode 100644 lib/src/models/ds_media_info.model.dart diff --git a/lib/src/controllers/chat/ds_file_message_bubble.controller.dart b/lib/src/controllers/chat/ds_file_message_bubble.controller.dart index a72dae42..a4cb43ef 100644 --- a/lib/src/controllers/chat/ds_file_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_file_message_bubble.controller.dart @@ -16,13 +16,11 @@ class DSFileMessageBubbleController extends GetxController { } Future openFile({ - required final String filename, required final String url, final Map? httpHeaders, }) => DSFileService.open( - filename, - url, + url: url, onDownloadStateChange: (loading) => isDownloading.value = loading, httpHeaders: httpHeaders, ); diff --git a/lib/src/controllers/chat/ds_image_message_bubble.controller.dart b/lib/src/controllers/chat/ds_image_message_bubble.controller.dart index 38baf0f5..9540bb13 100644 --- a/lib/src/controllers/chat/ds_image_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_image_message_bubble.controller.dart @@ -39,24 +39,20 @@ class DSImageMessageBubbleController extends GetxController { final uri = Uri.parse(url); - final fullPath = await DSDirectoryFormatter.getPath( + final cachePath = await DSDirectoryFormatter.getCachePath( type: mediaType!, - fileName: md5.convert(utf8.encode(uri.path)).toString(), + filename: md5.convert(utf8.encode(uri.path)).toString(), ); - if (await File(fullPath).exists()) { - localPath.value = fullPath; + if (File(cachePath).existsSync()) { + localPath.value = cachePath; return; } - final fileName = fullPath.split('/').last; - final path = fullPath.substring(0, fullPath.lastIndexOf('/')); - try { final savedFilePath = await DSFileService.download( - url, - fileName, - path: path, + url: url, + path: cachePath, onReceiveProgress: _onReceiveProgress, httpHeaders: shouldAuthenticate ? DSAuthService.httpHeaders : null, ); diff --git a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart index 8c896a05..fb77b914 100644 --- a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart @@ -10,7 +10,6 @@ import '../../services/ds_ffmpeg.service.dart'; import '../../services/ds_file.service.dart'; import '../../services/ds_toast.service.dart'; import '../../utils/ds_directory_formatter.util.dart'; -import '../../widgets/chat/video/ds_video_error.dialog.dart'; class DSVideoMessageBubbleController { final String url; @@ -46,19 +45,23 @@ class DSVideoMessageBubbleController { try { isLoadingThumbnail.value = true; final fileName = md5.convert(utf8.encode(Uri.parse(url).path)).toString(); - final fullPath = await DSDirectoryFormatter.getPath( + + final fullPath = await DSDirectoryFormatter.getCachePath( type: type, - fileName: fileName, + filename: fileName, ); - final fullThumbnailPath = await DSDirectoryFormatter.getPath( + + final fullThumbnailPath = await DSDirectoryFormatter.getCachePath( type: 'image/png', - fileName: '$fileName-thumbnail', + filename: '$fileName-thumbnail', ); + final file = File(fullPath); final thumbnailfile = File(fullThumbnailPath); - if (await thumbnailfile.exists()) { + + if (thumbnailfile.existsSync()) { thumbnail.value = thumbnailfile.path; - } else if (await file.exists() && thumbnail.value.isEmpty) { + } else if (file.existsSync() && thumbnail.value.isEmpty) { await _generateThumbnail(file.path); } } finally { @@ -68,28 +71,27 @@ class DSVideoMessageBubbleController { Future getFullThumbnailPath() async { final fileName = md5.convert(utf8.encode(Uri.parse(url).path)).toString(); - final mediaPath = await DSDirectoryFormatter.getPath( + + return DSDirectoryFormatter.getCachePath( type: 'image/png', - fileName: '$fileName-thumbnail', + filename: '$fileName-thumbnail', ); - return mediaPath; } Future downloadVideo() async { - final fileName = md5.convert(utf8.encode(Uri.parse(url).path)).toString(); isDownloading.value = true; try { - final fullPath = await DSDirectoryFormatter.getPath( + final cachePath = await DSDirectoryFormatter.getCachePath( type: 'video/mp4', - fileName: fileName, + filename: md5.convert(utf8.encode(Uri.parse(url).path)).toString(), ); - final outputFile = File(fullPath); - if (!await outputFile.exists()) { + final outputFile = File(cachePath); + + if (!outputFile.existsSync()) { final inputFilePath = await DSFileService.download( - url, - fileName, + url: url, httpHeaders: httpHeaders, ); @@ -98,16 +100,9 @@ class DSVideoMessageBubbleController { output: outputFile.path, ); - File(inputFilePath).delete(); + File(inputFilePath).deleteSync(); - if (!isSuccess) { - hasError.value = true; - await DSVideoErrorDialog.show( - filename: fileName, - url: url, - httpHeaders: httpHeaders, - ); - } + hasError.value = !isSuccess; } _generateThumbnail(outputFile.path); diff --git a/lib/src/controllers/ds_video_player.controller.dart b/lib/src/controllers/ds_video_player.controller.dart index 00f50711..578ddea5 100644 --- a/lib/src/controllers/ds_video_player.controller.dart +++ b/lib/src/controllers/ds_video_player.controller.dart @@ -69,10 +69,7 @@ class DSVideoPlayerController extends GetxController { Get.back(); Get.delete(); - final filename = url.substring(url.lastIndexOf('/')).substring(1); - await DSVideoErrorDialog.show( - filename: filename, url: url, httpHeaders: shouldAuthenticate ? DSAuthService.httpHeaders : null, ); diff --git a/lib/src/models/ds_media_info.model.dart b/lib/src/models/ds_media_info.model.dart deleted file mode 100644 index e9fad92d..00000000 --- a/lib/src/models/ds_media_info.model.dart +++ /dev/null @@ -1,21 +0,0 @@ -class DSMediaInfo { - final int? width; - final int? height; - final int? size; - - DSMediaInfo({ - this.width, - this.height, - this.size, - }); - - bool get hasDimensions => width != null && height != null; - - bool get isWidescreen => hasDimensions && width! > height!; - - double? get aspectRatio => hasDimensions - ? isWidescreen - ? width! / height! - : height! / width! - : null; -} diff --git a/lib/src/services/ds_ffmpeg.service.dart b/lib/src/services/ds_ffmpeg.service.dart index 292abc99..eeeac7b7 100644 --- a/lib/src/services/ds_ffmpeg.service.dart +++ b/lib/src/services/ds_ffmpeg.service.dart @@ -1,39 +1,57 @@ +import 'dart:async'; +import 'dart:io'; + import 'package:ffmpeg_kit_flutter_full_gpl/ffmpeg_kit.dart'; -import 'package:ffmpeg_kit_flutter_full_gpl/ffprobe_kit.dart'; import 'package:ffmpeg_kit_flutter_full_gpl/return_code.dart'; - -import '../models/ds_media_info.model.dart'; +import 'package:video_compress/video_compress.dart'; abstract class DSFFmpegService { - static const _maxSize = 10485760; // 10 MB - static const _targetResolution = 480; // 480p - static Future formatVideo({ required final String input, required final String output, - final bool shouldCompress = true, }) async { - final args = shouldCompress - ? await _getCompressVideoArgs( - path: input, - ) - : null; + var video = File(input); - final command = - '-hwaccel auto -i "$input" ${args != null ? '$args ' : ''}"$output"'; + if (input != output) { + final inputExtension = input.substring( + input.lastIndexOf('.') + 1, + ); - return _executeCommand( - command: command, - ); + final outputExtension = output.substring( + output.lastIndexOf('.') + 1, + ); + + if (inputExtension != outputExtension) { + final temp = await VideoCompress.compressVideo( + input, + quality: VideoQuality.HighestQuality, + frameRate: 60, + ); + + if (temp?.file != null) { + video.deleteSync(); + video = temp!.file!.copySync(output); + temp.file!.deleteSync(); + } + } else { + video = video.copySync(output); + } + } + + return video.exists(); } static Future getVideoThumbnail({ required final String input, required final String output, - }) => - _executeCommand( - command: '-i "$input" $_thumbnailArgs "$output"', - ); + }) async { + final temp = await VideoCompress.getFileThumbnail(input); + + final thumbnail = temp.copySync(output); + temp.deleteSync(); + + return thumbnail.exists(); + } static Future transcodeAudio({ required final String input, @@ -53,75 +71,6 @@ abstract class DSFFmpegService { '-i "$firstInput" -i "$secondInput" $_mergeAudioArgs "$output"', ); - static Future getMediaInfo({ - required final String path, - }) async { - int? width; - int? height; - int? size; - int? rotation; - - final session = await FFprobeKit.getMediaInformation(path); - final info = session.getMediaInformation(); - - if (info != null) { - size = int.tryParse(info.getSize() ?? '') ?? 0; - - final streams = info.getStreams(); - - if (streams.isNotEmpty) { - final properties = streams.first.getAllProperties(); - final List? sideData = properties?['side_data_list']; - - width = properties?['width']; - height = properties?['height']; - rotation = sideData?.first['rotation']; - } - } - - final isFlipped = rotation != null && rotation != 0; - - return DSMediaInfo( - width: isFlipped ? height : width, - height: isFlipped ? width : height, - size: size, - ); - } - - static Future _getCompressVideoArgs({ - required final String path, - }) async { - final info = await getMediaInfo( - path: path, - ); - - if (info.hasDimensions) { - final isOversized = - info.size == null || (info.size != null && info.size! > _maxSize); - - final isOverdimensioned = - info.width! > _targetResolution && info.height! > _targetResolution; - - if (isOversized && isOverdimensioned) { - final compressedResolution = - (info.aspectRatio! * _targetResolution).ceil(); - - final compressedWidth = - info.isWidescreen ? compressedResolution : _targetResolution; - - final compressedHeight = - info.isWidescreen ? _targetResolution : compressedResolution; - - return _compressVideoArgs( - width: compressedWidth, - height: compressedHeight, - ); - } - } - - return null; - } - static Future _executeCommand({ required final String command, }) async { @@ -141,17 +90,10 @@ abstract class DSFFmpegService { return '$hideInfoBanner $answerYes'; } - static String get _thumbnailArgs { - const getOneFrame = '-vframes 1'; - - return getOneFrame; - } - static String get _transcodeAudioArgs { - const codec = '-c:a libmp3lame'; - const quality = '-qscale:a 2'; + const codec = '-c:a aac'; - return '$codec $quality'; + return codec; } static String get _mergeAudioArgs { @@ -162,18 +104,4 @@ abstract class DSFFmpegService { return '$filter "$firstInput$secondInput$audioConcat"'; } - - static String _compressVideoArgs({ - required final int width, - required final int height, - }) { - final resolution = '-s ${width}x$height'; - const pixelFormat = '-pix_fmt yuv420p'; - const codec = '-c:v libx264'; - const preset = '-preset veryfast'; - const quality = '-crf 23'; - const audio = '-c:a aac'; - - return '$resolution $pixelFormat $codec $preset $quality $audio'; - } } diff --git a/lib/src/services/ds_file.service.dart b/lib/src/services/ds_file.service.dart index 1952ca00..a8ff487d 100644 --- a/lib/src/services/ds_file.service.dart +++ b/lib/src/services/ds_file.service.dart @@ -8,24 +8,24 @@ import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; abstract class DSFileService { - static Future open( - final String filename, - final String url, { + static Future open({ + required final String url, + final String? path, final void Function(bool)? onDownloadStateChange, final Map? httpHeaders, }) async { - final path = await download( - url, - filename, + final filePath = await download( + url: url, + path: path, onDownloadStateChange: onDownloadStateChange, httpHeaders: httpHeaders, ); - if (path?.isEmpty ?? true) { + if (filePath?.isEmpty ?? true) { return; } - final result = await OpenFilex.open(path); + final result = await OpenFilex.open(filePath); switch (result.type) { case ResultType.done: @@ -41,9 +41,8 @@ abstract class DSFileService { } } - static Future download( - final String url, - final String filename, { + static Future download({ + required final String url, final String? path, final void Function(bool)? onDownloadStateChange, final Map? httpHeaders, @@ -51,10 +50,14 @@ abstract class DSFileService { }) async { try { onDownloadStateChange?.call(true); - final savedFilePath = path_utils.join( - path ?? (await getTemporaryDirectory()).path, filename); - if (await File(savedFilePath).exists()) { + final savedFilePath = path ?? + path_utils.join( + (await getTemporaryDirectory()).path, + DateTime.now().toIso8601String(), + ); + + if (File(savedFilePath).existsSync()) { return savedFilePath; } diff --git a/lib/src/utils/ds_directory_formatter.util.dart b/lib/src/utils/ds_directory_formatter.util.dart index 49fb68c0..a03a5260 100644 --- a/lib/src/utils/ds_directory_formatter.util.dart +++ b/lib/src/utils/ds_directory_formatter.util.dart @@ -4,19 +4,22 @@ import 'package:get/get.dart'; import 'package:path_provider/path_provider.dart'; abstract class DSDirectoryFormatter { - static Future getPath({ + static Future getCachePath({ required final String type, - required final String fileName, + required final String filename, + String? extension, }) async { final cachePath = (await getApplicationCacheDirectory()).path; - final typeFolder = '${type.split('/').first.capitalizeFirst}'; - final extension = type.split('/').last; + final typeComponents = type.split('/'); + + final typeFolder = '${typeComponents.first.capitalizeFirst}'; + extension ??= typeComponents.last; final typePrefix = '${typeFolder.substring(0, 3).toUpperCase()}-'; final newFileName = - '${!fileName.startsWith(typePrefix) ? typePrefix : ''}$fileName'; + '${!filename.startsWith(typePrefix) ? typePrefix : ''}$filename'; final path = await _formatDirectory( type: typeFolder, diff --git a/lib/src/widgets/chat/audio/ds_audio_message_bubble.widget.dart b/lib/src/widgets/chat/audio/ds_audio_message_bubble.widget.dart index e1c955d4..ae9f308b 100644 --- a/lib/src/widgets/chat/audio/ds_audio_message_bubble.widget.dart +++ b/lib/src/widgets/chat/audio/ds_audio_message_bubble.widget.dart @@ -13,14 +13,12 @@ class DSAudioMessageBubble extends StatelessWidget { final List borderRadius; final DSMessageBubbleStyle style; final String? uniqueId; - final String audioType; final bool shouldAuthenticate; DSAudioMessageBubble({ super.key, required this.uri, required this.align, - required this.audioType, this.uniqueId, this.borderRadius = const [DSBorderRadius.all], this.shouldAuthenticate = false, @@ -44,8 +42,6 @@ class DSAudioMessageBubble extends StatelessWidget { ), child: DSAudioPlayer( uri: uri, - uniqueId: uniqueId, - audioType: audioType, shouldAuthenticate: shouldAuthenticate, controlForegroundColor: isLightBubbleBackground ? DSColors.neutralDarkRooftop diff --git a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart index a38a70a9..209b3cec 100644 --- a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart +++ b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart @@ -1,15 +1,17 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:just_audio/just_audio.dart'; -import 'package:path_provider/path_provider.dart'; import '../../../controllers/chat/ds_audio_player.controller.dart'; import '../../../services/ds_auth.service.dart'; import '../../../services/ds_ffmpeg.service.dart'; import '../../../services/ds_file.service.dart'; import '../../../themes/colors/ds_colors.theme.dart'; +import '../../../utils/ds_directory_formatter.util.dart'; import '../../buttons/ds_pause_button.widget.dart'; import '../../buttons/ds_play_button.widget.dart'; import 'ds_audio_seek_bar.widget.dart'; @@ -17,8 +19,6 @@ import 'ds_audio_speed_button.widget.dart'; class DSAudioPlayer extends StatefulWidget { final Uri uri; - final String uniqueId; - final String audioType; final Color labelColor; final Color bufferActiveTrackColor; final Color bufferInactiveTrackColor; @@ -29,10 +29,9 @@ class DSAudioPlayer extends StatefulWidget { final Color speedBorderColor; final bool shouldAuthenticate; - DSAudioPlayer({ + const DSAudioPlayer({ super.key, required this.uri, - required this.audioType, required this.labelColor, required this.bufferActiveTrackColor, required this.bufferInactiveTrackColor, @@ -42,8 +41,7 @@ class DSAudioPlayer extends StatefulWidget { required this.speedForegroundColor, required this.speedBorderColor, this.shouldAuthenticate = false, - final String? uniqueId, - }) : uniqueId = uniqueId ?? DateTime.now().toIso8601String(); + }); @override State createState() => _DSAudioPlayerState(); @@ -114,16 +112,7 @@ class _DSAudioPlayerState extends State ); try { - Platform.isIOS && widget.audioType.contains('ogg') - ? await _transcoder() - : await _controller.player.setAudioSource( - AudioSource.uri( - widget.uri, - headers: widget.shouldAuthenticate - ? DSAuthService.httpHeaders - : null, - ), - ); + await _loadAudio(); _controller.isInitialized.value = true; } catch (_) { @@ -131,28 +120,63 @@ class _DSAudioPlayerState extends State } } - Future _transcoder() async { - final inputFileName = 'AUDIO-${widget.uniqueId}.ogg'; + Future _loadAudio() async { + final outputPath = await DSDirectoryFormatter.getCachePath( + type: 'audio/mp4', + filename: md5.convert(utf8.encode(widget.uri.path)).toString(), + extension: 'm4a', + ); - final inputFilePath = await DSFileService.download( - widget.uri.toString(), - inputFileName, - httpHeaders: widget.shouldAuthenticate ? DSAuthService.httpHeaders : null, + final outputFile = File(outputPath); + var hasCachedFile = outputFile.existsSync(); + + if (!hasCachedFile) { + await _downloadAudio( + outputPath: outputPath, + ); + + hasCachedFile = outputFile.existsSync(); + } + + await _controller.player.setAudioSource( + hasCachedFile + ? AudioSource.file( + outputPath, + ) + : AudioSource.uri( + widget.uri, + headers: + widget.shouldAuthenticate ? DSAuthService.httpHeaders : null, + ), ); + } - final temporaryPath = (await getTemporaryDirectory()).path; - final outputFile = File("$temporaryPath/AUDIO-${widget.uniqueId}.mp3"); + Future _downloadAudio({ + required final String outputPath, + }) async { + final tempPath = await DSFileService.download( + url: widget.uri.toString(), + httpHeaders: widget.shouldAuthenticate ? DSAuthService.httpHeaders : null, + ); - if (await outputFile.exists()) { - await _controller.player.setFilePath(outputFile.path); - } else { + if (tempPath?.isNotEmpty ?? false) { final isSuccess = await DSFFmpegService.transcodeAudio( - input: inputFilePath!, - output: outputFile.path, + input: tempPath!, + output: outputPath, ); - if (isSuccess) { - await _controller.player.setFilePath(outputFile.path); + final tempFile = File(tempPath); + + if (tempFile.existsSync()) { + tempFile.deleteSync(); + } + + if (!isSuccess) { + final outputFile = File(outputPath); + + if (outputFile.existsSync()) { + outputFile.deleteSync(); + } } } } diff --git a/lib/src/widgets/chat/ds_file_message_bubble.widget.dart b/lib/src/widgets/chat/ds_file_message_bubble.widget.dart index 5f605fab..625ed975 100644 --- a/lib/src/widgets/chat/ds_file_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_file_message_bubble.widget.dart @@ -40,7 +40,6 @@ class DSFileMessageBubble extends StatelessWidget { Widget build(BuildContext context) { return GestureDetector( onTap: () => controller.openFile( - filename: filename, url: url, httpHeaders: shouldAuthenticate ? DSAuthService.httpHeaders : null, ), diff --git a/lib/src/widgets/chat/video/ds_video_error.dialog.dart b/lib/src/widgets/chat/video/ds_video_error.dialog.dart index 0b2c0da9..cb27bd9e 100644 --- a/lib/src/widgets/chat/video/ds_video_error.dialog.dart +++ b/lib/src/widgets/chat/video/ds_video_error.dialog.dart @@ -1,13 +1,16 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; import 'package:get/get.dart'; import '../../../services/ds_dialog.service.dart'; import '../../../services/ds_file.service.dart'; +import '../../../utils/ds_directory_formatter.util.dart'; import '../../buttons/ds_primary_button.widget.dart'; import '../../buttons/ds_secondary_button.widget.dart'; abstract class DSVideoErrorDialog { static Future show({ - required final String filename, required final String url, final Map? httpHeaders, }) async { @@ -19,9 +22,15 @@ abstract class DSVideoErrorDialog { primaryButton: DSPrimaryButton( onPressed: () async { Get.back(); + + final cachePath = await DSDirectoryFormatter.getCachePath( + type: 'video/mp4', + filename: md5.convert(utf8.encode(Uri.parse(url).path)).toString(), + ); + await DSFileService.open( - filename, - url, + url: url, + path: cachePath, httpHeaders: httpHeaders, ); }, diff --git a/lib/src/widgets/utils/ds_card.widget.dart b/lib/src/widgets/utils/ds_card.widget.dart index 9744fd57..1159404b 100644 --- a/lib/src/widgets/utils/ds_card.widget.dart +++ b/lib/src/widgets/utils/ds_card.widget.dart @@ -255,7 +255,6 @@ class DSCard extends StatelessWidget { borderRadius: borderRadius, style: style, uniqueId: messageId, - audioType: media.type, shouldAuthenticate: shouldAuthenticate, ); } else if (media.type.contains('image')) { diff --git a/lib/src/widgets/utils/ds_user_avatar.widget.dart b/lib/src/widgets/utils/ds_user_avatar.widget.dart index 8671bde2..4e8458a1 100644 --- a/lib/src/widgets/utils/ds_user_avatar.widget.dart +++ b/lib/src/widgets/utils/ds_user_avatar.widget.dart @@ -46,15 +46,16 @@ class DSUserAvatar extends StatelessWidget { String get _initials { String initials = ''; - if ((text?.isNotEmpty ?? false) && (int.tryParse(text!) == null)) { - initials = - RegExp(text!.split(' ').length >= 2 ? r'\b[A-Za-z]' : r'[A-Za-z]') - .allMatches(text!) - .map((m) => m.group(0)) - .join() - .toUpperCase(); + if (text?.isNotEmpty ?? false) { + initials = RegExp('${text!.split(' ').length >= 2 ? '\\b' : ''}[A-Za-z]') + .allMatches(text!) + .map((m) => m.group(0)) + .join() + .toUpperCase(); - initials = initials.substring(0, initials.length >= 2 ? 2 : 1); + if (initials.isNotEmpty) { + initials = initials.substring(0, initials.length >= 2 ? 2 : 1); + } } return initials; @@ -66,7 +67,7 @@ class DSUserAvatar extends StatelessWidget { backgroundColor: backgroundColor, child: Padding( padding: const EdgeInsets.all(2.0), - child: int.tryParse(text!) == null + child: _initials.isNotEmpty ? DSBodyText( _initials, color: textColor, diff --git a/pubspec.yaml b/pubspec.yaml index ce653e68..0c6cadc7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: map_launcher: ^2.5.0+1 mime: ^1.0.4 crypto: ^3.0.3 + video_compress: ^3.1.2 dev_dependencies: flutter_test: diff --git a/sample/lib/widgets/showcase/sample_message_bubble.showcase.dart b/sample/lib/widgets/showcase/sample_message_bubble.showcase.dart index a0f33e2a..4e226076 100644 --- a/sample/lib/widgets/showcase/sample_message_bubble.showcase.dart +++ b/sample/lib/widgets/showcase/sample_message_bubble.showcase.dart @@ -237,13 +237,11 @@ class SampleMessageBubbleShowcase extends StatelessWidget { align: DSAlign.left, uri: Uri.parse(_sampleAudio), uniqueId: 'audio1', - audioType: 'mp3', ), DSAudioMessageBubble( align: DSAlign.right, uri: Uri.parse(_sampleAudio), uniqueId: 'audio2', - audioType: 'mp3', ), DSVideoMessageBubble( align: DSAlign.right, diff --git a/sample/macos/Flutter/GeneratedPluginRegistrant.swift b/sample/macos/Flutter/GeneratedPluginRegistrant.swift index 803cf2c1..3744928a 100644 --- a/sample/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/sample/macos/Flutter/GeneratedPluginRegistrant.swift @@ -11,6 +11,7 @@ import just_audio import path_provider_foundation import sqflite import url_launcher_macos +import video_compress import wakelock_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { @@ -20,5 +21,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) } diff --git a/sample/pubspec.lock b/sample/pubspec.lock index 1bc07fec..60ee19a0 100644 --- a/sample/pubspec.lock +++ b/sample/pubspec.lock @@ -852,6 +852,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + video_compress: + dependency: transitive + description: + name: video_compress + sha256: "407693726e674a1e1958801deb2d9daf5a5297707ba6d03375007012dae7389a" + url: "https://pub.dev" + source: hosted + version: "3.1.2" video_player: dependency: transitive description: From 8b5d0f7ca9b90ece623cd8a36db8a308ea0ae586 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Tue, 31 Oct 2023 09:22:11 -0300 Subject: [PATCH 15/29] feat: delete input file after formatting video --- lib/src/services/ds_ffmpeg.service.dart | 13 ++++++++----- .../ds_custom_replies_icon_button.widget.dart | 3 +++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/src/services/ds_ffmpeg.service.dart b/lib/src/services/ds_ffmpeg.service.dart index eeeac7b7..b3092964 100644 --- a/lib/src/services/ds_ffmpeg.service.dart +++ b/lib/src/services/ds_ffmpeg.service.dart @@ -10,7 +10,7 @@ abstract class DSFFmpegService { required final String input, required final String output, }) async { - var video = File(input); + var outputVideo = File(output); if (input != output) { final inputExtension = input.substring( @@ -21,6 +21,8 @@ abstract class DSFFmpegService { output.lastIndexOf('.') + 1, ); + var inputVideo = File(input); + if (inputExtension != outputExtension) { final temp = await VideoCompress.compressVideo( input, @@ -29,16 +31,17 @@ abstract class DSFFmpegService { ); if (temp?.file != null) { - video.deleteSync(); - video = temp!.file!.copySync(output); + temp!.file!.copySync(output); temp.file!.deleteSync(); } } else { - video = video.copySync(output); + inputVideo.copySync(output); } + + inputVideo.deleteSync(); } - return video.exists(); + return outputVideo.exists(); } static Future getVideoThumbnail({ diff --git a/lib/src/widgets/buttons/ds_custom_replies_icon_button.widget.dart b/lib/src/widgets/buttons/ds_custom_replies_icon_button.widget.dart index 1f51a3ef..138526ae 100644 --- a/lib/src/widgets/buttons/ds_custom_replies_icon_button.widget.dart +++ b/lib/src/widgets/buttons/ds_custom_replies_icon_button.widget.dart @@ -7,17 +7,20 @@ import 'ds_icon_button.widget.dart'; class DSCustomRepliesIconButton extends StatelessWidget { final void Function() onPressed; final bool isVisible; + final bool isLoading; const DSCustomRepliesIconButton({ super.key, required this.onPressed, this.isVisible = true, + this.isLoading = false, }); @override Widget build(BuildContext context) => Visibility( visible: isVisible, child: DSIconButton( + isLoading: isLoading, onPressed: onPressed, icon: const Icon( DSIcons.message_talk_outline, From a00611e30d1728dc78cd4c18aad2d6ce908e6a28 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Tue, 31 Oct 2023 14:09:30 -0300 Subject: [PATCH 16/29] fix: remove deleteSync after video was downloaded --- .../controllers/chat/ds_video_message_bubble.controller.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart index fb77b914..79904663 100644 --- a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart @@ -100,8 +100,6 @@ class DSVideoMessageBubbleController { output: outputFile.path, ); - File(inputFilePath).deleteSync(); - hasError.value = !isSuccess; } From d13379079f1dbd21d4d4014859a9801935a9a24d Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Tue, 31 Oct 2023 16:17:26 -0300 Subject: [PATCH 17/29] fix: download extension in DSFileService --- lib/src/services/ds_ffmpeg.service.dart | 18 ++++++++++-- lib/src/services/ds_file.service.dart | 37 +++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/lib/src/services/ds_ffmpeg.service.dart b/lib/src/services/ds_ffmpeg.service.dart index b3092964..f0ba7726 100644 --- a/lib/src/services/ds_ffmpeg.service.dart +++ b/lib/src/services/ds_ffmpeg.service.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:ffmpeg_kit_flutter_full_gpl/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter_full_gpl/return_code.dart'; +import 'package:path/path.dart' as path_utils; import 'package:video_compress/video_compress.dart'; abstract class DSFFmpegService { @@ -13,17 +14,28 @@ abstract class DSFFmpegService { var outputVideo = File(output); if (input != output) { + final inputHasExtension = path_utils.extension(input).isNotEmpty; + final outputHasExtension = path_utils.extension(output).isNotEmpty; + + final lastInputDotIndex = inputHasExtension ? input.lastIndexOf('.') : -1; + + final lastOutputDotIndex = + outputHasExtension ? output.lastIndexOf('.') : -1; + final inputExtension = input.substring( - input.lastIndexOf('.') + 1, + lastInputDotIndex + 1, ); final outputExtension = output.substring( - output.lastIndexOf('.') + 1, + lastOutputDotIndex + 1, ); var inputVideo = File(input); - if (inputExtension != outputExtension) { + if (lastInputDotIndex >= 0 && + lastOutputDotIndex >= 0 && + inputExtension.isNotEmpty && + inputExtension != outputExtension) { final temp = await VideoCompress.compressVideo( input, quality: VideoQuality.HighestQuality, diff --git a/lib/src/services/ds_file.service.dart b/lib/src/services/ds_file.service.dart index a8ff487d..efd0e060 100644 --- a/lib/src/services/ds_file.service.dart +++ b/lib/src/services/ds_file.service.dart @@ -51,10 +51,10 @@ abstract class DSFileService { try { onDownloadStateChange?.call(true); - final savedFilePath = path ?? + var savedFilePath = path ?? path_utils.join( (await getTemporaryDirectory()).path, - DateTime.now().toIso8601String(), + DateTime.now().toIso8601String().replaceAll('.', ''), ); if (File(savedFilePath).existsSync()) { @@ -73,7 +73,38 @@ abstract class DSFileService { : null, ); - if (response.statusCode == 200) return savedFilePath; + if (response.statusCode == 200) { + final newExtension = getFileExtensionFromMime( + response.headers.map['content-type']?.first, + ); + + if (newExtension.isNotEmpty) { + final hasExtension = path_utils.extension(savedFilePath).isNotEmpty; + + final lastDotIndex = + hasExtension ? savedFilePath.lastIndexOf('.') : -1; + + late final String filename; + late final String savedExtension; + + if (lastDotIndex >= 0) { + filename = savedFilePath.substring(0, lastDotIndex); + savedExtension = savedFilePath.substring(lastDotIndex + 1); + } else { + filename = savedFilePath.substring(0); + savedExtension = ''; + } + + if (newExtension != savedExtension) { + final newFilePath = '$filename.$newExtension'; + + File(savedFilePath).renameSync(newFilePath); + savedFilePath = newFilePath; + } + } + + return savedFilePath; + } } finally { onDownloadStateChange?.call(false); } From 21c66b9a825ffc948c094d53b452e5ed81a5f5bd Mon Sep 17 00:00:00 2001 From: Andre Rossi Date: Tue, 31 Oct 2023 16:28:30 -0300 Subject: [PATCH 18/29] fix: check audio source to download --- lib/src/widgets/chat/audio/ds_audio_player.widget.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart index 209b3cec..11225964 100644 --- a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart +++ b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart @@ -121,6 +121,16 @@ class _DSAudioPlayerState extends State } Future _loadAudio() async { + if (!widget.uri.scheme.startsWith('http')) { + await _controller.player.setAudioSource( + AudioSource.uri( + widget.uri, + ), + ); + + return; + } + final outputPath = await DSDirectoryFormatter.getCachePath( type: 'audio/mp4', filename: md5.convert(utf8.encode(widget.uri.path)).toString(), From c5429c6aedb5e8036ed90011656d8c32a01802e9 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Wed, 1 Nov 2023 08:32:15 -0300 Subject: [PATCH 19/29] chore: update pubspec.lock from sample --- sample/pubspec.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample/pubspec.lock b/sample/pubspec.lock index 60ee19a0..d5a67aec 100644 --- a/sample/pubspec.lock +++ b/sample/pubspec.lock @@ -31,7 +31,7 @@ packages: path: ".." relative: true source: path - version: "0.0.84" + version: "0.0.85" boolean_selector: dependency: transitive description: From 35b472b1ac895056ab3bf930565720d4fed2a303 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Wed, 1 Nov 2023 08:40:38 -0300 Subject: [PATCH 20/29] refactor: rename service from DSFFMpegService to DSMediaFormatService --- lib/blip_ds.dart | 2 +- .../chat/ds_video_message_bubble.controller.dart | 6 +++--- ...{ds_ffmpeg.service.dart => ds_media_format.service.dart} | 2 +- lib/src/widgets/chat/audio/ds_audio_player.widget.dart | 4 ++-- sample/android/build.gradle | 1 + 5 files changed, 8 insertions(+), 7 deletions(-) rename lib/src/services/{ds_ffmpeg.service.dart => ds_media_format.service.dart} (98%) diff --git a/lib/blip_ds.dart b/lib/blip_ds.dart index 34cb395e..759dc59e 100644 --- a/lib/blip_ds.dart +++ b/lib/blip_ds.dart @@ -23,8 +23,8 @@ export 'src/models/ds_toast_props.model.dart' show DSToastProps; export 'src/services/ds_auth.service.dart' show DSAuthService; export 'src/services/ds_bottom_sheet.service.dart' show DSBottomSheetService; export 'src/services/ds_dialog.service.dart' show DSDialogService; -export 'src/services/ds_ffmpeg.service.dart' show DSFFmpegService; export 'src/services/ds_file.service.dart' show DSFileService; +export 'src/services/ds_media_format.service.dart' show DSMediaFormatService; export 'src/services/ds_toast.service.dart' show DSToastService; export 'src/themes/colors/ds_colors.theme.dart' show DSColors; export 'src/themes/colors/ds_linear_gradient.theme.dart' show DSLinearGradient; diff --git a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart index 7827ec21..bade8679 100644 --- a/lib/src/controllers/chat/ds_video_message_bubble.controller.dart +++ b/lib/src/controllers/chat/ds_video_message_bubble.controller.dart @@ -6,8 +6,8 @@ import 'package:file_sizes/file_sizes.dart'; import 'package:get/get.dart'; import '../../models/ds_toast_props.model.dart'; -import '../../services/ds_ffmpeg.service.dart'; import '../../services/ds_file.service.dart'; +import '../../services/ds_media_format.service.dart'; import '../../services/ds_toast.service.dart'; import '../../utils/ds_directory_formatter.util.dart'; @@ -101,7 +101,7 @@ class DSVideoMessageBubbleController { httpHeaders: httpHeaders, ); - final isSuccess = await DSFFmpegService.formatVideo( + final isSuccess = await DSMediaFormatService.formatVideo( input: inputFilePath!, output: cachePath, ); @@ -128,7 +128,7 @@ class DSVideoMessageBubbleController { Future _generateThumbnail(String path) async { final thumbnailPath = await getFullThumbnailPath(); - await DSFFmpegService.getVideoThumbnail( + await DSMediaFormatService.getVideoThumbnail( input: path, output: thumbnailPath, ); diff --git a/lib/src/services/ds_ffmpeg.service.dart b/lib/src/services/ds_media_format.service.dart similarity index 98% rename from lib/src/services/ds_ffmpeg.service.dart rename to lib/src/services/ds_media_format.service.dart index f0ba7726..9dcaf6e3 100644 --- a/lib/src/services/ds_ffmpeg.service.dart +++ b/lib/src/services/ds_media_format.service.dart @@ -6,7 +6,7 @@ import 'package:ffmpeg_kit_flutter_full_gpl/return_code.dart'; import 'package:path/path.dart' as path_utils; import 'package:video_compress/video_compress.dart'; -abstract class DSFFmpegService { +abstract class DSMediaFormatService { static Future formatVideo({ required final String input, required final String output, diff --git a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart index 11225964..4718cdb3 100644 --- a/lib/src/widgets/chat/audio/ds_audio_player.widget.dart +++ b/lib/src/widgets/chat/audio/ds_audio_player.widget.dart @@ -8,8 +8,8 @@ import 'package:just_audio/just_audio.dart'; import '../../../controllers/chat/ds_audio_player.controller.dart'; import '../../../services/ds_auth.service.dart'; -import '../../../services/ds_ffmpeg.service.dart'; import '../../../services/ds_file.service.dart'; +import '../../../services/ds_media_format.service.dart'; import '../../../themes/colors/ds_colors.theme.dart'; import '../../../utils/ds_directory_formatter.util.dart'; import '../../buttons/ds_pause_button.widget.dart'; @@ -170,7 +170,7 @@ class _DSAudioPlayerState extends State ); if (tempPath?.isNotEmpty ?? false) { - final isSuccess = await DSFFmpegService.transcodeAudio( + final isSuccess = await DSMediaFormatService.transcodeAudio( input: tempPath!, output: outputPath, ); diff --git a/sample/android/build.gradle b/sample/android/build.gradle index 3cdaac95..59437ce1 100644 --- a/sample/android/build.gradle +++ b/sample/android/build.gradle @@ -15,6 +15,7 @@ allprojects { repositories { google() mavenCentral() + jcenter() } } From bdb764fd3faea770e9a6c1f6ed2357ded64755b2 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Wed, 1 Nov 2023 10:42:11 -0300 Subject: [PATCH 21/29] fix: get extension from mime type when returning cache path --- lib/src/utils/ds_directory_formatter.util.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/utils/ds_directory_formatter.util.dart b/lib/src/utils/ds_directory_formatter.util.dart index a03a5260..1c527bbe 100644 --- a/lib/src/utils/ds_directory_formatter.util.dart +++ b/lib/src/utils/ds_directory_formatter.util.dart @@ -3,6 +3,8 @@ import 'dart:io'; import 'package:get/get.dart'; import 'package:path_provider/path_provider.dart'; +import '../services/ds_file.service.dart'; + abstract class DSDirectoryFormatter { static Future getCachePath({ required final String type, @@ -14,7 +16,7 @@ abstract class DSDirectoryFormatter { final typeComponents = type.split('/'); final typeFolder = '${typeComponents.first.capitalizeFirst}'; - extension ??= typeComponents.last; + extension ??= DSFileService.getFileExtensionFromMime(type); final typePrefix = '${typeFolder.substring(0, 3).toUpperCase()}-'; From f22ea1355e102a170cd9bfd01b96626c47378391 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Wed, 1 Nov 2023 11:46:52 -0300 Subject: [PATCH 22/29] fix: change extension from downloaded file only if it doesn't have an extension yet --- lib/src/services/ds_file.service.dart | 26 +++++-------------- .../utils/ds_directory_formatter.util.dart | 4 +-- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/lib/src/services/ds_file.service.dart b/lib/src/services/ds_file.service.dart index efd0e060..7430308a 100644 --- a/lib/src/services/ds_file.service.dart +++ b/lib/src/services/ds_file.service.dart @@ -74,28 +74,16 @@ abstract class DSFileService { ); if (response.statusCode == 200) { - final newExtension = getFileExtensionFromMime( - response.headers.map['content-type']?.first, - ); - - if (newExtension.isNotEmpty) { - final hasExtension = path_utils.extension(savedFilePath).isNotEmpty; - - final lastDotIndex = - hasExtension ? savedFilePath.lastIndexOf('.') : -1; + final hasExtension = path_utils.extension(savedFilePath).isNotEmpty; - late final String filename; - late final String savedExtension; + if (!hasExtension) { + final newExtension = getFileExtensionFromMime( + response.headers.map['content-type']?.first, + ); - if (lastDotIndex >= 0) { - filename = savedFilePath.substring(0, lastDotIndex); - savedExtension = savedFilePath.substring(lastDotIndex + 1); - } else { - filename = savedFilePath.substring(0); - savedExtension = ''; - } + if (newExtension.isNotEmpty) { + final filename = savedFilePath.substring(0); - if (newExtension != savedExtension) { final newFilePath = '$filename.$newExtension'; File(savedFilePath).renameSync(newFilePath); diff --git a/lib/src/utils/ds_directory_formatter.util.dart b/lib/src/utils/ds_directory_formatter.util.dart index 1c527bbe..a03a5260 100644 --- a/lib/src/utils/ds_directory_formatter.util.dart +++ b/lib/src/utils/ds_directory_formatter.util.dart @@ -3,8 +3,6 @@ import 'dart:io'; import 'package:get/get.dart'; import 'package:path_provider/path_provider.dart'; -import '../services/ds_file.service.dart'; - abstract class DSDirectoryFormatter { static Future getCachePath({ required final String type, @@ -16,7 +14,7 @@ abstract class DSDirectoryFormatter { final typeComponents = type.split('/'); final typeFolder = '${typeComponents.first.capitalizeFirst}'; - extension ??= DSFileService.getFileExtensionFromMime(type); + extension ??= typeComponents.last; final typePrefix = '${typeFolder.substring(0, 3).toUpperCase()}-'; From 52d19a4bed20b3555e81afb5eff367975107152e Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Wed, 1 Nov 2023 14:08:31 -0300 Subject: [PATCH 23/29] feat: add method to generate unique id in DSUtils --- lib/src/services/ds_file.service.dart | 4 +++- lib/src/utils/ds_utils.util.dart | 7 +++++++ lib/src/widgets/utils/ds_group_card.widget.dart | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/src/services/ds_file.service.dart b/lib/src/services/ds_file.service.dart index 7430308a..c4eeb2ef 100644 --- a/lib/src/services/ds_file.service.dart +++ b/lib/src/services/ds_file.service.dart @@ -7,6 +7,8 @@ import 'package:path/path.dart' as path_utils; import 'package:path_provider/path_provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import '../utils/ds_utils.util.dart'; + abstract class DSFileService { static Future open({ required final String url, @@ -54,7 +56,7 @@ abstract class DSFileService { var savedFilePath = path ?? path_utils.join( (await getTemporaryDirectory()).path, - DateTime.now().toIso8601String().replaceAll('.', ''), + DSUtils.generateUniqueID(), ); if (File(savedFilePath).existsSync()) { diff --git a/lib/src/utils/ds_utils.util.dart b/lib/src/utils/ds_utils.util.dart index 58d908f1..626f51e8 100644 --- a/lib/src/utils/ds_utils.util.dart +++ b/lib/src/utils/ds_utils.util.dart @@ -1,3 +1,7 @@ +import 'dart:convert'; + +import 'package:crypto/crypto.dart'; + import '../models/ds_country.model.dart'; /// All utility constants that are used by this Design System. @@ -7,6 +11,9 @@ abstract class DSUtils { static const bubbleMaxSize = 480.0; static const defaultAnimationDuration = Duration(milliseconds: 300); + static String generateUniqueID() => + md5.convert(utf8.encode(DateTime.now().toIso8601String())).toString(); + static const countriesList = [ DSCountry( code: '+55', diff --git a/lib/src/widgets/utils/ds_group_card.widget.dart b/lib/src/widgets/utils/ds_group_card.widget.dart index 2e6dee95..5224557c 100644 --- a/lib/src/widgets/utils/ds_group_card.widget.dart +++ b/lib/src/widgets/utils/ds_group_card.widget.dart @@ -333,7 +333,7 @@ class _DSGroupCardState extends State { 0, Padding( key: ValueKey( - message.id ?? DateTime.now().toIso8601String(), + message.id ?? DSUtils.generateUniqueID(), ), padding: const EdgeInsets.symmetric(horizontal: 16), child: Table( From c8d57071b17d9830fca20eb1a35b97058ca20151 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Wed, 1 Nov 2023 16:58:29 -0300 Subject: [PATCH 24/29] chore: upgrade app version from 0.0.85 to 0.0.86 --- CHANGELOG.md | 6 ++++++ pubspec.yaml | 2 +- sample/pubspec.lock | 18 +++++++++--------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f1910da..731678f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.0.86 + +- [DSMediaFormatService] Added service to format all kinds of media, like audios and videos. +- [DSAudioPlayer] Play audio from local storage when available. +- [DSFileService] Improved download method to get extension based on mime type. + ## 0.0.85 - [DSVideoMessageBubble] Added a progressive loading diff --git a/pubspec.yaml b/pubspec.yaml index f66c5c4f..94830b70 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: blip_ds description: Blip Design System for Flutter. -version: 0.0.85 +version: 0.0.86 homepage: https://github.com/takenet/blip-ds-flutter#readme repository: https://github.com/takenet/blip-ds-flutter diff --git a/sample/pubspec.lock b/sample/pubspec.lock index 67d875ba..5bd99f77 100644 --- a/sample/pubspec.lock +++ b/sample/pubspec.lock @@ -21,17 +21,17 @@ packages: dependency: transitive description: name: audio_session - sha256: e4acc4e9eaa32436dfc5d7aed7f0a370f2d7bb27ee27de30d6c4f220c2a05c73 + sha256: "8a2bc5e30520e18f3fb0e366793d78057fb64cd5287862c76af0c8771f2a52ad" url: "https://pub.dev" source: hosted - version: "0.1.13" + version: "0.1.16" blip_ds: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.0.81" + version: "0.0.86" boolean_selector: dependency: transitive description: @@ -331,26 +331,26 @@ packages: dependency: transitive description: name: just_audio - sha256: "7a5057a4d05c8f88ee968cec6fdfe1015577d5184e591d5ac15ab16d8f5ecb17" + sha256: "5ed0cd723e17dfd8cd4b0253726221e67f6546841ea4553635cf895061fc335b" url: "https://pub.dev" source: hosted - version: "0.9.31" + version: "0.9.35" just_audio_platform_interface: dependency: transitive description: name: just_audio_platform_interface - sha256: eff112d5138bea3ba544b6338b1e0537a32b5e1425e4d0dc38f732771cda7c84 + sha256: d8409da198bbc59426cd45d4c92fca522a2ec269b576ce29459d6d6fcaeb44df url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.2.1" just_audio_web: dependency: transitive description: name: just_audio_web - sha256: "89d8db6f19f3821bb6bf908c4bfb846079afb2ab575b783d781a6bf119e3abaf" + sha256: ff62f733f437b25a0ff590f0e295fa5441dcb465f1edbdb33b3dea264705bc13 url: "https://pub.dev" source: hosted - version: "0.4.7" + version: "0.4.8" libphonenumber_platform_interface: dependency: transitive description: From d84c166fa06f59c15006a191d23c634bd9a67748 Mon Sep 17 00:00:00 2001 From: Andre Rossi Date: Fri, 3 Nov 2023 10:50:04 -0300 Subject: [PATCH 25/29] fix: use relative imports --- lib/src/widgets/radio/ds_radio_tile.widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/widgets/radio/ds_radio_tile.widget.dart b/lib/src/widgets/radio/ds_radio_tile.widget.dart index 280d8ddf..154f95c9 100644 --- a/lib/src/widgets/radio/ds_radio_tile.widget.dart +++ b/lib/src/widgets/radio/ds_radio_tile.widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../../../blip_ds.dart'; +import 'ds_radio.widget.dart'; class DSRadioTile extends StatelessWidget { /// Create a Design System's tile widget with a radio button From 3cd90351f2cc1433e082c3c134c5e2748252b177 Mon Sep 17 00:00:00 2001 From: Marcelo Amaro Date: Mon, 6 Nov 2023 10:02:33 -0300 Subject: [PATCH 26/29] chore: remove intl_phone_number_input dependency --- pubspec.yaml | 1 - sample/pubspec.lock | 40 ---------------------------------------- 2 files changed, 41 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 70449d5d..c3fa3126 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,7 +33,6 @@ dependencies: pointer_interceptor: ^0.9.1 debounce_throttle: ^2.0.0 file_sizes: ^1.0.6 - intl_phone_number_input: ^0.7.3+1 mask_text_input_formatter: ^2.4.0 dotted_border: ^2.0.0+3 map_launcher: ^2.5.0+1 diff --git a/sample/pubspec.lock b/sample/pubspec.lock index c879cfd7..6bc70e19 100644 --- a/sample/pubspec.lock +++ b/sample/pubspec.lock @@ -152,14 +152,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0+3" - equatable: - dependency: transitive - description: - name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 - url: "https://pub.dev" - source: hosted - version: "2.0.5" fake_async: dependency: transitive description: @@ -303,14 +295,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" - intl_phone_number_input: - dependency: transitive - description: - name: intl_phone_number_input - sha256: b812c6e3924e56e65b9684076ccd8eb24126abe0c84091d35d75a25e613a1793 - url: "https://pub.dev" - source: hosted - version: "0.7.3+1" js: dependency: transitive description: @@ -343,30 +327,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.4.8" - libphonenumber_platform_interface: - dependency: transitive - description: - name: libphonenumber_platform_interface - sha256: cdd1230aad1d8e12fb4d9752d67ed2361b1cbb26fc8d202a9990c218553b3963 - url: "https://pub.dev" - source: hosted - version: "0.4.0" - libphonenumber_plugin: - dependency: transitive - description: - name: libphonenumber_plugin - sha256: "1be7c9de4beaed6dffdaf4a3d87758ef9b259c1111283736faef2179919e3ca2" - url: "https://pub.dev" - source: hosted - version: "0.3.1" - libphonenumber_web: - dependency: transitive - description: - name: libphonenumber_web - sha256: "35a106149d2f7a05a670d02be2f4ea288258d7bca2f8bdda8a96617151dfa5b9" - url: "https://pub.dev" - source: hosted - version: "0.3.0" linkify: dependency: transitive description: From 30734137a032daddd492e61c61833d468dbe925d Mon Sep 17 00:00:00 2001 From: Andre Rossi Date: Mon, 13 Nov 2023 09:52:37 -0300 Subject: [PATCH 27/29] feat: created DSProgressiveProgress widget --- lib/blip_ds.dart | 2 + .../chat/ds_image_message_bubble.widget.dart | 28 ++------ .../video/ds_video_message_bubble.widget.dart | 36 ++--------- .../utils/ds_progressive_progress.widget.dart | 64 +++++++++++++++++++ 4 files changed, 78 insertions(+), 52 deletions(-) create mode 100644 lib/src/widgets/utils/ds_progressive_progress.widget.dart diff --git a/lib/blip_ds.dart b/lib/blip_ds.dart index 759dc59e..dab4246c 100644 --- a/lib/blip_ds.dart +++ b/lib/blip_ds.dart @@ -158,4 +158,6 @@ export 'src/widgets/utils/ds_file_extension_icon.util.dart' export 'src/widgets/utils/ds_group_card.widget.dart' show DSGroupCard; export 'src/widgets/utils/ds_header.widget.dart' show DSHeader; export 'src/widgets/utils/ds_progress_bar.widget.dart' show DSProgressBar; +export 'src/widgets/utils/ds_progressive_progress.widget.dart' + show DSProgressiveProgress; export 'src/widgets/utils/ds_user_avatar.widget.dart' show DSUserAvatar; diff --git a/lib/src/widgets/chat/ds_image_message_bubble.widget.dart b/lib/src/widgets/chat/ds_image_message_bubble.widget.dart index 92311f76..96245274 100644 --- a/lib/src/widgets/chat/ds_image_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_image_message_bubble.widget.dart @@ -9,6 +9,7 @@ import '../../models/ds_message_bubble_style.model.dart'; import '../../themes/colors/ds_colors.theme.dart'; import '../texts/ds_caption_text.widget.dart'; import '../utils/ds_expanded_image.widget.dart'; +import '../utils/ds_progressive_progress.widget.dart'; import 'ds_document_select.widget.dart'; import 'ds_message_bubble.widget.dart'; import 'ds_show_more_text.widget.dart'; @@ -114,7 +115,11 @@ class _DSImageMessageBubbleState extends State isLoading: false, shouldAuthenticate: widget.shouldAuthenticate, ) - : _buildDownloadProgress(), + : DSProgressiveProgress( + currentProgress: _controller.downloadProgress, + maximumProgress: _controller.maximumProgress, + foregroundColor: foregroundColor, + ), ), if ((widget.title?.isNotEmpty ?? false) || (widget.text?.isNotEmpty ?? false)) @@ -161,27 +166,6 @@ class _DSImageMessageBubbleState extends State ); } - Widget _buildDownloadProgress() { - final foregroundColor = widget.style.isLightBubbleBackground(widget.align) - ? DSColors.neutralDarkCity - : DSColors.neutralLightSnow; - - final double percent = _controller.maximumProgress.value > 0 - ? _controller.downloadProgress.value / _controller.maximumProgress.value - : 0; - - return Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: CircularProgressIndicator( - color: foregroundColor, - backgroundColor: Colors.grey, - value: percent, - ), - ), - ); - } - @override bool get wantKeepAlive => true; } diff --git a/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart b/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart index f5eee8fc..76b38c15 100644 --- a/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart +++ b/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart @@ -11,7 +11,7 @@ import '../../../services/ds_auth.service.dart'; import '../../../themes/colors/ds_colors.theme.dart'; import '../../../themes/icons/ds_icons.dart'; import '../../buttons/ds_button.widget.dart'; -import '../../texts/ds_caption_small_text.widget.dart'; +import '../../utils/ds_progressive_progress.widget.dart'; import '../ds_message_bubble.widget.dart'; import '../ds_show_more_text.widget.dart'; import 'ds_video_body.widget.dart'; @@ -131,7 +131,11 @@ class _DSVideoMessageBubbleState extends State color: DSColors.neutralDarkRooftop, ) : _controller.isDownloading.value - ? _buildDownloadProgress(foregroundColor) + ? DSProgressiveProgress( + currentProgress: _controller.downloadProgress, + maximumProgress: _controller.maximumProgress, + foregroundColor: foregroundColor, + ) : _controller.thumbnail.isEmpty ? Center( child: SizedBox( @@ -186,32 +190,4 @@ class _DSVideoMessageBubbleState extends State ), ); } - - Widget _buildDownloadProgress(final Color foregroundColor) { - final double percent = _controller.maximumProgress.value > 0 - ? _controller.downloadProgress.value / _controller.maximumProgress.value - : 0; - - return AnimatedOpacity( - opacity: _controller.maximumProgress.value > 0 ? 1 : 0, - duration: const Duration(milliseconds: 250), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: CircularProgressIndicator( - color: foregroundColor, - backgroundColor: Colors.grey, - value: percent, - ), - ), - DSCaptionSmallText( - _controller.getDownloadProgress(), - color: foregroundColor, - ) - ], - ), - ); - } } diff --git a/lib/src/widgets/utils/ds_progressive_progress.widget.dart b/lib/src/widgets/utils/ds_progressive_progress.widget.dart new file mode 100644 index 00000000..77ff175e --- /dev/null +++ b/lib/src/widgets/utils/ds_progressive_progress.widget.dart @@ -0,0 +1,64 @@ +import 'package:file_sizes/file_sizes.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../themes/colors/ds_colors.theme.dart'; +import '../texts/ds_caption_small_text.widget.dart'; + +class DSProgressiveProgress extends StatelessWidget { + final RxInt maximumProgress; + final RxInt currentProgress; + final Color? foregroundColor; + + const DSProgressiveProgress({ + super.key, + required this.maximumProgress, + required this.currentProgress, + this.foregroundColor, + }); + + @override + Widget build(BuildContext context) { + return Obx( + () { + final double percent = maximumProgress.value > 0 + ? currentProgress.value / maximumProgress.value + : 0; + + return SizedBox( + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: CircularProgressIndicator( + color: DSColors.primary, + backgroundColor: Colors.grey, + value: currentProgress.value < maximumProgress.value + ? percent + : null, + ), + ), + DSCaptionSmallText( + _buildProgress(), + color: foregroundColor ?? DSColors.neutralDarkCity, + ) + ], + ), + ); + }, + ); + } + + String _buildProgress() { + String getSize(int value) => FileSize.getSize( + value, + precision: PrecisionValue.One, + ); + + return '${getSize(currentProgress.value)} / ${getSize(maximumProgress.value)}'; + } +} From 81745264dc8dec997054648a40e483c64cf12a66 Mon Sep 17 00:00:00 2001 From: Andre Rossi Date: Mon, 13 Nov 2023 13:17:32 -0300 Subject: [PATCH 28/29] feat: rename widget DSProgressiveProgress --- lib/blip_ds.dart | 4 ++-- lib/src/widgets/chat/ds_image_message_bubble.widget.dart | 4 ++-- .../widgets/chat/video/ds_video_message_bubble.widget.dart | 4 ++-- ..._progress.widget.dart => ds_circular_progress.widget.dart} | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename lib/src/widgets/utils/{ds_progressive_progress.widget.dart => ds_circular_progress.widget.dart} (95%) diff --git a/lib/blip_ds.dart b/lib/blip_ds.dart index dab4246c..c6723372 100644 --- a/lib/blip_ds.dart +++ b/lib/blip_ds.dart @@ -151,6 +151,8 @@ export 'src/widgets/utils/ds_cached_network_image_view.widget.dart' show DSCachedNetworkImageView; export 'src/widgets/utils/ds_card.widget.dart' show DSCard; export 'src/widgets/utils/ds_chip.widget.dart' show DSChip; +export 'src/widgets/utils/ds_circular_progress.widget.dart' + show DSCircularProgress; export 'src/widgets/utils/ds_divider.widget.dart' show DSDivider; export 'src/widgets/utils/ds_expanded_image.widget.dart' show DSExpandedImage; export 'src/widgets/utils/ds_file_extension_icon.util.dart' @@ -158,6 +160,4 @@ export 'src/widgets/utils/ds_file_extension_icon.util.dart' export 'src/widgets/utils/ds_group_card.widget.dart' show DSGroupCard; export 'src/widgets/utils/ds_header.widget.dart' show DSHeader; export 'src/widgets/utils/ds_progress_bar.widget.dart' show DSProgressBar; -export 'src/widgets/utils/ds_progressive_progress.widget.dart' - show DSProgressiveProgress; export 'src/widgets/utils/ds_user_avatar.widget.dart' show DSUserAvatar; diff --git a/lib/src/widgets/chat/ds_image_message_bubble.widget.dart b/lib/src/widgets/chat/ds_image_message_bubble.widget.dart index 96245274..260c9ecd 100644 --- a/lib/src/widgets/chat/ds_image_message_bubble.widget.dart +++ b/lib/src/widgets/chat/ds_image_message_bubble.widget.dart @@ -8,8 +8,8 @@ import '../../models/ds_document_select.model.dart'; import '../../models/ds_message_bubble_style.model.dart'; import '../../themes/colors/ds_colors.theme.dart'; import '../texts/ds_caption_text.widget.dart'; +import '../utils/ds_circular_progress.widget.dart'; import '../utils/ds_expanded_image.widget.dart'; -import '../utils/ds_progressive_progress.widget.dart'; import 'ds_document_select.widget.dart'; import 'ds_message_bubble.widget.dart'; import 'ds_show_more_text.widget.dart'; @@ -115,7 +115,7 @@ class _DSImageMessageBubbleState extends State isLoading: false, shouldAuthenticate: widget.shouldAuthenticate, ) - : DSProgressiveProgress( + : DSCircularProgress( currentProgress: _controller.downloadProgress, maximumProgress: _controller.maximumProgress, foregroundColor: foregroundColor, diff --git a/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart b/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart index 76b38c15..204850c0 100644 --- a/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart +++ b/lib/src/widgets/chat/video/ds_video_message_bubble.widget.dart @@ -11,7 +11,7 @@ import '../../../services/ds_auth.service.dart'; import '../../../themes/colors/ds_colors.theme.dart'; import '../../../themes/icons/ds_icons.dart'; import '../../buttons/ds_button.widget.dart'; -import '../../utils/ds_progressive_progress.widget.dart'; +import '../../utils/ds_circular_progress.widget.dart'; import '../ds_message_bubble.widget.dart'; import '../ds_show_more_text.widget.dart'; import 'ds_video_body.widget.dart'; @@ -131,7 +131,7 @@ class _DSVideoMessageBubbleState extends State color: DSColors.neutralDarkRooftop, ) : _controller.isDownloading.value - ? DSProgressiveProgress( + ? DSCircularProgress( currentProgress: _controller.downloadProgress, maximumProgress: _controller.maximumProgress, foregroundColor: foregroundColor, diff --git a/lib/src/widgets/utils/ds_progressive_progress.widget.dart b/lib/src/widgets/utils/ds_circular_progress.widget.dart similarity index 95% rename from lib/src/widgets/utils/ds_progressive_progress.widget.dart rename to lib/src/widgets/utils/ds_circular_progress.widget.dart index 77ff175e..7166996f 100644 --- a/lib/src/widgets/utils/ds_progressive_progress.widget.dart +++ b/lib/src/widgets/utils/ds_circular_progress.widget.dart @@ -5,12 +5,12 @@ import 'package:get/get.dart'; import '../../themes/colors/ds_colors.theme.dart'; import '../texts/ds_caption_small_text.widget.dart'; -class DSProgressiveProgress extends StatelessWidget { +class DSCircularProgress extends StatelessWidget { final RxInt maximumProgress; final RxInt currentProgress; final Color? foregroundColor; - const DSProgressiveProgress({ + const DSCircularProgress({ super.key, required this.maximumProgress, required this.currentProgress, From de852f52843286924f4498deed6ddfe2225f2546 Mon Sep 17 00:00:00 2001 From: Andre Rossi Date: Tue, 14 Nov 2023 09:15:47 -0300 Subject: [PATCH 29/29] feat: added enabled param --- .../fields/ds_search_input.widget.dart | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/src/widgets/fields/ds_search_input.widget.dart b/lib/src/widgets/fields/ds_search_input.widget.dart index 962b2c39..2eb6f984 100644 --- a/lib/src/widgets/fields/ds_search_input.widget.dart +++ b/lib/src/widgets/fields/ds_search_input.widget.dart @@ -15,6 +15,7 @@ class DSSearchInput extends StatelessWidget { this.hintText, this.iconBackgroundColor = DSColors.disabledBg, this.iconForegroundColor = DSColors.primaryNight, + this.enabled, }); final void Function(String term) onSearch; @@ -25,6 +26,7 @@ class DSSearchInput extends StatelessWidget { final String? hintText; final Color iconBackgroundColor; final Color iconForegroundColor; + final bool? enabled; // TODO: check if can use DSTextField or DSInputContainer @override @@ -32,6 +34,7 @@ class DSSearchInput extends StatelessWidget { return SizedBox( height: 44.0, child: TextField( + enabled: enabled ?? true, focusNode: focusNode, controller: controller, onChanged: onSearch, @@ -81,13 +84,8 @@ class DSSearchInput extends StatelessWidget { width: 1.0, ), ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(8.0), - borderSide: const BorderSide( - color: DSColors.neutralMediumWave, - width: 1.0, - ), - ), + disabledBorder: _getBorder(), + enabledBorder: _getBorder(), filled: true, hintText: hintText, hintStyle: const DSBodyTextStyle( @@ -97,4 +95,12 @@ class DSSearchInput extends StatelessWidget { ), ); } + + OutlineInputBorder _getBorder() => OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: const BorderSide( + color: DSColors.neutralMediumWave, + width: 1.0, + ), + ); }