Skip to content

Commit eba3bb4

Browse files
committed
Also add initial support for v2 metadata (--v2). Currently supported options:
* stereo_mode = {none | top-bottom | left-right} * projection = {none | equirectangular }
1 parent 008d63e commit eba3bb4

File tree

5 files changed

+366
-12
lines changed

5 files changed

+366
-12
lines changed

spatialmedia/__main__.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ def main():
4848
help=
4949
"injects spatial media metadata into the first file specified (.mp4 or "
5050
".mov) and saves the result to the second file specified")
51+
parser.add_argument(
52+
"-2",
53+
"--v2",
54+
action="store_true",
55+
help=
56+
"Uses v2 of the video metadata spec")
5157
video_group = parser.add_argument_group("Spherical Video")
5258
video_group.add_argument("-s",
5359
"--stereo",
@@ -91,10 +97,13 @@ def main():
9197
console("Injecting metadata requires both an input file and output file.")
9298
return
9399

94-
metadata = metadata_utils.Metadata()
95-
metadata.video = metadata_utils.generate_spherical_xml(args.projection,
96-
args.stereo_mode,
97-
args.crop)
100+
metadata = metadata_utils.Metadata(args.projection, args.stereo_mode)
101+
if not args.v2:
102+
metadata.projection = None
103+
metadata.stereo_mode = None
104+
metadata.video = metadata_utils.generate_spherical_xml(args.projection,
105+
args.stereo_mode,
106+
args.crop)
98107

99108
if args.spatial_audio:
100109
parsed_metadata = metadata_utils.parse_metadata(args.file[0], console)
@@ -110,7 +119,7 @@ def main():
110119
"spatial audio format." % (parsed_metadata.num_audio_channels))
111120
return
112121

113-
if metadata.video:
122+
if metadata.video or metadata.projection or metadata.stereo_mode:
114123
metadata_utils.inject_metadata(args.file[0], args.file[1], metadata,
115124
console)
116125
else:

spatialmedia/metadata_utils.py

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@
9595
]
9696

9797
class Metadata(object):
98-
def __init__(self):
98+
def __init__(self, projection=None, stereo_mode=None):
99+
self.projection = None if (not projection or projection == "none") else projection
100+
self.stereo_mode = None if (not stereo_mode or stereo_mode == "none") else stereo_mode
99101
self.video = None
100102
self.audio = None
101103

@@ -152,7 +154,7 @@ def spherical_uuid(metadata):
152154
return uuid_leaf
153155

154156

155-
def mpeg4_add_spherical(mpeg4_file, in_fh, metadata):
157+
def mpeg4_add_spherical_xml_v1(mpeg4_file, in_fh, metadata):
156158
"""Adds a spherical uuid box to an mpeg4 file for all video tracks.
157159
158160
Args:
@@ -184,6 +186,72 @@ def mpeg4_add_spherical(mpeg4_file, in_fh, metadata):
184186
mpeg4_file.resize()
185187
return True
186188

189+
def mpeg4_add_spherical_v2(mpeg4_file, in_fh, projection, stereo_mode):
190+
for element in mpeg4_file.moov_box.contents:
191+
if element.name == mpeg.constants.TAG_TRAK:
192+
for sub_element in element.contents:
193+
if sub_element.name != mpeg.constants.TAG_MDIA:
194+
continue
195+
for mdia_sub_element in sub_element.contents:
196+
if mdia_sub_element.name != mpeg.constants.TAG_HDLR:
197+
continue
198+
position = mdia_sub_element.content_start() + 8
199+
in_fh.seek(position)
200+
if in_fh.read(4) == mpeg.constants.TAG_VIDE:
201+
ret = inject_spatial_video_v2_atoms(
202+
in_fh, sub_element, projection, stereo_mode)
203+
mpeg4_file.resize()
204+
return ret
205+
206+
207+
def inject_spatial_video_v2_atoms(in_fh, video_media_atom, projection, stereo_mode):
208+
"""Adds spherical v2 boxes to an mpeg4 file for all video tracks.
209+
210+
Args:
211+
mpeg4_file: mpeg4, Mpeg4 file structure to add metadata.
212+
in_fh: file handle, Source for uncached file contents.
213+
metadata: string, xml metadata to inject into spherical tag.
214+
"""
215+
for atom in video_media_atom.contents:
216+
if atom.name != mpeg.constants.TAG_MINF:
217+
continue
218+
for element in atom.contents:
219+
if element.name != mpeg.constants.TAG_STBL:
220+
continue
221+
for sub_element in element.contents:
222+
if sub_element.name != mpeg.constants.TAG_STSD:
223+
continue
224+
for sample_description in sub_element.contents:
225+
if sample_description.name in\
226+
mpeg.constants.VIDEO_SAMPLE_DESCRIPTIONS:
227+
in_fh.seek(sample_description.position +
228+
sample_description.header_size + 16)
229+
# Should remove any existing boxes...
230+
if stereo_mode:
231+
st3d_atom = mpeg.sv3d.ST3DBox.create()
232+
st3d_atom.name = mpeg.constants.TAG_ST3D
233+
st3d_atom.set_stereo_mode_from_string(stereo_mode)
234+
235+
sample_description.remove(st3d_atom.name)
236+
sample_description.add(st3d_atom)
237+
238+
if projection:
239+
proj_atom = mpeg.container.Container(header_size=8)
240+
proj_atom.name = mpeg.constants.TAG_PROJ
241+
242+
proj_atom.add(mpeg.sv3d.PRHDBox.create())
243+
proj_atom.add(mpeg.sv3d.EQUIBox.create())
244+
245+
sv3d_atom = mpeg.container.Container(header_size=8)
246+
sv3d_atom.name = mpeg.constants.TAG_SV3D
247+
248+
sv3d_atom.add(proj_atom)
249+
250+
sample_description.remove(sv3d_atom.name)
251+
sample_description.add(sv3d_atom)
252+
253+
return True
254+
187255

188256
def mpeg4_add_spatial_audio(mpeg4_file, in_fh, audio_metadata, console):
189257
"""Adds spatial audio metadata to the first audio track of the input
@@ -355,6 +423,21 @@ def parse_spherical_mpeg4(mpeg4_file, fh, console):
355423
if sa3d_elem.name == mpeg.constants.TAG_SA3D:
356424
sa3d_elem.print_box(console)
357425
metadata.audio = sa3d_elem
426+
427+
for sv3d_container_elem in stsd_elem.contents:
428+
if sv3d_container_elem.name not in \
429+
mpeg.constants.VIDEO_SAMPLE_DESCRIPTIONS:
430+
continue
431+
for sub_elem in sv3d_container_elem.contents:
432+
if sub_elem.name == mpeg.constants.TAG_SV3D:
433+
console("\t\tSV3D {")
434+
sub_elem.print_box(console)
435+
console("\t\t}")
436+
elif sub_elem.name == mpeg.constants.TAG_ST3D:
437+
console("\t\tST3D {")
438+
sub_elem.print_box(console)
439+
console("\t\t} ")
440+
358441
return metadata
359442

360443
def parse_mpeg4(input_file, console):
@@ -378,9 +461,13 @@ def inject_mpeg4(input_file, output_file, metadata, console):
378461
if mpeg4_file is None:
379462
console("Error file could not be opened.")
380463

381-
if not mpeg4_add_spherical(mpeg4_file, in_fh, metadata.video):
464+
if metadata.video and not mpeg4_add_spherical_xml_v1(mpeg4_file, in_fh, metadata.video):
382465
console("Error failed to insert spherical data")
383466

467+
if ((metadata.projection or metadata.stereo_mode)
468+
and not mpeg4_add_spherical_v2(mpeg4_file, in_fh, metadata.projection, metadata.stereo_mode)):
469+
console("Error failed to insert spherical data v2")
470+
384471
if metadata.audio:
385472
if not mpeg4_add_audio_metadata(
386473
mpeg4_file, in_fh, metadata.audio, console):

spatialmedia/mpeg/constants.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,14 @@
2929
TAG_FTYP = b"ftyp"
3030
TAG_ESDS = b"esds"
3131
TAG_SOUN = b"soun"
32+
TAG_VIDE = b"vide"
3233
TAG_SA3D = b"SA3D"
3334

35+
TAG_PRHD = b"prhd"
36+
TAG_EQUI = b"equi"
37+
TAG_SVHD = b"svhd"
38+
TAG_ST3D = b"st3d"
39+
3440
# Container types.
3541
TAG_MOOV = b"moov"
3642
TAG_UDTA = b"udta"
@@ -43,6 +49,10 @@
4349
TAG_UUID = b"uuid"
4450
TAG_WAVE = b"wave"
4551

52+
TAG_SV3D = b"sv3d"
53+
TAG_PROJ = b"proj"
54+
55+
4656
# Sound sample descriptions.
4757
TAG_NONE = b"NONE"
4858
TAG_RAW_ = b"raw "
@@ -58,6 +68,14 @@
5868
TAG_MP4A = b"mp4a"
5969
TAG_OPUS = b"Opus"
6070

71+
# Video sample descriptions.
72+
TAG_AVC1 = b"avc1"
73+
TAG_VP09 = b"vp09"
74+
TAG_AV01 = b"av01"
75+
TAV_HEV1 = b"hev1"
76+
TAG_DVH1 = b"dvh1"
77+
TAG_APCN = b"apcn"
78+
6179
SOUND_SAMPLE_DESCRIPTIONS = frozenset([
6280
TAG_NONE,
6381
TAG_RAW_,
@@ -74,6 +92,16 @@
7492
TAG_OPUS,
7593
])
7694

95+
VIDEO_SAMPLE_DESCRIPTIONS = frozenset([
96+
TAG_NONE,
97+
TAG_AVC1,
98+
TAG_VP09,
99+
TAG_AV01,
100+
TAV_HEV1,
101+
TAG_DVH1,
102+
TAG_APCN,
103+
])
104+
77105
CONTAINERS_LIST = frozenset([
78106
TAG_MDIA,
79107
TAG_MINF,
@@ -83,4 +111,6 @@
83111
TAG_TRAK,
84112
TAG_UDTA,
85113
TAG_WAVE,
86-
]).union(SOUND_SAMPLE_DESCRIPTIONS)
114+
TAG_SV3D,
115+
TAG_PROJ
116+
]).union(SOUND_SAMPLE_DESCRIPTIONS).union(VIDEO_SAMPLE_DESCRIPTIONS)

spatialmedia/mpeg/container.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from spatialmedia.mpeg import box
2626
from spatialmedia.mpeg import constants
2727
from spatialmedia.mpeg import sa3d
28+
from spatialmedia.mpeg import sv3d
2829

2930
def load(fh, position, end):
3031
if position is None:
@@ -34,14 +35,15 @@ def load(fh, position, end):
3435
header_size = 8
3536
size = struct.unpack(">I", fh.read(4))[0]
3637
name = fh.read(4)
37-
3838
is_box = name not in constants.CONTAINERS_LIST
3939
# Handle the mp4a decompressor setting (wave -> mp4a).
4040
if name == constants.TAG_MP4A and size == 12:
4141
is_box = True
4242
if is_box:
4343
if name == constants.TAG_SA3D:
4444
return sa3d.load(fh, position, end)
45+
if sv3d.is_supported_box_name(name):
46+
return sv3d.load(fh, position, end)
4547
return box.load(fh, position, end)
4648

4749
if size == 1:
@@ -74,6 +76,17 @@ def load(fh, position, end):
7476
else:
7577
print("Unsupported sample description version:",
7678
sample_description_version)
79+
if name in constants.VIDEO_SAMPLE_DESCRIPTIONS:
80+
current_pos = fh.tell()
81+
fh.seek(current_pos + 8)
82+
sample_description_version = struct.unpack(">h", fh.read(2))[0]
83+
fh.seek(current_pos)
84+
85+
if sample_description_version == 0:
86+
padding = 78
87+
else:
88+
print("Unsupported video sample description version:",
89+
sample_description_version)
7790

7891
new_box = Container()
7992
new_box.name = name
@@ -106,10 +119,10 @@ def load_multiple(fh, position=None, end=None):
106119
class Container(box.Box):
107120
"""MPEG4 container box contents / behaviour."""
108121

109-
def __init__(self, padding=0):
122+
def __init__(self, padding=0, header_size=0):
110123
self.name = ""
111124
self.position = 0
112-
self.header_size = 0
125+
self.header_size = header_size
113126
self.content_size = 0
114127
self.contents = list()
115128
self.padding = padding
@@ -122,6 +135,10 @@ def resize(self):
122135
element.resize()
123136
self.content_size += element.size()
124137

138+
def print_box(self, console):
139+
for child in self.contents:
140+
child.print_box(console)
141+
125142
def print_structure(self, indent=""):
126143
"""Prints the box structure and recurses on contents."""
127144
size1 = self.header_size

0 commit comments

Comments
 (0)