嵌入式音视频开发——SRT协议
·
SRT协议详解
目录
1. SRT协议入门
1.1 什么是SRT
SRT(Secure Reliable Transport,安全可靠传输) 是专为超低延迟(亚秒级) 实时音视频流设计的开源传输协议,由Haivision公司开发,现由SRT联盟维护。
- 底层基于UDP协议,添加了可靠性保障
- 核心优势:在不可靠网络(互联网、4G/5G)下同时实现低延迟和高可靠性
- 开源仓库:https://github.com/Haivision/srt
- 生态支持:FFmpeg、OBS、VLC、Wowza等主流工具原生支持
1.2 为什么嵌入式音视频需要SRT
传统协议在弱网环境下存在致命缺陷:
- TCP(RTMP):丢包导致延迟累积和卡顿,典型延迟1-3秒
- 原始UDP(RTP/RTSP):速度快但不可靠,丢包直接花屏断流
- HLS:基于HTTP切片,延迟5-10秒,不适合实时场景
SRT完美解决了这个矛盾:在UDP低延迟基础上,添加智能丢包恢复机制,15%丢包率下仍能保持流畅播放。
1.3 主流流媒体协议对比
| 协议 | 底层传输 | 典型延迟 | 丢包恢复 | 弱网表现 | 主要场景 |
|---|---|---|---|---|---|
| SRT | UDP | 100-500ms | ARQ+FEC | 优秀 | 直播回传、安防监控、无人机图传 |
| RTMP | TCP | 500-2000ms | 依赖TCP | 差 | 传统直播推流 |
| RTSP+RTP | UDP | 100-300ms | 无/简单 | 一般 | 局域网监控 |
| HLS | HTTP(TCP) | 5000-10000ms | 无 | 差 | 点播、移动端直播 |
| WebRTC | UDP | <100ms | FEC+ARQ | 优秀 | 视频会议、实时通话 |
2. SRT协议深度解析
2.1 协议栈与数据包结构
协议栈层级
SRT是传输层协议,运行在UDP之上,属于应用层和传输层之间的"中间件协议":
应用层(音视频编码/解码)
↓
SRT协议层(可靠性、加密、拥塞控制)
↓
UDP协议层(无连接、低延迟)
↓
IP协议层
↓
数据链路层
- 完全在用户空间实现,无需修改内核
- 提供与TCP类似的API接口,易于移植
- 支持单播、多播和点对点传输
核心数据包类型
- 数据数据包:携带实际音视频数据
- 控制数据包:用于握手、确认、重传请求、拥塞控制等
2.2 核心工作原理
1. 4次握手连接建立
比TCP+TLS更快,适合嵌入式设备快速建连:
- Caller发送握手请求(版本、加密算法、初始序列号)
- Listener响应握手确认(确认参数、生成加密密钥)
- Caller发送最终确认(确认密钥,准备发数据)
- Listener发送连接就绪
2. 智能丢包恢复(ARQ+FEC)
- ARQ选择性重传:接收端检测丢包后发送NAK,只重传丢失的特定包,RTO动态调整
- FEC前向纠错:发送冗余数据包,接收端无需重传即可恢复丢失数据
- 低丢包(<5%):仅用ARQ
- 中等丢包(5%-15%):行FEC,冗余率10%-20%
- 高丢包(>15%):矩阵FEC,冗余率20%-30%
3. 固定端到端延迟(TsbPd)
- 发送端为每个数据包打时间戳
- 接收端根据配置的延迟值计算播放时间
- 数据包放入缓冲区直到播放时间到达,过期直接丢弃
- 嵌入式调优:局域网50-100ms,4G/5G 200-500ms,卫星1000-2000ms
4. 其他核心特性
- 动态拥塞控制:LiveCC算法,专为实时音视频设计
- 内置AES加密:支持128/192/256位端到端加密
- 防火墙/NAT穿透:特殊握手过程,无需永久开放外部端口
2.3 三种工作模式
- Caller-Listener模式(最常用):Listener被动监听,Caller主动连接
- 典型应用:嵌入式设备向云端推流,客户端从设备拉流
- Rendezvous模式(点对点):双方同时作为Caller和Listener,自动NAT穿透
- 典型应用:无人机图传、机器人远程控制
- 广播模式:基于UDP多播,适合局域网一对多传输
3. SRT核心API接口使用说明
3.1 库初始化与清理
srt_startup()
int srt_startup(void);
- 功能:初始化SRT库,必须在调用任何其他SRT函数前调用
- 返回值:成功返回0,失败返回-1
- 注意:只需在程序开始时调用一次
srt_cleanup()
int srt_cleanup(void);
- 功能:清理SRT库资源,释放所有内部状态
- 返回值:成功返回0,失败返回-1
- 注意:在程序退出前调用,确保所有套接字已关闭
3.2 套接字创建与销毁
srt_create_socket()
SRTSOCKET srt_create_socket(void);
- 功能:创建一个新的SRT套接字
- 返回值:成功返回套接字句柄,失败返回
SRT_ERROR - 示例:
SRTSOCKET sock = srt_create_socket(); if (sock == SRT_ERROR) { printf("创建套接字失败: %s\n", srt_getlasterror_str()); return -1; }
srt_close()
int srt_close(SRTSOCKET sock);
- 功能:关闭SRT套接字,释放相关资源
- 参数:
sock:要关闭的套接字句柄
- 返回值:成功返回0,失败返回
SRT_ERROR
3.3 参数设置与获取
SRT提供了丰富的参数配置选项,通过srt_setsockopt和srt_getsockopt进行设置和获取。
srt_setsockopt()
int srt_setsockopt(SRTSOCKET sock, int level, SRT_SOCKOPT optname,
const void *optval, int optlen);
- 功能:设置套接字选项
- 参数:
sock:套接字句柄level:选项级别,通常为0optname:选项名称(见下表)optval:指向选项值的指针optlen:选项值的长度
- 返回值:成功返回0,失败返回
SRT_ERROR
srt_getsockopt()
int srt_getsockopt(SRTSOCKET sock, int level, SRT_SOCKOPT optname,
void *optval, int *optlen);
- 功能:获取套接字选项
- 参数:类似
srt_setsockopt,optval为输出参数
常用选项列表
| 选项名 | 类型 | 说明 | 推荐值 |
|---|---|---|---|
SRTO_LATENCY | int | 端到端延迟(ms) | 200-500 |
SRTO_PAYLOADSIZE | int | 最大数据包大小 | 1316(TS包) |
SRTO_PASSPHRASE | char* | 加密密码 | 最多79字符 |
SRTO_PBKEYLEN | int | 加密密钥长度 | 16/24/32(AES-128/192/256) |
SRTO_FEC | int | FEC模式 | 0=关闭, 1=开启 |
SRTO_FECROWS | int | FEC行数 | 4-8 |
SRTO_FECCOLS | int | FEC列数 | 4-8 |
参数设置示例
// 设置延迟为200ms
int latency = 200;
srt_setsockopt(sock, 0, SRTO_LATENCY, &latency, sizeof(latency));
// 开启FEC
int fec_on = 1;
srt_setsockopt(sock, 0, SRTO_FEC, &fec_on, sizeof(fec_on));
// 设置加密
const char *passphrase = "my_secret_password";
srt_setsockopt(sock, 0, SRTO_PASSPHRASE, passphrase, strlen(passphrase));
int key_len = 16; // AES-128
srt_setsockopt(sock, 0, SRTO_PBKEYLEN, &key_len, sizeof(key_len));
3.4 地址解析与连接建立
srt_parse_uri()
int srt_parse_uri(const char *uri, int urilen, struct sockaddr *sa, int *salen);
- 功能:解析SRT URI字符串为套接字地址
- 参数:
uri:SRT URI,如srt://192.168.1.100:8888?mode=callerurilen:URI长度sa:输出的套接字地址结构salen:输入/输出,地址结构长度
- 返回值:成功返回0,失败返回
SRT_ERROR - 支持的URI参数:
mode:caller/listener/rendezvouslatency:延迟值passphrase:密码fec:FEC开关
srt_connect()
int srt_connect(SRTSOCKET sock, const struct sockaddr *name, int namelen);
- 功能:作为Caller主动连接到Listener
- 参数:
sock:套接字句柄name:目标地址namelen:地址长度
- 返回值:成功返回0,失败返回
SRT_ERROR - 注意:这是阻塞调用,会等待连接建立完成
srt_bind()
int srt_bind(SRTSOCKET sock, const struct sockaddr *name, int namelen);
- 功能:绑定套接字到本地地址(Listener模式)
- 参数:类似
srt_connect - 返回值:成功返回0,失败返回
SRT_ERROR
srt_listen()
int srt_listen(SRTSOCKET sock, int backlog);
- 功能:开始监听连接请求(Listener模式)
- 参数:
sock:套接字句柄backlog:等待连接队列的最大长度
- 返回值:成功返回0,失败返回
SRT_ERROR
srt_accept()
SRTSOCKET srt_accept(SRTSOCKET sock, struct sockaddr *addr, int *addrlen);
- 功能:接受一个传入的连接请求
- 参数:
sock:监听套接字句柄addr:输出,客户端地址addrlen:输入/输出,地址长度
- 返回值:成功返回新的连接套接字,失败返回
SRT_ERROR - 注意:这是阻塞调用,会等待连接到来
连接建立完整示例
Caller端(主动连接):
SRTSOCKET sock = srt_create_socket();
// 解析URI
sockaddr_wsa sa;
int salen = sizeof(sa);
const char *url = "srt://192.168.1.100:8888?mode=caller&latency=200";
srt_parse_uri(url, strlen(url), &sa, &salen);
// 连接
if (srt_connect(sock, (struct sockaddr *)&sa, salen) == SRT_ERROR) {
printf("连接失败: %s\n", srt_getlasterror_str());
return -1;
}
printf("连接成功!\n");
Listener端(被动监听):
SRTSOCKET listen_sock = srt_create_socket();
// 解析URI
sockaddr_wsa sa;
int salen = sizeof(sa);
const char *url = "srt://0.0.0.0:8888?mode=listener";
srt_parse_uri(url, strlen(url), &sa, &salen);
// 绑定并监听
srt_bind(listen_sock, (struct sockaddr *)&sa, salen);
srt_listen(listen_sock, 1);
printf("等待连接...\n");
SRTSOCKET client_sock = srt_accept(listen_sock, NULL, NULL);
printf("收到新连接!\n");
3.5 数据发送与接收
srt_send()
int srt_send(SRTSOCKET sock, const char *buf, int len);
- 功能:发送数据
- 参数:
sock:套接字句柄buf:要发送的数据缓冲区len:数据长度
- 返回值:成功返回发送的字节数,失败返回
SRT_ERROR - 注意:
- 这是阻塞调用,会等待数据被确认
- 单次发送长度不应超过
SRTO_PAYLOADSIZE设置的值
srt_sendmsg2()
int srt_sendmsg2(SRTSOCKET sock, const char *buf, int len,
const SRT_MSGCTRL *mctrl);
- 功能:高级发送函数,支持更多控制选项
- 参数:
mctrl:消息控制结构,可设置时间戳、TTL等
- 返回值:类似
srt_send
srt_recv()
int srt_recv(SRTSOCKET sock, char *buf, int len);
- 功能:接收数据
- 参数:
buf:接收数据的缓冲区len:缓冲区大小
- 返回值:成功返回接收的字节数,连接关闭返回0,失败返回
SRT_ERROR - 注意:这是阻塞调用,会等待数据到达
srt_recvmsg2()
int srt_recvmsg2(SRTSOCKET sock, char *buf, int len,
SRT_MSGCTRL *mctrl);
- 功能:高级接收函数,可获取消息元数据
- 参数:
mctrl:输出,消息控制结构,包含时间戳等信息
数据收发完整示例
发送端:
// 发送H.264数据
char h264_data[4096];
int data_len = read_h264_frame(h264_data, sizeof(h264_data));
int sent = srt_send(sock, h264_data, data_len);
if (sent == SRT_ERROR) {
printf("发送失败: %s\n", srt_getlasterror_str());
} else {
printf("发送了 %d 字节\n", sent);
}
接收端:
// 接收数据
char recv_buf[8192];
int recv_len = srt_recv(sock, recv_buf, sizeof(recv_buf));
if (recv_len > 0) {
printf("收到 %d 字节\n", recv_len);
process_h264_data(recv_buf, recv_len);
} else if (recv_len == 0) {
printf("连接已关闭\n");
} else {
printf("接收失败: %s\n", srt_getlasterror_str());
}
3.6 错误处理
srt_getlasterror()
int srt_getlasterror(void);
- 功能:获取最后一次错误的错误码
- 返回值:错误码
srt_getlasterror_str()
const char *srt_getlasterror_str(void);
- 功能:获取最后一次错误的可读描述字符串
- 返回值:错误描述字符串指针
- 示例:
if (srt_connect(sock, ...) == SRT_ERROR) { printf("错误: %s\n", srt_getlasterror_str()); }
3.7 完整的API使用流程
1. srt_startup() // 初始化库
↓
2. srt_create_socket() // 创建套接字
↓
3. srt_setsockopt() // 设置参数(延迟、FEC、加密等)
↓
4. srt_parse_uri() // 解析地址
↓
5a. srt_connect() // Caller:连接
或
5b. srt_bind() + srt_listen() + srt_accept() // Listener:监听并接受
↓
6. srt_send() / srt_recv() // 收发数据(循环)
↓
7. srt_close() // 关闭套接字
↓
8. srt_cleanup() // 清理库
4. RK3568 V4L2摄像头采集推流
使用V4L2摄像头采集,直接从/dev/video0取流,零拷贝硬件编码。
4.1 代码(rk3568_camera_srt_push.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/videodev2.h>
#include <pthread.h>
#include "rk_mpi.h"
#include <srt/srt.h>
// ====================== 配置区 ======================
#define VIDEO_DEV "/dev/video0"
#define WIDTH 1920
#define HEIGHT 1080
#define FPS 30
#define BITRATE 4000000
#define SRT_PUSH_URL "srt://192.168.1.100:8888?mode=caller"
#define SRT_LATENCY 200
// =====================================================
typedef struct {
void *start;
size_t length;
} v4l2_buf_t;
static int v4l2_fd = -1;
static v4l2_buf_t *v4l2_buffers = NULL;
static int n_buffers = 0;
static SRTSOCKET srt_sock = SRT_ERROR;
static MppCtx mpp_ctx = NULL;
static MppApi *mpp_api = NULL;
// V4L2摄像头初始化
int v4l2_init(void)
{
v4l2_fd = open(VIDEO_DEV, O_RDWR | O_NONBLOCK, 0);
if (v4l2_fd < 0) {
perror("open video0 failed");
return -1;
}
struct v4l2_capability cap;
if (ioctl(v4l2_fd, VIDIOC_QUERYCAP, &cap) < 0) {
perror("VIDIOC_QUERYCAP");
return -1;
}
// 设置NV12格式(RK摄像头默认输出)
struct v4l2_format fmt = {0};
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = WIDTH;
fmt.fmt.pix.height = HEIGHT;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_NV12;
fmt.fmt.pix.field = V4L2_FIELD_NONE;
if (ioctl(v4l2_fd, VIDIOC_S_FMT, &fmt) < 0) {
perror("VIDIOC_S_FMT");
return -1;
}
// 请求缓冲区
struct v4l2_requestbuffers req = {0};
req.count = 4;
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
req.memory = V4L2_MEMORY_MMAP;
if (ioctl(v4l2_fd, VIDIOC_REQBUFS, &req) < 0) {
perror("VIDIOC_REQBUFS");
return -1;
}
// 映射内核缓冲区
v4l2_buffers = (v4l2_buf_t *)calloc(req.count, sizeof(v4l2_buf_t));
for (n_buffers = 0; n_buffers < req.count; n_buffers++) {
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = n_buffers;
if (ioctl(v4l2_fd, VIDIOC_QUERYBUF, &buf) < 0) {
perror("VIDIOC_QUERYBUF");
return -1;
}
v4l2_buffers[n_buffers].length = buf.length;
v4l2_buffers[n_buffers].start = mmap(
NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED,
v4l2_fd, buf.m.offset
);
if (v4l2_buffers[n_buffers].start == MAP_FAILED) {
perror("mmap failed");
return -1;
}
}
// 入队缓冲区并开始流
for (int i = 0; i < n_buffers; i++) {
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = i;
ioctl(v4l2_fd, VIDIOC_QBUF, &buf);
}
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(v4l2_fd, VIDIOC_STREAMON, &type);
printf("V4L2 摄像头初始化成功 → %dx%d NV12\n", WIDTH, HEIGHT);
return 0;
}
// 读取一帧NV12数据
int v4l2_get_frame(void **nv12_data, size_t *nv12_size)
{
struct v4l2_buffer buf = {0};
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if (ioctl(v4l2_fd, VIDIOC_DQBUF, &buf) < 0)
return -1;
*nv12_data = v4l2_buffers[buf.index].start;
*nv12_size = buf.bytesused;
// 用完放回队列
ioctl(v4l2_fd, VIDIOC_QBUF, &buf);
return 0;
}
// SRT初始化
int srt_init(void)
{
srt_startup();
srt_sock = srt_create_socket();
if (srt_sock == SRT_ERROR) {
printf("srt socket create failed\n");
return -1;
}
int latency = SRT_LATENCY;
srt_setsockopt(srt_sock, 0, SRTO_LATENCY, &latency, sizeof(latency));
sockaddr_wsa sa;
int salen = sizeof(sa);
if (srt_parse_uri(SRT_PUSH_URL, strlen(SRT_PUSH_URL), &sa, &salen) == SRT_ERROR) {
printf("srt url error\n");
return -1;
}
if (srt_connect(srt_sock, (struct sockaddr *)&sa, salen) == SRT_ERROR) {
printf("srt connect failed: %s\n", srt_getlasterror_str());
return -1;
}
printf("SRT 连接成功!\n");
return 0;
}
// MPP硬件编码器初始化
int mpp_encoder_init(void)
{
MPP_RET ret;
ret = mpp_create(&mpp_ctx, &mpp_api);
if (ret != MPP_OK) return -1;
ret = mpp_init(mpp_ctx, MPP_CTX_ENC, MPP_VIDEO_CodingAVC);
if (ret != MPP_OK) return -1;
MppEncCfg cfg;
mpp_enc_cfg_init(&cfg);
cfg.width = WIDTH;
cfg.height = HEIGHT;
cfg.fps_in_num = FPS;
cfg.fps_in_denorm = 1;
cfg.rc_mode = MPP_ENC_RC_MODE_CBR;
cfg.bps_target = BITRATE;
cfg.gop = FPS * 2;
// 输入格式:NV12
cfg.pic_width = WIDTH;
cfg.pic_height = HEIGHT;
cfg.in_fmt = MPP_FMT_NV12;
mpp_api->control(mpp_ctx, MPP_ENC_SET_CFG, &cfg);
printf("MPP 硬件编码器初始化完成\n");
return 0;
}
int main(void)
{
if (v4l2_init() < 0 || srt_init() < 0 || mpp_encoder_init() < 0)
return -1;
MppFrame frame = NULL;
MppPacket pkt = NULL;
mpp_frame_init(&frame);
mpp_packet_init(&pkt, NULL, 0);
while (1) {
void *nv12_data = NULL;
size_t nv12_size = 0;
// 取真实摄像头帧
if (v4l2_get_frame(&nv12_data, &nv12_size) < 0) {
usleep(10000);
continue;
}
// 送入硬件编码器
mpp_frame_set_width(frame, WIDTH);
mpp_frame_set_height(frame, HEIGHT);
mpp_frame_set_fmt(frame, MPP_FMT_NV12);
mpp_frame_set_addr(frame, (void *)nv12_data);
mpp_frame_set_pts(frame, 0);
mpp_api->encode_put_frame(mpp_ctx, frame);
// 获取编码结果并推流
while (mpp_api->encode_get_packet(mpp_ctx, &pkt) == MPP_OK) {
void *data = mpp_packet_get_data(pkt);
int len = mpp_packet_get_length(pkt);
if (len > 0) {
srt_send(srt_sock, (char *)data, len);
}
}
}
return 0;
}
5. SRT拉流+解码+显示完整程序
跨平台接收端,使用FFmpeg拉取SRT流、解码、SDL显示,可在PC和嵌入式平台运行。
5.1 完整代码(srt_play.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL2/SDL.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#define SRT_PLAY_URL "srt://0.0.0.0:8888?mode=listener"
int main() {
avformat_network_init();
AVFormatContext *fmt_ctx = NULL;
AVDictionary *opts = NULL;
av_dict_set(&opts, "latency", "200", 0);
av_dict_set(&opts, "mode", "listener", 0);
if (avformat_open_input(&fmt_ctx, SRT_PLAY_URL, NULL, &opts) < 0) {
printf("Could not open SRT stream\n");
return -1;
}
if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
printf("Could not find stream info\n");
return -1;
}
// 查找视频流
int video_idx = -1;
for (int i = 0; i < fmt_ctx->nb_streams; i++) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_idx = i;
break;
}
}
if (video_idx == -1) {
printf("No video stream found\n");
return -1;
}
// 初始化解码器
AVCodecParameters *codec_par = fmt_ctx->streams[video_idx]->codecpar;
const AVCodec *codec = avcodec_find_decoder(codec_par->codec_id);
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(codec_ctx, codec_par);
avcodec_open2(codec_ctx, codec, NULL);
// 初始化SDL
SDL_Init(SDL_INIT_VIDEO);
SDL_Window *win = SDL_CreateWindow("SRT Player", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
codec_ctx->width, codec_ctx->height, 0);
SDL_Renderer *renderer = SDL_CreateRenderer(win, -1, 0);
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING,
codec_ctx->width, codec_ctx->height);
// 准备帧缓冲区
AVFrame *frame = av_frame_alloc();
AVFrame *sws_frame = av_frame_alloc();
uint8_t *out_buffer = (uint8_t *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, codec_ctx->width, codec_ctx->height, 1));
av_image_fill_arrays(sws_frame->data, sws_frame->linesize, out_buffer, AV_PIX_FMT_YUV420P, codec_ctx->width, codec_ctx->height, 1);
struct SwsContext *sws_ctx = sws_getContext(codec_ctx->width, codec_ctx->height, codec_ctx->pix_fmt,
codec_ctx->width, codec_ctx->height, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, NULL, NULL, NULL);
// 主循环
AVPacket pkt;
SDL_Event event;
while (1) {
SDL_PollEvent(&event);
if (event.type == SDL_QUIT) break;
if (av_read_frame(fmt_ctx, &pkt) < 0) break;
if (pkt.stream_index == video_idx) {
avcodec_send_packet(codec_ctx, &pkt);
while (avcodec_receive_frame(codec_ctx, frame) == 0) {
sws_scale(sws_ctx, (const uint8_t *const *)frame->data, frame->linesize, 0, frame->height,
sws_frame->data, sws_frame->linesize);
SDL_UpdateYUVTexture(texture, NULL,
sws_frame->data[0], sws_frame->linesize[0],
sws_frame->data[1], sws_frame->linesize[1],
sws_frame->data[2], sws_frame->linesize[2]);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
}
}
av_packet_unref(&pkt);
}
// 清理资源
av_free(out_buffer);
av_frame_free(&frame);
av_frame_free(&sws_frame);
sws_freeContext(sws_ctx);
SDL_DestroyTexture(texture);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(win);
SDL_Quit();
avcodec_free_context(&codec_ctx);
avformat_close_input(&fmt_ctx);
avformat_network_deinit();
return 0;
}
更多推荐



所有评论(0)