阿里云视频点播+thinkphp8上传视频(一)

环境: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]);
        }
    }
}
相关推荐
常州晟凯电子科技1 小时前
多对一无线视频传输系统评测
音视频·视频编解码
开发笔记-阿牛2 小时前
AM收音机蓝牙二合一设计复盘
人工智能·单片机·嵌入式硬件·音视频·音频
EasyCVR2 小时前
值班脱岗=事故前兆?视频汇聚/视频融合/视频分析平台EasyCVR岗位AI监测平台,7×24小时守住安全底线
安全·音视频·easycvr
做萤石二次开发的哈哈11 小时前
连锁门店、园区、交通枢纽的视频如何统一汇集?应用模板的接入与分权管理详解
人工智能·物联网·音视频·二次开发·视频监控·蓝海aiot一站式工作台·萤石云开放平台
david_tym11 小时前
语音降噪模型GTCRN学习笔记(一)
音视频
小猴子爱上树21 小时前
跨境电商AI图片翻译工具:批量图片翻译、视频字幕翻译与智能抠图一站式解决
大数据·人工智能·python·音视频
弈语道破AI1 天前
一稿出片!双十一品牌宣传AI视频生成工具推荐:即梦 Seedance 2.5 全球首发
人工智能·音视频
赛博仓鼠1 天前
Vidu Q4 Preview实测:15张参考图+3段音频驱动,2K/4K输出,国产视频生成新标杆
ai·aigc·音视频
小猴子爱上树1 天前
跨马翻译:批量图片翻译+视频字幕+智能抠图,一站式跨境电商翻译工具
python·音视频