/ src / library / av_processing / HevcParamParser.php
<?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;
    }
}
X