laravel python3 ffmpeg 实现视频转换工具demo实例 | laravel china 社区-大发黄金版app下载
laravel python3 实现视频转换工具
准备步骤
- 采取同步方式作为demo实现 如果需要大文件转换,可以放到队列中异步处理
- 调整nginx和php服务器的最大处理时间,否则同步方式的处理 如果传入大文件,处理到最后会因为nginx或者php造成中断
- 服务器安装ffmpeg 和python3,及python依赖库
实现功能
- 对视频的 编码方式、帧率、i帧间隔、b帧控制、码率、分辨率、及时间戳水印和自定义文字水印进行控制转码
- 对原视频和处理后的视频的信息展示
- 处理后的视频下载及保存
思路
编写python代码,调动ffmpeg进行视频相关控制并输出结果至指定文件,
blade编写前端,控制调整参数,laravel接收参数并调用python代码,将结果进行输出
代码参考
路由文件省略
前端blade
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<title>视频转换工具</title>
<meta name="csrf-token" content="{{ csrf_token() }}">
<style>
body {
font-family: 'segoe ui', tahoma, geneva, verdana, sans-serif;
background-color: #f4f6f9;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
.container {
background-color: #ffffff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 1000px;
max-width: 90%;
box-sizing: border-box;
}
h1 {
text-align: center;
margin-bottom: 25px;
color: #333333;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #555555;
font-weight: bold;
}
input[type="text"],
input[type="number"],
select,
input[type="file"],
input[type="color"] {
width: 100%;
padding: 10px;
border: 1px solid #cccccc;
border-radius: 4px;
box-sizing: border-box;
transition: border-color 0.3s;
}
input[type="text"]:focus,
input[type="number"]:focus,
select:focus,
input[type="file"]:focus,
input[type="color"]:focus {
border-color: #007bff;
outline: none;
}
.checkbox-group {
display: flex;
align-items: center;
}
.checkbox-group input {
margin-right: 10px;
}
button {
width: 100%;
padding: 12px;
background-color: #007bff;
color: #ffffff;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
#getinfobutton {
background-color: #28a745;
margin-top: 10px;
}
button:disabled {
background-color: #a0c8f0;
cursor: not-allowed;
}
button:hover:not(:disabled) {
background-color: #0056b3;
}
#getinfobutton:hover:not(:disabled) {
background-color: #218838;
}
#result, #videoinforesult {
margin-top: 20px;
text-align: center;
}
#videoinforesult {
text-align: left;
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
border: 1px solid #ddd;
}
#result a {
color: #007bff;
text-decoration: none;
font-weight: bold;
}
#result a:hover {
text-decoration: underline;
}
#error {
color: red;
font-weight: bold;
}
/* loading spinner styles */
.spinner-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
display: none; /* hidden by default */
}
.spinner {
border: 8px solid #f3f3f3;
border-top: 8px solid #007bff;
border-radius: 50%;
width: 60px;
height: 60px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
width: 95%;
padding: 20px;
}
button {
font-size: 14px;
padding: 10px;
}
#resultscontainer {
flex-direction: column;
}
}
/* 新增日志和 new_info 展示区域样式 */
#resultscontainer {
display: flex;
gap: 20px;
margin-top: 20px;
display: none; /* hidden by default */
}
#logresult {
flex: 1;
background-color: #f1f1f1;
padding: 15px;
border-radius: 5px;
border: 1px solid #ddd;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap; /* 保持日志格式 */
font-family: monospace;
color: #333333;
}
#newinforesult {
flex: 1;
background-color: #f9f9f9;
padding: 15px;
border-radius: 5px;
border: 1px solid #ddd;
max-height: 300px;
overflow-y: auto;
font-family: arial, sans-serif;
color: #333333;
}
/* 增加水印颜色选择器的尺寸 */
#watermark_color {
width: 60px; /* 调整宽度,使其更明显 */
height: 40px; /* 增加高度以便更好地显示颜色 */
padding: 0;
border: none;
cursor: pointer;
}
/* 自定义水印文字输入框样式 */
#customwatermarkinput {
margin-top: 10px;
}
</style>
</head>
<body>
@include('layouts.login')
@include('console')
@include('dock')
<div class="container">
<h1>视频转换工具</h1>
<label>1. h265与h264编码互相转换,建议文件在3分钟内可以使用此功能转换(预计需要5分钟才能转换完成)</label>
<label>2. 支持大小在5gb以内的文件</label>
<label>3. h264最大支持4096x2304,h265最大支持7680x4320,h.264和h.265编码器要求分辨率的宽度和高度除以2必须为偶数。</label>
<label>4. 为保证服务器性能,超过15分钟的转换均会被服务器中止,请确保需要转换的文件不会过大。</label>
<label>5. 视频复杂度低时会低于您设置的码率,您设置的码率可能无法完全达到预期标准,系统会针对您的视频自动匹配最接近您设定的码率。</label>
<form id="convertform">
@csrf
<div class="form-group">
<label for="file">选择视频文件:</label>
<input type="file" id="file" name="file" accept="video/*" required>
</div>
<div class="form-group">
<label for="rr">分辨率(宽 x 高):</label>
<input type="text" id="rr" name="rr" placeholder="例如:960x540" value="960x540" required>
</div>
<div class="form-group">
<label for="code_style">编码格式:</label>
<select id="code_style" name="code_style">
<option value="h264">h.264</option>
<option value="h265">h.265</option>
<!-- 可以根据需要添加更多编码格式 -->
</select>
</div>
<div class="form-group">
<label for="i_frame">关键帧间隔(i-frame interval):</label>
<input type="number" id="i_frame" name="i_frame" min="1" max="250" value="25" required>
</div>
<div class="form-group">
<label for="fps">帧率(fps):</label>
<input type="number" id="fps" name="fps" min="1" max="120" value="30" required>
</div>
<div class="form-group">
<label for="bitrate">码率(kbps):</label>
<input type="number" id="bitrate" name="bitrate" min="10" max="20000" value="1000" required>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="clear_b_frame" name="clear_b_frame" checked>
<label for="clear_b_frame">清除 b 帧</label>
</div>
<!-- 新增的水印选择框 -->
<div class="form-group">
<label for="watermark">水印:</label>
<select id="watermark" name="watermark" required>
<option value="0">不添加</option>
<option value="1">添加时间戳水印</option>
<option value="2">添加自定义文字水印</option>
</select>
</div>
<!-- 新增的水印颜色选择框(默认隐藏) -->
<div class="form-group" id="watermarkcolorgroup" style="display: none;">
<label for="watermark_color">水印颜色:</label>
<input type="color" id="watermark_color" name="watermark_color" value="#ffffff">
</div>
<!-- 新增的自定义水印文字输入框(默认隐藏) -->
<div class="form-group" id="customwatermarkinput" style="display: none;">
<label for="custom_watermark">自定义水印文字:</label>
<input type="text" id="custom_watermark" name="watermark_text" placeholder="例如:confidential" value="">
</div>
<!-- 结束 -->
<button type="submit" id="submitbutton">转换视频</button>
<button type="button" id="getinfobutton">获取视频信息</button>
</form>
<div id="result"></div>
<div id="resultscontainer">
<div id="logresult"></div>
<div id="newinforesult"></div>
</div>
<div id="videoinforesult"></div>
</div>
<!-- loading spinner -->
<div class="spinner-overlay" id="spinneroverlay">
<div class="spinner"></div>
</div>
<script src="{{ asset('gettoken.js') }}"></script>
<script>
document.addeventlistener('domcontentloaded', function() {
const form = document.getelementbyid('convertform');
const submitbutton = document.getelementbyid('submitbutton');
const getinfobutton = document.getelementbyid('getinfobutton');
const spinneroverlay = document.getelementbyid('spinneroverlay');
const logresultdiv = document.getelementbyid('logresult'); // 获取日志展示区域
const newinforesultdiv = document.getelementbyid('newinforesult'); // 获取 new_info 展示区域
const resultscontainer = document.getelementbyid('resultscontainer'); // 获取结果容器
let issubmitting = false;
// disable the submit button initially
submitbutton.disabled = true;
// store original values
let originalvalues = {};
let fileselected = false; // track whether a file has been selected
// 获取水印相关的表单元素
const watermarkselect = document.getelementbyid('watermark');
const watermarkcolorgroup = document.getelementbyid('watermarkcolorgroup');
const customwatermarkinput = document.getelementbyid('customwatermarkinput');
const customwatermarkfield = document.getelementbyid('custom_watermark');
// 处理水印选择变化
watermarkselect.addeventlistener('change', function() {
const selected = this.value;
if (selected === '1') {
// 添加时间戳水印,只显示颜色选择器
watermarkcolorgroup.style.display = 'block';
customwatermarkinput.style.display = 'none';
} else if (selected === '2') {
// 添加自定义文字水印,显示颜色选择器和文字输入框
watermarkcolorgroup.style.display = 'block';
customwatermarkinput.style.display = 'block';
} else {
// 不添加水印,隐藏颜色选择器和文字输入框
watermarkcolorgroup.style.display = 'none';
customwatermarkinput.style.display = 'none';
}
checkforparameterchanges();
});
// 处理视频转换表单提交
form.addeventlistener('submit', async function(event) {
event.preventdefault();
if (issubmitting) return;
issubmitting = true;
spinneroverlay.style.display = 'flex';
resultscontainer.style.display = 'none'; // 隐藏结果区域
const formdata = new formdata(form);
// check if the clear_b_frame checkbox is checked
const clearbframecheckbox = document.getelementbyid('clear_b_frame');
formdata.set('clear_b_frame', clearbframecheckbox.checked ? '1' : '0');
// 处理水印字段
const watermarktype = watermarkselect.value;
if (watermarktype === '2') {
const customwatermark = customwatermarkfield.value.trim();
if (customwatermark === '') {
alert('请填写自定义水印文字。');
spinneroverlay.style.display = 'none';
issubmitting = false;
return;
}
formdata.set('watermark_text', customwatermark);
} else {
formdata.delete('watermark_text'); // 不传递 watermark_text
}
submitbutton.disabled = true; // disable submit button during submission
const formelements = form.elements;
for (let i = 0; i < formelements.length; i) {
formelements[i].disabled = true; // disable all form elements
}
try {
const response = await fetch('/api/general/convert-video', {
method: 'post',
body: formdata
});
if (!response.ok) {
throw new error(`服务器返回状态码 ${response.status}`);
}
const result = await response.json();
const resultdiv = document.getelementbyid('result');
resultdiv.innerhtml = ''; // clear previous results
if (result.status === true) {
if (result.path) {
resultdiv.innerhtml = `<p>转换成功:<a href="${result.path}" download>点击下载视频</a></p>`;
} else {
resultdiv.innerhtml = `<p id="error">未知响应格式</p>`;
}
} else {
resultdiv.innerhtml = `<p id="error">错误:${result.error || '未知错误'}</p>`;
}
// 显示日志和 new_info 内容
if (result.log || result.new_info) {
resultscontainer.style.display = 'flex';
// 显示日志内容
if (result.log) {
logresultdiv.innertext = result.log;
} else {
logresultdiv.innertext = '无日志信息';
}
// 显示 new_info 内容
if (result.new_info) {
const info = result.new_info;
// 格式化 new_info 显示
newinforesultdiv.innerhtml = `
<h3>转换后视频信息</h3>
<p><strong>帧率 (framerate):</strong> ${info.framerate || '未知'}</p>
<p><strong>编码格式 (codec):</strong> ${info.codec || '未知'}</p>
<p><strong>分辨率 (resolution):</strong> ${info.resolution || '未知'}</p>
<p><strong>时长 (time):</strong> ${info.time ? formattime(info.time) : '未知'}</p>
<p><strong>关键帧间隔 (keyframe interval):</strong> ${info.keyframe_interval || '未知'}</p>
<p><strong>包含 b 帧 (contains b-frames):</strong> ${info.contains_b_frames ? '是' : '否'}</p>
<p><strong>码率 (bitrate):</strong> ${info.bitrate || '未知'}</p>
`;
} else {
newinforesultdiv.innertext = '无转换后视频信息';
}
}
} catch (error) {
console.error('error:', error);
const resultdiv = document.getelementbyid('result');
resultdiv.innerhtml = `<p id="error">请求失败,请稍后再试。</p>`;
} finally {
spinneroverlay.style.display = 'none';
// re-enable form elements
for (let i = 0; i < formelements.length; i) {
formelements[i].disabled = false; // enable all form elements
}
issubmitting = false;
}
});
// 处理获取视频信息按钮点击
getinfobutton.addeventlistener('click', async function() {
const fileinput = document.getelementbyid('file');
const file = fileinput.files[0];
if (!file) {
alert('请先选择一个视频文件');
return;
}
const formdata = new formdata();
formdata.append('file', file);
spinneroverlay.style.display = 'flex';
getinfobutton.disabled = true;
try {
const response = await fetch('/api/general/video-info', {
method: 'post',
body: formdata,
headers: {
'x-csrf-token': document.queryselector('meta[name="csrf-token"]').getattribute('content')
}
});
if (!response.ok) {
throw new error(`服务器返回状态码 ${response.status}`);
}
const info = await response.json();
const infodiv = document.getelementbyid('videoinforesult');
if (info && !info.error) {
// store original values for comparison
originalvalues = {
resolution: info.resolution || '960x540', // default value if not available
framerate: info.framerate || 30, // default value if not available
bitrate: info.bitrate ? info.bitrate.replace(' kb/s', '') : 1000, // remove ' kb/s' and set default
keyframe_interval: info.keyframe_interval || 25, // default value if not available
codec: info.codec === 'h265' ? 'h265' : 'h264', // set based on codec
watermark: '0', // default watermark
watermark_color: '#ffffff', // default color
watermark_text: '' // default text (unused)
};
// populate fields with fetched video info
document.getelementbyid('rr').value = originalvalues.resolution;
document.getelementbyid('fps').value = originalvalues.framerate;
document.getelementbyid('bitrate').value = originalvalues.bitrate;
document.getelementbyid('i_frame').value = originalvalues.keyframe_interval;
document.getelementbyid('code_style').value = originalvalues.codec;
// reset watermark selection and hide related fields
watermarkselect.value = '0';
watermarkcolorgroup.style.display = 'none';
customwatermarkinput.style.display = 'none';
// clear custom watermark input
customwatermarkfield.value = '';
// mark that a file has been selected and information has been retrieved
fileselected = true;
// enable submit button if watermark is not used or if parameters have changed
checkforparameterchanges();
infodiv.innerhtml = `
<h3>原视频信息</h3>
<p><strong>帧率 (framerate):</strong> ${info.framerate || '未知'}</p>
<p><strong>编码格式 (codec):</strong> ${info.codec || '未知'}</p>
<p><strong>分辨率 (resolution):</strong> ${info.resolution || '未知'}</p>
<p><strong>时长 (time):</strong> ${info.time ? formattime(info.time) : '未知'}</p>
<p><strong>关键帧间隔 (keyframe interval):</strong> ${info.keyframe_interval || '未知'}</p>
<p><strong>包含 b 帧 (contains b-frames):</strong> ${info.contains_b_frames ? '是' : '否'}</p>
<p><strong>码率 (bitrate):</strong> ${info.bitrate || '未知'}</p>
`;
} else {
infodiv.innerhtml = `<p id="error">无法获取视频信息:${info.error || '未知错误'}</p>`;
}
} catch (error) {
console.error('error:', error);
const infodiv = document.getelementbyid('videoinforesult');
infodiv.innerhtml = `<p id="error">请求失败,请稍后再试。</p>`;
} finally {
spinneroverlay.style.display = 'none';
getinfobutton.disabled = false; // 允许用户再次点击获取信息
}
});
// 检查参数变化的函数
function checkforparameterchanges() {
if (!fileselected || !originalvalues || object.keys(originalvalues).length === 0) {
submitbutton.disabled = true;
return;
}
const currentresolution = document.getelementbyid('rr').value;
const currentfps = document.getelementbyid('fps').value;
const currentbitrate = document.getelementbyid('bitrate').value;
const currentkeyframeinterval = document.getelementbyid('i_frame').value;
const currentcodec = document.getelementbyid('code_style').value;
const watermarktype = watermarkselect.value;
const currentwatermarkcolor = document.getelementbyid('watermark_color').value;
const currentcustomwatermark = document.getelementbyid('custom_watermark').value.trim();
// 检查当前值是否与原始值匹配
let isunchanged = (
currentresolution === originalvalues.resolution &&
currentfps == originalvalues.framerate &&
currentbitrate == originalvalues.bitrate &&
currentkeyframeinterval == originalvalues.keyframe_interval &&
currentcodec === originalvalues.codec &&
watermarktype === '0'
);
// 如果水印不是默认值 '0',则认为参数已更改
let iswatermarkchanged = false;
if (watermarktype === '1') {
// 添加时间戳水印,检查颜色是否变化
iswatermarkchanged = currentwatermarkcolor !== originalvalues.watermark_color;
} else if (watermarktype === '2') {
// 添加自定义文字水印,检查颜色和文字是否变化
iswatermarkchanged = (
currentwatermarkcolor !== originalvalues.watermark_color ||
currentcustomwatermark !== originalvalues.watermark_text
);
}
// enable submit button if any parameter has changed or watermark is added/modified
submitbutton.disabled = isunchanged && !iswatermarkchanged;
}
// 在输入框变化时实时检查参数
document.queryselectorall('input, select').foreach(element => {
element.addeventlistener('input', checkforparameterchanges);
element.addeventlistener('change', checkforparameterchanges);
});
// 格式化时间(秒)为时:分:秒
function formattime(seconds) {
const hrs = math.floor(seconds / 3600);
const mins = math.floor((seconds % 3600) / 60);
const secs = math.floor(seconds % 60);
return `${hrs}:${mins < 10 ? '0' : ''}${mins}:${secs < 10 ? '0' : ''}${secs}`;
}
// 页面刷新提醒
window.addeventlistener('beforeunload', function (e) {
if (issubmitting) {
e.preventdefault();
e.returnvalue = '';
}
});
});
</script>
</body>
</html>
后端控制器代码
namespace app\http\controllers\api;
use app\http\controllers\controller;
use app\services\apiservice;
use app\services\autotestservice;
use app\services\videoservice;
use illuminate\http\request;
use illuminate\support\facades\storage;
class videocontroller extends controller
{
protected $videoservice;
public function __construct(videoservice $videoservice)
{
$this->videoservice = $videoservice;
}
public function showffmpegpage()
{
return view('ffmpeg');
}
public function videoinfo(request $request)
{
// 定义存储路径和文件名(可根据需要自定义)
$storagepath = 'public/user-video';
$file = $request->file('file');
$filename = time() . '_' . $file->getclientoriginalname();
// 将文件存储到指定路径
$path = $file->storeas($storagepath, $filename);
// 获取文件的完整路径
$fullpath = storage::path($path);
$apiservice = new apiservice();
return $apiservice->getfpsandcodestyle($fullpath);
}
public function convert(request $request)
{
// 获取上传的文件
$file = $request->file('file');
// 定义存储路径和文件名(可根据需要自定义)
$storagepath = 'public/user-video';
// $filename = str_replace(' ', '', time() . '_' . $file->getclientoriginalname());
$filename = str_replace(' ', '', time() . $file->getclientoriginalname());
// 将文件存储到指定路径
$path = $file->storeas($storagepath, $filename);
// 获取文件的完整路径
$fullpath = storage::path($path);
// 获取其他参数
$rr = $request->input('rr', '960x540');
$codestyle = $request->input('code_style', 'h264');
$iframe = $request->input('i_frame', 25);
$clearbframe = $request->input('clear_b_frame', false);
$fps = $request->input('fps');
$bitrate = $request->input('bitrate');
$watermark = $request->input('watermark',0);
$watermarktext = $request->input('watermark_text');
$watermarkcolor = $request->input('watermark_color');
$server = new autotestservice();
$watermarkcontent=0;
if($watermark==1){
$watermarkcontent=1;
}
if($watermark==2){
$watermarkcontent=$watermarktext;
}
// 传递文件的完整路径到服务
return $server->convertedvideo($fullpath, $rr, $codestyle, $iframe, $clearbframe,$fps,$bitrate,$watermarkcontent,$watermarkcolor);
}
}
调用服务代码
public function convertedvideo($videopath, $rr, $codestyle, $iframe, $clearbframe, $fps, $bitrate,$watermark,$watermarkcolor)
{
// 取消 php 脚本的执行时间限制,以允许最长 20 分钟的等待
set_time_limit(0);
$apiservice=new apiservice();
// 定义保存路径
$savepath = storage::path('public/user-converted-video');
// 确保保存路径存在
if (!is_dir($savepath)) {
if (!mkdir($savepath, 0755, true)) {
return [
'status' => false,
'error' => '无法创建保存目录',
'log' => '',
'new_info' => [],
];
}
}
// 生成日志文件路径,基于视频文件名
$videofilebasename = pathinfo($videopath, pathinfo_filename); // 例如从 '/a/c/ads.mp4' 获取 'ads'
$logfilepath = $savepath . '/' . $videofilebasename . '.log'; // 例如 '/a/b/ads.log'
// 构建后台执行的命令,并将输出重定向到日志文件
$cmd = 'nohup python3 /var/www/autotest/platform/storage/app/public/converted_video.py '
. $videopath . ' '
. $savepath . ' '
. $rr . ' '
. $codestyle . ' '
. $iframe . ' '
. $clearbframe . ' '
. $fps . ' '
. $bitrate . " '"
. $watermark . "' '"
. $watermarkcolor .
"' > " . $logfilepath . ' 2>&1 &';
// ssh 连接设置
$host = 'workspace'; // workspace 容器的主机名或 ip
$username = 'root'; // ssh 用户名
$key = storage::get('insecure_id_rsa'); // 获取 ssh 私钥内容
$rsa = publickeyloader::load($key); // 加载私钥
// 建立 ssh 连接
$ssh = new ssh2($host, 22);
if (!$ssh->login($username, $rsa)) {
// 登录失败处理
return [
'status' => false,
'error' => 'ssh 登录失败',
'log' => '',
];
}
// 执行后台命令
$ssh->exec($cmd);
// 断开 ssh 连接
$ssh->disconnect();
// 开始轮询日志文件
$maxattempts = 180; // 15 分钟 / 5 秒
$attempt = 0;
$sleepseconds = 5;
$result = [
'status' => false,
'error' => '视频转换超时(超过20分钟)',
'log' => '',
];
while ($attempt < $maxattempts) {
// 每 5 秒等待
sleep($sleepseconds);
$attempt;
// 检查日志文件是否存在
if (!file_exists($logfilepath)) {
// 日志文件尚未创建,继续等待
continue;
}
// 读取日志文件内容
$logcontent = file_get_contents($logfilepath);
if ($logcontent === false) {
// 读取日志文件失败,继续等待
continue;
}
// 按行分割日志内容
$loglines = explode("\n", $logcontent);
// 获取最后一个非空行
$lastline = '';
for ($i = count($loglines) - 1; $i >= 0; $i--) {
$line = trim($loglines[$i]);
if ($line !== '') {
$lastline = $line;
break;
}
}
// 检查最后一行是否为 'true' 或 'false'
if ($lastline === 'true') {
// 转换成功,检查转换后的视频文件是否存在
$convertedfilename = basename($videopath); // 假设转换后文件名与原文件名相同
$convertedfilepath = $savepath . '/' . $convertedfilename;
if (file_exists($convertedfilepath)) {
// 获取外网可访问的 url
$convertedvideourl = storage::url('public/user-converted-video/' . $convertedfilename);
$result = [
'status' => true,
'path' => $convertedvideourl,
'log' => $logcontent,
'new_info' => $apiservice->getfpsandcodestyle(storage::path('public/user-converted-video/' . $convertedfilename)),
];
break;
} else {
// 日志中标记为成功,但未找到转换后的视频文件
$result = [
'status' => false,
'error' => '视频转换成功,但未找到生成的文件',
'log' => $logcontent,
'new_info' =>[],
];
break;
}
} elseif ($lastline === 'false') {
// 转换失败
$result = [
'status' => false,
'error' => '视频转换失败',
'log' => $logcontent,
'new_info' =>[],
];
break;
}
// 如果日志中未包含 'true' 或 'false',继续等待
}
// 返回最终结果
return $result;
}
public function getfpsandcodestyle($filepath)
{
if (!file_exists($filepath)) {
return[
'framerate' => 0,
'codec' => 'unknown',
'resolution' => 'unknown',
'time' => 0,
'keyframe_interval' => 'unknown', // 关键帧间隔
'contains_b_frames' => false, // 是否包含b帧
'bitrate' => 'unknown' // 码率
];
}
$ffmpegoutput = shell_exec('ffmpeg -i "' . $filepath . '" 2>&1');
$info = [
'framerate' => 0,
'codec' => 'unknown',
'resolution' => 'unknown',
'time' => 0,
'keyframe_interval' => 'unknown', // 关键帧间隔
'contains_b_frames' => false, // 是否包含b帧
'bitrate' => 'unknown' // 码率
];
// 匹配帧率
if (preg_match('/, (\d (\.\d )?) fps,/', $ffmpegoutput, $matches)) {
$info['framerate'] = (float)$matches[1];
}
// 匹配编码方式
if (preg_match('/video: (h264|hevc|h265|vp8|vp9|av1|mpeg2video|mpeg4|wmv|prores|[a-za-z0-9] )/', $ffmpegoutput, $matches)) {
$codec = $matches[1];
if ($codec === 'hevc') {
$codec = 'h265';
}
$info['codec'] = $codec;
}
// 匹配分辨率
if (preg_match('/, (\d{2,5}x\d{2,5})[, ]/', $ffmpegoutput, $matches)) {
$info['resolution'] = $matches[1];
}
// 匹配时长
if (preg_match('/duration: ((\d ):(\d ):(\d \.\d ))/s', $ffmpegoutput, $matches)) {
$hours = (int)$matches[2];
$minutes = (int)$matches[3];
$seconds = (float)$matches[4];
$info['time'] = round($hours * 3600 $minutes * 60 $seconds, 0);
}
// 匹配码率
if (preg_match('/bitrate: (\d kb\/s)/', $ffmpegoutput, $matches)) {
$info['bitrate'] = $matches[1];
}
// 使用 ffprobe 获取帧信息,包括帧类型
$ffprobeoutput = shell_exec('ffprobe -v error -read_intervals 0% 15 -select_streams v:0 -show_frames -show_entries frame=pict_type -print_format json "' . $filepath . '" 2>&1');
$ffprobedata = json_decode($ffprobeoutput, true);
$lastkeyframeindex = null;
$containsbframe = false;
if(!isset($ffprobedata['frames'])){
return $info;
}
// 遍历每一帧的信息,检查是否有b帧以及计算关键帧间隔
foreach ($ffprobedata['frames'] as $index => $frame) {
// 检查是否有b帧
if ($frame['pict_type'] === 'b') {
$containsbframe = true;
}
// 计算关键帧间隔(i帧之间的帧数)
if ($frame['pict_type'] === 'i') {
if ($lastkeyframeindex !== null) {
$info['keyframe_interval'] = $index - $lastkeyframeindex;
}
$lastkeyframeindex = $index;
}
}
// 设置是否包含b帧的信息
$info['contains_b_frames'] = $containsbframe;
return $info;
}
python处理脚本
import os
import subprocess
import sys
import json
def get_video_info(video_path):
"""使用 ffprobe 获取视频的基本信息,包括分辨率、帧率和 b 帧"""
cmd = [
'ffprobe', '-v', 'error', '-select_streams', 'v', '-show_entries',
'stream=width,height,r_frame_rate,has_b_frames,bit_rate', '-of', 'json', video_path
]
result = subprocess.run(cmd, stdout=subprocess.pipe, stderr=subprocess.pipe)
try:
stream_info = json.loads(result.stdout.decode('utf-8'))['streams'][0]
except (indexerror, json.jsondecodeerror):
print("无法获取视频信息。请确保输入文件是有效的视频文件。")
sys.exit(1)
# 返回流信息并确保返回值存在
return {
'width': stream_info.get('width'),
'height': stream_info.get('height'),
'r_frame_rate': stream_info.get('r_frame_rate'),
'has_b_frames': stream_info.get('has_b_frames'),
'bit_rate': stream_info.get('bit_rate', none) # 如果没有比特率信息,设置为 none
}
def convert_video(input_file, output_folder, resolution, codec, gop_size, remove_bframes, frame_rate, bit_rate, watermark, watermark_color):
# 设置输出文件夹
os.makedirs(output_folder, exist_ok=true)
# 输出文件路径
output_file = os.path.join(output_folder, os.path.basename(input_file))
# 检查输出文件是否已经存在
if os.path.exists(output_file):
print(f"文件 {output_file} 已存在,正在删除...")
os.remove(output_file)
print(f"已删除 {output_file}")
# 获取原始视频信息
video_info = get_video_info(input_file)
original_resolution = f"{video_info['width']}x{video_info['height']}"
try:
original_framerate = eval(video_info['r_frame_rate']) # 将帧率转换为浮点数
except (typeerror, syntaxerror):
original_framerate = 30.0 # 默认帧率
has_b_frames = int(video_info['has_b_frames']) # 0 表示没有 b 帧,>0 表示有 b 帧
print(f"原始分辨率: {original_resolution}, 原始帧率: {original_framerate}fps, 是否有b帧: {'有' if has_b_frames else '无'}")
# 获取原始码率,如果不存在则设置为 none
original_bitrate = int(video_info['bit_rate']) // 1000 if video_info['bit_rate'] else none
# 初始化 ffmpeg 命令
ffmpeg_cmd = ['ffmpeg', '-i', input_file, '-y']
# 构建视频过滤器列表
filters = []
# 处理分辨率
if resolution.lower() != original_resolution.lower():
print(f"分辨率将从 {original_resolution} 转换为 {resolution}")
# 添加 scale 过滤器,替换 'x' 为 ':'
filters.append(f'scale={resolution.replace("x", ":")}')
else:
print("分辨率与原始视频相同,跳过分辨率处理。")
# 处理水印
if watermark != '0':
if watermark == '1':
print("添加时间戳水印")
text = '%{pts\\:hms}'
else:
print(f"添加自定义文字水印: {watermark}")
text = watermark.replace("'", "\\'") # 转义单引号
# 使用 pts 或自定义文字作为水印文本
# 指定颜色
drawtext_filter = f"drawtext=fontsize=24:fontcolor={watermark_color}@0.8:x=10:y=10:text='{text}'"
filters.append(drawtext_filter)
else:
print("不添加水印")
# 如果有任何过滤器,添加到 ffmpeg 命令中
if filters:
filter_chain = ",".join(filters)
ffmpeg_cmd = ['-vf', filter_chain]
# 处理编码格式
if codec == 'h265':
print(f"转换为 h.265 编码")
ffmpeg_cmd = ['-c:v', 'libx265']
elif codec == 'h264':
print(f"转换为 h.264 编码")
ffmpeg_cmd = ['-c:v', 'libx264']
else:
print(f"未知的编码格式: {codec},跳过转换")
return
# 处理 i 帧间隔
ffmpeg_cmd = ['-g', str(gop_size)]
# 处理 b 帧移除
if remove_bframes == '1' and has_b_frames > 0:
print("移除 b 帧")
ffmpeg_cmd = ['-bf', '0']
elif has_b_frames == 0:
print("原始视频没有 b 帧,跳过 b 帧处理。")
else:
print("保留 b 帧")
ffmpeg_cmd = ['-bf', '2']
# 设置帧率
if frame_rate:
if float(frame_rate) != original_framerate:
print(f"设置帧率为 {frame_rate} fps")
ffmpeg_cmd = ['-r', str(frame_rate)]
else:
print("帧率与原始视频相同,跳过帧率处理。")
# 设置码率
if bit_rate:
if original_bitrate is not none and int(bit_rate) != original_bitrate: # 仅在存在原始码率时比较
print(f"设置码率为 {bit_rate} kbps")
ffmpeg_cmd = ['-b:v', f'{bit_rate}k']
elif original_bitrate is none:
print("原始视频没有比特率信息,设置码率为默认值")
ffmpeg_cmd = ['-b:v', f'{bit_rate}k']
else:
print("码率与原始视频相同,跳过码率处理。")
# 输出路径
ffmpeg_cmd = [output_file]
# 打印最终的 ffmpeg 命令(可选,便于调试)
print("执行的 ffmpeg 命令:", ' '.join(ffmpeg_cmd))
# 执行 ffmpeg 命令,输出信息
try:
process = subprocess.run(ffmpeg_cmd, stdout=subprocess.pipe, stderr=subprocess.pipe, text=true)
if process.returncode != 0:
print("ffmpeg 转换过程中出错:")
print(process.stderr)
print('false')
sys.exit(1)
else:
print(f"{os.path.basename(input_file)} 转换完成!")
print('true')
except exception as e:
print(f"发生异常: {e}")
print('false')
sys.exit(1)
if __name__ == "__main__":
try:
if len(sys.argv) != 11:
print("使用方法: python3 convert_video.py 视频文件路径 保存路径 分辨率 编码格式 i帧间隔 是否去掉b帧 帧率 码率 添加水印(0: 不使用, 1: 添加时间戳水印, 其他: 添加自定义文字) 水印颜色")
sys.exit(1)
video_path = sys.argv[1]
output_folder = sys.argv[2]
resolution = sys.argv[3]
codec = sys.argv[4]
gop_size = sys.argv[5]
remove_bframes = sys.argv[6]
frame_rate = sys.argv[7]
bit_rate = sys.argv[8]
watermark = sys.argv[9]
watermark_color = sys.argv[10]
# 检查水印参数
if watermark == '0':
pass # 不添加水印
elif watermark == '1':
pass # 添加时间戳水印
else:
if not watermark.strip():
print("添加自定义文字水印时,水印文字不能为空。")
sys.exit(1)
# 检查视频文件是否存在
if not os.path.isfile(video_path):
print(f"文件 {video_path} 不存在!")
sys.exit(1)
# 执行视频转换
convert_video(
video_path,
output_folder,
resolution,
codec,
gop_size,
remove_bframes,
frame_rate,
bit_rate,
watermark,
watermark_color
)
except exception as e:
print('false')
sys.exit(1)
展示效果
本作品采用《cc 协议》,转载必须注明作者和本文链接
推荐文章: