/ tests / WebmExtractorTest.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
 */
namespace seekquarry\yioop\tests;

use seekquarry\yioop\configs as C;
use seekquarry\yioop\library\av_processing\WebmExtractor;
use seekquarry\yioop\library\UnitTest;

/**
 * WebmExtractorTest checks that WebmExtractor reads a WebM file
 * correctly: how long the video runs, how large its frames are, what it
 * was coded with, which frames can be decoded on their own, and where in
 * the file each frame's bytes sit. A WebM keeps its frames in clusters
 * rather than in one table, so the reader walks those clusters to answer
 * any of these. Each case compares what the reader says against what
 * ffmpeg wrote.
 *
 * @author Chris Pollett
 */
class WebmExtractorTest extends UnitTest
{
    /**
     * VIDEO_SECONDS is how many seconds the test video runs, as ffmpeg
     * wrote it. The reader's answer is compared against this.
     * @var float
     */
    const VIDEO_SECONDS = 5.01;
    /**
     * VIDEO_WIDTH is how many pixels across each frame of the test video
     * is, as ffmpeg wrote it.
     * @var int
     */
    const VIDEO_WIDTH = 320;
    /**
     * VIDEO_HEIGHT is how many pixels down each frame of the test video
     * is, as ffmpeg wrote it.
     * @var int
     */
    const VIDEO_HEIGHT = 240;
    /**
     * $video_path stores where setUp wrote the WebM file that the cases
     * read. Each case opens a reader on this path.
     * @var string
     */
    public $video_path;
    /**
     * $reader stores the WebmExtractor under test, opened on the file at
     * $video_path.
     * @var WebmExtractor
     */
    public $reader;
    /**
     * setUp writes the test video out of the base64 it is kept in and
     * opens a reader on it. Every case here needs a real file, since the
     * reader walks the file's clusters. The file is small enough that
     * writing it for each case costs under a hundredth of a second.
     */
    public function setUp()
    {
        $this->video_path = C\WORK_DIRECTORY . "/temp/webmread" .
            getmypid() . ".webm";
        $held = file_get_contents(C\PARENT_DIR .
            "/tests/test_files/video_movie_webm.txt");
        file_put_contents($this->video_path, base64_decode(trim($held)));
        @chmod($this->video_path, 0777);
        $this->reader = new WebmExtractor($this->video_path);
    }
    /**
     * tearDown removes the video file that setUp wrote, so a run leaves
     * nothing behind in the work directory.
     */
    public function tearDown()
    {
        if (file_exists($this->video_path)) {
            unlink($this->video_path);
        }
    }
    /**
     * videosShapeIsReadTestCase checks that the reader gives back how
     * long the video runs, how large its frames are, and which container
     * it came out of. A wiki page sizes its player from these three, so
     * each is compared against what ffmpeg wrote.
     */
    public function videosShapeIsReadTestCase()
    {
        $this->assertTrue(abs($this->reader->durationSeconds() -
            self::VIDEO_SECONDS) < 0.05, "the video runs " .
            self::VIDEO_SECONDS . " seconds, and the reader says " .
            round($this->reader->durationSeconds(), 2));
        $this->assertEqual(self::VIDEO_WIDTH, $this->reader->frameWidth(),
            "each frame is " . self::VIDEO_WIDTH . " pixels across");
        $this->assertEqual(self::VIDEO_HEIGHT,
            $this->reader->frameHeight(),
            "and " . self::VIDEO_HEIGHT . " pixels down");
        $this->assertEqual("WebM", $this->reader->containerName(),
            "and the file says it is a WebM");
    }
    /**
     * whatTheVideoWasCodedWithIsNamedTestCase checks that the reader
     * names what the video was coded with. A caller picks a decoder from
     * that name, so a wrong one sends the frames to a decoder that
     * cannot read them.
     */
    public function whatTheVideoWasCodedWithIsNamedTestCase()
    {
        $this->assertEqual("vp9", $this->reader->codecKind(),
            "the video was coded with vp9");
        $this->assertEqual("V_VP9", $this->reader->codecName(),
            "which a WebM names V_VP9 in its own table");
    }
    /**
     * framesThatStandAloneAreFoundTestCase checks that the reader finds
     * the frames which can be decoded on their own, and hands back the
     * bytes of one. A thumbnail is taken from such a frame, since any
     * other frame needs the frames before it.
     */
    public function framesThatStandAloneAreFoundTestCase()
    {
        $syncs = $this->reader->syncSamples();
        $this->assertTrue(count($syncs) > 0,
            "the video holds at least one frame that stands alone, and " .
            "holds " . count($syncs));
        $first = $syncs[0];
        $this->assertTrue($this->reader->sampleTime($first) <
            self::VIDEO_SECONDS,
            "the first such frame sits inside the video's length");
        $bytes = $this->reader->sampleData($first);
        $this->assertTrue(strlen($bytes) > 0,
            "and its bytes can be read back, which came to " .
            strlen($bytes));
    }
    /**
     * frameForAMomentIsAtOrBeforeItTestCase checks that asking for
     * the frame at a moment gives one at or before that moment, never
     * after it. A thumbnail asked for two seconds in must not come from
     * the third second.
     */
    public function frameForAMomentIsAtOrBeforeItTestCase()
    {
        $syncs = $this->reader->syncSamples();
        $wanted = self::VIDEO_SECONDS / 2;
        $found = -1;
        foreach ($syncs as $one) {
            if ($this->reader->sampleTime($one) <= $wanted) {
                $found = $one;
            }
        }
        $this->assertTrue($found >= 0,
            "a frame at or before the middle of the video is found");
        $this->assertTrue($this->reader->sampleTime($found) <= $wanted,
            "and it does not sit after the moment asked for");
    }
    /**
     * fileThatIsNotAWebmIsRefusedTestCase checks that a file which is
     * not a WebM is refused rather than read as one. A reader that
     * carried on would hand its caller frames made of whatever bytes
     * happened to follow.
     */
    public function fileThatIsNotAWebmIsRefusedTestCase()
    {
        $where = C\WORK_DIRECTORY . "/temp/notwebm" . getmypid() . ".webm";
        file_put_contents($where, str_repeat("not a video at all", 20));
        @chmod($where, 0777);
        $refused = false;
        try {
            $reader = new WebmExtractor($where);
            $reader->durationSeconds();
        } catch (\Exception $trouble) {
            $refused = true;
        }
        if (file_exists($where)) {
            unlink($where);
        }
        $this->assertTrue($refused,
            "a file of ordinary text is refused rather than read");
    }
}
X