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;
/**
* @var array<string, mixed>
*/
protected array $dimensions = [];
/**
* @var 'landscape'|'portrait'|'squared'
*/
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 'pine'|'crimson'|'lake'|'amber'|'jacaranda'|'onyx' $theme
*/
public function __construct(
protected Episode $episode,
protected float $start,
protected float $end,
string $format,
string $theme,
) {
$this->duration = $end - $start;
$this->format = $format;
$this->episodeNumbering = $this->episodeNumbering($this->episode->number, $this->episode->season_number);
$this->dimensions = config('MediaClipper')
->formats[$format];
$this->colors = config('MediaClipper')
->themes[$theme];
helper('media');
helper(['media']);
$this->audioInput = media_path($this->episode->audio_file_path);
$this->episodeCoverPath = media_path($this->episode->cover->path);
@ -118,7 +132,7 @@ class VideoClip
{
// @phpstan-ignore
$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]",
'[resizedwaves][3:v][4:v][5:v]threshold[cleanwaves]',
'[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]",
"[1:v][waves_final]overlay=x={$this->dimensions['soundwaves']['x']}:y={$this->dimensions['soundwaves']['y']}:shortest=1,drawtext=fontfile=" . $this->getFont(
'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]",
"color=c=0x009486:s={$this->dimensions['width']}x{$this->dimensions['progressbar']['height']}[progressbar]",
) . ":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=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(
'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 = [
@ -142,12 +156,13 @@ class VideoClip
"-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=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) . '"',
'-map "[outv]"',
'-map 0:a',
'-acodec copy',
'-vcodec libx264',
'-pix_fmt yuv420p',
"{$this->videoClipOutput}",
];
@ -184,7 +199,7 @@ class VideoClip
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) {
return false;
@ -265,6 +280,8 @@ class VideoClip
$this->episodeNumbering,
$this->getFont('episodeNumbering'),
$this->dimensions['episodeNumbering']['fontsize'],
$this->colors['episodeNumberingText'],
$this->colors['episodeNumberingBg'],
$this->dimensions['episodeNumbering']['paddingX'],
$this->dimensions['episodeNumbering']['paddingY'],
);
@ -289,6 +306,8 @@ class VideoClip
return false;
}
imagefilter($quotes, IMG_FILTER_COLORIZE, ...$this->colors['quotes']);
$scaledQuotes = $this->scaleImage(
$quotes,
$this->dimensions['quotes']['width'],
@ -319,7 +338,7 @@ class VideoClip
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);
@ -327,7 +346,7 @@ class VideoClip
return null;
}
$coloredBackground = imagecolorallocate($background, 0, 86, 74);
$coloredBackground = imagecolorallocate($background, ...$this->colors['background']);
if ($coloredBackground === false) {
return null;
@ -450,9 +469,9 @@ class VideoClip
int $paragraphIndent = 0,
): bool {
// 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;
}
@ -470,7 +489,7 @@ class VideoClip
0,
$x + ($paragraphIndent * ($i === 0 ? 1 : 0)),
$y + $fontsize + ($leading * $i),
$white,
$textColor,
$fontPath,
$line
);
@ -510,6 +529,10 @@ class VideoClip
];
}
/**
* @param int[] $boxTextColor
* @param int[] $boxBgColor
*/
private function addTextWithBox(
GdImage $image,
int $x,
@ -517,14 +540,16 @@ class VideoClip
string $text,
string $fontPath,
int $fontsize,
array $boxTextColor,
array $boxBgColor,
int $paddingX = 0,
int $paddingY = 0,
): bool {
// Create some colors
$white = imagecolorallocate($image, 255, 255, 255);
$bgColor = imagecolorallocate($image, 0, 61, 11);
$textColor = imagecolorallocate($image, ...$boxTextColor);
$bgColor = imagecolorallocate($image, ...$boxBgColor);
if ($white === false || $bgColor === false) {
if ($textColor === false || $bgColor === false) {
return false;
}
@ -540,7 +565,7 @@ class VideoClip
$y2 = $y + $bbox['height'] + ($paddingY * 2);
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;
}

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(
'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',
],
);
$routes->post(
'video-clips',
'ClipsController::generateVideoClip/$1/$2',
'video-clips/new',
'VideoClipsController::attemptGenerate/$1/$2',
[
'as' => 'video-clips-generate',
'filter' => 'permission:podcast_episodes-edit',

View File

@ -18,7 +18,7 @@ use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use MediaClipper\VideoClip;
class ClipsController extends BaseController
class VideoClipsController extends BaseController
{
protected Podcast $podcast;
@ -55,7 +55,21 @@ class ClipsController extends BaseController
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');
@ -66,18 +80,19 @@ class ClipsController extends BaseController
replace_breadcrumb_params([
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 ?
$rules = [
'format' => 'required|in_list[landscape,portrait,squared]',
'start_time' => 'required|numeric',
'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)) {
@ -92,10 +107,11 @@ class ClipsController extends BaseController
(float) $this->request->getPost('start_time'),
(float) $this->request->getPost('end_time',),
$this->request->getPost('format'),
$this->request->getPost('theme'),
);
$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',
lang('Settings.images.regenerationSuccess')
);

View File

@ -15,6 +15,8 @@ return [
'episode-edit' => 'Edit episode',
'episode-persons-manage' => 'Manage persons',
'embed-add' => 'Embeddable player',
'clips' => 'Clips',
'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-persons-manage' => 'Gestion des intervenants',
'embed' => 'Lecteur intégré',
'clips' => 'Extraits',
'soundbites-edit' => 'Extraits sonores',
'video-clips-list' => 'Extraits video',
'video-clips-generate' => 'Nouvel extrait video',
];

View File

@ -3,7 +3,11 @@
$podcastNavigation = [
'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>
</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
type="number"
name="start_time"
label="START"
required="true"
value="0"
value="5"
/>
<Forms.Field
type="number"
name="end_time"
label="END"
required="true"
value="15"
value="10"
/>
<audio></audio>
<Button variant="primary" type="submit"><?= lang('Episode.video_clips.submit') ?></Button>
</form>