diff --git a/spatialmedia/__main__.py b/spatialmedia/__main__.py
index 6559d75..9abc697 100755
--- a/spatialmedia/__main__.py
+++ b/spatialmedia/__main__.py
@@ -48,25 +48,51 @@ def main():
help=
"injects spatial media metadata into the first file specified (.mp4 or "
".mov) and saves the result to the second file specified")
+
video_group = parser.add_argument_group("Spherical Video")
- video_group.add_argument("-s",
- "--stereo",
- action="store",
- dest="stereo_mode",
- metavar="STEREO-MODE",
- choices=["none", "top-bottom", "left-right"],
- default="none",
- help="stereo mode (none | top-bottom | left-right)")
video_group.add_argument(
- "-c",
- "--crop",
+ "-s",
+ "--stereo",
action="store",
+ dest="stereo_mode",
+ metavar="STEREO-MODE",
+ choices=["none", "top-bottom", "left-right"],
default=None,
- help=
- "crop region. Must specify 6 integers in the form of \"w:h:f_w:f_h:x:y\""
- " where w=CroppedAreaImageWidthPixels h=CroppedAreaImageHeightPixels "
- "f_w=FullPanoWidthPixels f_h=FullPanoHeightPixels "
- "x=CroppedAreaLeftPixels y=CroppedAreaTopPixels")
+ help="stereo mode (none | top-bottom | left-right)")
+ video_group.add_argument(
+ "-m",
+ "--projection",
+ action="store",
+ dest="projection",
+ metavar="SPHERICAL-MODE",
+ choices=["equirectangular", "cubemap"],
+ default=None,
+ help="projection (equirectangular | cubemap)")
+ video_group.add_argument(
+ "-y",
+ "--yaw",
+ action="store",
+ dest="yaw",
+ metavar="YAW",
+ default=0,
+ help="yaw")
+ video_group.add_argument(
+ "-p",
+ "--pitch",
+ action="store",
+ dest="pitch",
+ metavar="PITCH",
+ default=0,
+ help="pitch")
+ video_group.add_argument(
+ "-r",
+ "--roll",
+ action="store",
+ dest="roll",
+ metavar="ROLL",
+ default=0,
+ help="roll")
+
audio_group = parser.add_argument_group("Spatial Audio")
audio_group.add_argument(
"-a",
@@ -85,13 +111,18 @@ def main():
return
metadata = metadata_utils.Metadata()
- metadata.video = metadata_utils.generate_spherical_xml(args.stereo_mode,
- args.crop)
+
+ if args.stereo_mode:
+ metadata.stereo = args.stereo_mode
+
+ if args.projection:
+ metadata.spherical = args.projection
if args.spatial_audio:
metadata.audio = metadata_utils.SPATIAL_AUDIO_DEFAULT_METADATA
- if metadata.video:
+ if metadata.stereo or metadata.spherical or metadata.audio:
+ metadata.orientation = {"yaw": args.yaw, "pitch": args.pitch, "roll": args.roll}
metadata_utils.inject_metadata(args.file[0], args.file[1], metadata,
console)
else:
diff --git a/spatialmedia/metadata_utils.py b/spatialmedia/metadata_utils.py
index 695c21e..2cd2b21 100755
--- a/spatialmedia/metadata_utils.py
+++ b/spatialmedia/metadata_utils.py
@@ -22,70 +22,11 @@
import StringIO
import struct
import traceback
-import xml.etree
-import xml.etree.ElementTree
from spatialmedia import mpeg
MPEG_FILE_EXTENSIONS = [".mp4", ".mov"]
-SPHERICAL_UUID_ID = (
- "\xff\xcc\x82\x63\xf8\x55\x4a\x93\x88\x14\x58\x7a\x02\x52\x1f\xdd")
-
-# XML contents.
-RDF_PREFIX = " xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" "
-
-SPHERICAL_XML_HEADER = \
- ""\
- ""
-
-SPHERICAL_XML_CONTENTS = \
- "true"\
- "true"\
- ""\
- "Spherical Metadata Tool"\
- ""\
- "equirectangular"
-
-SPHERICAL_XML_CONTENTS_TOP_BOTTOM = \
- "top-bottom"
-SPHERICAL_XML_CONTENTS_LEFT_RIGHT = \
- "left-right"
-
-# Parameter order matches that of the crop option.
-SPHERICAL_XML_CONTENTS_CROP_FORMAT = \
- "{0}"\
- ""\
- "{1}"\
- ""\
- "{2}"\
- "{3}"\
- "{4}"\
- "{5}"
-
-SPHERICAL_XML_FOOTER = ""
-
-SPHERICAL_TAGS_LIST = [
- "Spherical",
- "Stitched",
- "StitchingSoftware",
- "ProjectionType",
- "SourceCount",
- "StereoMode",
- "InitialViewHeadingDegrees",
- "InitialViewPitchDegrees",
- "InitialViewRollDegrees",
- "Timestamp",
- "CroppedAreaImageWidthPixels",
- "CroppedAreaImageHeightPixels",
- "FullPanoWidthPixels",
- "FullPanoHeightPixels",
- "CroppedAreaLeftPixels",
- "CroppedAreaTopPixels",
-]
-
SPATIAL_AUDIO_DEFAULT_METADATA = {
"ambisonic_order": 1,
"ambisonic_type": "periphonic",
@@ -96,57 +37,73 @@
class Metadata(object):
def __init__(self):
+ self.stereo = None
+ self.spherical = None
+ self.orientation = None
self.video = None
self.audio = None
class ParsedMetadata(object):
def __init__(self):
- self.video = dict()
self.audio = None
+ self.video = dict()
self.num_audio_channels = 0
-SPHERICAL_PREFIX = "{http://ns.google.com/videos/1.0/spherical/}"
-SPHERICAL_TAGS = dict()
-for tag in SPHERICAL_TAGS_LIST:
- SPHERICAL_TAGS[SPHERICAL_PREFIX + tag] = tag
-
-integer_regex_group = "(\d+)"
-crop_regex = "^{0}$".format(":".join([integer_regex_group] * 6))
-
-
-def spherical_uuid(metadata):
- """Constructs a uuid containing spherical metadata.
+def mpeg4_add_spherical_v2(mpeg4_file, in_fh, spherical_metadata, console):
+ """Adds spherical metadata to the first video track of the input
+ mpeg4_file. Returns False on failure.
Args:
- metadata: String, xml to inject in spherical tag.
-
- Returns:
- uuid_leaf: a box containing spherical metadata.
+ mpeg4_file: mpeg4, Mpeg4 file structure to add metadata.
+ in_fh: file handle, Source for uncached file contents.
+ spherical_metadata: dictionary.
"""
- uuid_leaf = mpeg.Box()
- assert(len(SPHERICAL_UUID_ID) == 16)
- uuid_leaf.name = mpeg.constants.TAG_UUID
- uuid_leaf.header_size = 8
- uuid_leaf.content_size = 0
-
- uuid_leaf.contents = SPHERICAL_UUID_ID + metadata
- uuid_leaf.content_size = len(uuid_leaf.contents)
+ for element in mpeg4_file.moov_box.contents:
+ if element.name == mpeg.constants.TAG_TRAK:
+ for sub_element in element.contents:
+ if sub_element.name != mpeg.constants.TAG_MDIA:
+ continue
+ for mdia_sub_element in sub_element.contents:
+ if mdia_sub_element.name != mpeg.constants.TAG_HDLR:
+ continue
+ position = mdia_sub_element.content_start() + 8
+ in_fh.seek(position)
+ if in_fh.read(4) == mpeg.constants.TAG_VIDE:
+ return inject_spherical_atom(in_fh, sub_element, spherical_metadata, console)
+ return False
- return uuid_leaf
+def inject_spherical_atom(in_fh, video_media_atom, spherical_metadata, console):
+ for atom in video_media_atom.contents:
+ if atom.name != mpeg.constants.TAG_MINF:
+ continue
+ for element in atom.contents:
+ if element.name != mpeg.constants.TAG_STBL:
+ continue
+ for sub_element in element.contents:
+ if sub_element.name != mpeg.constants.TAG_STSD:
+ continue
+ for sample_description in sub_element.contents:
+ if sample_description.name in\
+ mpeg.constants.VIDEO_SAMPLE_DESCRIPTIONS:
+ in_fh.seek(sample_description.position +
+ sample_description.header_size + 16)
+ sv3d_atom = mpeg.sv3dBox.create(spherical_metadata)
+ sample_description.contents.append(sv3d_atom)
+ return True
+ return False
-def mpeg4_add_spherical(mpeg4_file, in_fh, metadata):
- """Adds a spherical uuid box to an mpeg4 file for all video tracks.
+def mpeg4_add_stereo(mpeg4_file, in_fh, stereo_metadata, console):
+ """Adds stereo-mode metadata to the first video track of the input
+ mpeg4_file. Returns False on failure.
Args:
mpeg4_file: mpeg4, Mpeg4 file structure to add metadata.
in_fh: file handle, Source for uncached file contents.
- metadata: string, xml metadata to inject into spherical tag.
+ stereo_metadata: string.
"""
for element in mpeg4_file.moov_box.contents:
if element.name == mpeg.constants.TAG_TRAK:
- added = False
- element.remove(mpeg.constants.TAG_UUID)
for sub_element in element.contents:
if sub_element.name != mpeg.constants.TAG_MDIA:
continue
@@ -155,17 +112,30 @@ def mpeg4_add_spherical(mpeg4_file, in_fh, metadata):
continue
position = mdia_sub_element.content_start() + 8
in_fh.seek(position)
- if in_fh.read(4) == mpeg.constants.TRAK_TYPE_VIDE:
- added = True
- break
+ if in_fh.read(4) == mpeg.constants.TAG_VIDE:
+ return inject_stereo_mode_atom(in_fh, sub_element, stereo_metadata, console)
+ return False
- if added:
- if not element.add(spherical_uuid(metadata)):
- return False
- break
+def inject_stereo_mode_atom(in_fh, video_media_atom, stereo_metadata, console):
+ for atom in video_media_atom.contents:
+ if atom.name != mpeg.constants.TAG_MINF:
+ continue
+ for element in atom.contents:
+ if element.name != mpeg.constants.TAG_STBL:
+ continue
+ for sub_element in element.contents:
+ if sub_element.name != mpeg.constants.TAG_STSD:
+ continue
+ for sample_description in sub_element.contents:
+ if sample_description.name in\
+ mpeg.constants.VIDEO_SAMPLE_DESCRIPTIONS:
+ in_fh.seek(sample_description.position +
+ sample_description.header_size + 16)
- mpeg4_file.resize()
- return True
+ st3d_atom = mpeg.st3dBox.create(stereo_metadata)
+ sample_description.contents.append(st3d_atom)
+ return True
+ return False
def mpeg4_add_spatial_audio(mpeg4_file, in_fh, audio_metadata, console):
"""Adds spatial audio metadata to the first audio track of the input
@@ -238,48 +208,6 @@ def inject_spatial_audio_atom(
sample_description.contents.append(sa3d_atom)
return True
-def parse_spherical_xml(contents, console):
- """Returns spherical metadata for a set of xml data.
-
- Args:
- contents: string, spherical metadata xml contents.
-
- Returns:
- dictionary containing the parsed spherical metadata values.
- """
- try:
- parsed_xml = xml.etree.ElementTree.XML(contents)
- except xml.etree.ElementTree.ParseError:
- try:
- console(traceback.format_exc())
- console(contents)
- index = contents.find(" full_width_pixels or
- cropped_height_pixels > full_height_pixels):
- print "Error with crop params: cropped area dimensions are "\
- "invalid: width = {width} height = {height}".format(
- width=cropped_width_pixels,
- height=cropped_height_pixels)
- return False
-
- # We are pretty restrictive and don't allow anything strange. There
- # could be use-cases for a horizontal offset that essentially
- # translates the domain, but we don't support this (so that no
- # extra work has to be done on the client).
- total_width = cropped_offset_left_pixels + cropped_width_pixels
- total_height = cropped_offset_top_pixels + cropped_height_pixels
- if (cropped_offset_left_pixels < 0 or
- cropped_offset_top_pixels < 0 or
- total_width > full_width_pixels or
- total_height > full_height_pixels):
- print "Error with crop params: cropped area offsets are "\
- "invalid: left = {left} top = {top} "\
- "left+cropped width: {total_width} "\
- "top+cropped height: {total_height}".format(
- left=cropped_offset_left_pixels,
- top=cropped_offset_top_pixels,
- total_width=total_width,
- total_height=total_height)
- return False
-
- additional_xml += SPHERICAL_XML_CONTENTS_CROP_FORMAT.format(
- cropped_width_pixels, cropped_height_pixels,
- full_width_pixels, full_height_pixels,
- cropped_offset_left_pixels, cropped_offset_top_pixels)
-
- spherical_xml = (SPHERICAL_XML_HEADER +
- SPHERICAL_XML_CONTENTS +
- additional_xml +
- SPHERICAL_XML_FOOTER)
- return spherical_xml
-
-
def get_descriptor_length(in_fh):
"""Derives the length of the MP4 elementary stream descriptor at the
current position in the input file.
diff --git a/spatialmedia/mpeg/__init__.py b/spatialmedia/mpeg/__init__.py
index 98e7475..0ae7159 100644
--- a/spatialmedia/mpeg/__init__.py
+++ b/spatialmedia/mpeg/__init__.py
@@ -16,6 +16,8 @@
# limitations under the License.
import spatialmedia.mpeg.sa3d
+import spatialmedia.mpeg.st3d
+import spatialmedia.mpeg.sv3d
import spatialmedia.mpeg.box
import spatialmedia.mpeg.constants
import spatialmedia.mpeg.container
@@ -25,7 +27,9 @@
Box = box.Box
SA3DBox = sa3d.SA3DBox
+st3dBox = st3d.st3dBox
+sv3dBox = sv3d.sv3dBox
Container = container.Container
Mpeg4Container = mpeg4_container.Mpeg4Container
-__all__ = ["box", "mpeg4", "container", "constants", "sa3d"]
+__all__ = ["box", "mpeg4", "container", "constants", "sa3d", "st3d", "sv3d"]
diff --git a/spatialmedia/mpeg/constants.py b/spatialmedia/mpeg/constants.py
index 7313811..9c08ef5 100755
--- a/spatialmedia/mpeg/constants.py
+++ b/spatialmedia/mpeg/constants.py
@@ -28,8 +28,11 @@
TAG_HDLR = "hdlr"
TAG_FTYP = "ftyp"
TAG_ESDS = "esds"
+TAG_VIDE = "vide"
TAG_SOUN = "soun"
TAG_SA3D = "SA3D"
+TAG_ST3D = "st3d"
+TAG_SV3D = "sv3d"
# Container types.
TAG_MOOV = "moov"
@@ -43,9 +46,15 @@
TAG_UUID = "uuid"
TAG_WAVE = "wave"
-# Sound sample descriptions.
TAG_NONE = "NONE"
TAG_RAW_ = "raw "
+
+# Video sample descriptions.
+TAG_AVC1 = "avc1"
+TAG_HVC1 = "hvc1"
+TAG_HEV1 = "hev1"
+
+# Sound sample descriptions.
TAG_TWOS = "twos"
TAG_SOWT = "sowt"
TAG_FL32 = "fl32"
@@ -57,6 +66,14 @@
TAG_LPCM = "lpcm"
TAG_MP4A = "mp4a"
+VIDEO_SAMPLE_DESCRIPTIONS = frozenset([
+ TAG_NONE,
+ TAG_RAW_,
+ TAG_AVC1,
+ TAG_HVC1,
+ TAG_HEV1,
+ ])
+
SOUND_SAMPLE_DESCRIPTIONS = frozenset([
TAG_NONE,
TAG_RAW_,
@@ -72,7 +89,7 @@
TAG_MP4A,
])
-CONTAINERS_LIST = frozenset([
+AUDIO_CONTAINERS_LIST = frozenset([
TAG_MDIA,
TAG_MINF,
TAG_MOOV,
@@ -83,3 +100,12 @@
TAG_WAVE,
]).union(SOUND_SAMPLE_DESCRIPTIONS)
+VIDEO_CONTAINERS_LIST = frozenset([
+ TAG_MDIA,
+ TAG_MINF,
+ TAG_MOOV,
+ TAG_STBL,
+ TAG_STSD,
+ TAG_TRAK,
+ TAG_UDTA,
+ ]).union(VIDEO_SAMPLE_DESCRIPTIONS)
diff --git a/spatialmedia/mpeg/container.py b/spatialmedia/mpeg/container.py
index 057a360..2e21ece 100755
--- a/spatialmedia/mpeg/container.py
+++ b/spatialmedia/mpeg/container.py
@@ -26,6 +26,8 @@
from spatialmedia.mpeg import box
from spatialmedia.mpeg import constants
from spatialmedia.mpeg import sa3d
+from spatialmedia.mpeg import st3d
+from spatialmedia.mpeg import sv3d
def load(fh, position, end):
if position is None:
@@ -36,13 +38,21 @@ def load(fh, position, end):
size = struct.unpack(">I", fh.read(4))[0]
name = fh.read(4)
- is_box = name not in constants.CONTAINERS_LIST
+ is_box = False
+ if (name not in constants.AUDIO_CONTAINERS_LIST) and \
+ (name not in constants.VIDEO_CONTAINERS_LIST):
+ is_box = True
# Handle the mp4a decompressor setting (wave -> mp4a).
if name == constants.TAG_MP4A and size == 12:
is_box = True
if is_box:
+ # this is only for printing contents while parsing
if name == constants.TAG_SA3D:
return sa3d.load(fh, position, end)
+ elif name == constants.TAG_ST3D:
+ return st3d.load(fh, position, end)
+ elif name == constants.TAG_SV3D:
+ return sv3d.load(fh, position, end)
return box.load(fh, position, end)
if size == 1:
@@ -75,6 +85,17 @@ def load(fh, position, end):
else:
print("Unsupported sample description version:",
sample_description_version)
+ if name in constants.VIDEO_SAMPLE_DESCRIPTIONS:
+ current_pos = fh.tell()
+ fh.seek(current_pos + 8)
+ sample_description_version = struct.unpack(">h", fh.read(2))[0]
+ fh.seek(current_pos)
+
+ if sample_description_version == 0:
+ padding = 78
+ else:
+ print("Unsupported sample description version:",
+ sample_description_version)
new_box = Container()
new_box.name = name
diff --git a/spatialmedia/mpeg/st3d.py b/spatialmedia/mpeg/st3d.py
new file mode 100644
index 0000000..ba2a403
--- /dev/null
+++ b/spatialmedia/mpeg/st3d.py
@@ -0,0 +1,110 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2017 Vimeo. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""MPEG st3d box processing classes.
+
+Enables the injection of an st3d MPEG-4. The st3d box specification
+conforms to that outlined in docs/spherical-video-v2-rfc.md
+"""
+
+import struct
+
+from spatialmedia.mpeg import box
+from spatialmedia.mpeg import constants
+
+
+def load(fh, position=None, end=None):
+ """ Loads the st3d box located at position in an mp4 file.
+
+ Args:
+ fh: file handle, input file handle.
+ position: int or None, current file position.
+
+ Returns:
+ new_box: box, st3d box loaded from the file location or None.
+ """
+ if position is None:
+ position = fh.tell()
+
+ fh.seek(position)
+ new_box = st3dBox()
+ new_box.position = position
+ size = struct.unpack(">I", fh.read(4))[0]
+ name = fh.read(4)
+
+ if (name != constants.TAG_ST3D):
+ print "Error: box is not an st3d box."
+ return None
+
+ if (position + size > end):
+ print "Error: st3d box size exceeds bounds."
+ return None
+
+ new_box.content_size = size - new_box.header_size
+ new_box.version = struct.unpack(">I", fh.read(4))[0]
+ new_box.stereo_mode = struct.unpack(">B", fh.read(1))[0]
+ return new_box
+
+
+class st3dBox(box.Box):
+ stereo_modes = {'none': 0, 'top-bottom': 1, 'left-right': 2}
+
+ def __init__(self):
+ box.Box.__init__(self)
+ self.name = constants.TAG_ST3D
+ self.header_size = 8
+ self.version = 0
+ self.stereo_mode = 0
+
+ @staticmethod
+ def create(stereo_metadata):
+ new_box = st3dBox()
+ new_box.header_size = 8
+ new_box.name = constants.TAG_ST3D
+ new_box.version = 0 # uint8 + uint24 (flags)
+ new_box.content_size += 4
+ new_box.stereo_mode = st3dBox.stereo_modes[stereo_metadata] # uint8
+ new_box.content_size += 1
+
+ return new_box
+
+ def stereo_mode_name(self):
+ return (key for key,value in st3dBox.stereo_modes.items()
+ if value==self.stereo_mode).next()
+
+ def print_box(self, console):
+ """ Prints the contents of this stereoscopic (st3d) box to the
+ console.
+ """
+ stereo_mode = self.stereo_mode_name()
+ console("\t\tStereo Mode: %s" % stereo_mode)
+
+ def get_metadata_string(self):
+ """ Outputs a concise single line stereo metadata string. """
+ return "Stereo mode: %s" % (self.stereo_mode_name())
+
+ def save(self, in_fh, out_fh, delta):
+ if (self.header_size == 16):
+ out_fh.write(struct.pack(">I", 1))
+ out_fh.write(struct.pack(">Q", self.size()))
+ out_fh.write(self.name)
+ elif(self.header_size == 8):
+ out_fh.write(struct.pack(">I", self.size()))
+ out_fh.write(self.name)
+
+ out_fh.write(struct.pack(">I", self.version))
+ out_fh.write(struct.pack(">B", self.stereo_mode))
diff --git a/spatialmedia/mpeg/sv3d.py b/spatialmedia/mpeg/sv3d.py
new file mode 100644
index 0000000..031f98f
--- /dev/null
+++ b/spatialmedia/mpeg/sv3d.py
@@ -0,0 +1,158 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# Copyright 2017 Vimeo. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""MPEG sv3d box processing classes.
+
+Enables the injection of an sv3d MPEG-4. The sv3d box specification
+conforms to that outlined in docs/spherical-video-v2-rfc.md
+"""
+
+import struct
+
+from spatialmedia.mpeg import box
+from spatialmedia.mpeg import constants
+
+
+def load(fh, position=None, end=None):
+ """ Loads the sv3d box located at position in an mp4 file.
+
+ Args:
+ fh: file handle, input file handle.
+ position: int or None, current file position.
+
+ Returns:
+ new_box: box, sv3d box loaded from the file location or None.
+ """
+ if position is None:
+ position = fh.tell()
+
+ fh.seek(position)
+ new_box = sv3dBox()
+ new_box.position = position
+ size = struct.unpack(">I", fh.read(4))[0]
+ name = fh.read(4)
+
+ if (name != constants.TAG_SV3D):
+ print "Error: box is not an sv3d box."
+ return None
+
+ if (position + size > end):
+ print "Error: sv3d box size exceeds bounds."
+ return None
+
+ fh.read(13) #svhd
+ fh.read(4 + 4) #proj
+ fh.read(4 + 4 + 4) #prhd
+ new_box.yaw = struct.unpack(">I", fh.read(4))[0] / 65536
+ new_box.pitch = struct.unpack(">I", fh.read(4))[0] / 65536
+ new_box.roll = struct.unpack(">I", fh.read(4))[0] / 65536
+ fh.read(4) #size
+ proj = fh.read(4)
+ if proj == "equi":
+ new_box.projection = "equirectangular"
+ elif proj == "cbmp":
+ new_box.projection = "cubemap"
+ else:
+ print "Unknown projection type."
+ return None
+
+ return new_box
+
+
+class sv3dBox(box.Box):
+ def __init__(self):
+ box.Box.__init__(self)
+ self.name = constants.TAG_SV3D
+ self.header_size = 8
+ self.proj_size = 0
+ self.content_size = 0
+ self.projection = ""
+ self.yaw = 0
+ self.pitch = 0
+ self.roll = 0
+
+ @staticmethod
+ def create(metadata):
+ new_box = sv3dBox()
+ new_box.header_size = 8
+ new_box.name = constants.TAG_SV3D
+ new_box.projection = metadata.spherical
+ if new_box.projection == "equirectangular":
+ new_box.content_size = 81 - new_box.header_size
+ new_box.proj_size = 60
+ elif new_box.projection == "cubemap":
+ new_box.content_size = 73 - new_box.header_size
+ new_box.proj_size = 52
+ new_box.yaw = float(metadata.orientation["yaw"])
+ new_box.pitch = float(metadata.orientation["pitch"])
+ new_box.roll = float(metadata.orientation["roll"])
+
+ return new_box
+
+ def print_box(self, console):
+ """ Prints the contents of this spherical (sv3d) box to the
+ console.
+ """
+ console("\t\tSpherical Mode: %s" % self.projection)
+ console("\t\t [Yaw: %.02f, Pitch: %.02f, Roll: %.02f]" % (self.yaw, self.pitch, self.roll))
+
+ def get_metadata_string(self):
+ """ Outputs a concise single line audio metadata string. """
+ return "Spherical mode: %s (%f,%f,%f)" % (self.projection, self.yaw, self.pitch, self.roll)
+
+ def save(self, in_fh, out_fh, delta):
+ if (self.header_size == 16):
+ out_fh.write(struct.pack(">I", 1))
+ out_fh.write(struct.pack(">Q", self.size()))
+ out_fh.write(self.name)
+ elif(self.header_size == 8):
+ out_fh.write(struct.pack(">I", self.content_size + self.header_size))
+ out_fh.write(self.name)
+
+ #svhd
+ out_fh.write(struct.pack(">I", 13)) # size
+ out_fh.write("svhd") # tag
+ out_fh.write(struct.pack(">I", 0)) # version+flags
+ out_fh.write(struct.pack(">B", 0)) # metadata
+
+ #proj
+ out_fh.write(struct.pack(">I", self.proj_size)) #size
+ out_fh.write("proj") # proj
+
+ #prhd
+ out_fh.write(struct.pack(">I", 24)) # size
+ out_fh.write("prhd") # tag
+ out_fh.write(struct.pack(">I", 0)) # version+flags
+ out_fh.write(struct.pack(">I", self.yaw * 65536)) # yaw
+ out_fh.write(struct.pack(">I", self.pitch * 65536)) # pitch
+ out_fh.write(struct.pack(">I", self.roll * 65536)) # roll
+
+ #cmbp or equi
+ if self.projection == "equirectangular":
+ out_fh.write(struct.pack(">I", 28)) # size
+ out_fh.write("equi") # tag
+ out_fh.write(struct.pack(">I", 0)) # version+flags
+ out_fh.write(struct.pack(">I", 0))
+ out_fh.write(struct.pack(">I", 0))
+ out_fh.write(struct.pack(">I", 0))
+ out_fh.write(struct.pack(">I", 0))
+ elif self.projection == "cubemap":
+ out_fh.write(struct.pack(">I", 20)) # size
+ out_fh.write("cbmp") # tag
+ out_fh.write(struct.pack(">I", 0)) # version+flags
+ out_fh.write(struct.pack(">I", 0)) # layout
+ out_fh.write(struct.pack(">I", 0)) # padding