<?php
/**
* SeekQuarry/Yioop --
* Open Source Pure PHP Search Engine, Crawler, and Indexer
*
* Copyright (C) 2009 - 2026 Chris Pollett chris@pollett.org
*
* LICENSE:
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* END LICENSE
*
* @author Chris Pollett chris@pollett.org
* @license https://www.gnu.org/licenses/ GPL3
* @link https://www.seekquarry.com/
* @copyright 2009 - 2026
* @filesource
*
* This file reads the settings an HEVC stream carries. A container hands
* them over before any frame is read.
*/
namespace seekquarry\yioop\library\av_processing;
/**
* HevcException raised when an H.265 stream cannot be read, either because it
* is damaged or because it uses a feature this decoder does not carry.
*/
class HevcException extends VideoException
{
}
/**
* HevcNal splits an H.265 stream into its coded units. The unit header is two
* bytes rather than the one H.264 uses, and the emulation prevention bytes are
* removed the same way.
*/
final class HevcNal
{
/**
* unitsFromStream splits a start coded stream into its units.
*
* @param string $stream bytes holding one or more start coded units
* @return array each unit as its type, layer, temporal id and payload
*/
public static function unitsFromStream(string $stream): array
{
$written = [];
$total = strlen($stream);
$position = 0;
while ($position + 3 <= $total) {
$start = strpos($stream, "\x00\x00\x01", $position);
if ($start === false) {
break;
}
$start += 3;
$next = strpos($stream, "\x00\x00\x01", $start);
$finish = $next === false ? $total : $next;
$tail = $finish;
while ($tail > $start && $stream[$tail - 1] === "\x00") {
$tail--;
}
if ($tail - $start >= 2) {
$first = ord($stream[$start]);
$second = ord($stream[$start + 1]);
$written[] = [
'type' => ($first >> 1) & 0x3F,
'layer' => (($first & 1) << 5) | ($second >> 3),
'temporal' => ($second & 7) - 1,
'rbsp' => H264Bits::unescape(
substr($stream, $start + 2, $tail - $start - 2)),
];
}
$position = $finish;
}
return $written;
}
}
/**
* HevcSps a sequence parameter set: the picture size, sample depth, block sizes
* and filter settings that hold for a run of pictures.
*/
final class HevcSps
{
/**
* $video_settings_id stores which video parameter set this refers to.
* @var int
*/
public int $video_settings_id = 0;
/**
* $chroma_format_setting stores how the chroma planes are sampled, 1
* meaning
* half in each direction.
* @var int
*/
public int $chroma_format_setting = 1;
/**
* $separate_color_plane stores true when the three planes are coded as
* separate pictures.
* @var bool
*/
public bool $separate_color_plane = false;
/**
* $frame_width stores width of the coded picture in samples.
* @var int
*/
public int $frame_width = 0;
/**
* $frame_height stores height of the coded picture in samples.
* @var int
*/
public int $frame_height = 0;
/**
* $crop_left stores samples cut from the left of the picture before it is
* shown.
* @var int
*/
public int $crop_left = 0;
/**
* $crop_right stores samples cut from the right of the picture before it is
* shown.
* @var int
*/
public int $crop_right = 0;
/**
* $crop_top stores samples cut from the top of the picture before it is
* shown.
* @var int
*/
public int $crop_top = 0;
/**
* $crop_bottom stores samples cut from the bottom of the picture before it
* is shown.
* @var int
*/
public int $crop_bottom = 0;
/**
* $bit_depth_luma stores bits in each brightness sample.
* @var int
*/
public int $bit_depth_luma = 8;
/**
* $bit_depth_chroma stores bits in each color sample.
* @var int
*/
public int $bit_depth_chroma = 8;
/**
* $picture_order_low_bit_count stores bits in the picture order
* count that are written
* in each slice.
* @var int
*/
public int $picture_order_low_bit_count = 4;
/**
* $power_of_two_min_blue_size stores the power of two of the smallest
* coding block.
* @var int
*/
public int $power_of_two_min_blue_size = 3;
/**
* $power_of_two_tree_block_size stores the power of two of the largest
* coding block.
* @var int
*/
public int $power_of_two_tree_block_size = 6;
/**
* $smallest_transform_power stores the power of two of the smallest
* transform.
* @var int
*/
public int $smallest_transform_power = 2;
/**
* $largest_transform_power stores the power of two of the largest
* transform.
* @var int
*/
public int $largest_transform_power = 5;
/**
* $max_transform_depth_self_guessed stores how far the transform tree may
* split
* below an intra coding block.
* @var int
*/
public int $max_transform_depth_self_guessed = 0;
/**
* $max_transform_depth_inter stores how far the transform tree may split
* below an inter coding block.
* @var int
*/
public int $max_transform_depth_inter = 0;
/**
* $scaling_list_enabled stores true when the stream supplies its own
* scaling values.
* @var bool
*/
public bool $scaling_list_enabled = false;
/**
* $scaling_list stores the scaling values, whether default or supplied.
* @var array
*/
public array $scaling_list = [];
/**
* $amp_enabled stores true when coding blocks may split into unequal
* halves.
* @var bool
*/
public bool $amp_enabled = false;
/**
* $sample_offset_enabled stores true when the sample adaptive offset
* filter is used.
* @var bool
*/
public bool $sample_offset_enabled = false;
/**
* $plain_samples_enabled stores true when blocks may store their samples
* as they are.
* @var bool
*/
public bool $plain_samples_enabled = false;
/**
* $plain_samples_bit_depth_luma stores bits in each stored brightness
* sample of such
* a block.
* @var int
*/
public int $plain_samples_bit_depth_luma = 8;
/**
* $plain_samples_bit_depth_chroma stores bits in each stored color sample
* of such a
* block.
* @var int
*/
public int $plain_samples_bit_depth_chroma = 8;
/**
* $power_of_two_min_plain_samples_blue_size stores the power of two of the
* smallest
* block that may store
* its samples.
* @var int
*/
public int $power_of_two_min_plain_samples_blue_size = 3;
/**
* $power_of_two_max_plain_samples_blue_size stores the power of two of the
* largest
* block that may store its
* samples.
* @var int
*/
public int $power_of_two_max_plain_samples_blue_size = 3;
/**
* $plain_samples_loop_filter_disabled stores true when such blocks are
* left out of
* the smoothing filter.
* @var bool
*/
public bool $plain_samples_loop_filter_disabled = false;
/**
* $count_short_term_reference_picture_sets stores how many reference
* picture sets the
* header lists.
* @var int
*/
public int $count_short_term_reference_picture_sets = 0;
/**
* $long_term_reference_pics_present stores true when the header lists long
* term
* reference pictures.
* @var bool
*/
public bool $long_term_reference_pics_present = false;
/**
* $strong_self_guessed_smoothing stores true when a stronger smoothing may
* be used
* on flat intra edges.
* @var bool
*/
public bool $strong_self_guessed_smoothing = false;
/**
* $count_long_term_reference_pics stores how many long term reference
* pictures the
* sequence header lists.
* @var int
*/
public int $count_long_term_reference_pics = 0;
/**
* $motion_from_other_pictures_allowed stores true when a slice may say
* whether it
* predicts motion from time.
* @var bool
*/
public bool $motion_from_other_pictures_allowed = false;
/**
* $reference_picture_set_counts stores how many offsets each reference
* picture set
* holds.
* @var array
*/
public array $reference_picture_set_counts = [];
/**
* croppedWidth width of the picture as it is shown, after the cropping the
* header asks for.
*
* @return int width in samples
*/
public function croppedWidth(): int
{
$step = $this->chroma_format_setting === 1 || $this
->chroma_format_setting === 2
? 2 : 1;
return $this->frame_width - $step * ($this->crop_left + $this
->crop_right);
}
/**
* croppedHeight height of the picture as it is shown, after the cropping
* the header asks for.
*
* @return int height in samples
*/
public function croppedHeight(): int
{
$step = $this->chroma_format_setting === 1 ? 2 : 1;
return $this->frame_height - $step * ($this->crop_top + $this
->crop_bottom);
}
}
/**
* HevcPps a picture parameter set: the quantizer offsets, the tile layout and
* the filter settings that hold for one picture.
*/
final class HevcPps
{
/**
* $sequence_settings_id stores which sequence parameter set this refers to.
* @var int
*/
public int $sequence_settings_id = 0;
/**
* $dependent_slice_segments_enabled stores true when each slice segment
* carries its own quantizer.
* @var bool
*/
public bool $dependent_slice_segments_enabled = false;
/**
* $output_flag_present stores true when each slice carries its own picture
* order count.
* @var bool
*/
public bool $output_flag_present = false;
/**
* $count_extra_slice_header_bits stores extra bits each slice header
* carries
* for the application's use.
* @var int
*/
public int $count_extra_slice_header_bits = 0;
/**
* $sign_data_hiding stores true when a residual may be coded without a
* transform.
* @var bool
*/
public bool $sign_data_hiding = false;
/**
* $constrained_self_guessed_guess stores true when prediction may only use
* samples
* that were coded alone.
* @var bool
*/
public bool $constrained_self_guessed_guess = false;
/**
* $arithmetic_starting_present stores true when each slice header carries
* a picture
* parameter identifier.
* @var bool
*/
public bool $arithmetic_starting_present = false;
/**
* $starting_quantizer stores the quantizer a picture starts at.
* Each slice may move away from it, and each block within a slice
* may move again.
* @var int
*/
public int $starting_quantizer = 26;
/**
* $transform_skip_enabled stores true when a block may skip its transform.
* @var bool
*/
public bool $transform_skip_enabled = false;
/**
* $block_quantizer_delta_enabled stores true when the quantizer may change
* within the
* picture.
* @var bool
*/
public bool $block_quantizer_delta_enabled = false;
/**
* $difference_block_quantizer_delta_depth stores how far below a coding
* tree block
* the
* quantizer may change.
* @var int
*/
public int $difference_block_quantizer_delta_depth = 0;
/**
* $blue_quantizer_offset stores shift applied to the blue color difference
* quantizer.
* @var int
*/
public int $blue_quantizer_offset = 0;
/**
* $red_quantizer_offset stores shift applied to the red color difference
* quantizer.
* @var int
*/
public int $red_quantizer_offset = 0;
/**
* $slice_chroma_quantizer_offsets_present stores true when each slice may
* shift
* the color quantizers again.
* @var bool
*/
public bool $slice_chroma_quantizer_offsets_present = false;
/**
* $weighted_guess stores true when neighboring samples are weighed for
* smoothing.
* @var bool
*/
public bool $weighted_guess = false;
/**
* $two_picture_weighting stores true when two references are weighed
* together.
* @var bool
*/
public bool $two_picture_weighting = false;
/**
* $plain_samples_allowed stores true when a block may be coded with no
* residual at all.
* @var bool
*/
public bool $plain_samples_allowed = false;
/**
* $tiles_enabled stores true when the picture is split into tiles.
* @var bool
*/
public bool $tiles_enabled = false;
/**
* $entropy_coding_sync_enabled stores true when neighboring rows share
* their coding state.
* @var bool
*/
public bool $entropy_coding_sync_enabled = false;
/**
* $loop_filter_across_slices stores true when smoothing crosses slice
* edges.
* @var bool
*/
public bool $loop_filter_across_slices = false;
/**
* $loop_filter_across_tiles stores true when smoothing crosses tile edges.
* @var bool
*/
public bool $loop_filter_across_tiles = true;
/**
* $deblocking_filter_control_present stores true when the smoothing
* settings are written here.
* @var bool
*/
public bool $deblocking_filter_control_present = false;
/**
* $deblocking_filter_disabled stores true when smoothing is turned off for
* the whole picture.
* @var bool
*/
public bool $deblocking_filter_disabled = false;
/**
* $beta_offset stores how far the smoothing filter may reach.
* @var int
*/
public int $beta_offset = 0;
/**
* $move_limit_offset stores how strongly the smoothing filter acts.
* @var int
*/
public int $move_limit_offset = 0;
/**
* $scaling_list_data_present stores true when the picture supplies its own
* scaling values.
* @var bool
*/
public bool $scaling_list_data_present = false;
/**
* $scaling_list stores the scaling values this picture supplies.
* @var array
*/
public array $scaling_list = [];
/**
* $deblocking_filter_override_enabled stores true when a slice may change
* the smoothing settings.
* @var bool
*/
public bool $deblocking_filter_override_enabled = false;
/**
* $chroma_quantizer_offset_list_enabled stores true when a block may shift
* the
* color quantizers.
* @var bool
*/
public bool $chroma_quantizer_offset_list_enabled = false;
/**
* $slice_header_extension_present stores true when the slice header carries
* extra bytes for an application.
* @var bool
*/
public bool $slice_header_extension_present = false;
/**
* $power_of_two_parallel_merge_level stores the power of two of the
* smallest block
* whose
* prediction is merged.
* @var int
*/
public int $power_of_two_parallel_merge_level = 2;
/**
* $count_tile_columns stores columns of tiles the picture is split into.
* @var int
*/
public int $count_tile_columns = 1;
/**
* $count_tile_rows stores rows of tiles the picture is split into.
* @var int
*/
public int $count_tile_rows = 1;
/**
* $uniform_spacing stores true when the tiles are all the same size.
* @var bool
*/
public bool $uniform_spacing = true;
/**
* $column_widths stores width of each tile column in coding tree blocks.
* @var array
*/
public array $column_widths = [];
/**
* $row_heights stores height of each tile row in coding tree blocks.
* @var array
*/
public array $row_heights = [];
}
/**
* HevcParamParser reads video, sequence and picture parameter sets out of an
* H.265 stream.
*/
final class HevcParamParser
{
/**
* PROFILE_FLAG_BITS is bits of profile and constraint flags that follow the
* profile identifier, before the level.
*/
private const PROFILE_FLAG_BITS = 80;
/**
* settingsFromSetupRecord reads the parameter sets out of the setup record
* a container stores.
*
* @param string $record the record's bytes
* @return array each set as its stored bytes
*/
public static function settingsFromSetupRecord(string $record): array
{
if (strlen($record) < 23) {
return [];
}
$groups = ord($record[22]);
$position = 23;
$sets = [];
for ($group = 0; $group < $groups; $group++) {
if ($position + 3 > strlen($record)) {
break;
}
$count = (ord($record[$position + 1]) << 8)
| ord($record[$position + 2]);
$position += 3;
for ($entry = 0; $entry < $count; $entry++) {
if ($position + 2 > strlen($record)) {
break 2;
}
$length = (ord($record[$position]) << 8)
| ord($record[$position + 1]);
$position += 2;
$sets[] = substr($record, $position, $length);
$position += $length;
}
}
return $sets;
}
/**
* readSequenceSettings reads a sequence parameter set. removed
*
* @param string $unpacked_bytes the unit's payload, with escapes already
* @return array the parsed set and the identifier it was given
*/
public static function readSequenceSettings(string $unpacked_bytes): array
{
$bits = new H264Bits($unpacked_bytes);
$sequence_settings = new HevcSps();
$sequence_settings->video_settings_id = $bits->readBits(4);
$max_sub_layers = $bits->readBits(3) + 1;
$bits->readBit();
self::skipProfileAndLevel($bits, $max_sub_layers);
$sequence_settings_id = $bits->readWholeNumber();
$sequence_settings->chroma_format_setting = $bits->readWholeNumber();
if ($sequence_settings->chroma_format_setting === 3) {
$sequence_settings->separate_color_plane = $bits->readBit() === 1;
}
$sequence_settings->frame_width = $bits->readWholeNumber();
$sequence_settings->frame_height = $bits->readWholeNumber();
if ($bits->readBit() === 1) {
$sequence_settings->crop_left = $bits->readWholeNumber();
$sequence_settings->crop_right = $bits->readWholeNumber();
$sequence_settings->crop_top = $bits->readWholeNumber();
$sequence_settings->crop_bottom = $bits->readWholeNumber();
}
$sequence_settings->bit_depth_luma = $bits->readWholeNumber() + 8;
$sequence_settings->bit_depth_chroma = $bits->readWholeNumber() + 8;
$sequence_settings->picture_order_low_bit_count = $bits
->readWholeNumber() + 4;
$ordering = $bits->readBit() === 1;
$first = $ordering ? 0 : $max_sub_layers - 1;
for ($layer = $first; $layer < $max_sub_layers; $layer++) {
$bits->readWholeNumber();
$bits->readWholeNumber();
$bits->readWholeNumber();
}
$sequence_settings->power_of_two_min_blue_size = $bits
->readWholeNumber() + 3;
$sequence_settings->power_of_two_tree_block_size =
$sequence_settings->power_of_two_min_blue_size + $bits
->readWholeNumber();
$sequence_settings->smallest_transform_power = $bits
->readWholeNumber() + 2;
$sequence_settings->largest_transform_power =
$sequence_settings->smallest_transform_power + $bits
->readWholeNumber();
$sequence_settings->max_transform_depth_inter = $bits
->readWholeNumber();
$sequence_settings->max_transform_depth_self_guessed = $bits
->readWholeNumber();
$sequence_settings->scaling_list_enabled = $bits->readBit() === 1;
if ($sequence_settings->scaling_list_enabled && $bits->readBit() ===
1) {
$sequence_settings->scaling_list = self::readScalingList($bits);
}
$sequence_settings->amp_enabled = $bits->readBit() === 1;
$sequence_settings->sample_offset_enabled = $bits->readBit() === 1;
$sequence_settings->plain_samples_enabled = $bits->readBit() === 1;
if ($sequence_settings->plain_samples_enabled) {
$sequence_settings->plain_samples_bit_depth_luma = $bits
->readBits(4) + 1;
$sequence_settings->plain_samples_bit_depth_chroma = $bits
->readBits(4) + 1;
$sequence_settings->power_of_two_min_plain_samples_blue_size = $bits
->readWholeNumber() + 3;
$sequence_settings->power_of_two_max_plain_samples_blue_size =
$sequence_settings
->power_of_two_min_plain_samples_blue_size + $bits
->readWholeNumber(
);
$sequence_settings->plain_samples_loop_filter_disabled =
$bits->readBit() === 1;
}
$sequence_settings->count_short_term_reference_picture_sets = $bits
->readWholeNumber();
$counts = [];
for ($set = 0; $set < $sequence_settings
->count_short_term_reference_picture_sets; $set++) {
$counts[$set] = self::skipReferencePictureSet($bits, $set,
$counts);
}
$sequence_settings->reference_picture_set_counts = $counts;
$sequence_settings->long_term_reference_pics_present = $bits
->readBit() === 1;
if ($sequence_settings->long_term_reference_pics_present) {
$sequence_settings->count_long_term_reference_pics = $bits
->readWholeNumber();
for ($index =
0; $index < $sequence_settings
->count_long_term_reference_pics; $index++) {
$bits->readBits($sequence_settings
->picture_order_low_bit_count);
$bits->readBit();
}
}
$sequence_settings->motion_from_other_pictures_allowed = $bits
->readBit() === 1;
$sequence_settings->strong_self_guessed_smoothing = $bits
->readBit() === 1;
if ($sequence_settings->frame_width < 1 || $sequence_settings
->frame_height < 1) {
throw new HevcException('H.265 picture size is missing');
}
return [$sequence_settings_id, $sequence_settings];
}
/**
* readPictureSettings reads a picture parameter set. removed
*
* @param string $unpacked_bytes the unit's payload, with escapes already
* @return array the parsed set and the identifier it was given
*/
public static function readPictureSettings(string $unpacked_bytes): array
{
$bits = new H264Bits($unpacked_bytes);
$picture_settings = new HevcPps();
$picture_settings_id = $bits->readWholeNumber();
$picture_settings->sequence_settings_id = $bits->readWholeNumber();
$picture_settings->dependent_slice_segments_enabled =
$bits->readBit() === 1;
$picture_settings->output_flag_present = $bits->readBit() === 1;
$picture_settings->count_extra_slice_header_bits = $bits->readBits(3);
$picture_settings->sign_data_hiding = $bits->readBit() === 1;
$picture_settings->arithmetic_starting_present = $bits->readBit() === 1;
$bits->readWholeNumber();
$bits->readWholeNumber();
$picture_settings->starting_quantizer = $bits->readSignedNumber() + 26;
$picture_settings->constrained_self_guessed_guess = $bits
->readBit() === 1;
$picture_settings->transform_skip_enabled = $bits->readBit() === 1;
$picture_settings->block_quantizer_delta_enabled = $bits
->readBit() === 1;
if ($picture_settings->block_quantizer_delta_enabled) {
$picture_settings->difference_block_quantizer_delta_depth = $bits
->readWholeNumber();
}
$picture_settings->blue_quantizer_offset = $bits->readSignedNumber();
$picture_settings->red_quantizer_offset = $bits->readSignedNumber();
$picture_settings->slice_chroma_quantizer_offsets_present =
$bits->readBit() === 1;
$picture_settings->weighted_guess = $bits->readBit() === 1;
$picture_settings->two_picture_weighting = $bits->readBit() === 1;
$picture_settings->plain_samples_allowed = $bits->readBit() === 1;
$picture_settings->tiles_enabled = $bits->readBit() === 1;
$picture_settings->entropy_coding_sync_enabled = $bits->readBit() === 1;
if ($picture_settings->tiles_enabled) {
$picture_settings->count_tile_columns = $bits
->readWholeNumber() + 1;
$picture_settings->count_tile_rows = $bits->readWholeNumber() + 1;
$picture_settings->uniform_spacing = $bits->readBit() === 1;
if (!$picture_settings->uniform_spacing) {
for ($column = 0; $column +
1 < $picture_settings->count_tile_columns;
$column++) {
$picture_settings->column_widths[] = $bits
->readWholeNumber() + 1;
}
for ($row = 0; $row +
1 < $picture_settings->count_tile_rows; $row++) {
$picture_settings->row_heights[] = $bits
->readWholeNumber() + 1;
}
}
$picture_settings->loop_filter_across_tiles =
$bits->readBit() === 1;
}
$picture_settings->loop_filter_across_slices = $bits->readBit() === 1;
$picture_settings->deblocking_filter_control_present =
$bits->readBit() === 1;
if ($picture_settings->deblocking_filter_control_present) {
$picture_settings->deblocking_filter_override_enabled =
$bits->readBit() === 1;
$picture_settings->deblocking_filter_disabled =
$bits->readBit() === 1;
if (!$picture_settings->deblocking_filter_disabled) {
$picture_settings->beta_offset = $bits->readSignedNumber() * 2;
$picture_settings->move_limit_offset = $bits
->readSignedNumber() * 2;
}
}
$picture_settings->scaling_list_data_present = $bits->readBit() === 1;
if ($picture_settings->scaling_list_data_present) {
$picture_settings->scaling_list = self::readScalingList($bits);
}
$bits->readBit();
$picture_settings->power_of_two_parallel_merge_level = $bits
->readWholeNumber() + 2;
$picture_settings->slice_header_extension_present =
$bits->readBit() === 1;
return [$picture_settings_id, $picture_settings];
}
/**
* skipProfileAndLevel passes over the profile, tier and level fields, which
* say what a player must support but do not affect how the samples are
* decoded.
*
* @param H264Bits $bits reader positioned at the profile fields
* @param int $max_sub_layers how many temporal layers the stream carries
*/
private static function skipProfileAndLevel(H264Bits $bits,
int $max_sub_layers): void
{
$bits->skipBits(2 + 1 + 5);
$bits->skipBits(self::PROFILE_FLAG_BITS);
$bits->skipBits(8);
$profile_present = [];
$level_present = [];
for ($layer = 0; $layer + 1 < $max_sub_layers; $layer++) {
$profile_present[$layer] = $bits->readBit() === 1;
$level_present[$layer] = $bits->readBit() === 1;
}
if ($max_sub_layers > 1) {
for ($layer = $max_sub_layers - 1; $layer < 8; $layer++) {
$bits->skipBits(2);
}
}
for ($layer = 0; $layer + 1 < $max_sub_layers; $layer++) {
if ($profile_present[$layer]) {
$bits->skipBits(2 + 1 + 5 + self::PROFILE_FLAG_BITS);
}
if ($level_present[$layer]) {
$bits->skipBits(8);
}
}
}
/**
* skipReferencePictureSet reads one set of reference picture offsets and
* hands back how many it held, which the next set may be written relative
* to. A keyframe uses none of these, but the fields that follow them in the
* header are needed, so they have to be read past exactly.
*
* @param H264Bits $bits reader positioned at the set
* @param int $index which set this is
* @param array $counts how many offsets each earlier set held
* @return int how many offsets this set holds
*/
public static function skipReferencePictureSet(H264Bits $bits, int $index,
array $counts): int
{
$predict = false;
if ($index !== 0) {
$predict = $bits->readBit() === 1;
}
if ($predict) {
$bits->readBit();
$bits->readWholeNumber();
$earlier = $counts[$index - 1] ?? 0;
$kept = 0;
for ($entry = 0; $entry <= $earlier; $entry++) {
$used = $bits->readBit() === 1;
$keep = $used;
if (!$used) {
$keep = $bits->readBit() === 1;
}
if ($keep) {
$kept++;
}
}
return $kept;
}
$negative = $bits->readWholeNumber();
$positive = $bits->readWholeNumber();
for ($entry = 0; $entry < $negative + $positive; $entry++) {
$bits->readWholeNumber();
$bits->readBit();
}
return $negative + $positive;
}
/**
* readScalingList reads the scaling values, which weigh the coefficients of
* a transform before they are dequantized.
*
* @param H264Bits $bits reader positioned at the values
* @return array the values, one list per size and kind of block
*/
private static function readScalingList(H264Bits $bits): array
{
$lists = [];
for ($size = 0; $size < 4; $size++) {
$matrices = $size === 3 ? 2 : 6;
for ($matrix = 0; $matrix < $matrices; $matrix++) {
if ($bits->readBit() === 0) {
$bits->readWholeNumber();
$lists[$size][$matrix] = null;
continue;
}
$count = min(64, 1 << (4 + ($size << 1)));
$values = [];
if ($size > 1) {
$values['dc'] = $bits->readSignedNumber() + 8;
}
$next = 8;
for ($entry = 0; $entry < $count; $entry++) {
$next = ($next + $bits->readSignedNumber() + 256) % 256;
$values[$entry] = $next;
}
$lists[$size][$matrix] = $values;
}
}
return $lists;
}
}