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 fdfc58779..f4645ee64 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 @@ -184,12 +184,29 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { metadataMap["Spherical Video"] = HashMap(GSpherical(bytes).describe()) metadataMap.remove(dirName) } - SonyVideoMetadata.USMT_UUID -> { + QuickTimeMetadata.USMT_UUID -> { val bytes = dir.getByteArray(Mp4UuidBoxDirectory.TAG_USER_DATA) - val fields = SonyVideoMetadata.parseUsmt(bytes) - if (fields.isNotEmpty()) { - dirMap.remove("Data") - dirMap.putAll(fields) + val blocks = QuickTimeMetadata.parseUsmt(bytes) + if (blocks.isNotEmpty()) { + metadataMap.remove(dirName) + 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 + } } } } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt index 37f99e16e..890f4136c 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/Metadata.kt @@ -24,7 +24,7 @@ object Metadata { 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_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 const val DIR_GPS = "GPS" // from metadata-extractor @@ -71,7 +71,7 @@ object Metadata { // optional time zone var timeZone: TimeZone? = null - val timeZoneMatcher = VIDEO_TIMEZONE_PATTERN.matcher(dateString) + val timeZoneMatcher = VIDEO_TIME_ZONE_PATTERN.matcher(dateString) if (timeZoneMatcher.find()) { timeZone = TimeZone.getTimeZone("GMT${timeZoneMatcher.group().replace("Z", "")}") dateString = timeZoneMatcher.replaceAll("") diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/QuickTimeMetadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/QuickTimeMetadata.kt new file mode 100644 index 000000000..0bbd57eda --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/QuickTimeMetadata.kt @@ -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 { + val blocks = ArrayList() + val boxType = String(data.copyOfRange(4, 8)) + if (boxType == METADATA_BOX_ID) { + blocks.addAll(parseQuicktimeMtdtBox(data)) + } + return blocks + } + + private fun parseQuicktimeMtdtBox(data: ByteArray): List { + val blocks = ArrayList() + 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" + } +} diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SonyVideoMetadata.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SonyVideoMetadata.kt deleted file mode 100644 index 181035c5f..000000000 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SonyVideoMetadata.kt +++ /dev/null @@ -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 { - val dirMap = HashMap() - 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 - } -} \ No newline at end of file