diff --git a/libavformat/rtcenc.c b/libavformat/rtcenc.c index e51359ea827f2..fbd64b856a351 100644 --- a/libavformat/rtcenc.c +++ b/libavformat/rtcenc.c @@ -35,47 +35,55 @@ #include "avio_internal.h" #include "libavutil/hmac.h" #include "libavutil/crc.h" +#include "network.h" +#include "libavutil/time.h" +/* The maximum size of an SDP, either offer or answer. */ #define MAX_SDP_SIZE 8192 +/* The maximum size of a UDP packet, should be smaller than the MTU. */ #define MAX_UDP_SIZE 1500 +/* The maximum number of retries for UDP transmission. */ +#define UDP_FAST_RETRIES 6 +/* The startup timeout for UDP transmission. */ +#define UDP_START_TIMEOUT 21 typedef struct RTCContext { AVClass *av_class; - /* Input audio and video codec parameters */ + /* input audio and video codec parameters */ AVCodecParameters *audio_par; AVCodecParameters *video_par; - /* The ICE username and pwd fragment generated by the muxer. */ + /* the ICE username and pwd fragment generated by the muxer. */ char ice_ufrag_local[9]; char ice_pwd_local[33]; - /* The SSRC of the audio and video stream, generated by the muxer. */ + /* the SSRC of the audio and video stream, generated by the muxer. */ uint32_t audio_ssrc; uint32_t video_ssrc; - /* The PT(Payload Type) of stream, generated by the muxer. */ + /* the PT(Payload Type) of stream, generated by the muxer. */ uint8_t audio_payload_type; uint8_t video_payload_type; /** - * The SDP offer generated by the muxer according to the codec parameters, + * the SDP offer generated by the muxer according to the codec parameters, * DTLS and ICE information. * */ char *sdp_offer; - /* The ICE username and pwd from remote server. */ + /* the ICE username and pwd from remote server. */ char *ice_ufrag_remote; char *ice_pwd_remote; /** - * The ICE candidate protocol, priority, host and port. Note that only + * the ICE candidate protocol, priority, host and port. Note that only * support one candidate for now. We will choose the first udp candidate. * We will support multiple candidates in the future. */ char *ice_protocol; char *ice_host; int ice_port; - /* The SDP answer received from the WebRTC server. */ + /* the SDP answer received from the WebRTC server. */ char *sdp_answer; - /* The UDP transport is used for delivering ICE, DTLS and SRTP packets. */ + /* the UDP transport is used for delivering ICE, DTLS and SRTP packets. */ URLContext *udp_uc; } RTCContext; @@ -200,7 +208,7 @@ static int generate_sdp_offer(AVFormatContext *s) char *tmp = av_mallocz(MAX_SDP_SIZE); if (!tmp) { - av_log(s, AV_LOG_ERROR, "Failed to alloc answer: %s", s->url); + av_log(s, AV_LOG_ERROR, "Failed to alloc answer: %s\n", s->url); return AVERROR(ENOMEM); } @@ -280,13 +288,13 @@ static int generate_sdp_offer(AVFormatContext *s) rtc->video_ssrc, rtc->video_ssrc); if (ret >= MAX_SDP_SIZE) { - av_log(s, AV_LOG_ERROR, "Offer %d exceed max %d, %s", ret, MAX_SDP_SIZE, tmp); + av_log(s, AV_LOG_ERROR, "Offer %d exceed max %d, %s\n", ret, MAX_SDP_SIZE, tmp); ret = AVERROR(EIO); goto end; } rtc->sdp_offer = av_strdup(tmp); - av_log(s, AV_LOG_VERBOSE, "Generated offer: %s", rtc->sdp_offer); + av_log(s, AV_LOG_VERBOSE, "Generated offer: %s\n", rtc->sdp_offer); end: av_free(tmp); @@ -339,18 +347,18 @@ static int exchange_sdp(AVFormatContext *s) int ret; char buf[MAX_URL_SIZE]; RTCContext *rtc = s->priv_data; - /* The URL context is an HTTP transport layer for the WHIP protocol. */ + /* the URL context is an HTTP transport layer for the WHIP protocol. */ URLContext *whip_uc = NULL; char *tmp = av_mallocz(MAX_SDP_SIZE); if (!tmp) { - av_log(s, AV_LOG_ERROR, "Failed to alloc answer: %s", s->url); + av_log(s, AV_LOG_ERROR, "Failed to alloc answer: %s\n", s->url); return AVERROR(ENOMEM); } ret = ffurl_alloc(&whip_uc, s->url, AVIO_FLAG_READ_WRITE, &s->interrupt_callback); if (ret < 0) { - av_log(s, AV_LOG_ERROR, "Failed to alloc HTTP context: %s", s->url); + av_log(s, AV_LOG_ERROR, "Failed to alloc HTTP context: %s\n", s->url); goto end; } @@ -363,33 +371,33 @@ static int exchange_sdp(AVFormatContext *s) ret = ffurl_connect(whip_uc, NULL); if (ret < 0) { - av_log(s, AV_LOG_ERROR, "Failed to request url=%s, offer: %s", s->url, rtc->sdp_offer); + av_log(s, AV_LOG_ERROR, "Failed to request url=%s, offer: %s\n", s->url, rtc->sdp_offer); goto end; } while (1) { ret = ffurl_read(whip_uc, buf, sizeof(buf)); if (ret == AVERROR_EOF) { - /* Reset the error because we read all response as answer util EOF. */ + /* reset the error because we read all response as answer util EOF. */ ret = 0; break; } if (ret <= 0) { - av_log(s, AV_LOG_ERROR, "Failed to read response from url=%s, offer is %s, answer is %s", + av_log(s, AV_LOG_ERROR, "Failed to read response from url=%s, offer is %s, answer is %s\n", s->url, rtc->sdp_offer, rtc->sdp_answer); goto end; } ret = av_strlcatf(tmp, MAX_SDP_SIZE, "%.*s", ret, buf); if (ret >= MAX_SDP_SIZE) { - av_log(s, AV_LOG_ERROR, "Answer %d exceed max size %d, %s", ret, MAX_SDP_SIZE, tmp); + av_log(s, AV_LOG_ERROR, "Answer %d exceed max size %d, %s\n", ret, MAX_SDP_SIZE, tmp); ret = AVERROR(EIO); goto end; } } rtc->sdp_answer = av_strdup(tmp); - av_log(s, AV_LOG_VERBOSE, "Got answer: %s", rtc->sdp_answer); + av_log(s, AV_LOG_VERBOSE, "Got answer: %s\n", rtc->sdp_answer); end: ffurl_closep(&whip_uc); @@ -413,7 +421,7 @@ static int parse_answer(AVFormatContext *s) pb = avio_alloc_context(rtc->sdp_answer, strlen(rtc->sdp_answer), AVIO_FLAG_READ, NULL, NULL, NULL, NULL); if (!pb) { - av_log(s, AV_LOG_ERROR, "Failed to alloc AVIOContext for answer: %s", rtc->sdp_answer); + av_log(s, AV_LOG_ERROR, "Failed to alloc AVIOContext for answer: %s\n", rtc->sdp_answer); ret = AVERROR(ENOMEM); goto end; } @@ -431,14 +439,14 @@ static int parse_answer(AVFormatContext *s) int priority, port; ret = sscanf(ptr, "%16s %d %128s %d typ host", protocol, &priority, host, &port); if (ret != 4) { - av_log(s, AV_LOG_ERROR, "Failed %d to parse line %d %s from %s", + av_log(s, AV_LOG_ERROR, "Failed %d to parse line %d %s from %s\n", ret, i, line, rtc->sdp_answer); ret = AVERROR(EIO); goto end; } if (av_strcasecmp(protocol, "udp")) { - av_log(s, AV_LOG_ERROR, "Protocol %s is not supported by RTC, choose udp, line %d %s of %s", + av_log(s, AV_LOG_ERROR, "Protocol %s is not supported by RTC, choose udp, line %d %s of %s\n", protocol, i, line, rtc->sdp_answer); ret = AVERROR(EIO); goto end; @@ -461,67 +469,57 @@ static int parse_answer(AVFormatContext *s) } /** - * Open the UDP transport and complete the ICE handshake. + * Create and marshal ICE binding request packet. The size of the response is + * returned in size_of_response. * * @return 0 if OK, AVERROR_xxx on error */ -static int ice_handshake(AVFormatContext *s) +static int ice_create_request(AVFormatContext *s, uint8_t *buf, int size, int *size_of_response) { int ret, len, crc32; - char url[256], buf[MAX_UDP_SIZE]; + char username[128]; AVIOContext *pb = NULL; AVHMAC *hmac = NULL; RTCContext *rtc = s->priv_data; - pb = avio_alloc_context(buf, sizeof(buf), AVIO_FLAG_WRITE, NULL, NULL, NULL, NULL); + pb = avio_alloc_context(buf, size, AVIO_FLAG_WRITE, NULL, NULL, NULL, NULL); if (!pb) { - av_log(s, AV_LOG_ERROR, "Failed to alloc AVIOContext for ICE"); + av_log(s, AV_LOG_ERROR, "Failed to alloc AVIOContext for ICE\n"); ret = AVERROR(ENOMEM); goto end; } hmac = av_hmac_alloc(AV_HMAC_SHA1); if (!hmac) { - av_log(s, AV_LOG_ERROR, "Failed to alloc AVHMAC for ICE"); + av_log(s, AV_LOG_ERROR, "Failed to alloc AVHMAC for ICE\n"); ret = AVERROR(ENOMEM); goto end; } - /* Build UDP URL and create the UDP context as transport. */ - ff_url_join(url, sizeof(url), "udp", NULL, rtc->ice_host, rtc->ice_port, NULL); - ret = ffurl_alloc(&rtc->udp_uc, url, AVIO_FLAG_WRITE | AVIO_FLAG_NONBLOCK, &s->interrupt_callback); - if (ret < 0) { - av_log(s, AV_LOG_ERROR, "Failed to open udp://%s:%d", rtc->ice_host, rtc->ice_port); - goto end; - } - - av_opt_set(rtc->udp_uc->priv_data, "connect", "1", 0); - av_opt_set(rtc->udp_uc->priv_data, "fifo_size", "0", 0); - - ret = ffurl_connect(rtc->udp_uc, NULL); - if (ret < 0) { - av_log(s, AV_LOG_ERROR, "Failed to connect udp://%s:%d", rtc->ice_host, rtc->ice_port); - goto end; - } - - /* Set the transport as READ and WRITE after connected. */ - rtc->udp_uc->flags |= AVIO_FLAG_READ; - - /* Build and send the STUN binding request. */ - /* Write 20 bytes header */ + /* write 20 bytes header */ avio_wb16(pb, 0x0001); /* STUN binding request */ avio_wb16(pb, 0); /* length */ avio_wb32(pb, 0x2112A442); /* magic cookie */ avio_wb32(pb, av_get_random_seed()); /* transaction ID */ avio_wb32(pb, av_get_random_seed()); /* transaction ID */ avio_wb32(pb, av_get_random_seed()); /* transaction ID */ - /* Write the username attribute */ - ret = snprintf(url, sizeof(url), "%s:%s", rtc->ice_ufrag_remote, rtc->ice_ufrag_local); + + /* the username is the concatenation of the two ICE ufrag */ + ret = snprintf(username, sizeof(username), "%s:%s", rtc->ice_ufrag_remote, rtc->ice_ufrag_local); + if (ret <= 0 || ret >= sizeof(username)) { + av_log(s, AV_LOG_ERROR, "Failed to build username %s:%s, max=%lu, ret=%d\n", + rtc->ice_ufrag_remote, rtc->ice_ufrag_local, sizeof(username), ret); + ret = AVERROR(EIO); + goto end; + } + + /* write the username attribute */ avio_wb16(pb, 0x0006); /* attribute type username */ avio_wb16(pb, ret); /* size of username */ - avio_write(pb, url, ret); /* bytes of username */ + avio_write(pb, username, ret); /* bytes of username */ ffio_fill(pb, 0, (4 - (ret % 4)) % 4); /* padding */ - /* Build and update message integrity */ + + /* build and update message integrity */ avio_wb16(pb, 0x0008); /* attribute type message integrity */ avio_wb16(pb, 20); /* size of message integrity */ ffio_fill(pb, 0, 20); /* fill with zero to directly write and skip it */ @@ -531,41 +529,106 @@ static int ice_handshake(AVFormatContext *s) av_hmac_init(hmac, rtc->ice_pwd_local, strlen(rtc->ice_pwd_local)); av_hmac_update(hmac, buf, len - 24); av_hmac_final(hmac, buf + len - 20, 20); - /* Write the fingerprint attribute */ + + /* write the fingerprint attribute */ avio_wb16(pb, 0x8028); /* attribute type fingerprint */ avio_wb16(pb, 4); /* size of fingerprint */ ffio_fill(pb, 0, 4); /* fill with zero to directly write and skip it */ len = avio_tell(pb); buf[2] = (len - 20) >> 8; buf[3] = (len - 20) & 0xFF; + /* refer to the av_hash_alloc("CRC32"), av_hash_init and av_hash_final */ crc32 = av_crc(av_crc_get_table(AV_CRC_32_IEEE_LE), 0xFFFFFFFF, buf, len - 8) ^ 0xFFFFFFFF; avio_skip(pb, -4); - avio_wb32(pb, crc32 ^ 0x5354554E); /* hash message by CRC32 */ + avio_wb32(pb, crc32 ^ 0x5354554E); /* xor with "STUN" */ + + *size_of_response = len; + +end: + avio_context_free(&pb); + av_hmac_free(hmac); + return ret; +} - ret = ffurl_write(rtc->udp_uc, buf, len); +/** + * Open the UDP transport and complete the ICE handshake. Use fast retransmit to + * handle packet loss for the binding request. + * + * @return 0 if OK, AVERROR_xxx on error + */ +static int ice_handshake(AVFormatContext *s) +{ + int ret, len, fast_retries = UDP_FAST_RETRIES, timeout = UDP_START_TIMEOUT; + char url[256], buf[MAX_UDP_SIZE]; + RTCContext *rtc = s->priv_data; + + /* Build UDP URL and create the UDP context as transport. */ + ff_url_join(url, sizeof(url), "udp", NULL, rtc->ice_host, rtc->ice_port, NULL); + ret = ffurl_alloc(&rtc->udp_uc, url, AVIO_FLAG_WRITE, &s->interrupt_callback); + if (ret < 0) { + av_log(s, AV_LOG_ERROR, "Failed to open udp://%s:%d\n", rtc->ice_host, rtc->ice_port); + goto end; + } + + av_opt_set(rtc->udp_uc->priv_data, "connect", "1", 0); + av_opt_set(rtc->udp_uc->priv_data, "fifo_size", "0", 0); + + ret = ffurl_connect(rtc->udp_uc, NULL); if (ret < 0) { - av_log(s, AV_LOG_ERROR, "Failed to send STUN binding request, size=%d", len); + av_log(s, AV_LOG_ERROR, "Failed to connect udp://%s:%d\n", rtc->ice_host, rtc->ice_port); goto end; } - /* Read the STUN binding response. */ - ret = ffurl_read(rtc->udp_uc, buf, sizeof(buf)); + /* make the socket non-blocking, set to READ and WRITE mode after connected */ + ff_socket_nonblock(ffurl_get_file_handle(rtc->udp_uc), 1); + rtc->udp_uc->flags |= AVIO_FLAG_READ | AVIO_FLAG_NONBLOCK; + + /* Build the STUN binding request. */ + ret = ice_create_request(s, buf, sizeof(buf), &len); if (ret < 0) { - av_log(s, AV_LOG_ERROR, "Failed to read STUN binding response"); + av_log(s, AV_LOG_ERROR, "Failed to create STUN binding request, len=%d\n", len); goto end; } + /* Fast retransmit the STUN binding request. */ + do { + ret = ffurl_write(rtc->udp_uc, buf, len); + if (ret < 0) { + av_log(s, AV_LOG_ERROR, "Failed to send STUN binding request, size=%d\n", len); + goto end; + } + + /* If max retries is 6 and start timeout is 21ms, the total timeout + * is about 21 + 42 + 84 + 168 + 336 + 672 = 1263ms. */ + if (fast_retries) { + av_usleep(timeout * 1000); + timeout *= 2; + } + + /* Read the STUN binding response. */ + ret = ffurl_read(rtc->udp_uc, buf, sizeof(buf)); + if (ret < 0) { + if (ret == AVERROR(EAGAIN) && fast_retries) { + fast_retries--; + continue; + } + av_log(s, AV_LOG_ERROR, "Failed to read STUN binding response, retries=%d\n", UDP_FAST_RETRIES); + goto end; + } + } while (ret < 0); + if (ret < 2 || buf[0] != 0x01 || buf[1] != 0x01) { - av_log(s, AV_LOG_ERROR, "Invalid STUN binding response, size=%d, type=%02X%02X", ret, buf[0], buf[1]); + av_log(s, AV_LOG_ERROR, "Invalid STUN binding response, size=%d, type=%02X%02X\n", ret, buf[0], buf[1]); ret = AVERROR(EIO); goto end; } - av_log(s, AV_LOG_VERBOSE, "ICE STUN ok, url=udp://%s:%d, username=%s:%s, request=%dB, response=%dB\n", - rtc->ice_host, rtc->ice_port, rtc->ice_ufrag_remote, rtc->ice_ufrag_local, len, ret); + + av_log(s, AV_LOG_VERBOSE, "ICE STUN ok, url=udp://%s:%d, username=%s:%s, req=%dB, res=%dB, arq=%d\n", + rtc->ice_host, rtc->ice_port, rtc->ice_ufrag_remote, rtc->ice_ufrag_local, len, ret, + UDP_FAST_RETRIES - fast_retries); + ret = 0; end: - avio_context_free(&pb); - av_hmac_free(hmac); return ret; } @@ -585,15 +648,17 @@ static av_cold int rtc_init(AVFormatContext *s) if ((ret = parse_answer(s)) < 0) return ret; - if ((ret = ice_handshake(s)) < 0) - return ret; - return 0; } static int rtc_write_header(AVFormatContext *s) { - return 0; + int ret; + + if ((ret = ice_handshake(s)) < 0) + return ret; + + return ret; } static int rtc_write_packet(AVFormatContext *s, AVPacket *pkt)