一、环境准备
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());
}
}