当前位置:首页 > 学海无涯 > 正文内容

PHP 实现在线视频播放完整方案:从后端存储到前端适配

清羽天2周前 (11-27)学海无涯13


在 Web 开发中,在线视频播放是电商展示、教育平台、企业宣传等场景的核心需求。PHP 作为主流的后端脚本语言,具备开发高效、部署简单、生态完善的优势,配合前端播放器组件,可快速实现跨浏览器、高兼容性的视频播放功能。本文将从技术选型、后端核心实现、前端集成、优化部署四个维度,手把手教你搭建 PHP 在线视频播放系统。

一、技术选型

1. 核心技术栈

  • 后端:PHP 7.4+(稳定兼容,支持大文件处理)、ThinkPHP 6.x(可选,快速构建接口)、Nginx(反向代理 + 静态资源处理)

  • 前端:HTML5 Video 原生 API、Video.js(增强播放体验,支持 HLS/DASH)

  • 存储方案

    • 本地存储(开发 / 测试环境):服务器磁盘直接存储

    • 云存储(生产环境):阿里云 OSS、腾讯云 COS(高可用、CDN 适配)

  • 传输协议

    • HTTP 分片传输(基础方案,支持断点续传)

    • HLS(HTTP Live Streaming,适配移动端 + 弱网环境)

  • 辅助工具:FFmpeg(视频转码,统一格式)、Redis(可选,缓存视频元信息)

2. 关键技术说明

  • Video.js:开源免费的跨浏览器播放器,支持 MP4、WebM、HLS(.m3u8)等格式,自带进度条、倍速、全屏等控件,可自定义皮肤。

  • 分片传输:通过解析 HTTP Range 请求头,将大视频文件分割为小片段(如 4MB / 片),前端按需加载,避免一次性加载大文件导致的卡顿。

  • 视频转码:用 FFmpeg 将原始视频(AVI、MKV、MOV 等)转为 MP4(兼容性最优)或 HLS 格式(.m3u8 索引 + ts 分片),解决不同设备格式兼容问题。

  • Nginx 静态资源优化:视频文件本质是静态资源,通过 Nginx 直接处理视频请求,比 PHP 原生输出更高效,降低后端压力。

二、后端实现(PHP + Nginx)

后端核心职责:视频上传(支持大文件)、分片播放接口、权限校验、视频转码

1. 环境准备

  • 安装 PHP 7.4+,开启fileinfocurl扩展(处理文件类型识别、HTTP 请求)

  • 安装 Nginx(配置静态资源访问 + 反向代理)

  • (可选)安装 FFmpeg:yum install ffmpeg(CentOS)或brew install ffmpeg(Mac)

2. 核心功能实现(原生 PHP + 少量框架思想)

(1)视频上传接口(支持大文件分片上传)

大文件直接上传易超时,采用「分片上传 + 合并」方案:前端分割文件,后端接收后拼接为完整视频。
php
运行
<?php/**
 * 视频分片上传接口
 * 接收参数:file(分片文件)、fileName(原文件名)、chunkIndex(分片索引)、totalChunks(总分片数)
 */header("Content-Type: application/json; charset=utf-8");header("Access-Control-Allow-Origin: *"); // 开发环境允许跨域,生产环境限制具体域名// 配置参数$videoStoragePath = '/www/video-storage/'; // 视频存储根目录$tempShardPath = $videoStoragePath . 'temp/'; // 临时分片目录$maxShardSize = 4 * 1024 * 1024; // 分片大小(4MB)// 确保目录存在if (!is_dir($videoStoragePath)) {
    mkdir($videoStoragePath, 0755, true);}if (!is_dir($tempShardPath)) {
    mkdir($tempShardPath, 0755, true);}// 接收请求参数$file = $_FILES['file'] ?? null;$fileName = $_POST['fileName'] ?? '';$chunkIndex = $_POST['chunkIndex'] ?? 0;$totalChunks = $_POST['totalChunks'] ?? 1;// 参数校验if (!$file || !$fileName || $chunkIndex < 0 || $totalChunks < 1) {
    echo json_encode(['code' => 400, 'msg' => '参数不全']);
    exit;}// 生成唯一文件标识(避免文件名重复)$fileHash = md5($fileName . microtime(true));$shardDir = $tempShardPath . $fileHash . '/';// 确保分片目录存在if (!is_dir($shardDir)) {
    mkdir($shardDir, 0755, true);}// 保存当前分片$shardFilePath = $shardDir . $chunkIndex;if (!move_uploaded_file($file['tmp_name'], $shardFilePath)) {
    echo json_encode(['code' => 500, 'msg' => '分片保存失败']);
    exit;}// 检查是否所有分片上传完成,若完成则合并if ($chunkIndex == $totalChunks - 1) {
    $finalFileName = $fileHash . '.' . pathinfo($fileName, PATHINFO_EXTENSION);
    $finalFilePath = $videoStoragePath . $finalFileName;

    // 合并所有分片(按索引顺序)
    $fp = fopen($finalFilePath, 'wb');
    for ($i = 0; $i < $totalChunks; $i++) {
        $currentShard = $shardDir . $i;
        if (!file_exists($currentShard)) {
            fclose($fp);
            echo json_encode(['code' => 500, 'msg' => '分片缺失,合并失败']);
            exit;
        }
        // 读取分片并写入最终文件
        fwrite($fp, file_get_contents($currentShard));
        // 删除临时分片
        unlink($currentShard);
    }
    fclose($fp);
    // 删除临时分片目录
    rmdir($shardDir);

    // (可选)调用FFmpeg转码为HLS格式(后台执行,避免阻塞)
    $hlsDir = $videoStoragePath . 'hls/' . $fileHash . '/';
    if (!is_dir($hlsDir)) {
        mkdir($hlsDir, 0755, true);
    }
    // 后台执行转码命令(&符号让命令在后台运行)
    $ffmpegCmd = "ffmpeg -i {$finalFilePath} -hls_time 10 -hls_list_size 0 -f hls {$hlsDir}index.m3u8 > /dev/null 2>&1 &";
    exec($ffmpegCmd);

    // 返回结果(包含播放地址)
    $playUrl = "http://your-domain.com/video/play/{$finalFileName}";
    $hlsPlayUrl = "http://your-domain.com/video/play/hls/{$fileHash}/index.m3u8";
    echo json_encode([
        'code' => 200,
        'msg' => '上传完成',
        'data' => [
            'playUrl' => $playUrl,
            'hlsPlayUrl' => $hlsPlayUrl,
            'fileName' => $finalFileName
        ]
    ]);} else {
    echo json_encode(['code' => 200, 'msg' => "分片{$chunkIndex}上传成功"]);}?>

(2)视频分片播放接口(支持断点续传)

通过解析 HTTP Range 头,返回对应字节范围的视频数据,实现断点续传和流畅播放。
php
运行
<?php/**
 * 视频流式播放接口
 * 支持Range请求,断点续传
 */header("Access-Control-Allow-Origin: *");$videoStoragePath = '/www/video-storage/';$fileName = $_GET['fileName'] ?? '';$hlsPath = $_GET['hlsPath'] ?? '';// 处理普通视频文件(MP4等)if ($fileName) {
    $videoPath = $videoStoragePath . $fileName;}// 处理HLS文件(.m3u8或.ts)elseif ($hlsPath) {
    $videoPath = $videoStoragePath . 'hls/' . $hlsPath;} else {
    http_response_code(404);
    exit('视频地址不存在');}// 检查文件是否存在if (!file_exists($videoPath) || !is_file($videoPath)) {
    http_response_code(404);
    exit('视频文件不存在');}// 获取文件信息$fileSize = filesize($videoPath);$mimeType = mime_content_type($videoPath); // 需要开启fileinfo扩展// 解析Range请求头(格式:bytes=0-1023 或 bytes=1024-)$range = $_SERVER['HTTP_RANGE'] ?? '';$start = 0;$end = $fileSize - 1;if ($range && strpos($range, 'bytes=') === 0) {
    $range = substr($range, 6);
    $rangeArr = explode('-', $range);
    $start = intval($rangeArr[0]);
    if (isset($rangeArr[1]) && $rangeArr[1] !== '') {
        $end = intval($rangeArr[1]);
    }
    // 修正范围(避免超出文件大小)
    $start = min($start, $end);
    $end = min($end, $fileSize - 1);}// 计算响应长度$responseLength = $end - $start + 1;// 设置响应头(关键:支持断点续传)http_response_code(206); // Partial Contentheader("Content-Type: {$mimeType}");header("Content-Length: {$responseLength}");header("Content-Range: bytes {$start}-{$end}/{$fileSize}");header("Accept-Ranges: bytes");header("Cache-Control: public, max-age=86400"); // 缓存1天,减少重复请求// 流式输出视频数据(避免一次性读取大文件到内存)$bufferSize = 4096; // 4KB缓冲区$fp = fopen($videoPath, 'rb');fseek($fp, $start); // 定位到起始字节while ($responseLength > 0 && !feof($fp)) {
    $readLength = min($bufferSize, $responseLength);
    $data = fread($fp, $readLength);
    echo $data;
    flush(); // 强制输出缓冲区
    $responseLength -= $readLength;}fclose($fp);exit;?>

(3)权限控制(可选)

限制视频仅授权用户可播放,通过 Token 验证实现:
php
运行
<?php/**
 * 带权限验证的播放接口
 */header("Access-Control-Allow-Origin: *");// 1. 验证Token(生产环境需从数据库/Redis查询合法Token)$token = $_GET['token'] ?? '';$validToken = 'your-valid-token'; // 实际应从用户表获取if ($token !== $validToken) {
    http_response_code(403);
    exit('无播放权限');}// 2. 后续逻辑同「分片播放接口」$videoStoragePath = '/www/video-storage/';$fileName = $_GET['fileName'] ?? '';// ...(省略文件存在校验、Range解析、流式输出等逻辑)?>

3. Nginx 配置优化(关键)

PHP 处理静态资源效率较低,通过 Nginx 直接处理视频请求,仅将权限校验、上传等动态逻辑交给 PHP。
nginx
server {
    listen 80;
    server_name your-domain.com; # 你的域名

    # 静态资源直接访问(视频文件)
    location ~* ^/video/storage/ {
        alias /www/video-storage/; # 视频存储目录绝对路径
        expires 1d; # 缓存1天
        add_header Cache-Control "public, max-age=86400";
        # 支持Range请求(Nginx原生支持,无需额外代码)
        add_header Accept-Ranges bytes;
        # 禁止直接访问目录
        autoindex off;
    }

    # 动态接口(上传、权限验证播放)
    location ~* ^/video/api/ {
        proxy_pass http://127.0.0.1:9000; # PHP-FPM地址
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        # 支持大文件上传(配合PHP的upload_max_filesize)
        client_max_body_size 1024m;
    }

    # PHP-FPM配置
    location ~ \.php$ {
        root /www/your-project/; # 项目根目录
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }}

三、前端实现(HTML + Video.js)

前端核心职责:视频上传(分片)、视频播放(兼容普通格式 + HLS)、进度显示

1. 视频上传组件(分片上传)

html
预览
<!DOCTYPE html><html lang="zh-CN"><head>
    <meta charset="UTF-8">
    <title>视频上传</title>
    <style>
        .progress { width: 500px; height: 20px; border: 1px solid #ccc; margin: 10px 0; }
        .progress-bar { height: 100%; background: #42b983; width: 0%; transition: width 0.3s; }
    </style></head><body>
    <input type="file" id="videoFile" accept="video/*" />
    <div id="fileInfo" style="display: none;">
        <p>文件名:<span id="fileName"></span></p>
        <p>大小:<span id="fileSize"></span></p>
        <button onclick="startUpload()">开始上传</button>
        <div class="progress">
            <div class="progress-bar" id="progressBar"></div>
        </div>
        <p>进度:<span id="progressText">0%</span></p>
    </div>

    <script>
        const fileInput = document.getElementById('videoFile');
        const fileInfo = document.getElementById('fileInfo');
        const fileNameEl = document.getElementById('fileName');
        const fileSizeEl = document.getElementById('fileSize');
        const progressBar = document.getElementById('progressBar');
        const progressText = document.getElementById('progressText');

        let file = null;
        const chunkSize = 4 * 1024 * 1024; // 4MB分片(与后端一致)

        // 选择文件后显示信息
        fileInput.addEventListener('change', (e) => {
            file = e.target.files[0];
            if (!file) return;
            fileInfo.style.display = 'block';
            fileNameEl.textContent = file.name;
            fileSizeEl.textContent = formatFileSize(file.size);
        });

        // 格式化文件大小
        function formatFileSize(size) {
            if (size < 1024 * 1024) return (size / 1024).toFixed(2) + 'KB';
            if (size < 1024 * 1024 * 1024) return (size / (1024 * 1024)).toFixed(2) + 'MB';
            return (size / (1024 * 1024 * 1024)).toFixed(2) + 'GB';
        }

        // 开始分片上传
        async function startUpload() {
            if (!file) return alert('请选择视频文件');

            const totalSize = file.size;
            const totalChunks = Math.ceil(totalSize / chunkSize);
            const fileName = file.name;

            for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
                // 计算当前分片的起始和结束位置
                const start = chunkIndex * chunkSize;
                const end = Math.min(start + chunkSize, totalSize);
                const chunk = file.slice(start, end);

                // 构建表单数据
                const formData = new FormData();
                formData.append('file', chunk);
                formData.append('fileName', fileName);
                formData.append('chunkIndex', chunkIndex);
                formData.append('totalChunks', totalChunks);

                try {
                    // 上传分片
                    await uploadChunk(formData, chunkIndex, totalChunks);
                } catch (err) {
                    alert(`第${chunkIndex + 1}分片上传失败:${err.message}`);
                    return;
                }
            }

            alert('上传完成!');
        }

        // 上传单个分片
        function uploadChunk(formData, chunkIndex, totalChunks) {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest();
                xhr.open('POST', 'http://your-domain.com/video/api/upload-shard.php');

                // 监听上传进度
                xhr.upload.addEventListener('progress', (e) => {
                    if (e.lengthComputable) {
                        // 单个分片的上传进度
                        const chunkProgress = e.loaded / e.total;
                        // 总进度 = 已完成分片数/总分片数 + 当前分片进度/总分片数
                        const totalProgress = Math.floor((chunkIndex + chunkProgress) / totalChunks * 100);
                        progressBar.style.width = `${totalProgress}%`;
                        progressText.textContent = `${totalProgress}%`;
                    }
                });

                xhr.onload = () => {
                    const res = JSON.parse(xhr.responseText);
                    if (res.code === 200) resolve(res);
                    else reject(new Error(res.msg));
                };

                xhr.onerror = () => reject(new Error('网络错误'));
                xhr.send(formData);
            });
        }
    </script></body></html>

2. 视频播放组件(支持普通格式 + HLS)

使用 Video.js 增强播放体验,同时兼容原生 HTML5 Video:
html
预览
<!DOCTYPE html><html lang="zh-CN"><head>
    <meta charset="UTF-8">
    <title>视频播放</title>
    <!-- 引入Video.js样式和脚本 -->
    <link href="https://vjs.zencdn.net/8.6.1/video-js.css" rel="stylesheet">
    <script src="https://vjs.zencdn.net/8.6.1/video.min.js"></script>
    <!-- 引入HLS插件(支持.m3u8格式) -->
    <script src="https://cdn.jsdelivr.net/npm/videojs-contrib-hls@5.15.0/dist/videojs-contrib-hls.min.js"></script>
    <style>
        .video-container { width: 100%; max-width: 1200px; margin: 0 auto; }
    </style></head><body>
    <div class="video-container">
        <video
            id="videoPlayer"
            class="video-js vjs-big-play-centered vjs-fluid"
            controls
            preload="auto"
            poster="https://via.placeholder.com/1200x675?text=视频封面"
        ></video>
    </div>

    <script>
        // 初始化播放器
        const player = videojs('videoPlayer', {
            autoplay: false, // 禁止自动播放(浏览器政策限制)
            controls: true, // 显示控制栏
            responsive: true, // 响应式布局
            fluid: true, // 自适应容器大小
            playbackRates: [0.5, 1.0, 1.5, 2.0] // 倍速播放选项
        });

        // 播放地址(后端返回的地址,支持MP4或HLS)
        const videoType = 'hls'; // 可选:mp4/hls
        let videoUrl = '';

        if (videoType === 'mp4') {
            // 普通MP4格式(Nginx直接访问静态资源,更高效)
            videoUrl = 'http://your-domain.com/video/storage/xxx.mp4';
            player.src({ src: videoUrl, type: 'video/mp4' });
        } else if (videoType === 'hls') {
            // HLS格式(.m3u8索引文件)
            videoUrl = 'http://your-domain.com/video/storage/hls/xxx/index.m3u8';
            player.src({ src: videoUrl, type: 'application/x-mpegURL' });
        }

        // 错误处理
        player.on('error', (err) => {
            console.error('播放失败:', player.error().message);
            alert('视频播放失败,请检查地址是否有效');
        });
    </script></body></html>

四、生产环境优化与部署

1. 性能优化

  • 存储优化:放弃本地存储,使用阿里云 OSS / 腾讯云 COS,配置 CDN 加速(如阿里云 CDN、腾讯云 CDN),降低源站压力,提升跨地域访问速度。

  • 转码优化

    • 批量转码:用 FFmpeg 将视频转为多码率(720p、1080p)的 HLS 格式,支持弱网自动切换码率。

    • 异步转码:上传完成后通过消息队列(如 RabbitMQ)或定时任务处理转码,避免阻塞上传接口。

  • 缓存优化

    • 视频元信息(大小、格式、播放地址)缓存到 Redis,减少数据库查询。

    • Nginx 配置Cache-ControlExpires头,缓存视频分片和 HLS 索引文件。

  • 防盗链与限流

    • 防盗链:Nginx 配置valid_referers,限制仅指定域名可访问视频。

    • 限流:用 Nginx 的limit_req模块限制单 IP 请求频率,避免恶意刷流量。

2. 部署注意事项

  • PHP 配置调整

    • php.ini中设置upload_max_filesize = 1024Mpost_max_size = 1024Mmax_execution_time = 300(支持大文件上传)。

  • Nginx 配置调整

    • client_max_body_size 1024m(允许大文件上传)。

    • 开启gzip压缩(针对 HLS 索引文件,不压缩视频文件)。

  • 安全配置

    • 视频存储目录设置为非 Web 根目录,避免直接访问。

    • 播放接口添加 Token 验证,防止视频被非法盗链播放。

    • 定期备份视频文件(云存储自带备份功能,可开启)。

3. 常见问题排查

  • 视频无法播放

    • 检查视频格式(优先 MP4/HLS)和 MIME 类型是否正确。

    • 检查跨域配置(前端和后端是否都允许跨域)。

    • 检查 Nginx 是否支持 Range 请求(确保配置中没有禁用Accept-Ranges)。

  • 分片上传失败

    • 检查服务器磁盘空间是否充足。

    • 检查分片大小是否与后端一致。

    • 检查网络稳定性(大文件上传建议用 HTTPS)。

  • HLS 转码失败

    • 确认 FFmpeg 已安装且可正常执行。

    • 检查视频文件是否损坏,尝试重新上传。

    • 查看转码日志(可将 FFmpeg 输出重定向到日志文件)。

五、总结

本文实现了一套基于 PHP 的完整在线视频播放系统,涵盖大文件分片上传、断点续传播放、HLS 格式适配、权限控制等核心功能。后端通过 PHP 处理上传和权限逻辑,Nginx 优化静态资源传输;前端通过 Video.js 实现跨浏览器兼容的播放体验,兼顾了开发效率和用户体验。
生产环境中,建议结合云存储、CDN 加速、异步转码等方案,进一步提升系统的稳定性和性能。如果需要更复杂的功能(如视频加密、弹幕、点播计费),可基于现有架构扩展,或集成专业的视频云服务(如阿里云视频点播、腾讯云 VOD)。


分享给朋友:

“PHP 实现在线视频播放完整方案:从后端存储到前端适配” 的相关文章

Java 自定义鼠标样式完全指南:从基础到进阶实践

在 Java 图形界面(GUI)开发中,默认鼠标样式往往难以满足个性化界面设计需求。无论是打造炫酷的游戏界面、专业的桌面应用,还是贴合品牌风格的工具软件,自定义鼠标样式都能显著提升用户体验。本文将从基础原理出发,结合 Swing 与 AWT 技术,通过实例详解 Java 自定义鼠标样式的实现方法,覆...

Python 链接数据库与基础增删改查(CRUD)操作详解

在 Python 开发中,数据库交互是后端开发、数据分析、自动化脚本等场景的核心能力 —— 无论是存储用户数据、处理业务逻辑,还是批量分析数据,都需要 Python 与数据库建立连接并执行操作。本文以 MySQL 数据库(Python 生态最常用的关系型数据库)为例,从环境准备、数据库连接...

Java 链接数据库与基础增删改查操作详解

在 Java 开发中,数据库交互是绝大多数应用的核心功能之一。无论是用户信息存储、业务数据统计还是日志记录,都需要通过 Java 程序与数据库建立连接并执行数据操作。本文将以 MySQL 数据库(最常用的关系型数据库之一)为例,从环境准备、数据库连接、基础增删改查(CRUD)操作到代码优化...

Unity 开发实战:实现银行存取款功能系统

在许多游戏中,银行系统都是重要的经济组成部分,它能帮助玩家管理虚拟资产、实现安全存储。本文将详细介绍如何在 Unity 中设计并实现一个完整的银行存取款功能,包括数据结构设计、UI 交互逻辑和安全验证机制。一、银行系统核心需求分析一个基础的银行系统应包含以下核心功能:账户余额查询存款功能(将背包货币...

Unity 开发实战:实现逼真的作物生长系统

作物生长系统是农场类、生存类游戏的核心玩法之一,一个设计精良的作物生长系统能极大提升游戏的沉浸感。本文将详细介绍如何在 Unity 中构建一个完整的作物生长系统,包括生长周期、环境影响、交互逻辑和可视化表现。一、作物生长系统核心需求分析一个真实的作物生长系统应包含以下核心要素:多阶段生长周期(种子→...