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..545a39c5ed 100644 --- a/mobile/lib/models/ffmpeg/ffprobe_props.dart +++ b/mobile/lib/models/ffmpeg/ffprobe_props.dart @@ -12,14 +12,14 @@ 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; 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!); } } } @@ -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'); } @@ -167,14 +167,41 @@ class FFProbeProps { if (key == FFProbeKeys.rFrameRate) { result.fps = _formatFPS(stream[key]); parsedData[key] = result.fps; - } else if (key == FFProbeKeys.codedWidth) { - result._codecWidth = stream[key].toString(); - parsedData[key] = result._codecWidth; + } + //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(); + 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[key] = result._width; + } + } else if (key == FFProbeKeys.height) { + final height = stream[key]; + if (height != null && height != 0) { + result._height = height.toString(); + parsedData[key] = 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; + } + } } } } @@ -182,7 +209,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..ba650d469f 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,24 @@ 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 an empty container if there's no data for the key. } 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(