diff --git a/poetry.lock b/poetry.lock index 110903e..6eaa5a6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -33,15 +33,18 @@ files = [ [[package]] name = "construct" -version = "2.8.8" -description = "A powerful declarative parser/builder for binary data" +version = "2.10.68" +description = "A powerful declarative symmetric parser/builder for binary data" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" files = [ - {file = "construct-2.8.8.tar.gz", hash = "sha256:1b84b8147f6fd15bcf64b737c3e8ac5100811ad80c830cb4b2545140511c4157"}, + {file = "construct-2.10.68.tar.gz", hash = "sha256:7b2a3fd8e5f597a5aa1d614c3bd516fa065db01704c72a1efaaeec6ef23d8b45"}, ] +[package.extras] +extras = ["arrow", "cloudpickle", "enum34", "lz4", "numpy", "ruamel.yaml"] + [[package]] name = "coverage" version = "7.2.3" @@ -275,4 +278,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = ">=3.7,<4.0" -content-hash = "9a2baac6978e2ce64197980dda6b562347469f581c3881b1fe9d59432d47bc51" +content-hash = "fc4ee9dd83c675556d17d4c93f297cc1c9dc13f756645b08b15155585b2ee984" diff --git a/pyproject.toml b/pyproject.toml index e6b8594..539bc33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ [tool.poetry.dependencies] python = ">=3.7,<4.0" -construct = "2.8.8" +construct = "2.10.68" [tool.poetry.group.dev.dependencies] coverage = { version="^7.2.3", extras=["toml"] } diff --git a/src/pymp4/adapters.py b/src/pymp4/adapters.py new file mode 100644 index 0000000..7eda9a9 --- /dev/null +++ b/src/pymp4/adapters.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from abc import ABC +from uuid import UUID + +from construct import Adapter, int2byte + + +class ISO6392TLanguageCode(Adapter, ABC): + def _decode(self, obj, context, path): + return "".join([ + chr(bit + 0x60) + for bit in ( + (obj >> 10) & 0b11111, + (obj >> 5) & 0b11111, + obj & 0b11111 + ) + ]) + + def _encode(self, obj, context, path): + bits = [ord(c) - 0x60 for c in obj] + return (bits[0] << 10) | (bits[1] << 5) | bits[2] + + +class MaskedInteger(Adapter, ABC): + def _decode(self, obj, context, path): + return obj & 0x1F + + def _encode(self, obj, context, path): + return obj & 0x1F + + +class UUIDBytes(Adapter, ABC): + def _decode(self, obj, context, path): + return UUID(bytes=obj) + + def _encode(self, obj, context, path): + return obj.bytes diff --git a/src/pymp4/parser.py b/src/pymp4/parser.py index e3ab8b2..eaca852 100644 --- a/src/pymp4/parser.py +++ b/src/pymp4/parser.py @@ -18,127 +18,60 @@ from uuid import UUID from construct import * -import construct.core from construct.lib import * +from pymp4.adapters import ISO6392TLanguageCode, MaskedInteger, UUIDBytes +from pymp4.subconstructs import TellPlusSizeOf + log = logging.getLogger(__name__) UNITY_MATRIX = [0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000] -class PrefixedIncludingSize(Subconstruct): - __slots__ = ["name", "lengthfield", "subcon"] - - def __init__(self, lengthfield, subcon): - super(PrefixedIncludingSize, self).__init__(subcon) - self.lengthfield = lengthfield - - def _parse(self, stream, context, path): - try: - lengthfield_size = self.lengthfield.sizeof() - length = self.lengthfield._parse(stream, context, path) - except SizeofError: - offset_start = stream.tell() - length = self.lengthfield._parse(stream, context, path) - lengthfield_size = stream.tell() - offset_start - - stream2 = BoundBytesIO(stream, length - lengthfield_size) - obj = self.subcon._parse(stream2, context, path) - return obj - - def _build(self, obj, stream, context, path): - try: - # needs to be both fixed size, seekable and tellable (third not checked) - self.lengthfield.sizeof() - if not stream.seekable: - raise SizeofError - offset_start = stream.tell() - self.lengthfield._build(0, stream, context, path) - self.subcon._build(obj, stream, context, path) - offset_end = stream.tell() - stream.seek(offset_start) - self.lengthfield._build(offset_end - offset_start, stream, context, path) - stream.seek(offset_end) - except SizeofError: - data = self.subcon.build(obj, context) - sl, p_sl = 0, 0 - dlen = len(data) - # do..while - i = 0 - while True: - i += 1 - p_sl = sl - sl = len(self.lengthfield.build(dlen + sl)) - if p_sl == sl: break - - self.lengthfield._build(dlen + sl, stream, context, path) - else: - self.lengthfield._build(len(data), stream, context, path) - construct.core._write_stream(stream, len(data), data) - - def _sizeof(self, context, path): - return self.lengthfield._sizeof(context, path) + self.subcon._sizeof(context, path) - - # Header box FileTypeBox = Struct( - "type" / Const(b"ftyp"), - "major_brand" / String(4), + "major_brand" / PaddedString(4, "ascii"), "minor_version" / Int32ub, - "compatible_brands" / GreedyRange(String(4)), + "compatible_brands" / GreedyRange(PaddedString(4, "ascii")), ) SegmentTypeBox = Struct( - "type" / Const(b"styp"), - "major_brand" / String(4), + "major_brand" / PaddedString(4, "ascii"), "minor_version" / Int32ub, - "compatible_brands" / GreedyRange(String(4)), + "compatible_brands" / GreedyRange(PaddedString(4, "ascii")), ) # Catch find boxes RawBox = Struct( - "type" / String(4, padchar=b" ", paddir="right"), + "type" / PaddedString(4, "ascii"), "data" / Default(GreedyBytes, b"") ) FreeBox = Struct( - "type" / Const(b"free"), "data" / GreedyBytes ) SkipBox = Struct( - "type" / Const(b"skip"), "data" / GreedyBytes ) # Movie boxes, contained in a moov Box MovieHeaderBox = Struct( - "type" / Const(b"mvhd"), "version" / Default(Int8ub, 0), "flags" / Default(Int24ub, 0), - Embedded(Switch(this.version, { - 1: Struct( - "creation_time" / Default(Int64ub, 0), - "modification_time" / Default(Int64ub, 0), - "timescale" / Default(Int32ub, 10000000), - "duration" / Int64ub - ), - 0: Struct( - "creation_time" / Default(Int32ub, 0), - "modification_time" / Default(Int32ub, 0), - "timescale" / Default(Int32ub, 10000000), - "duration" / Int32ub, - ), - })), + "creation_time" / Default(Switch(this.version, {0: Int32ub, 1: Int64ub}), 0), + "modification_time" / Default(Switch(this.version, {0: Int32ub, 1: Int64ub}), 0), + "timescale" / Default(Int32ub, 10000000), + "duration" / Switch(this.version, {0: Int32ub, 1: Int64ub}), "rate" / Default(Int32sb, 65536), "volume" / Default(Int16sb, 256), # below could be just Padding(10) but why not - Const(Int16ub, 0), - Const(Int32ub, 0), - Const(Int32ub, 0), + Const(0, Int16ub), + Const(0, Int32ub), + Const(0, Int32ub), "matrix" / Default(Int32sb[9], UNITY_MATRIX), "pre_defined" / Default(Int32ub[6], [0] * 6), "next_track_ID" / Default(Int32ub, 0xffffffff) @@ -147,25 +80,13 @@ def _sizeof(self, context, path): # Track boxes, contained in trak box TrackHeaderBox = Struct( - "type" / Const(b"tkhd"), "version" / Default(Int8ub, 0), "flags" / Default(Int24ub, 1), - Embedded(Switch(this.version, { - 1: Struct( - "creation_time" / Default(Int64ub, 0), - "modification_time" / Default(Int64ub, 0), - "track_ID" / Default(Int32ub, 1), - Padding(4), - "duration" / Default(Int64ub, 0), - ), - 0: Struct( - "creation_time" / Default(Int32ub, 0), - "modification_time" / Default(Int32ub, 0), - "track_ID" / Default(Int32ub, 1), - Padding(4), - "duration" / Default(Int32ub, 0), - ), - })), + "creation_time" / Default(Switch(this.version, {0: Int32ub, 1: Int64ub}), 0), + "modification_time" / Default(Switch(this.version, {0: Int32ub, 1: Int64ub}), 0), + "track_ID" / Default(Int32ub, 1), + Padding(4), + "duration" / Default(Switch(this.version, {0: Int32ub, 1: Int64ub}), 0), Padding(8), "layer" / Default(Int16sb, 0), "alternate_group" / Default(Int16sb, 0), @@ -177,11 +98,10 @@ def _sizeof(self, context, path): ) HDSSegmentBox = Struct( - "type" / Const(b"abst"), "version" / Default(Int8ub, 0), "flags" / Default(Int24ub, 0), "info_version" / Int32ub, - EmbeddedBitStruct( + "flags" / BitStruct( Padding(1), "profile" / Flag, "live" / Flag, @@ -191,20 +111,19 @@ def _sizeof(self, context, path): "time_scale" / Int32ub, "current_media_time" / Int64ub, "smpte_time_code_offset" / Int64ub, - "movie_identifier" / CString(), - "server_entry_table" / PrefixedArray(Int8ub, CString()), - "quality_entry_table" / PrefixedArray(Int8ub, CString()), - "drm_data" / CString(), - "metadata" / CString(), + "movie_identifier" / CString("ascii"), + "server_entry_table" / PrefixedArray(Int8ub, CString("ascii")), + "quality_entry_table" / PrefixedArray(Int8ub, CString("ascii")), + "drm_data" / CString("ascii"), + "metadata" / CString("ascii"), "segment_run_table" / PrefixedArray(Int8ub, LazyBound(lambda x: Box)), "fragment_run_table" / PrefixedArray(Int8ub, LazyBound(lambda x: Box)) ) HDSSegmentRunBox = Struct( - "type" / Const(b"asrt"), "version" / Default(Int8ub, 0), "flags" / Default(Int24ub, 0), - "quality_entry_table" / PrefixedArray(Int8ub, CString()), + "quality_entry_table" / PrefixedArray(Int8ub, CString("ascii")), "segment_run_enteries" / PrefixedArray(Int32ub, Struct( "first_segment" / Int32ub, "fragments_per_segment" / Int32ub @@ -212,14 +131,13 @@ def _sizeof(self, context, path): ) HDSFragmentRunBox = Struct( - "type" / Const(b"afrt"), "version" / Default(Int8ub, 0), "flags" / BitStruct( Padding(23), "update" / Flag ), "time_scale" / Int32ub, - "quality_entry_table" / PrefixedArray(Int8ub, CString()), + "quality_entry_table" / PrefixedArray(Int8ub, CString("ascii")), "fragment_run_enteries" / PrefixedArray(Int32ub, Struct( "first_fragment" / Int32ub, "first_fragment_timestamp" / Int64ub, @@ -231,51 +149,31 @@ def _sizeof(self, context, path): # Boxes contained by Media Box -class ISO6392TLanguageCode(Adapter): - def _decode(self, obj, context): - """ - Get the python representation of the obj - """ - return b''.join(map(int2byte, [c + 0x60 for c in bytearray(obj)])).decode("utf8") - - def _encode(self, obj, context): - """ - Get the bytes representation of the obj - """ - return [c - 0x60 for c in bytearray(obj.encode("utf8"))] - - MediaHeaderBox = Struct( - "type" / Const(b"mdhd"), "version" / Default(Int8ub, 0), - "flags" / Const(Int24ub, 0), + "flags" / Const(0, Int24ub), "creation_time" / IfThenElse(this.version == 1, Int64ub, Int32ub), "modification_time" / IfThenElse(this.version == 1, Int64ub, Int32ub), "timescale" / Int32ub, "duration" / IfThenElse(this.version == 1, Int64ub, Int32ub), - Embedded(BitStruct( - Padding(1), - "language" / ISO6392TLanguageCode(BitsInteger(5)[3]), - )), - Padding(2, pattern=b"\x00"), + "language" / ISO6392TLanguageCode(Int16ub), + Padding(2, pattern=b"\x00") ) HandlerReferenceBox = Struct( - "type" / Const(b"hdlr"), - "version" / Const(Int8ub, 0), - "flags" / Const(Int24ub, 0), + "version" / Const(0, Int8ub), + "flags" / Const(0, Int24ub), Padding(4, pattern=b"\x00"), - "handler_type" / String(4), + "handler_type" / PaddedString(4, "ascii"), Padding(12, pattern=b"\x00"), # Int32ub[3] - "name" / CString(encoding="utf8") + "name" / CString("utf8") ) # Boxes contained by Media Info Box VideoMediaHeaderBox = Struct( - "type" / Const(b"vmhd"), "version" / Default(Int8ub, 0), - "flags" / Const(Int24ub, 1), + "flags" / Const(1, Int24ub), "graphics_mode" / Default(Int16ub, 0), "opcolor" / Struct( "red" / Default(Int16ub, 0), @@ -284,28 +182,25 @@ def _encode(self, obj, context): ), ) -DataEntryUrlBox = PrefixedIncludingSize(Int32ub, Struct( - "type" / Const(b"url "), - "version" / Const(Int8ub, 0), +DataEntryUrlBox = Prefixed(Int32ub, Struct( + "version" / Const(0, Int8ub), "flags" / BitStruct( Padding(23), "self_contained" / Rebuild(Flag, ~this._.location) ), - "location" / If(~this.flags.self_contained, CString(encoding="utf8")), -)) + "location" / If(~this.flags.self_contained, CString("utf8")), +), includelength=True) -DataEntryUrnBox = PrefixedIncludingSize(Int32ub, Struct( - "type" / Const(b"urn "), - "version" / Const(Int8ub, 0), +DataEntryUrnBox = Prefixed(Int32ub, Struct( + "version" / Const(0, Int8ub), "flags" / BitStruct( Padding(23), "self_contained" / Rebuild(Flag, ~(this._.name & this._.location)) ), - "name" / If(this.flags == 0, CString(encoding="utf8")), - "location" / If(this.flags == 0, CString(encoding="utf8")), -)) + "name" / If(this.flags == 0, CString("utf8")), + "location" / If(this.flags == 0, CString("utf8")), +), includelength=True) DataReferenceBox = Struct( - "type" / Const(b"dref"), - "version" / Const(Int8ub, 0), + "version" / Const(0, Int8ub), "flags" / Default(Int24ub, 0), "data_entries" / PrefixedArray(Int32ub, Select(DataEntryUrnBox, DataEntryUrlBox)), ) @@ -314,41 +209,32 @@ def _encode(self, obj, context): MP4ASampleEntryBox = Struct( "version" / Default(Int16ub, 0), - "revision" / Const(Int16ub, 0), - "vendor" / Const(Int32ub, 0), + "revision" / Const(0, Int16ub), + "vendor" / Const(0, Int32ub), "channels" / Default(Int16ub, 2), "bits_per_sample" / Default(Int16ub, 16), "compression_id" / Default(Int16sb, 0), - "packet_size" / Const(Int16ub, 0), + "packet_size" / Const(0, Int16ub), "sampling_rate" / Int16ub, Padding(2) ) - -class MaskedInteger(Adapter): - def _decode(self, obj, context): - return obj & 0x1F - - def _encode(self, obj, context): - return obj & 0x1F - - AAVC = Struct( - "version" / Const(Int8ub, 1), + "version" / Const(1, Int8ub), "profile" / Int8ub, "compatibility" / Int8ub, "level" / Int8ub, - EmbeddedBitStruct( + "flags" / BitStruct( Padding(6, pattern=b'\x01'), "nal_unit_length_field" / Default(BitsInteger(2), 3), ), - "sps" / Default(PrefixedArray(MaskedInteger(Int8ub), PascalString(Int16ub)), []), - "pps" / Default(PrefixedArray(Int8ub, PascalString(Int16ub)), []) + "sps" / Default(PrefixedArray(MaskedInteger(Int8ub), PascalString(Int16ub, "ascii")), []), + "pps" / Default(PrefixedArray(Int8ub, PascalString(Int16ub, "ascii")), []) ) HVCC = Struct( - EmbeddedBitStruct( - "version" / Const(BitsInteger(8), 1), + "version" / Const(1, Int8ub), + "flags" / BitStruct( "profile_space" / BitsInteger(2), "general_tier_flag" / BitsInteger(1), "general_profile" / BitsInteger(5), @@ -377,8 +263,8 @@ def _encode(self, obj, context): AVC1SampleEntryBox = Struct( "version" / Default(Int16ub, 0), - "revision" / Const(Int16ub, 0), - "vendor" / Default(String(4, padchar=b" "), b"brdy"), + "revision" / Const(0, Int16ub), + "vendor" / Default(PaddedString(4, "ascii"), "brdy"), "temporal_quality" / Default(Int32ub, 0), "spatial_quality" / Default(Int32ub, 0), "width" / Int16ub, @@ -387,62 +273,58 @@ def _encode(self, obj, context): Padding(2), "vertical_resolution" / Default(Int16ub, 72), # TODO: actually a fixed point decimal Padding(2), - "data_size" / Const(Int32ub, 0), + "data_size" / Const(0, Int32ub), "frame_count" / Default(Int16ub, 1), - "compressor_name" / Default(String(32, padchar=b" "), ""), + "compressor_name" / Default(PaddedString(32, "ascii"), None), "depth" / Default(Int16ub, 24), "color_table_id" / Default(Int16sb, -1), - "avc_data" / PrefixedIncludingSize(Int32ub, Struct( - "type" / String(4, padchar=b" ", paddir="right"), - Embedded(Switch(this.type, { - b"avcC": AAVC, - b"hvcC": HVCC, - }, Struct("data" / GreedyBytes))) - )), + "avc_data" / Prefixed(Int32ub, Struct( + "type" / PaddedString(4, "ascii"), + "data" / Switch(this.type, { + "avcC": AAVC, + "hvcC": HVCC, + }, GreedyBytes) + ), includelength=True), "sample_info" / LazyBound(lambda _: GreedyRange(Box)) ) -SampleEntryBox = PrefixedIncludingSize(Int32ub, Struct( - "format" / String(4, padchar=b" ", paddir="right"), +SampleEntryBox = Prefixed(Int32ub, Struct( + "format" / PaddedString(4, "ascii"), Padding(6, pattern=b"\x00"), "data_reference_index" / Default(Int16ub, 1), - Embedded(Switch(this.format, { - b"ec-3": MP4ASampleEntryBox, - b"mp4a": MP4ASampleEntryBox, - b"enca": MP4ASampleEntryBox, - b"avc1": AVC1SampleEntryBox, - b"encv": AVC1SampleEntryBox, - b"wvtt": Struct("children" / LazyBound(lambda ctx: GreedyRange(Box))) - }, Struct("data" / GreedyBytes))) -)) + "data" / Switch(this.format, { + "ec-3": MP4ASampleEntryBox, + "mp4a": MP4ASampleEntryBox, + "enca": MP4ASampleEntryBox, + "avc1": AVC1SampleEntryBox, + "encv": AVC1SampleEntryBox, + "wvtt": Struct("children" / LazyBound(lambda: GreedyRange(Box))) + }, GreedyBytes) +), includelength=True) BitRateBox = Struct( - "type" / Const(b"btrt"), "bufferSizeDB" / Int32ub, "maxBitrate" / Int32ub, "avgBirate" / Int32ub, ) SampleDescriptionBox = Struct( - "type" / Const(b"stsd"), "version" / Default(Int8ub, 0), - "flags" / Const(Int24ub, 0), + "flags" / Const(0, Int24ub), "entries" / PrefixedArray(Int32ub, SampleEntryBox) ) SampleSizeBox = Struct( - "type" / Const(b"stsz"), "version" / Int8ub, - "flags" / Const(Int24ub, 0), + "flags" / Const(0, Int24ub), "sample_size" / Int32ub, "sample_count" / Int32ub, "entry_sizes" / If(this.sample_size == 0, Array(this.sample_count, Int32ub)) ) SampleSizeBox2 = Struct( - "type" / Const(b"stz2"), "version" / Int8ub, - "flags" / Const(Int24ub, 0), + "flags" / Const(0, Int24ub), Padding(3, pattern=b"\x00"), "field_size" / Int8ub, "sample_count" / Int24ub, @@ -452,15 +334,13 @@ def _encode(self, obj, context): ) SampleDegradationPriorityBox = Struct( - "type" / Const(b"stdp"), - "version" / Const(Int8ub, 0), - "flags" / Const(Int24ub, 0), + "version" / Const(0, Int8ub), + "flags" / Const(0, Int24ub), ) TimeToSampleBox = Struct( - "type" / Const(b"stts"), - "version" / Const(Int8ub, 0), - "flags" / Const(Int24ub, 0), + "version" / Const(0, Int8ub), + "flags" / Const(0, Int24ub), "entries" / Default(PrefixedArray(Int32ub, Struct( "sample_count" / Int32ub, "sample_delta" / Int32ub, @@ -468,18 +348,16 @@ def _encode(self, obj, context): ) SyncSampleBox = Struct( - "type" / Const(b"stss"), - "version" / Const(Int8ub, 0), - "flags" / Const(Int24ub, 0), + "version" / Const(0, Int8ub), + "flags" / Const(0, Int24ub), "entries" / Default(PrefixedArray(Int32ub, Struct( "sample_number" / Int32ub, )), []) ) SampleToChunkBox = Struct( - "type" / Const(b"stsc"), - "version" / Const(Int8ub, 0), - "flags" / Const(Int24ub, 0), + "version" / Const(0, Int8ub), + "flags" / Const(0, Int24ub), "entries" / Default(PrefixedArray(Int32ub, Struct( "first_chunk" / Int32ub, "samples_per_chunk" / Int32ub, @@ -488,18 +366,16 @@ def _encode(self, obj, context): ) ChunkOffsetBox = Struct( - "type" / Const(b"stco"), - "version" / Const(Int8ub, 0), - "flags" / Const(Int24ub, 0), + "version" / Const(0, Int8ub), + "flags" / Const(0, Int24ub), "entries" / Default(PrefixedArray(Int32ub, Struct( "chunk_offset" / Int32ub, )), []) ) ChunkLargeOffsetBox = Struct( - "type" / Const(b"co64"), - "version" / Const(Int8ub, 0), - "flags" / Const(Int24ub, 0), + "version" / Const(0, Int8ub), + "flags" / Const(0, Int24ub), "entries" / PrefixedArray(Int32ub, Struct( "chunk_offset" / Int64ub, )) @@ -508,16 +384,14 @@ def _encode(self, obj, context): # Movie Fragment boxes, contained in moof box MovieFragmentHeaderBox = Struct( - "type" / Const(b"mfhd"), - "version" / Const(Int8ub, 0), - "flags" / Const(Int24ub, 0), + "version" / Const(0, Int8ub), + "flags" / Const(0, Int24ub), "sequence_number" / Int32ub ) TrackFragmentBaseMediaDecodeTimeBox = Struct( - "type" / Const(b"tfdt"), "version" / Int8ub, - "flags" / Const(Int24ub, 0), + "flags" / Const(0, Int24ub), "baseMediaDecodeTime" / Switch(this.version, {1: Int64ub, 0: Int32ub}) ) @@ -533,7 +407,6 @@ def _encode(self, obj, context): ) TrackRunBox = Struct( - "type" / Const(b"trun"), "version" / Int8ub, "flags" / BitStruct( Padding(12), @@ -561,7 +434,6 @@ def _encode(self, obj, context): ) TrackFragmentHeaderBox = Struct( - "type" / Const(b"tfhd"), "version" / Int8ub, "flags" / BitStruct( Padding(6), @@ -584,18 +456,16 @@ def _encode(self, obj, context): ) MovieExtendsHeaderBox = Struct( - "type" / Const(b"mehd"), "version" / Default(Int8ub, 0), - "flags" / Const(Int24ub, 0), + "flags" / Const(0, Int24ub), "fragment_duration" / IfThenElse(this.version == 1, Default(Int64ub, 0), Default(Int32ub, 0)) ) TrackExtendsBox = Struct( - "type" / Const(b"trex"), - "version" / Const(Int8ub, 0), - "flags" / Const(Int24ub, 0), + "version" / Const(0, Int8ub), + "flags" / Const(0, Int24ub), "track_ID" / Int32ub, "default_sample_description_index" / Default(Int32ub, 1), "default_sample_duration" / Default(Int32ub, 0), @@ -604,9 +474,8 @@ def _encode(self, obj, context): ) SegmentIndexBox = Struct( - "type" / Const(b"sidx"), "version" / Int8ub, - "flags" / Const(Int24ub, 0), + "flags" / Const(0, Int24ub), "reference_ID" / Int32ub, "timescale" / Int32ub, "earliest_presentation_time" / IfThenElse(this.version == 0, Int32ub, Int64ub), @@ -624,8 +493,7 @@ def _encode(self, obj, context): ) SampleAuxiliaryInformationSizesBox = Struct( - "type" / Const(b"saiz"), - "version" / Const(Int8ub, 0), + "version" / Const(0, Int8ub), "flags" / BitStruct( Padding(23), "has_aux_info_type" / Flag, @@ -641,7 +509,6 @@ def _encode(self, obj, context): ) SampleAuxiliaryInformationOffsetsBox = Struct( - "type" / Const(b"saio"), "version" / Int8ub, "flags" / BitStruct( Padding(23), @@ -657,35 +524,24 @@ def _encode(self, obj, context): # Movie data box MovieDataBox = Struct( - "type" / Const(b"mdat"), "data" / GreedyBytes ) # Media Info Box SoundMediaHeaderBox = Struct( - "type" / Const(b"smhd"), - "version" / Const(Int8ub, 0), - "flags" / Const(Int24ub, 0), + "version" / Const(0, Int8ub), + "flags" / Const(0, Int24ub), "balance" / Default(Int16sb, 0), - "reserved" / Const(Int16ub, 0) + "reserved" / Const(0, Int16ub) ) # DASH Boxes -class UUIDBytes(Adapter): - def _decode(self, obj, context): - return UUID(bytes=obj) - - def _encode(self, obj, context): - return obj.bytes - - ProtectionSystemHeaderBox = Struct( - "type" / If(this._.type != b"uuid", Const(b"pssh")), "version" / Rebuild(Int8ub, lambda ctx: 1 if (hasattr(ctx, "key_IDs") and ctx.key_IDs) else 0), - "flags" / Const(Int24ub, 0), + "flags" / Const(0, Int24ub), "system_ID" / UUIDBytes(Bytes(16)), "key_IDs" / Default(If(this.version == 1, PrefixedArray(Int32ub, UUIDBytes(Bytes(16)))), @@ -694,10 +550,9 @@ def _encode(self, obj, context): ) TrackEncryptionBox = Struct( - "type" / If(this._.type != b"uuid", Const(b"tenc")), "version" / Default(OneOf(Int8ub, (0, 1)), 0), "flags" / Default(Int24ub, 0), - "_reserved" / Const(Int8ub, 0), + "_reserved" / Const(0, Int8ub), "default_byte_blocks" / Default(IfThenElse( this.version > 0, BitStruct( @@ -706,7 +561,7 @@ def _encode(self, obj, context): # count of unencrypted blocks in the protection pattern "skip" / Nibble ), - Const(Int8ub, 0) + Const(0, Int8ub) ), 0), "is_encrypted" / OneOf(Int8ub, (0, 1)), "iv_size" / OneOf(Int8ub, (0, 8, 16)), @@ -718,8 +573,7 @@ def _encode(self, obj, context): ) SampleEncryptionBox = Struct( - "type" / If(this._.type != b"uuid", Const(b"senc")), - "version" / Const(Int8ub, 0), + "version" / Const(0, Int8ub), "flags" / BitStruct( Padding(22), "has_subsample_encryption_info" / Flag, @@ -736,21 +590,18 @@ def _encode(self, obj, context): ) OriginalFormatBox = Struct( - "type" / Const(b"frma"), - "original_format" / Default(String(4), b"avc1") + "original_format" / Default(PaddedString(4, "ascii"), "avc1") ) SchemeTypeBox = Struct( - "type" / Const(b"schm"), "version" / Default(Int8ub, 0), "flags" / Default(Int24ub, 0), - "scheme_type" / Default(String(4), b"cenc"), + "scheme_type" / Default(PaddedString(4, "ascii"), "cenc"), "scheme_version" / Default(Int32ub, 0x00010000), - "schema_uri" / Default(If(this.flags & 1 == 1, CString()), None) + "schema_uri" / Default(If(this.flags & 1 == 1, CString("ascii")), None) ) ProtectionSchemeInformationBox = Struct( - "type" / Const(b"sinf"), # TODO: define which children are required 'schm', 'schi' and 'tenc' "children" / LazyBound(lambda _: GreedyRange(Box)) ) @@ -758,7 +609,6 @@ def _encode(self, obj, context): # PIFF boxes UUIDBox = Struct( - "type" / Const(b"uuid"), "extended_type" / UUIDBytes(Bytes(16)), "data" / Switch(this.extended_type, { UUID("A2394F52-5A9B-4F14-A244-6C427C648DF4"): SampleEncryptionBox, @@ -770,119 +620,98 @@ def _encode(self, obj, context): # WebVTT boxes CueIDBox = Struct( - "type" / Const(b"iden"), "cue_id" / GreedyString("utf8") ) CueSettingsBox = Struct( - "type" / Const(b"sttg"), "settings" / GreedyString("utf8") ) CuePayloadBox = Struct( - "type" / Const(b"payl"), "cue_text" / GreedyString("utf8") ) WebVTTConfigurationBox = Struct( - "type" / Const(b"vttC"), "config" / GreedyString("utf8") ) WebVTTSourceLabelBox = Struct( - "type" / Const(b"vlab"), "label" / GreedyString("utf8") ) -ContainerBoxLazy = LazyBound(lambda ctx: ContainerBox) - - -class TellMinusSizeOf(Subconstruct): - def __init__(self, subcon): - super(TellMinusSizeOf, self).__init__(subcon) - self.flagbuildnone = True - - def _parse(self, stream, context, path): - return stream.tell() - self.subcon.sizeof(context) - - def _build(self, obj, stream, context, path): - return b"" - - def sizeof(self, context=None, **kw): - return 0 - - -Box = PrefixedIncludingSize(Int32ub, Struct( - "offset" / TellMinusSizeOf(Int32ub), - "type" / Peek(String(4, padchar=b" ", paddir="right")), - Embedded(Switch(this.type, { - b"ftyp": FileTypeBox, - b"styp": SegmentTypeBox, - b"mvhd": MovieHeaderBox, - b"moov": ContainerBoxLazy, - b"moof": ContainerBoxLazy, - b"mfhd": MovieFragmentHeaderBox, - b"tfdt": TrackFragmentBaseMediaDecodeTimeBox, - b"trun": TrackRunBox, - b"tfhd": TrackFragmentHeaderBox, - b"traf": ContainerBoxLazy, - b"mvex": ContainerBoxLazy, - b"mehd": MovieExtendsHeaderBox, - b"trex": TrackExtendsBox, - b"trak": ContainerBoxLazy, - b"mdia": ContainerBoxLazy, - b"tkhd": TrackHeaderBox, - b"mdat": MovieDataBox, - b"free": FreeBox, - b"skip": SkipBox, - b"mdhd": MediaHeaderBox, - b"hdlr": HandlerReferenceBox, - b"minf": ContainerBoxLazy, - b"vmhd": VideoMediaHeaderBox, - b"dinf": ContainerBoxLazy, - b"dref": DataReferenceBox, - b"stbl": ContainerBoxLazy, - b"stsd": SampleDescriptionBox, - b"stsz": SampleSizeBox, - b"stz2": SampleSizeBox2, - b"stts": TimeToSampleBox, - b"stss": SyncSampleBox, - b"stsc": SampleToChunkBox, - b"stco": ChunkOffsetBox, - b"co64": ChunkLargeOffsetBox, - b"smhd": SoundMediaHeaderBox, - b"sidx": SegmentIndexBox, - b"saiz": SampleAuxiliaryInformationSizesBox, - b"saio": SampleAuxiliaryInformationOffsetsBox, - b"btrt": BitRateBox, +ContainerBoxLazy = LazyBound(lambda: ContainerBox) + + +Box = Prefixed(Int32ub, Struct( + "offset" / Tell, + "type" / PaddedString(4, "ascii"), + "data" / Switch(this.type, { + "ftyp": FileTypeBox, + "styp": SegmentTypeBox, + "mvhd": MovieHeaderBox, + "moov": ContainerBoxLazy, + "moof": ContainerBoxLazy, + "mfhd": MovieFragmentHeaderBox, + "tfdt": TrackFragmentBaseMediaDecodeTimeBox, + "trun": TrackRunBox, + "tfhd": TrackFragmentHeaderBox, + "traf": ContainerBoxLazy, + "mvex": ContainerBoxLazy, + "mehd": MovieExtendsHeaderBox, + "trex": TrackExtendsBox, + "trak": ContainerBoxLazy, + "mdia": ContainerBoxLazy, + "tkhd": TrackHeaderBox, + "mdat": MovieDataBox, + "free": FreeBox, + "skip": SkipBox, + "mdhd": MediaHeaderBox, + "hdlr": HandlerReferenceBox, + "minf": ContainerBoxLazy, + "vmhd": VideoMediaHeaderBox, + "dinf": ContainerBoxLazy, + "dref": DataReferenceBox, + "stbl": ContainerBoxLazy, + "stsd": SampleDescriptionBox, + "stsz": SampleSizeBox, + "stz2": SampleSizeBox2, + "stts": TimeToSampleBox, + "stss": SyncSampleBox, + "stsc": SampleToChunkBox, + "stco": ChunkOffsetBox, + "co64": ChunkLargeOffsetBox, + "smhd": SoundMediaHeaderBox, + "sidx": SegmentIndexBox, + "saiz": SampleAuxiliaryInformationSizesBox, + "saio": SampleAuxiliaryInformationOffsetsBox, + "btrt": BitRateBox, # dash - b"tenc": TrackEncryptionBox, - b"pssh": ProtectionSystemHeaderBox, - b"senc": SampleEncryptionBox, - b"sinf": ProtectionSchemeInformationBox, - b"frma": OriginalFormatBox, - b"schm": SchemeTypeBox, - b"schi": ContainerBoxLazy, + "tenc": TrackEncryptionBox, + "pssh": ProtectionSystemHeaderBox, + "senc": SampleEncryptionBox, + "sinf": ProtectionSchemeInformationBox, + "frma": OriginalFormatBox, + "schm": SchemeTypeBox, + "schi": ContainerBoxLazy, # piff - b"uuid": UUIDBox, + "uuid": UUIDBox, # HDS boxes - b'abst': HDSSegmentBox, - b'asrt': HDSSegmentRunBox, - b'afrt': HDSFragmentRunBox, + "abst": HDSSegmentBox, + "asrt": HDSSegmentRunBox, + "afrt": HDSFragmentRunBox, # WebVTT - b"vttC": WebVTTConfigurationBox, - b"vlab": WebVTTSourceLabelBox, - b"vttc": ContainerBoxLazy, - b"vttx": ContainerBoxLazy, - b"iden": CueIDBox, - b"sttg": CueSettingsBox, - b"payl": CuePayloadBox - }, default=RawBox)), - "end" / Tell -)) + "vttC": WebVTTConfigurationBox, + "vlab": WebVTTSourceLabelBox, + "vttc": ContainerBoxLazy, + "vttx": ContainerBoxLazy, + "iden": CueIDBox, + "sttg": CueSettingsBox, + "payl": CuePayloadBox + }, default=RawBox), + "end" / TellPlusSizeOf(Int32ub) +), includelength=True) ContainerBox = Struct( - "type" / String(4, padchar=b" ", paddir="right"), "children" / GreedyRange(Box) ) diff --git a/src/pymp4/subconstructs.py b/src/pymp4/subconstructs.py new file mode 100644 index 0000000..8ed31d7 --- /dev/null +++ b/src/pymp4/subconstructs.py @@ -0,0 +1,18 @@ +from abc import ABC + +from construct import Subconstruct + + +class TellPlusSizeOf(Subconstruct, ABC): + def __init__(self, subcon): + super(TellPlusSizeOf, self).__init__(subcon) + self.flagbuildnone = True + + def _parse(self, stream, context, path): + return stream.tell() + self.subcon.sizeof(context=context) + + def _build(self, obj, stream, context, path): + return b"" + + def sizeof(self, context=None, **kw): + return 0 diff --git a/tests/test_box.py b/tests/test_box.py index 44937ed..39d1922 100644 --- a/tests/test_box.py +++ b/tests/test_box.py @@ -17,7 +17,7 @@ import logging import unittest -from construct import Container +from construct import Container, ListContainer from pymp4.parser import Box log = logging.getLogger(__name__) @@ -27,70 +27,86 @@ class BoxTests(unittest.TestCase): def test_ftyp_parse(self): self.assertEqual( Box.parse(b'\x00\x00\x00\x18ftypiso5\x00\x00\x00\x01iso5avc1'), - Container(offset=0) - (type=b"ftyp") - (major_brand=b"iso5") - (minor_version=1) - (compatible_brands=[b"iso5", b"avc1"]) - (end=24) + Container( + offset=0, + type="ftyp", + data=Container( + major_brand="iso5", + minor_version=1, + compatible_brands=["iso5", "avc1"] + ), + end=24 + ) ) def test_ftyp_build(self): self.assertEqual( Box.build(dict( - type=b"ftyp", - major_brand=b"iso5", - minor_version=1, - compatible_brands=[b"iso5", b"avc1"])), + type="ftyp", + data=dict( + major_brand="iso5", + minor_version=1, + compatible_brands=["iso5", "avc1"] + ) + )), b'\x00\x00\x00\x18ftypiso5\x00\x00\x00\x01iso5avc1') def test_mdhd_parse(self): self.assertEqual( - Box.parse( - b'\x00\x00\x00\x20mdhd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0fB@\x00\x00\x00\x00U\xc4\x00\x00'), - Container(offset=0) - (type=b"mdhd")(version=0)(flags=0) - (creation_time=0) - (modification_time=0) - (timescale=1000000) - (duration=0) - (language="und") - (end=32) + Box.parse(b'\x00\x00\x00\x20mdhd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0fB@\x00\x00\x00\x00U\xc4\x00\x00'), + Container( + offset=0, + type="mdhd", + data=Container( + version=0, + flags=0, + creation_time=0, + modification_time=0, + timescale=1000000, + duration=0, + language="und" + ), + end=32 + ) ) def test_mdhd_build(self): mdhd_data = Box.build(dict( - type=b"mdhd", - creation_time=0, - modification_time=0, - timescale=1000000, - duration=0, - language=u"und")) + type="mdhd", + data=dict( + creation_time=0, + modification_time=0, + timescale=1000000, + duration=0, + language="und" + ))) self.assertEqual(len(mdhd_data), 32) self.assertEqual(mdhd_data, b'\x00\x00\x00\x20mdhd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0fB@\x00\x00\x00\x00U\xc4\x00\x00') mdhd_data64 = Box.build(dict( - type=b"mdhd", - version=1, - creation_time=0, - modification_time=0, - timescale=1000000, - duration=0, - language=u"und")) + type="mdhd", + data=dict( + version=1, + creation_time=0, + modification_time=0, + timescale=1000000, + duration=0, + language="und" + ))) self.assertEqual(len(mdhd_data64), 44) self.assertEqual(mdhd_data64, b'\x00\x00\x00,mdhd\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0fB@\x00\x00\x00\x00\x00\x00\x00\x00U\xc4\x00\x00') def test_moov_build(self): moov = \ - Container(type=b"moov")(children=[ # 96 bytes - Container(type=b"mvex")(children=[ # 88 bytes - Container(type=b"mehd")(version=0)(flags=0)(fragment_duration=0), # 16 bytes - Container(type=b"trex")(track_ID=1), # 32 bytes - Container(type=b"trex")(track_ID=2), # 32 bytes - ]) - ]) + Container(type="moov", data=Container(children=ListContainer([ # 96 bytes + Container(type="mvex", data=Container(children=ListContainer([ # 88 bytes + Container(type="mehd", data=Container(version=0, flags=0, fragment_duration=0)), # 16 bytes + Container(type="trex", data=Container(track_ID=1)), # 32 bytes + Container(type="trex", data=Container(track_ID=2)), # 32 bytes + ]))) + ]))) moov_data = Box.build(moov) @@ -108,15 +124,25 @@ def test_smhd_parse(self): in_bytes = b'\x00\x00\x00\x10smhd\x00\x00\x00\x00\x00\x00\x00\x00' self.assertEqual( Box.parse(in_bytes + b'padding'), - Container(offset=0) - (type=b"smhd")(version=0)(flags=0) - (balance=0)(reserved=0)(end=len(in_bytes)) + Container( + offset=0, + type="smhd", + data=Container( + version=0, + flags=0, + balance=0, + reserved=0 + ), + end=len(in_bytes) + ) ) def test_smhd_build(self): smhd_data = Box.build(dict( - type=b"smhd", - balance=0)) + type="smhd", + data=dict( + balance=0 + ))) self.assertEqual(len(smhd_data), 16), self.assertEqual(smhd_data, b'\x00\x00\x00\x10smhd\x00\x00\x00\x00\x00\x00\x00\x00') @@ -125,8 +151,20 @@ def test_stsd_parse(self): in_bytes = b'\x00\x00\x00\x50stsd\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x40tx3g\x00\x00\x00\x00\x00\x00\x00\x01' + tx3g_data self.assertEqual( Box.parse(in_bytes + b'padding'), - Container(offset=0) - (type=b"stsd")(version=0)(flags=0) - (entries=[Container(format=b'tx3g')(data_reference_index=1)(data=tx3g_data)]) - (end=len(in_bytes)) + Container( + offset=0, + type="stsd", + data=Container( + version=0, + flags=0, + entries=ListContainer([ + Container( + format="tx3g", + data_reference_index=1, + data=tx3g_data + ) + ]) + ), + end=len(in_bytes) + ) ) diff --git a/tests/test_dashboxes.py b/tests/test_dashboxes.py index 3500619..c2cc004 100644 --- a/tests/test_dashboxes.py +++ b/tests/test_dashboxes.py @@ -28,24 +28,31 @@ class BoxTests(unittest.TestCase): def test_tenc_parse(self): self.assertEqual( Box.parse(b'\x00\x00\x00 tenc\x00\x00\x00\x00\x00\x00\x01\x083{\x96C!\xb6CU\x9eY>\xcc\xb4l~\xf7'), - Container(offset=0) - (type=b"tenc") - (version=0) - (flags=0) - (_reserved=0) - (default_byte_blocks=0) - (is_encrypted=1) - (iv_size=8) - (key_ID=UUID('337b9643-21b6-4355-9e59-3eccb46c7ef7')) - (constant_iv=None) - (end=32) + Container( + offset=0, + type="tenc", + data=Container( + version=0, + flags=0, + _reserved=0, + default_byte_blocks=0, + is_encrypted=1, + iv_size=8, + key_ID=UUID('337b9643-21b6-4355-9e59-3eccb46c7ef7'), + constant_iv=None + ), + end=32 + ) ) def test_tenc_build(self): self.assertEqual( Box.build(dict( - type=b"tenc", - key_ID=UUID('337b9643-21b6-4355-9e59-3eccb46c7ef7'), - iv_size=8, - is_encrypted=1)), + type="tenc", + data=dict( + key_ID=UUID('337b9643-21b6-4355-9e59-3eccb46c7ef7'), + iv_size=8, + is_encrypted=1 + ) + )), b'\x00\x00\x00 tenc\x00\x00\x00\x00\x00\x00\x01\x083{\x96C!\xb6CU\x9eY>\xcc\xb4l~\xf7') diff --git a/tests/test_util.py b/tests/test_util.py index e0f7fbb..0e45347 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -17,7 +17,7 @@ import logging import unittest -from construct import Container +from construct import Container, ListContainer from pymp4.exceptions import BoxNotFound from pymp4.util import BoxUtil @@ -26,62 +26,79 @@ class BoxTests(unittest.TestCase): - box_data = Container(type=b"demo")(children=[ - Container(type=b"a ")(id=1), - Container(type=b"b ")(id=2), - Container(type=b"c ")(children=[ - Container(type=b"a ")(id=3), - Container(type=b"b ")(id=4), - ]), - Container(type=b"d ")(id=5), + box_data = Container( + type="demo", + children=ListContainer([ + Container(type="a ", id=1), + Container(type="b ", id=2), + Container( + type="c ", + children=ListContainer([ + Container(type="a ", id=3), + Container(type="b ", id=4) + ]) + ), + Container(type="d ", id=5) ]) - - box_extended_data = Container(type=b"test")(children=[ - Container(type=b"a ")(id=1, extended_type=b"e--a"), - Container(type=b"b ")(id=2, extended_type=b"e--b"), - ]) + ) + + box_extended_data = Container( + type="test", + children=ListContainer([ + Container( + type="a ", + id=1, + extended_type=b"e--a" + ), + Container( + type="b ", + id=2, + extended_type=b"e--b" + ) + ]) + ) def test_find(self): self.assertListEqual( - list(BoxUtil.find(self.box_data, b"b ")), - [Container(type=b"b ")(id=2), Container(type=b"b ")(id=4)] + list(BoxUtil.find(self.box_data, "b ")), + [Container(type="b ", id=2), Container(type="b ", id=4)] ) def test_find_after_nest(self): self.assertListEqual( - list(BoxUtil.find(self.box_data, b"d ")), - [Container(type=b"d ")(id=5)] + list(BoxUtil.find(self.box_data, "d ")), + [Container(type="d ", id=5)] ) def test_find_nested_type(self): self.assertListEqual( - list(BoxUtil.find(self.box_data, b"c ")), - [Container(type=b"c ")(children=[ - Container(type=b"a ")(id=3), - Container(type=b"b ")(id=4), - ])] + list(BoxUtil.find(self.box_data, "c ")), + [Container(type="c ", children=ListContainer([ + Container(type="a ", id=3), + Container(type="b ", id=4), + ]))] ) def test_find_empty(self): self.assertListEqual( - list(BoxUtil.find(self.box_data, b"f ")), + list(BoxUtil.find(self.box_data, "f ")), [] ) def test_first(self): self.assertEqual( - BoxUtil.first(self.box_data, b"b "), - Container(type=b"b ")(id=2) + BoxUtil.first(self.box_data, "b "), + Container(type="b ", id=2) ) def test_first_missing(self): self.assertRaises( BoxNotFound, - BoxUtil.first, self.box_data, b"f ", + BoxUtil.first, self.box_data, "f ", ) def test_find_extended(self): self.assertListEqual( list(BoxUtil.find_extended(self.box_extended_data, b"e--a")), - [Container(type=b"a ")(id=1, extended_type=b"e--a")] + [Container(type="a ", id=1, extended_type=b"e--a")] ) diff --git a/tests/test_webvtt_boxes.py b/tests/test_webvtt_boxes.py index 84f7db3..e322c2c 100644 --- a/tests/test_webvtt_boxes.py +++ b/tests/test_webvtt_boxes.py @@ -12,79 +12,109 @@ class BoxTests(unittest.TestCase): def test_iden_parse(self): self.assertEqual( Box.parse(b'\x00\x00\x00\x27iden2 - this is the second subtitle'), - Container(offset=0) - (type=b"iden") - (cue_id="2 - this is the second subtitle") - (end=39) + Container( + offset=0, + type="iden", + data=Container( + cue_id="2 - this is the second subtitle" + ), + end=39 + ) ) def test_iden_build(self): self.assertEqual( Box.build(dict( - type=b"iden", - cue_id="1 - first subtitle")), + type="iden", + data=dict( + cue_id="1 - first subtitle" + ))), b'\x00\x00\x00\x1aiden1 - first subtitle') def test_sttg_parse(self): self.assertEqual( Box.parse(b'\x00\x00\x003sttgline:10% position:50% size:48% align:center'), - Container(offset=0) - (type=b"sttg") - (settings="line:10% position:50% size:48% align:center") - (end=51) + Container( + offset=0, + type="sttg", + data=Container( + settings="line:10% position:50% size:48% align:center" + ), + end=51 + ) ) def test_sttg_build(self): self.assertEqual( Box.build(dict( - type=b"sttg", - settings="line:75% position:20% size:2em align:right")), + type="sttg", + data=dict( + settings="line:75% position:20% size:2em align:right" + ))), b'\x00\x00\x002sttgline:75% position:20% size:2em align:right') def test_payl_parse(self): self.assertEqual( Box.parse(b'\x00\x00\x00\x13payl[chuckling]'), - Container(offset=0) - (type=b"payl") - (cue_text="[chuckling]") - (end=19) + Container( + offset=0, + type="payl", + data=Container( + cue_text="[chuckling]" + ), + end=19 + ) ) def test_payl_build(self): self.assertEqual( Box.build(dict( - type=b"payl", - cue_text="I have a bad feeling about- [boom]")), + type="payl", + data=dict( + cue_text="I have a bad feeling about- [boom]" + ))), b'\x00\x00\x00*paylI have a bad feeling about- [boom]') def test_vttC_parse(self): self.assertEqual( Box.parse(b'\x00\x00\x00\x0evttCWEBVTT'), - Container(offset=0) - (type=b"vttC") - (config="WEBVTT") - (end=14) + Container( + offset=0, + type="vttC", + data=Container( + config="WEBVTT" + ), + end=14 + ) ) def test_vttC_build(self): self.assertEqual( Box.build(dict( - type=b"vttC", - config="WEBVTT with a text header\n\nSTYLE\n::cue {\ncolor: red;\n}")), + type="vttC", + data=dict( + config="WEBVTT with a text header\n\nSTYLE\n::cue {\ncolor: red;\n}" + ))), b'\x00\x00\x00>vttCWEBVTT with a text header\n\nSTYLE\n::cue {\ncolor: red;\n}') def test_vlab_parse(self): self.assertEqual( Box.parse(b'\x00\x00\x00\x14vlabsource_label'), - Container(offset=0) - (type=b"vlab") - (label="source_label") - (end=20) + Container( + offset=0, + type="vlab", + data=Container( + label="source_label" + ), + end=20 + ) ) def test_vlab_build(self): self.assertEqual( Box.build(dict( - type=b"vlab", - label="1234 \n test_label \n\n")), + type="vlab", + data=dict( + label="1234 \n test_label \n\n" + ))), b'\x00\x00\x00\x1cvlab1234 \n test_label \n\n')