소스 검색

Merge branch 'master' of moshaorui/mtb_dcatadmin_album into stable

moshaorui 4 주 전
부모
커밋
6acf449c90

+ 12 - 1
app/Admin/Controllers/SiteAlbumController.php

@@ -5,6 +5,7 @@ namespace App\Admin\Controllers;
 use App\Admin\Repositories\SiteAlbum;
 use App\Admin\Repositories\SiteAlbumFolder;
 use App\Admin\Repositories\SiteAlbumLog;
+use App\Admin\Repositories\SitePreviewVideo;
 use App\Libraries\CommonHelper;
 use App\Models\SiteAlbumFolder as SiteAlbumFolderModel;
 use Dcat\Admin\Form;
@@ -246,8 +247,9 @@ JS
             });
 
             $form->saved(function (Form $form) {
+                $id = $form->getKey();
                 if (empty($form->input('model')) == false) {
-                    $id = $form->getKey();
+
                     $action = $form->isCreating() ? 'add' : 'edit';
                     if ($action == 'add') {
                         $oldData = "[]";
@@ -257,6 +259,15 @@ JS
                     }
                     SiteAlbumLog::log($action, $id,$form->input('model'),$oldData);
                 }
+                //更新site_preview_video表
+                $video = $form->input('video');
+                $data = [];
+                foreach ($video as $value) {
+                    if ($value['_remove_'] != 1){
+                        $data[] = ['cover'=>$value['cover'],'video_title'=>$value['video_title'],'video_en_title' => $value['video_en_title'],'video_src'=>$value['video_src']];
+                    }
+                }
+                SitePreviewVideo::updatePreviewVideo($id,$data);
             });
 
             $form->tab(admin_trans_label('basic_info'), function (Form $form) {

+ 72 - 0
app/Admin/Repositories/SitePreviewVideo.php

@@ -0,0 +1,72 @@
+<?php
+
+namespace App\Admin\Repositories;
+
+use App\Models\SitePreviewVideo as Model;
+use Dcat\Admin\Repositories\EloquentRepository;
+use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\DB;
+
+class SitePreviewVideo extends EloquentRepository
+{
+    /**
+     * Model.
+     *
+     * @var string
+     */
+    protected $eloquentClass = Model::class;
+
+    /**
+     *
+     */
+    public static function updatePreviewVideo($album_id, $data) {
+        // Get existing videos from the database for the given album_id
+        $model = new Model();
+        $existingVideos = $model->where('album_id', $album_id)->get();
+
+        // Iterate over the $data array
+        foreach ($data as $value) {
+            $videoSrc = $value['video_src'];
+
+
+            // Check if the video_src already exists in the database
+            $existingVideo = $existingVideos->firstWhere('video_url', $videoSrc);
+
+            if ($existingVideo) {
+                // If the video exists and the video_url is the same, do nothing
+                continue;
+            } else {
+                // If the video_src doesn't exist, insert a new record
+                $model->create([
+                    'video_url' => $videoSrc,
+                    'preview_url' => '',
+                    'status' => 0, // Assuming status 1 means active or available
+                    'created_at' => Carbon::now(),
+                    'updated_at' => Carbon::now(),
+                    'album_id' => $album_id,
+                    'remark' => '', // Optional remark, you can customize this
+                ]);
+            }
+        }
+
+
+        // Now check for videos that are in the database but not in the $data array, and delete them
+        foreach ($existingVideos as $existing) {
+            $videoExistsInData = false;
+
+            // Check if the current video exists in the data array
+            foreach ($data as $value) {
+                if ($value['video_src'] === $existing->video_url) {
+                    $videoExistsInData = true;
+                    break;
+                }
+            }
+
+            // If the video does not exist in the data, delete it
+            if (!$videoExistsInData) {
+                $existing->delete();
+            }
+        }
+    }
+
+}

+ 279 - 0
app/Console/Commands/GeneratePreviewVideo.php

@@ -0,0 +1,279 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Libraries\CommonHelper;
+use Exception;
+use Illuminate\Console\Command;
+use Illuminate\Http\File;
+use Illuminate\Support\Carbon;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use Dcat\Admin\Traits\HasUploadedFile;
+use FFMpeg\FFMpeg;
+use FFMpeg\Coordinate\TimeCode;
+
+
+
+
+
+/*
+ * 生成预览10秒视频
+ *
+ * @author  <EMAIL>
+ * 运行命令:php artisan command:preview-video
+ */
+
+class GeneratePreviewVideo extends Command
+{
+    use HasUploadedFile;
+
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'command:preview-video';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = '生成预览视频';
+
+    public $timeSecond = 8;
+
+    /**
+     * Execute the console command.
+     *
+     * @return int
+     */
+    public function handle()
+    {
+//        $this->importVideo();
+//        exit;
+        ini_set('memory_limit', '512M');
+        $disk = $this->disk('oss');
+
+        //如果目录不存在则创建
+        $localPreviewPath = storage_path('tmp/previews');
+        if (!file_exists($localPreviewPath)) {
+            mkdir($localPreviewPath, 0777, true);
+        }
+
+        $reslut = DB::table('site_preview_video')
+            ->where('status', '==', 0) // 过滤非空 video 字段
+            ->orderBy('id') // 按主键排序确保顺序
+            ->limit(1) // 限制处理 1 条
+            ->get();
+
+        foreach ($reslut as $item) {
+            try {
+                $video_oss_url = $this->ossUrl($item->video_url);
+                $fileName = basename($video_oss_url);
+                $fileInfo = pathinfo($fileName);
+
+                //下载$video_url到本地
+                $localFilePath = $localPreviewPath. '/'. $fileName;
+                $this->saveLocal($video_oss_url, $localFilePath);
+
+
+                $previewName = $fileInfo['filename'] . '_preview.' . $fileInfo['extension'];
+                $previewPath = storage_path('tmp/previews/'.$previewName);
+                //截取视频前5秒
+                $this->generatePreview($localFilePath, $previewPath);
+
+                //上传到OSS
+                $uploadPath = 'videos/uploads/previews/'. date('Ym');
+                $file = new File($previewPath);
+                $disk->putFileAs($uploadPath, $file, $previewName);
+
+                DB::table('site_preview_video')
+                    ->where('id', $item->id)
+                    ->update([
+                        'preview_url' => $uploadPath . '/' . $previewName,
+                        'status' => 1,
+                        'updated_at' => Carbon::now()
+                    ]);
+
+                //删除本地文件
+                unlink($localFilePath);
+                unlink($previewPath);
+            } catch (\Exception $e) {
+                DB::table('site_preview_video')
+                    ->where('id', $item->id)
+                    ->update([
+                        'remark' => $e->getMessage(),
+                        'status' => -1,
+                        'updated_at' => Carbon::now()
+                    ]);
+            }
+        }
+
+        //$this->importVideo();
+        return dd('success');
+    }
+
+    private function saveLocal($url, $localPath) {
+        // 初始化 cURL 会话
+        $ch = curl_init($url);
+        // 设置 cURL 选项
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 返回数据而不是直接输出
+        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // 跟踪重定向
+        curl_setopt($ch, CURLOPT_TIMEOUT, 300); // 设置超时时间(秒)
+        // 执行 cURL 请求
+        $response = curl_exec($ch);
+        // 检查是否有错误
+        if (curl_errno($ch)) {
+            $error = 'cURL error: ' . curl_error($ch);
+            curl_close($ch);
+            throw new Exception($error);
+        }
+        // 获取 HTTP 状态码
+        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        curl_close($ch);
+        if ($httpCode != 200) {
+            throw new Exception("HTTP error: {$httpCode}");
+        }
+        // 打开本地文件进行写入
+        $local = fopen($localPath, 'wb');
+        if (!$local) {
+            throw new Exception("Failed to open local file: {$localPath}");
+        }
+        // 将响应数据写入本地文件
+        fwrite($local, $response);
+        fclose($local);
+        return $localPath;
+    }
+
+    private function ossUrl($url)
+    {
+        if (strpos($url, 'http:') === 0 || strpos($url, 'https:') === 0) {
+            return  $url;
+        }
+        if (env('PREVIEW_OSS_URL_INTERNAL')) {
+            return env('PREVIEW_OSS_URL_INTERNAL').'/'.$url;
+        }
+        return env('PREVIEW_OSS_URL').'/'.$url;
+    }
+
+    /*
+
+    public function generatePreview($inputUrl, $outputPath) {
+        $ffmpeg = FFMpeg::create([
+            'ffmpeg.binaries'  => '/usr/bin/ffmpeg',
+            'ffprobe.binaries' => '/usr/bin/ffprobe',
+            'timeout' => 300
+        ]);
+
+        $video = $ffmpeg->open($inputUrl);
+        $video->filters()->clip(TimeCode::fromSeconds(0), TimeCode::fromSeconds($this->timeSecond));
+        $video->filters()->framerate(new \FFMpeg\Coordinate\FrameRate(15), 60); // 降低帧率
+
+        // 输出为H.264编码的MP4
+        $format = new \FFMpeg\Format\Video\X264();
+        // 压缩参数:降低比特率以减小文件大小
+        $format->setKiloBitrate(200); // 设置视频比特率(kbps),500 是一个较低的值,可根据需要调整
+        $format->setAdditionalParameters(['-an']);  // 禁用音频,或者可以用 setAdditionalParameters(['-an'])
+        $video->save($format, $outputPath);
+        return $outputPath;
+    }*/
+
+    public function generatePreview($inputUrl, $outputPath) {
+        $ffmpeg = FFMpeg::create([
+            'ffmpeg.binaries'  => '/usr/bin/ffmpeg',
+            'ffprobe.binaries' => '/usr/bin/ffprobe',
+            'timeout' => 180
+        ]);
+
+        // Open the video
+        $video = $ffmpeg->open($inputUrl);
+
+        // Use ffprobe to get original video dimensions
+        $ffprobe = \FFMpeg\FFProbe::create(['ffprobe.binaries' => '/usr/bin/ffprobe']);
+        $stream = $ffprobe->streams($inputUrl)->videos()->first();
+        $originalWidth = $stream->getDimensions()->getWidth();
+        $originalHeight = $stream->getDimensions()->getHeight();
+
+        // Calculate adaptive height for a fixed width of 300
+        $newWidth = 300;
+        $newHeight = (int) round(($originalHeight / $originalWidth) * $newWidth);
+
+        // Ensure dimensions are even (required by H.264)
+        $newHeight = $newHeight % 2 === 0 ? $newHeight : $newHeight + 1;
+
+        // Apply filters
+        $video->filters()->clip(TimeCode::fromSeconds(0), TimeCode::fromSeconds($this->timeSecond));
+        $video->filters()->framerate(new \FFMpeg\Coordinate\FrameRate(15), 60); // Lower frame rate
+        $video->filters()->resize(new \FFMpeg\Coordinate\Dimension($newWidth, $newHeight)); // Adaptive resize
+
+        // Output as H.264 encoded MP4
+        $format = new \FFMpeg\Format\Video\X264();
+        $format->setKiloBitrate(150); // Set video bitrate (kbps)
+        $format->setAdditionalParameters(['-an']); // Disable audio
+
+        // Save the video
+        $video->save($format, $outputPath);
+        return $outputPath;
+    }
+
+
+    /*ssssssss
+     * 把旧数据导入新的预览表
+     */
+    function importVideo()
+    {
+        try {
+            // 分批处理数据,避免内存溢出
+            DB::table('site_album')
+                ->where('video', '<>', '[]') // 过滤非空 video 字段
+                ->orderBy('id') // 按主键排序确保顺序
+                ->chunkById(10000, function ($albums) { // 每次处理 100 条
+                    $insertData = [];
+
+                    foreach ($albums as $album) {
+                        // 跳过无效 JSON 数据
+                        $videos = json_decode($album->video, true);
+                        if (json_last_error() !== JSON_ERROR_NONE || !is_array($videos)) {
+                            Log::warning("Invalid JSON in album ID: {$album->id}");
+                            continue;
+                        }
+
+                        foreach ($videos as $video) {
+                            // 校验必要字段存在性
+                            if (empty($video['video_src']) || empty($video['cover'])) {
+                                Log::warning("Missing video_src or cover in album ID: {$album->id}");
+                                continue;
+                            }
+
+                            // 处理 URL 长度限制
+                            $videoUrl = substr($video['video_src'], 0, 255);
+
+                            // 构建插入数据
+                            $insertData[] = [
+                                'video_url'    => $videoUrl,
+                                'preview_url'  => '',
+                                'status'       => 0,
+                                'album_id'     => $album->id,
+                                'created_at'   => Carbon::now(),
+                                'updated_at'   => Carbon::now(),
+                            ];
+                        }
+                    }
+
+                    // 插入剩余数据
+                    if (!empty($insertData)) {
+                        DB::table('site_preview_video')->insert($insertData);
+                    }
+                });
+
+            return true;
+        } catch (\Exception $e) {
+            Log::error("Video import failed: " . $e->getMessage());
+            return false;
+        }
+    }
+
+
+}

+ 24 - 0
app/Models/SitePreviewVideo.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Models;
+
+use Dcat\Admin\Traits\HasDateTimeFormatter;
+
+use Illuminate\Database\Eloquent\Model;
+
+class SitePreviewVideo extends Model
+{
+	use HasDateTimeFormatter;
+    protected $table = 'site_preview_video';
+
+    protected $fillable = [
+        // ... other fillable attributes
+        'video_url', // Add this line
+        'preview_url',
+        'status',
+        'album_id',
+        'remark',
+        // Include other attributes if needed
+    ];
+
+}

+ 39 - 0
database/migrations/2025_03_19_124906_create_preview_video_table.php

@@ -0,0 +1,39 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+
+
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('site_preview_video', function (Blueprint $table) {
+            $table->id()->key()->autoIncrement();
+            $table->string('video_url');
+            $table->string('preview_url');
+            $table->tinyInteger('status')->default(0);
+            $table->bigInteger('album_id');
+            $table->string('remark');
+            $table->timestamp('created_at', 0);
+            $table->timestamp('updated_at', 0);
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('preview_video');
+    }
+};