简介:这是一套面向二次开发者的云切片转码系统完整源码,前端采用易语言编写并附带模块,后端为PHP实现且代码全开源无加密,适合有一定易语言或PHP基础、希望搭建或改造自有视频转码与播放平台的开发者。压缩包共355个文件,约43.59MB,涵盖43个php后端逻辑、50个js与23个html页面、19个css及14个scss、14个less样式资源,另有51个m3u8切片示例、75个gif界面素材,以及sql建库脚本、bat启动脚本和docx说明文档,结构完整便于按模块查阅。前端使用EXUI插件打造全新UI,支持在线支付并对接支付宝当面付,具备TS图床加密播放能力,可同步苹果cms数据。目前已有780人学习下载,读者可据此获得一套可直接部署、便于二次开发的转码系统骨架,快速理解前后端交互、支付接入与加密播放的实现思路。
1. 云切片转码系统到底在切什么:从一次上传卡顿说起
某开发者把一段 40 分钟的课程录像丢进自建的点播后台,进度条走到 60% 就再也不动了。服务器 CPU 拉满,内存没爆,磁盘 IO 也不高,日志里只有一行模糊的 ffmpeg 超时。这不是编码参数写错,而是整个处理链路的设计问题——单进程串行转码,一个文件没切完,后面全堵着。云切片转码系统要解决的就是这件事:把一个大视频拆成若干小片段,分片并行处理,再拼回一条可播放的流。标题里的“云切片转码系统源码”指的是一套可自部署的完整实现,前端用易语言做管理界面,后端用 PHP 调度转码任务,附带部署教程。它适合两类人:一是想在自己的服务器上搭一套点播或直播回看系统的开发者,二是需要理解切片转码调度逻辑、准备二次开发的技术人员。这套方案的核心价值不在转码算法本身,而在于任务拆分、状态管理和失败重试的工程化处理。
2. 切片转码的调度模型:为什么 PHP 做队列反而合适
2.1 切片转码的三个阶段与状态机
一套能跑的切片转码系统,任务生命周期必须拆成三个明确阶段:预处理、切片转码、合并校验。预处理阶段做的是探测源文件信息、生成缩略图、确定切片时长和码率档位。切片转码阶段按预设的切片时长(常见 4 到 10 秒)把源文件切成 N 段,每段独立调用转码器输出目标格式。合并校验阶段把转码后的分片按顺序拼接,校验总时长和首帧可解码性。
这三个阶段之间不是简单的线性关系。切片转码阶段内部是并行的,但并行度受 CPU 核心数和内存带宽限制。如果无限制地并发提交转码任务,系统会进入一种“假死”状态:进程都在,但每个都跑不满,上下文切换开销吃掉大部分算力。所以调度层必须维护一个任务状态机,典型状态包括:待处理、预处理中、切片中、转码中、合并中、已完成、失败待重试。每个状态迁移都要落库,否则进程崩溃后无法恢复。
PHP 在这里的角色不是“高性能计算”,而是“状态协调”。它负责接收上传请求、写入任务记录、按并发上限派发子任务、轮询子任务状态、触发重试。真正吃 CPU 的转码动作交给外部进程(如 ffmpeg)执行,PHP 只做进程管理和结果收集。这种分工让 PHP 的短生命周期特性反而成了优势:每个请求处理完就释放,不会因为一个长任务把整个调度器拖死。
2.2 用 PHP 实现一个带并发上限的任务派发器
下面这段代码展示的是调度层最核心的部分:从待处理队列中取出任务,按当前活跃进程数决定是否派发,并记录子任务状态。代码不依赖任何框架,纯 PHP CLI 运行。
<?php // dispatch.php - 切片转码任务派发器 // 运行方式: php dispatch.php // 建议配合 supervisor 或 systemd 常驻 $db = new PDO('mysql:host=127.0.0.1;dbname=transcode;charset=utf8mb4', 'user', 'pass'); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 最大并发转码进程数,建议设为 CPU 核心数的 1.5 倍 // 超过这个值会导致 IO 等待和上下文切换开销剧增 define('MAX_CONCURRENT', 6); // 单个切片转码超时时间(秒),超时后标记失败并进入重试队列 define('SLICE_TIMEOUT', 300); function getActiveCount(PDO $db): int { // 统计状态为“转码中”且未超时的子任务数 $stmt = $db->prepare( "SELECT COUNT(*) FROM slice_tasks WHERE status = 'transcoding' AND updated_at > DATE_SUB(NOW(), INTERVAL ? SECOND)" ); $stmt->execute([SLICE_TIMEOUT]); return (int)$stmt->fetchColumn(); } function fetchPendingSlices(PDO $db, int $limit): array { // 取出待处理的切片任务,按创建时间排序保证顺序 $stmt = $db->prepare( "SELECT id, source_path, slice_index, start_time, duration, target_format FROM slice_tasks WHERE status = 'pending' ORDER BY created_at ASC LIMIT ?" ); $stmt->bindValue(1, $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(PDO::FETCH_ASSOC); } function markTranscoding(PDO $db, int $taskId): void { $stmt = $db->prepare( "UPDATE slice_tasks SET status = 'transcoding', updated_at = NOW() WHERE id = ?" ); $stmt->execute([$taskId]); } function buildFfmpegCommand(array $task): string { // 关键参数说明: // -ss 放在 -i 前面是快速定位,放在后面是精确但慢 // -c:v libx264 指定 H.264 编码,兼容性最好 // -preset veryfast 在速度和质量之间取平衡 // -crf 23 是默认质量,数值越小质量越高文件越大 // -force_key_frames 强制在切片边界插入关键帧,保证独立可解码 $cmd = sprintf( 'ffmpeg -ss %s -i %s -t %s -c:v libx264 -preset veryfast -crf 23 ' . '-c:a aac -b:a 128k -force_key_frames "expr:gte(t,n_forced*%s)" ' . '-f mp4 -movflags +faststart %s 2>&1', escapeshellarg($task['start_time']), escapeshellarg($task['source_path']), escapeshellarg($task['duration']), escapeshellarg($task['duration']), escapeshellarg('/data/slices/' . $task['id'] . '.mp4') ); return $cmd; } // 主循环 while (true) { $active = getActiveCount($db); $slots = MAX_CONCURRENT - $active; if ($slots > 0) { $pending = fetchPendingSlices($db, $slots); foreach ($pending as $task) { markTranscoding($db, $task['id']); $cmd = buildFfmpegCommand($task); // 后台执行,不阻塞主循环 exec($cmd . ' > /dev/null 2>&1 &'); } } // 每 2 秒轮询一次,避免空转消耗 CPU sleep(2); }这段代码的逻辑核心是“先查活跃数,再决定取多少任务”。MAX_CONCURRENT不是拍脑袋定的,它取决于服务器的实际承载能力。一个 4 核 8G 的机器,如果切片时长是 6 秒、目标分辨率 720p,veryfast预设下单进程大约占用 0.8 到 1.2 个核心,设 6 是合理的。如果设到 12,每个进程只能分到半个核心,总处理时间反而更长。
SLICE_TIMEOUT的作用是防止僵尸任务。ffmpeg 在某些损坏的源文件上会卡死,不报错也不退出。超时机制配合一个独立的清理脚本,把超时的任务状态改回pending并增加重试计数,超过 3 次才标记为永久失败。
2.3 切片时长与关键帧对齐的参数取舍
切片时长不是随便设的。设 2 秒,切片数量多,调度开销大,合并时文件句柄压力高;设 30 秒,单个切片转码时间长,并行度上不去,一个切片失败重试的代价也大。常见做法是 4 到 10 秒,点播场景偏 10 秒,直播回看偏 4 秒。
更关键的是关键帧对齐。如果切片边界没有强制关键帧,转码后的分片在拼接处会出现花屏或音画不同步。-force_key_frames参数的作用就是让 ffmpeg 在指定的时间点强制插入 I 帧。表达式expr:gte(t,n_forced*6)的意思是:每当时间 t 达到 6 秒的整数倍时,强制一个关键帧。这样每个切片的起始位置都是关键帧,独立解码没有问题。
注意:
-ss放在-i前面是快速定位,精度到秒级;放在-i后面是精确解码定位,精度到帧级但速度慢很多。切片转码场景下,快速定位足够用,因为关键帧对齐已经保证了边界完整性。
3. 易语言前端怎么和后端 PHP 对接:接口设计与状态同步
3.1 易语言侧的任务提交与轮询逻辑
易语言在前端承担的是管理界面角色:上传文件、填写转码参数、查看任务进度、下载结果。它和后端 PHP 之间通过 HTTP 接口通信,数据格式用 JSON。易语言本身处理 JSON 比较麻烦,常见做法是引入一个 JSON 解析模块,或者让后端返回简单的分隔符文本。
下面是一个易语言提交任务的伪代码结构,展示的是核心调用逻辑:
.版本 2 .子程序 提交转码任务 .局部变量 提交地址, 文本型 .局部变量 提交数据, 文本型 .局部变量 返回结果, 文本型 .局部变量 任务ID, 文本型 提交地址 = “http://127.0.0.1:8080/api/task/create” 提交数据 = “file_path=” + 编码_URL编码 (本地文件路径, 真) + “&slice_duration=6&format=mp4” 返回结果 = 到文本 (网页_访问 (提交地址, 1, 提交数据, , , , , , , , )) ' 返回结果格式为 {"code":0,"task_id":"abc123","msg":"ok"} ' 易语言解析 JSON 需要额外模块,这里用文本分割简化处理 任务ID = 文本_取出中间文本 (返回结果, “task_id”:””, “””) .如果真 (任务ID ≠ “”) 编辑框_日志.加入文本 (“任务已提交,ID:” + 任务ID) .如果真结束这段代码的关键在于编码_URL编码这一步。文件路径里如果有中文或空格,不编码会导致后端接收到的路径不完整。另一个容易翻车的地方是超时设置:易语言的网页_访问默认超时时间较短,上传大文件或等待后端响应时容易断连,需要在调用前设置更长的超时值。
3.2 PHP 接口层的参数校验与防重复提交
后端接口不能信任前端传来的任何参数。文件路径要做白名单校验,只允许特定目录下的文件;切片时长要做范围限制,比如 2 到 30 秒;格式只接受 mp4 和 ts。防重复提交靠的是文件哈希:同一个文件在短时间内重复提交,直接返回已有任务 ID。
<?php // api/task/create.php - 任务创建接口 header('Content-Type: application/json; charset=utf-8'); $filePath = $_POST['file_path'] ?? ''; $sliceDuration = (int)($_POST['slice_duration'] ?? 6); $format = $_POST['format'] ?? 'mp4'; // 参数校验:切片时长限制在 2 到 30 秒之间 if ($sliceDuration < 2 || $sliceDuration > 30) { echo json_encode(['code' => 1, 'msg' => '切片时长必须在 2 到 30 秒之间']); exit; } // 格式白名单 if (!in_array($format, ['mp4', 'ts'])) { echo json_encode(['code' => 1, 'msg' => '不支持的输出格式']); exit; } // 路径校验:只允许 /data/uploads/ 目录下的文件 $realPath = realpath($filePath); $allowedBase = realpath('/data/uploads/'); if ($realPath === false || strpos($realPath, $allowedBase) !== 0) { echo json_encode(['code' => 1, 'msg' => '文件路径不合法']); exit; } // 防重复提交:用文件哈希作为去重键 $fileHash = md5_file($realPath); $db = new PDO('mysql:host=127.0.0.1;dbname=transcode;charset=utf8mb4', 'user', 'pass'); $stmt = $db->prepare( "SELECT id FROM tasks WHERE file_hash = ? AND status NOT IN ('failed', 'cancelled') LIMIT 1" ); $stmt->execute([$fileHash]); $existing = $stmt->fetchColumn(); if ($existing) { echo json_encode(['code' => 0, 'task_id' => $existing, 'msg' => '任务已存在']); exit; } // 写入任务记录 $stmt = $db->prepare( "INSERT INTO tasks (file_path, file_hash, slice_duration, target_format, status, created_at) VALUES (?, ?, ?, ?, 'pending', NOW())" ); $stmt->execute([$realPath, $fileHash, $sliceDuration, $format]); $taskId = $db->lastInsertId(); echo json_encode(['code' => 0, 'task_id' => $taskId, 'msg' => 'ok']);md5_file在大文件上会消耗一定时间,40 分钟的视频大约需要 1 到 2 秒。如果上传频率很高,这个开销可以接受,因为它避免的是重复转码带来的数倍资源浪费。realpath加前缀匹配是防目录穿越的基本手段,比单纯过滤../更可靠。
3.3 进度回传:轮询还是长连接
易语言前端获取进度有两种方式:定时轮询和长连接。轮询实现简单,每隔 2 到 3 秒请求一次/api/task/status?task_id=xxx,后端返回当前状态和已完成切片数。长连接需要易语言支持 WebSocket 或 SSE,实现复杂度高,但实时性好。
对于切片转码这种任务,进度更新频率不高(每个切片完成才更新一次),轮询完全够用。关键是轮询接口要轻量,只查数据库状态字段,不要每次去扫描文件系统。下面是一个状态查询接口的简化实现:
<?php // api/task/status.php header('Content-Type: application/json; charset=utf-8'); $taskId = (int)($_GET['task_id'] ?? 0); if ($taskId <= 0) { echo json_encode(['code' => 1, 'msg' => '参数错误']); exit; } $db = new PDO('mysql:host=127.0.0.1;dbname=transcode;charset=utf8mb4', 'user', 'pass'); // 主任务状态 $stmt = $db->prepare("SELECT status, total_slices, created_at FROM tasks WHERE id = ?"); $stmt->execute([$taskId]); $task = $stmt->fetch(PDO::FETCH_ASSOC); if (!$task) { echo json_encode(['code' => 1, 'msg' => '任务不存在']); exit; } // 已完成切片数 $stmt = $db->prepare( "SELECT COUNT(*) FROM slice_tasks WHERE task_id = ? AND status = 'done'" ); $stmt->execute([$taskId]); $doneSlices = (int)$stmt->fetchColumn(); echo json_encode([ 'code' => 0, 'status' => $task['status'], 'total' => (int)$task['total_slices'], 'done' => $doneSlices, 'percent' => $task['total_slices'] > 0 ? round($doneSlices / $task['total_slices'] * 100, 1) : 0 ]);这个接口每次请求只做两次简单查询,单次响应时间在 5 毫秒以内。即使前端每秒轮询一次,对数据库的压力也可以忽略。真正需要注意的是total_slices的写入时机:必须在预处理阶段就确定切片总数并写入主任务表,否则进度百分比无法计算。
4. 部署与排错:从零跑通一套切片转码系统的六个坑
4.1 环境依赖与目录权限的隐形陷阱
这套系统的运行依赖三个外部组件:PHP 7.4 以上(需要 PDO 和 exec 函数)、ffmpeg 4.0 以上、MySQL 5.7 或 8.0。PHP 的exec函数在很多默认配置里是被禁用的,需要检查php.ini中的disable_functions项,把exec、shell_exec、proc_open从禁用列表中移除。
目录权限是第一个高频翻车点。PHP 进程通常以www-data或nginx用户运行,而 ffmpeg 需要读取源文件、写入切片目录。如果/data/uploads/和/data/slices/的属主是 root,PHP 进程没有写权限,任务会卡在“预处理中”状态,日志里只有一行“Permission denied”。解决方法是把这两个目录的属组设为 PHP 运行用户所在的组,并赋予 775 权限。
# 创建目录并设置权限 mkdir -p /data/uploads /data/slices /data/output chown -R www-data:www-data /data/uploads /data/slices /data/output chmod -R 775 /data/uploads /data/slices /data/output # 验证 ffmpeg 是否可用 which ffmpeg ffmpeg -version | head -1 # 检查 PHP 禁用函数 php -i | grep disable_functions4.2 切片转码失败的四种典型现象与排查路径
第一种现象:任务状态一直是“转码中”,但 CPU 占用为 0。原因是 ffmpeg 进程已经退出,但 PHP 没有捕获到退出状态。排查方法是查看/data/slices/目录下是否有对应的输出文件,如果没有,手动执行一遍buildFfmpegCommand生成的命令,看报错信息。常见报错是“Unknown encoder libx264”,说明 ffmpeg 编译时没有启用 H.264 支持,需要重装带--enable-libx264的版本。
第二种现象:切片转码完成,但合并后的视频在切片边界处花屏。原因是关键帧没有对齐,某个切片的起始位置不是 I 帧。排查方法是单独播放每个切片文件,看第一个画面是否正常。解决方法是检查-force_key_frames表达式中的切片时长是否和实际切片时长一致。
第三种现象:任务在“合并中”卡住,输出文件大小为 0。原因是合并脚本使用的文件列表顺序不对,或者某个切片文件缺失。排查方法是检查合并前的切片文件数量是否等于total_slices,以及文件命名是否按序号排列。合并命令常见写法是ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp4,filelist.txt里每行格式为file '/data/slices/123_0.mp4'。
第四种现象:前端显示进度 100%,但下载链接 404。原因是输出文件写到了临时目录,没有移动到最终下载目录。排查方法是检查合并脚本的最后一步是否有mv或rename操作,以及目标目录是否有写权限。
4.3 并发数设错导致的“假死”与恢复
并发数设得过高,系统会进入一种“假死”状态:所有进程都在运行,但每个都跑得很慢,新任务提交后长时间没有响应。这时候top命令看到的负载很高,但实际吞吐量很低。恢复方法是先把MAX_CONCURRENT临时改为 1,让积压的任务慢慢消化,同时观察单个任务的处理时间,再逐步调高。
一个实用的经验公式是:MAX_CONCURRENT = CPU 核心数 × 1.5,但上限不超过 8。如果切片时长小于 4 秒,并发数还要再降,因为短切片的任务切换开销更大。另外,MySQL 的连接数也要同步调整,每个 PHP 调度进程会占用一个数据库连接,并发数 8 意味着至少 8 个常驻连接,加上前端轮询的连接,max_connections建议设为 50 以上。
注意:调整并发数后,不要立即重启调度器。先让当前活跃任务跑完,再重启,否则正在转码的切片会变成僵尸任务,需要手动清理状态。
5. 进阶技巧:用文件锁和重试队列把失败率压到 1% 以下
5.1 用 flock 防止调度器重复派发
调度器如果被意外启动了两个实例,同一个切片任务会被派发两次,两个 ffmpeg 进程写同一个输出文件,结果不可预测。解决办法是在调度器主循环入口加一个文件锁,确保同一时间只有一个调度器实例在运行。
<?php // 在 dispatch.php 主循环之前加入 $lockFile = fopen('/tmp/transcode_dispatch.lock', 'c'); if (!flock($lockFile, LOCK_EX | LOCK_NB)) { echo "另一个调度器实例正在运行,退出。\n"; exit(1); } // 主循环结束后释放锁(实际常驻进程不会走到这里) // flock($lockFile, LOCK_UN); // fclose($lockFile);LOCK_NB是非阻塞模式,获取不到锁立即返回失败,不会挂起等待。/tmp目录在重启后会清空,锁文件自动消失,不需要额外清理。
5.2 重试队列的退避策略与最大重试次数
失败任务不能立即重试,否则遇到损坏的源文件会陷入无限重试循环。合理的做法是记录重试次数,每次重试间隔按指数退避:第一次 30 秒,第二次 2 分钟,第三次 10 分钟,超过三次标记为永久失败并通知前端。
<?php // retry.php - 失败任务重试脚本,建议每分钟执行一次 $db = new PDO('mysql:host=127.0.0.1;dbname=transcode;charset=utf8mb4', 'user', 'pass'); // 取出失败且重试次数小于 3 的任务 $stmt = $db->prepare( "SELECT id, retry_count, last_retry_at FROM slice_tasks WHERE status = 'failed' AND retry_count < 3" ); $stmt->execute(); $failedTasks = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($failedTasks as $task) { $retryCount = (int)$task['retry_count']; // 退避间隔:30秒、120秒、600秒 $backoff = [30, 120, 600][$retryCount] ?? 600; $lastRetry = strtotime($task['last_retry_at'] ?? '1970-01-01'); if (time() - $lastRetry < $backoff) { continue; // 还没到重试时间 } $stmt = $db->prepare( "UPDATE slice_tasks SET status = 'pending', retry_count = retry_count + 1, last_retry_at = NOW() WHERE id = ?" ); $stmt->execute([$task['id']]); }这个脚本配合 crontab 每分钟跑一次,逻辑简单但有效。retry_count字段在任务创建时默认为 0,每次重试加 1。超过 3 次的任务不再自动重试,需要人工介入检查源文件是否损坏。
5.3 合并阶段的校验:用 ffprobe 确认输出完整性
合并完成后不能直接标记任务成功,必须用ffprobe校验输出文件的时长和流信息。如果输出时长和源文件时长偏差超过 2 秒,或者视频流不存在,说明合并有问题,需要回退到切片阶段重新处理。
#!/bin/bash # verify.sh - 校验合并后的输出文件 OUTPUT="$1" SOURCE_DURATION="$2" # 获取输出文件时长(秒) OUTPUT_DURATION=$(ffprobe -v error -show_entries format=duration \ -of default=noprint_wrappers=1:nokey=1 "$OUTPUT") # 获取视频流编码格式 VIDEO_CODEC=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name \ -of default=noprint_wrappers=1:nokey=1 "$OUTPUT") # 时长偏差超过 2 秒视为失败 DIFF=$(echo "$OUTPUT_DURATION $SOURCE_DURATION" | awk '{print $1 - $2}' | tr -d '-') if (( $(echo "$DIFF > 2" | bc -l) )); then echo "FAIL: duration mismatch, diff=${DIFF}s" exit 1 fi if [ -z "$VIDEO_CODEC" ]; then echo "FAIL: no video stream found" exit 1 fi echo "OK: duration=${OUTPUT_DURATION}s, codec=${VIDEO_CODEC}" exit 0这个脚本在合并完成后由 PHP 调用,返回 0 表示校验通过,返回 1 表示需要重试。ffprobe的-v error参数抑制了无关输出,只保留错误信息,方便脚本解析。
我自己的习惯是:每次调整切片时长或并发数之后,先拿一个 10 分钟左右的测试文件跑一遍全流程,确认输出文件的时长、首帧、音画同步都正常,再上生产环境。这套系统最怕的不是转码慢,而是转码完了才发现文件有问题,那时候源文件可能已经被清理了。希望帮到你。
本文还有配套的精品资源,点击获取