FFmpeg入门 - rtmp推流

news2024/11/19 18:22:28

FFmpeg入门 - 视频播放_音视频开发老马的博客-CSDN博客介绍了怎样用ffmpeg去播放视频.

里面用于打开视频流的avformat_open_input函数除了打开本地视频之外,实际上也能打开rtmp协议的远程视频,实现拉流:

./demo -p 本地视频路径
​
./demo -p rtmp://服务器ip/视频流路径

这篇文章我们来讲下怎样实现推流,然后和之前的demo代码配合就能完成推流、拉流的整个过程,实现直播。

rtmp服务器

整个直播的功能分成下面三个模块:

截屏2022-09-08 下午9.49.50.png

从上图我们可以看到rtmp是需要服务器做转发的,我们选用开源的srs.直接从github上把它的源码拉下来编译,然后直接启动即可:

git clone git@github.com:ossrs/srs.git
cd srs/trunk
./configure
make
./etc/init.d/srs start

如果是本地的电脑,这个时候就能在局域网内直接用它的内网ip去访问了.但如果是腾讯云、阿里云之类的云服务器还需要配置安全组开放下面几个端口的访问权限:

listen              1935;
max_connections     1000;
#srs_log_tank        file;
#srs_log_file        ./objs/srs.log;
daemon              on;
http_api {
    enabled         on;
    listen          1985;
}
http_server {
    enabled         on;
    listen          8080;
    dir             ./objs/nginx/html;
}
rtc_server {
    enabled on;
    listen 8000; # UDP port
    # @see https://ossrs.net/lts/zh-cn/docs/v4/doc/webrtc#config-candidate
    candidate $CANDIDATE;
}
...

当然如果这几个端口已经被占用的话可以修改配置文件conf/srs.conf去修改

服务器到这里就准备好了,浏览器访问下面网址对srs进行调试、配置:

http://服务器ip:8080/players/rtc_publisher.html http://服务器ip:1985/console/ng_index.html

推流

文末名片免费领取音视频开发学习资料,内容包括(C/C++,Linux 服务器开发,FFmpeg ,webRTC ,rtmp ,hls ,rtsp ,ffplay ,srs)以及音视频学习路线图等等。

准备输出流

我们选择推送本地的视频到rtmp服务器,所以第一步仍然是打开本地视频流:

bool VideoSender::Send(const string& srcUrl, const string& destUrl) {
    ...
    // 打开文件流读取文件头解析出视频信息如轨道信息、时长等
    // mFormatContext初始化为NULL,如果打开成功,它会被设置成非NULL的值
    // 这个方法实际可以打开多种来源的数据,url可以是本地路径、rtmp地址等
    // 在不需要的时候通过avformat_close_input关闭文件流
    if(avformat_open_input(&inputFormatContext, srcUrl.c_str(), NULL, NULL) < 0) {
        cout << "open " << srcUrl << " failed" << endl;
        break;
    }
​
    // 对于没有文件头的格式如MPEG或者H264裸流等,可以通过这个函数解析前几帧得到视频的信息
    if(avformat_find_stream_info(inputFormatContext, NULL) < 0) {
        cout << "can't find stream info in " << srcUrl << endl;
        break;
    }
​
    // 打印输入视频信息
    av_dump_format(inputFormatContext, 0, srcUrl.c_str(), 0);
    ...
}

本地视频打开之后,我们创建输出视频流上下文,然后为输出流创建轨道,最后打开输出视频流:

// 创建输出流上下文,outputFormatContext初始化为NULL,如果打开成功,它会被设置成非NULL的值,在不需要的时候使用avformat_free_context释放
// 输出流使用flv格式
if(avformat_alloc_output_context2(&outputFormatContext, NULL, "flv", destUrl.c_str()) < 0) {
    cout << "can't alloc output context for " << destUrl << endl;
    break;
}
​
// 拷贝编解码参数
if(!createOutputStreams(inputFormatContext, outputFormatContext)) {
    break;
}
​
// 打印输出视频信息
av_dump_format(outputFormatContext, 0, destUrl.c_str(), 1);
​
// 打开输出流,结束的时候使用avio_close关闭
if(avio_open(&outputFormatContext->pb, destUrl.c_str(), AVIO_FLAG_WRITE) < 0) {
    cout << "can't open avio " << destUrl << endl;
    break;
}

这里有个createOutputStreams用于根据本地视频文件的轨道信息,为输出流创建同样的轨道:

static bool createOutputStreams(AVFormatContext* inputFormatContext, AVFormatContext* outputFormatContext) {
    // 遍历输入流的所有轨道,拷贝编解码参数到输出流
    for(int i = 0 ; i < inputFormatContext->nb_streams ; i++) {
        // 为输出流创建轨道
        AVStream* stream = avformat_new_stream(outputFormatContext, NULL);
        if(NULL == stream) {
            cout << "can't create stream, index " << i << endl;
            return false;
        }
​
        // 编解码参数在AVCodecParameters中保存,从输入流拷贝到输出流
        if(avcodec_parameters_copy(stream->codecpar, inputFormatContext->streams[i]->codecpar) < 0) {
            cout << "can't copy codec paramters, stream index " << i << endl;
            return false;
        }
​
        // codec_tag代表了音视频数据采用的码流格式,不同的封装格式如flv、mp4等的支持情况是不一样的
        // 上面的avcodec_parameters_copy将输出流的codec_tag从输入拷贝过来变成了一样的
        // 由于我们输出流在avformat_alloc_output_context2的时候写死了flv格式
        // 如果输入流不是flv而是mp4等格式的话就可能会出现mp4里某种codec_tag在flv不支持导致推流失败的情况
        // 这里我们可以用av_codec_get_id从输出流的oformat的支持的codec_tag列表里面查找codec_id
        // 如果和codecpar的codec_id不一致的话代表不支持
        if(av_codec_get_id(outputFormatContext->oformat->codec_tag, stream->codecpar->codec_tag) != stream->codecpar->codec_id) {
            // 这里将codec_tag设置为0,FFmpeg会根据编码codec_id从封装格式的codec_tag列表中找到一个codec_tag
            stream->codecpar->codec_tag = 0;
        }
    }
    return true;
}

codec_id和codec_tag

这里可以看到对于编码器有codec_id和codec_tag两个字段去描述,codec_id代表的是数据的编码类型.而codec_tag用于更详细的描述编解码的格式信息,它对应的是FourCC(Four-Character Codes)数据。

例如codec_id都是AV_CODEC_ID_RAWVIDEO的裸数据,但它可能是YUV的裸数据也可能是RGB的裸数据:

// libavformat/isom.c
{ AV_CODEC_ID_RAWVIDEO, MKTAG('r', 'a', 'w', ' ') }, /* uncompressed RGB */
{ AV_CODEC_ID_RAWVIDEO, MKTAG('y', 'u', 'v', '2') }, /* uncompressed YUV422 */
{ AV_CODEC_ID_RAWVIDEO, MKTAG('2', 'v', 'u', 'y') }, /* uncompressed 8-bit 4:2:2 */
{ AV_CODEC_ID_RAWVIDEO, MKTAG('y', 'u', 'v', 's') }, /* same as 2VUY but byte-swapped */

又例如codec_id都是AV_CODEC_ID_H264,但实际上也有许多细分类型:

// libavformat/isom.c
{ AV_CODEC_ID_H264, MKTAG('a', 'v', 'c', '1') }, /* AVC-1/H.264 */
{ AV_CODEC_ID_H264, MKTAG('a', 'v', 'c', '2') },
{ AV_CODEC_ID_H264, MKTAG('a', 'v', 'c', '3') },
{ AV_CODEC_ID_H264, MKTAG('a', 'v', 'c', '4') },
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '5', 'p') }, /* AVC-Intra  50M 720p24/30/60 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '5', 'q') }, /* AVC-Intra  50M 720p25/50 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '5', '2') }, /* AVC-Intra  50M 1080p25/50 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '5', '3') }, /* AVC-Intra  50M 1080p24/30/60 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '5', '5') }, /* AVC-Intra  50M 1080i50 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '5', '6') }, /* AVC-Intra  50M 1080i60 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '1', 'p') }, /* AVC-Intra 100M 720p24/30/60 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '1', 'q') }, /* AVC-Intra 100M 720p25/50 */
{ AV_CODEC_ID_H264, MKTAG('a', 'i', '1', '2') }, /* AVC-Intra 100M 1080p25/50 */

可以看出来codec_tag是通过4个字母去表示的,我们来看看MKTAG的定义:

#define MKTAG(a,b,c,d) ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24))

最终它得到的是一个整数,例如MKTAG('a', 'v', 'c', '1')得到的值是0x31637661

  • 0x31 =1

  • 0x63 = c

  • 0x76 = v

  • 0x61 = a

我们可以用av_fourcc2str这个函数将最终的整数转换回字符串

回过头来看看这个判断:

if(av_codec_get_id(outputFormatContext->oformat->codec_tag, stream->codecpar->codec_tag) != stream->codecpar->codec_id)

大部分情况下如果codec_tag在输出流不支持的情况下av_codec_get_id拿到的是AV_CODEC_ID_NONE,所以大部分情况可以等价于:

if(av_codec_get_id(outputFormatContext->oformat->codec_tag, stream->codecpar->codec_tag) != AV_CODEC_ID_NONE)

不过也存在都是MKTAG('l', 'p', 'c', 'm'),但codec_id可能是AV_CODEC_ID_PCM_S16BE或者AV_CODEC_ID_PCM_S16LE的情况:

{ AV_CODEC_ID_PCM_S16BE,       MKTAG('l', 'p', 'c', 'm') },
{ AV_CODEC_ID_PCM_S16LE,       MKTAG('l', 'p', 'c', 'm') },

所以最好还是和原本的codec_id做比较会靠谱点。

写入视频数据

接着就是视频数据的写入了,主要有三个步骤,写入文件头、读取本地视频包并写入输出视频流、写入文件结尾:

// 设置flvflags为no_duration_filesize用于解决下面的报错
// [flv @ 0x14f808e00] Failed to update header with correct duration.
// [flv @ 0x14f808e00] Failed to update header with correct filesize
AVDictionary * opts = NULL;
av_dict_set(&opts, "flvflags", "no_duration_filesize", 0);
if(avformat_write_header(outputFormatContext, opts ? &opts : NULL) < 0) {
    cout << "write header to " << destUrl << " failed" << endl;
    break;
}
​
// 创建创建AVPacket接收数据包
// 无论是压缩的音频流还是压缩的视频流,都是由一个个数据包组成的
// 解码的过程实际就是从文件流中读取一个个数据包传给解码器去解码
// 对于视频,它通常应包含一个压缩帧
// 对于音频,它可能是一段压缩音频、包含多个压缩帧
// 在不需要的时候可以通过av_packet_free释放
packet = av_packet_alloc();
if(NULL == packet) {
    cout << "can't alloc packet" << endl;
    break;
}
​
...
​
// 从文件流里面读取出数据包,这里的数据包是编解码层的压缩数据
while(av_read_frame(inputFormatContext, packet) >= 0) {
    // 我们以视频轨道为基准去同步时间
    // 如果时间还没有到就添加延迟,避免向服务器推流速度过快
    ...
​
    // 往输出流写入数据
    av_interleaved_write_frame(outputFormatContext, packet);
​
    // 写入成之后压缩数据包的数据就不需要了,将它释放
    av_packet_unref(packet);
}
​
// 写入视频尾部信息
av_write_trailer(outputFormatContext);

帧同步

由于av_read_frame这里读取出来的是未解码的压缩数据速度很快,如果不做控制一下子就发送完成了,会造成数据堆积在服务器上。这里我们忽略网络传输耗时,依然通过视频包的pts做一定的同步:

while(av_read_frame(inputFormatContext, packet) >= 0) {
    // 我们以视频轨道为基准去同步时间
    // 如果时间还没有到就添加延迟,避免向服务器推流速度过快
    if(videoStreamIndex == packet->stream_index) {
        if(AV_NOPTS_VALUE == packet->pts) {
            // 有些视频流不带pts数据,按30fps将间隔统一成32ms
            av_usleep(32000);
        } else {
            // 带pts数据的视频流,我们计算出每一帧应该在什么时候播放
            int64_t nowTime = av_gettime() - startTime;
            int64_t pts = packet->pts * 1000 * 1000 * timeBaseFloat;
            if(pts > nowTime) {
                av_usleep(pts - nowTime);
            }
        }
    }
    // 往输出流写入数据
    av_interleaved_write_frame(outputFormatContext, packet);
​
    // 写入成之后压缩数据包的数据就不需要了,将它释放
    av_packet_unref(packet);
}

资源释放

等视频流读写完成之后就是最后的资源释放收尾工作了:

if(NULL != packet) {
    av_packet_free(&packet);
}
​
if(NULL != outputFormatContext) {
    if(NULL != outputFormatContext->pb) {
        avio_close(outputFormatContext->pb);
    }
    avformat_free_context(outputFormatContext);
}
​
if(NULL != inputFormatContext) {
    avformat_close_input(&inputFormatContext);
}

其他

源码和上篇博客的是同一个仓库,编译之后可以通过-s参数推流到服务器:

./demo -s video.flv rtmp://服务器ip/live/livestream

推流的同时就能使用-p参数去拉流进行实时播放:

./demo -p rtmp://服务器ip/live/livestream

这个demo只是简单的将本地视频文件推到服务器,实际上我们可以对他做些修改就能实现将摄像头的视频流推到服务器了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/29385.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JVM垃圾回收总结

常见面试题 如何判断对象是否死亡 简单介绍一下强引用、软引用、弱引用、虚引用 如何判断常量是一个废弃常量 如何判断类是一个无用类 垃圾收集有哪些算法、各自的特点&#xff1f; 常见的垃圾回收器有哪些&#xff1f; 介绍一下CMS&#xff0c;G1收集器&#xff1f; minor gc和…

[附源码]计算机毕业设计JAVA课后作业提交系统关键技术研究与系统实现

[附源码]计算机毕业设计JAVA课后作业提交系统关键技术研究与系统实现 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&am…

[附源码]计算机毕业设计JAVA课堂点名系统

[附源码]计算机毕业设计JAVA课堂点名系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis M…

【2】Anaconda基本命令以及相关工具:jupyter、numpy、Matplotilb

上一篇请移步【1】Anaconda基本命令以及相关工具&#xff1a;jupyter、numpy、Matplotilb_水w的博客-CSDN博客 目录 3 Numpy数组基础索引&#xff1a;索引和切片 ◼ 基础索引 4 Numpy非常重要的数组合并与拆分操作 ◼ 数组的合并-concatenate、vstack、hstack numpy.vstac…

生产制造管理:供应商管理系统

随着经济全球化和信息技术的快速推进发展&#xff0c;传统的管理模式早已不再适应现代市场竞争与生产制造的需要&#xff0c;以顾客需求为中心的供应链管理显得更为重要。供应链是围绕核心企业&#xff0c;通过对信息流、物流、资金流等关键部分的控制连成一个整体的功能网链结…

期末前端web大作业——我的家乡陕西介绍网页制作源码HTML+CSS+JavaScript

家乡旅游景点网页作业制作 网页代码运用了DIV盒子的使用方法&#xff0c;如盒子的嵌套、浮动、margin、border、background等属性的使用&#xff0c;外部大盒子设定居中&#xff0c;内部左中右布局&#xff0c;下方横向浮动排列&#xff0c;大学学习的前端知识点和布局方式都有…

[附源码]计算机毕业设计JAVA科院垃圾分类系统

[附源码]计算机毕业设计JAVA科院垃圾分类系统 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybati…

Flutter For Web——一个简单的图片素材网站

一个简单的图片素材网站效果视频登录注册页效果图UI初始化TabBarPageView组合登录账号输入按键处理SharedPreferences封装保存数据取出数据清除缓冲内容搜索栏效果图UI首页效果图UIDio网络请求Dio单例封装构造Dio对象GetPostResponse使用解析Json图片阅览UIDialog下载UI调用浏览…

Spring之IOC 为什么能解耦

1.1 什么是IOC &#xff08;1&#xff09;控制反转&#xff0c;把对象的创建和对象之间的调用过程&#xff0c;都交给Spring进行管理 &#xff08;2&#xff09;使用IOC目的&#xff1a;为了耦合性降低 1.2 IOC的底层原理 &#xff08;1&#xff09;使用的技术&#xff1a;…

完美解决-RuntimeError: CUDA error: device-side assert triggered

网上的解决方案意思是对的&#xff0c;但并没有给出相应的实际解决方法&#xff1a; 问题描述&#xff1a; 当使用ImageFolder方式构建数据集的时候&#xff1a; train_data torchvision.datasets.ImageFolder(train_path, transformtrain_transform)train_loader DataLoad…

学习Git看这一篇就够了

文章目录Git简单介绍官方网址Git是什么版本控制系统的演化Git安装 - Windows版需要熟悉的几个Linux命令Git命令行状态对应目录位置Git命令1. git init2. git status3. git add4. git commit5. git config6. git reset7. git diff练习 - 创建学生管理系统练习提交代码练习修改代…

传感模块:MATEKSYS Optical Flow LIDAR 3901-L0X

传感模块&#xff1a;MATEKSYS Optical Flow & LIDAR 3901-L0X1. 模块介绍2. 规格参数3. 使用方法Step1: 接线方式Step2: 安装方式Step3: 使用范围4. 存在问题4.1 MATEKSYS 3901-L0X 输出协议格式&#xff1f;4.1.1 支持光流计协议(iNav-CXOF)4.1.2 支持光流计激光测距协议…

混合SDN中的安全性问题研究

混合SDN中的安全性问题研究混合SDN中的安全性问题研究1.学习目标2.学习内容3.目前存在的问题4.解决办法1.关于欺骗ARP的讨论2.DDoS攻击探讨5.解决方案现有文献的解决方案6.目前面临的挑战申明&#xff1a; 未经许可&#xff0c;禁止以任何形式转载&#xff0c;若要引用&#xf…

美食杰项目(一)登录注册页

目录前言&#xff1a;具体效果&#xff1a;代码思路相应的组件&#xff1a;具体代码&#xff1a;all页面的具体代码&#xff1a;login页面具体代码&#xff1a;**登录和注册的基本功能都一样所以没有注释**enroll页面的具体代码&#xff1a;路由相关代码&#xff1a;相关引入&a…

Swagger2依赖的版本问题导致其配置文件一直报错的终极解决方案

Swagger2依赖的版本问题 在项目中使用的报错的版本 springboot2.2.1.RELEASE swagger2.9.2导致在写swagger的配置类时&#xff0c;一直引入不了依赖 导入正确的依赖 <!--swagger--> <dependency><groupId>io.springfox</groupId><artifactId>sp…

JIRA on K8s helm部署实战

JIRA on K8s helm部署实战jira on k8s实战waht&#xff1f;架构![在这里插入图片描述](https://img-blog.csdnimg.cn/7b007d9bfb4648c7b1ab816105f51701.png)如何选择chart官方的chartmox 的chart【1】mox chart 安装脚本【2】生产环境的yamljira 的sharedHome 和localHome 的区…

spring源码 - @Condition原理及运用

1.在源码中&#xff0c;在生成beanfinition中有有如一段代码 以下代码逻辑中执行this.conditionEvaluator.shouldSkip返回true直接跳出beandefinition生成逻辑 private <T> void doRegisterBean(Class<T> beanClass, Nullable String name,Nullable Class<? …

实验数据处理

来源 加热冷却温度实验&#xff0c;相同实验参数可能有一次或多次重复实验&#xff0c;一次实验中也可能有多次。如何分别每一次周期&#xff0c;并把每个周期的数据都分析出来&#xff0c;成为一个问题。 解决思想 想根据冷却后的平台划分不同周期&#xff0c;但是由于冷却…

web前端期末大作业【仿12306铁路官网首页】学生网页设计作业源码

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

分享5款同类软件中的翘楚,属于是WIN10必备良品

今天要给大家推荐的是5款软件&#xff0c;每个都是同类软件中的个中翘楚,请大家给我高调地使用起来,不用替我藏着掖着。 1.PPT插件——OneKeyTools OK插件是一款免费的PPT插件&#xff0c;让你的PPT制作有无限可能&#xff01;它的功能&#xff0c;太多了&#xff0c;比如图片…