def test_picture_not_allowed_to_change_between_fragments(self): fh1 = serialise_to_bytes( bitstream.FragmentHeader( fragment_slice_count=1, picture_number=1000, )) fh2 = serialise_to_bytes( bitstream.FragmentHeader( fragment_slice_count=1, picture_number=1001, )) state = bytes_to_state(fh1 + fh2) state["_last_picture_number"] = 1000 state["_last_picture_number_offset"] = (-1, 4) state["_picture_initial_fragment_offset"] = (-1, 0) state[ "picture_coding_mode"] = tables.PictureCodingModes.pictures_are_frames state["_num_pictures_in_sequence"] = 0 state["_fragment_slices_remaining"] = 1 state["fragment_slices_received"] = 0 state["slices_x"] = 1 state["slices_y"] = 1 decoder.fragment_header(state) with pytest.raises( decoder.PictureNumberChangedMidFragmentedPicture) as exc_info: decoder.fragment_header(state) assert exc_info.value.last_picture_number_offset == (-1, 4) assert exc_info.value.picture_number_offset == (len(fh1) + 4, 7) assert exc_info.value.last_picture_number == 1000 assert exc_info.value.picture_number == 1001
def test_parse_code_version_restriction(self): # A sequence with 1 HQ picture fragment but (incorrectly) major_version 2 seq = bitstream.Sequence(data_units=[ bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.sequence_header, ), sequence_header=bitstream.SequenceHeader( parse_parameters=bitstream.ParseParameters(major_version=2, ), video_parameters=bitstream.SourceParameters( frame_size=bitstream.FrameSize( # Don't waste time on full-sized frames custom_dimensions_flag=True, frame_width=4, frame_height=4, ), clean_area=bitstream.CleanArea( custom_clean_area_flag=True, clean_width=4, clean_height=4, ), ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( fragment_slice_count=0, ), transform_parameters=bitstream.TransformParameters( slice_parameters=bitstream.SliceParameters( slices_x=1, slices_y=1, ), ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( fragment_slice_count=1, ), ), ), bitstream.DataUnit(parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.end_of_sequence, ), ), ]) populate_parse_offsets(seq) state = bytes_to_state(serialise_to_bytes(seq)) with pytest.raises(decoder.ParseCodeNotSupportedByVersion): decoder.parse_stream(state)
def test_first_fragment_must_have_slice_count_zero( self, fragment_slice_count, fragment_slices_remaining, exp_fail, ): state = bytes_to_state( serialise_to_bytes( bitstream.FragmentHeader( fragment_slice_count=fragment_slice_count, ))) state["_picture_initial_fragment_offset"] = (-1, 7) state["fragment_slices_received"] = 0 state["_fragment_slices_remaining"] = fragment_slices_remaining # Required only when not failing state["_num_pictures_in_sequence"] = 0 state[ "picture_coding_mode"] = tables.PictureCodingModes.pictures_are_frames if exp_fail: with pytest.raises(decoder.FragmentedPictureRestarted) as exc_info: decoder.fragment_header(state) assert exc_info.value.initial_fragment_offset == (-1, 7) assert exc_info.value.this_fragment_offset == (0, 7) assert exc_info.value.fragment_slices_received == 0 assert exc_info.value.fragment_slices_remaining == fragment_slices_remaining else: decoder.fragment_header(state)
def test_must_not_have_too_many_slices( self, fragment_slice_count, fragment_slices_remaining, exp_fail, ): state = bytes_to_state( serialise_to_bytes( bitstream.FragmentHeader( fragment_slice_count=fragment_slice_count, picture_number=0, ))) state["_picture_initial_fragment_offset"] = (-1, 7) state["_fragment_slices_remaining"] = fragment_slices_remaining state["fragment_slices_received"] = 0 state["_last_picture_number"] = 0 # Only required in non-failing cases state["slices_x"] = 1 state["slices_y"] = 1 if exp_fail: with pytest.raises( decoder.TooManySlicesInFragmentedPicture) as exc_info: decoder.fragment_header(state) assert exc_info.value.initial_fragment_offset == (-1, 7) assert exc_info.value.this_fragment_offset == (0, 7) assert exc_info.value.fragment_slices_received == 0 assert exc_info.value.fragment_slices_remaining == fragment_slices_remaining assert exc_info.value.fragment_slice_count == fragment_slice_count else: decoder.fragment_header(state)
def test_picture_numbering_sanity_check(self): # Only a sanity check as assert_picture_number_incremented_as_expected # (which performs these checks) is tested fully elsewhere fh1 = serialise_to_bytes(bitstream.FragmentHeader(picture_number=1000)) fh2 = serialise_to_bytes(bitstream.FragmentHeader(picture_number=1001)) fh3 = serialise_to_bytes(bitstream.FragmentHeader(picture_number=1003)) state = bytes_to_state(fh1 + fh2 + fh3) state[ "picture_coding_mode"] = tables.PictureCodingModes.pictures_are_frames state["_num_pictures_in_sequence"] = 0 state["_fragment_slices_remaining"] = 0 decoder.fragment_header(state) decoder.fragment_header(state) with pytest.raises(decoder.NonConsecutivePictureNumbers) as exc_info: decoder.fragment_header(state) assert exc_info.value.last_picture_number_offset == (len(fh1) + 4, 7) assert exc_info.value.picture_number_offset == (len(fh1) + len(fh2) + 4, 7) assert exc_info.value.last_picture_number == 1001 assert exc_info.value.picture_number == 1003
def test_must_have_contiguous_slices( self, fragment_x_offset, fragment_y_offset, expected_fragment_x_offset, expected_fragment_y_offset, exp_fail, ): state = bytes_to_state( serialise_to_bytes( bitstream.FragmentHeader( picture_number=0, fragment_slice_count=1, fragment_x_offset=fragment_x_offset, fragment_y_offset=fragment_y_offset, ))) state["_picture_initial_fragment_offset"] = (-1, 7) state["_fragment_slices_remaining"] = 1 state["fragment_slices_received"] = (expected_fragment_y_offset * 10 + expected_fragment_x_offset) state["_last_picture_number"] = 0 state["slices_x"] = 10 state["slices_y"] = 10 if exp_fail: with pytest.raises( decoder.FragmentSlicesNotContiguous) as exc_info: decoder.fragment_header(state) assert exc_info.value.initial_fragment_offset == (-1, 7) assert exc_info.value.this_fragment_offset == (0, 7) assert exc_info.value.fragment_x_offset == fragment_x_offset assert exc_info.value.fragment_y_offset == fragment_y_offset assert (exc_info.value.expected_fragment_x_offset == expected_fragment_x_offset) assert (exc_info.value.expected_fragment_y_offset == expected_fragment_y_offset) else: decoder.fragment_header(state)
def test_whole_picture(parse_code, fragment_slice_counts): # A sanity check which runs fragmented picture decoding for whole pictures # and makes sure nothing crashes # Serialise a sample stream sh = bitstream.SequenceHeader( video_parameters=bitstream.SourceParameters( frame_size=bitstream.FrameSize( # Don't waste time on full-sized frames custom_dimensions_flag=True, frame_width=8, frame_height=8, ), clean_area=bitstream.CleanArea( custom_clean_area_flag=True, clean_width=8, clean_height=8, ), ), ) serialisation_state = State() sh_bytes = serialise_to_bytes(sh, serialisation_state) serialisation_state["parse_code"] = parse_code frag_bytes = b"" # Add the first (header) fragment in the picture frag_bytes += serialise_to_bytes( bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader(fragment_slice_count=0, ), transform_parameters=bitstream.TransformParameters( slice_parameters=bitstream.SliceParameters( slices_x=3, slices_y=2, )), ), serialisation_state, ) # Add the slice-containing fragments num_slices = 0 for fragment_slice_count in fragment_slice_counts: x = num_slices % 3 y = num_slices // 3 num_slices += fragment_slice_count frag_bytes += serialise_to_bytes( bitstream.FragmentParse(fragment_header=bitstream.FragmentHeader( fragment_slice_count=fragment_slice_count, fragment_x_offset=x, fragment_y_offset=y, ), ), serialisation_state, ) # Check it is parsed without failiures state = bytes_to_state(sh_bytes + frag_bytes) state["_num_pictures_in_sequence"] = 0 state["_fragment_slices_remaining"] = 0 decoder.sequence_header(state) # Parse header fragment decoder.byte_align(state) state["parse_code"] = parse_code decoder.fragment_parse(state) # Parse slice-containing fragments num_slices = 0 for fragment_slice_count in fragment_slice_counts: assert state["fragmented_picture_done"] is False decoder.byte_align(state) state["parse_code"] = parse_code decoder.fragment_parse(state) num_slices += fragment_slice_count assert state["fragment_slices_received"] == num_slices assert state["_fragment_slices_remaining"] == 6 - num_slices assert state["fragmented_picture_done"] is True
def test_output_picture(self): # This test adds a callback for output_picture and makes sure that both # fragments and pictures call it correctly (and that sanity-checks very # loosely that decoding etc. is happening). Finally, it also checks # that two concatenated sequences are read one after another. # A sequence with a HQ picture followed by a HQ fragment (both all # zeros) seq1 = bitstream.Sequence(data_units=[ bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.sequence_header, ), sequence_header=bitstream.SequenceHeader( video_parameters=bitstream.SourceParameters( frame_size=bitstream.FrameSize( # Don't waste time on full-sized frames custom_dimensions_flag=True, frame_width=4, frame_height=2, ), clean_area=bitstream.CleanArea( custom_clean_area_flag=True, clean_width=4, clean_height=2, ), color_diff_sampling_format=bitstream. ColorDiffSamplingFormat( custom_color_diff_format_flag=True, color_diff_format_index=tables. ColorDifferenceSamplingFormats. color_4_2_2, # noqa: E501 ), # Output values will be treated as 8-bit (and thus all # decode to 128) signal_range=bitstream.SignalRange( custom_signal_range_flag=True, index=tables.PresetSignalRanges. video_8bit_full_range, ), ), picture_coding_mode=tables.PictureCodingModes. pictures_are_frames, ), ), # A HQ picture bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture, ), picture_parse=bitstream.PictureParse( picture_header=bitstream.PictureHeader(picture_number=10, ), ), ), # A fragmented HQ picture (sent over two fragments to ensure the # callback only fires after a whole picture arrives) bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( picture_number=11, fragment_slice_count=0, ), transform_parameters=bitstream.TransformParameters( slice_parameters=bitstream.SliceParameters( slices_x=2, slices_y=1, ), ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( picture_number=11, fragment_slice_count=1, fragment_x_offset=0, fragment_y_offset=0, ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( picture_number=11, fragment_slice_count=1, fragment_x_offset=1, fragment_y_offset=0, ), ), ), bitstream.DataUnit(parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.end_of_sequence, ), ), ]) # Another (HQ) picture in a separate sequence seq2 = bitstream.Sequence(data_units=[ bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.sequence_header, ), sequence_header=bitstream.SequenceHeader( parse_parameters=bitstream.ParseParameters( major_version=2), video_parameters=bitstream.SourceParameters( frame_size=bitstream.FrameSize( # Don't waste time on full-sized frames custom_dimensions_flag=True, frame_width=4, frame_height=2, ), clean_area=bitstream.CleanArea( custom_clean_area_flag=True, clean_width=4, clean_height=2, ), color_diff_sampling_format=bitstream. ColorDiffSamplingFormat( custom_color_diff_format_flag=True, color_diff_format_index=tables. ColorDifferenceSamplingFormats. color_4_2_2, # noqa: E501 ), # Output values will be treated as 8-bit (and thus all # decode to 128) signal_range=bitstream.SignalRange( custom_signal_range_flag=True, index=tables.PresetSignalRanges. video_8bit_full_range, ), ), picture_coding_mode=tables.PictureCodingModes. pictures_are_frames, ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture, ), picture_parse=bitstream.PictureParse( picture_header=bitstream.PictureHeader( picture_number=12), ), ), bitstream.DataUnit(parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.end_of_sequence, ), ), ]) populate_parse_offsets(seq1) populate_parse_offsets(seq2) state = bytes_to_state( serialise_to_bytes(seq1) + serialise_to_bytes(seq2)) state["_output_picture_callback"] = Mock() decoder.parse_stream(state) assert state["_output_picture_callback"].call_count == 3 for i, (args, kwargs) in enumerate( state["_output_picture_callback"].call_args_list): assert kwargs == {} # Should get a 4x2 mid-gray frame with 4:2:2 color difference sampling assert args[0] == { "pic_num": 10 + i, "Y": [[128, 128, 128, 128], [128, 128, 128, 128]], "C1": [[128, 128], [128, 128]], "C2": [[128, 128], [128, 128]], } # Just sanity check the second argument looks like a set of video parameters assert args[1]["frame_width"] == 4 assert args[1]["frame_height"] == 2 assert args[1]["luma_offset"] == 0 assert args[1]["luma_offset"] == 0 assert args[1]["luma_excursion"] == 255 assert args[1]["color_diff_offset"] == 128 assert args[1]["color_diff_excursion"] == 255 # And the picture coding mode too... assert args[2] == tables.PictureCodingModes.pictures_are_frames
def test_picture_and_incomplete_fragment_interleaving_disallowed( self, num_slices_to_send, exp_fail): # A sequence with a 3x2 slice picture fragment with num_slices_to_send slices in # it followed by an HQ picture seq = bitstream.Sequence(data_units=[ bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.sequence_header, ), sequence_header=bitstream.SequenceHeader( video_parameters=bitstream.SourceParameters( frame_size=bitstream.FrameSize( # Don't waste time on full-sized pictures custom_dimensions_flag=True, frame_width=8, frame_height=8, ), clean_area=bitstream.CleanArea( custom_clean_area_flag=True, clean_width=8, clean_height=8, ), ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( picture_number=0, fragment_slice_count=0, ), transform_parameters=bitstream.TransformParameters( slice_parameters=bitstream.SliceParameters( slices_x=3, slices_y=2, ), ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture_fragment, ), fragment_parse=bitstream.FragmentParse( fragment_header=bitstream.FragmentHeader( picture_number=0, fragment_slice_count=num_slices_to_send, ), ), ), bitstream.DataUnit( parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.high_quality_picture, ), picture_parse=bitstream.PictureParse( picture_header=bitstream.PictureHeader(picture_number=1, ), ), ), bitstream.DataUnit(parse_info=bitstream.ParseInfo( parse_code=tables.ParseCodes.end_of_sequence, ), ), ]) # Don't include second (non-header) fragment if sending no slices if num_slices_to_send == 0: del seq["data_units"][2] populate_parse_offsets(seq) state = bytes_to_state(serialise_to_bytes(seq)) if exp_fail: with pytest.raises(decoder.PictureInterleavedWithFragmentedPicture ) as exc_info: decoder.parse_stream(state) first_fragment_offset = ( seq["data_units"][0]["parse_info"]["next_parse_offset"] + tables.PARSE_INFO_HEADER_BYTES) assert exc_info.value.initial_fragment_offset == ( first_fragment_offset, 7) picture_offset = ( sum(seq["data_units"][i]["parse_info"]["next_parse_offset"] for i in range(len(seq["data_units"]) - 2)) + tables.PARSE_INFO_HEADER_BYTES) assert exc_info.value.this_offset == (picture_offset, 7) assert exc_info.value.fragment_slices_received == num_slices_to_send assert exc_info.value.fragment_slices_remaining == 6 - num_slices_to_send else: decoder.parse_stream(state)