<?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
);
}
}