<?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 turns an H.264 keyframe into a picture. It reads the settings
* the stream carries, guesses each block from its neighbors, adds the
* coded differences, and smooths the block edges at the end.
*/
namespace seekquarry\yioop\library\av_processing;
/**
* H264Nal splits Annex-B, the form an H.264 stream takes on its own (start-code
* delimited) or length-prefixed byte streams into NAL (a unit an H.264 or HEVC
* stream is cut into), a unit an H.264 or HEVC stream is cut into units.
*/
final class H264Nal
{
/**
* unitsFromStream stream was cut into
*
* @return array the units the
* @param string $source the file or bytes being read
*/
public static function unitsFromStream(string $source): array
{
$written = [];
$count = strlen($source);
/* offset of the start-code prefix */
$code_at = [];
$payload_at = [];
for ($i = 0; $i + 3 <= $count; $i++) {
if ($source[$i] === "\x00" && $source[$i + 1] === "\x00") {
if ($source[$i + 2] === "\x01") {
$code_at[] = $i;
$payload_at[] = $i + 3;
$i += 2;
} elseif ($i + 4 <= $count && $source[$i + 2] === "\x00"
&& $source[$i + 3] === "\x01") {
$code_at[] = $i;
$payload_at[] = $i + 4;
$i += 3;
}
}
}
$letter = count($code_at);
for ($k = 0; $k < $letter; $k++) {
$from = $payload_at[$k];
$to = ($k + 1 < $letter) ? $code_at[$k + 1] : $count;
/* trailing_zero_8bits may pad the end of a unit */
while ($to > $from && $source[$to - 1] === "\x00") {
$to--;
}
$unit = substr($source, $from, $to - $from);
if ($unit !== '') {
$written[] = self::parseUnit($unit);
}
}
return $written;
}
/**
* parseUnit reads one unit of the stream and says which kind it is and what
* it holds. A container hands units over one at a time.
*
* @return array what the unit says
* @param string $unit the unit read out of the stream
*/
public static function parseUnit(string $unit): array
{
$high = ord($unit[0]);
return [
'type' => $high & 0x1F,
'refIdc' => ($high >> 5) & 3,
'rbsp' => H264Bits::unescape(substr($unit, 1)),
];
}
}
/**
* H264Exception raised when an H.264 stream cannot be read, either because it
* is damaged or because it uses a feature this decoder does not carry.
*/
class H264Exception extends VideoException {}
/**
* H264Sps a sequence parameter set: the picture size, sample depth, cropping
* and scaling values that hold for a run of pictures.
*/
final class H264Sps
{
/**
* $id stores which numbered set of settings this is. A stream may carry
* several and each slice names the one it uses.
* @var int
*/
public int $id = 0;
/**
* $profile_setting stores which of H.264's profiles the stream uses, which
* says
* what a decoder must be able to do.
* @var int
*/
public int $profile_setting = 0;
/**
* $constraint_flags stores further limits the stream promises to keep
* within, read alongside the profile.
* @var int
*/
public int $constraint_flags = 0;
/**
* $level_setting stores the level the stream claims, which bounds its
* picture
* size and rate.
* @var int
*/
public int $level_setting = 0;
/**
* $chroma_format_setting stores how the color planes are shrunk against the
* brightness: one means half in each direction, which is what most video
* uses.
* @var int
*/
public int $chroma_format_setting = 1;
/**
* $separate_color_plane stores whether the three planes are coded as
* separate pictures rather than together.
* @var bool
*/
public bool $separate_color_plane = false;
/**
* $bit_depth_luma stores how many bits one brightness sample takes.
* @var int
*/
public int $bit_depth_luma = 8;
/**
* $bit_depth_chroma stores how many bits one color sample takes.
* @var int
*/
public int $bit_depth_chroma = 8;
/**
* $lossless_blocks_allowed stores whether a block coded at the lowest
* quantizer skips the transform and stores its samples as they are. A
* stream that throws nothing away uses this; most do not.
* @var bool
*/
public bool $lossless_blocks_allowed = false;
/**
* $frame_number_bits stores how many bits the frame counter takes, so a
* decoder knows when the count wraps back to zero. The stream writes the
* number of bits rather than the largest count.
* @var int
*/
public int $frame_number_bits = 4;
/**
* $picture_order_count_type stores which of three ways the stream
* uses to say what order its pictures are shown in.
* @var int
*/
public int $picture_order_count_type = 0;
/**
* $picture_order_low_bit_count stores how many bits the showing-order
* counter takes, so a decoder knows when the count wraps back to
* zero.
* @var int
*/
public int $picture_order_low_bit_count = 4;
/**
* $delta_picture_order_always_zero stores whether the stream leaves
* out the fields that nudge a picture's place in showing order.
* Where it does, a picture is shown in the order it was decoded.
* @var bool
*/
public bool $delta_picture_order_always_zero = false;
/**
* $count_reference_frames_in_picture_order_cycle stores the number of
* reference frames in
* picture order cycle.
* @var int
*/
public int $count_reference_frames_in_picture_order_cycle = 0;
/**
* $max_count_reference_frames stores the largest number of reference
* frames.
* @var int
*/
public int $max_count_reference_frames = 0;
/**
* $picture_width_in_macroblocks stores the picture width in mbs.
* @var int
*/
public int $picture_width_in_macroblocks = 0;
/**
* $picture_height_in_map_units stores how tall a picture is, counted
* in rows of macroblocks. A decoder works out the height in
* pixels from this and the size of a macroblock.
* @var int
*/
public int $picture_height_in_map_units = 0;
/**
* $frame_macroblocks_only stores whether every picture is a whole frame. A
* stream
* that codes half-pictures is refused, since a thumbnail from one would be
* half a picture.
* @var bool
*/
public bool $frame_macroblocks_only = true;
/**
* $macroblock_adaptive_frame_field stores whether each macroblock may
* choose to hold a whole picture or half of one. This decoder
* reads whole pictures only.
* @var bool
*/
public bool $macroblock_adaptive_frame_field = false;
/**
* $frame_cropping stores whether the shown picture is smaller than the
* coded one, which happens when the size is not a whole number of
* macroblocks.
* @var bool
*/
public bool $frame_cropping = false;
/**
* $crop_left stores how much is cut from the left when it is.
* @var int
*/
public int $crop_left = 0;
/**
* $crop_right stores how much is cut from the right.
* @var int
*/
public int $crop_right = 0;
/**
* $crop_top stores how much is cut from the top.
* @var int
*/
public int $crop_top = 0;
/**
* $crop_bottom stores how much is cut from the bottom.
* @var int
*/
public int $crop_bottom = 0;
/**
* $scaling_matrix_present stores whether the stream carries its own tables
* for scaling values rather than using the fixed ones.
* @var bool
*/
public bool $scaling_matrix_present = false;
/**
* $scaling_values stores those tables, six for the four by four blocks and
* six for the eight by eight ones.
* @var array
*/
public array $scaling_values = [];
/**
* croppedWidth cropped output size
*
* @return int what was read
*/
public function croppedWidth(): int
{
$sub_w = ($this->chroma_format_setting === 3 ||
$this->chroma_format_setting === 0)
? 1 : 2;
return $this->picture_width_in_macroblocks * 16
- $sub_w * ($this->crop_left + $this->crop_right);
}
/**
* croppedHeight height of the picture after the cropping the header asks
* for, which is what a player shows.
*
* @return int what was read
*/
public function croppedHeight(): int
{
$sub_h = ($this->chroma_format_setting === 1) ? 2 : 1;
$mult = $this->frame_macroblocks_only ? 1 : 2;
return $this->picture_height_in_map_units * 16 *
($this->frame_macroblocks_only ? 1 : 2)
- $sub_h * $mult * ($this->crop_top + $this->crop_bottom);
}
}
/**
* H264Pps a picture parameter set: the quantizer offsets, the choice of entropy
* coding, and any scaling values that hold for one picture.
*/
final class H264Pps
{
/**
* $id stores which numbered set of picture settings this is. A slice names
* the one it uses.
* @var int
*/
public int $id = 0;
/**
* $sequence_settings_id stores which sequence settings these picture
* settings belong to.
* @var int
*/
public int $sequence_settings_id = 0;
/**
* $entropy_coding_mode stores whether the stream uses the arithmetic coding
* or the simpler one. The two are read by different classes.
* @var bool
*/
public bool $entropy_coding_mode = false;
/**
* $bottom_field_picture_order_present stores whether a slice
* carries a second showing-order field for the lower half of an
* interlaced picture.
* present.
* @var bool
*/
public bool $bottom_field_picture_order_present = false;
/**
* $count_slice_groups stores the number of slice groups.
* @var int
*/
public int $count_slice_groups = 1;
/**
* $weighted_guess stores whether frames that lean on one other frame weight
* it, which a keyframe never does.
* @var bool
*/
public bool $weighted_guess = false;
/**
* $two_picture_weighting_setting stores how a frame leaning on two other
* pictures weighs them against each other. A frame that stands
* on its own never leans on any, so a decoder reading only
* keyframes passes over it.
* @var int
*/
public int $two_picture_weighting_setting = 0;
/**
* $picture_starting_quantizer stores the quantizer a slice starts from
* before
* its own
* change is added.
* @var int
*/
public int $picture_starting_quantizer = 26;
/**
* $chroma_quantizer_index_offset stores the color quantizer index offset.
* @var int
*/
public int $chroma_quantizer_index_offset = 0;
/**
* $second_chroma_quantizer_index_offset stores the second color quantizer
* index
* offset.
* @var int
*/
public int $second_chroma_quantizer_index_offset = 0;
/**
* $deblocking_filter_control_present stores whether each slice says for
* itself how strongly its block edges are smoothed. Where a stream leaves
* this off, every slice is smoothed the same way.
* @var bool
*/
public bool $deblocking_filter_control_present = false;
/**
* $constrained_self_guessed_guess stores whether a block may be guessed
* from
* neighbors that leaned on another frame.
* @var bool
*/
public bool $constrained_self_guessed_guess = false;
/**
* $redundant_picture_count_present stores whether a slice
* carries a number saying it repeats an earlier picture. A
* stream may send a picture twice so a viewer that lost the
* first copy can still show it.
* @var bool
*/
public bool $redundant_picture_count_present = false;
/**
* $larger_transform_allowed stores whether eight by eight transforms are
* allowed
* as well as four by four.
* @var bool
*/
public bool $larger_transform_allowed = false;
/**
* $scaling_matrix_present stores whether these picture settings carry their
* own tables for scaling values rather than leaning on the sequence ones.
* @var bool
*/
public bool $scaling_matrix_present = false;
/**
* $scaling_values stores those tables, six for four by four blocks and six
* for eight by eight ones.
* @var array
*/
public array $scaling_values = [];
}
/**
* H264ParamParser reads sequence and picture parameter sets, and slice headers,
* out of a stream of coded units.
*/
final class H264ParamParser
{
/**
* scalingList reads one table of weights that a stream carries for
* scaling the values of a block. The weights arrive in the order
* the format visits a block's places, which runs corner to
* corner rather than row by row, and they are handed back in
* that same order
*
* @param H264Bits $bits the reader the stream's bits are taken from
* @param int $size how many bytes
* @param bool $use_default whether the format's own table is used
* @return array what was read
*/
private static function scalingList(H264Bits $bits, int $size, bool
&$use_default): array
{
$list = array_fill(0, $size, 8);
$last_scale = 8;
$next_scale = 8;
$use_default = false;
for ($j = 0; $j < $size; $j++) {
if ($next_scale !== 0) {
$delta = $bits->readSignedNumber();
$next_scale = ($last_scale + $delta + 256) % 256;
if ($j === 0 && $next_scale === 0) {
$use_default = true;
}
}
$list[$j] = ($next_scale === 0) ? $last_scale : $next_scale;
$last_scale = $list[$j];
}
return $list;
}
/**
* scalingMatrix reads every table of scaling weights a stream
* carries, and fills in the fall-
* back rules of 7.4.2.1.1. (eight by eight) lists), null for SPS
*
* @param array $written filled with lists 0..5 (four by four) and 6..11
* @param array $fallback_b PPS fall-back source (SPS
* @param H264Bits $bits the reader the stream's bits are taken from
* @param int $count how many
*/
private static function scalingMatrix(H264Bits $bits, int $count, array
&$written,
?array $fallback_b): void
{
for ($i = 0; $i < $count; $i++) {
$is_eight_by_eight = $i >= 6;
$size = $is_eight_by_eight ? 64 : 16;
$starting_weight_sent = $bits->readBit() === 1;
if ($starting_weight_sent) {
$use_default = false;
$list = self::scalingList($bits, $size, $use_default);
if ($use_default) {
$list = self::defaultList($i);
}
$written[$i] = $list;
continue;
}
/* not present: fall-back rule A (SPS) or B (PPS) */
if ($fallback_b !== null) {
if ($i === 0 || $i === 3 || $i === 6 || $i === 7) {
$written[$i] = $fallback_b[$i] ?? self::defaultList($i);
} else {
$written[$i] = $written[$i - 1];
}
} else {
if ($i === 0 || $i === 3 || $i === 6 || $i === 7) {
$written[$i] = self::defaultList($i);
} else {
$written[$i] = $written[$i - 1];
}
}
}
}
/**
* defaultList the scaling values used when the stream supplies none.
*
* @param int $i which one
* @return array what was read
*/
private static function defaultList(int $i): array
{
if ($i < 3) {
return H264Scan::DEFAULT_4X4_INTRA;
}
if ($i < 6) {
return H264Scan::DEFAULT_4X4_INTER;
}
return ($i % 2 === 0) ? H264Scan::DEFAULT_8X8_INTRA
: H264Scan::DEFAULT_8X8_INTER;
}
/**
* ppsExtensionAllowed works out whether the picture settings
* carry their trailing fields, which say whether eight by eight
* transforms are allowed and may hold tables of scaling weights.
* Asking only whether bits are left over is not enough, since the
* padding at the end of a unit reads as a plausible field. The
* profile the stream claims settles it: profiles that cannot use
* eight by eight transforms never carry those fields.
*
* @param H264Sps $sequence_settings the sequence settings the stream
* @return bool what was read
*/
private static function ppsExtensionAllowed(
H264Sps $sequence_settings): bool
{
$position = $sequence_settings->profile_setting;
if (($position === 66 || $position === 77 || $position === 88)
&& ($sequence_settings->constraint_flags & 0xE0) !== 0) {
return false;
}
return true;
}
/**
* readSequenceSettings reads a sequence parameter set: the picture size,
* the sample depth, the cropping and the scaling values. taken out
*
* @param string $unpacked the unit's bytes with the packing markers
* @return H264Sps what was read
*/
public static function readSequenceSettings(string $unpacked): H264Sps
{
$bits = new H264Bits($unpacked);
$source = new H264Sps();
$source->profile_setting = $bits->readBits(8);
$source->constraint_flags = $bits->readBits(8);
$source->level_setting = $bits->readBits(8);
$source->id = $bits->readWholeNumber();
$high_profiles
= [100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135];
if (in_array($source->profile_setting, $high_profiles, true)) {
$source->chroma_format_setting = $bits->readWholeNumber();
if ($source->chroma_format_setting === 3) {
$source->separate_color_plane = $bits->readBit() === 1;
}
$source->bit_depth_luma = $bits->readWholeNumber() + 8;
$source->bit_depth_chroma = $bits->readWholeNumber() + 8;
$source->lossless_blocks_allowed = $bits->readBit() === 1;
$source->scaling_matrix_present = $bits->readBit() === 1;
if ($source->scaling_matrix_present) {
$count = ($source->chroma_format_setting !== 3) ? 8 : 12;
self::scalingMatrix($bits, $count,
$source->scaling_values, null);
}
}
/* lists not signaled at all default to flat 16 */
for ($i = 0; $i < 12; $i++) {
if (!isset($source->scaling_values[$i])) {
$source->scaling_values[$i] = array_fill(0, $i >= 6 ? 64 : 16,
16);
}
}
$source->frame_number_bits = $bits->readWholeNumber() + 4;
$source->picture_order_count_type = $bits->readWholeNumber();
if ($source->picture_order_count_type === 0) {
$source->picture_order_low_bit_count = $bits
->readWholeNumber() + 4;
} elseif ($source->picture_order_count_type === 1) {
$source->delta_picture_order_always_zero = $bits->readBit() === 1;
/* offset_for_non_ref_pic */
$bits->readSignedNumber();
/* offset_for_top_to_bottom_field */
$bits->readSignedNumber();
$source->count_reference_frames_in_picture_order_cycle = $bits
->readWholeNumber();
for ($i = 0; $i < $source
->count_reference_frames_in_picture_order_cycle; $i++) {
$bits->readSignedNumber();
}
}
$source->max_count_reference_frames = $bits->readWholeNumber();
/* gaps_in_frame_num_value_allowed_flag */
$bits->readBit();
$source->picture_width_in_macroblocks = $bits->readWholeNumber() + 1;
$source->picture_height_in_map_units = $bits->readWholeNumber() + 1;
/* sanity bound so a corrupt sequence header cannot ask for a picture */
/* larger than any defined level (MaxFS for level 6.2 is 139264 MBs) */
if ($source->picture_width_in_macroblocks > 1055 ||
$source->picture_height_in_map_units > 1055
|| $source->picture_width_in_macroblocks *
$source->picture_height_in_map_units > 139264) {
throw new H264Exception('picture size out of range: '
. $source->picture_width_in_macroblocks . 'x' .
$source->picture_height_in_map_units
. ' macroblocks');
}
$source->frame_macroblocks_only = $bits->readBit() === 1;
if (!$source->frame_macroblocks_only) {
$source->macroblock_adaptive_frame_field = $bits->readBit() === 1;
}
/* direct_8x8_inference_flag */
$bits->readBit();
$source->frame_cropping = $bits->readBit() === 1;
if ($source->frame_cropping) {
$source->crop_left = $bits->readWholeNumber();
$source->crop_right = $bits->readWholeNumber();
$source->crop_top = $bits->readWholeNumber();
$source->crop_bottom = $bits->readWholeNumber();
}
/* VUI is not needed for pixel reconstruction */
return $source;
}
/**
* readPictureSettings reads a picture parameter set: the quantizer offsets,
* which entropy coding is used, and any scaling values of its own. taken
* out carries
*
* @param string $unpacked the unit's bytes with the packing markers
* @param H264Sps $sequence_settings the sequence settings the stream
* @return H264Pps what was read
*/
public static function readPictureSettings(string $unpacked,
H264Sps $sequence_settings): H264Pps
{
$bits = new H264Bits($unpacked);
$position = new H264Pps();
$position->id = $bits->readWholeNumber();
$position->sequence_settings_id = $bits->readWholeNumber();
$position->entropy_coding_mode = $bits->readBit() === 1;
$position->bottom_field_picture_order_present = $bits->readBit() === 1;
$position->count_slice_groups = $bits->readWholeNumber() + 1;
if ($position->count_slice_groups > 1) {
throw new H264Exception(
'slices written out of order or in groups are not '
. 'read');
}
/* num_ref_idx_l0_default_active_minus1 */
$bits->readWholeNumber();
/* num_ref_idx_l1_default_active_minus1 */
$bits->readWholeNumber();
$position->weighted_guess = $bits->readBit() === 1;
$position->two_picture_weighting_setting = $bits->readBits(2);
$position->picture_starting_quantizer = $bits->readSignedNumber() + 26;
/* pic_init_qs_minus26 */
$bits->readSignedNumber();
$position->chroma_quantizer_index_offset = $bits->readSignedNumber();
$position->second_chroma_quantizer_index_offset
= $position->chroma_quantizer_index_offset;
$position->deblocking_filter_control_present = $bits->readBit() === 1;
$position->constrained_self_guessed_guess = $bits->readBit() === 1;
$position->redundant_picture_count_present = $bits->readBit() === 1;
$position->scaling_values = $sequence_settings->scaling_values;
if ($bits->hasMoreToRead() &&
self::ppsExtensionAllowed($sequence_settings)) {
$position->larger_transform_allowed = $bits->readBit() === 1;
$position->scaling_matrix_present = $bits->readBit() === 1;
if ($position->scaling_matrix_present) {
$count = 6
+ (($sequence_settings->chroma_format_setting !== 3) ? 2
: 6) * ($position->larger_transform_allowed ? 1 : 0);
$lists = [];
/* fall-back rule set B falls back to the SPS lists only when
the */
/* SPS actually carried a scaling matrix; otherwise to the
defaults */
self::scalingMatrix(
$bits, $count, $lists,
$sequence_settings->scaling_matrix_present
? $sequence_settings->scaling_values : null);
foreach ($lists as $i => $level) {
$position->scaling_values[$i] = $level;
}
}
$position->second_chroma_quantizer_index_offset = $bits
->readSignedNumber();
}
return $position;
}
}
/**
* H264SliceHeader slice header fields the reconstruction path needs.
*/
final class H264SliceHeader
{
/**
* $first_macroblock_in_slice stores which macroblock this slice
* starts at, counting across the picture from its top left.
* @var int
*/
public int $first_macroblock_in_slice = 0;
/**
* $slice_type stores what kind of slice this is. Two and seven
* both mean a slice that stands on its own, which is the only
* kind this decoder reads.
* @var int
*/
public int $slice_type = 0;
/**
* $picture_settings_id stores which picture settings this slice uses.
* @var int
*/
public int $picture_settings_id = 0;
/**
* $frame_count stores which frame of the sequence this is, as the stream
* counts them.
* @var int
*/
public int $frame_count = 0;
/**
* $field_picture stores whether this slice codes half a picture. A stream
* that
* does is refused.
* @var bool
*/
public bool $field_picture = false;
/**
* $slice_quantizer stores the quantizer this slice starts at.
* @var int
*/
public int $slice_quantizer = 26;
/**
* $disable_deblocking_setting stores whether the edges of this slice's
* blocks
* are smoothed, and whether smoothing crosses into the next slice.
* @var int
*/
public int $disable_deblocking_setting = 0;
/**
* $slice_edge_threshold_step stores how far this slice moves the
* threshold an edge must be sharper than before it is smoothed.
* The stream writes half the step, so the value read is doubled.
* @var int
*/
public int $slice_edge_threshold_step = 0;
/**
* $slice_neighbor_threshold_step stores how far this slice moves
* the threshold a neighboring sample must be within before it is
* moved. The stream writes half the step, so it is doubled.
* @var int
*/
public int $slice_neighbor_threshold_step = 0;
/**
* $arithmetic_starting_setting stores which set of starting probabilities
* the
* arithmetic
* coding begins from.
* @var int
*/
public int $arithmetic_starting_setting = 0;
/**
* readSettings reads a slice header, which says which picture parameters
* the slice uses and where in the picture it starts. carries
*
* @param H264Bits $bits the reader the stream's bits are taken from
* @param int $stream_unit_type which kind of unit it is
* @param int $stream_unit_reference_setting how much later frames lean on
* this one
* @param H264Sps $sequence_settings the sequence settings the stream
* @param H264Pps $picture_settings the picture settings the stream carries
* @return self what was read
*/
public static function readSettings(H264Bits $bits, int $stream_unit_type,
int $stream_unit_reference_setting,
H264Sps $sequence_settings, H264Pps $picture_settings): self
{
$header = new self();
$header->first_macroblock_in_slice = $bits->readWholeNumber();
$header->slice_type = $bits->readWholeNumber();
$header->picture_settings_id = $bits->readWholeNumber();
$st = $header->slice_type % 5;
if ($st !== 2 && $st !== 4) {
$kind = $header->slice_type;
throw new H264Exception(
"only I and SI slices are supported (slice_type=$kind)");
}
if ($sequence_settings->separate_color_plane) {
$bits->readBits(2);
}
$header->frame_count =
$bits->readBits($sequence_settings->frame_number_bits);
if (!$sequence_settings->frame_macroblocks_only) {
$header->field_picture = $bits->readBit() === 1;
if ($header->field_picture) {
throw new H264Exception(
'field/interlaced coding is not supported');
}
}
$is_self_contained = ($stream_unit_type === 5);
if ($is_self_contained) {
/* idr_pic_id */
$bits->readWholeNumber();
}
if ($sequence_settings->picture_order_count_type === 0) {
$bits->readBits($sequence_settings
->picture_order_low_bit_count);
if ($picture_settings->bottom_field_picture_order_present &&
!$header->field_picture) {
$bits->readSignedNumber();
}
} elseif ($sequence_settings->picture_order_count_type === 1
&& !$sequence_settings->delta_picture_order_always_zero) {
$bits->readSignedNumber();
if ($picture_settings->bottom_field_picture_order_present &&
!$header->field_picture) {
$bits->readSignedNumber();
}
}
if ($picture_settings->redundant_picture_count_present) {
$bits->readWholeNumber();
}
if ($stream_unit_reference_setting !== 0) {
if ($is_self_contained) {
/* no_output_of_prior_pics_flag */
$bits->readBit();
/* long_term_reference_flag */
$bits->readBit();
} else {
/* adaptive_ref_pic_marking_mode_flag */
if ($bits->readBit() === 1) {
while (true) {
$value = $bits->readWholeNumber();
if ($value === 0) {
break;
}
if ($value === 1 || $value === 3) {
$bits->readWholeNumber();
}
if ($value === 2) {
$bits->readWholeNumber();
}
if ($value === 3 || $value === 6) {
$bits->readWholeNumber();
}
if ($value === 4) {
$bits->readWholeNumber();
}
if ($value === 5) {
break;
}
}
}
}
}
/* cabac_init_idc is absent for I slices */
$header->slice_quantizer = $picture_settings
->picture_starting_quantizer + $bits
->readSignedNumber();
if ($picture_settings->deblocking_filter_control_present) {
$header->disable_deblocking_setting = $bits->readWholeNumber();
if ($header->disable_deblocking_setting !== 1) {
$header->slice_edge_threshold_step = $bits->readSignedNumber();
$header->slice_neighbor_threshold_step = $bits
->readSignedNumber();
}
}
return $header;
}
}
/**
* H264Transform inverse scaling and transforms, clause 8.5 of ITU-T H.264. All
* arrays are flat, row-major.
*/
final class H264Transform
{
/**
* levelScale4x4 the table four by four values are scaled by, worked out
* once for each of the six quantizer remainders from the stream's own
* weights and the fixed adjustments
*
* @param array $weight_zigzag the order the values are written in
* @return array what was read
*/
public static function levelScale4x4(array $weight_zigzag): array
{
/* weight list arrives in zig-zag order; convert to raster */
$wide = array_fill(0, 16, 16);
foreach (H264Scan::ZZ4 as $block_order => $raster) {
$wide[$raster] = $weight_zigzag[$block_order];
}
$level_scale = [];
for ($matches = 0; $matches < 6; $matches++) {
$row = [];
for ($at = 0; $at < 16; $at++) {
$across = $at & 3;
$down = $at >> 2;
$column = (($across & 1) === ($down & 1)) ?
($across & 1) : 2;
$row[$at]
= $wide[$at] * H264Scan::V4[$matches][$column];
}
$level_scale[$matches] = $row;
}
return $level_scale;
}
/**
* levelScale8x8 levelScale8x8[m][64]
*
* @param array $weight_zigzag the order the values are written in
* @return array what was read
*/
public static function levelScale8x8(array $weight_zigzag): array
{
$wide = array_fill(0, 64, 16);
foreach (H264Scan::ZZ8 as $block_order => $raster) {
$wide[$raster] = $weight_zigzag[$block_order];
}
$level_scale = [];
for ($matches = 0; $matches < 6; $matches++) {
$row = [];
for ($at = 0; $at < 64; $at++) {
$across = $at & 7;
$down = $at >> 3;
$column = H264Scan::eightColumnTransform($across, $down);
$row[$at]
= $wide[$at] * H264Scan::V8[$matches][$column];
}
$level_scale[$matches] = $row;
}
return $level_scale;
}
/**
* dequant4x4 scales the coded values of a four by four block back to
* their own size, using the quantizer in force and the weights
* the stream carries. The standard calls
* this clause 8.5.12.1. $skip_first_value leaves index 0 untouched
* (I_16x16 luma and
* chroma, whose DC arrives already scaled from the DC transform).
*
* @param array $chunk the reader this part of the frame is taken from
* @param array $level_scale the table the values are scaled by
* @param int $q_p the quantizer the block was coded at
* @param bool $skip_first_value whether the first value is left out
* @return array what was read
*/
public static function dequant4x4(array $chunk, array $level_scale,
int $q_p,
bool $skip_first_value): array
{
$matches = $q_p % 6;
$source = intdiv($q_p, 6);
$level_scale = $level_scale[$matches];
$payload = array_fill(0, 16, 0);
$start = $skip_first_value ? 1 : 0;
if ($skip_first_value) {
$payload[0] = $chunk[0];
}
if ($q_p >= 24) {
$sh = $source - 4;
for ($i = $start; $i < 16; $i++) {
if ($chunk[$i] !== 0) {
$payload[$i] = ($chunk[$i] * $level_scale[$i]) << $sh;
}
}
} else {
$sh = 4 - $source;
$rounding = 1 << (3 - $source);
for ($i = $start; $i < 16; $i++) {
if ($chunk[$i] !== 0) {
$payload[$i] = ($chunk[$i] * $level_scale[$i] +
$rounding) >> $sh;
}
}
}
return $payload;
}
/**
* dequant8x8 scales the coded values of an eight by eight block back
* to their own size the same arithmetic uses for the smaller
* blocks. The standard calls
* this clause 8.5.13.1.
*
* @param array $chunk the reader this part of the frame is taken from
* @param array $level_scale the table the values are scaled by
* @param int $q_p the quantizer the block was coded at
* @return array what was read
*/
public static function dequant8x8(
array $chunk, array $level_scale, int $q_p): array
{
$matches = $q_p % 6;
$source = intdiv($q_p, 6);
$level_scale = $level_scale[$matches];
$payload = array_fill(0, 64, 0);
if ($q_p >= 36) {
$sh = $source - 6;
for ($i = 0; $i < 64; $i++) {
if ($chunk[$i] !== 0) {
$payload[$i] = ($chunk[$i] * $level_scale[$i]) << $sh;
}
}
} else {
$sh = 6 - $source;
$rounding = 1 << (5 - $source);
for ($i = 0; $i < 64; $i++) {
if ($chunk[$i] !== 0) {
$payload[$i] = ($chunk[$i] * $level_scale[$i] +
$rounding) >> $sh;
}
}
}
return $payload;
}
/**
* inverse4x4 turns the sixteen values of a four by four block back into
* differences from what was guessed, rounded as the standard asks. It calls
* this clause 8.5.12.2.
*
* @param array $payload the bytes the element carries
* @return array what was read
*/
public static function inverse4x4(array $payload): array
{
$entry = [];
for ($i = 0; $i < 4; $i++) {
$offset = $i * 4;
$difference_zero = $payload[$offset];
$difference_one = $payload[$offset + 1];
$difference_two = $payload[$offset + 2];
$difference_three = $payload[$offset + 3];
$edge_zero = $difference_zero + $difference_two;
$edge_one = $difference_zero - $difference_two;
$edge_two = ($difference_one >> 1) - $difference_three;
$edge_three = $difference_one + ($difference_three >> 1);
$entry[$offset] = $edge_zero + $edge_three;
$entry[$offset + 1] = $edge_one + $edge_two;
$entry[$offset + 2] = $edge_one - $edge_two;
$entry[$offset + 3] = $edge_zero - $edge_three;
}
$run = [];
for ($j = 0; $j < 4; $j++) {
$group_zero = $entry[$j];
$group_one = $entry[4 + $j];
$group_two = $entry[8 + $j];
$group_three = $entry[12 + $j];
$half_zero = $group_zero + $group_two;
$half_one = $group_zero - $group_two;
$half_two = ($group_one >> 1) - $group_three;
$half_three = $group_one + ($group_three >> 1);
$run[$j] = ($half_zero + $half_three + 32) >> 6;
$run[4 + $j] = ($half_one + $half_two + 32) >> 6;
$run[8 + $j] = ($half_one - $half_two + 32) >> 6;
$run[12 + $j] = ($half_zero - $half_three + 32) >> 6;
}
ksort($run);
return $run;
}
/**
* inverse8x8 turns the sixty-four values of an eight by eight block
* back into differences from what was guessed. The standard calls
* this clause 8.5.13.2.
*
* @param array $payload the bytes the element carries
* @return array what was read
*/
public static function inverse8x8(array $payload): array
{
$group = [];
for ($i = 0; $i < 8; $i++) {
$offset = $i * 8;
$difference_zero = $payload[$offset]; $difference_one =
$payload[$offset + 1]; $difference_two
= $payload[$offset + 2]; $difference_three
= $payload[$offset + 3];
$difference_four = $payload[$offset + 4]; $difference_five =
$payload[$offset + 5]; $difference_six
= $payload[$offset + 6]; $difference_seven
= $payload[$offset + 7];
$edge_zero = $difference_zero + $difference_four;
$edge_one = -$difference_three + $difference_five -
$difference_seven - ($difference_seven >> 1);
$edge_two = $difference_zero - $difference_four;
$edge_three = $difference_one + $difference_seven -
$difference_three - ($difference_three >> 1);
$edge_four = ($difference_two >> 1) - $difference_six;
$edge_five = -$difference_one + $difference_seven +
$difference_five + ($difference_five >> 1);
$edge_six = $difference_two + ($difference_six >> 1);
$edge_seven = $difference_three + $difference_five +
$difference_one + ($difference_one >> 1);
$filtered_zero = $edge_zero + $edge_six;
$filtered_one = $edge_one + ($edge_seven >> 2);
$filtered_two = $edge_two + $edge_four;
$filtered_three = $edge_three + ($edge_five >> 2);
$filtered_four = $edge_two - $edge_four;
$filtered_five = ($edge_three >> 2) - $edge_five;
$filtered_six = $edge_zero - $edge_six;
$filtered_seven = $edge_seven - ($edge_one >> 2);
$group[$offset] = $filtered_zero + $filtered_seven;
$group[$offset + 1] = $filtered_two + $filtered_five;
$group[$offset + 2] = $filtered_four + $filtered_three;
$group[$offset + 3] = $filtered_six + $filtered_one;
$group[$offset + 4] = $filtered_six - $filtered_one;
$group[$offset + 5] = $filtered_four - $filtered_three;
$group[$offset + 6] = $filtered_two - $filtered_five;
$group[$offset + 7] = $filtered_zero - $filtered_seven;
}
$run = array_fill(0, 64, 0);
for ($j = 0; $j < 8; $j++) {
$difference_zero = $group[$j]; $difference_one = $group[8 +
$j]; $difference_two
= $group[16 + $j]; $difference_three
= $group[24 + $j];
$difference_four = $group[32 + $j]; $difference_five =
$group[40 + $j]; $difference_six
= $group[48 + $j]; $difference_seven
= $group[56 + $j];
$edge_zero = $difference_zero + $difference_four;
$edge_one = -$difference_three + $difference_five -
$difference_seven - ($difference_seven >> 1);
$edge_two = $difference_zero - $difference_four;
$edge_three = $difference_one + $difference_seven -
$difference_three - ($difference_three >> 1);
$edge_four = ($difference_two >> 1) - $difference_six;
$edge_five = -$difference_one + $difference_seven +
$difference_five + ($difference_five >> 1);
$edge_six = $difference_two + ($difference_six >> 1);
$edge_seven = $difference_three + $difference_five +
$difference_one + ($difference_one >> 1);
$filtered_zero = $edge_zero + $edge_six;
$filtered_one = $edge_one + ($edge_seven >> 2);
$filtered_two = $edge_two + $edge_four;
$filtered_three = $edge_three + ($edge_five >> 2);
$filtered_four = $edge_two - $edge_four;
$filtered_five = ($edge_three >> 2) - $edge_five;
$filtered_six = $edge_zero - $edge_six;
$filtered_seven = $edge_seven - ($edge_one >> 2);
$run[$j] = ($filtered_zero + $filtered_seven + 32) >> 6;
$run[8 + $j] = ($filtered_two + $filtered_five + 32) >> 6;
$run[16 + $j] = ($filtered_four + $filtered_three + 32) >> 6;
$run[24 + $j] = ($filtered_six + $filtered_one + 32) >> 6;
$run[32 + $j] = ($filtered_six - $filtered_one + 32) >> 6;
$run[40 + $j] = ($filtered_four - $filtered_three + 32) >> 6;
$run[48 + $j] = ($filtered_two - $filtered_five + 32) >> 6;
$run[56 + $j] = ($filtered_zero - $filtered_seven + 32) >> 6;
}
return $run;
}
/**
* brightnessFirstValues turns the first value of each of a
* macroblock's sixteen brightness blocks back into a difference
* from what was guessed. A macroblock guessed as one whole square
* codes those first values together, so they are read and scaled
* together. The standard calls this clause 8.5.10.
*
* @param array $chunk the reader this part of the frame is taken from
* @param array $level_scale_four_by_four the table four by four values are
* scaled by
* @param int $q_p the quantizer the block was coded at
* @return array what was read
*/
public static function brightnessFirstValues(
array $chunk, array $level_scale_four_by_four, int $q_p): array
{
$frame = [];
for ($i = 0; $i < 4; $i++) {
$offset = $i * 4;
$left_zero = $chunk[$offset] + $chunk[$offset + 1] +
$chunk[$offset + 2]
+ $chunk[$offset + 3];
$left_one = $chunk[$offset] + $chunk[$offset + 1] -
$chunk[$offset + 2]
- $chunk[$offset + 3];
$left_two = $chunk[$offset] - $chunk[$offset + 1] -
$chunk[$offset + 2]
+ $chunk[$offset + 3];
$left_three = $chunk[$offset] - $chunk[$offset + 1] +
$chunk[$offset + 2]
- $chunk[$offset + 3];
$frame[$offset] = $left_zero; $frame[$offset + 1]
= $left_one; $frame[$offset + 2] =
$left_two; $frame[$offset + 3] = $left_three;
}
$group = array_fill(0, 16, 0);
for ($j = 0; $j < 4; $j++) {
$right_zero = $frame[$j] + $frame[4 + $j] + $frame[8 + $j]
+ $frame[12 + $j];
$bit_one = $frame[$j] + $frame[4 + $j] - $frame[8 + $j]
- $frame[12 + $j];
$right_two = $frame[$j] - $frame[4 + $j] - $frame[8 + $j]
+ $frame[12 + $j];
$right_three = $frame[$j] - $frame[4 + $j] + $frame[8 + $j]
- $frame[12 + $j];
$group[$j] = $right_zero; $group[4 + $j] = $bit_one; $group[8 + $j]
= $right_two; $group[12 + $j] = $right_three;
}
$scale = $level_scale_four_by_four[$q_p % 6][0];
$source = intdiv($q_p, 6);
$written = [];
if ($q_p >= 36) {
$sh = $source - 6;
for ($i = 0; $i < 16; $i++) {
$written[$i] = ($group[$i] * $scale) << $sh;
}
} else {
$sh = 6 - $source;
$rounding = 1 << (5 - $source);
for ($i = 0; $i < 16; $i++) {
$written[$i] = ($group[$i] * $scale + $rounding) >> $sh;
}
}
return $written;
}
/**
* chromaDc turns the four first values of a macroblock's color
* blocks back into differences from what was guessed, using a
* transform of sums and differences alone, and scales them by the
* quantizer in force. The standard calls this clause 8.5.11.
*
* @param array $chunk the reader this part of the frame is taken from
* @param array $level_scale_four_by_four the table four by four values are
* scaled by
* @param int $q_p the quantizer the block was coded at
* @return array what was read
*/
public static function chromaDc(
array $chunk, array $level_scale_four_by_four, int $q_p): array
{
$filtered_zero = array_sum(array_slice($chunk, 0, 4));
$filtered_one = $chunk[0] - $chunk[1] + $chunk[2] - $chunk[3];
$filtered_two = $chunk[0] + $chunk[1] - $chunk[2] - $chunk[3];
$filtered_three = $chunk[0] - $chunk[1] - $chunk[2] + $chunk[3];
$scale = $level_scale_four_by_four[$q_p % 6][0];
$source = intdiv($q_p, 6);
$written = [];
foreach ([$filtered_zero, $filtered_one, $filtered_two,
$filtered_three] as $i => $value) {
$written[$i] = (($value * $scale) << $source) >> 5;
}
return $written;
}
}
/**
* H264Intra intra sample prediction, clause 8.3 of ITU-T H.264. Every method
* reads its neighboring samples straight out of the plane being reconstructed
* and returns the predicted block as a flat row-major array. Unavailable
* neighbors are filled with 1 << (how many bits a sample takes - 1) so that a
* corrupt stream degrades instead of crashing; conforming streams never select
* a mode whose neighbors are missing.
*/
final class H264Intra
{
/**
* smooth3 average of three neighboring samples, weighted toward the middle
* one. This is how the format smooths an edge it predicts from.
*
* @param int $first sample before the middle one
* @param int $middle sample the result sits on
* @param int $last sample after the middle one
* @return int the smoothed value
*/
private static function smooth3($first, $middle, $last)
{
return ($first + 2 * $middle + $last + 2) >> 2;
}
/**
* smooth2 average of two neighboring samples, rounded upward.
*
* @param int $first one sample
* @param int $second the sample beside it
* @return int the average
*/
private static function smooth2($first, $second)
{
return ($first + $second + 1) >> 1;
}
/**
* MID is the middle value a sample can take, which is what a block is
* filled with where it has no neighbors to be guessed from.
* @var mixed
*/
private const MID = 128;
/**
* gatherReferences collects the samples above and to the left of a block,
* marking which of them exist. to read to read
*
* @param array $plane zero for luma, one and two for the chroma planes
* @param int $stride how many values one row of the picture takes
* @param int $block_x how far across the frame the block starts
* @param int $block_y how far down the frame the block starts
* @param int $top_count how many values the blocks above carried
* @param int $left_count how many values the blocks to the left carried
* @param bool $left_there whether the block to the left is there to read
* @param bool $above_there whether the block above is there to read
* @param bool $above_left_there whether the block above and left is there
* @param bool $above_right_there whether the block above and right is there
* @param int $top_right_start where the transform starts
* @return array what was read
*/
private static function gatherReferences(
array $plane, int $stride, int $block_x, int $block_y,
int $top_count, int $left_count,
bool $left_there, bool $above_there, bool $above_left_there,
bool $above_right_there, int $top_right_start
): array {
$above = [];
$left = [];
if ($above_there) {
$base = ($block_y - 1) * $stride + $block_x;
for ($across = 0; $across < $top_right_start; $across++) {
$above[$across] = $plane[$base + $across];
}
if ($above_right_there) {
for ($across = $top_right_start; $across <
$top_count; $across++) {
$above[$across] = $plane[$base + $across];
}
} else {
/* 8.3.1.2: substitute with the last available top sample */
$rep = $above[$top_right_start - 1];
for ($across = $top_right_start; $across <
$top_count; $across++) {
$above[$across] = $rep;
}
}
} else {
for ($across = 0; $across < $top_count; $across++) {
$above[$across] = self::MID;
}
}
if ($left_there) {
for ($down = 0; $down < $left_count; $down++) {
$left[$down] = $plane[($block_y + $down) * $stride +
$block_x - 1];
}
} else {
for ($down = 0; $down < $left_count; $down++) {
$left[$down] = self::MID;
}
}
$top_left = $above_left_there ? $plane[($block_y - 1) * $stride +
$block_x - 1] : self::MID;
$above[-1] = $top_left;
$left[-1] = $top_left;
return [$above, $left, $top_left];
}
/**
* holdInsideByte holds a sample inside the range a byte can carry.
*
* @param int $value the value read
* @return int what was read
*/
private static function holdInsideByte(int $value): int
{
return $value < 0 ? 0 : ($value > 255 ? 255 : $value);
}
/**
* pred4x4 guesses one four by four block of brightness from the
* samples above and to the left of it, and hands back the sixteen
* samples it guessed, row by row. The standard calls this clause
* 8.3.1.2.
*
* @param int $mode which way the block is guessed from its neighbors
* @param array $plane which of the picture's planes, brightness or color
* @param int $stride how many values one row of the picture takes
* @param int $block_x how far across the frame the block starts
* @param int $block_y how far down the frame the block starts
* @param bool $left_there whether the block to the left is there to read
* @param bool $above_there whether the block above is there to read
* @param bool $above_left_there whether the block above and left is there
* @param bool $above_right_there whether the block above and right is there
* @return array what was read
*/
public static function pred4x4(
int $mode, array $plane, int $stride, int $block_x, int $block_y,
bool $left_there, bool $above_there, bool $above_left_there,
bool $above_right_there
): array {
[$above, $left, $top_left] = self::gatherReferences(
$plane, $stride, $block_x, $block_y, 8, 4, $left_there,
$above_there, $above_left_there, $above_right_there, 4);
$position = array_fill(0, 16, 0);
switch ($mode) {
/* vertical */
case 0:
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$position[$down * 4 + $across] = $above[$across];
}
}
break;
/* horizontal */
case 1:
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$position[$down * 4 + $across] = $left[$down];
}
}
break;
/* DC */
case 2:
if ($above_there && $left_there) {
$first_value = (array_sum(array_slice($above, 0, 4))
+ array_sum(array_slice($left, 0, 4)) + 4) >> 3;
} elseif ($left_there) {
$first_value = (array_sum(array_slice($left, 0, 4)) +
2) >> 2;
} elseif ($above_there) {
$first_value = (array_sum(array_slice($above, 0, 4)) +
2) >> 2;
} else {
$first_value = self::MID;
}
$position = array_fill(0, 16, $first_value);
break;
/* diagonal down left */
case 3:
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$position[$down * 4 + $across]
= ($across === 3 && $down === 3)
? ($above[6] + 3 * $above[7] + 2) >> 2
: self::smooth3(
$above[$across + $down],
$above[$across + $down + 1],
$above[$across + $down + 2]);
}
}
break;
/* diagonal down right */
case 4:
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
if ($across > $down) {
$value
= self::smooth3(
$above[$across - $down - 2],
$above[$across - $down - 1],
$above[$across - $down]);
} elseif ($across < $down) {
$value
= self::smooth3(
$left[$down - $across - 2],
$left[$down - $across - 1],
$left[$down - $across]);
} else {
$value
= ($above[0] + 2 * $top_left + $left[0] +
2) >> 2;
}
$position[$down * 4 + $across] = $value;
}
}
break;
/* vertical right */
case 5:
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$last = 2 * $across - $down;
$slant = $across - ($down >> 1);
if ($last >= 0 && ($last & 1) === 0) {
$value
= self::smooth2(
$above[$slant - 1], $above[$slant]);
} elseif ($last > 0) {
$value
= self::smooth3(
$above[$slant - 2], $above[$slant - 1],
$above[$slant]);
} elseif ($last === -1) {
$value
= ($left[0] + 2 * $top_left + $above[0] +
2) >> 2;
} else {
$value
= self::smooth3(
$left[$down - 1], $left[$down - 2],
$left[$down - 3]);
}
$position[$down * 4 + $across] = $value;
}
}
break;
/* horizontal down */
case 6:
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$last = 2 * $down - $across;
$slant = $down - ($across >> 1);
if ($last >= 0 && ($last & 1) === 0) {
$value
= self::smooth2(
$left[$slant - 1], $left[$slant]);
} elseif ($last > 0) {
$value
= self::smooth3(
$left[$slant - 2], $left[$slant - 1],
$left[$slant]);
} elseif ($last === -1) {
$value
= ($left[0] + 2 * $top_left + $above[0] +
2) >> 2;
} else {
$value
= self::smooth3(
$above[$across - 1], $above[$across - 2],
$above[$across - 3]);
}
$position[$down * 4 + $across] = $value;
}
}
break;
/* vertical left */
case 7:
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$header = $down >> 1;
$position[$down * 4 + $across] = (($down & 1) === 0)
? self::smooth2(
$above[$across + $header],
$above[$across + $header + 1])
: self::smooth3(
$above[$across + $header],
$above[$across + $header + 1],
$above[$across + $header + 2]);
}
}
break;
/* horizontal up */
case 8:
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$last = $across + 2 * $down;
$header = $across >> 1;
if ($last < 5 && ($last & 1) === 0) {
$value
= self::smooth2(
$left[$down + $header],
$left[$down + $header + 1]);
} elseif ($last < 5) {
$value
= self::smooth3(
$left[$down + $header],
$left[$down + $header + 1],
$left[$down + $header + 2]);
} elseif ($last === 5) {
$value = ($left[2] + 3 * $left[3] + 2) >> 2;
} else {
$value = $left[3];
}
$position[$down * 4 + $across] = $value;
}
}
break;
default:
throw new H264Exception("bad Intra_4x4 mode $mode");
}
return $position;
}
/**
* filter8x8 smooths the samples an eight by eight block is guessed from,
* which the format asks for before the guess is made. The standard calls
* this clause 8.3.2.2.1. to read
*
* @param array $above what the row above holds
* @param array $left what the column to the left holds
* @param int $top_left the tl
* @param bool $left_there whether the block to the left is there to read
* @param bool $above_there whether the block above is there to read
* @param bool $above_left_there whether the block above and left is there
* @return array what was read
*/
private static function filter8x8(array $above, array $left, int $top_left,
bool $left_there, bool $above_there, bool $above_left_there): array
{
$transform = [];
$smoothing = [];
if ($above_left_there) {
if ($above_there && $left_there) {
$top_left_filtered = ($left[0] + 2 * $top_left + $above[0] +
2) >> 2;
} elseif ($above_there) {
$top_left_filtered = (3 * $top_left + $above[0] + 2) >> 2;
} elseif ($left_there) {
$top_left_filtered = (3 * $top_left + $left[0] + 2) >> 2;
} else {
$top_left_filtered = $top_left;
}
} else {
$top_left_filtered = $top_left;
}
if ($above_there) {
$transform[0] = $above_left_there
? ($top_left + 2 * $above[0] + $above[1] + 2) >> 2
: (3 * $above[0] + $above[1] + 2) >> 2;
for ($across = 1; $across < 15; $across++) {
$transform[$across]
= self::smooth3(
$above[$across - 1], $above[$across],
$above[$across + 1]);
}
$transform[15] = ($above[14] + 3 * $above[15] + 2) >> 2;
} else {
for ($across = 0; $across < 16; $across++) {
$transform[$across] = $above[$across];
}
}
if ($left_there) {
$smoothing[0] = $above_left_there
? ($top_left + 2 * $left[0] + $left[1] + 2) >> 2
: (3 * $left[0] + $left[1] + 2) >> 2;
for ($down = 1; $down < 7; $down++) {
$smoothing[$down]
= self::smooth3(
$left[$down - 1], $left[$down], $left[$down + 1]);
}
$smoothing[7] = ($left[6] + 3 * $left[7] + 2) >> 2;
} else {
for ($down = 0; $down < 8; $down++) {
$smoothing[$down] = $left[$down];
}
}
$transform[-1] = $top_left_filtered;
$smoothing[-1] = $top_left_filtered;
return [$transform, $smoothing, $top_left_filtered];
}
/**
* pred8x8 guesses one eight by eight block of brightness from the
* samples above and to the left of it, and hands back its
* sixty-four samples row by row. The standard calls this clause
* 8.3.2.2.
*
* @param int $mode which way the block is guessed from its neighbors
* @param array $plane which of the picture's planes, brightness or color
* @param int $stride how many values one row of the picture takes
* @param int $block_x how far across the frame the block starts
* @param int $block_y how far down the frame the block starts
* @param bool $left_there whether the block to the left is there to read
* @param bool $above_there whether the block above is there to read
* @param bool $above_left_there whether the block above and left is there
* @param bool $above_right_there whether the block above and right is there
* @return array what was read
*/
public static function pred8x8(
int $mode, array $plane, int $stride, int $block_x, int $block_y,
bool $left_there, bool $above_there, bool $above_left_there,
bool $above_right_there
): array {
[$held_zero, $first_pictures, $top_left_zero] = self::gatherReferences(
$plane, $stride, $block_x, $block_y, 16, 8, $left_there,
$above_there, $above_left_there, $above_right_there, 8);
[$above, $left, $top_left]
= self::filter8x8($held_zero, $first_pictures, $top_left_zero,
$left_there, $above_there,
$above_left_there);
$position = array_fill(0, 64, 0);
switch ($mode) {
case 0:
for ($down = 0; $down < 8; $down++) {
for ($across = 0; $across < 8; $across++) {
$position[$down * 8 + $across] = $above[$across];
}
}
break;
case 1:
for ($down = 0; $down < 8; $down++) {
for ($across = 0; $across < 8; $across++) {
$position[$down * 8 + $across] = $left[$down];
}
}
break;
case 2:
if ($above_there && $left_there) {
$source = 0;
for ($i = 0; $i < 8; $i++) {
$source += $above[$i] + $left[$i];
}
$first_value = ($source + 8) >> 4;
} elseif ($above_there) {
$source = 0;
for ($i = 0; $i < 8; $i++) {
$source += $above[$i];
}
$first_value = ($source + 4) >> 3;
} elseif ($left_there) {
$source = 0;
for ($i = 0; $i < 8; $i++) {
$source += $left[$i];
}
$first_value = ($source + 4) >> 3;
} else {
$first_value = self::MID;
}
$position = array_fill(0, 64, $first_value);
break;
case 3:
for ($down = 0; $down < 8; $down++) {
for ($across = 0; $across < 8; $across++) {
$position[$down * 8 + $across]
= ($across === 7 && $down === 7)
? ($above[14] + 3 * $above[15] + 2) >> 2
: self::smooth3(
$above[$across + $down],
$above[$across + $down + 1],
$above[$across + $down + 2]);
}
}
break;
case 4:
for ($down = 0; $down < 8; $down++) {
for ($across = 0; $across < 8; $across++) {
if ($across > $down) {
$value
= self::smooth3(
$above[$across - $down - 2],
$above[$across - $down - 1],
$above[$across - $down]);
} elseif ($across < $down) {
$value
= self::smooth3(
$left[$down - $across - 2],
$left[$down - $across - 1],
$left[$down - $across]);
} else {
$value
= ($above[0] + 2 * $top_left + $left[0] +
2) >> 2;
}
$position[$down * 8 + $across] = $value;
}
}
break;
case 5:
for ($down = 0; $down < 8; $down++) {
for ($across = 0; $across < 8; $across++) {
$last = 2 * $across - $down;
$slant = $across - ($down >> 1);
if ($last >= 0 && ($last & 1) === 0) {
$value
= self::smooth2(
$above[$slant - 1], $above[$slant]);
} elseif ($last > 0) {
$value
= self::smooth3(
$above[$slant - 2], $above[$slant - 1],
$above[$slant]);
} elseif ($last === -1) {
$value
= ($left[0] + 2 * $top_left + $above[0] +
2) >> 2;
} else {
$k = $down - 2 * $across;
$value
= self::smooth3($left[$k - 1], $left[$k - 2],
$left[$k - 3]);
}
$position[$down * 8 + $across] = $value;
}
}
break;
case 6:
for ($down = 0; $down < 8; $down++) {
for ($across = 0; $across < 8; $across++) {
$last = 2 * $down - $across;
$slant = $down - ($across >> 1);
if ($last >= 0 && ($last & 1) === 0) {
$value
= self::smooth2(
$left[$slant - 1], $left[$slant]);
} elseif ($last > 0) {
$value
= self::smooth3(
$left[$slant - 2], $left[$slant - 1],
$left[$slant]);
} elseif ($last === -1) {
$value
= ($left[0] + 2 * $top_left + $above[0] +
2) >> 2;
} else {
$k = $across - 2 * $down;
$value
= self::smooth3(
$above[$k - 1], $above[$k - 2],
$above[$k - 3]);
}
$position[$down * 8 + $across] = $value;
}
}
break;
case 7:
for ($down = 0; $down < 8; $down++) {
for ($across = 0; $across < 8; $across++) {
$header = $down >> 1;
$position[$down * 8 + $across] = (($down & 1) === 0)
? self::smooth2(
$above[$across + $header],
$above[$across + $header + 1])
: self::smooth3(
$above[$across + $header],
$above[$across + $header + 1],
$above[$across + $header + 2]);
}
}
break;
case 8:
for ($down = 0; $down < 8; $down++) {
for ($across = 0; $across < 8; $across++) {
$last = $across + 2 * $down;
$header = $across >> 1;
if ($last < 13 && ($last & 1) === 0) {
$value
= self::smooth2(
$left[$down + $header],
$left[$down + $header + 1]);
} elseif ($last < 13) {
$value
= self::smooth3(
$left[$down + $header],
$left[$down + $header + 1],
$left[$down + $header + 2]);
} elseif ($last === 13) {
$value = ($left[6] + 3 * $left[7] + 2) >> 2;
} else {
$value = $left[7];
}
$position[$down * 8 + $across] = $value;
}
}
break;
default:
throw new H264Exception("bad Intra_8x8 mode $mode");
}
return $position;
}
/**
* pred16x16 guesses a whole sixteen by sixteen macroblock of brightness at
* once, and hands back its samples row by row. The standard calls this
* clause 8.3.3. to read
*
* @param int $mode which way the block is guessed from its neighbors
* @param array $plane which of the picture's planes, brightness or color
* @param int $stride how many values one row of the picture takes
* @param int $block_x how far across the frame the block starts
* @param int $block_y how far down the frame the block starts
* @param bool $left_there whether the block to the left is there to read
* @param bool $above_there whether the block above is there to read
* @param bool $above_left_there whether the block above and left is there
* @return array what was read
*/
public static function pred16x16(
int $mode, array $plane, int $stride, int $block_x, int $block_y,
bool $left_there, bool $above_there, bool $above_left_there
): array {
$above = array_fill(0, 16, self::MID);
$left = array_fill(0, 16, self::MID);
if ($above_there) {
$base = ($block_y - 1) * $stride + $block_x;
for ($across = 0; $across < 16; $across++) {
$above[$across] = $plane[$base + $across];
}
}
if ($left_there) {
for ($down = 0; $down < 16; $down++) {
$left[$down] = $plane[($block_y + $down) * $stride +
$block_x - 1];
}
}
$position = array_fill(0, 256, 0);
switch ($mode) {
/* vertical */
case 0:
for ($down = 0; $down < 16; $down++) {
for ($across = 0; $across < 16; $across++) {
$position[$down * 16 + $across]
= $above[$across] ?? self::MID;
}
}
break;
/* horizontal */
case 1:
for ($down = 0; $down < 16; $down++) {
$value = $left[$down] ?? self::MID;
for ($across = 0; $across < 16; $across++) {
$position[$down * 16 + $across] = $value;
}
}
break;
/* DC */
case 2:
if ($above_there && $left_there) {
$first_value = (array_sum($above) + array_sum($left) +
16) >> 5;
} elseif ($above_there) {
$first_value = (array_sum($above) + 8) >> 4;
} elseif ($left_there) {
$first_value = (array_sum($left) + 8) >> 4;
} else {
$first_value = self::MID;
}
$position = array_fill(0, 256, $first_value);
break;
/* plane */
case 3:
$top_left = $above_left_there ? $plane[($block_y - 1) *
$stride + $block_x - 1] : self::MID;
$above[-1] = $top_left;
$left[-1] = $top_left;
$tall = 0;
$red_part = 0;
for ($i = 0; $i < 8; $i++) {
$tall += ($i + 1) * ($above[8 + $i] - $above[6 - $i]);
$red_part += ($i + 1) * ($left[8 + $i]
- $left[6 - $i]);
}
$amount = 16 * ($left[15] + $above[15]);
$bits = (5 * $tall + 32) >> 6;
$chunk = (5 * $red_part + 32) >> 6;
for ($down = 0; $down < 16; $down++) {
for ($across = 0; $across < 16; $across++) {
$slope = $bits * ($across - 7)
+ $chunk * ($down - 7);
$position[$down * 16 + $across]
= self::holdInsideByte(($amount + $slope
+ 16) >> 5);
}
}
break;
default:
throw new H264Exception("bad Intra_16x16 mode $mode");
}
return $position;
}
/**
* predChroma guesses both color planes of a macroblock from the
* samples above and to the left of it. The ways of guessing are
* numbered differently from brightness: zero fills the block with
* one value, one guesses across, two guesses down, and three
* follows the slope between the two edges. The standard calls this
* clause 8.3.4.
*
* @param int $mode which way the block is guessed from its neighbors
* @param array $plane which of the picture's planes, brightness or color
* @param int $stride how many values one row of the picture takes
* @param int $block_x how far across the frame the block starts
* @param int $block_y how far down the frame the block starts
* @param bool $left_there whether the block to the left is there to read
* @param bool $above_there whether the block above is there to read
* @param bool $above_left_there whether the block above and left is there
* @return array what was read
*/
public static function predChroma(
int $mode, array $plane, int $stride, int $block_x, int $block_y,
bool $left_there, bool $above_there, bool $above_left_there
): array {
$above = array_fill(0, 8, self::MID);
$left = array_fill(0, 8, self::MID);
if ($above_there) {
$base = ($block_y - 1) * $stride + $block_x;
for ($across = 0; $across < 8; $across++) {
$above[$across] = $plane[$base + $across];
}
}
if ($left_there) {
for ($down = 0; $down < 8; $down++) {
$left[$down] = $plane[($block_y + $down) * $stride +
$block_x - 1];
}
}
$position = array_fill(0, 64, 0);
switch ($mode) {
/* DC, computed per 4x4 sub-block */
case 0:
for ($block_y = 0; $block_y < 2; $block_y++) {
for ($block_x = 0; $block_x < 2; $block_x++) {
$x_o = $block_x * 4;
$y_o = $block_y * 4;
$sum_t = 0;
$sum_l = 0;
if ($above_there) {
for ($i = 0; $i < 4; $i++) {
$sum_t += $above[$x_o + $i];
}
}
if ($left_there) {
for ($i = 0; $i < 4; $i++) {
$sum_l += $left[$y_o + $i];
}
}
$corner = ($x_o === 0 && $y_o === 0)
|| ($x_o > 0 && $y_o > 0);
if ($corner) {
if ($above_there && $left_there) {
$first_value = ($sum_t + $sum_l + 4) >> 3;
} elseif ($above_there) {
$first_value = ($sum_t + 2) >> 2;
} elseif ($left_there) {
$first_value = ($sum_l + 2) >> 2;
} else {
$first_value = self::MID;
}
/* top-right sub-block prefers the top row */
} elseif ($x_o > 0) {
if ($above_there) {
$first_value = ($sum_t + 2) >> 2;
} elseif ($left_there) {
$first_value = ($sum_l + 2) >> 2;
} else {
$first_value = self::MID;
}
/* bottom-left sub-block prefers the left column */
} else {
if ($left_there) {
$first_value = ($sum_l + 2) >> 2;
} elseif ($above_there) {
$first_value = ($sum_t + 2) >> 2;
} else {
$first_value = self::MID;
}
}
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$position[($y_o + $down) * 8 + $x_o + $across]
= $first_value;
}
}
}
}
break;
/* horizontal */
case 1:
for ($down = 0; $down < 8; $down++) {
$value = $left[$down] ?? self::MID;
for ($across = 0; $across < 8; $across++) {
$position[$down * 8 + $across] = $value;
}
}
break;
/* vertical */
case 2:
for ($down = 0; $down < 8; $down++) {
for ($across = 0; $across < 8; $across++) {
$position[$down * 8 + $across]
= $above[$across] ?? self::MID;
}
}
break;
/* plane */
case 3:
$top_left = $above_left_there ? $plane[($block_y - 1) *
$stride + $block_x - 1] : self::MID;
$above[-1] = $top_left;
$left[-1] = $top_left;
$tall = 0;
$red_part = 0;
for ($i = 0; $i < 4; $i++) {
$tall += ($i + 1) * ($above[4 + $i] - $above[2 - $i]);
$red_part += ($i + 1) * ($left[4 + $i]
- $left[2 - $i]);
}
$amount = 16 * ($left[7] + $above[7]);
$bits = (34 * $tall + 32) >> 6;
$chunk = (34 * $red_part + 32) >> 6;
for ($down = 0; $down < 8; $down++) {
for ($across = 0; $across < 8; $across++) {
$slope = $bits * ($across - 3)
+ $chunk * ($down - 3);
$position[$down * 8 + $across]
= self::holdInsideByte(($amount + $slope
+ 16) >> 5);
}
}
break;
default:
throw new H264Exception("bad chroma prediction mode $mode");
}
return $position;
}
}
/**
* The H264Cavlc class reads a block's values with the simpler of
* the two codings H.264 offers, which the standard sets out in its
* clause 9.2.
* The tables that turn a run of bits back into a value are built
* once, the first time a block is read, and looked up by how many
* bits were read and what they held.
*/
final class H264Cavlc
{
/**
* $value_token stores the tables that turn the bits of a block's first
* field
* back into how many values it holds. Built once and shared.
* @var array
*/
private static array $value_token = [];
/**
* $total_zeros stores the tables for how many zeros sit before the last
* value.
* @var array
*/
private static array $total_zeros = [];
/**
* $chroma_first_value_total_zeros stores the color first value total zeros.
* @var array
*/
private static array $chroma_first_value_total_zeros = [];
/**
* $run_before stores the tables for how many zeros sit between values.
* @var array
*/
private static array $run_before = [];
/**
* $built stores whether those tables have been built yet, since they are
* made once for the life of the process.
* @var bool
*/
private static bool $built = false;
/**
* buildPicture builds the code tables the coefficients are read with, once
* for the whole run of the program.
*/
private static function buildPicture(): void
{
if (self::$built) {
return;
}
/* coeff_token: tables 0..3 keyed by nC range, table 4 is chroma DC (nC
== -1) */
for ($target = 0; $target < 4; $target++) {
$length = H264Tables::COEFF_TOKEN_LEN[$target];
$bits = H264Tables::COEFF_TOKEN_BITS[$target];
for ($i = 0; $i < 68; $i++) {
if ($length[$i] === 0) {
continue;
}
self::$value_token[$target][$length[$i]][$bits[$i]]
= [intdiv($i, 4), $i % 4];
}
}
$length = H264Tables::CHROMA_DC_TOKEN_LEN;
$bits = H264Tables::CHROMA_DC_TOKEN_BITS;
for ($i = 0; $i < 20; $i++) {
if ($length[$i] === 0) {
continue;
}
self::$value_token[4][$length[$i]][$bits[$i]] = [intdiv($i, 4),
$i % 4];
}
foreach (H264Tables::TOTAL_ZEROS_LEN as $row => $lens) {
foreach ($lens as $value => $level) {
if ($level === 0 && $value !== 0) {
continue;
}
$width = H264Tables::TOTAL_ZEROS_BITS[$row][$value];
self::$total_zeros[$row][$level][$width] = $value;
}
}
foreach (H264Tables::COLOR_FIRST_ZERO_COUNT_LENGTHS as $row => $lens) {
foreach ($lens as $value => $level) {
if ($level === 0) {
continue;
}
$width = H264Tables::COLOR_FIRST_ZERO_COUNT_BITS[$row][$value];
self::$chroma_first_value_total_zeros[$row][$level]
[$width] = $value;
}
}
foreach (H264Tables::RUN_LEN as $row => $lens) {
foreach ($lens as $value => $level) {
if ($level === 0) {
continue;
}
self::$run_before[$row][$level]
[H264Tables::RUN_BITS[$row][$value]] = $value;
}
}
self::$built = true;
}
/**
* readFromCodeTable reads a value written with a code table, where a
* shorter code stands for a commoner value. The table says which value each
* run of bits means.
*
* @param array $map [len][bits] => value
* @param H264Bits $bits the reader the stream's bits are taken from
* @param int $max_length the most bytes that may be read
* @param string $what which kind
* @return mixed the value the map holds for the bits read
*/
private static function readFromCodeTable(
H264Bits $bits, array $map, int $max_length, string $what)
{
for ($level = 1; $level <= $max_length; $level++) {
if (!isset($map[$level])) {
continue;
}
$value = $bits->lookAtBits($level);
if (isset($map[$level][$value])) {
$bits->skipBits($level);
return $map[$level][$value];
}
}
throw new H264Exception("invalid $what code");
}
/**
* residual the readBlockValues method reads the coded values of one block
* with the simpler of the two codings H.264 offers. How many values a block
* holds is written first, then their sizes, then the runs of zeros between
* them, each read from a table chosen by how many values the neighboring
* blocks held. The standard calls this clause 9.2. held, which chooses the
* table; zero less than one asks for the table used for the first values of
* the color planes. AC-only, 4 for chroma DC) how many values the block
* holds]
*
* @param int $n_c How many values the blocks above and to the left
* @param int $max_value number of coefficients in the block (16, 15 for
* @param int $start_position first scan position written (1 for AC-only
* blocks)
* @return array [levels indexed by scan position,
* @param H264Bits $bits the reader the stream's bits are taken from
*/
public static function residual(H264Bits $bits, int $n_c,
int $max_value,
int $start_position): array
{
self::buildPicture();
if ($n_c === -1) {
$table = 4;
} elseif ($n_c < 2) {
$table = 0;
} elseif ($n_c < 4) {
$table = 1;
} elseif ($n_c < 8) {
$table = 2;
} else {
$table = 3;
}
$written = array_fill(0, $start_position + $max_value, 0);
if ($table === 3) {
/* Where the neighbors held eight values or more, the
count is written as a fixed six bit code rather than
through a table: four times one less than how many
values the block holds, plus how many ones sit at its
end, with 3
meaning zero coefficients */
$value = $bits->readBits(6);
if ($value === 3) {
$total_value = 0;
$trailing_ones = 0;
} else {
$total_value = intdiv($value, 4) + 1;
$trailing_ones = $value % 4;
}
} else {
[$total_value, $trailing_ones]
= self::readFromCodeTable(
$bits, self::$value_token[$table], 16, 'coeff_token');
}
if ($total_value === 0) {
return [$written, 0];
}
if ($total_value > $max_value || $trailing_ones > 3) {
throw new H264Exception(
"coeff_token out of range ($total_value/$trailing_ones)");
}
$levels = [];
$suffix_length = ($total_value > 10 && $trailing_ones < 3) ? 1 : 0;
for ($i = 0; $i < $total_value; $i++) {
if ($i < $trailing_ones) {
$levels[$i] = $bits->readBit() === 1 ? -1 : 1;
continue;
}
$starting_weight_scale = 0;
while ($bits->readBit() === 0) {
$starting_weight_scale++;
if ($starting_weight_scale > 32) {
throw new H264Exception('invalid level_prefix');
}
}
$suffix_size = $suffix_length;
if ($starting_weight_scale === 14 && $suffix_length === 0) {
$suffix_size = 4;
} elseif ($starting_weight_scale >= 15) {
$suffix_size = $starting_weight_scale - 3;
}
$level_code = min(15, $starting_weight_scale) << $suffix_length;
if ($suffix_size > 0) {
$level_code += $bits->readBits($suffix_size);
}
if ($starting_weight_scale >= 15 && $suffix_length === 0) {
$level_code += 15;
}
if ($starting_weight_scale >= 16) {
$level_code += (1 << ($starting_weight_scale - 3)) - 4096;
}
if ($i === $trailing_ones && $trailing_ones < 3) {
$level_code += 2;
}
$levels[$i] = ($level_code % 2 === 0)
? ($level_code + 2) >> 1
: (-$level_code - 1) >> 1;
if ($suffix_length === 0) {
$suffix_length = 1;
}
if (abs($levels[$i]) > (3 << ($suffix_length - 1))
&& $suffix_length < 6) {
$suffix_length++;
}
}
$zeros_left = 0;
if ($total_value < $max_value) {
if ($n_c === -1) {
$zeros_left
= self::readFromCodeTable($bits,
self::$chroma_first_value_total_zeros[$total_value
- 1], 8, 'chroma total_zeros');
} else {
$zeros_left
= self::readFromCodeTable($bits,
self::$total_zeros[$total_value
- 1], 16, 'total_zeros');
}
}
$runs = array_fill(0, $total_value, 0);
for ($i = 0; $i < $total_value - 1; $i++) {
if ($zeros_left <= 0) {
break;
}
$row = min($zeros_left, 7) - 1;
$run = self::readFromCodeTable(
$bits, self::$run_before[$row], 11, 'run_before');
$runs[$i] = $run;
$zeros_left -= $run;
}
$runs[$total_value - 1] = $zeros_left;
$value_count = -1;
for ($i = $total_value - 1; $i >= 0; $i--) {
$value_count += $runs[$i] + 1;
$position = $start_position + $value_count;
if ($position >= count($written)) {
throw new H264Exception('coefficient position out of range');
}
$written[$position] = $levels[$i];
}
return [$written, $total_value];
}
}
/**
* The H264Cabac class reads a block's values with the arithmetic
* coding
* H.264 and HEVC may use decoding, clause 9.3 of
* ITU-T H.264.
*
* Reads a slice written with the arithmetic coding: the engine itself,
* and the working out of which probability to use for every
* syntax element that can appear in an I slice.
*/
final class H264Cabac
{
/**
* $bits stores the reader the arithmetic coding takes its bits from.
* @var H264Bits
*/
public H264Bits $bits;
/**
* $range stores how wide the range of values still in play is. Each value
* read narrows it by how likely that value was.
* @var int
*/
private int $range = 510;
/**
* $offset stores where in that range the number being read sits.
* @var int
*/
private int $offset = 0;
/**
* $weight_state stores how likely the more probable value is, one entry for
* each setting the coding keeps.
* @var array
*/
private array $weight_state = [];
/**
* $likelier_value stores which value is the more probable one, one entry
* per setting.
* @var array
*/
private array $likelier_value = [];
/**
* $slice_quantizer stores the quantizer the slice started at, which
* decides the
* probabilities the coding begins from.
* @var int
*/
private int $slice_quantizer = 26;
/**
* $previous_changed_quantizer stores whether the macroblock read before
* this one changed the quantizer. The coding weighs the next such change by
* whether the last one happened, so this is kept from one macroblock to the
* next.
* @var bool
*/
public bool $previous_changed_quantizer = false;
/**
* CTX_MB_TYPE_I is where each kind of decision's probabilities start in the
* one long list the coding keeps. The standard fixes these in its tables
* 9-11 and 9-34.
* @var mixed
*/
private const CTX_MB_TYPE_I = 3;
/**
* CTX_CHROMA_PRED is where the probabilities for chroma pred start in the
* one long list the coding keeps.
* @var mixed
*/
private const CTX_CHROMA_PRED = 64;
/**
* CTX_PREV_INTRA_FLAG is where the probabilities for prev intra flag start
* in the one long list the coding keeps.
* @var mixed
*/
private const CTX_PREV_INTRA_FLAG = 68;
/**
* CTX_REM_INTRA is where the probabilities for rem intra start in the one
* long list the coding keeps.
* @var mixed
*/
private const CTX_REM_INTRA = 69;
/**
* CTX_MB_QP_DELTA is where the probabilities for mb qp delta start in the
* one long list the coding keeps.
* @var mixed
*/
private const CTX_MB_QP_DELTA = 60;
/**
* CTX_CBP_LUMA is where the probabilities for which blocks carry values
* luma start in the one
* long list the coding keeps.
* @var mixed
*/
private const CTX_CBP_LUMA = 73;
/**
* CTX_CBP_CHROMA is where the probabilities for which blocks carry values
* chroma start in the one
* long list the coding keeps.
* @var mixed
*/
private const CTX_CBP_CHROMA = 77;
/**
* CTX_CBF is where the probabilities for cbf start in the one long list the
* coding keeps.
* @var mixed
*/
private const CTX_CBF = 85;
/**
* CTX_SIG is where the probabilities for sig start in the one long list the
* coding keeps.
* @var mixed
*/
private const CTX_SIG = 105;
/**
* CTX_LAST is where the probabilities for last start in the one long list
* the coding keeps.
* @var mixed
*/
private const CTX_LAST = 166;
/**
* CTX_ABS is where the probabilities for abs start in the one long list the
* coding keeps.
* @var mixed
*/
private const CTX_ABS = 227;
/**
* CTX_SIG_8X8 is where the probabilities for sig eight by eight start in
* the one long list the coding keeps.
* @var mixed
*/
private const CTX_SIG_8X8 = 402;
/**
* CTX_LAST_8X8 is where the probabilities for last eight by eight start in
* the one long list the coding keeps.
* @var mixed
*/
private const CTX_LAST_8X8 = 417;
/**
* CTX_ABS_8X8 is where the probabilities for abs eight by eight start in
* the one long list the coding keeps.
* @var mixed
*/
private const CTX_ABS_8X8 = 426;
/**
* CTX_TRANSFORM_8X8 is where the probabilities for transform eight by eight
* start in the one long list the coding keeps.
* @var mixed
*/
private const CTX_TRANSFORM_8X8 = 399;
/**
* CTX_TERMINATE is where the probabilities for terminate start in the one
* long list the coding keeps.
* @var mixed
*/
private const CTX_TERMINATE = 276;
/**
* CBF_CAT_OFFSET is where the probabilities for whether a block carries any
* values start, for each kind of block.
* @var mixed
*/
private const CBF_CAT_OFFSET = [0, 4, 8, 12, 16];
/**
* SIG_CAT_OFFSET is where the probabilities for which places in a block
* hold a value start, for each kind of block.
* @var mixed
*/
private const SIG_CAT_OFFSET = [0, 15, 29, 44, 47];
/**
* ABS_CAT_OFFSET is where the probabilities for how large a value is start,
* for each kind of block.
* @var mixed
*/
private const ABS_CAT_OFFSET = [0, 10, 20, 30, 39];
/**
* __construct sets up the arithmetic decoder for one slice.
*
* @param H264Bits $bits the reader the stream's bits are taken from
*/
public function __construct(H264Bits $bits)
{
$this->bits = $bits;
}
/**
* startReading sets up the arithmetic coding at the beginning of a slice.
* The coding weighs every decision it reads by how likely each answer is,
* and it keeps one such weight for each kind of decision it can make. Those
* weights have to start somewhere: the standard fixes a pair of numbers for
* each kind, and this works the starting weight out from that pair and from
* the quantizer the slice was coded at, since a coarsely coded slice has
* different odds from a finely coded one. It then reads the first nine bits
* of the slice, which set the range the reading works within.
*
* @param int $slice_quantizer The quantizer the slice was coded at.
*/
public function startReading(int $slice_quantizer): void
{
$this->slice_quantizer = $slice_quantizer;
$quantizer =
$slice_quantizer < 0 ? 0 : ($slice_quantizer > 51 ? 51
: $slice_quantizer);
foreach (H264Tables::CTX_INIT_I as $i => [$matches, $number]) {
$starting_weight = ((($matches * $quantizer) >> 4) + $number);
if ($starting_weight < 1) {
$starting_weight = 1;
} elseif ($starting_weight > 126) {
$starting_weight = 126;
}
if ($starting_weight <= 63) {
$this->weight_state[$i] = 63 - $starting_weight;
$this->likelier_value[$i] = 0;
} else {
$this->weight_state[$i] = $starting_weight - 64;
$this->likelier_value[$i] = 1;
}
}
$this->loadFirstBits();
}
/**
* loadFirstBits reads the nine bits that set where the reading begins
* within its range. Every value read afterwards narrows that range, so it
* must be filled before anything else.
*/
private function loadFirstBits(): void
{
$this->range = 510;
$this->offset = $this->bits->readBits(9);
}
/**
* startAfresh sets the arithmetic reading up again after a macroblock whose
* samples were stored as they are, with nothing coded. Such a macroblock is
* written on byte boundaries rather than through the coding, so the range
* and the place within it have to be filled afresh from the bits that
* follow it.
*/
public function startAfresh(): void
{
$this->loadFirstBits();
}
/**
* decodeDecision reads one value with the arithmetic coding. It narrows the
* range by how likely that value was, takes in more bits as the range
* shrinks, and moves the weight for that kind of decision toward whichever
* answer it read. The standard calls this clause 9.3.3.2.1.
*
* @param int $context_at which probability to read with
* @return int the value read, zero or one
*/
public function decodeDecision(int $context_at): int
{
$state = $this->weight_state[$context_at];
$likelier = $this->likelier_value[$context_at];
$quant = ($this->range >> 6) & 3;
$less_likely_range = H264Tables::RANGE_TAB_LPS[$state][$quant];
$this->range -= $less_likely_range;
if ($this->offset >= $this->range) {
$bin = 1 - $likelier;
$this->offset -= $this->range;
$this->range = $less_likely_range;
if ($state === 0) {
$this->likelier_value[$context_at] = 1 - $likelier;
}
$this->weight_state[$context_at] =
H264Tables::TRANS_IDX_LPS[$state];
} else {
$bin = $likelier;
$this->weight_state[$context_at] =
H264Tables::TRANS_IDX_MPS[$state];
}
while ($this->range < 256) {
$this->range <<= 1;
$this->offset = ($this->offset << 1) | $this->bits->readBit();
}
return $bin;
}
/**
* decodeBypass reads one value that the coding treats as equally likely
* either way, which costs a single bit and leaves the probabilities alone.
* The standard calls this clause 9.3.3.2.3.
*
* @return int the value read, zero or one
*/
public function decodeBypass(): int
{
$this->offset = ($this->offset << 1) | $this->bits->readBit();
if ($this->offset >= $this->range) {
$this->offset -= $this->range;
return 1;
}
return 0;
}
/**
* decodeTerminate reads the value that says a slice has ended, which the
* coding writes with a fixed probability. The standard calls this clause
* 9.3.3.2.4.
*
* @return int what was read
*/
public function decodeTerminate(): int
{
$this->range -= 2;
if ($this->offset >= $this->range) {
return 1;
}
while ($this->range < 256) {
$this->range <<= 1;
$this->offset = ($this->offset << 1) | $this->bits->readBit();
}
return 0;
}
/**
* pcmResyncBitPos works out where in the stream a macroblock's plain
* samples begin, for a macroblock whose samples were stored as they are
* rather than coded. After a terminating bin the engine still holds nine
* look-ahead bits that were never consumed, so the reader is wound back
* before byte alignment.
*
* @return int what was read
*/
public function pcmResyncBitPos(): int
{
return $this->bits->position - 7;
}
/**
* neighbors says which macroblocks sit above and to the left of the one
* being decoded, and whether they may be read. A block is guessed from
* those two, and a macroblock in another slice may not be used, so a block
* at a slice edge is guessed from fewer neighbors. number of the one above,
* each zero less than one where there is none to read.
*
* @param H264SliceDecoder $slice The slice being decoded.
* @return array The number of the macroblock to the left and the
*/
private function neighbors(H264SliceDecoder $slice): array
{
$frame = $slice->frameAt();
$across = $slice->macroblockAcross();
$down = $slice->macroblockDown();
$amount = $slice->mbAvail($across - 1, $down)
? ($down * $frame->macroblock_across + $across - 1) : -1;
$bits = $slice->mbAvail($across, $down - 1)
? (($down - 1) * $frame->macroblock_across + $across) : -1;
return [$amount, $bits];
}
/**
* readMacroblockKind mb_type for I slices, Tables 9-36 and 9-39
*
* @param H264SliceDecoder $slice the slice being decoded
* @return int what was read
*/
public function readMacroblockKind(H264SliceDecoder $slice): int
{
$frame = $slice->frameAt();
[$amount, $bits] = $this->neighbors($slice);
$step = 0;
if ($amount >= 0 && $frame->macroblock_kind[$amount] !== 0) {
$step++;
}
if ($bits >= 0 && $frame->macroblock_kind[$bits] !== 0) {
$step++;
}
if ($this->decodeDecision(self::CTX_MB_TYPE_I + $step) === 0) {
/* I_NxN */
return 0;
}
if ($this->decodeTerminate() === 1) {
/* macroblocks whose samples are stored as they are */
return 25;
}
$base = self::CTX_MB_TYPE_I + 2;
$macroblock_kind = 1;
$macroblock_kind += 12 * $this->decodeDecision($base + 1);
if ($this->decodeDecision($base + 2) === 1) {
$macroblock_kind += 4 + 4 * $this->decodeDecision($base + 3);
}
$macroblock_kind += 2 * $this->decodeDecision($base + 4);
$macroblock_kind += $this->decodeDecision($base + 5);
return $macroblock_kind;
}
/**
* decodeTransform8x8 reads whether a macroblock uses the larger transform.
*
* @param H264SliceDecoder $slice the slice being decoded
* @return int what was read
*/
public function decodeTransform8x8(H264SliceDecoder $slice): int
{
$frame = $slice->frameAt();
[$amount, $bits] = $this->neighbors($slice);
$step = 0;
if ($amount >= 0 && $frame->macroblock_larger_transform[$amount] ===
1) {
$step++;
}
if ($bits >= 0 && $frame->macroblock_larger_transform[$bits] === 1) {
$step++;
}
return $this->decodeDecision(self::CTX_TRANSFORM_8X8 + $step);
}
/**
* decodePrevIntraPredModeFlag reads whether a block takes the prediction
* mode its neighbors suggest.
*
* @return int what was read
*/
public function decodePrevIntraPredModeFlag(): int
{
return $this->decodeDecision(self::CTX_PREV_INTRA_FLAG);
}
/**
* decodeRemIntraPredMode reads which way a block is guessed where
* the expected way was refused. Three bits are read with the same
* probability, lowest bit first.
*
* @return int The way of guessing the stream named.
*/
public function decodeRemIntraPredMode(): int
{
$variant = $this->decodeDecision(self::CTX_REM_INTRA);
$variant |= $this->decodeDecision(self::CTX_REM_INTRA) << 1;
$variant |= $this->decodeDecision(self::CTX_REM_INTRA) << 2;
return $variant;
}
/**
* decodeChromaPredMode reads the prediction mode of the chroma planes.
*
* @param H264SliceDecoder $slice the slice being decoded
* @return int what was read
*/
public function decodeChromaPredMode(H264SliceDecoder $slice): int
{
$frame = $slice->frameAt();
[$amount, $bits] = $this->neighbors($slice);
$step = 0;
if ($amount >= 0 && $frame->macroblock_kind[$amount] !== 25
&& $frame->macroblock_chroma_mode[$amount] !== 0) {
$step++;
}
if ($bits >= 0 && $frame->macroblock_kind[$bits] !== 25
&& $frame->macroblock_chroma_mode[$bits] !== 0) {
$step++;
}
if ($this->decodeDecision(self::CTX_CHROMA_PRED + $step) === 0) {
return 0;
}
if ($this->decodeDecision(self::CTX_CHROMA_PRED + 3) === 0) {
return 1;
}
return $this->decodeDecision(self::CTX_CHROMA_PRED + 3) === 0 ? 2 : 3;
}
/**
* readBlocksWithValues reads which parts of a macroblock carry values at
* all. Parts that carry none are filled from the guess alone. carry values]
*
* @return array [which brightness blocks carry values, which color blocks
* @param H264SliceDecoder $slice the slice being decoded
*/
public function readBlocksWithValues(H264SliceDecoder $slice): array
{
$frame = $slice->frameAt();
[$amount, $bits] = $this->neighbors($slice);
$coded_blocks_a = $amount >= 0 ? $frame
->macroblock_coded_blocks_luma[$amount] : -1;
$coded_blocks_b = $bits >= 0 ? $frame
->macroblock_coded_blocks_luma[$bits] : -1;
$plain_samples_a = $amount >= 0 && $frame
->macroblock_kind[$amount] === 25;
$plain_samples_b = $bits >= 0 && $frame->macroblock_kind[$bits] === 25;
/* 9.3.3.1.1.4: condTermFlagN is 0 when the neighboring 8x8 block has
*/
/* residual data (or the neighbor is unavailable or macroblocks whose
samples are stored as they are) */
$bit_of
= function (int $position, int $coded_block_pattern,
bool $plain_samples,
int $block_at) use ($frame): int {
if ($position < 0 || $plain_samples) {
return 0;
}
return (($coded_block_pattern >> $block_at) & 1) !== 0 ? 0 : 1;
};
$coded_block_pattern = 0;
for ($i = 0; $i < 4; $i++) {
/* left neighbor of 8x8 block i */
if (($i & 1) === 1) {
$cond_a = ((($coded_block_pattern >> ($i - 1)) & 1) !== 0)
? 0 : 1;
} else {
$cond_a = $bit_of($amount, $coded_blocks_a,
$plain_samples_a, $i + 1);
}
/* top neighbor of 8x8 block i */
if (($i & 2) === 2) {
$cond_b = ((($coded_block_pattern >> ($i - 2)) & 1) !== 0)
? 0 : 1;
} else {
$cond_b = $bit_of($bits, $coded_blocks_b, $plain_samples_b,
$i + 2);
}
$coded_block_pattern |= $this
->decodeDecision(self::CTX_CBP_LUMA + $cond_a
+ 2 * $cond_b) << $i;
}
$channel_a = $amount >= 0 ? $frame
->macroblock_coded_blocks_chroma[$amount] : 0;
$channel_b = $bits >= 0 ? $frame
->macroblock_coded_blocks_chroma[$bits] : 0;
$corner_zero_a = ($amount >= 0 && ($plain_samples_a || $channel_a !==
0)) ? 1 : 0;
$corner_zero_b = ($bits >= 0 && ($plain_samples_b || $channel_b !==
0)) ? 1 : 0;
$coded_blocks_chroma = 0;
if ($this->decodeDecision(self::CTX_CBP_CHROMA + $corner_zero_a
+ 2 * $corner_zero_b) === 1) {
$corner_one_a = ($amount >= 0 && ($plain_samples_a || $channel_a ===
2)) ? 1 : 0;
$corner_one_b = ($bits >= 0 && ($plain_samples_b || $channel_b ===
2)) ? 1 : 0;
$coded_blocks_chroma = 1
+ $this->decodeDecision(self::CTX_CBP_CHROMA + 4 + $corner_one_a
+ 2 * $corner_one_b);
}
return [$coded_block_pattern, $coded_blocks_chroma];
}
/**
* readQuantizerChange reads how far this macroblock's quantizer sits from
* the slice's.
*
* @return int what was read
*/
public function readQuantizerChange(): int
{
$step = $this->previous_changed_quantizer ? 1 : 0;
if ($this->decodeDecision(self::CTX_MB_QP_DELTA + $step) === 0) {
return 0;
}
$k = 1;
if ($this->decodeDecision(self::CTX_MB_QP_DELTA + 2) === 1) {
$k = 2;
while ($this->decodeDecision(self::CTX_MB_QP_DELTA + 3) === 1) {
$k++;
if ($k > 128) {
throw new H264Exception('mb_qp_delta out of range');
}
}
}
$mag = ($k + 1) >> 1;
return ($k & 1) ? $mag : -$mag;
}
/**
* residual reads a block's values with the arithmetic
* coding method reads the coded values of one block with the arithmetic
* coding. It reads first whether the block holds anything at all, then
* which places in it hold a value, then how large each of those is, with
* every decision weighed by probabilities the coding keeps and updates. The
* standard calls this clauses 9.3.2.3 and 9.3.3.1.3. which probabilities to
* read it with. index, or chroma 4x4 index
*
* @param int $block_kind Which kind of block this is, which says
* @param int $max_value number of coefficients in the list
* @param int $start_position scan position of list entry 0
* @param int $block_at luma four by four z-index, luma eight by eight
* @param int $plane 0 = Cb, 1 = Cr (chroma categories only)
* @return array the values the block was coded as
* @param H264SliceDecoder $slice the slice being decoded
*/
public function residual(
H264SliceDecoder $slice, int $block_kind, int $max_value,
int $start_position,
int $block_at,
int $plane
): array {
$written = array_fill(0, $start_position + $max_value, 0);
if ($block_kind !== 5) {
$step = $this->valuePresenceContext($slice, $block_kind,
$block_at, $plane);
$context =
self::CTX_CBF + self::CBF_CAT_OFFSET[$block_kind] + $step;
if ($this->decodeDecision($context) === 0) {
return [$written, 0];
}
}
if ($block_kind === 5) {
$present_base = self::CTX_SIG_8X8;
$last_base = self::CTX_LAST_8X8;
$size_base = self::CTX_ABS_8X8;
} else {
$present_base = self::CTX_SIG + self::SIG_CAT_OFFSET[$block_kind];
$last_base = self::CTX_LAST + self::SIG_CAT_OFFSET[$block_kind];
$size_base = self::CTX_ABS + self::ABS_CAT_OFFSET[$block_kind];
}
$present = array_fill(0, $max_value, 0);
$count_value = $max_value;
for ($i = 0; $i < $count_value - 1; $i++) {
if ($block_kind === 5) {
$present_step = H264Tables::SIG_COEFF_8X8[$i];
$last_step = H264Tables::LAST_COEFF_8X8[$i];
} elseif ($block_kind === 3) {
$present_step = min($i, 2);
$last_step = $present_step;
} else {
$present_step = $i;
$last_step = $i;
}
if ($this->decodeDecision($present_base + $present_step) === 1) {
$present[$i] = 1;
if ($this->decodeDecision($last_base + $last_step) === 1) {
$count_value = $i + 1;
break;
}
}
}
$present[$count_value - 1] = 1;
$count_equal_to_one = 0;
$count_over_one = 0;
$total = 0;
for ($i = $count_value - 1; $i >= 0; $i--) {
if ($present[$i] === 0) {
continue;
}
$step_zero = ($count_over_one !== 0) ? 0 : min(4, 1 +
$count_equal_to_one);
$level = 1;
if ($this->decodeDecision($size_base + $step_zero) === 1) {
$step_n = 5 + min(4 - ($block_kind === 3 ? 1 : 0),
$count_over_one);
$k = 1;
while ($k < 14
&& $this->decodeDecision($size_base + $step_n) === 1) {
$k++;
}
if ($k === 14) {
$k += $this->readLargeValueTail();
}
$level = $k + 1;
$count_over_one++;
} else {
$count_equal_to_one++;
}
if ($this->decodeBypass() === 1) {
$level = -$level;
}
$written[$start_position + $i] = $level;
$total++;
}
return [$written, $total];
}
/**
* readLargeValueTail reads the part of a value that is larger than the
* coding writes through its probabilities. Those bits are read without
* weighing either answer, since a value that large is rare enough that
* weighing would not pay.
*
* @return int The number those bits spell out.
*/
private function readLargeValueTail(): int
{
$k = 0;
$value = 0;
while ($this->decodeBypass() === 1) {
$value += 1 << $k;
$k++;
if ($k > 30) {
throw new H264Exception('coefficient magnitude out of range');
}
}
while ($k > 0) {
$k--;
$value += $this->decodeBypass() << $k;
}
return $value;
}
/**
* valuePresenceContext works out which probability to read the
* whether-this-block-carries-values decision with. The answer
* depends on whether the blocks above and to the left carried any,
* so a block at the edge of a slice is read with a different
* probability. The standard calls this clause 9.3.3.1.1.9.
*
* @param H264SliceDecoder $slice the slice being decoded
* @param int $block_kind which kind of block the values belong to
* @param int $block_at which block within the macroblock
* @param int $plane which of the picture's planes, brightness or color
* @return int what was read
*/
private function valuePresenceContext(H264SliceDecoder $slice,
int $block_kind,
int $block_at,
int $plane): int
{
$frame = $slice->frameAt();
[$amount, $bits] = $this->neighbors($slice);
switch ($block_kind) {
/* Intra16x16 luma DC */
case 0:
$cond_a
= $this->cbfCond($amount < 0,
$amount >= 0 && $frame
->macroblock_kind[$amount] === 25,
$amount >= 0 && $frame->macroblock_kind[$amount] >= 1
&& $frame->macroblock_kind[$amount] <= 24,
$amount >= 0 ? $frame
->has_values_first_value_y[$amount] : 0);
$cond_b
= $this->cbfCond($bits < 0,
$bits >= 0 && $frame->macroblock_kind[$bits] === 25,
$bits >= 0 && $frame->macroblock_kind[$bits] >= 1
&& $frame->macroblock_kind[$bits] <= 24,
$bits >= 0 ? $frame->has_values_first_value_y[$bits] : 0);
return $cond_a + 2 * $cond_b;
case 1:
case 2: {
[$block_value_four, $block_row_four] =
self::xyFromZ($block_at);
$pixel_x = $slice
->macroblockAcross() * 16 + $block_value_four * 4;
$pixel_y = $slice->macroblockDown() * 16 +
$block_row_four * 4;
$left_values = $slice->nnzLumaBlk($pixel_x - 1, $pixel_y,
$block_at);
$above_values = $slice->nnzLumaBlk($pixel_x, $pixel_y - 1,
$block_at);
$cond_a = $this->cbfNeighborLuma($slice, $pixel_x - 1, $pixel_y,
$left_values);
$cond_b = $this->cbfNeighborLuma($slice, $pixel_x, $pixel_y - 1,
$above_values);
return $cond_a + 2 * $cond_b;
}
/* chroma DC */
case 3: {
$first_value_a = $plane === 0
? ($amount >= 0 ? $frame
->has_values_first_value_blue[$amount] : 0)
: ($amount >= 0 ? $frame
->has_values_first_value_red[$amount] : 0);
$first_value_b = $plane === 0 ? ($bits >= 0 ?
$frame->has_values_first_value_blue[$bits] : 0)
: ($bits >= 0 ? $frame
->has_values_first_value_red[$bits] : 0);
$cond_a
= $this->cbfCond($amount < 0,
$amount >= 0 && $frame
->macroblock_kind[$amount] === 25,
$amount >= 0 && $frame
->macroblock_coded_blocks_chroma[$amount] !== 0,
$first_value_a);
$cond_b
= $this->cbfCond($bits < 0,
$bits >= 0 && $frame->macroblock_kind[$bits] === 25,
$bits >= 0 && $frame
->macroblock_coded_blocks_chroma[$bits] !== 0,
$first_value_b);
return $cond_a + 2 * $cond_b;
}
/* chroma AC */
case 4: {
$pixel_x = $slice->macroblockAcross() * 8 + ($block_at & 1) * 4;
$pixel_y = $slice->macroblockDown() * 8 + ($block_at >> 1) * 4;
$cond_a = $this->cbfNeighborChroma($slice, $plane, $pixel_x - 1,
$pixel_y);
$cond_b = $this->cbfNeighborChroma($slice, $plane, $pixel_x,
$pixel_y - 1);
return $cond_a + 2 * $cond_b;
}
}
throw new H264Exception("bad ctxBlockCat $block_kind");
}
/**
* cbfCond works out which set of probabilities to read a coefficient flag
* with, given what the neighboring blocks held. read
*
* @param bool $macroblock_unavailable whether the macroblock is there to
* read
* @param bool $is_plain_samples whether the block's values are stored as
* they are
* @param bool $block_available whether the neighboring block is there to
* @param int $has_values whether the block carries any values at all
* @return int what was read
*/
private function cbfCond(bool $macroblock_unavailable,
bool $is_plain_samples,
bool $block_available, int $has_values): int
{
if ($macroblock_unavailable) {
/* current macroblock is always intra in an I slice */
return 1;
}
if ($is_plain_samples) {
return 1;
}
if (!$block_available) {
return 0;
}
return $has_values !== 0 ? 1 : 0;
}
/**
* cbfNeighborLuma works out whether the luma block beside a given one held
* any coefficients.
*
* @param H264SliceDecoder $slice the slice being decoded
* @param int $pixel_x how far across the frame the pixel is
* @param int $pixel_y how far down the frame the pixel is
* @param int $value_count how many values the block carried
* @return int what was read
*/
private function cbfNeighborLuma(H264SliceDecoder $slice, int $pixel_x,
int $pixel_y,
int $value_count): int
{
$frame = $slice->frameAt();
if ($pixel_x < 0 || $pixel_y < 0 || $pixel_x >= $frame->coded_width
|| $pixel_y >= $frame->coded_height) {
return 1;
}
$macroblock_x = $pixel_x >> 4;
$macroblock_y = $pixel_y >> 4;
$position = $macroblock_y * $frame->macroblock_across + $macroblock_x;
$is_current = ($macroblock_x === $slice
->macroblockAcross() && $macroblock_y === $slice
->macroblockDown());
if (!$is_current && !$slice->mbAvail($macroblock_x, $macroblock_y)) {
return 1;
}
if (!$is_current && $frame->macroblock_kind[$position] === 25) {
return 1;
}
return ($value_count > 0) ? 1 : 0;
}
/**
* cbfNeighborChroma works out whether the chroma block beside a given one
* held any coefficients.
*
* @param H264SliceDecoder $slice the slice being decoded
* @param int $plane zero for luma, one and two for the chroma planes
* @param int $pixel_x how far across the frame the pixel is
* @param int $pixel_y how far down the frame the pixel is
* @return int what was read
*/
private function cbfNeighborChroma(H264SliceDecoder $slice, int $plane,
int $pixel_x, int $pixel_y): int
{
$frame = $slice->frameAt();
if ($pixel_x < 0 || $pixel_y < 0 || $pixel_x >= $frame->color_width
|| $pixel_y >= $frame->color_height) {
return 1;
}
$macroblock_x = $pixel_x >> 3;
$macroblock_y = $pixel_y >> 3;
$position = $macroblock_y * $frame->macroblock_across + $macroblock_x;
$is_current = ($macroblock_x === $slice
->macroblockAcross() && $macroblock_y === $slice
->macroblockDown());
if (!$is_current && !$slice->mbAvail($macroblock_x, $macroblock_y)) {
return 1;
}
if (!$is_current && $frame->macroblock_kind[$position] === 25) {
return 1;
}
$number = $slice->nnzChromaBlk($plane, $pixel_x, $pixel_y);
return ($number > 0) ? 1 : 0;
}
/**
* xyFromZ the column and row of a block from its position in the order the
* format visits them.
*
* @param int $last the last one
* @return array what was read
*/
private static function xyFromZ(int $last): array
{
return [((($last >> 2) & 1) << 1)
+ ($last & 1), ((($last >> 3) & 1) << 1)
+ (($last >> 1) & 1)];
}
}
/**
* H264Frame reconstructed picture plus the per-macroblock state neighbors need.
*/
final class H264Frame
{
/**
* $macroblock_across stores how many macroblocks the frame is across.
* @var int
*/
public int $macroblock_across;
/**
* $macroblock_down stores how many macroblocks the frame is down.
* @var int
*/
public int $macroblock_down;
/**
* $coded_width stores how wide the coded frame is, in samples.
* @var int
*/
public int $coded_width;
/**
* $coded_height stores how tall the coded frame is, in samples.
* @var int
*/
public int $coded_height;
/**
* $color_width stores how wide each color plane is, which is half the
* brightness plane for the usual streams.
* @var int
*/
public int $color_width;
/**
* $color_height stores how tall each color plane is.
* @var int
*/
public int $color_height;
/**
* $luma stores the brightness of every sample in the frame being built.
* @var array
*/
public array $luma;
/**
* $blue stores how blue every sample is, away from gray.
* @var array
*/
public array $blue;
/**
* $red stores how red every sample is, away from gray.
* @var array
*/
public array $red;
/**
* $macroblock_kind stores what kind each macroblock is, kept for the whole
* frame
* because the smoothing afterwards asks about neighbors.
* @var array
*/
public array $macroblock_kind;
/**
* $macroblock_quantizer_y stores the quantizer each macroblock used, which
* the smoothing
* reads to decide how far a value may move.
* @var array
*/
public array $macroblock_quantizer_y;
/**
* $macroblock_quantizer_blue stores the quantizer each macroblock's
* blue plane was coded at, kept per macroblock so the smoothing
* that follows knows how coarse each one is.
* @var array
*/
public array $macroblock_quantizer_blue;
/**
* $macroblock_quantizer_red stores the same for each macroblock's
* red plane.
* @var array
*/
public array $macroblock_quantizer_red;
/**
* $macroblock_larger_transform stores whether each macroblock used the
* larger
* transform.
* @var array
*/
public array $macroblock_larger_transform;
/**
* $macroblock_slice stores slice id, -1 when not yet decoded
* @var array
*/
public array $macroblock_slice;
/**
* $macroblock_coded_blocks_luma stores the macroblock coded block
* brightness.
* @var array
*/
public array $macroblock_coded_blocks_luma;
/**
* $macroblock_coded_blocks_chroma stores the macroblock coded block color.
* @var array
*/
public array $macroblock_coded_blocks_chroma;
/**
* $macroblock_chroma_mode stores how each macroblock's color was guessed.
* @var array
*/
public array $macroblock_chroma_mode;
/**
* $macroblock_smoothing_off stores whether each macroblock's edges are
* smoothed,
* which a slice may turn off.
* @var array
*/
public array $macroblock_smoothing_off;
/**
* $macroblock_alpha_off stores how far each macroblock's smoothing
* threshold is
* nudged.
* @var array
*/
public array $macroblock_alpha_off;
/**
* $macroblock_beta_off stores how far its neighbor threshold is nudged.
* @var array
*/
public array $macroblock_beta_off;
/**
* $brightness_value_count stores how many values each four by four block
* of brightness
* carried. The smoothing looks at it, and so does the reading of the next
* block along.
* @var array
*/
public array $brightness_value_count;
/**
* $four_by_four_guess stores how each four by four block of brightness was
* guessed,
* kept because a block's guess depends on the blocks beside it.
* @var array
*/
public array $four_by_four_guess;
/**
* $blue_value_count stores the value count blue color.
* @var array
*/
public array $blue_value_count;
/**
* $red_value_count stores the value count red color.
* @var array
*/
public array $red_value_count;
/**
* $has_values_first_value_y stores may use) coded_block_flag of the DC
* blocks. The cbf
* first value y.
* @var array
*/
public array $has_values_first_value_y;
/**
* $has_values_first_value_blue stores the cbf first value blue color.
* @var array
*/
public array $has_values_first_value_blue;
/**
* $has_values_first_value_red stores the cbf first value red color.
* @var array
*/
public array $has_values_first_value_red;
/**
* __construct sets up an empty frame of the given size in macroblocks.
*
* @param int $macroblock_across width of the frame in macroblocks
* @param int $macroblock_down height of the frame in macroblocks
*/
public function __construct(int $macroblock_across, int $macroblock_down)
{
$this->macroblock_across = $macroblock_across;
$this->macroblock_down = $macroblock_down;
$this->coded_width = $macroblock_across * 16;
$this->coded_height = $macroblock_down * 16;
$this->color_width = $macroblock_across * 8;
$this->color_height = $macroblock_down * 8;
$this->luma = array_fill(0, $this->coded_width * $this
->coded_height, 0);
$this->blue = array_fill(0, $this->color_width * $this
->color_height, 0);
$this->red = array_fill(0, $this->color_width * $this->color_height, 0);
$number = $macroblock_across * $macroblock_down;
$this->macroblock_kind = array_fill(0, $number, -1);
$this->macroblock_quantizer_y = array_fill(0, $number, 0);
$this->macroblock_quantizer_blue = array_fill(0, $number, 0);
$this->macroblock_quantizer_red = array_fill(0, $number, 0);
$this->macroblock_larger_transform = array_fill(0, $number, 0);
$this->macroblock_slice = array_fill(0, $number, -1);
$this->macroblock_coded_blocks_luma = array_fill(0, $number, 0);
$this->macroblock_coded_blocks_chroma = array_fill(0, $number, 0);
$this->macroblock_chroma_mode = array_fill(0, $number, 0);
$this->macroblock_smoothing_off = array_fill(0, $number, 0);
$this->macroblock_alpha_off = array_fill(0, $number, 0);
$this->macroblock_beta_off = array_fill(0, $number, 0);
$this->has_values_first_value_y = array_fill(0, $number, 0);
$this->has_values_first_value_blue = array_fill(0, $number, 0);
$this->has_values_first_value_red = array_fill(0, $number, 0);
$this->brightness_value_count = array_fill(0,
(4 * $macroblock_across) * (4 * $macroblock_down), 0);
$this->four_by_four_guess = array_fill(0,
(4 * $macroblock_across) * (4 * $macroblock_down), 2);
$this->blue_value_count = array_fill(0,
(2 * $macroblock_across) * (2 * $macroblock_down), 0);
$this->red_value_count = array_fill(0,
(2 * $macroblock_across) * (2 * $macroblock_down), 0);
}
}
/**
* H264SliceDecoder decodes one I slice: macroblock syntax, intra prediction and
* residual reconstruction (clauses 7.3.5, 8.3, 8.5).
*/
final class H264SliceDecoder
{
/**
* $frame stores the frame being built, which each decoded block is written
* into.
* @var H264Frame
*/
public H264Frame $frame;
/**
* $bits stores the reader the slice takes its bits from.
* @var H264Bits
*/
public H264Bits $bits;
/**
* Z-scan index of a four by four luma block from its position in the
* macroblock.
*/
/**
* Z_FROM_XY is computed inline.
* @var mixed
*/
public const Z_FROM_XY = null;
/**
* $sequence_settings stores the settings that cover the whole sequence.
* @var H264Sps
*/
private H264Sps $sequence_settings;
/**
* $picture_settings stores the settings that cover this picture.
* @var H264Pps
*/
private H264Pps $picture_settings;
/**
* $header stores what this slice said about itself.
* @var H264SliceHeader
*/
private H264SliceHeader $header;
/**
* $arithmetic_reader stores the arithmetic reader, or nothing where there
* is none.
* @var H264Cabac
*/
private ?H264Cabac $arithmetic_reader = null;
/**
* $slice_id stores which slice of the picture this is, counted from zero.
* Smoothing may not cross from one slice into another.
* @var int
*/
private int $slice_id;
/**
* $macroblock_x stores which macroblock across is being decoded.
* @var int
*/
private int $macroblock_x = 0;
/**
* $macroblock_y stores which macroblock down is being decoded.
* @var int
*/
private int $macroblock_y = 0;
/**
* $macroblock_position stores that macroblock's number, counting across
* then down.
* @var int
*/
private int $macroblock_position = 0;
/**
* $quantizer_y stores the quantizer in force for brightness, which each
* macroblock
* may change.
* @var int
*/
private int $quantizer_y;
/**
* $quantizer_blue stores the quantizer for the blue color plane, worked
* out from the
* brightness one.
* @var int
*/
private int $quantizer_blue = 0;
/**
* $quantizer_red stores the quantizer for the red color plane.
* @var int
*/
private int $quantizer_red = 0;
/**
* $macroblock_kind stores what kind the macroblock being decoded is, which
* says how
* it was guessed and what it carries. Read by curMbType(), predIntraMode(),
* storeIntraModes().
* @var int
*/
private int $macroblock_kind = 0;
/**
* $is_sixteen_by_sixteen stores whether this macroblock was guessed as one
* sixteen by
* sixteen square rather than in smaller pieces.
* @var bool
*/
private bool $is_sixteen_by_sixteen = false;
/**
* $is_plain_samples stores whether this macroblock's samples were stored
* as they are,
* with nothing coded.
* @var bool
*/
private bool $is_plain_samples = false;
/**
* $whole_macroblock_guess stores how the whole macroblock was guessed,
* where it was
* guessed as one square.
* @var int
*/
private int $whole_macroblock_guess = 0;
/**
* $chroma_mode stores how this macroblock's color was guessed.
* @var int
*/
private int $chroma_mode = 0;
/**
* $coded_blocks_luma stores which four by four groups of the brightness
* carry
* values.
* @var int
*/
private int $coded_blocks_luma = 0;
/**
* $coded_blocks_chroma stores whether the color planes carry values, and
* of what
* kind.
* @var int
*/
private int $coded_blocks_chroma = 0;
/**
* $eight_by_eight_transform stores whether this macroblock used the larger
* transform.
* @var bool
*/
private bool $eight_by_eight_transform = false;
/**
* $bypass stores whether the coding reads this macroblock's values without
* weighing probabilities.
* @var bool
*/
private bool $bypass = false;
/**
* $luma_value stores the values read for the brightness blocks of this
* macroblock: sixteen blocks of sixteen values each, or four blocks of
* sixty-four where the larger transform was used.
* @var array
*/
private array $luma_value = [];
/**
* $luma_first_value stores the first value of each brightness block, which
* is coded
* apart from the rest when the macroblock is one square.
* @var array
*/
private array $luma_first_value = [];
/**
* $chroma_first_value stores the first value of each color block of this
* macroblock. Those first values are coded together, apart from
* the rest, so they are read and scaled together. The
* same way.
* @var array
*/
private array $chroma_first_value = [[], []];
/**
* $chroma_value stores the color values of the macroblock apart
* from the first of each block, which are read together and kept
* in $chroma_first_value.
* @var array
*/
private array $chroma_value = [[], []];
/**
* $ls_block_row_four stores the table brightness values are scaled by, for
* four
* by four
* blocks. Worked out once for the slice.
* @var array
*/
private array $ls_block_row_four;
/**
* $level_scale_eight_by_eight stores the table an eight by eight
* brightness block's
* values are scaled by, worked out once for the slice from the
* stream's own weights and the quantizer in force.
* @var array
*/
private array $level_scale_eight_by_eight;
/**
* $ls_blue stores the table the blue color values are scaled by.
* @var array
*/
private array $ls_blue;
/**
* $ls_red stores the table the red color values are scaled by.
* @var array
*/
private array $ls_red;
/**
* __construct sets up a decoder for one slice of a picture. coded against
* against
*
* @param H264Frame $frame frame the samples are written into
* @param H264Sps $sequence_settings sequence parameters the picture was
* @param H264Pps $picture_settings picture parameters this slice was coded
* @param H264SliceHeader $header this slice's own header
* @param H264Bits $bits reader positioned at the slice data
* @param int $slice_id number of this slice within the picture
*/
public function __construct(
H264Frame $frame, H264Sps $sequence_settings,
H264Pps $picture_settings, H264SliceHeader $header,
H264Bits $bits, int $slice_id
) {
$this->frame = $frame;
$this->sequence_settings = $sequence_settings;
$this->picture_settings = $picture_settings;
$this->header = $header;
$this->bits = $bits;
$this->slice_id = $slice_id;
$this->quantizer_y = $header->slice_quantizer;
$this->ls_block_row_four =
H264Transform::levelScale4x4($picture_settings->scaling_values[0]);
$this->ls_blue =
H264Transform::levelScale4x4($picture_settings->scaling_values[1]);
$this->ls_red =
H264Transform::levelScale4x4($picture_settings->scaling_values[2]);
$this->level_scale_eight_by_eight
= H264Transform::levelScale8x8($picture_settings->scaling_values[6]
?? array_fill(0, 64, 16));
}
/**
* xyFromZ the column and row of a block from its position in that order.
*
* @param int $last the last one
* @return array what was read
*/
private static function xyFromZ(int $last): array
{
return [((($last >> 2) & 1) << 1)
+ ($last & 1), ((($last >> 3) & 1) << 1)
+ (($last >> 1) & 1)];
}
/**
* mbAvailable says whether a neighboring macroblock can be used for
* prediction, which needs it to exist and to belong to the same slice.
*
* @param int $macroblock_x which macroblock across
* @param int $macroblock_y which macroblock down
* @return bool what was read
*/
private function mbAvailable(int $macroblock_x, int $macroblock_y): bool
{
if ($macroblock_x < 0 || $macroblock_y < 0 || $macroblock_x >= $this
->frame->macroblock_across
|| $macroblock_y >= $this->frame->macroblock_down) {
return false;
}
return $this->frame->macroblock_slice[$macroblock_y * $this->frame
->macroblock_across
+ $macroblock_x] === $this->slice_id;
}
/** is the luma sample at (px,py) already reconstructed and usable for
prediction? */
/**
* availLuma says whether the luma block beside a given one can be used.
*
* @param int $pixel_x how far across the frame the pixel is
* @param int $pixel_y how far down the frame the pixel is
* @param int $order_position where in that order the reading is
* @return bool what was read
*/
private function availLuma(int $pixel_x, int $pixel_y,
int $order_position): bool
{
if ($pixel_x < 0 || $pixel_y < 0 || $pixel_x >= $this->frame
->coded_width
|| $pixel_y >= $this->frame->coded_height) {
return false;
}
$macroblock_x = $pixel_x >> 4;
$macroblock_y = $pixel_y >> 4;
if ($macroblock_x !== $this->macroblock_x || $macroblock_y !== $this
->macroblock_y) {
return $this->mbAvailable($macroblock_x, $macroblock_y);
}
$block_value_four = ($pixel_x & 15) >> 2;
$block_row_four = ($pixel_y & 15) >> 2;
$last = (($block_row_four >> 1) << 3) +
(($block_value_four >> 1) << 2) +
(($block_row_four & 1) << 1) + ($block_value_four & 1);
return $last < $order_position;
}
/**
* availChroma says whether the chroma block beside a given one can be used.
*
* @param int $pixel_x how far across the frame the pixel is
* @param int $pixel_y how far down the frame the pixel is
* @return bool what was read
*/
private function availChroma(int $pixel_x, int $pixel_y): bool
{
if ($pixel_x < 0 || $pixel_y < 0 || $pixel_x >= $this->frame
->color_width
|| $pixel_y >= $this->frame->color_height) {
return false;
}
$macroblock_x = $pixel_x >> 3;
$macroblock_y = $pixel_y >> 3;
if ($macroblock_x === $this->macroblock_x && $macroblock_y === $this
->macroblock_y) {
/* blocks inside the current macroblock are parsed in order */
return true;
}
return $this->mbAvailable($macroblock_x, $macroblock_y);
}
/**
* useCabac says whether this slice uses arithmetic coding rather than the
* variable length codes.
*
* @return bool what was read
*/
private function useCabac(): bool
{
return $this->arithmetic_reader !== null;
}
/**
* Number of non-zero coefficients in the neighboring luma four by four
* block, or -1 if unavailable.
*/
/**
* nnzLumaAt works out how many coefficients the neighboring luma block
* held, which sets the code lengths for the current one.
*
* @param int $pixel_x how far across the frame the pixel is
* @param int $pixel_y how far down the frame the pixel is
* @param int $order_position where in that order the reading is
* @return int what was read
*/
private function nnzLumaAt(int $pixel_x, int $pixel_y,
int $order_position): int
{
if (!$this->availLuma($pixel_x, $pixel_y, $order_position)) {
return -1;
}
$block_x = $pixel_x >> 2;
$block_y = $pixel_y >> 2;
return $this->frame->brightness_value_count[$block_y * (4 * $this->frame
->macroblock_across) + $block_x];
}
/**
* nnzChromaAt works out how many coefficients the neighboring chroma block
* held.
*
* @param int $plane zero for luma, one and two for the chroma planes
* @param int $pixel_x how far across the frame the pixel is
* @param int $pixel_y how far down the frame the pixel is
* @return int what was read
*/
private function nnzChromaAt(int $plane, int $pixel_x, int $pixel_y): int
{
if (!$this->availChroma($pixel_x, $pixel_y)) {
return -1;
}
$block_x = $pixel_x >> 2;
$block_y = $pixel_y >> 2;
$at = $block_y * (2 * $this->frame->macroblock_across) + $block_x;
return $plane === 0 ? $this->frame->blue_value_count[$at] : $this->frame
->red_value_count[$at];
}
/**
* combineNc combines the counts from the block above and the block to the
* left into the single number the code tables are chosen by.
*
* @param int $left_values what the block to the left carried
* @param int $above_values what the block above carried
* @return int what was read
*/
private static function combineNc(int $left_values, int $above_values): int
{
if ($left_values >= 0 && $above_values >= 0) {
return ($left_values + $above_values + 1) >> 1;
}
if ($left_values >= 0) {
return $left_values;
}
if ($above_values >= 0) {
return $above_values;
}
return 0;
}
/**
* decodePicture decodes every macroblock of the slice into the frame.
*/
public function decodePicture(): void
{
$macroblock_across = $this->frame->macroblock_across;
$total = $macroblock_across * $this->frame->macroblock_down;
$this->macroblock_position = $this->header->first_macroblock_in_slice;
if ($this->picture_settings->entropy_coding_mode) {
$this->bits->alignToByte();
$this->arithmetic_reader = new H264Cabac($this->bits);
$this->arithmetic_reader->startReading($this->header
->slice_quantizer);
}
while (true) {
if ($this->macroblock_position >= $total) {
break;
}
$this->macroblock_x = $this
->macroblock_position % $macroblock_across;
$this->macroblock_y = intdiv($this->macroblock_position,
$macroblock_across);
$this->decodeMacroblock();
$this->frame->macroblock_slice[$this->macroblock_position] = $this
->slice_id;
$this->frame->macroblock_smoothing_off[$this->macroblock_position]
= $this->header->disable_deblocking_setting;
$this->frame->macroblock_alpha_off[$this->macroblock_position]
= $this->header->slice_edge_threshold_step * 2;
$this->frame->macroblock_beta_off[$this->macroblock_position]
= $this->header->slice_neighbor_threshold_step * 2;
$this->macroblock_position++;
if ($this->useCabac()) {
if ($this->arithmetic_reader->decodeTerminate() === 1) {
break;
}
} else {
if (!$this->bits->hasMoreToRead()) {
break;
}
}
}
}
/**
* decodeMacroblock decodes one macroblock: its type, its prediction modes,
* its coefficients, and the samples that come out of them.
*/
private function decodeMacroblock(): void
{
$this->luma_value = [];
$this->luma_first_value = array_fill(0, 16, 0);
$this->chroma_first_value = [array_fill(0, 4, 0), array_fill(0, 4, 0)];
$this->chroma_value = [[], []];
$this->eight_by_eight_transform = false;
$this->is_plain_samples = false;
$this->coded_blocks_luma = 0;
$this->coded_blocks_chroma = 0;
$macroblock_kind = $this->useCabac() ? $this->arithmetic_reader
->readMacroblockKind($this)
: $this->bits->readWholeNumber();
$this->macroblock_kind = $macroblock_kind;
if ($macroblock_kind === 25) {
$this->is_plain_samples = true;
$this->decodePcm();
return;
}
$four_by_four_guesses = array_fill(0, 16, 2);
if ($macroblock_kind === 0) {
$this->is_sixteen_by_sixteen = false;
if ($this->picture_settings->larger_transform_allowed) {
$this->eight_by_eight_transform = $this->useCabac()
? $this->arithmetic_reader->decodeTransform8x8($this) === 1
: $this->bits->readBit() === 1;
}
$count = $this->eight_by_eight_transform ? 4 : 16;
for ($i = 0; $i < $count; $i++) {
$block_in_order = $this->eight_by_eight_transform ? $i * 4 : $i;
$predicted = $this->predIntraMode($block_in_order);
if ($this->useCabac()) {
$previous_flag =
$this->arithmetic_reader->decodePrevIntraPredModeFlag();
$mode = $previous_flag === 1 ? $predicted
: $this->arithmetic_reader->decodeRemIntraPredMode();
if ($previous_flag !== 1) {
$mode = $mode + ($mode >= $predicted ? 1 : 0);
}
} else {
if ($this->bits->readBit() === 1) {
$mode = $predicted;
} else {
$remainder = $this->bits->readBits(3);
$mode = $remainder + ($remainder >= $predicted ? 1 : 0);
}
}
if ($this->eight_by_eight_transform) {
for ($k = 0; $k < 4; $k++) {
$four_by_four_guesses[$i * 4 + $k] = $mode;
}
} else {
$four_by_four_guesses[$i] = $mode;
}
/* record immediately: later blocks in this macroblock predict
from it */
$this->storeIntraModes($four_by_four_guesses);
}
} else {
$this->is_sixteen_by_sixteen = true;
$target = $macroblock_kind - 1;
$this->whole_macroblock_guess = $target & 3;
$this->coded_blocks_chroma = intdiv($target, 4) % 3;
$this->coded_blocks_luma = ($target >= 12) ? 15 : 0;
}
$this->storeIntraModes($four_by_four_guesses);
/* intra_chroma_pred_mode */
$this->chroma_mode = $this->useCabac()
? $this->arithmetic_reader->decodeChromaPredMode($this)
: $this->bits->readWholeNumber();
if ($this->chroma_mode > 3) {
throw new H264Exception('bad intra_chroma_pred_mode');
}
$this->frame->macroblock_chroma_mode[$this->macroblock_position] = $this
->chroma_mode;
if (!$this->is_sixteen_by_sixteen) {
if ($this->useCabac()) {
[$this->coded_blocks_luma, $this->coded_blocks_chroma]
= $this->arithmetic_reader->readBlocksWithValues($this);
} else {
$code = $this->bits->readWholeNumber();
if ($code > 47) {
throw new H264Exception('bad coded_block_pattern');
}
$coded_block_pattern =
H264Tables::CODE_TO_BLOCKS_WITH_VALUES[$code];
$this->coded_blocks_luma = $coded_block_pattern & 15;
$this->coded_blocks_chroma = $coded_block_pattern >> 4;
}
if ($this->coded_blocks_luma > 0 &&
$this->picture_settings->larger_transform_allowed
&& !$this->eight_by_eight_transform) {
/* transform_size_8x8_flag can also follow the CBP for Intra_NxN
*/
/* when it was not present earlier; that only happens for inter
*/
/* macroblocks, so nothing to do here. */
}
}
$this->frame->macroblock_coded_blocks_luma[$this->macroblock_position] =
$this->coded_blocks_luma;
$this->frame->macroblock_coded_blocks_chroma[$this
->macroblock_position] = $this->coded_blocks_chroma;
$this->frame->macroblock_larger_transform[$this->macroblock_position] =
$this->eight_by_eight_transform ? 1 : 0;
$this->frame->macroblock_kind[$this->macroblock_position] = $this
->macroblock_kind;
if ($this->coded_blocks_luma > 0 || $this
->coded_blocks_chroma > 0 || $this->is_sixteen_by_sixteen) {
$delta = $this->useCabac() ? $this->arithmetic_reader
->readQuantizerChange()
: $this->bits->readSignedNumber();
if ($this->useCabac()) {
$this->arithmetic_reader->previous_changed_quantizer =
($delta !== 0);
}
if ($delta !== 0) {
$this->quantizer_y = (($this
->quantizer_y + $delta + 52 + 52) % 52);
}
$this->parseResidual();
} else {
if ($this->useCabac()) {
$this->arithmetic_reader->previous_changed_quantizer = false;
}
$this->clearNnz();
}
$this->quantizer_blue = H264Scan::CHROMA_QP[$this
->holdQuantizerInRange($this
->quantizer_y
+ $this->picture_settings->chroma_quantizer_index_offset)];
$this->quantizer_red = H264Scan::CHROMA_QP[$this
->holdQuantizerInRange($this
->quantizer_y
+ $this->picture_settings->second_chroma_quantizer_index_offset)];
$this->bypass =
$this->sequence_settings->lossless_blocks_allowed
&& $this->quantizer_y === 0;
$this->frame->macroblock_quantizer_y[$this->macroblock_position] = $this
->quantizer_y;
$this->frame->macroblock_quantizer_blue[$this->macroblock_position] =
$this->quantizer_blue;
$this->frame->macroblock_quantizer_red[$this->macroblock_position] =
$this->quantizer_red;
$this->reconstruct();
}
/**
* holdQuantizerInRange holds a quantizer index inside the range the format
* allows.
*
* @param int $value the value read
* @return int what was read
*/
private function holdQuantizerInRange(int $value): int
{
return $value < 0 ? 0 : ($value > 51 ? 51 : $value);
}
/**
* storeIntraModes records the prediction modes of a macroblock so its
* neighbors can predict their own from them.
*
* @param array $modes the ways the blocks are guessed
*/
private function storeIntraModes(array $modes): void
{
$stride = 4 * $this->frame->macroblock_across;
$block_start_x = $this->macroblock_x * 4;
$block_start_y = $this->macroblock_y * 4;
for ($last = 0; $last < 16; $last++) {
[$block_value_four, $block_row_four] =
self::xyFromZ($last);
$this->frame
->four_by_four_guess[($block_start_y +
$block_row_four) * $stride + $block_start_x +
$block_value_four] =
($this->macroblock_kind === 0) ? $modes[$last] : 2;
}
}
/**
* predIntraMode works out which way a block is expected to be guessed, from
* the ways its neighbors were guessed. A stream writes only the difference
* from this expectation, so it must be worked out the same way on both
* sides. The standard calls this clause 8.3.1.1. them
*
* @param int $block_in_order which block, in the order the format visits
* @return int what was read
*/
private function predIntraMode(int $block_in_order): int
{
[$block_value_four, $block_row_four] =
self::xyFromZ($block_in_order);
$pixel_x = $this->macroblock_x * 16 + $block_value_four * 4;
$pixel_y = $this->macroblock_y * 16 + $block_row_four * 4;
$stride = 4 * $this->frame->macroblock_across;
$mode_a = 2;
$mode_b = 2;
$ok_a = $this->availLuma($pixel_x - 1, $pixel_y, $block_in_order);
$ok_b = $this->availLuma($pixel_x, $pixel_y - 1, $block_in_order);
if (!$ok_a || !$ok_b) {
return 2;
}
$a_macroblock = (($pixel_y) >> 4) * $this->frame
->macroblock_across + (($pixel_x - 1) >> 4);
$b_macroblock = ((($pixel_y - 1) >> 4) * $this->frame
->macroblock_across) + ($pixel_x >> 4);
$a_is_wide_n_n = ($this->frame->macroblock_kind[$a_macroblock] === 0)
|| ($a_macroblock === $this->macroblock_position && $this
->macroblock_kind === 0);
$b_is_wide_n_n = ($this->frame->macroblock_kind[$b_macroblock] === 0)
|| ($b_macroblock === $this->macroblock_position && $this
->macroblock_kind === 0);
if ($a_is_wide_n_n) {
$mode_a = $this->frame->four_by_four_guess[($pixel_y >> 2) *
$stride +
(($pixel_x - 1) >> 2)];
}
if ($b_is_wide_n_n) {
$mode_b = $this->frame->four_by_four_guess[(($pixel_y - 1) >> 2) *
$stride +
($pixel_x >> 2)];
}
return min($mode_a, $mode_b);
}
/**
* clearNnz forgets the coefficient counts of a macroblock, used when it
* carries none.
*/
private function clearNnz(): void
{
$sum_four = 4 * $this->frame->macroblock_across;
$sum_two = 2 * $this->frame->macroblock_across;
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$this->frame->brightness_value_count[($this->macroblock_y * 4 +
$down) * $sum_four +
$this->macroblock_x * 4
+ $across] = 0;
}
}
for ($down = 0; $down < 2; $down++) {
for ($across = 0; $across < 2; $across++) {
$i = ($this->macroblock_y * 2 + $down) * $sum_two + $this
->macroblock_x * 2 +
$across;
$this->frame->blue_value_count[$i] = 0;
$this->frame->red_value_count[$i] = 0;
}
}
}
/**
* setNnzLuma records how many coefficients a luma block held. them
*
* @param int $block_in_order which block, in the order the format visits
* @param int $value the value read
*/
private function setNnzLuma(int $block_in_order, int $value): void
{
[$block_value_four, $block_row_four] =
self::xyFromZ($block_in_order);
$sum_four = 4 * $this->frame->macroblock_across;
$this->frame->brightness_value_count[($this
->macroblock_y * 4 + $block_row_four) * $sum_four + $this
->macroblock_x *
4 + $block_value_four]
= $value;
}
/**
* setNnzChroma records how many coefficients a chroma block held.
*
* @param int $plane zero for luma, one and two for the chroma planes
* @param int $block_at which block within the macroblock
* @param int $value the value read
*/
private function setNnzChroma(int $plane, int $block_at, int $value): void
{
$sum_two = 2 * $this->frame->macroblock_across;
$i = ($this->macroblock_y * 2 + ($block_at >> 1)) * $sum_two + $this
->macroblock_x * 2 +
($block_at & 1);
if ($plane === 0) {
$this->frame->blue_value_count[$i] = $value;
} else {
$this->frame->red_value_count[$i] = $value;
}
}
/**
* decodePcm reads a macroblock whose samples are stored as they are, with
* no prediction and no transform.
*/
private function decodePcm(): void
{
$this->frame->macroblock_kind[$this->macroblock_position] = 25;
/* 8.7.2.2: an macroblocks whose samples are stored as they are
macroblock contributes qP = 0 to the deblocking */
/* filter, whatever the running slice QP happens to be */
$this->frame->macroblock_quantizer_y[$this->macroblock_position] = 0;
$this->frame->macroblock_coded_blocks_luma[$this
->macroblock_position] = 15;
$this->frame->macroblock_coded_blocks_chroma[$this
->macroblock_position] = 2;
if ($this->useCabac()) {
$this->bits->position = $this->arithmetic_reader->pcmResyncBitPos();
}
$this->bits->alignToByte();
$wide = $this->frame->coded_width;
for ($down = 0; $down < 16; $down++) {
for ($across = 0; $across < 16; $across++) {
$this->frame->luma[($this->macroblock_y * 16 + $down) * $wide
+ $this->macroblock_x * 16
+ $across] = $this->bits->readBits(8);
}
}
$code_word = $this->frame->color_width;
foreach ([0, 1] as $plane) {
for ($down = 0; $down < 8; $down++) {
for ($across = 0; $across < 8; $across++) {
$value = $this->bits->readBits(8);
$i = ($this->macroblock_y * 8 + $down) * $code_word + $this
->macroblock_x * 8
+ $across;
if ($plane === 0) {
$this->frame->blue[$i] = $value;
} else {
$this->frame->red[$i] = $value;
}
}
}
}
for ($last = 0; $last < 16; $last++) {
$this->setNnzLuma($last, 16);
}
for ($bits = 0; $bits < 4; $bits++) {
$this->setNnzChroma(0, $bits, 16);
$this->setNnzChroma(1, $bits, 16);
}
$this->frame->has_values_first_value_y[$this->macroblock_position] = 1;
$this->frame->has_values_first_value_blue[$this
->macroblock_position] = 1;
$this->frame->has_values_first_value_red[$this
->macroblock_position] = 1;
$this->frame->macroblock_quantizer_blue[$this->macroblock_position]
= H264Scan::CHROMA_QP[
$this->holdQuantizerInRange($this->picture_settings
->chroma_quantizer_index_offset)];
$this->frame->macroblock_quantizer_red[$this->macroblock_position]
= H264Scan::CHROMA_QP[
$this->holdQuantizerInRange($this->picture_settings
->second_chroma_quantizer_index_offset)];
if ($this->useCabac()) {
$this->arithmetic_reader->startAfresh();
}
}
/**
* parseResidual reads the coefficients of a macroblock.
*/
private function parseResidual(): void
{
if ($this->is_sixteen_by_sixteen) {
$neighbor_count = $this->lumaNcForBlock(0);
if ($this->useCabac()) {
[$levels, $number]
= $this->arithmetic_reader->residual($this, 0, 16, 0, 0, 0);
} else {
[$levels, $number] = H264Cavlc::residual($this->bits,
$neighbor_count, 16, 0);
}
$this->luma_first_value = $this->blockOrderToRows($levels);
$this->frame->has_values_first_value_y[$this->macroblock_position] =
$number > 0 ? 1 : 0;
} else {
$this->frame->has_values_first_value_y[$this
->macroblock_position] = 0;
}
if ($this->eight_by_eight_transform) {
$this->parseLuma8x8();
} else {
$this->parseLuma4x4();
}
$this->parseChroma();
}
/**
* lumaNcForBlock the neighbor count that picks the code table for one luma
* block. them
*
* @param int $block_in_order which block, in the order the format visits
* @return int what was read
*/
private function lumaNcForBlock(int $block_in_order): int
{
[$block_value_four, $block_row_four] =
self::xyFromZ($block_in_order);
$pixel_x = $this->macroblock_x * 16 + $block_value_four * 4;
$pixel_y = $this->macroblock_y * 16 + $block_row_four * 4;
$left_values = $this->nnzLumaAt($pixel_x - 1, $pixel_y,
$block_in_order);
$above_values = $this->nnzLumaAt($pixel_x, $pixel_y - 1,
$block_in_order);
return self::combineNc($left_values, $above_values);
}
/**
* parseLuma4x4 reads the coefficients of the sixteen small luma blocks.
*/
private function parseLuma4x4(): void
{
$max_value = $this->is_sixteen_by_sixteen ? 15 : 16;
$start = $this->is_sixteen_by_sixteen ? 1 : 0;
for ($eight_by_eight = 0; $eight_by_eight < 4; $eight_by_eight++) {
for ($four_by_four = 0; $four_by_four < 4; $four_by_four++) {
$block_in_order = $eight_by_eight * 4 + $four_by_four;
if (($this->coded_blocks_luma >> $eight_by_eight) & 1) {
$neighbor_count = $this->lumaNcForBlock($block_in_order);
if ($this->useCabac()) {
$block_kind = $this->is_sixteen_by_sixteen ? 1 : 2;
[$levels, $number] = $this->arithmetic_reader->residual(
$this, $block_kind, $max_value, $start,
$block_in_order, 0);
} else {
[$levels, $number] = H264Cavlc::residual(
$this->bits, $neighbor_count, $max_value, $start);
}
$this->luma_value[$block_in_order] =
$this->blockOrderToRows($levels);
$this->setNnzLuma($block_in_order, $number);
} else {
$this->luma_value[$block_in_order] = array_fill(0, 16, 0);
$this->setNnzLuma($block_in_order, 0);
}
}
}
}
/**
* parseLuma8x8 reads the coefficients of the four large luma blocks.
*/
private function parseLuma8x8(): void
{
for ($eight_by_eight = 0; $eight_by_eight < 4; $eight_by_eight++) {
if (!(($this->coded_blocks_luma >> $eight_by_eight) & 1)) {
$this->luma_value[$eight_by_eight] = array_fill(0, 64, 0);
for ($k = 0; $k < 4; $k++) {
$this->setNnzLuma($eight_by_eight * 4 + $k, 0);
}
continue;
}
$scan = array_fill(0, 64, 0);
if ($this->useCabac()) {
[$levels, $number]
= $this->arithmetic_reader->residual($this, 5, 64, 0,
$eight_by_eight, 0);
$scan = $levels;
for ($k = 0; $k < 4; $k++) {
$this->setNnzLuma($eight_by_eight * 4 + $k, $number);
}
} else {
for ($four_by_four = 0; $four_by_four < 4; $four_by_four++) {
$block_in_order = $eight_by_eight * 4 + $four_by_four;
$neighbor_count = $this->lumaNcForBlock($block_in_order);
[$levels, $number]
= H264Cavlc::residual($this->bits, $neighbor_count,
16, 0);
for ($k = 0; $k < 16; $k++) {
$scan[4 * $k + $four_by_four] = $levels[$k];
}
$this->setNnzLuma($block_in_order, $number);
}
}
$raster = array_fill(0, 64, 0);
foreach (H264Scan::ZZ8 as $block_order => $run) {
$raster[$run] = $scan[$block_order];
}
$this->luma_value[$eight_by_eight] = $raster;
}
}
/**
* parseChroma reads the chroma coefficients, whose flat terms are coded
* separately from the rest.
*/
private function parseChroma(): void
{
for ($plane = 0; $plane < 2; $plane++) {
$this->chroma_value[$plane] = [];
for ($bits = 0; $bits < 4; $bits++) {
$this->chroma_value[$plane][$bits] = array_fill(0, 16, 0);
}
}
if ($this->coded_blocks_chroma === 0) {
for ($plane = 0; $plane < 2; $plane++) {
for ($bits = 0; $bits < 4; $bits++) {
$this->setNnzChroma($plane, $bits, 0);
}
}
$this->frame->has_values_first_value_blue[$this
->macroblock_position] = 0;
$this->frame->has_values_first_value_red[$this
->macroblock_position] = 0;
return;
}
for ($plane = 0; $plane < 2; $plane++) {
if ($this->useCabac()) {
[$levels, $number]
= $this->arithmetic_reader->residual($this, 3, 4, 0, 0,
$plane);
} else {
[$levels, $number] = H264Cavlc::residual($this->bits, -1, 4, 0);
}
$this->chroma_first_value[$plane] = $levels;
if ($plane === 0) {
$this->frame->has_values_first_value_blue[$this
->macroblock_position] = $number > 0 ? 1 : 0;
} else {
$this->frame->has_values_first_value_red[$this
->macroblock_position] = $number > 0 ? 1 : 0;
}
}
for ($plane = 0; $plane < 2; $plane++) {
for ($bits = 0; $bits < 4; $bits++) {
if ($this->coded_blocks_chroma === 2) {
$pixel_x = $this->macroblock_x * 8 + ($bits & 1) * 4;
$pixel_y = $this->macroblock_y * 8 + ($bits >> 1) * 4;
$left_values = $this->nnzChromaAt($plane, $pixel_x - 1,
$pixel_y);
$above_values = $this->nnzChromaAt($plane, $pixel_x,
$pixel_y - 1);
$neighbor_count = self::combineNc($left_values,
$above_values);
if ($this->useCabac()) {
[$levels, $number] = $this->arithmetic_reader->residual(
$this, 4, 15, 1, $bits, $plane);
} else {
[$levels, $number]
= H264Cavlc::residual($this->bits, $neighbor_count,
15, 1);
}
$this->chroma_value[$plane][$bits]
= $this->blockOrderToRows($levels);
$this->setNnzChroma($plane, $bits, $number);
} else {
$this->setNnzChroma($plane, $bits, 0);
}
}
}
}
/**
* blockOrderToRows puts the sixteen values of a four by four block
* back into rows. A stream writes them in the order the format
* visits a block's places, which runs corner to corner, so they
* have to be put back before the block is drawn.
*
* @param array $levels scan-order coefficients
* @return array what was read
*/
private function blockOrderToRows(array $levels): array
{
$written = array_fill(0, 16, 0);
foreach (H264Scan::ZZ4 as $block_order => $run) {
$written[$run] = $levels[$block_order] ?? 0;
}
return $written;
}
/**
* reconstruct turns the prediction and the coefficients of a macroblock
* into samples in the frame.
*/
private function reconstruct(): void
{
if ($this->is_sixteen_by_sixteen) {
$this->reconstructI16();
} elseif ($this->eight_by_eight_transform) {
$this->reconstructI8x8();
} else {
$this->reconstructI4x4();
}
$this->reconstructChroma();
}
/**
* bypassAccumulate adds up the stored differences for a block whose
* samples were kept as they are, with no transform. The standard
* calls this clause 8.5.15. With the transform bypassed, vertically and
* horizontally predicted blocks carry storing each value as a difference
* from the one beside it residuals that have to be accumulated along the
* prediction direction. Other prediction modes leave the residual
* untouched.
*
* @param array $run how many values are skipped
* @param int $number which one
* @param int $mode which way the block is guessed from its neighbors
* @param bool $chroma whether this is a color plane
* @return array what was read
*/
private static function bypassAccumulate(array $run, int $number, int $mode,
bool $chroma): array
{
if ($chroma) {
if ($mode === 1) {
$across = true;
} elseif ($mode === 2) {
$across = false;
} else {
return $run;
}
} else {
if ($mode === 1) {
$across = true;
} elseif ($mode === 0) {
$across = false;
} else {
return $run;
}
}
if ($across) {
for ($i = 0; $i < $number; $i++) {
for ($j = 1; $j < $number; $j++) {
$run[$i * $number + $j] += $run[$i * $number + $j - 1];
}
}
} else {
for ($j = 0; $j < $number; $j++) {
for ($i = 1; $i < $number; $i++) {
$run[$i * $number + $j] += $run[($i - 1) * $number + $j];
}
}
}
return $run;
}
/**
* holdInsideByte holds a sample inside the range a byte can carry.
*
* @param int $value the value read
* @return int what was read
*/
private static function holdInsideByte(int $value): int
{
return $value < 0 ? 0 : ($value > 255 ? 255 : $value);
}
/**
* reconstructI4x4 rebuilds a macroblock predicted in four sample squares.
*/
private function reconstructI4x4(): void
{
$wide = $this->frame->coded_width;
$stride = 4 * $this->frame->macroblock_across;
for ($last = 0; $last < 16; $last++) {
[$block_value_four, $block_row_four] =
self::xyFromZ($last);
$pixel_x = $this->macroblock_x * 16 + $block_value_four * 4;
$pixel_y = $this->macroblock_y * 16 + $block_row_four * 4;
$mode = $this->frame->four_by_four_guess[($pixel_y >> 2) * $stride +
($pixel_x >> 2)];
$predicted = H264Intra::pred4x4(
$mode, $this->frame->luma, $wide, $pixel_x, $pixel_y,
$this->availLuma($pixel_x - 1, $pixel_y, $last),
$this->availLuma($pixel_x, $pixel_y - 1, $last),
$this->availLuma($pixel_x - 1, $pixel_y - 1, $last),
$this->availLuma($pixel_x + 4, $pixel_y - 1, $last)
);
$value = $this->luma_value[$last] ?? array_fill(0, 16, 0);
$run = null;
if ($this->bypass) {
$run = self::bypassAccumulate($value, 4, $mode, false);
} elseif ($this->hasNonZero($value)) {
$payload = H264Transform::dequant4x4(
$value, $this->ls_block_row_four, $this->quantizer_y,
false);
$run = H264Transform::inverse4x4($payload);
}
for ($down = 0; $down < 4; $down++) {
$row = ($pixel_y + $down) * $wide + $pixel_x;
for ($across = 0; $across < 4; $across++) {
$value = $predicted[$down * 4 + $across]
+ ($run === null ? 0 : $run[$down * 4 + $across]);
$this->frame->luma[$row
+ $across] = self::holdInsideByte($value);
}
}
}
}
/**
* reconstructI8x8 rebuilds a macroblock predicted in eight sample squares.
*/
private function reconstructI8x8(): void
{
$wide = $this->frame->coded_width;
$stride = 4 * $this->frame->macroblock_across;
for ($eight_by_eight = 0; $eight_by_eight < 4; $eight_by_eight++) {
$value_eight = $eight_by_eight & 1;
$row_eight = $eight_by_eight >> 1;
$pixel_x = $this->macroblock_x * 16 + $value_eight * 8;
$pixel_y = $this->macroblock_y * 16 + $row_eight * 8;
$last = $eight_by_eight * 4;
$mode = $this->frame->four_by_four_guess[($pixel_y >> 2) * $stride +
($pixel_x >> 2)];
$predicted = H264Intra::pred8x8(
$mode, $this->frame->luma, $wide, $pixel_x, $pixel_y,
$this->availLuma($pixel_x - 1, $pixel_y, $last),
$this->availLuma($pixel_x, $pixel_y - 1, $last),
$this->availLuma($pixel_x - 1, $pixel_y - 1, $last),
$this->availLuma($pixel_x + 8, $pixel_y - 1, $last)
);
$value = $this->luma_value[$eight_by_eight] ?? array_fill(0, 64, 0);
$run = null;
if ($this->bypass) {
$run = self::bypassAccumulate($value, 8, $mode, false);
} elseif ($this->hasNonZero($value)) {
$payload = H264Transform::dequant8x8(
$value, $this->level_scale_eight_by_eight, $this
->quantizer_y);
$run = H264Transform::inverse8x8($payload);
}
for ($down = 0; $down < 8; $down++) {
$row = ($pixel_y + $down) * $wide + $pixel_x;
for ($across = 0; $across < 8; $across++) {
$value = $predicted[$down * 8 + $across]
+ ($run === null ? 0 : $run[$down * 8 + $across]);
$this->frame->luma[$row
+ $across] = self::holdInsideByte($value);
}
}
}
}
/**
* reconstructI16 rebuilds a macroblock predicted as one sixteen sample
* square, whose flat terms are transformed together.
*/
private function reconstructI16(): void
{
$wide = $this->frame->coded_width;
$pixel_start_x = $this->macroblock_x * 16;
$pixel_start_y = $this->macroblock_y * 16;
$predicted = H264Intra::pred16x16(
$this->whole_macroblock_guess, $this->frame->luma, $wide,
$pixel_start_x, $pixel_start_y,
$this->mbAvailable($this->macroblock_x - 1, $this->macroblock_y),
$this->mbAvailable($this->macroblock_x, $this->macroblock_y - 1),
$this->mbAvailable($this->macroblock_x - 1, $this->macroblock_y - 1)
);
$first_value = $this->bypass
? $this->luma_first_value
: H264Transform::brightnessFirstValues($this->luma_first_value,
$this
->ls_block_row_four, $this->quantizer_y);
if ($this->bypass) {
$result = array_fill(0, 256, 0);
for ($last = 0; $last < 16; $last++) {
[$block_value_four, $block_row_four] =
self::xyFromZ($last);
$value = $this->luma_value[$last] ?? array_fill(0, 16, 0);
$value[0] = $first_value[$block_row_four * 4 +
$block_value_four];
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$result[($block_row_four * 4 + $down) * 16 +
$block_value_four * 4 +
$across]
= $value[$down * 4 + $across];
}
}
}
$result = self::bypassAccumulate($result, 16, $this
->whole_macroblock_guess,
false);
for ($down = 0; $down < 16; $down++) {
$row = ($pixel_start_y + $down) * $wide + $pixel_start_x;
for ($across = 0; $across < 16; $across++) {
$spot = $down * 16 + $across;
$this->frame->luma[$row + $across]
= self::holdInsideByte($predicted[$spot] + $result
[$spot]);
}
}
return;
}
for ($last = 0; $last < 16; $last++) {
[$block_value_four, $block_row_four] =
self::xyFromZ($last);
$value = $this->luma_value[$last] ?? array_fill(0, 16, 0);
$payload = H264Transform::dequant4x4(
$value, $this->ls_block_row_four, $this->quantizer_y, true);
$payload[0] = $first_value[$block_row_four * 4 +
$block_value_four];
$run = H264Transform::inverse4x4($payload);
$pixel_x = $pixel_start_x + $block_value_four * 4;
$pixel_y = $pixel_start_y + $block_row_four * 4;
for ($down = 0; $down < 4; $down++) {
$row = ($pixel_y + $down) * $wide + $pixel_x;
$prow =
($block_row_four * 4 + $down) * 16 +
$block_value_four * 4;
for ($across = 0; $across < 4; $across++) {
$this->frame->luma[$row + $across]
= self::holdInsideByte(
$predicted[$prow + $across] + $run[$down * 4 +
$across]);
}
}
}
}
/**
* reconstructChroma rebuilds the two chroma planes of a macroblock.
*/
private function reconstructChroma(): void
{
$code_word = $this->frame->color_width;
$pixel_start_x = $this->macroblock_x * 8;
$pixel_start_y = $this->macroblock_y * 8;
$left_there = $this->mbAvailable($this->macroblock_x - 1, $this
->macroblock_y);
$above_there = $this->mbAvailable($this->macroblock_x, $this
->macroblock_y - 1);
$above_left_there = $this->mbAvailable($this->macroblock_x - 1, $this
->macroblock_y - 1);
for ($plane = 0; $plane < 2; $plane++) {
$quantizer = $plane === 0 ? $this->quantizer_blue : $this
->quantizer_red;
$level_scale = $plane === 0 ? $this->ls_blue : $this->ls_red;
$predicted = ($plane === 0)
? H264Intra::predChroma(
$this->chroma_mode, $this->frame->blue, $code_word,
$pixel_start_x,
$pixel_start_y, $left_there,
$above_there,
$above_left_there)
: H264Intra::predChroma(
$this->chroma_mode, $this->frame->red, $code_word,
$pixel_start_x,
$pixel_start_y, $left_there,
$above_there,
$above_left_there);
if ($this->coded_blocks_chroma === 0) {
$first_value = [0, 0, 0, 0];
} elseif ($this->bypass) {
$first_value = $this->chroma_first_value[$plane];
} else {
$first_value = H264Transform::chromaDc(
$this->chroma_first_value[$plane], $level_scale,
$quantizer);
}
$written = [];
if ($this->bypass) {
$result = array_fill(0, 64, 0);
for ($bits = 0; $bits < 4; $bits++) {
$value
= $this->chroma_value[$plane][$bits]
?? array_fill(0, 16, 0);
$value[0] = $first_value[$bits];
$block_x = ($bits & 1) * 4;
$block_y = ($bits >> 1) * 4;
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$result[($block_y + $down) * 8 + $block_x + $across]
= $value[$down * 4 + $across];
}
}
}
$result = self::bypassAccumulate($result, 8, $this->chroma_mode,
true);
for ($i = 0; $i < 64; $i++) {
$written[$i] =
self::holdInsideByte($predicted[$i] + $result[$i]);
}
} else {
for ($bits = 0; $bits < 4; $bits++) {
$value
= $this->chroma_value[$plane][$bits] ?? array_fill(0, 16,
0);
$payload = H264Transform::dequant4x4($value, $level_scale,
$quantizer, true);
$payload[0] = $first_value[$bits];
$run = H264Transform::inverse4x4($payload);
$block_x = ($bits & 1) * 4;
$block_y = ($bits >> 1) * 4;
for ($down = 0; $down < 4; $down++) {
for ($across = 0; $across < 4; $across++) {
$written[($block_y + $down) * 8 + $block_x + $across] =
self::holdInsideByte($predicted
[($block_y + $down) * 8
+ $block_x + $across]
+ $run[$down * 4 + $across]);
}
}
}
}
for ($down = 0; $down < 8; $down++) {
$row = ($pixel_start_y + $down) * $code_word + $pixel_start_x;
for ($across = 0; $across < 8; $across++) {
if ($plane === 0) {
$this->frame->blue[$row + $across]
= $written[$down * 8 + $across];
} else {
$this->frame->red[$row + $across]
= $written[$down * 8 + $across];
}
}
}
}
}
/**
* hasNonZero says whether a block holds any coefficient at all.
*
* @param array $amount how much
* @return bool what was read
*/
private function hasNonZero(array $amount): bool
{
foreach ($amount as $value) {
if ($value !== 0) {
return true;
}
}
return false;
}
/**
* frameAt the frame being decoded.
*
* @return H264Frame what was read
*/
public function frameAt(): H264Frame { return $this->frame; }
/**
* macroblockAcross column of the macroblock being decoded.
*
* @return int what was read
*/
public function macroblockAcross(): int { return $this->macroblock_x; }
/**
* macroblockDown row of the macroblock being decoded.
*
* @return int what was read
*/
public function macroblockDown(): int { return $this->macroblock_y; }
/**
* mbAvail says whether a neighboring macroblock can be used.
*
* @param int $across how far across the block
* @param int $down how far down the block
* @param mixed $down how far down the block
* @return bool what was read
*/
public function mbAvail(int $across,
int $down): bool { return $this->mbAvailable($across, $down); }
/**
* nnzLumaBlk coefficient count of a luma block, for a neighbor to use.
*
* @param int $pixel_x how far across the frame the pixel is
* @param int $pixel_y how far down the frame the pixel is
* @param int $order_position where in that order the reading is
* @param mixed $pixel_y how far down the frame the pixel is
* @param mixed $order_position where in that order the reading is
* @return int what was read
*/
public function nnzLumaBlk(int $pixel_x, int $pixel_y,
int $order_position): int { return $this->nnzLumaAt($pixel_x, $pixel_y,
$order_position); }
/**
* nnzChromaBlk coefficient count of a chroma block, for a neighbor to use.
*
* @param int $plane zero for luma, one and two for the chroma planes
* @param int $pixel_x how far across the frame the pixel is
* @param int $pixel_y how far down the frame the pixel is
* @param mixed $pixel_x how far across the frame the pixel is
* @param mixed $pixel_y how far down the frame the pixel is
* @return int what was read
*/
public function nnzChromaBlk(int $plane, int $pixel_x,
int $pixel_y): int { return $this->nnzChromaAt($plane, $pixel_x,
$pixel_y); }
}
/**
* H264Deblock deblocking filter, clause 8.7. This decoder only handles I
* slices, so every macroblock is intra: the boundary strength is 4 on
* macroblock edges and 3 on internal edges, and no motion-vector or reference-
* index comparison is needed.
*/
final class H264Deblock
{
/**
* holdBetween holds a value between a lower and an upper bound.
*
* @param int $lo the lower end
* @param int $hi the upper end
* @param int $value the value read
* @return int what was read
*/
private static function holdBetween(int $lo, int $hi, int $value): int
{
return $value < $lo ? $lo : ($value > $hi ? $hi : $value);
}
/**
* holdInsideSample holds a sample inside the range a byte can carry.
*
* @param int $value the value read
* @return int what was read
*/
private static function holdInsideSample(int $value): int
{
return $value < 0 ? 0 : ($value > 255 ? 255 : $value);
}
/**
* applyOffsets smooths the sample values either side of every block edge of
* a frame.
*
* @param H264Frame $frame the stored bytes of one frame
*/
public static function applyOffsets(H264Frame $frame): void
{
for ($macroblock_y = 0; $macroblock_y < $frame
->macroblock_down; $macroblock_y++) {
for ($macroblock_x = 0; $macroblock_x < $frame
->macroblock_across; $macroblock_x++) {
$position = $macroblock_y * $frame
->macroblock_across + $macroblock_x;
if ($frame->macroblock_slice[$position] < 0) {
continue;
}
$setting = $frame->macroblock_smoothing_off[$position];
if ($setting === 1) {
continue;
}
$same_slice_only = ($setting === 2);
$held_eight = $frame
->macroblock_larger_transform[$position] === 1;
/* vertical edges, left to right */
for ($entry = 0; $entry < 4; $entry++) {
if ($held_eight && ($entry === 1 || $entry === 3)) {
continue;
}
if ($entry === 0) {
if ($macroblock_x === 0) {
continue;
}
$left = $position - 1;
if ($frame->macroblock_slice[$left] < 0) {
continue;
}
if ($same_slice_only
&& $frame->macroblock_slice[$left]
!== $frame->macroblock_slice[$position]) {
continue;
}
self::edgeLuma(
$frame, $position, $left, $macroblock_x * 16,
$macroblock_y * 16,
true, 0,
4);
self::edgeChroma(
$frame, $position, $left, $macroblock_x * 8,
$macroblock_y * 8, true, 0,
4);
} else {
self::edgeLuma($frame, $position, $position,
$macroblock_x * 16
+ $entry * 4, $macroblock_y * 16, true, 0, 3);
if ($entry === 2) {
self::edgeChroma($frame, $position, $position,
$macroblock_x * 8
+ 4, $macroblock_y * 8, true, 0, 3);
}
}
}
/* horizontal edges, top to bottom */
for ($entry = 0; $entry < 4; $entry++) {
if ($held_eight && ($entry === 1 || $entry === 3)) {
continue;
}
if ($entry === 0) {
if ($macroblock_y === 0) {
continue;
}
$up = $position - $frame->macroblock_across;
if ($frame->macroblock_slice[$up] < 0) {
continue;
}
if ($same_slice_only
&& $frame->macroblock_slice[$up]
!== $frame->macroblock_slice[$position]) {
continue;
}
self::edgeLuma(
$frame, $position, $up, $macroblock_x * 16,
$macroblock_y * 16, false,
0,
4);
self::edgeChroma(
$frame, $position, $up, $macroblock_x * 8,
$macroblock_y * 8, false, 0,
4);
} else {
self::edgeLuma($frame, $position, $position,
$macroblock_x * 16,
$macroblock_y * 16
+ $entry * 4, false, 0, 3);
if ($entry === 2) {
self::edgeChroma(
$frame, $position, $position,
$macroblock_x * 8, $macroblock_y * 8 + 4,
false,
0, 3);
}
}
}
}
}
}
/**
* edgeLuma horizontally)
*
* @param bool $vertical true filters a vertical edge (samples run
* @param H264Frame $frame the frame being built
* @param int $q_position where the block on the other side sits
* @param int $p_position where the block on one side of the edge sits
* @param int $block_x how far across the frame the block starts
* @param int $block_y how far down the frame the block starts
* @param int $unused not read; kept so the shape of the call is unchanged
* @param int $smooth_strength how strongly the edge is smoothed
*/
private static function edgeLuma(
H264Frame $frame, int $q_position, int $p_position, int $block_x,
int $block_y,
bool $vertical, int $unused, int $smooth_strength
): void {
$quantizer = ($frame->macroblock_quantizer_y[$q_position] + $frame
->macroblock_quantizer_y[$p_position] +
1) >> 1;
$index_a = self::holdBetween(0, 51, $quantizer + $frame
->macroblock_alpha_off[$q_position]);
$index_b = self::holdBetween(0, 51, $quantizer + $frame
->macroblock_beta_off[$q_position]);
$alpha = H264Tables::DEBLOCK_ALPHA[$index_a];
$beta = H264Tables::DEBLOCK_BETA[$index_b];
if ($alpha === 0 || $beta === 0) {
return;
}
$strength = ($smooth_strength >= 4) ? 3 : $smooth_strength;
$move_limit = H264Tables::EDGE_MOVE_LIMITS[$index_a][$strength];
$wide = $frame->coded_width;
for ($i = 0; $i < 16; $i++) {
if ($vertical) {
$base = ($block_y + $i) * $wide + $block_x;
$step = 1;
} else {
$base = ($block_y) * $wide + $block_x + $i;
$step = $wide;
}
self::filterLine(
$frame->luma, $base, $step, $alpha, $beta, $move_limit,
$smooth_strength, true);
}
}
/**
* edgeChroma filters one edge of the chroma planes.
*
* @param H264Frame $frame the stored bytes of one frame
* @param int $q_position where the block on the other side sits
* @param int $p_position where the block on one side of the edge sits
* @param int $block_x how far across the frame the block starts
* @param int $block_y how far down the frame the block starts
* @param bool $vertical whether the edge runs up and down
* @param int $unused not read; kept so the shape of the call is unchanged
* @param int $smooth_strength how strongly the edge is smoothed
*/
private static function edgeChroma(
H264Frame $frame, int $q_position, int $p_position, int $block_x,
int $block_y,
bool $vertical, int $unused, int $smooth_strength
): void {
$code_word = $frame->color_width;
foreach ([0, 1] as $plane) {
$quantizer_after = $plane === 0 ? $frame
->macroblock_quantizer_blue[$q_position]
: $frame->macroblock_quantizer_red[$q_position];
$quantizer_before = $plane === 0 ? $frame
->macroblock_quantizer_blue[$p_position]
: $frame->macroblock_quantizer_red[$p_position];
$quantizer = ($quantizer_after + $quantizer_before + 1) >> 1;
$index_a = self::holdBetween(0, 51, $quantizer + $frame
->macroblock_alpha_off[$q_position]);
$index_b = self::holdBetween(0, 51, $quantizer + $frame
->macroblock_beta_off[$q_position]);
$alpha = H264Tables::DEBLOCK_ALPHA[$index_a];
$beta = H264Tables::DEBLOCK_BETA[$index_b];
if ($alpha === 0 || $beta === 0) {
continue;
}
$strength = ($smooth_strength === 4) ? 3 : $smooth_strength;
$move_limit =
H264Tables::EDGE_MOVE_LIMITS[$index_a][$strength];
for ($i = 0; $i < 8; $i++) {
if ($vertical) {
$base = ($block_y + $i) * $code_word + $block_x;
$step = 1;
} else {
$base = $block_y * $code_word + $block_x + $i;
$step = $code_word;
}
if ($plane === 0) {
self::filterLine(
$frame->blue, $base, $step, $alpha, $beta,
$move_limit, $smooth_strength,
false);
} else {
self::filterLine(
$frame->red, $base, $step, $alpha, $beta,
$move_limit, $smooth_strength,
false);
}
}
}
}
/**
* filterLine smooths one line of samples that runs across a block edge,
* moving the values on either side toward each other by as much as the
* thresholds allow. The standard calls this clauses 8.7.2.3 and 8.7.2.4.
* The line is read from the value the rest are measured from, with the
* sample on the other side of the edge one step back.
*
* @param array $plane which of the picture's planes, brightness or color
* @param int $base the value the rest are measured from
* @param int $step how far to move each time
* @param int $alpha the threshold an edge is smoothed above
* @param int $beta the threshold a neighbor is smoothed above
* @param int $move_limit how far a value may be moved while smoothing
* @param int $smooth_strength how strongly the edge is smoothed
* @param bool $luma whether this is the brightness plane
*/
private static function filterLine(
array &$plane, int $base, int $step, int $alpha, int $beta,
int $move_limit,
int $smooth_strength, bool $luma
): void {
$after_edge = $plane[$base];
$after_edge_one = $plane[$base + $step];
$after_edge_two = $plane[$base + 2 * $step];
$after_edge_three = $plane[$base + 3 * $step];
$before_edge = $plane[$base - $step];
$before_edge_one = $plane[$base - 2 * $step];
$before_edge_two = $plane[$base - 3 * $step];
$before_edge_three = $plane[$base - 4 * $step];
if (abs($before_edge - $after_edge) >= $alpha ||
abs($before_edge_one - $before_edge) >= $beta
|| abs($after_edge_one - $after_edge) >= $beta) {
return;
}
$above_limit = abs($before_edge_two - $before_edge);
$below_limit = abs($after_edge_two - $after_edge);
if ($smooth_strength < 4) {
if ($luma) {
$move_limit = $move_limit + ($above_limit < $beta ? 1 : 0) +
($below_limit < $beta ? 1 : 0);
} else {
$move_limit = $move_limit + 1;
}
$delta = self::holdBetween(-$move_limit, $move_limit,
((($after_edge -
$before_edge) << 2) + ($before_edge_one - $after_edge_one)
+ 4) >> 3);
$plane[$base - $step] = self::holdInsideSample($before_edge +
$delta);
$plane[$base] = self::holdInsideSample($after_edge - $delta);
if ($luma && $above_limit < $beta) {
$plane[$base - 2 * $step] =
$before_edge_one + self::holdBetween(-$move_limit,
$move_limit,
($before_edge_two + (($before_edge + $after_edge +
1) >> 1) -
($before_edge_one << 1)) >> 1);
}
if ($luma && $below_limit < $beta) {
$plane[$base + $step] =
$after_edge_one + self::holdBetween(-$move_limit,
$move_limit,
($after_edge_two + (($before_edge + $after_edge +
1) >> 1) -
($after_edge_one << 1)) >> 1);
}
return;
}
/* bS == 4 */
$strong_p = $luma && $above_limit < $beta && abs($before_edge -
$after_edge) < (($alpha >> 2) +
2);
$strong_q = $luma && $below_limit < $beta && abs($before_edge -
$after_edge) < (($alpha >> 2) +
2);
if ($strong_p) {
$plane[$base - $step]
= ($before_edge_two + 2 * $before_edge_one + 2 *
$before_edge + 2 * $after_edge + $after_edge_one +
4) >> 3;
$plane[$base - 2 * $step] = ($before_edge_two +
$before_edge_one + $before_edge + $after_edge +
2) >> 2;
$plane[$base - 3 * $step]
= (2 * $before_edge_three + 3 * $before_edge_two +
$before_edge_one + $before_edge + $after_edge + 4) >> 3;
} else {
$plane[$base - $step] = (2 * $before_edge_one + $before_edge +
$after_edge_one + 2) >> 2;
}
if ($strong_q) {
$plane[$base]
= ($after_edge_two + 2 * $after_edge_one + 2 * $after_edge +
2 * $before_edge + $before_edge_one +
4) >> 3;
$plane[$base + $step] = ($after_edge_two + $after_edge_one +
$after_edge + $before_edge +
2) >> 2;
$plane[$base + 2 * $step]
= (2 * $after_edge_three + 3 * $after_edge_two +
$after_edge_one + $after_edge + $before_edge + 4) >> 3;
} else {
$plane[$base] = (2 * $after_edge_one + $after_edge +
$before_edge_one + 2) >> 2;
}
}
}
/**
* H264Picture the picture an H.264 keyframe decodes to.
*/
final class H264Picture extends VideoPicture
{
}
/**
* The H264Decoder class decodes one self contained H.264 picture in
* pure PHP.
*
* It reads slices that stand on their own, brightness with color at
* half width and half height, eight bits to a sample, either of the two
* codings the format offers, blocks guessed in four by four or eight by
* eight pieces or as one whole macroblock, macroblocks whose samples
* are stored as they are, the tables a stream may carry for scaling its
* values, several slices to a picture, and the smoothing of block
* edges.
*
* Not supported (each raises H264Exception rather than producing wrong pixels):
* P and B slices, half-pictures, or frames that mix whole and half pictures,
* 4:2:2 / 4:4:4 / monochrome,
* bit depths above 8, slices written out of order or in groups, and data
* partitioning.
*/
final class H264Decoder
{
/**
* $sequence_settings_map stores the sequence settings the stream has
* carried so far, kept
* by their number so a slice can name the one it uses.
* @var array
*/
private array $sequence_settings_map = [];
/**
* $picture_settings_map stores the picture settings the stream has carried
* so
* far, kept by the number each set gives itself, so a slice can
* name the set it was coded with.
* @var array
*/
private array $picture_settings_map = [];
/**
* addSettingsUnit feed a parameter set NAL (a unit an H.264 or HEVC stream
* is cut into) (as stored in avcC (the box holding an MP4's H.264
* settings), including its header byte)
*
* @param string $stream_unit_unit the unit read out of the stream
*/
public function addSettingsUnit(string $stream_unit_unit): void
{
$total = H264Nal::parseUnit($stream_unit_unit);
$this->consume($total);
}
/**
* consume takes the next unit off the stream and hands it to whichever
* reader deals with that kind.
*
* @param array $total how many bits
*/
private function consume(array $total): void
{
if ($total['type'] === 7) {
$sequence_settings =
H264ParamParser::readSequenceSettings($total['rbsp']);
$this->sequence_settings_map[$sequence_settings->id] =
$sequence_settings;
} elseif ($total['type'] === 8) {
$picture_settings
= H264ParamParser::readPictureSettings($total['rbsp'],
$this->sequenceSettingsFor($total['rbsp']));
$this->picture_settings_map[$picture_settings->id] =
$picture_settings;
}
}
/**
* sequenceSettingsFor the PPS references an SPS id; peek at it before the
* full parse unpacking
*
* @param string $picture_settings_unpacked the picture settings as they
* arrived, before
* @return H264Sps what was read
*/
private function sequenceSettingsFor(string $picture_settings_unpacked):
H264Sps
{
$bits = new H264Bits($picture_settings_unpacked);
/* pic_parameter_set_id */
$bits->readWholeNumber();
$sequence_settings_id = $bits->readWholeNumber();
if (!isset($this->sequence_settings_map[$sequence_settings_id])) {
throw new H264Exception("picture settings name sequence "
. "settings $sequence_settings_id, which the stream "
. "has not carried");
}
return $this->sequence_settings_map[$sequence_settings_id];
}
/**
* decodePlainStream decode one picture from an Annex-B byte stream (start-
* code delimited). Parameter sets present in the stream are picked up
* automatically.
*
* @param string $stream which stream of the file
* @return H264Picture what was read
*/
public function decodePlainStream(string $stream): H264Picture
{
return $this->decodeUnits(H264Nal::unitsFromStream($stream));
}
/**
* decodeUnits read out of the stream
*
* @param array $stream_units the units
* @return H264Picture what was read
*/
public function decodeUnits(array $stream_units): H264Picture
{
foreach ($stream_units as $total) {
if ($total['type'] === 7 || $total['type'] === 8) {
$this->consume($total);
}
}
$frame = null;
$sequence_settings = null;
$slice_id = 0;
$decoded_any = false;
foreach ($stream_units as $total) {
if ($total['type'] !== 1 && $total['type'] !== 5) {
continue;
}
if ($total['type'] === 2 || $total['type'] === 3
|| $total['type'] === 4) {
throw new H264Exception('data partitioning is not supported');
}
$bits = new H264Bits($total['rbsp']);
/* peek the PPS id to pick the parameter sets before the full parse
*/
$probe = new H264Bits($total['rbsp']);
$probe->readWholeNumber();
$probe->readWholeNumber();
$picture_settings_id = $probe->readWholeNumber();
if (!isset($this->picture_settings_map[$picture_settings_id])) {
throw new H264Exception("a slice names picture "
. "settings $picture_settings_id, which the "
. "stream has not carried");
}
$picture_settings = $this
->picture_settings_map[$picture_settings_id];
$sequence_settings = $this
->sequence_settings_map[$picture_settings
->sequence_settings_id];
if ($sequence_settings->chroma_format_setting !== 1) {
throw new H264Exception('only 4:2:0 chroma is supported');
}
if ($sequence_settings->bit_depth_luma !== 8 ||
$sequence_settings->bit_depth_chroma !== 8) {
throw new H264Exception('only 8-bit video is supported');
}
if (!$sequence_settings->frame_macroblocks_only) {
throw new H264Exception(
'half-picture coding is not read');
}
$header = H264SliceHeader::readSettings(
$bits, $total['type'], $total['refIdc'], $sequence_settings,
$picture_settings);
if ($frame === null) {
$macroblock_down = $sequence_settings
->picture_height_in_map_units;
$frame = new H264Frame($sequence_settings
->picture_width_in_macroblocks,
$macroblock_down);
}
$decoder = new H264SliceDecoder(
$frame, $sequence_settings, $picture_settings, $header, $bits,
$slice_id++);
$decoder->decodePicture();
$decoded_any = true;
}
if (!$decoded_any || $frame === null || $sequence_settings === null) {
throw new H264Exception('no decodable I slice found');
}
H264Deblock::applyOffsets($frame);
$crop_x = $sequence_settings->frame_cropping ? $sequence_settings
->crop_left * 2 : 0;
$crop_y = $sequence_settings->frame_cropping ? $sequence_settings
->crop_top * 2 : 0;
return new H264Picture(
$frame->luma, $frame->blue, $frame->red,
$sequence_settings->croppedWidth(),
$sequence_settings->croppedHeight(),
$frame->coded_width, $frame->color_width,
$crop_x, $crop_y
);
}
/**
* toImage turns the decoded planes into a picture the image library can
* work with, scaling it down where a width was asked for.
*
* @return GdImage what was read
* @param VideoPicture $position where the reading is
*/
public static function toImage(VideoPicture $position)
{
return $position->toImage();
}
}