Give video posters the colours the video shows (#411)
The worker made the poster with a plain scale=320:-2. ffmpeg then read every picture as BT.601 in the limited range, so a red of 255 in a BT.709 video came out as 229 in the JPEG. The picker showed paler posters than the video in the editor. - VideoFormat::colour reads matrix and range of the converted MP4. When the file says nothing, it follows Chrome: BT.709 from 720 lines up, BT.601 below. That guess was measured in Chromium with a red H.264 video without colour tags. - posterArgs turns the picture from that matrix into BT.601 in the full range, the way a JPEG is read. accurate_rnd and full_chroma_int keep dark greys from landing two steps too low. - The search for the first moving video stream is now one helper, used by plan and colour. - Posters made before this change stay as they are. Tests: - videos.test.php in the worker image: ALL PASSED, 90 checks. Three red videos go through the real worker: BT.709 tagged, 720p without tags and 360p in BT.601. Red comes out as 254. - Counter-check with the old filter: 3 checks red, red is 229 again. - php -l clean on all three files. The gate was not run, the change has no TypeScript. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ba68167704
commit
31ce6480b2
3 files changed
+90
-16
No files matched your search
@@ -80,6 +80,13 @@ check('nor is a song with a cover picture', $noPicture(['streams' => [stream('au
|
||||
check('a progress line says how far', VideoFormat::progress("out_time_us=2500000\n", 10.0) === 0.25);
|
||||
check('other lines say nothing', VideoFormat::progress('frame=12', 10.0) === null && VideoFormat::progress('out_time_us=1', 0.0) === null);
|
||||
check('progress never passes the end', VideoFormat::progress('out_time_us=99000000', 10.0) === 1.0);
|
||||
$colourOf = static fn(array $video): array => VideoFormat::colour(['streams' => [stream('video', $video)]]);
|
||||
check('a poster reads a video as the video says', $colourOf(['color_space' => 'bt709', 'height' => 360, 'color_range' => 'tv']) === ['matrix' => 'bt709', 'range' => 'tv']);
|
||||
check('BT.601 by either of its names', $colourOf(['color_space' => 'smpte170m', 'height' => 1080])['matrix'] === 'bt601' && $colourOf(['color_space' => 'bt470bg'])['matrix'] === 'bt601');
|
||||
check('without a word, as Chrome guesses: BT.709 from 720 lines', $colourOf(['height' => 720])['matrix'] === 'bt709' && $colourOf(['color_space' => 'unknown', 'height' => 1080])['matrix'] === 'bt709');
|
||||
check('and BT.601 below', $colourOf(['height' => 719])['matrix'] === 'bt601', $colourOf(['height' => 719]));
|
||||
check('the full range only when it says so', $colourOf(['color_range' => 'pc'])['range'] === 'pc' && $colourOf([])['range'] === 'tv');
|
||||
check('the poster turns it into what a JPEG is read as', str_contains(VideoFormat::posterArgs('in', 'out', 2.0, ['matrix' => 'bt709', 'range' => 'tv'])[13], 'in_color_matrix=bt709:in_range=tv:out_color_matrix=bt601:out_range=pc'));
|
||||
|
||||
// The store and its queue.
|
||||
$data = sys_get_temp_dir() . '/feuerwerk-php-videos-' . bin2hex(random_bytes(4));
|
||||
@@ -229,6 +236,40 @@ check('asking again queues it once', $store->convert($later['file'])['status'] =
|
||||
check('the worker converts it from the kept upload', $worker->runOnce() && $store->record($later['file'])['status'] === 'ready' && $store->record($later['file'])['original'] === $later['original']);
|
||||
check('a video not there cannot be converted', $store->convert("{$folder}/" . str_repeat('0', 64) . '.mp4') === null && $store->convert('../x.mp4') === null);
|
||||
|
||||
// The poster is as red as the video (#411): tagged BT.709, and untagged on both sides of 720 lines.
|
||||
/** A red picture in H.264, its colours turned with `$matrix`, said in the file only when `$tagged`. */
|
||||
function makeRed(string $path, string $size, string $matrix, bool $tagged): void
|
||||
{
|
||||
$tags = $tagged ? ['-colorspace', $matrix, '-color_primaries', $matrix, '-color_trc', $matrix] : [];
|
||||
$args = [
|
||||
'ffmpeg', '-v', 'error', '-y', '-f', 'lavfi', '-i', "color=0xff0000:size={$size}:rate=25:duration=0.6",
|
||||
'-vf', "scale=out_color_matrix={$matrix}:out_range=tv", '-pix_fmt', 'yuv420p', '-c:v', 'libx264', '-qp', '0',
|
||||
...$tags, $path,
|
||||
];
|
||||
exec(implode(' ', array_map('escapeshellarg', $args)), $out, $code);
|
||||
if ($code !== 0) {
|
||||
throw new RuntimeException("cannot make {$path}");
|
||||
}
|
||||
}
|
||||
|
||||
/** @return list<int> red, green, blue in the middle of a JPEG */
|
||||
function middleOf(string $jpeg): array
|
||||
{
|
||||
$rgb = (string) shell_exec('ffmpeg -v error -i ' . escapeshellarg($jpeg) . ' -vf format=rgb24,crop=1:1:iw/2:ih/2 -f rawvideo -pix_fmt rgb24 -');
|
||||
return array_values(unpack('C3', $rgb) ?: []);
|
||||
}
|
||||
|
||||
foreach ([['1280x720', 'bt709', true], ['1280x720', 'bt709', false], ['640x360', 'smpte170m', false]] as [$size, $matrix, $tagged]) {
|
||||
makeRed("{$tmp}/red.mp4", $size, $matrix, $tagged);
|
||||
$red = $store->store((string) file_get_contents("{$tmp}/red.mp4"), 'red.mp4', $folder);
|
||||
while ($worker->runOnce()) {
|
||||
}
|
||||
$rgb = middleOf("{$data}/videos/" . $store->record($red['file'])['poster']);
|
||||
$said = $tagged ? 'saying so' : 'saying nothing';
|
||||
check("a red {$size} video in {$matrix}, {$said}, has a red poster", count($rgb) === 3 && $rgb[0] >= 245 && $rgb[1] <= 10 && $rgb[2] <= 10, $rgb);
|
||||
$store->delete($red['file']);
|
||||
}
|
||||
|
||||
check('deleting a converted video takes poster, details and the upload with it', $store->delete($big['file'])
|
||||
&& glob("{$data}/videos/{$folder}/" . basename($big['file'], '.mp4') . '*') === []);
|
||||
check('deleting a file that is not there says so', $store->delete($big['file']) === false);
|
||||
|
||||
Reference in new issue
Block a user