[mob] Show video metadata inside fileInfo (#2466)
## Description ## Tests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
142
mobile/lib/ui/viewer/file/video_exif_dialog.dart
Normal file
142
mobile/lib/ui/viewer/file/video_exif_dialog.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
86
mobile/lib/ui/viewer/file_details/video_exif_item.dart
Normal file
86
mobile/lib/ui/viewer/file_details/video_exif_item.dart
Normal 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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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 {};
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user