资讯动态

云切片转码系统源码:PHP调度与易语言前端实战

发布时间:2026/10/14 4:15:22 来源:尧图企业网站定制
简介这是一套面向二次开发者的云切片转码系统完整源码前端采用易语言编写并附带模块后端为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:host127.0.0.1;dbnametranscode;charsetutf8mb4, 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 21, 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 21 ); } } // 每 2 秒轮询一次避免空转消耗 CPU sleep(2); }这段代码的逻辑核心是“先查活跃数再决定取多少任务”。MAX_CONCURRENT不是拍脑袋定的它取决于服务器的实际承载能力。一个 4 核 8G 的机器如果切片时长是 6 秒、目标分辨率 720pveryfast预设下单进程大约占用 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_duration6formatmp4” 返回结果 到文本 (网页_访问 (提交地址, 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; charsetutf-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:host127.0.0.1;dbnametranscode;charsetutf8mb4, 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_idxxx后端返回当前状态和已完成切片数。长连接需要易语言支持 WebSocket 或 SSE实现复杂度高但实时性好。对于切片转码这种任务进度更新频率不高每个切片完成才更新一次轮询完全够用。关键是轮询接口要轻量只查数据库状态字段不要每次去扫描文件系统。下面是一个状态查询接口的简化实现?php // api/task/status.php header(Content-Type: application/json; charsetutf-8); $taskId (int)($_GET[task_id] ?? 0); if ($taskId 0) { echo json_encode([code 1, msg 参数错误]); exit; } $db new PDO(mysql:host127.0.0.1;dbnametranscode;charsetutf8mb4, 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/的属主是 rootPHP 进程没有写权限任务会卡在“预处理中”状态日志里只有一行“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.mp4filelist.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:host127.0.0.1;dbnametranscode;charsetutf8mb4, 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 formatduration \ -of defaultnoprint_wrappers1:nokey1 $OUTPUT) # 获取视频流编码格式 VIDEO_CODEC$(ffprobe -v error -select_streams v:0 -show_entries streamcodec_name \ -of defaultnoprint_wrappers1:nokey1 $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 分钟左右的测试文件跑一遍全流程确认输出文件的时长、首帧、音画同步都正常再上生产环境。这套系统最怕的不是转码慢而是转码完了才发现文件有问题那时候源文件可能已经被清理了。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑