From 95c92b05723bcec3c7c794c550aa5c5dc5a2f89d Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Tue, 16 Jul 2024 17:38:52 +0530 Subject: [PATCH] Show fps, bitrate and dim for video --- mobile/lib/models/ffmpeg/ffprobe_props.dart | 30 ++++++++++++++++- .../models/file/extensions/file_props.dart | 2 ++ .../ui/viewer/file/file_details_widget.dart | 32 +++++++++---------- .../lib/ui/viewer/file/video_exif_dialog.dart | 4 +-- .../viewer/file_details/video_exif_item.dart | 22 +++++-------- 5 files changed, 56 insertions(+), 34 deletions(-) diff --git a/mobile/lib/models/ffmpeg/ffprobe_props.dart b/mobile/lib/models/ffmpeg/ffprobe_props.dart index 71478e01a1..dfb286897a 100644 --- a/mobile/lib/models/ffmpeg/ffprobe_props.dart +++ b/mobile/lib/models/ffmpeg/ffprobe_props.dart @@ -17,6 +17,20 @@ class FFProbeProps { DateTime? creationTime; String? bitrate; String? majorBrand; + String? fps; + String? codecWidth; + String? codecHeight; + + // dot separated bitrate, fps, codecWidth, codecHeight. Ignore null value + String get videoInfo { + final List info = []; + if (bitrate != null) info.add('$bitrate'); + if (fps != null) info.add('ƒ/$fps'); + if (codecWidth != null && codecHeight != null) { + info.add('$codecWidth x $codecHeight'); + } + return info.join(' * '); + } // toString() method @override @@ -37,6 +51,7 @@ class FFProbeProps { for (final key in json!.keys) { final stringKey = key.toString(); + switch (stringKey) { case FFProbeKeys.bitrate: case FFProbeKeys.bps: @@ -95,8 +110,21 @@ class FFProbeProps { parsedData[stringKey] = json[key]; } } + // iterate through the streams + final List streams = json["streams"]; + for (final stream in streams) { + final Map streamData = {}; + for (final key in stream.keys) { + if (key == "r_frame_rate") { + result.fps = stream[key]; + } else if (key == "coded_width") { + result.codecWidth = stream[key].toString(); + } else if (key == "coded_height") { + result.codecHeight = stream[key].toString(); + } + } + } result.prodData = parsedData; - return result; } diff --git a/mobile/lib/models/file/extensions/file_props.dart b/mobile/lib/models/file/extensions/file_props.dart index b3f5ae672a..64436ef2e3 100644 --- a/mobile/lib/models/file/extensions/file_props.dart +++ b/mobile/lib/models/file/extensions/file_props.dart @@ -14,6 +14,8 @@ extension FilePropsExtn on EnteFile { bool get isOwner => (ownerID == null) || (ownerID == Configuration.instance.getUserID()); + bool get isVideo => fileType == FileType.video; + bool get canEditMetaInfo => isUploaded && isOwner; bool get isTrash => this is TrashFile; diff --git a/mobile/lib/ui/viewer/file/file_details_widget.dart b/mobile/lib/ui/viewer/file/file_details_widget.dart index 2ff0cf2db4..51e6ea2914 100644 --- a/mobile/lib/ui/viewer/file/file_details_widget.dart +++ b/mobile/lib/ui/viewer/file/file_details_widget.dart @@ -4,6 +4,7 @@ import "dart:io"; import "package:exif/exif.dart"; import "package:ffmpeg_kit_flutter_min/ffprobe_kit.dart"; +import "package:flutter/foundation.dart"; import "package:flutter/material.dart"; import "package:logging/logging.dart"; import "package:photos/core/configuration.dart"; @@ -11,9 +12,11 @@ import "package:photos/core/event_bus.dart"; import "package:photos/events/people_changed_event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/ffmpeg/ffprobe_props.dart"; +import "package:photos/models/file/extensions/file_props.dart"; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/file_type.dart'; import "package:photos/models/metadata/file_magic.dart"; +import "package:photos/service_locator.dart"; import "package:photos/services/file_magic_service.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/buttons/icon_button_widget.dart'; @@ -101,7 +104,7 @@ class _FileDetailsWidgetState extends State { _exifData["exposureTime"] != null || _exifData["ISO"] != null; }); - } else { + } else if (flagService.internalUser && widget.file.isVideo) { getMediaInfo(); } getExif(widget.file).then((exif) { @@ -115,9 +118,7 @@ class _FileDetailsWidgetState extends State { final File? originFile = await getFile(widget.file, isOrigin: true); if (originFile == null) return; final session = await FFprobeKit.getMediaInformation(originFile.path); - final mediaInfo = session.getMediaInformation(); - if (mediaInfo == null) { final failStackTrace = await session.getFailStackTrace(); final output = await session.getOutput(); @@ -128,11 +129,10 @@ class _FileDetailsWidgetState extends State { } final properties = await FFProbeUtil.getProperties(mediaInfo); _videoMetadataNotifier.value = properties; - - // print all the properties - log("videoCustomProps ${properties.toString()}"); - log("PropData ${properties.prodData.toString()}"); - + if (kDebugMode) { + log("videoCustomProps ${properties.toString()}"); + log("PropData ${properties.prodData.toString()}"); + } setState(() {}); } @@ -268,19 +268,17 @@ class _FileDetailsWidgetState extends State { }, ), ]); - } else if (_videoMetadataNotifier.value != null) { + } else if (flagService.internalUser && widget.file.isVideo) { fileDetailsTiles.addAll([ ValueListenableBuilder( valueListenable: _videoMetadataNotifier, builder: (context, value, _) { - return (value != null && value.prodData != null) - ? Column( - children: [ - VideoExifRowItem(file, value.prodData), - const FileDetailsDivider(), - ], - ) - : const SizedBox.shrink(); + return Column( + children: [ + VideoExifRowItem(file, value), + const FileDetailsDivider(), + ], + ); }, ), ]); diff --git a/mobile/lib/ui/viewer/file/video_exif_dialog.dart b/mobile/lib/ui/viewer/file/video_exif_dialog.dart index 5f621f85ea..487a6e7f4e 100644 --- a/mobile/lib/ui/viewer/file/video_exif_dialog.dart +++ b/mobile/lib/ui/viewer/file/video_exif_dialog.dart @@ -79,7 +79,7 @@ class VideoExifDialog extends StatelessWidget { Widget _buildStreamInfo(BuildContext context, Map stream) { String titleString = stream['type']?.toString().toUpperCase() ?? ''; final codeName = stream['codec_name']?.toString().toUpperCase() ?? ''; - if (codeName != 'NULL') { + if (codeName != 'NULL' && codeName.isNotEmpty) { titleString += ' - $codeName'; } return ExpansionTile( @@ -87,7 +87,7 @@ class VideoExifDialog extends StatelessWidget { titleString, style: getEnteTextTheme(context).smallBold, ), - childrenPadding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), + childrenPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 4), tilePadding: const EdgeInsets.symmetric(vertical: 4), children: [ Column( diff --git a/mobile/lib/ui/viewer/file_details/video_exif_item.dart b/mobile/lib/ui/viewer/file_details/video_exif_item.dart index d160d7f40d..8b1e27eb4a 100644 --- a/mobile/lib/ui/viewer/file_details/video_exif_item.dart +++ b/mobile/lib/ui/viewer/file_details/video_exif_item.dart @@ -1,19 +1,20 @@ import "package:flutter/material.dart"; import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; import "package:photos/generated/l10n.dart"; +import "package:photos/models/ffmpeg/ffprobe_props.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/info_item_widget.dart"; -import "package:photos/ui/viewer/file/vid_exif_dialog.dart"; +import "package:photos/ui/viewer/file/video_exif_dialog.dart"; import "package:photos/utils/toast_util.dart"; class VideoExifRowItem extends StatefulWidget { final EnteFile file; - final Map? exif; + final FFProbeProps? props; const VideoExifRowItem( this.file, - this.exif, { + this.props, { super.key, }); @@ -34,7 +35,8 @@ class _VideoProbeInfoState extends State { return InfoItemWidget( leadingIcon: Icons.text_snippet_outlined, title: "Video Info", - subtitleSection: _exifButton(context, widget.file, widget.exif), + subtitleSection: + _exifButton(context, widget.file, widget.props?.prodData), onTap: _onTap, ); } @@ -46,24 +48,16 @@ class _VideoProbeInfoState extends State { ) async { late final String label; late final VoidCallback? onTap; - final Map data = {}; - if (exif != null) { - for (final key in exif.keys) { - if (exif[key] != null) { - data[key] = exif[key]; - } - } - } if (exif == null) { label = S.of(context).loadingExifData; onTap = null; } else if (exif.isNotEmpty) { - label = "Tap to view more details"; + label = "${widget.props?.videoInfo ?? ''} .."; onTap = () => showBarModalBottomSheet( context: context, builder: (BuildContext context) { return VideoExifDialog( - probeData: data, + probeData: exif, ); }, shape: const RoundedRectangleBorder(