音视频 h264编码等相关知识汇总

1,629 阅读21分钟

音频相关

音频编码过程.png

常见音频编解码器.png

音频质量比较.png

音频编码码率.png

  • 编译fdk_aac的编译参数 编译fdk_aac的编译参数.png

音频重采样.png

音频重采样的目的.png

  • 音频码率
    audio_bitrate.png

=========================================================================

h264相关

yuv420p 颜色空间相关

  • 4:4:4 表示不降低色度(UV)通道的采样率。每个 Y 分量对应一组 UV 分量。

  • 4:2:2 表示 2:1 水平下采样,没有垂直下采样。每两个 Y 分量共享一组 UV 分量。

  • 4:2:0 表示 2:1 水平下采样,同时 2:1 垂直下采样。每四个 Y 分量共享一组 UV 分量。

  • 4:1:1 表示 4:1 水平下采样,没有垂直下采样。每四个 Y 分量共享一组 UV 分量。4:1:1 采样比其他格式少见

  • 听这个听得懂:YUV图像格式介绍_哔哩哔哩_bilibili

yuv420_1.jpg

yuv420.jpg

yuv444_1.jpg

yuv422_1.jpg

yuv422.jpg

yuv2rgb3.jpg

yuv格式.jpg

yuv444.jpg

color_range.jpg

yuv2rgb2.jpg

yuv2rgb1.jpg

yuv2rgb.jpg

这篇文章有参考价值:音视频开发学习:H.264/AVC视频编解码技术 - 知乎 (zhihu.com)

颜色空间参考:www.jianshu.com/p/358bf8b7e…

  • 帧内预测是根据帧内已经编码的样本,为当前的样本计算出一个预测值,用当前样本值减去预测值得到一个残差值,目的就是为了减少传输的数据量。

  • NALU 以 0000 0001划分开

  • yuv420p一个pix占用字节数1.5Byte
    rgb 8bit位深,3通道(不含透明度),一个pix占用3Bytes

  • h264编码(pix:640*480 yuv420p fps=15 500kbps)常见压缩比1%
    常见电影_fps>=60; 视频直播_fps>=15

  • 码率经验值.参考https://docs,agora.io/cn 视频编码_声网_压缩码率建议.png

  • b帧多的缺点,占用cpu;解码耗时;不宜直播
    实时:i+p;转码:大量b帧,为减小存储

  • IDR帧,特殊的I帧,解码立即刷新帧,由于每个GOP间明显的差别

    • 特点:解码端遇到IDR会将缓存清空,重新解码
    • 每个GOP中第一帧就是IDR
  • h264默认编码(不是编码顺序,可以看作gop中帧顺序)
    IBBBPBBBPBBBI
    B帧间无相互关系,B帧参考始终是之前的I和之后的P帧

  • SPS PPS 在每个IDR帧前都会成对出现这两种帧(参数术语)

    • SPS 参数序列集(帧内参数,约束gop的参数)
      SPS_ID 帧数 参考帧数量(可参考一帧,亦可参考多帧) 解码图像尺寸 编码模式
    • PPS 图像参数集(约束GOP中每帧的参数)
      ID 熵编码 帧编码 片组数目(帧编码) 初始量化参数 区块滤波系数
  • 帧内压缩理论:

    • 1.相邻像素差别不大,有宏块预测的基础
    • 2.人眼对亮度的敏感超过色度
    • 3.yuv分开存储,利于压缩
  • 宏块预测有9种模式

  • 残差值 = 原始图像 - 预测出的图像
    压缩时,预测图象压缩+残差值压缩(补偿残差);主要用在帧内压缩
    运动补偿:在解码时将残差值的影响考虑在内

  • 运动估计:根据宏块匹配的手段找到运动矢量的过程称运动估计

  • 宏块查找:目的是找到宏块的运动轨迹(运动矢量)

  • 宏块查找算法:
    三步搜索 二位对数搜索 四步搜索 钻石搜索

  • 压缩编码的主要手段是:运动矢量+补偿压缩(残差值补偿),图像还原的原理也是依据这两点

  • 花屏,马赛克原因:GOP中丢帧(主要丢失的是P,B帧,运动矢量,残差值也丢失)

  • 花屏的兄弟-卡顿:当GOP丢帧时,就丢弃掉GOP内所有帧,直到下一个GOP的IDR帧到来;这种情况的刷新周期就取决于IDR帧间隔(I帧间隔) 卡顿和花屏不能兼得,互斥关系

  • 无损压缩示意图

stateDiagram-v2
图像矩阵 --> 数据集中在矩阵中的一角 : DCT变换  
数据集中在矩阵中的一角 --> 压缩后图像 : vlc压缩,mpeg2,类似哈夫曼算法,高频数据短编码
数据集中在矩阵中的一角 --> 压缩后图像 : CABAC,h264
graph LR
图像矩阵 --DCT变换-->

矩阵,数据集中在矩阵中的一角 
--vlc压缩,mpeg2,类似哈夫曼算法,高频数据短编码-->
压缩后图像  

矩阵,数据集中在矩阵中的一角 
--CABAC,h264-->
压缩后图像  

常见封装编码格式.png

视频编码_帧内压缩_mpeg压缩.png

视频编码_帧间压缩.png

视频编码_帧间压缩_b帧.png

视频编码_gop.png

视频编码_码率格式.png

视频编码_发展史.png

视频编码_编码规则.png

视频编码_编码顺序.png

视频编码_结合到雷霄骅VideoEye软件分析编码序列.png

视频编码_dsp硬解码流程.png

视频编码_dsp硬解码流程1.png

视频编码_信源编码器流程.png

视频压缩_残差值和预测值压缩.png

视频编码_h264编码流程图.png

视频编码_h264解码流程图.png

视频编码_h264编码参数.png

视频编码_h264_profile.png

视频编码_h264_profile1.png

视频编码_h264_level.png

视频编码_h264_分辨率.png

视频编码_h264_帧相关.png

pps.png

sps中帧率的计算.png

视频编码_SliceHeader.png

  • ffmpeg_h264编码器参数设置 视频编码_李超_编码器参数设置.png

视频编码_李超_编码器参数设置1.png

视频编码_李超_编码器参数设置2.png

nalu

视频编码_视频序列.png

nalu.png

nalu_header与nalu_body.png

nalu_header.png 视频编码_编码结构.png 视频编码_编码结构1.png

读乔帮主 h264 编码博客

nalu 中的 slice 和 IDR 帧的对应:www.zzsin.com/article/avc…

大部分情况下,一个 slice 就是一帧
这就是为什么 IDR 帧通常只在流媒体格式(RTMP,RTP)中被强调,而在类似 MP4 之类的储存格式中,因为不必担心丢包等,所以往往只有开头一个 IDR 帧。

slice 是什么?www.zzsin.com/article/sli…

slice是否就是帧:www.zzsin.com/article/sli…

点出了slice, 帧, 宏块间的关系 首先要讲明白一点,那就是 Slice 不是帧。我们经常在博客中看到一些说法,例如:这个 NALU 是一个 I 帧,这个 NALU 是一个 P 帧。其实这些说法严格意义上来说都是不太对的。
在编码的时候,一帧图像首先会被分割成若干宏块,然后对每个宏块进行编码压缩,之后,会将宏块写入到 Slice 里面,最后再加上一些修饰变成 NALU。
这个过程中有一点要注意,那就是一帧图像产生宏块,不一定要写到一个 Slice 里面。
可以看到 slice_type 规定了 slice 的类型,这也解释了之前的一些误区。有人说根据 NALU 的类型就能判读是 I Slice 还是 P Slice 还是 B Slice。其实是错误的,要判断这些必须要读到 slice_type 才行。

帧内编码,帧内预测:www.zzsin.com/article/int…

www.zzsin.com/article/pre…

=================================================================================

协议与传输

udp rtp rtcp

udp报文.png
udp通信.png

rtcp包.png

rtmp

参考干货:RTMP 协议:为什么直播推流协议都爱用它?丨音视频基础 (qq.com)
rtmp握手.png

rtmp建立连接.png

rtmp创建流.png

rtmp推流.png

rtmp拉流.png
rtmps与c端的交互过程是比较复杂的,上面是chunk的格式,chunk之上是消息,消息.服务于数据封装,是 RTMP 协议中的基本数据单元;块,服务于网络传输
通过这种分层的设计,就可以将大的消息(Message)数据分包成小的块(Chunk)通过网络来进行传输,这个也是 RTMP 能够实现降低延时的核心原因下面介绍rtmp 消息格式

  • rtmp消息格式
    rtmp消息格式.png
    rtmp消息类型.png
  • rtmp chunk(并不是消息格式,再次确认 ) rtmp消息格式.png
  • fmt 两个bit, 00(metadata 和流刚开始时的绝对时间戳,音视频帧 ) 01(大多数情况) 11 三类是最常见的类型
    rtmp_fmt_header.png
  • rtmp就是在下面的tag data flv协议.png
  • flv_header
    flv_header.png
  • tag_header flv_tag_header.png
  • librtmp转载packet(librtmp中的数据结构)
    librtmp转载packet.png
  • rtmp参考:mp.weixin.qq.com/s?__biz=MjM…
  • 通常的协议的实现会给不同类型的消息赋予不同的优先级,当传输能力受到限制时它会影响消息下层流发送的队列顺序。
  • 问题:因为一个流当中可以交错传输多种消息类型的Chunk,那么多个Chunk怎么标记同属于同一类Message的呢?
    答案:是通过Chunk Stream ID区分的,同一个Chunk Stream ID 必然属于同一个Message
  • chunk stream id (chunk中的字段)表明该 chunk 是从哪个 message 中切分出来的
  • msg type id (chunk有这个字段,区分负载的音视频数据类型), 如视频为9或音频为8
  • Message StreamID (RTMP Message, chunk都有这个字段)是音视频流的唯一ID, 一路流如果既有音频包又有视频包,那么这路流音频包的StreamID和他视频包的StreamID相同
  • RTMP流中视频和音频拥有单独的Chunk Stream ID 比如音频的cs id=20,视频的cs id=21。
  • 默认的Chunk Size(Chunk data 部分)是128字节
  • RTMP Chunk Header-为什么存在不同长度? 一般情况下,msg stream id是不会变的,所以针对视频或音频, 除 了第一个RTMP Chunk Header是12Bytes的,后续即可采用8Bytes 的。 (5.3. Chunking) 如果消息的长度(message length)和类型(msg type id, 如视频为9或音 频为8)又相同,即可将这两部分也省去,RTMP Chunk Header采用 4Bytes类型的。 如果当前Chunk与之前的Chunk相比, msg stream id相同,msg type id相同,message length相同,而且都属于同一个消息(由同一个 Message切割成),这类Chunk的时间戳(timestamp)也是相同的,故后 续的也可以省去,RTMP Chunk Header采用1 Byte类型的。 当Chunk Size足够大时(一般不这么干),此时所有的Message都只能 相应切割成一个Chunk,该Chunk仅msg stream id相同。此时基本上 除了第一个Chunk的Header是12Bytes外,其它所有Chunk的Header都 是8Bytes
  • 带宽和一帧数据量来估算传输耗时,大致评估出带宽
    如果一帧1080P I帧数据量为244KBytes,假设带宽为10Mbit/s,传输一帧的 耗时为: 24410248/(1010241024) = 0.190625秒=190毫秒 假如要实时传输25帧的I帧文件,即允许每帧传输最大耗时为40毫秒,需要 带宽47.65625Mbit,这还没包括传输中的ACK数据。所以要把一帧切成Chunk 来传输,确保一些比较小的帧(音频帧,控制帧)能及时传到
  • RTMP Chunk Stream层级没有优先级的划分,而是在高层次Message stream提供优先级的划分。 注:5. RTMP Chunk Stream 不同类型的消息会被分配不同的优先级,当网络传输能力受限时, 优先级用来控制消息在网络底层的排队顺序。 比如当客户端网络不佳时,流媒体服务器可能会选择丢弃视频消息, 以保证音频消息可及时送达客户端
  • 有人用wireshark转包分析发现,rtmp流的chunk视频流(或音频流)除第 一个视频时间戳为绝对时间戳外,后续的时间戳均为timestamp delta,即当前时间戳与上一个时间戳的差值。
    比如帧率为25帧/秒的视频流,timestamp delta基本上都为40ms
    通常情况下,Chunk的时间戳(包括绝对时间戳和Timestamp delta) 是3个字节。 但时间戳值(比如时间戳差值ts delta)超过0xFFFFFF时,启用Extended Timestamp(4个字节) 来表示时间戳。 通常情况下 -- 3字节

=======================================================================================

lichao ffmpeg 精讲

时间基.png

计算下一帧pts.png

视频同步到音频的思路.png

mp4_2_flv.png

播放器线程模型

播放器线程模型.png

yuv nv12 bgr 内存模型,颜色空间

blog.csdn.net/byhook/arti…

=======================================================================

命令行

ffplay -pixel_format nv12  -f rawvideo -video_size 1920x1080  /mnt/sdb1/Projects/sdc/pre_post0.yuv 

========================================================================

常用推流格式

  • 值得参考:www.cnblogs.com/remember-fo…
  • linux 桌面流推到 rtsp 服务器上
    ffmpeg -video_size 1920x1080 -f x11grab -i :0.0 -framerate 25 -vcodec libx264 -rtsp_transport tcp -pix_fmt yuv420p -f rtsp rtsp://192.168.2.66/sivid   
    
  • windows 桌面流推到 rtsp 服务器上
    ffmpeg -f gdigrab -i desktop -vcodec libx264 -preset ultrafast -framerate 25 -vcodec libx264 -rtsp_transport tcp -pix_fmt yuv420p -f rtsp rtsp://10.10.1.119:554/zhou
    
    推到zlmediakit
    ffmpeg -f gdigrab -i desktop -vcodec libx264 -preset ultrafast -framerate 25 -vcodec libx264 -rtsp_transport tcp -pix_fmt yuv420p -f rtsp rtsp://10.10.1.209:554/live/zhou                                                                                               
    
  • windows 桌面推流到 rtmp 服务器上
    ffmpeg -f gdigrab -i desktop -vcodec libx264 -preset ultrafast -framerate 25 -vcodec libx264  -pix_fmt yuv420p -f flv rtmp://10.10.1.189:1935/live/test/zhou2 
    
    推到zlmediakit
    ffmpeg -f gdigrab -i desktop -vcodec libx264 -preset ultrafast -framerate 25 -vcodec libx264  -pix_fmt yuv420p -f flv rtmp://10.10.1.209:1935/live/test/zhou1  
    
  • mp4文件推到 rtmp 服务器
    ffmpeg -re -stream_loop -1 -i ./铁路覆盖3_crop.mp4 -vcodec libx264 -x264opts keyint=25 -an -f flv -y rtmp://10.10.1.209/live/zhou1
    
  • windows 推单屏到 easyDarwin rtsp
    ffmpeg -video_size 1920x1080 -f gdigrab -i desktop  -vcodec libx264 -preset ultrafast -framerate 25 -vcodec libx264 -rtsp_transport tcp -pix_fmt yuv420p -f rtsp rtsp://192.168.2.38:666/live/test
    
  • linux desktop to rtsp server, +1920 check one screen in two
    # echo display hardware 
    echo $DISPLAY
    # select one screen :1.0  
    ffmpeg -video_size 1920x1080 -f x11grab -i :1.0+1920  -framerate 25 -vcodec libx264 -rtsp_transport tcp -pix_fmt yuv420p -f rtsp rtsp://10.10.1.189/zhou1
    
  • 推到内网darwin服务器
    ffmpeg -f gdigrab -i desktop -vcodec libx264 -preset ultrafast -framerate 25 -vcodec libx264 -rtsp_transport tcp -pix_fmt yuv420p -f rtsp rtsp://192.168.2.66/zhou
    
  • xiongmai
    ffplay "rtsp://192.168.0.81:554/user=admin&password=&channel=0&stream=1.sdp?real_stream"  
    
  • dahua:
    ffplay "rtsp://admin:admin2019@192.168.0.123:554/cam/realmonitor?channel=1&subtype=0"
    dahua:ffplay "rtsp://admin:admin123456@192.168.0.108:554/cam/realmonitor?channel=1&subtype=0"  
    
  • 高德:
    ffplay "rtsp://admin:admin2019@192.168.0.30:554/cam/realmonitor?channel=1&subtype=0"  
    
  • 艾睿:
    ffplay "rtsp://192.168.0.25:554/stream0"  
    
  • 其他一些测试的视频流
    rtmp://58.200.131.2:1935/livetv/hunantv
    http://ivi.bupt.edu.cn/hls/cctv7hd.m3u8
    ffmpeg -re -i "rtsp://192.168.0.81:554/user=admin&password=&channel=0&stream=1.sdp?real_stream" -vcodec h264 -acodec aac -f rtsp -rtsp_transport tcp rtsp://127.0.0.1/live/test
    
    //查看视频流参数
    ffprobe -show_streams "rtsp://192.168.0.25:554/stream0"
    ffprobe -show_format "rtsp://192.168.0.25:554/stream0"		
    ffprobe -i "rtsp://192.168.0.25:554/stream0"  
    

博客对此解释很详细
www.cnblogs.com/viruscih/ar…

  • xiongmai相机推向nginx服务器
    ffmpeg -i "rtsp://192.168.0.86:554/user=admin&password=&channel=0&stream=1.sdp?real_stream" -c copy -flvflags no_duration_filesize -f flv rtmp://www.z-coding.com.cn/live/zhimakeji192.168.0.86  
    
  • 读取nginx-rtmp视频流
    ffplay "http://www.z-coding.com.cn:8088/live?app=live&stream=zhimakeji192.168.0.85"
    
  • xiongmai相机推向ZLMediaServer
    ffmpeg -i "rtsp://192.168.0.93:554/user=admin&password=&channel=0&stream=1.sdp?real_stream"  -c copy -f rtsp -rtsp_transport udp rtsp://192.168.0.41:554/live/stream0
    
  • 艾睿相机视频流推向MediaServer
    ffmpeg -i "rtsp://192.168.0.25:554/stream0" -c copy -f rtsp -rtsp_transport udp rtsp://192.168.0.41:554/live/stream0
    
  • 持续运行的bat
    :run 
    ffmpeg -i "rtsp://192.168.0.93:554/user=admin&password=&channel=0&stream=1.sdp?real_stream"  -c copy -f rtsp -rtsp_transport udp rtsp://192.168.0.41:554/live/stream0
    goto run
    
    或者
    :start 
    ffmpeg -i "rtsp://192.168.0.93:554/user=admin&password=&channel=0&stream=1.sdp?real_stream"  -c copy -f rtsp -rtsp_transport udp rtsp://192.168.0.41:554/live/stream0
    run start
    
  • 将大华相机的h264裸流,不封装以udp格式推向ZLmediaKit服务器,注意-f输出格式,可指定成flv, udp可换作tcp
    ffmpeg -i "rtsp://admin:admin2019@192.168.0.109:554/cam/realmonitor?channel=1&subtype=0" -c copy -f rtsp -rtsp_transport udp rtsp://192.168.0.57:554/live/z
    
  • 本地视频文件以rtsp推到达尔文服务器
    ffmpeg -re  -stream_loop -1 -i ./德州国鼎新型建材有限公司-4-车辆连续触发告警1.mp4  -vcodec copy -rtsp_transport tcp -f rtsp rtsp://10.10.1.189/zhou
    
  • 自动推流脚本
    #!/bin/bash  
    ffmpeg -re  -stream_loop -1 -i $1  -vcodec copy -rtsp_transport tcp -f rtsp rtsp://$2/zhou
    

ZLMediaServer保存.ts切片文件,供客户使用

在线播放.m3u8的网址:
m3u8视频在线播放器 (m3u8player.top)
视频源格式:
http://192.168.0.41/live/stream0/hls.m3u8
ZLMediaServer服务器下的文件格式
与MediaServer同级目录下 /www/live/stream0/*.m3u8

把mp4 flv文件放入http服务的根目录下即www目录下,测试下点播是否成功

# 首先,艾睿相机的视频流保存为.mp4
ffmpeg -y -i rtsp://192.168.0.41:554/live/stream0 -vcodec copy -f mp4 test.mp4

在http服务端下放有test.MP4和test.flv及hsl.m3u8的文件,测试前端能否播放,以下三个文件均能在前端加载出视频 --> 点播ok

  • mp4_play 前端原生支持mp4
<!DOCTYPE html>
<html>
<head> 
<meta charset="utf-8"> 
<title>菜鸟教程(runoob.com)</title> 
</head>
<body>
<object width="500" height="300" data="http://192.168.0.41/test.mp4"></object>
</body>
</html>
  • hls_play
<!DOCTYPE html>
<html>
    <head>
        <title>播放器</title>
    </head>
    <body>
        
		<script src="https://cdn.jsdelivr.net/hls.js/latest/hls.min.js"></script>
		<video id="video"></video>
		<script>
			if(Hls.isSupported()) {
				var video = document.getElementById('video');
				var hls = new Hls();
				hls.loadSource('http://192.168.0.41/live/stream0/hls.m3u8');
				hls.attachMedia(video);
				hls.on(Hls.Events.MANIFEST_PARSED,function() {
				video.play();
				});
			}
		</script>
    </body>
</html>
  • flv_player 需要在线加载flv.js
<!DOCTYPE html>
<html>

<head>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type">
    <title>flv.js demo</title>
    <style>
        .mainContainer {
    display: block;
    width: 1024px;
    margin-left: auto;
    margin-right: auto;
}

.urlInput {
    display: block;
    width: 100%;
    margin-left: auto;
    margin-right: auto;
    margin-top: 8px;
    margin-bottom: 8px;
}

.centeredVideo {
    display: block;
    width: 100%;
    height: 576px;
    margin-left: auto;
    margin-right: auto;
    margin-bottom: auto;
}

.controls {
    display: block;
    width: 100%;
    text-align: left;
    margin-left: auto;
    margin-right: auto;
}
    </style>
</head>

<body>
    <div class="mainContainer">
        <video id="videoElement" class="centeredVideo" controls autoplay width="1024" height="576">Your browser is too old which doesn't support HTML5 video.</video>
    </div>
    <br>
    <div class="controls">
        <!--<button onclick="flv_load()">加载</button>-->
        <button onclick="flv_start()">开始</button>
        <button onclick="flv_pause()">暂停</button>
        <button onclick="flv_destroy()">停止</button>
        <input style="width:100px" type="text" name="seekpoint" />
        <button onclick="flv_seekto()">跳转</button>
    </div>
    <script src="https://cdn.bootcdn.net/ajax/libs/flv.js/1.5.0/flv.js"></script>
    <script>
        var player = document.getElementById('videoElement');
        if (flvjs.isSupported()) {
            var flvPlayer = flvjs.createPlayer({
                type: 'flv',
                url: 'http://192.168.0.41/test.flv'
            });
            flvPlayer.attachMediaElement(videoElement);
            flvPlayer.load(); //加载
        }

        function flv_start() {
            player.play();
        }

        function flv_pause() {
            player.pause();
        }

        function flv_destroy() {
            player.pause();
            player.unload();
            player.detachMediaElement();
            player.destroy();
            player = null;
        }

        function flv_seekto() {
            player.currentTime = parseFloat(document.getElementsByName('seekpoint')[0].value);
        }
    </script>
</body>
</html>
  • 推流的反馈信息
C:\Users\zhouqinan>ffmpeg -i "rtsp://192.168.0.93:554/user=admin&password=&channel=0&stream=1.sdp?real_stream"  -c copy -f rtsp -rtsp_transport udp rtsp://192.168.0.41:554/live/stream3
ffmpeg version 4.3.1 Copyright (c) 2000-2020 the FFmpeg developers
  built with gcc 10.2.1 (GCC) 20200726
  configuration: --disable-static --enable-shared --enable-gpl --enable-version3 --enable-sdl2 --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libdav1d --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libsrt --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvo-amrwbenc --enable-libmysofa --enable-libspeex --enable-libxvid --enable-libaom --enable-libgsm --enable-librav1e --disable-w32threads --enable-libmfx --enable-ffnvcodec --enable-cuda-llvm --enable-cuvid --enable-d3d11va --enable-nvenc --enable-nvdec --enable-dxva2 --enable-avisynth --enable-libopenmpt --enable-amf
  libavutil      56. 51.100 / 56. 51.100
  libavcodec     58. 91.100 / 58. 91.100
  libavformat    58. 45.100 / 58. 45.100
  libavdevice    58. 10.100 / 58. 10.100
  libavfilter     7. 85.100 /  7. 85.100
  libswscale      5.  7.100 /  5.  7.100
  libswresample   3.  7.100 /  3.  7.100
  libpostproc    55.  7.100 / 55.  7.100
Guessed Channel Layout for Input Stream #0.1 : mono
Input #0, rtsp, from 'rtsp://192.168.0.93:554/user=admin&password=&channel=0&stream=1.sdp?real_stream':
  Metadata:
    title           : RTSP Session
  Duration: N/A, start: 0.000000, bitrate: N/A
    Stream #0:0: Video: h264 (Main), yuv420p(progressive), 704x576, 25 fps, 25 tbr, 90k tbn, 50 tbc
    Stream #0:1: Audio: pcm_alaw, 8000 Hz, mono, s16, 64 kb/s
Output #0, rtsp, to 'rtsp://192.168.0.41:554/live/stream3':
  Metadata:
    title           : RTSP Session
    encoder         : Lavf58.45.100
    Stream #0:0: Video: h264 (Main), yuv420p(progressive), 704x576, q=2-31, 25 fps, 25 tbr, 90k tbn, 25 tbc
    Stream #0:1: Audio: pcm_alaw, 8000 Hz, mono, s16, 64 kb/s
Stream mapping:
  Stream #0:0 -> #0:0 (copy)
  Stream #0:1 -> #0:1 (copy)
Press [q] to stop, [?] for help
[rtsp @ 0000028b22694180] Non-monotonous DTS in output stream 0:0; previous: 21600, current: 3600; changing to 21601. This may result in incorrect timestamps in the output file.
[rtsp @ 0000028b22694180] Non-monotonous DTS in output stream 0:0; previous: 21601, current: 7200; changing to 21602. This may result in incorrect timestamps in the output file.
[rtsp @ 0000028b22694180] Non-monotonous DTS in output stream 0:0; previous: 21602, current: 10800; changing to 21603. This may result in incorrect timestamps in the output file.
[rtsp @ 0000028b22694180] Non-monotonous DTS in output stream 0:0; previous: 21603, current: 14400; changing to 21604. This may result in incorrect timestamps in the output file.
[rtsp @ 0000028b22694180] Non-monotonous DTS in output stream 0:0; previous: 21604, current: 18000; changing to 21605. This may result in incorrect timestamps in the output file.
[rtsp @ 0000028b22694180] Non-monotonous DTS in output stream 0:0; previous: 21605, current: 21600; changing to 21606. This may result in incorrect timestamps in the output file.
frame=  114 fps= 34 q=-1.0 Lsize=N/A time=00:00:04.56 bitrate=N/A speed=1.38x
video:151kB audio:36kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown
Exiting normally, received signal 2.

=========================================================================

编程实践

公网测试流rtsp://34.227.104.115:554/vod/mp4:BigBuckBunny_115k.mp4

lichao, 打开 ios 设备音频输入,不编码,原始音频数据重采样保存到 pcm 文件上

//
//  testc.c
//  myapp
//
//  Created by lichao on 2020/1/30.
//  Copyright © 2020年 lichao. All rights reserved.
//

#include "testc.h"
#include <string.h>

static int rec_status = 0;

void set_status(int status){
    rec_status = status;
}

SwrContext* init_swr(){

    SwrContext *swr_ctx = NULL;

    //channel, number/
    swr_ctx = swr_alloc_set_opts(NULL,                //ctx
                                 AV_CH_LAYOUT_STEREO, //输出channel布局
                                 AV_SAMPLE_FMT_S16,   //输出的采样格式
                                 44100,               //采样率
                                 AV_CH_LAYOUT_STEREO, //输入channel布局
                                 AV_SAMPLE_FMT_FLT,   //输入的采样格式
                                 44100,               //输入的采样率
                                 0, NULL);

    if(!swr_ctx){

    }

    if(swr_init(swr_ctx) < 0){

    }

    return swr_ctx;
}

void rec_audio() {

    int ret = 0;
    char errors[1024] = {0, };

    //重采样缓冲区
    uint8_t **src_data = NULL;
    int src_linesize = 0;

    uint8_t **dst_data = NULL;
    int dst_linesize = 0;

    //ctx
    AVFormatContext *fmt_ctx = NULL;
    AVDictionary *options = NULL;

    //pakcet
    AVPacket pkt;

    //[[video device]:[audio device]]
    char *devicename = ":0";

    //set log level
    av_log_set_level(AV_LOG_DEBUG);

    //start record
    rec_status = 1;

    //register audio device
    avdevice_register_all();

    //get format
    AVInputFormat *iformat = av_find_input_format("avfoundation");

    //open device
    if((ret = avformat_open_input(&fmt_ctx, devicename, iformat, &options)) < 0 ){
        av_strerror(ret, errors, 1024);
        fprintf(stderr, "Failed to open audio device, [%d]%s\n", ret, errors);
        return;
    }

    //create file
    char *out = "/Users/lichao/Downloads/av_base/audio.pcm";
    FILE *outfile = fopen(out, "wb+");

    SwrContext* swr_ctx = init_swr();

    //4096/4=1024/2=512
    //创建输入缓冲区
    av_samples_alloc_array_and_samples(&src_data,         //输出缓冲区地址
                                       &src_linesize,     //缓冲区的大小
                                       2,                 //通道个数
                                       512,               //单通道采样个数
                                       AV_SAMPLE_FMT_FLT, //采样格式
                                       0);

    //创建输出缓冲区
    av_samples_alloc_array_and_samples(&dst_data,         //输出缓冲区地址
                                       &dst_linesize,     //缓冲区的大小
                                       2,                 //通道个数
                                       512,               //单通道采样个数
                                       AV_SAMPLE_FMT_S16, //采样格式
                                       0);
    //read data from device
    while((ret = av_read_frame(fmt_ctx, &pkt)) == 0 &&
          rec_status) {

        av_log(NULL, AV_LOG_INFO,
               "packet size is %d(%p)\n",
               pkt.size, pkt.data);

        //进行内存拷贝,按字节拷贝的
        memcpy((void*)src_data[0], (void*)pkt.data, pkt.size);

        //重采样
        swr_convert(swr_ctx,                    //重采样的上下文
                    dst_data,                   //输出结果缓冲区
                    512,                        //每个通道的采样数
                    (const uint8_t **)src_data, //输入缓冲区
                    512);                       //输入单个通道的采样数

        //write file
        //fwrite(pkt.data, 1, pkt.size, outfile);
        fwrite(dst_data[0], 1, dst_linesize, outfile);
        fflush(outfile);
        av_packet_unref(&pkt); //release pkt
    }

    //close file
    fclose(outfile);

    //释放输入输出缓冲区
    if(src_data){
        av_freep(&src_data[0]);
    }
    av_freep(&src_data);

    if(dst_data){
        av_freep(&dst_data[0]);
    }
    av_freep(&dst_data);

    //释放重采样的上下文
    swr_free(&swr_ctx);

    //close device and release ctx
    avformat_close_input(&fmt_ctx);

    av_log(NULL, AV_LOG_DEBUG, "finish!\n");

    return;
}

#if 0
int main(int argc, char *argv[])
{
    rec_audio();
    return 0;
}
#endif

lichao, 打开 ios 设备音频输入,重采样后编码成 aac 文件

//
//  testc.c
//  myapp
//
//  Created by lichao on 2020/1/30.
//  Copyright © 2020年 lichao. All rights reserved.
//

#include "testc.h"
#include <string.h>

static int rec_status = 0;

void set_status(int status){
    rec_status = status;
}

//[in]
//[out]
//ret
//@brief encode audio data
static void encode(AVCodecContext *ctx,
            AVFrame *frame,
            AVPacket *pkt,
            FILE *output){

    int ret = 0;

    //将数据送编码器
    ret = avcodec_send_frame(ctx, frame);

    //如果ret>=0说明数据设置成功
    while(ret >= 0){
        //获取编码后的音频数据,如果成功,需要重复获取,直到失败为止
        ret = avcodec_receive_packet(ctx, pkt);

        if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF){
            return;
        }else if( ret < 0){
            printf("Error, encoding audio frame\n");
            exit(-1);
        }

        //write file
        fwrite(pkt->data, 1, pkt->size, output);
        fflush(output);
    }

    return;
}

//[in]
//[out]
//
static AVCodecContext* open_coder(){

    //打开编码器
    //avcodec_find_encoder(AV_CODEC_ID_AAC);
    AVCodec *codec = avcodec_find_encoder_by_name("libfdk_aac");

    //创建 codec 上下文
    AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);

    codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16;          //输入音频的采样大小
    codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;    //输入音频的channel layout
    codec_ctx->channels = 2;                            //输入音频 channel 个数
    codec_ctx->sample_rate = 44100;                     //输入音频的采样率
    codec_ctx->bit_rate = 0; //AAC_LC: 128K, AAC HE: 64K, AAC HE V2: 32K
    codec_ctx->profile = FF_PROFILE_AAC_HE_V2; //阅读 ffmpeg 代码

    //打开编码器
    if(avcodec_open2(codec_ctx, codec, NULL)<0){
        //

        return NULL;
    }

    return codec_ctx;
}

static
SwrContext* init_swr(){

    SwrContext *swr_ctx = NULL;

    //channel, number/
    swr_ctx = swr_alloc_set_opts(NULL,                //ctx
                                 AV_CH_LAYOUT_STEREO, //输出channel布局
                                 AV_SAMPLE_FMT_S16,   //输出的采样格式
                                 44100,               //采样率
                                 AV_CH_LAYOUT_STEREO, //输入channel布局
                                 AV_SAMPLE_FMT_FLT,   //输入的采样格式
                                 44100,               //输入的采样率
                                 0, NULL);

    if(!swr_ctx){

    }

    if(swr_init(swr_ctx) < 0){

    }

    return swr_ctx;
}

/**
  * @brief open audio device
  * @return succ: AVFormatContext*, fail: NULL
  */
static
AVFormatContext* open_dev(){

    int ret = 0;
    char errors[1024] = {0, };

    //ctx
    AVFormatContext *fmt_ctx = NULL;
    AVDictionary *options = NULL;

    //[[video device]:[audio device]]
    char *devicename = ":0";

    //get format
    AVInputFormat *iformat = av_find_input_format("avfoundation");

    //open device
    if((ret = avformat_open_input(&fmt_ctx, devicename, iformat, &options)) < 0 ){
        av_strerror(ret, errors, 1024);
        fprintf(stderr, "Failed to open audio device, [%d]%s\n", ret, errors);
        return NULL;
    }

    return fmt_ctx;
}

/**
 * @brief xxxx
 * @return xxx
 */
static
AVFrame* create_frame(){

    AVFrame *frame = NULL;

    //音频输入数据
    frame = av_frame_alloc();
    if(!frame){
        printf("Error, No Memory!\n");
        goto __ERROR;
    }

    //set parameters
    frame->nb_samples     = 512;                //单通道一个音频帧的采样数
    frame->format         = AV_SAMPLE_FMT_S16;  //每个采样的大小
    frame->channel_layout = AV_CH_LAYOUT_STEREO; //channel layout

    //alloc inner memory
    av_frame_get_buffer(frame, 0); // 512 * 2 * = 2048
    if(!frame->data[0]){
        printf("Error, Failed to alloc buf in frame!\n");
        //内存泄漏
        goto __ERROR;
    }

    return frame;

__ERROR:
    if(frame){
        av_frame_free(&frame);
    }

    return NULL;
}

static
void alloc_data_4_resample(uint8_t ***src_data,
                           int *src_linesize,
                           uint8_t *** dst_data,
                           int *dst_linesize){
    //4096/4=1024/2=512
    //创建输入缓冲区
    av_samples_alloc_array_and_samples(src_data,         //输出缓冲区地址
                                       src_linesize,     //缓冲区的大小
                                       2,                 //通道个数
                                       512,               //单通道采样个数
                                       AV_SAMPLE_FMT_FLT, //采样格式
                                       0);

    //创建输出缓冲区
    av_samples_alloc_array_and_samples(dst_data,         //输出缓冲区地址
                                       dst_linesize,     //缓冲区的大小
                                       2,                 //通道个数
                                       512,               //单通道采样个数
                                       AV_SAMPLE_FMT_S16, //采样格式
                                       0);
}

/**
 */
static
void free_data_4_resample(uint8_t **src_data, uint8_t **dst_data){
    //释放输入输出缓冲区
    if(src_data){
        av_freep(&src_data[0]);
    }
    av_freep(&src_data);

    if(dst_data){
        av_freep(&dst_data[0]);
    }
    av_freep(&dst_data);
}

/**
 */
static
void read_data_and_encode(AVFormatContext *fmt_ctx, //
                          AVCodecContext *c_ctx,
                          SwrContext* swr_ctx,
                          FILE *outfile){

    int ret = 0;

    //pakcet
    AVPacket pkt;
    AVFrame *frame = NULL;
    AVPacket *newpkt = NULL;

    //重采样缓冲区
    uint8_t **src_data = NULL;
    int src_linesize = 0;

    uint8_t **dst_data = NULL;
    int dst_linesize = 0;

    frame = create_frame();
    if(!frame){
        //printf(...)
        goto __ERROR;
    }

    newpkt = av_packet_alloc(); //分配编码后的数据空间
    if(!newpkt){
        printf("Error, Failed to alloc buf in frame!\n");
        goto __ERROR;
    }

    //分配重采样输入/输出缓冲区
    alloc_data_4_resample(&src_data, &src_linesize, &dst_data, &dst_linesize);

    //read data from device
    while((ret = av_read_frame(fmt_ctx, &pkt)) == 0 && rec_status) {

        //进行内存拷贝,按字节拷贝的
        memcpy((void*)src_data[0], (void*)pkt.data, pkt.size);

        //重采样
        swr_convert(swr_ctx,                    //重采样的上下文
                    dst_data,                   //输出结果缓冲区
                    512,                        //每个通道的采样数
                    (const uint8_t **)src_data, //输入缓冲区
                    512);                       //输入单个通道的采样数

        //将重采样的数据拷贝到 frame 中
        memcpy((void *)frame->data[0], dst_data[0], dst_linesize);

        //encode
        encode(c_ctx, frame, newpkt, outfile);

        //
        av_packet_unref(&pkt); //release pkt
    }

    //强制将编码器缓冲区中的音频进行编码输出
    encode(c_ctx, NULL, newpkt, outfile);

__ERROR:
    //释放 AVFrame 和 AVPacket
    if(frame){
        av_frame_free(&frame);
    }

    if(newpkt){
        av_packet_free(&newpkt);
    }

    //释放重采样缓冲区
    free_data_4_resample(src_data, dst_data);
}

void rec_audio() {

    //context
    AVFormatContext *fmt_ctx = NULL;
    AVCodecContext *c_ctx = NULL;
    SwrContext* swr_ctx = NULL;

    //set log level
    av_log_set_level(AV_LOG_DEBUG);

    //register audio device
    avdevice_register_all();

    //start record
    rec_status = 1;

    //create file
    //char *out = "/Users/lichao/Downloads/av_base/audio.pcm";
    char *out = "/Users/lichao/Downloads/av_base/audio.aac";
    FILE *outfile = fopen(out, "wb+");
    if(!outfile){
        printf("Error, Failed to open file!\n");
        goto __ERROR;
    }

    //打开设备
    fmt_ctx = open_dev();
    if(!fmt_ctx){
        printf("Error, Failed to open device!\n");
        goto __ERROR;
    }

    //打开编码器上下文
    c_ctx = open_coder();
    if(!c_ctx){
        printf("...");
        goto __ERROR;
    }

    //初始化重采样上下文
    swr_ctx = init_swr();
    if(!swr_ctx){
        printf("Error, Failed to alloc buf in frame!\n");
        goto __ERROR;
    }

    //encode
    read_data_and_encode(fmt_ctx, c_ctx, swr_ctx, outfile);

__ERROR:
    //释放重采样的上下文
    if(swr_ctx){
        swr_free(&swr_ctx);
    }

    if(c_ctx){
        avcodec_free_context(&c_ctx);
    }

    //close device and release ctx
    if(fmt_ctx) {
        avformat_close_input(&fmt_ctx);
    }

    if(outfile){
        //close file
        fclose(outfile);
    }

    av_log(NULL, AV_LOG_DEBUG, "finish!\n");

    return;
}

#if 0
int main(int argc, char *argv[])
{
    rec_audio();
    return 0;
}
#endif

首先不知道ffmpegAVDictionary怎么设置(这个上下文是自己创建的还是ffmpeg中的变量,从代码上看不是自己申请的);而且还不知道向哪个上下文设置,是AVFormatContext还是AVCodec?也不知道有哪些键值对可以设置?

参考博客:FFMPEG Tips (5) 如何利用 AVDictionary 配置参数 - 知乎 (zhihu.com)
自己总结的三个文件可以看到设置的健有哪些,类型为AVOption的静态数组

  • libavcodec\options_table.h
  • libavformat\options_table.h
  • libavformat\rtsp.c
  • 比如有这些设置
    案例一:
    void FFmpegThread::initOption()
    {
        //设置缓存大小,1080p可将值调大 
        av_dict_set(&options, "buffer_size", "8192000", 0); 
        //以tcp方式打开,如果以udp方式打开将tcp替换为udp 
        av_dict_set(&options, "rtsp_transport", transport.toLatin1().constData(), 0); 
        //设置超时断开连接时间,单位微秒,3000000表示3秒 
        av_dict_set(&options, "stimeout", "3000000", 0); 
        //设置最大时延,单位微秒,1000000表示1秒 
        av_dict_set(&options, "max_delay", "1000000", 0); 
        //自动开启线程数 
        av_dict_set(&options, "threads", "auto", 0);  
        //设置USB摄像机分辨率 
        if (url.startsWith("video")) 
        { 
            QString size = QString("%1x%2").arg(videoWidth).arg(videoHeight);
            av_dict_set(&options, "video_size", size.toLatin1().constData(), 0); 
        }    
    }
    
    bool FFmpegThread::initInput()
    {
        //实例化格式处理上下文
        formatCtx = avformat_alloc_context();
    
        //先判断是否是本地设备(video=设备名字符串),打开的方式不一样
        int result = -1;
        if (url.startsWith("video")) {
    #if defined(Q_OS_WIN)
            AVInputFormat *ifmt = av_find_input_format("dshow");
    #elif defined(Q_OS_LINUX)
            AVInputFormat *ifmt = av_find_input_format("video4linux2");
    #elif defined(Q_OS_MAC)
            AVInputFormat *ifmt = av_find_input_format("avfoundation");
    #endif
            result = avformat_open_input(&formatCtx, url.toStdString().data(), ifmt, &options);
        } else {
            result = avformat_open_input(&formatCtx, url.toStdString().data(), NULL, &options);
        }
    
        if (result < 0) {
            qDebug() << TIMEMS << "open input error" << url;
            return false;
        }
    
        //释放设置参数
        if (options != NULL) {
            av_dict_free(&options);
        }
    
        //获取流信息
        result = avformat_find_stream_info(formatCtx, NULL);
        if (result < 0) {
            qDebug() << TIMEMS << "find stream info error";
            return false;
        }
    
        return true;
    }
    

yuv420sp(前置条件:要知到是 nv12 还是 nv21,以华为sdc相机为例,直接拿到 nv21) 转成 bgr,转得正确否以实际imwrite得到的图像为准,这么测试,一下就知道了

    auto rows = input.height;  
    auto cols = input.width;
    cv::Mat yuv420sp(rows * 3 / 2, cols, CV_8UC1, input.data[0]);
    cv::Mat bgr(input.height, input.width, CV_8UC3);  
    cv::cvtColor(yuv420sp, bgr, cv::COLOR_YUV2BGR_NV21);  
    cv::imwrite("aft_post.jpg", bgr);

要想得到nv21,曲线救国: .jpg --> I420 --> 手工调整 u v 分量(把uuu vvv 平行,调整成 vuvuvu 交错)

static void TransFormJpg2Yuv(const cv::Mat &mat_bgr, unsigned char *yuv_nv21)
{
    int w = mat_bgr.cols;
    int h = mat_bgr.rows;
    int wh = w * h;
    int uv_len = wh / 2;
    uint8_t *yuv = (uint8_t *)malloc(uv_len); 

    int imageLength = wh * 3 / 2; 
    cv::Mat out;
    cv::cvtColor(mat_bgr, out, cv::COLOR_BGR2YUV_IYUV);             
    memcpy(yuv_nv21, out.data, wh * sizeof(unsigned char));     
    memcpy(yuv, out.data + wh, uv_len * sizeof(unsigned char)); 
    int num = 0;                                                
    for (int j = wh; j != imageLength; j += 2)
    {
        yuv_nv21[j + 1] = yuv[num];      
        yuv_nv21[j] = yuv[wh / 4 + num]; 
        ++num;
    }
    free(yuv);
}

static bool Adjust(const cv::Mat& mat_bgr, std::vector<uint8_t>& vec_yuv)
{       
    if (mat_bgr.empty())
    {
        std::cout<<"read image failed !"<<std::endl;
        return -1;
    }
    uint64_t w = 0;
    uint64_t h = 0;
    if (mat_bgr.cols % 16 != 0 || mat_bgr.rows % 16 != 0)
    {
        w = mat_bgr.cols / 16 * 16;  
        h = mat_bgr.rows / 16 * 16;   
        cv::resize(mat_bgr, mat_bgr, cv::Size(w, h));
    }
    else
    {
        w = mat_bgr.cols;
        h = mat_bgr.rows;
    }

        vec_yuv.resize(w * h * 3 / 2);    

    TransFormJpg2Yuv(mat_bgr, &vec_yuv[0]);

    return 0;
}

熟悉了 yuv420p 和 nv12(yuv420sp的一种)的结构后自己实现,bgr --> nv12

// utils  
bool Align(const cv::Mat& img, cv::Mat& img_align, uint8_t align = 16)
{
    if (img.empty())
    {
        AI_Log("DevFrame", KLERROR, "in Align input img is empty");
        return false;
    }

    uint64_t w = 0;
    uint64_t h = 0;
    if (img.cols % align != 0 || img.rows % align != 0)
    {
        w = img.cols / align * align; 
        h = img.rows / align * align; 
        cv::resize(img, img_align, cv::Size(w, h));
    }
    else    img_align = img;
    
    return true;
}

static void BGR2NV12(const cv::Mat& img, std::unique_ptr<uint8_t[]>& unp_data)
{
    auto w = img.cols;
    auto h = img.rows;
    auto wh = w * h;
    auto img_size = wh * 3 / 2; 

    uint8_t* p_nv12 = new uint8_t[img_size];
    unp_data.reset(p_nv12);          
    
    cv::Mat yuv420p;
    // Enum value, cv::COLOR_BGR2YUV_IYUV same as cv::COLOR_BGR2YUV_I420
    cv::cvtColor(img, yuv420p, cv::COLOR_BGR2YUV_IYUV);             
    if(!yuv420p.isContinuous())
    {
        yuv420p = yuv420p.clone();
    }

    uint8_t* p_yuv420p = yuv420p.data;
    
    // Copy Y
    memcpy(p_nv12, p_yuv420p, wh);   

    // Copy uv
    uint32_t uv_offset = wh;
    uint32_t num = 0;    
    for (int j = wh; j != img_size; j += 2)
    {
        p_nv12[j] = p_yuv420p[uv_offset + num];      
        p_nv12[j + 1] = p_yuv420p[uv_offset + wh / 4 + num]; 
        ++num;
    } 
}

librtmp puller

#include "rtmp_imple.h"  
#include "ai_log.h"

RtmpImple::~RtmpImple()
{
    RTMPPacket_Free(&packet_);
}

bool RtmpImple::Init(const std::string& url, uint32_t time_out)
{   
    url_ = url;  

    bool flag_init_success{false};  

    do
    {
        sp_rtmp_.reset(RTMP_Alloc(), [](RTMP* p_rtmp){  RTMP_Close(p_rtmp);
                                                        RTMP_Free(p_rtmp);
                                                        });    
        if(!sp_rtmp_.get())
        {        
            AI_Log("RtmpImple", KLERROR, " alloc RTMP context failed");                                                      
            break;
        }   

        RTMP_Init(sp_rtmp_.get());                

        if(FALSE == RTMP_SetupURL(sp_rtmp_.get(), url_.c_str()))
        {
            AI_Log("RtmpImple", KLERROR, " RTMP set server url:{} failed", url_);                                                      
            break;
        }        
        sp_rtmp_->Link.timeout = time_out;
        sp_rtmp_->Link.lFlags |= RTMP_LF_LIVE;        

        flag_init_success = true;
    } while (false);    

    flag_init_success ? AI_Log("RtmpImple", KLINFO, " RtmpImple init success, url:{} ", url_) : AI_Log("RtmpImple", KLERROR, " RtmpImple init failed, url:{} ", url_);  

    return flag_init_success;
}

bool RtmpImple::Connect()
{
    auto flag_connected{false};

    do
    {
        if(FALSE == RTMP_Connect(sp_rtmp_.get(), NULL)) 
        {
            AI_Log("RtmpImple", KLERROR, " RTMP try connect server {} failed", url_);                                                      
            break;
        }

        if(FALSE == RTMP_ConnectStream(sp_rtmp_.get(), 0)) 
        {
            AI_Log("RtmpImple", KLERROR, " RTMP connect stream failed, url:{}", url_);                                                      
            break;
        }

        if(FALSE == RTMP_IsConnected(sp_rtmp_.get())) 
        {
            AI_Log("RtmpImple", KLERROR, " RTMP check connect failed, url:{}", url_);                                                      
            break;
        }

        flag_connected = true;
    } while (false);

    flag_connected ? AI_Log("RtmpImple", KLINFO, " RtmpImple connect success, url:{}", url_) : AI_Log("RtmpImple", KLERROR, " RtmpImple connect failed, url:{}", url_);   
    if(flag_connected)
    {
        // init codec 
        call_back_hanlde_codec_("H264/90000");  
    }

    return flag_connected;
}  

bool RtmpImple::CheckConneted()
{    
    auto result = RTMP_IsConnected(sp_rtmp_.get());  
    auto ret = (result == TRUE)? true : false;
    return ret;
}

bool RtmpImple::GetNALU()
{        
    RTMPPacket_Reset(&packet_);  
    packet_.m_body = NULL;
    packet_.m_chunk = NULL;  

    while (RTMP_IsConnected(sp_rtmp_.get()) && (TRUE == RTMP_ReadPacket(sp_rtmp_.get(), &packet_)))
    {
        if (!RTMPPacket_IsReady(&packet_))   continue;  
        else break;              
    }

    auto result = RTMP_ClientPacket(sp_rtmp_.get(), &packet_);    
    auto ret = (TRUE==result)? true: false;
    if(!ret)    return false;  

    if (packet_.m_packetType == RTMP_PACKET_TYPE_VIDEO) 
    {
                bool keyframe = 0x17 == packet_.m_body[0] ? true : false;
                bool sequence = 0x00 == packet_.m_body[1];

                // printf("keyframe=%s, sequence=%s\n", keyframe ? "true" : "false", sequence ? "true" : "false");

        const auto len_nalu_header = sizeof(nalu_header_) / sizeof(uint8_t);
        // SPS/PPS sequence
                if (sequence) 
        {
                        uint32_t offset = 10;
            uint32_t sps_len{0};
                        uint32_t sps_num = packet_.m_body[offset++] & 0x1f;  
            std::shared_ptr<uint8_t[]> sp_buf_sps;
                        for (int i = 0; i < sps_num; i++) 
            {
                                uint8_t ch0 = packet_.m_body[offset];
                                uint8_t ch1 = packet_.m_body[offset + 1];
                                sps_len = ((ch0 << 8) | ch1);
                                offset += 2;
                                // Write sps data
                                // fwrite(nalu_header, sizeof(uint8_t), 4, _file_ptr);
                                // fwrite(packet.m_body + offset, sizeof(uint8_t), sps_len, _file_ptr);                
                sp_buf_sps = std::shared_ptr<uint8_t[]>(new uint8_t[len_nalu_header + sps_len]);   
                memcpy(sp_buf_sps.get(), &nalu_header_[0], len_nalu_header); 
                memcpy(sp_buf_sps.get() + len_nalu_header, packet_.m_body + offset, sps_len);
                offset += sps_len;
                        }
            AI_Log("RtmpImple", KLINFO, " sps_num:{}", sps_num);      


            uint32_t pps_len{0};
                        uint32_t pps_num = packet_.m_body[offset++] & 0x1f;
            std::shared_ptr<uint8_t[]> sp_buf_pps;
                        for (int i = 0; i < pps_num; i++) 
            {
                                uint8_t ch0 = packet_.m_body[offset];
                                uint8_t ch1 = packet_.m_body[offset + 1];
                                pps_len = ((ch0 << 8) | ch1);
                                offset += 2;
                                // Write pps data
                                // fwrite(nalu_header, sizeof(uint8_t), 4, _file_ptr);
                                // fwrite(packet.m_body + offset, sizeof(uint8_t), pps_len, _file_ptr);

                sp_buf_pps = std::shared_ptr<uint8_t[]>(new uint8_t [len_nalu_header + pps_len]); 
                memcpy(sp_buf_pps.get(), &nalu_header_[0], len_nalu_header); 
                memcpy(sp_buf_pps.get() + len_nalu_header, packet_.m_body + offset, pps_len); 
                offset += pps_len;  
                        }

            auto len_sps_pps_2naluheader = sps_len + pps_len + 2*len_nalu_header;
            std::shared_ptr<uint8_t[]> sp_buf(new uint8_t[len_sps_pps_2naluheader]);  
            memcpy(sp_buf.get(),                                sp_buf_sps.get(), sps_len + len_nalu_header);  
            memcpy(sp_buf.get() + sps_len + len_nalu_header,    sp_buf_pps.get(), pps_len + len_nalu_header);  

            call_back_hanlde_sps_pps_(sp_buf.get(), len_sps_pps_2naluheader, 0, 0); 
            AI_Log("RtmpImple", KLINFO, " pps_num:{}", pps_num);                          
                }
                // Nalu frames
                else 
        {
                        uint32_t offset = 5;
                        uint8_t ch0 = packet_.m_body[offset];
                        uint8_t ch1 = packet_.m_body[offset + 1];
                        uint8_t ch2 = packet_.m_body[offset + 2];
                        uint8_t ch3 = packet_.m_body[offset + 3];
                        uint32_t data_len = ((ch0 << 24) | (ch1 << 16) | (ch2 << 8) | ch3);
                        offset += 4;
                        // Write nalu data(already started with '0x00,0x00,0x00,0x01')
                        // fwrite(nalu_header, sizeof(uint8_t), 4, _file_ptr);
                        // fwrite(packet.m_body + offset, sizeof(uint8_t), data_len, _file_ptr);
                        // offset += data_len;  

            if(packet_.m_nBodySize >= 9)    data_len = packet_.m_nBodySize - 9;    
            else return false;

            std::shared_ptr<uint8_t[]> sp_buf(new uint8_t[data_len]);  
            memcpy(sp_buf.get(), packet_.m_body + offset, data_len);
            call_back_hanlde_frame_(sp_buf.get(), data_len, packet_.m_nTimeStamp);
            // AI_Log("RtmpImple", KLINFO, " get Nalu frame once, timestamp: {}", packet_.m_nTimeStamp);     
                }
    }

    RTMPPacket_Free(&packet_);    

    return true;
}  

void RtmpImple::BindHandleFrame(std::function<void(uint8_t* data, uint64_t size, uint32_t pkt_ts)> call_back_hanlde_frame)
{
    call_back_hanlde_frame_ = call_back_hanlde_frame;
}

void RtmpImple::BindHandleSPSPPS(std::function<void(uint8_t* data, uint64_t size, uint32_t width, uint32_t height)> call_back_hanlde_sps_pps)
{
    call_back_hanlde_sps_pps_ = call_back_hanlde_sps_pps;
}

void RtmpImple::BindHandleCODEC(std::function<void(const std::string& codec)> call_back_hanlde_codec)
{
    call_back_hanlde_codec_ = call_back_hanlde_codec;
}

最简单的win socket 示例:c.biancheng.net/cpp/html/30…

下载可执行的windows ffmpeg

Windows下下载安装ffmpeg - 知乎 (zhihu.com)

收藏的网页,我没时间看这些,先记下吧

mat 的内存布局,涉及到 step size elemSize