H264-YUV通过RTP接收视频流ffmpeg解码SDL实时播放
写在前面的话
写一个简单的播放器,通过RTP接收视频流,进行实时播放。最初,使用ffplay或者vlc接收按照SDP协议文件可以播放视频,但是视频中断后重启,不能正确的解包,时常会出现如下的错误信息。
[sdp @ 0x7ffb35034e00] RTP: dropping old packet received too late
Last message repeated 15 times
使用ffplay播放udp视频。
➜ view-show ffplay -protocol_whitelist "file,http,https,rtp,udp,tcp,tls" test.sdp
➜ view-show cat test.sdp
m=video 6665 RTP/AVP 96
a=rtpmap:96 H264/90000
c=IN IP4 0.0.0.0
分析ffmpeg原代码,在rtpdec.c代码文件中,这个错误的原因是ffplay接收RTP视频流时,如果前一个RTP包的流水号大于后一个帧时,会将当前RTP报文丢弃。
static int rtp_parse_one_packet(RTPDemuxContext *s, AVPacket *pkt, uint8_t **bufptr, int len)
{
...
if ((s->seq == 0 && !s->queue) || s->queue_size <= 1) {
/* First packet, or no reordering */
return rtp_parse_packet_internal(s, pkt, buf, len);
} else {
uint16_t seq = AV_RB16(buf + 2);
int16_t diff = seq - s->seq;
if (diff < 0) {
/* 注意看这里 Packet older than the previously emitted one, drop */ av_log(s->ic, AV_LOG_WARNING,
"RTP: dropping old packet received too late\n");
return -1;
} else if (diff <= 1) {
/* Correct packet */
rv = rtp_parse_packet_internal(s, pkt, buf, len);
return rv;
} else {
/* Still missing some packet, enqueue this one. */
rv = enqueue_packet(s, buf, len);
if (rv < 0)
return rv;
*bufptr = NULL;
/* Return the first enqueued packet if the queue is full,
* even if we're missing something */
if (s->queue_len >= s->queue_size) {
av_log(s->ic, AV_LOG_WARNING, "jitter buffer full\n");
return rtp_parse_queued_packet(s, pkt);
}
return -1;
}
}
}
但是,实际的业务场合中,对于一个大的视频文件,会按照MTU(以太网1500)拆分成很多个RTP报文(1400大小),多帧视频拆包的个数远超过RTP协议中的流水号限制 65536。
为了绕开这个问题,计划重新做一个简单的视频播放器,功能类似于ffplay接收UDP报文,然后播放。
总体视频传输流程

使用的库简要说明
| 库 | 说明和作用 |
| opencv | 摄像头或视频文件读取,图像处理 |
| x264 |
H264视频编码 |
| ffmpeg | H264视频解码 |
| SDL2 | 视频播放 |
| RTP | RTP视频流打包 |
细节分析
Opencv-x264-RTP视频编码流程

通过OpenCV读取视频文件代码
void handleVideo(const char* pFileName) {
Mat frame;
cv::VideoCapture capture(pFileName);
while (true) {
capture >> frame;
if (frame.empty()) {
break;
}
STREAM_PUSH_INS->push(&frame);
}
}
通过x264编码视频帧代码
bool X264Encoder::EncodeOneBuf(cv::Mat *yuvMat, Str *resStr) {
TimeMeasurer tm;
memset(yuv_buffer_, 0, m_width * m_height * 3);
uint8_t* yuv_buffer =(uint8_t*) yuvMat->data;
memcpy(picture_in_.img.plane[0], yuv_buffer, m_width*m_height);
yuv_buffer += m_width*m_height;
memcpy(picture_in_.img.plane[1], yuv_buffer, m_width*m_height / 4);
yuv_buffer += m_width*m_height / 4;
memcpy(picture_in_.img.plane[2], yuv_buffer, m_width*m_height / 4);
picture_in_.i_type = X264_TYPE_IDR;
int64_t i_pts = 0;
picture_in_.i_pts = i_pts++;
x264_nal_t *nals;
int nnal;
int h264size = 0;
x264_picture_t pic_out;
x264_picture_init(&pic_out);
x264_encoder_encode(x264_encoder_, &nals, &nnal, &picture_in_, &pic_out);
x264_nal_t *nal;
for (nal = nals; nal < nals + nnal; nal++) {
memcpy((char*)resStr->data + h264size,nal->p_payload,nal->i_payload);
h264size = h264size + nal->i_payload;
}
resStr->size = h264size;
LOG_INFO("x264.encode.cost: %lu", tm.Elapsed());
return true;
}
YUV格式分析
YUV编码中使用IYUV,也叫YUV420p或者I420,
如下是YUV420p的数据格式。

YUV420P分Y,U,V三个分量
U分量紧跟在Y分量之后,接着V分量(即:YUV)
因此数据格式为 YYYY YYYY UU VV
RTP协议定义
typedef struct rtp_header {
/* little-endian */
/* byte 0 */
uint8_t csrc_len: 4; /* bit: 0~3 */
uint8_t extension: 1; /* bit: 4 */
uint8_t padding: 1; /* bit: 5*/
uint8_t version: 2; /* bit: 6~7 */
/* byte 1 */
uint8_t payload_type: 7; /* bit: 0~6 */
uint8_t marker: 1; /* bit: 7 */
/* bytes 2, 3 */
uint16_t seq_no;
/* bytes 4-7 */
uint32_t timestamp;
/* bytes 8-11 */
uint32_t ssrc;
} __attribute__ ((packed)) rtp_header_t; /* 12 bytes */
ffmpeg做H264视频解码分析

ffmpeg做H264解码初始化代码
AVCodec *gCodec = NULL;
AVCodecContext *gCodec_ctx = NULL;
AVCodecParserContext *gParser = NULL;
AVFrame *gAVFrame = NULL; void doAVCodecInit() {
avcodec_register(&ff_h264_decoder);
av_register_codec_parser(&ff_h264_parser); gCodec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!gCodec) {
fprintf(stderr, "Codec not found\n");
exit(1);
} gCodec_ctx = avcodec_alloc_context3(gCodec);
if (!gCodec_ctx) {
fprintf(stderr, "Could not allocate video codec context\n");
exit(1);
} if (avcodec_open2(gCodec_ctx, gCodec, NULL) < 0) {
fprintf(stderr, "Could not open codec\n");
exit(1);
} gParser = av_parser_init(AV_CODEC_ID_H264);
if (!gParser) {
fprintf(stderr, "Could not create H264 parser\n");
exit(1);
} gAVFrame = av_frame_alloc();
if (!gAVFrame) {
fprintf(stderr, "Could not allocate video frame\n");
exit(1);
} initPacket();
}
ffmpeg循环做H264解码代码
static int doDecodeFrame(AVPacket *pkt, unsigned int frame_index) {
int got_frame = 0;
do {
int len = avcodec_decode_video2(gCodec_ctx, gAVFrame, &got_frame, pkt);
if (len < 0) {
fprintf(stderr, "Error while decoding frame %d\n", frame_index);
return len;
}
if (got_frame) {
//printf("Got frame %d\n", frame_index);
//fflush(stdout);
yuv_show(gAVFrame->data, gAVFrame->linesize, gAVFrame->width, gAVFrame->height);
}
} while (0);
return 0;
}
int doPackDecode(struct ImagePacket *packetPtr) {
uint8_t *data = NULL;
int size = 0;
int bytes_used = av_parser_parse2(gParser, gCodec_ctx, &data, &size, packetPtr->buf_, packetPtr->len_, 0, 0,
AV_NOPTS_VALUE);
if (size == 0) {
return -1;
}
// We have data of one packet, decode it; or decode whatever when ending
AVPacket packet;
av_init_packet(&packet);
packet.data = data;
packet.size = size;
int ret = doDecodeFrame(&packet, packetPtr->frame_index_);
if (ret < 0) {
return -1;
}
return 0;
}
SDL视频渲染流程

SDL初始化代码
#define LOAD_YUV420P 0 #define HAS_BORDER 1 const int bpp = 12; const int screen_w = 1434, screen_h = 806;
const int pixel_w = 1434, pixel_h = 806; //const int screen_w=1920,screen_h=1080;
//const int pixel_w=1920,pixel_h=1080; SDL_Window *gScreen = NULL;
SDL_Renderer *gSdlRenderer = NULL;
SDL_Texture *gSdlTexture = NULL;
SDL_Rect sdlRect; //Refresh Event
#define REFRESH_EVENT (SDL_USEREVENT + 1) int thread_exit = 0; int refresh_video(void *opaque) {
while (thread_exit == 0) {
SDL_Event event;
event.type = REFRESH_EVENT;
SDL_PushEvent(&event);
SDL_Delay(10);
}
return 0;
} int doSDLInit() {
if (SDL_Init(SDL_INIT_EVERYTHING)) {
printf("Could not initialize SDL - %s\n", SDL_GetError());
return -1;
} //SDL 2.0 Support for multiple windows
gScreen = SDL_CreateWindow("Video-View", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
screen_w, screen_h, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (!gScreen) {
printf("SDL: could not create window - exiting:%s\n", SDL_GetError());
return -1;
}
gSdlRenderer = SDL_CreateRenderer(gScreen, -1, 0);
int pixformat = SDL_PIXELFORMAT_IYUV; gSdlTexture = SDL_CreateTexture(gSdlRenderer, pixformat, SDL_TEXTUREACCESS_STREAMING, pixel_w, pixel_h); int border = 0; sdlRect.x = 0 + border;
sdlRect.y = 0 + border;
sdlRect.w = screen_w - border * 2;
sdlRect.h = screen_h - border * 2; SDL_Thread *refresh_thread = SDL_CreateThread(refresh_video, NULL, NULL);
return 0;
}
SDL循环渲染代码
SDL_Event event;
SDL_WaitEvent(&event);
if (event.type == REFRESH_EVENT) {
SDL_UpdateTexture(gSdlTexture, NULL, gImagePacket->buf_, pixel_w);
SDL_RenderClear(gSdlRenderer);
SDL_RenderCopy(gSdlRenderer, gSdlTexture, NULL, &sdlRect);
SDL_RenderPresent(gSdlRenderer);
//Delay 40ms
SDL_Delay(1);
} else if (event.type == SDL_QUIT) {
_exit(0);
}
参考:https://blog.csdn.net/leixiaohua1020/article/details/40525591/
发送和接收总的流程如下图所示

视频源码请参考我的github项目。
接收端启动方式:
./viewer 6665
发送端启动方式
/bin/video-pusher 127.0.0.1 6665 ./test-video/d001.mp4
测试视频使用1280*720的视频文件。可以找一段1280*720的视频验证。
视频读取编码和发送:
https://github.com/gityf/img-video/tree/master/video/opencv-x264-rtp-pusher
视频接收解密和播放:
https://github.com/gityf/img-video/tree/master/video/ffmpeg-h264-sdl-view
结束,祝玩的开心!
H264-YUV通过RTP接收视频流ffmpeg解码SDL实时播放的更多相关文章
- ffmpeg结合SDL编写播放器(一)
ffmpeg 工具是一个高效快速的命令行工具,进行视音频不同格式之间的转换. ffmpeg命令行 ffmpeg可以读取任意数量的输入“文件”(可以是常规文件,管道,网络流,抓取设备等)读取,由 -i ...
- ffmpeg结合SDL编写播放器(三)
接下来是解析影片的帧 /*** project.c ***/ #include<stdio.h> #include<libavcodec/avcodec.h> #include ...
- ffmpeg结合SDL编写播放器
创建播放窗口 SDL_Surface *screen = NULL; screen = SDL_SetVideoMode(pCodecCtx->width, pCodecCtx->heig ...
- ffmpeg结合SDL编写播放器(二)
我们将对帧数据做一些处理,比如将每一帧的 图像转为jpg或者bmp或者ppm等格式保存下来. 举例:在ffmpeg-2.8.8文件夹下编写test.c程序 /* test.c */ #include& ...
- 在iOS平台使用ffmpeg解码h264视频流(转)
在iOS平台使用ffmpeg解码h264视频流,有需要的朋友可以参考下. 对于视频文件和rtsp之类的主流视频传输协议,ffmpeg提供avformat_open_input接口,直接将文件路径或UR ...
- 在iOS平台使用ffmpeg解码h264视频流
来源:http://www.aichengxu.com/view/37145 在iOS平台使用ffmpeg解码h264视频流,有需要的朋友可以参考下. 对于视频文件和rtsp之类的主流视频传输协议,f ...
- 使用X264编码yuv格式的视频帧使用ffmpeg解码h264视频帧
前面一篇博客介绍在centos上搭建点击打开链接ffmpeg及x264开发环境.以下就来问个样例: 1.利用x264库将YUV格式视频文件编码为h264格式视频文件 2.利用ffmpeh库将h264格 ...
- FFmpeg解码H264及swscale缩放详解
本文概要: 本文介绍著名开源音视频编解码库ffmpeg如何解码h264码流,比较详细阐述了其h264码流输入过程,解码原理,解码过程.同时,大部分应用环境下,以原始码流视频大小展示并不是最佳方式,因此 ...
- 【图像处理】FFmpeg解码H264及swscale缩放详解
http://blog.csdn.net/gubenpeiyuan/article/details/19548019 主题 FFmpeg 本文概要: 本文介绍著名开源音视频编解码库ffmpeg如何 ...
随机推荐
- CAS单点登陆/oAuth2授权登陆
单点登陆 CAS是一个单点登录框架,即Central Authentication Service(中心认证服务) ,开始是由耶鲁大学的一个组织开发,后来归到apereo去管,github地址:htt ...
- 【WebAPI】从零开始学会使用.NET Core WebAPI
介绍 以后会慢慢总结在项目使用中或者学习到的webAPI相关的知识,在这里做记录. 我会从最开始的如何创建WebAPI项目到项目的后续知识一点一点的开始讲述记录. 通过简单有效的方式,让我们能够快速的 ...
- Java的运行原理(转载)
在Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器.这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口.编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由 ...
- SpringCloud各个组件说明
对于SpringCloud来说,首先我们需要认识一些基本的组件,这会让我们之后的讨论和交流更有效率. 组件名字和发音 如果你都不知道别人再说什么,或者别人都不知道你再讲什么,就很尴尬了. Eureka ...
- 【Flask-RESTPlus系列】Part3:请求解析
0x00 内容概览 请求解析 基本参数 必需参数 多值和列表 其他目标 参数位置 参数多个位置 高级类型处理 解析器继承 文件上传 错误处理 错误消息 参考链接 0x01 请求解析 注意:Flask- ...
- linux解压war包的命令
网上很多人说用jar包解压,但jar命令解压时不能指定目录,推荐使用unzip解压war包. 一.命令名: unzip 功 能说明:解压缩zip文 件 语 法:unzip [-cflptuvz][-a ...
- shiro源码篇 - shiro的filter,你值得拥有
前言 开心一刻 已经报废了一年多的电脑,今天特么突然开机了,吓老子一跳,只见电脑管家缓缓地出来了,本次开机一共用时一年零六个月,打败了全国0%的电脑,电脑管家已经对您的电脑失去信心,然后它把自己卸载了 ...
- java反射注解妙用-获取所有接口说明
转载请注明出处:https://www.cnblogs.com/wenjunwei/p/10293490.html 前言 最近在做项目权限,使用shiro实现restful接口权限管理,对整个项目都进 ...
- MySQL高可用之组复制技术(2):配置单主模型的组复制
MySQL组复制系列文章: MySQL组复制大纲 MySQL组复制(1):组复制技术简介 MySQL组复制(2):配置单主模型的组复制 MySQL组复制(3):配置多主模型的组复制 MySQL组复制( ...
- Keras入门(一)搭建深度神经网络(DNN)解决多分类问题
Keras介绍 Keras是一个开源的高层神经网络API,由纯Python编写而成,其后端可以基于Tensorflow.Theano.MXNet以及CNTK.Keras 为支持快速实验而生,能够把 ...