一、環境準備
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:
<?php
return [
'access_key_id' => 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 基礎分片上傳代碼
<?php
use OSS\OssClient;
use OSS\Core\OssException;
class VideoUploader
{
private $ossClient;
private $bucket;
public function __construct()
{
$this->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 視頻上傳控制器
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Str;
class VideoController extends Controller
{
protected $videoUploader;
public function __construct()
{
$this->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 = `
<video controls poster="${snapshotUrl}">
<source src="${videoUrl}" type="video/mp4">
</video>
<img src="${snapshotUrl}" alt="視頻截圖" />
`;
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());
}
}