李帅

1.新增字体管理。

......@@ -4,21 +4,21 @@ namespace App\Admin\Controllers;
use App\Admin\Renderable\PoemTable;
use App\Admin\Renderable\TemplateTable;
use App\Jobs\MakeImages;
use App\Jobs\MakeVideo;
use App\Models\AdminMakeVideo;
use App\Jobs\AdminMakeImmerse;
use App\Admin\Repositories\AdminMakeVideo;
use App\Models\Immerse;
use App\Models\Order;
use App\Models\OnePoem;
use App\Models\VideoTemp;
use Dcat\Admin\Form;
use Dcat\Admin\Grid;
use Dcat\Admin\Layout\Content;
use Dcat\Admin\Show;
use Dcat\Admin\Http\Controllers\AdminController;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class AdminMakeVideoController extends AdminController
{
protected $title = '制作历史记录';
/**
* Make a grid builder.
*
......@@ -26,33 +26,36 @@ class AdminMakeVideoController extends AdminController
*/
protected function grid()
{
return Grid::make(new Immerse(), function (Grid $grid) {
$grid->model()->where('user_id','=',1);
return Grid::make(new AdminMakeVideo(), function (Grid $grid) {
// 设置自定义视图
$grid->setActionClass(Grid\Displayers\Actions::class);
$grid->column('id')->sortable();
$grid->column('title','标题');
$grid->column('content','有感');
$grid->column('url')->display(function ($url){
return "<a target='_blank' href='". $url ."'>查看</a>";
$grid->column('type','类型')->using([1 => '视频', 2 => '音频']);
$grid->column('video_url')->display(function ($url){
if ($url == '' || $url == null) return '';
$path = Storage::disk('public')->url($url);
return "<a target='_blank' href='". $path ."'>查看</a>";
});
$grid->column('type','类型')->using([1 => '音频', 2 => '视频']);
$grid->column('duration');
$grid->column('size');
$grid->column('images_url')->display(function ($url){
if ($url == '' || $url == null) return '';
$path = Storage::disk('public')->url($url);
return "<a target='_blank' href='". $path ."'>查看</a>";
});
$grid->column('feel','有感');
$grid->column('weather','天气');
$grid->column('huangli','黄历');
$grid->column('poem_id');
$grid->column('temp_id');
$grid->column('thumbnail')->image();
$grid->column('bgm')->display(function ($url){
if (Str::of($url)->contains('.mp3'))
return "<a target='_blank' href='". $url ."'>查看</a>";
else
return "<a target='_blank' href='". $url ."'>下载</a>";
});
$grid->column('thumbnail');
$grid->column('thumbnail_url')->image();
$grid->column('created_at');
$grid->column('updated_at')->sortable();
$grid->filter(function (Grid\Filter $filter) {
$filter->equal('id');
});
});
}
......@@ -93,9 +96,10 @@ class AdminMakeVideoController extends AdminController
return Form::make(new AdminMakeVideo(), function (Form $form) {
$form->display('id');
$form->selectTable('poem_id','选择一言')
->title('一言诗词库')
->from(PoemTable::make());
$form->selectTable('temp_id','选择模板')
->title('模板选择')
->from(TemplateTable::make())
->model(VideoTemp::class,'id','title');
$form->radio('type')
->options([1=>'视频', 2=>'图文音频'])
......@@ -109,36 +113,25 @@ class AdminMakeVideoController extends AdminController
->addElementClass('video_url');
})
->when(2,function (Form $form){
$form->multipleImage('images_url','上传图片')
->limit(5)
$form->image('images_url','上传图片')
->uniqueName()
->addElementClass('images_url');
})
->default(1);
$form->radio('bg_music','背景音')
->options(['无', '有'])
->when(1,function (Form $form){
$form->file('bgm_url','上传背景音')
->accept('mp3,aac,wav')
->autoUpload()
->uniqueName()
->addElementClass('bg_music');
})
->default(0);
$form->selectTable('poem_id','选择一言')
->title('一言诗词库')
->from(PoemTable::make())
->model(OnePoem::class,'id','title');
$form->textarea('feel','有感');
$form->selectTable('temp_id','选择模板')
->title('模板选择')
->from(TemplateTable::make());
$form->text('weather','天气');
$form->text('huangli','黄历')->default('宜');
$form->radio('thumbnail','封面')
->options([1=>'手动上传', 2=>'自动截屏'])
->when(1,function (Form $form){
$form->multipleImage('thumbnail_url','上传图片')
->limit(5)
$form->image('thumbnail_url','上传图片')
->uniqueName();
// ->addElementClass('bg_img_url');
})
->when(2,function (Form $form){
$form->html('');
......@@ -147,31 +140,12 @@ class AdminMakeVideoController extends AdminController
$form->display('created_at');
$form->display('updated_at');
});
}
public function store()
{
$all = request()->all();
if (isset($all['upload_column'])) return $this->form()->store();
try{
$video = AdminMakeVideo::query()->create($all);
if ($all['type'] == 1){
// 添加至队列
MakeVideo::dispatch($video);
}elseif ($all['type'] == 2){ // 新增类型,不止视频,还有静态图。
// MakeImages
MakeImages::dispatch($video);
}else{
return $this->form()->response()->error('类型选择错误');
}
}catch (\Exception $exception){
return $this->form()->response()->error($exception->getMessage());
}
return $this->form()->response()->refresh()->success(trans('admin.save_succeeded'));
$form->saved(function (Form $form, $result) {
$model = $form->repository()->model();
AdminMakeImmerse::dispatch($model);
});
});
}
public function destroy($id)
......
<?php
namespace App\Admin\Controllers;
use App\Admin\Renderable\PoemTable;
use App\Admin\Renderable\TemplateTable;
use App\Jobs\AdminMakeImmerse;
use App\Jobs\MakeImages;
use App\Jobs\MakeVideo;
use App\Admin\Repositories\AdminMakeVideo;
use App\Models\Immerse;
use App\Models\OnePoem;
use App\Models\VideoTemp;
use Dcat\Admin\Form;
use Dcat\Admin\Grid;
use Dcat\Admin\Show;
use Dcat\Admin\Http\Controllers\AdminController;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class ImmerseController extends AdminController
{
/**
* Make a grid builder.
*
* @return Grid
*/
protected function grid()
{
return Grid::make(new Immerse(), function (Grid $grid) {
$grid->model()->where('user_id','=',1);
// 设置自定义视图
$grid->setActionClass(Grid\Displayers\Actions::class);
$grid->column('id')->sortable();
$grid->column('content','有感');
$grid->column('url')->display(function ($url){
return "<a target='_blank' href='". $url ."'>查看</a>";
});
$grid->column('type','类型')->using([1 => '音频', 2 => '视频']);
$grid->column('duration','时长');
$grid->column('size','大小');
$grid->column('poem_id');
$grid->column('temp_id');
$grid->column('thumbnail')->image();
$grid->column('bgm')->display(function ($url){
if (Str::of($url)->contains('.mp3'))
return "<a target='_blank' href='". $url ."'>查看</a>";
else
return "<a target='_blank' href='". $url ."'>下载</a>";
});
$grid->column('created_at');
$grid->column('updated_at')->sortable();
$grid->filter(function (Grid\Filter $filter) {
$filter->equal('id');
});
});
}
public function destroy($id)
{
$immerse = Immerse::query()->find($id);
Storage::disk('public')->delete($immerse->url);
Storage::disk('public')->delete($immerse->thumbnail);
Storage::disk('public')->delete($immerse->bgm);
$immerse->delete();
return $this->form()->response()->refresh()->success('删除成功');
}
}
......@@ -2,6 +2,8 @@
namespace App\Admin\Controllers;
use App\Admin\Renderable\FontTable;
use App\Models\Font;
use App\Models\VideoTemp;
use App\Models\Component;
use Dcat\Admin\Form;
......@@ -47,22 +49,9 @@ class VideoTempController extends AdminController
$th = ['id','模板id','名称','位置','字号','字体颜色','背景色','背景厚度','透明度','避免剪切','创建时间','修改时间'];
return Table::make($th, $this->components->toArray())->withBorder();
});
// $grid->column('type');
// $grid->column('bg_type');
// $grid->column('bg_url')->image('/storage/');
// $grid->column('bg_music');
$grid->column('state')->switch();
// $grid->column('sn');
// $grid->column('top');
// $grid->column('left');
// $grid->column('font_size');
$grid->column('created_at');
$grid->column('updated_at')->sortable();
//
// $grid->filter(function (Grid\Filter $filter) {
// $filter->equal('id');
//
// });
});
}
......@@ -99,75 +88,7 @@ class VideoTempController extends AdminController
*/
protected function form()
{
return Form::make(new VideoTemp(), function (Form $form) {
$form->display('id');
$form->block(7, function (Form\BlockForm $form) {
// 设置标题
$form->title('基本设置');
// 显示底部提交按钮
$form->showFooter();
// 设置字段宽度
$form->width(8, 3);
$form->text('title');
$form->radio('bg_music')
->options(['无', '有'])
->when(1,function (Form\BlockForm $form){
$form->file('bgm_url')
->accept('mp3,aac,wav')
->autoUpload()
->uniqueName()
->addElementClass('bgm_url');
})
->default(0);
$form->checkbox('components','组件')
->when('every_poem', $this->buildCheckBoxOption('every_poem',$form))
->when('one_poem', $this->buildCheckBoxOption('one_poem',$form))
->when('weather', $this->buildCheckBoxOption('weather',$form))
->when('date', $this->buildCheckBoxOption('date',$form))
->when('feel', $this->buildCheckBoxOption('feel',$form))
->default(['one_poem','weather','date'])
->options([
'every_poem' => '每日一言组件',
'one_poem' => '一言组件',
'weather' => '天气组件',
'date' => '日期组件',
'feel' => '临境有感组件',
]);
$form->hidden('state')
->saving(function ($v) {
return $v;
});
});
$form->block(5, function (Form\BlockForm $form) {
$form->html(view('admin.form.phone'));
});
$form->display('created_at');
$form->display('updated_at');
});
}
public function edit($id, Content $content)
{
return $content
->translation($this->translation())
->title($this->title())
->description($this->description()['edit'] ?? trans('admin.edit'))
->body($this->form2()->edit($id));
}
public function form2()
{
return Form::make(VideoTemp::with('components'), function (Form $form) {
// dd($form->model()->toArray());
$form->display('id');
$form->block(7, function (Form\BlockForm $form) {
// 设置标题
......@@ -190,29 +111,33 @@ class VideoTempController extends AdminController
})
->default(0);
$form->checkbox('components','组件')
// ->when('every_poem', $this->buildCheckBoxOption('every_poem',$form))
// ->when('one_poem', $this->buildCheckBoxOption('one_poem',$form))
// ->when('weather', $this->buildCheckBoxOption('weather',$form))
// ->when('date', $this->buildCheckBoxOption('date',$form))
// ->when('feel', $this->buildCheckBoxOption('feel',$form))
->default(['one_poem','weather','date'])
->options([
$form->hasMany('components','组件', function (Form\NestedForm $form) {
$form->select('name','组件名称')->options([
'every_poem' => '每日一言组件',
'one_poem' => '一言组件',
'weather' => '天气组件',
'date' => '日期组件',
'feel' => '临境有感组件',
])
->customFormat(function ($v) {
if (! $v) {
return [];
}
]);
$form->select('position','组件位置')->options(VideoTemp::POSITION_OPTIONS);
$form->number('text_bg_box', '背景厚度')->default(0)
->addElementClass('text_bg_box')->help('设置背景块边缘厚度(用于在背景块边缘用背景色填充一圈),默认为0');
$form->color('text_bg_color', '背景色')->default('#5c6bc6')->addElementClass('text_bg_color');
$form->selectTable('font_file','字体')
->title('字体选择')
->from(FontTable::make())
->model(Font::class,'file','name');
$form->number('font_size', '字号')->default(12)->min(12);
$form->color('text_color', '字体颜色')->default('#f5f5f5')->addElementClass('text_color');
$form->number('opacity', '透明度')->min(0)->max(100)
->addElementClass('opacity')->default(100)
->help('范围为0-100,100表示不透明,0表示完全透明');
$form->switch('fix_bounds', '避免剪切');
return array_column($v, 'id');
});;
});
$form->hidden('state')
$form->hidden('state')->default(1)
->saving(function ($v) {
return $v;
});
......@@ -226,76 +151,4 @@ class VideoTempController extends AdminController
$form->display('updated_at');
});
}
public function store()
{
$all = \request()->all();
try{
DB::transaction(function ()use ($all){
$vide_temp = VideoTemp::query()->create([
'title' => $all['title'],
'state' => 1,
]);
foreach ($all['components'] as $component) {
if ($component !== null){
Component::query()->create([
'temp_id' => $vide_temp->id,
'name' => $component,
'position' => $all['pos_' . $component],
'font_size' => $all['font_size_' . $component],
'text_color' => $all['text_color_' . $component],
'text_bg_color' => $all['text_bg_color_' . $component],
'text_bg_box' => $all['text_bg_box_' . $component],
'opacity' => $all['opacity_' . $component],
'fix_bounds' => $all['fix_bounds_' . $component],
]);
}
}
});
}catch (\Exception $exception){
return $this->form()->response()->error($exception->getMessage());
}
return $this->form()->response()->refresh()->success(trans('admin.save_succeeded'));
}
public function buildCheckBoxOption($prefix, Form\BlockForm $form)
{
return function ()use ($prefix, $form) {
switch ($prefix) {
case 'every_poem':
$label = '每日一言位置';
break;
case 'one_poem':
$label = '一言位置';
break;
case 'weather':
$label = '天气位置';
break;
case 'date':
$label = '日期位置';
break;
case 'feel':
$label = '有感位置';
break;
default:
$label = '组件位置';
}
$form->divider();
$form->select('pos_' . $prefix, $label)->options(VideoTemp::POSITION_OPTIONS);
$form->number('text_bg_box_' . $prefix, '背景厚度')->default(0)
->addElementClass('text_bg_box_' . $prefix)->help('设置背景块边缘厚度(用于在背景块边缘用背景色填充一圈),默认为0');
$form->color('text_bg_color_' . $prefix, '背景色')->default('#5c6bc6')->addElementClass('text_bg_color_' . $prefix);
$form->number('font_size_' . $prefix, '字号')->min(12);
$form->color('text_color_' . $prefix, '字体颜色')->default('#f5f5f5')->addElementClass('text_color_' . $prefix);
$form->number('opacity_' . $prefix, '透明度')->min(0)->max(100)
->addElementClass('opacity_' . $prefix)->default(100)
->help('范围为0-100,100表示不透明,0表示完全透明');
$form->switch('fix_bounds_' . $prefix, '避免剪切');
};
}
}
......
<?php
/**
* Created by PhpStorm.
* User: lishuai
* Date: 2022/1/10
* Time: 5:57 PM
*/
namespace App\Admin\Renderable;
use App\Admin\Repositories\OnePoem;
use App\Models\Font;
use Dcat\Admin\Grid;
use Dcat\Admin\Grid\LazyRenderable;
use Illuminate\Support\Facades\Storage;
class FontTable extends LazyRenderable
{
public function grid(): Grid
{
return Grid::make(new Font(), function (Grid $grid) {
$grid->paginate(10);
$grid->disableActions();
$grid->quickSearch(['name']);
$grid->column('id')->sortable();
$grid->column('name');
$grid->column('file','字体')->display(function ($item){
$url = Storage::disk('public')->url($item);
return "<style>
@font-face {
font-family: 'ParlandoFont{$this->id}';
src: url('{$url}') format('truetype');
font-weight: normal;
font-style: normal;
}
.mfont-{$this->id}{
font-family: 'ParlandoFont{$this->id}';
color: red;
}
</style><span class='mfont-{$this->id} fa-2x'>字体示例:这里是临境有感</span>";
});
$grid->column('created_at');
$grid->column('updated_at')->sortable();
$grid->filter(function (Grid\Filter $filter) {
$filter->like('name','名称');
});
});
}
// public function grid(): Grid
// {
// return Grid::make(new OnePoem(), function (Grid $grid) {
// $grid->column('id', 'ID')->sortable();
// $grid->column('title');
// $grid->column('author');
// $grid->column('content');
// $grid->column('annotate');
//// $grid->column('spelling');
//// $grid->column('en');
//
// $grid->quickSearch(['title', 'author', 'content', 'annotate']);
//
// $grid->paginate(10);
// $grid->disableActions();
//
// $grid->filter(function (Grid\Filter $filter) {
// $filter->like('title')->width(3);
// $filter->like('author')->width(3);
// $filter->like('content')->width(3);
// });
// });
}
\ No newline at end of file
......@@ -28,7 +28,8 @@ Route::group([
$router->group(['prefix'=>'/linjing'],function (Router $router){
$router->resource('/font', 'FontController');
$router->resource('/template', 'VideoTempController');
$router->resource('/official', 'AdminMakeVideoController');
$router->resource('/make', 'AdminMakeVideoController');
$router->resource('/official', 'ImmerseController');
});
/** 订单*/
......
......@@ -2,6 +2,7 @@
namespace App\Console\Commands;
use App\Jobs\AdminMakeImmerse;
use App\Jobs\MakeImages;
use App\Jobs\UserMakeImmerse;
use App\Models\AdminMakeVideo;
......@@ -65,6 +66,10 @@ class DevFFmpeg extends Command
*/
public function handle()
{
dd(AdminMakeVideo::query()->find(33)->temp->components->toArray());
AdminMakeImmerse::dispatch(AdminMakeVideo::query()->find(33)->temp->components);
dd(1);
// 分情况 1.用户视频,2.用户录音
// 注意事项:1.考虑用户是否会员;非会员分辨率 720x1280,会员分辨率1440x2560
......
<?php
namespace App\Jobs;
use App\Models\Immerse;
use App\Models\VideoTemp;
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 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 $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->first();
// 素材准备
$drawtext = $this->getTextContentString();
$watermark = $this->getAbsolutePath('ffmpeg/LOGO_eng.png');
$is_bgm = $template->bg_music;
$bgm = $this->getAbsolutePath($template->bgm_url);
// 区分类型
if ($adminMakeVideo->type == 1) { // 视频
$file = $this->getAbsolutePath($adminMakeVideo->video_url);
// 分析视频
$media_info = $this->mediainfo($file);
if ($media_info['format']['nb_streams'] >= 2) {
/** 音频视频轨都有 */
if ($is_bgm) {
// 有背景音 融合
$audio = $this->getAbsolutePath($this->getTempPath('.mp3'));
$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'));
$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');
return;
}
$thumbnail = $this->getTempPath('.jpg',false);
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',false);
$cmd = $this->ffmpeg . ' -y '.
' -i ' . escapeshellarg($file).
' -i ' . escapeshellarg($watermark).
$audio_input .
' -filter_complex "[0:0]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' => '',
'content' => $adminMakeVideo->feel,
'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' => $bgm ?? '',
]);
}else{ // 图文
$image = $this->getAbsolutePath($adminMakeVideo->images_url);
if ($this->adminMakeVideo->type == 2 && $is_bgm == 0){
// 没有背景音,单图一张,输出为单图。
$output = $this->getTempPath('.png',false);
$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',false);
// 分析背景音
$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',false);
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' => '',
'content' => $this->adminMakeVideo->feel,
'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' => $bgm ?? '',
]);
}
}
public function mediainfo($file)
{
$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);
}
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);
}
} else {
// return ErrorUtil::triggerErrorMsg('proc_open error');
}
}
/**
* 获取输出临时文件名
* @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 $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 ?? 'ffmpeg/arialuni.ttf');
$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;
$text_file = $this->getAbsolutePath($this->getTempPath('.txt'));
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 'weather':
$content = $this->adminMakeVideo->weather;
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'text=' . escapeshellarg($content) . ':' .
'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时');
$drawtext .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'text=' . escapeshellarg($content) . ':' .
'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 .= 'drawtext="'.
'fontfile=' . escapeshellarg($font_file) . ':' .
'text=' . escapeshellarg($content) . ':' .
'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;
}
}
......@@ -10,4 +10,9 @@ class Component extends Model
use HasFactory;
protected $guarded = [''];
public function video_temp()
{
return $this->belongsTo('App\Models\VideoTemp','temp_id');
}
}
......
......@@ -10,5 +10,10 @@ class OnePoem extends Model
{
use HasDateTimeFormatter;
protected $table = 'one_poem';
public function admin_make_video()
{
return $this->belongsTo(AdminMakeVideo::class,'poem_id');
}
}
......
......@@ -11,15 +11,15 @@ class VideoTemp extends Model
use HasDateTimeFormatter;
const POSITION_OPTIONS = [
'topLeft'=>'上左','topMiddle'=>'上中','topRight'=>'上右',
'midLeft'=>'中左','midMiddle'=>'中中','midRight'=>'中右',
'botLeft'=>'下左','botMiddle'=>'下中','botRight'=>'下右',
'topLeft'=>'顶部左对齐','topMiddle'=>'顶部居中','topRight'=>'顶部右对齐',
'midLeft'=>'居中左对齐','midMiddle'=>'完全居中','midRight'=>'居中右对齐',
'botLeft'=>'底部左对齐','botMiddle'=>'底部居中','botRight'=>'底部右对齐',
];
const POSITION_FFMPEG = [
'topLeft' => ['0', 'text_h'], 'topMiddle' => ['(w-text_w)/2', 'text_h'], 'topRight' => ['w-text_w', 'text_h'],
'topLeft' => ['0', 'text_h+68'], 'topMiddle' => ['(w-text_w)/2', 'text_h+68'], 'topRight' => ['w-text_w', 'text_h+68'],
'midLeft' => ['0', '(h-text_h)/2'], 'midMiddle' => ['(w-text_w)/2', '(h-text_h)/2'], 'midRight' => ['w-text_w', '(h-text_h)/2'],
'botLeft' => ['0', 'h-text_h*2'], 'botMiddle' => ['(w-text_w)/2', 'h-text_h*2'], 'botRight' => ['w-text_w', 'h-text_h*2'],
'botLeft' => ['0', 'h-text_h*2-68'], 'botMiddle' => ['(w-text_w)/2', 'h-text_h*2-68'], 'botRight' => ['w-text_w', 'h-text_h*2-68'],
];
protected $table = 'video_temp';
......@@ -36,4 +36,9 @@ class VideoTemp extends Model
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']);
}
public function admin_make_video()
{
return $this->belongsTo(AdminMakeVideo::class,'temp_id');
}
}
......
......@@ -15,6 +15,7 @@ class AlterComponentsTable extends Migration
{
Schema::table('components', function (Blueprint $table) {
$table->string('font_file')->after('position')->comment('字体文件路径');
$table->string('text_bg_box')->after('text_bg_color')->comment('背景厚度');
$table->string('fix_bounds')->after('opacity')->comment('超出避免剪切');
});
......@@ -23,6 +24,13 @@ class AlterComponentsTable extends Migration
$table->unsignedTinyInteger('bg_music')->after('title')->comment('0=没有,1=有');
$table->string('bgm_url')->nullable()->after('bg_music')->comment('背景音乐地址');
});
Schema::table('admin_make_video', function (Blueprint $table) {
$table->string('weather')->after('feel')->default('')->comment('天气');
$table->string('huangli')->after('weather')->default('')->comment('黄历');
});
Schema::dropColumns('admin_make_video', ['bg_music', 'bgm_url']);
}
/**
......@@ -33,7 +41,8 @@ class AlterComponentsTable extends Migration
public function down()
{
//
Schema::dropColumns('components', ['text_bg_box', 'fix_bounds']);
Schema::dropColumns('components', ['font_file','text_bg_box', 'fix_bounds']);
Schema::dropColumns('video_temp', ['bg_music', 'bgm_url']);
Schema::dropColumns('admin_make_video', ['weather', 'huangli']);
}
}
......