├── 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('