info: added metadata for Spherical Video V1

This commit is contained in:
Thibault Deckers 2020-12-28 12:50:10 +09:00
parent c93393a365
commit e127a5ebca
3 changed files with 117 additions and 8 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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<String, String?> = 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
}
}
}