2929#include "mux.h"
3030#include "libavutil/opt.h"
3131#include "libavcodec/avcodec.h"
32+ #include "libavutil/avstring.h"
33+ #include "url.h"
34+ #include "libavutil/random_seed.h"
3235
3336typedef struct RTCContext {
3437 AVClass * av_class ;
3538
3639 /* Input audio and video codec parameters */
3740 AVCodecParameters * audio_par ;
3841 AVCodecParameters * video_par ;
42+
43+ /* The ICE username and pwd fragment generated by the muxer. */
44+ char ice_ufrag_local [9 ];
45+ char ice_pwd_local [33 ];
46+ /* The SSRC of the audio and video stream, generated by the muxer. */
47+ uint32_t audio_ssrc ;
48+ uint32_t video_ssrc ;
49+ /* The PT(Payload Type) of stream, generated by the muxer. */
50+ uint8_t audio_pt ;
51+ uint8_t video_pt ;
52+ /**
53+ * The SDP offer generated by the muxer according to the codec parameters,
54+ * DTLS and ICE information.
55+ * */
56+ char * sdp_offer ;
57+ /* The SDP answer received from the WebRTC server. */
58+ char * sdp_answer ;
59+ /* The HTTP URL context is the transport layer for the WHIP protocol. */
60+ URLContext * whip_uc ;
3961} RTCContext ;
4062
4163/**
4264 * Only support video(h264) and audio(opus) for now. Note that only baseline
4365 * and constrained baseline of h264 are supported.
66+ *
67+ * @return 0 if OK, AVERROR_xxx on error
4468 */
4569static int check_codec (AVFormatContext * s )
4670{
@@ -112,13 +136,245 @@ static int check_codec(AVFormatContext *s)
112136 return 0 ;
113137}
114138
139+ /**
140+ * Generate SDP offer according to the codec parameters, DTLS and ICE information.
141+ * The below is an example of SDP offer:
142+ *
143+ * v=0
144+ * o=FFmpeg 4489045141692799359 2 IN IP4 127.0.0.1
145+ * s=FFmpegPublishSession
146+ * t=0 0
147+ * a=group:BUNDLE 0 1
148+ * a=extmap-allow-mixed
149+ * a=msid-semantic: WMS
150+ *
151+ * m=audio 9 UDP/TLS/RTP/SAVPF 111
152+ * c=IN IP4 0.0.0.0
153+ * a=ice-ufrag:a174B
154+ * a=ice-pwd:wY8rJ3gNLxL3eWZs6UPOxy
155+ * 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
156+ * a=setup:actpass
157+ * a=mid:0
158+ * a=sendonly
159+ * a=msid:FFmpeg audio
160+ * a=rtcp-mux
161+ * a=rtpmap:111 opus/48000/2
162+ * a=ssrc:4267647086 cname:FFmpeg
163+ * a=ssrc:4267647086 msid:FFmpeg audio
164+ *
165+ * m=video 9 UDP/TLS/RTP/SAVPF 106
166+ * c=IN IP4 0.0.0.0
167+ * a=ice-ufrag:a174B
168+ * a=ice-pwd:wY8rJ3gNLxL3eWZs6UPOxy
169+ * 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
170+ * a=setup:actpass
171+ * a=mid:1
172+ * a=sendonly
173+ * a=msid:FFmpeg video
174+ * a=rtcp-mux
175+ * a=rtcp-rsize
176+ * a=rtpmap:106 H264/90000
177+ * a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
178+ * a=ssrc:107169110 cname:FFmpeg
179+ * a=ssrc:107169110 msid:FFmpeg video
180+ *
181+ * Note that we don't use av_sdp_create to generate SDP offer because it doesn't
182+ * support DTLS and ICE information.
183+ *
184+ * @return 0 if OK, AVERROR_xxx on error
185+ */
186+ static int generate_sdp_offer (AVFormatContext * s )
187+ {
188+ int profile_iop ;
189+ RTCContext * rtc = s -> priv_data ;
190+
191+ if (rtc -> sdp_offer ) {
192+ av_log (s , AV_LOG_ERROR , "SDP offer is already set\n" );
193+ return AVERROR (EINVAL );
194+ }
195+
196+ /* Generate a random ICE ufrag and password by av_get_random_seed. */
197+ snprintf (rtc -> ice_ufrag_local , sizeof (rtc -> ice_ufrag_local ), "%08x" ,
198+ av_get_random_seed ());
199+ snprintf (rtc -> ice_pwd_local , sizeof (rtc -> ice_pwd_local ), "%08x%08x%08x%08x" ,
200+ av_get_random_seed (), av_get_random_seed (), av_get_random_seed (),
201+ av_get_random_seed ());
202+
203+ /* Generate audio and video SSRCs. */
204+ rtc -> audio_ssrc = av_get_random_seed ();
205+ rtc -> video_ssrc = av_get_random_seed ();
206+
207+ /* Set up the PT(Payload Type). */
208+ rtc -> audio_pt = 111 ;
209+ rtc -> video_pt = 106 ;
210+
211+ profile_iop = rtc -> video_par -> profile & FF_PROFILE_H264_CONSTRAINED ? 0xe0 : 0x00 ;
212+ rtc -> sdp_offer = av_asprintf (
213+ "v=0\r\n"
214+ "o=FFmpeg 4489045141692799359 2 IN IP4 127.0.0.1\r\n"
215+ "s=FFmpegPublishSession\r\n"
216+ "t=0 0\r\n"
217+ "a=group:BUNDLE 0 1\r\n"
218+ "a=extmap-allow-mixed\r\n"
219+ "a=msid-semantic: WMS\r\n"
220+ ""
221+ "m=audio 9 UDP/TLS/RTP/SAVPF %s\r\n"
222+ "c=IN IP4 0.0.0.0\r\n"
223+ "a=ice-ufrag:%s\r\n"
224+ "a=ice-pwd:%s\r\n"
225+ "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"
226+ "a=setup:active\r\n"
227+ "a=mid:0\r\n"
228+ "a=sendonly\r\n"
229+ "a=msid:FFmpeg audio\r\n"
230+ "a=rtcp-mux\r\n"
231+ "a=rtpmap:%u opus/48000/2\r\n"
232+ "a=ssrc:%u cname:FFmpeg\r\n"
233+ "a=ssrc:%u msid:FFmpeg audio\r\n"
234+ ""
235+ "m=video 9 UDP/TLS/RTP/SAVPF %u\r\n"
236+ "c=IN IP4 0.0.0.0\r\n"
237+ "a=ice-ufrag:%s\r\n"
238+ "a=ice-pwd:%s\r\n"
239+ "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"
240+ "a=setup:active\r\n"
241+ "a=mid:1\r\n"
242+ "a=sendonly\r\n"
243+ "a=msid:FFmpeg video\r\n"
244+ "a=rtcp-mux\r\n"
245+ "a=rtcp-rsize\r\n"
246+ "a=rtpmap:%u H264/90000\r\n"
247+ "a=fmtp:%u level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=%02x%02x%02x\r\n"
248+ "a=ssrc:%u cname:FFmpeg\r\n"
249+ "a=ssrc:%u msid:FFmpeg video\r\n" ,
250+ rtc -> audio_pt ,
251+ rtc -> ice_ufrag_local ,
252+ rtc -> ice_pwd_local ,
253+ rtc -> audio_pt ,
254+ rtc -> audio_ssrc ,
255+ rtc -> audio_ssrc ,
256+ rtc -> video_pt ,
257+ rtc -> ice_ufrag_local ,
258+ rtc -> ice_pwd_local ,
259+ rtc -> video_pt ,
260+ rtc -> video_pt ,
261+ rtc -> video_par -> profile & (~FF_PROFILE_H264_CONSTRAINED ),
262+ profile_iop ,
263+ rtc -> video_par -> level ,
264+ rtc -> video_ssrc ,
265+ rtc -> video_ssrc
266+ );
267+ av_log (s , AV_LOG_VERBOSE , "Generated offer: %s" , rtc -> sdp_offer );
268+
269+ return 0 ;
270+ }
271+
272+ /**
273+ * Exchange SDP offer with WebRTC peer to get the answer.
274+ * The below is an example of SDP answer:
275+ *
276+ * v=0
277+ * o=SRS/6.0.42(Bee) 107408542208384 2 IN IP4 0.0.0.0
278+ * s=SRSPublishSession
279+ * t=0 0
280+ * a=ice-lite
281+ * a=group:BUNDLE 0 1
282+ * a=msid-semantic: WMS live/show
283+ *
284+ * m=audio 9 UDP/TLS/RTP/SAVPF 111
285+ * c=IN IP4 0.0.0.0
286+ * a=ice-ufrag:ex9061f9
287+ * a=ice-pwd:bi8k19m9n836187b00d1gm3946234w85
288+ * 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
289+ * a=setup:passive
290+ * a=mid:0
291+ * a=recvonly
292+ * a=rtcp-mux
293+ * a=rtcp-rsize
294+ * a=rtpmap:111 opus/48000/2
295+ * a=candidate:0 1 udp 2130706431 172.20.10.7 8000 typ host generation 0
296+ *
297+ * m=video 9 UDP/TLS/RTP/SAVPF 106
298+ * c=IN IP4 0.0.0.0
299+ * a=ice-ufrag:ex9061f9
300+ * a=ice-pwd:bi8k19m9n836187b00d1gm3946234w85
301+ * 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
302+ * a=setup:passive
303+ * a=mid:1
304+ * a=recvonly
305+ * a=rtcp-mux
306+ * a=rtcp-rsize
307+ * a=rtpmap:106 H264/90000
308+ * a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01e
309+ * a=candidate:0 1 udp 2130706431 172.20.10.7 8000 typ host generation 0
310+ *
311+ * @return 0 if OK, AVERROR_xxx on error
312+ */
313+ static int exchange_sdp (AVFormatContext * s )
314+ {
315+ int ret ;
316+ char headers [MAX_URL_SIZE ], buf [MAX_URL_SIZE ];
317+ char * p ;
318+ RTCContext * rtc = s -> priv_data ;
319+
320+ ret = ffurl_alloc (& rtc -> whip_uc , s -> url , AVIO_FLAG_READ_WRITE , & s -> interrupt_callback );
321+ if (ret < 0 ) {
322+ av_log (s , AV_LOG_ERROR , "Failed to alloc HTTP context: %s" , s -> url );
323+ return ret ;
324+ }
325+
326+ /* Set the options to disable chunked_post and enable post for the WHIP IO. */
327+ av_opt_set (rtc -> whip_uc -> priv_data , "chunked_post" , "0" , 0 );
328+ /* Set the HTTP header Content-Type for the SDP offer. */
329+ snprintf (headers , sizeof (headers ),
330+ "Cache-Control: no-cache\r\n"
331+ "Content-Type: application/sdp\r\n" );
332+ av_opt_set (rtc -> whip_uc -> priv_data , "headers" , headers , 0 );
333+ /* Set the offer as the post data, to send when connection HTTP. */
334+ av_opt_set_bin (rtc -> whip_uc -> priv_data , "post_data" , rtc -> sdp_offer , (int )strlen (rtc -> sdp_offer ), 0 );
335+
336+ /* Open the HTTP URL with POST and send the SDP offer. */
337+ ret = ffurl_connect (rtc -> whip_uc , NULL );
338+ if (ret < 0 ) {
339+ av_log (s , AV_LOG_ERROR , "Failed to open WHIP URL: %s" , s -> url );
340+ return ret ;
341+ }
342+
343+ /* Read the answer from response */
344+ for (;;) {
345+ ret = ffurl_read (rtc -> whip_uc , buf , sizeof (buf ));
346+ if (ret == AVERROR_EOF ) {
347+ /* Reset the error because we read all response as answer util EOF. */
348+ ret = 0 ;
349+ break ;
350+ }
351+ if (ret <= 0 ) {
352+ av_log (s , AV_LOG_ERROR , "Failed to read response from URL: %s" , s -> url );
353+ return ret ;
354+ }
355+
356+ p = rtc -> sdp_answer ;
357+ rtc -> sdp_answer = av_asprintf ("%s%.*s" , p ? p : "" , ret , buf );
358+ av_free (p );
359+ }
360+ av_log (s , AV_LOG_VERBOSE , "Got answer: %s" , rtc -> sdp_answer );
361+
362+ return ret ;
363+ }
364+
115365static int rtc_init (AVFormatContext * s )
116366{
117367 int ret ;
118368
119369 if ((ret = check_codec (s )) < 0 )
120370 return ret ;
121371
372+ if ((ret = generate_sdp_offer (s )) < 0 )
373+ return ret ;
374+
375+ if ((ret = exchange_sdp (s )) < 0 )
376+ return ret ;
377+
122378 return 0 ;
123379}
124380
@@ -139,6 +395,10 @@ static int rtc_write_trailer(AVFormatContext *s)
139395
140396static void rtc_deinit (AVFormatContext * s )
141397{
398+ RTCContext * rtc = s -> priv_data ;
399+ av_freep (& rtc -> sdp_offer );
400+ av_freep (& rtc -> sdp_answer );
401+ ffurl_closep (& rtc -> whip_uc );
142402}
143403
144404static const AVOption options [] = {
0 commit comments