From af5e9b51e19ee0ca27e8e38f061096b655615d18 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Thu, 1 Aug 2024 16:51:36 +0530 Subject: [PATCH 1/5] [mob][photos] Fix bug in parsing rotation metadata from video using FFProbe --- mobile/lib/models/ffmpeg/ffprobe_keys.dart | 14 ++++++++++++++ mobile/lib/models/ffmpeg/ffprobe_props.dart | 7 ++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/mobile/lib/models/ffmpeg/ffprobe_keys.dart b/mobile/lib/models/ffmpeg/ffprobe_keys.dart index 5eddee32c8..55c26c29f1 100644 --- a/mobile/lib/models/ffmpeg/ffprobe_keys.dart +++ b/mobile/lib/models/ffmpeg/ffprobe_keys.dart @@ -72,6 +72,7 @@ class FFProbeKeys { static const xiaomiSlowMoment = 'com.xiaomi.slow_moment'; static const sideDataList = 'side_data_list'; static const rotation = 'rotation'; + static const sideDataType = 'side_data_type'; } class MediaStreamTypes { @@ -83,3 +84,16 @@ class MediaStreamTypes { static const unknown = 'unknown'; static const video = 'video'; } + +enum SideDataType { + displayMatrix; + + getString() { + switch (this) { + case SideDataType.displayMatrix: + return 'Display Matrix'; + default: + assert(false, 'Unknown side data type: $this'); + } + } +} diff --git a/mobile/lib/models/ffmpeg/ffprobe_props.dart b/mobile/lib/models/ffmpeg/ffprobe_props.dart index b72c59f5ba..28a92869d1 100644 --- a/mobile/lib/models/ffmpeg/ffprobe_props.dart +++ b/mobile/lib/models/ffmpeg/ffprobe_props.dart @@ -174,7 +174,12 @@ class FFProbeProps { result._codecHeight = stream[key].toString(); parsedData[key] = result._codecHeight; } else if (key == FFProbeKeys.sideDataList) { - result._rotation = stream[key][0][FFProbeKeys.rotation]; + for (Map sideData in stream[key]) { + if (sideData[FFProbeKeys.sideDataType] == + SideDataType.displayMatrix.getString()) { + result._rotation = sideData[FFProbeKeys.rotation]; + } + } } } } From 7c202a4edbf00d138fa0aefbd2f4a9b1f99c7325 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Fri, 2 Aug 2024 17:26:36 +0530 Subject: [PATCH 2/5] [mob][photos] Consider the 'width' and 'height' key also when parsing dimensions of video --- mobile/lib/models/ffmpeg/ffprobe_props.dart | 58 ++++++++++++++------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/mobile/lib/models/ffmpeg/ffprobe_props.dart b/mobile/lib/models/ffmpeg/ffprobe_props.dart index 28a92869d1..302fcef20d 100644 --- a/mobile/lib/models/ffmpeg/ffprobe_props.dart +++ b/mobile/lib/models/ffmpeg/ffprobe_props.dart @@ -18,8 +18,8 @@ class FFProbeProps { String? bitrate; String? majorBrand; String? fps; - String? _codecWidth; - String? _codecHeight; + String? _width; + String? _height; int? _rotation; // dot separated bitrate, fps, codecWidth, codecHeight. Ignore null value @@ -27,36 +27,36 @@ class FFProbeProps { 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'); + if (_width != null && _height != null) { + info.add('$_width x $_height'); } return info.join(' * '); } int? get width { - if (_codecWidth == null || _codecHeight == null) return null; - final intCodecWidth = int.tryParse(_codecWidth!); + if (_width == null || _height == null) return null; + final intWidth = int.tryParse(_width!); if (_rotation == null) { - return intCodecWidth; + return intWidth; } else { if ((_rotation! ~/ 90).isEven) { - return intCodecWidth; + return intWidth; } else { - return int.tryParse(_codecHeight!); + return int.tryParse(_height!); } } } int? get height { - if (_codecWidth == null || _codecHeight == null) return null; - final intCodecHeight = int.tryParse(_codecHeight!); + if (_width == null || _height == null) return null; + final intHeight = int.tryParse(_height!); if (_rotation == null) { - return intCodecHeight; + return intHeight; } else { if ((_rotation! ~/ 90).isEven) { - return intCodecHeight; + return intHeight; } else { - return int.tryParse(_codecWidth!); + return int.tryParse(_width!); } } } @@ -168,15 +168,33 @@ class FFProbeProps { result.fps = _formatFPS(stream[key]); parsedData[key] = result.fps; } else if (key == FFProbeKeys.codedWidth) { - result._codecWidth = stream[key].toString(); - parsedData[key] = result._codecWidth; + final width = stream[key]; + if (width != null || width != "0") { + result._width = width.toString(); + parsedData[key] = result._width; + } } else if (key == FFProbeKeys.codedHeight) { - result._codecHeight = stream[key].toString(); - parsedData[key] = result._codecHeight; + final height = stream[key]; + if (height != null && height != "0") { + result._height = height.toString(); + parsedData[key] = result._height; + } + } else if (key == FFProbeKeys.width) { + final width = stream[key]; + if (width != null || width != "0") { + result._width = width.toString(); + parsedData[FFProbeKeys.width] = result._width; + } + } else if (key == FFProbeKeys.height) { + final height = stream[key]; + if (height != null && height != "0") { + result._height = height.toString(); + parsedData[FFProbeKeys.width] = result._height; + } } else if (key == FFProbeKeys.sideDataList) { + // result._rotation = stream[key][0][FFProbeKeys.rotation]; for (Map sideData in stream[key]) { - if (sideData[FFProbeKeys.sideDataType] == - SideDataType.displayMatrix.getString()) { + if (sideData["side_data_type"] == "Display Matrix") { result._rotation = sideData[FFProbeKeys.rotation]; } } From e1713851b3509c88f5af891d67db065db56524c6 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Mon, 5 Aug 2024 15:09:16 +0530 Subject: [PATCH 3/5] [mob][photos] Show accurate height and width in file info for a video --- mobile/lib/models/ffmpeg/ffprobe_props.dart | 20 +++---- .../ui/viewer/file/file_details_widget.dart | 2 +- .../lib/ui/viewer/file/video_exif_dialog.dart | 52 ++++++++++++------- .../viewer/file_details/video_exif_item.dart | 11 ++-- 4 files changed, 49 insertions(+), 36 deletions(-) diff --git a/mobile/lib/models/ffmpeg/ffprobe_props.dart b/mobile/lib/models/ffmpeg/ffprobe_props.dart index 302fcef20d..59adba31ad 100644 --- a/mobile/lib/models/ffmpeg/ffprobe_props.dart +++ b/mobile/lib/models/ffmpeg/ffprobe_props.dart @@ -12,7 +12,7 @@ import "package:photos/models/ffmpeg/mp4.dart"; import "package:photos/models/location/location.dart"; class FFProbeProps { - Map? prodData; + Map? propData; Location? location; DateTime? creationTimeUTC; String? bitrate; @@ -72,8 +72,8 @@ class FFProbeProps { @override String toString() { final buffer = StringBuffer(); - for (final key in prodData!.keys) { - final value = prodData![key]; + for (final key in propData!.keys) { + final value = propData![key]; if (value != null) { buffer.writeln('$key: $value'); } @@ -169,33 +169,33 @@ class FFProbeProps { parsedData[key] = result.fps; } else if (key == FFProbeKeys.codedWidth) { final width = stream[key]; - if (width != null || width != "0") { + if (width != null && width != 0) { result._width = width.toString(); parsedData[key] = result._width; } } else if (key == FFProbeKeys.codedHeight) { final height = stream[key]; - if (height != null && height != "0") { + if (height != null && height != 0) { result._height = height.toString(); parsedData[key] = result._height; } } else if (key == FFProbeKeys.width) { final width = stream[key]; - if (width != null || width != "0") { + if (width != null && width != 0) { result._width = width.toString(); parsedData[FFProbeKeys.width] = result._width; } } else if (key == FFProbeKeys.height) { final height = stream[key]; - if (height != null && height != "0") { + if (height != null && height != 0) { result._height = height.toString(); - parsedData[FFProbeKeys.width] = result._height; + parsedData[FFProbeKeys.height] = result._height; } } else if (key == FFProbeKeys.sideDataList) { - // result._rotation = stream[key][0][FFProbeKeys.rotation]; for (Map sideData in stream[key]) { if (sideData["side_data_type"] == "Display Matrix") { result._rotation = sideData[FFProbeKeys.rotation]; + parsedData[FFProbeKeys.rotation] = result._rotation; } } } @@ -205,7 +205,7 @@ class FFProbeProps { newStreams.add(metadata); } parsedData["streams"] = newStreams; - result.prodData = parsedData; + result.propData = parsedData; return result; } diff --git a/mobile/lib/ui/viewer/file/file_details_widget.dart b/mobile/lib/ui/viewer/file/file_details_widget.dart index d726d655f7..fef9428724 100644 --- a/mobile/lib/ui/viewer/file/file_details_widget.dart +++ b/mobile/lib/ui/viewer/file/file_details_widget.dart @@ -128,7 +128,7 @@ class _FileDetailsWidgetState extends State { _videoMetadataNotifier.value = properties; if (kDebugMode) { log("videoCustomProps ${properties.toString()}"); - log("PropData ${properties?.prodData.toString()}"); + log("PropData ${properties?.propData.toString()}"); } setState(() {}); } diff --git a/mobile/lib/ui/viewer/file/video_exif_dialog.dart b/mobile/lib/ui/viewer/file/video_exif_dialog.dart index 8fd713c832..59147378f4 100644 --- a/mobile/lib/ui/viewer/file/video_exif_dialog.dart +++ b/mobile/lib/ui/viewer/file/video_exif_dialog.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import "package:photos/l10n/l10n.dart"; import "package:photos/models/ffmpeg/ffprobe_keys.dart"; +import "package:photos/models/ffmpeg/ffprobe_props.dart"; import "package:photos/theme/ente_theme.dart"; class VideoExifDialog extends StatelessWidget { - final Map probeData; + final FFProbeProps props; - const VideoExifDialog({Key? key, required this.probeData}) : super(key: key); + const VideoExifDialog({Key? key, required this.props}) : super(key: key); @override Widget build(BuildContext context) { @@ -48,23 +49,23 @@ class VideoExifDialog extends StatelessWidget { context.l10n.videoInfo, style: getEnteTextTheme(context).large, ), - _buildInfoRow(context, 'Creation Time', probeData, 'creation_time'), - _buildInfoRow(context, 'Duration', probeData, 'duration'), - _buildInfoRow(context, context.l10n.location, probeData, 'location'), - _buildInfoRow(context, 'Bitrate', probeData, 'bitrate'), - _buildInfoRow(context, 'Frame Rate', probeData, FFProbeKeys.rFrameRate), - _buildInfoRow(context, 'Width', probeData, FFProbeKeys.codedWidth), - _buildInfoRow(context, 'Height', probeData, FFProbeKeys.codedHeight), - _buildInfoRow(context, 'Model', probeData, 'com.apple.quicktime.model'), - _buildInfoRow(context, 'OS', probeData, 'com.apple.quicktime.software'), - _buildInfoRow(context, 'Major Brand', probeData, 'major_brand'), - _buildInfoRow(context, 'Format', probeData, 'format'), + _buildInfoRow(context, 'Creation Time', props, 'creation_time'), + _buildInfoRow(context, 'Duration', props, 'duration'), + _buildInfoRow(context, context.l10n.location, props, 'location'), + _buildInfoRow(context, 'Bitrate', props, 'bitrate'), + _buildInfoRow(context, 'Frame Rate', props, FFProbeKeys.rFrameRate), + _buildInfoRow(context, 'Width', props, null), + _buildInfoRow(context, 'Height', props, null), + _buildInfoRow(context, 'Model', props, 'com.apple.quicktime.model'), + _buildInfoRow(context, 'OS', props, 'com.apple.quicktime.software'), + _buildInfoRow(context, 'Major Brand', props, 'major_brand'), + _buildInfoRow(context, 'Format', props, 'format'), ], ); } Widget _buildStreamsList(BuildContext context) { - final List streams = probeData['streams']; + final List streams = props.propData!['streams']; final List> data = []; for (final stream in streams) { final Map streamData = {}; @@ -113,7 +114,12 @@ class VideoExifDialog extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: stream.entries .map( - (entry) => _buildInfoRow(context, entry.key, stream, entry.key), + (entry) => _buildInfoRow( + context, + entry.key, + FFProbeProps()..propData = stream, + entry.key, + ), ) .toList(), ), @@ -124,15 +130,23 @@ class VideoExifDialog extends StatelessWidget { Widget _buildInfoRow( BuildContext context, String rowName, - Map data, - String dataKey, + FFProbeProps data, + String? dataKey, ) { + final propData = data.propData; rowName = rowName.replaceAll('_', ' '); rowName = rowName[0].toUpperCase() + rowName.substring(1); try { - final value = data[dataKey]; + dynamic value; + + if (rowName == 'Width' || rowName == 'Height') { + rowName == 'Width' ? value = data.width : value = data.height; + } else { + value = propData![dataKey]; + } + if (value == null) { - return Container(); // Return an empty container if there's no data for the key. + return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), 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 aa9512d662..a3d51d6c9a 100644 --- a/mobile/lib/ui/viewer/file_details/video_exif_item.dart +++ b/mobile/lib/ui/viewer/file_details/video_exif_item.dart @@ -35,8 +35,7 @@ class _VideoProbeInfoState extends State { return InfoItemWidget( leadingIcon: Icons.text_snippet_outlined, title: S.of(context).videoInfo, - subtitleSection: - _exifButton(context, widget.file, widget.props?.prodData), + subtitleSection: _exifButton(context, widget.file, widget.props), onTap: _onTap, ); } @@ -44,20 +43,20 @@ class _VideoProbeInfoState extends State { Future> _exifButton( BuildContext context, EnteFile file, - Map? exif, + FFProbeProps? props, ) async { late final String label; late final VoidCallback? onTap; - if (exif == null) { + if (props?.propData == null) { label = S.of(context).loadingExifData; onTap = null; - } else if (exif.isNotEmpty) { + } else if (props!.propData!.isNotEmpty) { label = "${widget.props?.videoInfo ?? ''} .."; onTap = () => showBarModalBottomSheet( context: context, builder: (BuildContext context) { return VideoExifDialog( - probeData: exif, + props: props, ); }, shape: const RoundedRectangleBorder( From f69214461df7a44ff3c38aa54367d5f94fc59ecb Mon Sep 17 00:00:00 2001 From: ashilkn Date: Mon, 5 Aug 2024 18:09:07 +0530 Subject: [PATCH 4/5] [mob][photos] Add todo comment --- mobile/lib/models/ffmpeg/ffprobe_props.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mobile/lib/models/ffmpeg/ffprobe_props.dart b/mobile/lib/models/ffmpeg/ffprobe_props.dart index 59adba31ad..e85430a62f 100644 --- a/mobile/lib/models/ffmpeg/ffprobe_props.dart +++ b/mobile/lib/models/ffmpeg/ffprobe_props.dart @@ -167,7 +167,11 @@ class FFProbeProps { if (key == FFProbeKeys.rFrameRate) { result.fps = _formatFPS(stream[key]); parsedData[key] = result.fps; - } else if (key == FFProbeKeys.codedWidth) { + } + //TODO: Use `height` and `width` instead of `codedHeight` and `codedWidth` + //for better accuracy. `height' and `width` will give the video's "visual" + //height and width. + else if (key == FFProbeKeys.codedWidth) { final width = stream[key]; if (width != null && width != 0) { result._width = width.toString(); From 2916bcfda81babccd3b4079f64525e7e29ee8fba Mon Sep 17 00:00:00 2001 From: ashilkn Date: Mon, 5 Aug 2024 18:24:29 +0530 Subject: [PATCH 5/5] [mob][photos] chore --- mobile/lib/models/ffmpeg/ffprobe_props.dart | 4 ++-- mobile/lib/ui/viewer/file/video_exif_dialog.dart | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mobile/lib/models/ffmpeg/ffprobe_props.dart b/mobile/lib/models/ffmpeg/ffprobe_props.dart index e85430a62f..545a39c5ed 100644 --- a/mobile/lib/models/ffmpeg/ffprobe_props.dart +++ b/mobile/lib/models/ffmpeg/ffprobe_props.dart @@ -187,13 +187,13 @@ class FFProbeProps { final width = stream[key]; if (width != null && width != 0) { result._width = width.toString(); - parsedData[FFProbeKeys.width] = result._width; + parsedData[key] = result._width; } } else if (key == FFProbeKeys.height) { final height = stream[key]; if (height != null && height != 0) { result._height = height.toString(); - parsedData[FFProbeKeys.height] = result._height; + parsedData[key] = result._height; } } else if (key == FFProbeKeys.sideDataList) { for (Map sideData in stream[key]) { diff --git a/mobile/lib/ui/viewer/file/video_exif_dialog.dart b/mobile/lib/ui/viewer/file/video_exif_dialog.dart index 59147378f4..ba650d469f 100644 --- a/mobile/lib/ui/viewer/file/video_exif_dialog.dart +++ b/mobile/lib/ui/viewer/file/video_exif_dialog.dart @@ -146,7 +146,8 @@ class VideoExifDialog extends StatelessWidget { } if (value == null) { - return const SizedBox.shrink(); + return const SizedBox + .shrink(); // Return an empty container if there's no data for the key. } return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0),