123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579 |
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
- // WebRTC support -- Note that this relies on parts of the interface code that usually goes in ui.js
- define(["require", "jquery", "util", "session", "ui", "peers", "storage", "windowing"], function (require, $, util, session, ui, peers, storage, windowing) {
- var webrtc = util.Module("webrtc");
- var assert = util.assert;
- session.RTCSupported = !!(window.mozRTCPeerConnection ||
- window.webkitRTCPeerConnection ||
- window.RTCPeerConnection);
- if (session.RTCSupported && $.browser.mozilla && parseInt($.browser.version, 10) <= 19) {
- // In a few versions of Firefox (18 and 19) these APIs are present but
- // not actually usable
- // See: https://bugzilla.mozilla.org/show_bug.cgi?id=828839
- // Because they could be pref'd on we'll do a quick check:
- try {
- (function () {
- var conn = new window.mozRTCPeerConnection();
- })();
- } catch (e) {
- session.RTCSupported = false;
- }
- }
- var mediaConstraints = {
- mandatory: {
- OfferToReceiveAudio: true,
- OfferToReceiveVideo: false
- }
- };
- if (window.mozRTCPeerConnection) {
- mediaConstraints.mandatory.MozDontOfferDataChannel = true;
- }
- var URL = window.webkitURL || window.URL;
- var RTCSessionDescription = window.mozRTCSessionDescription || window.webkitRTCSessionDescription || window.RTCSessionDescription;
- var RTCIceCandidate = window.mozRTCIceCandidate || window.webkitRTCIceCandidate || window.RTCIceCandidate;
- function makePeerConnection() {
- // Based roughly off: https://github.com/firebase/gupshup/blob/gh-pages/js/chat.js
- if (window.webkitRTCPeerConnection) {
- return new webkitRTCPeerConnection({
- "iceServers": [{"url": "stun:stun.l.google.com:19302"}]
- }, {
- "optional": [{"DtlsSrtpKeyAgreement": true}]
- });
- }
- if (window.mozRTCPeerConnection) {
- return new mozRTCPeerConnection({
- // Or stun:124.124.124..2 ?
- "iceServers": [{"url": "stun:23.21.150.121"}]
- }, {
- "optional": []
- });
- }
- throw new util.AssertionError("Called makePeerConnection() without supported connection");
- }
- function ensureCryptoLine(sdp) {
- if (! window.mozRTCPeerConnection) {
- return sdp;
- }
- var sdpLinesIn = sdp.split('\r\n');
- var sdpLinesOut = [];
- // Search for m line.
- for (var i = 0; i < sdpLinesIn.length; i++) {
- sdpLinesOut.push(sdpLinesIn[i]);
- if (sdpLinesIn[i].search('m=') !== -1) {
- sdpLinesOut.push("a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
- }
- }
- sdp = sdpLinesOut.join('\r\n');
- return sdp;
- }
- function getUserMedia(options, success, failure) {
- failure = failure || function (error) {
- console.error("Error in getUserMedia:", error);
- };
- (navigator.getUserMedia ||
- navigator.mozGetUserMedia ||
- navigator.webkitGetUserMedia ||
- navigator.msGetUserMedia).call(navigator, options, success, failure);
- }
- /****************************************
- * getUserMedia Avatar support
- */
- session.on("ui-ready", function () {
- $("#togetherjs-self-avatar").click(function () {
- var avatar = peers.Self.avatar;
- if (avatar) {
- $preview.attr("src", avatar);
- }
- ui.displayToggle("#togetherjs-avatar-edit");
- });
- if (! session.RTCSupported) {
- $("#togetherjs-avatar-edit-rtc").hide();
- }
- var avatarData = null;
- var $preview = $("#togetherjs-self-avatar-preview");
- var $accept = $("#togetherjs-self-avatar-accept");
- var $cancel = $("#togetherjs-self-avatar-cancel");
- var $takePic = $("#togetherjs-avatar-use-camera");
- var $video = $("#togetherjs-avatar-video");
- var $upload = $("#togetherjs-avatar-upload");
- $takePic.click(function () {
- if (! streaming) {
- startStreaming();
- return;
- }
- takePicture();
- });
- function savePicture(dataUrl) {
- avatarData = dataUrl;
- $preview.attr("src", avatarData);
- $accept.attr("disabled", null);
- }
- $accept.click(function () {
- peers.Self.update({avatar: avatarData});
- ui.displayToggle("#togetherjs-no-avatar-edit");
- // FIXME: these probably shouldn't be two elements:
- $("#togetherjs-participants-other").show();
- $accept.attr("disabled", "1");
- });
- $cancel.click(function () {
- ui.displayToggle("#togetherjs-no-avatar-edit");
- // FIXME: like above:
- $("#togetherjs-participants-other").show();
- });
- var streaming = false;
- function startStreaming() {
- getUserMedia({
- video: true,
- audio: false
- },
- function(stream) {
- streaming = true;
- $video[0].src = URL.createObjectURL(stream);
- $video[0].play();
- },
- function(err) {
- // FIXME: should pop up help or something in the case of a user
- // cancel
- console.error("getUserMedia error:", err);
- }
- );
- }
- function takePicture() {
- assert(streaming);
- var height = $video[0].videoHeight;
- var width = $video[0].videoWidth;
- width = width * (session.AVATAR_SIZE / height);
- height = session.AVATAR_SIZE;
- var $canvas = $("<canvas>");
- $canvas[0].height = session.AVATAR_SIZE;
- $canvas[0].width = session.AVATAR_SIZE;
- var context = $canvas[0].getContext("2d");
- context.arc(session.AVATAR_SIZE/2, session.AVATAR_SIZE/2, session.AVATAR_SIZE/2, 0, Math.PI*2);
- context.closePath();
- context.clip();
- context.drawImage($video[0], (session.AVATAR_SIZE - width) / 2, 0, width, height);
- savePicture($canvas[0].toDataURL("image/png"));
- }
- $upload.on("change", function () {
- var reader = new FileReader();
- reader.onload = function () {
- // FIXME: I don't actually know it's JPEG, but it's probably a
- // good enough guess:
- var url = "data:image/jpeg;base64," + util.blobToBase64(this.result);
- convertImage(url, function (result) {
- savePicture(result);
- });
- };
- reader.onerror = function () {
- console.error("Error reading file:", this.error);
- };
- reader.readAsArrayBuffer(this.files[0]);
- });
- function convertImage(imageUrl, callback) {
- var $canvas = $("<canvas>");
- $canvas[0].height = session.AVATAR_SIZE;
- $canvas[0].width = session.AVATAR_SIZE;
- var context = $canvas[0].getContext("2d");
- var img = new Image();
- img.src = imageUrl;
- // Sometimes the DOM updates immediately to call
- // naturalWidth/etc, and sometimes it doesn't; using setTimeout
- // gives it a chance to catch up
- setTimeout(function () {
- var width = img.naturalWidth || img.width;
- var height = img.naturalHeight || img.height;
- width = width * (session.AVATAR_SIZE / height);
- height = session.AVATAR_SIZE;
- context.drawImage(img, 0, 0, width, height);
- callback($canvas[0].toDataURL("image/png"));
- });
- }
- });
- /****************************************
- * RTC support
- */
- function audioButton(selector) {
- ui.displayToggle(selector);
- if (selector == "#togetherjs-audio-incoming") {
- $("#togetherjs-audio-button").addClass("togetherjs-animated").addClass("togetherjs-color-alert");
- } else {
- $("#togetherjs-audio-button").removeClass("togetherjs-animated").removeClass("togetherjs-color-alert");
- }
- }
- session.on("ui-ready", function () {
- $("#togetherjs-audio-button").click(function () {
- if ($("#togetherjs-rtc-info").is(":visible")) {
- windowing.hide();
- return;
- }
- if (session.RTCSupported) {
- enableAudio();
- } else {
- windowing.show("#togetherjs-rtc-not-supported");
- }
- });
- if (! session.RTCSupported) {
- audioButton("#togetherjs-audio-unavailable");
- return;
- }
- audioButton("#togetherjs-audio-ready");
- var audioStream = null;
- var accepted = false;
- var connected = false;
- var $audio = $("#togetherjs-audio-element");
- var offerSent = null;
- var offerReceived = null;
- var offerDescription = false;
- var answerSent = null;
- var answerReceived = null;
- var answerDescription = false;
- var _connection = null;
- var iceCandidate = null;
- function enableAudio() {
- accepted = true;
- storage.settings.get("dontShowRtcInfo").then(function (dontShow) {
- if (! dontShow) {
- windowing.show("#togetherjs-rtc-info");
- }
- });
- if (! audioStream) {
- startStreaming(connect);
- return;
- }
- if (! connected) {
- connect();
- }
- toggleMute();
- }
- ui.container.find("#togetherjs-rtc-info .togetherjs-dont-show-again").change(function () {
- storage.settings.set("dontShowRtcInfo", this.checked);
- });
- function error() {
- console.warn.apply(console, arguments);
- var s = "";
- for (var i=0; i<arguments.length; i++) {
- if (s) {
- s += " ";
- }
- var a = arguments[i];
- if (typeof a == "string") {
- s += a;
- } else {
- var repl;
- try {
- repl = JSON.stringify(a);
- } catch (e) {
- }
- if (! repl) {
- repl = "" + a;
- }
- s += repl;
- }
- }
- audioButton("#togetherjs-audio-error");
- // FIXME: this title doesn't seem to display?
- $("#togetherjs-audio-error").attr("title", s);
- }
- function startStreaming(callback) {
- getUserMedia(
- {
- video: false,
- audio: true
- },
- function (stream) {
- audioStream = stream;
- attachMedia("#togetherjs-local-audio", stream);
- if (callback) {
- callback();
- }
- },
- function (err) {
- // FIXME: handle cancel case
- if (err && err.code == 1) {
- // User cancel
- return;
- }
- error("getUserMedia error:", err);
- }
- );
- }
- function attachMedia(element, media) {
- element = $(element)[0];
- console.log("Attaching", media, "to", element);
- if (window.mozRTCPeerConnection) {
- element.mozSrcObject = media;
- element.play();
- } else {
- element.autoplay = true;
- element.src = URL.createObjectURL(media);
- }
- }
- function getConnection() {
- assert(audioStream);
- if (_connection) {
- return _connection;
- }
- try {
- _connection = makePeerConnection();
- } catch (e) {
- error("Error creating PeerConnection:", e);
- throw e;
- }
- _connection.onaddstream = function (event) {
- console.log("got event", event, event.type);
- attachMedia($audio, event.stream);
- audioButton("#togetherjs-audio-active");
- };
- _connection.onstatechange = function () {
- // FIXME: this doesn't seem to work:
- // Actually just doesn't work on Firefox
- console.log("state change", _connection.readyState);
- if (_connection.readyState == "closed") {
- audioButton("#togetherjs-audio-ready");
- }
- };
- _connection.onicecandidate = function (event) {
- if (event.candidate) {
- session.send({
- type: "rtc-ice-candidate",
- candidate: {
- sdpMLineIndex: event.candidate.sdpMLineIndex,
- sdpMid: event.candidate.sdpMid,
- candidate: event.candidate.candidate
- }
- });
- }
- };
- _connection.addStream(audioStream);
- return _connection;
- }
- function addIceCandidate() {
- if (iceCandidate) {
- console.log("adding ice", iceCandidate);
- _connection.addIceCandidate(new RTCIceCandidate(iceCandidate));
- }
- }
- function connect() {
- var connection = getConnection();
- if (offerReceived && (! offerDescription)) {
- connection.setRemoteDescription(
- new RTCSessionDescription({
- type: "offer",
- sdp: offerReceived
- }),
- function () {
- offerDescription = true;
- addIceCandidate();
- connect();
- },
- function (err) {
- error("Error doing RTC setRemoteDescription:", err);
- }
- );
- return;
- }
- if (! (offerSent || offerReceived)) {
- connection.createOffer(function (offer) {
- console.log("made offer", offer);
- offer.sdp = ensureCryptoLine(offer.sdp);
- connection.setLocalDescription(
- offer,
- function () {
- session.send({
- type: "rtc-offer",
- offer: offer.sdp
- });
- offerSent = offer;
- audioButton("#togetherjs-audio-outgoing");
- },
- function (err) {
- error("Error doing RTC setLocalDescription:", err);
- },
- mediaConstraints
- );
- }, function (err) {
- error("Error doing RTC createOffer:", err);
- });
- } else if (! (answerSent || answerReceived)) {
- // FIXME: I might have only needed this due to my own bugs, this might
- // not actually time out
- var timeout = setTimeout(function () {
- if (! answerSent) {
- error("createAnswer Timed out; reload or restart browser");
- }
- }, 2000);
- connection.createAnswer(function (answer) {
- answer.sdp = ensureCryptoLine(answer.sdp);
- clearTimeout(timeout);
- connection.setLocalDescription(
- answer,
- function () {
- session.send({
- type: "rtc-answer",
- answer: answer.sdp
- });
- answerSent = answer;
- },
- function (err) {
- clearTimeout(timeout);
- error("Error doing RTC setLocalDescription:", err);
- },
- mediaConstraints
- );
- }, function (err) {
- error("Error doing RTC createAnswer:", err);
- });
- }
- }
- function toggleMute() {
- // FIXME: implement. Actually, wait for this to be implementable - currently
- // muting of localStreams isn't possible
- // FIXME: replace with hang-up?
- }
- session.hub.on("rtc-offer", function (msg) {
- if (offerReceived || answerSent || answerReceived || offerSent) {
- abort();
- }
- offerReceived = msg.offer;
- if (! accepted) {
- audioButton("#togetherjs-audio-incoming");
- return;
- }
- function run() {
- var connection = getConnection();
- connection.setRemoteDescription(
- new RTCSessionDescription({
- type: "offer",
- sdp: offerReceived
- }),
- function () {
- offerDescription = true;
- addIceCandidate();
- connect();
- },
- function (err) {
- error("Error doing RTC setRemoteDescription:", err);
- }
- );
- }
- if (! audioStream) {
- startStreaming(run);
- } else {
- run();
- }
- });
- session.hub.on("rtc-answer", function (msg) {
- if (answerSent || answerReceived || offerReceived || (! offerSent)) {
- abort();
- // Basically we have to abort and try again. We'll expect the other
- // client to restart when appropriate
- session.send({type: "rtc-abort"});
- return;
- }
- answerReceived = msg.answer;
- assert(offerSent);
- assert(audioStream);
- var connection = getConnection();
- connection.setRemoteDescription(
- new RTCSessionDescription({
- type: "answer",
- sdp: answerReceived
- }),
- function () {
- answerDescription = true;
- // FIXME: I don't think this connect is ever needed?
- connect();
- },
- function (err) {
- error("Error doing RTC setRemoteDescription:", err);
- }
- );
- });
- session.hub.on("rtc-ice-candidate", function (msg) {
- iceCandidate = msg.candidate;
- if (offerDescription || answerDescription) {
- addIceCandidate();
- }
- });
- session.hub.on("rtc-abort", function (msg) {
- abort();
- if (! accepted) {
- return;
- }
- if (! audioStream) {
- startStreaming(function () {
- connect();
- });
- } else {
- connect();
- }
- });
- session.hub.on("hello", function (msg) {
- // FIXME: displayToggle should be set due to
- // _connection.onstatechange, but that's not working, so
- // instead:
- audioButton("#togetherjs-audio-ready");
- if (accepted && (offerSent || answerSent)) {
- abort();
- connect();
- }
- });
- function abort() {
- answerSent = answerReceived = offerSent = offerReceived = null;
- answerDescription = offerDescription = false;
- _connection = null;
- $audio[0].removeAttribute("src");
- }
- });
- return webrtc;
- });
|