李帅

1.后台可上传图文内容。

......@@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Jobs\MakeImages;
use App\Models\AdminMakeVideo;
use App\Models\Immerse;
use App\Models\VideoTemp;
......@@ -50,6 +51,71 @@ class DevFFmpeg extends Command
*/
public function handle()
{
MakeImages::dispatch(AdminMakeVideo::query()->find(24));
dd(1);
$image = Storage::disk('public')->path('images/73f18d443820334c51c36f443c9683b3.png');
$watermark = Storage::disk('public')->path('ffmpeg/LOGO_eng.png');
$end_wallpaper = Storage::disk('public')->path('ffmpeg/output_new_end_wallpaper.png');
// 制作最后一帧
$size = '1242x2208';
$time_length = 0.7;
$r = 24;
$last_frame_video = $this->getTempPath('.mp4');
$font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
$cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($image) .
' -i ' . escapeshellarg($watermark) .
" -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
' -filter_complex "'.
' [0:0] ' . $this->getTextContentString() .
'[text];[text][1:0]overlay=20:20[water];' .
' [water]select=\'eq(n,0)\',setpts=PTS-STARTPTS[lastframe];[2:v][lastframe]overlay[v] " ' .
' -map [v] -map 3:a ' . escapeshellarg($last_frame_video);
$output = $this->execmd($cmd);
// 利用最后一帧制作动画
$signature_x = 0;
$signature_y = -20;
$animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
dd($animate);
// 这样实现不了
$cmd = $this->ffmpeg . ' -y -i ' .
escapeshellarg($image) .
" -f lavfi -i nullsrc=s=1242x2208:d=0.7:r=24 ".
' -i ' . escapeshellarg($watermark) .
' -i ' . escapeshellarg($end_wallpaper) .
' -filter_complex "' .
' [0:v] ' . $this->getTextContentString() .
' [text];[text][2:0]overlay=20:20[water];' .
' [water]select=\'eq(n,1)\',setpts=PTS-STARTPTS[lastframe];[1:v][lastframe]overlay[last];' .
' [last]boxblur=8[blur];' .
' [blur][3:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];' .
' [lay]geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];' .
' [lay][grad]alphamerge[alpha];' .
// ' [last][alpha]overlay[concat2];'.
' [water][alpha] concat=n=2:v=1[v]" ' .
escapeshellarg($this->getTempPath('.mp4'));
$output = $this->execmd($cmd);
dd($output);
dd(Str::contains("/Users/lishuai/Documents/source/OnePoem-Server/storage/app/public/ffmpeg/output_16479198841364.mp4",'/storage/app/public/'));
$path = '/Users/lishuai/Desktop/test/';
......
......@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Models\Immerse;
use App\Models\UserMakeVideo;
use App\Jobs\UserMakeVideo as MakeVideo;
use App\Jobs\UserMakeImages as MakeImages;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;
......@@ -34,8 +35,9 @@ class ImmerseController extends Controller
public function store(Request $request)
{
$validator = Validator::make($request->all(),[
'video_url' => 'required|string',
'video_id' => 'required',
'item_url' => 'required|string',
'item_id' => 'required',
'type' => 'required',
'content' => 'sometimes',
'weather' => 'sometimes',
'thumbnail_url' => 'sometimes',
......@@ -47,32 +49,54 @@ class ImmerseController extends Controller
$validated = $validator->validated();
if (Str::contains($validated['video_url'],'//')){
$video_url = '' ;
}elseif (Str::contains($validated['video_url'],'/storage/app/public/')){
$video_url = $validated['video_url'];
if (Str::contains($validated['item_url'],'//')){
$item_url = '' ;
}elseif (Str::contains($validated['item_url'],'/storage/app/public/')){
$item_url = $validated['item_url'];
}else{
$video_url = Storage::disk('public')->path($validated['video_url']);
$item_url = Storage::disk('public')->path($validated['item_url']);
}
$immerse = Immerse::query()->find($request->item_id);
if ($validated['type'] == 1){
// 图文音频
$create = UserMakeVideo::query()->create([
'poem_id' => $immerse->poem_id,
'type' => $immerse->type,
'video_url' => $item_url,
'image_url' => $immerse->image_url,
'bg_music' => $immerse->bg_music,
'bgm_url' => $immerse->bgm_url,
'feel' => $validated['content'],
'weather' => $validated['weather'],
'temp_id' => $immerse->temp_id,
'thumbnail' => $validated['thumbnail_url'] ? 1 : 0,
'thumbnail_url' => $validated['thumbnail_url'],
]);
// 添加至队列
MakeImages::dispatch($create);
}else{
// 视频
$create = UserMakeVideo::query()->create([
'poem_id' => $immerse->poem_id,
'type' => $immerse->type,
'video_url' => $item_url,
'image_url' => $immerse->image_url,
'bg_music' => $immerse->bg_music,
'bgm_url' => $immerse->bgm_url,
'feel' => $validated['content'],
'weather' => $validated['weather'],
'temp_id' => $immerse->temp_id,
'thumbnail' => $validated['thumbnail_url'] ? 1 : 0,
'thumbnail_url' => $validated['thumbnail_url'],
]);
// 添加至队列
MakeVideo::dispatch($create);
}
$immerse = Immerse::query()->find($request->video_id);
$video = UserMakeVideo::query()->create([
'poem_id' => $immerse->poem_id,
'type' => $immerse->type,
'video_url' => $video_url,
'image_url' => $immerse->image_url,
'bg_music' => $immerse->bg_music,
'bgm_url' => $immerse->bgm_url,
'feel' => $validated['content'],
'weather' => $validated['weather'],
'temp_id' => $immerse->temp_id,
'thumbnail' => $validated['thumbnail_url'] ? 1 : 0,
'thumbnail_url' => $validated['thumbnail_url'],
]);
// 添加至队列
MakeVideo::dispatch($video);
return Response::created();
}
......
......@@ -3,12 +3,16 @@
namespace App\Jobs;
use App\Models\AdminMakeVideo;
use App\Models\Immerse;
use App\Models\VideoTemp;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
class MakeImages implements ShouldQueue
{
......@@ -22,6 +26,10 @@ class MakeImages implements ShouldQueue
protected $ffplay;
protected $width;
protected $height;
/**
* Create a new job instance.
* @param AdminMakeVideo $adminMakeVideo
......@@ -43,9 +51,513 @@ class MakeImages implements ShouldQueue
*/
public function handle()
{
//思路:
// if 有背景音 多张图合成视频,时长为音频时长,音频加入背景音
// else 没有背景音,单图一张,输出为单图。
$watermark = Storage::disk('public')->path('ffmpeg/LOGO_eng.png');
$image = Storage::disk('public')->path($this->adminMakeVideo->images_url);
$media_info = $this->mediainfo($image);
$this->width = $width = $media_info['streams'][0]['width'];
$this->height = $height = $media_info['streams'][0]['height'];
if ($this->adminMakeVideo->type == 2 && $this->adminMakeVideo->bg_music == 0){
// 没有背景音,单图一张,输出为单图。
$output = $this->getTempPath('.png',false);
$cmd = $this->ffmpeg . ' -y '.
' -i ' . escapeshellarg($image).
' -i ' . escapeshellarg($watermark).
' -filter_complex "[0:0] ' .
$this->getTextContentString().
' [text];[text]'.
' [1:0]overlay=20:20" ' .
escapeshellarg($output);
if (!$this->execmd($cmd)) return;
// 全部合成以后创建 临境
$video_info = $this->mediainfo($output);
$create = [
'user_id' => 1,
'title' => '',
'content' => $this->adminMakeVideo->feel,
'url' => $output,
'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
'duration' => 0,
'size' => $video_info['format']['size'],
'poem_id' => $this->adminMakeVideo->poem_id,
'temp_id' => $this->adminMakeVideo->temp_id,
'thumbnail' => '',
'bgm' => $this->adminMakeVideo->bgm_url,
];
}else{
$end_wallpaper = Storage::disk('public')->path('ffmpeg/end_wallpaper.png');
$thumbnail = Storage::disk('public')->path('ffmpeg/thumbnail.png');
$font = Storage::disk('public')->path('ffmpeg') . "/arialuni.ttf";
$signature = "一言 · 官方出品";
// 生成贴纸和签名
$end_wallpaper = $this->wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font);
// 有背景音 单图合成视频,时长为音频时长,音频加入背景音
$bgm = Storage::disk('public')->path($this->adminMakeVideo->bgm_url);
// 制作最后一帧
$size = $this->width . 'x' . $this->height;
$time_length = 0.7;
$r = 24;
$last_frame_video = $this->getTempPath('.mp4');
$font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
$cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($image) .
' -i ' . escapeshellarg($watermark) .
" -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
' -filter_complex "'.
' [0:0] ' . $this->getTextContentString() .
'[text];[text][1:0]overlay=20:20[water];' .
' [water]select=\'eq(n,0)\',setpts=PTS-STARTPTS[lastframe];[2:v][lastframe]overlay[v] " ' .
' -map [v] -map 3:a ' . escapeshellarg($last_frame_video);
if (!$this->execmd($cmd)) return;
// 利用最后一帧制作动画
$signature_x = 0;
$signature_y = -20;
$animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
$output = $this->getTempPath('.mp4',false);
$cmd = $this->ffmpeg . ' -y ' .
' -i ' . escapeshellarg($image).
' -i ' . escapeshellarg($watermark).
' -i ' . escapeshellarg($bgm) .
' -i ' . escapeshellarg($animate) .
' -filter_complex "[0:0] ' . $this->getTextContentString().
'[text];[text][1:0]overlay=20:20[water];' .
'[water][2:a][3:v][3:a]concat=n=2:v=1:a=1[v][a]" '.
' -map [v] -map [a] '.
' -c:v libx264 -bt 256k -r 25' .
' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast '
. escapeshellarg($output);
if (!$this->execmd($cmd)) return;
// 全部合成以后创建 临境
$video_info = $this->mediainfo($output);
$create = [
'user_id' => 1,
'title' => '',
'content' => $this->adminMakeVideo->feel,
'url' => $output,
'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
'duration' => $video_info['format']['duration'],
'size' => $video_info['format']['size'],
'poem_id' => $this->adminMakeVideo->poem_id,
'temp_id' => $this->adminMakeVideo->temp_id,
'thumbnail' => '',
'bgm' => $this->adminMakeVideo->bgm_url,
];
}
Immerse::query()->create($create);
}
/***
* 获取视频信息(配合ffprobe)
* @param $file
* @param bool $cache
* @return mixed
*/
public function mediainfo($file, $cache = true) {
global $_mediainfo;
$cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
if ($cache && isset($_mediainfo[$file])) {
return $_mediainfo[$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);
}
if ($cache) {
$mediainfo[$file] = $data;
}
return $data;
}
/**
* 获取输出临时文件名
* @param string $ext
* @param bool $is_temp
* @return string
*/
public function getTempPath($ext = '.mp4',$is_temp = true)
{
$filename = "/output_" . time() . rand(0, 10000);
$prefix = $is_temp ? 'temp/' : 'video/';
$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 = $prefix . $dir_l1 . '/' . $dir_l2;
if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
return Storage::disk('public')->path($dir . $filename . $ext);
}
/**
* 执行命令
* @param $cmd
* @param bool $update_progress
* @return string
*/
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);
}
} else {
// return ErrorUtil::triggerErrorMsg('proc_open error');
}
}
public function getTextContentString()
{
$components = $this->adminMakeVideo->temp()->first()->components()->get();
$font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
$drawtext = '';
foreach ($components as $component) {
switch ($component->name){
case 'one_poem':
$content = $this->adminMakeVideo->poem->content;
$text_file = $this->getTempPath('.txt');
file_put_contents($text_file, $content);
$text_color = $component->text_color ?? 'white';
$text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
$opacity = $component->opacity ? $component->opacity / 100 : '0.5';
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font) . ':' .
'textfile=' . escapeshellarg($text_file) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
'fontcolor=' . $text_color . '@1.0:' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
case 'every_poem':
break;
case 'weather':
$content = '多云';
$text_color = $component->text_color ?? 'white';
$text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
$opacity = $component->opacity ? $component->opacity / 100 : '0.5';
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font) . ':' .
'text=' . escapeshellarg($content) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
'fontcolor=' . $text_color . '@1.0:' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
case 'date':
$content = Carbon::now()->format('Y年m月d日H时');
$text_color = $component->text_color ?? 'white';
$text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
$opacity = $component->opacity ? $component->opacity / 100 : '0.5';
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font) . ':' .
'text=' . escapeshellarg($content) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
'fontcolor=' . $text_color . '@1.0:' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
case 'feel':
$content = $this->adminMakeVideo->feel;
$text_color = $component->text_color ?? 'white';
$text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
$opacity = $component->opacity ? $component->opacity / 100 : '0.5';
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font) . ':' .
'text=' . escapeshellarg($content) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
'fontcolor=' . $text_color . '@1.0:' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
}
}
return rtrim($drawtext,', ');
}
/**
* @param $width
* @param $content
* @return float
*/
public function calcFontSize($width, $content)
{
$max_len = 1;
foreach (explode("\n",$content) as $item){
if (mb_strlen($item) > $max_len){
$max_len = mb_strlen($item);
}
}
return ceil($this->width * $width / 100 / $max_len);
}
/**
* 贴纸和签名
* @param $end_wallpaper
* @param $thumbnail
* @param $signature
* @param $font
* @return string
*/
public function wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font) {
$_imagetype = $this->getImageType($thumbnail);
$_img = null;
switch ($_imagetype) {
case 'gif':
if (function_exists('imagecreatefromgif')) {
$_img = imagecreatefromgif($thumbnail);
}
break;
case 'jpg':
case 'jpeg':
$_img = imagecreatefromjpeg($thumbnail);
break;
case 'png':
$_img = imagecreatefrompng($thumbnail);
break;
default:
$_img = imagecreatefromstring($thumbnail);
break;
}
$width = 130;
$height = 130;
$_width = 130;
$_height = 130;
if(is_resource($_img)){
$_width = imagesx($_img);
$_height = imagesy($_img);
}
$bite = $_width / $_height;
if($_width > $_height){
if($_width > $width){
$height = round($width / $bite);
}
}else{
if($_height > $height){
$width = round($height * $bite);
}
}
$tmpimg = imagecreatetruecolor($width,$height);
if(function_exists('imagecopyresampled')) {
imagecopyresampled($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
} else {
imagecopyresized($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
}
if(is_resource($_img)) imagedestroy($_img);
$_img = $this->getCircleAvatar($tmpimg);
if(is_resource($tmpimg)) imagedestroy($tmpimg);
$wp = $this->imagesMerge($end_wallpaper, $_img);
// $white = imagecolorallocate($wp, 0xd0, 0xcd, 0xcc);
$white = imagecolorallocate($wp, 0xDC, 0x14, 0x3C); //fixme 字体颜色
imagettftext($wp, 20, 0, 75, 240, $white, $font, $signature);
// $dst = "./output_new_end_wallpaper.png";
$dst = $this->getTempPath('.png');
imagepng($wp, $dst);
if(is_resource($end_wallpaper)) imagedestroy($end_wallpaper);
if(is_resource($_img)) imagedestroy($_img);
return $dst;
}
/**
* 获取图像文件类型
* @param $img_name
* @return string
*/
public function getImageType($img_name)
{
if (preg_match("/\.(jpg|jpeg|gif|png)$/i", $img_name, $matches)){
$type = strtolower($matches[1]);
}else{
$type = "string";
}
return $type;
}
/**
* 多图融合
* @param $end_wallpaper
* @param $thumbnail
* @return resource
*/
public function imagesMerge($end_wallpaper, $thumbnail) {
$end_wallpaper = imagecreatefrompng($end_wallpaper);
$background = imagecreatefrompng(Storage::disk('public')->path('ffmpeg/background.png'));
imagesavealpha($background,true);
$temp_wallpaper = imagecreatetruecolor(350, 204);
$color = imagecolorallocate($temp_wallpaper, 0xd0, 0xcd, 0xcc);
// $color = imagecolorallocate($temp_wallpaper, 0xDC, 0x14, 0x3C);
imagefill($temp_wallpaper, 0, 0, $color);
imageColorTransparent($temp_wallpaper, $color);
imagecopyresampled($temp_wallpaper, $end_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), imagesx($end_wallpaper), imagesy($end_wallpaper));
imagecopymerge($background, $temp_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), 60);
imagecopymerge($background, $thumbnail, 127, 26, 0, 0, imagesx($thumbnail), imagesy($thumbnail), 100);
return $background;
}
/**
* 获取圆形头像
* @param $img
* @param int $dst_w
* @param int $dst_h
* @return resource
*/
public function getCircleAvatar($img, $dst_w = 96, $dst_h = 96)
{
$w = 130;
$h = 130;
$src = imagecreatetruecolor($dst_w, $dst_h);
imagecopyresized($src, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
$newpic = imagecreatetruecolor($dst_w, $dst_h);
imagealphablending($newpic, false);
imagecopyresampled($newpic, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
$mask = imagecreatetruecolor($dst_w, $dst_h);
$transparent = imagecolorallocate($mask, 255, 0, 0);
imagecolortransparent($mask,$transparent);
imagefilledellipse($mask, $dst_w / 2, $dst_h / 2, $dst_w, $dst_h, $transparent);
$red = imagecolorallocate($mask, 0, 0, 0);
imagecopymerge($newpic, $mask, 0, 0, 0, 0, $dst_w, $dst_h, 100);
imagecolortransparent($newpic,$red);
imagesavealpha($newpic,true);
imagefill($newpic, 0, 0, $red);
imagedestroy($mask);
return $newpic;
}
/**
* 用最后一帧和贴纸制作动画
* @param $last_frame_video
* @param $end_wallpaper
* @param $signature
* @param $signature_x
* @param $signature_y
* @param $font
* @return bool|string
*/
public function makeAnimate($last_frame_video, $end_wallpaper, $signature, $signature_x, $signature_y, $font) {
$signature_x = $signature_x >= 0 ? '+' . $signature_x : '-' . abs($signature_x);
$signature_y = $signature_y >= 0 ? '+' . $signature_y : '-' . abs($signature_y);
$video = $this->getTempPath();
if ($signature !== '') {
$cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
' -filter_complex "'.
'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
'[0:v]boxblur=8[blur];'.
'[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];[lay]'.
'drawtext='.
'fontfile=' . escapeshellarg($font) . ':'.
'text=' . escapeshellarg($signature) . ':'.
'fontsize=23:'.
'fontcolor=white@1.0:'.
'x=main_w/2' . $signature_x . ':'.
'y=main_h/2' . $signature_y . '[text];[text]'.
'[grad]alphamerge[alpha];'.
'[0:v][alpha]overlay'.
'" ' . escapeshellarg($video);
} else {
$cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
' -filter_complex "'.
'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
'[0:v]boxblur=8[blur];'.
'[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];'.
'[lay][grad]alphamerge[alpha];'.
'[0:v][alpha]overlay'.
'" ' . escapeshellarg($video);
}
if ($this->execmd($cmd)) {
return $video;
} else {
return false;
}
}
}
......
<?php
namespace App\Jobs;
use App\Models\AdminMakeVideo;
use App\Models\Immerse;
use App\Models\VideoTemp;
use Carbon\Carbon;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
class UserMakeImages implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $adminMakeVideo;
protected $ffmpeg;
protected $ffprobe;
protected $ffplay;
protected $width;
protected $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->ffplay = env('FFPLAY_CMD');
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
$watermark = Storage::disk('public')->path('ffmpeg/LOGO_eng.png');
$image = Storage::disk('public')->path($this->adminMakeVideo->images_url);
$media_info = $this->mediainfo($image);
$this->width = $width = $media_info['streams'][0]['width'];
$this->height = $height = $media_info['streams'][0]['height'];
if ($this->adminMakeVideo->type == 2 && $this->adminMakeVideo->bg_music == 0){
// 没有背景音,单图一张,输出为单图。
$output = $this->getTempPath('.png',false);
$cmd = $this->ffmpeg . ' -y '.
' -i ' . escapeshellarg($image).
' -i ' . escapeshellarg($watermark).
' -filter_complex "[0:0] ' .
$this->getTextContentString().
' [text];[text]'.
' [1:0]overlay=20:20" ' .
escapeshellarg($output);
if (!$this->execmd($cmd)) return;
// 全部合成以后创建 临境
$video_info = $this->mediainfo($output);
$create = [
'user_id' => 1,
'title' => '',
'content' => $this->adminMakeVideo->feel,
'url' => $output,
'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
'duration' => 0,
'size' => $video_info['format']['size'],
'poem_id' => $this->adminMakeVideo->poem_id,
'temp_id' => $this->adminMakeVideo->temp_id,
'thumbnail' => '',
'bgm' => $this->adminMakeVideo->bgm_url,
];
}else{
$end_wallpaper = Storage::disk('public')->path('ffmpeg/end_wallpaper.png');
$thumbnail = Storage::disk('public')->path('ffmpeg/thumbnail.png');
$font = Storage::disk('public')->path('ffmpeg') . "/arialuni.ttf";
$signature = "一言 · 官方出品";
// 生成贴纸和签名
$end_wallpaper = $this->wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font);
// 有背景音 单图合成视频,时长为音频时长,音频加入背景音
$bgm = Storage::disk('public')->path($this->adminMakeVideo->bgm_url);
// 制作最后一帧
$size = $this->width . 'x' . $this->height;
$time_length = 0.7;
$r = 24;
$last_frame_video = $this->getTempPath('.mp4');
$font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
$cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($image) .
' -i ' . escapeshellarg($watermark) .
" -f lavfi -i nullsrc=s={$size}:d={$time_length}:r={$r} -f lavfi -i aevalsrc=0:duration={$time_length}" .
' -filter_complex "'.
' [0:0] ' . $this->getTextContentString() .
'[text];[text][1:0]overlay=20:20[water];' .
' [water]select=\'eq(n,0)\',setpts=PTS-STARTPTS[lastframe];[2:v][lastframe]overlay[v] " ' .
' -map [v] -map 3:a ' . escapeshellarg($last_frame_video);
if (!$this->execmd($cmd)) return;
// 利用最后一帧制作动画
$signature_x = 0;
$signature_y = -20;
$animate = $this->makeAnimate($last_frame_video, $end_wallpaper, '', $signature_x, $signature_y, $font);
$output = $this->getTempPath('.mp4',false);
$cmd = $this->ffmpeg . ' -y ' .
' -i ' . escapeshellarg($image).
' -i ' . escapeshellarg($watermark).
' -i ' . escapeshellarg($bgm) .
' -i ' . escapeshellarg($animate) .
' -filter_complex "[0:0] ' . $this->getTextContentString().
'[text];[text][1:0]overlay=20:20[water];' .
'[water][2:a][3:v][3:a]concat=n=2:v=1:a=1[v][a]" '.
' -map [v] -map [a] '.
' -c:v libx264 -bt 256k -r 25' .
' -ar 44100 -ac 2 -qmin 30 -qmax 60 -profile:v baseline -preset fast '
. escapeshellarg($output);
if (!$this->execmd($cmd)) return;
// 全部合成以后创建 临境
$video_info = $this->mediainfo($output);
$create = [
'user_id' => 1,
'title' => '',
'content' => $this->adminMakeVideo->feel,
'url' => $output,
'type' => $this->adminMakeVideo->type == 1 ? 2 : 1,
'duration' => $video_info['format']['duration'],
'size' => $video_info['format']['size'],
'poem_id' => $this->adminMakeVideo->poem_id,
'temp_id' => $this->adminMakeVideo->temp_id,
'thumbnail' => '',
'bgm' => $this->adminMakeVideo->bgm_url,
];
}
Immerse::query()->create($create);
}
/***
* 获取视频信息(配合ffprobe)
* @param $file
* @param bool $cache
* @return mixed
*/
public function mediainfo($file, $cache = true) {
global $_mediainfo;
$cmd = $this->ffprobe . ' -v quiet -print_format json -show_format -show_streams ' . escapeshellarg($file);
if ($cache && isset($_mediainfo[$file])) {
return $_mediainfo[$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);
}
if ($cache) {
$mediainfo[$file] = $data;
}
return $data;
}
/**
* 获取输出临时文件名
* @param string $ext
* @param bool $is_temp
* @return string
*/
public function getTempPath($ext = '.mp4',$is_temp = true)
{
$filename = "/output_" . time() . rand(0, 10000);
$prefix = $is_temp ? 'temp/' : 'video/';
$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 = $prefix . $dir_l1 . '/' . $dir_l2;
if( !Storage::disk('public')->exists($dir)) Storage::disk('public')->makeDirectory($dir);
return Storage::disk('public')->path($dir . $filename . $ext);
}
/**
* 执行命令
* @param $cmd
* @param bool $update_progress
* @return string
*/
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);
}
} else {
// return ErrorUtil::triggerErrorMsg('proc_open error');
}
}
public function getTextContentString()
{
$components = $this->adminMakeVideo->temp()->first()->components()->get();
$font = Storage::disk('public')->path('ffmpeg/arialuni.ttf');
$drawtext = '';
foreach ($components as $component) {
switch ($component->name){
case 'one_poem':
$content = $this->adminMakeVideo->poem->content;
$text_file = $this->getTempPath('.txt');
file_put_contents($text_file, $content);
$text_color = $component->text_color ?? 'white';
$text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
$opacity = $component->opacity ? $component->opacity / 100 : '0.5';
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font) . ':' .
'textfile=' . escapeshellarg($text_file) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
'fontcolor=' . $text_color . '@1.0:' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
case 'every_poem':
break;
case 'weather':
$content = '多云';
$text_color = $component->text_color ?? 'white';
$text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
$opacity = $component->opacity ? $component->opacity / 100 : '0.5';
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font) . ':' .
'text=' . escapeshellarg($content) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
'fontcolor=' . $text_color . '@1.0:' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
case 'date':
$content = Carbon::now()->format('Y年m月d日H时');
$text_color = $component->text_color ?? 'white';
$text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
$opacity = $component->opacity ? $component->opacity / 100 : '0.5';
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font) . ':' .
'text=' . escapeshellarg($content) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
'fontcolor=' . $text_color . '@1.0:' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
case 'feel':
$content = $this->adminMakeVideo->feel;
$text_color = $component->text_color ?? 'white';
$text_bg_color = $component->text_bg_color ?? '0xd0cdcc';
$opacity = $component->opacity ? $component->opacity / 100 : '0.5';
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font) . ':' .
'text=' . escapeshellarg($content) . ':' .
'fontsize=' . $this->calcFontSize($component->font_size,$content) . ':' .
'fontcolor=' . $text_color . '@1.0:' .
'x=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][0]) . ':' .
'y=' . escapeshellarg(VideoTemp::POSITION_FFMPEG[$component->position][1]) . ':' .
'box=1:boxcolor=' . $text_bg_color . '@' . $opacity . '", ';
break;
}
}
return rtrim($drawtext,', ');
}
/**
* @param $width
* @param $content
* @return float
*/
public function calcFontSize($width, $content)
{
$max_len = 1;
foreach (explode("\n",$content) as $item){
if (mb_strlen($item) > $max_len){
$max_len = mb_strlen($item);
}
}
return ceil($this->width * $width / 100 / $max_len);
}
/**
* 贴纸和签名
* @param $end_wallpaper
* @param $thumbnail
* @param $signature
* @param $font
* @return string
*/
public function wallpaperWithSignature($end_wallpaper, $thumbnail, $signature, $font) {
$_imagetype = $this->getImageType($thumbnail);
$_img = null;
switch ($_imagetype) {
case 'gif':
if (function_exists('imagecreatefromgif')) {
$_img = imagecreatefromgif($thumbnail);
}
break;
case 'jpg':
case 'jpeg':
$_img = imagecreatefromjpeg($thumbnail);
break;
case 'png':
$_img = imagecreatefrompng($thumbnail);
break;
default:
$_img = imagecreatefromstring($thumbnail);
break;
}
$width = 130;
$height = 130;
$_width = 130;
$_height = 130;
if(is_resource($_img)){
$_width = imagesx($_img);
$_height = imagesy($_img);
}
$bite = $_width / $_height;
if($_width > $_height){
if($_width > $width){
$height = round($width / $bite);
}
}else{
if($_height > $height){
$width = round($height * $bite);
}
}
$tmpimg = imagecreatetruecolor($width,$height);
if(function_exists('imagecopyresampled')) {
imagecopyresampled($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
} else {
imagecopyresized($tmpimg, $_img, 0, 0, 0, 0, $width, $height, $_width, $_height);
}
if(is_resource($_img)) imagedestroy($_img);
$_img = $this->getCircleAvatar($tmpimg);
if(is_resource($tmpimg)) imagedestroy($tmpimg);
$wp = $this->imagesMerge($end_wallpaper, $_img);
// $white = imagecolorallocate($wp, 0xd0, 0xcd, 0xcc);
$white = imagecolorallocate($wp, 0xDC, 0x14, 0x3C); //fixme 字体颜色
imagettftext($wp, 20, 0, 75, 240, $white, $font, $signature);
// $dst = "./output_new_end_wallpaper.png";
$dst = $this->getTempPath('.png');
imagepng($wp, $dst);
if(is_resource($end_wallpaper)) imagedestroy($end_wallpaper);
if(is_resource($_img)) imagedestroy($_img);
return $dst;
}
/**
* 获取图像文件类型
* @param $img_name
* @return string
*/
public function getImageType($img_name)
{
if (preg_match("/\.(jpg|jpeg|gif|png)$/i", $img_name, $matches)){
$type = strtolower($matches[1]);
}else{
$type = "string";
}
return $type;
}
/**
* 多图融合
* @param $end_wallpaper
* @param $thumbnail
* @return resource
*/
public function imagesMerge($end_wallpaper, $thumbnail) {
$end_wallpaper = imagecreatefrompng($end_wallpaper);
$background = imagecreatefrompng(Storage::disk('public')->path('ffmpeg/background.png'));
imagesavealpha($background,true);
$temp_wallpaper = imagecreatetruecolor(350, 204);
$color = imagecolorallocate($temp_wallpaper, 0xd0, 0xcd, 0xcc);
// $color = imagecolorallocate($temp_wallpaper, 0xDC, 0x14, 0x3C);
imagefill($temp_wallpaper, 0, 0, $color);
imageColorTransparent($temp_wallpaper, $color);
imagecopyresampled($temp_wallpaper, $end_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), imagesx($end_wallpaper), imagesy($end_wallpaper));
imagecopymerge($background, $temp_wallpaper, 0, 0, 0, 0, imagesx($temp_wallpaper), imagesy($temp_wallpaper), 60);
imagecopymerge($background, $thumbnail, 127, 26, 0, 0, imagesx($thumbnail), imagesy($thumbnail), 100);
return $background;
}
/**
* 获取圆形头像
* @param $img
* @param int $dst_w
* @param int $dst_h
* @return resource
*/
public function getCircleAvatar($img, $dst_w = 96, $dst_h = 96)
{
$w = 130;
$h = 130;
$src = imagecreatetruecolor($dst_w, $dst_h);
imagecopyresized($src, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
$newpic = imagecreatetruecolor($dst_w, $dst_h);
imagealphablending($newpic, false);
imagecopyresampled($newpic, $img, 0, 0, 0, 0, $dst_w, $dst_h, $w, $h);
$mask = imagecreatetruecolor($dst_w, $dst_h);
$transparent = imagecolorallocate($mask, 255, 0, 0);
imagecolortransparent($mask,$transparent);
imagefilledellipse($mask, $dst_w / 2, $dst_h / 2, $dst_w, $dst_h, $transparent);
$red = imagecolorallocate($mask, 0, 0, 0);
imagecopymerge($newpic, $mask, 0, 0, 0, 0, $dst_w, $dst_h, 100);
imagecolortransparent($newpic,$red);
imagesavealpha($newpic,true);
imagefill($newpic, 0, 0, $red);
imagedestroy($mask);
return $newpic;
}
/**
* 用最后一帧和贴纸制作动画
* @param $last_frame_video
* @param $end_wallpaper
* @param $signature
* @param $signature_x
* @param $signature_y
* @param $font
* @return bool|string
*/
public function makeAnimate($last_frame_video, $end_wallpaper, $signature, $signature_x, $signature_y, $font) {
$signature_x = $signature_x >= 0 ? '+' . $signature_x : '-' . abs($signature_x);
$signature_y = $signature_y >= 0 ? '+' . $signature_y : '-' . abs($signature_y);
$video = $this->getTempPath();
if ($signature !== '') {
$cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
' -filter_complex "'.
'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
'[0:v]boxblur=8[blur];'.
'[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];[lay]'.
'drawtext='.
'fontfile=' . escapeshellarg($font) . ':'.
'text=' . escapeshellarg($signature) . ':'.
'fontsize=23:'.
'fontcolor=white@1.0:'.
'x=main_w/2' . $signature_x . ':'.
'y=main_h/2' . $signature_y . '[text];[text]'.
'[grad]alphamerge[alpha];'.
'[0:v][alpha]overlay'.
'" ' . escapeshellarg($video);
} else {
$cmd = $this->ffmpeg . ' -y -i ' . escapeshellarg($last_frame_video) .
' -loop 1 -i ' . escapeshellarg($end_wallpaper) .
' -filter_complex "'.
'geq=lum=\'if(lte(T,0.6), 255*T*(1/0.6),255)\',format=gray[grad];'.
'[0:v]boxblur=8[blur];'.
'[blur][1:v]overlay=(main_w-overlay_w)/2:(main_h-overlay_h)/2 [lay];'.
'[lay][grad]alphamerge[alpha];'.
'[0:v][alpha]overlay'.
'" ' . escapeshellarg($video);
}
if ($this->execmd($cmd)) {
return $video;
} else {
return false;
}
}
}
......@@ -33,15 +33,6 @@ class AdminMakeVideo extends Model
return Storage::disk('public')->url($this->thumbnail_url);
}
public function getImagesUrl()
{
if (Str::contains($this->images_url, '//')) {
return $this->images_url;
}
return Storage::disk('public')->url($this->images_url);
}
public function poem()
{
return $this->hasOne(OnePoem::class,'id','poem_id');
......