Skip to content

Commit 598aecc

Browse files
yangrtcwinlinvip
authored andcommitted
WHIP: Generate offer and exchange with server to get answer.
1 parent 26d4cea commit 598aecc

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed

libavformat/rtcenc.c

+260
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,42 @@
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

3336
typedef 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
*/
4569
static 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+
115365
static 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

140396
static 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

144404
static const AVOption options[] = {

0 commit comments

Comments
 (0)