info: improvements for HEVC/AAC profiles, sony video USMT/MTDT boxes

This commit is contained in:
Thibault Deckers 2021-07-07 14:59:50 +09:00
parent dabc63c00b
commit 15857ccc9f
9 changed files with 244 additions and 32 deletions

View file

@ -102,7 +102,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
val metadata = ImageMetadataReader.readMetadata(input)
foundExif = metadata.containsDirectoryOfType(ExifDirectoryBase::class.java)
foundXmp = metadata.containsDirectoryOfType(XmpDirectory::class.java)
val uuidDirCount = HashMap<String, Int>()
for (dir in metadata.directories.filter {
it.tagCount > 0
&& it !is FileTypeDirectory
@ -110,6 +110,16 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}) {
// directory name
var dirName = dir.name
if (dir is Mp4UuidBoxDirectory) {
val uuid = dir.getString(Mp4UuidBoxDirectory.TAG_UUID).substringBefore('-')
dirName += " $uuid"
val count = uuidDirCount[uuid] ?: 0
uuidDirCount[uuid] = count + 1
if (count > 0) {
dirName += " ($count)"
}
}
// exclude directories known to be redundant with info derived on the Dart side
// they are excluded by name instead of runtime type because excluding `Mp4Directory`
@ -168,10 +178,20 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
}
if (dir is Mp4UuidBoxDirectory) {
if (dir.getString(Mp4UuidBoxDirectory.TAG_UUID) == GSpherical.SPHERICAL_VIDEO_V1_UUID) {
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
metadataMap["Spherical Video"] = HashMap(GSpherical(bytes).describe())
metadataMap.remove(dirName)
when (dir.getString(Mp4UuidBoxDirectory.TAG_UUID)) {
GSpherical.SPHERICAL_VIDEO_V1_UUID -> {
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
metadataMap["Spherical Video"] = HashMap(GSpherical(bytes).describe())
metadataMap.remove(dirName)
}
SonyVideoMetadata.USMT_UUID -> {
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
val fields = SonyVideoMetadata.parseUsmt(bytes)
if (fields.isNotEmpty()) {
dirMap.remove("Data")
dirMap.putAll(fields)
}
}
}
}
}

View file

@ -0,0 +1,73 @@
package deckers.thibault.aves.metadata
import java.math.BigInteger
import java.nio.charset.Charset
import java.util.*
object SonyVideoMetadata {
const val PROF_UUID = "50524f46-21d2-4fce-bb88-695cfac9c740"
const val USMT_UUID = "55534d54-21d2-4fce-bb88-695cfac9c740"
fun parseUsmt(data: ByteArray): HashMap<String, String> {
val dirMap = HashMap<String, String>()
var bytes = data
var size = BigInteger(bytes.copyOfRange(0, 4)).toInt()
val box = String(bytes.copyOfRange(4, 8))
if (box == "MTDT") {
val blockCount = BigInteger(bytes.copyOfRange(8, 10)).toInt()
bytes = bytes.copyOfRange(10, size)
size -= 10
for (i in 0 until blockCount) {
// cf https://github.com/sonyxperiadev/MultimediaForAndroidLibrary
// cf https://rubenlaguna.com/post/2007-02-25-how-to-read-title-in-sony-psp-mp4-files/
val blockSize = BigInteger(bytes.copyOfRange(0, 2)).toInt()
val blockType = BigInteger(bytes.copyOfRange(2, 6)).toInt()
// ISO 639 language code written as 3 groups of 5 bits for each letter (ascii code - 0x60)
// e.g. 0x55c4 -> 10101 01110 00100 -> 21 14 4 -> "und"
val language = BigInteger(bytes.copyOfRange(6, 8)).toInt()
val c1 = Character.toChars((language shr 10 and 0x1F) + 0x60)[0]
val c2 = Character.toChars((language shr 5 and 0x1F) + 0x60)[0]
val c3 = Character.toChars((language and 0x1F) + 0x60)[0]
val languageString = "$c1$c2$c3"
val encoding = BigInteger(bytes.copyOfRange(8, 10)).toInt()
val payload = bytes.copyOfRange(10, blockSize)
val payloadString = when (encoding) {
// 0x00: short array
0x00 -> {
payload
.asList()
.chunked(2)
.map { (h, l) -> ((h.toInt() shl 8) + l.toInt()).toShort() }
.joinToString()
}
// 0x01: string
0x01 -> String(payload, Charset.forName("UTF-16BE")).trim()
// 0x101: artwork/icon
else -> "0x${payload.joinToString("") { "%02x".format(it) }}"
}
val blockTypeString = when (blockType) {
0x01 -> "Title"
0x03 -> "Timestamp"
0x04 -> "Creator name"
0x0A -> "End of track"
else -> "0x${"%02x".format(blockType)}"
}
val prefix = if (blockCount > 1) "$i/" else ""
dirMap["${prefix}Data"] = payloadString
dirMap["${prefix}Language"] = languageString
dirMap["${prefix}Type"] = blockTypeString
bytes = bytes.copyOfRange(blockSize, bytes.size)
}
}
return dirMap
}
}

View file

@ -0,0 +1,16 @@
class Codecs {
static const aac = 'aac';
static const ac3 = 'ac3';
static const eac3 = 'eac3';
static const h264 = 'h264';
static const hevc = 'hevc';
static const matroska = 'matroska';
static const mpeg4 = 'mpeg4';
static const mpts = 'mpegts';
static const opus = 'opus';
static const pgs = 'hdmv_pgs_subtitle';
static const subrip = 'subrip';
static const theora = 'theora';
static const vorbis = 'vorbis';
static const webm = 'webm';
}

View file

@ -2,8 +2,11 @@ import 'dart:async';
import 'package:aves/model/entry.dart';
import 'package:aves/model/video/channel_layouts.dart';
import 'package:aves/model/video/h264.dart';
import 'package:aves/model/video/codecs.dart';
import 'package:aves/model/video/keys.dart';
import 'package:aves/model/video/profiles/aac.dart';
import 'package:aves/model/video/profiles/h264.dart';
import 'package:aves/model/video/profiles/hevc.dart';
import 'package:aves/ref/languages.dart';
import 'package:aves/ref/mp4.dart';
import 'package:aves/utils/file_utils.dart';
@ -20,16 +23,19 @@ class VideoMetadataFormatter {
static final _durationPattern = RegExp(r'(\d+):(\d+):(\d+)(.\d+)');
static final _locationPattern = RegExp(r'([+-][.0-9]+)');
static final Map<String, String> _codecNames = {
'ac3': 'AC-3',
'eac3': 'E-AC-3',
'h264': 'AVC (H.264)',
'hdmv_pgs_subtitle': 'PGS',
'hevc': 'HEVC (H.265)',
'matroska': 'Matroska',
'mpeg4': 'MPEG-4 Visual',
'mpegts': 'MPEG-TS',
'subrip': 'SubRip',
'webm': 'WebM',
Codecs.ac3: 'AC-3',
Codecs.eac3: 'E-AC-3',
Codecs.h264: 'AVC (H.264)',
Codecs.hevc: 'HEVC (H.265)',
Codecs.matroska: 'Matroska',
Codecs.mpeg4: 'MPEG-4 Visual',
Codecs.mpts: 'MPEG-TS',
Codecs.opus: 'Opus',
Codecs.pgs: 'PGS',
Codecs.subrip: 'SubRip',
Codecs.theora: 'Theora',
Codecs.vorbis: 'Vorbis',
Codecs.webm: 'WebM',
};
static Future<Map> getVideoMetadata(AvesEntry entry) async {
@ -153,15 +159,36 @@ class VideoMetadataFormatter {
}
break;
case Keys.codecProfileId:
if (codec == 'h264') {
{
final profile = int.tryParse(value);
final levelString = info[Keys.codecLevel];
if (profile != null && profile != 0 && levelString != null) {
final level = int.tryParse(levelString) ?? 0;
save('Codec Profile', H264.formatProfile(profile, level));
if (profile != null) {
String? profileString;
switch (codec) {
case Codecs.h264:
case Codecs.hevc:
{
final levelString = info[Keys.codecLevel];
if (levelString != null) {
final level = int.tryParse(levelString) ?? 0;
if (codec == Codecs.h264) {
profileString = H264.formatProfile(profile, level);
} else {
profileString = Hevc.formatProfile(profile, level);
}
}
break;
}
case Codecs.aac:
profileString = AAC.formatProfile(profile);
break;
default:
profileString = profile.toString();
break;
}
save('Codec Profile', profileString);
}
break;
}
break;
case Keys.compatibleBrands:
final formattedBrands = RegExp(r'.{4}').allMatches(value).map((m) {
final brand = m.group(0)!;

View file

@ -0,0 +1,33 @@
class AAC {
static const profileMain = 0;
static const profileLowComplexity = 1;
static const profileScalableSampleRate = 2;
static const profileLongTermPrediction = 3;
static const profileHighEfficiency = 4;
static const profileHighEfficiencyV2 = 28;
static const profileLowDelay = 22;
static const profileLowDelayV2 = 38;
static String formatProfile(int profileIndex) {
switch (profileIndex) {
case profileMain:
return 'Main';
case profileLowComplexity:
return 'LC';
case profileLongTermPrediction:
return 'LTP';
case profileScalableSampleRate:
return 'SSR';
case profileHighEfficiency:
return 'HE-AAC';
case profileHighEfficiencyV2:
return 'HE-AAC v2';
case profileLowDelay:
return 'LD';
case profileLowDelayV2:
return 'ELD';
default:
return '$profileIndex';
}
}
}

View file

@ -1,21 +1,29 @@
class H264 {
static const profileConstrained = 1 << 9;
static const profileIntra = 1 << 11;
static const profileBaseline = 66;
static const profileConstrainedBaseline = 66 | profileConstrained;
static const profileMain = 77;
static const profileExtended = 88;
static const profileHigh = 100;
static const profileHigh10 = 110;
static const profileHigh10Intra = 110 | profileIntra;
static const profileHigh422 = 122;
static const profileHigh422Intra = 122 | profileIntra;
static const profileHigh444 = 144;
static const profileHigh444Predictive = 244;
// intra
static const profileHigh10Intra = 110 | profileIntra;
static const profileHigh422Intra = 122 | profileIntra;
static const profileHigh444Intra = 244 | profileIntra;
static const profileCAVLC444 = 44;
static String formatProfile(int profileIndex, int level) {
// multiview
static const profileMultiviewHigh = 118;
static const profileStereoHigh = 128;
static const profileMultiviewDepthHigh = 138;
static String formatProfile(int profileIndex, int levelIndex) {
String profile;
switch (profileIndex) {
case profileBaseline:
@ -60,7 +68,8 @@ class H264 {
default:
return '$profileIndex';
}
if (level < 10) return profile;
return '$profile Profile, Level ${level % 10 == 0 ? level ~/ 10 : level / 10}';
if (levelIndex <= 0) return profile;
final level = (levelIndex / 10.0).toStringAsFixed(levelIndex % 10 != 0 ? 1 : 0);
return '$profile Profile, Level $level';
}
}

View file

@ -0,0 +1,29 @@
class Hevc {
static const profileMain = 1;
static const profileMain10 = 2;
static const profileMainStillPicture = 3;
static const profileRExt = 4;
static String formatProfile(int profileIndex, int levelIndex) {
String profile;
switch (profileIndex) {
case profileMain:
profile = 'Main';
break;
case profileMain10:
profile = 'Main 10';
break;
case profileMainStillPicture:
profile = 'Main Still Picture';
break;
case profileRExt:
profile = 'Format Range';
break;
default:
return '$profileIndex';
}
if (levelIndex <= 0) return profile;
final level = (levelIndex / 30.0).toStringAsFixed(levelIndex % 10 != 0 ? 1 : 0);
return '$profile Profile, Level $level';
}
}

View file

@ -41,6 +41,7 @@ class MimeTypes {
static const anyVideo = 'video/*';
static const avi = 'video/avi';
static const mov = 'video/quicktime';
static const mp2t = 'video/mp2t'; // .m2ts
static const mp4 = 'video/mp4';

View file

@ -1,12 +1,16 @@
import 'package:aves/ref/mime_types.dart';
class MimeUtils {
static String displayType(String mime) {
switch (mime) {
case 'image/x-icon':
return 'ICO';
case 'image/x-jg':
case MimeTypes.art:
return 'ART';
case 'image/vnd.adobe.photoshop':
case 'image/x-photoshop':
case MimeTypes.ico:
return 'ICO';
case MimeTypes.mov:
return 'MOV';
case MimeTypes.psdVnd:
case MimeTypes.psdX:
return 'PSD';
default:
final patterns = [