Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make is_key_frame and temporal_offset optional #150

Merged
merged 2 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added examples/coded_video_9.gsf
Binary file not shown.
14 changes: 9 additions & 5 deletions gsf_docs/gsf.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Grain Sequence Format

**Version 8.0**
**Version 9.0**

A Grain Sequence Format (GSF) file contains a sequence of grains from one or more flows. It has the mimetype application/x-ips-gsf and a filename typically uses the suffix `.gsf`.

The GSF file uses version **2.0** of the [SSB format](ssb.md) that defines the base file structure and data types.
The GSF file uses version **2.1** of the [SSB format](ssb.md) that defines the base file structure and data types.


## General File Structure
Expand All @@ -15,10 +15,10 @@ Each file begins with a 12 octet [SSB header](ssb.md#general-file-structure):
|---------------|------------|----------|----------|
| signature | "SSBB" | Tag | 4 octets |
| file_type | "grsg" | Tag | 4 octets |
| major_version | 0x0008 | Unsigned | 2 octets |
| major_version | 0x0009 | Unsigned | 2 octets |
| minor_version | 0x0000 | Unsigned | 2 octets |

The current GSF version is 8.0. See the [SSB Versioning ](ssb.md#versioning) section for a description of how versioning works from a reader's perspective.
The current GSF version is 9.0. See the [SSB Versioning ](ssb.md#versioning) section for a description of how versioning works from a reader's perspective.

Every GSF file starts with a single [head](#head-block) block, which itself contains other types of blocks, followed by a (possibly empty) sequence of [grai](#grai-block) blocks and finally a [grai](#grai-block) terminator block.

Expand Down Expand Up @@ -359,7 +359,11 @@ The *format* and *layout* parameters are enumerated values as defined in [cogenu
| UNKNOWN | 0xfffffffe |
| INVALID | 0xffffffff |

The *layouts* are the same as those described in the [vghd](#vghd-block) block. The *origin_width* and *origin_height* are the original frame dimensions that were input to the encoder and is the output of the decoder after applying any clipping. The *coded_width* and *coded_height* are the frame dimensions used to encode from, eg. including padding to meet the fixed macroblock size requirement. The *key_frame* is set to true if the video frame is a key frame, eg. an I-frame. The *temporal_offset* is the offset between display and stored order for inter-frame coding schemes (offset = display - stored).
The *layouts* are the same as those described in the [vghd](#vghd-block) block. The *origin_width* and *origin_height* are the original frame dimensions that were input to the encoder and is the output of the decoder after applying any clipping. The *coded_width* and *coded_height* are the frame dimensions used to encode from, eg. including padding to meet the fixed macroblock size requirement.

The *key_frame* is set to 1 if the video frame is a key frame, eg. an I-frame, or 0 if it is not a key frame. A value >= 2 indicates that the *key_frame* value is unknown.

The *temporal_offset* is the offset between display and stored order for inter-frame coding schemes (offset = display - stored). A value 2147483647 (0x7fffffff) indicates the *temporal_offset* value is unknown.

The [cghd](#cghd-block) block is followed by an optional [unof](#unof-block) block (with any other blocks in-between).

Expand Down
3 changes: 2 additions & 1 deletion gsf_docs/ssb.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Sequence Store Binary Format

**Version 2.0**
**Version 2.1**

The Sequence Store Binary (SSB) format is a basic format for binary encoding data for storage or transfer. It is the basis for the storage segment data files as well as the [Grain Sequence Format (GSF)](gsf.md) used for external storage and transfer. A SSB file would typically use the filename suffix `.ssb`, but some file types would have their own preferred suffix, eg. `.gsf` for GSF.

Expand All @@ -20,6 +20,7 @@ A number of data types are defined for the SSB format structures and the data it
| Unsigned | | 1 to 8 | An unsigned integer |
| Signed | | 1 to 8 | A two's-complement signed integer |
| Boolean | | 1 | 0 is false and 1 is true. Readers must treat a non-0 as true |
| | | | unless the property definition states otherwise. |
| Rational | | 8 | An unsigned rational number. A null value is where the |
| | | | Numerator equals 0. Readers need to be aware that the |
| | | | Denominator can also be 0 and treat it as null or invalid. |
Expand Down
2 changes: 1 addition & 1 deletion mediagrains/comparison/_internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ def compare_coded_video_grains(self, a: CodedVideoGrain, b: CodedVideoGrain, chi
'cog_frame_layout']:
path = self._identifier + '.' + key
children[key] = EqualityComparisonResult(
path, getattr(a, key), getattr(b, key), options=self._options, attr=key)
path, getattr(a, key, None), getattr(b, key, None), options=self._options, attr=key)

for key in ['unit_offsets']:
path = self._identifier + '.' + key
Expand Down
46 changes: 28 additions & 18 deletions mediagrains/grains/CodedVideoGrain.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class CodedVideoGrain(Grain):
The coded video height in pixels

temporal_offset
A signed integer value indicating the offset from the origin timestamp of
An optional signed integer value indicating the offset from the origin timestamp of
this grain to the expected presentation time of the picture in frames.

unit_offsets
Expand All @@ -119,8 +119,8 @@ def __init__(self,
origin_height: int = 1080,
coded_width: Optional[int] = None,
coded_height: Optional[int] = None,
is_key_frame: bool = False,
temporal_offset: int = 0,
is_key_frame: Optional[bool] = None,
temporal_offset: Optional[int] = None,
length: Optional[int] = None,
cog_frame_layout: CogFrameLayout = CogFrameLayout.UNKNOWN,
unit_offsets: Optional[List[int]] = None):
Expand Down Expand Up @@ -170,12 +170,14 @@ def __init__(self,
"origin_height": origin_height,
"coded_width": coded_width,
"coded_height": coded_height,
"layout": cog_frame_layout,
"is_key_frame": is_key_frame,
"temporal_offset": temporal_offset
"layout": cog_frame_layout
}
},
}
if is_key_frame is not None:
meta["grain"]["cog_coded_frame"]["is_key_frame"] = is_key_frame
if temporal_offset is not None:
meta["grain"]["cog_coded_frame"]["temporal_offset"] = temporal_offset

if data is None:
data = bytearray(length)
Expand All @@ -202,12 +204,8 @@ def __init__(self,
self.meta['grain']['cog_coded_frame']['coded_width'] = 0
if 'coded_height' not in self.meta['grain']['cog_coded_frame']:
self.meta['grain']['cog_coded_frame']['coded_height'] = 0
if 'temporal_offset' not in self.meta['grain']['cog_coded_frame']:
self.meta['grain']['cog_coded_frame']['temporal_offset'] = 0
if 'length' not in self.meta['grain']['cog_coded_frame']:
self.meta['grain']['cog_coded_frame']['length'] = 0
if 'is_key_frame' not in self.meta['grain']['cog_coded_frame']:
self.meta['grain']['cog_coded_frame']['is_key_frame'] = False
self.meta['grain']['cog_coded_frame']['format'] = int(self.meta['grain']['cog_coded_frame']['format'])
self.meta['grain']['cog_coded_frame']['layout'] = int(self.meta['grain']['cog_coded_frame']['layout'])

Expand Down Expand Up @@ -276,20 +274,32 @@ def coded_height(self, value: int) -> None:
self.meta['grain']['cog_coded_frame']['coded_height'] = value

@property
def is_key_frame(self) -> bool:
return self.meta['grain']['cog_coded_frame']['is_key_frame']
def is_key_frame(self) -> bool | None:
return self.meta['grain']['cog_coded_frame'].get('is_key_frame')

@is_key_frame.setter
def is_key_frame(self, value: bool) -> None:
self.meta['grain']['cog_coded_frame']['is_key_frame'] = bool(value)
def is_key_frame(self, value: bool | None) -> None:
if value is not None:
self.meta['grain']['cog_coded_frame']['is_key_frame'] = bool(value)
else:
try:
del self.meta['grain']['cog_coded_frame']['is_key_frame']
except KeyError:
pass

@property
def temporal_offset(self) -> int:
return self.meta['grain']['cog_coded_frame']['temporal_offset']
def temporal_offset(self) -> int | None:
return self.meta['grain']['cog_coded_frame'].get('temporal_offset')

@temporal_offset.setter
def temporal_offset(self, value: int) -> None:
self.meta['grain']['cog_coded_frame']['temporal_offset'] = value
def temporal_offset(self, value: int | None) -> None:
if value is not None:
self.meta['grain']['cog_coded_frame']['temporal_offset'] = value
else:
try:
del self.meta['grain']['cog_coded_frame']['temporal_offset']
except KeyError:
pass

@property
def source_aspect_ratio(self) -> Optional[Fraction]:
Expand Down
52 changes: 42 additions & 10 deletions mediagrains/gsf.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,17 @@ def read_bool(self):
n = self.read_uint(1)
return (n != 0)

def read_optional_bool(self):
"""Read an optional boolean value

:returns: Boolean | None value
:raises EOFError: If there are no more bytes left in the source"""
n = self.read_uint(1)
if n > 1:
return None
else:
return (n != 0)

def read_sint(self, length: int) -> int:
"""Read a 2's complement signed integer

Expand All @@ -598,6 +609,21 @@ def read_sint(self, length: int) -> int:
r -= (1 << (8*length))
return r

def read_optional_sint(self, length: int, null_value: int) -> int | None:
"""Read an optional 2's complement signed integer

:param length: Number of bytes used to store the integer
:param null_value: Value that indicates it is null
:returns: Signed integer
:raises EOFError: If there are fewer than `length` bytes left in the source
"""
r = self.read_uint(length)
if r == null_value:
return None
if (r >> ((8*length) - 1)) == 1:
r -= (1 << (8*length))
return r

def read_string(self, length: int) -> str:
"""Read a fixed-length string, treating it as UTF-8

Expand Down Expand Up @@ -896,8 +922,16 @@ def _sync_decode_gbhd(self, gbhd_block: SyncGSFBlock) -> GrainMetadataDict:
meta['grain']['cog_coded_frame']['origin_height'] = gbhd_child.read_uint(4)
meta['grain']['cog_coded_frame']['coded_width'] = gbhd_child.read_uint(4)
meta['grain']['cog_coded_frame']['coded_height'] = gbhd_child.read_uint(4)
meta['grain']['cog_coded_frame']['is_key_frame'] = gbhd_child.read_bool()
meta['grain']['cog_coded_frame']['temporal_offset'] = gbhd_child.read_sint(4)
if self.major < 9:
meta['grain']['cog_coded_frame']['is_key_frame'] = gbhd_child.read_bool()
meta['grain']['cog_coded_frame']['temporal_offset'] = gbhd_child.read_sint(4)
else:
is_key_frame = gbhd_child.read_optional_bool()
if is_key_frame is not None:
meta['grain']['cog_coded_frame']['is_key_frame'] = is_key_frame
temporal_offset = gbhd_child.read_optional_sint(4, 0x7fffffff)
if temporal_offset is not None:
meta['grain']['cog_coded_frame']['temporal_offset'] = temporal_offset

for unof_block in gbhd_child.child_blocks():
if unof_block.tag != 'unof':
Expand Down Expand Up @@ -979,7 +1013,7 @@ async def _decode_file_headers(self) -> None:
:raises GSFDecodeError: If the file doesn't have a "head" block
"""
(self.major, self.minor) = await self._decode_ssb_header()
if self.major not in [7, 8]:
if self.major not in [7, 8, 9]:
raise GSFDecodeBadVersionError(f"Unknown Version {self.major}.{self.minor}", 0, self.major, self.minor)

try:
Expand Down Expand Up @@ -1136,7 +1170,7 @@ def _decode_file_headers(self) -> None:
:raises GSFDecodeError: If the file doesn't have a "head" block
"""
(self.major, self.minor) = self._decode_ssb_header()
if self.major not in [7, 8]:
if self.major not in [7, 8, 9]:
raise GSFDecodeBadVersionError(f"Unknown Version {self.major}.{self.minor}", 0, self.major, self.minor)

try:
Expand Down Expand Up @@ -1770,12 +1804,10 @@ class GSFEncoder(object):
tags -- a tuple of tags
segments -- a frozendict of GSFEncoderSegments

The current version of the library is designed for compatibility with v.8.0 of the GSF format. Setting a
different version number will simply change the reported version number in the file, but will not alter the
syntax at all. If future versions of this code add support for other versions of GSF then this will change."""
The current version of the library is designed for compatibility with v.9.0 of the GSF format."""
def __init__(self,
file: Union[IO[bytes], AsyncBinaryIO, OpenAsyncBinaryIO],
major: int = 8,
major: int = 9,
minor: int = 0,
id: Optional[UUID] = None,
created: Optional[datetime] = None,
Expand Down Expand Up @@ -2281,8 +2313,8 @@ def _encode_cghd_for_grain(self, grain: CodedVideoGrain) -> bytes:
_encode_uint(int(grain.origin_height), 4) +
_encode_uint(int(grain.coded_width), 4) +
_encode_uint(int(grain.coded_height), 4) +
_encode_uint(1 if grain.is_key_frame else 0, 1) +
_encode_uint(int(grain.temporal_offset), 4))
_encode_uint(int(grain.is_key_frame) if grain.is_key_frame is not None else 3, 1) +
_encode_sint(int(grain.temporal_offset) if grain.temporal_offset is not None else 0x7fffffff, 4))

if len(grain.unit_offsets) > 0:
data += (b"unof" +
Expand Down
11 changes: 10 additions & 1 deletion mediagrains/tools/extract_from_gsf.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ def gsf_probe():
parser.add_argument("input_file", help="Input file. Specify - for stdin", type=str)
parser.add_argument("-a", "--all-timestamps", help="Print all the grain timestamps in the file",
action="store_true")
parser.add_argument("-g", "--all-grains", help="Print all the grains in the file",
action="store_true")

args = parser.parse_args()

Expand All @@ -70,12 +72,19 @@ def gsf_probe():
for grain, local_id in decoder.grains(load_lazily=True):
this_segment = file_data["segments"][local_id]

if args.all_grains:
try:
this_segment["grain_data"].append(grain.meta["grain"])
except KeyError:
this_segment["grain_data"] = [grain.meta["grain"]]

try:
this_segment["timerange"] = \
this_segment["timerange"].extend_to_encompass_timerange(grain.origin_timerange())
except KeyError:
this_segment["timerange"] = grain.origin_timerange()
this_segment["grain_data"] = grain.meta["grain"]
if not args.all_grains:
this_segment["grain_data"] = grain.meta["grain"]

if args.all_timestamps:
grain_ts_data = {
Expand Down
4 changes: 2 additions & 2 deletions mediagrains/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,11 @@ class _GrainGrainMetadataDict_cogcodedframe_MANDATORY (TypedDict):
coded_width: int
coded_height: int
layout: Union[int, CogFrameLayout]
is_key_frame: bool
temporal_offset: int


class _GrainGrainMetadataDict_cogcodedframe (_GrainGrainMetadataDict_cogcodedframe_MANDATORY, total=False):
is_key_frame: bool
temporal_offset: int
unit_offsets: Sequence[int]
length: int
source_aspect_ratio: Union[FractionDict, Fraction, RationalTypes]
Expand Down
6 changes: 3 additions & 3 deletions mediagrains/utils/h264_grain_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,11 @@ def grains(self) -> typing.Iterator[CodedVideoGrain]:

new_grain = copy.deepcopy(self.template_grain)
new_grain.origin_timestamp = norm_origin_ts
if frame_info is not None and frame_info.key_frame is not None:
if frame_info is not None:
new_grain.is_key_frame = frame_info.key_frame
else:
new_grain.is_key_frame = False
new_grain.temporal_offset = 0 # Not parsed
new_grain.is_key_frame = None
new_grain.temporal_offset = None # Not parsed
new_grain.unit_offsets = unit_offsets # type: ignore
new_grain.data = frame_data

Expand Down
Loading
Loading