Laravel 阿里云 OSS 视频上传完整方案

一、环境准备

1.1 安装 OSS SDK

composer require aliyuncs/oss-sdk-php

1.2 环境配置

.env 文件中添加:

OSS_ACCESS_KEY_ID=你的AccessKeyId
OSS_ACCESS_KEY_SECRET=你的AccessKeySecret
OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
OSS_BUCKET=你的Bucket名称
OSS_IS_PRIVATE=false

1.3 创建配置文件

创建 config/oss.php

 env('OSS_ACCESS_KEY_ID'),
    'access_key_secret' => env('OSS_ACCESS_KEY_SECRET'),
    'endpoint' => env('OSS_ENDPOINT'),
    'bucket' => env('OSS_BUCKET'),
    'is_private' => env('OSS_IS_PRIVATE', false),
];

二、分片上传大视频

2.1 基础分片上传代码

ossClient = new OssClient(
            config('oss.access_key_id'),
            config('oss.access_key_secret'),
            config('oss.endpoint')
        );
        $this->bucket = config('oss.bucket');
    }

    /**
     * 分片上传视频文件
     */
    public function multipartUpload($filePath, $objectName)
    {
        try {
            // 初始化分片上传
            $uploadId = $this->ossClient->initiateMultipartUpload($this->bucket, $objectName);

            $partSize = 5 * 1024 * 1024; // 每片5MB
            $uploadFileSize = filesize($filePath);
            $pieces = $this->ossClient->generateMultiuploadParts($uploadFileSize, $partSize);
            $uploadParts = [];

            // 逐个上传分片
            foreach ($pieces as $i => $piece) {
                $fromPos = $piece[$this->ossClient::OSS_SEEK_TO];
                $toPos = $piece[$this->ossClient::OSS_LENGTH];
                
                $uploadPart = fopen($filePath, 'r');
                fseek($uploadPart, $fromPos);
                
                $eTag = $this->ossClient->uploadPart($this->bucket, $objectName, $uploadId, [
                    'partNumber' => ($i + 1),
                    'uploadPart' => $uploadPart,
                    'length' => $toPos,
                ]);
                
                fclose($uploadPart);
                
                $uploadParts[] = [
                    'PartNumber' => ($i + 1),
                    'ETag' => $eTag,
                ];
            }

            // 完成分片上传
            $result = $this->ossClient->completeMultipartUpload(
                $this->bucket, 
                $objectName, 
                $uploadId, 
                $uploadParts
            );

            return [
                'success' => true,
                'url' => $this->getPublicUrl($objectName),
                'etag' => $result
            ];

        } catch (OssException $e) {
            return [
                'success' => false,
                'error' => $e->getMessage()
            ];
        }
    }

    private function getPublicUrl($objectName)
    {
        return "https://{$this->bucket}." . config('oss.endpoint') . "/{$objectName}";
    }
}

2.2 优化版本(带进度回调)

public function multipartUploadWithProgress($filePath, $objectName, $progressCallback = null)
{
    try {
        $uploadId = $this->ossClient->initiateMultipartUpload($this->bucket, $objectName);
        $partSize = 5 * 1024 * 1024;
        $uploadFileSize = filesize($filePath);
        $pieces = $this->ossClient->generateMultiuploadParts($uploadFileSize, $partSize);
        $uploadParts = [];

        foreach ($pieces as $i => $piece) {
            $fromPos = $piece[$this->ossClient::OSS_SEEK_TO];
            $toPos = $piece[$this->ossClient::OSS_LENGTH];
            
            $uploadPart = fopen($filePath, 'r');
            fseek($uploadPart, $fromPos);
            
            $eTag = $this->ossClient->uploadPart($this->bucket, $objectName, $uploadId, [
                'partNumber' => ($i + 1),
                'uploadPart' => $uploadPart,
                'length' => $toPos,
            ]);
            
            fclose($uploadPart);
            
            $uploadParts[] = [
                'PartNumber' => ($i + 1),
                'ETag' => $eTag,
            ];

            // 进度回调
            if ($progressCallback) {
                $progress = round(($i + 1) / count($pieces) * 100, 2);
                call_user_func($progressCallback, $progress, $i + 1, count($pieces));
            }
        }

        $result = $this->ossClient->completeMultipartUpload(
            $this->bucket, 
            $objectName, 
            $uploadId, 
            $uploadParts
        );

        return ['success' => true, 'url' => $this->getPublicUrl($objectName), 'etag' => $result];

    } catch (OssException $e) {
        return ['success' => false, 'error' => $e->getMessage()];
    }
}

三、视频截图功能

3.1 生成第一帧截图

/**
 * 获取视频第一帧截图
 */
public function getVideoSnapshot($objectName, $width = 800, $height = 600, $time = 0)
{
    $process = "video/snapshot,t_{$time},f_jpg,w_{$width},h_{$height},m_fast";
    
    // 公共读取的 Bucket
    if (!config('oss.is_private')) {
        $endpoint = config('oss.endpoint');
        return "https://{$this->bucket}.{$endpoint}/{$objectName}?x-oss-process={$process}";
    }
    
    // 私有 Bucket,需要签名
    return $this->ossClient->signUrl($this->bucket, $objectName, 3600, 'GET', [
        'x-oss-process' => $process
    ]);
}

3.2 多种截图参数

/**
 * 生成多种规格的截图
 */
public function generateMultipleSnapshots($objectName, $configs = [])
{
    $defaultConfigs = [
        'thumbnail' => ['width' => 200, 'height' => 150, 'time' => 0],
        'medium' => ['width' => 600, 'height' => 400, 'time' => 0],
        'large' => ['width' => 1200, 'height' => 800, 'time' => 0],
    ];
    
    $configs = array_merge($defaultConfigs, $configs);
    $snapshots = [];
    
    foreach ($configs as $name => $config) {
        $snapshots[$name] = $this->getVideoSnapshot(
            $objectName,
            $config['width'],
            $config['height'],
            $config['time']
        );
    }
    
    return $snapshots;
}

四、控制器实现

4.1 视频上传控制器

videoUploader = new VideoUploader();
    }

    /**
     * 上传视频
     */
    public function upload(Request $request): JsonResponse
    {
        $request->validate([
            'video' => 'required|file|mimes:mp4,avi,mov,wmv,flv,webm|max:2048000' // 最大2GB
        ]);

        $file = $request->file('video');
        $extension = $file->getClientOriginalExtension();
        
        // 生成存储路径
        $objectName = 'videos/' . date('Y/m/d/') . Str::uuid() . '.' . $extension;

        // 执行上传
        $result = $this->videoUploader->multipartUpload($file->getRealPath(), $objectName);

        if ($result['success']) {
            // 生成截图
            $snapshotUrl = $this->videoUploader->getVideoSnapshot($objectName);
            
            return response()->json([
                'success' => true,
                'data' => [
                    'url' => $result['url'],
                    'object_name' => $objectName,
                    'snapshot_url' => $snapshotUrl,
                    'size' => $file->getSize(),
                ]
            ]);
        }

        return response()->json([
            'success' => false,
            'message' => $result['error']
        ], 500);
    }

    /**
     * 获取视频截图
     */
    public function getSnapshot(Request $request): JsonResponse
    {
        $request->validate([
            'object_name' => 'required|string',
            'width' => 'integer|min:100|max:1920',
            'height' => 'integer|min:100|max:1080',
            'time' => 'integer|min:0',
        ]);

        $objectName = $request->input('object_name');
        $width = $request->input('width', 800);
        $height = $request->input('height', 600);
        $time = $request->input('time', 0);

        $snapshotUrl = $this->videoUploader->getVideoSnapshot($objectName, $width, $height, $time);

        return response()->json([
            'success' => true,
            'data' => ['snapshot_url' => $snapshotUrl]
        ]);
    }
}

五、路由配置

5.1 API 路由

routes/api.php 中添加:

Route::prefix('video')->group(function () {
    Route::post('upload', [VideoController::class, 'upload']);
    Route::post('snapshot', [VideoController::class, 'getSnapshot']);
});

六、前端调用示例

6.1 JavaScript 上传

// 上传视频
async function uploadVideo(file) {
    const formData = new FormData();
    formData.append('video', file);

    try {
        const response = await fetch('/api/video/upload', {
            method: 'POST',
            body: formData,
            headers: {
                'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
            }
        });

        const result = await response.json();
        
        if (result.success) {
            console.log('上传成功:', result.data);
            // 显示视频和截图
            showVideo(result.data.url, result.data.snapshot_url);
        } else {
            console.error('上传失败:', result.message);
        }
    } catch (error) {
        console.error('上传错误:', error);
    }
}

// 显示视频
function showVideo(videoUrl, snapshotUrl) {
    const videoHtml = `
        
        视频截图
    `;
    document.getElementById('video-container').innerHTML = videoHtml;
}

6.2 带进度条的上传

function uploadVideoWithProgress(file, progressCallback) {
    const formData = new FormData();
    formData.append('video', file);

    const xhr = new XMLHttpRequest();
    
    // 监听上传进度
    xhr.upload.addEventListener('progress', (e) => {
        if (e.lengthComputable) {
            const progress = (e.loaded / e.total) * 100;
            progressCallback(Math.round(progress));
        }
    });

    xhr.addEventListener('load', () => {
        if (xhr.status === 200) {
            const result = JSON.parse(xhr.responseText);
            console.log('上传完成:', result);
        }
    });

    xhr.open('POST', '/api/video/upload');
    xhr.setRequestHeader('X-CSRF-TOKEN', document.querySelector('meta[name="csrf-token"]').getAttribute('content'));
    xhr.send(formData);
}

七、最佳实践

7.1 错误处理

try {
    $result = $this->videoUploader->multipartUpload($filePath, $objectName);
} catch (Exception $e) {
    Log::error('视频上传失败', [
        'file' => $objectName,
        'error' => $e->getMessage(),
        'trace' => $e->getTraceAsString()
    ]);
    
    return response()->json([
        'success' => false,
        'message' => '上传失败,请稍后重试'
    ], 500);
}

7.2 文件验证

public function validateVideoFile($file)
{
    $allowedMimes = ['video/mp4', 'video/avi', 'video/mov', 'video/wmv'];
    $maxSize = 2 * 1024 * 1024 * 1024; // 2GB

    if (!in_array($file->getMimeType(), $allowedMimes)) {
        throw new InvalidArgumentException('不支持的视频格式');
    }

    if ($file->getSize() > $maxSize) {
        throw new InvalidArgumentException('文件大小超过限制');
    }

    return true;
}

7.3 性能优化

  • 分片大小建议 5-10MB
  • 并发上传时控制同时上传的分片数量
  • 使用 CDN 加速访问
  • 对于小文件可以直接使用简单上传

7.4 安全考虑

  • 验证文件类型和大小
  • 使用随机文件名避免冲突
  • 私有 Bucket 使用签名 URL
  • 设置合理的 URL 过期时间

八、常见问题

8.1 上传失败重试

public function uploadWithRetry($filePath, $objectName, $maxRetries = 3)
{
    for ($i = 0; $i < $maxRetries; $i++) {
        $result = $this->multipartUpload($filePath, $objectName);
        
        if ($result['success']) {
            return $result;
        }
        
        if ($i < $maxRetries - 1) {
            sleep(pow(2, $i)); // 指数退避
        }
    }
    
    return $result;
}

8.2 清理未完成的分片上传

public function cleanupIncompleteUploads()
{
    try {
        $listResult = $this->ossClient->listMultipartUploads($this->bucket);
        
        foreach ($listResult->getUploads() as $upload) {
            // 清理超过24小时的未完成上传
            if (time() - strtotime($upload->getInitiated()) > 86400) {
                $this->ossClient->abortMultipartUpload(
                    $this->bucket,
                    $upload->getKey(),
                    $upload->getUploadId()
                );
            }
        }
    } catch (OssException $e) {
        Log::error('清理未完成上传失败: ' . $e->getMessage());
    }
}

你可能感兴趣的:(phplavarel)