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_payload_type ;
51
+ uint8_t video_payload_type ;
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
{
@@ -103,13 +127,240 @@ static int check_codec(AVFormatContext *s)
103
127
return 0 ;
104
128
}
105
129
130
+ /**
131
+ * Generate SDP offer according to the codec parameters, DTLS and ICE information.
132
+ * The below is an example of SDP offer:
133
+ *
134
+ * v=0
135
+ * o=FFmpeg 4489045141692799359 2 IN IP4 127.0.0.1
136
+ * s=FFmpegPublishSession
137
+ * t=0 0
138
+ * a=group:BUNDLE 0 1
139
+ * a=extmap-allow-mixed
140
+ * a=msid-semantic: WMS
141
+ *
142
+ * m=audio 9 UDP/TLS/RTP/SAVPF 111
143
+ * c=IN IP4 0.0.0.0
144
+ * a=ice-ufrag:a174B
145
+ * a=ice-pwd:wY8rJ3gNLxL3eWZs6UPOxy
146
+ * 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
147
+ * a=setup:actpass
148
+ * a=mid:0
149
+ * a=sendonly
150
+ * a=msid:FFmpeg audio
151
+ * a=rtcp-mux
152
+ * a=rtpmap:111 opus/48000/2
153
+ * a=ssrc:4267647086 cname:FFmpeg
154
+ * a=ssrc:4267647086 msid:FFmpeg audio
155
+ *
156
+ * m=video 9 UDP/TLS/RTP/SAVPF 106
157
+ * c=IN IP4 0.0.0.0
158
+ * a=ice-ufrag:a174B
159
+ * a=ice-pwd:wY8rJ3gNLxL3eWZs6UPOxy
160
+ * 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
161
+ * a=setup:actpass
162
+ * a=mid:1
163
+ * a=sendonly
164
+ * a=msid:FFmpeg video
165
+ * a=rtcp-mux
166
+ * a=rtcp-rsize
167
+ * a=rtpmap:106 H264/90000
168
+ * a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
169
+ * a=ssrc:107169110 cname:FFmpeg
170
+ * a=ssrc:107169110 msid:FFmpeg video
171
+ *
172
+ * Note that we don't use av_sdp_create to generate SDP offer because it doesn't
173
+ * support DTLS and ICE information.
174
+ *
175
+ * @return 0 if OK, AVERROR_xxx on error
176
+ */
177
+ static int generate_sdp_offer (AVFormatContext * s )
178
+ {
179
+ int profile_iop ;
180
+ RTCContext * rtc = s -> priv_data ;
181
+
182
+ if (rtc -> sdp_offer ) {
183
+ av_log (s , AV_LOG_ERROR , "SDP offer is already set\n" );
184
+ return AVERROR (EINVAL );
185
+ }
186
+
187
+ snprintf (rtc -> ice_ufrag_local , sizeof (rtc -> ice_ufrag_local ), "%08x" ,
188
+ av_get_random_seed ());
189
+ snprintf (rtc -> ice_pwd_local , sizeof (rtc -> ice_pwd_local ), "%08x%08x%08x%08x" ,
190
+ av_get_random_seed (), av_get_random_seed (), av_get_random_seed (),
191
+ av_get_random_seed ());
192
+
193
+ rtc -> audio_ssrc = av_get_random_seed ();
194
+ rtc -> video_ssrc = av_get_random_seed ();
195
+
196
+ rtc -> audio_payload_type = 111 ;
197
+ rtc -> video_payload_type = 106 ;
198
+
199
+ profile_iop = rtc -> video_par -> profile & FF_PROFILE_H264_CONSTRAINED ? 0xe0 : 0x00 ;
200
+ rtc -> sdp_offer = av_asprintf (
201
+ "v=0\r\n"
202
+ "o=FFmpeg 4489045141692799359 2 IN IP4 127.0.0.1\r\n"
203
+ "s=FFmpegPublishSession\r\n"
204
+ "t=0 0\r\n"
205
+ "a=group:BUNDLE 0 1\r\n"
206
+ "a=extmap-allow-mixed\r\n"
207
+ "a=msid-semantic: WMS\r\n"
208
+ ""
209
+ "m=audio 9 UDP/TLS/RTP/SAVPF %u\r\n"
210
+ "c=IN IP4 0.0.0.0\r\n"
211
+ "a=ice-ufrag:%s\r\n"
212
+ "a=ice-pwd:%s\r\n"
213
+ "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"
214
+ "a=setup:active\r\n"
215
+ "a=mid:0\r\n"
216
+ "a=sendonly\r\n"
217
+ "a=msid:FFmpeg audio\r\n"
218
+ "a=rtcp-mux\r\n"
219
+ "a=rtpmap:%u opus/%d/%d\r\n"
220
+ "a=ssrc:%u cname:FFmpeg\r\n"
221
+ "a=ssrc:%u msid:FFmpeg audio\r\n"
222
+ ""
223
+ "m=video 9 UDP/TLS/RTP/SAVPF %u\r\n"
224
+ "c=IN IP4 0.0.0.0\r\n"
225
+ "a=ice-ufrag:%s\r\n"
226
+ "a=ice-pwd:%s\r\n"
227
+ "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"
228
+ "a=setup:active\r\n"
229
+ "a=mid:1\r\n"
230
+ "a=sendonly\r\n"
231
+ "a=msid:FFmpeg video\r\n"
232
+ "a=rtcp-mux\r\n"
233
+ "a=rtcp-rsize\r\n"
234
+ "a=rtpmap:%u H264/90000\r\n"
235
+ "a=fmtp:%u level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=%02x%02x%02x\r\n"
236
+ "a=ssrc:%u cname:FFmpeg\r\n"
237
+ "a=ssrc:%u msid:FFmpeg video\r\n" ,
238
+ rtc -> audio_payload_type ,
239
+ rtc -> ice_ufrag_local ,
240
+ rtc -> ice_pwd_local ,
241
+ rtc -> audio_payload_type ,
242
+ rtc -> audio_par -> sample_rate ,
243
+ rtc -> audio_par -> ch_layout .nb_channels ,
244
+ rtc -> audio_ssrc ,
245
+ rtc -> audio_ssrc ,
246
+ rtc -> video_payload_type ,
247
+ rtc -> ice_ufrag_local ,
248
+ rtc -> ice_pwd_local ,
249
+ rtc -> video_payload_type ,
250
+ rtc -> video_payload_type ,
251
+ rtc -> video_par -> profile & (~FF_PROFILE_H264_CONSTRAINED ),
252
+ profile_iop ,
253
+ rtc -> video_par -> level ,
254
+ rtc -> video_ssrc ,
255
+ rtc -> video_ssrc
256
+ );
257
+ av_log (s , AV_LOG_VERBOSE , "Generated offer: %s" , rtc -> sdp_offer );
258
+
259
+ return 0 ;
260
+ }
261
+
262
+ /**
263
+ * Exchange SDP offer with WebRTC peer to get the answer.
264
+ * The below is an example of SDP answer:
265
+ *
266
+ * v=0
267
+ * o=SRS/6.0.42(Bee) 107408542208384 2 IN IP4 0.0.0.0
268
+ * s=SRSPublishSession
269
+ * t=0 0
270
+ * a=ice-lite
271
+ * a=group:BUNDLE 0 1
272
+ * a=msid-semantic: WMS live/show
273
+ *
274
+ * m=audio 9 UDP/TLS/RTP/SAVPF 111
275
+ * c=IN IP4 0.0.0.0
276
+ * a=ice-ufrag:ex9061f9
277
+ * a=ice-pwd:bi8k19m9n836187b00d1gm3946234w85
278
+ * 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
279
+ * a=setup:passive
280
+ * a=mid:0
281
+ * a=recvonly
282
+ * a=rtcp-mux
283
+ * a=rtcp-rsize
284
+ * a=rtpmap:111 opus/48000/2
285
+ * a=candidate:0 1 udp 2130706431 172.20.10.7 8000 typ host generation 0
286
+ *
287
+ * m=video 9 UDP/TLS/RTP/SAVPF 106
288
+ * c=IN IP4 0.0.0.0
289
+ * a=ice-ufrag:ex9061f9
290
+ * a=ice-pwd:bi8k19m9n836187b00d1gm3946234w85
291
+ * 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
292
+ * a=setup:passive
293
+ * a=mid:1
294
+ * a=recvonly
295
+ * a=rtcp-mux
296
+ * a=rtcp-rsize
297
+ * a=rtpmap:106 H264/90000
298
+ * a=fmtp:106 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01e
299
+ * a=candidate:0 1 udp 2130706431 172.20.10.7 8000 typ host generation 0
300
+ *
301
+ * @return 0 if OK, AVERROR_xxx on error
302
+ */
303
+ static int exchange_sdp (AVFormatContext * s )
304
+ {
305
+ int ret ;
306
+ char headers [MAX_URL_SIZE ], buf [MAX_URL_SIZE ];
307
+ char * p ;
308
+ RTCContext * rtc = s -> priv_data ;
309
+
310
+ ret = ffurl_alloc (& rtc -> whip_uc , s -> url , AVIO_FLAG_READ_WRITE , & s -> interrupt_callback );
311
+ if (ret < 0 ) {
312
+ av_log (s , AV_LOG_ERROR , "Failed to alloc HTTP context: %s" , s -> url );
313
+ return ret ;
314
+ }
315
+
316
+ snprintf (headers , sizeof (headers ),
317
+ "Cache-Control: no-cache\r\n"
318
+ "Content-Type: application/sdp\r\n" );
319
+ av_opt_set (rtc -> whip_uc -> priv_data , "headers" , headers , 0 );
320
+ av_opt_set (rtc -> whip_uc -> priv_data , "chunked_post" , "0" , 0 );
321
+ av_opt_set_bin (rtc -> whip_uc -> priv_data , "post_data" , rtc -> sdp_offer , (int )strlen (rtc -> sdp_offer ), 0 );
322
+
323
+ ret = ffurl_connect (rtc -> whip_uc , NULL );
324
+ if (ret < 0 ) {
325
+ av_log (s , AV_LOG_ERROR , "Failed to request url=%s, offer: %s" , s -> url , rtc -> sdp_offer );
326
+ return ret ;
327
+ }
328
+
329
+ for (;;) {
330
+ ret = ffurl_read (rtc -> whip_uc , buf , sizeof (buf ));
331
+ if (ret == AVERROR_EOF ) {
332
+ /* Reset the error because we read all response as answer util EOF. */
333
+ ret = 0 ;
334
+ break ;
335
+ }
336
+ if (ret <= 0 ) {
337
+ av_log (s , AV_LOG_ERROR , "Failed to read response from url=%s, offer is %s, answer is %s" ,
338
+ s -> url , rtc -> sdp_offer , rtc -> sdp_answer );
339
+ return ret ;
340
+ }
341
+
342
+ p = rtc -> sdp_answer ;
343
+ rtc -> sdp_answer = av_asprintf ("%s%.*s" , p ? p : "" , ret , buf );
344
+ av_free (p );
345
+ }
346
+ av_log (s , AV_LOG_VERBOSE , "Got answer: %s" , rtc -> sdp_answer );
347
+
348
+ return ret ;
349
+ }
350
+
106
351
static int rtc_init (AVFormatContext * s )
107
352
{
108
353
int ret ;
109
354
110
355
if ((ret = check_codec (s )) < 0 )
111
356
return ret ;
112
357
358
+ if ((ret = generate_sdp_offer (s )) < 0 )
359
+ return ret ;
360
+
361
+ if ((ret = exchange_sdp (s )) < 0 )
362
+ return ret ;
363
+
113
364
return 0 ;
114
365
}
115
366
@@ -130,6 +381,10 @@ static int rtc_write_trailer(AVFormatContext *s)
130
381
131
382
static void rtc_deinit (AVFormatContext * s )
132
383
{
384
+ RTCContext * rtc = s -> priv_data ;
385
+ av_freep (& rtc -> sdp_offer );
386
+ av_freep (& rtc -> sdp_answer );
387
+ ffurl_closep (& rtc -> whip_uc );
133
388
}
134
389
135
390
static const AVOption options [] = {
0 commit comments