/ src / library / av_processing / WebmExtractor.php
<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 *
 * This class reads a Matroska or WebM file. It finds the video track and
 * the frames that stand on their own.
 */
namespace seekquarry\yioop\library\av_processing;
/**
 * WebmExtractor reads a Matroska or WebM file, finding the video track and
 * the frames that stand on their own.
 */
final class WebmExtractor extends VideoExtractor
{
    /**
     * $hevc stores the H.265 decoder, made when the first such frame is asked
     * for.
     * @var HevcDecoder
     */
    private ?HevcDecoder $hevc = null;
    use ByteSource;
    /**
     * $clock_units_per_second stores how many thousand-millionths of a second
     * one tick
     * of the file's clock stands for. Every time in the file is in ticks, so
     * this turns them into seconds. Read by durationSeconds(), frameRate(),
     * sampleTime().
     * @var int
     */
    private int $clock_units_per_second = 1000000;
    /**
     * $file_length_ticks stores how long the file runs, in those ticks, as its
     * header says. Where the header leaves it out, the last frame's time is
     * used instead.
     * @var float
     */
    private float $file_length_ticks = 0.0;
    /**
     * $track_number stores which track of the file carries the video. Every
     * block names the track it belongs to, so blocks of any other track are
     * passed over.
     * @var int
     */
    private int $track_number = -1;
    /**
     * $codec_name stores the name the file gives the codec, such as the one it
     * uses for VP9 or for a the MPEG-4 family of formats/ISO/AVC. codecKind()
     * reads it to decide which decoder a frame goes to.
     * @var string
     */
    private string $codec_name = '';
    /**
     * $frame_width stores how wide a frame is, in pixels, as the track's header
     * says. A thumbnail is scaled against it without decoding anything.
     * @var int
     */
    private int $frame_width = 0;
    /**
     * $frame_height stores how tall a frame is, in pixels, as the
     * file's own header gives it. A thumbnail keeps the shape of
     * the frame, so its height follows from this and the width.
     * @var int
     */
    private int $frame_height = 0;
    /**
     * $codec_setup_bytes stores the settings the file keeps beside the track
     * for its codec. For H.264 and HEVC a decoder needs them before it can read
     * a frame.
     * @var string
     */
    private string $codec_setup_bytes = '';
    /**
     * $default_frame_length stores how long a frame lasts where the file does
     * not say otherwise, in ticks. frameRate() turns it into frames a second.
     * @var float
     */
    private float $default_frame_length = 0.0;
    /**
     * $frame_table stores where each frame sits in the file, when it is shown,
     * and whether it stands on its own. This is what a seek walks. Read by
     * durationSeconds(), frameCount(), frameRate().
     * @var array
     */
    private array $frame_table = [];
    /**
     * $self_contained_frames stores which of those frames stand on their own,
     * by their place in the list above. A thumbnail is decoded from one of
     * these.
     * @var array
     */
    private array $self_contained_frames = [];
    /**
     * $h264_settings stores the H.264 settings settings, or nothing where there
     * is none.
     * @var AvcConfig
     */
    private ?AvcConfig $h264_settings = null;
    /**
     * $vp8 stores the vp8, or nothing where there is none.
     * @var Vp8Decoder
     */
    private ?Vp8Decoder $vp8 = null;
    /**
     * $vp9 stores the vp9, or nothing where there is none.
     * @var Vp9Header
     */
    private ?Vp9Header $vp9 = null;
    /**
     * ID_SEGMENT is the number naming the part of the file that holds
     * everything else: the tracks, the timing and the frames.
     * @var mixed
     */
    private const ID_SEGMENT        = 0x18538067;
    /**
     * ID_INFO is the number naming the part that carries the file's clock and
     * how long it runs.
     * @var mixed
     */
    private const ID_INFO           = 0x1549A966;
    /**
     * ID_TIMECODE_SCALE is the number naming the field that says how long one
     * tick of that clock is.
     * @var mixed
     */
    private const ID_TIMECODE_SCALE = 0x2AD7B1;
    /**
     * ID_DURATION is the number naming the field that says how many ticks the
     * file runs for.
     * @var mixed
     */
    private const ID_DURATION       = 0x4489;
    /**
     * ID_TRACKS is the number naming the part that lists the file's tracks.
     * @var mixed
     */
    private const ID_TRACKS         = 0x1654AE6B;
    /**
     * ID_TRACK_ENTRY is the number naming one track within that list.
     * @var mixed
     */
    private const ID_TRACK_ENTRY    = 0xAE;
    /**
     * ID_TRACK_NUMBER is the number naming the field that gives a track its
     * number, which every frame refers back to.
     * @var mixed
     */
    private const ID_TRACK_NUMBER   = 0xD7;
    /**
     * ID_TRACK_TYPE is the number naming the field that says whether a track
     * carries video, sound or something else.
     * @var mixed
     */
    private const ID_TRACK_TYPE     = 0x83;
    /**
     * ID_CODEC_ID is the number naming the field that says which codec a track
     * uses.
     * @var mixed
     */
    private const ID_CODEC_ID       = 0x86;
    /**
     * ID_CODEC_PRIVATE is the number naming the field that carries the settings
     * a codec needs before a frame can be read.
     * @var mixed
     */
    private const ID_CODEC_PRIVATE  = 0x63A2;
    /**
     * ID_DEFAULT_FRAME_LENGTH is the number naming the field that says how
     * long a frame
     * lasts where the file does not say otherwise.
     * @var mixed
     */
    private const ID_DEFAULT_FRAME_LENGTH    = 0x23E383;
    /**
     * ID_VIDEO is the number naming the part of a track that describes its
     * picture.
     * @var mixed
     */
    private const ID_VIDEO          = 0xE0;
    /**
     * ID_PIXEL_WIDTH is the number naming the field that gives a frame's width.
     * @var mixed
     */
    private const ID_PIXEL_WIDTH    = 0xB0;
    /**
     * ID_PIXEL_HEIGHT is the number naming the field that gives a frame's
     * height.
     * @var mixed
     */
    private const ID_PIXEL_HEIGHT   = 0xBA;
    /**
     * ID_CLUSTER is the number naming a group of frames that share a starting
     * time.
     * @var mixed
     */
    private const ID_CLUSTER        = 0x1F43B675;
    /**
     * ID_TIMECODE is the number naming the field that gives that starting time.
     * @var mixed
     */
    private const ID_TIMECODE       = 0xE7;
    /**
     * ID_SIMPLE_BLOCK is the number naming a frame written on its own, which is
     * how most video frames are stored.
     * @var mixed
     */
    private const ID_SIMPLE_BLOCK   = 0xA3;
    /**
     * ID_BLOCK_GROUP is the number naming a frame written with extra fields
     * around it, such as how long it lasts.
     * @var mixed
     */
    private const ID_BLOCK_GROUP    = 0xA0;
    /**
     * ID_BLOCK is the number naming the frame inside such a group.
     * @var mixed
     */
    private const ID_BLOCK          = 0xA1;
    /**
     * __construct opens a Matroska or WebM file and indexes its video track.
     * The element tree is walked once so that the byte range, time and keyframe
     * flag of every frame are known before anything is decoded.
     *
     * @param string $path file to read
     */
    public function __construct(string $path)
    {
        $this->openSource($path);
        $this->readSettings();
    }
    /**
     * readVariableLength read a variable-length integer. marker]
     *
     * @return array [value, bytes consumed, all-ones
     * @param int $off how far into the file to read from
     * @param bool $keep_marker whether the packing markers are left in
     */
    private function readVariableLength(int $off, bool $keep_marker): array
    {
        $first = $this->readBytesAt($off, 1);
        if ($first === '') {
            throw new VideoException('unexpected end of file in EBML');
        }
        $right_zero = ord($first);
        if ($right_zero === 0) {
            throw new VideoException('invalid EBML length');
        }
        $length = 1;
        $mask = 0x80;
        while (($right_zero & $mask) === 0) {
            $mask >>= 1;
            $length++;
        }
        $bytes = $this->readBytesAt($off, $length);
        if (strlen($bytes) < $length) {
            throw new VideoException('truncated EBML element');
        }
        $value = $keep_marker ? $right_zero : ($right_zero & ($mask - 1));
        $unknown = !$keep_marker && ($right_zero & ($mask - 1)) === ($mask - 1);
        for ($i = 1; $i < $length; $i++) {
            $value = ($value << 8) | ord($bytes[$i]);
            if (ord($bytes[$i]) !== 0xFF) {
                $unknown = false;
            }
        }
        return [$value, $length, $unknown];
    }
    /**
     * readWholeNumberFrom reads a whole number stored most significant byte
     * first.
     *
     * @param string $source the file or bytes being read
     * @return int what was read
     */
    private static function readWholeNumberFrom(string $source): int
    {
        $variant = 0;
        for ($i = 0, $total = strlen($source); $i < $total; $i++) {
            $variant = ($variant << 8) | ord($source[$i]);
        }
        return $variant;
    }
    /**
     * floatOf reads a floating point number of four or eight bytes.
     *
     * @param string $source the file or bytes being read
     * @return float what was read
     */
    private static function floatOf(string $source): float
    {
        if (strlen($source) === 4) {
            return unpack('G', $source)[1];
        }
        if (strlen($source) === 8) {
            return unpack('E', $source)[1];
        }
        return 0.0;
    }
    /**
     * readSettings walks the file and gathers the track and its frames.
     */
    private function readSettings(): void
    {
        $this->walkChunks(0, $this->sourceSize(), 0);
        if ($this->track_number < 0) {
            throw new VideoException('no video track in this WebM file');
        }
        if ($this->frame_table === []) {
            throw new VideoException('no video frames in this WebM file');
        }
        usort($this->frame_table, static fn(array $amount, array $bits): int
            => $amount[2] <=> $bits[2]);
        foreach ($this->frame_table as $i => $field) {
            if ($field[3]) {
                $this->self_contained_frames[] = $i;
            }
        }
    }
    /**
     * walkChunks walks one level of the element tree, descending where it must.
     *
     * @param int $start where it starts
     * @param int $end where it ends
     * @param int $depth how deep in the tree
     * @param int $cluster_time when the cluster this block belongs to starts
     */
    private function walkChunks(int $start, int $end, int $depth,
        int $cluster_time
        = 0): void
    {
        if ($depth > 8) {
            return;
        }
        $position = $start;
        while ($position < $end && $position < $this->sourceSize()) {
            [$id, $id_length] = $this->readVariableLength($position, true);
            [$size, $size_length, $unknown] = $this
                ->readVariableLength($position + $id_length,
                false);
            $payload = $position + $id_length + $size_length;
            if ($unknown) {
                /* an element of unknown length runs to the end of its parent */
                $size = $end - $payload;
            }
            $next = $payload + $size;
            if ($size < 0 || $payload > $this->sourceSize()) {
                break;
            }
            switch ($id) {
                case self::ID_SEGMENT:
                case self::ID_TRACKS:
                case self::ID_TRACK_ENTRY:
                case self::ID_INFO:
                case self::ID_VIDEO:
                    $this->walkChunks($payload, min($end, $next), $depth + 1);
                    break;
                case self::ID_CLUSTER:
                    $this->walkCluster($payload, min($end, $next), $depth + 1);
                    break;
                case self::ID_TIMECODE_SCALE:
                    $this->clock_units_per_second
                        = max(1, self::readWholeNumberFrom($this
                            ->readBytesAt($payload, $size)));
                    break;
                case self::ID_DURATION:
                    $this->file_length_ticks
                        = self::floatOf($this->readBytesAt($payload, $size));
                    break;
                case self::ID_TRACK_NUMBER:
                    $this->pending_track_number
                        = self::readWholeNumberFrom($this
                            ->readBytesAt($payload, $size));
                    break;
                case self::ID_TRACK_TYPE:
                    $this->pending_track_type
                        = self::readWholeNumberFrom($this
                            ->readBytesAt($payload, $size));
                    break;
                case self::ID_CODEC_ID:
                    $this->pending_codec_name
                        = rtrim($this->readBytesAt($payload, $size), "\x00");
                    break;
                case self::ID_CODEC_PRIVATE:
                    $this->pending_codec_setup_bytes = $this
                        ->readBytesAt($payload,
                        $size);
                    break;
                case self::ID_DEFAULT_FRAME_LENGTH:
                    $this->pending_frame_length
                        = (float) self::readWholeNumberFrom($this
                            ->readBytesAt($payload, $size));
                    break;
                case self::ID_PIXEL_WIDTH:
                    $this->pending_frame_width
                        = self::readWholeNumberFrom($this
                            ->readBytesAt($payload, $size));
                    break;
                case self::ID_PIXEL_HEIGHT:
                    $this->pending_frame_height
                        = self::readWholeNumberFrom($this
                            ->readBytesAt($payload, $size));
                    break;
            }
            /* a TrackEntry ends: keep it if it is the first video track */
            if ($id === self::ID_TRACK_ENTRY) {
                if ($this->pending_track_type === 1 &&
                    $this->track_number < 0) {
                    $this->track_number = $this->pending_track_number;
                    $this->codec_name = $this->pending_codec_name;
                    $this->codec_setup_bytes = $this->pending_codec_setup_bytes;
                    $this->frame_width = $this->pending_frame_width;
                    $this->frame_height = $this->pending_frame_height;
                    $this->default_frame_length = $this->pending_frame_length;
                }
                $this->pending_track_type = 0;
                $this->pending_track_number = -1;
                $this->pending_codec_name = '';
                $this->pending_codec_setup_bytes = '';
                $this->pending_frame_width = 0;
                $this->pending_frame_height = 0;
                $this->pending_frame_length = 0.0;
            }
            if ($next <= $position) {
                break;
            }
            $position = $next;
        }
    }
    /**
     * $pending_track_number stores the track being read while the file's header
     * is walked. A track's fields arrive one at a time, so they are gathered
     * here and kept only if the track turns out to be the video one.
     * @var int
     */
    private int $pending_track_number = -1;
    /**
     * $pending_track_type stores whether the track being walked
     * carries video, sound or something else. A track's fields arrive
     * one at a time, so they are gathered here and kept only if the
     * track turns out to be the video one.
     * @var int
     */
    private int $pending_track_type = 0;
    /**
     * $pending_codec_name stores that track's codec name, held until the track
     * is known to be the one wanted.
     * @var string
     */
    private string $pending_codec_name = '';
    /**
     * $pending_codec_setup_bytes stores the codec settings of the track
     * being walked, kept until the track is known to be the video
     * one. A track's fields arrive one at a time. The
     * same way.
     * @var string
     */
    private string $pending_codec_setup_bytes = '';
    /**
     * $pending_frame_width stores the frame width of the track being
     * walked, kept until that track is known to be the video one. Way.
     * @var int
     */
    private int $pending_frame_width = 0;
    /**
     * $pending_frame_height stores the frame height of the track being
     * walked, kept until that track is known to be the video one. S
     * way.
     * @var int
     */
    private int $pending_frame_height = 0;
    /**
     * $pending_frame_length stores how long a frame of the track being
     * walked lasts, kept until that track is known to be the video
     * one. S
     * way.
     * @var float
     */
    private float $pending_frame_length = 0.0;
    /**
     * walkCluster walks a cluster, which holds the frames and their times.
     *
     * @param int $start where it starts
     * @param int $end where it ends
     * @param int $depth how deep in the tree
     */
    private function walkCluster(int $start, int $end, int $depth): void
    {
        $cluster_time = 0;
        $position = $start;
        while ($position < $end && $position < $this->sourceSize()) {
            [$id, $id_length] = $this->readVariableLength($position, true);
            [$size, $size_length, $unknown] = $this
                ->readVariableLength($position + $id_length,
                false);
            $payload = $position + $id_length + $size_length;
            if ($unknown) {
                $size = $end - $payload;
            }
            $next = $payload + $size;
            if ($id === self::ID_TIMECODE) {
                $cluster_time = self::readWholeNumberFrom($this
                    ->readBytesAt($payload, $size));
            } elseif ($id === self::ID_SIMPLE_BLOCK) {
                $this->readBlock($payload, $size, $cluster_time, null);
            } elseif ($id === self::ID_BLOCK_GROUP) {
                /* a Block inside a BlockGroup carries no keyframe flag of its
                  */
                /* own; the absence of a ReferenceBlock is what marks it */
                $inner = $payload;
                $block_at = null;
                $block_size = 0;
                $has_reference = false;
                while ($inner < min($end, $next)) {
                    [$item_id, $item_id_length] = $this
                        ->readVariableLength($inner,
                        true);
                    [$item_size, $item_size_length] = $this
                        ->readVariableLength($inner + $item_id_length,
                        false);
                    $inner_payload = $inner + $item_id_length +
                        $item_size_length;
                    if ($item_id === self::ID_BLOCK) {
                        $block_at = $inner_payload;
                        $block_size = $item_size;
                    /* ReferenceBlock */
                    } elseif ($item_id === 0xFB) {
                        $has_reference = true;
                    }
                    $inner = $inner_payload + $item_size;
                    if ($item_size < 0) {
                        break;
                    }
                }
                if ($block_at !== null) {
                    $this->readBlock(
                        $block_at, $block_size, $cluster_time, !$has_reference);
                }
            }
            if ($next <= $position) {
                break;
            }
            $position = $next;
        }
    }
    /** @param bool|null $keyframe null means take the flag from the block
      header */
    /**
     * readBlock records one frame's byte range, time and keyframe flag.
     *
     * @param int $offset where in the file to start reading
     * @param int $size how many bytes
     * @param int $cluster_time when the cluster this block belongs to starts
     * @param bool $keyframe whether this frame stands on its own
     */
    private function readBlock(int $offset, int $size, int $cluster_time,
        ?bool $keyframe): void
    {
        if ($size < 4) {
            return;
        }
        [$track, $track_length] = $this->readVariableLength($offset, false);
        if ($track !== $this->track_number) {
            return;
        }
        $head = $this->readBytesAt($offset + $track_length, 3);
        if (strlen($head) < 3) {
            return;
        }
        $relative = (ord($head[0]) << 8) | ord($head[1]);
        if ($relative >= 0x8000) {
            $relative -= 0x10000;
        }
        $flags = ord($head[2]);
        $is_key = $keyframe ?? (($flags & 0x80) !== 0);
        $lacing = ($flags >> 1) & 3;
        $data_start = $offset + $track_length + 3;
        $data_end = $offset + $size;
        if ($lacing !== 0) {
            /* laced blocks pack several frames together; only the first is */
            /* needed for a thumbnail and only keyframes are ever decoded */
            $data_start += 1;
        }
        if ($data_end <= $data_start) {
            return;
        }
        $this->frame_table[]
            = [$data_start, $data_end - $data_start, $cluster_time
                + $relative, $is_key];
    }
    /**
     * frameRate frames a second, from the track's stated frame duration if it
     * gives one, otherwise from the spread of frame times.
     *
     * @return float what was read
     */
    private function frameRate(): float
    {
        if ($this->default_frame_length > 0) {
            return 1e9 / $this->default_frame_length;
        }
        if (count($this->frame_table) > 1) {
            $span = ($this->frame_table[count($this->frame_table) - 1][2]
                - $this->frame_table[0][2]) * $this
                    ->clock_units_per_second / 1e9;
            if ($span > 0) {
                return (count($this->frame_table) - 1) / $span;
            }
        }
        return 25.0;
    }
    /**
     * durationSeconds works out how long the video runs.
     *
     * @return float what was read
     */
    public function durationSeconds(): float
    {
        if ($this->file_length_ticks > 0) {
            return $this->file_length_ticks * $this
                ->clock_units_per_second / 1e9;
        }
        return count($this->frame_table) / $this->frameRate();
    }
    /**
     * syncSamples positions of the frames that can be decoded on their own.
     *
     * @return array what was read
     */
    public function syncSamples(): array
    {
        return $this->self_contained_frames;
    }
    /**
     * sampleTime works out when a frame is shown, in seconds from the start.
     *
     * @param int $index position of the sample in decode order
     * @return float what was read
     */
    public function sampleTime(int $index): float
    {
        if (!isset($this->frame_table[$index])) {
            return 0.0;
        }
        return $this->frame_table[$index][2] * $this
            ->clock_units_per_second / 1e9;
    }
    /**
     * sampleData the stored bytes of one frame.
     *
     * @param int $index position of the sample in decode order
     * @return string what was read
     */
    public function sampleData(int $index): string
    {
        if (!isset($this->frame_table[$index])) {
            throw new VideoException("no frame at index $index");
        }
        return $this->readBytesAt(
            $this->frame_table[$index][0], $this->frame_table[$index][1]);
    }
    /**
     * codecName the codec name as the file spells it.
     *
     * @return string what was read
     */
    public function codecName(): string
    {
        return $this->codec_name;
    }
    /**
     * containerName webM when the track carries a codec that format allows,
     * Matroska otherwise.
     *
     * @return string what was read
     */
    public function containerName(): string
    {
        /* WebM is the subset of Matroska carrying VP8, VP9 or AV1 */
        return in_array($this->codec_name, ['V_VP8', 'V_VP9', 'V_AV1'], true)
            ? 'WebM' : 'Matroska';
    }
    /**
     * vp9Header the VP9 header of the first keyframe, parsed on demand
     *
     * @return Vp9Header what was read
     */
    public function vp9Header(): Vp9Header
    {
        if ($this->vp9 === null) {
            $self_contained_frames = $this->syncSamples();
            if ($self_contained_frames === []) {
                throw new VideoException('no VP9 keyframe to inspect');
            }
            $this->vp9 = Vp9Header::readSettings($this
                ->sampleData($self_contained_frames[0]));
        }
        return $this->vp9;
    }
    /**
     * codecKind works out which decoder handles this track.
     *
     * @return string what was read
     */
    public function codecKind(): string
    {
        if ($this->codec_name === 'V_VP8') {
            return 'vp8';
        }
        if ($this->codec_name === 'V_VP9') {
            return 'vp9';
        }
        if ($this->codec_name === 'V_MPEGH/ISO/HEVC') {
            return 'hevc';
        }
        /* Matroska also carries H.264, whose decoder is already here */
        if ($this->codec_name === 'V_MPEG4/ISO/AVC') {
            return 'h264';
        }
        return $this->codec_name;
    }
    /**
     * avcConfig the H.264 setup record the container carries, parsed once.
     *
     * @return AvcConfig what was read
     */
    private function avcConfig(): AvcConfig
    {
        if ($this->h264_settings === null) {
            $this->h264_settings
                = ($this->codec_setup_bytes !== ''
                    && ord($this->codec_setup_bytes[0]) === 1)
                ? AvcConfig::readSettings($this->codec_setup_bytes)
                : new AvcConfig();
        }
        return $this->h264_settings;
    }
    /**
     * frameWidth width of the picture in samples.
     *
     * @return int what was read
     */
    public function frameWidth(): int
    {
        return $this->frame_width;
    }
    /**
     * frameHeight height of the picture in samples.
     *
     * @return int what was read
     */
    public function frameHeight(): int
    {
        return $this->frame_height;
    }
    /**
     * frameCount works out how many frames the video holds.
     *
     * @return int what was read
     */
    public function frameCount(): int
    {
        return count($this->frame_table);
    }
    /**
     * settingsUnits the H.264 parameter sets the container carries, if any.
     *
     * @return array what was read
     */
    public function settingsUnits(): array
    {
        if ($this->codecKind() === 'hevc') {
            return HevcParamParser::settingsFromSetupRecord($this
                ->codecPrivate());
        }
        $chunk = $this->avcConfig();
        return array_values(array_filter([$chunk->sequence_settings,
            $chunk->picture_settings]));
    }
    /**
     * toPlainStream turns a stored H.264 frame into a stream of start coded
     * units.
     *
     * @param string $sample the stored bytes of one sample
     * @return string what was read
     */
    public function toPlainStream(string $sample): string
    {
        if ($this->codecKind() === 'hevc') {
            $settings = new AvcConfig();
            $record = $this->codecPrivate();
            $settings->stream_unit_length_size = (strlen($record) < 22) ?
                4 : (ord($record[21]) & 3) + 1;
            return self::plainStreamFrom($settings, $sample);
        }
        if ($this->codecKind() !== 'h264') {
            throw new VideoException(
                "'{$this->codec_name}' is not an H.264 stream");
        }
        return self::plainStreamFrom($this->avcConfig(), $sample);
    }
    /**
     * decodeVp8 decodes one VP8 keyframe into a picture.
     *
     * @param string $frame the stored bytes of one frame
     * @return VideoPicture what was read
     */
    public function decodeVp8(string $frame): VideoPicture
    {
        if ($this->vp8 === null) {
            $this->vp8 = new Vp8Decoder();
        }
        return $this->vp8->decodeKeyframe($frame);
    }
    /**
     * decodeVp9 decodes one VP9 keyframe into a picture.
     *
     * @param string $frame the stored bytes of one frame
     * @return VideoPicture what was read
     */
    public function decodeVp9(string $frame): VideoPicture
    {
        $header = Vp9Header::readSettings($frame);
        $decoder = new Vp9Entropy($header, $frame);
        $decoder->decodePicture();
        return $decoder->toPicture();
    }
    /**
     * unsupportedDetail extra words describing a codec this reader can index
     * but not decode, so the refusal names what was found.
     *
     * @return string what was read
     */
    protected function unsupportedDetail(): string
    {
        if ($this->codec_name !== 'V_VP9') {
            return '';
        }
        try {
            return ' (' . $this->vp9Header()->describe() . ')';
        } catch (Throwable $entry) {
            return '';
        }
    }
    /**
     * codecPrivate the codec's private setup data as the container stores it.
     *
     * @return string what was read
     */
    public function codecPrivate(): string
    {
        return $this->codec_setup_bytes;
    }
    /**
     * decodeHevc decodes one H.265 keyframe into a picture.
     *
     * @param string $frame the stored bytes of one frame
     * @return VideoPicture the decoded picture
     */
    public function decodeHevc(string $frame): VideoPicture
    {
        if ($this->hevc === null) {
            $this->hevc = new HevcDecoder();
            $this->hevc->consumeStored($this->settingsUnits());
        }
        return $this->hevc->decodeKeyframe($this->toPlainStream($frame));
    }
}
X