阿里云视频点播+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]);
        }
    }
}
相关推荐
微帧Visionular1 天前
AI视频单帧清晰,播放时却闪烁,原因是什么?
人工智能·深度学习·音视频
辰辰观影1 天前
Windows网盘视频播放器推荐:无广告4K HDR怎么选
音视频
2601_966827451 天前
电视端云盘在线播放怎么选?支持4K HDR的应用推荐
音视频
开开心心就好1 天前
音频降噪剪辑工具,音量裁剪一键处理
运维·服务器·前端·spring·ocr·音视频·1024程序员节
AI视频工具指南1 天前
AI视频的声音和画面怎么配合?先分清谁在传递信息
人工智能·aigc·音视频
辰辰观影1 天前
无广告播放器推荐来啦
音视频
TrueDei1 天前
GStreamer 使用 valve 分别控制音视频流的暂停与恢复
音视频
一条破秋裤1 天前
基于 Hi3516CV610 的立定跳远视频测量系统调研与实施方案
arm开发·音视频
2601_966827451 天前
视频里的特效字幕怎么翻译?外挂字幕与AI字幕一键处理教程
音视频
八月的雨季 最後的冰吻1 天前
音视频面试--延迟优化
面试·职场和发展·音视频