├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── lib ├── sdp_transform.dart └── src │ ├── grammar.dart │ ├── parser.dart │ └── writer.dart ├── pubspec.yaml ├── renovate.json └── test ├── regexp_test.dart ├── sdp_parse_test.dart └── sdp_test_data ├── alac.sdp ├── asterisk.sdp ├── dante-aes67.sdp ├── extmap-encrypt.sdp ├── hacky.sdp ├── icelite.sdp ├── invalid.sdp ├── jsep.sdp ├── jssip.sdp ├── normal.sdp ├── sctp-dtls-26.sdp ├── simulcast.sdp ├── ssrc.sdp ├── st2022-6.sdp ├── st2110-20.sdp ├── tcp-active.sdp └── tcp-passive.sdp /.gitignore: -------------------------------------------------------------------------------- 1 | build/testfile.dill 2 | build/app.dill 3 | .vscode/launch.json 4 | .dart_tool/pub/bin/sdk-version 5 | .dart_tool/pub/bin/test/test.dart.snapshot.dart2 6 | .packages 7 | pubspec.lock 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | -------------------------------------------- 4 | [0.3.2] - 2021.09.04 5 | 6 | * Remove print in parse. 7 | 8 | [0.3.1] - 2021.09.04 9 | 10 | * Fixed some bugs. 11 | 12 | [0.3.0] - 2021.04.04 13 | 14 | * Null safety. 15 | 16 | [0.2.0] - 2019.12.13 17 | 18 | * Fixed few bugs. 19 | 20 | [0.1.0] - 2019.01.20 21 | 22 | * Implement parser, writer and some functions and pass the test. 23 | 24 | [0.0.1] - 2019.01.19 25 | 26 | * Initial release. 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 湖北捷智云技术有限公司 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sdp-transform 2 | [![pub package](https://img.shields.io/pub/v/sdp_transform.svg)](https://pub.dartlang.org/packages/sdp_transform) [![Gitter](https://badges.gitter.im/flutter-webrtc/Lobby.svg)](https://gitter.im/flutter-webrtc/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 3 | 4 | Dart version of the [sdp-transform](https://github.com/clux/sdp-transform) JavaScript library. 5 | 6 | dart-sdp-transform is a simple parser and writer of SDP. Defines internal grammar based on RFC4566 - SDP, RFC5245 - ICE, and many more. 7 | 8 | ## Usage 9 | 10 | ```dart 11 | 12 | import 'dart:io'; 13 | import 'dart:convert'; 14 | import 'package:sdp_transform/sdp_transform.dart'; 15 | 16 | main() { 17 | new File('./test/ssrc.sdp').readAsString().then((String contents) { 18 | print('original sdp => ' + contents); 19 | var session = parse(contents); 20 | print('convert to json => ' + json.encode(session)); 21 | 22 | var params = parseParams(session['media'][1]['fmtp'][0]['config']); 23 | print('params => ' + params.toString()); 24 | 25 | var payloads = parsePayloads(session['media'][1]["payloads"]); 26 | print('payloads => ' + payloads.toString()); 27 | 28 | String imageAttributesStr = "[x=1280,y=720] [x=320,y=180]"; 29 | var imageAttributes = parseImageAttributes(imageAttributesStr); 30 | print('imageAttributes => ' + imageAttributes.toString()); 31 | 32 | // // a=simulcast:send 1,~4;2;3 recv c 33 | String simulcastAttributesStr = "1,~4;2;3"; 34 | var simulcastAttributes = parseSimulcastStreamList(simulcastAttributesStr); 35 | print('simulcastAttributes => ' + simulcastAttributes.toString()); 36 | 37 | var sdp = write(session, null); 38 | print('convert to sdp => ' + sdp); 39 | }); 40 | } 41 | 42 | ``` 43 | -------------------------------------------------------------------------------- /lib/sdp_transform.dart: -------------------------------------------------------------------------------- 1 | export 'src/grammar.dart'; 2 | export 'src/parser.dart'; 3 | export 'src/writer.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/grammar.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | 3 | var grammar = { 4 | 'v': [ 5 | {'name': 'version', 'reg': r'^(\d*)'} 6 | ], 7 | 'o': [ 8 | { 9 | // o=- 20518 0 IN IP4 203.0.113.1 10 | // NB: sessionId will be a String in most cases because it is huge 11 | 'name': 'origin', 12 | 'reg': r'^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)', 13 | 'names': [ 14 | 'username', 15 | 'sessionId', 16 | 'sessionVersion', 17 | 'netType', 18 | 'ipVer', 19 | 'address' 20 | ], 21 | 'format': '%s %s %d %s IP%d %s' 22 | } 23 | ], 24 | // default parsing of these only (though some of these feel outdated) 25 | 's': [ 26 | {'name': 'name', 'reg': r'^(\S*)'} 27 | ], 28 | 'i': [ 29 | {'name': 'description'} 30 | ], 31 | 'u': [ 32 | {'name': 'uri'} 33 | ], 34 | 'e': [ 35 | {'name': 'email'} 36 | ], 37 | 'p': [ 38 | {'name': 'phone'} 39 | ], 40 | 'z': [ 41 | {'name': 'timezones'} 42 | ], // TODO: this one can actually be parsed properly... 43 | 'r': [ 44 | {'name': 'repeats'} 45 | ], // TODO: this one can also be parsed properly 46 | // k: [{}], // outdated thing ignored 47 | 't': [ 48 | { 49 | // t=0 0 50 | 'name': 'timing', 51 | 'reg': r'^(\d*) (\d*)', 52 | 'names': ['start', 'stop'], 53 | 'format': '%d %d' 54 | } 55 | ], 56 | 'c': [ 57 | { 58 | // c=IN IP4 10.47.197.26 59 | 'name': 'connection', 60 | 'reg': r'^IN IP(\d) (\S*)', 61 | 'names': ['version', 'ip'], 62 | 'format': 'IN IP%d %s' 63 | } 64 | ], 65 | 'b': [ 66 | { 67 | // b=AS:4000 68 | 'push': 'bandwidth', 69 | 'reg': r'^(TIAS|AS|CT|RR|RS):(\d*)', 70 | 'names': ['type', 'limit'], 71 | 'format': '%s:%s' 72 | } 73 | ], 74 | 'm': [ 75 | { 76 | // m=video 51744 RTP/AVP 126 97 98 34 31 77 | // NB: special - pushes to session 78 | // TODO: rtp/fmtp should be filtered by the payloads found here? 79 | 'reg': r'^(\w*) (\d*) ([\w/]*)(?: (.*))?', 80 | 'names': ['type', 'port', 'protocol', 'payloads'], 81 | 'format': '%s %d %s %s' 82 | } 83 | ], 84 | 'a': [ 85 | { 86 | // a=rtpmap:110 opus/48000/2 87 | 'push': 'rtp', 88 | 'reg': r'^rtpmap:(\d*) ([\w\-.]*)(?:\s*\/(\d*)(?:\s*\/(\S*))?)?', 89 | 'names': ['payload', 'codec', 'rate', 'encoding'], 90 | 'format': (o) { 91 | return (o['encoding'] != null) 92 | ? 'rtpmap:%d %s/%s/%s' 93 | : (o['rate'] != null) 94 | ? 'rtpmap:%d %s/%s' 95 | : 'rtpmap:%d %s'; 96 | } as dynamic 97 | }, 98 | { 99 | // a=fmtp:108 profile-level-id=24;object=23;bitrate=64000 100 | // a=fmtp:111 minptime=10; useinbandfec=1 101 | 'push': 'fmtp', 102 | 'reg': r'^fmtp:(\d*) ([\S| ]*)', 103 | 'names': ['payload', 'config'], 104 | 'format': 'fmtp:%d %s' 105 | }, 106 | { 107 | // a=control:streamid=0 108 | 'name': 'control', 109 | 'reg': r'^control:(.*)', 110 | 'format': 'control:%s' 111 | }, 112 | { 113 | // a=rtcp:65179 IN IP4 193.84.77.194 114 | 'name': 'rtcp', 115 | 'reg': r'^rtcp:(\d*)(?: (\S*) IP(\d) (\S*))?', 116 | 'names': ['port', 'netType', 'ipVer', 'address'], 117 | 'format': (o) { 118 | return (o['address'] != null) ? 'rtcp:%d %s IP%d %s' : 'rtcp:%d'; 119 | } as dynamic 120 | }, 121 | { 122 | // a=rtcp-fb:98 trr-int 100 123 | 'push': 'rtcpFbTrrInt', 124 | 'reg': r'^rtcp-fb:(\*|\d*) trr-int (\d*)', 125 | 'names': ['payload', 'value'], 126 | 'format': 'rtcp-fb:%d trr-int %d' 127 | }, 128 | { 129 | // a=rtcp-fb:98 nack rpsi 130 | 'push': 'rtcpFb', 131 | 'reg': r'^rtcp-fb:(\*|\d*) ([\w-_]*)(?: ([\w-_]*))?', 132 | 'names': ['payload', 'type', 'subtype'], 133 | 'format': (o) { 134 | return (o['subtype'] != null) ? 'rtcp-fb:%s %s %s' : 'rtcp-fb:%s %s'; 135 | } as dynamic 136 | }, 137 | { 138 | // a=extmap:2 urn:ietf:params:rtp-hdrext:toffset 139 | // a=extmap:1/recvonly URI-gps-string 140 | 'push': 'ext', 141 | 'reg': r'^extmap:(\d+)(?:\/(\w+))? (\S*)(?: (\S*))?', 142 | 'names': ['value', 'direction', 'uri', 'config'], 143 | 'format': (o) { 144 | return 'extmap:%d' + 145 | (o['direction'] != null ? '/%s' : '%v') + 146 | ' %s' + 147 | (o['config'] != null ? ' %s' : ''); 148 | } as dynamic 149 | }, 150 | { 151 | // a=extmap-allow-mixed 152 | 'name': 'extmap-allow-mixed', 153 | 'push': 'extmapAllowMixed', 154 | 'reg': r'^extmap-allow-mixed', 155 | }, 156 | { 157 | // a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:PS1uQCVeeCFCanVmcjkpPywjNWhcYD0mXXtxaVBR|2^20|1:32 158 | 'push': 'crypto', 159 | 'reg': r'^crypto:(\d*) ([\w_]*) (\S*)(?: (\S*))?', 160 | 'names': ['id', 'suite', 'config', 'sessionConfig'], 161 | 'format': (o) { 162 | return (o['sessionConfig'] != null) 163 | ? 'crypto:%d %s %s %s' 164 | : 'crypto:%d %s %s'; 165 | } as dynamic 166 | }, 167 | { 168 | // a=setup:actpass 169 | 'name': 'setup', 170 | 'reg': r'^setup:(\w*)', 171 | 'format': 'setup:%s' 172 | }, 173 | { 174 | // a=mid:1 175 | 'name': 'mid', 176 | 'reg': r'^mid:([^\s]*)', 177 | 'format': 'mid:%s' 178 | }, 179 | { 180 | // a=msid:0c8b064d-d807-43b4-b434-f92a889d8587 98178685-d409-46e0-8e16-7ef0db0db64a 181 | 'name': 'msid', 182 | 'reg': r'^msid:(.*)', 183 | 'format': 'msid:%s' 184 | }, 185 | { 186 | // a=ptime:20 187 | 'name': 'ptime', 188 | 'reg': r'^ptime:(\d*)', 189 | 'format': 'ptime:%d' 190 | }, 191 | { 192 | // a=maxptime:60 193 | 'name': 'maxptime', 194 | 'reg': r'^maxptime:(\d*)', 195 | 'format': 'maxptime:%d' 196 | }, 197 | { 198 | // a=sendrecv 199 | 'name': 'direction', 200 | 'reg': r'^(sendrecv|recvonly|sendonly|inactive)' 201 | }, 202 | { 203 | // a=ice-lite 204 | 'name': 'icelite', 205 | 'reg': r'^(ice-lite)' 206 | }, 207 | { 208 | // a=ice-ufrag:F7gI 209 | 'name': 'iceUfrag', 210 | 'reg': r'^ice-ufrag:(\S*)', 211 | 'format': 'ice-ufrag:%s' 212 | }, 213 | { 214 | // a=ice-pwd:x9cml/YzichV2+XlhiMu8g 215 | 'name': 'icePwd', 216 | 'reg': r'^ice-pwd:(\S*)', 217 | 'format': 'ice-pwd:%s' 218 | }, 219 | { 220 | // a=fingerprint:SHA-1 00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33 221 | 'name': 'fingerprint', 222 | 'reg': r'^fingerprint:(\S*) (\S*)', 223 | 'names': ['type', 'hash'], 224 | 'format': 'fingerprint:%s %s' 225 | }, 226 | { 227 | // a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host 228 | // a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0 network-id 3 network-cost 10 229 | // a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 network-id 3 network-cost 10 230 | // a=candidate:229815620 1 tcp 1518280447 192.168.150.19 60017 typ host tcptype active generation 0 network-id 3 network-cost 10 231 | // a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0 network-id 3 network-cost 10 232 | 'push': 'candidates', 233 | 'reg': 234 | r'^candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?(?: network-id (\d*))?(?: network-cost (\d*))?', 235 | 'names': [ 236 | 'foundation', 237 | 'component', 238 | 'transport', 239 | 'priority', 240 | 'ip', 241 | 'port', 242 | 'type', 243 | 'raddr', 244 | 'rport', 245 | 'tcptype', 246 | 'generation', 247 | 'network-id', 248 | 'network-cost' 249 | ], 250 | 'format': (o) { 251 | var str = 'candidate:%s %d %s %d %s %d typ %s'; 252 | 253 | str += (o['raddr'] != null) ? ' raddr %s rport %d' : '%v%v'; 254 | 255 | // NB: candidate has three optional chunks, so %void middles one if it's missing 256 | str += (o['tcptype'] != null) ? ' tcptype %s' : '%v'; 257 | 258 | if (o['generation'] != null) { 259 | str += ' generation %d'; 260 | } 261 | 262 | str += (o['network-id'] != null) ? ' network-id %d' : '%v'; 263 | str += (o['network-cost'] != null) ? ' network-cost %d' : '%v'; 264 | return str; 265 | } as dynamic 266 | }, 267 | { 268 | // a=end-of-candidates (keep after the candidates line for readability) 269 | 'name': 'endOfCandidates', 270 | 'reg': r'^(end-of-candidates)' 271 | }, 272 | { 273 | // a=remote-candidates:1 203.0.113.1 54400 2 203.0.113.1 54401 ... 274 | 'name': 'remoteCandidates', 275 | 'reg': r'^remote-candidates:(.*)', 276 | 'format': 'remote-candidates:%s' 277 | }, 278 | { 279 | // a=ice-options:google-ice 280 | 'name': 'iceOptions', 281 | 'reg': r'^ice-options:(\S*)', 282 | 'format': 'ice-options:%s' 283 | }, 284 | { 285 | // a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1 286 | 'push': 'ssrcs', 287 | 'reg': r'^ssrc:(\d*) ([\w_-]*)(?::(.*))?', 288 | 'names': ['id', 'attribute', 'value'], 289 | 'format': (o) { 290 | var str = 'ssrc:%d'; 291 | if (o['attribute'] != null) { 292 | str += ' %s'; 293 | if (o['value'] != null) { 294 | str += ':%s'; 295 | } 296 | } 297 | return str; 298 | } as dynamic 299 | }, 300 | { 301 | // a=ssrc-group:FEC 1 2 302 | // a=ssrc-group:FEC-FR 3004364195 1080772241 303 | 'push': 'ssrcGroups', 304 | // token-char = %x21 / %x23-27 / %x2A-2B / %x2D-2E / %x30-39 / %x41-5A / %x5E-7E 305 | 'reg': 306 | r'^ssrc-group:([\x21\x23\x24\x25\x26\x27\x2A\x2B\x2D\x2E\w]*) (.*)', 307 | 'names': ['semantics', 'ssrcs'], 308 | 'format': 'ssrc-group:%s %s' 309 | }, 310 | { 311 | // a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV 312 | 'name': 'msidSemantic', 313 | 'reg': r'^msid-semantic:\s?(\w*) ?(\S*)', 314 | 'names': ['semantic', 'token'], 315 | 'format': 'msid-semantic: %s %s' // space after ':' is not accidental 316 | }, 317 | { 318 | // a=group:BUNDLE audio video 319 | 'push': 'groups', 320 | 'reg': r'^group:(\w*) ?(.*)', 321 | 'names': ['type', 'mids'], 322 | 'format': 'group:%s %s' 323 | }, 324 | { 325 | // a=rtcp-mux 326 | 'name': 'rtcpMux', 327 | 'reg': r'^(rtcp-mux)' 328 | }, 329 | { 330 | // a=rtcp-rsize 331 | 'name': 'rtcpRsize', 332 | 'reg': r'^(rtcp-rsize)' 333 | }, 334 | { 335 | // a=sctpmap:5000 webrtc-datachannel 1024 336 | 'name': 'sctpmap', 337 | 'reg': r'^sctpmap:([\w_/]*) (\S*)(?: (\S*))?', 338 | 'names': ['sctpmapNumber', 'app', 'maxMessageSize'], 339 | 'format': (o) { 340 | return (o['maxMessageSize'] != null) 341 | ? 'sctpmap:%s %s %s' 342 | : 'sctpmap:%s %s'; 343 | } as dynamic 344 | }, 345 | { 346 | // a=x-google-flag:conference 347 | 'name': 'xGoogleFlag', 348 | 'reg': r'^x-google-flag:([^\s]*)', 349 | 'format': 'x-google-flag:%s' 350 | }, 351 | { 352 | // a=rid:1 send max-width=1280;max-height=720;max-fps=30;depend=0 353 | 'push': 'rids', 354 | 'reg': r'^rid:([\d\w]+) (\w+)(?: ([\S| ]*))?', 355 | 'names': ['id', 'direction', 'params'], 356 | 'format': (o) { 357 | return (o['params'] != null) ? 'rid:%s %s %s' : 'rid:%s %s'; 358 | } as dynamic 359 | }, 360 | { 361 | // a=imageattr:97 send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] recv [x=330,y=250] 362 | // a=imageattr:* send [x=800,y=640] recv * 363 | // a=imageattr:100 recv [x=320,y=240] 364 | 'push': 'imageattrs', 365 | 'reg': new RegExp( 366 | // a=imageattr:97 367 | '^imageattr:(\\d+|\\*)' + 368 | // send [x=800,y=640,sar=1.1,q=0.6] [x=480,y=320] 369 | '[\\s\\t]+(send|recv)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*)' + 370 | // recv [x=330,y=250] 371 | '(?:[\\s\\t]+(recv|send)[\\s\\t]+(\\*|\\[\\S+\\](?:[\\s\\t]+\\[\\S+\\])*))?'), 372 | 'names': ['pt', 'dir1', 'attrs1', 'dir2', 'attrs2'], 373 | 'format': (o) { 374 | return 'imageattr:%s %s %s' + (o['dir2'] != null ? ' %s %s' : ''); 375 | } as dynamic 376 | }, 377 | { 378 | // a=simulcast:send 1,2,3;~4,~5 recv 6;~7,~8 379 | // a=simulcast:recv 1;4,5 send 6;7 380 | 'name': 'simulcast', 381 | 'reg': new RegExp( 382 | // a=simulcast: 383 | '^simulcast:' + 384 | // send 1,2,3;~4,~5 385 | '(send|recv) ([a-zA-Z0-9\\-_~;,]+)' + 386 | // space + recv 6;~7,~8 387 | '(?:\\s?(send|recv) ([a-zA-Z0-9\\-_~;,]+))?' + 388 | // end 389 | '\$'), 390 | 'names': ['dir1', 'list1', 'dir2', 'list2'], 391 | 'format': (o) { 392 | return 'simulcast:%s %s' + (o['dir2'] != null ? ' %s %s' : ''); 393 | } as dynamic 394 | }, 395 | { 396 | // old simulcast draft 03 (implemented by Firefox) 397 | // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-03 398 | // a=simulcast: recv pt=97;98 send pt=97 399 | // a=simulcast: send rid=5;6;7 paused=6,7 400 | 'name': 'simulcast_03', 401 | 'reg': r'^simulcast:[\s\t]+([\S+\s\t]+)$', 402 | 'names': ['value'], 403 | 'format': 'simulcast: %s' 404 | }, 405 | { 406 | // a=framerate:25 407 | // a=framerate:29.97 408 | 'name': 'framerate', 409 | 'reg': r'^framerate:(\d+(?:$|\.\d+))', 410 | 'format': 'framerate:%s' 411 | }, 412 | { 413 | // RFC4570 414 | // a=source-filter: incl IN IP4 239.5.2.31 10.1.15.5 415 | 'name': 'sourceFilter', 416 | 'reg': r'^source-filter: *(excl|incl) (\S*) (IP4|IP6|\*) (\S*) (.*)', 417 | 'names': [ 418 | 'filterMode', 419 | 'netType', 420 | 'addressTypes', 421 | 'destAddress', 422 | 'srcList' 423 | ], 424 | 'format': 'source-filter: %s %s %s %s %s' 425 | }, 426 | { 427 | // a=bundle-only 428 | 'name': 'bundleOnly', 429 | 'reg': r'^(bundle-only)' 430 | }, 431 | { 432 | // a=label:1 433 | 'name': 'label', 434 | 'reg': r'^label:(.+)', 435 | 'format': 'label:%s' 436 | }, 437 | { 438 | // RFC version 26 for SCTP over DTLS 439 | // https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26#section-5 440 | 'name': 'sctpPort', 441 | 'reg': r'^sctp-port:(\d+)$', 442 | 'format': 'sctp-port:%s' 443 | }, 444 | { 445 | // RFC version 26 for SCTP over DTLS 446 | // https://tools.ietf.org/html/draft-ietf-mmusic-sctp-sdp-26#section-6 447 | 'name': 'maxMessageSize', 448 | 'reg': r'^max-message-size:(\d+)$', 449 | 'format': 'max-message-size:%s' 450 | }, 451 | { 452 | // any a= that we don't understand is kepts verbatim on media.invalid 453 | 'push': 'invalid', 454 | 'names': ['value'] 455 | } 456 | ] 457 | }; 458 | -------------------------------------------------------------------------------- /lib/src/parser.dart: -------------------------------------------------------------------------------- 1 | import './grammar.dart' show grammar; 2 | import 'dart:convert'; 3 | 4 | dynamic toIntIfInt(v) { 5 | return v != null 6 | ? int.tryParse(v) != null 7 | ? int.parse(v) 8 | : v 9 | : null; 10 | } 11 | 12 | void attachProperties(Iterable match, 13 | Map location, names, rawName) { 14 | if ((rawName != null && rawName.length > 0) && 15 | (names == null || names.length == 0)) { 16 | match.forEach((m) { 17 | assert(rawName != null); 18 | location[rawName] = toIntIfInt(m.groupCount == 0 ? m.input : m.group(1)); 19 | }); 20 | } else { 21 | match.forEach((m) { 22 | for (var i = 0; i < m.groupCount; i++) { 23 | assert(names[i] != null); 24 | location[names[i].toString()] = toIntIfInt(m.group(i + 1)); 25 | } 26 | }); 27 | } 28 | } 29 | 30 | void parseReg( 31 | Map obj, Map location, String content) { 32 | var needsBlank = obj['name'] != null && obj['names'] != null; 33 | if (obj['push'] != null && location[obj['push']] == null) { 34 | assert(obj['push'] != null); 35 | location[obj['push']] = []; 36 | } else if (needsBlank && location[obj['name']] == null) { 37 | assert(obj['name'] != null); 38 | location[obj['name']] = createMap(); 39 | } 40 | 41 | var keyLocation = obj['push'] != null 42 | ? createMap() 43 | : // blank object that will be pushed 44 | needsBlank 45 | ? location[obj['name']] 46 | : location; // otherwise, named location or root 47 | 48 | if (obj['reg'] is RegExp) { 49 | attachProperties( 50 | obj['reg'].allMatches(content), keyLocation, obj['names'], obj['name']); 51 | } else { 52 | attachProperties(RegExp(obj['reg']).allMatches(content), keyLocation, 53 | obj['names'], obj['name']); 54 | } 55 | 56 | if (obj['push'] != null) { 57 | location[obj['push']].add(keyLocation); 58 | } 59 | } 60 | 61 | Map parse(String sdp) { 62 | Map session = Map(); 63 | var medias = []; 64 | 65 | var location = 66 | session; // points at where properties go under (one of the above) 67 | 68 | // parse lines we understand 69 | LineSplitter().convert(sdp).forEach((l) { 70 | if (l != '') { 71 | var type = l[0]; 72 | var content = l.substring(2); 73 | if (type == 'm') { 74 | Map media = createMap(); 75 | media['rtp'] = []; 76 | media['fmtp'] = []; 77 | location = media; // point at latest media line 78 | medias.add(media); 79 | } 80 | if (grammar[type] != null) { 81 | for (var j = 0; j < grammar[type]!.length; j += 1) { 82 | var obj = grammar[type]![j]; 83 | if (obj['reg'] == null) { 84 | if (obj['name'] != null) { 85 | location[obj['name'] as String] = content; 86 | } else { 87 | print("trying to add null key"); 88 | } 89 | continue; 90 | } 91 | 92 | if (obj['reg'] is RegExp) { 93 | if ((obj['reg'] as RegExp).hasMatch(content)) { 94 | parseReg(obj, location, content); 95 | return; 96 | } 97 | } else if (RegExp(obj['reg'] as String).hasMatch(content)) { 98 | parseReg(obj, location, content); 99 | return; 100 | } 101 | } 102 | if (location['invalid'] == null) { 103 | location['invalid'] = []; 104 | } 105 | Map tmp = createMap(); 106 | tmp['value'] = content; 107 | location['invalid'].add(tmp); 108 | } else { 109 | print("ERROR unknown grammer type " + type); 110 | } 111 | } 112 | }); 113 | session['media'] = medias; // link it up 114 | return session; 115 | } 116 | 117 | Map parseParams(String str) { 118 | Map params = createMap(); 119 | str.split(new RegExp(r';').pattern).forEach((line) { 120 | // only split at the first '=' as there may be an '=' in the value as well 121 | int idx = line.indexOf("="); 122 | String key; 123 | String value = ""; 124 | if (idx == -1) { 125 | key = line; 126 | } else { 127 | key = line.substring(0, idx).trim(); 128 | value = line.substring(idx + 1, line.length).trim(); 129 | } 130 | 131 | params[key] = toIntIfInt(value); 132 | }); 133 | return params; 134 | } 135 | 136 | List parsePayloads(str) { 137 | return str.split(' '); 138 | } 139 | 140 | List parseRemoteCandidates(String str) { 141 | var candidates = []; 142 | List parts = []; 143 | str.split(' ').forEach((dynamic v) { 144 | dynamic value = toIntIfInt(v); 145 | if (value != null) { 146 | parts.add(value); 147 | } 148 | }); 149 | for (var i = 0; i < parts.length; i += 3) { 150 | candidates 151 | .add({'component': parts[i], 'ip': parts[i + 1], 'port': parts[i + 2]}); 152 | } 153 | return candidates; 154 | } 155 | 156 | List> parseImageAttributes(String str) { 157 | List> attributes = []; 158 | str.split(' ').forEach((item) { 159 | Map params = createMap(); 160 | item.substring(1, item.length - 1).split(',').forEach((attr) { 161 | List kv = attr.split(new RegExp(r'=').pattern); 162 | params[kv[0]] = toIntIfInt(kv[1]); 163 | }); 164 | attributes.add(params); 165 | }); 166 | return attributes; 167 | } 168 | 169 | Map createMap() { 170 | return Map(); 171 | } 172 | 173 | List parseSimulcastStreamList(String str) { 174 | List attributes = []; 175 | str.split(';').forEach((stream) { 176 | List scids = []; 177 | stream.split(',').forEach((format) { 178 | var scid, paused = false; 179 | if (format[0] != '~') { 180 | scid = toIntIfInt(format); 181 | } else { 182 | scid = toIntIfInt(format.substring(1, format.length)); 183 | paused = true; 184 | } 185 | Map data = createMap(); 186 | data['scid'] = scid; 187 | data['paused'] = paused; 188 | scids.add(data); 189 | }); 190 | attributes.add(scids); 191 | }); 192 | return attributes; 193 | } 194 | -------------------------------------------------------------------------------- /lib/src/writer.dart: -------------------------------------------------------------------------------- 1 | import './grammar.dart' show grammar; 2 | 3 | var formatRegExp = new RegExp(r'%d|%v|%s'); 4 | 5 | format(formatStr, args) { 6 | var i = 0; 7 | return formatStr.replaceAllMapped( 8 | formatRegExp, (Match m) => args[i++].toString()); 9 | } 10 | 11 | makeLine(type, obj, location) { 12 | var str; 13 | 14 | if (obj['format'] != null) { 15 | var format = obj['format']; 16 | if (format is Function) { 17 | str = format(obj['push'] != null ? location : location[obj['name']]); 18 | } else { 19 | str = obj['format']; 20 | } 21 | } else { 22 | try { 23 | str = '${location[obj['name']]}'; 24 | } catch (e) { 25 | print('e = ' + e.toString()); 26 | } 27 | } 28 | var formatStr = type + '=' + str.toString(); 29 | var args = []; 30 | if (obj['names'] != null) { 31 | for (var i = 0; i < obj['names'].length; i += 1) { 32 | var n = obj['names'][i]; 33 | if (obj['name'] != null) { 34 | args.add(location[obj['name']][n].toString()); 35 | } else { 36 | // for mLine and push attributes 37 | var arg = location[obj['names'][i]] ?? ''; 38 | args.add(arg.toString()); 39 | } 40 | } 41 | } else { 42 | args.add(location[obj['name']]); 43 | } 44 | return format(formatStr, args); 45 | } 46 | 47 | // RFC specified order 48 | var defaultOuterOrder = [ 49 | 'v', 50 | 'o', 51 | 's', 52 | 'i', 53 | 'u', 54 | 'e', 55 | 'p', 56 | 'c', 57 | 'b', 58 | 't', 59 | 'r', 60 | 'z', 61 | 'a' 62 | ]; 63 | 64 | var defaultInnerOrder = ['i', 'c', 'b', 'a']; 65 | 66 | String write(Map session, Map? opts) { 67 | opts = opts ?? {'outerOrder': null, 'innerOrder': null}; 68 | 69 | // ensure certain properties exist 70 | if (session['version'] == null) { 71 | session['version'] = 0; // 'v=0' must be there (only defined version atm) 72 | } 73 | if (session['name'] == null) { 74 | session['name'] = ' '; // 's= ' must be there if no meaningful name set 75 | } 76 | 77 | session['media'].forEach((mLine) { 78 | if (mLine['payloads'] == null) { 79 | mLine['payloads'] = ''; 80 | } 81 | }); 82 | 83 | var outerOrder = opts['souterOrder'] ?? defaultOuterOrder; 84 | var innerOrder = opts['innerOrder'] ?? defaultInnerOrder; 85 | var sdp = []; 86 | 87 | // loop through outerOrder for matching properties on session 88 | outerOrder.forEach((type) { 89 | grammar[type]!.forEach((obj) { 90 | if (obj['name'] != null && session[obj['name']] != null) { 91 | sdp.add(makeLine(type, obj, session)); 92 | } else if (obj['push'] != null && session[obj['push']] != null) { 93 | session[obj['push']].forEach((el) { 94 | sdp.add(makeLine(type, obj, el)); 95 | }); 96 | } 97 | }); 98 | }); 99 | 100 | // then for each media line, follow the innerOrder 101 | session['media'].forEach((mLine) { 102 | sdp.add(makeLine('m', grammar['m']![0], mLine)); 103 | innerOrder.forEach((type) { 104 | grammar[type]!.forEach((obj) { 105 | if (obj['name'] != null && mLine[obj['name']] != null) { 106 | sdp.add(makeLine(type, obj, mLine)); 107 | } else if (obj['push'] != null && mLine[obj['push']] != null) { 108 | mLine[obj['push']].forEach((el) { 109 | sdp.add(makeLine(type, obj, el)); 110 | }); 111 | } 112 | }); 113 | }); 114 | }); 115 | 116 | return sdp.join('\r\n') + '\r\n'; 117 | } 118 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: sdp_transform 2 | version: 0.3.2 3 | description: Dart implementation of sdp-transform 4 | homepage: https://github.com/cloudwebrtc/dart-sdp-transform 5 | environment: 6 | sdk: '>=2.12.0 <3.0.0' 7 | 8 | dev_dependencies: 9 | test: ^1.9.3 -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/regexp_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:core'; 2 | import "package:test/test.dart"; 3 | 4 | void main() { 5 | test("basictest", basictest); 6 | test("basictest2", basictest2); 7 | test("basictest3", basictest3); 8 | test("basictest4", basictest4); 9 | } 10 | 11 | basictest() { 12 | RegExp exp = new RegExp(r"(\w+)"); 13 | String str = "Parse my string"; 14 | Iterable matches = exp.allMatches(str); 15 | matches.forEach((Match m) { 16 | String match = m.group(0)!; 17 | print('value = ' + match); 18 | }); 19 | } 20 | 21 | basictest2() { 22 | RegExp exp = new RegExp(r'^(\S*) (\d*) (\d*) (\S*) IP(\d) (\S*)$'); 23 | String str = "o=- 20518 0 IN IP4 203.0.113.1"; 24 | Iterable matches = exp.allMatches(str); 25 | matches.forEach((Match m) { 26 | print('group(0) = ' + m.group(0)!); 27 | for (var i = 0; i < m.groupCount; i++) 28 | print('group(' + (1 + i).toString() + ') = ' + m.group(i + 1)!); 29 | }); 30 | } 31 | 32 | basictest3() { 33 | RegExp exp = new RegExp( 34 | r'^a=candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?(?: network-id (\d*))?(?: network-cost (\d*))?'); 35 | String str = 36 | "a=candidate:3289912957 2 tcp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 tcptype passive generation 0 network-id 3 network-cost 10"; 37 | var names = [ 38 | 'foundation', 39 | 'component', 40 | 'transport', 41 | 'priority', 42 | 'ip', 43 | 'port', 44 | 'type', 45 | 'raddr', 46 | 'rport', 47 | 'tcptype', 48 | 'generation', 49 | 'network-id', 50 | 'network-cost' 51 | ]; 52 | Iterable matches = exp.allMatches(str); 53 | matches.forEach((Match m) { 54 | print('group(0) = ' + m.group(0)!); 55 | for (var i = 0; i < m.groupCount; i++) 56 | if (m.group(i + 1) != null) 57 | print('group(' + 58 | (1 + i).toString() + 59 | ') ' + 60 | names[i] + 61 | ' = ' + 62 | m.group(i + 1)!); 63 | }); 64 | } 65 | 66 | basictest4() { 67 | RegExp exp = new RegExp( 68 | r'^a=candidate:(\S*) (\d*) (\S*) (\d*) (\S*) (\d*) typ (\S*)(?: raddr (\S*) rport (\d*))?(?: tcptype (\S*))?(?: generation (\d*))?(?: network-id (\d*))?(?: network-cost (\d*))?'); 69 | String str = "a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host"; 70 | var names = [ 71 | 'foundation', 72 | 'component', 73 | 'transport', 74 | 'priority', 75 | 'ip', 76 | 'port', 77 | 'type', 78 | 'raddr', 79 | 'rport', 80 | 'tcptype', 81 | 'generation', 82 | 'network-id', 83 | 'network-cost' 84 | ]; 85 | Iterable matches = exp.allMatches(str); 86 | matches.forEach((Match m) { 87 | print('group(0) = ' + 88 | m.group(0)! + 89 | ", groupCount = " + 90 | m.groupCount.toString()); 91 | for (var i = 0; i < m.groupCount; i++) 92 | if (m.group(i + 1) != null) 93 | print('group(' + 94 | (1 + i).toString() + 95 | ') ' + 96 | names[i] + 97 | ' = ' + 98 | m.group(i + 1)!); 99 | }); 100 | } 101 | -------------------------------------------------------------------------------- /test/sdp_parse_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import '../lib/sdp_transform.dart'; 3 | import "package:test/test.dart"; 4 | 5 | main() { 6 | test("NormalSdp", _testNormalSdp); 7 | test("HackySdp", _testHackySdp); 8 | test("IceliteSdp", _testIceliteSdp); 9 | test("InvalidSdp", _testInvalidSdp); 10 | test("JssipSdp", _testJssipSdp); 11 | test("JsepSdp", _testJsepSdp); 12 | test("AlacSdp", _testAlacSdp); 13 | test("SsrcSdp", _testSsrcSdp); 14 | test("SimulcastSdp", _testSimulcastSdp); 15 | test("St2202_6Sdp", _testSt2202_6Sdp); 16 | test("St2110_20Sdp", _testSt2110_20Sdp); 17 | test("SctpDtls26Sdp", _testSctpDtls26Sdp); 18 | test("ExtmapEncryptSdp", _testExtmapEncryptSdp); 19 | test("DanteAes67", _testDanteAes67); 20 | test("TcpActive", _testTcpActive); 21 | test("TcpPassive", _testTcpPassive); 22 | test("asterisk", _testAsterisk); 23 | } 24 | 25 | void ok(dynamic test, String message) { 26 | if (test != null) { 27 | print(message); 28 | } else { 29 | assert(false); 30 | } 31 | } 32 | 33 | void equal(dynamic value, dynamic match, String message) { 34 | if (value != match) { 35 | print(message); 36 | assert(false, message); 37 | } 38 | } 39 | 40 | deepEqual(dynamic value, dynamic match, String message) {} 41 | 42 | _testAsterisk() async { 43 | String sdp = 44 | await new File('./test/sdp_test_data/asterisk.sdp').readAsString(); 45 | Map session = parse(sdp + ''); 46 | ok(session, 'got session info'); 47 | } 48 | 49 | _testNormalSdp() async { 50 | String sdp = await new File('./test/sdp_test_data/normal.sdp').readAsString(); 51 | 52 | Map session = parse(sdp + ''); 53 | ok(session, 'got session info'); 54 | dynamic media = session['media']; 55 | ok(media != null && media.length > 0, 'got media'); 56 | 57 | equal(session['origin']['username'], '-', 'origin username'); 58 | equal(session['origin']['sessionId'], 20518, 'origin sessionId'); 59 | equal(session['origin']['sessionVersion'], 0, 'origin sessionVersion'); 60 | equal(session['origin']['netType'], 'IN', 'origin netType'); 61 | equal(session['origin']['ipVer'], 4, 'origin ipVer'); 62 | equal(session['origin']['address'], '203.0.113.1', 'origin address'); 63 | 64 | equal(session['connection']['ip'], '203.0.113.1', 'session connect ip'); 65 | equal(session['connection']['version'], 4, 'session connect ip ver'); 66 | 67 | // global ICE and fingerprint 68 | equal(session['iceUfrag'], 'F7gI', 'global ufrag'); 69 | equal(session['icePwd'], 'x9cml/YzichV2+XlhiMu8g', 'global pwd'); 70 | 71 | dynamic audio = media[0]; 72 | equal(audio['type'], 'audio', 'audio type'); 73 | equal(audio['port'], 54400, 'audio port'); 74 | equal(audio['protocol'], 'RTP/SAVPF', 'audio protocol'); 75 | equal(audio['direction'], 'sendrecv', 'audio direction'); 76 | equal(audio['rtp'][0]['payload'], 0, 'audio rtp 0 payload'); 77 | equal(audio['rtp'][0]['codec'], 'PCMU', 'audio rtp 0 codec'); 78 | equal(audio['rtp'][0]['rate'], 8000, 'audio rtp 0 rate'); 79 | equal(audio['rtp'][1]['payload'], 96, 'audio rtp 1 payload'); 80 | equal(audio['rtp'][1]['codec'], 'opus', 'audio rtp 1 codec'); 81 | equal(audio['rtp'][1]['rate'], 48000, 'audio rtp 1 rate'); 82 | deepEqual( 83 | audio['ext'][0], {'value': 1, 'uri': 'URI-toffset'}, 'audio extension 0'); 84 | deepEqual( 85 | audio['ext'][1], 86 | {'value': 2, 'direction': 'recvonly', 'uri': 'URI-gps-string'}, 87 | 'audio extension 1'); 88 | deepEqual( 89 | audio['extmapAllowMixed'], 90 | [ 91 | {'extmap-allow-mixed': 'extmap-allow-mixed'} 92 | ], 93 | 'extmap-allow-mixed'); 94 | 95 | dynamic video = media[1]; 96 | equal(video['type'], 'video', 'video type'); 97 | equal(video['port'], 55400, 'video port'); 98 | equal(video['protocol'], 'RTP/SAVPF', 'video protocol'); 99 | equal(video['direction'], 'sendrecv', 'video direction'); 100 | equal(video['rtp'][0]['payload'], 97, 'video rtp 0 payload'); 101 | equal(video['rtp'][0]['codec'], 'H264', 'video rtp 0 codec'); 102 | equal(video['rtp'][0]['rate'], 90000, 'video rtp 0 rate'); 103 | equal(video['fmtp'][0]['payload'], 97, 'video fmtp 0 payload'); 104 | dynamic vidFmtp = parseParams(video['fmtp'][0]['config']); 105 | equal(vidFmtp['profile-level-id'], '4d0028', 'video fmtp 0 profile-level-id'); 106 | equal(vidFmtp['packetization-mode'], 1, 'video fmtp 0 packetization-mode'); 107 | equal(vidFmtp['sprop-parameter-sets'], 'Z0IAH5WoFAFuQA==,aM48gA==', 108 | 'video fmtp 0 sprop-parameter-sets'); 109 | equal(video['fmtp'][1]['payload'], 98, 'video fmtp 1 payload'); 110 | dynamic vidFmtp2 = parseParams(video['fmtp'][1]['config']); 111 | equal(vidFmtp2['minptime'], 10, 'video fmtp 1 minptime'); 112 | equal(vidFmtp2['useinbandfec'], 1, 'video fmtp 1 useinbandfec'); 113 | equal(video['rtp'][1]['payload'], 98, 'video rtp 1 payload'); 114 | equal(video['rtp'][1]['codec'], 'VP8', 'video rtp 1 codec'); 115 | equal(video['rtp'][1]['rate'], 90000, 'video rtp 1 rate'); 116 | equal(video['rtcpFb'][0]['payload'], '*', 'video rtcp-fb 0 payload'); 117 | equal(video['rtcpFb'][0]['type'], 'nack', 'video rtcp-fb 0 type'); 118 | equal(video['rtcpFb'][1]['payload'], 98, 'video rtcp-fb 0 payload'); 119 | equal(video['rtcpFb'][1]['type'], 'nack', 'video rtcp-fb 0 type'); 120 | equal(video['rtcpFb'][1]['subtype'], 'rpsi', 'video rtcp-fb 0 subtype'); 121 | equal(video['rtcpFbTrrInt'][0]['payload'], 98, 122 | 'video rtcp-fb trr-int 0 payload'); 123 | equal( 124 | video['rtcpFbTrrInt'][0]['value'], 100, 'video rtcp-fb trr-int 0 value'); 125 | equal(video['crypto'][0]['id'], 1, 'video crypto 0 id'); 126 | equal(video['crypto'][0]['suite'], 'AES_CM_128_HMAC_SHA1_32', 127 | 'video crypto 0 suite'); 128 | equal( 129 | video['crypto'][0]['config'], 130 | 'inline:keNcG3HezSNID7LmfDa9J4lfdUL8W1F7TNJKcbuy|2^20|1:32', 131 | 'video crypto 0 config'); 132 | equal(video['ssrcs'].length, 3, 'video got 3 ssrc lines'); 133 | // test ssrc with attr:value 134 | deepEqual( 135 | video['ssrcs'][0], 136 | {'id': 1399694169, 'attribute': 'foo', 'value': 'bar'}, 137 | 'video 1st ssrc line attr:value'); 138 | // test ssrc with attr only 139 | deepEqual( 140 | video['ssrcs'][1], 141 | { 142 | 'id': 1399694169, 143 | 'attribute': 'baz', 144 | }, 145 | 'video 2nd ssrc line attr only'); 146 | // test ssrc with at-tr:value 147 | deepEqual( 148 | video['ssrcs'][2], 149 | {'id': 1399694169, 'attribute': 'foo-bar', 'value': 'baz'}, 150 | 'video 3rd ssrc line attr with dash'); 151 | 152 | // ICE candidates (same for both audio and video in this case) 153 | int i = 0; 154 | [audio['candidates'], video['candidates']].forEach((cs) { 155 | dynamic str = (i == 0) ? 'audio ' : 'video '; 156 | dynamic port = (i == 0) ? 54400 : 55400; 157 | 158 | equal(cs.length, 4, str + 'got 4 candidates'); 159 | equal(cs[0]['foundation'], 0, str + 'ice candidate 0 foundation'); 160 | equal(cs[0]['component'], 1, str + 'ice candidate 0 component'); 161 | equal(cs[0]['transport'], 'UDP', str + 'ice candidate 0 transport'); 162 | equal(cs[0]['priority'], 2113667327, str + 'ice candidate 0 priority'); 163 | equal(cs[0]['ip'], '203.0.113.1', str + 'ice candidate 0 ip'); 164 | equal(cs[0]['port'], port, str + 'ice candidate 0 port'); 165 | equal(cs[0]['type'], 'host', str + 'ice candidate 0 type'); 166 | equal(cs[1]['foundation'], 1, str + 'ice candidate 1 foundation'); 167 | equal(cs[1]['component'], 2, str + 'ice candidate 1 component'); 168 | equal(cs[1]['transport'], 'UDP', str + 'ice candidate 1 transport'); 169 | equal(cs[1]['priority'], 2113667326, str + 'ice candidate 1 priority'); 170 | equal(cs[1]['ip'], '203.0.113.1', str + 'ice candidate 1 ip'); 171 | equal(cs[1]['port'], port + 1, str + 'ice candidate 1 port'); 172 | equal(cs[1]['type'], 'host', str + 'ice candidate 1 type'); 173 | equal(cs[2]['foundation'], 2, str + 'ice candidate 2 foundation'); 174 | equal(cs[2]['component'], 1, str + 'ice candidate 2 component'); 175 | equal(cs[2]['transport'], 'UDP', str + 'ice candidate 2 transport'); 176 | equal(cs[2]['priority'], 1686052607, str + 'ice candidate 2 priority'); 177 | equal(cs[2]['ip'], '203.0.113.1', str + 'ice candidate 2 ip'); 178 | equal(cs[2]['port'], port + 2, str + 'ice candidate 2 port'); 179 | equal(cs[2]['type'], 'srflx', str + 'ice candidate 2 type'); 180 | equal(cs[2]['raddr'], '192.168.1.145', str + 'ice candidate 2 raddr'); 181 | equal(cs[2]['rport'], port + 2, str + 'ice candidate 2 rport'); 182 | equal(cs[2]['generation'], 0, str + 'ice candidate 2 generation'); 183 | equal(cs[2]['network-id'], 3, str + 'ice candidate 2 network-id'); 184 | equal(cs[2]['network-cost'], (i == 0 ? 10 : null), 185 | str + 'ice candidate 2 network-cost'); 186 | equal(cs[3]['foundation'], 3, str + 'ice candidate 3 foundation'); 187 | equal(cs[3]['component'], 2, str + 'ice candidate 3 component'); 188 | equal(cs[3]['transport'], 'UDP', str + 'ice candidate 3 transport'); 189 | equal(cs[3]['priority'], 1686052606, str + 'ice candidate 3 priority'); 190 | equal(cs[3]['ip'], '203.0.113.1', str + 'ice candidate 3 ip'); 191 | equal(cs[3]['port'], port + 3, str + 'ice candidate 3 port'); 192 | equal(cs[3]['type'], 'srflx', str + 'ice candidate 3 type'); 193 | equal(cs[3]['raddr'], '192.168.1.145', str + 'ice candidate 3 raddr'); 194 | equal(cs[3]['rport'], port + 3, str + 'ice candidate 3 rport'); 195 | equal(cs[3]['generation'], 0, str + 'ice candidate 3 generation'); 196 | equal(cs[3]['network-id'], 3, str + 'ice candidate 3 network-id'); 197 | equal(cs[3]['network-cost'], (i == 0 ? 10 : null), 198 | str + 'ice candidate 3 network-cost'); 199 | i++; 200 | }); 201 | 202 | equal(media.length, 2, 'got 2 m-lines'); 203 | } 204 | 205 | /* 206 | * Test for an sdp that started out as something from chrome 207 | * it's since been hacked to include tests for other stuff 208 | * ignore the name 209 | */ 210 | _testHackySdp() async { 211 | String sdp = await new File('./test/sdp_test_data/hacky.sdp').readAsString(); 212 | 213 | Map session = parse(sdp + ''); 214 | ok(session, 'got session info'); 215 | dynamic media = session['media']; 216 | ok(media != null && media.length > 0, 'got media'); 217 | 218 | equal( 219 | session['origin']['sessionId'], 3710604898417546434, 'origin sessionId'); 220 | ok(session['groups'], 'parsing session groups'); 221 | equal(session['groups'].length, 1, 'one grouping'); 222 | equal(session['groups'][0]['type'], 'BUNDLE', 'grouping is BUNDLE'); 223 | equal(session['groups'][0]['mids'], 'audio video', 'bundling audio video'); 224 | ok(session['msidSemantic'], 'have an msid semantic'); 225 | equal(session['msidSemantic']['semantic'], 'WMS', 'webrtc semantic'); 226 | equal(session['msidSemantic']['token'], 227 | 'Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV', 'semantic token'); 228 | 229 | // verify a=rtcp:65179 IN IP4 193.84.77.194 230 | equal(media[0]['rtcp']['port'], 1, 'rtcp port'); 231 | equal(media[0]['rtcp']['netType'], 'IN', 'rtcp netType'); 232 | equal(media[0]['rtcp']['ipVer'], 4, 'rtcp ipVer'); 233 | equal(media[0]['rtcp']['address'], '0.0.0.0', 'rtcp address'); 234 | 235 | // verify ice tcp types 236 | equal(media[0]['candidates'][0]['tcptype'], null, 'no tcptype'); 237 | equal(media[0]['candidates'][1]['tcptype'], 'active', 'active tcptype'); 238 | equal(media[0]['candidates'][1]['transport'], 'tcp', 'tcp transport'); 239 | equal(media[0]['candidates'][1]['generation'], 0, 'generation 0'); 240 | equal(media[0]['candidates'][1]['type'], 'host', 'tcp host'); 241 | equal(media[0]['candidates'][2]['generation'], null, 'no generation'); 242 | equal(media[0]['candidates'][2]['type'], 'host', 'tcp host'); 243 | equal(media[0]['candidates'][2]['tcptype'], 'active', 'active tcptype'); 244 | equal(media[0]['candidates'][3]['tcptype'], 'passive', 'passive tcptype'); 245 | equal(media[0]['candidates'][4]['tcptype'], 'so', 'so tcptype'); 246 | // raddr + rport + tcptype + generation 247 | equal(media[0]['candidates'][5]['type'], 'srflx', 'tcp srflx'); 248 | equal(media[0]['candidates'][5]['rport'], 9, 'tcp rport'); 249 | equal(media[0]['candidates'][5]['raddr'], '10.0.1.1', 'tcp raddr'); 250 | equal(media[0]['candidates'][5]['tcptype'], 'active', 'active tcptype'); 251 | equal(media[0]['candidates'][6]['tcptype'], 'passive', 'passive tcptype'); 252 | equal(media[0]['candidates'][6]['rport'], 8998, 'tcp rport'); 253 | equal(media[0]['candidates'][6]['raddr'], '10.0.1.1', 'tcp raddr'); 254 | equal(media[0]['candidates'][6]['generation'], 5, 'tcp generation'); 255 | 256 | // and verify it works without specifying the ip 257 | equal(media[1]['rtcp']['port'], 12312, 'rtcp port'); 258 | equal(media[1]['rtcp']['netType'], null, 'rtcp netType'); 259 | equal(media[1]['rtcp']['ipVer'], null, 'rtcp ipVer'); 260 | equal(media[1]['rtcp']['address'], null, 'rtcp address'); 261 | 262 | // verify a=rtpmap:126 telephone-event/8000 263 | dynamic lastRtp = media[0]['rtp'].length - 1; 264 | equal(media[0]['rtp'][lastRtp]['codec'], 'telephone-event', 'dtmf codec'); 265 | equal(media[0]['rtp'][lastRtp]['rate'], 8000, 'dtmf rate'); 266 | 267 | equal(media[0]['iceOptions'], 'google-ice', 'ice options parsed'); 268 | equal(media[0]['maxptime'], 60, 'maxptime parsed'); 269 | equal(media[0]['rtcpMux'], 'rtcp-mux', 'rtcp-mux present'); 270 | 271 | equal(media[0]['rtp'][0]['codec'], 'opus', 'audio rtp 0 codec'); 272 | equal(media[0]['rtp'][0]['encoding'], 2, 'audio rtp 0 encoding'); 273 | 274 | ok(media[0]['ssrcs'], 'have ssrc lines'); 275 | equal(media[0]['ssrcs'].length, 4, 'got 4 ssrc lines'); 276 | dynamic ssrcs = media[0]['ssrcs']; 277 | deepEqual( 278 | ssrcs[0], 279 | {'id': 2754920552, 'attribute': 'cname', 'value': 't9YU8M1UxTF8Y1A1'}, 280 | '1st ssrc line'); 281 | 282 | deepEqual( 283 | ssrcs[1], 284 | { 285 | 'id': 2754920552, 286 | 'attribute': 'msid', 287 | 'value': 288 | 'Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVa0' 289 | }, 290 | '2nd ssrc line'); 291 | 292 | deepEqual( 293 | ssrcs[2], 294 | { 295 | 'id': 2754920552, 296 | 'attribute': 'mslabel', 297 | 'value': 'Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV' 298 | }, 299 | '3rd ssrc line'); 300 | 301 | deepEqual( 302 | ssrcs[3], 303 | { 304 | 'id': 2754920552, 305 | 'attribute': 'label', 306 | 'value': 'Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVa0' 307 | }, 308 | '4th ssrc line'); 309 | 310 | // verify a=sctpmap:5000 webrtc-datachannel 1024 311 | ok(media[2]['sctpmap'], 'we have sctpmap'); 312 | equal(media[2]['sctpmap']['sctpmapNumber'], 5000, 'sctpmap number is 5000'); 313 | equal(media[2]['sctpmap']['app'], 'webrtc-datachannel', 314 | 'sctpmap app is webrtc-datachannel'); 315 | equal(media[2]['sctpmap']['maxMessageSize'], 1024, 316 | 'sctpmap maxMessageSize is 1024'); 317 | 318 | // verify a=framerate:29.97 319 | ok(media[2]['framerate'], 'we have framerate'); 320 | equal(media[2]['framerate'], '29.97', 'framerate is 29.97'); 321 | 322 | // verify a=label:1 323 | ok(media[0]['label'], 'we have label'); 324 | equal(media[0]['label'], 1, 'label is 1'); 325 | } 326 | 327 | _testIceliteSdp() async { 328 | String sdp = 329 | await new File('./test/sdp_test_data/icelite.sdp').readAsString(); 330 | 331 | Map session = parse(sdp + ''); 332 | ok(session, 'got session info'); 333 | equal(session['icelite'], 'ice-lite', 'icelite parsed'); 334 | 335 | dynamic rew = write(session, null); 336 | ok(rew.indexOf('a=ice-lite\r\n') >= 0, 'got ice-lite'); 337 | ok(rew.indexOf('m=') > rew.indexOf('a=ice-lite'), 'session level icelite'); 338 | } 339 | 340 | _testInvalidSdp() async { 341 | String sdp = 342 | await new File('./test/sdp_test_data/invalid.sdp').readAsString(); 343 | 344 | Map session = parse(sdp + ''); 345 | ok(session, 'got session info'); 346 | dynamic media = session['media']; 347 | ok(media != null && media.length > 0, 'got media'); 348 | 349 | // verify a=rtcp:65179 IN IP4 193.84.77.194 350 | equal(media[0]['rtcp']['port'], 1, 'rtcp port'); 351 | equal(media[0]['rtcp']['netType'], 'IN', 'rtcp netType'); 352 | equal(media[0]['rtcp']['ipVer'], 7, 'rtcp ipVer'); 353 | equal(media[0]['rtcp']['address'], 'X', 'rtcp address'); 354 | equal( 355 | media[0]['invalid'].length, 1, 'found exactly 1 invalid line'); // f= lost 356 | equal(media[0]['invalid'][0]['value'], 'goo:hithere', 'copied verbatim'); 357 | } 358 | 359 | _testJssipSdp() async { 360 | String sdp = await new File('./test/sdp_test_data/jssip.sdp').readAsString(); 361 | 362 | Map session = parse(sdp + ''); 363 | ok(session, 'got session info'); 364 | dynamic media = session['media']; 365 | ok(media != null && media.length > 0, 'got media'); 366 | 367 | dynamic audio = media[0]; 368 | dynamic audCands = audio['candidates']; 369 | equal(audCands.length, 6, '6 candidates'); 370 | 371 | // testing ice optionals: 372 | deepEqual( 373 | audCands[0], 374 | { 375 | 'foundation': 1162875081, 376 | 'component': 1, 377 | 'transport': 'udp', 378 | 'priority': 2113937151, 379 | 'ip': '192.168.34.75', 380 | 'port': 60017, 381 | 'type': 'host', 382 | 'generation': 0, 383 | }, 384 | 'audio candidate 0'); 385 | deepEqual( 386 | audCands[2], 387 | { 388 | 'foundation': 3289912957, 389 | 'component': 1, 390 | 'transport': 'udp', 391 | 'priority': 1845501695, 392 | 'ip': '193.84.77.194', 393 | 'port': 60017, 394 | 'type': 'srflx', 395 | 'raddr': '192.168.34.75', 396 | 'rport': 60017, 397 | 'generation': 0, 398 | }, 399 | 'audio candidate 2 (raddr rport)'); 400 | deepEqual( 401 | audCands[4], 402 | { 403 | 'foundation': 198437945, 404 | 'component': 1, 405 | 'transport': 'tcp', 406 | 'priority': 1509957375, 407 | 'ip': '192.168.34.75', 408 | 'port': 0, 409 | 'type': 'host', 410 | 'generation': 0 411 | }, 412 | 'audio candidate 4 (tcp)'); 413 | } 414 | 415 | _testJsepSdp() async { 416 | String sdp = await new File('./test/sdp_test_data/jsep.sdp').readAsString(); 417 | 418 | Map session = parse(sdp + ''); 419 | ok(session, 'got session info'); 420 | dynamic media = session['media']; 421 | ok(media != null && media.length == 2, 'got media'); 422 | 423 | dynamic video = media[1]; 424 | equal(video['ssrcGroups'].length, 1, '1 ssrc grouping'); 425 | deepEqual(video['ssrcGroups'][0], 426 | {'semantics': 'FID', 'ssrcs': '1366781083 1366781084'}, 'ssrc-group'); 427 | 428 | equal( 429 | video['msid'], 430 | '61317484-2ed4-49d7-9eb7-1414322a7aae f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0', 431 | 'msid'); 432 | 433 | ok(video['rtcpRsize'], 'rtcp-rsize present'); 434 | ok(video['bundleOnly'], 'bundle-only present'); 435 | 436 | // video contains 'a=end-of-candidates' 437 | // we want to ensure this comes after the candidate lines 438 | // so this is the only place we actually test the writer in here 439 | ok(video['endOfCandidates'], 'have end of candidates marker'); 440 | dynamic rewritten = write(session, null).split('\r\n'); 441 | dynamic idx = rewritten.indexOf('a=end-of-candidates'); 442 | equal((rewritten[idx - 1] as String).substring(0, 11), 'a=candidate', 443 | 'marker after candidate'); 444 | } 445 | 446 | _testAlacSdp() async { 447 | String sdp = await new File('./test/sdp_test_data/alac.sdp').readAsString(); 448 | 449 | Map session = parse(sdp + ''); 450 | ok(session, 'got session info'); 451 | dynamic media = session['media']; 452 | ok(media != null && media.length > 0, 'got media'); 453 | 454 | dynamic audio = media[0]; 455 | equal(audio['type'], 'audio', 'audio type'); 456 | equal(audio['protocol'], 'RTP/AVP', 'audio protocol'); 457 | equal(audio['fmtp'][0]['payload'], 96, 'audio fmtp 0 payload'); 458 | equal(audio['fmtp'][0]['config'], '352 0 16 40 10 14 2 255 0 0 44100', 459 | 'audio fmtp 0 config'); 460 | equal(audio['rtp'][0]['payload'], 96, 'audio rtp 0 payload'); 461 | equal(audio['rtp'][0]['codec'], 'AppleLossless', 'audio rtp 0 codec'); 462 | equal(audio['rtp'][0]['rate'], null, 'audio rtp 0 rate'); 463 | equal(audio['rtp'][0]['encoding'], null, 'audio rtp 0 encoding'); 464 | } 465 | 466 | testOnvifSdp() async { 467 | String sdp = await new File('./test/sdp_test_data/onvif.sdp').readAsString(); 468 | 469 | Map session = parse(sdp + ''); 470 | ok(session, 'got session info'); 471 | dynamic media = session['media']; 472 | ok(media != null && media.length > 0, 'got media'); 473 | 474 | dynamic audio = media[0]; 475 | equal(audio['type'], 'audio', 'audio type'); 476 | equal(audio['port'], 0, 'audio port'); 477 | equal(audio['protocol'], 'RTP/AVP', 'audio protocol'); 478 | equal(audio['control'], 'rtsp://example.com/onvif_camera/audio', 479 | 'audio control'); 480 | equal(audio['payloads'], 0, 'audio payloads'); 481 | 482 | dynamic video = media[1]; 483 | equal(video['type'], 'video', 'video type'); 484 | equal(video['port'], 0, 'video port'); 485 | equal(video['protocol'], 'RTP/AVP', 'video protocol'); 486 | equal(video['control'], 'rtsp://example.com/onvif_camera/video', 487 | 'video control'); 488 | equal(video['payloads'], 26, 'video payloads'); 489 | 490 | dynamic application = media[2]; 491 | equal(application['type'], 'application', 'application type'); 492 | equal(application['port'], 0, 'application port'); 493 | equal(application['protocol'], 'RTP/AVP', 'application protocol'); 494 | equal(application['control'], 'rtsp://example.com/onvif_camera/metadata', 495 | 'application control'); 496 | equal(application['payloads'], 107, 'application payloads'); 497 | equal(application['direction'], 'recvonly', 'application direction'); 498 | equal(application['rtp'][0]['payload'], 107, 'application rtp 0 payload'); 499 | equal(application['rtp'][0]['codec'], 'vnd.onvif.metadata', 500 | 'application rtp 0 codec'); 501 | equal(application['rtp'][0]['rate'], 90000, 'application rtp 0 rate'); 502 | equal(application['rtp'][0]['encoding'], null, 'application rtp 0 encoding'); 503 | } 504 | 505 | _testSsrcSdp() async { 506 | String sdp = await new File('./test/sdp_test_data/ssrc.sdp').readAsString(); 507 | 508 | Map session = parse(sdp + ''); 509 | ok(session, 'got session info'); 510 | dynamic media = session['media']; 511 | ok(media != null && media.length > 0, 'got media'); 512 | 513 | dynamic video = media[1]; 514 | equal(video['ssrcGroups'].length, 2, 'video got 2 ssrc-group lines'); 515 | 516 | dynamic expectedSsrc = [ 517 | {'semantics': 'FID', 'ssrcs': '3004364195 1126032854'}, 518 | {'semantics': 'FEC-FR', 'ssrcs': '3004364195 1080772241'} 519 | ]; 520 | deepEqual(video['ssrcGroups'], expectedSsrc, 'video ssrc-group obj'); 521 | } 522 | 523 | _testSimulcastSdp() async { 524 | String sdp = 525 | await new File('./test/sdp_test_data/simulcast.sdp').readAsString(); 526 | 527 | Map session = parse(sdp + ''); 528 | ok(session, 'got session info'); 529 | dynamic media = session['media']; 530 | ok(media != null && media.length > 0, 'got media'); 531 | 532 | dynamic video = media[1]; 533 | equal(video['type'], 'video', 'video type'); 534 | 535 | // test rid lines 536 | equal(video['rids'].length, 5, 'video got 5 rid lines'); 537 | // test rid 1 538 | deepEqual( 539 | video['rids'][0], 540 | { 541 | 'id': 1, 542 | 'direction': 'send', 543 | 'params': 'pt=97;max-width=1280;max-height=720;max-fps=30' 544 | }, 545 | 'video 1st rid line'); 546 | // test rid 2 547 | deepEqual(video['rids'][1], {'id': 2, 'direction': 'send', 'params': 'pt=98'}, 548 | 'video 2nd rid line'); 549 | // test rid 3 550 | deepEqual(video['rids'][2], {'id': 3, 'direction': 'send', 'params': 'pt=99'}, 551 | 'video 3rd rid line'); 552 | // test rid 4 553 | deepEqual(video['rids'][3], 554 | {'id': 4, 'direction': 'send', 'params': 'pt=100'}, 'video 4th rid line'); 555 | // test rid 5 556 | deepEqual( 557 | video['rids'][4], 558 | {'id': 'c', 'direction': 'recv', 'params': 'pt=97'}, 559 | 'video 5th rid line'); 560 | // test rid 1 params 561 | dynamic rid1Params = parseParams(video['rids'][0]['params']); 562 | deepEqual( 563 | rid1Params, 564 | {'pt': 97, 'max-width': 1280, 'max-height': 720, 'max-fps': 30}, 565 | 'video 1st rid params'); 566 | // test rid 2 params 567 | dynamic rid2Params = parseParams(video['rids'][1]['params']); 568 | deepEqual(rid2Params, {'pt': 98}, 'video 2nd rid params'); 569 | // test rid 3 params 570 | dynamic rid3Params = parseParams(video['rids'][2]['params']); 571 | deepEqual(rid3Params, {'pt': 99}, 'video 3rd rid params'); 572 | // test rid 4 params 573 | dynamic rid4Params = parseParams(video['rids'][3]['params']); 574 | deepEqual(rid4Params, {'pt': 100}, 'video 4th rid params'); 575 | // test rid 5 params 576 | dynamic rid5Params = parseParams(video['rids'][4]['params']); 577 | deepEqual(rid5Params, {'pt': 97}, 'video 5th rid params'); 578 | 579 | // test imageattr lines 580 | equal(video['imageattrs'].length, 5, 'video got 5 imageattr lines'); 581 | // test imageattr 1 582 | deepEqual( 583 | video['imageattrs'][0], 584 | { 585 | 'pt': 97, 586 | 'dir1': 'send', 587 | 'attrs1': '[x=1280,y=720]', 588 | 'dir2': 'recv', 589 | 'attrs2': '[x=1280,y=720] [x=320,y=180] [x=160,y=90]' 590 | }, 591 | 'video 1st imageattr line'); 592 | // test imageattr 2 593 | deepEqual( 594 | video['imageattrs'][1], 595 | {'pt': 98, 'dir1': 'send', 'attrs1': '[x=320,y=180]'}, 596 | 'video 2nd imageattr line'); 597 | // test imageattr 3 598 | deepEqual( 599 | video['imageattrs'][2], 600 | {'pt': 99, 'dir1': 'send', 'attrs1': '[x=160,y=90]'}, 601 | 'video 3rd imageattr line'); 602 | // test imageattr 4 603 | deepEqual( 604 | video['imageattrs'][3], 605 | { 606 | 'pt': 100, 607 | 'dir1': 'recv', 608 | 'attrs1': '[x=1280,y=720] [x=320,y=180]', 609 | 'dir2': 'send', 610 | 'attrs2': '[x=1280,y=720]' 611 | }, 612 | 'video 4th imageattr line'); 613 | // test imageattr 5 614 | deepEqual(video['imageattrs'][4], {'pt': '*', 'dir1': 'recv', 'attrs1': '*'}, 615 | 'video 5th imageattr line'); 616 | // test imageattr 1 send params 617 | dynamic imageattr1SendParams = 618 | parseImageAttributes(video['imageattrs'][0]['attrs1']); 619 | deepEqual( 620 | imageattr1SendParams, 621 | [ 622 | {'x': 1280, 'y': 720} 623 | ], 624 | 'video 1st imageattr send params'); 625 | // test imageattr 1 recv params 626 | dynamic imageattr1RecvParams = 627 | parseImageAttributes(video['imageattrs'][0]['attrs2']); 628 | deepEqual( 629 | imageattr1RecvParams, 630 | [ 631 | {'x': 1280, 'y': 720}, 632 | {'x': 320, 'y': 180}, 633 | {'x': 160, 'y': 90}, 634 | ], 635 | 'video 1st imageattr recv params'); 636 | // test imageattr 2 send params 637 | dynamic imageattr2SendParams = 638 | parseImageAttributes(video['imageattrs'][1]['attrs1']); 639 | deepEqual( 640 | imageattr2SendParams, 641 | [ 642 | {'x': 320, 'y': 180} 643 | ], 644 | 'video 2nd imageattr send params'); 645 | // test imageattr 3 send params 646 | dynamic imageattr3SendParams = 647 | parseImageAttributes(video['imageattrs'][2]['attrs1']); 648 | deepEqual( 649 | imageattr3SendParams, 650 | [ 651 | {'x': 160, 'y': 90} 652 | ], 653 | 'video 3rd imageattr send params'); 654 | // test imageattr 4 recv params 655 | dynamic imageattr4RecvParams = 656 | parseImageAttributes(video['imageattrs'][3]['attrs1']); 657 | deepEqual( 658 | imageattr4RecvParams, 659 | [ 660 | {'x': 1280, 'y': 720}, 661 | {'x': 320, 'y': 180}, 662 | ], 663 | 'video 4th imageattr recv params'); 664 | // test imageattr 4 send params 665 | dynamic imageattr4SendParams = 666 | parseImageAttributes(video['imageattrs'][3]['attrs2']); 667 | deepEqual( 668 | imageattr4SendParams, 669 | [ 670 | {'x': 1280, 'y': 720} 671 | ], 672 | 'video 4th imageattr send params'); 673 | // test imageattr 5 recv params 674 | equal( 675 | video['imageattrs'][4]['attrs1'], '*', 'video 5th imageattr recv params'); 676 | 677 | // test simulcast line 678 | deepEqual( 679 | video['simulcast'], 680 | {'dir1': 'send', 'list1': '1,~4;2;3', 'dir2': 'recv', 'list2': 'c'}, 681 | 'video simulcast line'); 682 | // test simulcast send streams 683 | dynamic simulcastSendStreams = 684 | parseSimulcastStreamList(video['simulcast']['list1']); 685 | deepEqual( 686 | simulcastSendStreams, 687 | [ 688 | [ 689 | {'scid': 1, 'paused': false}, 690 | {'scid': 4, 'paused': true} 691 | ], 692 | [ 693 | {'scid': 2, 'paused': false} 694 | ], 695 | [ 696 | {'scid': 3, 'paused': false} 697 | ] 698 | ], 699 | 'video simulcast send streams'); 700 | // test simulcast recv streams 701 | dynamic simulcastRecvStreams = 702 | parseSimulcastStreamList(video['simulcast']['list2']); 703 | deepEqual( 704 | simulcastRecvStreams, 705 | [ 706 | [ 707 | {'scid': 'c', 'paused': false} 708 | ] 709 | ], 710 | 'video simulcast recv streams'); 711 | 712 | // test simulcast version 03 line 713 | // test simulcast line 714 | deepEqual( 715 | video['simulcast_03'], 716 | {'value': 'send rid=1,4;2;3 paused=4 recv rid=c'}, 717 | 'video simulcast draft 03 line'); 718 | } 719 | 720 | _testSt2202_6Sdp() async { 721 | String sdp = 722 | await new File('./test/sdp_test_data/st2022-6.sdp').readAsString(); 723 | 724 | Map session = parse(sdp + ''); 725 | ok(session, 'got session info'); 726 | dynamic media = session['media']; 727 | ok(media != null && media.length > 0, 'got media'); 728 | 729 | dynamic video = media[0]; 730 | dynamic sourceFilter = video['sourceFilter']; 731 | equal(sourceFilter['filterMode'], 'incl', 'filter-mode is "incl"'); 732 | equal(sourceFilter['netType'], 'IN', 'nettype is "IN"'); 733 | equal(sourceFilter['addressTypes'], 'IP4', 'address-type is "IP4"'); 734 | equal( 735 | sourceFilter['destAddress'], '239.0.0.1', 'dest-address is "239.0.0.1"'); 736 | equal( 737 | sourceFilter['srcList'], '192.168.20.20', 'src-list is "192.168.20.20"'); 738 | } 739 | 740 | _testSt2110_20Sdp() async { 741 | String sdp = 742 | await new File('./test/sdp_test_data/st2110-20.sdp').readAsString(); 743 | 744 | Map session = parse(sdp + ''); 745 | ok(session, 'got session info'); 746 | dynamic media = session['media']; 747 | ok(media != null && media.length > 0, 'got media'); 748 | 749 | dynamic video = media[0]; 750 | dynamic sourceFilter = video['sourceFilter']; 751 | equal(sourceFilter['filterMode'], 'incl', 'filter-mode is "incl"'); 752 | equal(sourceFilter['netType'], 'IN', 'nettype is "IN"'); 753 | equal(sourceFilter['addressTypes'], 'IP4', 'address-type is "IP4"'); 754 | equal(sourceFilter['destAddress'], '239.100.9.10', 755 | 'dest-address is "239.100.9.10"'); 756 | equal( 757 | sourceFilter['srcList'], '192.168.100.2', 'src-list is "192.168.100.2"'); 758 | 759 | equal(video['type'], 'video', 'video type'); 760 | dynamic fmtp0Params = parseParams(video['fmtp'][0]['config']); 761 | deepEqual( 762 | fmtp0Params, 763 | { 764 | 'sampling': 'YCbCr-4:2:2', 765 | 'width': 1280, 766 | 'height': 720, 767 | 'interlace': null, 768 | 'exactframerate': '60000/1001', 769 | 'depth': 10, 770 | 'TCS': 'SDR', 771 | 'colorimetry': 'BT709', 772 | 'PM': '2110GPM', 773 | 'SSN': 'ST2110-20:2017' 774 | }, 775 | 'video 5th rid params'); 776 | } 777 | 778 | _testSctpDtls26Sdp() async { 779 | String sdp = 780 | await new File('./test/sdp_test_data/sctp-dtls-26.sdp').readAsString(); 781 | 782 | Map session = parse(sdp + ''); 783 | ok(session, 'got session info'); 784 | dynamic media = session['media']; 785 | ok(media != null && media.length > 0, 'got media'); 786 | 787 | equal( 788 | session['origin']['sessionId'], 5636137646675714991, 'origin sessionId'); 789 | ok(session['groups'], 'parsing session groups'); 790 | equal(session['groups'].length, 1, 'one grouping'); 791 | equal(session['groups'][0]['type'], 'BUNDLE', 'grouping is BUNDLE'); 792 | equal(session['groups'][0]['mids'], 'data', 'bundling data'); 793 | ok(session['msidSemantic'], 'have an msid semantic'); 794 | equal(session['msidSemantic']['semantic'], 'WMS', 'webrtc semantic'); 795 | 796 | // verify media is data application 797 | equal(media[0]['type'], 'application', 'media type application'); 798 | equal(media[0]['mid'], 'data', 'media id pplication'); 799 | 800 | // verify protocol and ports 801 | equal(media[0]['protocol'], 'UDP/DTLS/SCTP', 'protocol is UDP/DTLS/SCTP'); 802 | equal(media[0]['port'], 9, 'the UDP port value is 9'); 803 | equal( 804 | media[0]['sctpPort'], 5000, 'the offerer/answer SCTP port value is 5000'); 805 | 806 | // verify maxMessageSize 807 | equal(media[0]['maxMessageSize'], 10000, 'maximum message size is 10000'); 808 | } 809 | 810 | _testExtmapEncryptSdp() async { 811 | String sdp = 812 | await new File('./test/sdp_test_data/extmap-encrypt.sdp').readAsString(); 813 | 814 | Map session = parse(sdp + ''); 815 | ok(session, 'got session info'); 816 | dynamic media = session['media']; 817 | ok(media != null && media.length > 0, 'got media'); 818 | 819 | equal(session['origin']['username'], '-', 'origin username'); 820 | equal(session['origin']['sessionId'], 20518, 'origin sessionId'); 821 | equal(session['origin']['sessionVersion'], 0, 'origin sessionVersion'); 822 | equal(session['origin']['netType'], 'IN', 'origin netType'); 823 | equal(session['origin']['ipVer'], 4, 'origin ipVer'); 824 | equal(session['origin']['address'], '203.0.113.1', 'origin address'); 825 | 826 | equal(session['connection']['ip'], '203.0.113.1', 'session connect ip'); 827 | equal(session['connection']['version'], 4, 'session connect ip ver'); 828 | 829 | dynamic audio = media[0]; 830 | equal(audio['type'], 'audio', 'audio type'); 831 | equal(audio['port'], 54400, 'audio port'); 832 | equal(audio['protocol'], 'RTP/SAVPF', 'audio protocol'); 833 | equal(audio['rtp'][0]['payload'], 96, 'audio rtp 0 payload'); 834 | equal(audio['rtp'][0]['codec'], 'opus', 'audio rtp 0 codec'); 835 | equal(audio['rtp'][0]['rate'], 48000, 'audio rtp 0 rate'); 836 | 837 | // extmap and encrypted extmap 838 | deepEqual( 839 | audio['ext'][0], 840 | {'value': 1, 'direction': 'sendonly', 'uri': 'URI-toffset'}, 841 | 'audio extension 0'); 842 | deepEqual( 843 | audio['ext'][1], 844 | {'value': 2, 'uri': 'urn:ietf:params:rtp-hdrext:toffset'}, 845 | 'audio extension 1'); 846 | deepEqual( 847 | audio['ext'][2], 848 | { 849 | 'value': 3, 850 | 'encrypt-uri': 'urn:ietf:params:rtp-hdrext:encrypt', 851 | 'uri': 'urn:ietf:params:rtp-hdrext:smpte-tc', 852 | 'config': '25@600/24' 853 | }, 854 | 'audio extension 2'); 855 | deepEqual( 856 | audio['ext'][3], 857 | { 858 | 'value': 4, 859 | 'direction': 'recvonly', 860 | 'encrypt-uri': 'urn:ietf:params:rtp-hdrext:encrypt', 861 | 'uri': 'URI-gps-string' 862 | }, 863 | 'audio extension 3'); 864 | 865 | equal(media.length, 1, 'got 1 m-lines'); 866 | } 867 | 868 | _testDanteAes67() async { 869 | String sdp = 870 | await new File('./test/sdp_test_data/dante-aes67.sdp').readAsString(); 871 | 872 | Map session = parse(sdp + ''); 873 | ok(session, 'got session info'); 874 | dynamic media = session['media']; 875 | ok(media != null && media.length == 1, 'got single media'); 876 | 877 | equal(session['origin']['username'], '-', 'origin username'); 878 | equal(session['origin']['sessionId'], 1423986, 'origin sessionId'); 879 | equal(session['origin']['sessionVersion'], 1423994, 'origin sessionVersion'); 880 | equal(session['origin']['netType'], 'IN', 'origin netType'); 881 | equal(session['origin']['ipVer'], 4, 'origin ipVer'); 882 | equal(session['origin']['address'], '169.254.98.63', 'origin address'); 883 | 884 | equal(session['name'], 'AOIP44-serial-1614 : 2', 'Session Name'); 885 | //equal(session['keywords'], 'Dante', 'Keywords'); 886 | 887 | equal(session['connection']['ip'], '239.65.125.63/32', 'session connect ip'); 888 | equal(session['connection']['version'], 4, 'session connect ip ver'); 889 | 890 | dynamic audio = media[0]; 891 | equal(audio['type'], 'audio', 'audio type'); 892 | equal(audio['port'], 5004, 'audio port'); 893 | equal(audio['protocol'], 'RTP/AVP', 'audio protocol'); 894 | equal(audio['direction'], 'recvonly', 'audio direction'); 895 | equal(audio['description'], '2 channels: TxChan 0, TxChan 1', 896 | 'audio description'); 897 | equal(audio['ptime'], 1, 'audio packet duration'); 898 | equal(audio['rtp'][0]['payload'], 97, 'audio rtp payload type'); 899 | equal(audio['rtp'][0]['codec'], 'L24', 'audio rtp codec'); 900 | equal(audio['rtp'][0]['rate'], 48000, 'audio sample rate'); 901 | equal(audio['rtp'][0]['encoding'], 2, 'audio channels'); 902 | } 903 | 904 | _testTcpActive() async { 905 | String sdp = 906 | await new File('./test/sdp_test_data/tcp-active.sdp').readAsString(); 907 | 908 | Map session = parse(sdp + ''); 909 | ok(session, 'got session info'); 910 | dynamic media = session['media']; 911 | ok(media != null && media.length == 1, 'got single media'); 912 | 913 | equal(session['origin']['username'], '-', 'origin username'); 914 | equal(session['origin']['sessionId'], 1562876543, 'origin sessionId'); 915 | equal(session['origin']['sessionVersion'], 11, 'origin sessionVersion'); 916 | equal(session['origin']['netType'], 'IN', 'origin netType'); 917 | equal(session['origin']['ipVer'], 4, 'origin ipVer'); 918 | equal(session['origin']['address'], '192.0.2.3', 'origin address'); 919 | 920 | dynamic image = media[0]; 921 | equal(image['type'], 'image', 'image type'); 922 | equal(image['port'], 9, 'port'); 923 | equal(image['connection']['version'], 4, 'Connection is IPv4'); 924 | equal(image['connection']['ip'], '192.0.2.3', 'Connection address'); 925 | equal(image['protocol'], 'TCP', 'TCP protocol'); 926 | equal(image['payloads'], 't38', 'TCP payload'); 927 | equal(image['setup'], 'active', 'setup active'); 928 | //equal(image['connectionType'], 'new', 'new connection'); 929 | } 930 | 931 | _testTcpPassive() async { 932 | String sdp = 933 | await new File('./test/sdp_test_data/tcp-passive.sdp').readAsString(); 934 | 935 | Map session = parse(sdp + ''); 936 | ok(session, 'got session info'); 937 | dynamic media = session['media']; 938 | ok(media != null && media.length == 1, 'got single media'); 939 | 940 | equal(session['origin']['username'], '-', 'origin username'); 941 | equal(session['origin']['sessionId'], 1562876543, 'origin sessionId'); 942 | equal(session['origin']['sessionVersion'], 11, 'origin sessionVersion'); 943 | equal(session['origin']['netType'], 'IN', 'origin netType'); 944 | equal(session['origin']['ipVer'], 4, 'origin ipVer'); 945 | equal(session['origin']['address'], '192.0.2.2', 'origin address'); 946 | 947 | dynamic image = media[0]; 948 | equal(image['type'], 'image', 'image type'); 949 | equal(image['port'], 54111, 'port'); 950 | equal(image['connection']['version'], 4, 'Connection is IPv4'); 951 | equal(image['connection']['ip'], '192.0.2.2', 'Connection address'); 952 | equal(image['protocol'], 'TCP', 'TCP protocol'); 953 | equal(image['payloads'], 't38', 'TCP payload'); 954 | equal(image['setup'], 'passive', 'setup passive'); 955 | // equal(image['connectionType'], 'existing', 'existing connection'); 956 | } 957 | -------------------------------------------------------------------------------- /test/sdp_test_data/alac.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=iTunes 3413821438 0 IN IP4 fe80::217:f2ff:fe0f:e0f6 3 | s=iTunes 4 | c=IN IP4 fe80::5a55:caff:fe1a:e187 5 | t=0 0 6 | m=audio 0 RTP/AVP 96 7 | a=rtpmap:96 AppleLossless 8 | a=fmtp:96 352 0 16 40 10 14 2 255 0 0 44100 9 | a=fpaeskey:RlBMWQECAQAAAAA8AAAAAPFOnNe+zWb5/n4L5KZkE2AAAAAQlDx69reTdwHF9LaNmhiRURTAbcL4brYAceAkZ49YirXm62N4 10 | a=aesiv:5b+YZi9Ikb845BmNhaVo+Q 11 | -------------------------------------------------------------------------------- /test/sdp_test_data/asterisk.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 118690628 118690628 IN IP4 10.10.10.10 3 | s=Asterisk 4 | c=IN IP4 10.10.10.10 5 | t=0 0 6 | a=msid-semantic:WMS * 7 | a=group:BUNDLE audio-0 8 | m=audio 11414 UDP/TLS/RTP/SAVPF 8 18 101 9 | a=connection:new 10 | a=setup:actpass 11 | a=fingerprint:SHA-256 98:1B:4C:A8:D2:48:0B:92:2F:5B:BF:DA:29:C1:EE:59:B7:4B:28:E7:89:47:31:FA:27:1F:06:01:1B:8C:4D:7B 12 | a=ice-ufrag:7dac32e54888b96e16c4139647caba54 13 | a=ice-pwd:307928eb41a2bde2294b5d0010397746 14 | a=candidate:Ha00000e 1 UDP 2130706431 10.10.10.10 11414 typ host 15 | a=candidate:H671d4072 1 UDP 2130706431 10.10.10.10 11414 typ host 16 | a=candidate:Hac110001 1 UDP 2130706431 10.10.10.10 11414 typ host 17 | a=rtpmap:8 PCMA/8000 18 | a=rtpmap:18 G729/8000 19 | a=fmtp:18 annexb=no 20 | a=rtpmap:101 telephone-event/8000 21 | a=fmtp:101 0-16 22 | a=ptime:20 23 | a=maxptime:150 24 | a=sendrecv 25 | a=rtcp-mux 26 | a=ssrc:1241161643 cname:16d280ee-5d6e-4458-b10d-59aed9c7bc22 27 | a=msid:a25d8ad4-0e42-41ef-aed5-1128713460c7 a77a9723-c5a3-449f-ad80-de91946498c0 28 | a=rtcp-fb:* transport-cc 29 | a=mid:audio-0 30 | -------------------------------------------------------------------------------- /test/sdp_test_data/dante-aes67.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 1423986 1423994 IN IP4 169.254.98.63 3 | s=AOIP44-serial-1614 : 2 4 | c=IN IP4 239.65.125.63/32 5 | t=0 0 6 | a=keywds:Dante 7 | m=audio 5004 RTP/AVP 97 8 | i=2 channels: TxChan 0, TxChan 1 9 | a=recvonly 10 | a=rtpmap:97 L24/48000/2 11 | a=ptime:1 -------------------------------------------------------------------------------- /test/sdp_test_data/extmap-encrypt.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 20518 0 IN IP4 203.0.113.1 3 | s= 4 | t=0 0 5 | c=IN IP4 203.0.113.1 6 | m=audio 54400 RTP/SAVPF 96 7 | a=rtpmap:96 opus/48000 8 | a=extmap:1/sendonly URI-toffset 9 | a=extmap:2 urn:ietf:params:rtp-hdrext:toffset 10 | a=extmap:3 urn:ietf:params:rtp-hdrext:encrypt urn:ietf:params:rtp-hdrext:smpte-tc 25@600/24 11 | a=extmap:4/recvonly urn:ietf:params:rtp-hdrext:encrypt URI-gps-string -------------------------------------------------------------------------------- /test/sdp_test_data/hacky.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 3710604898417546434 2 IN IP4 127.0.0.1 3 | s=- 4 | t=0 0 5 | a=group:BUNDLE audio video 6 | a=msid-semantic: WMS Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV 7 | m=audio 1 RTP/SAVPF 111 103 104 0 8 107 106 105 13 126 8 | c=IN IP4 0.0.0.0 9 | a=rtcp:1 IN IP4 0.0.0.0 10 | a=candidate:1127303604 1 udp 2122260223 0.0.0.0 60672 typ host generation 0 11 | a=candidate:229815620 1 tcp 1518280447 0.0.0.0 0 typ host tcptype active generation 0 12 | a=candidate:1 1 TCP 2128609279 10.0.1.1 9 typ host tcptype active 13 | a=candidate:2 1 TCP 2124414975 10.0.1.1 8998 typ host tcptype passive 14 | a=candidate:3 1 TCP 2120220671 10.0.1.1 8999 typ host tcptype so 15 | a=candidate:4 1 TCP 1688207359 192.0.2.3 9 typ srflx raddr 10.0.1.1 rport 9 tcptype active 16 | a=candidate:5 1 TCP 1684013055 192.0.2.3 45664 typ srflx raddr 10.0.1.1 rport 8998 tcptype passive generation 5 17 | a=candidate:6 1 TCP 1692401663 192.0.2.3 45687 typ srflx raddr 10.0.1.1 rport 8999 tcptype so 18 | a=ice-ufrag:lat6xwB1/flm+VwG 19 | a=ice-pwd:L5+HonleGeFHa8jPZLc/kr0E 20 | a=ice-options:google-ice 21 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 22 | a=sendrecv 23 | a=mid:audio 24 | a=rtcp-mux 25 | a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:8QVQSHJ2AM8gIumHpYRRdWHyZ5NkLhaTD1AENOWx 26 | a=rtpmap:111 opus/48000/2 27 | a=fmtp:111 minptime=10 28 | a=rtpmap:103 ISAC/16000 29 | a=rtpmap:104 ISAC/32000 30 | a=rtpmap:0 PCMU/8000 31 | a=rtpmap:8 PCMA/8000 32 | a=rtpmap:107 CN/48000 33 | a=rtpmap:106 CN/32000 34 | a=rtpmap:105 CN/16000 35 | a=rtpmap:13 CN/8000 36 | a=rtpmap:126 telephone-event/8000 37 | a=maxptime:60 38 | a=ssrc:2754920552 cname:t9YU8M1UxTF8Y1A1 39 | a=ssrc:2754920552 msid:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVa0 40 | a=ssrc:2754920552 mslabel:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV 41 | a=ssrc:2754920552 label:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVa0 42 | a=label:1 43 | m=video 1 RTP/SAVPF 100 116 117 44 | c=IN IP4 0.0.0.0 45 | a=rtcp:12312 46 | a=ice-ufrag:lat6xwB1/flm+VwG 47 | a=ice-pwd:L5+HonleGeFHa8jPZLc/kr0E 48 | a=ice-options:google-ice 49 | a=extmap:2 urn:ietf:params:rtp-hdrext:toffset 50 | a=sendrecv 51 | a=mid:video 52 | a=rtcp-mux 53 | a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:8QVQSHJ2AM8gIumHpYRRdWHyZ5NkLhaTD1AENOWx 54 | a=rtpmap:100 VP8/90000 55 | a=rtcp-fb:100 ccm fir 56 | a=rtcp-fb:100 nack 57 | a=rtcp-fb:100 goog-remb 58 | a=rtpmap:116 red/90000 59 | a=rtpmap:117 ulpfec/90000 60 | a=ssrc:2566107569 cname:t9YU8M1UxTF8Y1A1 61 | a=ssrc:2566107569 msid:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVv0 62 | a=ssrc:2566107569 mslabel:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlV 63 | a=ssrc:2566107569 label:Jvlam5X3SX1OP6pn20zWogvaKJz5Hjf9OnlVv0 64 | m=application 9 DTLS/SCTP 5000 65 | c=IN IP4 0.0.0.0 66 | b=AS:30 67 | a=framerate:29.97 68 | a=ice-ufrag:pDUB98Lc+2dc5+JF 69 | a=ice-pwd:G/CIMBOa9RQINDL4Y8NjpotH 70 | a=fingerprint:sha-256 F0:37:78:FE:3D:13:E9:10:B5:0C:4C:9E:48:37:E7:A0:F8:16:DC:1A:2C:69:67:B0:DF:E6:CB:73:F8:EF:BA:02 71 | a=setup:active 72 | a=mid:33db2c4da91d73fd 73 | a=sctpmap:5000 webrtc-datachannel 1024 74 | -------------------------------------------------------------------------------- /test/sdp_test_data/icelite.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 3622532974 3622532974 IN IP4 192.168.100.100 3 | s=- 4 | c=IN IP4 192.168.100.100 5 | t=0 0 6 | a=ice-lite 7 | m=audio 10018 RTP/SAVPF 8 0 101 8 | a=rtpmap:8 PCMA/8000 9 | a=rtpmap:0 PCMU/8000 10 | a=rtpmap:101 telephone-event/8000 11 | a=fmtp:101 0-15 12 | a=direction:both 13 | a=sendrecv 14 | a=rtcp-mux 15 | a=setup:actpass 16 | a=fingerprint:sha-256 CE:17:02:86:E2:E8:B0:EF:F9:F3:3F:82:8A:A6:F0:EF:30:73:1D:5D:B3:5A:60:D7:AC:FE:F0:E3:DF:D5:D9:7B 17 | a=ice-ufrag:nXET 18 | a=ice-pwd:d0iwx/Qam8JnuvL+wkcXee 19 | a=candidate:X 1 UDP 659136 192.168.100.100 10018 typ host 20 | a=candidate:X 2 UDP 659134 192.168.100.100 10019 typ host -------------------------------------------------------------------------------- /test/sdp_test_data/invalid.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 3710604898417546434 2 IN IP4 127.0.0.1 3 | s=- 4 | t=0 0 5 | m=audio 1 RTP/AVP 0 6 | c=IN IP4 0.0.0.0 7 | a=rtcp:1 IN IP7 X 8 | a=rtpmap:0 PCMU/8000 9 | a=goo:hithere 10 | f=invalid:yes 11 | -------------------------------------------------------------------------------- /test/sdp_test_data/jsep.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4962303333179871722 1 IN IP4 0.0.0.0 3 | s=- 4 | t=0 0 5 | a=msid-semantic:WMS 6 | a=group:BUNDLE a1 v1 7 | m=audio 56500 UDP/TLS/RTP/SAVPF 96 0 8 97 98 8 | c=IN IP4 192.0.2.1 9 | a=mid:a1 10 | a=rtcp:56501 IN IP4 192.0.2.1 11 | a=msid:47017fee-b6c1-4162-929c-a25110252400 f83006c5-a0ff-4e0a-9ed9-d3e6747be7d9 12 | a=sendrecv 13 | a=rtpmap:96 opus/48000/2 14 | a=rtpmap:0 PCMU/8000 15 | a=rtpmap:8 PCMA/8000 16 | a=rtpmap:97 telephone-event/8000 17 | a=rtpmap:98 telephone-event/48000 18 | a=maxptime:120 19 | a=ice-ufrag:ETEn1v9DoTMB9J4r 20 | a=ice-pwd:OtSK0WpNtpUjkY4+86js7ZQl 21 | a=ice-options:trickle 22 | a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2 23 | a=setup:actpass 24 | a=rtcp-mux 25 | a=rtcp-rsize 26 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 27 | a=extmap:2 urn:ietf:params:rtp-hdrext:sdes:mid 28 | a=ssrc:1732846380 cname:EocUG1f0fcg/yvY7 29 | a=candidate:3348148302 1 udp 2113937151 192.0.2.1 56500 typ host 30 | a=candidate:3348148302 2 udp 2113937151 192.0.2.1 56501 typ host 31 | a=end-of-candidates 32 | m=video 0 UDP/TLS/RTP/SAVPF 100 101 33 | c=IN IP4 192.0.2.1 34 | a=rtcp:56503 IN IP4 192.0.2.1 35 | a=mid:v1 36 | a=bundle-only 37 | a=msid:61317484-2ed4-49d7-9eb7-1414322a7aae f30bdb4a-5db8-49b5-bcdc-e0c9a23172e0 38 | a=sendrecv 39 | a=rtpmap:100 VP8/90000 40 | a=rtpmap:101 rtx/90000 41 | a=fmtp:101 apt=100 42 | a=ice-ufrag:BGKkWnG5GmiUpdIV 43 | a=ice-pwd:mqyWsAjvtKwTGnvhPztQ9mIf 44 | a=ice-options:trickle 45 | a=fingerprint:sha-256 19:E2:1C:3B:4B:9F:81:E6:B8:5C:F4:A5:A8:D8:73:04:BB:05:2F:70:9F:04:A9:0E:05:E9:26:33:E8:70:88:A2 46 | a=setup:actpass 47 | a=rtcp-mux 48 | a=rtcp-rsize 49 | a=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid 50 | a=rtcp-fb:100 ccm fir 51 | a=rtcp-fb:100 nack 52 | a=rtcp-fb:100 nack pli 53 | a=ssrc:1366781083 cname:EocUG1f0fcg/yvY7 54 | a=ssrc:1366781084 cname:EocUG1f0fcg/yvY7 55 | a=ssrc-group:FID 1366781083 1366781084 56 | a=end-of-candidates 57 | -------------------------------------------------------------------------------- /test/sdp_test_data/jssip.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 1334496563563564720 2 IN IP4 127.0.0.1 3 | s=- 4 | t=0 0 5 | a=group:BUNDLE audio 6 | a=msid-semantic: WMS KOaPIn6F0Qm9PuOA6WHfjdfqWMt9sGl6uOqg 7 | m=audio 60017 RTP/SAVPF 111 103 104 0 8 106 105 13 126 8 | c=IN IP4 193.84.77.194 9 | a=rtcp:60017 IN IP4 193.84.77.194 10 | a=candidate:1162875081 1 udp 2113937151 192.168.34.75 60017 typ host generation 0 11 | a=candidate:1162875081 2 udp 2113937151 192.168.34.75 60017 typ host generation 0 12 | a=candidate:3289912957 1 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 13 | a=candidate:3289912957 2 udp 1845501695 193.84.77.194 60017 typ srflx raddr 192.168.34.75 rport 60017 generation 0 14 | a=candidate:198437945 1 tcp 1509957375 192.168.34.75 0 typ host generation 0 15 | a=candidate:198437945 2 tcp 1509957375 192.168.34.75 0 typ host generation 0 16 | a=ice-ufrag:5I2uVefP13X1wzOY 17 | a=ice-pwd:e46UjXntt0K/xTncQcDBQePn 18 | a=ice-options:google-ice 19 | a=fingerprint:sha-256 79:14:AB:AB:93:7F:07:E8:91:1A:11:16:36:D0:11:66:C4:4F:31:A0:74:46:65:58:70:E5:09:95:48:F4:4B:D9 20 | a=setup:actpass 21 | a=mid:audio 22 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 23 | a=sendrecv 24 | a=rtcp-mux 25 | a=crypto:0 AES_CM_128_HMAC_SHA1_32 inline:6JYKxLF+o2nhouDHr5J0oNb3CEGK3I/HHv9idGTY 26 | a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:ayId2M5kCitGTEEI9OjgEqatTA0IXGpQhFjmKOGk 27 | a=rtpmap:111 opus/48000/2 28 | a=fmtp:111 minptime=10 29 | a=rtpmap:103 ISAC/16000 30 | a=rtpmap:104 ISAC/32000 31 | a=rtpmap:0 PCMU/8000 32 | a=rtpmap:8 PCMA/8000 33 | a=rtpmap:106 CN/32000 34 | a=rtpmap:105 CN/16000 35 | a=rtpmap:13 CN/8000 36 | a=rtpmap:126 telephone-event/8000 37 | a=maxptime:60 38 | a=ssrc:1399694169 cname:w7AkLB30C7pk/PFE 39 | a=ssrc:1399694169 msid:KOaPIn6F0Qm9PuOA6WHfjdfqWMt9sGl6uOqg 775dca64-4698-455b-8a02-89833bd24773 40 | a=ssrc:1399694169 mslabel:KOaPIn6F0Qm9PuOA6WHfjdfqWMt9sGl6uOqg 41 | a=ssrc:1399694169 label:775dca64-4698-455b-8a02-89833bd24773 42 | -------------------------------------------------------------------------------- /test/sdp_test_data/normal.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 20518 0 IN IP4 203.0.113.1 3 | s= 4 | t=0 0 5 | c=IN IP4 203.0.113.1 6 | a=ice-ufrag:F7gI 7 | a=ice-pwd:x9cml/YzichV2+XlhiMu8g 8 | a=fingerprint:sha-1 42:89:c5:c6:55:9d:6e:c8:e8:83:55:2a:39:f9:b6:eb:e9:a3:a9:e7 9 | m=audio 54400 RTP/SAVPF 0 96 10 | a=rtpmap:0 PCMU/8000 11 | a=rtpmap:96 opus/48000 12 | a=extmap:1 URI-toffset 13 | a=extmap:2/recvonly URI-gps-string 14 | a=extmap-allow-mixed 15 | a=ptime:20 16 | a=sendrecv 17 | a=candidate:0 1 UDP 2113667327 203.0.113.1 54400 typ host 18 | a=candidate:1 2 UDP 2113667326 203.0.113.1 54401 typ host 19 | a=candidate:2 1 UDP 1686052607 203.0.113.1 54402 typ srflx raddr 192.168.1.145 rport 54402 generation 0 network-id 3 network-cost 10 20 | a=candidate:3 2 UDP 1686052606 203.0.113.1 54403 typ srflx raddr 192.168.1.145 rport 54403 generation 0 network-id 3 network-cost 10 21 | m=video 55400 RTP/SAVPF 97 98 22 | a=rtpmap:97 H264/90000 23 | a=fmtp:97 profile-level-id=4d0028;packetization-mode=1;sprop-parameter-sets=Z0IAH5WoFAFuQA==,aM48gA== 24 | a=fmtp:98 minptime=10; useinbandfec=1 25 | a=rtpmap:98 VP8/90000 26 | a=rtcp-fb:* nack 27 | a=rtcp-fb:98 nack rpsi 28 | a=rtcp-fb:98 trr-int 100 29 | a=crypto:1 AES_CM_128_HMAC_SHA1_32 inline:keNcG3HezSNID7LmfDa9J4lfdUL8W1F7TNJKcbuy|2^20|1:32 30 | a=sendrecv 31 | a=candidate:0 1 UDP 2113667327 203.0.113.1 55400 typ host 32 | a=candidate:1 2 UDP 2113667326 203.0.113.1 55401 typ host 33 | a=candidate:2 1 UDP 1686052607 203.0.113.1 55402 typ srflx raddr 192.168.1.145 rport 55402 generation 0 network-id 3 34 | a=candidate:3 2 UDP 1686052606 203.0.113.1 55403 typ srflx raddr 192.168.1.145 rport 55403 generation 0 network-id 3 35 | a=ssrc:1399694169 foo:bar 36 | a=ssrc:1399694169 baz 37 | a=ssrc:1399694169 foo-bar:baz -------------------------------------------------------------------------------- /test/sdp_test_data/sctp-dtls-26.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 5636137646675714991 2 IN IP4 127.0.0.1 3 | s=- 4 | t=0 0 5 | a=group:BUNDLE data 6 | a=msid-semantic: WMS 7 | m=application 9 UDP/DTLS/SCTP webrtc-datachannel 8 | c=IN IP4 0.0.0.0 9 | a=sctp-port:5000 10 | a=ice-ufrag:8qF7 11 | a=ice-pwd:zjQd1U0/CufgXINHcPcdd0Bd 12 | a=ice-options:trickle 13 | a=fingerprint:sha-256 10:8E:F5:D7:A2:B3:63:EF:BD:64:8C:5F:56:A0:66:05:9F:B1:5C:1A:C5:79:BD:EE:90:92:C4:1A:C4:B7:1F:58 14 | a=setup:actpass 15 | a=mid:data 16 | a=max-message-size:10000 -------------------------------------------------------------------------------- /test/sdp_test_data/simulcast.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=alice 2362969037 2362969040 IN IP4 192.0.2.156 3 | s=Simulcast Enabled Client 4 | t=0 0 5 | c=IN IP4 192.0.2.156 6 | m=audio 49200 RTP/AVP 0 7 | a=rtpmap:0 PCMU/8000 8 | m=video 49300 RTP/AVP 97 98 99 100 9 | a=rtpmap:97 H264/90000 10 | a=rtpmap:98 H264/90000 11 | a=rtpmap:99 H264/90000 12 | a=rtpmap:100 VP8/90000 13 | a=fmtp:97 profile-level-id=42c01f; max-fs=3600; max-mbps=108000 14 | a=fmtp:98 profile-level-id=42c00b; max-fs=240; max-mbps=3600 15 | a=fmtp:99 profile-level-id=42c00b; max-fs=120; max-mbps=1800 16 | a=extmap:1 urn:ietf:params:rtp-hdrext:sdes:RtpStreamId 17 | a=imageattr:97 send [x=1280,y=720] recv [x=1280,y=720] [x=320,y=180] [x=160,y=90] 18 | a=imageattr:98 send [x=320,y=180] 19 | a=imageattr:99 send [x=160,y=90] 20 | a=imageattr:100 recv [x=1280,y=720] [x=320,y=180] send [x=1280,y=720] 21 | a=imageattr:* recv * 22 | a=rid:1 send pt=97;max-width=1280;max-height=720;max-fps=30 23 | a=rid:2 send pt=98 24 | a=rid:3 send pt=99 25 | a=rid:4 send pt=100 26 | a=rid:c recv pt=97 27 | a=simulcast:send 1,~4;2;3 recv c 28 | a=simulcast: send rid=1,4;2;3 paused=4 recv rid=c 29 | -------------------------------------------------------------------------------- /test/sdp_test_data/ssrc.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4327261771880257373 2 IN IP4 127.0.0.1 3 | s=- 4 | t=0 0 5 | a=group:BUNDLE audio video 6 | a=msid-semantic: WMS xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj 7 | m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126 8 | c=IN IP4 0.0.0.0 9 | a=rtcp:9 IN IP4 0.0.0.0 10 | a=ice-ufrag:ez5G 11 | a=ice-pwd:1F1qS++jzWLSQi0qQDZkX/QV 12 | a=fingerprint:sha-256 D2:FA:0E:C3:22:59:5E:14:95:69:92:3D:13:B4:84:24:2C:C2:A2:C0:3E:FD:34:8E:5E:EA:6F:AF:52:CE:E6:0F 13 | a=setup:actpass 14 | a=mid:audio 15 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 16 | a=sendrecv 17 | a=rtcp-mux 18 | a=rtpmap:111 opus/48000/2 19 | a=rtcp-fb:111 transport-cc 20 | a=fmtp:111 minptime=10;useinbandfec=1 21 | a=rtpmap:103 ISAC/16000 22 | a=rtpmap:104 ISAC/32000 23 | a=rtpmap:9 G722/8000 24 | a=rtpmap:0 PCMU/8000 25 | a=rtpmap:8 PCMA/8000 26 | a=rtpmap:106 CN/32000 27 | a=rtpmap:105 CN/16000 28 | a=rtpmap:13 CN/8000 29 | a=rtpmap:110 telephone-event/48000 30 | a=rtpmap:112 telephone-event/32000 31 | a=rtpmap:113 telephone-event/16000 32 | a=rtpmap:126 telephone-event/8000 33 | a=ssrc:3510681183 cname:loqPWNg7JMmrFUnr 34 | a=ssrc:3510681183 msid:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj 7ea47500-22eb-4815-a899-c74ef321b6ee 35 | a=ssrc:3510681183 mslabel:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj 36 | a=ssrc:3510681183 label:7ea47500-22eb-4815-a899-c74ef321b6ee 37 | m=video 9 UDP/TLS/RTP/SAVPF 96 98 100 102 127 125 97 99 101 124 38 | c=IN IP4 0.0.0.0 39 | a=rtcp:9 IN IP4 0.0.0.0 40 | a=ice-ufrag:ez5G 41 | a=ice-pwd:1F1qS++jzWLSQi0qQDZkX/QV 42 | a=fingerprint:sha-256 D2:FA:0E:C3:22:59:5E:14:95:69:92:3D:13:B4:84:24:2C:C2:A2:C0:3E:FD:34:8E:5E:EA:6F:AF:52:CE:E6:0F 43 | a=setup:actpass 44 | a=mid:video 45 | a=extmap:2 urn:ietf:params:rtp-hdrext:toffset 46 | a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time 47 | a=extmap:4 urn:3gpp:video-orientation 48 | a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 49 | a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay 50 | a=sendrecv 51 | a=rtcp-mux 52 | a=rtcp-rsize 53 | a=rtpmap:96 VP8/90000 54 | a=rtcp-fb:96 ccm fir 55 | a=rtcp-fb:96 nack 56 | a=rtcp-fb:96 nack pli 57 | a=rtcp-fb:96 goog-remb 58 | a=rtcp-fb:96 transport-cc 59 | a=rtpmap:98 VP9/90000 60 | a=rtcp-fb:98 ccm fir 61 | a=rtcp-fb:98 nack 62 | a=rtcp-fb:98 nack pli 63 | a=rtcp-fb:98 goog-remb 64 | a=rtcp-fb:98 transport-cc 65 | a=rtpmap:100 H264/90000 66 | a=rtcp-fb:100 ccm fir 67 | a=rtcp-fb:100 nack 68 | a=rtcp-fb:100 nack pli 69 | a=rtcp-fb:100 goog-remb 70 | a=rtcp-fb:100 transport-cc 71 | a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f 72 | a=rtpmap:102 red/90000 73 | a=rtpmap:127 ulpfec/90000 74 | a=rtpmap:125 flexfec-03/90000 75 | a=rtcp-fb:125 ccm fir 76 | a=rtcp-fb:125 nack 77 | a=rtcp-fb:125 nack pli 78 | a=rtcp-fb:125 goog-remb 79 | a=rtcp-fb:125 transport-cc 80 | a=fmtp:125 repair-window=10000000 81 | a=rtpmap:97 rtx/90000 82 | a=fmtp:97 apt=96 83 | a=rtpmap:99 rtx/90000 84 | a=fmtp:99 apt=98 85 | a=rtpmap:101 rtx/90000 86 | a=fmtp:101 apt=100 87 | a=rtpmap:124 rtx/90000 88 | a=fmtp:124 apt=102 89 | a=ssrc-group:FID 3004364195 1126032854 90 | a=ssrc-group:FEC-FR 3004364195 1080772241 91 | a=ssrc:3004364195 cname:loqPWNg7JMmrFUnr 92 | a=ssrc:3004364195 msid:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj cf093ab0-0b28-4930-8fe1-7ca8d529be25 93 | a=ssrc:3004364195 mslabel:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj 94 | a=ssrc:3004364195 label:cf093ab0-0b28-4930-8fe1-7ca8d529be25 95 | a=ssrc:1126032854 cname:loqPWNg7JMmrFUnr 96 | a=ssrc:1126032854 msid:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj cf093ab0-0b28-4930-8fe1-7ca8d529be25 97 | a=ssrc:1126032854 mslabel:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj 98 | a=ssrc:1126032854 label:cf093ab0-0b28-4930-8fe1-7ca8d529be25 99 | a=ssrc:1080772241 cname:loqPWNg7JMmrFUnr 100 | a=ssrc:1080772241 msid:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj cf093ab0-0b28-4930-8fe1-7ca8d529be25 101 | a=ssrc:1080772241 mslabel:xIKmAwWv4ft4ULxNJGhkHzvPaCkc8EKo4SGj 102 | a=ssrc:1080772241 label:cf093ab0-0b28-4930-8fe1-7ca8d529be25 103 | -------------------------------------------------------------------------------- /test/sdp_test_data/st2022-6.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 198403 11 IN IP4 192.168.20.20 3 | s=st2022-6 source 4 | t=0 0 5 | m=video 2004 RTP/AVP 98 6 | c=IN IP4 239.0.0.1/32 7 | a=rtpmap:98 SMPTE2022-6/27000000 8 | a=source-filter: incl IN IP4 239.0.0.1 192.168.20.20 9 | -------------------------------------------------------------------------------- /test/sdp_test_data/st2110-20.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 123456 11 IN IP4 192.168.100.2 3 | s=Example of a SMPTE ST2110-20 signal 4 | i=this example is for 720p interlaced video 5 | t=0 0 6 | a=recvonly 7 | a=group:DUP primary secondary 8 | m=video 50000 RTP/AVP 112 9 | c=IN IP4 239.100.9.10/32 10 | a=source-filter:incl IN IP4 239.100.9.10 192.168.100.2 11 | a=rtpmap:112 raw/90000 12 | a=fmtp:112 sampling=YCbCr-4:2:2; width=1280; height=720; interlace; exactframerate=60000/1001; depth=10; TCS=SDR; colorimetry=BT709; PM=2110GPM; SSN=ST2110-20:2017; 13 | a=ts-refclk:ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:37 14 | a=mediaclk:direct=0 15 | a=mid:primary 16 | m=video 50020 RTP/AVP 112 17 | c=IN IP4 239.101.9.10/32 18 | a=source-filter:incl IN IP4 239.101.9.10 192.168.101.2 19 | a=rtpmap:112 raw/90000 20 | a=fmtp:112 sampling=YCbCr-4:2:2; width=1280; height=720; interlace; exactframerate=60000/1001; depth=10; TCS=SDR; colorimetry=BT709; PM=2110GPM; SSN=ST2110-20:2017; 21 | a=ts-refclk:ptp=IEEE1588-2008:39-A7-94-FF-FE-07-CB-D0:37 22 | a=mediaclk:direct=0 23 | a=mid:secondary; 24 | -------------------------------------------------------------------------------- /test/sdp_test_data/tcp-active.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 1562876543 11 IN IP4 192.0.2.3 3 | s=RFC4145 Example 7.4.2 4 | m=image 9 TCP t38 5 | c=IN IP4 192.0.2.3 6 | a=setup:active 7 | a=connection:new -------------------------------------------------------------------------------- /test/sdp_test_data/tcp-passive.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 1562876543 11 IN IP4 192.0.2.2 3 | s=RFC4145 Example 7.3.1 4 | m=image 54111 TCP t38 5 | c=IN IP4 192.0.2.2 6 | a=setup:passive 7 | a=connection:existing --------------------------------------------------------------------------------