29
29
#include "mux.h"
30
30
#include "libavutil/opt.h"
31
31
#include "libavcodec/avcodec.h"
32
+ #include "libavutil/avstring.h"
33
+ #include "url.h"
34
+ #include "libavutil/random_seed.h"
32
35
33
36
typedef struct RTCContext {
34
37
AVClass * av_class ;
35
38
36
39
/* Input audio and video codec parameters */
37
40
AVCodecParameters * audio_par ;
38
41
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 ;
39
61
} RTCContext ;
40
62
41
63
/**
42
64
* Only support video(h264) and audio(opus) for now. Note that only baseline
43
65
* and constrained baseline of h264 are supported.
66
+ *
67
+ * @return 0 if OK, AVERROR_xxx on error
44
68
*/
45
69
static int check_codec (AVFormatContext * s )
46
70
{
@@ -112,13 +136,245 @@ static int check_codec(AVFormatContext *s)
112
136
return 0 ;
113
137
}
114
138
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
+
115
365
static int rtc_init (AVFormatContext * s )
116
366
{
117
367
int ret ;
118
368
119
369
if ((ret = check_codec (s )) < 0 )
120
370
return ret ;
121
371
372
+ if ((ret = generate_sdp_offer (s )) < 0 )
373
+ return ret ;
374
+
375
+ if ((ret = exchange_sdp (s )) < 0 )
376
+ return ret ;
377
+
122
378
return 0 ;
123
379
}
124
380
@@ -139,6 +395,10 @@ static int rtc_write_trailer(AVFormatContext *s)
139
395
140
396
static void rtc_deinit (AVFormatContext * s )
141
397
{
398
+ RTCContext * rtc = s -> priv_data ;
399
+ av_freep (& rtc -> sdp_offer );
400
+ av_freep (& rtc -> sdp_answer );
401
+ ffurl_closep (& rtc -> whip_uc );
142
402
}
143
403
144
404
static const AVOption options [] = {
0 commit comments