info: improved USMT parsing
This commit is contained in:
parent
ca221635e9
commit
6c5536c443
4 changed files with 116 additions and 80 deletions
|
@ -184,12 +184,29 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
metadataMap["Spherical Video"] = HashMap(GSpherical(bytes).describe())
|
metadataMap["Spherical Video"] = HashMap(GSpherical(bytes).describe())
|
||||||
metadataMap.remove(dirName)
|
metadataMap.remove(dirName)
|
||||||
}
|
}
|
||||||
SonyVideoMetadata.USMT_UUID -> {
|
QuickTimeMetadata.USMT_UUID -> {
|
||||||
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
|
val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA)
|
||||||
val fields = SonyVideoMetadata.parseUsmt(bytes)
|
val blocks = QuickTimeMetadata.parseUsmt(bytes)
|
||||||
if (fields.isNotEmpty()) {
|
if (blocks.isNotEmpty()) {
|
||||||
dirMap.remove("Data")
|
metadataMap.remove(dirName)
|
||||||
dirMap.putAll(fields)
|
dirName = "QuickTime User Media"
|
||||||
|
val usmt = metadataMap[dirName] ?: HashMap()
|
||||||
|
metadataMap[dirName] = usmt
|
||||||
|
|
||||||
|
blocks.forEach {
|
||||||
|
var key = it.type
|
||||||
|
var value = it.value
|
||||||
|
val language = it.language
|
||||||
|
|
||||||
|
var i = 0
|
||||||
|
while (usmt.containsKey(key)) {
|
||||||
|
key = it.type + " (${++i})"
|
||||||
|
}
|
||||||
|
if (language != "und") {
|
||||||
|
value += " ($language)"
|
||||||
|
}
|
||||||
|
usmt[key] = value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ object Metadata {
|
||||||
val VIDEO_LOCATION_PATTERN: Pattern = Pattern.compile("([+-][.0-9]+)([+-][.0-9]+).*")
|
val VIDEO_LOCATION_PATTERN: Pattern = Pattern.compile("([+-][.0-9]+)([+-][.0-9]+).*")
|
||||||
|
|
||||||
private val VIDEO_DATE_SUBSECOND_PATTERN = Pattern.compile("(\\d{6})(\\.\\d+)")
|
private val VIDEO_DATE_SUBSECOND_PATTERN = Pattern.compile("(\\d{6})(\\.\\d+)")
|
||||||
private val VIDEO_TIMEZONE_PATTERN = Pattern.compile("(Z|[+-]\\d{4})$")
|
private val VIDEO_TIME_ZONE_PATTERN = Pattern.compile("(Z|[+-]\\d{4})$")
|
||||||
|
|
||||||
// directory names, as shown when listing all metadata
|
// directory names, as shown when listing all metadata
|
||||||
const val DIR_GPS = "GPS" // from metadata-extractor
|
const val DIR_GPS = "GPS" // from metadata-extractor
|
||||||
|
@ -71,7 +71,7 @@ object Metadata {
|
||||||
|
|
||||||
// optional time zone
|
// optional time zone
|
||||||
var timeZone: TimeZone? = null
|
var timeZone: TimeZone? = null
|
||||||
val timeZoneMatcher = VIDEO_TIMEZONE_PATTERN.matcher(dateString)
|
val timeZoneMatcher = VIDEO_TIME_ZONE_PATTERN.matcher(dateString)
|
||||||
if (timeZoneMatcher.find()) {
|
if (timeZoneMatcher.find()) {
|
||||||
timeZone = TimeZone.getTimeZone("GMT${timeZoneMatcher.group().replace("Z", "")}")
|
timeZone = TimeZone.getTimeZone("GMT${timeZoneMatcher.group().replace("Z", "")}")
|
||||||
dateString = timeZoneMatcher.replaceAll("")
|
dateString = timeZoneMatcher.replaceAll("")
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
package deckers.thibault.aves.metadata
|
||||||
|
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class QuickTimeMetadataBlock(val type: String, val value: String, val language: String)
|
||||||
|
|
||||||
|
object QuickTimeMetadata {
|
||||||
|
// QuickTime Profile Tags
|
||||||
|
// cf https://exiftool.org/TagNames/QuickTime.html#Profile
|
||||||
|
// cf https://www.ffmpeg.org/doxygen/1.1/movenc_8c_source.html#l02839
|
||||||
|
const val PROF_UUID = "50524f46-21d2-4fce-bb88-695cfac9c740"
|
||||||
|
|
||||||
|
// QuickTime UserMedia Tags
|
||||||
|
// cf https://github.com/sonyxperiadev/MultimediaForAndroidLibrary/blob/master/library/src/main/java/com/sonymobile/android/media/internal/VUParser.java
|
||||||
|
// cf https://rubenlaguna.com/post/2007-02-25-how-to-read-title-in-sony-psp-mp4-files/
|
||||||
|
const val USMT_UUID = "55534d54-21d2-4fce-bb88-695cfac9c740"
|
||||||
|
|
||||||
|
private const val METADATA_BOX_ID = "MTDT"
|
||||||
|
|
||||||
|
fun parseUsmt(data: ByteArray): List<QuickTimeMetadataBlock> {
|
||||||
|
val blocks = ArrayList<QuickTimeMetadataBlock>()
|
||||||
|
val boxType = String(data.copyOfRange(4, 8))
|
||||||
|
if (boxType == METADATA_BOX_ID) {
|
||||||
|
blocks.addAll(parseQuicktimeMtdtBox(data))
|
||||||
|
}
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseQuicktimeMtdtBox(data: ByteArray): List<QuickTimeMetadataBlock> {
|
||||||
|
val blocks = ArrayList<QuickTimeMetadataBlock>()
|
||||||
|
var bytes = data
|
||||||
|
val boxDataSize = BigInteger(data.copyOfRange(0, 4)).toInt()
|
||||||
|
val blockCount = BigInteger(bytes.copyOfRange(8, 10)).toInt()
|
||||||
|
bytes = bytes.copyOfRange(10, boxDataSize)
|
||||||
|
|
||||||
|
for (i in 0 until blockCount) {
|
||||||
|
val blockSize = BigInteger(bytes.copyOfRange(0, 2)).toInt()
|
||||||
|
val blockType = BigInteger(bytes.copyOfRange(2, 6)).toInt()
|
||||||
|
val language = parseLanguage(bytes.copyOfRange(6, 8))
|
||||||
|
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 -> "Creation Time"
|
||||||
|
0x04 -> "Software"
|
||||||
|
0x0A -> "Track property"
|
||||||
|
0x0B -> "Time zone"
|
||||||
|
0x0C -> "Modification Time"
|
||||||
|
else -> "0x${"%02x".format(blockType)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
blocks.add(
|
||||||
|
QuickTimeMetadataBlock(
|
||||||
|
type = blockTypeString,
|
||||||
|
value = payloadString,
|
||||||
|
language = language,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
bytes = bytes.copyOfRange(blockSize, bytes.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
private fun parseLanguage(bytes: ByteArray): String {
|
||||||
|
val i = BigInteger(bytes).toInt()
|
||||||
|
val c1 = Character.toChars((i shr 10 and 0x1F) + 0x60)[0]
|
||||||
|
val c2 = Character.toChars((i shr 5 and 0x1F) + 0x60)[0]
|
||||||
|
val c3 = Character.toChars((i and 0x1F) + 0x60)[0]
|
||||||
|
return "$c1$c2$c3"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,73 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue