FFmpeg+SDL实时解码和渲染H264视频流
前言
之前实现了Android手机摄像头数据的TCP实时传输,今天接着聊聊,如何在PC端把接收到的H264视频流实时解码并渲染出来。这次使用的语言是C++,框架有FFmpeg和SDL2。
解码
解码部分使用FFmpeg,首先,需要初始化H264解码器:
int H264Decoder::init() {
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (codec == nullptr) {
printf("No H264 decoder found\n");
return -1;
}
codecCtx = avcodec_alloc_context3(codec);
codecCtx->flags |= AV_CODEC_FLAG_LOW_DELAY;
if (avcodec_open2(codecCtx, codec, nullptr) < 0) {
printf("Failed to open codec\n");
return -2;
}
packet = av_packet_alloc();
m_Frame = av_frame_alloc();
parser = av_parser_init(AV_CODEC_ID_H264);
return 0;
}
然后,使用创建TCP连接到我们的Android端,读取数据包:
bool read_data(SOCKET socket, void* data, unsigned int len) {
while (len > 0) {
int ret = recv(socket, (char*)data, len, 0);
if (ret <= 0) {
return false;
}
len -= ret;
data = (char*)data + ret;
}
return true;
}
bool read_int(SOCKET socket, ULONG* value) {
bool ret = read_data(socket, value, 4);
if (ret) {
*value = ntohl(*value);
}
return ret;
}
int PacketReceiver::readPacket(unsigned char** data, unsigned long* size) {
ULONG pkgSize = 0;
bool ret = read_int(m_Socket, &pkgSize);
if (!ret) {
printf("Failed to read packet size\n");
return -1;
}
if (m_DataLen < pkgSize) {
if (m_Data != nullptr) {
delete[] m_Data;
}
m_Data = new unsigned char[pkgSize];
m_DataLen = pkgSize;
}
if (!read_data(m_Socket, m_Data, pkgSize)) {
printf("Failed to read packet data\n");
return -2;
}
*data = m_Data;
*size = pkgSize;
return 0;
}
再把每个数据包传送给H264解码器解码
int H264Decoder::decode(unsigned char* data, int size, AVFrame** frame) {
int new_pkg_ret = av_new_packet(packet, size);
if (new_pkg_ret != 0) {
printf("Failed to create new packet\n");
return -1;
}
memcpy(packet->data, data, size);
int ret = avcodec_send_packet(codecCtx, packet);
if (ret < 0 && ret != AVERROR(EAGAIN)) {
printf("Failed to parse packet\n");
return -1;
}
ret = avcodec_receive_frame(codecCtx, m_Frame);
if (ret == AVERROR(EAGAIN)) {
*frame = nullptr;
return 0;
}
if (ret != 0) {
printf("Failed to read frame\n");
return -1;
}
*frame = m_Frame;
av_packet_unref(packet);
return 0;
}
解码器解码后,最终得到的是AVFrame对象,代表一帧画面,数据格式一般为YUV格式(跟编码端选择的像素格式有关)。
渲染
通过使用SDL2,我们可以直接渲染YUV数据,无需手动转成RGB。
首先,我们先初始化SDL2并创建渲染窗口:
int YuvRender::init(int video_width, int video_height) {
SDL_Init(SDL_INIT_VIDEO);
SDL_Rect bounds;
SDL_GetDisplayUsableBounds(0, &bounds);
int winWidth = video_width;
int winHeight = video_height;
if (winWidth > bounds.w || winHeight > bounds.h) {
float widthRatio = 1.0 * winWidth / bounds.w;
float heightRatio = 1.0 * winHeight / bounds.h;
float maxRatio = widthRatio > heightRatio ? widthRatio : heightRatio;
winWidth = int(winWidth / maxRatio);
winHeight = int(winHeight / maxRatio);
}
SDL_Window* window = SDL_CreateWindow(
"NetCameraViewer",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
winWidth,
winHeight,
SDL_WINDOW_OPENGL
);
m_Renderer = SDL_CreateRenderer(window, -1, 0);
m_Texture = SDL_CreateTexture(
m_Renderer,
SDL_PIXELFORMAT_IYUV,
SDL_TEXTUREACCESS_STREAMING,
video_width,
video_height
);
m_VideoWidth = video_width;
m_VideoHeight = video_height;
m_Rect.x = 0;
m_Rect.y = 0;
m_Rect.w = winWidth;
m_Rect.h = winHeight;
return 0;
}
每次解码出一帧画面的时候,再调用render函数渲染:
int YuvRender::render(unsigned char* data[], int pitch[]) {
int uvHeight = m_VideoHeight / 2;
int ySize = pitch[0] * m_VideoHeight;
int uSize = pitch[1] * uvHeight;
int vSize = pitch[2] * uvHeight;
int buffSize = ySize + uSize + vSize;
if (m_FrameBufferSize < buffSize) {
if (m_FrameBuffer != nullptr) {
delete[] m_FrameBuffer;
}
m_FrameBuffer = new unsigned char[buffSize];
m_FrameBufferSize = buffSize;
}
SDL_memcpy(m_FrameBuffer, data[0], ySize);
SDL_memcpy(m_FrameBuffer + ySize, data[1], uSize);
SDL_memcpy(m_FrameBuffer + ySize + uSize, data[2], vSize);
SDL_UpdateTexture(m_Texture, NULL, m_FrameBuffer, pitch[0]);
SDL_RenderClear(m_Renderer);
SDL_RenderCopy(m_Renderer, m_Texture, NULL, &m_Rect);
SDL_RenderPresent(m_Renderer);
SDL_PollEvent(&m_Event);
if (m_Event.type == SDL_QUIT) {
exit(0);
}
return 0;
}
性能
在搭载AMD Ryzen 5 5600U的机器上,1800 x 1350的分辨率,解码一帧平均25ms, 渲染1~2ms,加上编码和传输延时,总体延时在70ms左右。
完整源码已上传至Github: https://github.com/kasonyang/net-camera/tree/main/viewer-app
FFmpeg+SDL实时解码和渲染H264视频流的更多相关文章
- 在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 ...
- OpenCV实时美颜摄像并生成H264视频流
为什么美颜摄像这么简单的功能,OpenCV这个开源项目网上很少有代码呢?对于在windows平台下,生成h264视频流也比价麻烦,没有现成的api可以使用,需要借助MinGw编译libx264,或者f ...
- 新版本ffmpeg解码非完整H264帧失败
按照ffmpeg/doc/examples/decoding_encoding.c中video_decode_example解码H264,新版本ffmpeg解码非完整H264帧,定量读取数据直接给av ...
- 音视频处理之FFmpeg+SDL视频播放器20180409
一.FFmpeg视频解码器 1.视频解码知识 1).纯净的视频解码流程 压缩编码数据->像素数据. 例如解码H.264,就是“H.264码流->YUV”. 2).一般的视频解码流程 视频码 ...
- 音视频处理之FFmpeg+SDL+MFC视频播放器20180411
一.FFmpeg+SDL+MFC视频播放器 1.MFC知识 1).创建MFC工程的方法 打开VC++ 文件->新建->项目->MFC应用程序 应用程序类型->基于对话框 取消勾 ...
- 最简单的基于FFMPEG+SDL的视频播放器 ver2 (採用SDL2.0)
===================================================== 最简单的基于FFmpeg的视频播放器系列文章列表: 100行代码实现最简单的基于FFMPEG ...
- 最简单的基于FFMPEG+SDL的视频播放器 ver2 (采用SDL2.0)
===================================================== 最简单的基于FFmpeg的视频播放器系列文章列表: 100行代码实现最简单的基于FFMPEG ...
- 基于<最简单的基于FFMPEG+SDL的视频播放器 ver2 (采用SDL2.0)>的一些个人总结
最近因为项目接近收尾阶段,所以变的没有之前那么忙了,所以最近重新拿起了之前的一些FFMPEG和SDL的相关流媒体播放器的例子在看. 同时自己也用FFMPEG2.01,SDL2.01结合MFC以及网上罗 ...
- 用JavaCV改写“100行代码实现最简单的基于FFMPEG+SDL的视频播放器 ”
FFMPEG的文档少,JavaCV的文档就更少了.从网上找到这篇100行代码实现最简单的基于FFMPEG+SDL的视频播放器.地址是http://blog.csdn.net/leixiaohua102 ...
随机推荐
- Gartner最新报告,分析超大规模边缘解决方案
当下,酝酿能量的超级边缘. 最近,我们在谈视频化狂飙.谈AIGC颠覆.谈算力动能不足,很少谈及边缘.但"边缘"恰恰与这一切相关,且越发密不可分,它是未来技术发展的极大影响因子. & ...
- 深入理解 python 虚拟机:字节码教程(3)——深入剖析循环实现原理
深入理解 python 虚拟机:字节码教程(3)--深入剖析循环实现原理 在本篇文章当中主要给大家介绍 cpython 当中跟循环相关的字节码,这部分字节码相比起其他字节码来说相对复杂一点,通过分析这 ...
- 使用Ref还是Reactive?
我喜欢Vue 3的Composition API,它提供了两种方法来为Vue组件添加响应式状态:ref和reactive.当你使用ref时到处使用.value是很麻烦的,但当你用reactive创建的 ...
- Python-faker的简单使用
前言: faker是一个开源的python库,安装完成后只需要调用Faker库,就可以帮助我们创建需要的数据. 一.安装 1.执行如下命令安装 pip3 install faker 2.进入File ...
- 如何用 KMP 偏序 Z 函数
KMP 算法求解字符串匹配的过程中 \(next\) 数组有着繁多的应用,主要是可以帮我们求 border. 然而用 \(s\) 串匹配 \(t\) 串产生的 \(f\) 数组应用相对较少. \(f\ ...
- Prism Sample 5 View Injection
这里稍微复杂了点,定义视图A的过程是一样的: <UserControl x:Class="ViewInjection.Views.ViewA" xmlns="htt ...
- map和multimap
map相对于set区别,map具有键值和实值,所有元素根据键值自动排序,pair的第一个值被称为键值key,pair的第二个值被称为实值value.map也是以红黑树为底层实现机制,根据key进行排序 ...
- 在docker容器里,ffmpeg给视频文件内嵌字幕文件,不生效,如何解决?
用ffmpeg命令,发现执行成功,但视频文件就是没有字幕.看不出问题出现在什么地方.后来直接用ffmpeg添加水印命令测试,发现是缺少字体文件,如下图所示: 报Fontconfig error: Ca ...
- 2020-09-02:Sqoop的工作原理?
福哥答案2020-09-02: [答案来自此链接](https://www.kaops.com/ivquest/104670)下图显示了Sqoop的工作流程: Sqoop导入导入工具将单个表从RDBM ...
- 2021-01-21:java中,HashMap的读流程是什么?
福哥答案2020-01-21: jdk1.7读流程:1.key是否为空值null,如果为空,直接遍历table[0]链表,寻找key==null键.调用的是getForNullKey()方法.如下:1 ...