李帅

1.优化一言添加

......@@ -125,6 +125,7 @@ class VideoTempController extends AdminController
$form->radio('draw', '文字效果')
->options(['fade'=>'淡入淡出', 'fix'=>'固定显示'])->default('fade')
->when('fade',function (Form\NestedForm $form){
$form->number('fade_time', 'fade时间')->default(1500);
$form->selectTable('font_file','字体')
->title('字体选择')
->from(FontTable::make())
......
......@@ -66,6 +66,12 @@ class DevFFmpeg extends Command
*/
public function handle()
{
// $json = shell_exec(env('FFPROBE_CMD') . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg(storage_path('app/public/uploads/131/309/K4LTgHuDhcmDr3MZBDkd0vyMUmRbfBxrFbU0CoNs.png')). ' 2>&1');
// $arr = json_decode($json,true);
// dd($arr);
// dd(AdminMakeVideo::query()->find(1)->poem2());
dd(AdminMakeVideo::query()->find(1)->poem2->verses->toArray());
return 0;
dd(AdminMakeVideo::query()->find(33)->temp->components->toArray());
AdminMakeImmerse::dispatch(AdminMakeVideo::query()->find(33)->temp->components);
......
......@@ -51,52 +51,7 @@ class MakeVideo implements ShouldQueue
// 素材准备
$drawtext = $this->getTextContentString();
if ($media_info['format']['nb_streams'] >= 2) {
/** 音频视频轨都有 */
if ($is_bgm) {
// 有背景音 融合
$audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
$cmd = $this->ffmpeg .
' -y -i ' . escapeshellarg($file) .
' -y -i ' . escapeshellarg($bgm) .
' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
'-ar 48000 -ab 64k ' . escapeshellarg($audio);
if (!$this->execmd($cmd)) return;
$audio_input = ' -i ' . escapeshellarg($audio);
$audio_filter = '2:a';
} else {
// 没有背景音
$audio_input = '';
$audio_filter = '0:a';
}
} elseif ($media_info['format']['nb_streams'] == 1) {
/** 只有视频轨 */
// 生成一段无声音频
$audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
$cmd = $this->ffmpeg .
' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($media_info['format']['duration']) .
' -ar 48000 -ab 64k ' . escapeshellarg($audio);
if (!$this->execmd($cmd)) return;
if ($is_bgm) {
// 有背景音 融合
$audio_empty = $audio;
$audio = $this->getAbsolutePath($this->getTempPath('.mp3'));
$cmd = $this->ffmpeg .
' -y -i ' . escapeshellarg($audio_empty) .
' -y -i ' . escapeshellarg($bgm) .
' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
'-ar 48000 -ab 64k ' . escapeshellarg($audio);
if (!$this->execmd($cmd)) return;
}
$audio_input = ' -i ' . escapeshellarg($audio);
$audio_filter = '2:a';
} else {
/** 音频视频轨都没有 */
Log::channel('daily')->error('视频没有video track, url:' . $file);
return;
}
$thumbnail = $this->getTempPath('.jpg','thumbnail');
if ($adminMakeVideo->thumbnail == 2){
......@@ -174,9 +129,57 @@ class MakeVideo implements ShouldQueue
// 准备素材
// 组装文字参数
$drawtext = $this->getTextContentString();
// 合成视频
if ($this->media_info['format']['nb_streams'] >= 2) {
/** 音频视频轨都有 */
if ($is_bgm) {
// 有背景音 融合
$audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
$cmd = $this->ffmpeg .
' -y -i ' . escapeshellarg($file) .
' -y -i ' . escapeshellarg($bgm) .
' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
'-ar 48000 -ab 64k ' . escapeshellarg($audio);
if (!$this->execmd($cmd)) return;
$audio_input = ' -i ' . escapeshellarg($audio);
$audio_filter = '2:a';
} else {
// 没有背景音
$audio_input = '';
$audio_filter = '0:a';
}
} elseif ($media_info['format']['nb_streams'] == 1) {
/** 只有视频轨 */
// 生成一段无声音频
$audio = $this->getAbsolutePath($this->getTempPath('.mp3','audio'));
$cmd = $this->ffmpeg .
' -y -f lavfi -i aevalsrc=0:duration=' . escapeshellarg($media_info['format']['duration']) .
' -ar 48000 -ab 64k ' . escapeshellarg($audio);
if (!$this->execmd($cmd)) return;
if ($is_bgm) {
// 有背景音 融合
$audio_empty = $audio;
$audio = $this->getAbsolutePath($this->getTempPath('.mp3'));
$cmd = $this->ffmpeg .
' -y -i ' . escapeshellarg($audio_empty) .
' -y -i ' . escapeshellarg($bgm) .
' -filter_complex amix=inputs=2:duration=first:dropout_transition=2 ' .
'-ar 48000 -ab 64k ' . escapeshellarg($audio);
if (!$this->execmd($cmd)) return;
}
$audio_input = ' -i ' . escapeshellarg($audio);
$audio_filter = '2:a';
} else {
/** 音频视频轨都没有 */
Log::channel('daily')->error('视频没有video track, url:' . $file);
return;
}
// 制作封面图
// 分析视频 入库
......@@ -194,7 +197,7 @@ class MakeVideo implements ShouldQueue
if ($this->media_info) return $this->media_info;
$cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
$output = $this->execmd($cmd);
$output = $this->execCmd($cmd);
$data = json_decode($output, true);
if (json_last_error() === JSON_ERROR_UTF8) {
$output = mb_convert_encoding($output, "UTF-8");
......@@ -204,57 +207,140 @@ class MakeVideo implements ShouldQueue
return $data;
}
public function execmd($cmd, $update_progress = false) {
echo $cmd . "\n". "\n". "\n";
$descriptorspec = array(
1 => array("pipe", "w"), // 标准输出,子进程向此管道中写入数据
);
$process = proc_open("{$cmd} 2>&1", $descriptorspec, $pipes);
if (is_resource($process)) {
$error0 = '';
$error1 = '';
$stdout = '';
while (!feof($pipes[1])) {
$line = fgets($pipes[1], 150);
$stdout .= $line;
if ($line) {
//记录错误
$error0 = $error1;
$error1 = $line;
if ($update_progress &&
false !== strpos($line, 'size=') &&
false !== strpos($line, 'time=') &&
false !== strpos($line, 'bitrate='))
public function execCmd($cmd)
{
return shell_exec("{$cmd} 2>&1");
}
public function getTextContentString()
{
//记录进度 size= 3142kB time=00:00:47.22 bitrate= 545.1kbits/s
$line = explode(' ', $line);
$time = null;
foreach ($line as $item) {
$item = explode('=', $item);
if (isset($item[0]) && isset($item[1]) && $item[0] == 'time') {
$time = $item[1];
$components = $this->adminMakeVideo->temp->components;
$drawtext = '';
foreach ($components as $component) {
$text_color = $component->text_color ?? 'white';
$text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
$opacity = $component->opacity ? $component->opacity / 100 : 0.5;
$font_file = $this->getAbsolutePath($component->font_file);
$text_bg_box = $component->text_bg_box ?? 0;
// 文字淡入淡出模式
if ($component->draw == 'fade'){
$contents = []; //
switch ($component->name){
case 'one_poem':
foreach ($this->adminMakeVideo->poem2->verses as $item) {
if ($item->stanza != '') $contents[] = $item->stanza;
}
break;
case 'one_poem_with_annotate':
foreach ($this->adminMakeVideo->poem2->verses as $item) {
if ($item->stanza != '') $contents[] = $item->stanza;
if ($item->annotate != '') $contents[] = $item->annotate;
}
break;
case 'weather':
$contents[] = $this->adminMakeVideo->weather;
break;
case 'date':
$contents[] = Carbon::now()->format('Y年m月d日H时');
break;
case 'feel':
$contents[] = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。';
break;
}
$FID = $FOD = floatval($component->fade_time / 1000);
$round = round($this->media_info['format']['duration'] / count($contents),1);
if ($round < 1) $round = 1;
$sub_text = '';
foreach ($contents as $key => $content){
$DS = $key * $round;
$DE = $DS + $round;
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$sub_text .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'textfile=' . escapeshellarg($text_file) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
'fontcolor_expr=' . escapeshellarg($text_color . '%{eif\\\\: clip(255*(1*between(t\\, ' . $DS . ' + ' . $FID . '\\, ' . $DE . ' - ' . $FOD . ') + ((t - ' . $DS . ')/' . $FID . ')*between(t\\, ' . $DS . '\\, ' . $DS . ' + ' . $FID . ') + (-(t - ' . $DE . ')/' . $FOD . ')*between(t\\, ' . $DE . ' - ' . $FOD . '\\, ' . $DE . '))\\, 0\\, 255) \\\\: x\\\\: 2 }') . ':' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'", ';
}
$drawtext .= $sub_text;
}
// 文字固定模式
if ($component->draw == 'fix'){
$contents = []; //
switch ($component->name){
case 'one_poem_with_annotate':
case 'one_poem':
$stanzas = '';
foreach ($this->adminMakeVideo->poem2->verses as $item) {
if ($item->stanza != '') $stanzas = $item->stanza . "\n";
}
// 切记:在调用 proc_close 之前关闭所有的管道以避免死锁。
fclose($pipes[1]);
$exitedcode = proc_close($process);
if ($exitedcode === 0) {
return $stdout;
} else {
$error = trim($error0,"\n") . ' '. trim($error1,"\n");
// LogUtil::write(array("cmd:{$cmd}", "errno:{$exitedcode}", "stdout:{$stdout}"), __CLASS__);
// ErrorUtil::triggerErrorMsg($error, $exitedcode);
Log::error("cmd:{$cmd}");
Log::error($error);
Log::error("stdout:{$stdout}");
$contents[] = $stanzas;
break;
case 'weather':
$contents[] = $this->adminMakeVideo->weather;
break;
case 'date':
$contents[] = Carbon::now()->format('Y年m月d日H时');
break;
case 'feel':
$contents[] = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。';
break;
}
} else {
// return ErrorUtil::triggerErrorMsg('proc_open error');
Log::error('proc_open error');
$sub_text = '';
foreach ($contents as $key => $content){
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$sub_text .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'textfile=' . escapeshellarg($text_file) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size) . ':' .
'fontcolor=' . $text_color . '@' . $opacity . ':' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'box=1:boxborderw='. $text_bg_box . ':' .
'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
}
$drawtext .= $sub_text;
}
}
return rtrim($drawtext,', ');
}
public function calcFontSize($width)
{
return ceil($this->output_width / 360 * $width);
}
/**
* 获取输出临时文件名
* @param string $ext
* @param string $dir
* @return string
*/
public function getTempPath($ext = '.mp4',$dir = 'video')
{
$filename = "/output_" . time() . rand(0, 10000);
$hash_hex = md5($filename);
// 16进制表示的字符串一共32字节,表示16个二进制字节。
// 前16个字符用来第一级求摸,后16个用做第二级
$hash_hex_l1 = substr($hash_hex, 0, 8);
$hash_hex_l2 = substr($hash_hex, 8, 8);
$dir_l1 = hexdec($hash_hex_l1) % 256;
$dir_l2 = hexdec($hash_hex_l2) % 512;
$dir = $dir . '/' . $dir_l1 . '/' . $dir_l2;
if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
return $dir . $filename . $ext;
}
}
......
......@@ -38,6 +38,11 @@ class AdminMakeVideo extends Model
return $this->hasOne(OnePoem::class,'id','poem_id');
}
public function poem2()
{
return $this->hasOne(OnePoem2::class,'id','poem_id');
}
public function temp()
{
return $this->hasOne(VideoTemp::class,'id','temp_id');
......
......@@ -34,7 +34,7 @@ class VideoTemp extends Model
public function componentsTable()
{
return $this->hasMany('App\Models\Component', 'temp_id')
->select(['id', 'temp_id', 'name', 'position', 'font_size', 'text_color', 'text_bg_color', 'text_bg_box','opacity','fix_bounds']);
->select(['id', 'temp_id', 'name', 'position', 'font_size', 'text_color', 'text_bg_color', 'text_bg_box','opacity']);
}
public function admin_make_video()
......
......@@ -17,6 +17,7 @@ class UpdateComponentsTable extends Migration
Schema::table('components', function (Blueprint $table) {
$table->string('draw')->after('position')->comment('文字效果');
$table->integer('fade_time')->after('draw')->default(1500)->comment('fade切换时间(毫秒)');
});
}
......@@ -27,6 +28,8 @@ class UpdateComponentsTable extends Migration
*/
public function down()
{
Schema::dropColumns('components', ['draw', 'fade_time']);
Schema::table('components', function (Blueprint $table) {
$table->string('fix_bounds')->after('opacity')->comment('超出避免剪切');
});
......