info: streams
This commit is contained in:
parent
4526df5a77
commit
51a593e0fc
7 changed files with 159 additions and 87 deletions
|
@ -63,32 +63,32 @@ class ChannelLayouts {
|
|||
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: 'stereo 2.0 • FL FR',
|
||||
LAYOUT_2POINT1: 'stereo 2.1 • FL FR LFE',
|
||||
LAYOUT_2_1: 'surround 3.0 • FL FR BC',
|
||||
LAYOUT_SURROUND: 'stereo 3.0 • FL FR FC',
|
||||
LAYOUT_3POINT1: 'stereo 3.1 • FL FR FC LFE',
|
||||
LAYOUT_4POINT0: 'surround 4.0 • FL FR FC BC',
|
||||
LAYOUT_4POINT1: 'surround 4.1 • FL FR FC BC LFE',
|
||||
LAYOUT_2_2: 'quad (side) • FL FR SL SR',
|
||||
LAYOUT_QUAD: 'quad (back) • FL FR BL BR',
|
||||
LAYOUT_5POINT0: '5.0 (side) • FL FR FC SL SR',
|
||||
LAYOUT_5POINT1: '5.1 (side) • FL FR FC SL SR LFE',
|
||||
LAYOUT_5POINT0_BACK: '5.0 (back) • FL FR FC BL BR',
|
||||
LAYOUT_5POINT1_BACK: '5.1 (back) • FL FR FC BL BR LFE',
|
||||
LAYOUT_6POINT0: '6.0 (side) • FL FR FC SL SR BC',
|
||||
LAYOUT_6POINT0_FRONT: '6.0 (front) • FL FR FLC FRC SL SR',
|
||||
LAYOUT_HEXAGONAL: 'hexagonal • FL FR FC BL BR BC',
|
||||
LAYOUT_6POINT1: '6.1 (side) • FL FR FC SL SR BC LFE',
|
||||
LAYOUT_6POINT1_BACK: '6.1 (back) • FL FR FC BL BR BC LFE',
|
||||
LAYOUT_6POINT1_FRONT: '6.1 (front) • FL FR FLC FRC SL SR LFE',
|
||||
LAYOUT_7POINT0: 'surround 7.0 • FL FR FC SL SR BL BR',
|
||||
LAYOUT_7POINT0_FRONT: 'wide 7.0 • FL FR FC FLC FRC SL SR',
|
||||
LAYOUT_7POINT1: 'surround 7.1 • FL FR FC SL SR BL BR LFE',
|
||||
LAYOUT_7POINT1_WIDE: 'wide 7.1 • FL FR FC FLC FRC SL SR LFE',
|
||||
LAYOUT_7POINT1_WIDE_BACK: 'wide 7.1 (back) • FL FR FC FLC FRC BL BR LFE',
|
||||
LAYOUT_OCTAGONAL: 'octagonal • FL FR FC SL SR BL BR BC',
|
||||
LAYOUT_HEXADECAGONAL: 'hexadecagonal • FL FR FC WL WR TFL TFR TFC SL SR BL BR BC TBL TBR TBC',
|
||||
LAYOUT_STEREO_DOWNMIX: 'stereo downmix',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -6,6 +6,29 @@ import 'package:aves/utils/math_utils.dart';
|
|||
import 'package:fijkplayer/fijkplayer.dart';
|
||||
|
||||
class StreamInfo {
|
||||
static const keyBitrate = 'bitrate';
|
||||
static const keyChannelLayout = 'channel_layout';
|
||||
static const keyCodecName = 'codec_name';
|
||||
static const keyFpsDen = 'fps_den';
|
||||
static const keyFpsNum = 'fps_num';
|
||||
static const keyHeight = 'height';
|
||||
static const keyIndex = 'index';
|
||||
static const keyLanguage = 'language';
|
||||
static const keySampleRate = 'sample_rate';
|
||||
static const keySarDen = 'sar_den';
|
||||
static const keySarNum = 'sar_num';
|
||||
static const keyTbrDen = 'tbr_den';
|
||||
static const keyTbrNum = 'tbr_num';
|
||||
static const keyType = 'type';
|
||||
static const keyWidth = 'width';
|
||||
|
||||
static const typeAudio = 'audio';
|
||||
static const typeMetadata = 'metadata';
|
||||
static const typeSubtitle = 'subtitle';
|
||||
static const typeTimedText = 'timedtext';
|
||||
static const typeUnknown = 'unknown';
|
||||
static const typeVideo = 'video';
|
||||
|
||||
static Future<Map> getVideoInfo(AvesEntry entry) async {
|
||||
final player = FijkPlayer();
|
||||
await player.setDataSource(entry.uri, autoPlay: false);
|
||||
|
@ -53,51 +76,51 @@ class StreamInfo {
|
|||
if (value != null) {
|
||||
final key = kv.key;
|
||||
switch (key) {
|
||||
case 'index':
|
||||
case 'fps_num':
|
||||
case 'sar_num':
|
||||
case 'tbr_num':
|
||||
case 'tbr_den':
|
||||
case keyIndex:
|
||||
case keyFpsNum:
|
||||
case keySarNum:
|
||||
case keyTbrNum:
|
||||
case keyTbrDen:
|
||||
break;
|
||||
case 'bitrate':
|
||||
case keyBitrate:
|
||||
dir['Bitrate'] = formatBitrate(value, round: 1);
|
||||
break;
|
||||
case 'channel_layout':
|
||||
dir['Channel Layout'] = ChannelLayouts.names[value] ?? value.toString();
|
||||
case keyChannelLayout:
|
||||
dir['Channel Layout'] = ChannelLayouts.names[value] ?? 'unknown ($value)';
|
||||
break;
|
||||
case 'codec_name':
|
||||
case keyCodecName:
|
||||
dir['Codec'] = value.toString().toUpperCase().replaceAll('_', ' ');
|
||||
break;
|
||||
case 'fps_den':
|
||||
dir['Frame Rate'] = roundToPrecision(stream['fps_num'] / stream['fps_den'], decimals: 3).toString();
|
||||
case keyFpsDen:
|
||||
dir['Frame Rate'] = roundToPrecision(stream[keyFpsNum] / stream[keyFpsDen], decimals: 3).toString();
|
||||
break;
|
||||
case 'height':
|
||||
case keyHeight:
|
||||
dir['Height'] = '$value pixels';
|
||||
break;
|
||||
case 'language':
|
||||
case keyLanguage:
|
||||
dir['Language'] = value;
|
||||
break;
|
||||
case 'sample_rate':
|
||||
case keySampleRate:
|
||||
dir['Sample Rate'] = '$value Hz';
|
||||
break;
|
||||
case 'sar_den':
|
||||
dir['SAR'] = '${stream['sar_num']}:${stream['sar_den']}';
|
||||
case keySarDen:
|
||||
dir['SAR'] = '${stream[keySarNum]}:${stream[keySarDen]}';
|
||||
break;
|
||||
case 'type':
|
||||
case keyType:
|
||||
switch (value) {
|
||||
case 'timedtext':
|
||||
case typeTimedText:
|
||||
dir['Type'] = 'timed text';
|
||||
break;
|
||||
case 'audio':
|
||||
case 'video':
|
||||
case 'metadata':
|
||||
case 'subtitle':
|
||||
case 'unknown':
|
||||
case typeAudio:
|
||||
case typeMetadata:
|
||||
case typeSubtitle:
|
||||
case typeUnknown:
|
||||
case typeVideo:
|
||||
default:
|
||||
dir['Type'] = value;
|
||||
}
|
||||
break;
|
||||
case 'width':
|
||||
case keyWidth:
|
||||
dir['Width'] = '$value pixels';
|
||||
break;
|
||||
default:
|
||||
|
|
|
@ -5,7 +5,6 @@ class AIcons {
|
|||
static const IconData allCollection = Icons.collections_outlined;
|
||||
static const IconData image = Icons.photo_outlined;
|
||||
static const IconData video = Icons.movie_outlined;
|
||||
static const IconData audio = Icons.audiotrack_outlined;
|
||||
static const IconData vector = Icons.code_outlined;
|
||||
|
||||
static const IconData android = Icons.android;
|
||||
|
|
|
@ -61,6 +61,10 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
|||
// `mediacodec-all-videos`: MediaCodec: enable all videos, default: 0, in [0, 1]
|
||||
option.setPlayerOption('mediacodec-all-videos', hwAccelerationEnabled ? 1 : 0);
|
||||
|
||||
// TODO TLAD try subs
|
||||
// `subtitle`: decode subtitle stream, default: 0, in [0, 1]
|
||||
// option.setPlayerOption('subtitle', 1);
|
||||
|
||||
_instance.applyOptions(option);
|
||||
|
||||
_instance.addListener(_onValueChanged);
|
||||
|
@ -122,6 +126,8 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
|||
|
||||
@override
|
||||
Widget buildPlayerWidget(BuildContext context, AvesEntry entry) {
|
||||
// TODO TLAD derive DAR (Display Aspect Ratio) from SAR (Storage Aspect Ratio), if any
|
||||
// e.g. 960x536 (~16:9) with SAR 4:3 should be displayed as ~2.39:1
|
||||
return FijkView(
|
||||
player: _instance,
|
||||
fit: FijkFit(
|
||||
|
|
|
@ -108,7 +108,7 @@ class InfoSearchDelegate extends SearchDelegate {
|
|||
title: kv.key,
|
||||
dir: kv.value,
|
||||
initiallyExpanded: true,
|
||||
showPrefixChildren: false,
|
||||
showThumbnails: false,
|
||||
))
|
||||
.toList();
|
||||
return SafeArea(
|
||||
|
|
|
@ -3,7 +3,6 @@ import 'dart:collection';
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/ref/brand_colors.dart';
|
||||
import 'package:aves/services/svg_metadata_service.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/color_utils.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
|
@ -21,7 +20,7 @@ class MetadataDirTile extends StatelessWidget {
|
|||
final String title;
|
||||
final MetadataDirectory dir;
|
||||
final ValueNotifier<String> expandedDirectoryNotifier;
|
||||
final bool initiallyExpanded, showPrefixChildren;
|
||||
final bool initiallyExpanded, showThumbnails;
|
||||
|
||||
const MetadataDirTile({
|
||||
@required this.entry,
|
||||
|
@ -29,7 +28,7 @@ class MetadataDirTile extends StatelessWidget {
|
|||
@required this.dir,
|
||||
this.expandedDirectoryNotifier,
|
||||
this.initiallyExpanded = false,
|
||||
this.showPrefixChildren = true,
|
||||
this.showThumbnails = true,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -48,32 +47,23 @@ class MetadataDirTile extends StatelessWidget {
|
|||
}
|
||||
|
||||
Widget thumbnail;
|
||||
final prefixChildren = <Widget>[];
|
||||
if (showPrefixChildren) {
|
||||
if (showThumbnails) {
|
||||
switch (dirName) {
|
||||
case MetadataDirectory.exifThumbnailDirectory:
|
||||
thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.exif, entry: entry);
|
||||
break;
|
||||
case MetadataDirectory.mediaDirectory:
|
||||
thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.embedded, entry: entry);
|
||||
Widget builder(IconData data) => Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Icon(data),
|
||||
);
|
||||
if (tags['Has Video'] == 'yes') prefixChildren.add(builder(AIcons.video));
|
||||
if (tags['Has Audio'] == 'yes') prefixChildren.add(builder(AIcons.audio));
|
||||
if (tags['Has Image'] == 'yes') prefixChildren.add(builder(AIcons.image));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return AvesExpansionTile(
|
||||
title: title,
|
||||
color: BrandColors.get(dirName) ?? stringToColor(dirName),
|
||||
color: dir.color ?? BrandColors.get(dirName) ?? stringToColor(dirName),
|
||||
expandedNotifier: expandedDirectoryNotifier,
|
||||
initiallyExpanded: initiallyExpanded,
|
||||
children: [
|
||||
if (prefixChildren.isNotEmpty) Wrap(children: prefixChildren),
|
||||
if (thumbnail != null) thumbnail,
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:aves/services/services.dart';
|
|||
import 'package:aves/services/svg_metadata_service.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/color_utils.dart';
|
||||
import 'package:aves/widgets/viewer/info/common.dart';
|
||||
import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
@ -142,18 +143,6 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> 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<Map>();
|
||||
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 ?? '';
|
||||
|
||||
|
@ -165,15 +154,13 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
|||
}
|
||||
|
||||
final rawTags = dirKV.value as Map ?? {};
|
||||
final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries.map((tagKV) {
|
||||
final value = (tagKV.value as String ?? '').trim();
|
||||
if (value.isEmpty) return null;
|
||||
final tagName = tagKV.key as String ?? '';
|
||||
return MapEntry(tagName, value);
|
||||
}).where((kv) => kv != null)));
|
||||
return MetadataDirectory(directoryName, parent, tags);
|
||||
return MetadataDirectory(directoryName, parent, _toSortedTags(rawTags));
|
||||
}).toList();
|
||||
|
||||
if (entry.isVideo || (entry.mimeType == MimeTypes.heif && entry.isMultipage)) {
|
||||
directories.addAll(await _getStreamDirectories());
|
||||
}
|
||||
|
||||
final titledDirectories = directories.map((dir) {
|
||||
var title = dir.name;
|
||||
if (directories.where((dir) => dir.name == title).length > 1 && dir.parent?.isNotEmpty == true) {
|
||||
|
@ -191,12 +178,79 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
|||
_expandedDirectoryNotifier.value = null;
|
||||
}
|
||||
|
||||
Future<List<MetadataDirectory>> _getStreamDirectories() async {
|
||||
final directories = <MetadataDirectory>[];
|
||||
final info = await StreamInfo.getVideoInfo(entry);
|
||||
if (info.containsKey('streams')) {
|
||||
String getTypeText(Map stream) {
|
||||
final type = stream[StreamInfo.keyType] ?? StreamInfo.typeUnknown;
|
||||
switch (type) {
|
||||
case StreamInfo.typeAudio:
|
||||
return 'Audio';
|
||||
case StreamInfo.typeMetadata:
|
||||
return 'Metadata';
|
||||
case StreamInfo.typeSubtitle:
|
||||
case StreamInfo.typeTimedText:
|
||||
return 'Text';
|
||||
case StreamInfo.typeVideo:
|
||||
return stream.containsKey(StreamInfo.keyFpsDen) ? 'Video' : 'Image';
|
||||
case StreamInfo.typeUnknown:
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
final allStreams = (info['streams'] as List).cast<Map>();
|
||||
final unknownStreams = allStreams.where((stream) => stream[StreamInfo.keyType] == StreamInfo.typeUnknown).toList();
|
||||
final knownStreams = allStreams.whereNot(unknownStreams.contains);
|
||||
|
||||
// display known streams as separate directories (e.g. video, audio, subs)
|
||||
if (knownStreams.isNotEmpty) {
|
||||
final indexDigits = knownStreams.length.toString().length;
|
||||
|
||||
for (final stream in knownStreams) {
|
||||
final index = (stream[StreamInfo.keyIndex] ?? 0) + 1;
|
||||
final typeText = getTypeText(stream);
|
||||
final dirName = 'Stream ${index.toString().padLeft(indexDigits, '0')} • $typeText';
|
||||
final rawTags = StreamInfo.formatStreamInfo(stream);
|
||||
final color = stringToColor(typeText);
|
||||
directories.add(MetadataDirectory(dirName, null, _toSortedTags(rawTags), color: color));
|
||||
}
|
||||
}
|
||||
|
||||
// display unknown streams as attachments (e.g. fonts)
|
||||
if (unknownStreams.isNotEmpty) {
|
||||
final unknownCodecCount = <String, int>{};
|
||||
for (final stream in unknownStreams) {
|
||||
final codec = (stream[StreamInfo.keyCodecName] as String ?? 'unknown').toUpperCase();
|
||||
unknownCodecCount[codec] = (unknownCodecCount[codec] ?? 0) + 1;
|
||||
}
|
||||
if (unknownCodecCount.isNotEmpty) {
|
||||
final rawTags = unknownCodecCount.map((key, value) => MapEntry(key, value.toString()));
|
||||
directories.add(MetadataDirectory('Attachments', null, _toSortedTags(rawTags)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return directories;
|
||||
}
|
||||
|
||||
SplayTreeMap<String, String> _toSortedTags(Map rawTags) {
|
||||
final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries.map((tagKV) {
|
||||
final value = (tagKV.value as String ?? '').trim();
|
||||
if (value.isEmpty) return null;
|
||||
final tagName = tagKV.key as String ?? '';
|
||||
return MapEntry(tagName, value);
|
||||
}).where((kv) => kv != null)));
|
||||
return tags;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
||||
class MetadataDirectory {
|
||||
final String name;
|
||||
final Color color;
|
||||
final String parent;
|
||||
final SplayTreeMap<String, String> allTags;
|
||||
final SplayTreeMap<String, String> tags;
|
||||
|
@ -206,12 +260,12 @@ class MetadataDirectory {
|
|||
static const xmpDirectory = 'XMP'; // from metadata-extractor
|
||||
static const mediaDirectory = 'Media'; // additional media (video/audio/images) directory
|
||||
|
||||
const MetadataDirectory(this.name, this.parent, SplayTreeMap<String, String> allTags, {SplayTreeMap<String, String> tags})
|
||||
const MetadataDirectory(this.name, this.parent, SplayTreeMap<String, String> allTags, {SplayTreeMap<String, String> tags, this.color})
|
||||
: allTags = allTags,
|
||||
tags = tags ?? allTags;
|
||||
|
||||
MetadataDirectory filterKeys(bool Function(String key) testKey) {
|
||||
final filteredTags = SplayTreeMap.of(Map.fromEntries(allTags.entries.where((kv) => testKey(kv.key))));
|
||||
return MetadataDirectory(name, parent, tags, tags: filteredTags);
|
||||
return MetadataDirectory(name, parent, tags, tags: filteredTags, color: color);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue