From e127a5ebca35d8241ff47ac5cbaccd26b1eef149 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 28 Dec 2020 12:50:10 +0900 Subject: [PATCH] info: added metadata for Spherical Video V1 --- .../aves/channel/calls/MetadataHandler.kt | 14 ++- .../thibault/aves/metadata/Metadata.kt | 3 - .../thibault/aves/metadata/SphericalVideo.kt | 108 ++++++++++++++++++ 3 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 android/app/src/main/kotlin/deckers/thibault/aves/metadata/SphericalVideo.kt 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 0d2d41135..22752003f 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 @@ -21,17 +21,15 @@ import com.drew.metadata.iptc.IptcDirectory import com.drew.metadata.mp4.media.Mp4UuidBoxDirectory import com.drew.metadata.webp.WebpDirectory import com.drew.metadata.xmp.XmpDirectory +import deckers.thibault.aves.metadata.* import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDouble import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeInt import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeRational -import deckers.thibault.aves.metadata.Geotiff -import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDateMillis import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeDescription import deckers.thibault.aves.metadata.MediaMetadataRetrieverHelper.getSafeInt -import deckers.thibault.aves.metadata.Metadata import deckers.thibault.aves.metadata.Metadata.getRotationDegreesForExifCode import deckers.thibault.aves.metadata.Metadata.isFlippedForExifCode import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeBoolean @@ -40,7 +38,6 @@ import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeInt import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeRational import deckers.thibault.aves.metadata.MetadataExtractorHelper.getSafeString import deckers.thibault.aves.metadata.MetadataExtractorHelper.isGeoTiff -import deckers.thibault.aves.metadata.XMP import deckers.thibault.aves.metadata.XMP.getSafeDateMillis import deckers.thibault.aves.metadata.XMP.getSafeLocalizedText import deckers.thibault.aves.metadata.XMP.isPanorama @@ -142,6 +139,13 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { // remove this stat as it is not actual XMP data dirMap.remove(dir.getTagName(XmpDirectory.TAG_XMP_VALUE_COUNT)) } + + 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()) + } + } } } } catch (e: Exception) { @@ -348,7 +352,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { // identification of spherical video (aka 360° video) if (metadata.getDirectoriesOfType(Mp4UuidBoxDirectory::class.java).any { - it.getString(Mp4UuidBoxDirectory.TAG_UUID) == Metadata.SPHERICAL_VIDEO_V1_UUID + it.getString(Mp4UuidBoxDirectory.TAG_UUID) == GSpherical.SPHERICAL_VIDEO_V1_UUID }) { flags = flags or MASK_IS_360 } 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 fb6750232..47b649f64 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 @@ -19,9 +19,6 @@ object Metadata { // "+51.3328-000.7053+113.474/" (Apple) val VIDEO_LOCATION_PATTERN: Pattern = Pattern.compile("([+-][.0-9]+)([+-][.0-9]+).*") - // cf https://github.com/google/spatial-media - const val SPHERICAL_VIDEO_V1_UUID = "ffcc8263-f855-4a93-8814-587a02521fdd" - // directory names, as shown when listing all metadata const val DIR_GPS = "GPS" // from metadata-extractor const val DIR_XMP = "XMP" // from metadata-extractor diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SphericalVideo.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SphericalVideo.kt new file mode 100644 index 000000000..6bba753d9 --- /dev/null +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/SphericalVideo.kt @@ -0,0 +1,108 @@ +package deckers.thibault.aves.metadata + +import android.util.Log +import android.util.Xml +import deckers.thibault.aves.utils.LogUtils +import org.xmlpull.v1.XmlPullParser +import java.io.ByteArrayInputStream + +class GSpherical(bytes: ByteArray) { + var spherical: Boolean = false + var stitched: Boolean = false + var stitchingSoftware: String = "" + var projectionType: String = "" + var stereoMode: String? = null + var sourceCount: Int? = null + var initialViewHeadingDegrees: Int? = null + var initialViewPitchDegrees: Int? = null + var initialViewRollDegrees: Int? = null + var timestamp: Int? = null + var fullPanoWidthPixels: Int? = null + var fullPanoHeightPixels: Int? = null + var croppedAreaImageWidthPixels: Int? = null + var croppedAreaImageHeightPixels: Int? = null + var croppedAreaLeftPixels: Int? = null + var croppedAreaTopPixels: Int? = null + + init { + try { + ByteArrayInputStream(bytes).use { + val parser = Xml.newPullParser().apply { + setInput(it, null) + nextTag() + require(XmlPullParser.START_TAG, RDF_NS, "SphericalVideo") + } + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.eventType != XmlPullParser.START_TAG) continue + if (parser.namespace == GSPHERICAL_NS) { + when (val tag = parser.name) { + "Spherical" -> spherical = readTag(parser, tag) == "true" + "Stitched" -> stitched = readTag(parser, tag) == "true" + "StitchingSoftware" -> stitchingSoftware = readTag(parser, tag) + "ProjectionType" -> projectionType = readTag(parser, tag) + "StereoMode" -> stereoMode = readTag(parser, tag) + "SourceCount" -> sourceCount = Integer.parseInt(readTag(parser, tag)) + "InitialViewHeadingDegrees" -> initialViewHeadingDegrees = Integer.parseInt(readTag(parser, tag)) + "InitialViewPitchDegrees" -> initialViewPitchDegrees = Integer.parseInt(readTag(parser, tag)) + "InitialViewRollDegrees" -> initialViewRollDegrees = Integer.parseInt(readTag(parser, tag)) + "Timestamp" -> timestamp = Integer.parseInt(readTag(parser, tag)) + "FullPanoWidthPixels" -> fullPanoWidthPixels = Integer.parseInt(readTag(parser, tag)) + "FullPanoHeightPixels" -> fullPanoHeightPixels = Integer.parseInt(readTag(parser, tag)) + "CroppedAreaImageWidthPixels" -> croppedAreaImageWidthPixels = Integer.parseInt(readTag(parser, tag)) + "CroppedAreaImageHeightPixels" -> croppedAreaImageHeightPixels = Integer.parseInt(readTag(parser, tag)) + "CroppedAreaLeftPixels" -> croppedAreaLeftPixels = Integer.parseInt(readTag(parser, tag)) + "CroppedAreaTopPixels" -> croppedAreaTopPixels = Integer.parseInt(readTag(parser, tag)) + } + } + } + } + } catch (e: Exception) { + Log.w(LOG_TAG, "failed to parse XML", e) + } + } + + fun describe(): Map = hashMapOf( + "Spherical" to spherical.toString(), + "Stitched" to stitched.toString(), + "Stitching Software" to stitchingSoftware, + "Projection Type" to projectionType, + "Stereo Mode" to stereoMode, + "Source Count" to sourceCount?.toString(), + "Initial View Heading Degrees" to initialViewHeadingDegrees?.toString(), + "Initial View Pitch Degrees" to initialViewPitchDegrees?.toString(), + "Initial View Roll Degrees" to initialViewRollDegrees?.toString(), + "Timestamp" to timestamp?.toString(), + "Full Panorama Width Pixels" to fullPanoWidthPixels?.toString(), + "Full Panorama Height Pixels" to fullPanoHeightPixels?.toString(), + "Cropped Area Image Width Pixels" to croppedAreaImageWidthPixels?.toString(), + "Cropped Area Image Height Pixels" to croppedAreaImageHeightPixels?.toString(), + "Cropped Area Left Pixels" to croppedAreaLeftPixels?.toString(), + "Cropped Area Top Pixels" to croppedAreaTopPixels?.toString(), + ).filterValues { it != null } + + companion object SphericalVideo { + private val LOG_TAG = LogUtils.createTag(SphericalVideo::class.java) + + // cf https://github.com/google/spatial-media + const val SPHERICAL_VIDEO_V1_UUID = "ffcc8263-f855-4a93-8814-587a02521fdd" + + const val RDF_NS = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" + const val GSPHERICAL_NS = "http://ns.google.com/videos/1.0/spherical/" + + private fun readText(parser: XmlPullParser): String { + var text = "" + if (parser.next() == XmlPullParser.TEXT) { + text = parser.text + parser.nextTag() + } + return text + } + + private fun readTag(parser: XmlPullParser, tag: String): String { + parser.require(XmlPullParser.START_TAG, GSPHERICAL_NS, tag) + val text = readText(parser) + parser.require(XmlPullParser.END_TAG, GSPHERICAL_NS, tag) + return text + } + } +} \ No newline at end of file