前言

现如今,大部分带内容的网站或应用都有视频区了,不说是大厂平台,就连个人开发者也相继在自己网站或小程序上迭代出视频板块。那既然有了视频模块,除个性化推荐,智能审核等这种费钱又耗时的功能外(个人开发者暂缓)。最基本的视频上传,视频播放自然必不可少吧。

既然要强调省钱,我当前不会对接点播服务了。毕竟为了有一定的审核和推荐功能,我打算做人工审核。那剩下的关于播放有一定的体验度,还得要用一下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的临时访问凭证和接收上传成功回调。

如何开发视频上传和播放功能时,既省钱又体验好?_FFmpeg_02

视频播放

关于视频播放,网站早期的做法是将OSS的视频地址直接丢到前端的video标签中,当在手机播放时会出现卡顿或播放缓慢等问题。最后决定使用OSS的HLS的构建,就是在后台将视频推流一份在OSS的LiveChannel中,前端通过读取m3u8播放视频。

思路
  1. 前端上传视频,后台审核成功调用OSS的LiveChannel创建。
  2. 根据通道创建返回的推流地址和播放地址,存入数据库。
  3. 在服务端起一个进程用FFMPEG进行视频推流。
  4. 前端播放视频时选择m3u8的方式。

如何开发视频上传和播放功能时,既省钱又体验好?_短视频_03

示例代码
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 (fileObj.name) {
            $(".el-upload-list").css("display", "block");
            $(".el-upload-list li").css("border", "1px solid #20a0ff");
            $("#videoName").text(fileObj.name);
        } 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>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.
  • 214.
  • 215.
  • 216.
  • 217.
  • 218.
  • 219.
  • 220.
  • 221.
  • 222.
  • 223.
  • 224.
  • 225.
  • 226.
  • 227.
  • 228.
  • 229.
  • 230.
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);
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
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;
    }
 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
  • 145.
  • 146.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 179.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.
  • 194.
  • 195.
  • 196.
  • 197.
  • 198.
  • 199.
  • 200.
  • 201.
  • 202.
  • 203.
  • 204.
  • 205.
  • 206.
  • 207.
  • 208.
  • 209.
  • 210.
  • 211.
  • 212.
  • 213.
  • 214.
  • 215.
  • 216.
  • 217.
  • 218.
  • 219.
  • 220.
  • 221.
  • 222.
  • 223.
  • 224.
  • 225.
  • 226.
  • 227.
  • 228.
  • 229.
  • 230.
  • 231.
  • 232.
  • 233.
  • 234.
  • 235.
  • 236.
  • 237.
  • 238.
  • 239.
  • 240.
  • 241.
  • 242.
  • 243.
  • 244.
  • 245.
  • 246.
  • 247.
  • 248.
  • 249.
  • 250.
  • 251.
  • 252.
  • 253.
  • 254.
  • 255.
  • 256.
  • 257.
  • 258.
  • 259.
  • 260.
  • 261.
  • 262.
  • 263.
  • 264.
  • 265.
  • 266.
  • 267.
  • 268.
  • 269.
  • 270.
  • 271.
  • 272.
  • 273.
  • 274.
  • 275.
  • 276.
  • 277.
  • 278.
  • 279.
  • 280.
  • 281.
  • 282.
  • 283.
  • 284.
  • 285.
  • 286.
  • 287.
  • 288.
  • 289.
  • 290.
  • 291.
  • 292.
  • 293.
  • 294.
  • 295.
  • 296.
  • 297.
  • 298.
  • 299.
  • 300.
  • 301.
  • 302.
  • 303.
  • 304.
  • 305.
  • 306.
  • 307.
  • 308.
  • 309.
  • 310.
  • 311.
  • 312.
  • 313.
  • 314.
  • 315.
  • 316.
  • 317.
  • 318.
  • 319.
  • 320.
  • 321.
  • 322.
  • 323.
  • 324.
  • 325.
  • 326.
  • 327.
  • 328.
  • 329.
  • 330.
  • 331.
  • 332.
  • 333.
  • 334.
  • 335.
  • 336.
  • 337.
  • 338.
  • 339.
  • 340.
  • 341.
  • 342.
  • 343.
  • 344.
  • 345.
  • 346.
  • 347.
  • 348.
  • 349.
  • 350.
  • 351.
  • 352.
  • 353.
  • 354.
  • 355.
  • 356.
  • 357.
  • 358.
  • 359.
  • 360.
  • 361.
  • 362.
  • 363.
  • 364.
  • 365.
  • 366.
  • 367.
  • 368.
  • 369.
  • 370.
  • 371.
  • 372.
  • 373.
  • 374.
  • 375.
  • 376.
  • 377.
  • 378.
  • 379.
  • 380.
  • 381.
  • 382.
  • 383.
  • 384.
  • 385.
  • 386.
  • 387.
  • 388.
  • 389.
  • 390.
  • 391.
  • 392.
  • 393.
  • 394.
  • 395.
  • 396.
  • 397.
  • 398.
  • 399.

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://www.neiqiaosu.com/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);
 
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

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倍
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
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%  多路复用开销
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

如何开发视频上传和播放功能时,既省钱又体验好?_FFmpeg_04