Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
weblate committed Jan 9, 2025
2 parents b6f55a8 + 550c72e commit 06ce5e3
Show file tree
Hide file tree
Showing 20 changed files with 510 additions and 93 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.

## <a id="unreleased"></a>[Unreleased]

### Changed

- Video: use `media-kit` instead of `ffmpeg-kit` for metadata fetch
- Info: show video chapters

### Fixed

- crash when cataloguing some videos

## <a id="v1.12.1"></a>[v1.12.1] - 2025-01-05

### Added
Expand Down
5 changes: 0 additions & 5 deletions lib/model/app/dependencies.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,6 @@ class Dependencies {
licenseUrl: 'https://github.com/material-foundation/flutter-packages/blob/main/packages/dynamic_color/LICENSE',
sourceUrl: 'https://github.com/material-foundation/flutter-packages/tree/main/packages/dynamic_color',
),
Dependency(
name: 'FFmpegKit (Aves fork)',
license: lgpl3,
sourceUrl: 'https://github.com/deckerst/ffmpeg-kit',
),
Dependency(
name: 'Floating',
license: mit,
Expand Down
1 change: 1 addition & 0 deletions lib/model/entry/entry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ class AvesEntry with AvesEntryBase {

// the MIME type reported by the Media Store is unreliable
// so we use the one found during cataloguing if possible
@override
String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;

bool get isCatalogued => _catalogMetadata != null;
Expand Down
18 changes: 17 additions & 1 deletion lib/model/entry/extensions/info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/metadata/svg_metadata_service.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/theme/format.dart';
import 'package:aves/theme/text.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart';
import 'package:aves_model/aves_model.dart';
Expand Down Expand Up @@ -82,6 +83,21 @@ extension ExtraAvesEntryInfo on AvesEntry {
directories.add(MetadataDirectory(MetadataDirectory.mediaDirectory, _toSortedTags(formattedMediaTags)));
}

if (mediaInfo.containsKey(Keys.chapters)) {
final allChapters = (mediaInfo.remove(Keys.chapters) as List).cast<Map>();
if (allChapters.isNotEmpty) {
allChapters.sortBy((v) => v[Keys.time] as num? ?? 0);

final chapterTags = SplayTreeMap.of(Map.fromEntries(allChapters.mapIndexed((i, chapter) {
final chapterNumber = i + 1;
final time = Duration(seconds: (chapter[Keys.time] as num? ?? 0).round());
final title = chapter[Keys.title] as String? ?? 'Chapter $chapterNumber';
return MapEntry('$chapterNumber${AText.separator}${formatFriendlyDuration(time)}', title);
})), compareNatural);
directories.add(MetadataDirectory('Chapters', chapterTags));
}
}

if (mediaInfo.containsKey(Keys.streams)) {
String getTypeText(Map stream) {
final type = stream[Keys.streamType] ?? MediaStreamTypes.unknown;
Expand All @@ -96,7 +112,7 @@ extension ExtraAvesEntryInfo on AvesEntry {
case MediaStreamTypes.timedText:
return 'Text';
case MediaStreamTypes.video:
return stream.containsKey(Keys.fpsDen) ? 'Video' : 'Image';
return stream.containsKey(Keys.fpsDen) || stream.containsKey(Keys.fps) ? 'Video' : 'Image';
case MediaStreamTypes.unknown:
default:
return 'Unknown';
Expand Down
78 changes: 45 additions & 33 deletions lib/model/media/video/metadata.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:aves/model/media/video/codecs.dart';
import 'package:aves/model/media/video/profiles/aac.dart';
import 'package:aves/model/media/video/profiles/h264.dart';
import 'package:aves/model/media/video/profiles/hevc.dart';
import 'package:aves/model/media/video/stereo_3d_modes.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/ref/languages.dart';
import 'package:aves/ref/locales.dart';
Expand Down Expand Up @@ -52,10 +53,10 @@ class VideoMetadataFormatter {
final streams = mediaInfo[Keys.streams];
if (streams is List) {
final allStreamInfo = streams.cast<Map>();
final sizedStream = allStreamInfo.firstWhereOrNull((stream) => stream.containsKey(Keys.width) && stream.containsKey(Keys.height));
final sizedStream = allStreamInfo.firstWhereOrNull((stream) => stream.containsKey(Keys.videoWidth) && stream.containsKey(Keys.videoHeight));
if (sizedStream != null) {
final width = sizedStream[Keys.width];
final height = sizedStream[Keys.height];
final width = sizedStream[Keys.videoWidth];
final height = sizedStream[Keys.videoHeight];
if (width is int && height is int) {
fields['width'] = width;
fields['height'] = height;
Expand All @@ -68,7 +69,7 @@ class VideoMetadataFormatter {
fields['durationMillis'] = (durationMicros / 1000).round();
} else {
final duration = _parseDuration(mediaInfo[Keys.duration]);
if (duration != null) {
if (duration != null && duration > Duration.zero) {
fields['durationMillis'] = duration.inMilliseconds;
}
}
Expand All @@ -82,7 +83,7 @@ class VideoMetadataFormatter {

if (entry.mimeType == MimeTypes.avif) {
final duration = _parseDuration(mediaInfo[Keys.duration]);
if (duration == null) return null;
if (duration == null || duration == Duration.zero) return null;

catalogMetadata = catalogMetadata.copyWith(isAnimated: true);
}
Expand Down Expand Up @@ -189,13 +190,14 @@ class VideoMetadataFormatter {
}
key = (key ?? (kv.key as String)).toLowerCase();

void save(String key, String? value) {
void save(String key, dynamic value) {
if (value != null) {
dir[keyLanguage != null ? '$key ($keyLanguage)' : key] = value;
dir[keyLanguage != null ? '$key ($keyLanguage)' : key] = value.toString();
}
}

switch (key) {
case Keys.chapters:
case Keys.codecLevel:
case Keys.codecTag:
case Keys.codecTagString:
Expand All @@ -219,24 +221,22 @@ class VideoMetadataFormatter {
break;
case Keys.androidCaptureFramerate:
final captureFps = double.parse(value);
save('Capture Frame Rate', '${roundToPrecision(captureFps, decimals: 3).toString()} FPS');
save('Capture Frame Rate', '${roundToPrecision(captureFps, decimals: 3)} FPS');
case Keys.androidManufacturer:
save('Android Manufacturer', value);
case Keys.androidModel:
save('Android Model', value);
case Keys.androidVersion:
save('Android Version', value);
case Keys.audioChannels:
save('Audio Channels', value);
case Keys.bitrate:
case Keys.bps:
save('Bit Rate', _formatMetric(value, 'b/s'));
case Keys.bitsPerRawSample:
save('Bits Per Raw Sample', value);
case Keys.byteCount:
save('Size', _formatFilesize(value));
case Keys.channelLayout:
save('Channel Layout', _formatChannelLayout(value));
case Keys.chromaLocation:
save('Chroma Location', value);
case Keys.codecName:
if (value != 'none') {
save('Format', _formatCodecName(value));
Expand All @@ -245,20 +245,28 @@ class VideoMetadataFormatter {
if (streamType == MediaStreamTypes.video) {
// this is just a short name used by FFmpeg
// user-friendly descriptions for related enums are defined in libavutil/pixfmt.h
save('Pixel Format', (value as String).toUpperCase());
save('Pixel Format', value.toString().toUpperCase());
}
case Keys.hwPixelFormat:
save('Hardware Pixel Format', value.toString().toUpperCase());
case Keys.codedHeight:
save('Coded Height', '$value pixels');
case Keys.codedWidth:
save('Coded Width', '$value pixels');
case Keys.decoderHeight:
save('Decoder Height', '$value pixels');
case Keys.decoderWidth:
save('Decoder Width', '$value pixels');
case Keys.colorMatrix:
save('Color Matrix', value.toString().toUpperCase());
case Keys.colorPrimaries:
save('Color Primaries', (value as String).toUpperCase());
save('Color Primaries', value.toString().toUpperCase());
case Keys.colorRange:
save('Color Range', (value as String).toUpperCase());
save('Color Range', value.toString().toUpperCase());
case Keys.colorSpace:
save('Color Space', (value as String).toUpperCase());
save('Color Space', value.toString().toUpperCase());
case Keys.colorTransfer:
save('Color Transfer', (value as String).toUpperCase());
save('Color Transfer', value.toString().toUpperCase());
case Keys.codecProfileId:
{
final profile = int.tryParse(value);
Expand Down Expand Up @@ -294,8 +302,6 @@ class VideoMetadataFormatter {
save('Compatible Brands', formattedBrands);
case Keys.creationTime:
save('Creation Time', _formatDate(value));
case Keys.dar:
save('Display Aspect Ratio', value);
case Keys.date:
if (value is String && value != '0') {
final charCount = value.length;
Expand All @@ -307,28 +313,26 @@ class VideoMetadataFormatter {
if (value != 0) save('Duration', formatPreciseDuration(Duration(microseconds: value)));
case Keys.extraDataSize:
save('Extra Data Size', _formatFilesize(value));
case Keys.fieldOrder:
save('Field Order', value);
case Keys.fps:
save('Frame Rate', '${roundToPrecision(info[Keys.fps], decimals: 3)} FPS');
case Keys.fpsDen:
save('Frame Rate', '${roundToPrecision(info[Keys.fpsNum] / info[Keys.fpsDen], decimals: 3).toString()} FPS');
save('Frame Rate', '${roundToPrecision(info[Keys.fpsNum] / info[Keys.fpsDen], decimals: 3)} FPS');
case Keys.frameCount:
save('Frame Count', value);
case Keys.handlerName:
save('Handler Name', value);
case Keys.gamma:
save('Gamma', value.toString().toUpperCase());
case Keys.hasBFrames:
save('Has B-Frames', value);
case Keys.height:
save('Height', '$value pixels');
case Keys.hearingImpaired:
save('Hearing impaired', value);
case Keys.language:
if (value != 'und') save('Language', _formatLanguage(value));
case Keys.location:
save('Location', _formatLocation(value));
case Keys.majorBrand:
save('Major Brand', _formatBrand(value));
case Keys.mediaFormat:
save('Format', (value as String).splitMapJoin(',', onMatch: (s) => ', ', onNonMatch: _formatCodecName));
case Keys.mediaType:
save('Media Type', value);
save('Format', value.toString().splitMapJoin(',', onMatch: (s) => ', ', onNonMatch: _formatCodecName));
case Keys.minorVersion:
if (value != '0') save('Minor Version', value);
case Keys.nalLengthSize:
Expand All @@ -347,7 +351,7 @@ class VideoMetadataFormatter {
case Keys.rotate:
save('Rotation', '$value°');
case Keys.sampleFormat:
save('Sample Format', (value as String).toUpperCase());
save('Sample Format', value.toString().toUpperCase());
case Keys.sampleRate:
save('Sample Rate', _formatMetric(value, 'Hz'));
case Keys.sar:
Expand All @@ -371,18 +375,24 @@ class VideoMetadataFormatter {
save('Stats Writing App', value);
case Keys.statisticsWritingDateUtc:
save('Stats Writing Date', _formatDate(value));
case Keys.stereo3dMode:
save('Stereo 3D Mode', _formatStereo3dMode(value));
case Keys.timeBase:
save('Time Base', value);
case Keys.track:
if (value != '0') save('Track', value);
case Keys.vendorId:
save('Vendor ID', value);
case Keys.width:
save('Width', '$value pixels');
case Keys.videoHeight:
save('Video Height', '$value pixels');
case Keys.videoWidth:
save('Video Width', '$value pixels');
case Keys.visualImpaired:
save('Visual impaired', value);
case Keys.xiaomiSlowMoment:
save('Xiaomi Slow Moment', value);
default:
save(key.toSentenceCase(), value.toString());
save(key.toSentenceCase(), value);
}
} catch (error) {
debugPrint('failed to process video info key=${kv.key} value=${kv.value}, error=$error');
Expand Down Expand Up @@ -411,6 +421,8 @@ class VideoMetadataFormatter {
return date.toIso8601String();
}

static String _formatStereo3dMode(String value) => Stereo3dModes.names[value] ?? value;

// input example: '00:00:05.408000000' or '5.408000'
static Duration? _parseDuration(String? value) {
if (value == null) return null;
Expand Down
22 changes: 22 additions & 0 deletions lib/model/media/video/stereo_3d_modes.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class Stereo3dModes {
static const names = {
'ab2l': 'above below half height left first',
'tb2l': 'above below half height left first',
'ab2r': 'above below half height right first',
'tb2r': 'above below half height right first',
'abl': 'above below left first',
'tbl': 'above below left first',
'abr': 'above below right first',
'tbr': 'above below right first',
'al': 'alternating frames left first',
'ar': 'alternating frames right first',
'sbs2l': 'side by side half width left first',
'sbs2r': 'side by side half width right first',
'sbsl': 'side by side left first',
'sbsr': 'side by side right first',
'irl': 'interleave rows left first',
'irr': 'interleave rows right first',
'icl': 'interleave columns left first',
'icr': 'interleave columns right first',
};
}
1 change: 1 addition & 0 deletions lib/services/analysis_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ Future<void> _init() async {
await mobileServices.init();
await settings.init(monitorPlatformSettings: false);
await reportService.init();
videoMetadataFetcher.init();

final analyzer = Analyzer();
_channel.setMethodCallHandler((call) {
Expand Down
3 changes: 1 addition & 2 deletions lib/services/common/services.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import 'package:aves_report_platform/aves_report_platform.dart';
import 'package:aves_services/aves_services.dart';
import 'package:aves_services_platform/aves_services_platform.dart';
import 'package:aves_video/aves_video.dart';
import 'package:aves_video_ffmpeg/aves_video_ffmpeg.dart';
import 'package:aves_video_mpv/aves_video_mpv.dart';
import 'package:get_it/get_it.dart';
import 'package:path/path.dart' as p;
Expand Down Expand Up @@ -58,7 +57,7 @@ void initPlatformServices() {
getIt.registerLazySingleton<AvesAvailability>(LiveAvesAvailability.new);
getIt.registerLazySingleton<LocalMediaDb>(SqfliteLocalMediaDb.new);
getIt.registerLazySingleton<AvesVideoControllerFactory>(MpvVideoControllerFactory.new);
getIt.registerLazySingleton<AvesVideoMetadataFetcher>(FfmpegVideoMetadataFetcher.new);
getIt.registerLazySingleton<AvesVideoMetadataFetcher>(MpvVideoMetadataFetcher.new);

getIt.registerLazySingleton<AppService>(PlatformAppService.new);
getIt.registerLazySingleton<AppProfileService>(PlatformAppProfileService.new);
Expand Down
7 changes: 5 additions & 2 deletions lib/utils/string_utils.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
extension ExtraString on String {
static final _sentenceCaseStep1 = RegExp(r'([A-Z][a-z]|\[)');
static final _sentenceCaseStep2 = RegExp(r'([a-z])([A-Z])');
static final _sentenceCaseStep2 = RegExp(r'_([a-z])');
static final _sentenceCaseStep3 = RegExp(r'([a-z])([A-Z])');

String toSentenceCase() {
var s = replaceFirstMapped(RegExp('.'), (m) => m.group(0)!.toUpperCase());
return s.replaceAllMapped(_sentenceCaseStep1, (m) => ' ${m.group(1)}').replaceAllMapped(_sentenceCaseStep2, (m) => '${m.group(1)} ${m.group(2)}').trim();
s = s.replaceAllMapped(_sentenceCaseStep1, (m) => ' ${m.group(1)}');
s = s.replaceAllMapped(_sentenceCaseStep2, (m) => m.group(0)!.toUpperCase()).replaceAll('_', ' ');
return s.replaceAllMapped(_sentenceCaseStep3, (m) => '${m.group(1)} ${m.group(2)}').trim();
}
}
1 change: 1 addition & 0 deletions lib/widgets/aves_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
await _onTvLayoutChanged();
_monitorSettings();
videoControllerFactory.init();
videoMetadataFetcher.init();

unawaited(deviceService.setLocaleConfig(AvesApp.supportedLocales));
unawaited(storageService.deleteTempDirectory());
Expand Down
2 changes: 2 additions & 0 deletions plugins/aves_model/lib/src/entry/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ mixin AvesEntryBase {

int? get pageId;

String get mimeType;

String? get path;

String? get bestTitle;
Expand Down
Loading

0 comments on commit 06ce5e3

Please sign in to comment.