<?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\Mp4Extractor;
use seekquarry\yioop\library\UnitTest;
/**
* Mp4ExtractorTest checks that Mp4Extractor reads an MP4 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 wiki page asks a video for these
* before it draws a player or a thumbnail. A reader that gets any of
* them wrong shows a broken video rather than failing outright, so each
* case here compares what the reader says against what ffmpeg wrote.
*
* @author Chris Pollett
*/
class Mp4ExtractorTest 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 = 10.0;
/**
* 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 MP4 file that the cases
* read. Each case opens a reader on this path.
* @var string
*/
public $video_path;
/**
* $reader stores the Mp4Extractor under test, opened on the file at
* $video_path.
* @var Mp4Extractor
*/
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 a container's own tables. 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/mp4read" .
getmypid() . ".mp4";
$held = file_get_contents(C\PARENT_DIR .
"/tests/test_files/video_white_mp4.txt");
file_put_contents($this->video_path, base64_decode(trim($held)));
@chmod($this->video_path, 0777);
$this->reader = new Mp4Extractor($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("MP4", $this->reader->containerName(),
"and the file says it is an MP4");
}
/**
* 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("h264", $this->reader->codecKind(),
"the video was coded with h264");
$this->assertEqual("avc1", $this->reader->codecName(),
"which an MP4 names avc1 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 three seconds in must not come
* from the fourth 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");
}
/**
* fileThatIsNotAnMp4IsRefusedTestCase checks that a file which is
* not an MP4 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 fileThatIsNotAnMp4IsRefusedTestCase()
{
$where = C\WORK_DIRECTORY . "/temp/notmp4" . getmypid() . ".mp4";
file_put_contents($where, str_repeat("not a video at all", 20));
@chmod($where, 0777);
$refused = false;
try {
$reader = new Mp4Extractor($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");
}
}