Toggle navigation
Toggle navigation
This project
Loading...
Sign in
OnePoem
/
OnePoem-Server
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Snippets
Network
Create a new issue
Builds
Commits
Issue Boards
Authored by
李帅
2023-03-23 19:03:40 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
9b29518cde786358622e477b786d7aeee8695553
9b29518c
1 parent
3cf3b6b8
1.重构一言表
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
774 additions
and
445 deletions
app/Console/Commands/DevFFmpeg.php
app/Jobs/AdminMakeImmerse.bak
app/Jobs/AdminMakeImmerse.php
app/Jobs/MakeVideo.php
app/Console/Commands/DevFFmpeg.php
View file @
9b29518
...
...
@@ -70,9 +70,10 @@ class DevFFmpeg extends Command
// $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
());
// dd(AdminMakeVideo::query()->find(1)->poem2->verses->toArray());
// return 0;
// dd(AdminMakeVideo::query()->find(33)->temp->components->toArray());
dd
(
AdminMakeVideo
::
query
()
->
find
(
33
)
->
temp
->
toArray
());
AdminMakeImmerse
::
dispatch
(
AdminMakeVideo
::
query
()
->
find
(
33
)
->
temp
->
components
);
...
...
app/Jobs/AdminMakeImmerse.bak
0 → 100644
View file @
9b29518
<?php
namespace App\Jobs;
use App\Models\Immerse;
use App\Models\VideoTemp;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\AdminMakeVideo;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class AdminMakeImmerse implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $adminMakeVideo;
protected $ffmpeg;
protected $ffprobe;
protected $media_info;
protected $output_width;
protected $output_height;
/**
* Create a new job instance.
* @param AdminMakeVideo $adminMakeVideo
* @return void
*/
public function __construct(AdminMakeVideo $adminMakeVideo)
{
$this->adminMakeVideo = $adminMakeVideo;
$this->ffmpeg = env('FFMPEG_CMD');
$this->ffprobe = env('FFPROBE_CMD');
$this->output_width = 720;
$this->output_height = 1280;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$adminMakeVideo = $this->adminMakeVideo;
// 模板
$template = $adminMakeVideo->temp->where('state',1)->first();
// 素材准备
$watermark = $this->getAbsolutePath('images/LOGO_eng.png');
$is_bgm = $template->bg_music == 1; //是否手动上传背景音
$bgm = $this->getAbsolutePath($template->bgm_url);
// 区分类型
if ($adminMakeVideo->type == 1) { // 视频
$file = $this->getAbsolutePath($adminMakeVideo->video_url);
// 分析视频
$media_info = $this->mediainfo($file);
// 素材准备
$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){
// 截取中间帧作为视频封面
$frame = ceil($media_info['streams'][0]['nb_frames'] / 2);
$cmd = $this->ffmpeg . ' -y ' .
' -i ' . escapeshellarg($file) .
' -filter_complex "[0:v]select=\'eq(n,' . $frame . ')\'[img]" ' .
' -map [img]'.
' -frames:v 1 -s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
escapeshellarg($this->getAbsolutePath($thumbnail));
if (!$this->execmd($cmd)) return ;
}else{
// 手动上传封面
$origin_thumbnail = Storage::disk('public')->path($adminMakeVideo->thumbnail_url);
// 将封面分辨率改为指定分辨率
$cmd = $this->ffmpeg . ' -y ' .
' -i ' . escapeshellarg($origin_thumbnail) .
'-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
escapeshellarg($this->getAbsolutePath($thumbnail));
if (!$this->execmd($cmd)) return ;
}
$output = $this->getTempPath('.mp4','video');
$cmd = $this->ffmpeg . ' -y '.
' -i ' . escapeshellarg($file).
' -i ' . escapeshellarg($watermark).
$audio_input .
' -filter_complex "[0:v]scale=' . $this->output_width . ':' . $this->output_height . ',' . $drawtext .
' [text];[text]'.
' [1:v]overlay=20:20[v]" ' .
' -map [v] -map '. $audio_filter .
' -c:v libx264 -bt 256k -r 25' .
' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast ' .
escapeshellarg($this->getAbsolutePath($output));
if (!$this->execmd($cmd)) return ;
$video_info = $this->mediainfo($this->getAbsolutePath($output));
Immerse::query()->create([
'user_id' => 1,
'title' => '',
'weather' => $adminMakeVideo->weather,
'huangli' => $adminMakeVideo->huangli,
'content' => $adminMakeVideo->feel,
'location' => $adminMakeVideo->location,
'longitude' => $adminMakeVideo->longitude,
'latitude' => $adminMakeVideo->latitude,
'url' => $output,
'type' => $adminMakeVideo->type == 1 ? 2 : 1,
'upload_file' => '',
'duration' => $video_info['format']['duration'],
'size' => $video_info['format']['size'],
'origin_video_url' => $this->adminMakeVideo->video_url,
'origin_image_url' => '',
'poem_id' => $this->adminMakeVideo->poem_id,
'temp_id' => $this->adminMakeVideo->temp_id,
'thumbnail' => $thumbnail,
'state' => 1,
'bgm' => $is_bgm ? $bgm : '',
]);
}else{ // 图文
$image = $this->getAbsolutePath($adminMakeVideo->images_url);
// 素材准备
$drawtext = $this->getTextContentString();
if ($this->adminMakeVideo->type == 2 && !$is_bgm){
// 没有背景音,单图一张,输出为单图。
$output = $this->getTempPath('.png','thumbnail');
$cmd = $this->ffmpeg . ' -y '.
' -i ' . escapeshellarg($image).
' -i ' . escapeshellarg($watermark).
' -filter_complex "[0:0]scale=' . $this->output_width . ':' . $this->output_height . ',' .
$drawtext . ' [text];[text][1:0]overlay=20:20" ' .
escapeshellarg($this->getAbsolutePath($output));
if (!$this->execmd($cmd)) return ;
$thumbnail = $output;
}else{
// 有背景音 单图合成视频,时长为音频时长,音频加入背景音
$output = $this->getTempPath('.mp4','video');
// 分析背景音
$mediainfo = $this->mediainfo($bgm);
// 记录媒体信息时长
$duration = $mediainfo['format']['duration'] ?: 0;
// 单图、水印、bgm 合成视频
$cmd = $this->ffmpeg . ' -y ' .
' -loop 1 -i ' . escapeshellarg($image) .
' -i ' . escapeshellarg($watermark) .
' -i ' . escapeshellarg($bgm) .
' -filter_complex "[0:v]scale=' . $this->output_width . ':' . $this->output_height . ',setdar=dar=9/16,' . $drawtext .
' [text];[text][2:v]overlay=20:20[v]"' .
' -map [v] -map 1:0 ' .
' -c:v libx264 -bt 256k -r 25 -t ' . $duration .
' -ar 48000 -ac 2 -qmin 30 -qmax 60 -profile:v high -pix_fmt yuv420p -preset fast '.
escapeshellarg($this->getAbsolutePath($output));
if (!$this->execmd($cmd)) return ;
$thumbnail = $this->getTempPath('.jpg','thumbnail');
if ($adminMakeVideo->thumbnail == 2){
// 将封面分辨率改为指定分辨率
$cmd = $this->ffmpeg . ' -y ' .
' -i ' . escapeshellarg($image) .
'-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
escapeshellarg($this->getAbsolutePath($thumbnail));
if (!$this->execmd($cmd)) return ;
}else{
// 手动上传封面
$origin_thumbnail = $this->getAbsolutePath($adminMakeVideo->thumbnail_url);
// 将封面分辨率改为指定分辨率
$cmd = $this->ffmpeg . ' -y ' .
' -i ' . escapeshellarg($origin_thumbnail) .
'-s ' . $this->output_width . 'x' . $this->output_height . ' -preset superfast ' .
escapeshellarg($this->getAbsolutePath($thumbnail));
if (!$this->execmd($cmd)) return ;
}
}
// 全部合成以后创建 临境
$video_info = $this->mediainfo($output);
Immerse::query()->create([
'user_id' => 1,
'title' => '',
'weather' => $adminMakeVideo->weather,
'huangli' => $adminMakeVideo->huangli,
'content' => $adminMakeVideo->feel,
'location' => $adminMakeVideo->location,
'longitude' => $adminMakeVideo->longitude,
'latitude' => $adminMakeVideo->latitude,
'url' => $output,
'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
'upload_file' => '',
'duration' => $video_info['format']['duration'] ?? 0,
'size' => $video_info['format']['size'] ?? 0,
'origin_video_url' => '',
'origin_image_url' => $this->adminMakeVideo->images_url,
'poem_id' => $this->adminMakeVideo->poem_id,
'temp_id' => $this->adminMakeVideo->temp_id,
'thumbnail' => $thumbnail,
'state' => 1,
'bgm' => $is_bgm ? $bgm : '',
]);
}
}
public function mediainfo($file)
{
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);
$data = json_decode($output, true);
if (json_last_error() === JSON_ERROR_UTF8) {
$output = mb_convert_encoding($output, "UTF-8");
$data = json_decode($output, true);
}
$this->media_info = $data;
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='))
{
//记录进度 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];
break;
}
}
}
}
}
// 切记:在调用 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}");
}
} else {
// return ErrorUtil::triggerErrorMsg('proc_open error');
Log::error('proc_open error');
}
}
/**
* 获取输出临时文件名
* @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;
}
public function getAbsolutePath($path)
{
if ($path == '') return '';
return Storage::disk('public')->path($path);
}
public function getTextContentString()
{
$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;
$fix_bounds = $component->fix_bounds == 1;
switch ($component->name){
case 'every_poem':
case 'one_poem':
$content = $this->adminMakeVideo->poem->content;
$contents = explode("\n",$content); //计算诗词行数
if ($this->media_info['format']['duration'] < 2 * count($contents)) {
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$drawtext .= '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]) . ':' .
'fix_bounds='. $fix_bounds . ':' .
'box=1:boxborderw='. $text_bg_box . ':' .
'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
}else{
$FID = $FOD = 1.0;
if ($this->media_info['format']['duration'] > 3 * count($contents)) $FID = $FOD = 1.5;
$round = round($this->media_info['format']['duration'] / count($contents),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]) . ':' .
'fix_bounds='. $fix_bounds . '", ';
}
$drawtext .= $sub_text;
}
break;
case 'weather':
$content = $this->adminMakeVideo->weather;
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$drawtext .= '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]) . ':' .
'fix_bounds='. $fix_bounds . ':' .
'box=1:boxborderw='. $text_bg_box . ':' .
'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
case 'date':
$content = Carbon::now()->format('Y年m月d日H时');
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$drawtext .= '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]) . ':' .
'fix_bounds='. $fix_bounds . ':' .
'box=1:boxborderw='. $text_bg_box . ':' .
'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
case 'feel':
$content = $this->adminMakeVideo->feel ?: '读此一言,仿佛身临其境。';
$text_file = $this->getAbsolutePath($this->getTempPath('.txt','text'));
file_put_contents($text_file, $content);
$drawtext .= '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]) . ':' .
'fix_bounds='. $fix_bounds . ':' .
'box=1:boxborderw='. $text_bg_box . ':' .
'boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
}
}
return rtrim($drawtext,', ');
}
public function calcFontSize($width)
{
return ceil($this->output_width / 360 * $width);
}
public function calcBorderSize($width)
{
return ceil($this->output_width / 360 * $width);
}
public function getTextHeight()
{
$height = $this->output_height;
}
}
app/Jobs/AdminMakeImmerse.php
View file @
9b29518
...
...
@@ -51,225 +51,143 @@ class AdminMakeImmerse implements ShouldQueue
*/
public
function
handle
()
{
$adminMakeVideo
=
$this
->
adminMakeVideo
;
$file
=
$this
->
getAbsolutePath
(
$this
->
adminMakeVideo
->
video_url
);
// 分析视频
$this
->
media_info
=
$this
->
mediaInfo
(
$file
);
// 模板
$template
=
$adminMakeVideo
->
temp
->
where
(
'state'
,
1
)
->
first
();
// 素材准备
// 准备素材
$watermark
=
$this
->
getAbsolutePath
(
'images/LOGO_eng.png'
);
$is_bgm
=
$template
->
bg_music
==
1
;
//是否手动上传背景音
$bgm
=
$this
->
getAbsolutePath
(
$template
->
bgm_url
);
// 区分类型
if
(
$adminMakeVideo
->
type
==
1
)
{
// 视频
$file
=
$this
->
getAbsolutePath
(
$adminMakeVideo
->
video_url
);
// 分析视频
$media_info
=
$this
->
mediainfo
(
$file
);
// 素材准备
$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
)
{
/** 只有视频轨 */
// 生成一段无声音频
// 组装文字参数
$drawtext
=
$this
->
getTextContentString
();
// 判断双轨 没有则制作空轨
$is_bgm
=
$this
->
adminMakeVideo
->
temp
->
bg_music
==
1
;
//是否手动上传背景音
if
(
$this
->
media_info
[
'format'
][
'nb_streams'
]
>=
2
)
{
/** 音频视频轨都有 */
if
(
$is_bgm
)
{
// 有背景音 融合
$audio
=
$this
->
getAbsolutePath
(
$this
->
getTempPath
(
'.mp3'
,
'audio'
));
$bgm
=
$this
->
getAbsolutePath
(
$this
->
adminMakeVideo
->
temp
->
bgm_url
);
$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
;
}
' -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
->
execCmd
(
$cmd
))
return
;
$audio_input
=
' -i '
.
escapeshellarg
(
$audio
);
$audio_filter
=
'2:a'
;
}
else
{
/
** 音频视频轨都没有 */
Log
::
channel
(
'daily'
)
->
error
(
'视频没有video track, url:'
.
$file
)
;
return
;
/
/ 没有背景音
$audio_input
=
''
;
$audio_filter
=
'0:a'
;
}
$thumbnail
=
$this
->
getTempPath
(
'.jpg'
,
'thumbnail'
);
if
(
$adminMakeVideo
->
thumbnail
==
2
){
// 截取中间帧作为视频封面
$frame
=
ceil
(
$media_info
[
'streams'
][
0
][
'nb_frames'
]
/
2
);
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$file
)
.
' -filter_complex "[0:v]select=\'eq(n,'
.
$frame
.
')\'[img]" '
.
' -map [img]'
.
' -frames:v 1 -s '
.
$this
->
output_width
.
'x'
.
$this
->
output_height
.
' -preset superfast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$thumbnail
));
if
(
!
$this
->
execmd
(
$cmd
))
return
;
}
else
{
// 手动上传封面
$origin_thumbnail
=
Storage
::
disk
(
'public'
)
->
path
(
$adminMakeVideo
->
thumbnail_url
);
// 将封面分辨率改为指定分辨率
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$origin_thumbnail
)
.
'-s '
.
$this
->
output_width
.
'x'
.
$this
->
output_height
.
' -preset superfast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$thumbnail
));
if
(
!
$this
->
execmd
(
$cmd
))
return
;
}
$output
=
$this
->
getTempPath
(
'.mp4'
,
'video'
);
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$file
)
.
' -i '
.
escapeshellarg
(
$watermark
)
.
$audio_input
.
' -filter_complex "[0:v]scale='
.
$this
->
output_width
.
':'
.
$this
->
output_height
.
','
.
$drawtext
.
' [text];[text]'
.
' [1:v]overlay=20:20[v]" '
.
' -map [v] -map '
.
$audio_filter
.
' -c:v libx264 -bt 256k -r 25'
.
' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$output
));
if
(
!
$this
->
execmd
(
$cmd
))
return
;
$video_info
=
$this
->
mediainfo
(
$this
->
getAbsolutePath
(
$output
));
Immerse
::
query
()
->
create
([
'user_id'
=>
1
,
'title'
=>
''
,
'weather'
=>
$adminMakeVideo
->
weather
,
'huangli'
=>
$adminMakeVideo
->
huangli
,
'content'
=>
$adminMakeVideo
->
feel
,
'location'
=>
$adminMakeVideo
->
location
,
'longitude'
=>
$adminMakeVideo
->
longitude
,
'latitude'
=>
$adminMakeVideo
->
latitude
,
'url'
=>
$output
,
'type'
=>
$adminMakeVideo
->
type
==
1
?
2
:
1
,
'upload_file'
=>
''
,
'duration'
=>
$video_info
[
'format'
][
'duration'
],
'size'
=>
$video_info
[
'format'
][
'size'
],
'origin_video_url'
=>
$this
->
adminMakeVideo
->
video_url
,
'origin_image_url'
=>
''
,
'poem_id'
=>
$this
->
adminMakeVideo
->
poem_id
,
'temp_id'
=>
$this
->
adminMakeVideo
->
temp_id
,
'thumbnail'
=>
$thumbnail
,
'state'
=>
1
,
'bgm'
=>
$is_bgm
?
$bgm
:
''
,
]);
}
else
{
// 图文
$image
=
$this
->
getAbsolutePath
(
$adminMakeVideo
->
images_url
);
// 素材准备
$drawtext
=
$this
->
getTextContentString
();
if
(
$this
->
adminMakeVideo
->
type
==
2
&&
!
$is_bgm
){
// 没有背景音,单图一张,输出为单图。
$output
=
$this
->
getTempPath
(
'.png'
,
'thumbnail'
);
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$image
)
.
' -i '
.
escapeshellarg
(
$watermark
)
.
' -filter_complex "[0:0]scale='
.
$this
->
output_width
.
':'
.
$this
->
output_height
.
','
.
$drawtext
.
' [text];[text][1:0]overlay=20:20" '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$output
));
if
(
!
$this
->
execmd
(
$cmd
))
return
;
$thumbnail
=
$output
;
}
else
{
// 有背景音 单图合成视频,时长为音频时长,音频加入背景音
$output
=
$this
->
getTempPath
(
'.mp4'
,
'video'
);
// 分析背景音
$mediainfo
=
$this
->
mediainfo
(
$bgm
);
// 记录媒体信息时长
$duration
=
$mediainfo
[
'format'
][
'duration'
]
?:
0
;
// 单图、水印、bgm 合成视频
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -loop 1 -i '
.
escapeshellarg
(
$image
)
.
' -i '
.
escapeshellarg
(
$watermark
)
.
' -i '
.
escapeshellarg
(
$bgm
)
.
' -filter_complex "[0:v]scale='
.
$this
->
output_width
.
':'
.
$this
->
output_height
.
',setdar=dar=9/16,'
.
$drawtext
.
' [text];[text][2:v]overlay=20:20[v]"'
.
' -map [v] -map 1:0 '
.
' -c:v libx264 -bt 256k -r 25 -t '
.
$duration
.
' -ar 48000 -ac 2 -qmin 30 -qmax 60 -profile:v high -pix_fmt yuv420p -preset fast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$output
));
if
(
!
$this
->
execmd
(
$cmd
))
return
;
$thumbnail
=
$this
->
getTempPath
(
'.jpg'
,
'thumbnail'
);
if
(
$adminMakeVideo
->
thumbnail
==
2
){
// 将封面分辨率改为指定分辨率
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$image
)
.
'-s '
.
$this
->
output_width
.
'x'
.
$this
->
output_height
.
' -preset superfast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$thumbnail
));
if
(
!
$this
->
execmd
(
$cmd
))
return
;
}
else
{
// 手动上传封面
$origin_thumbnail
=
$this
->
getAbsolutePath
(
$adminMakeVideo
->
thumbnail_url
);
// 将封面分辨率改为指定分辨率
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$origin_thumbnail
)
.
'-s '
.
$this
->
output_width
.
'x'
.
$this
->
output_height
.
' -preset superfast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$thumbnail
));
if
(
!
$this
->
execmd
(
$cmd
))
return
;
}
}
elseif
(
$this
->
media_info
[
'format'
][
'nb_streams'
]
==
1
)
{
/** 只有视频轨 */
// 生成一段无声音频
$audio
=
$this
->
getAbsolutePath
(
$this
->
getTempPath
(
'.mp3'
,
'audio'
));
$cmd
=
$this
->
ffmpeg
.
' -y -f lavfi -i aevalsrc=0:duration='
.
escapeshellarg
(
$this
->
media_info
[
'format'
][
'duration'
])
.
' -ar 48000 -ab 64k '
.
escapeshellarg
(
$audio
);
if
(
!
$this
->
execCmd
(
$cmd
))
return
;
if
(
$is_bgm
)
{
// 有背景音 融合
$audio_empty
=
$audio
;
$bgm
=
$this
->
getAbsolutePath
(
$this
->
adminMakeVideo
->
temp
->
bgm_url
);
$audio
=
$this
->
getAbsolutePath
(
$this
->
getTempPath
(
'.mp3'
,
'audio'
));
$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
->
execCmd
(
$cmd
))
return
;
}
$audio_input
=
' -i '
.
escapeshellarg
(
$audio
);
$audio_filter
=
'2:a'
;
}
else
{
/** 音频视频轨都没有 */
Log
::
channel
(
'daily'
)
->
error
(
'视频没有video track, url:'
.
$file
);
return
;
}
// 全部合成以后创建 临境
$video_info
=
$this
->
mediainfo
(
$output
);
Immerse
::
query
()
->
create
([
'user_id'
=>
1
,
'title'
=>
''
,
'weather'
=>
$adminMakeVideo
->
weather
,
'huangli'
=>
$adminMakeVideo
->
huangli
,
'content'
=>
$adminMakeVideo
->
feel
,
'location'
=>
$adminMakeVideo
->
location
,
'longitude'
=>
$adminMakeVideo
->
longitude
,
'latitude'
=>
$adminMakeVideo
->
latitude
,
'url'
=>
$output
,
'type'
=>
$this
->
adminMakeVideo
->
type
==
1
?
2
:
1
,
'upload_file'
=>
''
,
'duration'
=>
$video_info
[
'format'
][
'duration'
]
??
0
,
'size'
=>
$video_info
[
'format'
][
'size'
]
??
0
,
'origin_video_url'
=>
''
,
'origin_image_url'
=>
$this
->
adminMakeVideo
->
images_url
,
'poem_id'
=>
$this
->
adminMakeVideo
->
poem_id
,
'temp_id'
=>
$this
->
adminMakeVideo
->
temp_id
,
'thumbnail'
=>
$thumbnail
,
'state'
=>
1
,
'bgm'
=>
$is_bgm
?
$bgm
:
''
,
]);
// 制作封面图
$thumbnail
=
$this
->
getTempPath
(
'.jpg'
,
'thumbnail'
);
if
(
$this
->
adminMakeVideo
->
thumbnail
==
2
){
// 截取中间帧作为视频封面
$frame
=
ceil
(
$this
->
media_info
[
'streams'
][
0
][
'nb_frames'
]
/
2
);
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$file
)
.
' -filter_complex "[0:v]select=\'eq(n,'
.
$frame
.
')\'[img]" '
.
' -map [img]'
.
' -frames:v 1 -s '
.
$this
->
output_width
.
'x'
.
$this
->
output_height
.
' -preset superfast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$thumbnail
));
if
(
!
$this
->
execCmd
(
$cmd
))
return
;
}
else
{
// 手动上传封面
$origin_thumbnail
=
$this
->
getAbsolutePath
(
$this
->
adminMakeVideo
->
thumbnail_url
);
// 将封面分辨率改为指定分辨率
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$origin_thumbnail
)
.
'-s '
.
$this
->
output_width
.
'x'
.
$this
->
output_height
.
' -preset superfast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$thumbnail
));
if
(
!
$this
->
execCmd
(
$cmd
))
return
;
}
// 合成视频
$output
=
$this
->
getTempPath
(
'.mp4'
,
'video'
);
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$file
)
.
' -i '
.
escapeshellarg
(
$watermark
)
.
$audio_input
.
' -filter_complex "[0:v]scale='
.
$this
->
output_width
.
':'
.
$this
->
output_height
.
','
.
$drawtext
.
' [text];[text]'
.
' [1:v]overlay=20:20[v]" '
.
' -map [v] -map '
.
$audio_filter
.
' -c:v libx264 -bt 256k -r 25'
.
' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$output
));
if
(
!
$this
->
execCmd
(
$cmd
))
return
;
// 分析视频 入库
$video_info
=
$this
->
mediainfo
(
$this
->
getAbsolutePath
(
$output
));
Immerse
::
query
()
->
create
([
'user_id'
=>
1
,
'title'
=>
''
,
'weather'
=>
$this
->
adminMakeVideo
->
weather
,
'huangli'
=>
$this
->
adminMakeVideo
->
huangli
,
'content'
=>
$this
->
adminMakeVideo
->
feel
,
'location'
=>
$this
->
adminMakeVideo
->
location
,
'longitude'
=>
$this
->
adminMakeVideo
->
longitude
,
'latitude'
=>
$this
->
adminMakeVideo
->
latitude
,
'url'
=>
$output
,
'type'
=>
$this
->
adminMakeVideo
->
type
==
1
?
2
:
1
,
'upload_file'
=>
''
,
'duration'
=>
$video_info
[
'format'
][
'duration'
],
'size'
=>
$video_info
[
'format'
][
'size'
],
'origin_video_url'
=>
$this
->
adminMakeVideo
->
video_url
,
'origin_image_url'
=>
''
,
'poem_id'
=>
$this
->
adminMakeVideo
->
poem_id
,
'temp_id'
=>
$this
->
adminMakeVideo
->
temp_id
,
'thumbnail'
=>
$thumbnail
,
'state'
=>
1
,
'bgm'
=>
$is_bgm
?
$bgm
:
''
,
]);
}
public
function
mediainfo
(
$file
)
public
function
getAbsolutePath
(
$path
)
{
if
(
$path
==
''
)
return
''
;
return
Storage
::
disk
(
'public'
)
->
path
(
$path
);
}
public
function
mediaInfo
(
$file
)
{
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
->
exec
C
md
(
$cmd
);
$data
=
json_decode
(
$output
,
true
);
if
(
json_last_error
()
===
JSON_ERROR_UTF8
)
{
$output
=
mb_convert_encoding
(
$output
,
"UTF-8"
);
...
...
@@ -279,90 +197,9 @@ class AdminMakeImmerse 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='
))
{
//记录进度 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
];
break
;
}
}
}
}
}
// 切记:在调用 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
}
"
);
}
}
else
{
// return ErrorUtil::triggerErrorMsg('proc_open error');
Log
::
error
(
'proc_open error'
);
}
}
/**
* 获取输出临时文件名
* @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
;
}
public
function
getAbsolutePath
(
$path
)
public
function
execCmd
(
$cmd
)
{
if
(
$path
==
''
)
return
''
;
return
Storage
::
disk
(
'public'
)
->
path
(
$path
);
return
shell_exec
(
"
{
$cmd
}
2>&1"
);
}
public
function
getTextContentString
()
...
...
@@ -371,103 +208,97 @@ class AdminMakeImmerse implements ShouldQueue
$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
;
$fix_bounds
=
$component
->
fix_bounds
==
1
;
switch
(
$component
->
name
){
case
'every_poem'
:
case
'one_poem'
:
$content
=
$this
->
adminMakeVideo
->
poem
->
content
;
$contents
=
explode
(
"
\n
"
,
$content
);
//计算诗词行数
if
(
$this
->
media_info
[
'format'
][
'duration'
]
<
2
*
count
(
$contents
))
{
$text_file
=
$this
->
getAbsolutePath
(
$this
->
getTempPath
(
'.txt'
,
'text'
));
file_put_contents
(
$text_file
,
$content
);
$drawtext
.=
'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
])
.
':'
.
'fix_bounds='
.
$fix_bounds
.
':'
.
'box=1:boxborderw='
.
$text_bg_box
.
':'
.
'boxcolor='
.
$text_bg_color
.
'@'
.
$opacity
.
'", '
;
}
else
{
$FID
=
$FOD
=
1.0
;
if
(
$this
->
media_info
[
'format'
][
'duration'
]
>
3
*
count
(
$contents
))
$FID
=
$FOD
=
1.5
;
$round
=
round
(
$this
->
media_info
[
'format'
][
'duration'
]
/
count
(
$contents
),
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
])
.
':'
.
'fix_bounds='
.
$fix_bounds
.
'", '
;
// 文字淡入淡出模式
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
;
}
$drawtext
.=
$sub_text
;
}
break
;
case
'weather'
:
$content
=
$this
->
adminMakeVideo
->
weather
;
$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
);
$
draw
text
.=
'drawtext="'
.
$
sub_
text
.=
'drawtext="'
.
'fontfile='
.
escapeshellarg
(
$font_file
)
.
':'
.
'textfile='
.
escapeshellarg
(
$text_file
)
.
':'
.
'fontsize='
.
$this
->
calcFontSize
(
$component
->
font_size
)
.
':'
.
'fontcolor
='
.
$text_color
.
'@'
.
$opacity
.
':'
.
'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
])
.
':'
.
'fix_bounds='
.
$fix_bounds
.
':'
.
'box=1:boxborderw='
.
$text_bg_box
.
':'
.
'boxcolor='
.
$text_bg_color
.
'@'
.
$opacity
.
'", '
;
'", '
;
}
break
;
case
'date'
:
$content
=
Carbon
::
now
()
->
format
(
'Y年m月d日H时'
);
$text_file
=
$this
->
getAbsolutePath
(
$this
->
getTempPath
(
'.txt'
,
'text'
));
file_put_contents
(
$text_file
,
$content
);
$drawtext
.=
'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
])
.
':'
.
'fix_bounds='
.
$fix_bounds
.
':'
.
'box=1:boxborderw='
.
$text_bg_box
.
':'
.
'boxcolor='
.
$text_bg_color
.
'@'
.
$opacity
.
'", '
;
break
;
case
'feel'
:
$content
=
$this
->
adminMakeVideo
->
feel
?:
'读此一言,仿佛身临其境。'
;
$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
"
;
}
$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
;
}
$sub_text
=
''
;
foreach
(
$contents
as
$key
=>
$content
){
$text_file
=
$this
->
getAbsolutePath
(
$this
->
getTempPath
(
'.txt'
,
'text'
));
file_put_contents
(
$text_file
,
$content
);
$
draw
text
.=
'drawtext="'
.
$
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
])
.
':'
.
'fix_bounds='
.
$fix_bounds
.
':'
.
'box=1:boxborderw='
.
$text_bg_box
.
':'
.
'boxcolor='
.
$text_bg_color
.
'@'
.
$opacity
.
'", '
;
break
;
}
$drawtext
.=
$sub_text
;
}
}
...
...
@@ -479,14 +310,27 @@ class AdminMakeImmerse implements ShouldQueue
return
ceil
(
$this
->
output_width
/
360
*
$width
);
}
public
function
calcBorderSize
(
$width
)
/**
* 获取输出临时文件名
* @param string $ext
* @param string $dir
* @return string
*/
public
function
getTempPath
(
$ext
=
'.mp4'
,
$dir
=
'video'
)
{
return
ceil
(
$this
->
output_width
/
360
*
$width
);
}
$filename
=
"/output_"
.
time
()
.
rand
(
0
,
10000
);
public
function
getTextHeight
()
{
$height
=
$this
->
output_height
;
$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
;
}
}
...
...
app/Jobs/MakeVideo.php
View file @
9b29518
...
...
@@ -44,75 +44,6 @@ class MakeVideo implements ShouldQueue
$this
->
ffprobe
=
env
(
'FFPROBE_CMD'
);
$this
->
output_width
=
720
;
$this
->
output_height
=
1280
;
$file
=
$this
->
getAbsolutePath
(
$adminMakeVideo
->
video_url
);
// 分析视频
$media_info
=
$this
->
mediainfo
(
$file
);
// 素材准备
$drawtext
=
$this
->
getTextContentString
();
$thumbnail
=
$this
->
getTempPath
(
'.jpg'
,
'thumbnail'
);
if
(
$adminMakeVideo
->
thumbnail
==
2
){
// 截取中间帧作为视频封面
$frame
=
ceil
(
$media_info
[
'streams'
][
0
][
'nb_frames'
]
/
2
);
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$file
)
.
' -filter_complex "[0:v]select=\'eq(n,'
.
$frame
.
')\'[img]" '
.
' -map [img]'
.
' -frames:v 1 -s '
.
$this
->
output_width
.
'x'
.
$this
->
output_height
.
' -preset superfast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$thumbnail
));
if
(
!
$this
->
execmd
(
$cmd
))
return
;
}
else
{
// 手动上传封面
$origin_thumbnail
=
Storage
::
disk
(
'public'
)
->
path
(
$adminMakeVideo
->
thumbnail_url
);
// 将封面分辨率改为指定分辨率
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$origin_thumbnail
)
.
'-s '
.
$this
->
output_width
.
'x'
.
$this
->
output_height
.
' -preset superfast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$thumbnail
));
if
(
!
$this
->
execmd
(
$cmd
))
return
;
}
$output
=
$this
->
getTempPath
(
'.mp4'
,
'video'
);
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$file
)
.
' -i '
.
escapeshellarg
(
$watermark
)
.
$audio_input
.
' -filter_complex "[0:v]scale='
.
$this
->
output_width
.
':'
.
$this
->
output_height
.
','
.
$drawtext
.
' [text];[text]'
.
' [1:v]overlay=20:20[v]" '
.
' -map [v] -map '
.
$audio_filter
.
' -c:v libx264 -bt 256k -r 25'
.
' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$output
));
if
(
!
$this
->
execmd
(
$cmd
))
return
;
$video_info
=
$this
->
mediainfo
(
$this
->
getAbsolutePath
(
$output
));
Immerse
::
query
()
->
create
([
'user_id'
=>
1
,
'title'
=>
''
,
'weather'
=>
$adminMakeVideo
->
weather
,
'huangli'
=>
$adminMakeVideo
->
huangli
,
'content'
=>
$adminMakeVideo
->
feel
,
'location'
=>
$adminMakeVideo
->
location
,
'longitude'
=>
$adminMakeVideo
->
longitude
,
'latitude'
=>
$adminMakeVideo
->
latitude
,
'url'
=>
$output
,
'type'
=>
$adminMakeVideo
->
type
==
1
?
2
:
1
,
'upload_file'
=>
''
,
'duration'
=>
$video_info
[
'format'
][
'duration'
],
'size'
=>
$video_info
[
'format'
][
'size'
],
'origin_video_url'
=>
$this
->
adminMakeVideo
->
video_url
,
'origin_image_url'
=>
''
,
'poem_id'
=>
$this
->
adminMakeVideo
->
poem_id
,
'temp_id'
=>
$this
->
adminMakeVideo
->
temp_id
,
'thumbnail'
=>
$thumbnail
,
'state'
=>
1
,
'bgm'
=>
$is_bgm
?
$bgm
:
''
,
]);
}
/**
...
...
@@ -127,23 +58,24 @@ class MakeVideo implements ShouldQueue
$this
->
media_info
=
$this
->
mediaInfo
(
$file
);
// 准备素材
$watermark
=
$this
->
getAbsolutePath
(
'images/LOGO_eng.png'
);
// 组装文字参数
$drawtext
=
$this
->
getTextContentString
();
// 合成视频
if
(
$this
->
media_info
[
'format'
][
'nb_streams'
]
>=
2
)
{
/** 音频视频轨都有 */
// 判断双轨 没有则制作空轨
$is_bgm
=
$this
->
adminMakeVideo
->
temp
->
bg_music
==
1
;
//是否手动上传背景音
if
(
$this
->
media_info
[
'format'
][
'nb_streams'
]
>=
2
)
{
/** 音频视频轨都有 */
if
(
$is_bgm
)
{
// 有背景音 融合
$audio
=
$this
->
getAbsolutePath
(
$this
->
getTempPath
(
'.mp3'
,
'audio'
));
$bgm
=
$this
->
getAbsolutePath
(
$this
->
adminMakeVideo
->
temp
->
bgm_url
);
$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
;
if
(
!
$this
->
exec
C
md
(
$cmd
))
return
;
$audio_input
=
' -i '
.
escapeshellarg
(
$audio
);
$audio_filter
=
'2:a'
;
...
...
@@ -152,25 +84,25 @@ class MakeVideo implements ShouldQueue
$audio_input
=
''
;
$audio_filter
=
'0:a'
;
}
}
elseif
(
$media_info
[
'format'
][
'nb_streams'
]
==
1
)
{
/** 只有视频轨 */
}
elseif
(
$this
->
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'
])
.
' -y -f lavfi -i aevalsrc=0:duration='
.
escapeshellarg
(
$
this
->
media_info
[
'format'
][
'duration'
])
.
' -ar 48000 -ab 64k '
.
escapeshellarg
(
$audio
);
if
(
!
$this
->
execmd
(
$cmd
))
return
;
if
(
!
$this
->
exec
C
md
(
$cmd
))
return
;
if
(
$is_bgm
)
{
// 有背景音 融合
$audio_empty
=
$audio
;
$audio
=
$this
->
getAbsolutePath
(
$this
->
getTempPath
(
'.mp3'
));
$bgm
=
$this
->
getAbsolutePath
(
$this
->
adminMakeVideo
->
temp
->
bgm_url
);
$audio
=
$this
->
getAbsolutePath
(
$this
->
getTempPath
(
'.mp3'
,
'audio'
));
$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
;
if
(
!
$this
->
exec
C
md
(
$cmd
))
return
;
}
$audio_input
=
' -i '
.
escapeshellarg
(
$audio
);
$audio_filter
=
'2:a'
;
...
...
@@ -181,8 +113,68 @@ class MakeVideo implements ShouldQueue
}
// 制作封面图
$thumbnail
=
$this
->
getTempPath
(
'.jpg'
,
'thumbnail'
);
if
(
$this
->
adminMakeVideo
->
thumbnail
==
2
){
// 截取中间帧作为视频封面
$frame
=
ceil
(
$this
->
media_info
[
'streams'
][
0
][
'nb_frames'
]
/
2
);
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$file
)
.
' -filter_complex "[0:v]select=\'eq(n,'
.
$frame
.
')\'[img]" '
.
' -map [img]'
.
' -frames:v 1 -s '
.
$this
->
output_width
.
'x'
.
$this
->
output_height
.
' -preset superfast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$thumbnail
));
if
(
!
$this
->
execCmd
(
$cmd
))
return
;
}
else
{
// 手动上传封面
$origin_thumbnail
=
$this
->
getAbsolutePath
(
$this
->
adminMakeVideo
->
thumbnail_url
);
// 将封面分辨率改为指定分辨率
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$origin_thumbnail
)
.
'-s '
.
$this
->
output_width
.
'x'
.
$this
->
output_height
.
' -preset superfast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$thumbnail
));
if
(
!
$this
->
execCmd
(
$cmd
))
return
;
}
// 合成视频
$output
=
$this
->
getTempPath
(
'.mp4'
,
'video'
);
$cmd
=
$this
->
ffmpeg
.
' -y '
.
' -i '
.
escapeshellarg
(
$file
)
.
' -i '
.
escapeshellarg
(
$watermark
)
.
$audio_input
.
' -filter_complex "[0:v]scale='
.
$this
->
output_width
.
':'
.
$this
->
output_height
.
','
.
$drawtext
.
' [text];[text]'
.
' [1:v]overlay=20:20[v]" '
.
' -map [v] -map '
.
$audio_filter
.
' -c:v libx264 -bt 256k -r 25'
.
' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast '
.
escapeshellarg
(
$this
->
getAbsolutePath
(
$output
));
if
(
!
$this
->
execCmd
(
$cmd
))
return
;
// 分析视频 入库
$video_info
=
$this
->
mediainfo
(
$this
->
getAbsolutePath
(
$output
));
Immerse
::
query
()
->
create
([
'user_id'
=>
1
,
'title'
=>
''
,
'weather'
=>
$this
->
adminMakeVideo
->
weather
,
'huangli'
=>
$this
->
adminMakeVideo
->
huangli
,
'content'
=>
$this
->
adminMakeVideo
->
feel
,
'location'
=>
$this
->
adminMakeVideo
->
location
,
'longitude'
=>
$this
->
adminMakeVideo
->
longitude
,
'latitude'
=>
$this
->
adminMakeVideo
->
latitude
,
'url'
=>
$output
,
'type'
=>
$this
->
adminMakeVideo
->
type
==
1
?
2
:
1
,
'upload_file'
=>
''
,
'duration'
=>
$video_info
[
'format'
][
'duration'
],
'size'
=>
$video_info
[
'format'
][
'size'
],
'origin_video_url'
=>
$this
->
adminMakeVideo
->
video_url
,
'origin_image_url'
=>
''
,
'poem_id'
=>
$this
->
adminMakeVideo
->
poem_id
,
'temp_id'
=>
$this
->
adminMakeVideo
->
temp_id
,
'thumbnail'
=>
$thumbnail
,
'state'
=>
1
,
'bgm'
=>
$is_bgm
?
$bgm
:
''
,
]);
}
public
function
getAbsolutePath
(
$path
)
...
...
Please
register
or
login
to post a comment