音视频入门基础:RTP专题(9)——FFmpeg接收RTP流的原理和内部实现
- 开源代码
- 2025-09-05 18:24:02

一、引言
由《音视频入门基础: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); else udp_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); else udp_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_CLOEXEC fd = socket(af, type | SOCK_CLOEXEC, proto); if (fd == -1 && errno == EINVAL) #endif { fd = socket(af, type, proto); #if HAVE_FCNTL if (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_NOSIGPIPE if (fd != -1) { if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &(int){1}, sizeof(int))) { av_log(logctx, AV_LOG_WARNING, "setsockopt(SO_NOSIGPIPE) failed\n"); } } #endif return 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数据:
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客户端先被创建,然后服务器才创建。所以可以实现推流端先推流(UDP客户端先发送音视频数据,不管服务器有没有接收到),接收端再接收RTP音视频数据。
音视频入门基础:RTP专题(9)——FFmpeg接收RTP流的原理和内部实现由讯客互联开源代码栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“音视频入门基础:RTP专题(9)——FFmpeg接收RTP流的原理和内部实现”