├── tools
├── index.html
├── tidy.config
├── reflow.sh
├── tester.js
├── new_example.py
└── gen_examples.py
├── Makefile
├── .travis.yml
├── .gitignore
├── Makefile.fluffy
├── CONTRIBUTING.md
└── README.md
/tools/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PUSH_GHPAGES_BRANCHES = false
2 |
3 | include lib/main.mk
4 |
5 | lib/main.mk:
6 | ifneq (,$(shell git submodule status lib 2>/dev/null))
7 | git submodule sync
8 | git submodule update --init
9 | else
10 | git clone --depth 10 -b master https://github.com/ekr/i-d-template.git lib
11 | endif
12 |
--------------------------------------------------------------------------------
/tools/tidy.config:
--------------------------------------------------------------------------------
1 | indent: yes
2 | indent-spaces: 2
3 | wrap: 72
4 | vertical-space: yes
5 | wrap-sections: yes
6 | quiet: true
7 | tidy-mark: no
8 | output-encoding: ascii
9 | output-xml: true
10 | gnu-emacs: true
11 | indent-cdata: no
12 | drop-empty-paras: no
13 | show-warnings: yes
14 | input-xml: true
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | sudo: false
3 | dist: trusty
4 | addons:
5 | apt:
6 | packages:
7 | - python-pip
8 |
9 | install:
10 | - pip install xml2rfc
11 |
12 | script: make ghpages
13 |
14 | env:
15 | global:
16 | - secure: "Dyu6BRI5Gyidgnshtz4qNvDtXfGLhsoOH9KIyGpk+3RgDYpE2t0uL2D1oWAr7oNbgzHuBpEkd4HaSigs0Yu5UgmQXvhwjBO/ChrleX9g4lVx7qjkOGEz94o6B/FI/ygqmQ819V4CyldZkSYyAJSaL0/OanwuKZ6CejKpCyJYJeo="
17 |
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | lib
3 | venv
4 | foo
5 | bar
6 | old
7 | old?
8 | foo.html
9 | bar.html
10 | draft-ietf-rtcweb-jsep.txt.old
11 | draft-ietf-rtcweb-jsep.txt
12 | draft-ietf-rtcweb-jsep.html
13 | draft-ietf-rtcweb-jsep.diff.html
14 | draft-ietf-rtcweb-jsep.pdf
15 | draft-ietf-rtcweb-jsep.old.raw
16 | draft-ietf-rtcweb-jsep.old.xml
17 | draft-ietf-rtcweb-jsep.old.html
18 | draft-ietf-rtcweb-jsep.old.txt
19 | draft-ietf-rtcweb-jsep.raw
20 |
--------------------------------------------------------------------------------
/tools/reflow.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # HTMLTidy cleans up the XML according to tidy.config, but also strips
4 | # newlines between tags, spaces after tags, and adds trailing
5 | # whitespace. So, we postprocess accordingly.
6 | tidy -config tools/tidy.config < draft-ietf-rtcweb-jsep.xml \
7 | | perl -pe 's/(.*)/\n$1/' \
8 | | sed -e "s/\(<\/xref>\)\([a-zA-Z\(]\)/\1 \2/g" \
9 | | sed -e "s/\(\)\([a-zA-Z\(]\)/\1 \2/g" \
10 | | sed -e "s/ *$//" \
11 | > foo.xml
12 | mv foo.xml draft-ietf-rtcweb-jsep.xml
13 |
--------------------------------------------------------------------------------
/tools/tester.js:
--------------------------------------------------------------------------------
1 | function canonicalize(sdp) {
2 | let lines = sdp.split("\n");
3 | let output = "";
4 |
5 | for (l in lines) {
6 | let trimmed = lines[l].trim()
7 | if (lines[l].length === 0) {
8 | continue;
9 | }
10 | if (lines[l].startsWith(' ')) {
11 | // Line folding; add a space unless this is a fingerprint.
12 | if (!lines[l - 1].endsWith(':')) {
13 | output += ' ';
14 | }
15 | } else if (output.length > 0) {
16 | // No line folding and not first line.
17 | output += "\n";
18 | }
19 | output += trimmed;
20 | }
21 | return output + "\n";
22 | }
23 |
24 | function test() {
25 | let sdp = document.getElementById("offer").value;
26 | console.log("Original SDP" + sdp);
27 | let canon = canonicalize(sdp);
28 |
29 | console.log("Canonical SDP:" + canon);
30 |
31 | let pc = new RTCPeerConnection();
32 | pc.setRemoteDescription(
33 | {
34 | type : "offer",
35 | sdp : canon
36 | },
37 | function () {
38 | alert("Success");
39 | },
40 | function (e) {
41 | alert("Error "+e);
42 | });
43 | }
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/Makefile.fluffy:
--------------------------------------------------------------------------------
1 | xml2rfc ?= xml2rfc -v
2 | kramdown-rfc2629 ?= kramdown-rfc2629
3 | idnits ?= idnits
4 |
5 | draft := draft-ietf-rtcweb-jsep
6 |
7 | current_ver := $(shell git tag | grep "$(draft)" | tail -1 | sed -e"s/.*-//")
8 | ifeq "${current_ver}" ""
9 | next_ver ?= 00
10 | else
11 | next_ver ?= $(shell printf "%.2d" $$((1$(current_ver)-99)))
12 | endif
13 | next := $(draft)-$(next_ver)
14 |
15 | .PHONY: latest submit clean
16 |
17 | latest: $(draft).txt $(draft).html
18 |
19 | submit: $(next).txt
20 |
21 | idnits: $(next).txt
22 | $(idnits) $<
23 |
24 | diff: $(draft).diff.html
25 |
26 | clean:
27 | -rm -f $(draft).txt $(draft).raw $(draft).old.raw $(draft).html $(draft).diff.html
28 | -rm -f $(next).txt $(next).raw $(next).html
29 | -rm -f $(draft)-[0-9][0-9].xml
30 |
31 |
32 | $(next).xml: $(draft).xml
33 | sed -e"s/$(basename $<)-latest/$(basename $@)/" $< > $@
34 |
35 | #%.xml: %.md
36 | # $(kramdown-rfc2629) $< > $@
37 |
38 | %.txt: %.xml
39 | $(xml2rfc) $< --text --out $@
40 |
41 | %.raw: %.xml
42 | $(xml2rfc) $< --raw --out $@
43 |
44 | %.html: %.xml
45 | $(xml2rfc) $< --html --out $@
46 |
47 | $(draft).diff.html: $(draft).old.raw $(draft).raw
48 | htmlwdiff $^ > $@
49 |
50 | upload: $(draft).html $(draft).txt
51 | python upload-draft.py $(draft).html
52 |
--------------------------------------------------------------------------------
/tools/new_example.py:
--------------------------------------------------------------------------------
1 | import base64
2 | import random
3 | import sys
4 | import uuid
5 |
6 | FORMAT_STR1 = """\
7 | ms{0[num]} = [
8 | {{ 'type': 'audio', 'mid': 'a1',
9 | 'ms': '{0[ms]}',
10 | 'mst': '{0[msta]}',
11 | 'host_port': {0[hp]}, 'srflx_port': {0[sp]}, 'relay_port': {0[rp]},
12 | 'ice_ufrag': '{0[ufrag]}', 'ice_pwd': '{0[pwd]}',
13 | 'dtls_dir': '{0[ddir]}' }},
14 | {{ 'type': 'video', 'mid': 'v1',
15 | 'ms': '{0[ms]}',
16 | 'mst': '{0[mstv]}' }}
17 | ]
18 | fp{0[num]} = '{0[fp]}'
19 | pc{0[num]} = PeerConnection(session_id = '{0[id]}', trickle = True,
20 | bundle_policy = 'max-bundle', mux_policy = 'require',
21 | ip_last_quad = {0[ip]}, fingerprint = fp{0[num]}, m_sections = ms{0[num]})
22 | """
23 |
24 | FORMAT_STR2 = """\
25 | o = pc1.create_offer()
26 | output_desc('offer-{0}1', o, draft)
27 | a = pc2.create_answer()
28 | output_desc('answer-{0}1', a, draft)
29 | """
30 |
31 | def random_bytes(bytes):
32 | return ''.join(chr(random.getrandbits(8)) for _ in range(bytes))
33 |
34 | def random_uuid_str():
35 | return uuid.UUID(bytes=random_bytes(16))
36 |
37 | def make_obj(num):
38 | return {
39 | 'num': num,
40 | 'id': random.getrandbits(63),
41 | 'ip': num * 100,
42 | 'fp': ':'.join('%02x' % ord(b) for b in random_bytes(32)).upper(),
43 | 'ufrag': base64.b64encode(random_bytes(3)),
44 | 'pwd': base64.b64encode(random_bytes(18)),
45 | 'ddir': ['passive', 'active'][num - 1],
46 | 'ms': random_uuid_str(),
47 | 'msta': random_uuid_str(),
48 | 'mstv': random_uuid_str(),
49 | 'hp': 10000 + num * 100,
50 | 'sp': 11000 + num * 100,
51 | 'rp': 12000 + num * 100,
52 | }
53 |
54 | def main():
55 | # Use the example name as a seed
56 | if len(sys.argv) > 1:
57 | letter = sys.argv[1]
58 | else:
59 | letter = 'X'
60 | random.seed(ord(letter))
61 | print FORMAT_STR1.format(make_obj(1))
62 | print FORMAT_STR1.format(make_obj(2))
63 | print FORMAT_STR2.format(letter)
64 |
65 | if __name__ == '__main__':
66 | main()
67 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to JSEP
2 |
3 | Before submitting feedback, please familiarize yourself with our current issues
4 | list and review the [working
5 | group home page](http://trac.tools.ietf.org/wg/rtcweb/trac/wiki). If you're
6 | new to this, you may also want to read the [Tao of the
7 | IETF](http://www.ietf.org/tao.html).
8 |
9 | Be aware that all contributions to the specification fall under the "NOTE WELL"
10 | terms outlined below.
11 |
12 | 1. The best way to provide feedback (editorial or design) and ask questions is
13 | sending an e-mail to [our mailing
14 | list](https://www.ietf.org/mailman/listinfo/rtcweb). This will assure that
15 | the entire Working Group sees your input in a timely fashion.
16 |
17 | 2. If you have **editorial** suggestions (i.e., those that do not change the
18 | meaning of the specification), you can either:
19 |
20 | a) Fork this repository and submit a pull request; this is the lowest
21 | friction way to get editorial changes in.
22 |
23 | b) Submit a new issue to Github, and mention that you believe it is editorial
24 | in the issue body. It is not necessary to notify the mailing list for
25 | editorial issues.
26 |
27 | c) Make comments on individual commits in Github. Note that this feedback is
28 | processed only with best effort by the editors, so it should only be used for
29 | quick editorial suggestions or questions.
30 |
31 | 3. For non-editorial (i.e., **design**) issues, you can also create an issue on
32 | Github. However, you **must notify the mailing list** when creating such issues,
33 | providing a link to the issue in the message body.
34 |
35 | Note that **github issues are not for substantial discussions**; the only
36 | appropriate place to discuss design issues is on the mailing list itself.
37 |
38 |
39 | # NOTE WELL
40 |
41 | Any submission to the [IETF](http://www.ietf.org/) intended by the Contributor
42 | for publication as all or part of an IETF Internet-Draft or RFC and any
43 | statement made within the context of an IETF activity is considered an "IETF
44 | Contribution". Such statements include oral statements in IETF sessions, as
45 | well as written and electronic communications made at any time or place, which
46 | are addressed to:
47 |
48 | * The IETF plenary session
49 | * The IESG, or any member thereof on behalf of the IESG
50 | * Any IETF mailing list, including the IETF list itself, any working group
51 | or design team list, or any other list functioning under IETF auspices
52 | * Any IETF working group or portion thereof
53 | * Any Birds of a Feather (BOF) session
54 | * The IAB or any member thereof on behalf of the IAB
55 | * The RFC Editor or the Internet-Drafts function
56 | * All IETF Contributions are subject to the rules of
57 | [RFC 5378](http://tools.ietf.org/html/rfc5378) and
58 | [RFC 3979](http://tools.ietf.org/html/rfc3979)
59 | (updated by [RFC 4879](http://tools.ietf.org/html/rfc4879)).
60 |
61 | Statements made outside of an IETF session, mailing list or other function,
62 | that are clearly not intended to be input to an IETF activity, group or
63 | function, are not IETF Contributions in the context of this notice.
64 |
65 | Please consult [RFC 5378](http://tools.ietf.org/html/rfc5378) and [RFC
66 | 3979](http://tools.ietf.org/html/rfc3979) for details.
67 |
68 | A participant in any IETF activity is deemed to accept all IETF rules of
69 | process, as documented in Best Current Practices RFCs and IESG Statements.
70 |
71 | A participant in any IETF activity acknowledges that written, audio and video
72 | records of meetings may be made and may be available to the public.
73 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | RTCWEB JSEP Draft Specification
2 | ===============================
3 |
4 | This is the working area for the [IETF RTCWEB Working
5 | Group](http://trac.tools.ietf.org/wg/rtcweb/trac/wiki) draft of [JSEP](http://tools.ietf.org/html/draft-ietf-rtcweb-jsep)
6 |
7 | JSEP specification:
8 | * [Editor's copy](http://rtcweb-wg.github.io/jsep/)
9 | * [Working Group Draft](http://tools.ietf.org/html/draft-ietf-rtcweb-jsep)
10 |
11 | Status Into:
12 |
13 | [Status Info](http://waffle.io/rtcweb-wg/jsep)
14 |
15 |
16 | Contributing
17 | ------------
18 |
19 | Before submitting feedback, please familiarize yourself with our current issues
20 | list and review the [working
21 | group home page](http://trac.tools.ietf.org/wg/rtcweb/trac/wiki). If you're
22 | new to this, you may also want to read the [Tao of the
23 | IETF](http://www.ietf.org/tao.html).
24 |
25 | Be aware that all contributions to the specification fall under the "NOTE WELL"
26 | terms outlined below.
27 |
28 | 1. The best way to provide feedback (editorial or design) and ask questions is
29 | sending an e-mail to [our mailing
30 | list](https://www.ietf.org/mailman/listinfo/rtcweb). This will assure that
31 | the entire Working Group sees your input in a timely fashion.
32 |
33 | 2. If you have **editorial** suggestions (i.e., those that do not change the
34 | meaning of the specification), you can either:
35 |
36 | a) Fork this repository and submit a pull request; this is the lowest
37 | friction way to get editorial changes in.
38 |
39 | b) Submit a new issue to Github, and mention that you believe it is editorial
40 | in the issue body. It is not necessary to notify the mailing list for
41 | editorial issues.
42 |
43 | c) Make comments on individual commits in Github. Note that this feedback is
44 | processed only with best effort by the editors, so it should only be used for
45 | quick editorial suggestions or questions.
46 |
47 | 3. For non-editorial (i.e., **design**) issues, you can also create an issue on
48 | Github. However, you **must notify the mailing list** when creating such issues,
49 | providing a link to the issue in the message body.
50 |
51 | Note that **github issues are not for substantial discussions**; the only
52 | appropriate place to discuss design issues is on the mailing list itself.
53 |
54 |
55 | NOTE WELL
56 | ---------
57 |
58 | Any submission to the [IETF](http://www.ietf.org/) intended by the Contributor
59 | for publication as all or part of an IETF Internet-Draft or RFC and any
60 | statement made within the context of an IETF activity is considered an "IETF
61 | Contribution". Such statements include oral statements in IETF sessions, as
62 | well as written and electronic communications made at any time or place, which
63 | are addressed to:
64 |
65 | * The IETF plenary session
66 | * The IESG, or any member thereof on behalf of the IESG
67 | * Any IETF mailing list, including the IETF list itself, any working group
68 | or design team list, or any other list functioning under IETF auspices
69 | * Any IETF working group or portion thereof
70 | * Any Birds of a Feather (BOF) session
71 | * The IAB or any member thereof on behalf of the IAB
72 | * The RFC Editor or the Internet-Drafts function
73 | * All IETF Contributions are subject to the rules of
74 | [RFC 5378](http://tools.ietf.org/html/rfc5378) and
75 | [RFC 3979](http://tools.ietf.org/html/rfc3979)
76 | (updated by [RFC 4879](http://tools.ietf.org/html/rfc4879)).
77 |
78 | Statements made outside of an IETF session, mailing list or other function,
79 | that are clearly not intended to be input to an IETF activity, group or
80 | function, are not IETF Contributions in the context of this notice.
81 |
82 | Please consult [RFC 5378](http://tools.ietf.org/html/rfc5378) and [RFC
83 | 3979](http://tools.ietf.org/html/rfc3979) for details.
84 |
85 | A participant in any IETF activity is deemed to accept all IETF rules of
86 | process, as documented in Best Current Practices RFCs and IESG Statements.
87 |
88 | A participant in any IETF activity acknowledges that written, audio and video
89 | records of meetings may be made and may be available to the public.
90 |
91 | Build Information
92 | -------------
93 |
94 | Examples are generated with
95 |
96 | ```
97 | python tools/gen_examples.py --replace draft-ietf-rtcweb-jsep.xml
98 | ```
99 |
100 | The document can be reflowed with
101 |
102 | ```
103 | tools/reflow.sh
104 | ```
105 |
106 |
--------------------------------------------------------------------------------
/tools/gen_examples.py:
--------------------------------------------------------------------------------
1 | import argparse
2 |
3 | class PeerConnection:
4 | SESSION_SDP = \
5 | """v=0
6 | o=- {0.session_id} {0.version} IN IP4 0.0.0.0
7 | s=-
8 | t=0 0
9 | a=ice-options:trickle
10 | """
11 |
12 | BUNDLE_GROUP_SDP = 'a=group:BUNDLE {0}\n'
13 | LS_GROUP_SDP = 'a=group:LS {0}\n'
14 |
15 | AUDIO_SDP = \
16 | """m=audio {0[default_port]} UDP/TLS/RTP/SAVPF 96 0 8 97 98
17 | c=IN IP4 {0[default_ip]}
18 | a=mid:{0[mid]}
19 | a={0[direction]}
20 | a=rtpmap:96 opus/48000/2
21 | a=rtpmap:0 PCMU/8000
22 | a=rtpmap:8 PCMA/8000
23 | a=rtpmap:97 telephone-event/8000
24 | a=rtpmap:98 telephone-event/48000
25 | a=fmtp:97 0-15
26 | a=fmtp:98 0-15
27 | a=maxptime:120
28 | a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
29 | a=extmap:2 urn:ietf:params:rtp-hdrext:ssrc-audio-level
30 | a=msid:{0[ms]} {0[mst]}
31 | """
32 |
33 | VIDEO_SDP = \
34 | """m=video {0[default_port]} UDP/TLS/RTP/SAVPF 100 101 102 103 104
35 | c=IN IP4 {0[default_ip]}
36 | a=mid:{0[mid]}
37 | a={0[direction]}
38 | a=rtpmap:100 VP8/90000
39 | a=rtpmap:101 H264/90000
40 | a=fmtp:101 packetization-mode=1;profile-level-id=42e01f
41 | a=rtpmap:102 rtx/90000
42 | a=fmtp:102 apt=100
43 | a=rtpmap:103 rtx/90000
44 | a=fmtp:103 apt=101
45 | a=rtpmap:104 flexfec/90000
46 | a=imageattr:100 recv [x=[48:1920],y=[48:1080],q=1.0]
47 | a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:mid
48 | a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
49 | a=rtcp-fb:100 ccm fir
50 | a=rtcp-fb:100 nack
51 | a=rtcp-fb:100 nack pli
52 | a=msid:{0[ms]} {0[mst]}
53 | a=rid:1 send
54 | a=rid:2 send
55 | a=rid:3 send
56 | a=simulcast:send 1;2;3
57 | """
58 |
59 | DATA_SDP = \
60 | """m=application {0[default_port]} UDP/DTLS/SCTP webrtc-datachannel
61 | c=IN IP4 {0[default_ip]}
62 | a=mid:{0[mid]}
63 | a=sctp-port:5000
64 | a=max-message-size:65536
65 | """
66 |
67 | MEDIA_TABLE = {
68 | 'audio': AUDIO_SDP, 'video': VIDEO_SDP, 'application': DATA_SDP
69 | }
70 |
71 | TRANSPORT_SDP = \
72 | """a=ice-ufrag:{0[ice_ufrag]}
73 | a=ice-pwd:{0[ice_pwd]}
74 | a=fingerprint:sha-256 {0[dtls_fingerprint]}
75 | a=setup:{0[dtls_dir]}
76 | a=tls-id:{0[tls_id]}
77 | a=rtcp:{0[default_rtcp]} IN IP4 {0[default_ip]}
78 | a=rtcp-mux
79 | a=rtcp-mux-only
80 | a=rtcp-rsize
81 | """
82 |
83 | BUNDLE_ONLY_SDP = 'a=bundle-only\n'
84 |
85 | CANDIDATE_ATTR = 'candidate:{0} {1} {2} {3} {4} {5} typ {6}'
86 | CANDIDATE_ATTR_WITH_RADDR = CANDIDATE_ATTR + ' raddr {7} rport {8}'
87 |
88 | END_OF_CANDIDATES_SDP = 'a=end-of-candidates\n'
89 |
90 | def __init__(self, session_id, trickle, bundle_policy, mux_policy,
91 | ip_last_quad, fingerprint, tls_id, m_sections):
92 | self.session_id = session_id
93 | self.trickle = trickle
94 | self.bundle_policy = bundle_policy
95 | self.mux_policy = mux_policy
96 | self.fingerprint = fingerprint
97 | self.tls_id = tls_id
98 | self.m_sections = m_sections
99 | # IETF-approved example IPs
100 | self.host_ip = '203.0.113.' + str(ip_last_quad)
101 | self.srflx_ip = '198.51.100.' + str(ip_last_quad)
102 | self.relay_ip = '192.0.2.' + str(ip_last_quad)
103 | self.version = 0
104 |
105 | def get_port(self, m_section, type):
106 | # get port from current section, bundle section, or None if type disallowed
107 | if type in m_section:
108 | return m_section[type]
109 | elif type in self.m_sections[0]:
110 | return self.m_sections[0][type]
111 | return None
112 |
113 | def select_default_candidates(self, m_section, bundle_only, num_components):
114 | if self.trickle and self.version == 1:
115 | default_ip = '0.0.0.0'
116 | if not bundle_only:
117 | default_port = default_rtcp = 9
118 | else:
119 | default_port = default_rtcp = 0
120 | else:
121 | default_port = self.get_port(m_section, 'relay_port')
122 | if default_port:
123 | default_ip = self.relay_ip
124 | else:
125 | default_port = self.get_port(m_section, 'host_port')
126 | default_ip = self.host_ip
127 | # tricky way to make rtcp port be rtp + 1, only if offering non-mux
128 | default_rtcp = default_port + num_components - 1
129 |
130 | m_section['default_ip'] = default_ip
131 | m_section['default_port'] = default_port
132 | m_section['default_rtcp'] = default_rtcp
133 |
134 | # removes the first attr of the form a=foo or a=foo:bar and returns new SDP
135 | def remove_attribute(self, sdp, attrib):
136 | # look for the whole attribute, to avoid finding a=rtcp inside of a=rtcp-mux
137 | suffix = ':' if ':' not in attrib else ' '
138 | start = sdp.find(attrib + suffix)
139 | if start == -1:
140 | start = sdp.find(attrib + '\n')
141 | if start == -1:
142 | return sdp
143 |
144 | end = sdp.find('\n', start)
145 | return sdp[:start] + sdp[end + 1:]
146 |
147 | # removes multiple instance of an attribute at once
148 | def remove_attributes(self, sdp, attrib):
149 | in_sdp = sdp
150 | out_sdp = self.remove_attribute(in_sdp, attrib)
151 | while out_sdp != in_sdp:
152 | in_sdp = out_sdp
153 | out_sdp = self.remove_attribute(in_sdp, attrib)
154 | return out_sdp
155 |
156 | # creates 'candidate:1 1 udp 999999 1.1.1.1:1111 type host'
157 | def create_candidate_attr(self, component, type, addr, port, raddr, rport):
158 | TYPE_PRIORITIES = {'host': 126, 'srflx': 110, 'relay': 0}
159 | if raddr:
160 | formatter = self.CANDIDATE_ATTR_WITH_RADDR
161 | else:
162 | formatter = self.CANDIDATE_ATTR
163 | foundation = 1
164 | protocol = 'udp'
165 | priority = TYPE_PRIORITIES[type] << 24 | (256 - component)
166 | return formatter.format(foundation, component, protocol, priority,
167 | addr, port, type, raddr, rport)
168 |
169 | # creates {'ufrag': 'wzyz', 'index':0, 'mid':'a1', 'attr': 'candidate:blah'}
170 | def create_candidate_obj(self, ufrag, index, mid,
171 | component, type, addr, port, raddr, rport):
172 | return {'ufrag': ufrag, 'index': index, 'mid': mid,
173 | 'attr': self.create_candidate_attr(component, type, addr, port,
174 | raddr, rport) }
175 |
176 | # if port exists of type |type|, returns list of candidates with size |num|
177 | def maybe_create_candidates_of_type(self, m_section, index, num, type, rtype):
178 | candidates = []
179 | port = self.get_port(m_section, type + '_port')
180 | if port:
181 | addr = getattr(self, type + '_ip')
182 | if rtype:
183 | # srflx/relay candidates; 0.0.0.0 if no related candidate
184 | rport = self.get_port(m_section, rtype + '_port')
185 | if rport:
186 | raddr = getattr(self, rtype + '_ip')
187 | else:
188 | rport = 0
189 | raddr = '0.0.0.0'
190 | else:
191 | # host candidates
192 | rport = 0
193 | raddr = None
194 |
195 | for i in range(0, num):
196 | candidates.append(self.create_candidate_obj(
197 | m_section['ice_ufrag'], index, m_section['mid'],
198 | i + 1, type, addr, port + i, raddr, rport + i))
199 |
200 | return candidates
201 |
202 | def create_candidates(self, m_section, index, num):
203 | candidates = []
204 | candidates.extend(
205 | self.maybe_create_candidates_of_type(m_section, index, num, 'host', None))
206 | candidates.extend(
207 | self.maybe_create_candidates_of_type(m_section, index, num, 'srflx',
208 | 'host'))
209 | candidates.extend(
210 | self.maybe_create_candidates_of_type(m_section, index, num, 'relay',
211 | 'srflx'))
212 | return candidates
213 |
214 | def add_m_section(self, desc, m_section):
215 | stype = desc['type']
216 | index = self.m_sections.index(m_section)
217 | bundled = not 'ice_ufrag' in m_section
218 | bundle_only = bundled and self.version == 1 and stype == 'offer'
219 | num_components = 1
220 | if self.mux_policy == 'negotiate' and stype == 'offer':
221 | num_components = 2
222 | rtcp_mux_only = self.mux_policy == 'require'
223 |
224 | copy = m_section.copy()
225 | self.select_default_candidates(copy, bundle_only, num_components)
226 | copy['dtls_fingerprint'] = self.fingerprint
227 | copy['tls_id'] = self.tls_id
228 | # always use actpass in offers
229 | if stype == 'offer':
230 | copy['dtls_dir'] = 'actpass'
231 | if copy['type'] != 'application':
232 | if 'direction' not in copy:
233 | copy['direction'] = 'sendrecv'
234 |
235 | # create the right template and fill it in
236 | formatter = self.MEDIA_TABLE[copy['type']]
237 | if not bundled:
238 | formatter += self.TRANSPORT_SDP
239 | if bundle_only:
240 | formatter += self.BUNDLE_ONLY_SDP
241 | if num_components != 2:
242 | formatter = self.remove_attribute(formatter, 'a=rtcp')
243 | if not rtcp_mux_only:
244 | formatter = self.remove_attribute(formatter, 'a=rtcp-mux-only')
245 | if 'direction' in copy and 'send' not in copy['direction']:
246 | formatter = self.remove_attribute(formatter, 'a=msid')
247 |
248 | # apply options as needed
249 | options = copy['options'] if 'options' in copy else []
250 | if 'fec' not in options:
251 | formatter = formatter.replace('100 101 102 103 104', '100 101 102 103')
252 | formatter = self.remove_attribute(formatter, 'a=rtpmap:104')
253 | formatter = self.remove_attribute(formatter, 'a=fmtp:104')
254 | if 'imageattr' not in options:
255 | formatter = self.remove_attribute(formatter, 'a=imageattr')
256 | if 'simulcast' not in options:
257 | formatter = self.remove_attributes(formatter, 'a=rid')
258 | formatter = self.remove_attribute(formatter, 'a=simulcast')
259 |
260 | sdp = formatter.format(copy)
261 |
262 | # if not bundling, create candidates either in SDP or separately
263 | if not bundled:
264 | candidates = self.create_candidates(copy, index, num_components)
265 | if not self.trickle or self.version > 1:
266 | for candidate in candidates:
267 | sdp += 'a=' + candidate['attr'] + '\n'
268 | sdp += self.END_OF_CANDIDATES_SDP
269 | else:
270 | desc['candidates'].extend(candidates)
271 |
272 | desc['sdp'] += sdp
273 |
274 | def create_desc(self, type):
275 | self.version += 1
276 | desc = { 'type': type, 'sdp': '', 'candidates': [] }
277 | sdp = self.SESSION_SDP.format(self)
278 |
279 | # generate BUNDLE group and append
280 | bundle_list = [m_section['mid'] for m_section in self.m_sections]
281 | bundle_group = ' '.join(bundle_list)
282 | sdp += self.BUNDLE_GROUP_SDP.format(bundle_group)
283 |
284 | # LS includes m= section mids with the same MS, or no MS
285 | # (may need to do the latter only for answers)
286 | ls_list = [m_section['mid'] for m_section in self.m_sections if
287 | m_section['type'] != 'application' and
288 | ('ms' not in m_section or
289 | m_section['ms'] == self.m_sections[0]['ms'])]
290 | # hack to fix example 7.2; proper fix is to make the answer consider the
291 | # offer LS contents, but I am leaving that for another day
292 | if 'v2' in ls_list:
293 | ls_list.remove('v2')
294 | if len(ls_list) > 1:
295 | ls_group = ' '.join(ls_list)
296 | sdp += self.LS_GROUP_SDP.format(ls_group)
297 |
298 | # fill in individual m= sections
299 | desc['sdp'] = sdp
300 | for m_section in self.m_sections:
301 | self.add_m_section(desc, m_section)
302 |
303 | # clean up the leading whitespace in the constants
304 | desc['sdp'] = desc['sdp'].replace(' ', '')
305 | return desc
306 | def create_offer(self):
307 | return self.create_desc('offer')
308 | def create_answer(self):
309 | return self.create_desc('answer')
310 |
311 | def split_line(line, *positions):
312 | lines = []
313 | indent = 0
314 | last_pos = 0
315 | if len(line) > 72:
316 | for pos in positions:
317 | if pos == -1:
318 | break
319 | lines.append(' ' * indent + line[last_pos:pos].strip())
320 | if indent == 0:
321 | indent = line.find(':') + 1
322 | last_pos = pos
323 |
324 | lines.append(' ' * indent + line[last_pos:].strip())
325 | return lines
326 |
327 | def format_sdp(sdp):
328 | lines_pre = sdp.split('\n')[:-1]
329 | lines_post = []
330 | for line in lines_pre:
331 | if line.startswith('m=') and len(lines_post) > 0:
332 | # add blank line between m= sections
333 | lines_post.append('')
334 | lines_post.append(line)
335 | elif line.startswith('a=msid'):
336 | # wrap long msid lines
337 | lines_post.extend(split_line(line, line.find(' ')))
338 | elif line.startswith('a=fingerprint'):
339 | # wrap long fingerprint lines
340 | lines_post.extend(split_line(line, 21, 70))
341 | elif line.startswith('a=candidate'):
342 | # wrap long candidate lines
343 | lines_post.extend(split_line(line, line.find('raddr')))
344 | else:
345 | lines_post.append(line)
346 | return lines_post
347 |
348 | # turn a candidate object into something like
349 | # ufrag 7sFv
350 | # index 0
351 | # mid a1
352 | # attr candidate:1 1 udp 255 66.77.88.99 50416 typ relay
353 | # raddr 55.66.77.88 rport 64532
354 | def format_candidate(cand_obj):
355 | lines = []
356 | for key in ['ufrag', 'index', 'mid', 'attr']:
357 | if key != 'attr':
358 | lines.append('{0: <5} {1}'.format(key, cand_obj[key]))
359 | else:
360 | attr_line = 'attr ' + cand_obj['attr']
361 | lines.extend(split_line(attr_line, attr_line.find('raddr')))
362 | return lines
363 |
364 | # update a specific in-place in the draft
365 | def replace_artwork(draft_lines, name, artwork_lines):
366 | draft_copy = draft_lines[:]
367 | del draft_lines[:]
368 | found = False
369 | for draft_line in draft_copy:
370 | if found and '' in draft_line:
371 | for artwork_line in artwork_lines:
372 | draft_lines.append(artwork_line + '\n')
373 | draft_lines.append(']]>\n')
374 | found = False
375 | if not found:
376 | draft_lines.append(draft_line)
377 | if ('') in draft_line:
378 | found = True
379 | draft_lines.append('