音视频入门基础:RTP专题(9)——FFmpeg接收RTP流的原理和内部实现

一、引言

由《音视频入门基础:RTP专题(2)——使用FFmpeg命令生成RTP流》可以知道,推流端通过下面FFmpeg命令可以将一个媒体文件转推RTP,生成RTP流:

ffmpeg -re -stream_loop -1 -i input.mp4 -vcodec copy -an -f rtp rtp://192.168.0.103:6005 -acodec copy -vn -sdp_file XXX.sdp -f rtp rtp://192.168.0.103:7005

接收端通过命令:ffmpeg -protocol_whitelist "file,rtp,udp" -i XXX.sdp 可以查看生成的RTP流的信息:

由《音视频入门基础:RTP专题(8)——使用Wireshark分析RTP》可以知道,上述推流端的本质是创建了一个UDP客户端,将媒体文件input.mp4的视频数据发送到IP为192.168.0.103的UDP服务器的6005端口,音频数据发送到该UDP服务器的7005端口。与之对应,接收端的本质是创建了一个UDP服务器来接收推流端发送的基于UDP的RTP数据。下面讲述接收端的FFmpeg,其源码中接收RTP流的内部实现。

二、FFmpeg接收RTP流的内部实现

(一)给addrinfo结构赋值

当音视频流为RTP流时,FFmpeg的avformat_open_input函数内部会调用rtp_open函数,而rtp_open函数的底层又会调用udp_open函数,udp_open函数内部又调用了udp_socket_create函数:

/* put it in UDP context */
/* return non zero if error */
static int udp_open(URLContext *h, const char *uri, int flags)
{
//...udp_fd = udp_socket_create(h, &my_addr, &len, s->localaddr);
//...
}

udp_socket_create函数内部会通过语句:

res0 = ff_ip_resolve_host(h, (localaddr && localaddr[0]) ? localaddr : NULL,s->local_port,SOCK_DGRAM, family, AI_PASSIVE) 根据给定的主机名和需要创建的UDP服务器的端口,返回一个struct addrinfo结构,从而给addrinfo结构赋值。其中localaddr包含主机名,s->local_port为需要创建的UDP服务器的端口,该端口号来源于推流端的FFmpeg命令生成的SDP文件中的端口信息:

static int udp_socket_create(URLContext *h, struct sockaddr_storage *addr,socklen_t *addr_len, const char *localaddr)
{UDPContext *s = h->priv_data;int udp_fd = -1;struct addrinfo *res0, *res;int family = AF_UNSPEC;if (((struct sockaddr *) &s->dest_addr)->sa_family)family = ((struct sockaddr *) &s->dest_addr)->sa_family;res0 = ff_ip_resolve_host(h, (localaddr && localaddr[0]) ? localaddr : NULL,s->local_port,SOCK_DGRAM, family, AI_PASSIVE);if (!res0)goto fail;for (res = res0; res; res=res->ai_next) {if (s->udplite_coverage)udp_fd = ff_socket(res->ai_family, SOCK_DGRAM, IPPROTO_UDPLITE, h);elseudp_fd = ff_socket(res->ai_family, SOCK_DGRAM, 0, h);if (udp_fd != -1) break;ff_log_net_error(h, AV_LOG_ERROR, "socket");}if (udp_fd < 0)goto fail;memcpy(addr, res->ai_addr, res->ai_addrlen);*addr_len = res->ai_addrlen;freeaddrinfo(res0);return udp_fd;fail:if (udp_fd >= 0)closesocket(udp_fd);if(res0)freeaddrinfo(res0);return -1;
}

ff_ip_resolve_host函数定义在libavformat/ip.c中,可以看到其内部通过getaddrinfo函数根据给定的主机名和服务名,返回一个struct addrinfo结构。关于getaddrinfo函数的用法可以参考:《百度百科——getaddrinfo》:

struct addrinfo *ff_ip_resolve_host(void *log_ctx,const char *hostname, int port,int type, int family, int flags)
{struct addrinfo hints = { 0 }, *res = 0;int error;char sport[16];const char *node = 0, *service = "0";if (port > 0) {snprintf(sport, sizeof(sport), "%d", port);service = sport;}if ((hostname) && (hostname[0] != '\0') && (hostname[0] != '?')) {node = hostname;}hints.ai_socktype = type;hints.ai_family   = family;hints.ai_flags    = flags;if ((error = getaddrinfo(node, service, &hints, &res))) {res = NULL;av_log(log_ctx, AV_LOG_ERROR, "getaddrinfo(%s, %s): %s\n",node ? node : "unknown",service,gai_strerror(error));}return res;
}

执行完上述操作后,udp_open函数中的变量my_addr会得到主机名和需要创建的UDP服务器的端口信息:

/* put it in UDP context */
/* return non zero if error */
static int udp_open(URLContext *h, const char *uri, int flags)
{
//...udp_fd = udp_socket_create(h, &my_addr, &len, s->localaddr);
//...
}

(二)创建套接字

给addrinfo结构赋值后,udp_socket_create函数内部会执行语句:udp_fd = ff_socket(res->ai_family, SOCK_DGRAM, 0, h) 来创建套接字,SOCK_DGRAM表示是基于UDP:

static int udp_socket_create(URLContext *h, struct sockaddr_storage *addr,socklen_t *addr_len, const char *localaddr)
{
//...res0 = ff_ip_resolve_host(h, (localaddr && localaddr[0]) ? localaddr : NULL,s->local_port,SOCK_DGRAM, family, AI_PASSIVE);
//...for (res = res0; res; res=res->ai_next) {if (s->udplite_coverage)udp_fd = ff_socket(res->ai_family, SOCK_DGRAM, IPPROTO_UDPLITE, h);elseudp_fd = ff_socket(res->ai_family, SOCK_DGRAM, 0, h);if (udp_fd != -1) break;ff_log_net_error(h, AV_LOG_ERROR, "socket");}
//...
}

 ff_socket函数定义在libavformat/network.c中,可以看到该函数内部通过socket函数创建了套接字。音视频流为RTP的情况下,形参type的值为SOCK_DGRAM,所以此时创建的是UDP套接字:


int ff_socket(int af, int type, int proto, void *logctx)
{int fd;#ifdef SOCK_CLOEXECfd = socket(af, type | SOCK_CLOEXEC, proto);if (fd == -1 && errno == EINVAL)
#endif{fd = socket(af, type, proto);
#if HAVE_FCNTLif (fd != -1) {if (fcntl(fd, F_SETFD, FD_CLOEXEC) == -1)av_log(logctx, AV_LOG_DEBUG, "Failed to set close on exec\n");}
#endif}
#ifdef SO_NOSIGPIPEif (fd != -1) {if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &(int){1}, sizeof(int))) {av_log(logctx, AV_LOG_WARNING, "setsockopt(SO_NOSIGPIPE) failed\n");}}
#endifreturn fd;
}

(三)绑定套接字

创建完UDP套接字后,udp_open函数中会通过bind函数绑定上述创建的UDP套接字到my_addr上。从上面我们已经可以知道,my_addr包含需要创建的UDP服务器的端口信息:

​/* put it in UDP context */
/* return non zero if error */
static int udp_open(URLContext *h, const char *uri, int flags)
{
//...udp_fd = udp_socket_create(h, &my_addr, &len, s->localaddr);
//.../* bind to the local address if not multicast or if the multicast* bind failed *//* the bind is needed to give a port to the socket now */if (bind_ret < 0 && bind(udp_fd,(struct sockaddr *)&my_addr, len) < 0) {ff_log_net_error(h, AV_LOG_ERROR, "bind failed");ret = ff_neterrno();goto fail;}
//...
}​

(四)监视文件描述符是否可读并接收数据

绑定完套接字后,FFmpeg的avformat_find_stream_info函数底层会调用rtp_read函数,而rtp_read函数内部会通过poll函数监视文件描述符(上述创建的UDP套接字)是否可读。如果可读,那就通过recvfrom函数接收UDP数据(基于UDP的RTP音视频数据):

static int rtp_read(URLContext *h, uint8_t *buf, int size)
{RTPContext *s = h->priv_data;int len, n, i;struct pollfd p[2] = {{s->rtp_fd, POLLIN, 0}, {s->rtcp_fd, POLLIN, 0}};int poll_delay = h->flags & AVIO_FLAG_NONBLOCK ? 0 : POLLING_TIME;struct sockaddr_storage *addrs[2] = { &s->last_rtp_source, &s->last_rtcp_source };socklen_t *addr_lens[2] = { &s->last_rtp_source_len, &s->last_rtcp_source_len };int runs = h->rw_timeout / 1000 / POLLING_TIME;for(;;) {if (ff_check_interrupt(&h->interrupt_callback))return AVERROR_EXIT;n = poll(p, 2, poll_delay);if (n > 0) {/* first try RTCP, then RTP */for (i = 1; i >= 0; i--) {if (!(p[i].revents & POLLIN))continue;*addr_lens[i] = sizeof(*addrs[i]);len = recvfrom(p[i].fd, buf, size, 0,(struct sockaddr *)addrs[i], addr_lens[i]);if (len < 0) {if (ff_neterrno() == AVERROR(EAGAIN) ||ff_neterrno() == AVERROR(EINTR))continue;return AVERROR(EIO);}if (ff_ip_check_source_lists(addrs[i], &s->filters))continue;return len;}} else if (n == 0 && h->rw_timeout > 0 && --runs <= 0) {return AVERROR(ETIMEDOUT);} else if (n < 0) {if (ff_neterrno() == AVERROR(EINTR))continue;return AVERROR(EIO);}if (h->flags & AVIO_FLAG_NONBLOCK)return AVERROR(EAGAIN);}
}

三、总结

1.从上面的代码分析可以看出来,接收端的FFmpeg接收RTP数据,其原理就是创建了一个UDP服务器来接收推流端发送的基于UDP的RTP数据,本质就是接收UDP数据。该UDP服务器的实现原理跟《Linux下使用poll函数编写UDP客户端、服务器程序》中展示的UDP服务器是一样的,都是调用了socket、bind、poll、recvfrom这几个函数。

2.跟普通的UDP服务器相比,FFmpeg接收RTP流时创建的UDP服务器,其端口来源于推流端的FFmpeg命令生成的SDP文件中的端口信息。也就是说此时的流程是推流端的UDP客户端先被创建,然后通过SDP把需要创建的UDP服务器的端口号发送给接收端,最后接收端才根据这个端口号创建UDP服务器。所以可以实现推流端先推流(UDP客户端先发送音视频数据,不管服务器有没有接收到),接收端再接收RTP音视频数据。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大模型Deepseek的使用_基于阿里云百炼和Chatbox

目录 前言1. 云服务商2. ChatBox参考 前言 上篇博文中探索了&#xff08;本地&#xff09;部署大语言模型&#xff0c;适合微调、数据高隐私性等场景。随着Deepseek-R1的发布&#xff0c;大语言模型的可及性得到极大提升&#xff0c;应用场景不断增加&#xff0c;对高可用的方…

zookeeper watch

目录 回顾回调&观察者模式&发布订阅模式Zookeeper 客户端/ 服务端 watchgetChildren 为例最后归纳 回顾回调&观察者模式&发布订阅模式 回调的思想 类A的a()方法调用类B的b()方法类B的b()方法执行完毕主动调用类A的callback()方法 回调分为同步回调和异步回调…

PAT乙组(1016 部分A+B 1017 A除以B)C语言超详细

文章目录 1016 部分AB1017 A除以B 1016 部分AB 输入样例 1&#xff1a; 3862767 6 13530293 3输出样例 1&#xff1a; 399输入样例 2&#xff1a; 3862767 1 13530293 8输出样例 2&#xff1a; 0代码长度限制 16 KB 时间限制 150 ms 内存限制 64 MB 栈限制 8192 KB 思路解析…

论文笔记:Multi-Head Mixture-of-Experts

2024 neurips 1 背景 稀疏混合专家&#xff08;SMoE&#xff09;可在不显著增加训练和推理成本的前提下提升模型的能力【比如Mixtral 8*7B&#xff0c;表现可以媲美LLaMA-2 70B】 但它也有两个问题 专家激活率低&#xff08;下图左&#xff09; 在优化时只有一小部分专家会被…

【Azure 架构师学习笔记】- Azure Databricks (11) -- UC搭建

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Databricks】系列。 接上文 【Azure 架构师学习笔记】- Azure Databricks (10) – UC 使用 前言 由于ADB 的更新速度很快&#xff0c;在几个月之后重新搭建ADB 时发现UC 已经更新了很多&#xff0c;为了后续做ADB 的功…

解锁养生秘籍,拥抱健康生活

在这个快节奏的时代&#xff0c;人们行色匆匆&#xff0c;常常在忙碌中忽略了健康。其实&#xff0c;养生并非遥不可及&#xff0c;它就藏在生活的细微之处&#xff0c;等待我们去发现和实践。 规律作息是健康的基础。日出而作&#xff0c;日落而息&#xff0c;顺应自然规律&am…

动手学Agent——Day2

文章目录 一、用 Llama-index 创建 Agent1. 测试模型2. 自定义一个接口类3. 使用 ReActAgent & FunctionTool 构建 Agent 二、数据库对话 Agent1. SQLite 数据库1.1 创建数据库 & 连接1.2 创建、插入、查询、更新、删除数据1.3 关闭连接建立数据库 2. ollama3. 配置对话…

最新国内 ChatGPT Plus/Pro 获取教程

最后更新版本&#xff1a;20250202 教程介绍&#xff1a; 本文将详细介绍如何快速获取一张虚拟信用卡&#xff0c;并通过该卡来获取ChatGPT Plus和ChatGPT Pro。 # 教程全程约15分钟开通ChatGPT Plus会员帐号前准备工作 一个尚未升级的ChatGPT帐号&#xff01;一张虚拟信用卡…

Redis哈希槽机制的实现

Redis哈希槽机制的实现 Redis集群使用哈希槽&#xff08;Hash Slot&#xff09;来管理数据分布&#xff0c;整个集群被划分为固定的16384个哈希槽。当我们在集群中存储一个键时&#xff0c;Redis会先对键进行哈希运算&#xff0c;得到一个哈希值。然后&#xff0c;Redis将该哈…

下载安装运行测试开源vision-language-action(VLA)模型OpenVLA

1. 安装 项目官网OpenVLA 首先按照官网提示的以下代码&#xff0c;执行创建环境->安装最小依赖->git克隆项目等 # Create and activate conda environment conda create -n openvla python3.10 -y conda activate openvla# Install PyTorch. Below is a sample comma…

外贸跨境订货系统流程设计、功能列表及源码输出

在全球化的商业环境下&#xff0c;外贸跨境订货系统对于企业拓展国际市场、提升运营效率至关重要。该系统旨在为外贸企业提供一个便捷、高效、安全的订货平台&#xff0c;实现商品展示、订单管理、物流跟踪等功能&#xff0c;满足跨境业务的多样化需求。以下将详细阐述外贸订货…

排序算法复习——包括插入排序、希尔排序、冒泡排序、快排(包括霍尔法、挖坑法、快慢指针法)、堆排、选择排序、归并排序等 (代码采用c/c++混编)

1.插入排序 插入排序就像我们打斗地主的时候&#xff0c;有一大把牌我们来不及理&#xff0c;就会一张一张的拿然后把拿到的牌放到合适的位置。 对于插入排序我们可以将待排序的数组理解为那一堆没有整理的牌&#xff0c;将排序好的部分理解为手上的牌&#xff0c;对于第i张牌我…

RocketMQ 5.0安装部署

0.前言 在微服务架构逐渐成为主流的今天&#xff0c;消息队列如同数字世界的快递员&#xff0c;承担着系统间高效通信的重要使命。 Apache RocketMQ 自诞生以来&#xff0c;因其架构简单、业务功能丰富、具备极强可扩展性等特点被众多企业开发者以及云厂商广泛采用。历经十余…

Jetson Agx Orin平台preferred_stride调试记录--1924x720图像异常

1.问题描述 硬件: AGX Orin 在Jetpack 5.0.1和Jetpack 5.0.2上测试验证 图像分辨率在1920x720和1024x1920下图像采集正常 但是当采集图像分辨率为1924x720视频时,图像输出异常 像素格式:yuv_uyvy16 gstreamer命令如下 gst-launch-1.0 v4l2src device=/dev/video0 ! …

【玩转全栈】----Django模板语法、请求与响应

目录 一、引言 二、模板语法 三、传参 1、视图函数到模板文件 2、模板文件到视图函数 四、引入静态文件 五、请求与响应 ?1、请求 2、响应 六、综合小案例 1、源码展示 2、注意事项以及部分解释 3、展示 一、引言 像之前那个页面&#xff0c;太过简陋&#xff0c;而且一个完整…

#渗透测试#批量漏洞挖掘#CyberPanel面板远程命令执行漏洞(CVE-2024-51567)

免责声明 本教程仅为合法的教学目的而准备,严禁用于任何形式的违法犯罪活动及其他商业行为,在使用本教程前,您应确保该行为符合当地的法律法规,继续阅读即表示您需自行承担所有操作的后果,如有异议,请立即停止本文章读。 目录 一、漏洞特征与影响 二、修复方案与技术细…

C++多态

目录 多态的概念多态的定义及实现协变析构函数的重写通过一段代码理解多态C11 final 和 override重载、覆盖(重写)、隐藏(重定义)的对比多态调用原理单继承中的虚函数表抽象类多继承中的虚函数表 多态的概念 概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c;具体…

PosgreSQL比MySQL更优秀吗?

一日&#xff0c;一群开发者对PosgreSQL是不是比MySQL更优秀进行了激烈的辩论&#xff0c;双方吵的都要打起来了 正方有以下理由&#xff1a; PostgreSQL严格遵循SQL标准规范&#xff0c;相较MySQL在语法兼容性和功能完整性方面展现出更强的体系化设计&#xff0c;尤其在事务处…

『大模型笔记』Jason Wei: 大语言模型的扩展范式!

Jason Wei: 大语言模型的扩展范式! 文章目录 一. What is scaling and why do it?1. 什么是Scaling?2. 为什么要Scaling?二. Paradigm 1: Scaling next-word prediction1. 下一个词预测2. 极限多任务学习3. Why does scaling work?三. The challenge with next-word predi…

TCP协议(Transmission Control Protocol)

TCP协议&#xff0c;即传输控制协议&#xff0c;其最大的特征就是对传输的数据进行可靠、高效的控制&#xff0c;其段格式如下&#xff1a; 源端口和目的端口号表示数据从哪个进程来&#xff0c;到哪个进程去&#xff0c;四位报头长度表示的是TCP头部有多少个4字节&#xff0c;…