/ src / library / av_processing / Vp9Header.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 class reads what a VP9 frame says about itself, before any of its
 * values are read.
 */
namespace seekquarry\yioop\library\av_processing;
/**
 * Vp9Header reads what a VP9 frame says about itself, before any of its
 * values are read.
 */
final class Vp9Header
{
    /**
     * $profile stores which of VP9's four profiles the stream uses, which
     * decides how many bits a sample takes and whether the color planes are
     * shrunk.
     * @var int
     */
    public int $profile = 0;
    /**
     * $show_existing_frame stores whether this frame is a picture already
     * decoded being shown again rather than a new one. Nothing is decoded for
     * it.
     * @var bool
     */
    public bool $show_existing_frame = false;
    /**
     * $keyframe stores whether the frame stands on its own. Only these can be
     * decoded without the frames before them, which is what a thumbnail needs.
     * @var bool
     */
    public bool $keyframe = false;
    /**
     * $show_frame stores whether the frame is shown when it is decoded. A frame
     * may be decoded only to be leaned on by later ones.
     * @var bool
     */
    public bool $show_frame = false;
    /**
     * $error_resilient stores whether the stream is written so a decoder can
     * pick it up mid-way. It changes which probabilities carry over from frame
     * to frame.
     * @var bool
     */
    public bool $error_resilient = false;
    /**
     * $bit_depth stores how many bits one sample takes, eight for most streams
     * and ten or twelve for the deeper profiles.
     * @var int
     */
    public int $bit_depth = 8;
    /**
     * $color_space stores which set of color meanings the samples carry, which
     * decides the sums that turn them into red, green and blue.
     * @var int
     */
    public int $color_space = 0;
    /**
     * $subsampling_x stores how many pixels across share one color value. Two
     * for most streams.
     * @var int
     */
    public int $subsampling_x = 1;
    /**
     * $subsampling_y stores how many pixels down share one color value.
     * @var int
     */
    public int $subsampling_y = 1;
    /**
     * $frame_width stores how wide the frame is, in pixels, as the header says.
     * @var int
     */
    public int $frame_width = 0;
    /**
     * $frame_height stores how tall the frame is, in pixels.
     * @var int
     */
    public int $frame_height = 0;
    /**
     * $render_width stores how wide the frame is meant to be shown, which may
     * differ from how it was coded.
     * @var int
     */
    public int $render_width = 0;
    /**
     * $render_height stores how tall the frame is meant to be shown.
     * @var int
     */
    public int $render_height = 0;
    /**
     * $loop_filter_level stores how strongly block edges are smoothed after
     * decoding. Zero leaves them alone.
     * @var int
     */
    public int $loop_filter_level = 0;
    /**
     * $loop_filter_level_bit stores bit position of the filter level field, for
     * tools that rewrite it.
     * @var int
     */
    public int $loop_filter_level_bit = 0;
    /**
     * $loop_filter_sharpness stores how much that smoothing is held back at
     * sharp edges, so real detail is not blurred away.
     * @var int
     */
    public int $loop_filter_sharpness = 0;
    /**
     * $loop_filter_delta_enabled stores whether the smoothing of block edges is
     * nudged by how a block was guessed and which picture it leans on. Where
     * this is off, every block is smoothed by the same amount.
     * @var bool
     */
    public bool $loop_filter_delta_enabled = false;
    /**
     * $loop_filter_reference_deltas stores adjustments by reference frame;
     * intra
     * pictures use the first.
     * @var array
     */
    public array $loop_filter_reference_deltas = [1, 0, -1, -1];
    /**
     * $loop_filter_mode_deltas stores how the smoothing is nudged for blocks
     * guessed one way rather than another.
     * @var array
     */
    public array $loop_filter_mode_deltas = [0, 0];
    /**
     * $base_q_position stores the quantizer the whole frame starts from. A
     * larger
     * one means coarser values and a smaller file.
     * @var int
     */
    public int $base_q_position = 0;
    /**
     * $lossless stores whether the frame was coded with nothing thrown away,
     * which uses a different transform.
     * @var bool
     */
    public bool $lossless = false;
    /**
     * $delta_brightness_quantizer_first_value stores the delta qy first value.
     * @var int
     */
    public int $delta_brightness_quantizer_first_value = 0;
    /**
     * $delta_color_quantizer_first_value stores how far the quantizer
     * for the first value of a color block sits from the frame's own
     * quantizer.
     * @var int
     */
    public int $delta_color_quantizer_first_value = 0;
    /**
     * $delta_color_quantizer_other_values stores how far the quantizer
     * for the rest of a color block's values sits from the frame's own
     * quantizer.
     * @var int
     */
    public int $delta_color_quantizer_other_values = 0;
    /**
     * $segmentation_enabled stores whether the frame is split into segments
     * that carry their own quantizer and smoothing.
     * @var bool
     */
    public bool $segmentation_enabled = false;
    /**
     * $segmentation_update_map stores whether this frame says afresh which
     * block belongs to which segment.
     * @var bool
     */
    public bool $segmentation_update_map = false;
    /**
     * $segment_tree_probs stores how likely each segment is, used while reading
     * which segment a block belongs to.
     * @var array
     */
    public array $segment_tree_probs = [255, 255, 255, 255, 255, 255, 255];
    /**
     * $segment_absolute stores true when a segment's values replace the frame's
     * rather than shift it.
     * @var bool
     */
    public bool $segment_absolute = false;
    /**
     * $segment_feature_enabled stores which of the four adjustments each
     * segment carries.
     * @var array
     */
    public array $segment_feature_enabled = [];
    /**
     * $segment_feature_value stores the value of each adjustment a segment
     * carries.
     * @var array
     */
    public array $segment_feature_value = [];
    /**
     * $tile_cols_power_of_two stores how many tiles the frame is split into
     * across,
     * given as a power of two. Tiles can be read side by side.
     * @var int
     */
    public int $tile_cols_power_of_two = 0;
    /**
     * $tile_rows_power_of_two stores how many tiles the frame is split into
     * down.
     * @var int
     */
    public int $tile_rows_power_of_two = 0;
    /**
     * $compressed_header_size stores how many bytes the frame's coded header
     * takes, which follows the plain one.
     * @var int
     */
    public int $compressed_header_size = 0;
    /**
     * $uncompressed_header_bytes stores how many bytes the plain header took,
     * so the coded one that follows can be found.
     * @var int
     */
    public int $uncompressed_header_bytes = 0;
    /**
     * SYNC_CODE is the three bytes a VP9 frame starts with, which say that what
     * follows is VP9 rather than something else.
     * @var mixed
     */
    private const SYNC_CODE = 0x498342;
    /**
     * SEGMENT_QUANT_BITS is bits in a segment's alternate quantizer index.
     */
    private const SEGMENT_QUANT_BITS = 8;
    /**
     * SEGMENT_FILTER_BITS is bits in a segment's alternate loop filter level.
     */
    private const SEGMENT_FILTER_BITS = 6;
    /**
     * SEGMENT_REFERENCE_BITS is bits naming a segment's reference picture.
     */
    private const SEGMENT_REFERENCE_BITS = 2;
    /**
     * SEGMENT_FEATURE_QUANT is position of the alternate quantizer among a
     * segment's adjustments.
     */
    public const SEGMENT_FEATURE_QUANT = 0;
    /**
     * SEGMENT_FEATURE_FILTER is position of the alternate filter level among a
     * segment's adjustments.
     */
    public const SEGMENT_FEATURE_FILTER = 1;
    /**
     * readSettings reads the header of a keyframe and hands back what it says.
     *
     * @param string $frame the stored bytes of one frame
     * @return self what was read
     */
    public static function readSettings(string $frame): self
    {
        $header = new self();
        for ($seg = 0; $seg < 8; $seg++) {
            $header->segment_feature_enabled[$seg] = [false, false, false,
                false];
            $header->segment_feature_value[$seg] = [0, 0, 0, 0];
        }
        $bits = new BitReader($frame);
        if ($bits->readBits(2) !== 2) {
            throw new VideoException('not a VP9 frame');
        }
        $low = $bits->readBit();
        $high = $bits->readBit();
        $header->profile = ($high << 1) | $low;
        if ($header->profile === 3) {
            /* reserved */
            $bits->readBit();
        }
        $header->show_existing_frame = $bits->readBit() === 1;
        if ($header->show_existing_frame) {
            $bits->readBits(3);
            return $header;
        }
        $header->keyframe = $bits->readBit() === 0;
        $header->show_frame = $bits->readBit() === 1;
        $header->error_resilient = $bits->readBit() === 1;
        if (!$header->keyframe) {
            /* inter frames reference earlier pictures, which is where a */
            /* decoder would need everything this one does not have */
            return $header;
        }
        if ($bits->readBits(24) !== self::SYNC_CODE) {
            throw new VideoException('missing VP9 keyframe sync code');
        }
        $header->readColorConfig($bits);
        $header->frame_width = $bits->readBits(16) + 1;
        $header->frame_height = $bits->readBits(16) + 1;
        if ($bits->readBit() === 1) {
            $header->render_width = $bits->readBits(16) + 1;
            $header->render_height = $bits->readBits(16) + 1;
        } else {
            $header->render_width = $header->frame_width;
            $header->render_height = $header->frame_height;
        }
        if (!$header->error_resilient) {
            /* refresh frame context */
            $bits->readBit();
            /* frame parallel decoding mode */
            $bits->readBit();
        }
        /* frame context index */
        $bits->readBits(2);
        $header->readLoopFilterParams($bits);
        $header->readQuantizationParams($bits);
        $header->readSegmentationParams($bits);
        $header->readTileInfo($bits);
        $header->compressed_header_size = $bits->readBits(16);
        $header->uncompressed_header_bytes = intdiv($bits->position + 7, 8);
        if ($header->compressed_header_size < 1
            || $header->uncompressed_header_bytes
                + $header->compressed_header_size > strlen($frame)) {
            throw new VideoException(
                'VP9 header sizes are inconsistent with the frame');
        }
        $header->readTiles($frame);
        $header->readCompressedHeader($frame);
        return $header;
    }
    /**
     * readColorConfig reads the sample depth, the color space and how the
     * chroma planes are subsampled.
     *
     * @param BitReader $bits the reader the stream's bits are taken from
     */
    private function readColorConfig(BitReader $bits): void
    {
        if ($this->profile >= 2) {
            $this->bit_depth = $bits->readBit() === 1 ? 12 : 10;
        } else {
            $this->bit_depth = 8;
        }
        $this->color_space = $bits->readBits(3);
        /* 7 is sRGB */
        if ($this->color_space !== 7) {
            /* color range */
            $bits->readBit();
            if ($this->profile === 1 || $this->profile === 3) {
                $this->subsampling_x = $bits->readBit();
                $this->subsampling_y = $bits->readBit();
                /* reserved */
                $bits->readBit();
            } else {
                $this->subsampling_x = 1;
                $this->subsampling_y = 1;
            }
        } else {
            $this->subsampling_x = 0;
            $this->subsampling_y = 0;
            if ($this->profile === 1 || $this->profile === 3) {
                /* reserved */
                $bits->readBit();
            }
        }
    }
    /**
     * readLoopFilterParams reads how strongly the block edges are smoothed and
     * what adjustments particular blocks carry.
     *
     * @param BitReader $bits the reader the stream's bits are taken from
     */
    private function readLoopFilterParams(BitReader $bits): void
    {
        $this->loop_filter_level_bit = $bits->position;
        $this->loop_filter_level = $bits->readBits(6);
        $this->loop_filter_sharpness = $bits->readBits(3);
        $this->loop_filter_delta_enabled = $bits->readBit() === 1;
        if ($this->loop_filter_delta_enabled && $bits->readBit() === 1) {
            for ($i = 0; $i < 4; $i++) {
                if ($bits->readBit() === 1) {
                    $magnitude = $bits->readBits(6);
                    $this->loop_filter_reference_deltas[$i] =
                        ($bits->readBit() === 1) ? -$magnitude : $magnitude;
                }
            }
            for ($i = 0; $i < 2; $i++) {
                if ($bits->readBit() === 1) {
                    $magnitude = $bits->readBits(6);
                    $this->loop_filter_mode_deltas[$i] =
                        ($bits->readBit() === 1) ? -$magnitude : $magnitude;
                }
            }
        }
    }
    /**
     * readQuantizationParams reads the frame's quantizer index and the small
     * shifts applied to the flat terms and to chroma.
     *
     * @param BitReader $bits the reader the stream's bits are taken from
     */
    private function readQuantizationParams(BitReader $bits): void
    {
        $this->base_q_position = $bits->readBits(8);
        $deltas = [];
        for ($i = 0; $i < 3; $i++) {
            $deltas[$i] = 0;
            if ($bits->readBit() === 1) {
                $deltas[$i] = $bits->readBits(4);
                if ($bits->readBit() === 1) {
                    $deltas[$i] = -$deltas[$i];
                }
            }
        }
        $this->delta_brightness_quantizer_first_value = $deltas[0];
        $this->delta_color_quantizer_first_value = $deltas[1];
        $this->delta_color_quantizer_other_values = $deltas[2];
        $this->lossless = $this->base_q_position === 0
            && $deltas[0] === 0 && $deltas[1] === 0 && $deltas[2] === 0;
    }
    /**
     * readSegmentationParams reads how the picture is split into segments and
     * what each segment adjusts.
     *
     * @param BitReader $bits the reader the stream's bits are taken from
     */
    private function readSegmentationParams(BitReader $bits): void
    {
        $this->segmentation_enabled = $bits->readBit() === 1;
        if (!$this->segmentation_enabled) {
            return;
        }
        $this->segmentation_update_map = $bits->readBit() === 1;
        if ($this->segmentation_update_map) {
            for ($i = 0; $i < 7; $i++) {
                $this->segment_tree_probs[$i] = ($bits->readBit() === 1)
                    ? $bits->readBits(8) : 255;
            }
            /* temporal update */
            if ($bits->readBit() === 1) {
                for ($i = 0; $i < 3; $i++) {
                    if ($bits->readBit() === 1) {
                        $bits->readBits(8);
                    }
                }
            }
        }
        /* update data */
        if ($bits->readBit() === 1) {
            $this->segment_absolute = $bits->readBit() === 1;
            $widths = [
                self::SEGMENT_QUANT_BITS, self::SEGMENT_FILTER_BITS,
                self::SEGMENT_REFERENCE_BITS, 0,
            ];
            $signed = [true, true, false, false];
            for ($seg = 0; $seg < 8; $seg++) {
                for ($field = 0; $field < 4; $field++) {
                    $this->segment_feature_enabled[$seg][$field] = false;
                    $this->segment_feature_value[$seg][$field] = 0;
                    if ($bits->readBit() !== 1) {
                        continue;
                    }
                    $this->segment_feature_enabled[$seg][$field] = true;
                    $value = 0;
                    if ($widths[$field] > 0) {
                        $value = $bits->readBits($widths[$field]);
                    }
                    if ($signed[$field] && $bits->readBit() === 1) {
                        $value = -$value;
                    }
                    $this->segment_feature_value[$seg][$field] = $value;
                }
            }
        }
    }
    /**
     * readTileInfo reads how many tiles the picture is split into, each of
     * which is coded on its own.
     *
     * @param BitReader $bits the reader the stream's bits are taken from
     */
    private function readTileInfo(BitReader $bits): void
    {
        $superblock_cols = intdiv($this->frame_width + 63, 64);
        $min_power_of_two = 0;
        while ((64 << $min_power_of_two) < $superblock_cols) {
            $min_power_of_two++;
        }
        $max_power_of_two = 1;
        while (($superblock_cols >> $max_power_of_two) >= 4) {
            $max_power_of_two++;
        }
        $max_power_of_two--;
        $this->tile_cols_power_of_two = $min_power_of_two;
        while ($this->tile_cols_power_of_two < $max_power_of_two) {
            if ($bits->readBit() === 1) {
                $this->tile_cols_power_of_two++;
            } else {
                break;
            }
        }
        $this->tile_rows_power_of_two = $bits->readBit();
        if ($this->tile_rows_power_of_two === 1) {
            $this->tile_rows_power_of_two += $bits->readBit();
        }
    }
    /**
     * $tiles stores where each tile's bytes sit in the frame. The entropy
     * reader walks them one at a time.
     * @var array
     */
    public array $tiles = [];
    /**
     * $transform_mode stores which transform sizes the frame allows, from one
     * fixed
     * size up to a choice made block by block.
     * @var int
     */
    public int $transform_mode = 0;
    /**
     * $compressed_header_clean stores whether the coded header ended where it
     * should. A parse that has drifted shows itself here.
     * @var bool
     */
    public bool $compressed_header_clean = false;
    /**
     * $value_probs stores probability state after the compressed header has
     * been
     * applied.
     * @var array
     */
    public array $value_probs = [];
    /**
     * $eight_transform_probabilities stores how likely each transform size is
     * for an eight
     * by eight
     * block.
     * @var array
     */
    public array $eight_transform_probabilities = [];
    /**
     * $sixteen_transform_probabilities stores how likely each transform size
     * is for a
     * sixteen by sixteen block, read from the frame's header.
     * @var array
     */
    public array $sixteen_transform_probabilities = [];
    /**
     * $thirty_two_transform_probabilities stores how likely each transform
     * size is for a
     * thirty-two by thirty-two block, read from the frame's header.
     * @var array
     */
    public array $thirty_two_transform_probabilities = [];
    /**
     * $skip_probs stores how likely a block is to carry no values at all.
     * @var array
     */
    public array $skip_probs = [];
    /**
     * readTiles split the tile data that follows the compressed header. Every
     * tile but the last is preceded by a four-byte size, so a correct reading
     * of the tile counts makes the sizes account for the remaining bytes
     * exactly.
     *
     * @param string $frame the frame being built
     */
    private function readTiles(string $frame): void
    {
        $position = $this->uncompressed_header_bytes +
            $this->compressed_header_size;
        $count = (1 << $this->tile_cols_power_of_two) * (1 << $this
            ->tile_rows_power_of_two);
        $end = strlen($frame);
        $this->tiles = [];
        for ($i = 0; $i < $count; $i++) {
            if ($i === $count - 1) {
                $size = $end - $position;
            } else {
                if ($position + 4 > $end) {
                    throw new VideoException(
                'VP9 tile size runs past the frame');
                }
                $size
                    = (ord($frame[$position]) << 24) |
                        (ord($frame[$position + 1]) << 16)
                    | (ord($frame[$position + 2]) << 8) |
                        ord($frame[$position + 3]);
                $position += 4;
            }
            if ($size < 1 || $position + $size > $end) {
                throw new VideoException('VP9 tile runs past the frame');
            }
            $this->tiles[] = [$position, $size];
            $position += $size;
        }
        if ($position !== $end) {
            throw new VideoException(
                'VP9 tile sizes do not account for the frame');
        }
    }
    /**
     * readCompressedHeader the compressed header: transform mode, then
     * probability updates for the transform size, coefficient and skip models.
     * The probability values themselves are not needed to read it, because
     * every update is coded against fixed probabilities, so this parses
     * correctly without the model tables a decoder would need.
     *
     * @param string $frame the frame being built
     */
    private function readCompressedHeader(string $frame): void
    {
        $reader = new Vp9BoolDecoder($frame, $this->uncompressed_header_bytes,
            $this->compressed_header_size);
        if ($this->lossless) {
            /* 4x4 only */
            $this->transform_mode = 0;
        } else {
            $this->transform_mode = $reader->literal(2);
            if ($this->transform_mode === 3) {
                $this->transform_mode += $reader->literal(1);
            }
        }
        $this->eight_transform_probabilities = Vp9Tables::DEFAULT_TX8P;
        $this->sixteen_transform_probabilities = Vp9Tables::DEFAULT_TX16P;
        $this->thirty_two_transform_probabilities = Vp9Tables::DEFAULT_TX32P;
        $this->skip_probs = Vp9Tables::DEFAULT_SKIP;
        $this->value_probs = Vp9Tables::DEFAULT_COEF_PROBS;
        /* select per block */
        if ($this->transform_mode === 4) {
            for ($i = 0; $i < 2; $i++) {
                $this->eight_transform_probabilities[$i] =
                    self::diffUpdateProb($reader,
                    $this->eight_transform_probabilities[$i]);
            }
            for ($i = 0; $i < 2; $i++) {
                for ($j = 0; $j < 2; $j++) {
                    $this->sixteen_transform_probabilities[$i][$j] =
                        self::diffUpdateProb($reader,
                        $this->sixteen_transform_probabilities[$i][$j]);
                }
            }
            for ($i = 0; $i < 2; $i++) {
                for ($j = 0; $j < 3; $j++) {
                    $this->thirty_two_transform_probabilities[$i][$j] =
                        self::diffUpdateProb($reader,
                        $this->thirty_two_transform_probabilities[$i][$j]);
                }
            }
        }
        /* coefficient models, one set per transform size up to the largest the
          */
        /* transform mode allows */
        static $biggest = [0, 1, 2, 3, 3];
        for ($transform_size = 0; $transform_size <= $biggest[$this
            ->transform_mode]; $transform_size++) {
            if ($reader->literal(1) !== 1) {
                continue;
            }
            for ($i = 0; $i < 2; $i++) {
                for ($j = 0; $j < 2; $j++) {
                    for ($k = 0; $k < 6; $k++) {
                        $contexts = ($k === 0) ? 3 : 6;
                        for ($left_bit
                            = 0; $left_bit < $contexts; $left_bit++) {
                            for ($matches = 0; $matches < 3; $matches++) {
                                $probs =
                                    &$this
                                        ->value_probs[$transform_size]
                                        [$i][$j][$k];
                                $probs[$left_bit][$matches] =
                                    self::diffUpdateProb($reader,
                                    $probs[$left_bit][$matches]);
                                unset($probs);
                            }
                        }
                    }
                }
            }
        }
        for ($i = 0; $i < 3; $i++) {
            $this->skip_probs[$i] = self::diffUpdateProb($reader,
                $this->skip_probs[$i]);
        }
        $this->compressed_header_clean = $reader->paddingIsZero();
    }
    /**
     * diffUpdateProb one probability update, coded as a flag then a a form
     * where larger numbers cost more bits delta
     *
     * @param Vp9BoolDecoder $reader the reader the stream's bits are taken from
     * @param int $prob the probability a value is read with
     * @return int what was read
     */
    private static function diffUpdateProb(Vp9BoolDecoder $reader,
        int $prob): int
    {
        if ($reader->readOneBit(252) === 1) {
            return self::invRemapProb(self::decodeTermSubexp($reader), $prob);
        }
        return $prob;
    }
    /**
     * invRemapProb map a coded delta back onto a probability. The delta is
     * expressed relative to the existing value through a fixed permutation,
     * which keeps small changes cheap to code.
     *
     * @param int $delta how much the value changes by
     * @param int $prob the probability a value is read with
     * @return int what was read
     */
    private static function invRemapProb(int $delta, int $prob): int
    {
        $matches = $prob - 1;
        $value = Vp9Tables::INV_MAP[$delta];
        if (($matches << 1) <= 255) {
            return 1 + self::invRecenterNonneg($value, $matches);
        }
        return 255 - self::invRecenterNonneg($value, 254 - $matches);
    }
    /**
     * invRecenterNonneg undoes the way a probability update is written relative
     * to the value it replaces.
     *
     * @param int $value the value read
     * @param int $matches what the pattern found
     * @return int what was read
     */
    private static function invRecenterNonneg(int $value, int $matches): int
    {
        if ($value > 2 * $matches) {
            return $value;
        }
        return ($value & 1) ? $matches - (($value + 1) >> 1) : $matches
            + ($value >> 1);
    }
    /**
     * decodeTermSubexp reads a number written in the format's variable length
     * form, which spends fewer bits on small values.
     *
     * @param Vp9BoolDecoder $reader the reader the stream's bits are taken from
     * @return int what was read
     */
    private static function decodeTermSubexp(Vp9BoolDecoder $reader): int
    {
        if ($reader->literal(1) === 0) {
            return $reader->literal(4);
        }
        if ($reader->literal(1) === 0) {
            return $reader->literal(4) + 16;
        }
        if ($reader->literal(1) === 0) {
            return $reader->literal(5) + 32;
        }
        $value = $reader->literal(7);
        if ($value < 65) {
            return $value + 64;
        }
        return ($value << 1) - 1 + $reader->literal(1);
    }
    /**
     * describe a description of the stream, for the error a caller sees
     *
     * @return string what was read
     */
    public function describe(): string
    {
        return sprintf(
            'VP9 profile %d, %d-bit, %s, %dx%d, base q %d',
            $this->profile,
            $this->bit_depth,
            $this->subsampling_x && $this->subsampling_y ? '4:2:0'
                : ($this->subsampling_x || $this->subsampling_y ? '4:2:2'
                    : '4:4:4'),
            $this->frame_width,
            $this->frame_height,
            $this->base_q_position
        );
    }
}
X