info: improvements for HEVC/AAC profiles, sony video USMT/MTDT boxes
This commit is contained in:
parent
dabc63c00b
commit
15857ccc9f
9 changed files with 244 additions and 32 deletions
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
16
lib/model/video/codecs.dart
Normal file
16
lib/model/video/codecs.dart
Normal 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';
|
||||
}
|
|
@ -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)!;
|
||||
|
|
33
lib/model/video/profiles/aac.dart
Normal file
33
lib/model/video/profiles/aac.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
29
lib/model/video/profiles/hevc.dart
Normal file
29
lib/model/video/profiles/hevc.dart
Normal 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';
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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 = [
|
||||
|
|
Loading…
Reference in a new issue