前言
现如今,大部分带内容的网站或应用都有视频区了,不说是大厂平台,就连个人开发者也相继在自己网站或小程序上迭代出视频板块。那既然有了视频模块,除个性化推荐,智能审核等这种费钱又耗时的功能外(个人开发者暂缓)。最基本的视频上传,视频播放自然必不可少吧。
既然要强调省钱,我当前不会对接点播服务了。毕竟为了有一定的审核和推荐功能,我打算做人工审核。那剩下的关于播放有一定的体验度,还得要用一下OSS了(还是要花一点嘛)。因为上传有现成的分片上传,播放有HLS流,以下着重讲关于视频播放的优化,上传部分就说一下思路哦。
视频上传
因为网站关于上传只有一些图片,所以只用了OSS的常规文件上传。但是视频,不说长视频,现在一些稍微几十秒的短视频动则几十兆上百兆,更别说高清的。而且早期的上传是放在服务端完成,所以当客户端提交大型文件时,不光nginx(413 Request Entity Too Large)和fpm会报错,到了OSS客户端调用也会报错(Allowed memory size of 268435456 bytes exhausted (tried to allocate 67108896 bytes))。
开始是尝试在服务端改成分片上传,但是测试时发现,每次提交文件过来时都要先将视频本地化,存在服务器上后再分片提交到OSS,回调成功删除服务器文件,并且要调整nginx等的提交的最大值限制。最后就决定把上传部分放到了前端,服务器只提供OSS的临时访问凭证和接收上传成功回调。
视频播放
关于视频播放,网站早期的做法是将OSS的视频地址直接丢到前端的video标签中,当在手机播放时会出现卡顿或播放缓慢等问题。最后决定使用OSS的HLS的构建,就是在后台将视频推流一份在OSS的LiveChannel中,前端通过读取m3u8播放视频。
思路
- 前端上传视频,后台审核成功调用OSS的LiveChannel创建。
- 根据通道创建返回的推流地址和播放地址,存入数据库。
- 在服务端起一个进程用FFMPEG进行视频推流。
- 前端播放视频时选择m3u8的方式。
示例代码
1. 前端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>普通上传模板,需要修改成分片上传</title>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<style>
#content {
border: 1px solid saddlebrown;
padding: 16px;
border-radius: 2px
}
.list {
top: 15px;
width: 140px;
height: 40px;
border: 1px solid #0082E6;
display: inline-block;
border-radius: 2px;
position: relative;
line-height: 40px;
}
#file {
position: absolute;
opacity: 0;
color: white;
width: 100%;
height: 100%;
z-index: 100;
}
.list span {
display: inline-block;
text-align: center;
width: 100%;
line-height: 40px;
position: absolute;
color: #0082E6;
}
video {
margin-top: 8px;
border-radius: 4px;
}
._p {
margin: 14px;
}
._p input {
display: inline-block;
width: 70%;
margin-left: 6px;
}
._p span {
font-size: 15px;
}
</style>
</head>
<body>
<div id="content">
<p class="_p"><span>视频标题</span>:
<input id="title" type="text" class="form-control" placeholder="请输入视频名称">
</p>
<p class="_p">
<span>选择视频: </span>
<a class="list" href="javascript:;">
<input id="file" type="file" name="myfile" onchange="UpladFile();"/>
<span>选择视频</span>
</a>
<!--上传速度显示-->
<span id="time"></span>
</p>
<!--显示消失-->
<ul class="el-upload-list el-upload-list--text" style="display: none;">
<li tabindex="0" class="el-upload-list__item is-success">
<a class="el-upload-list__item-name">
<i class="el-icon-document"></i>
<span id="videoName">food.jpeg</span>
</a>
<label class="el-upload-list__item-status-label">
<i class="el-icon-upload-success el-icon-circle-check"></i>
</label>
<i class="el-icon-close" onclick="del();"></i>
<i class="el-icon-close-tip"></i>
</li>
</ul>
<!--进度条-->
<div class="el-progress el-progress--line" style="display: none;">
<div class="el-progress-bar">
<div class="el-progress-bar__outer" style="height: 6px;">
<div class="el-progress-bar__inner" style="width: 0%;">
</div>
</div>
</div>
<div class="el-progress__text" style="font-size: 14.4px;">0%</div>
</div>
<p class="_p"><span>上传视频</span>:
<button class="btn btn-primary" type="button" onclick="sub();">上传</button>
</p>
<!--预览框-->
<div class="preview">
</div>
</div>
<script>
var xhr;//异步请求对象
var ot; //时间
var oloaded;//大小
//上传文件方法
function UpladFile() {
var fileObj = document.getElementById("file").files[0]; // js 获取文件对象
if () {
$(".el-upload-list").css("display", "block");
$(".el-upload-list li").css("border", "1px solid #20a0ff");
$("#videoName").text();
} else {
alert("请选择文件");
}
}
/*点击取消*/
function del() {
$("#file").val('');
$(".el-upload-list").css("display", "none");
}
/*点击提交*/
function sub() {
var fileObj = document.getElementById("file").files[0]; // js 获取文件对象
if (fileObj == undefined || fileObj == "") {
alert("请选择文件");
return false;
}
var title = $.trim($("#title").val());
if (title == '') {
alert("请填写视频标题");
return false;
}
var url = "/uploadFile"; // 接收上传文件的后台地址
var form = new FormData(); // FormData 对象
form.append("mf", fileObj); // 文件对象
form.append("title", title); // 标题
xhr = new XMLHttpRequest(); // XMLHttpRequest 对象
xhr.open("post", url, true); // post方式,url为服务器请求地址,true 该参数规定请求是否异步处理。
xhr.onload = uploadComplete; // 请求完成
xhr.onerror = uploadFailed; // 请求失败
xhr.upload.onprogress = progressFunction; //【上传进度调用方法实现】
xhr.upload.onloadstart = function () { //上传开始执行方法
ot = new Date().getTime(); //设置上传开始时间
oloaded = 0; //设置上传开始时,以上传的文件大小为0
};
xhr.send(form); //开始上传,发送form数据
}
//上传进度实现方法,上传过程中会频繁调用该方法
function progressFunction(evt) {
// event.total是需要传输的总字节,event.loaded是已经传输的字节。如果event.lengthComputable不为真,则event.total等于0
if (evt.lengthComputable) {
$(".el-progress--line").css("display", "block");
/*进度条显示进度*/
$(".el-progress-bar__inner").css("width", Math.round(evt.loaded / evt.total * 100) + "%");
$(".el-progress__text").html(Math.round(evt.loaded / evt.total * 100) + "%");
}
var time = document.getElementById("time");
var nt = new Date().getTime(); //获取当前时间
var pertime = (nt - ot) / 1000; //计算出上次调用该方法时到现在的时间差,单位为s
ot = new Date().getTime(); //重新赋值时间,用于下次计算
var perload = evt.loaded - oloaded; //计算该分段上传的文件大小,单位b
oloaded = evt.loaded; //重新赋值已上传文件大小,用以下次计算
//上传速度计算
var speed = perload / pertime; //单位b/s
var bspeed = speed;
var units = 'b/s'; //单位名称
if (speed / 1024 > 1) {
speed = speed / 1024;
units = 'k/s';
}
if (speed / 1024 > 1) {
speed = speed / 1024;
units = 'M/s';
}
speed = speed.toFixed(1);
//剩余时间
var resttime = ((evt.total - evt.loaded) / bspeed).toFixed(1);
time.innerHTML = '上传速度:' + speed + units + ',剩余时间:' + resttime + 's';
if (bspeed == 0)
time.innerHTML = '上传已取消';
}
//上传成功响应
function uploadComplete(evt) {
//服务断接收完文件返回的结果 注意返回的字符串要去掉双引号
if (evt.target.responseText) {
var responseData = JSON.parse(evt.target.responseText);
var mediaUrl = responseData.data.url;
alert("上传成功!");
$(".preview").append("<video controls='' autoplay='' name='media'><source src=" + mediaUrl + " type='video/mp4'></video>");
} else {
alert("上传失败");
}
}
//上传失败
function uploadFailed(evt) {
alert("上传失败!");
}
</script>
</body>
</html>
2. 后端API层
<?php
namespace app\index\controller;
use think\Db;
use think\Console;
use think\Controller;
class OssTest extends Controller
{
// 获取临时访问凭证
public function getTempPlicy()
{
$oss = \oss\OssFactory::factory("AliOss");
$result = $oss->getRamPolicy();
return $result;
}
// 简单上传接口
public function uploadFile()
{
$files = request()->file();
$input['title'] = input('post.title');
$uploadFiled = $files['mf'];
// 获取文件信息
$fileInfo = $uploadFiled->getInfo();
$oss = \oss\OssFactory::factory("AliOss");
$result = $oss->upload($uploadFiled, "");
//$width = getimagesize($imgUrl)[0];
//$height = getimagesize($imgUrl)[1];
return $result;
}
// 分片上传接口
public function multipartUpload()
{
$files = request()->file();
$input['title'] = input('post.title');
$uploadFiled = $files['mf'];
$oss = \oss\OssFactory::factory("AliOss");
$result = $oss->multipartUpload();
return $result;
}
// 分段Push
public function multionPush()
{
global $_W;
global $_GPC;
$fileArr = $_FILES['mf'];
$title = $_GPC['title'];
// 设置预览目录,上传成功的路径
$previewPath = "../shiping/";
$ext = pathinfo($fileArr['name'], PATHINFO_EXTENSION);//获取当前上传文件扩展名
$arrExt = array('3gp','rmvb','flv','wmv','avi','mkv','mp4','mp3','wav',);
if(!in_array($ext,$arrExt)) {
exit(json_encode(-1,JSON_UNESCAPED_UNICODE)); //视/音频或采用了不合适的扩展名!
} else {
//文件上传到预览目录
$previewName = 'pre_'.md5(mt_rand(1000,9999)).time().'.'.$ext; //文件重命名
$previewSrc = $previewPath.$previewName;
if(move_uploaded_file($fileArr['tmp_name'],$previewSrc)){ // 上传文件操作,上传失败的操作
exit($previewName);
} else {
//上传成功的失败的操作
exit(json_encode(0,JSON_UNESCAPED_UNICODE));
}
}
}// 创建推流通道
public function putLiveChannel()
{
$oss = \oss\OssFactory::factory("AliOss");
$result = $oss->putLiveChannel('iphone8');
return $this->jsonData(200, 'ok', $result);
}
// 获取推流加签地址
public function getSignRtmpUrl()
{
$oss = \oss\OssFactory::factory("AliOss");
$params = array('params' => array('playlistName' => 'newest.m3u8'));
$result = $oss->getSignRtmpUrl($params);
return $this->jsonData(200, 'ok', $result);
}
// 获取推流上传通道历史记录
public function getLiveChannelHistory()
{
$oss = \oss\OssFactory::factory("AliOss");
//$params = array('params' => array('playlistName' => 'newest.m3u8'));
$result = $oss->getLiveChannelHistory();
return $this->jsonData(200, 'ok', $result);
}
// 查询推流上传信息
public function getLiveChannelInfo()
{
$oss = \oss\OssFactory::factory("AliOss");
$result = $oss->getLiveChannelInfo();
return $this->jsonData(200, 'ok', $result);
}
// 查询m3u8的推流上传状态
public function getLiveChannelStatus()
{
$oss = \oss\OssFactory::factory("AliOss");
$result = $oss->getLiveChannelStatus();
return $this->jsonData(200, 'ok', $result);
}
}
3. OSS操作层
<?php
namespace oss;
require_once 'lib/aliyun-oss/autoload.php';
use OSS\OssClient;
use OSS\Model\LiveChannelConfig;
use OSS\Core\OssException;
use OSS\Core\OssUtil;
use Mimey\MimeTypes;
use AlibabaCloud\SDK\Sts\V20150401\Sts;
use Darabonba\OpenApi\Models\Config;
use AlibabaCloud\SDK\Sts\V20150401\Models\AssumeRoleRequest;
use AlibabaCloud\Tea\Utils\Utils\RuntimeOptions;
class AliOss extends OssBase
{
public $config = [];
public function __construct()
{
$this->config = include 'config/ali_oss_config.php';
}
// 获取临时访问凭证
// composer require alibabacloud/sts-20150401
public function getRamPolicy()
{
$config = $this->config;
try {
// 填写步骤1创建的RAM用户AccessKey。
$config = new Config([
"accessKeyId" => $config['AccessKeyID'],
"accessKeySecret" => $config['AccessKeySecret']
]);
$config->endpoint = "sts.cn-shenzhen.aliyuncs.com";
$client = new Sts($config);
$assumeRoleRequest = new AssumeRoleRequest([
// roleArn填写步骤2获取的角色ARN,例如acs:ram::175708322470****:role/ramtest。
"roleArn" => "acs:ram::175708322470****:role/ramtest",
// roleSessionName用于自定义角色会话名称,用来区分不同的令牌,例如填写为sessiontest。
"roleSessionName" => "sessiontest",
// durationSeconds用于设置临时访问凭证有效时间单位为秒,最小值为900,最大值以当前角色设定的最大会话时间为准。本示例指定有效时间为3000秒。
"durationSeconds" => 3000,
// policy填写自定义权限策略,用于进一步限制STS临时访问凭证的权限。如果不指定Policy,则返回的STS临时访问凭证默认拥有指定角色的所有权限。
// 临时访问凭证最后获得的权限是步骤4设置的角色权限和该Policy设置权限的交集。
//"policy" => ""
]);
$runtime = new RuntimeOptions([]);
$result = $client->assumeRoleWithOptions($assumeRoleRequest, $runtime);
printf("AccessKeyId:" . $result->body->credentials->accessKeyId . PHP_EOL);
printf("AccessKeySecret:" . $result->body->credentials->accessKeySecret . PHP_EOL);
printf("Expiration:" . $result->body->credentials->expiration . PHP_EOL);
printf("SecurityToken:" . $result->body->credentials->securityToken . PHP_EOL);
} catch (Exception $e) {
printf($e->getMessage() . PHP_EOL);
}
return $result;
}
public function upload($file, $dir = "")
{
$config = $this->config;
$fileInfo = $file->getInfo();
// 获取文件扩展名
//$extName = pathinfo($fileInfo['name'], PATHINFO_EXTENSION);
if ($dir === "") {
$dir = "tmp/";
}
$fileName = $dir . $this->getFileName($fileInfo);
$res['code'] = 1;
try {
$ossClient = new OssClient($config['AccessKeyID'], $config['AccessKeySecret'], $config['EndPoint']);
$result = $ossClient->uploadFile($config['Bucket'], $fileName, $fileInfo['tmp_name']);
$res['data']['url'] = $result['info']['url'];
$res['data']['path'] = $fileName;
} catch (OssException $e) {
$res['code'] = 0;
$res['message'] = $e->getMessage();
}
return $res;
}
// 单文件流上传
public function objectUpload($content, $file_name = '')
{
$config = $this->config;
$res['code'] = 1;
$res['message'] = '';
try {
$ossClient = new OssClient($config['AccessKeyID'], $config['AccessKeySecret'], $config['EndPoint']);
$result = $ossClient->putObject($config['Bucket'], $file_name, $content);
$res['data']['url'] = $result['info']['url'];
$res['data']['path'] = explode("/", $file_name)[1];
} catch (OssException $e) {
$res['code'] = 0;
$res['message'] = $e->getMessage();
}
return $res;
}
// 分段上传
public function multipartUpload()
{
$config = $this->config;
$ossClient = new OssClient($config['AccessKeyID'], $config['AccessKeySecret'], $config['EndPoint']);
// 填写Bucket名称,例如examplebucket。
$bucket = $config['Bucket'];
//填写不包含Bucket名称在内的Object完整路径,例如exampledir/exampleobject.txt。
$object = $config['Bucket'] . 'lv_0_20230528160220.mp4';
// 填写本地文件的完整路径。
$uploadFile = 'D:\\lv_0_20230528160220.mp4';
$initOptions = array(
OssClient::OSS_HEADERS => array(
// 指定该Object被下载时的网页缓存行为。
// 'Cache-Control' => 'no-cache',
// 指定该Object被下载时的名称。
// 'Content-Disposition' => 'attachment;filename=oss_download.jpg',
// 指定该Object被下载时的内容编码格式。
// 'Content-Encoding' => 'utf-8',
// 指定过期时间,单位为毫秒。
// 'Expires' => 150,
// 指定初始化分片上传时是否覆盖同名Object。此处设置为true,表示禁止覆盖同名Object。
//'x-oss-forbid-overwrite' => 'true',
// 指定上传该Object的每个part时使用的服务器端加密方式。
// 'x-oss-server-side-encryption'=> 'KMS',
// 指定Object的加密算法。
// 'x-oss-server-side-data-encryption'=>'SM4',
// 指定KMS托管的用户主密钥。
//'x-oss-server-side-encryption-key-id' => '9468da86-3509-4f8d-a61e-6eab1eac****',
// 指定Object的存储类型。
// 'x-oss-storage-class' => 'Standard',
// 指定Object的对象标签,可同时设置多个标签。
// 'x-oss-tagging' => 'TagA=A&TagB=B',
),
);
try {
//返回uploadId。uploadId是分片上传事件的唯一标识,您可以根据uploadId发起相关的操作,如取消分片上传、查询分片上传等。
$uploadId = $ossClient->initiateMultipartUpload($bucket, $object, $initOptions);
print("initiateMultipartUpload OK" . "\n");
} catch (OssException $e) {
printf($e->getMessage() . "\n");
return;
}
$partSize = 10 * 1024 * 1024;
$uploadFileSize = sprintf('%u', filesize($uploadFile));
$pieces = $ossClient->generateMultiuploadParts($uploadFileSize, $partSize);
$responseUploadPart = array();
$uploadPosition = 0;
$isCheckMd5 = true;
foreach ($pieces as $i => $piece) {
$fromPos = $uploadPosition + (integer)$piece[$ossClient::OSS_SEEK_TO];
$toPos = (integer)$piece[$ossClient::OSS_LENGTH] + $fromPos - 1;
$upOptions = array(
// 上传文件。
$ossClient::OSS_FILE_UPLOAD => $uploadFile,
// 设置分片号。
$ossClient::OSS_PART_NUM => ($i + 1),
// 指定分片上传起始位置。
$ossClient::OSS_SEEK_TO => $fromPos,
// 指定文件长度。
$ossClient::OSS_LENGTH => $toPos - $fromPos + 1,
// 是否开启MD5校验,true为开启。
$ossClient::OSS_CHECK_MD5 => $isCheckMd5,
);
// 开启MD5校验。
if ($isCheckMd5) {
$contentMd5 = OssUtil::getMd5SumForFile($uploadFile, $fromPos, $toPos);
$upOptions[$ossClient::OSS_CONTENT_MD5] = $contentMd5;
}
try {
// 上传分片。
$responseUploadPart[] = $ossClient->uploadPart($bucket, $object, $uploadId, $upOptions);
printf("initiateMultipartUpload, uploadPart - part#{$i} OK\n");
} catch (OssException $e) {
printf("initiateMultipartUpload, uploadPart - part#{$i} FAILED\n");
printf($e->getMessage() . "\n");
return;
}
}
// $uploadParts是由每个分片的ETag和分片号(PartNumber)组成的数组。
$uploadParts = array();
foreach ($responseUploadPart as $i => $eTag) {
$uploadParts[] = array(
'PartNumber' => ($i + 1),
'ETag' => $eTag,
);
}
$comOptions['headers'] = array(
// 指定完成分片上传时是否覆盖同名Object。此处设置为true,表示禁止覆盖同名Object。
// 'x-oss-forbid-overwrite' => 'true',
// 如果指定了x-oss-complete-all:yes,则OSS会列举当前uploadId已上传的所有Part,然后按照PartNumber的序号排序并执行CompleteMultipartUpload操作。
// 'x-oss-complete-all'=> 'yes'
);
$res = [];
try {
// 执行completeMultipartUpload操作时,需要提供所有有效的$uploadParts。OSS收到提交的$uploadParts后,会逐一验证每个分片的有效性。当所有的数据分片验证通过后,OSS将把这些分片组合成一个完整的文件。
$res = $ossClient->completeMultipartUpload($bucket, $object, $uploadId, $uploadParts, $comOptions);
printf("Complete Multipart Upload OK\n");
} catch (OssException $e) {
printf("Complete Multipart Upload FAILED\n");
printf($e->getMessage() . "\n");
return;
}
return $res;
}
// 列举文件
public function listBuckets($bucket = "hhbusiness", $prefix = "", $pagesize = 20, $maker = "")
{
$config = $this->config;
$res['code'] = 1;
$res['message'] = '';
$res['data'] = "";
try {
$ossClient = new OssClient($config['AccessKeyID'], $config['AccessKeySecret'], $config['EndPoint']);
$delimiter = '/';
$nextMarker = "";
//$maxkeys = 1;
$options = array(
'delimiter' => $delimiter,
'prefix' => $prefix,
'max-keys' => $pagesize,
'marker' => $maker,
);
$result['obj'] = $ossClient->listObjects($bucket, $options);
$result['next_marker'] = $result['obj']->getNextMarker();
// 没有值
if ($result['obj']->getIsTruncated() !== "true") {
}
$res['code'] = 1;
$res['data'] = $result;
} catch (OssException $e) {
$res['code'] = 0;
$res['message'] = $e->getMessage();
}
return $res;
}
public function putLiveChannel($liveChannelName)
{
$ossClientConfig = $this->config;
$config = new LiveChannelConfig(array(
'description' => 'live channel test',
'type' => 'HLS',
'fragDuration' => 2,
'fragCount' => 5,
'playListName' => $liveChannelName . '.m3u8'
));
$ossClient = new OssClient($ossClientConfig['AccessKeyID'], $ossClientConfig['AccessKeySecret'], $ossClientConfig['EndPoint']);
$info = $ossClient->putBucketLiveChannel($ossClientConfig['Bucket'], 'test_rtmp_live', $config);
$data['channel_name'] = $info->getName();
$data['channel_description'] = $info->getDescription();
$data['publishurls'] = $info->getPublishUrls();
$data['playurls'] = $info->getPlayUrls();
$data['publishurls_sig'] = $ossClient->signRtmpUrl($ossClientConfig['Bucket'], "test_rtmp_live", 3600, array('params' => array('playlistName' => $liveChannelName . '.m3u8')));
return $data;
}
public function getSignRtmpUrl($param = [])
{
$ossClientConfig = $this->config;
$ossClient = new OssClient($ossClientConfig['AccessKeyID'], $ossClientConfig['AccessKeySecret'], $ossClientConfig['EndPoint']);
$url = $ossClient->signRtmpUrl($ossClientConfig['Bucket'], "test_rtmp_live", 3600, $param);
return $url;
}
public function getLiveChannelHistory()
{
$ossClientConfig = $this->config;
$ossClient = new OssClient($ossClientConfig['AccessKeyID'], $ossClientConfig['AccessKeySecret'], $ossClientConfig['EndPoint']);
$history = $ossClient->getLiveChannelHistory($ossClientConfig['Bucket'], "test_rtmp_live");
$list = [];
foreach ($history->getLiveRecordList() as $recordList) {
$data['startTime'] = $recordList->getStartTime();
$data['endTime'] = $recordList->getEndTime();
$data['remoteAddr'] = $recordList->getRemoteAddr();
$list[] = $data;
}
return $list;
}
public function getLiveChannelInfo()
{
$ossClientConfig = $this->config;
$ossClient = new OssClient($ossClientConfig['AccessKeyID'], $ossClientConfig['AccessKeySecret'], $ossClientConfig['EndPoint']);
$info = $ossClient->getLiveChannelInfo($ossClientConfig['Bucket'], 'test_rtmp_live');
$data['description'] = $info->getDescription();
$data['status'] = $info->getStatus();
$data['type'] = $info->getType();
$data['fragDuration'] = $info->getFragDuration();
$data['fragCount'] = $info->getFragCount();
$data['playListName'] = $info->getPlayListName();
return $data;
}
public function getLiveChannelStatus()
{
$ossClientConfig = $this->config;
$ossClient = new OssClient($ossClientConfig['AccessKeyID'], $ossClientConfig['AccessKeySecret'], $ossClientConfig['EndPoint']);
$status = $ossClient->getLiveChannelStatus($ossClientConfig['Bucket'], "test_rtmp_live");
$data['status'] = $status->getStatus();
$data['ConnectedTime'] = $status->getConnectedTime();
$data['VideoWidth'] = $status->getVideoWidth();
$data['VideoHeight'] = $status->getVideoHeight();
$data['VideoFrameRate'] = $status->getVideoFrameRate();
$data['VideoBandwidth'] = $status->getVideoBandwidth();
$data['VideoCodec'] = $status->getVideoCodec();
$data['AudioBandwidth'] = $status->getAudioBandwidth();
$data['AudioSampleRate'] = $status->getAudioSampleRate();
$data['AdioCodec'] = $status->getAudioCodec();
return $data;
}
private function guid($rand = "")
{
$charid = strtoupper(md5(uniqid(mt_rand(), true) . $rand));
$hyphen = chr(45);// "-"
$uuid = substr($charid, 0, 8) . $hyphen . substr($charid, 8, 4) . $hyphen . substr($charid, 12, 4) . $hyphen . substr($charid, 16, 4) . $hyphen . substr($charid, 20, 12);
return $uuid;
}
// 获取指定长度的随机字母数字组合的字符串
private function random($length = 32, $repeat = True)
{
$pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
if ($repeat === True) {
$pool = str_repeat($pool, $length);
}
return substr(str_shuffle($pool), 0, $length);
}
private function getFileName($file)
{
$rand = $this->random();
$guid = $this->guid($rand);
$mime_content_type = $extName = pathinfo($file['name'], PATHINFO_EXTENSION);;
return $guid . "." . $mime_content_type;
}
}
4. 推流指令
<?php
namespace app\common\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;
class PushRtmp extends Command
{
protected function configure()
{
$this->setName('push:rtmp')->setDescription('rtmp推流');
}
protected function execute(Input $input, Output $output)
{
$command = "ffmpeg -re -i https:///global_source/B7689D4E-ED00-B402-EBD9-4F52C3B97247.mp4 -c copy -f flv \"rtmp://def.oss-cn-shenzhen.aliyuncs.com/live/test_rtmp_live?playlistName=newest.m3u8&OSSAccessKeyId=LTAIp7VhX2YoazcF&Expires=1685606516&Signature=ucEuVujPN2AeHFGbpwF%2BgHqfNPk%3D\"";
$result = exec($command, $output, $resultVar);
var_dump($output);
}
}
FFmpeg参数说明
frame= 9753 fps= 30 q=-1.0 Lsize= 129340kB time=00:05:25.09 bitrate=3259.2kbits/s speed= 1x
frame= 9753:表示拍摄的帧数为9753
fps=246:表示每秒拍摄246张照片
q=-1.0:表示视频的质量为-1.0,即质量较差
Lsize= 129340kB:表示视频的大小为129340KB
time=00:05:25.09:表示视频的时长为5分25秒9毫秒
bitrate=3259.2kbits/s:表示视频的码率为3259.2KB/s
speed=8.21x:表示视频的播放速度为8.21倍
video:116301kB audio:12596kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.344442%
video:116301kB 视频数据量
audio:12596kB 音频数据量
subtitle:0kB 字幕数据量
other streams:0kB 其他数据量
global headers:0kB 全局数据头
muxing overhead: 0.344442% 多路复用开销