From cf69b017c1f47d7408b7da6759e800b318f8dc6b Mon Sep 17 00:00:00 2001 From: yangrtc Date: Sat, 22 Apr 2023 07:00:55 +0800 Subject: [PATCH] WHIP: Generate offer and exchange with server to get answer. --- libavformat/rtcenc.c | 260 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 260 insertions(+) diff --git a/libavformat/rtcenc.c b/libavformat/rtcenc.c index 46bcb969e14d1..def6991410a60 100644 --- a/libavformat/rtcenc.c +++ b/libavformat/rtcenc.c @@ -29,6 +29,9 @@ #include "mux.h" #include "libavutil/opt.h" #include "libavcodec/avcodec.h" +#include "libavutil/avstring.h" +#include "url.h" +#include "libavutil/random_seed.h" typedef struct RTCContext { AVClass *av_class; @@ -36,11 +39,32 @@ typedef struct RTCContext { /* Input audio and video codec parameters */ AVCodecParameters *audio_par; AVCodecParameters *video_par; + + /* 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. */ + uint32_t audio_ssrc; + uint32_t video_ssrc; + /* The PT(Payload Type) of stream, generated by the muxer. */ + uint8_t audio_pt; + uint8_t video_pt; + /** + * The SDP offer generated by the muxer according to the codec parameters, + * DTLS and ICE information. + * */ + char *sdp_offer; + /* The SDP answer received from the WebRTC server. */ + char *sdp_answer; + /* The HTTP URL context is the transport layer for the WHIP protocol. */ + URLContext *whip_uc; } RTCContext; /** * Only support video(h264) and audio(opus) for now. Note that only baseline * and constrained baseline of h264 are supported. + * + * @return 0 if OK, AVERROR_xxx on error */ static int check_codec(AVFormatContext *s) { @@ -112,6 +136,232 @@ static int check_codec(AVFormatContext *s) return 0; } +/** + * Generate SDP offer according to the codec parameters, DTLS and ICE information. + * The below is an example of SDP offer: + * + * v=0 + * o=FFmpeg 4489045141692799359 2 IN IP4 127.0.0.1 + * s=FFmpegPublishSession + * t=0 0 + * a=group:BUNDLE 0 1 + * a=extmap-allow-mixed + * a=msid-semantic: WMS + * + * m=audio 9 UDP/TLS/RTP/SAVPF 111 + * c=IN IP4 0.0.0.0 + * a=ice-ufrag:a174B + * a=ice-pwd:wY8rJ3gNLxL3eWZs6UPOxy + * a=fingerprint:sha-256 EE:FE:A2:E5:6A:21:78:60:71:2C:21:DC:1A:2C:98:12:0C:E8:AD:68:07:61:1B:0E:FC:46:97:1E:BC:97:4A:54 + * a=setup:actpass + * a=mid:0 + * a=sendonly + * a=msid:FFmpeg audio + * a=rtcp-mux + * a=rtpmap:111 opus/48000/2 + * a=ssrc:4267647086 cname:FFmpeg + * a=ssrc:4267647086 msid:FFmpeg audio + * + * m=video 9 UDP/TLS/RTP/SAVPF 106 + * c=IN IP4 0.0.0.0 + * a=ice-ufrag:a174B + * a=ice-pwd:wY8rJ3gNLxL3eWZs6UPOxy + * a=fingerprint:sha-256 EE:FE:A2:E5:6A:21:78:60:71:2C:21:DC:1A:2C:98:12:0C:E8:AD:68:07:61:1B:0E:FC:46:97:1E:BC:97:4A:54 + * a=setup:actpass + * a=mid:1 + * a=sendonly + * a=msid:FFmpeg video + * a=rtcp-mux + * a=rtcp-rsize + * a=rtpmap:106 H264/90000 + * a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f + * a=ssrc:107169110 cname:FFmpeg + * a=ssrc:107169110 msid:FFmpeg video + * + * Note that we don't use av_sdp_create to generate SDP offer because it doesn't + * support DTLS and ICE information. + * + * @return 0 if OK, AVERROR_xxx on error + */ +static int generate_sdp_offer(AVFormatContext *s) +{ + int profile_iop; + RTCContext *rtc = s->priv_data; + + if (rtc->sdp_offer) { + av_log(s, AV_LOG_ERROR, "SDP offer is already set\n"); + return AVERROR(EINVAL); + } + + /* Generate a random ICE ufrag and password by av_get_random_seed. */ + snprintf(rtc->ice_ufrag_local, sizeof(rtc->ice_ufrag_local), "%08x", + av_get_random_seed()); + snprintf(rtc->ice_pwd_local, sizeof(rtc->ice_pwd_local), "%08x%08x%08x%08x", + av_get_random_seed(), av_get_random_seed(), av_get_random_seed(), + av_get_random_seed()); + + /* Generate audio and video SSRCs. */ + rtc->audio_ssrc = av_get_random_seed(); + rtc->video_ssrc = av_get_random_seed(); + + /* Set up the PT(Payload Type). */ + rtc->audio_pt = 111; + rtc->video_pt = 106; + + profile_iop = rtc->video_par->profile & FF_PROFILE_H264_CONSTRAINED ? 0xe0 : 0x00; + rtc->sdp_offer = av_asprintf( + "v=0\r\n" + "o=FFmpeg 4489045141692799359 2 IN IP4 127.0.0.1\r\n" + "s=FFmpegPublishSession\r\n" + "t=0 0\r\n" + "a=group:BUNDLE 0 1\r\n" + "a=extmap-allow-mixed\r\n" + "a=msid-semantic: WMS\r\n" + "" + "m=audio 9 UDP/TLS/RTP/SAVPF %u\r\n" + "c=IN IP4 0.0.0.0\r\n" + "a=ice-ufrag:%s\r\n" + "a=ice-pwd:%s\r\n" + "a=fingerprint:sha-256 EE:FE:A2:E5:6A:21:78:60:71:2C:21:DC:1A:2C:98:12:0C:E8:AD:68:07:61:1B:0E:FC:46:97:1E:BC:97:4A:54\r\n" + "a=setup:active\r\n" + "a=mid:0\r\n" + "a=sendonly\r\n" + "a=msid:FFmpeg audio\r\n" + "a=rtcp-mux\r\n" + "a=rtpmap:%u opus/48000/2\r\n" + "a=ssrc:%u cname:FFmpeg\r\n" + "a=ssrc:%u msid:FFmpeg audio\r\n" + "" + "m=video 9 UDP/TLS/RTP/SAVPF %u\r\n" + "c=IN IP4 0.0.0.0\r\n" + "a=ice-ufrag:%s\r\n" + "a=ice-pwd:%s\r\n" + "a=fingerprint:sha-256 EE:FE:A2:E5:6A:21:78:60:71:2C:21:DC:1A:2C:98:12:0C:E8:AD:68:07:61:1B:0E:FC:46:97:1E:BC:97:4A:54\r\n" + "a=setup:active\r\n" + "a=mid:1\r\n" + "a=sendonly\r\n" + "a=msid:FFmpeg video\r\n" + "a=rtcp-mux\r\n" + "a=rtcp-rsize\r\n" + "a=rtpmap:%u H264/90000\r\n" + "a=fmtp:%u level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=%02x%02x%02x\r\n" + "a=ssrc:%u cname:FFmpeg\r\n" + "a=ssrc:%u msid:FFmpeg video\r\n", + rtc->audio_pt, + rtc->ice_ufrag_local, + rtc->ice_pwd_local, + rtc->audio_pt, + rtc->audio_ssrc, + rtc->audio_ssrc, + rtc->video_pt, + rtc->ice_ufrag_local, + rtc->ice_pwd_local, + rtc->video_pt, + rtc->video_pt, + rtc->video_par->profile & (~FF_PROFILE_H264_CONSTRAINED), + profile_iop, + rtc->video_par->level, + rtc->video_ssrc, + rtc->video_ssrc + ); + av_log(s, AV_LOG_VERBOSE, "Generated offer: %s", rtc->sdp_offer); + + return 0; +} + +/** + * Exchange SDP offer with WebRTC peer to get the answer. + * The below is an example of SDP answer: + * + * v=0 + * o=SRS/6.0.42(Bee) 107408542208384 2 IN IP4 0.0.0.0 + * s=SRSPublishSession + * t=0 0 + * a=ice-lite + * a=group:BUNDLE 0 1 + * a=msid-semantic: WMS live/show + * + * m=audio 9 UDP/TLS/RTP/SAVPF 111 + * c=IN IP4 0.0.0.0 + * a=ice-ufrag:ex9061f9 + * a=ice-pwd:bi8k19m9n836187b00d1gm3946234w85 + * a=fingerprint:sha-256 68:DD:7A:95:27:BD:0A:99:F4:7A:83:21:2F:50:15:2A:1D:1F:8A:D8:96:24:42:2D:A1:83:99:BF:F1:E2:11:A2 + * a=setup:passive + * a=mid:0 + * a=recvonly + * a=rtcp-mux + * a=rtcp-rsize + * a=rtpmap:111 opus/48000/2 + * a=candidate:0 1 udp 2130706431 172.20.10.7 8000 typ host generation 0 + * + * m=video 9 UDP/TLS/RTP/SAVPF 106 + * c=IN IP4 0.0.0.0 + * a=ice-ufrag:ex9061f9 + * a=ice-pwd:bi8k19m9n836187b00d1gm3946234w85 + * a=fingerprint:sha-256 68:DD:7A:95:27:BD:0A:99:F4:7A:83:21:2F:50:15:2A:1D:1F:8A:D8:96:24:42:2D:A1:83:99:BF:F1:E2:11:A2 + * a=setup:passive + * a=mid:1 + * a=recvonly + * a=rtcp-mux + * a=rtcp-rsize + * a=rtpmap:106 H264/90000 + * a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01e + * a=candidate:0 1 udp 2130706431 172.20.10.7 8000 typ host generation 0 + * + * @return 0 if OK, AVERROR_xxx on error + */ +static int exchange_sdp(AVFormatContext *s) +{ + int ret; + char headers[MAX_URL_SIZE], buf[MAX_URL_SIZE]; + char *p; + RTCContext *rtc = s->priv_data; + + ret = ffurl_alloc(&rtc->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); + return ret; + } + + /* Set the options to disable chunked_post and enable post for the WHIP IO. */ + av_opt_set(rtc->whip_uc->priv_data, "chunked_post", "0", 0); + /* Set the HTTP header Content-Type for the SDP offer. */ + snprintf(headers, sizeof(headers), + "Cache-Control: no-cache\r\n" + "Content-Type: application/sdp\r\n"); + av_opt_set(rtc->whip_uc->priv_data, "headers", headers, 0); + /* Set the offer as the post data, to send when connection HTTP. */ + av_opt_set_bin(rtc->whip_uc->priv_data, "post_data", rtc->sdp_offer, (int)strlen(rtc->sdp_offer), 0); + + /* Open the HTTP URL with POST and send the SDP offer. */ + ret = ffurl_connect(rtc->whip_uc, NULL); + if (ret < 0) { + av_log(s, AV_LOG_ERROR, "Failed to open WHIP URL: %s", s->url); + return ret; + } + + /* Read the answer from response */ + for (;;) { + ret = ffurl_read(rtc->whip_uc, buf, sizeof(buf)); + if (ret == AVERROR_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", s->url); + return ret; + } + + p = rtc->sdp_answer; + rtc->sdp_answer = av_asprintf("%s%.*s", p ? p : "", ret, buf); + av_free(p); + } + av_log(s, AV_LOG_VERBOSE, "Got answer: %s", rtc->sdp_answer); + + return ret; +} + static int rtc_init(AVFormatContext *s) { int ret; @@ -119,6 +369,12 @@ static int rtc_init(AVFormatContext *s) if ((ret = check_codec(s)) < 0) return ret; + if ((ret = generate_sdp_offer(s)) < 0) + return ret; + + if ((ret = exchange_sdp(s)) < 0) + return ret; + return 0; } @@ -139,6 +395,10 @@ static int rtc_write_trailer(AVFormatContext *s) static void rtc_deinit(AVFormatContext *s) { + RTCContext *rtc = s->priv_data; + av_freep(&rtc->sdp_offer); + av_freep(&rtc->sdp_answer); + ffurl_closep(&rtc->whip_uc); } static const AVOption options[] = {