[mob] Show video metadata inside fileInfo (#2466)

## Description

## Tests
This commit is contained in:
Neeraj Gupta
2024-07-16 18:02:29 +05:30
committed by GitHub
11 changed files with 414 additions and 205 deletions

View File

@@ -199,6 +199,8 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- smart_auth (0.0.1):
- Flutter
- sqflite (0.0.3):
- Flutter
- FlutterMacOS
@@ -277,6 +279,7 @@ DEPENDENCIES:
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- smart_auth (from `.symlinks/plugins/smart_auth/ios`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
- uni_links (from `.symlinks/plugins/uni_links/ios`)
@@ -397,6 +400,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
smart_auth:
:path: ".symlinks/plugins/smart_auth/ios"
sqflite:
:path: ".symlinks/plugins/sqflite/darwin"
sqlite3_flutter_libs:
@@ -466,7 +471,7 @@ SPEC CHECKSUMS:
package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
receive_sharing_intent: 6837b01768e567fe8562182397bf43d63d8c6437
screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625
@@ -477,6 +482,7 @@ SPEC CHECKSUMS:
SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: 02d1f07eaaa01f80a1c16b4b31dfcbb3345ee01a
sqlite3_flutter_libs: af0e8fe9bce48abddd1ffdbbf839db0302d72d80

View File

@@ -329,6 +329,7 @@
"${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework",
"${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework",
"${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework",
"${BUILT_PRODUCTS_DIR}/smart_auth/smart_auth.framework",
"${BUILT_PRODUCTS_DIR}/sqflite/sqflite.framework",
"${BUILT_PRODUCTS_DIR}/sqlite3/sqlite3.framework",
"${BUILT_PRODUCTS_DIR}/sqlite3_flutter_libs/sqlite3_flutter_libs.framework",
@@ -420,6 +421,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/smart_auth.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqflite.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sqlite3_flutter_libs.framework",

View File

@@ -28,6 +28,7 @@ class FFProbeKeys {
static const date = 'date';
static const disposition = 'disposition';
static const duration = 'duration';
static const quickTimeLocation ="com.apple.quicktime.location.ISO6709";
static const durationMicros = 'duration_us';
static const encoder = 'encoder';
static const extraDataSize = 'extradata_size';

View File

@@ -1,5 +1,7 @@
// Adapted from: https://github.com/deckerst/aves
import "dart:developer";
import "package:collection/collection.dart";
import "package:intl/intl.dart";
import "package:photos/models/ffmpeg/channel_layouts.dart";
@@ -10,197 +12,122 @@ import "package:photos/models/ffmpeg/mp4.dart";
import "package:photos/models/location/location.dart";
class FFProbeProps {
final double? captureFps;
final String? androidManufacturer;
final String? androidModel;
final String? androidVersion;
final String? bitRate;
final String? bitsPerRawSample;
final String? byteCount;
final String? channelLayout;
final String? chromaLocation;
final String? codecName;
final String? codecPixelFormat;
final int? codedHeight;
final int? codedWidth;
final String? colorPrimaries;
final String? colorRange;
final String? colorSpace;
final String? colorTransfer;
final String? colorProfile;
final String? compatibleBrands;
final String? creationTime;
final String? displayAspectRatio;
final DateTime? date;
final String? duration;
final String? durationMicros;
final String? extraDataSize;
final String? fieldOrder;
final String? fpsDen;
final int? frameCount;
final String? handlerName;
final bool? hasBFrames;
final int? height;
final String? language;
final Location? location;
final String? majorBrand;
final String? mediaFormat;
final String? mediaType;
final String? minorVersion;
final String? nalLengthSize;
final String? quicktimeLocationAccuracyHorizontal;
final int? rFrameRate;
final String? rotate;
final String? sampleFormat;
final String? sampleRate;
final String? sampleAspectRatio;
final String? sarDen;
final int? segmentCount;
final String? sourceOshash;
final String? startMicros;
final String? startPts;
final String? startTime;
final String? statisticsWritingApp;
final String? statisticsWritingDateUtc;
final String? timeBase;
final String? track;
final String? vendorId;
final int? width;
final String? xiaomiSlowMoment;
Map<String, dynamic>? prodData;
Location? location;
DateTime? creationTime;
String? bitrate;
String? majorBrand;
String? fps;
String? codecWidth;
String? codecHeight;
FFProbeProps({
required this.captureFps,
required this.androidManufacturer,
required this.androidModel,
required this.androidVersion,
required this.bitRate,
required this.bitsPerRawSample,
required this.byteCount,
required this.channelLayout,
required this.chromaLocation,
required this.codecName,
required this.codecPixelFormat,
required this.codedHeight,
required this.codedWidth,
required this.colorPrimaries,
required this.colorRange,
required this.colorSpace,
required this.colorTransfer,
required this.colorProfile,
required this.compatibleBrands,
required this.creationTime,
required this.displayAspectRatio,
required this.date,
required this.duration,
required this.durationMicros,
required this.extraDataSize,
required this.fieldOrder,
required this.fpsDen,
required this.frameCount,
required this.handlerName,
required this.hasBFrames,
required this.height,
required this.language,
required this.location,
required this.majorBrand,
required this.mediaFormat,
required this.mediaType,
required this.minorVersion,
required this.nalLengthSize,
required this.quicktimeLocationAccuracyHorizontal,
required this.rFrameRate,
required this.rotate,
required this.sampleFormat,
required this.sampleRate,
required this.sampleAspectRatio,
required this.sarDen,
required this.segmentCount,
required this.sourceOshash,
required this.startMicros,
required this.startPts,
required this.startTime,
required this.statisticsWritingApp,
required this.statisticsWritingDateUtc,
required this.timeBase,
required this.track,
required this.vendorId,
required this.width,
required this.xiaomiSlowMoment,
});
// dot separated bitrate, fps, codecWidth, codecHeight. Ignore null value
String get videoInfo {
final List<String> 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(' * ');
}
factory FFProbeProps.fromJson(Map<dynamic, dynamic>? json) {
return FFProbeProps(
captureFps:
double.tryParse(json?[FFProbeKeys.androidCaptureFramerate] ?? ""),
androidManufacturer: json?[FFProbeKeys.androidManufacturer],
androidModel: json?[FFProbeKeys.androidModel],
androidVersion: json?[FFProbeKeys.androidVersion],
bitRate: _formatMetric(
json?[FFProbeKeys.bitrate] ?? json?[FFProbeKeys.bps],
'b/s',
),
bitsPerRawSample: json?[FFProbeKeys.bitsPerRawSample],
byteCount: _formatFilesize(json?[FFProbeKeys.byteCount]),
channelLayout: _formatChannelLayout(json?[FFProbeKeys.channelLayout]),
chromaLocation: json?[FFProbeKeys.chromaLocation],
codecName: _formatCodecName(json?[FFProbeKeys.codecName]),
codecPixelFormat:
(json?[FFProbeKeys.codecPixelFormat] as String?)?.toUpperCase(),
codedHeight: int.tryParse(json?[FFProbeKeys.codedHeight] ?? ""),
codedWidth: int.tryParse(json?[FFProbeKeys.codedWidth] ?? ""),
colorPrimaries:
(json?[FFProbeKeys.colorPrimaries] as String?)?.toUpperCase(),
colorRange: (json?[FFProbeKeys.colorRange] as String?)?.toUpperCase(),
colorSpace: (json?[FFProbeKeys.colorSpace] as String?)?.toUpperCase(),
colorTransfer:
(json?[FFProbeKeys.colorTransfer] as String?)?.toUpperCase(),
colorProfile: json?[FFProbeKeys.colorTransfer],
compatibleBrands: json?[FFProbeKeys.compatibleBrands],
creationTime: _formatDate(json?[FFProbeKeys.creationTime] ?? ""),
displayAspectRatio: json?[FFProbeKeys.dar],
date: DateTime.tryParse(json?[FFProbeKeys.date] ?? ""),
duration: _formatDuration(json?[FFProbeKeys.durationMicros]),
durationMicros: formatPreciseDuration(
Duration(
microseconds:
int.tryParse(json?[FFProbeKeys.durationMicros] ?? "") ?? 0,
),
),
extraDataSize: json?[FFProbeKeys.extraDataSize],
fieldOrder: json?[FFProbeKeys.fieldOrder],
fpsDen: json?[FFProbeKeys.fpsDen],
frameCount: int.tryParse(json?[FFProbeKeys.frameCount] ?? ""),
handlerName: json?[FFProbeKeys.handlerName],
hasBFrames: json?[FFProbeKeys.hasBFrames],
height: int.tryParse(json?[FFProbeKeys.height] ?? ""),
language: json?[FFProbeKeys.language],
location: _formatLocation(json?[FFProbeKeys.location]),
majorBrand: json?[FFProbeKeys.majorBrand],
mediaFormat: json?[FFProbeKeys.mediaFormat],
mediaType: json?[FFProbeKeys.mediaType],
minorVersion: json?[FFProbeKeys.minorVersion],
nalLengthSize: json?[FFProbeKeys.nalLengthSize],
quicktimeLocationAccuracyHorizontal:
json?[FFProbeKeys.quicktimeLocationAccuracyHorizontal],
rFrameRate: int.tryParse(json?[FFProbeKeys.rFrameRate] ?? ""),
rotate: json?[FFProbeKeys.rotate],
sampleFormat: json?[FFProbeKeys.sampleFormat],
sampleRate: json?[FFProbeKeys.sampleRate],
sampleAspectRatio: json?[FFProbeKeys.sar],
sarDen: json?[FFProbeKeys.sarDen],
segmentCount: int.tryParse(json?[FFProbeKeys.segmentCount] ?? ""),
sourceOshash: json?[FFProbeKeys.sourceOshash],
startMicros: json?[FFProbeKeys.startMicros],
startPts: json?[FFProbeKeys.startPts],
startTime: _formatDuration(json?[FFProbeKeys.startTime]),
statisticsWritingApp: json?[FFProbeKeys.statisticsWritingApp],
statisticsWritingDateUtc: json?[FFProbeKeys.statisticsWritingDateUtc],
timeBase: json?[FFProbeKeys.timeBase],
track: json?[FFProbeKeys.title],
vendorId: json?[FFProbeKeys.vendorId],
width: int.tryParse(json?[FFProbeKeys.width] ?? ""),
xiaomiSlowMoment: json?[FFProbeKeys.xiaomiSlowMoment],
);
// toString() method
@override
String toString() {
final buffer = StringBuffer();
for (final key in prodData!.keys) {
final value = prodData![key];
if (value != null) {
buffer.writeln('$key: $value');
}
}
return buffer.toString();
}
static fromJson(Map<dynamic, dynamic>? json) {
final Map<String, dynamic> parsedData = {};
final FFProbeProps result = FFProbeProps();
for (final key in json!.keys) {
final stringKey = key.toString();
switch (stringKey) {
case FFProbeKeys.bitrate:
case FFProbeKeys.bps:
result.bitrate = _formatMetric(json[key], 'b/s');
parsedData[stringKey] = result.bitrate;
break;
case FFProbeKeys.byteCount:
parsedData[stringKey] = _formatFilesize(json[key]);
break;
case FFProbeKeys.channelLayout:
parsedData[stringKey] = _formatChannelLayout(json[key]);
break;
case FFProbeKeys.codecName:
parsedData[stringKey] = _formatCodecName(json[key]);
break;
case FFProbeKeys.codecPixelFormat:
case FFProbeKeys.colorPrimaries:
case FFProbeKeys.colorRange:
case FFProbeKeys.colorSpace:
case FFProbeKeys.colorTransfer:
parsedData[stringKey] = (json[key] as String?)?.toUpperCase();
break;
case FFProbeKeys.creationTime:
parsedData[stringKey] = _formatDate(json[key] ?? "");
break;
case FFProbeKeys.durationMicros:
parsedData[stringKey] = formatPreciseDuration(
Duration(microseconds: int.tryParse(json[key] ?? "") ?? 0),
);
break;
case FFProbeKeys.duration:
parsedData[stringKey] = _formatDuration(json[key]);
case FFProbeKeys.location:
result.location = _formatLocation(json[key]);
if (result.location != null) {
parsedData[stringKey] =
'${result.location!.latitude}, ${result.location!.longitude}';
}
break;
case FFProbeKeys.quickTimeLocation:
result.location =
_formatLocation(json[FFProbeKeys.quickTimeLocation]);
if (result.location != null) {
parsedData[FFProbeKeys.location] =
'${result.location!.latitude}, ${result.location!.longitude}';
}
break;
case FFProbeKeys.majorBrand:
result.majorBrand = _formatBrand(json[key]);
parsedData[stringKey] = result.majorBrand;
break;
case FFProbeKeys.startTime:
parsedData[stringKey] = _formatDuration(json[key]);
break;
default:
parsedData[stringKey] = json[key];
}
}
// iterate through the streams
final List<dynamic> streams = json["streams"];
for (final stream in streams) {
for (final key in stream.keys) {
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;
} else if (key == FFProbeKeys.codedHeight) {
result.codecHeight = stream[key].toString();
parsedData[key] = result.codecHeight;
}
}
}
result.prodData = parsedData;
return result;
}
static String _formatBrand(String value) => Mp4.brands[value] ?? value;
@@ -235,11 +162,13 @@ class FFProbeProps {
// input example: '2021-04-12T09:14:37.000000Z'
static String? _formatDate(String value) {
final date = DateTime.tryParse(value);
if (date == null) return value;
final dateInUtc = DateTime.tryParse(value);
if (dateInUtc == null) return value;
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
if (date == epoch) return null;
return date.toIso8601String();
if (dateInUtc == epoch) return null;
final newDate =
DateTime.fromMicrosecondsSinceEpoch(dateInUtc.microsecondsSinceEpoch);
return formatDateTime(newDate, 'en_US', false);
}
// input example: '00:00:05.408000000' or '5.408000'
@@ -281,7 +210,7 @@ class FFProbeProps {
static String? _formatDuration(String? value) {
if (value == null) return null;
final duration = _parseDuration(value);
return duration != null ? formatPreciseDuration(duration) : value;
return duration != null ? formatFriendlyDuration(duration) : value;
}
static String? _formatFilesize(dynamic value) {
@@ -291,6 +220,20 @@ class FFProbeProps {
return size != null ? formatFileSize(asciiLocale, size) : value;
}
static String? _formatFPS(dynamic value) {
if (value == null) return null;
final int? t = int.tryParse(value.split('/')[0]);
final int? b = int.tryParse(value.split('/')[1]);
if (t != null && b != null) {
// return the value upto 2 decimal places. ignore even two decimal places
// if t is perfectly divisible by b
return (t % b == 0)
? (t / b).toStringAsFixed(0)
: (t / b).toStringAsFixed(2);
}
return value;
}
static String _formatLanguage(String value) {
final language = Language.living639_2
.firstWhereOrNull((language) => language.iso639_2 == value);
@@ -315,6 +258,7 @@ class FFProbeProps {
longitude: coordinates[1],
);
} catch (e) {
log('failed to parse location: $value', error: e);
return null;
}
}

View File

@@ -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;

View File

@@ -1,17 +1,22 @@
import "dart:async";
import "dart:developer";
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";
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';
@@ -26,6 +31,7 @@ import 'package:photos/ui/viewer/file_details/exif_item_widgets.dart';
import "package:photos/ui/viewer/file_details/faces_item_widget.dart";
import "package:photos/ui/viewer/file_details/file_properties_item_widget.dart";
import "package:photos/ui/viewer/file_details/location_tags_widget.dart";
import "package:photos/ui/viewer/file_details/video_exif_item.dart";
import "package:photos/utils/exif_util.dart";
import "package:photos/utils/ffprobe_util.dart";
import "package:photos/utils/file_util.dart";
@@ -44,7 +50,6 @@ class FileDetailsWidget extends StatefulWidget {
}
class _FileDetailsWidgetState extends State<FileDetailsWidget> {
final ValueNotifier<Map<String, IfdTag>?> _exifNotifier = ValueNotifier(null);
final Map<String, dynamic> _exifData = {
"focalLength": null,
"fNumber": null,
@@ -64,8 +69,11 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
bool _isImage = false;
late int _currentUserID;
bool showExifListTile = false;
final ValueNotifier<Map<String, IfdTag>?> _exifNotifier = ValueNotifier(null);
final ValueNotifier<bool> hasLocationData = ValueNotifier(false);
final Logger _logger = Logger("_FileDetailsWidgetState");
final ValueNotifier<FFProbeProps?> _videoMetadataNotifier =
ValueNotifier(null);
@override
void initState() {
@@ -96,7 +104,7 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
_exifData["exposureTime"] != null ||
_exifData["ISO"] != null;
});
} else {
} else if (flagService.internalUser && widget.file.isVideo) {
getMediaInfo();
}
getExif(widget.file).then((exif) {
@@ -111,7 +119,6 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
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();
@@ -120,14 +127,19 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
);
return;
}
//todo:(neeraj) Use probe data for back filling location
final _ = await FFProbeUtil.getProperties(mediaInfo);
final properties = await FFProbeUtil.getProperties(mediaInfo);
_videoMetadataNotifier.value = properties;
if (kDebugMode) {
log("videoCustomProps ${properties.toString()}");
log("PropData ${properties.prodData.toString()}");
}
setState(() {});
}
@override
void dispose() {
_exifNotifier.dispose();
_videoMetadataNotifier.dispose();
_peopleChangedEvent.cancel();
super.dispose();
}
@@ -256,6 +268,20 @@ class _FileDetailsWidgetState extends State<FileDetailsWidget> {
},
),
]);
} else if (flagService.internalUser && widget.file.isVideo) {
fileDetailsTiles.addAll([
ValueListenableBuilder(
valueListenable: _videoMetadataNotifier,
builder: (context, value, _) {
return Column(
children: [
VideoExifRowItem(file, value),
const FileDetailsDivider(),
],
);
},
),
]);
}
if (LocalSettings.instance.isFaceIndexingEnabled) {

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/models/file/file.dart';
import 'package:photos/models/file/file_type.dart';
import "package:photos/ui/viewer/file/video_widget_new.dart";
import "package:photos/ui/viewer/file/video_widget.dart";
import "package:photos/ui/viewer/file/zoomable_live_image_new.dart";
class FileWidget extends StatelessWidget {
@@ -38,7 +38,7 @@ class FileWidget extends StatelessWidget {
key: key ?? ValueKey(fileKey),
);
} else if (file.fileType == FileType.video) {
return VideoWidgetNew(
return VideoWidget(
file,
tagPrefix: tagPrefix,
playbackCallback: playbackCallback,

View File

@@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import "package:photos/models/ffmpeg/ffprobe_keys.dart";
import "package:photos/theme/ente_theme.dart";
class VideoExifDialog extends StatelessWidget {
final Map<String, dynamic> probeData;
const VideoExifDialog({Key? key, required this.probeData}) : super(key: key);
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildGeneralInfo(context),
const SizedBox(height: 8),
_buildSection(context, 'Streams', _buildStreamsList(context)),
],
),
),
);
}
Widget _buildSection(BuildContext context, String title, Widget content) {
return ExpansionTile(
initiallyExpanded: true,
title: Text(title, style: getEnteTextTheme(context).largeFaint),
childrenPadding: const EdgeInsets.symmetric(vertical: 2),
tilePadding: const EdgeInsets.symmetric(vertical: 4),
children: [content],
);
}
Widget _buildGeneralInfo(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildInfoRow(context, 'Creation Time', probeData, 'creation_time'),
_buildInfoRow(context, 'Duration', probeData, 'duration'),
_buildInfoRow(context, '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'),
],
);
}
Widget _buildStreamsList(BuildContext context) {
final List<dynamic> streams = probeData['streams'];
final List<Map<String, dynamic>> data = [];
for (final stream in streams) {
final Map<String, dynamic> streamData = {};
for (final key in stream.keys) {
final dynamic value = stream[key];
// print type of value
if (value is int ||
value is double ||
value is String ||
value is bool) {
streamData[key] = stream[key];
} else {
streamData[key] = stream[key].toString();
}
}
data.add(streamData);
}
return Column(
children:
data.map((stream) => _buildStreamInfo(context, stream)).toList(),
);
}
Widget _buildStreamInfo(BuildContext context, Map<String, dynamic> stream) {
String titleString = stream['type']?.toString().toUpperCase() ?? '';
final codeName = stream['codec_name']?.toString().toUpperCase() ?? '';
if (codeName != 'NULL' && codeName.isNotEmpty) {
titleString += ' - $codeName';
}
return ExpansionTile(
title: Text(
titleString,
style: getEnteTextTheme(context).smallBold,
),
childrenPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 4),
tilePadding: const EdgeInsets.symmetric(vertical: 4),
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: stream.entries
.map(
(entry) => _buildInfoRow(context, entry.key, stream, entry.key),
)
.toList(),
),
],
);
}
Widget _buildInfoRow(
BuildContext context,
String rowName,
Map<String, dynamic> data,
String dataKey,
) {
rowName = rowName.replaceAll('_', ' ');
rowName = rowName[0].toUpperCase() + rowName.substring(1);
try {
final value = data[dataKey];
if (value == null) {
return Container(); // Return an empty container if there's no data for the key.
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 150,
child: Text(
rowName,
style: getEnteTextTheme(context).smallMuted,
),
),
Expanded(child: Text(value.toString())),
],
),
);
} catch (e, _) {
return const SizedBox.shrink();
}
}
}

View File

@@ -0,0 +1,86 @@
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/video_exif_dialog.dart";
import "package:photos/utils/toast_util.dart";
class VideoExifRowItem extends StatefulWidget {
final EnteFile file;
final FFProbeProps? props;
const VideoExifRowItem(
this.file,
this.props, {
super.key,
});
@override
State<VideoExifRowItem> createState() => _VideoProbeInfoState();
}
class _VideoProbeInfoState extends State<VideoExifRowItem> {
VoidCallback? _onTap;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return InfoItemWidget(
leadingIcon: Icons.text_snippet_outlined,
title: "Video Info",
subtitleSection:
_exifButton(context, widget.file, widget.props?.prodData),
onTap: _onTap,
);
}
Future<List<Widget>> _exifButton(
BuildContext context,
EnteFile file,
Map<String, dynamic>? exif,
) async {
late final String label;
late final VoidCallback? onTap;
if (exif == null) {
label = S.of(context).loadingExifData;
onTap = null;
} else if (exif.isNotEmpty) {
label = "${widget.props?.videoInfo ?? ''} ..";
onTap = () => showBarModalBottomSheet(
context: context,
builder: (BuildContext context) {
return VideoExifDialog(
probeData: exif,
);
},
shape: const RoundedRectangleBorder(
side: BorderSide(width: 0),
borderRadius: BorderRadius.vertical(
top: Radius.circular(5),
),
),
topControl: const SizedBox.shrink(),
backgroundColor: getEnteColorScheme(context).backgroundElevated,
barrierColor: backdropFaintDark,
enableDrag: true,
);
} else {
label = S.of(context).noExifData;
onTap =
() => showShortToast(context, S.of(context).thisImageHasNoExifData);
}
setState(() {
_onTap = onTap;
});
return Future.value([
Text(label, style: getEnteTextTheme(context).miniBoldMuted),
]);
}
}

View File

@@ -14,7 +14,7 @@ class FFProbeUtil {
static Future<FFProbeProps> getProperties(
MediaInformation mediaInformation,
) async {
final properties = await _getMetadata(mediaInformation);
final properties = await getMetadata(mediaInformation);
try {
return FFProbeProps.fromJson(properties);
@@ -28,7 +28,7 @@ class FFProbeUtil {
}
}
static Future<Map> _getMetadata(MediaInformation information) async {
static Future<Map> getMetadata(MediaInformation information) async {
final props = information.getAllProperties();
if (props == null) return {};

View File

@@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.9.7+907
version: 0.9.8+908
publish_to: none
environment: