├── 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 |
42 |
43 |
44 |
45 |
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 |
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";var ha=s("pubnubs")||{};z("load",window,function(){setTimeout(S,0)});PUBNUB.rdx=function(a,c){if(!c)return G[a].onerror();G[a].responseText=unescape(c);G[a].onload()};G.id=J;window.jQuery&&(window.jQuery.PUBNUB=
23 | PUBNUB);"undefined"!==typeof module&&(module.exports=PUBNUB)&&S()};
24 | })();
25 | (function(){
26 | "use strict";var sjcl=window['sjcl']={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}};
27 | sjcl.cipher.aes=function(a){this.h[0][0][0]||this.w();var b,c,d,e,f=this.h[0][4],g=this.h[1];b=a.length;var h=1;if(b!==4&&b!==6&&b!==8)throw new sjcl.exception.invalid("invalid aes key size");this.a=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(a%b===0||b===8&&a%b===4){c=f[c>>>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";var ha=s("pubnubs")||{};z("load",window,function(){setTimeout(S,0)});PUBNUB.rdx=function(a,c){if(!c)return G[a].onerror();G[a].responseText=unescape(c);G[a].onload()};G.id=J;window.jQuery&&(window.jQuery.PUBNUB=
23 | PUBNUB);"undefined"!==typeof module&&(module.exports=PUBNUB)&&S()};
24 | })();
25 | (function(){
26 | "use strict";var sjcl=window['sjcl']={cipher:{},hash:{},keyexchange:{},mode:{},misc:{},codec:{},exception:{corrupt:function(a){this.toString=function(){return"CORRUPT: "+this.message};this.message=a},invalid:function(a){this.toString=function(){return"INVALID: "+this.message};this.message=a},bug:function(a){this.toString=function(){return"BUG: "+this.message};this.message=a},notReady:function(a){this.toString=function(){return"NOT READY: "+this.message};this.message=a}}};
27 | sjcl.cipher.aes=function(a){this.h[0][0][0]||this.w();var b,c,d,e,f=this.h[0][4],g=this.h[1];b=a.length;var h=1;if(b!==4&&b!==6&&b!==8)throw new sjcl.exception.invalid("invalid aes key size");this.a=[d=a.slice(0),e=[]];for(a=b;a<4*b+28;a++){c=d[a-1];if(a%b===0||b===8&&a%b===4){c=f[c>>>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