aves/plugins/aves_video_ffmpeg/lib/src/metadata.dart
2023-08-05 19:57:07 +02:00

146 lines
4.1 KiB
Dart

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<Map> 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<dynamic, dynamic> 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);
}
<String>{
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);
}
}
});
}
}