环境:tp8+php8.3\8.4;
前端代码:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>阿里云视频点播上传测试</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 40px 16px;
background: #f5f7fa;
font-family: Arial, "Microsoft YaHei", sans-serif;
color: #303133;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 32px;
background: #fff;
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, .05);
}
h2 {
margin: 0 0 10px;
font-size: 24px;
}
.description {
color: #909399;
font-size: 14px;
margin-bottom: 30px;
}
.form-item {
margin-bottom: 24px;
}
.form-item label {
display: block;
margin-bottom: 10px;
font-size: 14px;
font-weight: bold;
}
.form-control {
display: block;
width: 100%;
padding: 12px;
border: 1px solid #dcdfe6;
border-radius: 6px;
font-size: 14px;
}
.file-info {
margin-top: 10px;
font-size: 13px;
color: #606266;
}
.btn {
padding: 11px 22px;
margin-right: 10px;
margin-bottom: 8px;
border: 0;
border-radius: 6px;
color: #fff;
background: #1677ff;
cursor: pointer;
font-size: 14px;
}
.btn-danger {
background: #f56c6c;
}
.btn-secondary {
background: #909399;
}
.btn:disabled {
opacity: .5;
cursor: not-allowed;
}
.progress {
height: 14px;
margin-top: 25px;
background: #ebeef5;
border-radius: 10px;
overflow: hidden;
}
.progress-bar {
width: 0;
height: 100%;
background: #16a34a;
transition: width .2s;
}
.progress-text {
margin-top: 12px;
font-size: 14px;
color: #606266;
}
.result {
margin-top: 25px;
padding: 20px;
border-radius: 8px;
background: #f8fafc;
}
.result-title {
font-weight: bold;
margin-bottom: 12px;
}
pre {
margin: 0;
white-space: pre-wrap;
overflow-wrap: anywhere;
font-size: 13px;
line-height: 1.8;
}
.success {
color: #16a34a;
}
.error {
color: #f56c6c;
}
</style>
<!-- 阿里云 VOD JavaScript SDK 2.0.0 -->
<script src="https://g.alicdn.com/apsara-media-box/imp-web-vod-upload/2.0.0/vod-upload.umd.js"></script>
</head>
<body>
<div class="container">
<h2>阿里云视频点播上传</h2>
<div class="description">
ThinkPHP 8.1 + PHP 8.4 + VOD SDK 2.0.0
</div>
<!-- 视频标题 -->
<div class="form-item">
<label for="videoTitle">视频标题</label>
<input type="text" id="videoTitle" class="form-control" maxlength="128" placeholder="请输入视频标题">
</div>
<!-- 视频文件 -->
<div class="form-item">
<label for="videoFile">选择视频文件</label>
<input type="file" id="videoFile" class="form-control" accept=".mp4,.mov,.m4v,.webm,.mkv">
<div id="fileInfo" class="file-info">
尚未选择视频文件
</div>
</div>
<!-- 操作按钮 -->
<div>
<button type="button" id="uploadBtn" class="btn">
开始上传
</button>
<button type="button" id="cancelBtn" class="btn btn-danger" disabled>
取消上传
</button>
<button type="button" id="copyBtn" class="btn btn-secondary" disabled>
复制 VideoId
</button>
</div>
<!-- 上传进度 -->
<div class="progress">
<div id="progressBar" class="progress-bar"></div>
</div>
<div id="progressText" class="progress-text">
等待上传
</div>
<!-- 上传结果 -->
<div class="result">
<div class="result-title">上传结果</div>
<pre id="resultText">暂无上传记录</pre>
</div>
</div>
<script>
'use strict';
/**
* ThinkPHP 8.1 接口地址
*/
const CREATE_URL = '/admin/aliyun/createUploadVideo';
const REFRESH_URL = '/admin/aliyun/refreshUploadVideo';
/**
* 页面元素
*/
const videoTitle = document.getElementById('videoTitle');
const videoFile = document.getElementById('videoFile');
const fileInfo = document.getElementById('fileInfo');
const uploadBtn = document.getElementById('uploadBtn');
const cancelBtn = document.getElementById('cancelBtn');
const copyBtn = document.getElementById('copyBtn');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const resultText = document.getElementById('resultText');
/**
* 全局状态
*/
let uploader = null;
let currentTask = null;
let currentVideoId = '';
let uploading = false;
/**
* 检测 SDK
*
* 注意:真实全局对象是 AliyunVodUpload
*/
function checkSDK() {
if (
!window.AliyunVodUpload ||
typeof window.AliyunVodUpload.createUploader !== 'function'
) {
throw new Error(
'阿里云 VOD SDK 加载失败:AliyunVodUpload 不存在'
);
}
console.log('阿里云 VOD SDK 加载成功');
}
/**
* 更新进度
*/
function updateProgress(percent, loaded = 0, total = 0) {
const value = Math.min(
100,
Math.max(0, percent * 100)
).toFixed(1);
progressBar.style.width = value + '%';
const loadedMB = (loaded / 1048576).toFixed(2);
const totalMB = (total / 1048576).toFixed(2);
progressText.textContent =
`上传进度:${value}% (${loadedMB} / ${totalMB} MB)`;
}
/**
* 请求 ThinkPHP 后端
*
* 使用 form-urlencoded,
* 对应 PHP $request->post()
*/
/**
* 请求 ThinkPHP 8.1 后端
* 支持阿里云 PHP SDK 的小驼峰返回字段
*/
async function requestApi(url, params) {
const body = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
body.append(key, String(value ?? ''));
});
const response = await fetch(url, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
},
body: body.toString()
});
const responseText = await response.text();
let data;
try {
data = JSON.parse(responseText);
} catch (error) {
throw new Error('后端没有返回 JSON,HTTP状态:' + response.status);
}
if (!response.ok || data.code === 1) {
throw new Error(data.msg || data.message || '获取上传凭证失败');
}
// 兼容 PHP SDK 小驼峰字段及大驼峰字段
const uploadAuth = data.uploadAuth ?? data.UploadAuth;
const uploadAddress = data.uploadAddress ?? data.UploadAddress;
const videoId = data.videoId ?? data.VideoId;
if (!uploadAuth || !uploadAddress || !videoId) {
throw new Error('阿里云返回的上传凭证不完整');
}
// 统一为 JS SDK 需要的格式
return {
UploadAuth: uploadAuth,
UploadAddress: uploadAddress,
VideoId: videoId
};
}
/**
* 初始化上传器
*/
function initUploader() {
checkSDK();
const { createUploader } = window.AliyunVodUpload;
uploader = createUploader({
/**
* 获取上传凭证
*/
getAuth: async (ctx) => {
console.log('凭证请求类型:', ctx.kind);
switch (ctx.kind) {
/**
* 首次上传
*/
case 'create-video':
return await requestApi(
CREATE_URL,
{
fileName: ctx.file.name,
title: ctx.meta?.title || ctx.file.name.replace(/\.[^.]+$/, '')
}
);
/**
* 凭证刷新 / 断点续传
*/
case 'refresh-video':
return await requestApi(REFRESH_URL, {videoId: ctx.videoId});
default:
throw new Error('不支持的上传类型:' + ctx.kind);
}
},
/**
* 并行分片数量
*/
parallel: 4,
/**
* 每个分片大小:1MB
*/
partSize: 1024 * 1024,
/**
* 上传失败重试次数
*/
retry: {count: 3}
});
console.log('阿里云上传器初始化完成');
}
/**
* 视频文件选择事件
*/
videoFile.addEventListener('change', function () {
const file = this.files[0];
if (!file) {
fileInfo.textContent = '尚未选择视频文件';
return;
}
const fileSizeMB = (file.size / 1048576).toFixed(2);
fileInfo.textContent = `文件名:${file.name} | 大小:${fileSizeMB} MB`;
if (!videoTitle.value.trim()) {
videoTitle.value = file.name.replace(/\.[^.]+$/, '');
}
progressBar.style.width = '0%';
progressText.textContent = '等待上传';
resultText.textContent = '暂无上传记录';
currentVideoId = '';
copyBtn.disabled = true;
});
/**
* 开始上传
*/
uploadBtn.addEventListener('click', async function () {
if (uploading) {
return;
}
const file = videoFile.files[0];
const title = videoTitle.value.trim();
if (!file) {
alert('请先选择视频文件');
return;
}
if (!title) {
alert('请输入视频标题');
return;
}
if (file.size === 0) {
alert('不能上传空文件');
return;
}
const extension = file.name.split('.').pop().toLowerCase();
const allowedExtensions = ['mp4', 'mov', 'm4v', 'webm', 'mkv'];
if (!allowedExtensions.includes(extension)) {
alert('不支持此视频格式');
return;
}
try {
/**
* 第一次上传时初始化
*/
if (!uploader) {
initUploader();
}
uploading = true;
uploadBtn.disabled = true;
cancelBtn.disabled = false;
videoFile.disabled = true;
videoTitle.disabled = true;
copyBtn.disabled = true;
currentVideoId = '';
progressBar.style.width = '0%';
progressText.textContent = '正在获取上传凭证...';
resultText.textContent = '正在准备上传...';
/**
* 创建上传任务
*
* 注意:这里不能直接 await uploader.upload()
* 因为还需要保存任务对象用于 abort()
*/
currentTask = uploader.upload(file, {
meta: {title: title},
/**
* SDK progress: 0 ~ 1
*/
onProgress: (percent, loaded, total) => {
updateProgress(percent, loaded, total);
}
});
/**
* 等待上传结束
*/
const result = await currentTask;
currentVideoId = result.videoId || '';
progressBar.style.width = '100%';
progressText.textContent = '视频上传成功!';
resultText.textContent = JSON.stringify({
success: true,
videoId: result.videoId,
etag: result.etag,
durationMs: result.durationMs,
uploadTaskId: result.uploadTaskId
}, null, 2);
copyBtn.disabled = !currentVideoId;
console.log('阿里云视频上传成功:', result);
} catch (error) {
if (error.name === 'AbortError' || error.code === 'UPLOAD.ABORTED') {
progressText.textContent = '上传已取消,重新上传可尝试断点续传';
resultText.textContent = '用户取消上传';
} else {
progressText.textContent = '视频上传失败';
resultText.textContent = JSON.stringify({
success: false,
code: error.code || '',
message: error.message || String(error),
suggestion: error.suggestion || ''
}, null, 2);
console.error('阿里云上传失败:', error);
}
} finally {
uploading = false;
currentTask = null;
uploadBtn.disabled = false;
cancelBtn.disabled = true;
videoFile.disabled = false;
videoTitle.disabled = false;
}
});
/**
* 取消上传
*/
cancelBtn.addEventListener('click', function () {
if (!currentTask) {
return;
}
console.log('正在取消上传');
currentTask.abort();
});
/**
* 复制 VideoId
*/
copyBtn.addEventListener('click', async function () {
if (!currentVideoId) {
return;
}
try {
await navigator.clipboard.writeText(currentVideoId);
alert('VideoId 已复制到剪贴板');
} catch (error) {
console.error('复制失败:', error);
alert('复制失败,请手动复制 VideoId');
}
});
/**
* 页面退出时释放 SDK 资源
*/
window.addEventListener('pagehide', function () {
if (uploader && typeof uploader.dispose === 'function') {
uploader.dispose();
}
});
/**
* 页面初始化检测
*/
try {
checkSDK();
console.log('createUploader 类型:', typeof window.AliyunVodUpload.createUploader);
} catch (error) {
progressText.textContent = error.message;
progressText.classList.add('error');
console.error(error);
}
</script>
</body>
</html>
后端代码:
php
<?php
declare(strict_types=1);
namespace app\admin\controller;
use AlibabaCloud\SDK\Vod\V20170321\Vod;
use \Exception;
use AlibabaCloud\Tea\Exception\TeaError;
use AlibabaCloud\Tea\Utils\Utils;
use Darabonba\OpenApi\Models\Config;
use AlibabaCloud\SDK\Vod\V20170321\Models\CreateUploadVideoRequest;
use AlibabaCloud\SDK\Vod\V20170321\Models\RefreshUploadVideoRequest;
use AlibabaCloud\Tea\Utils\Utils\RuntimeOptions;
use think\response\Json;
use think\response\View;
class Aliyun
{
/**
* 视频上传页面
*/
public function index(): View
{
return view('/aliyun/index');
}
private function createClient(): Vod
{
$config = new Config([
"accessKeyId" => 'LT********',
"accessKeySecret" => 'nQKKy2YfK9So2PsTMoBJMJdYjanXNN'
]);
$config->endpoint = "vod.cn-shanghai.aliyuncs.com";
return new Vod($config);
}
/**
* 获取音视频上传地址和凭证;
*/
public function createUploadVideo(): Json
{
$client = $this->createClient();
$createUploadVideoRequest = new CreateUploadVideoRequest([
"fileName" => "demo.mp4",
"title" => "demo"
]);
$runtime = new RuntimeOptions([]);
try {
$result = $client->createUploadVideoWithOptions($createUploadVideoRequest, $runtime);
$res = $result->body;
return json($res);
}
catch (Exception $error) {
if (!($error instanceof TeaError)) {
$error = new TeaError([], $error->getMessage(), $error->getCode(), $error);
}
$errMsg = Utils::assertAsString($error->message);
return json(['code'=>500, 'msg'=>$errMsg]);
}
}
/**
*用于视频文件上传超时后重新获取视频上传凭证
*/
public function refreshUploadVideo(): Json
{
$client = self::createClient();
$refreshUploadVideoRequest = new RefreshUploadVideoRequest([
"videoId" => "80aca47ad03271f0a7415420848d0102"
]);
$runtime = new RuntimeOptions([]);
try {
$result = $client->refreshUploadVideoWithOptions($refreshUploadVideoRequest, $runtime);
$res = $result->body;
return json($res);
}
catch (Exception $error) {
if (!($error instanceof TeaError)) {
$error = new TeaError([], $error->getMessage(), $error->getCode(), $error);
}
$errMsg = Utils::assertAsString($error->message);
return json(['code'=>500, 'msg'=>$errMsg]);
}
}
}