feat(video-clips): replace hardcoded colors with config's theme colors

This commit is contained in:
Yassine Doghri 2021-12-07 16:58:12 +00:00
parent 827ca03f61
commit e462abf6d6
11 changed files with 143 additions and 38 deletions

View File

@ -203,4 +203,24 @@ class MediaClipper extends BaseConfig
], ],
], ],
]; ];
/**
* @var array<string, array<string, string|int[]>>
*/
public array $themes = [
'pine' => [
'background' => [0, 86, 74],
'text' => [255, 255, 255],
// subtitle hex color is BGR (Blue, Green, Red),
'subtitles' => 'FFFFFF',
// quotes image MUST BE black
'quotes' => [0, 148, 134],
'episodeNumberingBg' => [0, 61, 11],
'episodeNumberingText' => [255, 255, 255],
'progressbar' => '009486',
'timestampBg' => '00564A',
'timestampText' => 'FFFFFF',
'soundwaves' => 'F2FAF9',
],
];
} }

View File

@ -44,32 +44,46 @@ class VideoClip
protected ?string $episodeNumbering = null; protected ?string $episodeNumbering = null;
/**
* @var array<string, mixed>
*/
protected array $dimensions = [];
/** /**
* @var 'landscape'|'portrait'|'squared' * @var 'landscape'|'portrait'|'squared'
*/ */
protected string $format = 'landscape'; protected string $format = 'landscape';
/**
* @var array<string, mixed>
*/
protected array $dimensions = [];
/**
* @var 'pine'|'crimson'|'lake'|'amber'|'jacaranda'|'onyx'
*/
protected string $theme = 'pine';
/**
* @var array<string, mixed>
*/
protected array $colors = [];
/** /**
* @param 'landscape'|'portrait'|'squared' $format * @param 'landscape'|'portrait'|'squared' $format
* @param 'pine'|'crimson'|'lake'|'amber'|'jacaranda'|'onyx' $theme
*/ */
public function __construct( public function __construct(
protected Episode $episode, protected Episode $episode,
protected float $start, protected float $start,
protected float $end, protected float $end,
string $format, string $format,
string $theme,
) { ) {
$this->duration = $end - $start; $this->duration = $end - $start;
$this->format = $format; $this->format = $format;
$this->episodeNumbering = $this->episodeNumbering($this->episode->number, $this->episode->season_number); $this->episodeNumbering = $this->episodeNumbering($this->episode->number, $this->episode->season_number);
$this->dimensions = config('MediaClipper') $this->dimensions = config('MediaClipper')
->formats[$format]; ->formats[$format];
$this->colors = config('MediaClipper')
->themes[$theme];
helper('media'); helper(['media']);
$this->audioInput = media_path($this->episode->audio_file_path); $this->audioInput = media_path($this->episode->audio_file_path);
$this->episodeCoverPath = media_path($this->episode->cover->path); $this->episodeCoverPath = media_path($this->episode->cover->path);
@ -118,7 +132,7 @@ class VideoClip
{ {
// @phpstan-ignore // @phpstan-ignore
$filters = [ $filters = [
"[0:a]aformat=channel_layouts=mono,showwaves=s={$this->dimensions['soundwaves']['width']}x{$this->dimensions['soundwaves']['height']}:mode=cline:rate=10:colors=white,format=yuva420p[waves]", "[0:a]aformat=channel_layouts=mono,showwaves=s={$this->dimensions['soundwaves']['width']}x{$this->dimensions['soundwaves']['height']}:mode=cline:rate=10:colors=0xFFFFFF,format=yuva420p[waves]",
"[waves]scale={$this->dimensions['width']}:{$this->dimensions['height']}:flags=neighbor[resizedwaves]", "[waves]scale={$this->dimensions['width']}:{$this->dimensions['height']}:flags=neighbor[resizedwaves]",
'[resizedwaves][3:v][4:v][5:v]threshold[cleanwaves]', '[resizedwaves][3:v][4:v][5:v]threshold[cleanwaves]',
'[cleanwaves][2:v]alphamerge[waves_t]', '[cleanwaves][2:v]alphamerge[waves_t]',
@ -128,11 +142,11 @@ class VideoClip
"[waves_t3]scale={$this->dimensions['soundwaves']['rescaleWidth']}:{$this->dimensions['soundwaves']['rescaleHeight']}[waves_final]", "[waves_t3]scale={$this->dimensions['soundwaves']['rescaleWidth']}:{$this->dimensions['soundwaves']['rescaleHeight']}[waves_final]",
"[1:v][waves_final]overlay=x={$this->dimensions['soundwaves']['x']}:y={$this->dimensions['soundwaves']['y']}:shortest=1,drawtext=fontfile=" . $this->getFont( "[1:v][waves_final]overlay=x={$this->dimensions['soundwaves']['x']}:y={$this->dimensions['soundwaves']['y']}:shortest=1,drawtext=fontfile=" . $this->getFont(
'timestamp' 'timestamp'
) . ":text='%{pts\:gmtime\:{$this->start}\:%H\\\\\\\\\\:%M\\\\\\\\\\:%S\}':x={$this->dimensions['timestamp']['x']}:y={$this->dimensions['timestamp']['y']}:fontsize={$this->dimensions['timestamp']['fontsize']}:fontcolor=white:box=1:boxcolor=0x00564A:boxborderw={$this->dimensions['timestamp']['padding']}[v3]", ) . ":text='%{pts\:gmtime\:{$this->start}\:%H\\\\\\\\\\:%M\\\\\\\\\\:%S\}':x={$this->dimensions['timestamp']['x']}:y={$this->dimensions['timestamp']['y']}:fontsize={$this->dimensions['timestamp']['fontsize']}:fontcolor=0x{$this->colors['timestampText']}:box=1:boxcolor=0x{$this->colors['timestampBg']}:boxborderw={$this->dimensions['timestamp']['padding']},format=yuv420p,colormatrix=bt601:bt2020[v3]",
"color=c=0x009486:s={$this->dimensions['width']}x{$this->dimensions['progressbar']['height']}[progressbar]", "color=c=0x{$this->colors['progressbar']}:s={$this->dimensions['width']}x{$this->dimensions['progressbar']['height']}[progressbar]",
"[v3][progressbar]overlay=-w+(w/{$this->duration})*t:0:shortest=1:format=rgb,subtitles={$this->subtitlesClipOutput}:fontsdir=" . config( "[v3][progressbar]overlay=-w+(w/{$this->duration})*t:0:shortest=1:format=rgb,subtitles={$this->subtitlesClipOutput}:fontsdir=" . config(
'MediaClipper' 'MediaClipper'
)->fontsFolder . ":force_style='Fontname=" . self::FONTS['subtitles'] . ",Alignment=5,Fontsize={$this->dimensions['subtitles']['fontsize']},BorderStyle=1,Outline=0,Shadow=0,MarginL={$this->dimensions['subtitles']['marginL']},MarginR={$this->dimensions['subtitles']['marginR']},MarginV={$this->dimensions['subtitles']['marginV']}'[outv]", )->fontsFolder . ":force_style='Fontname=" . self::FONTS['subtitles'] . ",Alignment=5,Fontsize={$this->dimensions['subtitles']['fontsize']},PrimaryColour=&H{$this->colors['subtitles']}&,BorderStyle=1,Outline=0,Shadow=0,MarginL={$this->dimensions['subtitles']['marginL']},MarginR={$this->dimensions['subtitles']['marginR']},MarginV={$this->dimensions['subtitles']['marginV']}',format=yuv420p,colormatrix=bt601:bt2020[outv]",
]; ];
$videoClipCmd = [ $videoClipCmd = [
@ -142,12 +156,13 @@ class VideoClip
"-loop 1 -framerate 30 -i {$this->dimensions['soundwaves']['mask']}", "-loop 1 -framerate 30 -i {$this->dimensions['soundwaves']['mask']}",
"-f lavfi -i color=gray:{$this->dimensions['width']}x{$this->dimensions['height']}", "-f lavfi -i color=gray:{$this->dimensions['width']}x{$this->dimensions['height']}",
"-f lavfi -i color=black:{$this->dimensions['width']}x{$this->dimensions['height']}", "-f lavfi -i color=black:{$this->dimensions['width']}x{$this->dimensions['height']}",
"-f lavfi -i color=white:{$this->dimensions['width']}x{$this->dimensions['height']}", "-f lavfi -i color=0x{$this->colors['soundwaves']}:{$this->dimensions['width']}x{$this->dimensions['height']}",
'-filter_complex "' . implode(';', $filters) . '"', '-filter_complex "' . implode(';', $filters) . '"',
'-map "[outv]"', '-map "[outv]"',
'-map 0:a', '-map 0:a',
'-acodec copy', '-acodec copy',
'-vcodec libx264', '-vcodec libx264',
'-pix_fmt yuv420p',
"{$this->videoClipOutput}", "{$this->videoClipOutput}",
]; ];
@ -184,7 +199,7 @@ class VideoClip
private function generateVideoClipBg(): bool private function generateVideoClipBg(): bool
{ {
$background = $this->generateColouredBg($this->dimensions['width'], $this->dimensions['height']); $background = $this->generateBackground($this->dimensions['width'], $this->dimensions['height']);
if ($background === null) { if ($background === null) {
return false; return false;
@ -265,6 +280,8 @@ class VideoClip
$this->episodeNumbering, $this->episodeNumbering,
$this->getFont('episodeNumbering'), $this->getFont('episodeNumbering'),
$this->dimensions['episodeNumbering']['fontsize'], $this->dimensions['episodeNumbering']['fontsize'],
$this->colors['episodeNumberingText'],
$this->colors['episodeNumberingBg'],
$this->dimensions['episodeNumbering']['paddingX'], $this->dimensions['episodeNumbering']['paddingX'],
$this->dimensions['episodeNumbering']['paddingY'], $this->dimensions['episodeNumbering']['paddingY'],
); );
@ -289,6 +306,8 @@ class VideoClip
return false; return false;
} }
imagefilter($quotes, IMG_FILTER_COLORIZE, ...$this->colors['quotes']);
$scaledQuotes = $this->scaleImage( $scaledQuotes = $this->scaleImage(
$quotes, $quotes,
$this->dimensions['quotes']['width'], $this->dimensions['quotes']['width'],
@ -319,7 +338,7 @@ class VideoClip
return config('MediaClipper')->fontsFolder . self::FONTS[$name]; return config('MediaClipper')->fontsFolder . self::FONTS[$name];
} }
private function generateColouredBg(int $width, int $height): ?GdImage private function generateBackground(int $width, int $height): ?GdImage
{ {
$background = imagecreatetruecolor($width, $height); $background = imagecreatetruecolor($width, $height);
@ -327,7 +346,7 @@ class VideoClip
return null; return null;
} }
$coloredBackground = imagecolorallocate($background, 0, 86, 74); $coloredBackground = imagecolorallocate($background, ...$this->colors['background']);
if ($coloredBackground === false) { if ($coloredBackground === false) {
return null; return null;
@ -450,9 +469,9 @@ class VideoClip
int $paragraphIndent = 0, int $paragraphIndent = 0,
): bool { ): bool {
// Allocate A Color For The Text // Allocate A Color For The Text
$white = imagecolorallocate($image, 255, 255, 255); $textColor = imagecolorallocate($image, ...$this->colors['text']);
if ($white === false) { if ($textColor === false) {
return false; return false;
} }
@ -470,7 +489,7 @@ class VideoClip
0, 0,
$x + ($paragraphIndent * ($i === 0 ? 1 : 0)), $x + ($paragraphIndent * ($i === 0 ? 1 : 0)),
$y + $fontsize + ($leading * $i), $y + $fontsize + ($leading * $i),
$white, $textColor,
$fontPath, $fontPath,
$line $line
); );
@ -510,6 +529,10 @@ class VideoClip
]; ];
} }
/**
* @param int[] $boxTextColor
* @param int[] $boxBgColor
*/
private function addTextWithBox( private function addTextWithBox(
GdImage $image, GdImage $image,
int $x, int $x,
@ -517,14 +540,16 @@ class VideoClip
string $text, string $text,
string $fontPath, string $fontPath,
int $fontsize, int $fontsize,
array $boxTextColor,
array $boxBgColor,
int $paddingX = 0, int $paddingX = 0,
int $paddingY = 0, int $paddingY = 0,
): bool { ): bool {
// Create some colors // Create some colors
$white = imagecolorallocate($image, 255, 255, 255); $textColor = imagecolorallocate($image, ...$boxTextColor);
$bgColor = imagecolorallocate($image, 0, 61, 11); $bgColor = imagecolorallocate($image, ...$boxBgColor);
if ($white === false || $bgColor === false) { if ($textColor === false || $bgColor === false) {
return false; return false;
} }
@ -540,7 +565,7 @@ class VideoClip
$y2 = $y + $bbox['height'] + ($paddingY * 2); $y2 = $y + $bbox['height'] + ($paddingY * 2);
imagefilledrectangle($image, $x, $y, $x2, $y2, $bgColor); imagefilledrectangle($image, $x, $y, $x2, $y2, $bgColor);
imagettftext($image, $fontsize, 0, $x1, $y1, $white, $fontPath, $text); imagettftext($image, $fontsize, 0, $x1, $y1, $textColor, $fontPath, $text);
return true; return true;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g>
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M17.998 7l2.31-4h.7c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h3.006l-2.31 4h2.31l2.31-4h3.69l-2.31 4h2.31l2.31-4h3.69l-2.31 4h2.31z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 345 B

View File

@ -352,15 +352,23 @@ $routes->group(
); );
$routes->get( $routes->get(
'video-clips', 'video-clips',
'ClipsController::videoClips/$1/$2', 'VideoClipsController::list/$1/$2',
[ [
'as' => 'video-clips', 'as' => 'video-clips-list',
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->get(
'video-clips/new',
'VideoClipsController::generate/$1/$2',
[
'as' => 'video-clips-generate',
'filter' => 'permission:podcast_episodes-edit', 'filter' => 'permission:podcast_episodes-edit',
], ],
); );
$routes->post( $routes->post(
'video-clips', 'video-clips/new',
'ClipsController::generateVideoClip/$1/$2', 'VideoClipsController::attemptGenerate/$1/$2',
[ [
'as' => 'video-clips-generate', 'as' => 'video-clips-generate',
'filter' => 'permission:podcast_episodes-edit', 'filter' => 'permission:podcast_episodes-edit',

View File

@ -18,7 +18,7 @@ use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RedirectResponse;
use MediaClipper\VideoClip; use MediaClipper\VideoClip;
class ClipsController extends BaseController class VideoClipsController extends BaseController
{ {
protected Podcast $podcast; protected Podcast $podcast;
@ -55,7 +55,21 @@ class ClipsController extends BaseController
return $this->{$method}(...$params); return $this->{$method}(...$params);
} }
public function videoClips(): string public function list(): string
{
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('episode/video_clips_list', $data);
}
public function generate(): string
{ {
helper('form'); helper('form');
@ -66,18 +80,19 @@ class ClipsController extends BaseController
replace_breadcrumb_params([ replace_breadcrumb_params([
0 => $this->podcast->title, 0 => $this->podcast->title,
1 => $this->episode->slug, 1 => $this->episode->title,
]); ]);
return view('episode/video_clips', $data); return view('episode/video_clips_new', $data);
} }
public function generateVideoClip(): RedirectResponse public function attemptGenerate(): RedirectResponse
{ {
// TODO: add end_time greater than start_time, with minimum ? // TODO: add end_time greater than start_time, with minimum ?
$rules = [ $rules = [
'format' => 'required|in_list[landscape,portrait,squared]',
'start_time' => 'required|numeric', 'start_time' => 'required|numeric',
'end_time' => 'required|numeric|differs[start_time]', 'end_time' => 'required|numeric|differs[start_time]',
'format' => 'required|in_list[' . implode(',', array_keys(config('MediaClipper')->formats)) . ']',
'theme' => 'required|in_list[' . implode(',', array_keys(config('Colors')->themes)) . ']',
]; ];
if (! $this->validate($rules)) { if (! $this->validate($rules)) {
@ -92,10 +107,11 @@ class ClipsController extends BaseController
(float) $this->request->getPost('start_time'), (float) $this->request->getPost('start_time'),
(float) $this->request->getPost('end_time',), (float) $this->request->getPost('end_time',),
$this->request->getPost('format'), $this->request->getPost('format'),
$this->request->getPost('theme'),
); );
$clipper->generate(); $clipper->generate();
return redirect()->route('video-clips', [$this->podcast->id, $this->episode->id])->with( return redirect()->route('video-clips-generate', [$this->podcast->id, $this->episode->id])->with(
'message', 'message',
lang('Settings.images.regenerationSuccess') lang('Settings.images.regenerationSuccess')
); );

View File

@ -15,6 +15,8 @@ return [
'episode-edit' => 'Edit episode', 'episode-edit' => 'Edit episode',
'episode-persons-manage' => 'Manage persons', 'episode-persons-manage' => 'Manage persons',
'embed-add' => 'Embeddable player', 'embed-add' => 'Embeddable player',
'clips' => 'Clips',
'soundbites-edit' => 'Soundbites', 'soundbites-edit' => 'Soundbites',
'video-clips' => 'Video clips', 'video-clips-list' => 'Video clips',
'video-clips-generate' => 'New video clip',
]; ];

View File

@ -15,5 +15,8 @@ return [
'episode-edit' => 'Modifier lépisode', 'episode-edit' => 'Modifier lépisode',
'episode-persons-manage' => 'Gestion des intervenants', 'episode-persons-manage' => 'Gestion des intervenants',
'embed' => 'Lecteur intégré', 'embed' => 'Lecteur intégré',
'clips' => 'Extraits',
'soundbites-edit' => 'Extraits sonores', 'soundbites-edit' => 'Extraits sonores',
'video-clips-list' => 'Extraits video',
'video-clips-generate' => 'Nouvel extrait video',
]; ];

View File

@ -3,7 +3,11 @@
$podcastNavigation = [ $podcastNavigation = [
'dashboard' => [ 'dashboard' => [
'icon' => 'dashboard', 'icon' => 'dashboard',
'items' => ['episode-view', 'episode-edit', 'episode-persons-manage', 'embed-add', 'soundbites-edit', 'video-clips'], 'items' => ['episode-view', 'episode-edit', 'episode-persons-manage', 'embed-add'],
],
'clips' => [
'icon' => 'clapperboard',
'items' => ['video-clips-list', 'video-clips-generate', 'soundbites-edit'],
], ],
]; ?> ]; ?>

View File

@ -0,0 +1,13 @@
<?= $this->extend('_layout') ?>
<?= $this->section('title') ?>
<?= lang('Episode.video_clips.title') ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= lang('Episode.video_clips.title') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?= $this->endSection() ?>

View File

@ -28,23 +28,31 @@
</div> </div>
</fieldset> </fieldset>
<div class="grid gap-4 grid-cols-colorButtons">
<?php foreach (config('Colors')->themes as $themeName => $color): ?>
<Forms.ColorRadioButton
class="theme-<?= $themeName ?> mx-auto"
value="<?= $themeName ?>"
name="theme"
isChecked="<?= $themeName === 'pine' ? 'true' : 'false' ?>" ><?= lang('Settings.theme.' . $themeName) ?></Forms.ColorRadioButton>
<?php endforeach; ?>
</div>
<Forms.Field <Forms.Field
type="number" type="number"
name="start_time" name="start_time"
label="START" label="START"
required="true" required="true"
value="0" value="5"
/> />
<Forms.Field <Forms.Field
type="number" type="number"
name="end_time" name="end_time"
label="END" label="END"
required="true" required="true"
value="15" value="10"
/> />
<audio></audio>
<Button variant="primary" type="submit"><?= lang('Episode.video_clips.submit') ?></Button> <Button variant="primary" type="submit"><?= lang('Episode.video_clips.submit') ?></Button>
</form> </form>