import 'package:aves_model/aves_model.dart'; import 'package:aves_video/aves_video.dart'; import 'package:ffmpeg_kit_flutter/ffmpeg_kit_config.dart'; import 'package:ffmpeg_kit_flutter/ffprobe_kit.dart'; import 'package:flutter/foundation.dart'; class FfmpegVideoMetadataFetcher extends AvesVideoMetadataFetcher { static const chaptersKey = 'chapters'; static const formatKey = 'format'; static const streamsKey = 'streams'; @override void init() {} @override Future getMetadata(AvesEntryBase entry) async { var uri = entry.uri; if (uri.startsWith('content://')) { final safUri = await FFmpegKitConfig.getSafParameterForRead(uri); if (safUri == null) { debugPrint('failed to get SAF URI for entry=$entry'); return {}; } uri = safUri; } final session = await FFprobeKit.getMediaInformation(uri); final information = session.getMediaInformation(); if (information == null) { final failStackTrace = await session.getFailStackTrace(); final output = await session.getOutput(); debugPrint('failed to get video metadata for entry=$entry, failStackTrace=$failStackTrace, output=$output'); return {}; } final props = information.getAllProperties(); if (props == null) return {}; final chapters = props[chaptersKey]; if (chapters is List) { if (chapters.isEmpty) { props.remove(chaptersKey); } } final format = props.remove(formatKey); if (format is Map) { format.remove(Keys.filename); format.remove('size'); _normalizeGroup(format); props.addAll(format); } final streams = props[streamsKey]; if (streams is List) { streams.forEach((stream) { if (stream is Map) { _normalizeGroup(stream); final fps = stream[Keys.avgFrameRate]; if (fps is String) { final parts = fps.split('/'); if (parts.length == 2) { final num = int.tryParse(parts[0]); final den = int.tryParse(parts[1]); if (num != null && den != null) { if (den > 0) { stream[Keys.fpsNum] = num; stream[Keys.fpsDen] = den; } stream.remove(Keys.avgFrameRate); } } } final disposition = stream[Keys.disposition]; if (disposition is Map) { disposition.removeWhere((key, value) => value == 0); stream[Keys.disposition] = disposition.keys.join(', '); } final idValue = stream['id']; if (idValue is String) { final id = int.tryParse(idValue); if (id != null) { stream[Keys.index] = id - 1; stream.remove('id'); } } if (stream[Keys.streamType] == 'data') { stream[Keys.streamType] = MediaStreamTypes.metadata; } } }); } return props; } void _normalizeGroup(Map stream) { void replaceKey(k1, k2) { final v = stream.remove(k1); if (v != null) { stream[k2] = v; } } replaceKey('bit_rate', Keys.bitrate); replaceKey('codec_type', Keys.streamType); replaceKey('format_name', Keys.mediaFormat); replaceKey('level', Keys.codecLevel); replaceKey('nb_frames', Keys.frameCount); replaceKey('pix_fmt', Keys.codecPixelFormat); replaceKey('profile', Keys.codecProfileId); final tags = stream.remove('tags'); if (tags is Map) { stream.addAll(tags); } { Keys.codecProfileId, Keys.rFrameRate, 'bits_per_sample', 'closed_captions', 'codec_long_name', 'film_grain', 'has_b_frames', 'start_pts', 'start_time', 'vendor_id', }.forEach((key) { final value = stream[key]; switch (value) { case final num v: if (v == 0) { stream.remove(key); } case final String v: if (double.tryParse(v) == 0 || v == '0/0' || v == 'unknown' || v == '[0][0][0][0]') { stream.remove(key); } } }); } }