├── .gitignore ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── build.js ├── getusermedia.bundle.js ├── getusermedia.js ├── package.json └── test ├── run-tests └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | browsers 5 | 6 | #Jetbrains ides 7 | .idea 8 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.bundle.js 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi": false, 3 | "expr": true, 4 | "loopfunc": true, 5 | "curly": false, 6 | "evil": true, 7 | "white": true, 8 | "undef": true, 9 | "browser": true, 10 | "node": true, 11 | "trailing": true, 12 | "indent": 4, 13 | "latedef": true, 14 | "newcap": true 15 | } 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - 6 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | env: 13 | matrix: 14 | - BROWSER=chrome BVER=stable 15 | - BROWSER=chrome BVER=beta 16 | - BROWSER=chrome BVER=unstable 17 | - BROWSER=firefox BVER=stable 18 | - BROWSER=firefox BVER=beta 19 | - BROWSER=firefox BVER=unstable 20 | - BROWSER=firefox BVER=esr 21 | 22 | matrix: 23 | fast_finish: true 24 | 25 | allow_failures: 26 | - env: BROWSER=chrome BVER=unstable 27 | - env: BROWSER=firefox BVER=unstable 28 | 29 | before_script: 30 | - ./node_modules/travis-multirunner/setup.sh 31 | - export DISPLAY=:99.0 32 | - sh -e /etc/init.d/xvfb start 33 | 34 | after_failure: 35 | - for file in *.log; do echo $file; echo "======================"; cat $file; done || true 36 | 37 | script: 38 | - npm run lint 39 | - npm run test-travis 40 | 41 | notifications: 42 | email: 43 | - build@andyet.com 44 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | =================== 3 | 4 | * 2.0.1 5 | * update webrtc-adapter to v2 6 | 7 | * 2.0.0 8 | * breaking change: #28 remove fake flag 9 | * feature: support nodejs runtime environments (eg: electron) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # getUserMedia 2 | 3 | ## What is this? 4 | 5 | A tiny browser module that gives us a simple API for getting access to a user's camera or microphone by wrapping the `navigator.getUserMedia` API in modern browsers. 6 | 7 | This is important because as of right now, all browsers return errors differently. More on that below. 8 | 9 | This lib also gracefully handles the lack of support in a browser. So you never have to check first (see error handling below). 10 | 11 | It gives us a cleaner node.js-style, error-first API and cross-browser handling. No browser support checking necessary, lack of support is treated in the same way as when the user rejects the request: the callback gets passed an error as the first argument. 12 | 13 | Suitable for use with browserify/CommonJS on the client. 14 | 15 | If you're not using browserify or you want AMD support use `getusermedia.bundle.js`. Note that if no module system is detected it simply attaches a function called `getUserMedia` to `window`. 16 | 17 | 18 | 19 | ## Installing 20 | 21 | ``` 22 | npm install getusermedia 23 | ``` 24 | 25 | ## How to use it 26 | 27 | 28 | With this helper it's clean/simple to get access to a user's camera, mic, etc. 29 | 30 | ```js 31 | var getUserMedia = require('getusermedia'); 32 | 33 | getUserMedia(function (err, stream) { 34 | // if the browser doesn't support user media 35 | // or the user says "no" the error gets passed 36 | // as the first argument. 37 | if (err) { 38 | console.log('failed'); 39 | } else { 40 | console.log('got a stream', stream); 41 | } 42 | }); 43 | ``` 44 | 45 | Passing in options is optional. It defaults to `{video: true, audio: true}`; 46 | 47 | ```js 48 | // optionally pass constraints as the first argument 49 | // they just passed through. 50 | getUserMedia({video: true, audio: false}, function (err, stream) { ... }); 51 | ``` 52 | 53 | 54 | ## Why? Because it's super ugly without this tool 55 | 56 | ```js 57 | // first deal with browser prefixes 58 | var getUserMedia = navigator.getUserMedia || 59 | navigator.mozGetUserMedia || 60 | navigator.webkitGetUserMedia; 61 | 62 | // make sure it's supported and bind to navigator 63 | if (getUserMedia) { 64 | getUserMedia = getUserMedia.bind(navigator); 65 | } else { 66 | // have to figure out how to handle the error somehow 67 | } 68 | 69 | // then deal with a weird, positional error handling API 70 | getUserMedia( 71 | // media constraints 72 | {video: true, audio: true}, 73 | // success callback 74 | function (stream) { 75 | // gets stream if successful 76 | }, 77 | // error callback 78 | function (error) { 79 | // called if failed to get media 80 | } 81 | ) 82 | ``` 83 | 84 | 85 | ## Handling errors (summary) 86 | 87 | All failed calls to `getusermedia` in this library will return an error object (of type `NavigatorUserMediaError`) as the first argument to the callback. All will have a `.name` according to [the specification](http://w3c.github.io/mediacapture-main/getusermedia.html#mediastreamerror) 88 | 89 | ## License 90 | 91 | MIT 92 | 93 | ## Created By 94 | 95 | If you like this, follow: [@HenrikJoreteg](http://twitter.com/henrikjoreteg) on twitter. 96 | 97 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | var bundle = require('browserify')({ standalone: 'getUserMedia' }), 2 | fs = require('fs'); 3 | 4 | 5 | bundle.add('./getusermedia'); 6 | bundle.bundle().pipe(fs.createWriteStream('getusermedia.bundle.js')); 7 | -------------------------------------------------------------------------------- /getusermedia.bundle.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.getUserMedia = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0 ? 'm=' + part : part).trim() + '\r\n'; 105 | }); 106 | }; 107 | 108 | // Returns lines that start with a certain prefix. 109 | SDPUtils.matchPrefix = function(blob, prefix) { 110 | return SDPUtils.splitLines(blob).filter(function(line) { 111 | return line.indexOf(prefix) === 0; 112 | }); 113 | }; 114 | 115 | // Parses an ICE candidate line. Sample input: 116 | // candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8 117 | // rport 55996" 118 | SDPUtils.parseCandidate = function(line) { 119 | var parts; 120 | // Parse both variants. 121 | if (line.indexOf('a=candidate:') === 0) { 122 | parts = line.substring(12).split(' '); 123 | } else { 124 | parts = line.substring(10).split(' '); 125 | } 126 | 127 | var candidate = { 128 | foundation: parts[0], 129 | component: parts[1], 130 | protocol: parts[2].toLowerCase(), 131 | priority: parseInt(parts[3], 10), 132 | ip: parts[4], 133 | port: parseInt(parts[5], 10), 134 | // skip parts[6] == 'typ' 135 | type: parts[7] 136 | }; 137 | 138 | for (var i = 8; i < parts.length; i += 2) { 139 | switch (parts[i]) { 140 | case 'raddr': 141 | candidate.relatedAddress = parts[i + 1]; 142 | break; 143 | case 'rport': 144 | candidate.relatedPort = parseInt(parts[i + 1], 10); 145 | break; 146 | case 'tcptype': 147 | candidate.tcpType = parts[i + 1]; 148 | break; 149 | default: // Unknown extensions are silently ignored. 150 | break; 151 | } 152 | } 153 | return candidate; 154 | }; 155 | 156 | // Translates a candidate object into SDP candidate attribute. 157 | SDPUtils.writeCandidate = function(candidate) { 158 | var sdp = []; 159 | sdp.push(candidate.foundation); 160 | sdp.push(candidate.component); 161 | sdp.push(candidate.protocol.toUpperCase()); 162 | sdp.push(candidate.priority); 163 | sdp.push(candidate.ip); 164 | sdp.push(candidate.port); 165 | 166 | var type = candidate.type; 167 | sdp.push('typ'); 168 | sdp.push(type); 169 | if (type !== 'host' && candidate.relatedAddress && 170 | candidate.relatedPort) { 171 | sdp.push('raddr'); 172 | sdp.push(candidate.relatedAddress); // was: relAddr 173 | sdp.push('rport'); 174 | sdp.push(candidate.relatedPort); // was: relPort 175 | } 176 | if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') { 177 | sdp.push('tcptype'); 178 | sdp.push(candidate.tcpType); 179 | } 180 | return 'candidate:' + sdp.join(' '); 181 | }; 182 | 183 | // Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input: 184 | // a=rtpmap:111 opus/48000/2 185 | SDPUtils.parseRtpMap = function(line) { 186 | var parts = line.substr(9).split(' '); 187 | var parsed = { 188 | payloadType: parseInt(parts.shift(), 10) // was: id 189 | }; 190 | 191 | parts = parts[0].split('/'); 192 | 193 | parsed.name = parts[0]; 194 | parsed.clockRate = parseInt(parts[1], 10); // was: clockrate 195 | // was: channels 196 | parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1; 197 | return parsed; 198 | }; 199 | 200 | // Generate an a=rtpmap line from RTCRtpCodecCapability or 201 | // RTCRtpCodecParameters. 202 | SDPUtils.writeRtpMap = function(codec) { 203 | var pt = codec.payloadType; 204 | if (codec.preferredPayloadType !== undefined) { 205 | pt = codec.preferredPayloadType; 206 | } 207 | return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate + 208 | (codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n'; 209 | }; 210 | 211 | // Parses an a=extmap line (headerextension from RFC 5285). Sample input: 212 | // a=extmap:2 urn:ietf:params:rtp-hdrext:toffset 213 | SDPUtils.parseExtmap = function(line) { 214 | var parts = line.substr(9).split(' '); 215 | return { 216 | id: parseInt(parts[0], 10), 217 | uri: parts[1] 218 | }; 219 | }; 220 | 221 | // Generates a=extmap line from RTCRtpHeaderExtensionParameters or 222 | // RTCRtpHeaderExtension. 223 | SDPUtils.writeExtmap = function(headerExtension) { 224 | return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) + 225 | ' ' + headerExtension.uri + '\r\n'; 226 | }; 227 | 228 | // Parses an ftmp line, returns dictionary. Sample input: 229 | // a=fmtp:96 vbr=on;cng=on 230 | // Also deals with vbr=on; cng=on 231 | SDPUtils.parseFmtp = function(line) { 232 | var parsed = {}; 233 | var kv; 234 | var parts = line.substr(line.indexOf(' ') + 1).split(';'); 235 | for (var j = 0; j < parts.length; j++) { 236 | kv = parts[j].trim().split('='); 237 | parsed[kv[0].trim()] = kv[1]; 238 | } 239 | return parsed; 240 | }; 241 | 242 | // Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters. 243 | SDPUtils.writeFmtp = function(codec) { 244 | var line = ''; 245 | var pt = codec.payloadType; 246 | if (codec.preferredPayloadType !== undefined) { 247 | pt = codec.preferredPayloadType; 248 | } 249 | if (codec.parameters && Object.keys(codec.parameters).length) { 250 | var params = []; 251 | Object.keys(codec.parameters).forEach(function(param) { 252 | params.push(param + '=' + codec.parameters[param]); 253 | }); 254 | line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n'; 255 | } 256 | return line; 257 | }; 258 | 259 | // Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input: 260 | // a=rtcp-fb:98 nack rpsi 261 | SDPUtils.parseRtcpFb = function(line) { 262 | var parts = line.substr(line.indexOf(' ') + 1).split(' '); 263 | return { 264 | type: parts.shift(), 265 | parameter: parts.join(' ') 266 | }; 267 | }; 268 | // Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters. 269 | SDPUtils.writeRtcpFb = function(codec) { 270 | var lines = ''; 271 | var pt = codec.payloadType; 272 | if (codec.preferredPayloadType !== undefined) { 273 | pt = codec.preferredPayloadType; 274 | } 275 | if (codec.rtcpFeedback && codec.rtcpFeedback.length) { 276 | // FIXME: special handling for trr-int? 277 | codec.rtcpFeedback.forEach(function(fb) { 278 | lines += 'a=rtcp-fb:' + pt + ' ' + fb.type + 279 | (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') + 280 | '\r\n'; 281 | }); 282 | } 283 | return lines; 284 | }; 285 | 286 | // Parses an RFC 5576 ssrc media attribute. Sample input: 287 | // a=ssrc:3735928559 cname:something 288 | SDPUtils.parseSsrcMedia = function(line) { 289 | var sp = line.indexOf(' '); 290 | var parts = { 291 | ssrc: parseInt(line.substr(7, sp - 7), 10) 292 | }; 293 | var colon = line.indexOf(':', sp); 294 | if (colon > -1) { 295 | parts.attribute = line.substr(sp + 1, colon - sp - 1); 296 | parts.value = line.substr(colon + 1); 297 | } else { 298 | parts.attribute = line.substr(sp + 1); 299 | } 300 | return parts; 301 | }; 302 | 303 | // Extracts DTLS parameters from SDP media section or sessionpart. 304 | // FIXME: for consistency with other functions this should only 305 | // get the fingerprint line as input. See also getIceParameters. 306 | SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) { 307 | var lines = SDPUtils.splitLines(mediaSection); 308 | // Search in session part, too. 309 | lines = lines.concat(SDPUtils.splitLines(sessionpart)); 310 | var fpLine = lines.filter(function(line) { 311 | return line.indexOf('a=fingerprint:') === 0; 312 | })[0].substr(14); 313 | // Note: a=setup line is ignored since we use the 'auto' role. 314 | var dtlsParameters = { 315 | role: 'auto', 316 | fingerprints: [{ 317 | algorithm: fpLine.split(' ')[0], 318 | value: fpLine.split(' ')[1] 319 | }] 320 | }; 321 | return dtlsParameters; 322 | }; 323 | 324 | // Serializes DTLS parameters to SDP. 325 | SDPUtils.writeDtlsParameters = function(params, setupType) { 326 | var sdp = 'a=setup:' + setupType + '\r\n'; 327 | params.fingerprints.forEach(function(fp) { 328 | sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n'; 329 | }); 330 | return sdp; 331 | }; 332 | // Parses ICE information from SDP media section or sessionpart. 333 | // FIXME: for consistency with other functions this should only 334 | // get the ice-ufrag and ice-pwd lines as input. 335 | SDPUtils.getIceParameters = function(mediaSection, sessionpart) { 336 | var lines = SDPUtils.splitLines(mediaSection); 337 | // Search in session part, too. 338 | lines = lines.concat(SDPUtils.splitLines(sessionpart)); 339 | var iceParameters = { 340 | usernameFragment: lines.filter(function(line) { 341 | return line.indexOf('a=ice-ufrag:') === 0; 342 | })[0].substr(12), 343 | password: lines.filter(function(line) { 344 | return line.indexOf('a=ice-pwd:') === 0; 345 | })[0].substr(10) 346 | }; 347 | return iceParameters; 348 | }; 349 | 350 | // Serializes ICE parameters to SDP. 351 | SDPUtils.writeIceParameters = function(params) { 352 | return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' + 353 | 'a=ice-pwd:' + params.password + '\r\n'; 354 | }; 355 | 356 | // Parses the SDP media section and returns RTCRtpParameters. 357 | SDPUtils.parseRtpParameters = function(mediaSection) { 358 | var description = { 359 | codecs: [], 360 | headerExtensions: [], 361 | fecMechanisms: [], 362 | rtcp: [] 363 | }; 364 | var lines = SDPUtils.splitLines(mediaSection); 365 | var mline = lines[0].split(' '); 366 | for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..] 367 | var pt = mline[i]; 368 | var rtpmapline = SDPUtils.matchPrefix( 369 | mediaSection, 'a=rtpmap:' + pt + ' ')[0]; 370 | if (rtpmapline) { 371 | var codec = SDPUtils.parseRtpMap(rtpmapline); 372 | var fmtps = SDPUtils.matchPrefix( 373 | mediaSection, 'a=fmtp:' + pt + ' '); 374 | // Only the first a=fmtp: is considered. 375 | codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {}; 376 | codec.rtcpFeedback = SDPUtils.matchPrefix( 377 | mediaSection, 'a=rtcp-fb:' + pt + ' ') 378 | .map(SDPUtils.parseRtcpFb); 379 | description.codecs.push(codec); 380 | // parse FEC mechanisms from rtpmap lines. 381 | switch (codec.name.toUpperCase()) { 382 | case 'RED': 383 | case 'ULPFEC': 384 | description.fecMechanisms.push(codec.name.toUpperCase()); 385 | break; 386 | default: // only RED and ULPFEC are recognized as FEC mechanisms. 387 | break; 388 | } 389 | } 390 | } 391 | SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) { 392 | description.headerExtensions.push(SDPUtils.parseExtmap(line)); 393 | }); 394 | // FIXME: parse rtcp. 395 | return description; 396 | }; 397 | 398 | // Generates parts of the SDP media section describing the capabilities / 399 | // parameters. 400 | SDPUtils.writeRtpDescription = function(kind, caps) { 401 | var sdp = ''; 402 | 403 | // Build the mline. 404 | sdp += 'm=' + kind + ' '; 405 | sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs. 406 | sdp += ' UDP/TLS/RTP/SAVPF '; 407 | sdp += caps.codecs.map(function(codec) { 408 | if (codec.preferredPayloadType !== undefined) { 409 | return codec.preferredPayloadType; 410 | } 411 | return codec.payloadType; 412 | }).join(' ') + '\r\n'; 413 | 414 | sdp += 'c=IN IP4 0.0.0.0\r\n'; 415 | sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n'; 416 | 417 | // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb. 418 | caps.codecs.forEach(function(codec) { 419 | sdp += SDPUtils.writeRtpMap(codec); 420 | sdp += SDPUtils.writeFmtp(codec); 421 | sdp += SDPUtils.writeRtcpFb(codec); 422 | }); 423 | // FIXME: add headerExtensions, fecMechanismş and rtcp. 424 | sdp += 'a=rtcp-mux\r\n'; 425 | return sdp; 426 | }; 427 | 428 | // Parses the SDP media section and returns an array of 429 | // RTCRtpEncodingParameters. 430 | SDPUtils.parseRtpEncodingParameters = function(mediaSection) { 431 | var encodingParameters = []; 432 | var description = SDPUtils.parseRtpParameters(mediaSection); 433 | var hasRed = description.fecMechanisms.indexOf('RED') !== -1; 434 | var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1; 435 | 436 | // filter a=ssrc:... cname:, ignore PlanB-msid 437 | var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') 438 | .map(function(line) { 439 | return SDPUtils.parseSsrcMedia(line); 440 | }) 441 | .filter(function(parts) { 442 | return parts.attribute === 'cname'; 443 | }); 444 | var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc; 445 | var secondarySsrc; 446 | 447 | var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID') 448 | .map(function(line) { 449 | var parts = line.split(' '); 450 | parts.shift(); 451 | return parts.map(function(part) { 452 | return parseInt(part, 10); 453 | }); 454 | }); 455 | if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) { 456 | secondarySsrc = flows[0][1]; 457 | } 458 | 459 | description.codecs.forEach(function(codec) { 460 | if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) { 461 | var encParam = { 462 | ssrc: primarySsrc, 463 | codecPayloadType: parseInt(codec.parameters.apt, 10), 464 | rtx: { 465 | payloadType: codec.payloadType, 466 | ssrc: secondarySsrc 467 | } 468 | }; 469 | encodingParameters.push(encParam); 470 | if (hasRed) { 471 | encParam = JSON.parse(JSON.stringify(encParam)); 472 | encParam.fec = { 473 | ssrc: secondarySsrc, 474 | mechanism: hasUlpfec ? 'red+ulpfec' : 'red' 475 | }; 476 | encodingParameters.push(encParam); 477 | } 478 | } 479 | }); 480 | if (encodingParameters.length === 0 && primarySsrc) { 481 | encodingParameters.push({ 482 | ssrc: primarySsrc 483 | }); 484 | } 485 | 486 | // we support both b=AS and b=TIAS but interpret AS as TIAS. 487 | var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b='); 488 | if (bandwidth.length) { 489 | if (bandwidth[0].indexOf('b=TIAS:') === 0) { 490 | bandwidth = parseInt(bandwidth[0].substr(7), 10); 491 | } else if (bandwidth[0].indexOf('b=AS:') === 0) { 492 | bandwidth = parseInt(bandwidth[0].substr(5), 10); 493 | } 494 | encodingParameters.forEach(function(params) { 495 | params.maxBitrate = bandwidth; 496 | }); 497 | } 498 | return encodingParameters; 499 | }; 500 | 501 | SDPUtils.writeSessionBoilerplate = function() { 502 | // FIXME: sess-id should be an NTP timestamp. 503 | return 'v=0\r\n' + 504 | 'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' + 505 | 's=-\r\n' + 506 | 't=0 0\r\n'; 507 | }; 508 | 509 | SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) { 510 | var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps); 511 | 512 | // Map ICE parameters (ufrag, pwd) to SDP. 513 | sdp += SDPUtils.writeIceParameters( 514 | transceiver.iceGatherer.getLocalParameters()); 515 | 516 | // Map DTLS parameters to SDP. 517 | sdp += SDPUtils.writeDtlsParameters( 518 | transceiver.dtlsTransport.getLocalParameters(), 519 | type === 'offer' ? 'actpass' : 'active'); 520 | 521 | sdp += 'a=mid:' + transceiver.mid + '\r\n'; 522 | 523 | if (transceiver.rtpSender && transceiver.rtpReceiver) { 524 | sdp += 'a=sendrecv\r\n'; 525 | } else if (transceiver.rtpSender) { 526 | sdp += 'a=sendonly\r\n'; 527 | } else if (transceiver.rtpReceiver) { 528 | sdp += 'a=recvonly\r\n'; 529 | } else { 530 | sdp += 'a=inactive\r\n'; 531 | } 532 | 533 | // FIXME: for RTX there might be multiple SSRCs. Not implemented in Edge yet. 534 | if (transceiver.rtpSender) { 535 | var msid = 'msid:' + stream.id + ' ' + 536 | transceiver.rtpSender.track.id + '\r\n'; 537 | sdp += 'a=' + msid; 538 | sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + 539 | ' ' + msid; 540 | } 541 | // FIXME: this should be written by writeRtpDescription. 542 | sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc + 543 | ' cname:' + SDPUtils.localCName + '\r\n'; 544 | return sdp; 545 | }; 546 | 547 | // Gets the direction from the mediaSection or the sessionpart. 548 | SDPUtils.getDirection = function(mediaSection, sessionpart) { 549 | // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv. 550 | var lines = SDPUtils.splitLines(mediaSection); 551 | for (var i = 0; i < lines.length; i++) { 552 | switch (lines[i]) { 553 | case 'a=sendrecv': 554 | case 'a=sendonly': 555 | case 'a=recvonly': 556 | case 'a=inactive': 557 | return lines[i].substr(2); 558 | default: 559 | // FIXME: What should happen here? 560 | } 561 | } 562 | if (sessionpart) { 563 | return SDPUtils.getDirection(sessionpart); 564 | } 565 | return 'sendrecv'; 566 | }; 567 | 568 | // Expose public methods. 569 | module.exports = SDPUtils; 570 | 571 | },{}],3:[function(require,module,exports){ 572 | /* 573 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 574 | * 575 | * Use of this source code is governed by a BSD-style license 576 | * that can be found in the LICENSE file in the root of the source 577 | * tree. 578 | */ 579 | /* eslint-env node */ 580 | 581 | 'use strict'; 582 | 583 | // Shimming starts here. 584 | (function() { 585 | // Utils. 586 | var logging = require('./utils').log; 587 | var browserDetails = require('./utils').browserDetails; 588 | // Export to the adapter global object visible in the browser. 589 | module.exports.browserDetails = browserDetails; 590 | module.exports.extractVersion = require('./utils').extractVersion; 591 | module.exports.disableLog = require('./utils').disableLog; 592 | 593 | // Uncomment the line below if you want logging to occur, including logging 594 | // for the switch statement below. Can also be turned on in the browser via 595 | // adapter.disableLog(false), but then logging from the switch statement below 596 | // will not appear. 597 | // require('./utils').disableLog(false); 598 | 599 | // Browser shims. 600 | var chromeShim = require('./chrome/chrome_shim') || null; 601 | var edgeShim = require('./edge/edge_shim') || null; 602 | var firefoxShim = require('./firefox/firefox_shim') || null; 603 | var safariShim = require('./safari/safari_shim') || null; 604 | 605 | // Shim browser if found. 606 | switch (browserDetails.browser) { 607 | case 'opera': // fallthrough as it uses chrome shims 608 | case 'chrome': 609 | if (!chromeShim || !chromeShim.shimPeerConnection) { 610 | logging('Chrome shim is not included in this adapter release.'); 611 | return; 612 | } 613 | logging('adapter.js shimming chrome.'); 614 | // Export to the adapter global object visible in the browser. 615 | module.exports.browserShim = chromeShim; 616 | 617 | chromeShim.shimGetUserMedia(); 618 | chromeShim.shimMediaStream(); 619 | chromeShim.shimSourceObject(); 620 | chromeShim.shimPeerConnection(); 621 | chromeShim.shimOnTrack(); 622 | break; 623 | case 'firefox': 624 | if (!firefoxShim || !firefoxShim.shimPeerConnection) { 625 | logging('Firefox shim is not included in this adapter release.'); 626 | return; 627 | } 628 | logging('adapter.js shimming firefox.'); 629 | // Export to the adapter global object visible in the browser. 630 | module.exports.browserShim = firefoxShim; 631 | 632 | firefoxShim.shimGetUserMedia(); 633 | firefoxShim.shimSourceObject(); 634 | firefoxShim.shimPeerConnection(); 635 | firefoxShim.shimOnTrack(); 636 | break; 637 | case 'edge': 638 | if (!edgeShim || !edgeShim.shimPeerConnection) { 639 | logging('MS edge shim is not included in this adapter release.'); 640 | return; 641 | } 642 | logging('adapter.js shimming edge.'); 643 | // Export to the adapter global object visible in the browser. 644 | module.exports.browserShim = edgeShim; 645 | 646 | edgeShim.shimGetUserMedia(); 647 | edgeShim.shimPeerConnection(); 648 | break; 649 | case 'safari': 650 | if (!safariShim) { 651 | logging('Safari shim is not included in this adapter release.'); 652 | return; 653 | } 654 | logging('adapter.js shimming safari.'); 655 | // Export to the adapter global object visible in the browser. 656 | module.exports.browserShim = safariShim; 657 | 658 | safariShim.shimGetUserMedia(); 659 | break; 660 | default: 661 | logging('Unsupported browser!'); 662 | } 663 | })(); 664 | 665 | },{"./chrome/chrome_shim":4,"./edge/edge_shim":6,"./firefox/firefox_shim":8,"./safari/safari_shim":10,"./utils":11}],4:[function(require,module,exports){ 666 | 667 | /* 668 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 669 | * 670 | * Use of this source code is governed by a BSD-style license 671 | * that can be found in the LICENSE file in the root of the source 672 | * tree. 673 | */ 674 | /* eslint-env node */ 675 | 'use strict'; 676 | var logging = require('../utils.js').log; 677 | var browserDetails = require('../utils.js').browserDetails; 678 | 679 | var chromeShim = { 680 | shimMediaStream: function() { 681 | window.MediaStream = window.MediaStream || window.webkitMediaStream; 682 | }, 683 | 684 | shimOnTrack: function() { 685 | if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in 686 | window.RTCPeerConnection.prototype)) { 687 | Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { 688 | get: function() { 689 | return this._ontrack; 690 | }, 691 | set: function(f) { 692 | var self = this; 693 | if (this._ontrack) { 694 | this.removeEventListener('track', this._ontrack); 695 | this.removeEventListener('addstream', this._ontrackpoly); 696 | } 697 | this.addEventListener('track', this._ontrack = f); 698 | this.addEventListener('addstream', this._ontrackpoly = function(e) { 699 | // onaddstream does not fire when a track is added to an existing 700 | // stream. But stream.onaddtrack is implemented so we use that. 701 | e.stream.addEventListener('addtrack', function(te) { 702 | var event = new Event('track'); 703 | event.track = te.track; 704 | event.receiver = {track: te.track}; 705 | event.streams = [e.stream]; 706 | self.dispatchEvent(event); 707 | }); 708 | e.stream.getTracks().forEach(function(track) { 709 | var event = new Event('track'); 710 | event.track = track; 711 | event.receiver = {track: track}; 712 | event.streams = [e.stream]; 713 | this.dispatchEvent(event); 714 | }.bind(this)); 715 | }.bind(this)); 716 | } 717 | }); 718 | } 719 | }, 720 | 721 | shimSourceObject: function() { 722 | if (typeof window === 'object') { 723 | if (window.HTMLMediaElement && 724 | !('srcObject' in window.HTMLMediaElement.prototype)) { 725 | // Shim the srcObject property, once, when HTMLMediaElement is found. 726 | Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { 727 | get: function() { 728 | return this._srcObject; 729 | }, 730 | set: function(stream) { 731 | var self = this; 732 | // Use _srcObject as a private property for this shim 733 | this._srcObject = stream; 734 | if (this.src) { 735 | URL.revokeObjectURL(this.src); 736 | } 737 | 738 | if (!stream) { 739 | this.src = ''; 740 | return; 741 | } 742 | this.src = URL.createObjectURL(stream); 743 | // We need to recreate the blob url when a track is added or 744 | // removed. Doing it manually since we want to avoid a recursion. 745 | stream.addEventListener('addtrack', function() { 746 | if (self.src) { 747 | URL.revokeObjectURL(self.src); 748 | } 749 | self.src = URL.createObjectURL(stream); 750 | }); 751 | stream.addEventListener('removetrack', function() { 752 | if (self.src) { 753 | URL.revokeObjectURL(self.src); 754 | } 755 | self.src = URL.createObjectURL(stream); 756 | }); 757 | } 758 | }); 759 | } 760 | } 761 | }, 762 | 763 | shimPeerConnection: function() { 764 | // The RTCPeerConnection object. 765 | window.RTCPeerConnection = function(pcConfig, pcConstraints) { 766 | // Translate iceTransportPolicy to iceTransports, 767 | // see https://code.google.com/p/webrtc/issues/detail?id=4869 768 | logging('PeerConnection'); 769 | if (pcConfig && pcConfig.iceTransportPolicy) { 770 | pcConfig.iceTransports = pcConfig.iceTransportPolicy; 771 | } 772 | 773 | var pc = new webkitRTCPeerConnection(pcConfig, pcConstraints); 774 | var origGetStats = pc.getStats.bind(pc); 775 | pc.getStats = function(selector, successCallback, errorCallback) { 776 | var self = this; 777 | var args = arguments; 778 | 779 | // If selector is a function then we are in the old style stats so just 780 | // pass back the original getStats format to avoid breaking old users. 781 | if (arguments.length > 0 && typeof selector === 'function') { 782 | return origGetStats(selector, successCallback); 783 | } 784 | 785 | var fixChromeStats_ = function(response) { 786 | var standardReport = {}; 787 | var reports = response.result(); 788 | reports.forEach(function(report) { 789 | var standardStats = { 790 | id: report.id, 791 | timestamp: report.timestamp, 792 | type: report.type 793 | }; 794 | report.names().forEach(function(name) { 795 | standardStats[name] = report.stat(name); 796 | }); 797 | standardReport[standardStats.id] = standardStats; 798 | }); 799 | 800 | return standardReport; 801 | }; 802 | 803 | // shim getStats with maplike support 804 | var makeMapStats = function(stats, legacyStats) { 805 | var map = new Map(Object.keys(stats).map(function(key) { 806 | return[key, stats[key]]; 807 | })); 808 | legacyStats = legacyStats || stats; 809 | Object.keys(legacyStats).forEach(function(key) { 810 | map[key] = legacyStats[key]; 811 | }); 812 | return map; 813 | }; 814 | 815 | if (arguments.length >= 2) { 816 | var successCallbackWrapper_ = function(response) { 817 | args[1](makeMapStats(fixChromeStats_(response))); 818 | }; 819 | 820 | return origGetStats.apply(this, [successCallbackWrapper_, 821 | arguments[0]]); 822 | } 823 | 824 | // promise-support 825 | return new Promise(function(resolve, reject) { 826 | if (args.length === 1 && typeof selector === 'object') { 827 | origGetStats.apply(self, [ 828 | function(response) { 829 | resolve(makeMapStats(fixChromeStats_(response))); 830 | }, reject]); 831 | } else { 832 | // Preserve legacy chrome stats only on legacy access of stats obj 833 | origGetStats.apply(self, [ 834 | function(response) { 835 | resolve(makeMapStats(fixChromeStats_(response), 836 | response.result())); 837 | }, reject]); 838 | } 839 | }).then(successCallback, errorCallback); 840 | }; 841 | 842 | return pc; 843 | }; 844 | window.RTCPeerConnection.prototype = webkitRTCPeerConnection.prototype; 845 | 846 | // wrap static methods. Currently just generateCertificate. 847 | if (webkitRTCPeerConnection.generateCertificate) { 848 | Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { 849 | get: function() { 850 | return webkitRTCPeerConnection.generateCertificate; 851 | } 852 | }); 853 | } 854 | 855 | ['createOffer', 'createAnswer'].forEach(function(method) { 856 | var nativeMethod = webkitRTCPeerConnection.prototype[method]; 857 | webkitRTCPeerConnection.prototype[method] = function() { 858 | var self = this; 859 | if (arguments.length < 1 || (arguments.length === 1 && 860 | typeof arguments[0] === 'object')) { 861 | var opts = arguments.length === 1 ? arguments[0] : undefined; 862 | return new Promise(function(resolve, reject) { 863 | nativeMethod.apply(self, [resolve, reject, opts]); 864 | }); 865 | } 866 | return nativeMethod.apply(this, arguments); 867 | }; 868 | }); 869 | 870 | // add promise support -- natively available in Chrome 51 871 | if (browserDetails.version < 51) { 872 | ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] 873 | .forEach(function(method) { 874 | var nativeMethod = webkitRTCPeerConnection.prototype[method]; 875 | webkitRTCPeerConnection.prototype[method] = function() { 876 | var args = arguments; 877 | var self = this; 878 | var promise = new Promise(function(resolve, reject) { 879 | nativeMethod.apply(self, [args[0], resolve, reject]); 880 | }); 881 | if (args.length < 2) { 882 | return promise; 883 | } 884 | return promise.then(function() { 885 | args[1].apply(null, []); 886 | }, 887 | function(err) { 888 | if (args.length >= 3) { 889 | args[2].apply(null, [err]); 890 | } 891 | }); 892 | }; 893 | }); 894 | } 895 | 896 | // shim implicit creation of RTCSessionDescription/RTCIceCandidate 897 | ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] 898 | .forEach(function(method) { 899 | var nativeMethod = webkitRTCPeerConnection.prototype[method]; 900 | webkitRTCPeerConnection.prototype[method] = function() { 901 | arguments[0] = new ((method === 'addIceCandidate') ? 902 | RTCIceCandidate : RTCSessionDescription)(arguments[0]); 903 | return nativeMethod.apply(this, arguments); 904 | }; 905 | }); 906 | 907 | // support for addIceCandidate(null) 908 | var nativeAddIceCandidate = 909 | RTCPeerConnection.prototype.addIceCandidate; 910 | RTCPeerConnection.prototype.addIceCandidate = function() { 911 | return arguments[0] === null ? Promise.resolve() 912 | : nativeAddIceCandidate.apply(this, arguments); 913 | }; 914 | } 915 | }; 916 | 917 | 918 | // Expose public methods. 919 | module.exports = { 920 | shimMediaStream: chromeShim.shimMediaStream, 921 | shimOnTrack: chromeShim.shimOnTrack, 922 | shimSourceObject: chromeShim.shimSourceObject, 923 | shimPeerConnection: chromeShim.shimPeerConnection, 924 | shimGetUserMedia: require('./getusermedia') 925 | }; 926 | 927 | },{"../utils.js":11,"./getusermedia":5}],5:[function(require,module,exports){ 928 | /* 929 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 930 | * 931 | * Use of this source code is governed by a BSD-style license 932 | * that can be found in the LICENSE file in the root of the source 933 | * tree. 934 | */ 935 | /* eslint-env node */ 936 | 'use strict'; 937 | var logging = require('../utils.js').log; 938 | 939 | // Expose public methods. 940 | module.exports = function() { 941 | var constraintsToChrome_ = function(c) { 942 | if (typeof c !== 'object' || c.mandatory || c.optional) { 943 | return c; 944 | } 945 | var cc = {}; 946 | Object.keys(c).forEach(function(key) { 947 | if (key === 'require' || key === 'advanced' || key === 'mediaSource') { 948 | return; 949 | } 950 | var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]}; 951 | if (r.exact !== undefined && typeof r.exact === 'number') { 952 | r.min = r.max = r.exact; 953 | } 954 | var oldname_ = function(prefix, name) { 955 | if (prefix) { 956 | return prefix + name.charAt(0).toUpperCase() + name.slice(1); 957 | } 958 | return (name === 'deviceId') ? 'sourceId' : name; 959 | }; 960 | if (r.ideal !== undefined) { 961 | cc.optional = cc.optional || []; 962 | var oc = {}; 963 | if (typeof r.ideal === 'number') { 964 | oc[oldname_('min', key)] = r.ideal; 965 | cc.optional.push(oc); 966 | oc = {}; 967 | oc[oldname_('max', key)] = r.ideal; 968 | cc.optional.push(oc); 969 | } else { 970 | oc[oldname_('', key)] = r.ideal; 971 | cc.optional.push(oc); 972 | } 973 | } 974 | if (r.exact !== undefined && typeof r.exact !== 'number') { 975 | cc.mandatory = cc.mandatory || {}; 976 | cc.mandatory[oldname_('', key)] = r.exact; 977 | } else { 978 | ['min', 'max'].forEach(function(mix) { 979 | if (r[mix] !== undefined) { 980 | cc.mandatory = cc.mandatory || {}; 981 | cc.mandatory[oldname_(mix, key)] = r[mix]; 982 | } 983 | }); 984 | } 985 | }); 986 | if (c.advanced) { 987 | cc.optional = (cc.optional || []).concat(c.advanced); 988 | } 989 | return cc; 990 | }; 991 | 992 | var shimConstraints_ = function(constraints, func) { 993 | constraints = JSON.parse(JSON.stringify(constraints)); 994 | if (constraints && constraints.audio) { 995 | constraints.audio = constraintsToChrome_(constraints.audio); 996 | } 997 | if (constraints && typeof constraints.video === 'object') { 998 | // Shim facingMode for mobile, where it defaults to "user". 999 | var face = constraints.video.facingMode; 1000 | face = face && ((typeof face === 'object') ? face : {ideal: face}); 1001 | 1002 | if ((face && (face.exact === 'user' || face.exact === 'environment' || 1003 | face.ideal === 'user' || face.ideal === 'environment')) && 1004 | !(navigator.mediaDevices.getSupportedConstraints && 1005 | navigator.mediaDevices.getSupportedConstraints().facingMode)) { 1006 | delete constraints.video.facingMode; 1007 | if (face.exact === 'environment' || face.ideal === 'environment') { 1008 | // Look for "back" in label, or use last cam (typically back cam). 1009 | return navigator.mediaDevices.enumerateDevices() 1010 | .then(function(devices) { 1011 | devices = devices.filter(function(d) { 1012 | return d.kind === 'videoinput'; 1013 | }); 1014 | var back = devices.find(function(d) { 1015 | return d.label.toLowerCase().indexOf('back') !== -1; 1016 | }) || (devices.length && devices[devices.length - 1]); 1017 | if (back) { 1018 | constraints.video.deviceId = face.exact ? {exact: back.deviceId} : 1019 | {ideal: back.deviceId}; 1020 | } 1021 | constraints.video = constraintsToChrome_(constraints.video); 1022 | logging('chrome: ' + JSON.stringify(constraints)); 1023 | return func(constraints); 1024 | }); 1025 | } 1026 | } 1027 | constraints.video = constraintsToChrome_(constraints.video); 1028 | } 1029 | logging('chrome: ' + JSON.stringify(constraints)); 1030 | return func(constraints); 1031 | }; 1032 | 1033 | var shimError_ = function(e) { 1034 | return { 1035 | name: { 1036 | PermissionDeniedError: 'NotAllowedError', 1037 | ConstraintNotSatisfiedError: 'OverconstrainedError' 1038 | }[e.name] || e.name, 1039 | message: e.message, 1040 | constraint: e.constraintName, 1041 | toString: function() { 1042 | return this.name + (this.message && ': ') + this.message; 1043 | } 1044 | }; 1045 | }; 1046 | 1047 | var getUserMedia_ = function(constraints, onSuccess, onError) { 1048 | shimConstraints_(constraints, function(c) { 1049 | navigator.webkitGetUserMedia(c, onSuccess, function(e) { 1050 | onError(shimError_(e)); 1051 | }); 1052 | }); 1053 | }; 1054 | 1055 | navigator.getUserMedia = getUserMedia_; 1056 | 1057 | // Returns the result of getUserMedia as a Promise. 1058 | var getUserMediaPromise_ = function(constraints) { 1059 | return new Promise(function(resolve, reject) { 1060 | navigator.getUserMedia(constraints, resolve, reject); 1061 | }); 1062 | }; 1063 | 1064 | if (!navigator.mediaDevices) { 1065 | navigator.mediaDevices = { 1066 | getUserMedia: getUserMediaPromise_, 1067 | enumerateDevices: function() { 1068 | return new Promise(function(resolve) { 1069 | var kinds = {audio: 'audioinput', video: 'videoinput'}; 1070 | return MediaStreamTrack.getSources(function(devices) { 1071 | resolve(devices.map(function(device) { 1072 | return {label: device.label, 1073 | kind: kinds[device.kind], 1074 | deviceId: device.id, 1075 | groupId: ''}; 1076 | })); 1077 | }); 1078 | }); 1079 | } 1080 | }; 1081 | } 1082 | 1083 | // A shim for getUserMedia method on the mediaDevices object. 1084 | // TODO(KaptenJansson) remove once implemented in Chrome stable. 1085 | if (!navigator.mediaDevices.getUserMedia) { 1086 | navigator.mediaDevices.getUserMedia = function(constraints) { 1087 | return getUserMediaPromise_(constraints); 1088 | }; 1089 | } else { 1090 | // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia 1091 | // function which returns a Promise, it does not accept spec-style 1092 | // constraints. 1093 | var origGetUserMedia = navigator.mediaDevices.getUserMedia. 1094 | bind(navigator.mediaDevices); 1095 | navigator.mediaDevices.getUserMedia = function(cs) { 1096 | return shimConstraints_(cs, function(c) { 1097 | return origGetUserMedia(c).catch(function(e) { 1098 | return Promise.reject(shimError_(e)); 1099 | }); 1100 | }); 1101 | }; 1102 | } 1103 | 1104 | // Dummy devicechange event methods. 1105 | // TODO(KaptenJansson) remove once implemented in Chrome stable. 1106 | if (typeof navigator.mediaDevices.addEventListener === 'undefined') { 1107 | navigator.mediaDevices.addEventListener = function() { 1108 | logging('Dummy mediaDevices.addEventListener called.'); 1109 | }; 1110 | } 1111 | if (typeof navigator.mediaDevices.removeEventListener === 'undefined') { 1112 | navigator.mediaDevices.removeEventListener = function() { 1113 | logging('Dummy mediaDevices.removeEventListener called.'); 1114 | }; 1115 | } 1116 | }; 1117 | 1118 | },{"../utils.js":11}],6:[function(require,module,exports){ 1119 | /* 1120 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 1121 | * 1122 | * Use of this source code is governed by a BSD-style license 1123 | * that can be found in the LICENSE file in the root of the source 1124 | * tree. 1125 | */ 1126 | /* eslint-env node */ 1127 | 'use strict'; 1128 | 1129 | var SDPUtils = require('sdp'); 1130 | var browserDetails = require('../utils').browserDetails; 1131 | 1132 | var edgeShim = { 1133 | shimPeerConnection: function() { 1134 | if (window.RTCIceGatherer) { 1135 | // ORTC defines an RTCIceCandidate object but no constructor. 1136 | // Not implemented in Edge. 1137 | if (!window.RTCIceCandidate) { 1138 | window.RTCIceCandidate = function(args) { 1139 | return args; 1140 | }; 1141 | } 1142 | // ORTC does not have a session description object but 1143 | // other browsers (i.e. Chrome) that will support both PC and ORTC 1144 | // in the future might have this defined already. 1145 | if (!window.RTCSessionDescription) { 1146 | window.RTCSessionDescription = function(args) { 1147 | return args; 1148 | }; 1149 | } 1150 | } 1151 | 1152 | window.RTCPeerConnection = function(config) { 1153 | var self = this; 1154 | 1155 | var _eventTarget = document.createDocumentFragment(); 1156 | ['addEventListener', 'removeEventListener', 'dispatchEvent'] 1157 | .forEach(function(method) { 1158 | self[method] = _eventTarget[method].bind(_eventTarget); 1159 | }); 1160 | 1161 | this.onicecandidate = null; 1162 | this.onaddstream = null; 1163 | this.ontrack = null; 1164 | this.onremovestream = null; 1165 | this.onsignalingstatechange = null; 1166 | this.oniceconnectionstatechange = null; 1167 | this.onnegotiationneeded = null; 1168 | this.ondatachannel = null; 1169 | 1170 | this.localStreams = []; 1171 | this.remoteStreams = []; 1172 | this.getLocalStreams = function() { 1173 | return self.localStreams; 1174 | }; 1175 | this.getRemoteStreams = function() { 1176 | return self.remoteStreams; 1177 | }; 1178 | 1179 | this.localDescription = new RTCSessionDescription({ 1180 | type: '', 1181 | sdp: '' 1182 | }); 1183 | this.remoteDescription = new RTCSessionDescription({ 1184 | type: '', 1185 | sdp: '' 1186 | }); 1187 | this.signalingState = 'stable'; 1188 | this.iceConnectionState = 'new'; 1189 | this.iceGatheringState = 'new'; 1190 | 1191 | this.iceOptions = { 1192 | gatherPolicy: 'all', 1193 | iceServers: [] 1194 | }; 1195 | if (config && config.iceTransportPolicy) { 1196 | switch (config.iceTransportPolicy) { 1197 | case 'all': 1198 | case 'relay': 1199 | this.iceOptions.gatherPolicy = config.iceTransportPolicy; 1200 | break; 1201 | case 'none': 1202 | // FIXME: remove once implementation and spec have added this. 1203 | throw new TypeError('iceTransportPolicy "none" not supported'); 1204 | default: 1205 | // don't set iceTransportPolicy. 1206 | break; 1207 | } 1208 | } 1209 | this.usingBundle = config && config.bundlePolicy === 'max-bundle'; 1210 | 1211 | if (config && config.iceServers) { 1212 | // Edge does not like 1213 | // 1) stun: 1214 | // 2) turn: that does not have all of turn:host:port?transport=udp 1215 | // 3) turn: with ipv6 addresses 1216 | var iceServers = JSON.parse(JSON.stringify(config.iceServers)); 1217 | this.iceOptions.iceServers = iceServers.filter(function(server) { 1218 | if (server && server.urls) { 1219 | var urls = server.urls; 1220 | if (typeof urls === 'string') { 1221 | urls = [urls]; 1222 | } 1223 | urls = urls.filter(function(url) { 1224 | return (url.indexOf('turn:') === 0 && 1225 | url.indexOf('transport=udp') !== -1 && 1226 | url.indexOf('turn:[') === -1) || 1227 | (url.indexOf('stun:') === 0 && 1228 | browserDetails.version >= 14393); 1229 | })[0]; 1230 | return !!urls; 1231 | } 1232 | return false; 1233 | }); 1234 | } 1235 | 1236 | // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ... 1237 | // everything that is needed to describe a SDP m-line. 1238 | this.transceivers = []; 1239 | 1240 | // since the iceGatherer is currently created in createOffer but we 1241 | // must not emit candidates until after setLocalDescription we buffer 1242 | // them in this array. 1243 | this._localIceCandidatesBuffer = []; 1244 | }; 1245 | 1246 | window.RTCPeerConnection.prototype._emitBufferedCandidates = function() { 1247 | var self = this; 1248 | var sections = SDPUtils.splitSections(self.localDescription.sdp); 1249 | // FIXME: need to apply ice candidates in a way which is async but 1250 | // in-order 1251 | this._localIceCandidatesBuffer.forEach(function(event) { 1252 | var end = !event.candidate || Object.keys(event.candidate).length === 0; 1253 | if (end) { 1254 | for (var j = 1; j < sections.length; j++) { 1255 | if (sections[j].indexOf('\r\na=end-of-candidates\r\n') === -1) { 1256 | sections[j] += 'a=end-of-candidates\r\n'; 1257 | } 1258 | } 1259 | } else if (event.candidate.candidate.indexOf('typ endOfCandidates') 1260 | === -1) { 1261 | sections[event.candidate.sdpMLineIndex + 1] += 1262 | 'a=' + event.candidate.candidate + '\r\n'; 1263 | } 1264 | self.localDescription.sdp = sections.join(''); 1265 | self.dispatchEvent(event); 1266 | if (self.onicecandidate !== null) { 1267 | self.onicecandidate(event); 1268 | } 1269 | if (!event.candidate && self.iceGatheringState !== 'complete') { 1270 | var complete = self.transceivers.every(function(transceiver) { 1271 | return transceiver.iceGatherer && 1272 | transceiver.iceGatherer.state === 'completed'; 1273 | }); 1274 | if (complete) { 1275 | self.iceGatheringState = 'complete'; 1276 | } 1277 | } 1278 | }); 1279 | this._localIceCandidatesBuffer = []; 1280 | }; 1281 | 1282 | window.RTCPeerConnection.prototype.addStream = function(stream) { 1283 | // Clone is necessary for local demos mostly, attaching directly 1284 | // to two different senders does not work (build 10547). 1285 | this.localStreams.push(stream.clone()); 1286 | this._maybeFireNegotiationNeeded(); 1287 | }; 1288 | 1289 | window.RTCPeerConnection.prototype.removeStream = function(stream) { 1290 | var idx = this.localStreams.indexOf(stream); 1291 | if (idx > -1) { 1292 | this.localStreams.splice(idx, 1); 1293 | this._maybeFireNegotiationNeeded(); 1294 | } 1295 | }; 1296 | 1297 | window.RTCPeerConnection.prototype.getSenders = function() { 1298 | return this.transceivers.filter(function(transceiver) { 1299 | return !!transceiver.rtpSender; 1300 | }) 1301 | .map(function(transceiver) { 1302 | return transceiver.rtpSender; 1303 | }); 1304 | }; 1305 | 1306 | window.RTCPeerConnection.prototype.getReceivers = function() { 1307 | return this.transceivers.filter(function(transceiver) { 1308 | return !!transceiver.rtpReceiver; 1309 | }) 1310 | .map(function(transceiver) { 1311 | return transceiver.rtpReceiver; 1312 | }); 1313 | }; 1314 | 1315 | // Determines the intersection of local and remote capabilities. 1316 | window.RTCPeerConnection.prototype._getCommonCapabilities = 1317 | function(localCapabilities, remoteCapabilities) { 1318 | var commonCapabilities = { 1319 | codecs: [], 1320 | headerExtensions: [], 1321 | fecMechanisms: [] 1322 | }; 1323 | localCapabilities.codecs.forEach(function(lCodec) { 1324 | for (var i = 0; i < remoteCapabilities.codecs.length; i++) { 1325 | var rCodec = remoteCapabilities.codecs[i]; 1326 | if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() && 1327 | lCodec.clockRate === rCodec.clockRate && 1328 | lCodec.numChannels === rCodec.numChannels) { 1329 | // push rCodec so we reply with offerer payload type 1330 | commonCapabilities.codecs.push(rCodec); 1331 | 1332 | // determine common feedback mechanisms 1333 | rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) { 1334 | for (var j = 0; j < lCodec.rtcpFeedback.length; j++) { 1335 | if (lCodec.rtcpFeedback[j].type === fb.type && 1336 | lCodec.rtcpFeedback[j].parameter === fb.parameter) { 1337 | return true; 1338 | } 1339 | } 1340 | return false; 1341 | }); 1342 | // FIXME: also need to determine .parameters 1343 | // see https://github.com/openpeer/ortc/issues/569 1344 | break; 1345 | } 1346 | } 1347 | }); 1348 | 1349 | localCapabilities.headerExtensions 1350 | .forEach(function(lHeaderExtension) { 1351 | for (var i = 0; i < remoteCapabilities.headerExtensions.length; 1352 | i++) { 1353 | var rHeaderExtension = remoteCapabilities.headerExtensions[i]; 1354 | if (lHeaderExtension.uri === rHeaderExtension.uri) { 1355 | commonCapabilities.headerExtensions.push(rHeaderExtension); 1356 | break; 1357 | } 1358 | } 1359 | }); 1360 | 1361 | // FIXME: fecMechanisms 1362 | return commonCapabilities; 1363 | }; 1364 | 1365 | // Create ICE gatherer, ICE transport and DTLS transport. 1366 | window.RTCPeerConnection.prototype._createIceAndDtlsTransports = 1367 | function(mid, sdpMLineIndex) { 1368 | var self = this; 1369 | var iceGatherer = new RTCIceGatherer(self.iceOptions); 1370 | var iceTransport = new RTCIceTransport(iceGatherer); 1371 | iceGatherer.onlocalcandidate = function(evt) { 1372 | var event = new Event('icecandidate'); 1373 | event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex}; 1374 | 1375 | var cand = evt.candidate; 1376 | var end = !cand || Object.keys(cand).length === 0; 1377 | // Edge emits an empty object for RTCIceCandidateComplete‥ 1378 | if (end) { 1379 | // polyfill since RTCIceGatherer.state is not implemented in 1380 | // Edge 10547 yet. 1381 | if (iceGatherer.state === undefined) { 1382 | iceGatherer.state = 'completed'; 1383 | } 1384 | 1385 | // Emit a candidate with type endOfCandidates to make the samples 1386 | // work. Edge requires addIceCandidate with this empty candidate 1387 | // to start checking. The real solution is to signal 1388 | // end-of-candidates to the other side when getting the null 1389 | // candidate but some apps (like the samples) don't do that. 1390 | event.candidate.candidate = 1391 | 'candidate:1 1 udp 1 0.0.0.0 9 typ endOfCandidates'; 1392 | } else { 1393 | // RTCIceCandidate doesn't have a component, needs to be added 1394 | cand.component = iceTransport.component === 'RTCP' ? 2 : 1; 1395 | event.candidate.candidate = SDPUtils.writeCandidate(cand); 1396 | } 1397 | 1398 | // update local description. 1399 | var sections = SDPUtils.splitSections(self.localDescription.sdp); 1400 | if (event.candidate.candidate.indexOf('typ endOfCandidates') 1401 | === -1) { 1402 | sections[event.candidate.sdpMLineIndex + 1] += 1403 | 'a=' + event.candidate.candidate + '\r\n'; 1404 | } else { 1405 | sections[event.candidate.sdpMLineIndex + 1] += 1406 | 'a=end-of-candidates\r\n'; 1407 | } 1408 | self.localDescription.sdp = sections.join(''); 1409 | 1410 | var complete = self.transceivers.every(function(transceiver) { 1411 | return transceiver.iceGatherer && 1412 | transceiver.iceGatherer.state === 'completed'; 1413 | }); 1414 | 1415 | // Emit candidate if localDescription is set. 1416 | // Also emits null candidate when all gatherers are complete. 1417 | switch (self.iceGatheringState) { 1418 | case 'new': 1419 | self._localIceCandidatesBuffer.push(event); 1420 | if (end && complete) { 1421 | self._localIceCandidatesBuffer.push( 1422 | new Event('icecandidate')); 1423 | } 1424 | break; 1425 | case 'gathering': 1426 | self._emitBufferedCandidates(); 1427 | self.dispatchEvent(event); 1428 | if (self.onicecandidate !== null) { 1429 | self.onicecandidate(event); 1430 | } 1431 | if (complete) { 1432 | self.dispatchEvent(new Event('icecandidate')); 1433 | if (self.onicecandidate !== null) { 1434 | self.onicecandidate(new Event('icecandidate')); 1435 | } 1436 | self.iceGatheringState = 'complete'; 1437 | } 1438 | break; 1439 | case 'complete': 1440 | // should not happen... currently! 1441 | break; 1442 | default: // no-op. 1443 | break; 1444 | } 1445 | }; 1446 | iceTransport.onicestatechange = function() { 1447 | self._updateConnectionState(); 1448 | }; 1449 | 1450 | var dtlsTransport = new RTCDtlsTransport(iceTransport); 1451 | dtlsTransport.ondtlsstatechange = function() { 1452 | self._updateConnectionState(); 1453 | }; 1454 | dtlsTransport.onerror = function() { 1455 | // onerror does not set state to failed by itself. 1456 | dtlsTransport.state = 'failed'; 1457 | self._updateConnectionState(); 1458 | }; 1459 | 1460 | return { 1461 | iceGatherer: iceGatherer, 1462 | iceTransport: iceTransport, 1463 | dtlsTransport: dtlsTransport 1464 | }; 1465 | }; 1466 | 1467 | // Start the RTP Sender and Receiver for a transceiver. 1468 | window.RTCPeerConnection.prototype._transceive = function(transceiver, 1469 | send, recv) { 1470 | var params = this._getCommonCapabilities(transceiver.localCapabilities, 1471 | transceiver.remoteCapabilities); 1472 | if (send && transceiver.rtpSender) { 1473 | params.encodings = transceiver.sendEncodingParameters; 1474 | params.rtcp = { 1475 | cname: SDPUtils.localCName 1476 | }; 1477 | if (transceiver.recvEncodingParameters.length) { 1478 | params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc; 1479 | } 1480 | transceiver.rtpSender.send(params); 1481 | } 1482 | if (recv && transceiver.rtpReceiver) { 1483 | params.encodings = transceiver.recvEncodingParameters; 1484 | params.rtcp = { 1485 | cname: transceiver.cname 1486 | }; 1487 | if (transceiver.sendEncodingParameters.length) { 1488 | params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc; 1489 | } 1490 | transceiver.rtpReceiver.receive(params); 1491 | } 1492 | }; 1493 | 1494 | window.RTCPeerConnection.prototype.setLocalDescription = 1495 | function(description) { 1496 | var self = this; 1497 | var sections; 1498 | var sessionpart; 1499 | if (description.type === 'offer') { 1500 | // FIXME: What was the purpose of this empty if statement? 1501 | // if (!this._pendingOffer) { 1502 | // } else { 1503 | if (this._pendingOffer) { 1504 | // VERY limited support for SDP munging. Limited to: 1505 | // * changing the order of codecs 1506 | sections = SDPUtils.splitSections(description.sdp); 1507 | sessionpart = sections.shift(); 1508 | sections.forEach(function(mediaSection, sdpMLineIndex) { 1509 | var caps = SDPUtils.parseRtpParameters(mediaSection); 1510 | self._pendingOffer[sdpMLineIndex].localCapabilities = caps; 1511 | }); 1512 | this.transceivers = this._pendingOffer; 1513 | delete this._pendingOffer; 1514 | } 1515 | } else if (description.type === 'answer') { 1516 | sections = SDPUtils.splitSections(self.remoteDescription.sdp); 1517 | sessionpart = sections.shift(); 1518 | var isIceLite = SDPUtils.matchPrefix(sessionpart, 1519 | 'a=ice-lite').length > 0; 1520 | sections.forEach(function(mediaSection, sdpMLineIndex) { 1521 | var transceiver = self.transceivers[sdpMLineIndex]; 1522 | var iceGatherer = transceiver.iceGatherer; 1523 | var iceTransport = transceiver.iceTransport; 1524 | var dtlsTransport = transceiver.dtlsTransport; 1525 | var localCapabilities = transceiver.localCapabilities; 1526 | var remoteCapabilities = transceiver.remoteCapabilities; 1527 | var rejected = mediaSection.split('\n', 1)[0] 1528 | .split(' ', 2)[1] === '0'; 1529 | 1530 | if (!rejected) { 1531 | var remoteIceParameters = SDPUtils.getIceParameters( 1532 | mediaSection, sessionpart); 1533 | if (isIceLite) { 1534 | var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:') 1535 | .map(function(cand) { 1536 | return SDPUtils.parseCandidate(cand); 1537 | }) 1538 | .filter(function(cand) { 1539 | return cand.component === '1'; 1540 | }); 1541 | // ice-lite only includes host candidates in the SDP so we can 1542 | // use setRemoteCandidates (which implies an 1543 | // RTCIceCandidateComplete) 1544 | if (cands.length) { 1545 | iceTransport.setRemoteCandidates(cands); 1546 | } 1547 | } 1548 | var remoteDtlsParameters = SDPUtils.getDtlsParameters( 1549 | mediaSection, sessionpart); 1550 | if (isIceLite) { 1551 | remoteDtlsParameters.role = 'server'; 1552 | } 1553 | 1554 | if (!self.usingBundle || sdpMLineIndex === 0) { 1555 | iceTransport.start(iceGatherer, remoteIceParameters, 1556 | isIceLite ? 'controlling' : 'controlled'); 1557 | dtlsTransport.start(remoteDtlsParameters); 1558 | } 1559 | 1560 | // Calculate intersection of capabilities. 1561 | var params = self._getCommonCapabilities(localCapabilities, 1562 | remoteCapabilities); 1563 | 1564 | // Start the RTCRtpSender. The RTCRtpReceiver for this 1565 | // transceiver has already been started in setRemoteDescription. 1566 | self._transceive(transceiver, 1567 | params.codecs.length > 0, 1568 | false); 1569 | } 1570 | }); 1571 | } 1572 | 1573 | this.localDescription = { 1574 | type: description.type, 1575 | sdp: description.sdp 1576 | }; 1577 | switch (description.type) { 1578 | case 'offer': 1579 | this._updateSignalingState('have-local-offer'); 1580 | break; 1581 | case 'answer': 1582 | this._updateSignalingState('stable'); 1583 | break; 1584 | default: 1585 | throw new TypeError('unsupported type "' + description.type + 1586 | '"'); 1587 | } 1588 | 1589 | // If a success callback was provided, emit ICE candidates after it 1590 | // has been executed. Otherwise, emit callback after the Promise is 1591 | // resolved. 1592 | var hasCallback = arguments.length > 1 && 1593 | typeof arguments[1] === 'function'; 1594 | if (hasCallback) { 1595 | var cb = arguments[1]; 1596 | window.setTimeout(function() { 1597 | cb(); 1598 | if (self.iceGatheringState === 'new') { 1599 | self.iceGatheringState = 'gathering'; 1600 | } 1601 | self._emitBufferedCandidates(); 1602 | }, 0); 1603 | } 1604 | var p = Promise.resolve(); 1605 | p.then(function() { 1606 | if (!hasCallback) { 1607 | if (self.iceGatheringState === 'new') { 1608 | self.iceGatheringState = 'gathering'; 1609 | } 1610 | // Usually candidates will be emitted earlier. 1611 | window.setTimeout(self._emitBufferedCandidates.bind(self), 500); 1612 | } 1613 | }); 1614 | return p; 1615 | }; 1616 | 1617 | window.RTCPeerConnection.prototype.setRemoteDescription = 1618 | function(description) { 1619 | var self = this; 1620 | var stream = new MediaStream(); 1621 | var receiverList = []; 1622 | var sections = SDPUtils.splitSections(description.sdp); 1623 | var sessionpart = sections.shift(); 1624 | var isIceLite = SDPUtils.matchPrefix(sessionpart, 1625 | 'a=ice-lite').length > 0; 1626 | this.usingBundle = SDPUtils.matchPrefix(sessionpart, 1627 | 'a=group:BUNDLE ').length > 0; 1628 | sections.forEach(function(mediaSection, sdpMLineIndex) { 1629 | var lines = SDPUtils.splitLines(mediaSection); 1630 | var mline = lines[0].substr(2).split(' '); 1631 | var kind = mline[0]; 1632 | var rejected = mline[1] === '0'; 1633 | var direction = SDPUtils.getDirection(mediaSection, sessionpart); 1634 | 1635 | var transceiver; 1636 | var iceGatherer; 1637 | var iceTransport; 1638 | var dtlsTransport; 1639 | var rtpSender; 1640 | var rtpReceiver; 1641 | var sendEncodingParameters; 1642 | var recvEncodingParameters; 1643 | var localCapabilities; 1644 | 1645 | var track; 1646 | // FIXME: ensure the mediaSection has rtcp-mux set. 1647 | var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection); 1648 | var remoteIceParameters; 1649 | var remoteDtlsParameters; 1650 | if (!rejected) { 1651 | remoteIceParameters = SDPUtils.getIceParameters(mediaSection, 1652 | sessionpart); 1653 | remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection, 1654 | sessionpart); 1655 | remoteDtlsParameters.role = 'client'; 1656 | } 1657 | recvEncodingParameters = 1658 | SDPUtils.parseRtpEncodingParameters(mediaSection); 1659 | 1660 | var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:'); 1661 | if (mid.length) { 1662 | mid = mid[0].substr(6); 1663 | } else { 1664 | mid = SDPUtils.generateIdentifier(); 1665 | } 1666 | 1667 | var cname; 1668 | // Gets the first SSRC. Note that with RTX there might be multiple 1669 | // SSRCs. 1670 | var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:') 1671 | .map(function(line) { 1672 | return SDPUtils.parseSsrcMedia(line); 1673 | }) 1674 | .filter(function(obj) { 1675 | return obj.attribute === 'cname'; 1676 | })[0]; 1677 | if (remoteSsrc) { 1678 | cname = remoteSsrc.value; 1679 | } 1680 | 1681 | var isComplete = SDPUtils.matchPrefix(mediaSection, 1682 | 'a=end-of-candidates', sessionpart).length > 0; 1683 | var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:') 1684 | .map(function(cand) { 1685 | return SDPUtils.parseCandidate(cand); 1686 | }) 1687 | .filter(function(cand) { 1688 | return cand.component === '1'; 1689 | }); 1690 | if (description.type === 'offer' && !rejected) { 1691 | var transports = self.usingBundle && sdpMLineIndex > 0 ? { 1692 | iceGatherer: self.transceivers[0].iceGatherer, 1693 | iceTransport: self.transceivers[0].iceTransport, 1694 | dtlsTransport: self.transceivers[0].dtlsTransport 1695 | } : self._createIceAndDtlsTransports(mid, sdpMLineIndex); 1696 | 1697 | if (isComplete) { 1698 | transports.iceTransport.setRemoteCandidates(cands); 1699 | } 1700 | 1701 | localCapabilities = RTCRtpReceiver.getCapabilities(kind); 1702 | sendEncodingParameters = [{ 1703 | ssrc: (2 * sdpMLineIndex + 2) * 1001 1704 | }]; 1705 | 1706 | rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind); 1707 | 1708 | track = rtpReceiver.track; 1709 | receiverList.push([track, rtpReceiver]); 1710 | // FIXME: not correct when there are multiple streams but that is 1711 | // not currently supported in this shim. 1712 | stream.addTrack(track); 1713 | 1714 | // FIXME: look at direction. 1715 | if (self.localStreams.length > 0 && 1716 | self.localStreams[0].getTracks().length >= sdpMLineIndex) { 1717 | var localTrack; 1718 | if (kind === 'audio') { 1719 | localTrack = self.localStreams[0].getAudioTracks()[0]; 1720 | } else if (kind === 'video') { 1721 | localTrack = self.localStreams[0].getVideoTracks()[0]; 1722 | } 1723 | if (localTrack) { 1724 | rtpSender = new RTCRtpSender(localTrack, 1725 | transports.dtlsTransport); 1726 | } 1727 | } 1728 | 1729 | self.transceivers[sdpMLineIndex] = { 1730 | iceGatherer: transports.iceGatherer, 1731 | iceTransport: transports.iceTransport, 1732 | dtlsTransport: transports.dtlsTransport, 1733 | localCapabilities: localCapabilities, 1734 | remoteCapabilities: remoteCapabilities, 1735 | rtpSender: rtpSender, 1736 | rtpReceiver: rtpReceiver, 1737 | kind: kind, 1738 | mid: mid, 1739 | cname: cname, 1740 | sendEncodingParameters: sendEncodingParameters, 1741 | recvEncodingParameters: recvEncodingParameters 1742 | }; 1743 | // Start the RTCRtpReceiver now. The RTPSender is started in 1744 | // setLocalDescription. 1745 | self._transceive(self.transceivers[sdpMLineIndex], 1746 | false, 1747 | direction === 'sendrecv' || direction === 'sendonly'); 1748 | } else if (description.type === 'answer' && !rejected) { 1749 | transceiver = self.transceivers[sdpMLineIndex]; 1750 | iceGatherer = transceiver.iceGatherer; 1751 | iceTransport = transceiver.iceTransport; 1752 | dtlsTransport = transceiver.dtlsTransport; 1753 | rtpSender = transceiver.rtpSender; 1754 | rtpReceiver = transceiver.rtpReceiver; 1755 | sendEncodingParameters = transceiver.sendEncodingParameters; 1756 | localCapabilities = transceiver.localCapabilities; 1757 | 1758 | self.transceivers[sdpMLineIndex].recvEncodingParameters = 1759 | recvEncodingParameters; 1760 | self.transceivers[sdpMLineIndex].remoteCapabilities = 1761 | remoteCapabilities; 1762 | self.transceivers[sdpMLineIndex].cname = cname; 1763 | 1764 | if ((isIceLite || isComplete) && cands.length) { 1765 | iceTransport.setRemoteCandidates(cands); 1766 | } 1767 | if (!self.usingBundle || sdpMLineIndex === 0) { 1768 | iceTransport.start(iceGatherer, remoteIceParameters, 1769 | 'controlling'); 1770 | dtlsTransport.start(remoteDtlsParameters); 1771 | } 1772 | 1773 | self._transceive(transceiver, 1774 | direction === 'sendrecv' || direction === 'recvonly', 1775 | direction === 'sendrecv' || direction === 'sendonly'); 1776 | 1777 | if (rtpReceiver && 1778 | (direction === 'sendrecv' || direction === 'sendonly')) { 1779 | track = rtpReceiver.track; 1780 | receiverList.push([track, rtpReceiver]); 1781 | stream.addTrack(track); 1782 | } else { 1783 | // FIXME: actually the receiver should be created later. 1784 | delete transceiver.rtpReceiver; 1785 | } 1786 | } 1787 | }); 1788 | 1789 | this.remoteDescription = { 1790 | type: description.type, 1791 | sdp: description.sdp 1792 | }; 1793 | switch (description.type) { 1794 | case 'offer': 1795 | this._updateSignalingState('have-remote-offer'); 1796 | break; 1797 | case 'answer': 1798 | this._updateSignalingState('stable'); 1799 | break; 1800 | default: 1801 | throw new TypeError('unsupported type "' + description.type + 1802 | '"'); 1803 | } 1804 | if (stream.getTracks().length) { 1805 | self.remoteStreams.push(stream); 1806 | window.setTimeout(function() { 1807 | var event = new Event('addstream'); 1808 | event.stream = stream; 1809 | self.dispatchEvent(event); 1810 | if (self.onaddstream !== null) { 1811 | window.setTimeout(function() { 1812 | self.onaddstream(event); 1813 | }, 0); 1814 | } 1815 | 1816 | receiverList.forEach(function(item) { 1817 | var track = item[0]; 1818 | var receiver = item[1]; 1819 | var trackEvent = new Event('track'); 1820 | trackEvent.track = track; 1821 | trackEvent.receiver = receiver; 1822 | trackEvent.streams = [stream]; 1823 | self.dispatchEvent(event); 1824 | if (self.ontrack !== null) { 1825 | window.setTimeout(function() { 1826 | self.ontrack(trackEvent); 1827 | }, 0); 1828 | } 1829 | }); 1830 | }, 0); 1831 | } 1832 | if (arguments.length > 1 && typeof arguments[1] === 'function') { 1833 | window.setTimeout(arguments[1], 0); 1834 | } 1835 | return Promise.resolve(); 1836 | }; 1837 | 1838 | window.RTCPeerConnection.prototype.close = function() { 1839 | this.transceivers.forEach(function(transceiver) { 1840 | /* not yet 1841 | if (transceiver.iceGatherer) { 1842 | transceiver.iceGatherer.close(); 1843 | } 1844 | */ 1845 | if (transceiver.iceTransport) { 1846 | transceiver.iceTransport.stop(); 1847 | } 1848 | if (transceiver.dtlsTransport) { 1849 | transceiver.dtlsTransport.stop(); 1850 | } 1851 | if (transceiver.rtpSender) { 1852 | transceiver.rtpSender.stop(); 1853 | } 1854 | if (transceiver.rtpReceiver) { 1855 | transceiver.rtpReceiver.stop(); 1856 | } 1857 | }); 1858 | // FIXME: clean up tracks, local streams, remote streams, etc 1859 | this._updateSignalingState('closed'); 1860 | }; 1861 | 1862 | // Update the signaling state. 1863 | window.RTCPeerConnection.prototype._updateSignalingState = 1864 | function(newState) { 1865 | this.signalingState = newState; 1866 | var event = new Event('signalingstatechange'); 1867 | this.dispatchEvent(event); 1868 | if (this.onsignalingstatechange !== null) { 1869 | this.onsignalingstatechange(event); 1870 | } 1871 | }; 1872 | 1873 | // Determine whether to fire the negotiationneeded event. 1874 | window.RTCPeerConnection.prototype._maybeFireNegotiationNeeded = 1875 | function() { 1876 | // Fire away (for now). 1877 | var event = new Event('negotiationneeded'); 1878 | this.dispatchEvent(event); 1879 | if (this.onnegotiationneeded !== null) { 1880 | this.onnegotiationneeded(event); 1881 | } 1882 | }; 1883 | 1884 | // Update the connection state. 1885 | window.RTCPeerConnection.prototype._updateConnectionState = function() { 1886 | var self = this; 1887 | var newState; 1888 | var states = { 1889 | 'new': 0, 1890 | closed: 0, 1891 | connecting: 0, 1892 | checking: 0, 1893 | connected: 0, 1894 | completed: 0, 1895 | failed: 0 1896 | }; 1897 | this.transceivers.forEach(function(transceiver) { 1898 | states[transceiver.iceTransport.state]++; 1899 | states[transceiver.dtlsTransport.state]++; 1900 | }); 1901 | // ICETransport.completed and connected are the same for this purpose. 1902 | states.connected += states.completed; 1903 | 1904 | newState = 'new'; 1905 | if (states.failed > 0) { 1906 | newState = 'failed'; 1907 | } else if (states.connecting > 0 || states.checking > 0) { 1908 | newState = 'connecting'; 1909 | } else if (states.disconnected > 0) { 1910 | newState = 'disconnected'; 1911 | } else if (states.new > 0) { 1912 | newState = 'new'; 1913 | } else if (states.connected > 0 || states.completed > 0) { 1914 | newState = 'connected'; 1915 | } 1916 | 1917 | if (newState !== self.iceConnectionState) { 1918 | self.iceConnectionState = newState; 1919 | var event = new Event('iceconnectionstatechange'); 1920 | this.dispatchEvent(event); 1921 | if (this.oniceconnectionstatechange !== null) { 1922 | this.oniceconnectionstatechange(event); 1923 | } 1924 | } 1925 | }; 1926 | 1927 | window.RTCPeerConnection.prototype.createOffer = function() { 1928 | var self = this; 1929 | if (this._pendingOffer) { 1930 | throw new Error('createOffer called while there is a pending offer.'); 1931 | } 1932 | var offerOptions; 1933 | if (arguments.length === 1 && typeof arguments[0] !== 'function') { 1934 | offerOptions = arguments[0]; 1935 | } else if (arguments.length === 3) { 1936 | offerOptions = arguments[2]; 1937 | } 1938 | 1939 | var tracks = []; 1940 | var numAudioTracks = 0; 1941 | var numVideoTracks = 0; 1942 | // Default to sendrecv. 1943 | if (this.localStreams.length) { 1944 | numAudioTracks = this.localStreams[0].getAudioTracks().length; 1945 | numVideoTracks = this.localStreams[0].getVideoTracks().length; 1946 | } 1947 | // Determine number of audio and video tracks we need to send/recv. 1948 | if (offerOptions) { 1949 | // Reject Chrome legacy constraints. 1950 | if (offerOptions.mandatory || offerOptions.optional) { 1951 | throw new TypeError( 1952 | 'Legacy mandatory/optional constraints not supported.'); 1953 | } 1954 | if (offerOptions.offerToReceiveAudio !== undefined) { 1955 | numAudioTracks = offerOptions.offerToReceiveAudio; 1956 | } 1957 | if (offerOptions.offerToReceiveVideo !== undefined) { 1958 | numVideoTracks = offerOptions.offerToReceiveVideo; 1959 | } 1960 | } 1961 | if (this.localStreams.length) { 1962 | // Push local streams. 1963 | this.localStreams[0].getTracks().forEach(function(track) { 1964 | tracks.push({ 1965 | kind: track.kind, 1966 | track: track, 1967 | wantReceive: track.kind === 'audio' ? 1968 | numAudioTracks > 0 : numVideoTracks > 0 1969 | }); 1970 | if (track.kind === 'audio') { 1971 | numAudioTracks--; 1972 | } else if (track.kind === 'video') { 1973 | numVideoTracks--; 1974 | } 1975 | }); 1976 | } 1977 | // Create M-lines for recvonly streams. 1978 | while (numAudioTracks > 0 || numVideoTracks > 0) { 1979 | if (numAudioTracks > 0) { 1980 | tracks.push({ 1981 | kind: 'audio', 1982 | wantReceive: true 1983 | }); 1984 | numAudioTracks--; 1985 | } 1986 | if (numVideoTracks > 0) { 1987 | tracks.push({ 1988 | kind: 'video', 1989 | wantReceive: true 1990 | }); 1991 | numVideoTracks--; 1992 | } 1993 | } 1994 | 1995 | var sdp = SDPUtils.writeSessionBoilerplate(); 1996 | var transceivers = []; 1997 | tracks.forEach(function(mline, sdpMLineIndex) { 1998 | // For each track, create an ice gatherer, ice transport, 1999 | // dtls transport, potentially rtpsender and rtpreceiver. 2000 | var track = mline.track; 2001 | var kind = mline.kind; 2002 | var mid = SDPUtils.generateIdentifier(); 2003 | 2004 | var transports = self.usingBundle && sdpMLineIndex > 0 ? { 2005 | iceGatherer: transceivers[0].iceGatherer, 2006 | iceTransport: transceivers[0].iceTransport, 2007 | dtlsTransport: transceivers[0].dtlsTransport 2008 | } : self._createIceAndDtlsTransports(mid, sdpMLineIndex); 2009 | 2010 | var localCapabilities = RTCRtpSender.getCapabilities(kind); 2011 | var rtpSender; 2012 | var rtpReceiver; 2013 | 2014 | // generate an ssrc now, to be used later in rtpSender.send 2015 | var sendEncodingParameters = [{ 2016 | ssrc: (2 * sdpMLineIndex + 1) * 1001 2017 | }]; 2018 | if (track) { 2019 | rtpSender = new RTCRtpSender(track, transports.dtlsTransport); 2020 | } 2021 | 2022 | if (mline.wantReceive) { 2023 | rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind); 2024 | } 2025 | 2026 | transceivers[sdpMLineIndex] = { 2027 | iceGatherer: transports.iceGatherer, 2028 | iceTransport: transports.iceTransport, 2029 | dtlsTransport: transports.dtlsTransport, 2030 | localCapabilities: localCapabilities, 2031 | remoteCapabilities: null, 2032 | rtpSender: rtpSender, 2033 | rtpReceiver: rtpReceiver, 2034 | kind: kind, 2035 | mid: mid, 2036 | sendEncodingParameters: sendEncodingParameters, 2037 | recvEncodingParameters: null 2038 | }; 2039 | }); 2040 | if (this.usingBundle) { 2041 | sdp += 'a=group:BUNDLE ' + transceivers.map(function(t) { 2042 | return t.mid; 2043 | }).join(' ') + '\r\n'; 2044 | } 2045 | tracks.forEach(function(mline, sdpMLineIndex) { 2046 | var transceiver = transceivers[sdpMLineIndex]; 2047 | sdp += SDPUtils.writeMediaSection(transceiver, 2048 | transceiver.localCapabilities, 'offer', self.localStreams[0]); 2049 | }); 2050 | 2051 | this._pendingOffer = transceivers; 2052 | var desc = new RTCSessionDescription({ 2053 | type: 'offer', 2054 | sdp: sdp 2055 | }); 2056 | if (arguments.length && typeof arguments[0] === 'function') { 2057 | window.setTimeout(arguments[0], 0, desc); 2058 | } 2059 | return Promise.resolve(desc); 2060 | }; 2061 | 2062 | window.RTCPeerConnection.prototype.createAnswer = function() { 2063 | var self = this; 2064 | 2065 | var sdp = SDPUtils.writeSessionBoilerplate(); 2066 | if (this.usingBundle) { 2067 | sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) { 2068 | return t.mid; 2069 | }).join(' ') + '\r\n'; 2070 | } 2071 | this.transceivers.forEach(function(transceiver) { 2072 | // Calculate intersection of capabilities. 2073 | var commonCapabilities = self._getCommonCapabilities( 2074 | transceiver.localCapabilities, 2075 | transceiver.remoteCapabilities); 2076 | 2077 | sdp += SDPUtils.writeMediaSection(transceiver, commonCapabilities, 2078 | 'answer', self.localStreams[0]); 2079 | }); 2080 | 2081 | var desc = new RTCSessionDescription({ 2082 | type: 'answer', 2083 | sdp: sdp 2084 | }); 2085 | if (arguments.length && typeof arguments[0] === 'function') { 2086 | window.setTimeout(arguments[0], 0, desc); 2087 | } 2088 | return Promise.resolve(desc); 2089 | }; 2090 | 2091 | window.RTCPeerConnection.prototype.addIceCandidate = function(candidate) { 2092 | if (candidate === null) { 2093 | this.transceivers.forEach(function(transceiver) { 2094 | transceiver.iceTransport.addRemoteCandidate({}); 2095 | }); 2096 | } else { 2097 | var mLineIndex = candidate.sdpMLineIndex; 2098 | if (candidate.sdpMid) { 2099 | for (var i = 0; i < this.transceivers.length; i++) { 2100 | if (this.transceivers[i].mid === candidate.sdpMid) { 2101 | mLineIndex = i; 2102 | break; 2103 | } 2104 | } 2105 | } 2106 | var transceiver = this.transceivers[mLineIndex]; 2107 | if (transceiver) { 2108 | var cand = Object.keys(candidate.candidate).length > 0 ? 2109 | SDPUtils.parseCandidate(candidate.candidate) : {}; 2110 | // Ignore Chrome's invalid candidates since Edge does not like them. 2111 | if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) { 2112 | return; 2113 | } 2114 | // Ignore RTCP candidates, we assume RTCP-MUX. 2115 | if (cand.component !== '1') { 2116 | return; 2117 | } 2118 | // A dirty hack to make samples work. 2119 | if (cand.type === 'endOfCandidates') { 2120 | cand = {}; 2121 | } 2122 | transceiver.iceTransport.addRemoteCandidate(cand); 2123 | 2124 | // update the remoteDescription. 2125 | var sections = SDPUtils.splitSections(this.remoteDescription.sdp); 2126 | sections[mLineIndex + 1] += (cand.type ? candidate.candidate.trim() 2127 | : 'a=end-of-candidates') + '\r\n'; 2128 | this.remoteDescription.sdp = sections.join(''); 2129 | } 2130 | } 2131 | if (arguments.length > 1 && typeof arguments[1] === 'function') { 2132 | window.setTimeout(arguments[1], 0); 2133 | } 2134 | return Promise.resolve(); 2135 | }; 2136 | 2137 | window.RTCPeerConnection.prototype.getStats = function() { 2138 | var promises = []; 2139 | this.transceivers.forEach(function(transceiver) { 2140 | ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport', 2141 | 'dtlsTransport'].forEach(function(method) { 2142 | if (transceiver[method]) { 2143 | promises.push(transceiver[method].getStats()); 2144 | } 2145 | }); 2146 | }); 2147 | var cb = arguments.length > 1 && typeof arguments[1] === 'function' && 2148 | arguments[1]; 2149 | return new Promise(function(resolve) { 2150 | // shim getStats with maplike support 2151 | var results = new Map(); 2152 | Promise.all(promises).then(function(res) { 2153 | res.forEach(function(result) { 2154 | Object.keys(result).forEach(function(id) { 2155 | results.set(id, result[id]); 2156 | results[id] = result[id]; 2157 | }); 2158 | }); 2159 | if (cb) { 2160 | window.setTimeout(cb, 0, results); 2161 | } 2162 | resolve(results); 2163 | }); 2164 | }); 2165 | }; 2166 | } 2167 | }; 2168 | 2169 | // Expose public methods. 2170 | module.exports = { 2171 | shimPeerConnection: edgeShim.shimPeerConnection, 2172 | shimGetUserMedia: require('./getusermedia') 2173 | }; 2174 | 2175 | },{"../utils":11,"./getusermedia":7,"sdp":2}],7:[function(require,module,exports){ 2176 | /* 2177 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 2178 | * 2179 | * Use of this source code is governed by a BSD-style license 2180 | * that can be found in the LICENSE file in the root of the source 2181 | * tree. 2182 | */ 2183 | /* eslint-env node */ 2184 | 'use strict'; 2185 | 2186 | // Expose public methods. 2187 | module.exports = function() { 2188 | var shimError_ = function(e) { 2189 | return { 2190 | name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name, 2191 | message: e.message, 2192 | constraint: e.constraint, 2193 | toString: function() { 2194 | return this.name; 2195 | } 2196 | }; 2197 | }; 2198 | 2199 | // getUserMedia error shim. 2200 | var origGetUserMedia = navigator.mediaDevices.getUserMedia. 2201 | bind(navigator.mediaDevices); 2202 | navigator.mediaDevices.getUserMedia = function(c) { 2203 | return origGetUserMedia(c).catch(function(e) { 2204 | return Promise.reject(shimError_(e)); 2205 | }); 2206 | }; 2207 | }; 2208 | 2209 | },{}],8:[function(require,module,exports){ 2210 | /* 2211 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 2212 | * 2213 | * Use of this source code is governed by a BSD-style license 2214 | * that can be found in the LICENSE file in the root of the source 2215 | * tree. 2216 | */ 2217 | /* eslint-env node */ 2218 | 'use strict'; 2219 | 2220 | var browserDetails = require('../utils').browserDetails; 2221 | 2222 | var firefoxShim = { 2223 | shimOnTrack: function() { 2224 | if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in 2225 | window.RTCPeerConnection.prototype)) { 2226 | Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', { 2227 | get: function() { 2228 | return this._ontrack; 2229 | }, 2230 | set: function(f) { 2231 | if (this._ontrack) { 2232 | this.removeEventListener('track', this._ontrack); 2233 | this.removeEventListener('addstream', this._ontrackpoly); 2234 | } 2235 | this.addEventListener('track', this._ontrack = f); 2236 | this.addEventListener('addstream', this._ontrackpoly = function(e) { 2237 | e.stream.getTracks().forEach(function(track) { 2238 | var event = new Event('track'); 2239 | event.track = track; 2240 | event.receiver = {track: track}; 2241 | event.streams = [e.stream]; 2242 | this.dispatchEvent(event); 2243 | }.bind(this)); 2244 | }.bind(this)); 2245 | } 2246 | }); 2247 | } 2248 | }, 2249 | 2250 | shimSourceObject: function() { 2251 | // Firefox has supported mozSrcObject since FF22, unprefixed in 42. 2252 | if (typeof window === 'object') { 2253 | if (window.HTMLMediaElement && 2254 | !('srcObject' in window.HTMLMediaElement.prototype)) { 2255 | // Shim the srcObject property, once, when HTMLMediaElement is found. 2256 | Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', { 2257 | get: function() { 2258 | return this.mozSrcObject; 2259 | }, 2260 | set: function(stream) { 2261 | this.mozSrcObject = stream; 2262 | } 2263 | }); 2264 | } 2265 | } 2266 | }, 2267 | 2268 | shimPeerConnection: function() { 2269 | if (typeof window !== 'object' || !(window.RTCPeerConnection || 2270 | window.mozRTCPeerConnection)) { 2271 | return; // probably media.peerconnection.enabled=false in about:config 2272 | } 2273 | // The RTCPeerConnection object. 2274 | if (!window.RTCPeerConnection) { 2275 | window.RTCPeerConnection = function(pcConfig, pcConstraints) { 2276 | if (browserDetails.version < 38) { 2277 | // .urls is not supported in FF < 38. 2278 | // create RTCIceServers with a single url. 2279 | if (pcConfig && pcConfig.iceServers) { 2280 | var newIceServers = []; 2281 | for (var i = 0; i < pcConfig.iceServers.length; i++) { 2282 | var server = pcConfig.iceServers[i]; 2283 | if (server.hasOwnProperty('urls')) { 2284 | for (var j = 0; j < server.urls.length; j++) { 2285 | var newServer = { 2286 | url: server.urls[j] 2287 | }; 2288 | if (server.urls[j].indexOf('turn') === 0) { 2289 | newServer.username = server.username; 2290 | newServer.credential = server.credential; 2291 | } 2292 | newIceServers.push(newServer); 2293 | } 2294 | } else { 2295 | newIceServers.push(pcConfig.iceServers[i]); 2296 | } 2297 | } 2298 | pcConfig.iceServers = newIceServers; 2299 | } 2300 | } 2301 | return new mozRTCPeerConnection(pcConfig, pcConstraints); 2302 | }; 2303 | window.RTCPeerConnection.prototype = mozRTCPeerConnection.prototype; 2304 | 2305 | // wrap static methods. Currently just generateCertificate. 2306 | if (mozRTCPeerConnection.generateCertificate) { 2307 | Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', { 2308 | get: function() { 2309 | return mozRTCPeerConnection.generateCertificate; 2310 | } 2311 | }); 2312 | } 2313 | 2314 | window.RTCSessionDescription = mozRTCSessionDescription; 2315 | window.RTCIceCandidate = mozRTCIceCandidate; 2316 | } 2317 | 2318 | // shim away need for obsolete RTCIceCandidate/RTCSessionDescription. 2319 | ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'] 2320 | .forEach(function(method) { 2321 | var nativeMethod = RTCPeerConnection.prototype[method]; 2322 | RTCPeerConnection.prototype[method] = function() { 2323 | arguments[0] = new ((method === 'addIceCandidate') ? 2324 | RTCIceCandidate : RTCSessionDescription)(arguments[0]); 2325 | return nativeMethod.apply(this, arguments); 2326 | }; 2327 | }); 2328 | 2329 | // support for addIceCandidate(null) 2330 | var nativeAddIceCandidate = 2331 | RTCPeerConnection.prototype.addIceCandidate; 2332 | RTCPeerConnection.prototype.addIceCandidate = function() { 2333 | return arguments[0] === null ? Promise.resolve() 2334 | : nativeAddIceCandidate.apply(this, arguments); 2335 | }; 2336 | 2337 | // shim getStats with maplike support 2338 | var makeMapStats = function(stats) { 2339 | var map = new Map(); 2340 | Object.keys(stats).forEach(function(key) { 2341 | map.set(key, stats[key]); 2342 | map[key] = stats[key]; 2343 | }); 2344 | return map; 2345 | }; 2346 | 2347 | var nativeGetStats = RTCPeerConnection.prototype.getStats; 2348 | RTCPeerConnection.prototype.getStats = function(selector, onSucc, onErr) { 2349 | return nativeGetStats.apply(this, [selector || null]) 2350 | .then(function(stats) { 2351 | return makeMapStats(stats); 2352 | }) 2353 | .then(onSucc, onErr); 2354 | }; 2355 | } 2356 | }; 2357 | 2358 | // Expose public methods. 2359 | module.exports = { 2360 | shimOnTrack: firefoxShim.shimOnTrack, 2361 | shimSourceObject: firefoxShim.shimSourceObject, 2362 | shimPeerConnection: firefoxShim.shimPeerConnection, 2363 | shimGetUserMedia: require('./getusermedia') 2364 | }; 2365 | 2366 | },{"../utils":11,"./getusermedia":9}],9:[function(require,module,exports){ 2367 | /* 2368 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 2369 | * 2370 | * Use of this source code is governed by a BSD-style license 2371 | * that can be found in the LICENSE file in the root of the source 2372 | * tree. 2373 | */ 2374 | /* eslint-env node */ 2375 | 'use strict'; 2376 | 2377 | var logging = require('../utils').log; 2378 | var browserDetails = require('../utils').browserDetails; 2379 | 2380 | // Expose public methods. 2381 | module.exports = function() { 2382 | var shimError_ = function(e) { 2383 | return { 2384 | name: { 2385 | SecurityError: 'NotAllowedError', 2386 | PermissionDeniedError: 'NotAllowedError' 2387 | }[e.name] || e.name, 2388 | message: { 2389 | 'The operation is insecure.': 'The request is not allowed by the ' + 2390 | 'user agent or the platform in the current context.' 2391 | }[e.message] || e.message, 2392 | constraint: e.constraint, 2393 | toString: function() { 2394 | return this.name + (this.message && ': ') + this.message; 2395 | } 2396 | }; 2397 | }; 2398 | 2399 | // getUserMedia constraints shim. 2400 | var getUserMedia_ = function(constraints, onSuccess, onError) { 2401 | var constraintsToFF37_ = function(c) { 2402 | if (typeof c !== 'object' || c.require) { 2403 | return c; 2404 | } 2405 | var require = []; 2406 | Object.keys(c).forEach(function(key) { 2407 | if (key === 'require' || key === 'advanced' || key === 'mediaSource') { 2408 | return; 2409 | } 2410 | var r = c[key] = (typeof c[key] === 'object') ? 2411 | c[key] : {ideal: c[key]}; 2412 | if (r.min !== undefined || 2413 | r.max !== undefined || r.exact !== undefined) { 2414 | require.push(key); 2415 | } 2416 | if (r.exact !== undefined) { 2417 | if (typeof r.exact === 'number') { 2418 | r. min = r.max = r.exact; 2419 | } else { 2420 | c[key] = r.exact; 2421 | } 2422 | delete r.exact; 2423 | } 2424 | if (r.ideal !== undefined) { 2425 | c.advanced = c.advanced || []; 2426 | var oc = {}; 2427 | if (typeof r.ideal === 'number') { 2428 | oc[key] = {min: r.ideal, max: r.ideal}; 2429 | } else { 2430 | oc[key] = r.ideal; 2431 | } 2432 | c.advanced.push(oc); 2433 | delete r.ideal; 2434 | if (!Object.keys(r).length) { 2435 | delete c[key]; 2436 | } 2437 | } 2438 | }); 2439 | if (require.length) { 2440 | c.require = require; 2441 | } 2442 | return c; 2443 | }; 2444 | constraints = JSON.parse(JSON.stringify(constraints)); 2445 | if (browserDetails.version < 38) { 2446 | logging('spec: ' + JSON.stringify(constraints)); 2447 | if (constraints.audio) { 2448 | constraints.audio = constraintsToFF37_(constraints.audio); 2449 | } 2450 | if (constraints.video) { 2451 | constraints.video = constraintsToFF37_(constraints.video); 2452 | } 2453 | logging('ff37: ' + JSON.stringify(constraints)); 2454 | } 2455 | return navigator.mozGetUserMedia(constraints, onSuccess, function(e) { 2456 | onError(shimError_(e)); 2457 | }); 2458 | }; 2459 | 2460 | // Returns the result of getUserMedia as a Promise. 2461 | var getUserMediaPromise_ = function(constraints) { 2462 | return new Promise(function(resolve, reject) { 2463 | getUserMedia_(constraints, resolve, reject); 2464 | }); 2465 | }; 2466 | 2467 | // Shim for mediaDevices on older versions. 2468 | if (!navigator.mediaDevices) { 2469 | navigator.mediaDevices = {getUserMedia: getUserMediaPromise_, 2470 | addEventListener: function() { }, 2471 | removeEventListener: function() { } 2472 | }; 2473 | } 2474 | navigator.mediaDevices.enumerateDevices = 2475 | navigator.mediaDevices.enumerateDevices || function() { 2476 | return new Promise(function(resolve) { 2477 | var infos = [ 2478 | {kind: 'audioinput', deviceId: 'default', label: '', groupId: ''}, 2479 | {kind: 'videoinput', deviceId: 'default', label: '', groupId: ''} 2480 | ]; 2481 | resolve(infos); 2482 | }); 2483 | }; 2484 | 2485 | if (browserDetails.version < 41) { 2486 | // Work around http://bugzil.la/1169665 2487 | var orgEnumerateDevices = 2488 | navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices); 2489 | navigator.mediaDevices.enumerateDevices = function() { 2490 | return orgEnumerateDevices().then(undefined, function(e) { 2491 | if (e.name === 'NotFoundError') { 2492 | return []; 2493 | } 2494 | throw e; 2495 | }); 2496 | }; 2497 | } 2498 | if (browserDetails.version < 49) { 2499 | var origGetUserMedia = navigator.mediaDevices.getUserMedia. 2500 | bind(navigator.mediaDevices); 2501 | navigator.mediaDevices.getUserMedia = function(c) { 2502 | return origGetUserMedia(c).catch(function(e) { 2503 | return Promise.reject(shimError_(e)); 2504 | }); 2505 | }; 2506 | } 2507 | navigator.getUserMedia = function(constraints, onSuccess, onError) { 2508 | if (browserDetails.version < 44) { 2509 | return getUserMedia_(constraints, onSuccess, onError); 2510 | } 2511 | // Replace Firefox 44+'s deprecation warning with unprefixed version. 2512 | console.warn('navigator.getUserMedia has been replaced by ' + 2513 | 'navigator.mediaDevices.getUserMedia'); 2514 | navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError); 2515 | }; 2516 | }; 2517 | 2518 | },{"../utils":11}],10:[function(require,module,exports){ 2519 | /* 2520 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 2521 | * 2522 | * Use of this source code is governed by a BSD-style license 2523 | * that can be found in the LICENSE file in the root of the source 2524 | * tree. 2525 | */ 2526 | 'use strict'; 2527 | var safariShim = { 2528 | // TODO: DrAlex, should be here, double check against LayoutTests 2529 | // shimOnTrack: function() { }, 2530 | 2531 | // TODO: once the back-end for the mac port is done, add. 2532 | // TODO: check for webkitGTK+ 2533 | // shimPeerConnection: function() { }, 2534 | 2535 | shimGetUserMedia: function() { 2536 | navigator.getUserMedia = navigator.webkitGetUserMedia; 2537 | } 2538 | }; 2539 | 2540 | // Expose public methods. 2541 | module.exports = { 2542 | shimGetUserMedia: safariShim.shimGetUserMedia 2543 | // TODO 2544 | // shimOnTrack: safariShim.shimOnTrack, 2545 | // shimPeerConnection: safariShim.shimPeerConnection 2546 | }; 2547 | 2548 | },{}],11:[function(require,module,exports){ 2549 | /* 2550 | * Copyright (c) 2016 The WebRTC project authors. All Rights Reserved. 2551 | * 2552 | * Use of this source code is governed by a BSD-style license 2553 | * that can be found in the LICENSE file in the root of the source 2554 | * tree. 2555 | */ 2556 | /* eslint-env node */ 2557 | 'use strict'; 2558 | 2559 | var logDisabled_ = true; 2560 | 2561 | // Utility methods. 2562 | var utils = { 2563 | disableLog: function(bool) { 2564 | if (typeof bool !== 'boolean') { 2565 | return new Error('Argument type: ' + typeof bool + 2566 | '. Please use a boolean.'); 2567 | } 2568 | logDisabled_ = bool; 2569 | return (bool) ? 'adapter.js logging disabled' : 2570 | 'adapter.js logging enabled'; 2571 | }, 2572 | 2573 | log: function() { 2574 | if (typeof window === 'object') { 2575 | if (logDisabled_) { 2576 | return; 2577 | } 2578 | if (typeof console !== 'undefined' && typeof console.log === 'function') { 2579 | console.log.apply(console, arguments); 2580 | } 2581 | } 2582 | }, 2583 | 2584 | /** 2585 | * Extract browser version out of the provided user agent string. 2586 | * 2587 | * @param {!string} uastring userAgent string. 2588 | * @param {!string} expr Regular expression used as match criteria. 2589 | * @param {!number} pos position in the version string to be returned. 2590 | * @return {!number} browser version. 2591 | */ 2592 | extractVersion: function(uastring, expr, pos) { 2593 | var match = uastring.match(expr); 2594 | return match && match.length >= pos && parseInt(match[pos], 10); 2595 | }, 2596 | 2597 | /** 2598 | * Browser detector. 2599 | * 2600 | * @return {object} result containing browser and version 2601 | * properties. 2602 | */ 2603 | detectBrowser: function() { 2604 | // Returned result object. 2605 | var result = {}; 2606 | result.browser = null; 2607 | result.version = null; 2608 | 2609 | // Fail early if it's not a browser 2610 | if (typeof window === 'undefined' || !window.navigator) { 2611 | result.browser = 'Not a browser.'; 2612 | return result; 2613 | } 2614 | 2615 | // Firefox. 2616 | if (navigator.mozGetUserMedia) { 2617 | result.browser = 'firefox'; 2618 | result.version = this.extractVersion(navigator.userAgent, 2619 | /Firefox\/([0-9]+)\./, 1); 2620 | 2621 | // all webkit-based browsers 2622 | } else if (navigator.webkitGetUserMedia) { 2623 | // Chrome, Chromium, Webview, Opera, all use the chrome shim for now 2624 | if (window.webkitRTCPeerConnection) { 2625 | result.browser = 'chrome'; 2626 | result.version = this.extractVersion(navigator.userAgent, 2627 | /Chrom(e|ium)\/([0-9]+)\./, 2); 2628 | 2629 | // Safari or unknown webkit-based 2630 | // for the time being Safari has support for MediaStreams but not webRTC 2631 | } else { 2632 | // Safari UA substrings of interest for reference: 2633 | // - webkit version: AppleWebKit/602.1.25 (also used in Op,Cr) 2634 | // - safari UI version: Version/9.0.3 (unique to Safari) 2635 | // - safari UI webkit version: Safari/601.4.4 (also used in Op,Cr) 2636 | // 2637 | // if the webkit version and safari UI webkit versions are equals, 2638 | // ... this is a stable version. 2639 | // 2640 | // only the internal webkit version is important today to know if 2641 | // media streams are supported 2642 | // 2643 | if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) { 2644 | result.browser = 'safari'; 2645 | result.version = this.extractVersion(navigator.userAgent, 2646 | /AppleWebKit\/([0-9]+)\./, 1); 2647 | 2648 | // unknown webkit-based browser 2649 | } else { 2650 | result.browser = 'Unsupported webkit-based browser ' + 2651 | 'with GUM support but no WebRTC support.'; 2652 | return result; 2653 | } 2654 | } 2655 | 2656 | // Edge. 2657 | } else if (navigator.mediaDevices && 2658 | navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) { 2659 | result.browser = 'edge'; 2660 | result.version = this.extractVersion(navigator.userAgent, 2661 | /Edge\/(\d+).(\d+)$/, 2); 2662 | 2663 | // Default fallthrough: not supported. 2664 | } else { 2665 | result.browser = 'Not a supported browser.'; 2666 | return result; 2667 | } 2668 | 2669 | return result; 2670 | } 2671 | }; 2672 | 2673 | // Export. 2674 | module.exports = { 2675 | log: utils.log, 2676 | disableLog: utils.disableLog, 2677 | browserDetails: utils.detectBrowser(), 2678 | extractVersion: utils.extractVersion 2679 | }; 2680 | 2681 | },{}]},{},[1])(1) 2682 | }); -------------------------------------------------------------------------------- /getusermedia.js: -------------------------------------------------------------------------------- 1 | // getUserMedia helper by @HenrikJoreteg used for navigator.getUserMedia shim 2 | var adapter = require('webrtc-adapter'); 3 | 4 | module.exports = function (constraints, cb) { 5 | var error; 6 | var haveOpts = arguments.length === 2; 7 | var defaultOpts = {video: true, audio: true}; 8 | 9 | var denied = 'PermissionDeniedError'; 10 | var altDenied = 'PERMISSION_DENIED'; 11 | var notSatisfied = 'ConstraintNotSatisfiedError'; 12 | 13 | // make constraints optional 14 | if (!haveOpts) { 15 | cb = constraints; 16 | constraints = defaultOpts; 17 | } 18 | 19 | // treat lack of browser support like an error 20 | if (typeof navigator === 'undefined' || !navigator.getUserMedia) { 21 | // throw proper error per spec 22 | error = new Error('MediaStreamError'); 23 | error.name = 'NotSupportedError'; 24 | 25 | // keep all callbacks async 26 | return setTimeout(function () { 27 | cb(error); 28 | }, 0); 29 | } 30 | 31 | // normalize error handling when no media types are requested 32 | if (!constraints.audio && !constraints.video) { 33 | error = new Error('MediaStreamError'); 34 | error.name = 'NoMediaRequestedError'; 35 | 36 | // keep all callbacks async 37 | return setTimeout(function () { 38 | cb(error); 39 | }, 0); 40 | } 41 | 42 | navigator.mediaDevices.getUserMedia(constraints) 43 | .then(function (stream) { 44 | cb(null, stream); 45 | }).catch(function (err) { 46 | var error; 47 | // coerce into an error object since FF gives us a string 48 | // there are only two valid names according to the spec 49 | // we coerce all non-denied to "constraint not satisfied". 50 | if (typeof err === 'string') { 51 | error = new Error('MediaStreamError'); 52 | if (err === denied || err === altDenied) { 53 | error.name = denied; 54 | } else { 55 | error.name = notSatisfied; 56 | } 57 | } else { 58 | // if we get an error object make sure '.name' property is set 59 | // according to spec: http://dev.w3.org/2011/webrtc/editor/getusermedia.html#navigatorusermediaerror-and-navigatorusermediaerrorcallback 60 | error = err; 61 | if (!error.name) { 62 | // this is likely chrome which 63 | // sets a property called "ERROR_DENIED" on the error object 64 | // if so we make sure to set a name 65 | if (error[denied]) { 66 | err.name = denied; 67 | } else { 68 | err.name = notSatisfied; 69 | } 70 | } 71 | } 72 | 73 | cb(error); 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getusermedia", 3 | "description": "cross-browser getUserMedia shim with node.js style error-first API.", 4 | "version": "2.0.1", 5 | "author": "Henrik Joreteg ", 6 | "devDependencies": { 7 | "browserify": "^13.0.1", 8 | "precommit-hook": "^3.0.0", 9 | "tape": "^4.0.0", 10 | "travis-multirunner": "^3.0.0", 11 | "testling": "^1.7.1" 12 | }, 13 | "keywords": [ 14 | "browser", 15 | "getUserMedia", 16 | "WebRTC" 17 | ], 18 | "license": "MIT", 19 | "dependencies": { 20 | "webrtc-adapter": "^2.0.2" 21 | }, 22 | "testling": { 23 | "files": "test/test.js" 24 | }, 25 | "scripts": { 26 | "test-travis": "test/run-tests", 27 | "lint": "jshint .", 28 | "validate": "npm ls" 29 | }, 30 | "main": "getusermedia.js", 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/otalk/getUserMedia" 34 | }, 35 | "pre-commit": [ 36 | "lint", 37 | "validate", 38 | "test" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /test/run-tests: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Run testling with a default set of parameters 4 | # 5 | BINDIR=./browsers/bin 6 | export BROWSER=${BROWSER-chrome} 7 | export BVER=${BVER-stable} 8 | BROWSERBIN=$BINDIR/$BROWSER-$BVER 9 | if [ ! -x $BROWSERBIN ]; then 10 | echo "Installing browser" 11 | ./node_modules/travis-multirunner/setup.sh 12 | fi 13 | echo "Starting browser" 14 | PATH=$PATH:./node_modules/.bin 15 | 16 | testling -x start-${BROWSER} 17 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // This is a basic test file for use with testling. 3 | // The test script language comes from tape. 4 | /* jshint node: true */ 5 | /* global Promise */ 6 | var test = require('tape'); 7 | 8 | var getUserMedia = require('../getusermedia'); 9 | 10 | test('getUserMedia audio-only', function (t) { 11 | var constraints = {audio: true, fake: true}; 12 | getUserMedia(constraints, function (err, stream) { 13 | if (err) { 14 | t.fail(err.toString()); 15 | return; 16 | } 17 | t.pass('got stream'); 18 | t.end(); 19 | }); 20 | }); 21 | 22 | test('getUserMedia video-only', function (t) { 23 | var constraints = {video: true, fake: true}; 24 | getUserMedia(constraints, function (err, stream) { 25 | if (err) { 26 | t.fail(err.toString()); 27 | return; 28 | } 29 | t.pass('got stream'); 30 | t.end(); 31 | }); 32 | }); 33 | 34 | test('getUserMedia audio-video', function (t) { 35 | var constraints = {audio: true, video: true, fake: true}; 36 | getUserMedia(constraints, function (err, stream) { 37 | if (err) { 38 | t.fail(err.toString()); 39 | return; 40 | } 41 | t.pass('got stream'); 42 | t.end(); 43 | }); 44 | }); 45 | --------------------------------------------------------------------------------