#1368 crashfix, using media-kit instead of ffmpeg-kit for video metadata fetch;
info: show video chapters
This commit is contained in:
parent
07f253d587
commit
550c72e994
20 changed files with 510 additions and 93 deletions
|
@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## <a id="unreleased"></a>[Unreleased]
|
## <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
|
## <a id="v1.12.1"></a>[v1.12.1] - 2025-01-05
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -73,11 +73,6 @@ class Dependencies {
|
||||||
licenseUrl: 'https://github.com/material-foundation/flutter-packages/blob/main/packages/dynamic_color/LICENSE',
|
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',
|
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(
|
Dependency(
|
||||||
name: 'Floating',
|
name: 'Floating',
|
||||||
license: mit,
|
license: mit,
|
||||||
|
|
|
@ -232,6 +232,7 @@ class AvesEntry with AvesEntryBase {
|
||||||
|
|
||||||
// the MIME type reported by the Media Store is unreliable
|
// the MIME type reported by the Media Store is unreliable
|
||||||
// so we use the one found during cataloguing if possible
|
// so we use the one found during cataloguing if possible
|
||||||
|
@override
|
||||||
String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;
|
String get mimeType => _catalogMetadata?.mimeType ?? sourceMimeType;
|
||||||
|
|
||||||
bool get isCatalogued => _catalogMetadata != null;
|
bool get isCatalogued => _catalogMetadata != null;
|
||||||
|
|
|
@ -9,6 +9,7 @@ import 'package:aves/ref/mime_types.dart';
|
||||||
import 'package:aves/services/common/services.dart';
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/services/metadata/svg_metadata_service.dart';
|
import 'package:aves/services/metadata/svg_metadata_service.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
|
import 'package:aves/theme/format.dart';
|
||||||
import 'package:aves/theme/text.dart';
|
import 'package:aves/theme/text.dart';
|
||||||
import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart';
|
import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart';
|
||||||
import 'package:aves_model/aves_model.dart';
|
import 'package:aves_model/aves_model.dart';
|
||||||
|
@ -82,6 +83,21 @@ extension ExtraAvesEntryInfo on AvesEntry {
|
||||||
directories.add(MetadataDirectory(MetadataDirectory.mediaDirectory, _toSortedTags(formattedMediaTags)));
|
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)) {
|
if (mediaInfo.containsKey(Keys.streams)) {
|
||||||
String getTypeText(Map stream) {
|
String getTypeText(Map stream) {
|
||||||
final type = stream[Keys.streamType] ?? MediaStreamTypes.unknown;
|
final type = stream[Keys.streamType] ?? MediaStreamTypes.unknown;
|
||||||
|
@ -96,7 +112,7 @@ extension ExtraAvesEntryInfo on AvesEntry {
|
||||||
case MediaStreamTypes.timedText:
|
case MediaStreamTypes.timedText:
|
||||||
return 'Text';
|
return 'Text';
|
||||||
case MediaStreamTypes.video:
|
case MediaStreamTypes.video:
|
||||||
return stream.containsKey(Keys.fpsDen) ? 'Video' : 'Image';
|
return stream.containsKey(Keys.fpsDen) || stream.containsKey(Keys.fps) ? 'Video' : 'Image';
|
||||||
case MediaStreamTypes.unknown:
|
case MediaStreamTypes.unknown:
|
||||||
default:
|
default:
|
||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
|
|
|
@ -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/aac.dart';
|
||||||
import 'package:aves/model/media/video/profiles/h264.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/profiles/hevc.dart';
|
||||||
|
import 'package:aves/model/media/video/stereo_3d_modes.dart';
|
||||||
import 'package:aves/model/metadata/catalog.dart';
|
import 'package:aves/model/metadata/catalog.dart';
|
||||||
import 'package:aves/ref/languages.dart';
|
import 'package:aves/ref/languages.dart';
|
||||||
import 'package:aves/ref/locales.dart';
|
import 'package:aves/ref/locales.dart';
|
||||||
|
@ -52,10 +53,10 @@ class VideoMetadataFormatter {
|
||||||
final streams = mediaInfo[Keys.streams];
|
final streams = mediaInfo[Keys.streams];
|
||||||
if (streams is List) {
|
if (streams is List) {
|
||||||
final allStreamInfo = streams.cast<Map>();
|
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) {
|
if (sizedStream != null) {
|
||||||
final width = sizedStream[Keys.width];
|
final width = sizedStream[Keys.videoWidth];
|
||||||
final height = sizedStream[Keys.height];
|
final height = sizedStream[Keys.videoHeight];
|
||||||
if (width is int && height is int) {
|
if (width is int && height is int) {
|
||||||
fields['width'] = width;
|
fields['width'] = width;
|
||||||
fields['height'] = height;
|
fields['height'] = height;
|
||||||
|
@ -68,7 +69,7 @@ class VideoMetadataFormatter {
|
||||||
fields['durationMillis'] = (durationMicros / 1000).round();
|
fields['durationMillis'] = (durationMicros / 1000).round();
|
||||||
} else {
|
} else {
|
||||||
final duration = _parseDuration(mediaInfo[Keys.duration]);
|
final duration = _parseDuration(mediaInfo[Keys.duration]);
|
||||||
if (duration != null) {
|
if (duration != null && duration > Duration.zero) {
|
||||||
fields['durationMillis'] = duration.inMilliseconds;
|
fields['durationMillis'] = duration.inMilliseconds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,7 +83,7 @@ class VideoMetadataFormatter {
|
||||||
|
|
||||||
if (entry.mimeType == MimeTypes.avif) {
|
if (entry.mimeType == MimeTypes.avif) {
|
||||||
final duration = _parseDuration(mediaInfo[Keys.duration]);
|
final duration = _parseDuration(mediaInfo[Keys.duration]);
|
||||||
if (duration == null) return null;
|
if (duration == null || duration == Duration.zero) return null;
|
||||||
|
|
||||||
catalogMetadata = catalogMetadata.copyWith(isAnimated: true);
|
catalogMetadata = catalogMetadata.copyWith(isAnimated: true);
|
||||||
}
|
}
|
||||||
|
@ -189,13 +190,14 @@ class VideoMetadataFormatter {
|
||||||
}
|
}
|
||||||
key = (key ?? (kv.key as String)).toLowerCase();
|
key = (key ?? (kv.key as String)).toLowerCase();
|
||||||
|
|
||||||
void save(String key, String? value) {
|
void save(String key, dynamic value) {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
dir[keyLanguage != null ? '$key ($keyLanguage)' : key] = value;
|
dir[keyLanguage != null ? '$key ($keyLanguage)' : key] = value.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
|
case Keys.chapters:
|
||||||
case Keys.codecLevel:
|
case Keys.codecLevel:
|
||||||
case Keys.codecTag:
|
case Keys.codecTag:
|
||||||
case Keys.codecTagString:
|
case Keys.codecTagString:
|
||||||
|
@ -219,24 +221,22 @@ class VideoMetadataFormatter {
|
||||||
break;
|
break;
|
||||||
case Keys.androidCaptureFramerate:
|
case Keys.androidCaptureFramerate:
|
||||||
final captureFps = double.parse(value);
|
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:
|
case Keys.androidManufacturer:
|
||||||
save('Android Manufacturer', value);
|
save('Android Manufacturer', value);
|
||||||
case Keys.androidModel:
|
case Keys.androidModel:
|
||||||
save('Android Model', value);
|
save('Android Model', value);
|
||||||
case Keys.androidVersion:
|
case Keys.androidVersion:
|
||||||
save('Android Version', value);
|
save('Android Version', value);
|
||||||
|
case Keys.audioChannels:
|
||||||
|
save('Audio Channels', value);
|
||||||
case Keys.bitrate:
|
case Keys.bitrate:
|
||||||
case Keys.bps:
|
case Keys.bps:
|
||||||
save('Bit Rate', _formatMetric(value, 'b/s'));
|
save('Bit Rate', _formatMetric(value, 'b/s'));
|
||||||
case Keys.bitsPerRawSample:
|
|
||||||
save('Bits Per Raw Sample', value);
|
|
||||||
case Keys.byteCount:
|
case Keys.byteCount:
|
||||||
save('Size', _formatFilesize(value));
|
save('Size', _formatFilesize(value));
|
||||||
case Keys.channelLayout:
|
case Keys.channelLayout:
|
||||||
save('Channel Layout', _formatChannelLayout(value));
|
save('Channel Layout', _formatChannelLayout(value));
|
||||||
case Keys.chromaLocation:
|
|
||||||
save('Chroma Location', value);
|
|
||||||
case Keys.codecName:
|
case Keys.codecName:
|
||||||
if (value != 'none') {
|
if (value != 'none') {
|
||||||
save('Format', _formatCodecName(value));
|
save('Format', _formatCodecName(value));
|
||||||
|
@ -245,20 +245,28 @@ class VideoMetadataFormatter {
|
||||||
if (streamType == MediaStreamTypes.video) {
|
if (streamType == MediaStreamTypes.video) {
|
||||||
// this is just a short name used by FFmpeg
|
// this is just a short name used by FFmpeg
|
||||||
// user-friendly descriptions for related enums are defined in libavutil/pixfmt.h
|
// 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:
|
case Keys.codedHeight:
|
||||||
save('Coded Height', '$value pixels');
|
save('Coded Height', '$value pixels');
|
||||||
case Keys.codedWidth:
|
case Keys.codedWidth:
|
||||||
save('Coded Width', '$value pixels');
|
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:
|
case Keys.colorPrimaries:
|
||||||
save('Color Primaries', (value as String).toUpperCase());
|
save('Color Primaries', value.toString().toUpperCase());
|
||||||
case Keys.colorRange:
|
case Keys.colorRange:
|
||||||
save('Color Range', (value as String).toUpperCase());
|
save('Color Range', value.toString().toUpperCase());
|
||||||
case Keys.colorSpace:
|
case Keys.colorSpace:
|
||||||
save('Color Space', (value as String).toUpperCase());
|
save('Color Space', value.toString().toUpperCase());
|
||||||
case Keys.colorTransfer:
|
case Keys.colorTransfer:
|
||||||
save('Color Transfer', (value as String).toUpperCase());
|
save('Color Transfer', value.toString().toUpperCase());
|
||||||
case Keys.codecProfileId:
|
case Keys.codecProfileId:
|
||||||
{
|
{
|
||||||
final profile = int.tryParse(value);
|
final profile = int.tryParse(value);
|
||||||
|
@ -294,8 +302,6 @@ class VideoMetadataFormatter {
|
||||||
save('Compatible Brands', formattedBrands);
|
save('Compatible Brands', formattedBrands);
|
||||||
case Keys.creationTime:
|
case Keys.creationTime:
|
||||||
save('Creation Time', _formatDate(value));
|
save('Creation Time', _formatDate(value));
|
||||||
case Keys.dar:
|
|
||||||
save('Display Aspect Ratio', value);
|
|
||||||
case Keys.date:
|
case Keys.date:
|
||||||
if (value is String && value != '0') {
|
if (value is String && value != '0') {
|
||||||
final charCount = value.length;
|
final charCount = value.length;
|
||||||
|
@ -307,18 +313,18 @@ class VideoMetadataFormatter {
|
||||||
if (value != 0) save('Duration', formatPreciseDuration(Duration(microseconds: value)));
|
if (value != 0) save('Duration', formatPreciseDuration(Duration(microseconds: value)));
|
||||||
case Keys.extraDataSize:
|
case Keys.extraDataSize:
|
||||||
save('Extra Data Size', _formatFilesize(value));
|
save('Extra Data Size', _formatFilesize(value));
|
||||||
case Keys.fieldOrder:
|
case Keys.fps:
|
||||||
save('Field Order', value);
|
save('Frame Rate', '${roundToPrecision(info[Keys.fps], decimals: 3)} FPS');
|
||||||
case Keys.fpsDen:
|
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:
|
case Keys.frameCount:
|
||||||
save('Frame Count', value);
|
save('Frame Count', value);
|
||||||
case Keys.handlerName:
|
case Keys.gamma:
|
||||||
save('Handler Name', value);
|
save('Gamma', value.toString().toUpperCase());
|
||||||
case Keys.hasBFrames:
|
case Keys.hasBFrames:
|
||||||
save('Has B-Frames', value);
|
save('Has B-Frames', value);
|
||||||
case Keys.height:
|
case Keys.hearingImpaired:
|
||||||
save('Height', '$value pixels');
|
save('Hearing impaired', value);
|
||||||
case Keys.language:
|
case Keys.language:
|
||||||
if (value != 'und') save('Language', _formatLanguage(value));
|
if (value != 'und') save('Language', _formatLanguage(value));
|
||||||
case Keys.location:
|
case Keys.location:
|
||||||
|
@ -326,9 +332,7 @@ class VideoMetadataFormatter {
|
||||||
case Keys.majorBrand:
|
case Keys.majorBrand:
|
||||||
save('Major Brand', _formatBrand(value));
|
save('Major Brand', _formatBrand(value));
|
||||||
case Keys.mediaFormat:
|
case Keys.mediaFormat:
|
||||||
save('Format', (value as String).splitMapJoin(',', onMatch: (s) => ', ', onNonMatch: _formatCodecName));
|
save('Format', value.toString().splitMapJoin(',', onMatch: (s) => ', ', onNonMatch: _formatCodecName));
|
||||||
case Keys.mediaType:
|
|
||||||
save('Media Type', value);
|
|
||||||
case Keys.minorVersion:
|
case Keys.minorVersion:
|
||||||
if (value != '0') save('Minor Version', value);
|
if (value != '0') save('Minor Version', value);
|
||||||
case Keys.nalLengthSize:
|
case Keys.nalLengthSize:
|
||||||
|
@ -347,7 +351,7 @@ class VideoMetadataFormatter {
|
||||||
case Keys.rotate:
|
case Keys.rotate:
|
||||||
save('Rotation', '$value°');
|
save('Rotation', '$value°');
|
||||||
case Keys.sampleFormat:
|
case Keys.sampleFormat:
|
||||||
save('Sample Format', (value as String).toUpperCase());
|
save('Sample Format', value.toString().toUpperCase());
|
||||||
case Keys.sampleRate:
|
case Keys.sampleRate:
|
||||||
save('Sample Rate', _formatMetric(value, 'Hz'));
|
save('Sample Rate', _formatMetric(value, 'Hz'));
|
||||||
case Keys.sar:
|
case Keys.sar:
|
||||||
|
@ -371,18 +375,24 @@ class VideoMetadataFormatter {
|
||||||
save('Stats Writing App', value);
|
save('Stats Writing App', value);
|
||||||
case Keys.statisticsWritingDateUtc:
|
case Keys.statisticsWritingDateUtc:
|
||||||
save('Stats Writing Date', _formatDate(value));
|
save('Stats Writing Date', _formatDate(value));
|
||||||
|
case Keys.stereo3dMode:
|
||||||
|
save('Stereo 3D Mode', _formatStereo3dMode(value));
|
||||||
case Keys.timeBase:
|
case Keys.timeBase:
|
||||||
save('Time Base', value);
|
save('Time Base', value);
|
||||||
case Keys.track:
|
case Keys.track:
|
||||||
if (value != '0') save('Track', value);
|
if (value != '0') save('Track', value);
|
||||||
case Keys.vendorId:
|
case Keys.vendorId:
|
||||||
save('Vendor ID', value);
|
save('Vendor ID', value);
|
||||||
case Keys.width:
|
case Keys.videoHeight:
|
||||||
save('Width', '$value pixels');
|
save('Video Height', '$value pixels');
|
||||||
|
case Keys.videoWidth:
|
||||||
|
save('Video Width', '$value pixels');
|
||||||
|
case Keys.visualImpaired:
|
||||||
|
save('Visual impaired', value);
|
||||||
case Keys.xiaomiSlowMoment:
|
case Keys.xiaomiSlowMoment:
|
||||||
save('Xiaomi Slow Moment', value);
|
save('Xiaomi Slow Moment', value);
|
||||||
default:
|
default:
|
||||||
save(key.toSentenceCase(), value.toString());
|
save(key.toSentenceCase(), value);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debugPrint('failed to process video info key=${kv.key} value=${kv.value}, error=$error');
|
debugPrint('failed to process video info key=${kv.key} value=${kv.value}, error=$error');
|
||||||
|
@ -411,6 +421,8 @@ class VideoMetadataFormatter {
|
||||||
return date.toIso8601String();
|
return date.toIso8601String();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String _formatStereo3dMode(String value) => Stereo3dModes.names[value] ?? value;
|
||||||
|
|
||||||
// input example: '00:00:05.408000000' or '5.408000'
|
// input example: '00:00:05.408000000' or '5.408000'
|
||||||
static Duration? _parseDuration(String? value) {
|
static Duration? _parseDuration(String? value) {
|
||||||
if (value == null) return null;
|
if (value == null) return null;
|
||||||
|
|
22
lib/model/media/video/stereo_3d_modes.dart
Normal file
22
lib/model/media/video/stereo_3d_modes.dart
Normal file
|
@ -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',
|
||||||
|
};
|
||||||
|
}
|
|
@ -60,6 +60,7 @@ Future<void> _init() async {
|
||||||
await mobileServices.init();
|
await mobileServices.init();
|
||||||
await settings.init(monitorPlatformSettings: false);
|
await settings.init(monitorPlatformSettings: false);
|
||||||
await reportService.init();
|
await reportService.init();
|
||||||
|
videoMetadataFetcher.init();
|
||||||
|
|
||||||
final analyzer = Analyzer();
|
final analyzer = Analyzer();
|
||||||
_channel.setMethodCallHandler((call) {
|
_channel.setMethodCallHandler((call) {
|
||||||
|
|
|
@ -21,7 +21,6 @@ import 'package:aves_report_platform/aves_report_platform.dart';
|
||||||
import 'package:aves_services/aves_services.dart';
|
import 'package:aves_services/aves_services.dart';
|
||||||
import 'package:aves_services_platform/aves_services_platform.dart';
|
import 'package:aves_services_platform/aves_services_platform.dart';
|
||||||
import 'package:aves_video/aves_video.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:aves_video_mpv/aves_video_mpv.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
@ -58,7 +57,7 @@ void initPlatformServices() {
|
||||||
getIt.registerLazySingleton<AvesAvailability>(LiveAvesAvailability.new);
|
getIt.registerLazySingleton<AvesAvailability>(LiveAvesAvailability.new);
|
||||||
getIt.registerLazySingleton<LocalMediaDb>(SqfliteLocalMediaDb.new);
|
getIt.registerLazySingleton<LocalMediaDb>(SqfliteLocalMediaDb.new);
|
||||||
getIt.registerLazySingleton<AvesVideoControllerFactory>(MpvVideoControllerFactory.new);
|
getIt.registerLazySingleton<AvesVideoControllerFactory>(MpvVideoControllerFactory.new);
|
||||||
getIt.registerLazySingleton<AvesVideoMetadataFetcher>(FfmpegVideoMetadataFetcher.new);
|
getIt.registerLazySingleton<AvesVideoMetadataFetcher>(MpvVideoMetadataFetcher.new);
|
||||||
|
|
||||||
getIt.registerLazySingleton<AppService>(PlatformAppService.new);
|
getIt.registerLazySingleton<AppService>(PlatformAppService.new);
|
||||||
getIt.registerLazySingleton<AppProfileService>(PlatformAppProfileService.new);
|
getIt.registerLazySingleton<AppProfileService>(PlatformAppProfileService.new);
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
extension ExtraString on String {
|
extension ExtraString on String {
|
||||||
static final _sentenceCaseStep1 = RegExp(r'([A-Z][a-z]|\[)');
|
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() {
|
String toSentenceCase() {
|
||||||
var s = replaceFirstMapped(RegExp('.'), (m) => m.group(0)!.toUpperCase());
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -498,6 +498,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
await _onTvLayoutChanged();
|
await _onTvLayoutChanged();
|
||||||
_monitorSettings();
|
_monitorSettings();
|
||||||
videoControllerFactory.init();
|
videoControllerFactory.init();
|
||||||
|
videoMetadataFetcher.init();
|
||||||
|
|
||||||
unawaited(deviceService.setLocaleConfig(AvesApp.supportedLocales));
|
unawaited(deviceService.setLocaleConfig(AvesApp.supportedLocales));
|
||||||
unawaited(storageService.deleteTempDirectory());
|
unawaited(storageService.deleteTempDirectory());
|
||||||
|
|
|
@ -9,6 +9,8 @@ mixin AvesEntryBase {
|
||||||
|
|
||||||
int? get pageId;
|
int? get pageId;
|
||||||
|
|
||||||
|
String get mimeType;
|
||||||
|
|
||||||
String? get path;
|
String? get path;
|
||||||
|
|
||||||
String? get bestTitle;
|
String? get bestTitle;
|
||||||
|
|
|
@ -2,18 +2,23 @@
|
||||||
// they originate from FFmpeg, fijkplayer, and other software
|
// they originate from FFmpeg, fijkplayer, and other software
|
||||||
// that write additional metadata to media files
|
// that write additional metadata to media files
|
||||||
class Keys {
|
class Keys {
|
||||||
|
static const alpha = 'alpha';
|
||||||
static const androidCaptureFramerate = 'com.android.capture.fps';
|
static const androidCaptureFramerate = 'com.android.capture.fps';
|
||||||
static const androidManufacturer = 'com.android.manufacturer';
|
static const androidManufacturer = 'com.android.manufacturer';
|
||||||
static const androidModel = 'com.android.model';
|
static const androidModel = 'com.android.model';
|
||||||
static const androidVersion = 'com.android.version';
|
static const androidVersion = 'com.android.version';
|
||||||
|
static const audioChannels = 'audio-channels';
|
||||||
static const avgFrameRate = 'avg_frame_rate';
|
static const avgFrameRate = 'avg_frame_rate';
|
||||||
static const bps = 'bps';
|
static const bps = 'bps';
|
||||||
static const bitrate = 'bitrate';
|
static const bitrate = 'bitrate';
|
||||||
static const bitsPerRawSample = 'bits_per_raw_sample';
|
static const bitsPerSample = 'bits_per_sample';
|
||||||
static const byteCount = 'number_of_bytes';
|
static const byteCount = 'number_of_bytes';
|
||||||
static const channelLayout = 'channel_layout';
|
static const channelLayout = 'channel_layout';
|
||||||
|
static const chapters = 'chapters';
|
||||||
static const chromaLocation = 'chroma_location';
|
static const chromaLocation = 'chroma_location';
|
||||||
|
static const closedCaptions = 'closed_captions';
|
||||||
static const codecLevel = 'codec_level';
|
static const codecLevel = 'codec_level';
|
||||||
|
static const codecLongName = 'codec_long_name';
|
||||||
static const codecName = 'codec_name';
|
static const codecName = 'codec_name';
|
||||||
static const codecPixelFormat = 'codec_pixel_format';
|
static const codecPixelFormat = 'codec_pixel_format';
|
||||||
static const codecProfileId = 'codec_profile_id';
|
static const codecProfileId = 'codec_profile_id';
|
||||||
|
@ -21,6 +26,8 @@ class Keys {
|
||||||
static const codecTagString = 'codec_tag_string';
|
static const codecTagString = 'codec_tag_string';
|
||||||
static const codedHeight = 'coded_height';
|
static const codedHeight = 'coded_height';
|
||||||
static const codedWidth = 'coded_width';
|
static const codedWidth = 'coded_width';
|
||||||
|
static const colorLevels = 'color_levels';
|
||||||
|
static const colorMatrix = 'color_matrix';
|
||||||
static const colorPrimaries = 'color_primaries';
|
static const colorPrimaries = 'color_primaries';
|
||||||
static const colorRange = 'color_range';
|
static const colorRange = 'color_range';
|
||||||
static const colorSpace = 'color_space';
|
static const colorSpace = 'color_space';
|
||||||
|
@ -29,29 +36,34 @@ class Keys {
|
||||||
static const creationTime = 'creation_time';
|
static const creationTime = 'creation_time';
|
||||||
static const dar = 'display_aspect_ratio';
|
static const dar = 'display_aspect_ratio';
|
||||||
static const date = 'date';
|
static const date = 'date';
|
||||||
|
static const decoderHeight = 'dh';
|
||||||
|
static const decoderWidth = 'dw';
|
||||||
static const disposition = 'disposition';
|
static const disposition = 'disposition';
|
||||||
static const duration = 'duration';
|
static const duration = 'duration';
|
||||||
static const durationMicros = 'duration_us';
|
static const durationMicros = 'duration_us';
|
||||||
static const durationTs = 'duration_ts';
|
static const durationTs = 'duration_ts';
|
||||||
static const encoder = 'encoder';
|
static const encoder = 'encoder';
|
||||||
static const extraDataSize = 'extradata_size';
|
static const extraDataSize = 'extradata_size';
|
||||||
static const fieldOrder = 'field_order';
|
|
||||||
static const filename = 'filename';
|
static const filename = 'filename';
|
||||||
|
static const filmGrain = 'film_grain';
|
||||||
static const fpsDen = 'fps_den';
|
static const fpsDen = 'fps_den';
|
||||||
static const fpsNum = 'fps_num';
|
static const fpsNum = 'fps_num';
|
||||||
|
static const fps = 'fps';
|
||||||
static const frameCount = 'number_of_frames';
|
static const frameCount = 'number_of_frames';
|
||||||
static const handlerName = 'handler_name';
|
static const gamma = 'gamma';
|
||||||
static const hasBFrames = 'has_b_frames';
|
static const hasBFrames = 'has_b_frames';
|
||||||
static const height = 'height';
|
static const hearingImpaired = 'hearing_impaired';
|
||||||
|
static const hwPixelFormat = 'hw_pixel_format';
|
||||||
static const index = 'index';
|
static const index = 'index';
|
||||||
static const isAvc = 'is_avc';
|
static const isAvc = 'is_avc';
|
||||||
static const language = 'language';
|
static const language = 'language';
|
||||||
|
static const light = 'light';
|
||||||
static const location = 'location';
|
static const location = 'location';
|
||||||
static const majorBrand = 'major_brand';
|
static const majorBrand = 'major_brand';
|
||||||
static const mediaFormat = 'format';
|
static const mediaFormat = 'format';
|
||||||
static const mediaType = 'media_type';
|
|
||||||
static const minorVersion = 'minor_version';
|
static const minorVersion = 'minor_version';
|
||||||
static const nalLengthSize = 'nal_length_size';
|
static const nalLengthSize = 'nal_length_size';
|
||||||
|
static const par = 'pixel_aspect_ratio';
|
||||||
static const probeScore = 'probe_score';
|
static const probeScore = 'probe_score';
|
||||||
static const programCount = 'nb_programs';
|
static const programCount = 'nb_programs';
|
||||||
static const quicktimeCreationDate = 'com.apple.quicktime.creationdate';
|
static const quicktimeCreationDate = 'com.apple.quicktime.creationdate';
|
||||||
|
@ -78,16 +90,20 @@ class Keys {
|
||||||
static const statisticsTags = '_statistics_tags';
|
static const statisticsTags = '_statistics_tags';
|
||||||
static const statisticsWritingApp = '_statistics_writing_app';
|
static const statisticsWritingApp = '_statistics_writing_app';
|
||||||
static const statisticsWritingDateUtc = '_statistics_writing_date_utc';
|
static const statisticsWritingDateUtc = '_statistics_writing_date_utc';
|
||||||
|
static const stereo3dMode = 'stereo_3d_mode';
|
||||||
static const streamCount = 'nb_streams';
|
static const streamCount = 'nb_streams';
|
||||||
static const streams = 'streams';
|
static const streams = 'streams';
|
||||||
static const tbrDen = 'tbr_den';
|
static const tbrDen = 'tbr_den';
|
||||||
static const tbrNum = 'tbr_num';
|
static const tbrNum = 'tbr_num';
|
||||||
|
static const time = 'time';
|
||||||
static const segmentCount = 'segment_count';
|
static const segmentCount = 'segment_count';
|
||||||
static const streamType = 'type';
|
static const streamType = 'type';
|
||||||
static const title = 'title';
|
static const title = 'title';
|
||||||
static const timeBase = 'time_base';
|
static const timeBase = 'time_base';
|
||||||
static const track = 'track';
|
static const track = 'track';
|
||||||
static const vendorId = 'vendor_id';
|
static const vendorId = 'vendor_id';
|
||||||
static const width = 'width';
|
static const videoHeight = 'height';
|
||||||
|
static const videoWidth = 'width';
|
||||||
|
static const visualImpaired = 'visual_impaired';
|
||||||
static const xiaomiSlowMoment = 'com.xiaomi.slow_moment';
|
static const xiaomiSlowMoment = 'com.xiaomi.slow_moment';
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,7 +98,7 @@ class FfmpegVideoMetadataFetcher extends AvesVideoMetadataFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _normalizeGroup(Map<dynamic, dynamic> stream) {
|
void _normalizeGroup(Map<dynamic, dynamic> stream) {
|
||||||
void replaceKey(k1, k2) {
|
void replaceKey(String k1, String k2) {
|
||||||
final v = stream.remove(k1);
|
final v = stream.remove(k1);
|
||||||
if (v != null) {
|
if (v != null) {
|
||||||
stream[k2] = v;
|
stream[k2] = v;
|
||||||
|
@ -119,16 +119,16 @@ class FfmpegVideoMetadataFetcher extends AvesVideoMetadataFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
<String>{
|
<String>{
|
||||||
|
Keys.bitsPerSample,
|
||||||
|
Keys.closedCaptions,
|
||||||
|
Keys.codecLongName,
|
||||||
Keys.codecProfileId,
|
Keys.codecProfileId,
|
||||||
|
Keys.filmGrain,
|
||||||
|
Keys.hasBFrames,
|
||||||
Keys.rFrameRate,
|
Keys.rFrameRate,
|
||||||
'bits_per_sample',
|
Keys.startPts,
|
||||||
'closed_captions',
|
Keys.startTime,
|
||||||
'codec_long_name',
|
Keys.vendorId,
|
||||||
'film_grain',
|
|
||||||
'has_b_frames',
|
|
||||||
'start_pts',
|
|
||||||
'start_time',
|
|
||||||
'vendor_id',
|
|
||||||
}.forEach((key) {
|
}.forEach((key) {
|
||||||
final value = stream[key];
|
final value = stream[key];
|
||||||
switch (value) {
|
switch (value) {
|
||||||
|
|
158
plugins/aves_video_ffmpeg/pubspec.lock
Normal file
158
plugins/aves_video_ffmpeg/pubspec.lock
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
aves_model:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "../aves_model"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.0.1"
|
||||||
|
aves_utils:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
path: "../aves_utils"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.0.1"
|
||||||
|
aves_video:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "../aves_video"
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "0.0.1"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.0"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.0"
|
||||||
|
equatable:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: equatable
|
||||||
|
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.7"
|
||||||
|
ffmpeg_kit_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "flutter/flutter"
|
||||||
|
ref: background-lts
|
||||||
|
resolved-ref: "24213bd2334265cfc240525fb9a218b85ad4d872"
|
||||||
|
url: "https://github.com/deckerst/ffmpeg-kit.git"
|
||||||
|
source: git
|
||||||
|
version: "6.0.3"
|
||||||
|
ffmpeg_kit_flutter_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffmpeg_kit_flutter_platform_interface
|
||||||
|
sha256: addf046ae44e190ad0101b2fde2ad909a3cd08a2a109f6106d2f7048b7abedee
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_lints
|
||||||
|
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.0"
|
||||||
|
leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker
|
||||||
|
sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.0.8"
|
||||||
|
lints:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.1.1"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.11.1"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.15.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
plugin_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: plugin_platform_interface
|
||||||
|
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.8"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "14.3.1"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.6.0 <4.0.0"
|
||||||
|
flutter: ">=2.0.0"
|
|
@ -4,7 +4,7 @@ publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.6.0
|
sdk: ^3.6.0
|
||||||
resolution: workspace
|
#resolution: workspace
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export 'src/controller.dart';
|
export 'src/controller.dart';
|
||||||
export 'src/factory.dart';
|
export 'src/factory.dart';
|
||||||
|
export 'src/metadata.dart';
|
||||||
|
|
199
plugins/aves_video_mpv/lib/src/metadata.dart
Normal file
199
plugins/aves_video_mpv/lib/src/metadata.dart
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:aves_model/aves_model.dart';
|
||||||
|
import 'package:aves_video/aves_video.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:media_kit/media_kit.dart';
|
||||||
|
|
||||||
|
class MpvVideoMetadataFetcher extends AvesVideoMetadataFetcher {
|
||||||
|
static const mpvTypeAudio = 'audio';
|
||||||
|
static const mpvTypeVideo = 'video';
|
||||||
|
static const mpvTypeSub = 'sub';
|
||||||
|
|
||||||
|
static const probeTimeoutImage = 500;
|
||||||
|
static const probeTimeoutVideo = 5000;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void init() => MediaKit.ensureInitialized();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map> getMetadata(AvesEntryBase entry) async {
|
||||||
|
final player = Player(
|
||||||
|
configuration: PlayerConfiguration(
|
||||||
|
logLevel: MPVLogLevel.warn,
|
||||||
|
protocolWhitelist: [
|
||||||
|
...const PlayerConfiguration().protocolWhitelist,
|
||||||
|
// Android `content` URIs are considered unsafe by default,
|
||||||
|
// as they are transferred via a custom `fd` protocol
|
||||||
|
'fd',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final platform = player.platform;
|
||||||
|
if (platform is! NativePlayer) {
|
||||||
|
throw Exception('Platform player ${platform.runtimeType} does not support property retrieval');
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to enable video decoding to retrieve video params,
|
||||||
|
// but it is disabled by default unless a `VideoController` is attached.
|
||||||
|
// Attaching a `VideoController` is problematic, because `player.open()` may not return
|
||||||
|
// unless a new frame is rendered, and triggering fails from a background service.
|
||||||
|
// It is simpler to enable the video track via properties.
|
||||||
|
await platform.setProperty('vid', 'auto');
|
||||||
|
|
||||||
|
// deselect audio track to prevent triggering Android audio sessions
|
||||||
|
await platform.setProperty('aid', 'no');
|
||||||
|
|
||||||
|
final videoDecodedCompleter = Completer();
|
||||||
|
StreamSubscription? subscription;
|
||||||
|
subscription = player.stream.videoParams.listen((v) {
|
||||||
|
if (v.par != null) {
|
||||||
|
subscription?.cancel();
|
||||||
|
videoDecodedCompleter.complete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await player.open(Media(entry.uri), play: false);
|
||||||
|
|
||||||
|
final timeoutMillis = entry.mimeType.startsWith('image') ? probeTimeoutImage : probeTimeoutVideo;
|
||||||
|
await Future.any([videoDecodedCompleter.future, Future.delayed(Duration(milliseconds: timeoutMillis))]);
|
||||||
|
|
||||||
|
final fields = <String, dynamic>{};
|
||||||
|
|
||||||
|
final videoParams = player.state.videoParams;
|
||||||
|
if (videoParams.par == null) {
|
||||||
|
debugPrint('failed to probe video metadata within $timeoutMillis ms for entry=$entry');
|
||||||
|
} else {
|
||||||
|
// mpv properties: https://mpv.io/manual/stable/#property-list
|
||||||
|
|
||||||
|
// mpv doc: "duration with milliseconds"
|
||||||
|
final durationMs = await platform.getProperty('duration/full');
|
||||||
|
if (durationMs.isNotEmpty) {
|
||||||
|
fields[Keys.duration] = durationMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mpv doc: "metadata key/value pairs"
|
||||||
|
// note: seems to match FFprobe "format" > "tags" fields
|
||||||
|
final metadata = await platform.getProperty('metadata');
|
||||||
|
if (metadata.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
jsonDecode(metadata).forEach((key, value) {
|
||||||
|
fields[key] = value;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('failed to parse metadata=$metadata with error=$error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final tracks = await platform.getProperty('track-list');
|
||||||
|
if (tracks.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final tracksJson = jsonDecode(tracks);
|
||||||
|
if (tracksJson is List && tracksJson.isNotEmpty) {
|
||||||
|
fields[Keys.streams] = tracksJson.whereType<Map>().map((stream) {
|
||||||
|
return _normalizeStream(stream.cast<String, dynamic>(), videoParams);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('failed to parse tracks=$tracks with error=$error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final chapters = await platform.getProperty('chapter-list');
|
||||||
|
if (chapters.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
final chaptersJson = jsonDecode(chapters);
|
||||||
|
if (chaptersJson is List && chaptersJson.isNotEmpty) {
|
||||||
|
final chapterMaps = chaptersJson.whereType<Map>().toList();
|
||||||
|
if (chapterMaps.isNotEmpty) {
|
||||||
|
fields[Keys.chapters] = chapterMaps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint('failed to parse chapters=$chapters with error=$error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await player.dispose();
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _normalizeStream(Map<String, dynamic> stream, VideoParams videoParams) {
|
||||||
|
void replaceKey(String k1, String k2) {
|
||||||
|
final v = stream.remove(k1);
|
||||||
|
if (v != null) {
|
||||||
|
stream[k2] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeIfFalse(String k) {
|
||||||
|
if (stream[k] == false) {
|
||||||
|
stream.remove(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.remove('id');
|
||||||
|
stream.remove('decoder-desc');
|
||||||
|
stream.remove('main-selection');
|
||||||
|
stream.remove('selected');
|
||||||
|
stream.remove('src-id');
|
||||||
|
replaceKey('ff-index', Keys.index);
|
||||||
|
replaceKey('codec', Keys.codecName);
|
||||||
|
replaceKey('lang', Keys.language);
|
||||||
|
replaceKey('demux-bitrate', Keys.bitrate);
|
||||||
|
replaceKey('demux-channel-count', Keys.audioChannels);
|
||||||
|
replaceKey('demux-fps', Keys.fps);
|
||||||
|
replaceKey('demux-samplerate', Keys.sampleRate);
|
||||||
|
replaceKey('hearing-impaired', Keys.hearingImpaired);
|
||||||
|
replaceKey('visual-impaired', Keys.visualImpaired);
|
||||||
|
|
||||||
|
stream.removeWhere((k, v) => k.startsWith('demux-'));
|
||||||
|
removeIfFalse('albumart');
|
||||||
|
removeIfFalse('default');
|
||||||
|
removeIfFalse('dependent');
|
||||||
|
removeIfFalse('external');
|
||||||
|
removeIfFalse('forced');
|
||||||
|
removeIfFalse(Keys.hearingImpaired);
|
||||||
|
removeIfFalse(Keys.visualImpaired);
|
||||||
|
|
||||||
|
final isImage = stream.remove('image');
|
||||||
|
switch (stream.remove('type')) {
|
||||||
|
case mpvTypeAudio:
|
||||||
|
stream[Keys.streamType] = MediaStreamTypes.audio;
|
||||||
|
case mpvTypeVideo:
|
||||||
|
stream[Keys.streamType] = MediaStreamTypes.video;
|
||||||
|
if (isImage) {
|
||||||
|
stream.remove(Keys.fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some video properties are not in the video track props but accessible via `video-params` (or `video-out-params`).
|
||||||
|
// These parameters are already stored in the player state, as `videoParams`.
|
||||||
|
// Parameters `sigPeak` and `averageBpp` are ignored.
|
||||||
|
final videoParamsTags = <String, dynamic>{
|
||||||
|
Keys.alpha: videoParams.alpha,
|
||||||
|
Keys.chromaLocation: videoParams.chromaLocation,
|
||||||
|
Keys.codecPixelFormat: videoParams.pixelformat,
|
||||||
|
Keys.colorLevels: videoParams.colorlevels,
|
||||||
|
Keys.colorMatrix: videoParams.colormatrix,
|
||||||
|
Keys.colorPrimaries: videoParams.primaries,
|
||||||
|
Keys.dar: videoParams.aspect,
|
||||||
|
Keys.decoderHeight: videoParams.dh,
|
||||||
|
Keys.decoderWidth: videoParams.dw,
|
||||||
|
Keys.gamma: videoParams.gamma,
|
||||||
|
Keys.hwPixelFormat: videoParams.hwPixelformat,
|
||||||
|
Keys.light: videoParams.light,
|
||||||
|
Keys.par: videoParams.par,
|
||||||
|
Keys.rotate: videoParams.rotate,
|
||||||
|
Keys.stereo3dMode: videoParams.stereoIn,
|
||||||
|
Keys.videoHeight: videoParams.h,
|
||||||
|
Keys.videoWidth: videoParams.w,
|
||||||
|
}..removeWhere((k, v) => v == null);
|
||||||
|
stream.addAll(videoParamsTags);
|
||||||
|
case mpvTypeSub:
|
||||||
|
stream[Keys.streamType] = MediaStreamTypes.subtitle;
|
||||||
|
}
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
}
|
41
pubspec.lock
41
pubspec.lock
|
@ -13,10 +13,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _flutterfire_internals
|
name: _flutterfire_internals
|
||||||
sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe
|
sha256: "27899c95f9e7ec06c8310e6e0eac967707714b9f1450c4a58fa00ca011a4a8ae"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.48"
|
version: "1.3.49"
|
||||||
_macros:
|
_macros:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: dart
|
description: dart
|
||||||
|
@ -289,23 +289,6 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.3"
|
||||||
ffmpeg_kit_flutter:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
path: "flutter/flutter"
|
|
||||||
ref: background-lts
|
|
||||||
resolved-ref: "24213bd2334265cfc240525fb9a218b85ad4d872"
|
|
||||||
url: "https://github.com/deckerst/ffmpeg-kit.git"
|
|
||||||
source: git
|
|
||||||
version: "6.0.3"
|
|
||||||
ffmpeg_kit_flutter_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: ffmpeg_kit_flutter_platform_interface
|
|
||||||
sha256: addf046ae44e190ad0101b2fde2ad909a3cd08a2a109f6106d2f7048b7abedee
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.1"
|
|
||||||
file:
|
file:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -318,10 +301,10 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core
|
name: firebase_core
|
||||||
sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde"
|
sha256: "0307c1fde82e2b8b97e0be2dab93612aff9a72f31ebe9bfac66ed8b37ef7c568"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.9.0"
|
version: "3.10.0"
|
||||||
firebase_core_platform_interface:
|
firebase_core_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -342,18 +325,18 @@ packages:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_crashlytics
|
name: firebase_crashlytics
|
||||||
sha256: e235c8452d5622fc271404592388fde179e4b62c50e777ad3c8c3369296104ed
|
sha256: f6adb65fa3d6391a79f0e60833bb4cdc468ce0c318831c90057ee11e0909cd29
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.0"
|
version: "4.3.0"
|
||||||
firebase_crashlytics_platform_interface:
|
firebase_crashlytics_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_crashlytics_platform_interface
|
name: firebase_crashlytics_platform_interface
|
||||||
sha256: "4ddadf44ed0a202f3acad053f12c083877940fa8cc1a9f747ae09e1ef4372160"
|
sha256: "6635166c22c6f75f634b8e77b70fcc43b24af4cfee28f975249dbdbd9769a702"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.7.0"
|
version: "3.8.0"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -813,8 +796,8 @@ packages:
|
||||||
dependency: "direct overridden"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: media_kit
|
path: media_kit
|
||||||
ref: d094ba83715b0ac893e546781b2862e855d34502
|
ref: "4d8c634c28d439384aab40b9d2edff83077f37c9"
|
||||||
resolved-ref: d094ba83715b0ac893e546781b2862e855d34502
|
resolved-ref: "4d8c634c28d439384aab40b9d2edff83077f37c9"
|
||||||
url: "https://github.com/media-kit/media-kit.git"
|
url: "https://github.com/media-kit/media-kit.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.1.11"
|
version: "1.1.11"
|
||||||
|
@ -830,8 +813,8 @@ packages:
|
||||||
dependency: "direct overridden"
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
path: media_kit_video
|
path: media_kit_video
|
||||||
ref: d094ba83715b0ac893e546781b2862e855d34502
|
ref: "4d8c634c28d439384aab40b9d2edff83077f37c9"
|
||||||
resolved-ref: d094ba83715b0ac893e546781b2862e855d34502
|
resolved-ref: "4d8c634c28d439384aab40b9d2edff83077f37c9"
|
||||||
url: "https://github.com/media-kit/media-kit.git"
|
url: "https://github.com/media-kit/media-kit.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.2.5"
|
version: "1.2.5"
|
||||||
|
|
|
@ -28,7 +28,6 @@ workspace:
|
||||||
- plugins/aves_ui
|
- plugins/aves_ui
|
||||||
- plugins/aves_utils
|
- plugins/aves_utils
|
||||||
- plugins/aves_video
|
- plugins/aves_video
|
||||||
- plugins/aves_video_ffmpeg
|
|
||||||
- plugins/aves_video_mpv
|
- plugins/aves_video_mpv
|
||||||
|
|
||||||
# use `scripts/apply_flavor_{flavor}.sh` to set the right dependencies for the flavor
|
# use `scripts/apply_flavor_{flavor}.sh` to set the right dependencies for the flavor
|
||||||
|
@ -55,8 +54,6 @@ dependencies:
|
||||||
path: plugins/aves_services_google
|
path: plugins/aves_services_google
|
||||||
aves_video:
|
aves_video:
|
||||||
path: plugins/aves_video
|
path: plugins/aves_video
|
||||||
aves_video_ffmpeg:
|
|
||||||
path: plugins/aves_video_ffmpeg
|
|
||||||
aves_video_mpv:
|
aves_video_mpv:
|
||||||
path: plugins/aves_video_mpv
|
path: plugins/aves_video_mpv
|
||||||
aves_ui:
|
aves_ui:
|
||||||
|
@ -137,12 +134,12 @@ dependency_overrides:
|
||||||
media_kit:
|
media_kit:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/media-kit/media-kit.git
|
url: https://github.com/media-kit/media-kit.git
|
||||||
ref: d094ba83715b0ac893e546781b2862e855d34502
|
ref: 4d8c634c28d439384aab40b9d2edff83077f37c9
|
||||||
path: media_kit
|
path: media_kit
|
||||||
media_kit_video:
|
media_kit_video:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/media-kit/media-kit.git
|
url: https://github.com/media-kit/media-kit.git
|
||||||
ref: d094ba83715b0ac893e546781b2862e855d34502
|
ref: 4d8c634c28d439384aab40b9d2edff83077f37c9
|
||||||
path: media_kit_video
|
path: media_kit_video
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
|
|
@ -13,5 +13,7 @@ void main() {
|
||||||
|
|
||||||
expect('H'.toSentenceCase(), 'H');
|
expect('H'.toSentenceCase(), 'H');
|
||||||
expect('LW[1]'.toSentenceCase(), 'LW [1]');
|
expect('LW[1]'.toSentenceCase(), 'LW [1]');
|
||||||
|
|
||||||
|
expect('bits_per_raw_sample'.toSentenceCase(), 'Bits Per Raw Sample');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue