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