├── LICENSE ├── images ├── accept.gif ├── font.woff ├── loader.gif ├── search.gif ├── spinner.gif ├── background.png ├── right-arrow.gif └── fork-webrtc-experiment.png ├── App_Data ├── WebRTCData.mdf └── WebRTCData_log.LDF ├── bin ├── System.Data.Linq.dll ├── WebRTCExperiment.dll └── WebRTCExperiment.pdb ├── Global.asax ├── crossdomain.xml ├── GetRandomNumbers.cs ├── WebRTCExperiment.sln ├── TempExtensions.cs ├── WebRTCExperiment.csproj.user ├── Web.Debug.config ├── Web.Release.config ├── Properties └── AssemblyInfo.cs ├── Global.asax.cs ├── Models ├── WebRTC.dbml.layout ├── WebRTC.dbml └── WebRTC.designer.cs ├── README.md ├── js ├── 1-helper.js ├── 2-rtc-functions.js ├── RTCPeerConnection.js ├── RTCPeerConnection-Helpers.js ├── answer-socket.js ├── 4-ui.js ├── master-socket.js └── socket.io.js ├── Web.config ├── Views ├── Web.config └── WebRTC │ └── Index.cshtml ├── ToAgoDateTime.cs ├── StyleSheet.css ├── WebRTCExperiment.csproj ├── Controllers └── WebRTCController.cs ├── JavaScript.js └── socket.io.js /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/LICENSE -------------------------------------------------------------------------------- /images/accept.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/images/accept.gif -------------------------------------------------------------------------------- /images/font.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/images/font.woff -------------------------------------------------------------------------------- /images/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/images/loader.gif -------------------------------------------------------------------------------- /images/search.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/images/search.gif -------------------------------------------------------------------------------- /images/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/images/spinner.gif -------------------------------------------------------------------------------- /images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/images/background.png -------------------------------------------------------------------------------- /images/right-arrow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/images/right-arrow.gif -------------------------------------------------------------------------------- /App_Data/WebRTCData.mdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/App_Data/WebRTCData.mdf -------------------------------------------------------------------------------- /bin/System.Data.Linq.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/bin/System.Data.Linq.dll -------------------------------------------------------------------------------- /bin/WebRTCExperiment.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/bin/WebRTCExperiment.dll -------------------------------------------------------------------------------- /bin/WebRTCExperiment.pdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/bin/WebRTCExperiment.pdb -------------------------------------------------------------------------------- /App_Data/WebRTCData_log.LDF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/App_Data/WebRTCData_log.LDF -------------------------------------------------------------------------------- /Global.asax: -------------------------------------------------------------------------------- 1 | <%@ Application Codebehind="Global.asax.cs" Inherits="WebRTCExperiment.MvcApplication" Language="C#" %> 2 | -------------------------------------------------------------------------------- /images/fork-webrtc-experiment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muaz-khan/WebRTC-ASPNET-MVC/HEAD/images/fork-webrtc-experiment.png -------------------------------------------------------------------------------- /crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /GetRandomNumbers.cs: -------------------------------------------------------------------------------- 1 | /* Muaz Khan – http://twitter.com/muazkh */ 2 | using System; 3 | using System.Linq; 4 | 5 | namespace WebRTCExperiment 6 | { 7 | public class RandomNumbers 8 | { 9 | internal static string GetRandomNumbers(int length = 6) 10 | { 11 | var values = new byte[length]; 12 | var rnd = new Random(); 13 | rnd.NextBytes(values); 14 | return values.Aggregate(string.Empty, (current, v) => current + v.ToString()); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /WebRTCExperiment.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 11.00 3 | # Visual Studio 2010 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebRTCExperiment", "WebRTCExperiment.csproj", "{5D58330D-7272-4724-8B96-E7BF0E753853}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Any CPU = Debug|Any CPU 9 | Release|Any CPU = Release|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {5D58330D-7272-4724-8B96-E7BF0E753853}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {5D58330D-7272-4724-8B96-E7BF0E753853}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {5D58330D-7272-4724-8B96-E7BF0E753853}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {5D58330D-7272-4724-8B96-E7BF0E753853}.Release|Any CPU.Build.0 = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | EndGlobal 21 | -------------------------------------------------------------------------------- /TempExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace WebRTCExperiment 4 | { 5 | /* These extension methods are just for feedback panel! */ 6 | public static class TempExtensions 7 | { 8 | public static string GetValidatedString(this string text) 9 | { 10 | return text.Replace("-equal", "=").Replace("_plus_", "+").Replace("--", " ").Replace("-qmark", "?").Replace("-nsign", "#").Replace("-n", "
").Replace("-lt", "<").Replace("-gt", ">").Replace("-amp", "&").Replace("__", "-"); 11 | } 12 | 13 | public static string ResolveLinks(this string body) 14 | { 15 | if (string.IsNullOrEmpty(body)) return body; 16 | 17 | const string regex = @"((www\.|(http|https|ftp|news|file)+\:\/\/)[_.a-z0-9-]+\.[a-z0-9\/_:@=.+?,##%&~-]*[^.|\'|\# |!|\(|?|,| |>|<|;|\)])"; 18 | var r = new Regex(regex, RegexOptions.IgnoreCase); 19 | 20 | body = r.Replace(body, "$1").Replace("href=\"www", "href=\"http://www"); 21 | 22 | return body; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /WebRTCExperiment.csproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ProjectFiles 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | CurrentPage 13 | True 14 | False 15 | False 16 | False 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | False 26 | True 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Web.Debug.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 29 | 30 | -------------------------------------------------------------------------------- /Web.Release.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | 30 | 31 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("WebRTCExperiment")] 9 | [assembly: AssemblyDescription("Muaz Khan (@muazkh)")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("WebRTC")] 12 | [assembly: AssemblyProduct("WebRTCExperiment")] 13 | [assembly: AssemblyCopyright("Copyright © 2012 Muaz Khan")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("dab8df25-b4b0-48be-8260-4d4ae30b906a")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Revision and Build Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /Global.asax.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Web; 5 | using System.Web.Mvc; 6 | using System.Web.Routing; 7 | 8 | namespace WebRTCExperiment 9 | { 10 | // Note: For instructions on enabling IIS6 or IIS7 classic mode, 11 | // visit http://go.microsoft.com/?LinkId=9394801 12 | 13 | public class MvcApplication : System.Web.HttpApplication 14 | { 15 | public static void RegisterGlobalFilters(GlobalFilterCollection filters) 16 | { 17 | filters.Add(new HandleErrorAttribute()); 18 | } 19 | 20 | public static void RegisterRoutes(RouteCollection routes) 21 | { 22 | routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 23 | 24 | routes.MapRoute("message", "message", new { controller = "Email", action = "SendEmail" }); 25 | 26 | routes.MapRoute( 27 | "Default", // Route name 28 | "{controller}/{action}/{id}", // URL with parameters 29 | new { controller = "WebRTC", action = "Index", id = UrlParameter.Optional } // Parameter defaults 30 | ); 31 | 32 | } 33 | 34 | protected void Application_Start() 35 | { 36 | AreaRegistration.RegisterAllAreas(); 37 | 38 | RegisterGlobalFilters(GlobalFilters.Filters); 39 | RegisterRoutes(RouteTable.Routes); 40 | } 41 | 42 | /* To allow Cross-Origin Requests from: https://webrtc-experiment.appspot.com/ */ 43 | protected void Application_BeginRequest(object sender, EventArgs e) 44 | { 45 | Response.AddHeader("Access-Control-Allow-Origin", "*"); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /Models/WebRTC.dbml.layout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## [WebRTC](https://www.webrtc-experiment.com/) and ASP.NET MVC! 2 | 3 | 1. A simple WebRTC one-to-one demo written in ASP.NET MVC (C#) in **September, 2012!** 4 | 2. Supports public rooms as well as **password-protected private rooms**! 5 | 4. MS-SQL database is used as signaling gateway! 6 | 7 | Main Repository: https://github.com/muaz-khan/WebRTC-Experiment 8 | 9 | = 10 | 11 | ##### Browser Support 12 | 13 | This demo works fine on following web-browsers: 14 | 15 | | Browser | Support | 16 | | ------------- |-------------| 17 | | Firefox | [Stable](http://www.mozilla.org/en-US/firefox/new/) / [Aurora](http://www.mozilla.org/en-US/firefox/aurora/) / [Nightly](http://nightly.mozilla.org/) | 18 | | Google Chrome | [Stable](https://www.google.com/intl/en_uk/chrome/browser/) / [Canary](https://www.google.com/intl/en/chrome/browser/canary.html) / [Beta](https://www.google.com/intl/en/chrome/browser/beta.html) / [Dev](https://www.google.com/intl/en/chrome/browser/index.html?extra=devchannel#eula) | 19 | | Opera | [Stable](http://www.opera.com/) / [NEXT](http://www.opera.com/computer/next) | 20 | | Android | [Chrome](https://play.google.com/store/apps/details?id=com.chrome.beta&hl=en) / [Firefox](https://play.google.com/store/apps/details?id=org.mozilla.firefox) | 21 | 22 | = 23 | 24 | ##### Muaz Khan (muazkh@gmail.com) - [@muazkh](https://twitter.com/muazkh) / [@WebRTCWeb](https://twitter.com/WebRTCWeb) 25 | 26 | 27 | 28 | = 29 | 30 | ##### License 31 | 32 | All [WebRTC Experiments](https://www.webrtc-experiment.com/) are released under [MIT licence](https://www.webrtc-experiment.com/licence/) . Copyright (c) [Muaz Khan](https://plus.google.com/+MuazKhan). 33 | -------------------------------------------------------------------------------- /js/1-helper.js: -------------------------------------------------------------------------------- 1 | var global = {}; 2 | 3 | function $(n, t, i) { 4 | try { 5 | return i ? n && !t ? i.querySelector(n) : i.querySelectorAll(n) : n && !t ? window.document.querySelector(n) : window.document.querySelectorAll(n); 6 | } catch (r) { 7 | return document.getElementById(n.replace("#", "")); 8 | } 9 | } Object.prototype.each = function (n) { 10 | for (var i = this.length, t = 0; t < i; t++) n(this[t]); return this; 11 | }, Object.prototype.hide = function () { return this.length != undefined ? this.each(function (n) { n.style.display = "none"; }) : typeof this == "object" && (this.style.display = "none"), this; }, Object.prototype.show = function (n) { return this.length != undefined ? this.each(function (t) { t.style.display = n ? n : "block"; }) : typeof this == "object" && (this.style.display = n ? n : "block"), this; }, Object.prototype.css = function (n, t) { return this.style[n] = t, this; }, Object.prototype.slideDown = function (n) { return this.css("max-height", (n || 1e6) + "px"); }, Object.prototype.slideUp = function () { return this.css("max-height", "0"); }; 12 | 13 | /* log messages and title */ 14 | function log(message) { 15 | var logOutput = $('.log').show(); 16 | 17 | console.log(message); 18 | document.title = message; 19 | logOutput.innerHTML = message; 20 | } 21 | 22 | /* helps generating unique tokens for users and rooms */ 23 | function uniqueToken() { 24 | var s4 = function () { 25 | return Math.floor(Math.random() * 0x10000).toString(16); 26 | }; 27 | return s4() + s4() + "-" + s4() + "-" + s4() + "-" + s4() + "-" + s4() + s4() + s4(); 28 | } 29 | 30 | /* disable input boxes and anchor links until socket open */ 31 | function disable(isdisable) { 32 | 33 | if (isdisable) { 34 | $('input', true).each(function (element) { element.setAttribute('disabled', true); }); 35 | } 36 | else { 37 | $('input', true).each(function (element) { element.removeAttribute('disabled'); }); 38 | } 39 | } 40 | 41 | disable(true); -------------------------------------------------------------------------------- /js/2-rtc-functions.js: -------------------------------------------------------------------------------- 1 | /* send (i.e. transmit) offer/answer sdp */ 2 | function sendsdp(sdp, socket, isopus) { 3 | sdp = JSON.stringify(sdp); 4 | 5 | /* because sdp size is larger than what pubnub supports for single request...that's why it is splitted into two parts */ 6 | var firstPart = sdp.substr(0, 700), 7 | secondPart = sdp.substr(701, sdp.length - 1); 8 | 9 | /* transmitting first sdp part */ 10 | socket.send({ 11 | userToken: global.userToken, 12 | firstPart: firstPart, 13 | 14 | /* let other end know that whether you support opus */ 15 | isopus: isopus 16 | }); 17 | 18 | /* transmitting second sdp part */ 19 | socket.send({ 20 | userToken: global.userToken, 21 | secondPart: secondPart, 22 | 23 | /* let other end know that whether you support opus */ 24 | isopus: isopus 25 | }); 26 | } 27 | 28 | /* send (i.e. transmit) ICE candidates */ 29 | 30 | function sendice(candidate, socket) { 31 | socket.send({ 32 | userToken: global.userToken, /* unique ID to identify the sender */ 33 | candidate: { 34 | sdpMLineIndex: candidate.sdpMLineIndex, 35 | candidate: JSON.stringify(candidate.candidate) 36 | } 37 | }); 38 | } 39 | 40 | function gotstream(event, recheck) { 41 | 42 | if (event) { 43 | 44 | var video = document.createElement('video'); 45 | video.src = clientVideo.src; 46 | video.play(); 47 | 48 | participants.appendChild(video, participants.firstChild); 49 | 50 | clientVideo.pause(); 51 | 52 | if (!navigator.mozGetUserMedia) clientVideo.src = URL.createObjectURL(event.stream); 53 | else clientVideo.mozSrcObject = event.stream; 54 | 55 | 56 | clientVideo.play(); 57 | 58 | gotstream(null, true); 59 | } 60 | 61 | if (recheck) { 62 | if (!(clientVideo.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || clientVideo.paused || clientVideo.currentTime <= 0)) { 63 | finallyGotStream(); 64 | } else 65 | setTimeout(function() { 66 | gotstream(null, true); 67 | }, 500); 68 | } 69 | } 70 | 71 | function finallyGotStream() { 72 | clientVideo.css('-webkit-transform', 'rotate(0deg)'); 73 | global.isGotRemoteStream = true; 74 | } -------------------------------------------------------------------------------- /Web.config: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /Views/Web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Models/WebRTC.dbml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
-------------------------------------------------------------------------------- /js/RTCPeerConnection.js: -------------------------------------------------------------------------------- 1 | window.PeerConnection = window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.RTCPeerConnection; 2 | window.SessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription; 3 | window.IceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.RTCIceCandidate; 4 | 5 | window.defaults = { 6 | iceServers: { "iceServers": [{ "url": "stun:stun.l.google.com:19302" }] }, 7 | constraints: { 'mandatory': { 'OfferToReceiveAudio': true, 'OfferToReceiveVideo': true } } 8 | }; 9 | 10 | var RTCPeerConnection = function(options) { 11 | 12 | var iceServers = options.iceServers || defaults.iceServers; 13 | var constraints = options.constraints || defaults.constraints; 14 | 15 | var peerConnection = new PeerConnection(iceServers); 16 | 17 | peerConnection.onicecandidate = onicecandidate; 18 | peerConnection.onaddstream = onaddstream; 19 | peerConnection.addStream(options.stream); 20 | 21 | function onicecandidate(event) { 22 | if (!event.candidate || !peerConnection) return; 23 | if (options.getice) options.getice(event.candidate); 24 | } 25 | 26 | function onaddstream(event) { 27 | options.gotstream && options.gotstream(event); 28 | } 29 | 30 | function createOffer() { 31 | if (!options.onoffer) return; 32 | 33 | peerConnection.createOffer(function(sessionDescription) { 34 | 35 | /* opus? use it dear! */ 36 | options.isopus && (sessionDescription = codecs.opus(sessionDescription)); 37 | 38 | peerConnection.setLocalDescription(sessionDescription); 39 | options.onoffer(sessionDescription); 40 | 41 | }, function () {}, constraints); 42 | } 43 | 44 | createOffer(); 45 | 46 | function createAnswer() { 47 | if (!options.onanswer) return; 48 | 49 | peerConnection.setRemoteDescription(new SessionDescription(options.offer)); 50 | peerConnection.createAnswer(function(sessionDescription) { 51 | 52 | /* opus? use it dear! */ 53 | options.isopus && (sessionDescription = codecs.opus(sessionDescription)); 54 | 55 | peerConnection.setLocalDescription(sessionDescription); 56 | options.onanswer(sessionDescription); 57 | 58 | }, function () { }, constraints); 59 | } 60 | 61 | createAnswer(); 62 | 63 | return { 64 | /* offerer got answer sdp; MUST pass sdp over this function */ 65 | onanswer: function(sdp) { 66 | peerConnection.setRemoteDescription(new SessionDescription(sdp)); 67 | }, 68 | 69 | /* got ICE from other end; MUST pass those candidates over this function */ 70 | addice: function(candidate) { 71 | peerConnection.addIceCandidate(new IceCandidate({ 72 | sdpMLineIndex: candidate.sdpMLineIndex, 73 | candidate: candidate.candidate 74 | })); 75 | } 76 | }; 77 | }; 78 | 79 | function getUserMedia(options) { 80 | var URL = window.webkitURL || window.URL; 81 | navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.getUserMedia; 82 | 83 | navigator.getUserMedia(options.constraints || { audio: true, video: true }, 84 | function (stream) { 85 | 86 | if (options.video) 87 | if (!navigator.mozGetUserMedia) options.video.src = URL.createObjectURL(stream); 88 | else options.video.mozSrcObject = stream; 89 | 90 | options.onsuccess && options.onsuccess(stream); 91 | 92 | return stream; 93 | }, options.onerror); 94 | } -------------------------------------------------------------------------------- /ToAgoDateTime.cs: -------------------------------------------------------------------------------- 1 | /* Muaz Khan – http://twitter.com/muazkh */ 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Web; 6 | 7 | namespace WebRTCExperiment 8 | { 9 | public static class ToAgoDateTime 10 | { 11 | /// 12 | /// Convert date into timeago format 13 | /// 14 | /// date 15 | /// compare with 16 | /// toago formatted string 17 | public static string ToAgo(this DateTime time, DateTime diffWith) 18 | { 19 | if (time > diffWith) 20 | { 21 | return time.LaterTime(diffWith); 22 | } 23 | return time < diffWith ? time.EarlierTime(diffWith) : time.ToShortDateString(); 24 | } 25 | 26 | #region private methods 27 | 28 | private static string LaterTime(this DateTime time, DateTime differenceWth) 29 | { 30 | var timeDifference = time - differenceWth; 31 | var days = timeDifference.Days; 32 | 33 | var result = string.Empty; 34 | 35 | if (days > 30) return time.ToShortDateString(); 36 | 37 | if (days == 1) 38 | { 39 | result += days + " day "; 40 | goto End; 41 | } 42 | else if (days > 1) 43 | { 44 | result += days + " days "; 45 | goto End; 46 | } 47 | 48 | var hours = timeDifference.Hours; 49 | 50 | if (hours == 1) 51 | { 52 | result += hours + " hour "; 53 | goto End; 54 | } 55 | else if (hours > 1) 56 | { 57 | result += hours + " hours "; 58 | goto End; 59 | } 60 | 61 | var minutes = timeDifference.Minutes; 62 | if (minutes == 1) 63 | { 64 | result += minutes + " minute "; 65 | } 66 | else if (minutes > 1) 67 | { 68 | result += minutes + " minutes "; 69 | } 70 | End: 71 | return string.IsNullOrWhiteSpace(result) ? "a few seconds ago" : result + "later"; 72 | } 73 | 74 | private static string EarlierTime(this DateTime time, DateTime differenceWth) 75 | { 76 | var timeDifference = differenceWth - time; 77 | var days = timeDifference.Days; 78 | 79 | var result = string.Empty; 80 | 81 | if (days > 30) return time.ToShortDateString(); 82 | 83 | if (days == 1) 84 | { 85 | result += days + " day "; 86 | goto End; 87 | } 88 | else if (days > 1) 89 | { 90 | result += days + " days "; 91 | goto End; 92 | } 93 | 94 | var hours = timeDifference.Hours; 95 | 96 | if (hours == 1) 97 | { 98 | result += hours + " hour "; 99 | goto End; 100 | } 101 | else if (hours > 1) 102 | { 103 | result += hours + " hours "; 104 | goto End; 105 | } 106 | 107 | var minutes = timeDifference.Minutes; 108 | if (minutes == 1) 109 | { 110 | result += minutes + " minute "; 111 | } 112 | else if (minutes > 1) 113 | { 114 | result += minutes + " minutes "; 115 | } 116 | End: 117 | return 118 | string.IsNullOrWhiteSpace(result) 119 | ? "a few seconds ago" 120 | : result 121 | + "ago"; 122 | } 123 | 124 | #endregion 125 | } 126 | } -------------------------------------------------------------------------------- /js/RTCPeerConnection-Helpers.js: -------------------------------------------------------------------------------- 1 | var codecs = {}; 2 | 3 | /* this function credit goes to Google Chrome WebRTC team! */ 4 | codecs.opus = function (sessionDescription) { 5 | 6 | /* no opus? use other codec! */ 7 | if (!isopus) return sessionDescription; 8 | 9 | var sdp = sessionDescription.sdp; 10 | 11 | /* Opus? use it! */ 12 | function preferOpus() { 13 | var sdpLines = sdp.split('\r\n'); 14 | 15 | // Search for m line. 16 | for (var i = 0; i < sdpLines.length; i++) { 17 | if (sdpLines[i].search('m=audio') !== -1) { 18 | var mLineIndex = i; 19 | break; 20 | } 21 | } 22 | if (mLineIndex === null) 23 | return sdp; 24 | 25 | // If Opus is available, set it as the default in m line. 26 | for (var i = 0; i < sdpLines.length; i++) { 27 | if (sdpLines[i].search('opus/48000') !== -1) { 28 | var opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i); 29 | if (opusPayload) 30 | sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex], opusPayload); 31 | break; 32 | } 33 | } 34 | 35 | // Remove CN in m line and sdp. 36 | sdpLines = removeCN(sdpLines, mLineIndex); 37 | 38 | sdp = sdpLines.join('\r\n'); 39 | return sdp; 40 | } 41 | 42 | function extractSdp(sdpLine, pattern) { 43 | var _result = sdpLine.match(pattern); 44 | return (_result && _result.length == 2) ? _result[1] : null; 45 | } 46 | 47 | // Set the selected codec to the first in m line. 48 | function setDefaultCodec(mLine, payload) { 49 | var elements = mLine.split(' '); 50 | var newLine = new Array(); 51 | var index = 0; 52 | for (var i = 0; i < elements.length; i++) { 53 | if (index === 3) // Format of media starts from the fourth. 54 | newLine[index++] = payload; // Put target payload to the first. 55 | if (elements[i] !== payload) 56 | newLine[index++] = elements[i]; 57 | } 58 | return newLine.join(' '); 59 | } 60 | 61 | // Strip CN from sdp before CN constraints is ready. 62 | function removeCN(sdpLines, mLineIndex) { 63 | var mLineElements = sdpLines[mLineIndex].split(' '); 64 | // Scan from end for the convenience of removing an item. 65 | for (var i = sdpLines.length - 1; i >= 0; i--) { 66 | var payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i); 67 | if (payload) { 68 | var cnPos = mLineElements.indexOf(payload); 69 | if (cnPos !== -1) { 70 | // Remove CN payload from m line. 71 | mLineElements.splice(cnPos, 1); 72 | } 73 | // Remove CN line in sdp 74 | sdpLines.splice(i, 1); 75 | } 76 | } 77 | 78 | sdpLines[mLineIndex] = mLineElements.join(' '); 79 | return sdpLines; 80 | } 81 | 82 | 83 | var result; 84 | 85 | /* in case of error; use default codec; otherwise use opus */ 86 | try { 87 | result = preferOpus(); 88 | console.log('using opus codec!'); 89 | } 90 | catch (e) { 91 | console.error(e); 92 | result = sessionDescription.sdp; 93 | } 94 | 95 | return new SessionDescription({ 96 | sdp: result, 97 | type: sessionDescription.type 98 | }); 99 | }; 100 | 101 | /* check support of opus codec */ 102 | codecs.isopus = function () { 103 | var result = true; 104 | new PeerConnection(defaults.iceServers).createOffer(function (sessionDescription) { 105 | result = sessionDescription.sdp.indexOf('opus') !== -1; 106 | }, null, defaults.constraints); 107 | return result; 108 | }; 109 | 110 | /* used to know opus codec support */ 111 | var isopus = !!codecs.isopus(); -------------------------------------------------------------------------------- /js/answer-socket.js: -------------------------------------------------------------------------------- 1 | global.defaultChannel = 'WebRTC video Broadcast'; 2 | 3 | /* container: contains videos from all participants */ 4 | var participants = $('#participants').css('max-height', (innerHeight - 100) + 'px'); 5 | 6 | /* master socket is created for owner; answer socket for participant */ 7 | var socket = { 8 | master: null, 9 | answer: null 10 | }; 11 | 12 | answerSocket(global.defaultChannel, showListsAndBoxes); 13 | 14 | /* create answer socket; it connects with master socket to join broadcasted room */ 15 | function answerSocket(channel, onopen) { 16 | var socket_config = window.socket_config; 17 | 18 | socket_config.channel = channel || global.defaultChannel; 19 | socket.answer = io.connect('http://pubsub.pubnub.com/webrtc-experiment', socket_config); 20 | 21 | socket.answer.on('connect', onopen || function() {}); 22 | socket.answer.on('message', socketResponse); 23 | } 24 | 25 | var invokedOnce = false; 26 | function selfInvoker() { 27 | if (invokedOnce) return; 28 | 29 | invokedOnce = true; 30 | createAnswer(global.sdp, socket.answer); 31 | } 32 | 33 | function socketResponse(response) { 34 | /* if same user sent message; don't get! */ 35 | if (response.userToken === global.userToken) return; 36 | 37 | /* both ends MUST support opus; otherwise don't use it! */ 38 | response.isopus !== 'undefined' && (window.isopus = response.isopus && isopus); 39 | 40 | /* not yet joined or created any room!..search the room for current site visitor! */ 41 | if (global.isGetAvailableRoom && response.roomToken) getAvailableRooms(response); 42 | 43 | /* either offer or answer sdp sent by other end */ 44 | if (response.firstPart || response.secondPart) { 45 | 46 | /* because sdp size is larger than what pubnub supports for single request...that's why it is splitted into two parts */ 47 | if (response.firstPart) { 48 | global.firstPart = response.firstPart; 49 | 50 | if (global.secondPart) { 51 | global.sdp = JSON.parse(global.firstPart + global.secondPart); 52 | selfInvoker(); 53 | } 54 | } 55 | if (response.secondPart) { 56 | global.secondPart = response.secondPart; 57 | if (global.firstPart) { 58 | global.sdp = JSON.parse(global.firstPart + global.secondPart); 59 | selfInvoker(); 60 | } 61 | } 62 | } 63 | 64 | /* process ice candidates sent by other end */ 65 | else if (global.rtc && response.candidate && !global.isGotRemoteStream) { 66 | global.rtc.addice({ 67 | sdpMLineIndex: response.candidate.sdpMLineIndex, 68 | candidate: JSON.parse(response.candidate.candidate) 69 | }); 70 | 71 | } 72 | /* other end closed the webpage! The user is being informed. */ 73 | else if (response.end && global.isGotRemoteStream) refreshUI(); 74 | } 75 | 76 | /* got offer sdp from master socket; pass your answer sdp over that socket to join broadcasted room */ 77 | function createAnswer(sdp, socket) { 78 | var config = { 79 | getice: function(candidate) { 80 | sendice(candidate, socket); 81 | }, 82 | gotstream: gotstream, 83 | iceServers: iceServers, 84 | stream: global.clientStream, 85 | onanswer: function(answerSDP) { 86 | sendsdp(answerSDP, socket, window.isopus); 87 | }, 88 | 89 | isopus: window.isopus 90 | }; 91 | 92 | /* pass offer sdp sent by master socket */ 93 | config.offer = sdp; 94 | 95 | /* create RTC peer connection for participant */ 96 | global.rtc = RTCPeerConnection(config); 97 | } 98 | 99 | /* other end tried to close the webpage.....ending the peer connection! */ 100 | function onexit() { 101 | /* if broadcaster (i.e. master socket) ? ... stop broadcasting */ 102 | if (global.offerer) 103 | socket.master.send({ 104 | end: true, 105 | userToken: global.userToken 106 | }); 107 | 108 | /*not broadcaster? tell broadcaster that you're going away! */ 109 | else 110 | socket.answer.send({ 111 | end: true, 112 | userToken: global.userToken 113 | }); 114 | } 115 | 116 | window.onbeforeunload = onexit; 117 | window.onunload = onexit; -------------------------------------------------------------------------------- /StyleSheet.css: -------------------------------------------------------------------------------- 1 | html, html * 2 | { 3 | padding: 0; 4 | margin: 0; 5 | -webkit-user-select: none; 6 | font-family:Georgia, Verdana, Arial, Sans-Serif; 7 | 8 | -webkit-transition: all .8s ease; 9 | -moz-transition: all .8s ease; 10 | -o-transition: all .8s ease; 11 | -ms-transition: all .8s ease; 12 | transition: all .8s ease; 13 | 14 | overflow:hidden; 15 | } 16 | 17 | html 18 | { 19 | color: #420808; 20 | background: #420808; 21 | } 22 | html, body { 23 | overflow: hidden; 24 | } 25 | 26 | header 27 | { 28 | background: rgba(214, 206, 206, 0.65); 29 | border-bottom: 5px solid rgba(32, 26, 26, 0.28); 30 | padding: 5px 15px; 31 | } 32 | 33 | .author 34 | { 35 | float:right; 36 | } 37 | 38 | h1, h2 39 | { 40 | text-shadow: 0 0 5px #572E2E; 41 | color: #420808; 42 | font-size: 3em; 43 | } 44 | h2 45 | { 46 | font-size:2em; 47 | } 48 | 49 | .slogan { 50 | font-size: 20px; 51 | } 52 | 53 | a 54 | { 55 | color: white; 56 | text-decoration: none; 57 | } 58 | 59 | a:hover 60 | { 61 | color: #DBCBCB; 62 | } 63 | 64 | footer 65 | { 66 | position: fixed; 67 | bottom: 0; 68 | background: rgba(18, 112, 165, 0.54); 69 | width: 100%; 70 | text-align: center; 71 | font-size: 28px; 72 | color: #FFF0F0; 73 | } 74 | 75 | footer span 76 | { 77 | background: rgba(1, 11, 15, 0.23); 78 | padding: 0 .5em; 79 | } 80 | 81 | menu { 82 | float: right; 83 | } 84 | menu a { 85 | text-shadow: 0 0 5px #572E2E; 86 | color: #420808; 87 | } 88 | 89 | menu a:hover { 90 | text-shadow: 0 0 5px black; 91 | color: black; 92 | } 93 | 94 | aside 95 | { 96 | position:fixed; 97 | margin-left:80%; 98 | width: 20%; 99 | max-height: 80.8%; 100 | overflow: auto; 101 | } 102 | aside h2 103 | { 104 | font-size: 1.5em; 105 | font-weight: normal; 106 | } 107 | 108 | aside a 109 | { 110 | color:red!important; 111 | } 112 | 113 | aside div 114 | { 115 | background: rgba(238, 236, 227, 0.64); 116 | border: 1px solid rgba(32, 26, 26, 0.28); 117 | padding: 10px; 118 | } 119 | aside div:hover 120 | { 121 | background: rgba(238, 236, 227, 0.8); 122 | } 123 | 124 | aside span, #create-room, #search-room, #send-chat { 125 | margin: 5px; 126 | background: rgba(82, 28, 28, 0.65); 127 | padding: 6px 17px; 128 | display: inline-block; 129 | color: white; 130 | cursor: pointer; 131 | } 132 | 133 | aside span:hover, #create-room:hover, #search-room:hover, #send-chat:hover { 134 | background: rgba(82, 28, 28, 0.80); 135 | } 136 | 137 | aside span:active, #create-room:active, #search-room:active, #send-chat:active { 138 | background: rgba(82, 28, 28, 1); 139 | } 140 | 141 | .create-room-panel, .private-room, .chat-box, .stats { 142 | position: fixed; 143 | margin: 5%; 144 | box-shadow: 0 0 6px #DFDFDF; 145 | background: rgba(214, 206, 206, 0.65); 146 | border: 2px solid rgba(32, 26, 26, 0.28); 147 | } 148 | 149 | input 150 | { 151 | -webkit-user-select: initial; 152 | outline:none!important; 153 | font-size: 25px; 154 | } 155 | 156 | label 157 | { 158 | width:150px; 159 | display:inline-block; 160 | font-size:20px; 161 | } 162 | 163 | small 164 | { 165 | display:block; 166 | font-size:11px; 167 | } 168 | 169 | .create-room-panel h2, .create-room-panel div, .private-room h2, .private-room div, .chat-box h2, .chat-box div, .stats h2, .stats div 170 | { 171 | border-bottom: 2px solid rgba(32, 26, 26, 0.28); 172 | padding:10px 20px; 173 | } 174 | 175 | .private-room 176 | { 177 | bottom: 4%; 178 | left: 34%; 179 | right: 16%; 180 | } 181 | .private-room h2, .stats h2 182 | { 183 | font-size: 20px; 184 | font-weight: normal; 185 | } 186 | 187 | .stats 188 | { 189 | left: 44%; 190 | right:16%; 191 | top:-100%; 192 | border-top:0; 193 | box-shadow:none; 194 | } 195 | 196 | .stats div label 197 | { 198 | width: auto; 199 | } 200 | 201 | .stats span { 202 | float: right; 203 | font-size: 2em; 204 | } 205 | 206 | .stats div, .stats h2 207 | { 208 | padding:5px; 209 | border-bottom: 1px solid rgba(32, 26, 26, 0.28); 210 | } 211 | 212 | #search-room, #send-chat 213 | { 214 | float: right; 215 | margin-top: 1px; 216 | } 217 | 218 | #send-chat 219 | { 220 | margin: -33px 0; 221 | } 222 | 223 | .plusone-gplus { 224 | position: fixed; 225 | bottom: 5%; 226 | margin-left: 1em; 227 | } 228 | 229 | .chat-box { 230 | bottom: 0; 231 | right: 0; 232 | margin: 0; 233 | z-index: 10000; 234 | display: none; 235 | } -------------------------------------------------------------------------------- /Views/WebRTC/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = null; 3 | } 4 | 5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | WebRTC Experiment ® Muaz Khan 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | Source code on Github! 35 | 36 |
Share your feelings directly & privately with friends!
40 |

WebRTC Experiment

41 |
42 | 43 | 44 | 45 |
46 |

Create a room

47 |
48 | 49 | 50 | Enter your full name. 51 |
52 | 53 |
54 | 55 | 56 | Enter human-readable room name. 57 |
58 | 59 |
60 | 61 | 62 | Enter your partner's email or unique token/word. 63 |
64 | 65 | 66 | 67 | 68 |
Create Room
69 |
70 | 71 |
72 |

Statistics Report

73 | 74 |
75 | 0 76 | 77 | Number of rooms created. 78 |
79 |
80 | 0 81 | 82 | Number of rooms publicly shared. 83 |
84 |
85 | 0 86 | 87 | Number of rooms privately shared. 88 |
89 |
90 | 0 91 | 92 | Number of rooms in which no one participated. 93 |
94 |
95 | 0 96 | 97 | Number of rooms that worked fine. Both participants shared cameras finely! 98 |
99 |
100 | 101 |
102 |

Searching for a private room?

103 |
104 | 105 | 106 | 107 |
Search
108 | 109 | Enter the email or token that your room partner given you. 110 |
111 |
112 | 113 | 114 | 115 | 116 |
117 | 0 available rooms, 0 private available rooms and 0 public active rooms 118 |
119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /js/4-ui.js: -------------------------------------------------------------------------------- 1 | /* client video + capture client camera */ 2 | var clientVideo = $('#client-video'); 3 | function captureCamera(callback) { 4 | getUserMedia({ 5 | video: clientVideo, 6 | onsuccess: function (stream) { 7 | global.clientStream = stream; 8 | 9 | clientVideo.show().play(); 10 | 11 | setTimeout(function() { 12 | clientVideo.css('-webkit-transform', 'rotate(360deg)'); 13 | }, 1000); 14 | 15 | $('.visible', true).hide(); 16 | 17 | callback && callback(); 18 | }, 19 | onerror: function () { 20 | alert('Two possible situations: 1) another window is using your webcam, or 2) you\'ve not allowed you camera. Webcam is mandatory of this app!'); 21 | location.reload(); 22 | } 23 | }); 24 | } 25 | 26 | /* possible situations 27 | 1) you joined a room 28 | 2) someone joined your room (i.e. you found a participant!) 29 | */ 30 | 31 | function hideListsAndBoxes() { 32 | disable(true); 33 | global.isGetAvailableRoom = false; 34 | } 35 | 36 | /* waiting until socket is open -- then we will enable input boxes */ 37 | hideListsAndBoxes(); 38 | 39 | /* primarily called when someone left your room */ 40 | 41 | function showListsAndBoxes() { 42 | disable(false); 43 | 44 | global.isGetAvailableRoom = true; 45 | } 46 | 47 | /* the user have not yet allowed his camera access */ 48 | global.mediaAccessAlertMessage = 'This app wants to use your camera and microphone.\n\nGrant it the access!'; 49 | 50 | /* generating a unique token for current user */ 51 | global.userToken = uniqueToken(); 52 | 53 | /* you wanted to create a private room! */ 54 | $('#is-private').onchange = function() { 55 | if (this.checked) $('#partner-email').css('padding', '10px 20px').css('height', 'auto').css('border-bottom', '1px double #CACACA').slideDown().querySelector('#private-token').focus(); 56 | else $('#partner-email').css('padding', 0).css('border-bottom', 0).slideUp(); 57 | }; 58 | 59 | function broadcastNow(stream) { 60 | global.clientStream = stream; 61 | 62 | global.isGetAvailableRoom = false; 63 | global.roomName = uniqueToken(); 64 | global.roomToken = uniqueToken(); 65 | global.offerer = true; 66 | masterSocket(null, spreadRoom); 67 | } 68 | 69 | function spreadRoom() { 70 | var g = global; 71 | 72 | socket.master.send({ 73 | roomToken: g.roomToken, 74 | ownerToken: g.userToken, 75 | roomName: g.roomName 76 | }); 77 | 78 | /* propagate room around the globe! */ 79 | setTimeout(spreadRoom, 3000); 80 | } 81 | 82 | /* you tried to search a private room! */ 83 | $('#search-room').onclick = function () { 84 | var email = $('input#email'); 85 | if (!email.value.length) { 86 | alert('Please enter the email or unique token/word that your partner given you.'); 87 | email.focus(); 88 | return; 89 | } 90 | 91 | global.searchPrivateRoom = email.value; 92 | email.setAttribute('disabled', true); 93 | 94 | socket.master && (socket.master = null); 95 | socket.answer && (socket.answer = null); 96 | 97 | answerSocket(email.value, function () { 98 | email.value = ''; 99 | email.removeAttribute('disabled'); 100 | }); 101 | }; 102 | 103 | /* if other end close the room; refreshing the UI for current user */ 104 | 105 | function refreshUI() { 106 | disable(false); 107 | global.rtc = null; 108 | 109 | global.isGetAvailableRoom = true; 110 | global.isGotRemoteStream = false; 111 | } 112 | 113 | /* searching public (or private) rooms */ 114 | global.isGetAvailableRoom = true; 115 | var publicRooms = $('#public-rooms'); 116 | 117 | function getAvailableRooms(response) { 118 | if (!global.isGetAvailableRoom || !response.ownerToken) return; 119 | 120 | /* room is already visible in the current user's page */ 121 | var alreadyExist = $('#' + response.ownerToken); 122 | if (alreadyExist) return; 123 | 124 | /* showing the room for current user */ 125 | var blockquote = document.createElement('blockquote'); 126 | blockquote.setAttribute('id', response.ownerToken); 127 | 128 | blockquote.innerHTML = response.roomName + 'Join Room'; 129 | 130 | publicRooms.insertBefore(blockquote, publicRooms.childNodes[0]); 131 | 132 | /* allowing user to join rooms! */ 133 | $('.join', true).each(function (span) { 134 | span.onclick = function () { 135 | global.isGetAvailableRoom = false; 136 | hideListsAndBoxes(); 137 | 138 | global.roomToken = this.id; 139 | 140 | var forUser = this.parentNode.id; 141 | 142 | captureCamera(function () { 143 | /* telling room owner that I'm your participant! */ 144 | socket.answer.send({ 145 | participant: global.userToken, 146 | userToken: global.userToken, 147 | forUser: forUser, 148 | 149 | /* let other end know that whether you support opus */ 150 | isopus: isopus 151 | }); 152 | 153 | socket.master && (socket.master = null); 154 | socket.answer && (socket.answer = null); 155 | 156 | answerSocket(global.userToken); 157 | }); 158 | }; 159 | }); 160 | } -------------------------------------------------------------------------------- /js/master-socket.js: -------------------------------------------------------------------------------- 1 | masterSocket(); 2 | 3 | /* master socket handles all new/old connections/participants */ 4 | function masterSocket(channel, onopen) { 5 | var socket_config = window.socket_config; 6 | 7 | /* if private broadcasted room; unique channel will be passed */ 8 | socket_config.channel = channel || global.defaultChannel; 9 | socket.master = io.connect('http://pubsub.pubnub.com/webrtc-experiment', socket_config); 10 | 11 | socket.master.on('connect', connect); 12 | socket.master.on('message', callback); 13 | 14 | function connect() { 15 | showListsAndBoxes(); 16 | onopen && onopen(); 17 | } 18 | 19 | function callback(data) { 20 | if (data.roomToken || data.userToken == global.userToken) return; 21 | if (!data.participant || data.forUser != global.userToken) return; 22 | if (data.participant) { 23 | /* found a participant? .... open new socket for him */ 24 | openSocket(data.userToken); 25 | } 26 | } 27 | } 28 | 29 | /* this function creates unique sockets and unique peers to handle the real broadcasting! 30 | * in this case; participant closes his old (public) socket; and creates new socket and sets channel === his own unique token (global.userToken) */ 31 | function openSocket(channel) { 32 | var socket_config = window.socket_config, 33 | isGotRemoteStream, 34 | peer, 35 | 36 | /* inner variable stores firstPart and secondPart of the answer SDP sent by the participant */ 37 | inner = {}, 38 | 39 | /* unique remote video from participant */ 40 | video, 41 | 42 | /* Amazing situation!....in the same broadcasted room; one or more peers can create peer connections 43 | * using opus codec; while other peers can use some other codec. Everything is working fine! */ 44 | isopus = window.isopus; 45 | 46 | /* here channel === unique token of the participant (global.userToken -> of the participant) */ 47 | socket_config.channel = channel; 48 | var socket = io.connect('http://pubsub.pubnub.com/webrtc-experiment', socket_config); 49 | 50 | socket.on('connect', opened); 51 | socket.on('message', callback); 52 | 53 | /* unique socket opened */ 54 | function opened() { 55 | var config = { 56 | iceServers: iceServers, 57 | stream: global.clientStream, 58 | onoffer: function (sdp) { sendsdp(sdp, socket, isopus); }, 59 | getice: function(candidate) { sendice(candidate, socket); }, 60 | gotstream: gotstream, 61 | isopus: isopus 62 | }; 63 | 64 | /* unique peer got video from participant; */ 65 | video = document.createElement('video'); 66 | video.css('-webkit-transform', 'rotate(0deg)'); 67 | 68 | /* and added in the "participants" video list: */ 69 | participants.appendChild(video, participants.firstChild); 70 | 71 | /* unique peer connection opened */ 72 | peer = RTCPeerConnection(config); 73 | } 74 | 75 | var invokedOnce = false; 76 | function selfInvoker() { 77 | if (invokedOnce) return; 78 | 79 | if (!peer) setTimeout(selfInvoker, 100); 80 | else { 81 | invokedOnce = true; 82 | peer.onanswer(inner.sdp); 83 | } 84 | } 85 | 86 | /* unique socket got message from participant */ 87 | function callback(response) { 88 | if (response.userToken == global.userToken) return; 89 | 90 | /* both ends MUST support opus; otherwise don't use opus! */ 91 | response.isopus !== 'undefined' && (isopus = response.isopus && isopus); 92 | 93 | /* because sdp size is larger than what pubnub supports for single request...that's why it is splitted into two parts */ 94 | if (response.firstPart || response.secondPart) { 95 | if (response.firstPart) { 96 | inner.firstPart = response.firstPart; 97 | if (inner.secondPart) { 98 | inner.sdp = JSON.parse(inner.firstPart + inner.secondPart); 99 | selfInvoker(); 100 | } 101 | } 102 | if (response.secondPart) { 103 | inner.secondPart = response.secondPart; 104 | if (inner.firstPart) { 105 | inner.sdp = JSON.parse(inner.firstPart + inner.secondPart); 106 | selfInvoker(); 107 | } 108 | } 109 | } 110 | 111 | /* process ice candidates sent by participant */ 112 | if (response.candidate && !isGotRemoteStream) { 113 | peer && peer.addice({ 114 | sdpMLineIndex: response.candidate.sdpMLineIndex, 115 | candidate: JSON.parse(response.candidate.candidate) 116 | }); 117 | } 118 | 119 | if (response.end) video && participants.removeChild(video); 120 | } 121 | 122 | /* sub socket got stream */ 123 | function gotstream(event, recheck) { 124 | if (event) { 125 | if (!navigator.mozGetUserMedia) video.src = URL.createObjectURL(event.stream); 126 | else video.mozSrcObject = event.stream; 127 | 128 | video.play(); 129 | 130 | gotstream(null, true); 131 | } 132 | 133 | if (recheck) { 134 | if (!(video.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || video.paused || video.currentTime <= 0)) { 135 | isGotRemoteStream = true; 136 | 137 | video.css('-webkit-transform', 'rotate(360deg)'); 138 | 139 | } else setTimeout(function () { gotstream(null, true); }, 50); 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /WebRTCExperiment.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | 8 | 9 | 2.0 10 | {5D58330D-7272-4724-8B96-E7BF0E753853} 11 | {E53F8FEA-EAE0-44A6-8774-FFD645390401};{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} 12 | Library 13 | Properties 14 | WebRTCExperiment 15 | WebRTCExperiment 16 | v4.0 17 | false 18 | 19 | 20 | 21 | 22 | 4.0 23 | false 24 | 25 | 26 | 27 | 28 | 29 | 30 | true 31 | full 32 | false 33 | bin\ 34 | DEBUG;TRACE 35 | prompt 36 | 4 37 | 38 | 39 | pdbonly 40 | true 41 | bin\ 42 | TRACE 43 | prompt 44 | 4 45 | true 46 | true 47 | 48 | 49 | 50 | True 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Global.asax 80 | 81 | 82 | True 83 | True 84 | WebRTC.dbml 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | WebRTCData.mdf 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | Web.config 119 | 120 | 121 | Web.config 122 | 123 | 124 | 125 | 126 | 127 | 128 | MSLinqToSQLGenerator 129 | WebRTC.designer.cs 130 | Designer 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | WebRTC.dbml 140 | 141 | 142 | 143 | 10.0 144 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 145 | 146 | 147 | 148 | 149 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | False 163 | True 164 | 49933 165 | / 166 | 167 | 168 | False 169 | False 170 | 171 | 172 | False 173 | 174 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /Controllers/WebRTCController.cs: -------------------------------------------------------------------------------- 1 | /* Muaz Khan : Nov 14, 2012!! 2 | * @muazk: http://twitter.com/muazkh 3 | * Github: github.com/muaz-khan 4 | ******************************/ 5 | using System; 6 | using System.Linq; 7 | using System.Web.Mvc; 8 | using WebRTCExperiment.Models; 9 | using System.Web.WebPages; 10 | 11 | namespace WebRTCExperiment.Controllers 12 | { 13 | public class WebRTCController : Controller 14 | { 15 | public ActionResult Index() 16 | { 17 | //return RedirectPermanent("https://www.webrtc-experiment.com/"); 18 | return View(); 19 | } 20 | 21 | readonly WebRTCDataContext _db = new WebRTCDataContext(); 22 | 23 | #region Create / Join room 24 | 25 | [HttpPost] 26 | public JsonResult CreateRoom(string ownerName, string roomName, string partnerEmail = null) 27 | { 28 | if (ownerName.IsEmpty() || roomName.IsEmpty()) return Json(false); 29 | 30 | back: 31 | string token = RandomNumbers.GetRandomNumbers(); 32 | if (_db.Rooms.Any(r => r.Token == token)) goto back; 33 | 34 | back2: 35 | string ownerToken = RandomNumbers.GetRandomNumbers(); 36 | if (_db.Rooms.Any(r => r.OwnerToken == ownerToken)) goto back2; 37 | 38 | var room = new Room 39 | { 40 | Token = token, 41 | Name = roomName.GetValidatedString(), 42 | OwnerName = ownerName.GetValidatedString(), 43 | OwnerToken = ownerToken, 44 | LastUpdated = DateTime.Now, 45 | SharedWith = partnerEmail.IsEmpty() ? "Public" : partnerEmail, 46 | Status = Status.Available 47 | }; 48 | 49 | _db.Rooms.InsertOnSubmit(room); 50 | _db.SubmitChanges(); 51 | 52 | return Json(new 53 | { 54 | roomToken = room.Token, 55 | ownerToken = room.OwnerToken 56 | }); 57 | } 58 | 59 | [HttpPost] 60 | public JsonResult JoinRoom(string participant, string roomToken, string partnerEmail = null) 61 | { 62 | if (participant.IsEmpty() || roomToken.IsEmpty()) return Json(false); 63 | 64 | var room = _db.Rooms.FirstOrDefault(r => r.Token == roomToken); 65 | if (room == null) return Json(false); 66 | 67 | if (room.SharedWith != "Public") 68 | { 69 | if (partnerEmail.IsEmpty()) return Json(false); 70 | if (room.SharedWith != partnerEmail) return Json(false); 71 | } 72 | 73 | back: 74 | string participantToken = RandomNumbers.GetRandomNumbers(); 75 | if (_db.Rooms.Any(r => r.OwnerToken == participantToken)) goto back; 76 | 77 | room.ParticipantName = participant.GetValidatedString(); 78 | room.ParticipantToken = participantToken; 79 | room.LastUpdated = DateTime.Now; 80 | room.Status = Status.Active; 81 | 82 | _db.SubmitChanges(); 83 | 84 | return Json(new 85 | { 86 | participantToken, 87 | friend = room.OwnerName 88 | }); 89 | } 90 | 91 | #endregion 92 | 93 | #region Search rooms 94 | 95 | [HttpPost] 96 | public JsonResult SearchPublicRooms(string partnerEmail) 97 | { 98 | if (!partnerEmail.IsEmpty()) return SearchPrivateRooms(partnerEmail); 99 | 100 | var rooms = _db.Rooms.Where(r => r.SharedWith == "Public" && r.Status == Status.Available && r.LastUpdated.AddMinutes(1) > DateTime.Now).OrderByDescending(o => o.ID); 101 | return Json( 102 | new 103 | { 104 | rooms = rooms.Select(r => new 105 | { 106 | roomName = r.Name, 107 | ownerName = r.OwnerName, 108 | roomToken = r.Token 109 | }), 110 | availableRooms = rooms.Count(), 111 | publicActiveRooms= _db.Rooms.Count(r => r.Status == Status.Active && r.LastUpdated.AddMinutes(1) > DateTime.Now && r.SharedWith == "Public"), 112 | privateAvailableRooms = _db.Rooms.Count(r => r.Status == Status.Available && r.LastUpdated.AddMinutes(1) > DateTime.Now && r.SharedWith != "Public") 113 | } 114 | ); 115 | } 116 | 117 | [HttpPost] 118 | public JsonResult SearchPrivateRooms(string partnerEmail) 119 | { 120 | if (partnerEmail.IsEmpty()) return Json(false); 121 | 122 | var rooms = _db.Rooms.Where(r => r.SharedWith == partnerEmail && r.Status == Status.Available && r.LastUpdated.AddMinutes(1) > DateTime.Now).OrderByDescending(o => o.ID); 123 | return Json(new 124 | { 125 | rooms = rooms.Select(r => new 126 | { 127 | roomName = r.Name, 128 | ownerName = r.OwnerName, 129 | roomToken = r.Token 130 | }) 131 | }); 132 | } 133 | 134 | #endregion 135 | 136 | #region SDP Messages 137 | 138 | [HttpPost] 139 | public JsonResult PostSDP(string sdp, string roomToken, string userToken) 140 | { 141 | if (sdp.IsEmpty() || roomToken.IsEmpty() || userToken.IsEmpty()) return Json(false); 142 | 143 | var sdpMessage = new SDPMessage 144 | { 145 | SDP = sdp, 146 | IsProcessed = false, 147 | RoomToken = roomToken, 148 | Sender = userToken 149 | }; 150 | 151 | _db.SDPMessages.InsertOnSubmit(sdpMessage); 152 | _db.SubmitChanges(); 153 | 154 | return Json(true); 155 | } 156 | 157 | [HttpPost] 158 | public JsonResult GetSDP(string roomToken, string userToken) 159 | { 160 | if (roomToken.IsEmpty() || userToken.IsEmpty()) return Json(false); 161 | 162 | var sdp = _db.SDPMessages.FirstOrDefault(s => s.RoomToken == roomToken && s.Sender != userToken && !s.IsProcessed); 163 | 164 | if(sdp == null) return Json(false); 165 | 166 | sdp.IsProcessed = true; 167 | _db.SubmitChanges(); 168 | 169 | return Json(new 170 | { 171 | sdp = sdp.SDP 172 | }); 173 | } 174 | 175 | #endregion 176 | 177 | #region ICE Candidates 178 | 179 | [HttpPost] 180 | public JsonResult PostICE(string candidate, string label, string roomToken, string userToken) 181 | { 182 | if (candidate.IsEmpty() || label.IsEmpty() || roomToken.IsEmpty() || userToken.IsEmpty()) return Json(false); 183 | 184 | var candidateTable = new CandidatesTable 185 | { 186 | Candidate = candidate, 187 | Label = label, 188 | IsProcessed = false, 189 | RoomToken = roomToken, 190 | Sender = userToken 191 | }; 192 | 193 | _db.CandidatesTables.InsertOnSubmit(candidateTable); 194 | _db.SubmitChanges(); 195 | 196 | return Json(true); 197 | } 198 | 199 | [HttpPost] 200 | public JsonResult GetICE(string roomToken, string userToken) 201 | { 202 | if (roomToken.IsEmpty() || userToken.IsEmpty()) return Json(false); 203 | 204 | var candidate = _db.CandidatesTables.FirstOrDefault(c => c.RoomToken == roomToken && c.Sender != userToken && !c.IsProcessed); 205 | if (candidate == null) return Json(false); 206 | 207 | candidate.IsProcessed = true; 208 | _db.SubmitChanges(); 209 | 210 | return Json(new 211 | { 212 | candidate = candidate.Candidate, 213 | label = candidate.Label 214 | }); 215 | } 216 | 217 | #endregion 218 | 219 | #region Extras 220 | 221 | [HttpPost] 222 | public JsonResult GetParticipant(string roomToken, string ownerToken) 223 | { 224 | if (roomToken.IsEmpty() || ownerToken.IsEmpty()) return Json(false); 225 | 226 | var room = _db.Rooms.FirstOrDefault(r => r.Token == roomToken && r.OwnerToken == ownerToken); 227 | if (room == null) return Json(false); 228 | 229 | room.LastUpdated = DateTime.Now; 230 | _db.SubmitChanges(); 231 | 232 | if (room.ParticipantName.IsEmpty()) return Json(false); 233 | return Json(new { participant = room.ParticipantName }); 234 | } 235 | 236 | [HttpPost] 237 | public JsonResult Stats() 238 | { 239 | var numberOfRooms = _db.Rooms.Count(); 240 | var numberOfPublicRooms = _db.Rooms.Count(r => r.SharedWith == "Public"); 241 | var numberOfPrivateRooms = _db.Rooms.Count(r => r.SharedWith != "Public"); 242 | var numberOfEmptyRooms = _db.Rooms.Count(r => r.ParticipantName == null); 243 | var numberOfFullRooms = _db.Rooms.Count(r => r.ParticipantName != null); 244 | return Json(new { numberOfRooms, numberOfPublicRooms, numberOfPrivateRooms, numberOfEmptyRooms, numberOfFullRooms }); 245 | } 246 | 247 | #endregion 248 | } 249 | struct Status 250 | { 251 | public const string Available = "Available"; 252 | public const string Active = "Active"; 253 | } 254 | } -------------------------------------------------------------------------------- /Models/WebRTC.designer.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable 1591 2 | //------------------------------------------------------------------------------ 3 | // 4 | // This code was generated by a tool. 5 | // Runtime Version:4.0.30319.17929 6 | // 7 | // Changes to this file may cause incorrect behavior and will be lost if 8 | // the code is regenerated. 9 | // 10 | //------------------------------------------------------------------------------ 11 | 12 | namespace WebRTCExperiment.Models 13 | { 14 | using System.Data.Linq; 15 | using System.Data.Linq.Mapping; 16 | using System.Data; 17 | using System.Collections.Generic; 18 | using System.Reflection; 19 | using System.Linq; 20 | using System.Linq.Expressions; 21 | using System.ComponentModel; 22 | using System; 23 | 24 | 25 | [global::System.Data.Linq.Mapping.DatabaseAttribute(Name="WebRTCData")] 26 | public partial class WebRTCDataContext : System.Data.Linq.DataContext 27 | { 28 | 29 | private static System.Data.Linq.Mapping.MappingSource mappingSource = new AttributeMappingSource(); 30 | 31 | #region Extensibility Method Definitions 32 | partial void OnCreated(); 33 | partial void InsertCandidatesTable(CandidatesTable instance); 34 | partial void UpdateCandidatesTable(CandidatesTable instance); 35 | partial void DeleteCandidatesTable(CandidatesTable instance); 36 | partial void InsertRoom(Room instance); 37 | partial void UpdateRoom(Room instance); 38 | partial void DeleteRoom(Room instance); 39 | partial void InsertSDPMessage(SDPMessage instance); 40 | partial void UpdateSDPMessage(SDPMessage instance); 41 | partial void DeleteSDPMessage(SDPMessage instance); 42 | #endregion 43 | 44 | public WebRTCDataContext() : 45 | base(global::System.Configuration.ConfigurationManager.ConnectionStrings["WebRTCDataConnectionString"].ConnectionString, mappingSource) 46 | { 47 | OnCreated(); 48 | } 49 | 50 | public WebRTCDataContext(string connection) : 51 | base(connection, mappingSource) 52 | { 53 | OnCreated(); 54 | } 55 | 56 | public WebRTCDataContext(System.Data.IDbConnection connection) : 57 | base(connection, mappingSource) 58 | { 59 | OnCreated(); 60 | } 61 | 62 | public WebRTCDataContext(string connection, System.Data.Linq.Mapping.MappingSource mappingSource) : 63 | base(connection, mappingSource) 64 | { 65 | OnCreated(); 66 | } 67 | 68 | public WebRTCDataContext(System.Data.IDbConnection connection, System.Data.Linq.Mapping.MappingSource mappingSource) : 69 | base(connection, mappingSource) 70 | { 71 | OnCreated(); 72 | } 73 | 74 | public System.Data.Linq.Table CandidatesTables 75 | { 76 | get 77 | { 78 | return this.GetTable(); 79 | } 80 | } 81 | 82 | public System.Data.Linq.Table Rooms 83 | { 84 | get 85 | { 86 | return this.GetTable(); 87 | } 88 | } 89 | 90 | public System.Data.Linq.Table SDPMessages 91 | { 92 | get 93 | { 94 | return this.GetTable(); 95 | } 96 | } 97 | } 98 | 99 | [global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.CandidatesTable")] 100 | public partial class CandidatesTable : INotifyPropertyChanging, INotifyPropertyChanged 101 | { 102 | 103 | private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty); 104 | 105 | private int _ID; 106 | 107 | private string _Candidate; 108 | 109 | private string _Label; 110 | 111 | private string _RoomToken; 112 | 113 | private string _Sender; 114 | 115 | private bool _IsProcessed; 116 | 117 | #region Extensibility Method Definitions 118 | partial void OnLoaded(); 119 | partial void OnValidate(System.Data.Linq.ChangeAction action); 120 | partial void OnCreated(); 121 | partial void OnIDChanging(int value); 122 | partial void OnIDChanged(); 123 | partial void OnCandidateChanging(string value); 124 | partial void OnCandidateChanged(); 125 | partial void OnLabelChanging(string value); 126 | partial void OnLabelChanged(); 127 | partial void OnRoomTokenChanging(string value); 128 | partial void OnRoomTokenChanged(); 129 | partial void OnSenderChanging(string value); 130 | partial void OnSenderChanged(); 131 | partial void OnIsProcessedChanging(bool value); 132 | partial void OnIsProcessedChanged(); 133 | #endregion 134 | 135 | public CandidatesTable() 136 | { 137 | OnCreated(); 138 | } 139 | 140 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_ID", AutoSync=AutoSync.OnInsert, DbType="Int NOT NULL IDENTITY", IsPrimaryKey=true, IsDbGenerated=true)] 141 | public int ID 142 | { 143 | get 144 | { 145 | return this._ID; 146 | } 147 | set 148 | { 149 | if ((this._ID != value)) 150 | { 151 | this.OnIDChanging(value); 152 | this.SendPropertyChanging(); 153 | this._ID = value; 154 | this.SendPropertyChanged("ID"); 155 | this.OnIDChanged(); 156 | } 157 | } 158 | } 159 | 160 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Candidate", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false)] 161 | public string Candidate 162 | { 163 | get 164 | { 165 | return this._Candidate; 166 | } 167 | set 168 | { 169 | if ((this._Candidate != value)) 170 | { 171 | this.OnCandidateChanging(value); 172 | this.SendPropertyChanging(); 173 | this._Candidate = value; 174 | this.SendPropertyChanged("Candidate"); 175 | this.OnCandidateChanged(); 176 | } 177 | } 178 | } 179 | 180 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Label", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false)] 181 | public string Label 182 | { 183 | get 184 | { 185 | return this._Label; 186 | } 187 | set 188 | { 189 | if ((this._Label != value)) 190 | { 191 | this.OnLabelChanging(value); 192 | this.SendPropertyChanging(); 193 | this._Label = value; 194 | this.SendPropertyChanged("Label"); 195 | this.OnLabelChanged(); 196 | } 197 | } 198 | } 199 | 200 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_RoomToken", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false)] 201 | public string RoomToken 202 | { 203 | get 204 | { 205 | return this._RoomToken; 206 | } 207 | set 208 | { 209 | if ((this._RoomToken != value)) 210 | { 211 | this.OnRoomTokenChanging(value); 212 | this.SendPropertyChanging(); 213 | this._RoomToken = value; 214 | this.SendPropertyChanged("RoomToken"); 215 | this.OnRoomTokenChanged(); 216 | } 217 | } 218 | } 219 | 220 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Sender", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false)] 221 | public string Sender 222 | { 223 | get 224 | { 225 | return this._Sender; 226 | } 227 | set 228 | { 229 | if ((this._Sender != value)) 230 | { 231 | this.OnSenderChanging(value); 232 | this.SendPropertyChanging(); 233 | this._Sender = value; 234 | this.SendPropertyChanged("Sender"); 235 | this.OnSenderChanged(); 236 | } 237 | } 238 | } 239 | 240 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_IsProcessed", DbType="Bit NOT NULL")] 241 | public bool IsProcessed 242 | { 243 | get 244 | { 245 | return this._IsProcessed; 246 | } 247 | set 248 | { 249 | if ((this._IsProcessed != value)) 250 | { 251 | this.OnIsProcessedChanging(value); 252 | this.SendPropertyChanging(); 253 | this._IsProcessed = value; 254 | this.SendPropertyChanged("IsProcessed"); 255 | this.OnIsProcessedChanged(); 256 | } 257 | } 258 | } 259 | 260 | public event PropertyChangingEventHandler PropertyChanging; 261 | 262 | public event PropertyChangedEventHandler PropertyChanged; 263 | 264 | protected virtual void SendPropertyChanging() 265 | { 266 | if ((this.PropertyChanging != null)) 267 | { 268 | this.PropertyChanging(this, emptyChangingEventArgs); 269 | } 270 | } 271 | 272 | protected virtual void SendPropertyChanged(String propertyName) 273 | { 274 | if ((this.PropertyChanged != null)) 275 | { 276 | this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 277 | } 278 | } 279 | } 280 | 281 | [global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.Room")] 282 | public partial class Room : INotifyPropertyChanging, INotifyPropertyChanged 283 | { 284 | 285 | private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty); 286 | 287 | private int _ID; 288 | 289 | private string _Token; 290 | 291 | private string _Name; 292 | 293 | private string _SharedWith; 294 | 295 | private string _Status; 296 | 297 | private System.DateTime _LastUpdated; 298 | 299 | private string _OwnerName; 300 | 301 | private string _OwnerToken; 302 | 303 | private string _ParticipantName; 304 | 305 | private string _ParticipantToken; 306 | 307 | #region Extensibility Method Definitions 308 | partial void OnLoaded(); 309 | partial void OnValidate(System.Data.Linq.ChangeAction action); 310 | partial void OnCreated(); 311 | partial void OnIDChanging(int value); 312 | partial void OnIDChanged(); 313 | partial void OnTokenChanging(string value); 314 | partial void OnTokenChanged(); 315 | partial void OnNameChanging(string value); 316 | partial void OnNameChanged(); 317 | partial void OnSharedWithChanging(string value); 318 | partial void OnSharedWithChanged(); 319 | partial void OnStatusChanging(string value); 320 | partial void OnStatusChanged(); 321 | partial void OnLastUpdatedChanging(System.DateTime value); 322 | partial void OnLastUpdatedChanged(); 323 | partial void OnOwnerNameChanging(string value); 324 | partial void OnOwnerNameChanged(); 325 | partial void OnOwnerTokenChanging(string value); 326 | partial void OnOwnerTokenChanged(); 327 | partial void OnParticipantNameChanging(string value); 328 | partial void OnParticipantNameChanged(); 329 | partial void OnParticipantTokenChanging(string value); 330 | partial void OnParticipantTokenChanged(); 331 | #endregion 332 | 333 | public Room() 334 | { 335 | OnCreated(); 336 | } 337 | 338 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_ID", AutoSync=AutoSync.OnInsert, DbType="Int NOT NULL IDENTITY", IsPrimaryKey=true, IsDbGenerated=true)] 339 | public int ID 340 | { 341 | get 342 | { 343 | return this._ID; 344 | } 345 | set 346 | { 347 | if ((this._ID != value)) 348 | { 349 | this.OnIDChanging(value); 350 | this.SendPropertyChanging(); 351 | this._ID = value; 352 | this.SendPropertyChanged("ID"); 353 | this.OnIDChanged(); 354 | } 355 | } 356 | } 357 | 358 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Token", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false)] 359 | public string Token 360 | { 361 | get 362 | { 363 | return this._Token; 364 | } 365 | set 366 | { 367 | if ((this._Token != value)) 368 | { 369 | this.OnTokenChanging(value); 370 | this.SendPropertyChanging(); 371 | this._Token = value; 372 | this.SendPropertyChanged("Token"); 373 | this.OnTokenChanged(); 374 | } 375 | } 376 | } 377 | 378 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Name", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false)] 379 | public string Name 380 | { 381 | get 382 | { 383 | return this._Name; 384 | } 385 | set 386 | { 387 | if ((this._Name != value)) 388 | { 389 | this.OnNameChanging(value); 390 | this.SendPropertyChanging(); 391 | this._Name = value; 392 | this.SendPropertyChanged("Name"); 393 | this.OnNameChanged(); 394 | } 395 | } 396 | } 397 | 398 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_SharedWith", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false)] 399 | public string SharedWith 400 | { 401 | get 402 | { 403 | return this._SharedWith; 404 | } 405 | set 406 | { 407 | if ((this._SharedWith != value)) 408 | { 409 | this.OnSharedWithChanging(value); 410 | this.SendPropertyChanging(); 411 | this._SharedWith = value; 412 | this.SendPropertyChanged("SharedWith"); 413 | this.OnSharedWithChanged(); 414 | } 415 | } 416 | } 417 | 418 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Status", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false)] 419 | public string Status 420 | { 421 | get 422 | { 423 | return this._Status; 424 | } 425 | set 426 | { 427 | if ((this._Status != value)) 428 | { 429 | this.OnStatusChanging(value); 430 | this.SendPropertyChanging(); 431 | this._Status = value; 432 | this.SendPropertyChanged("Status"); 433 | this.OnStatusChanged(); 434 | } 435 | } 436 | } 437 | 438 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_LastUpdated", DbType="DateTime NOT NULL")] 439 | public System.DateTime LastUpdated 440 | { 441 | get 442 | { 443 | return this._LastUpdated; 444 | } 445 | set 446 | { 447 | if ((this._LastUpdated != value)) 448 | { 449 | this.OnLastUpdatedChanging(value); 450 | this.SendPropertyChanging(); 451 | this._LastUpdated = value; 452 | this.SendPropertyChanged("LastUpdated"); 453 | this.OnLastUpdatedChanged(); 454 | } 455 | } 456 | } 457 | 458 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_OwnerName", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false)] 459 | public string OwnerName 460 | { 461 | get 462 | { 463 | return this._OwnerName; 464 | } 465 | set 466 | { 467 | if ((this._OwnerName != value)) 468 | { 469 | this.OnOwnerNameChanging(value); 470 | this.SendPropertyChanging(); 471 | this._OwnerName = value; 472 | this.SendPropertyChanged("OwnerName"); 473 | this.OnOwnerNameChanged(); 474 | } 475 | } 476 | } 477 | 478 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_OwnerToken", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false)] 479 | public string OwnerToken 480 | { 481 | get 482 | { 483 | return this._OwnerToken; 484 | } 485 | set 486 | { 487 | if ((this._OwnerToken != value)) 488 | { 489 | this.OnOwnerTokenChanging(value); 490 | this.SendPropertyChanging(); 491 | this._OwnerToken = value; 492 | this.SendPropertyChanged("OwnerToken"); 493 | this.OnOwnerTokenChanged(); 494 | } 495 | } 496 | } 497 | 498 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_ParticipantName", DbType="NVarChar(MAX)")] 499 | public string ParticipantName 500 | { 501 | get 502 | { 503 | return this._ParticipantName; 504 | } 505 | set 506 | { 507 | if ((this._ParticipantName != value)) 508 | { 509 | this.OnParticipantNameChanging(value); 510 | this.SendPropertyChanging(); 511 | this._ParticipantName = value; 512 | this.SendPropertyChanged("ParticipantName"); 513 | this.OnParticipantNameChanged(); 514 | } 515 | } 516 | } 517 | 518 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_ParticipantToken", DbType="NVarChar(MAX)")] 519 | public string ParticipantToken 520 | { 521 | get 522 | { 523 | return this._ParticipantToken; 524 | } 525 | set 526 | { 527 | if ((this._ParticipantToken != value)) 528 | { 529 | this.OnParticipantTokenChanging(value); 530 | this.SendPropertyChanging(); 531 | this._ParticipantToken = value; 532 | this.SendPropertyChanged("ParticipantToken"); 533 | this.OnParticipantTokenChanged(); 534 | } 535 | } 536 | } 537 | 538 | public event PropertyChangingEventHandler PropertyChanging; 539 | 540 | public event PropertyChangedEventHandler PropertyChanged; 541 | 542 | protected virtual void SendPropertyChanging() 543 | { 544 | if ((this.PropertyChanging != null)) 545 | { 546 | this.PropertyChanging(this, emptyChangingEventArgs); 547 | } 548 | } 549 | 550 | protected virtual void SendPropertyChanged(String propertyName) 551 | { 552 | if ((this.PropertyChanged != null)) 553 | { 554 | this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 555 | } 556 | } 557 | } 558 | 559 | [global::System.Data.Linq.Mapping.TableAttribute(Name="dbo.SDPMessage")] 560 | public partial class SDPMessage : INotifyPropertyChanging, INotifyPropertyChanged 561 | { 562 | 563 | private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty); 564 | 565 | private int _ID; 566 | 567 | private string _SDP; 568 | 569 | private bool _IsProcessed; 570 | 571 | private string _RoomToken; 572 | 573 | private string _Sender; 574 | 575 | #region Extensibility Method Definitions 576 | partial void OnLoaded(); 577 | partial void OnValidate(System.Data.Linq.ChangeAction action); 578 | partial void OnCreated(); 579 | partial void OnIDChanging(int value); 580 | partial void OnIDChanged(); 581 | partial void OnSDPChanging(string value); 582 | partial void OnSDPChanged(); 583 | partial void OnIsProcessedChanging(bool value); 584 | partial void OnIsProcessedChanged(); 585 | partial void OnRoomTokenChanging(string value); 586 | partial void OnRoomTokenChanged(); 587 | partial void OnSenderChanging(string value); 588 | partial void OnSenderChanged(); 589 | #endregion 590 | 591 | public SDPMessage() 592 | { 593 | OnCreated(); 594 | } 595 | 596 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_ID", AutoSync=AutoSync.OnInsert, DbType="Int NOT NULL IDENTITY", IsPrimaryKey=true, IsDbGenerated=true)] 597 | public int ID 598 | { 599 | get 600 | { 601 | return this._ID; 602 | } 603 | set 604 | { 605 | if ((this._ID != value)) 606 | { 607 | this.OnIDChanging(value); 608 | this.SendPropertyChanging(); 609 | this._ID = value; 610 | this.SendPropertyChanged("ID"); 611 | this.OnIDChanged(); 612 | } 613 | } 614 | } 615 | 616 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_SDP", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false)] 617 | public string SDP 618 | { 619 | get 620 | { 621 | return this._SDP; 622 | } 623 | set 624 | { 625 | if ((this._SDP != value)) 626 | { 627 | this.OnSDPChanging(value); 628 | this.SendPropertyChanging(); 629 | this._SDP = value; 630 | this.SendPropertyChanged("SDP"); 631 | this.OnSDPChanged(); 632 | } 633 | } 634 | } 635 | 636 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_IsProcessed", DbType="Bit NOT NULL")] 637 | public bool IsProcessed 638 | { 639 | get 640 | { 641 | return this._IsProcessed; 642 | } 643 | set 644 | { 645 | if ((this._IsProcessed != value)) 646 | { 647 | this.OnIsProcessedChanging(value); 648 | this.SendPropertyChanging(); 649 | this._IsProcessed = value; 650 | this.SendPropertyChanged("IsProcessed"); 651 | this.OnIsProcessedChanged(); 652 | } 653 | } 654 | } 655 | 656 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_RoomToken", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false)] 657 | public string RoomToken 658 | { 659 | get 660 | { 661 | return this._RoomToken; 662 | } 663 | set 664 | { 665 | if ((this._RoomToken != value)) 666 | { 667 | this.OnRoomTokenChanging(value); 668 | this.SendPropertyChanging(); 669 | this._RoomToken = value; 670 | this.SendPropertyChanged("RoomToken"); 671 | this.OnRoomTokenChanged(); 672 | } 673 | } 674 | } 675 | 676 | [global::System.Data.Linq.Mapping.ColumnAttribute(Storage="_Sender", DbType="NVarChar(MAX) NOT NULL", CanBeNull=false)] 677 | public string Sender 678 | { 679 | get 680 | { 681 | return this._Sender; 682 | } 683 | set 684 | { 685 | if ((this._Sender != value)) 686 | { 687 | this.OnSenderChanging(value); 688 | this.SendPropertyChanging(); 689 | this._Sender = value; 690 | this.SendPropertyChanged("Sender"); 691 | this.OnSenderChanged(); 692 | } 693 | } 694 | } 695 | 696 | public event PropertyChangingEventHandler PropertyChanging; 697 | 698 | public event PropertyChangedEventHandler PropertyChanged; 699 | 700 | protected virtual void SendPropertyChanging() 701 | { 702 | if ((this.PropertyChanging != null)) 703 | { 704 | this.PropertyChanging(this, emptyChangingEventArgs); 705 | } 706 | } 707 | 708 | protected virtual void SendPropertyChanged(String propertyName) 709 | { 710 | if ((this.PropertyChanged != null)) 711 | { 712 | this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 713 | } 714 | } 715 | } 716 | } 717 | #pragma warning restore 1591 718 | -------------------------------------------------------------------------------- /JavaScript.js: -------------------------------------------------------------------------------- 1 | 2 | // this experiment was written in August 2012 ... it is too old! You can try www.RTCMultiConnection.org/docs instead! 3 | 4 | var $ = function(term, selectAll, elem) { 5 | try { 6 | if (!elem) { 7 | if (term && !selectAll) return window.document.querySelector(term); 8 | return window.document.querySelectorAll(term); 9 | } else { 10 | if (term && !selectAll) return elem.querySelector(term); 11 | return elem.querySelectorAll(term); 12 | } 13 | } catch(error) { 14 | return document.getElementById(term.replace('#', '')); 15 | } 16 | }; 17 | 18 | Object.prototype.bind = function(eventName, callback) { 19 | if (this.length != undefined) { 20 | var length = this.length; 21 | for (var i = 0; i < length; i++) { 22 | this[i].addEventListener(eventName, callback, false); 23 | } 24 | } else if (typeof this == 'object') this.addEventListener(eventName, callback, false); 25 | return this; 26 | }; 27 | 28 | Object.prototype.each = function(callback) { 29 | var length = this.length; 30 | for (var i = 0; i < length; i++) { 31 | callback(this[i]); 32 | } 33 | return this; 34 | }; 35 | 36 | Object.prototype.find = function(element) { 37 | return this.querySelector(element); 38 | }; 39 | 40 | FormData.prototype.appendData = function(name, value) { 41 | if (value || value == 0) this.append(name, value); 42 | }; 43 | 44 | $.ajax = function(url, options) { 45 | 46 | var _url = options ? url : url.url; 47 | options = options || url; 48 | 49 | var xhr = new XMLHttpRequest(); 50 | xhr.onreadystatechange = function() { 51 | if (this.readyState == 4 && this.status == 200) 52 | options.success(JSON.parse(xhr.responseText)); 53 | }; 54 | 55 | xhr.open(options.type ? options.type : 'POST', _url); 56 | 57 | var formData = new window.FormData(), 58 | data = options.data; 59 | 60 | if (data) { 61 | formData.appendData('ownerName', data.ownerName); 62 | formData.appendData('ownerToken', data.ownerToken); 63 | 64 | formData.appendData('roomName', data.roomName); 65 | formData.appendData('roomToken', data.roomToken); 66 | 67 | formData.appendData('partnerEmail', data.partnerEmail); 68 | formData.appendData('userToken', data.userToken); 69 | formData.appendData('participant', data.participant); 70 | 71 | formData.appendData('sdp', data.sdp); 72 | formData.appendData('candidate', data.candidate); 73 | formData.appendData('label', data.label); 74 | 75 | formData.appendData('message', data.message); 76 | } 77 | 78 | xhr.send(formData); 79 | }; 80 | 81 | Object.prototype.prepend = function(prependMe) { 82 | return this.insertBefore(prependMe, this.firstChild); 83 | }; 84 | 85 | Object.prototype.hide = function() /* set display:none; to one or more elements */ 86 | { 87 | if (this.length != undefined) /* if more than one elements */ { 88 | this.each(function(elem) { 89 | elem.style.display = 'none'; 90 | }); 91 | } else if (typeof this == 'object') /* if only one element */ { 92 | this.style.display = 'none'; 93 | } 94 | return this; 95 | }; 96 | 97 | Object.prototype.show = function(value) /* set display:block; to one or more elements */ 98 | { 99 | if (this.length != undefined) /* if more than one elemens */ { 100 | this.each(function(elem) { 101 | if (value) elem.style.display = value; 102 | else elem.style.display = 'block'; 103 | }); 104 | } else if (typeof this == 'object') /* if only one element */ { 105 | if (value) this.style.display = value; 106 | else this.style.display = 'block'; 107 | } 108 | return this; 109 | }; 110 | 111 | Object.prototype.css = function(prop, value) { 112 | this.style[prop] = value; 113 | return this; 114 | }; 115 | 116 | Object.prototype.html = function(value) { 117 | if (value) this.innerHTML = value; 118 | else return this.innerHTML; 119 | return this; 120 | }; 121 | 122 | $.once = function(seconds, callback) { 123 | var counter = 0; 124 | var time = window.setInterval(function() { 125 | counter++; 126 | if (counter >= seconds) { 127 | callback(); 128 | window.clearInterval(time); 129 | } 130 | }, 1000); 131 | }; 132 | 133 | Object.prototype.slideDown = function(maxHeight) { 134 | return this.css('max-height', (maxHeight || 1000000) + 'px'); 135 | }; 136 | 137 | Object.prototype.slideUp = function() { 138 | return this.css('max-height', '0'); 139 | }; 140 | 141 | String.prototype.validate = function() { 142 | return this.replace( /-/g , '__').replace( /\?/g , '-qmark').replace( / /g , '--').replace( /\n/g , '-n').replace( //g , '-gt').replace( /&/g , '-amp').replace( /#/g , '-nsign').replace( /__t-n/g , '__t').replace( /\+/g , '_plus_').replace( /=/g , '-equal'); 143 | }; 144 | 145 | 146 | /* -------------------------------------------------------------------------------------------------------------------------- */ 147 | 148 | window.PeerConnection = window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.RTCPeerConnection; 149 | window.SessionDescription = window.RTCSessionDescription || window.mozRTCSessionDescription || window.RTCSessionDescription; 150 | window.IceCandidate = window.RTCIceCandidate || window.mozRTCIceCandidate || window.RTCIceCandidate; 151 | 152 | window.URL = window.webkitURL || window.URL; 153 | navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.getUserMedia; 154 | 155 | /* -------------------------------------------------------------------------------------------------------------------------- */ 156 | var global = { }; 157 | 158 | var RTC = { }, peerConnection; 159 | 160 | var chromeVersion = !!navigator.mozGetUserMedia ? 0 : parseInt(navigator.userAgent.match( /Chrom(e|ium)\/([0-9]+)\./ )[2]); 161 | var isChrome = !!navigator.webkitGetUserMedia; 162 | var isFirefox = !!navigator.mozGetUserMedia; 163 | 164 | RTC.init = function() { 165 | try { 166 | var iceServers = []; 167 | 168 | if (isFirefox) { 169 | iceServers.push({ 170 | url: 'stun:23.21.150.121' 171 | }); 172 | 173 | iceServers.push({ 174 | url: 'stun:stun.services.mozilla.com' 175 | }); 176 | } 177 | 178 | if (isChrome) { 179 | iceServers.push({ 180 | url: 'stun:stun.l.google.com:19302' 181 | }); 182 | 183 | iceServers.push({ 184 | url: 'stun:stun.anyfirewall.com:3478' 185 | }); 186 | } 187 | 188 | if (isChrome && chromeVersion < 28) { 189 | iceServers.push({ 190 | url: 'turn:homeo@turn.bistri.com:80', 191 | credential: 'homeo' 192 | }); 193 | } 194 | 195 | if (isChrome && chromeVersion >= 28) { 196 | iceServers.push({ 197 | url: 'turn:turn.bistri.com:80', 198 | credential: 'homeo', 199 | username: 'homeo' 200 | }); 201 | 202 | iceServers.push({ 203 | url: 'turn:turn.anyfirewall.com:443?transport=tcp', 204 | credential: 'webrtc', 205 | username: 'webrtc' 206 | }); 207 | } 208 | 209 | peerConnection = new window.PeerConnection({ "iceServers": iceServers }); 210 | peerConnection.onicecandidate = RTC.checkLocalICE; 211 | 212 | peerConnection.onaddstream = RTC.checkRemoteStream; 213 | peerConnection.addStream(global.clientStream); 214 | } catch(e) { 215 | document.title = 'WebRTC is not supported in this web browser!'; 216 | alert('WebRTC is not supported in this web browser!'); 217 | } 218 | }; 219 | 220 | var sdpConstraints = { 221 | optional: [], 222 | mandatory: { 223 | OfferToReceiveAudio: true, 224 | OfferToReceiveVideo: true 225 | } 226 | }; 227 | 228 | RTC.createOffer = function() { 229 | document.title = 'Creating offer...'; 230 | 231 | RTC.init(); 232 | 233 | peerConnection.createOffer(function(sessionDescription) { 234 | peerConnection.setLocalDescription(sessionDescription); 235 | 236 | document.title = 'Created offer successfully!'; 237 | sdp = JSON.stringify(sessionDescription); 238 | 239 | var data = { 240 | sdp: sdp, 241 | userToken: global.userToken, 242 | roomToken: global.roomToken 243 | }; 244 | 245 | $.ajax('/WebRTC/PostSDP', { 246 | data: data, 247 | success: function(response) { 248 | if (response) { 249 | document.title = 'Posted offer successfully!'; 250 | 251 | RTC.checkRemoteICE(); 252 | RTC.waitForAnswer(); 253 | } 254 | } 255 | }); 256 | 257 | }, onSdpError, sdpConstraints); 258 | }; 259 | 260 | RTC.waitForAnswer = function() { 261 | document.title = 'Waiting for answer...'; 262 | 263 | var data = { 264 | userToken: global.userToken, 265 | roomToken: global.roomToken 266 | }; 267 | 268 | $.ajax('/WebRTC/GetSDP', { 269 | data: data, 270 | success: function(response) { 271 | if (response !== false) { 272 | document.title = 'Got answer...'; 273 | response = response.sdp; 274 | try { 275 | sdp = JSON.parse(response); 276 | peerConnection.setRemoteDescription(new window.SessionDescription(sdp)); 277 | } catch(e) { 278 | sdp = response; 279 | peerConnection.setRemoteDescription(new window.SessionDescription(sdp)); 280 | } 281 | } else 282 | setTimeout(RTC.waitForAnswer, 100); 283 | } 284 | }); 285 | }; 286 | 287 | RTC.waitForOffer = function() { 288 | document.title = 'Waiting for offer...'; 289 | var data = { 290 | userToken: global.userToken, 291 | roomToken: global.roomToken 292 | }; 293 | 294 | $.ajax('/WebRTC/GetSDP', { 295 | data: data, 296 | success: function(response) { 297 | if (response !== false) { 298 | document.title = 'Got offer...'; 299 | RTC.createAnswer(response.sdp); 300 | } else setTimeout(RTC.waitForOffer, 100); 301 | } 302 | }); 303 | }; 304 | 305 | RTC.createAnswer = function(sdpResponse) { 306 | RTC.init(); 307 | 308 | document.title = 'Creating answer...'; 309 | 310 | var sdp; 311 | try { 312 | sdp = JSON.parse(sdpResponse); 313 | 314 | peerConnection.setRemoteDescription(new window.SessionDescription(sdp)); 315 | } catch(e) { 316 | sdp = sdpResponse; 317 | 318 | peerConnection.setRemoteDescription(new window.SessionDescription(sdp)); 319 | } 320 | 321 | peerConnection.createAnswer(function(sessionDescription) { 322 | peerConnection.setLocalDescription(sessionDescription); 323 | 324 | document.title = 'Created answer successfully!'; 325 | 326 | sdp = JSON.stringify(sessionDescription); 327 | 328 | var data = { 329 | sdp: sdp, 330 | userToken: global.userToken, 331 | roomToken: global.roomToken 332 | }; 333 | 334 | $.ajax('/WebRTC/PostSDP', { 335 | data: data, 336 | success: function() { 337 | document.title = 'Posted answer successfully!'; 338 | } 339 | }); 340 | 341 | }, onSdpError, sdpConstraints); 342 | }; 343 | 344 | RTC.checkRemoteICE = function() { 345 | if (global.isGotRemoteStream) return; 346 | 347 | if (!peerConnection) { 348 | setTimeout(RTC.checkRemoteICE, 1000); 349 | return; 350 | } 351 | 352 | var data = { 353 | userToken: global.userToken, 354 | roomToken: global.roomToken 355 | }; 356 | 357 | $.ajax('/WebRTC/GetICE', { 358 | data: data, 359 | success: function(response) { 360 | if (response === false && !global.isGotRemoteStream) setTimeout(RTC.checkRemoteICE, 1000); 361 | else { 362 | try { 363 | candidate = new window.IceCandidate({ sdpMLineIndex: response.label, candidate: JSON.parse(response.candidate) }); 364 | peerConnection.addIceCandidate(candidate); 365 | 366 | !global.isGotRemoteStream && setTimeout(RTC.checkRemoteICE, 10); 367 | } catch(e) { 368 | try { 369 | candidate = new window.IceCandidate({ sdpMLineIndex: response.label, candidate: JSON.parse(response.candidate) }); 370 | peerConnection.addIceCandidate(candidate); 371 | 372 | !global.isGotRemoteStream && setTimeout(RTC.checkRemoteICE, 10); 373 | } catch(e) { 374 | !global.isGotRemoteStream && setTimeout(RTC.checkRemoteICE, 1000); 375 | } 376 | } 377 | } 378 | } 379 | }); 380 | }; 381 | 382 | RTC.checkLocalICE = function(event) { 383 | if (global.isGotRemoteStream) return; 384 | 385 | var candidate = event.candidate; 386 | 387 | if (candidate) { 388 | var data = { 389 | candidate: JSON.stringify(candidate.candidate), 390 | label: candidate.sdpMLineIndex, 391 | userToken: global.userToken, 392 | roomToken: global.roomToken 393 | }; 394 | 395 | $.ajax('/WebRTC/PostICE', { 396 | data: data, 397 | success: function() { 398 | document.title = 'Posted an ICE candidate!'; 399 | } 400 | }); 401 | } 402 | }; 403 | 404 | var remoteVideo = $('#remote-video'); 405 | 406 | RTC.checkRemoteStream = function(remoteEvent) { 407 | if (remoteEvent) { 408 | document.title = 'Got a clue for remote video stream!'; 409 | 410 | clientVideo.pause(); 411 | clientVideo.hide(); 412 | 413 | remoteVideo.show(); 414 | remoteVideo.play(); 415 | 416 | if (!navigator.mozGetUserMedia) remoteVideo.src = window.URL.createObjectURL(remoteEvent.stream); 417 | else remoteVideo.mozSrcObject = remoteEvent.stream; 418 | 419 | RTC.waitUntilRemoteStreamStartFlowing(); 420 | } 421 | }; 422 | 423 | RTC.waitUntilRemoteStreamStartFlowing = function() { 424 | document.title = 'Waiting for remote stream flow!'; 425 | if (!(remoteVideo.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA || remoteVideo.paused || remoteVideo.currentTime <= 0)) { 426 | global.isGotRemoteStream = true; 427 | 428 | document.title = 'Finally got the remote stream!'; 429 | } else setTimeout(RTC.waitUntilRemoteStreamStartFlowing, 3000); 430 | }; 431 | 432 | /* -------------------------------------------------------------------------------------------------------------------------- */ 433 | 434 | function hideListsAndBoxes() { 435 | $('.create-room-panel').css('left', '-100%'); 436 | $('aside').css('right', '-100%'); 437 | $('.private-room').css('bottom', '-100%'); 438 | $('.stats').css('top', '-100%'); 439 | 440 | global.isGetAvailableRoom = false; 441 | } 442 | 443 | global.mediaAccessAlertMessage = 'This app wants to use your camera and microphone.\n\nGrant it the access!'; 444 | 445 | var Room = { 446 | createRoom: function(isChecked, partnerEmail) { 447 | if (!global.clientStream) { 448 | alert(global.mediaAccessAlertMessage); 449 | return; 450 | } 451 | 452 | hideListsAndBoxes(); 453 | 454 | var data = { 455 | roomName: global.roomName.validate(), 456 | ownerName: global.userName.validate() 457 | }; 458 | 459 | if (isChecked) data.partnerEmail = partnerEmail.value.validate(); 460 | 461 | $.ajax('/WebRTC/CreateRoom', { 462 | data: data, 463 | success: function(response) { 464 | if (response !== false) { 465 | global.roomToken = response.roomToken; 466 | global.userToken = response.ownerToken; 467 | 468 | document.title = 'Created room: ' + global.roomName; 469 | 470 | Room.waitForParticipant(); 471 | } 472 | } 473 | }); 474 | }, 475 | joinRoom: function(element) { 476 | if (!global.clientStream) { 477 | alert(global.mediaAccessAlertMessage); 478 | return; 479 | } 480 | 481 | hideListsAndBoxes(); 482 | 483 | var data = { 484 | roomToken: element.id, 485 | participant: prompt('Enter your name', 'Anonymous').validate() 486 | }; 487 | 488 | var email = $('#email'); 489 | if (email.value.length) data.partnerEmail = email.value.validate(); 490 | 491 | $.ajax('/WebRTC/JoinRoom', { 492 | data: data, 493 | success: function(response) { 494 | if (response != false) { 495 | global.userToken = response.participantToken; 496 | 497 | $('footer').html('Connected with ' + response.friend + '!'); 498 | document.title = 'Connected with ' + response.friend + '!'; 499 | 500 | RTC.checkRemoteICE(); 501 | 502 | setTimeout(function() { 503 | RTC.waitForOffer(); 504 | }, 3000); 505 | } 506 | } 507 | }); 508 | }, 509 | waitForParticipant: function() { 510 | $('footer').html('Waiting for someone to participate.'); 511 | document.title = 'Waiting for someone to participate.'; 512 | 513 | var data = { 514 | roomToken: global.roomToken, 515 | ownerToken: global.userToken 516 | }; 517 | 518 | $.ajax('/WebRTC/GetParticipant', { 519 | data: data, 520 | success: function(response) { 521 | if (response !== false) { 522 | global.participant = response.participant; 523 | 524 | $('footer').html('Connected with ' + response.participant + '!'); 525 | document.title = 'Connected with ' + response.participant + '!'; 526 | 527 | RTC.createOffer(); 528 | } else { 529 | $('footer').html(''); 530 | setTimeout(Room.waitForParticipant, 3000); 531 | } 532 | } 533 | }); 534 | } 535 | }; 536 | 537 | /* -------------------------------------------------------------------------------------------------------------------------- */ 538 | 539 | $('#background-image').bind('load', function() { 540 | this.css('width', innerWidth + 'px').css('height', innerHeight + 'px'); 541 | }); 542 | 543 | $('#is-private').bind('change', function() { 544 | if (this.checked) $('#partner-email').css('padding', '10px 20px').css('border-bottom', '2px solid rgba(32, 26, 26, 0.28)').slideDown().find('#partner-email').focus(); 545 | else $('#partner-email').css('padding', 0).css('border-bottom', 0).slideUp(); 546 | }); 547 | 548 | $('#create-room').bind('click', function() { 549 | var fullName = $('#full-name'), 550 | roomName = $('#room-name'), 551 | partnerEmail = $('input#partner-email'); 552 | 553 | if (fullName.value.length <= 0) { 554 | alert('Please enter your full name.'); 555 | fullName.focus(); 556 | return; 557 | } 558 | 559 | if (roomName.value.length <= 0) { 560 | alert('Please enter room name.'); 561 | roomName.focus(); 562 | return; 563 | } 564 | 565 | var isChecked = $('#is-private').checked; 566 | 567 | if (isChecked && partnerEmail.value.length <= 0) { 568 | alert('Please enter your partner\'s email or token.'); 569 | partnerEmail.focus(); 570 | return; 571 | } 572 | 573 | global.userName = fullName.value; 574 | global.roomName = roomName.value; 575 | 576 | Room.createRoom(isChecked, partnerEmail); 577 | }); 578 | 579 | $('#search-room').bind('click', function() { 580 | var email = $('input#email'); 581 | if (!email.value.length) { 582 | alert('Please enter the email or unique token/word that your partner given you.'); 583 | email.focus(); 584 | return; 585 | } 586 | 587 | global.searchPrivateRoom = email.value; 588 | 589 | $('.private-room').hide(); 590 | $('footer').html('Searching private room for: ' + global.searchPrivateRoom); 591 | }); 592 | 593 | /* -------------------------------------------------------------------------------------------------------------------------- */ 594 | 595 | var clientVideo = $('#client-video'); 596 | 597 | function captureCamera() { 598 | navigator.getUserMedia({ audio: true, video: true }, 599 | function(stream) { 600 | 601 | if (!navigator.mozGetUserMedia) clientVideo.src = window.URL.createObjectURL(stream); 602 | else clientVideo.mozSrcObject = stream; 603 | 604 | global.clientStream = stream; 605 | 606 | clientVideo.play(); 607 | }, 608 | function() { 609 | location.reload(); 610 | }); 611 | } 612 | 613 | captureCamera(); 614 | 615 | /* -------------------------------------------------------------------------------------------------------------------------- */ 616 | global.isGetAvailableRoom = true; 617 | 618 | function getAvailableRooms() { 619 | if (!global.isGetAvailableRoom) return; 620 | 621 | var data = { }; 622 | if (global.searchPrivateRoom) data.partnerEmail = global.searchPrivateRoom; 623 | 624 | $.ajax('/WebRTC/SearchPublicRooms', { 625 | data: data, 626 | success: function(response) { 627 | if (!global.searchPrivateRoom) { 628 | $('#active-rooms').html(response.publicActiveRooms); 629 | $('#available-rooms').html(response.availableRooms); 630 | $('#private-rooms').html(response.privateAvailableRooms); 631 | } 632 | 633 | document.title = response.availableRooms + ' available public rooms, ' + response.publicActiveRooms + ' active public rooms and ' + response.privateAvailableRooms + ' available private rooms'; 634 | 635 | var rooms = response.rooms; 636 | if (!rooms.length) { 637 | $('aside').html('

No room found!

No available room found.
'); 638 | } else { 639 | var html = ''; 640 | rooms.each(function(room) { 641 | html += '

' + room.roomName + '

Created by ' + room.ownerName + 'Join
'; 642 | }); 643 | 644 | $('aside').html(html); 645 | $('aside span', true).each(function(span) { 646 | span.bind('click', function() { 647 | global.roomToken = this.id; 648 | Room.joinRoom(this); 649 | }); 650 | }); 651 | } 652 | setTimeout(getAvailableRooms, 10000); 653 | } 654 | }); 655 | } 656 | 657 | getAvailableRooms(); 658 | 659 | function getStats() { 660 | $.ajax('/WebRTC/Stats', { 661 | success: function(response) { 662 | $('#number-of-rooms').html(response.numberOfRooms); 663 | $('#number-of-public-rooms').html(response.numberOfPublicRooms); 664 | $('#number-of-private-rooms').html(response.numberOfPrivateRooms); 665 | $('#number-of-empty-rooms').html(response.numberOfEmptyRooms); 666 | $('#number-of-full-rooms').html(response.numberOfFullRooms); 667 | 668 | $('.stats').css('top', '9.5%'); 669 | } 670 | }); 671 | } 672 | 673 | getStats(); 674 | 675 | /* -------------------------------------------------------------------------------------------------------------------------- */ 676 | 677 | function onSdpError(e) { 678 | console.error(e); 679 | } -------------------------------------------------------------------------------- /js/socket.io.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | function n(){return function(){}} 3 | window.JSON&&window.JSON.stringify||function(){function a(a){b.lastIndex=0;return b.test(a)?'"'+a.replace(b,function(a){var b=j[a];return"string"===typeof b?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function c(b,j){var i,l,h,g,m=e,k,d=j[b];d&&("object"===typeof d&&"function"===typeof d.toJSON)&&(d=d.toJSON(b));"function"===typeof q&&(d=q.call(j,b,d));switch(typeof d){case "string":return a(d);case "number":return isFinite(d)?String(d):"null";case "boolean":case "null":return String(d); 4 | case "object":if(!d)return"null";e+=f;k=[];if("[object Array]"===Object.prototype.toString.apply(d)){g=d.length;for(i=0;ir()?(clearTimeout(e),e=setTimeout(b,c)):(f=r(),a())}var e,f=0;return b},s=function(a){return document.getElementById(a)},t=function(a){console.error(a)},w=function(a,c){var b=[];v(a.split(/\s+/),function(a){v((c||document).getElementsByTagName(a),function(a){b.push(a)})});return b},v=function(a,c){if(a&&c)if("undefined"!=typeof a[0])for(var b=0,e=a.length;b"-_.!~*'()".indexOf(a)?a:"%"+a.charCodeAt(0).toString(16).toUpperCase()}).join("")},N=function(a){function c(a,b){V||(V=1,a||la(b),d.onerror=null,clearTimeout(ma),setTimeout(function(){a&&na();var b=s(u),c=b&&b.parentNode;c&&c.removeChild(b)},J))}if(F||G()){a:{var b,e,f=function(){if(!q){q=1;clearTimeout(B);try{e=JSON.parse(b.responseText)}catch(a){return h(1)}l(e)}},j=0,q=0,x=a.timeout||K,B=setTimeout(function(){h(1)},x),i=a.b||n(),l=a.c||n(),h=function(a){j||(j=1,clearTimeout(B), 11 | b&&(b.onerror=b.onload=null,b.abort&&b.abort(),b=null),a&&i())};try{b=G()||window.XDomainRequest&&new XDomainRequest||new XMLHttpRequest;b.onerror=b.onabort=function(){h(1)};b.onload=b.onloadend=f;b.timeout=x;var g=a.url.join(L);if(a.data){var f=[],m,g=g+"?";for(m in a.data)f.push(m+"="+a.data[m]);g+=f.join(M)}b.open("GET",g,typeof("undefined"===a.g));b.send()}catch(k){h(0);F=0;a=N(a);break a}a=h}return a}var d=E("script"),g=a.a,u=p(),V=0,ma=setTimeout(function(){c(1)},a.timeout||K),na=a.b||n(),la= 12 | a.c||n();window[g]=function(a){c(0,a)};a.g||(d[O]=O);d.onerror=function(){c(1)};d.src=a.url.join(L);if(a.data){g=[];d.src+="?";for(key in a.data)g.push(key+"="+a.data[key]);d.src+=g.join(M)}C(d,"id",u);A().appendChild(d);return c},P=function(a){var c=[];v(a,function(a,e){e.f&&c.push(a)});return c.sort()},S=function(){PUBNUB.time(r);PUBNUB.time(function(){setTimeout(function(){R||(R=1,v(ga,function(a){a()}))},J)})},G=function(){if(!ha.get)return 0;var a={id:G.id++,send:n(),abort:function(){a.id={}}, 13 | open:function(c,b){G[a.id]=a;ha.get(a.id,b)}};return a},aa=1,ea=/{([\w\-]+)}/g,O="async",L="/",M="&",ia=31E4,K=1E4,J=1E3,T="-pnpres",F=-1==navigator.userAgent.indexOf("MSIE 6");window.console||(window.console=window.console||{});console.error||(console.error=(window.opera||{}).postError||n());var U,W=window.localStorage;U={get:function(a){try{return W?W.getItem(a):-1==document.cookie.indexOf(a)?null:((document.cookie||"").match(RegExp(a+"=([^;]+)"))||[])[1]||null}catch(c){}},set:function(a,c){try{if(W)return W.setItem(a, 14 | c)&&0;document.cookie=a+"="+c+"; expires=Thu, 1 Aug 2030 20:00:00 UTC; path=/"}catch(b){}}};var X,Y=Math.floor(20*Math.random());X=function(a){return 0++Y?Y:Y=1))||a};var Z={list:{},unbind:function(a){Z.list[a]=[]},bind:function(a,c){(Z.list[a]=Z.list[a]||[]).push(c)},fire:function(a,c){v(Z.list[a]||[],function(a){a(c)})}},$=s("pubnub")||{},R=0,ga=[],qa=function(a){function c(){}function b(){}function e(a){v(P(f),function(b){a(f[b]||{})})}var f={}, 15 | j=0,q=0,x=0,B=0,i=0,l=0,h=a.publish_key||"",g=a.subscribe_key||"",m=a.ssl?"s":"",k=a.uuid||U.get(g+"uuid")||"",d="http"+m+"://"+(a.origin||"pubsub.pubnub.com"),u={history:function(a,b){var b=a.callback||b,c=a.count||a.limit||100,e=a.reverse||"false",f=a.error||n(),i=a.channel,k=a.start,h=a.end,j={},l=H();if(!i)return t("Missing Channel");if(!b)return t("Missing Callback");if(!g)return t("Missing Subscribe Key");j.count=c;j.reverse=e;k&&(j.start=k);h&&(j.end=h);N({a:l,data:j,c:function(a){b(a)},b:f, 16 | url:[d,"v2","history","sub-key",g,"channel",I(i)]})},time:function(a){var b=H(),c=X(d);N({a:b,url:[c,"time",b],c:function(b){a(b[0])},b:function(){a(0)}})},uuid:function(a){var b="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(a){var b=16*Math.random()|0;return("x"==a?b:b&3|8).toString(16)});a&&a(b);return b},publish:function(a,b){var b=b||a.callback||n(),c=a.message,e=a.channel,f=H();if(!c)return t("Missing Message");if(!e)return t("Missing Channel");if(!h)return t("Missing Publish Key"); 17 | if(!g)return t("Missing Subscribe Key");c=JSON.stringify(c);c=[d,"publish",h,g,0,I(e),f,I(c)];N({a:f,c:function(a){b(a)},b:function(){b([0,"Disconnected"])},url:c,data:{uuid:k}})},unsubscribe:function(a){a=a.channel;a=y((a.join?a.join(","):""+a).split(","),function(a){return a+","+a+T}).join(",");v(a.split(","),function(a){R&&b(a,0);f[a]={}});R&&c()},subscribe:function(a,b){function h(){var a=H(),b=P(f).join(",");b&&(x=N({timeout:ia,a:a,data:{uuid:k},url:[ca,"subscribe",g,I(b),a,l],b:function(){e(function(a){a.d|| 18 | (a.d=1,a.i(a.name))});ca=X(d);setTimeout(h,J);u.time(function(a){e(function(b){a&&b.d?(b.d=0,b.j(b.name)):b.error()})})},c:function(a){if(!a)return setTimeout(h,10);e(function(a){a.e||(a.e=1,a.h(a.name))});l=!l&&B?U.get(g)||a[1]:a[1];U.set(g,a[1]);var b,c=(2>>24]<<24^f[c>>16&255]<<16^f[c>>8&255]<<8^f[c&255];if(a%b===0){c=c<<8^c>>>24^h<<24;h=h<<1^(h>>7)*283}}d[a]=d[a-b]^c}for(b=0;a;b++,a--){c=d[b&3?a:a-4];e[b]=a<=4||b<4?c:g[0][f[c>>>24]]^g[1][f[c>>16&255]]^g[2][f[c>>8&255]]^ 28 | g[3][f[c&255]]}}; 29 | sjcl.cipher.aes.prototype={encrypt:function(a){return this.H(a,0)},decrypt:function(a){return this.H(a,1)},h:[[[],[],[],[],[]],[[],[],[],[],[]]],w:function(){var a=this.h[0],b=this.h[1],c=a[4],d=b[4],e,f,g,h=[],i=[],k,j,l,m;for(e=0;e<0x100;e++)i[(h[e]=e<<1^(e>>7)*283)^e]=e;for(f=g=0;!c[f];f^=k||1,g=i[g]||1){l=g^g<<1^g<<2^g<<3^g<<4;l=l>>8^l&255^99;c[f]=l;d[l]=f;j=h[e=h[k=h[f]]];m=j*0x1010101^e*0x10001^k*0x101^f*0x1010100;j=h[l]*0x101^l*0x1010100;for(e=0;e<4;e++){a[e][f]=j=j<<24^j>>>8;b[e][l]=m=m<<24^m>>>8}}for(e= 30 | 0;e<5;e++){a[e]=a[e].slice(0);b[e]=b[e].slice(0)}},H:function(a,b){if(a.length!==4)throw new sjcl.exception.invalid("invalid aes block size");var c=this.a[b],d=a[0]^c[0],e=a[b?3:1]^c[1],f=a[2]^c[2];a=a[b?1:3]^c[3];var g,h,i,k=c.length/4-2,j,l=4,m=[0,0,0,0];g=this.h[b];var n=g[0],o=g[1],p=g[2],q=g[3],r=g[4];for(j=0;j>>24]^o[e>>16&255]^p[f>>8&255]^q[a&255]^c[l];h=n[e>>>24]^o[f>>16&255]^p[a>>8&255]^q[d&255]^c[l+1];i=n[f>>>24]^o[a>>16&255]^p[d>>8&255]^q[e&255]^c[l+2];a=n[a>>>24]^o[d>>16& 31 | 255]^p[e>>8&255]^q[f&255]^c[l+3];l+=4;d=g;e=h;f=i}for(j=0;j<4;j++){m[b?3&-j:j]=r[d>>>24]<<24^r[e>>16&255]<<16^r[f>>8&255]<<8^r[a&255]^c[l++];g=d;d=e;e=f;f=a;a=g}return m}}; 32 | sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.P(a.slice(b/32),32-(b&31)).slice(1);return c===undefined?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var d=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-d^a[b/32+1|0]>>>d:a[b/32|0]>>>d)&(1<0&&b)a[c-1]=sjcl.bitArray.partial(b,a[c-1]&2147483648>>b-1,1);return a},partial:function(a,b,c){if(a===32)return b;return(c?b|0:b<<32-a)+a*0x10000000000},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return false;var c=0,d;for(d=0;d=32;b-=32){d.push(c);c=0}if(b===0)return d.concat(a);for(e=0;e>>b);c=a[e]<<32-b}e=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(e);d.push(sjcl.bitArray.partial(b+a&31,b+a>32?c:d.pop(),1));return d},k:function(a,b){return[a[0]^b[0],a[1]^b[1],a[2]^b[2],a[3]^b[3]]}}; 35 | sjcl.codec.utf8String={fromBits:function(a){var b="",c=sjcl.bitArray.bitLength(a),d,e;for(d=0;d>>24);e<<=8}return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c>>e)>>>26);if(e<6){g=a[c]<<6-e;e+=26;c++}else{g<<=6;e-=6}}for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d=0,e=sjcl.codec.base64.D,f=0,g;if(b)e=e.substr(0,62)+"-_";for(b=0;b26){d-=26;c.push(f^g>>>d);f=g<<32-d}else{d+=6;f^=g<<32-d}}d&56&&c.push(sjcl.bitArray.partial(d&56,f,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.a[0]||this.w();if(a){this.n=a.n.slice(0);this.i=a.i.slice(0);this.e=a.e}else this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()}; 39 | sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.n=this.N.slice(0);this.i=[];this.e=0;return this},update:function(a){if(typeof a==="string")a=sjcl.codec.utf8String.toBits(a);var b,c=this.i=sjcl.bitArray.concat(this.i,a);b=this.e;a=this.e=b+sjcl.bitArray.bitLength(a);for(b=512+b&-512;b<=a;b+=512)this.C(c.splice(0,16));return this},finalize:function(){var a,b=this.i,c=this.n;b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.e/ 40 | 4294967296));for(b.push(this.e|0);b.length;)this.C(b.splice(0,16));this.reset();return c},N:[],a:[],w:function(){function a(e){return(e-Math.floor(e))*0x100000000|0}var b=0,c=2,d;a:for(;b<64;c++){for(d=2;d*d<=c;d++)if(c%d===0)continue a;if(b<8)this.N[b]=a(Math.pow(c,0.5));this.a[b]=a(Math.pow(c,1/3));b++}},C:function(a){var b,c,d=a.slice(0),e=this.n,f=this.a,g=e[0],h=e[1],i=e[2],k=e[3],j=e[4],l=e[5],m=e[6],n=e[7];for(a=0;a<64;a++){if(a<16)b=d[a];else{b=d[a+1&15];c=d[a+14&15];b=d[a&15]=(b>>>7^b>>>18^ 41 | b>>>3^b<<25^b<<14)+(c>>>17^c>>>19^c>>>10^c<<15^c<<13)+d[a&15]+d[a+9&15]|0}b=b+n+(j>>>6^j>>>11^j>>>25^j<<26^j<<21^j<<7)+(m^j&(l^m))+f[a];n=m;m=l;l=j;j=k+b|0;k=i;i=h;h=g;g=b+(h&i^k&(h^i))+(h>>>2^h>>>13^h>>>22^h<<30^h<<19^h<<10)|0}e[0]=e[0]+g|0;e[1]=e[1]+h|0;e[2]=e[2]+i|0;e[3]=e[3]+k|0;e[4]=e[4]+j|0;e[5]=e[5]+l|0;e[6]=e[6]+m|0;e[7]=e[7]+n|0}}; 42 | sjcl.mode.ccm={name:"ccm",encrypt:function(a,b,c,d,e){var f,g=b.slice(0),h=sjcl.bitArray,i=h.bitLength(c)/8,k=h.bitLength(g)/8;e=e||64;d=d||[];if(i<7)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(f=2;f<4&&k>>>8*f;f++);if(f<15-i)f=15-i;c=h.clamp(c,8*(15-f));b=sjcl.mode.ccm.G(a,b,c,d,e,f);g=sjcl.mode.ccm.I(a,g,c,b,e,f);return h.concat(g.data,g.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var f=sjcl.bitArray,g=f.bitLength(c)/8,h=f.bitLength(b),i=f.clamp(b,h-e),k=f.bitSlice(b, 43 | h-e);h=(h-e)/8;if(g<7)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(b=2;b<4&&h>>>8*b;b++);if(b<15-g)b=15-g;c=f.clamp(c,8*(15-b));i=sjcl.mode.ccm.I(a,i,c,k,e,b);a=sjcl.mode.ccm.G(a,i.data,c,d,e,b);if(!f.equal(i.tag,a))throw new sjcl.exception.corrupt("ccm: tag doesn't match");return i.data},G:function(a,b,c,d,e,f){var g=[],h=sjcl.bitArray,i=h.k;e/=8;if(e%2||e<4||e>16)throw new sjcl.exception.invalid("ccm: invalid tag length");if(d.length>0xffffffff||b.length>0xffffffff)throw new sjcl.exception.bug("ccm: can't deal with 4GiB or more data"); 44 | f=[h.partial(8,(d.length?64:0)|e-2<<2|f-1)];f=h.concat(f,c);f[3]|=h.bitLength(b)/8;f=a.encrypt(f);if(d.length){c=h.bitLength(d)/8;if(c<=65279)g=[h.partial(16,c)];else if(c<=0xffffffff)g=h.concat([h.partial(16,65534)],[c]);g=h.concat(g,d);for(d=0;d>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^(a[0]>>>31)*135]}};sjcl.misc.hmac=function(a,b){this.M=b=b||sjcl.hash.sha256;var c=[[],[]],d=b.prototype.blockSize/32;this.l=[new b,new b];if(a.length>d)a=b.hash(a);for(b=0;b0;){b++;e>>>=1}this.b[g].update([d,this.J++,2,b,f,a.length].concat(a));break;case "string":if(b===undefined)b=a.length;this.b[g].update([d,this.J++,3,b,f,a.length]);this.b[g].update(a);break;default:throw new sjcl.exception.bug("random: addEntropy only supports number, array or string");}this.j[g]+=b;this.f+=b;if(h===0){this.isReady()!==0&&this.K("seeded",Math.max(this.g, 54 | this.f));this.K("progress",this.getProgress())}},isReady:function(a){a=this.B[a!==undefined?a:this.t];return this.g&&this.g>=a?this.j[0]>80&&(new Date).valueOf()>this.O?3:1:this.f>=a?2:0},getProgress:function(a){a=this.B[a?a:this.t];return this.g>=a?1["0"]:this.f>a?1["0"]:this.f/a},startCollectors:function(){if(!this.m){if(window.addEventListener){window.addEventListener("load",this.o,false);window.addEventListener("mousemove",this.p,false)}else if(document.attachEvent){document.attachEvent("onload", 55 | this.o);document.attachEvent("onmousemove",this.p)}else throw new sjcl.exception.bug("can't attach event");this.m=true}},stopCollectors:function(){if(this.m){if(window.removeEventListener){window.removeEventListener("load",this.o,false);window.removeEventListener("mousemove",this.p,false)}else if(window.detachEvent){window.detachEvent("onload",this.o);window.detachEvent("onmousemove",this.p)}this.m=false}},addEventListener:function(a,b){this.r[a][this.Q++]=b},removeEventListener:function(a,b){var c; 56 | a=this.r[a];var d=[];for(c in a)a.hasOwnProperty(c)&&a[c]===b&&d.push(c);for(b=0;b=1<this.g)this.g=c;this.z++; 58 | this.T(b)},p:function(a){sjcl.random.addEntropy([a.x||a.clientX||a.offsetX,a.y||a.clientY||a.offsetY],2,"mouse")},o:function(){sjcl.random.addEntropy(new Date,2,"loadtime")},K:function(a,b){var c;a=sjcl.random.r[a];var d=[];for(c in a)a.hasOwnProperty(c)&&d.push(a[c]);for(c=0;c 60 | 4)throw new sjcl.exception.invalid("json encrypt: invalid parameters");if(typeof a==="string"){c=sjcl.misc.cachedPbkdf2(a,f);a=c.key.slice(0,f.ks/32);f.salt=c.salt}if(typeof b==="string")b=sjcl.codec.utf8String.toBits(b);c=new sjcl.cipher[f.cipher](a);e.c(d,f);d.key=a;f.ct=sjcl.mode[f.mode].encrypt(c,b,f.iv,f.adata,f.ts);return e.encode(e.V(f,e.defaults))},decrypt:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json;b=e.c(e.c(e.c({},e.defaults),e.decode(b)),c,true);if(typeof b.salt==="string")b.salt= 61 | sjcl.codec.base64.toBits(b.salt);if(typeof b.iv==="string")b.iv=sjcl.codec.base64.toBits(b.iv);if(!sjcl.mode[b.mode]||!sjcl.cipher[b.cipher]||typeof a==="string"&&b.iter<=100||b.ts!==64&&b.ts!==96&&b.ts!==128||b.ks!==128&&b.ks!==192&&b.ks!==0x100||!b.iv||b.iv.length<2||b.iv.length>4)throw new sjcl.exception.invalid("json decrypt: invalid parameters");if(typeof a==="string"){c=sjcl.misc.cachedPbkdf2(a,b);a=c.key.slice(0,b.ks/32);b.salt=c.salt}c=new sjcl.cipher[b.cipher](a);c=sjcl.mode[b.mode].decrypt(c, 62 | b.ct,b.iv,b.adata,b.ts);e.c(d,b);d.key=a;return sjcl.codec.utf8String.fromBits(c)},encode:function(a){var b,c="{",d="";for(b in a)if(a.hasOwnProperty(b)){if(!b.match(/^[a-z0-9]+$/i))throw new sjcl.exception.invalid("json encode: invalid property name");c+=d+b+":";d=",";switch(typeof a[b]){case "number":case "boolean":c+=a[b];break;case "string":c+='"'+escape(a[b])+'"';break;case "object":c+='"'+sjcl.codec.base64.fromBits(a[b],1)+'"';break;default:throw new sjcl.exception.bug("json encode: unsupported type"); 63 | }}return c+"}"},decode:function(a){a=a.replace(/\s/g,"");if(!a.match(/^\{.*\}$/))throw new sjcl.exception.invalid("json decode: this isn't json!");a=a.replace(/^\{|\}$/g,"").split(/,/);var b={},c,d;for(c=0;cr()?(clearTimeout(e),e=setTimeout(b,c)):(f=r(),a())}var e,f=0;return b},s=function(a){return document.getElementById(a)},t=function(a){console.error(a)},w=function(a,c){var b=[];v(a.split(/\s+/),function(a){v((c||document).getElementsByTagName(a),function(a){b.push(a)})});return b},v=function(a,c){if(a&&c)if("undefined"!=typeof a[0])for(var b=0,e=a.length;b"-_.!~*'()".indexOf(a)?a:"%"+a.charCodeAt(0).toString(16).toUpperCase()}).join("")},N=function(a){function c(a,b){V||(V=1,a||la(b),d.onerror=null,clearTimeout(ma),setTimeout(function(){a&&na();var b=s(u),c=b&&b.parentNode;c&&c.removeChild(b)},J))}if(F||G()){a:{var b,e,f=function(){if(!q){q=1;clearTimeout(B);try{e=JSON.parse(b.responseText)}catch(a){return h(1)}l(e)}},j=0,q=0,x=a.timeout||K,B=setTimeout(function(){h(1)},x),i=a.b||n(),l=a.c||n(),h=function(a){j||(j=1,clearTimeout(B), 11 | b&&(b.onerror=b.onload=null,b.abort&&b.abort(),b=null),a&&i())};try{b=G()||window.XDomainRequest&&new XDomainRequest||new XMLHttpRequest;b.onerror=b.onabort=function(){h(1)};b.onload=b.onloadend=f;b.timeout=x;var g=a.url.join(L);if(a.data){var f=[],m,g=g+"?";for(m in a.data)f.push(m+"="+a.data[m]);g+=f.join(M)}b.open("GET",g,typeof("undefined"===a.g));b.send()}catch(k){h(0);F=0;a=N(a);break a}a=h}return a}var d=E("script"),g=a.a,u=p(),V=0,ma=setTimeout(function(){c(1)},a.timeout||K),na=a.b||n(),la= 12 | a.c||n();window[g]=function(a){c(0,a)};a.g||(d[O]=O);d.onerror=function(){c(1)};d.src=a.url.join(L);if(a.data){g=[];d.src+="?";for(key in a.data)g.push(key+"="+a.data[key]);d.src+=g.join(M)}C(d,"id",u);A().appendChild(d);return c},P=function(a){var c=[];v(a,function(a,e){e.f&&c.push(a)});return c.sort()},S=function(){PUBNUB.time(r);PUBNUB.time(function(){setTimeout(function(){R||(R=1,v(ga,function(a){a()}))},J)})},G=function(){if(!ha.get)return 0;var a={id:G.id++,send:n(),abort:function(){a.id={}}, 13 | open:function(c,b){G[a.id]=a;ha.get(a.id,b)}};return a},aa=1,ea=/{([\w\-]+)}/g,O="async",L="/",M="&",ia=31E4,K=1E4,J=1E3,T="-pnpres",F=-1==navigator.userAgent.indexOf("MSIE 6");window.console||(window.console=window.console||{});console.error||(console.error=(window.opera||{}).postError||n());var U,W=window.localStorage;U={get:function(a){try{return W?W.getItem(a):-1==document.cookie.indexOf(a)?null:((document.cookie||"").match(RegExp(a+"=([^;]+)"))||[])[1]||null}catch(c){}},set:function(a,c){try{if(W)return W.setItem(a, 14 | c)&&0;document.cookie=a+"="+c+"; expires=Thu, 1 Aug 2030 20:00:00 UTC; path=/"}catch(b){}}};var X,Y=Math.floor(20*Math.random());X=function(a){return 0++Y?Y:Y=1))||a};var Z={list:{},unbind:function(a){Z.list[a]=[]},bind:function(a,c){(Z.list[a]=Z.list[a]||[]).push(c)},fire:function(a,c){v(Z.list[a]||[],function(a){a(c)})}},$=s("pubnub")||{},R=0,ga=[],qa=function(a){function c(){}function b(){}function e(a){v(P(f),function(b){a(f[b]||{})})}var f={}, 15 | j=0,q=0,x=0,B=0,i=0,l=0,h=a.publish_key||"",g=a.subscribe_key||"",m=a.ssl?"s":"",k=a.uuid||U.get(g+"uuid")||"",d="http"+m+"://"+(a.origin||"pubsub.pubnub.com"),u={history:function(a,b){var b=a.callback||b,c=a.count||a.limit||100,e=a.reverse||"false",f=a.error||n(),i=a.channel,k=a.start,h=a.end,j={},l=H();if(!i)return t("Missing Channel");if(!b)return t("Missing Callback");if(!g)return t("Missing Subscribe Key");j.count=c;j.reverse=e;k&&(j.start=k);h&&(j.end=h);N({a:l,data:j,c:function(a){b(a)},b:f, 16 | url:[d,"v2","history","sub-key",g,"channel",I(i)]})},time:function(a){var b=H(),c=X(d);N({a:b,url:[c,"time",b],c:function(b){a(b[0])},b:function(){a(0)}})},uuid:function(a){var b="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(a){var b=16*Math.random()|0;return("x"==a?b:b&3|8).toString(16)});a&&a(b);return b},publish:function(a,b){var b=b||a.callback||n(),c=a.message,e=a.channel,f=H();if(!c)return t("Missing Message");if(!e)return t("Missing Channel");if(!h)return t("Missing Publish Key"); 17 | if(!g)return t("Missing Subscribe Key");c=JSON.stringify(c);c=[d,"publish",h,g,0,I(e),f,I(c)];N({a:f,c:function(a){b(a)},b:function(){b([0,"Disconnected"])},url:c,data:{uuid:k}})},unsubscribe:function(a){a=a.channel;a=y((a.join?a.join(","):""+a).split(","),function(a){return a+","+a+T}).join(",");v(a.split(","),function(a){R&&b(a,0);f[a]={}});R&&c()},subscribe:function(a,b){function h(){var a=H(),b=P(f).join(",");b&&(x=N({timeout:ia,a:a,data:{uuid:k},url:[ca,"subscribe",g,I(b),a,l],b:function(){e(function(a){a.d|| 18 | (a.d=1,a.i(a.name))});ca=X(d);setTimeout(h,J);u.time(function(a){e(function(b){a&&b.d?(b.d=0,b.j(b.name)):b.error()})})},c:function(a){if(!a)return setTimeout(h,10);e(function(a){a.e||(a.e=1,a.h(a.name))});l=!l&&B?U.get(g)||a[1]:a[1];U.set(g,a[1]);var b,c=(2>>24]<<24^f[c>>16&255]<<16^f[c>>8&255]<<8^f[c&255];if(a%b===0){c=c<<8^c>>>24^h<<24;h=h<<1^(h>>7)*283}}d[a]=d[a-b]^c}for(b=0;a;b++,a--){c=d[b&3?a:a-4];e[b]=a<=4||b<4?c:g[0][f[c>>>24]]^g[1][f[c>>16&255]]^g[2][f[c>>8&255]]^ 28 | g[3][f[c&255]]}}; 29 | sjcl.cipher.aes.prototype={encrypt:function(a){return this.H(a,0)},decrypt:function(a){return this.H(a,1)},h:[[[],[],[],[],[]],[[],[],[],[],[]]],w:function(){var a=this.h[0],b=this.h[1],c=a[4],d=b[4],e,f,g,h=[],i=[],k,j,l,m;for(e=0;e<0x100;e++)i[(h[e]=e<<1^(e>>7)*283)^e]=e;for(f=g=0;!c[f];f^=k||1,g=i[g]||1){l=g^g<<1^g<<2^g<<3^g<<4;l=l>>8^l&255^99;c[f]=l;d[l]=f;j=h[e=h[k=h[f]]];m=j*0x1010101^e*0x10001^k*0x101^f*0x1010100;j=h[l]*0x101^l*0x1010100;for(e=0;e<4;e++){a[e][f]=j=j<<24^j>>>8;b[e][l]=m=m<<24^m>>>8}}for(e= 30 | 0;e<5;e++){a[e]=a[e].slice(0);b[e]=b[e].slice(0)}},H:function(a,b){if(a.length!==4)throw new sjcl.exception.invalid("invalid aes block size");var c=this.a[b],d=a[0]^c[0],e=a[b?3:1]^c[1],f=a[2]^c[2];a=a[b?1:3]^c[3];var g,h,i,k=c.length/4-2,j,l=4,m=[0,0,0,0];g=this.h[b];var n=g[0],o=g[1],p=g[2],q=g[3],r=g[4];for(j=0;j>>24]^o[e>>16&255]^p[f>>8&255]^q[a&255]^c[l];h=n[e>>>24]^o[f>>16&255]^p[a>>8&255]^q[d&255]^c[l+1];i=n[f>>>24]^o[a>>16&255]^p[d>>8&255]^q[e&255]^c[l+2];a=n[a>>>24]^o[d>>16& 31 | 255]^p[e>>8&255]^q[f&255]^c[l+3];l+=4;d=g;e=h;f=i}for(j=0;j<4;j++){m[b?3&-j:j]=r[d>>>24]<<24^r[e>>16&255]<<16^r[f>>8&255]<<8^r[a&255]^c[l++];g=d;d=e;e=f;f=a;a=g}return m}}; 32 | sjcl.bitArray={bitSlice:function(a,b,c){a=sjcl.bitArray.P(a.slice(b/32),32-(b&31)).slice(1);return c===undefined?a:sjcl.bitArray.clamp(a,c-b)},extract:function(a,b,c){var d=Math.floor(-b-c&31);return((b+c-1^b)&-32?a[b/32|0]<<32-d^a[b/32+1|0]>>>d:a[b/32|0]>>>d)&(1<0&&b)a[c-1]=sjcl.bitArray.partial(b,a[c-1]&2147483648>>b-1,1);return a},partial:function(a,b,c){if(a===32)return b;return(c?b|0:b<<32-a)+a*0x10000000000},getPartial:function(a){return Math.round(a/0x10000000000)||32},equal:function(a,b){if(sjcl.bitArray.bitLength(a)!==sjcl.bitArray.bitLength(b))return false;var c=0,d;for(d=0;d=32;b-=32){d.push(c);c=0}if(b===0)return d.concat(a);for(e=0;e>>b);c=a[e]<<32-b}e=a.length?a[a.length-1]:0;a=sjcl.bitArray.getPartial(e);d.push(sjcl.bitArray.partial(b+a&31,b+a>32?c:d.pop(),1));return d},k:function(a,b){return[a[0]^b[0],a[1]^b[1],a[2]^b[2],a[3]^b[3]]}}; 35 | sjcl.codec.utf8String={fromBits:function(a){var b="",c=sjcl.bitArray.bitLength(a),d,e;for(d=0;d>>24);e<<=8}return decodeURIComponent(escape(b))},toBits:function(a){a=unescape(encodeURIComponent(a));var b=[],c,d=0;for(c=0;c>>e)>>>26);if(e<6){g=a[c]<<6-e;e+=26;c++}else{g<<=6;e-=6}}for(;d.length&3&&!b;)d+="=";return d},toBits:function(a,b){a=a.replace(/\s|=/g,"");var c=[],d=0,e=sjcl.codec.base64.D,f=0,g;if(b)e=e.substr(0,62)+"-_";for(b=0;b26){d-=26;c.push(f^g>>>d);f=g<<32-d}else{d+=6;f^=g<<32-d}}d&56&&c.push(sjcl.bitArray.partial(d&56,f,1));return c}};sjcl.codec.base64url={fromBits:function(a){return sjcl.codec.base64.fromBits(a,1,1)},toBits:function(a){return sjcl.codec.base64.toBits(a,1)}};sjcl.hash.sha256=function(a){this.a[0]||this.w();if(a){this.n=a.n.slice(0);this.i=a.i.slice(0);this.e=a.e}else this.reset()};sjcl.hash.sha256.hash=function(a){return(new sjcl.hash.sha256).update(a).finalize()}; 39 | sjcl.hash.sha256.prototype={blockSize:512,reset:function(){this.n=this.N.slice(0);this.i=[];this.e=0;return this},update:function(a){if(typeof a==="string")a=sjcl.codec.utf8String.toBits(a);var b,c=this.i=sjcl.bitArray.concat(this.i,a);b=this.e;a=this.e=b+sjcl.bitArray.bitLength(a);for(b=512+b&-512;b<=a;b+=512)this.C(c.splice(0,16));return this},finalize:function(){var a,b=this.i,c=this.n;b=sjcl.bitArray.concat(b,[sjcl.bitArray.partial(1,1)]);for(a=b.length+2;a&15;a++)b.push(0);b.push(Math.floor(this.e/ 40 | 4294967296));for(b.push(this.e|0);b.length;)this.C(b.splice(0,16));this.reset();return c},N:[],a:[],w:function(){function a(e){return(e-Math.floor(e))*0x100000000|0}var b=0,c=2,d;a:for(;b<64;c++){for(d=2;d*d<=c;d++)if(c%d===0)continue a;if(b<8)this.N[b]=a(Math.pow(c,0.5));this.a[b]=a(Math.pow(c,1/3));b++}},C:function(a){var b,c,d=a.slice(0),e=this.n,f=this.a,g=e[0],h=e[1],i=e[2],k=e[3],j=e[4],l=e[5],m=e[6],n=e[7];for(a=0;a<64;a++){if(a<16)b=d[a];else{b=d[a+1&15];c=d[a+14&15];b=d[a&15]=(b>>>7^b>>>18^ 41 | b>>>3^b<<25^b<<14)+(c>>>17^c>>>19^c>>>10^c<<15^c<<13)+d[a&15]+d[a+9&15]|0}b=b+n+(j>>>6^j>>>11^j>>>25^j<<26^j<<21^j<<7)+(m^j&(l^m))+f[a];n=m;m=l;l=j;j=k+b|0;k=i;i=h;h=g;g=b+(h&i^k&(h^i))+(h>>>2^h>>>13^h>>>22^h<<30^h<<19^h<<10)|0}e[0]=e[0]+g|0;e[1]=e[1]+h|0;e[2]=e[2]+i|0;e[3]=e[3]+k|0;e[4]=e[4]+j|0;e[5]=e[5]+l|0;e[6]=e[6]+m|0;e[7]=e[7]+n|0}}; 42 | sjcl.mode.ccm={name:"ccm",encrypt:function(a,b,c,d,e){var f,g=b.slice(0),h=sjcl.bitArray,i=h.bitLength(c)/8,k=h.bitLength(g)/8;e=e||64;d=d||[];if(i<7)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(f=2;f<4&&k>>>8*f;f++);if(f<15-i)f=15-i;c=h.clamp(c,8*(15-f));b=sjcl.mode.ccm.G(a,b,c,d,e,f);g=sjcl.mode.ccm.I(a,g,c,b,e,f);return h.concat(g.data,g.tag)},decrypt:function(a,b,c,d,e){e=e||64;d=d||[];var f=sjcl.bitArray,g=f.bitLength(c)/8,h=f.bitLength(b),i=f.clamp(b,h-e),k=f.bitSlice(b, 43 | h-e);h=(h-e)/8;if(g<7)throw new sjcl.exception.invalid("ccm: iv must be at least 7 bytes");for(b=2;b<4&&h>>>8*b;b++);if(b<15-g)b=15-g;c=f.clamp(c,8*(15-b));i=sjcl.mode.ccm.I(a,i,c,k,e,b);a=sjcl.mode.ccm.G(a,i.data,c,d,e,b);if(!f.equal(i.tag,a))throw new sjcl.exception.corrupt("ccm: tag doesn't match");return i.data},G:function(a,b,c,d,e,f){var g=[],h=sjcl.bitArray,i=h.k;e/=8;if(e%2||e<4||e>16)throw new sjcl.exception.invalid("ccm: invalid tag length");if(d.length>0xffffffff||b.length>0xffffffff)throw new sjcl.exception.bug("ccm: can't deal with 4GiB or more data"); 44 | f=[h.partial(8,(d.length?64:0)|e-2<<2|f-1)];f=h.concat(f,c);f[3]|=h.bitLength(b)/8;f=a.encrypt(f);if(d.length){c=h.bitLength(d)/8;if(c<=65279)g=[h.partial(16,c)];else if(c<=0xffffffff)g=h.concat([h.partial(16,65534)],[c]);g=h.concat(g,d);for(d=0;d>>31,a[1]<<1^a[2]>>>31,a[2]<<1^a[3]>>>31,a[3]<<1^(a[0]>>>31)*135]}};sjcl.misc.hmac=function(a,b){this.M=b=b||sjcl.hash.sha256;var c=[[],[]],d=b.prototype.blockSize/32;this.l=[new b,new b];if(a.length>d)a=b.hash(a);for(b=0;b0;){b++;e>>>=1}this.b[g].update([d,this.J++,2,b,f,a.length].concat(a));break;case "string":if(b===undefined)b=a.length;this.b[g].update([d,this.J++,3,b,f,a.length]);this.b[g].update(a);break;default:throw new sjcl.exception.bug("random: addEntropy only supports number, array or string");}this.j[g]+=b;this.f+=b;if(h===0){this.isReady()!==0&&this.K("seeded",Math.max(this.g, 54 | this.f));this.K("progress",this.getProgress())}},isReady:function(a){a=this.B[a!==undefined?a:this.t];return this.g&&this.g>=a?this.j[0]>80&&(new Date).valueOf()>this.O?3:1:this.f>=a?2:0},getProgress:function(a){a=this.B[a?a:this.t];return this.g>=a?1["0"]:this.f>a?1["0"]:this.f/a},startCollectors:function(){if(!this.m){if(window.addEventListener){window.addEventListener("load",this.o,false);window.addEventListener("mousemove",this.p,false)}else if(document.attachEvent){document.attachEvent("onload", 55 | this.o);document.attachEvent("onmousemove",this.p)}else throw new sjcl.exception.bug("can't attach event");this.m=true}},stopCollectors:function(){if(this.m){if(window.removeEventListener){window.removeEventListener("load",this.o,false);window.removeEventListener("mousemove",this.p,false)}else if(window.detachEvent){window.detachEvent("onload",this.o);window.detachEvent("onmousemove",this.p)}this.m=false}},addEventListener:function(a,b){this.r[a][this.Q++]=b},removeEventListener:function(a,b){var c; 56 | a=this.r[a];var d=[];for(c in a)a.hasOwnProperty(c)&&a[c]===b&&d.push(c);for(b=0;b=1<this.g)this.g=c;this.z++; 58 | this.T(b)},p:function(a){sjcl.random.addEntropy([a.x||a.clientX||a.offsetX,a.y||a.clientY||a.offsetY],2,"mouse")},o:function(){sjcl.random.addEntropy(new Date,2,"loadtime")},K:function(a,b){var c;a=sjcl.random.r[a];var d=[];for(c in a)a.hasOwnProperty(c)&&d.push(a[c]);for(c=0;c 60 | 4)throw new sjcl.exception.invalid("json encrypt: invalid parameters");if(typeof a==="string"){c=sjcl.misc.cachedPbkdf2(a,f);a=c.key.slice(0,f.ks/32);f.salt=c.salt}if(typeof b==="string")b=sjcl.codec.utf8String.toBits(b);c=new sjcl.cipher[f.cipher](a);e.c(d,f);d.key=a;f.ct=sjcl.mode[f.mode].encrypt(c,b,f.iv,f.adata,f.ts);return e.encode(e.V(f,e.defaults))},decrypt:function(a,b,c,d){c=c||{};d=d||{};var e=sjcl.json;b=e.c(e.c(e.c({},e.defaults),e.decode(b)),c,true);if(typeof b.salt==="string")b.salt= 61 | sjcl.codec.base64.toBits(b.salt);if(typeof b.iv==="string")b.iv=sjcl.codec.base64.toBits(b.iv);if(!sjcl.mode[b.mode]||!sjcl.cipher[b.cipher]||typeof a==="string"&&b.iter<=100||b.ts!==64&&b.ts!==96&&b.ts!==128||b.ks!==128&&b.ks!==192&&b.ks!==0x100||!b.iv||b.iv.length<2||b.iv.length>4)throw new sjcl.exception.invalid("json decrypt: invalid parameters");if(typeof a==="string"){c=sjcl.misc.cachedPbkdf2(a,b);a=c.key.slice(0,b.ks/32);b.salt=c.salt}c=new sjcl.cipher[b.cipher](a);c=sjcl.mode[b.mode].decrypt(c, 62 | b.ct,b.iv,b.adata,b.ts);e.c(d,b);d.key=a;return sjcl.codec.utf8String.fromBits(c)},encode:function(a){var b,c="{",d="";for(b in a)if(a.hasOwnProperty(b)){if(!b.match(/^[a-z0-9]+$/i))throw new sjcl.exception.invalid("json encode: invalid property name");c+=d+b+":";d=",";switch(typeof a[b]){case "number":case "boolean":c+=a[b];break;case "string":c+='"'+escape(a[b])+'"';break;case "object":c+='"'+sjcl.codec.base64.fromBits(a[b],1)+'"';break;default:throw new sjcl.exception.bug("json encode: unsupported type"); 63 | }}return c+"}"},decode:function(a){a=a.replace(/\s/g,"");if(!a.match(/^\{.*\}$/))throw new sjcl.exception.invalid("json decode: this isn't json!");a=a.replace(/^\{|\}$/g,"").split(/,/);var b={},c,d;for(c=0;c