diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index ccde43d8a..128df0f8d 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -210,6 +210,9 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { if (mediaDir.isNotEmpty()) { metadataMap[Metadata.DIR_MEDIA] = mediaDir } + // Android's `MediaExtractor` and `MediaPlayer` cannot be used for details + // about embedded images as they do not list them as separate tracks + // and only identify at most one } if (metadataMap.isNotEmpty()) { diff --git a/lib/model/video/channel_layouts.dart b/lib/model/video/channel_layouts.dart new file mode 100644 index 000000000..f98a2364c --- /dev/null +++ b/lib/model/video/channel_layouts.dart @@ -0,0 +1,94 @@ +// channel layout constants from FFmpeg libavutil/channel_layout.h +class ChannelLayouts { + // ignore_for_file: constant_identifier_names + + static const FRONT_LEFT = 0x00000001; + static const FRONT_RIGHT = 0x00000002; + static const FRONT_CENTER = 0x00000004; + static const LOW_FREQUENCY = 0x00000008; + static const BACK_LEFT = 0x00000010; + static const BACK_RIGHT = 0x00000020; + static const FRONT_LEFT_OF_CENTER = 0x00000040; + static const FRONT_RIGHT_OF_CENTER = 0x00000080; + static const BACK_CENTER = 0x00000100; + static const SIDE_LEFT = 0x00000200; + static const SIDE_RIGHT = 0x00000400; + static const TOP_CENTER = 0x00000800; + static const TOP_FRONT_LEFT = 0x00001000; + static const TOP_FRONT_CENTER = 0x00002000; + static const TOP_FRONT_RIGHT = 0x00004000; + static const TOP_BACK_LEFT = 0x00008000; + static const TOP_BACK_CENTER = 0x00010000; + static const TOP_BACK_RIGHT = 0x00020000; + static const STEREO_LEFT = 0x20000000; + static const STEREO_RIGHT = 0x40000000; + + static const WIDE_LEFT = 0x0000000080000000; + static const WIDE_RIGHT = 0x0000000100000000; + static const SURROUND_DIRECT_LEFT = 0x0000000200000000; + static const SURROUND_DIRECT_RIGHT = 0x0000000400000000; + static const LOW_FREQUENCY_2 = 0x0000000800000000; + + static const LAYOUT_NATIVE = 0x8000000000000000; + + static const LAYOUT_MONO = (FRONT_CENTER); + static const LAYOUT_STEREO = (FRONT_LEFT | FRONT_RIGHT); + static const LAYOUT_2POINT1 = (LAYOUT_STEREO | LOW_FREQUENCY); + static const LAYOUT_2_1 = (LAYOUT_STEREO | BACK_CENTER); + static const LAYOUT_SURROUND = (LAYOUT_STEREO | FRONT_CENTER); + static const LAYOUT_3POINT1 = (LAYOUT_SURROUND | LOW_FREQUENCY); + static const LAYOUT_4POINT0 = (LAYOUT_SURROUND | BACK_CENTER); + static const LAYOUT_4POINT1 = (LAYOUT_4POINT0 | LOW_FREQUENCY); + static const LAYOUT_2_2 = (LAYOUT_STEREO | SIDE_LEFT | SIDE_RIGHT); + static const LAYOUT_QUAD = (LAYOUT_STEREO | BACK_LEFT | BACK_RIGHT); + static const LAYOUT_5POINT0 = (LAYOUT_SURROUND | SIDE_LEFT | SIDE_RIGHT); + static const LAYOUT_5POINT1 = (LAYOUT_5POINT0 | LOW_FREQUENCY); + static const LAYOUT_5POINT0_BACK = (LAYOUT_SURROUND | BACK_LEFT | BACK_RIGHT); + static const LAYOUT_5POINT1_BACK = (LAYOUT_5POINT0_BACK | LOW_FREQUENCY); + static const LAYOUT_6POINT0 = (LAYOUT_5POINT0 | BACK_CENTER); + static const LAYOUT_6POINT0_FRONT = (LAYOUT_2_2 | FRONT_LEFT_OF_CENTER | FRONT_RIGHT_OF_CENTER); + static const LAYOUT_HEXAGONAL = (LAYOUT_5POINT0_BACK | BACK_CENTER); + static const LAYOUT_6POINT1 = (LAYOUT_5POINT1 | BACK_CENTER); + static const LAYOUT_6POINT1_BACK = (LAYOUT_5POINT1_BACK | BACK_CENTER); + static const LAYOUT_6POINT1_FRONT = (LAYOUT_6POINT0_FRONT | LOW_FREQUENCY); + static const LAYOUT_7POINT0 = (LAYOUT_5POINT0 | BACK_LEFT | BACK_RIGHT); + static const LAYOUT_7POINT0_FRONT = (LAYOUT_5POINT0 | FRONT_LEFT_OF_CENTER | FRONT_RIGHT_OF_CENTER); + static const LAYOUT_7POINT1 = (LAYOUT_5POINT1 | BACK_LEFT | BACK_RIGHT); + static const LAYOUT_7POINT1_WIDE = (LAYOUT_5POINT1 | FRONT_LEFT_OF_CENTER | FRONT_RIGHT_OF_CENTER); + static const LAYOUT_7POINT1_WIDE_BACK = (LAYOUT_5POINT1_BACK | FRONT_LEFT_OF_CENTER | FRONT_RIGHT_OF_CENTER); + static const LAYOUT_OCTAGONAL = (LAYOUT_5POINT0 | BACK_LEFT | BACK_CENTER | BACK_RIGHT); + static const LAYOUT_HEXADECAGONAL = (LAYOUT_OCTAGONAL | WIDE_LEFT | WIDE_RIGHT | TOP_BACK_LEFT | TOP_BACK_RIGHT | TOP_BACK_CENTER | TOP_FRONT_CENTER | TOP_FRONT_LEFT | TOP_FRONT_RIGHT); + static const LAYOUT_STEREO_DOWNMIX = (STEREO_LEFT | STEREO_RIGHT); + + static const names = { + LAYOUT_NATIVE: 'native', + LAYOUT_MONO: 'mono', + LAYOUT_STEREO: 'stereo', + LAYOUT_2POINT1: '2.1', + LAYOUT_2_1: '2_1', + LAYOUT_SURROUND: 'surround', + LAYOUT_3POINT1: '3.1', + LAYOUT_4POINT0: '4.0', + LAYOUT_4POINT1: '4.1', + LAYOUT_2_2: '2_2', + LAYOUT_QUAD: 'quad', + LAYOUT_5POINT0: '5.0', + LAYOUT_5POINT1: '5.1', + LAYOUT_5POINT0_BACK: '5.0 back', + LAYOUT_5POINT1_BACK: '5.1 back', + LAYOUT_6POINT0: '6.0', + LAYOUT_6POINT0_FRONT: '6.0 front', + LAYOUT_HEXAGONAL: 'hexagonal', + LAYOUT_6POINT1: '6.1', + LAYOUT_6POINT1_BACK: '6.1 back', + LAYOUT_6POINT1_FRONT: '6.1 front', + LAYOUT_7POINT0: '7.0', + LAYOUT_7POINT0_FRONT: '7.0 front', + LAYOUT_7POINT1: '7.1', + LAYOUT_7POINT1_WIDE: '7.1 wide', + LAYOUT_7POINT1_WIDE_BACK: '7.1 wide back', + LAYOUT_OCTAGONAL: 'octagonal', + LAYOUT_HEXADECAGONAL: 'hexadecagonal', + LAYOUT_STEREO_DOWNMIX: 'stereo downmix', + }; +} diff --git a/lib/model/video/streams.dart b/lib/model/video/streams.dart new file mode 100644 index 000000000..753c564e3 --- /dev/null +++ b/lib/model/video/streams.dart @@ -0,0 +1,110 @@ +import 'dart:async'; + +import 'package:aves/model/entry.dart'; +import 'package:aves/model/video/channel_layouts.dart'; +import 'package:aves/utils/math_utils.dart'; +import 'package:fijkplayer/fijkplayer.dart'; + +class StreamInfo { + static Future getVideoInfo(AvesEntry entry) async { + final player = FijkPlayer(); + await player.setDataSource(entry.uri, autoPlay: false); + + final completer = Completer(); + void onChange() { + if ([FijkState.prepared, FijkState.error].contains(player.state)) { + completer.complete(); + } + } + + player.addListener(onChange); + await player.prepareAsync(); + await completer.future; + player.removeListener(onChange); + + final info = await player.getInfo(); + await player.release(); + return info; + } + + static String formatBitrate(int size, {int round = 2}) { + const divider = 1000; + const symbol = 'bit/s'; + + if (size < divider) return '$size $symbol'; + + if (size < divider * divider && size % divider == 0) { + return '${(size / divider).toStringAsFixed(0)} K$symbol'; + } + if (size < divider * divider) { + return '${(size / divider).toStringAsFixed(round)} K$symbol'; + } + + if (size < divider * divider * divider && size % divider == 0) { + return '${(size / (divider * divider)).toStringAsFixed(0)} M$symbol'; + } + return '${(size / divider / divider).toStringAsFixed(round)} M$symbol'; + } + + static Map formatStreamInfo(Map stream) { + final dir = {}; + for (final kv in stream.entries) { + final value = kv.value; + if (value != null) { + final key = kv.key; + switch (key) { + case 'index': + case 'fps_num': + case 'sar_num': + case 'tbr_num': + case 'tbr_den': + break; + case 'bitrate': + dir['Bitrate'] = formatBitrate(value, round: 1); + break; + case 'channel_layout': + dir['Channel Layout'] = ChannelLayouts.names[value] ?? value.toString(); + break; + case 'codec_name': + dir['Codec'] = value.toString().toUpperCase().replaceAll('_', ' '); + break; + case 'fps_den': + dir['Frame Rate'] = roundToPrecision(stream['fps_num'] / stream['fps_den'], decimals: 3).toString(); + break; + case 'height': + dir['Height'] = '$value pixels'; + break; + case 'language': + dir['Language'] = value; + break; + case 'sample_rate': + dir['Sample Rate'] = '$value Hz'; + break; + case 'sar_den': + dir['SAR'] = '${stream['sar_num']}:${stream['sar_den']}'; + break; + case 'type': + switch (value) { + case 'timedtext': + dir['Type'] = 'timed text'; + break; + case 'audio': + case 'video': + case 'metadata': + case 'subtitle': + case 'unknown': + default: + dir['Type'] = value; + } + break; + case 'width': + dir['Width'] = '$value pixels'; + break; + default: + dir[key] = value.toString(); + } + } + } + return dir; + } +} diff --git a/lib/widgets/viewer/info/metadata/metadata_section.dart b/lib/widgets/viewer/info/metadata/metadata_section.dart index 9a866b422..e774b0954 100644 --- a/lib/widgets/viewer/info/metadata/metadata_section.dart +++ b/lib/widgets/viewer/info/metadata/metadata_section.dart @@ -1,6 +1,9 @@ +import 'dart:async'; import 'dart:collection'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/video/streams.dart'; +import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/services.dart'; import 'package:aves/services/svg_metadata_service.dart'; import 'package:aves/theme/durations.dart'; @@ -139,6 +142,18 @@ class _MetadataSectionSliverState extends State with Auto if (_loadedMetadataUri.value == entry.uri) return; if (isVisible) { final rawMetadata = await (entry.isSvg ? SvgMetadataService.getAllMetadata(entry) : metadataService.getAllMetadata(entry)) ?? {}; + if (entry.isVideo || (entry.mimeType == MimeTypes.heif && entry.isMultipage)) { + final info = await StreamInfo.getVideoInfo(entry); + if (info.containsKey('streams')) { + final streams = (info['streams'] as List).cast(); + for (final stream in streams) { + if (stream.containsKey('index')) { + final index = stream['index'] + 1; + rawMetadata['Stream $index'] = StreamInfo.formatStreamInfo(stream); + } + } + } + } final directories = rawMetadata.entries.map((dirKV) { var directoryName = dirKV.key as String ?? ''; diff --git a/pubspec.lock b/pubspec.lock index 0dcb3dc5e..4bbc4f28d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -211,7 +211,7 @@ packages: description: path: "." ref: aves - resolved-ref: a4640923c7c6141ff543f676a8cb7d2fe8b0ffba + resolved-ref: "118fa08b30a7948df04d2692649b124a09ccb480" url: "git://github.com/deckerst/fijkplayer.git" source: git version: "0.8.7" diff --git a/pubspec.yaml b/pubspec.yaml index 4f7ae6514..75836504f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,9 +42,6 @@ dependencies: firebase_analytics: firebase_crashlytics: flutter_highlight: -# flutter_ijkplayer: -# git: -# url: git://github.com/deckerst/flutter_ijkplayer.git flutter_localized_locales: flutter_map: flutter_markdown: