From fe283aaa90fb18129ddc31fecacb970136a34cc9 Mon Sep 17 00:00:00 2001 From: Cole Richardson Date: Fri, 3 Apr 2026 19:53:45 +0000 Subject: [PATCH] Intial support for svhd inside sv3d --- spatialmedia/metadata_utils.py | 3 ++ spatialmedia/mpeg/sv3d.py | 50 ++++++++++++++++++++++++++++++++-- spatialmedia_test.py | 12 ++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/spatialmedia/metadata_utils.py b/spatialmedia/metadata_utils.py index cb7590b..35b6532 100755 --- a/spatialmedia/metadata_utils.py +++ b/spatialmedia/metadata_utils.py @@ -243,6 +243,8 @@ def inject_spatial_video_v2_atoms(in_fh, video_media_atom, projection, stereo_mo sample_description.add(st3d_atom) if projection: + svhd_atom = mpeg.sv3d.SVHDBox.create() + proj_atom = mpeg.container.Container(header_size=8) proj_atom.name = mpeg.constants.TAG_PROJ @@ -252,6 +254,7 @@ def inject_spatial_video_v2_atoms(in_fh, video_media_atom, projection, stereo_mo sv3d_atom = mpeg.container.Container(header_size=8) sv3d_atom.name = mpeg.constants.TAG_SV3D + sv3d_atom.add(svhd_atom) sv3d_atom.add(proj_atom) sample_description.remove(sv3d_atom.name) diff --git a/spatialmedia/mpeg/sv3d.py b/spatialmedia/mpeg/sv3d.py index 7e2c137..468da22 100644 --- a/spatialmedia/mpeg/sv3d.py +++ b/spatialmedia/mpeg/sv3d.py @@ -29,7 +29,8 @@ def is_supported_box_name(name): """Returns true if the box name is a supported sv3d box.""" - return (name == constants.TAG_PRHD or + return (name == constants.TAG_SVHD or + name == constants.TAG_PRHD or name == constants.TAG_EQUI or name == constants.TAG_ST3D) @@ -51,7 +52,9 @@ def load(fh, position=None, end=None): size = struct.unpack(">I", fh.read(4))[0] name = fh.read(4) - if name == constants.TAG_PRHD: + if name == constants.TAG_SVHD: + box = SVHDBox() + elif name == constants.TAG_PRHD: box = PRHDBox() elif name == constants.TAG_EQUI: box = EQUIBox() @@ -67,6 +70,49 @@ def load(fh, position=None, end=None): return box +class SVHDBox(box.Box): + """Spherical Video Header (svhd) FullBox; mandatory first child of sv3d per v2 RFC.""" + + def __init__(self, metadata_source="Spherical Metadata Tool"): + box.Box.__init__(self) + self.name = constants.TAG_SVHD + self.header_size = 8 + self.metadata_source = metadata_source + self.content_size = 4 + len(self._metadata_source_bytes()) + 1 + + def _metadata_source_bytes(self): + return self.metadata_source.encode("utf-8") + + @staticmethod + def create(metadata_source="Spherical Metadata Tool"): + return SVHDBox(metadata_source=metadata_source) + + def print_box(self, console): + """Prints the contents of this box to console.""" + console("\t\t\tSVHD {") + console("\t\t\t\tMetadata Source: %s" % self.metadata_source) + console("\t\t\t}") + + 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", 0)) # Version and flags + out_fh.write(self._metadata_source_bytes() + b"\0") + + def load_content(self, in_fh): + in_fh.read(4) # Version and flags + raw = in_fh.read(self.content_size - 4) + if raw.endswith(b"\0"): + raw = raw[:-1] + self.metadata_source = raw.decode("utf-8") + self.content_size = 4 + len(self._metadata_source_bytes()) + 1 + + class PRHDBox(box.Box): def __init__(self): box.Box.__init__(self) diff --git a/spatialmedia_test.py b/spatialmedia_test.py index cd39ce0..39fc189 100644 --- a/spatialmedia_test.py +++ b/spatialmedia_test.py @@ -67,6 +67,9 @@ def test_inject_v2_equirect_mono(self): 'data/testsrc_320x240_h264.mp4', f'{_OUTPUT_DIR}/equirect_mono.mp4']) self.assertTrue(contents.find('SV3D') >= 0) + self.assertTrue(contents.find('SVHD') >= 0) + self.assertTrue( + contents.find('Spherical Metadata Tool') >= 0) self.assertTrue(contents.find('PRHD') >= 0) self.assertTrue(contents.find('EQUI') >= 0) self.assertFalse(contents.find('ST3D') >= 0) @@ -83,6 +86,9 @@ def test_inject_v2_equirect_mono_with_bounds(self): 'data/testsrc_320x240_h264.mp4', f'{_OUTPUT_DIR}/equirect_mono.mp4']) self.assertTrue(contents.find('SV3D') >= 0) + self.assertTrue(contents.find('SVHD') >= 0) + self.assertTrue( + contents.find('Spherical Metadata Tool') >= 0) self.assertTrue(contents.find('PRHD') >= 0) self.assertTrue(contents.find('EQUI') >= 0) self.assertFalse(contents.find('ST3D') >= 0) @@ -98,6 +104,9 @@ def test_inject_v2_equirect_mono_vp9(self): f'{_OUTPUT_DIR}/equirect_mono_vp9.mp4']) self.assertTrue(contents.find('SV3D') >= 0) + self.assertTrue(contents.find('SVHD') >= 0) + self.assertTrue( + contents.find('Spherical Metadata Tool') >= 0) self.assertTrue(contents.find('PRHD') >= 0) self.assertTrue(contents.find('EQUI') >= 0) self.assertFalse(contents.find('ST3D') >= 0) @@ -108,6 +117,9 @@ def test_inject_v2_equirect_mono_prores(self): 'data/testsrc_32x24_prores.mov', f'{_OUTPUT_DIR}/equirect_mono_prores.mov']) self.assertTrue(contents.find('SV3D') >= 0) + self.assertTrue(contents.find('SVHD') >= 0) + self.assertTrue( + contents.find('Spherical Metadata Tool') >= 0) self.assertTrue(contents.find('PRHD') >= 0) self.assertTrue(contents.find('EQUI') >= 0) self.assertFalse(contents.find('ST3D') >= 0)