WebSync On-Demand Disruption and Resolution

by anton.venema 13. May 2013 01:50

On Friday morning (May 10), we experienced a rather large surge in traffic to our WebSync On-Demand cluster that resulted in some intermittent service disruption (high latency). In response, we deployed additional resources and the problem was resolved.

On Sunday morning (May 12), we noticed higher-than-normal latency in our server response time again. A high-level report on server performance showed much higher-than-normal CPU usages and ballooning memory consumption. The issue appeared isolated to IIS, and resetting the On-Demand worker processes temporarily resolved the issue... but only temporarily. We created some memory dumps and fired up WinDbg to dig deeper into the memory heap. It didn't take long to find that over 70% of the memory allocated within the process was coming from thousands of copies of a very specific string - one with exactly 68,460 bytes - the minified WebSync On-Demand JavaScript client. This pointed us to our WebSync On-Demand JavaScript client loader, which supports the loading of old JavaScript clients by attaching a v={version} key/value pair to the query string of the client script URL. We have several versions of the JavaScript client cached within WebSync On-Demand, but certainly not thousands! After looking at the caching code and referring back to our earlier memory dump, it became clear that a new (very active) customer on the cluster was using a query string that effectively broke our caching system, causing a new copy of the JavaScript to be cached on the server for every single page load. The exposed bug in our caching system was patched and uploaded immediately to all the servers. As of around 2:00pm PST, any issues related to this bug have been resolved, and the servers are running smoothly.

Tags:

Product Release x.2.0

by anton.venema 6. May 2013 00:49

WebSync 4.2.0, IceLink 2.2.0, and TheRest 2.2.0 are publicly available. All products have had their examples re-worked to be easier to find/deploy and cover more relevant use cases. MonoDroid and MonoTouch have been renamed to Xamarin.Android and Xamarin.iOS per Xamarin's product name changes. Other major changes include:

  • WebSync has new demos - Collaborative Mapping, ExtJS Stock Ticker, Multipage Chat, and Photo Phun.
  • WebSync has new examples for native Android and PHP.
  • TheRest has a new example for native Android.
  • IceLink received a ton of bug fixes and improvements.
  • IceLink now has WebRTC libraries for Java, Android, .NET Compact, Xamarin.iOS/Android, Windows Phone 8, and Windows 8 (VP8 newly available on Java).
  • IceLink now has server support for all platforms. Any device can become a relay node!
  • IceLink has more descriptive error messages, particularly when a connection fails due to Community restrictions.
  • IceLink has a vastly improved API for ConnectionHubs - more intuitive, more flexible, and easier to use.

One important item to note is that the API for IceLink ConnectionHubs have changed. Previously, ConnectionHubs were "started" and "stopped" which meant different things depending on the provider. The WebSync provider, for example, subscribed to a channel when the hub was started, and unsubscribed when it was stopped; new clients joining the same channel were linked, and existing clients leaving were unlinked. The problem was that this concept didn't translate well to other session negotiation technologies.

The new ConnectionHub API gets rid of "Start" and "Stop" and replaces it with "Link" and "Unlink". Hubs still abstract away the session negotiation and exchange of offers, answers, and candidates, but don't force the concept of starting and stopping on the provider. The WebSync provider, for example, now allows you to subscribe to a channel in your own code, and call "Link" when a client joins the same channel or "Unlink" when a client leaves the channel. This separates the concept of a link "trigger" from the link establishment process itself - a little more coding on your part, but tons more flexibility. ConnectionHubs simply manage a set of connections. You decide what creates or destroys them.

// OLD WAY (less code, less intuitive, less flexibility)

var client = new Client();
client.Connect();

var hub = new ConnectionHub(client, "/channel");
hub.Start(streamDescriptions)
{
    OnLinkInit = (e) =>
    {
        // peer initializing a new connection
    }
}

// NEW WAY (more code, more intuitive, more flexibility)
var client = new Client();
var hub = new ConnectionHub(client, streamDescriptions)
{
    OnLinkInit = (e) =>
    {
        // peer initializing a new connection
    }
}

client.Connect();
client.Subscribe(new SubscribeArgs("/channel")
{
    OnClientSubscribe = (e) =>
    {
        var peer = e.SubscribedClient;
        client.Link(new PeerClient(peer.Id, peer.BoundRecords));
    },
    OnClientUnsubscribe = (e) =>
    {
        var peer = e.UnsubscribedClient;
        client.Unlink(new PeerClient(peer.Id, peer.BoundRecords));
    }
});

All the examples and documentation have been updated to reflect the new API. Should you have any questions, please feel free, as always, to contact us!

Visit the Downloads page!

Tags:

New Releases - WebSync, IceLink, TheRest

by anton.venema 10. April 2013 03:41

Fresh new releases of WebSync (4.1.5), IceLink (2.1.5), and TheRest (2.1.5) are now available! Notable changes include:

  • Added support for running WebSync and TheRest clients within a node.js application. Check out the JavaScript example in the download!
  • Updated the WebSync SQL provider to address recent changes to SQL Azure.
  • Added the experimental MeshProvider to WebSync, which lets you run a scalable cluster... without SQL server!
  • Added support in IceLink for sending packets to specific peers within a hub.
  • Fixed a bug in IceLink that created jibberish when sending an encrypted packet to multiple peers.
  • Fixed a bug in IceLink for .NET that prevented access to audio/video capture when using the .NET 4.0 security model.

We're especially proud of the new MeshProvider in WebSync, which creates a fully-connected network in your server cluster to eliminate the need for any additional back-end services like SQL Server. For large deployments, this should reduce network traffic, eliminate single points of failure, and improve performance significantly. Please check it out and give us your feedback!

Tags:

Build an Auction Website with WebSync

by Ben Swayne 1. March 2013 10:00

Now that WebSync 4 is out of Beta, it’s time to put it to use!

This demo will be a simple auction website that demonstrates how WebSync enables you to keep your client’s browser up to date at all times even during intense back and forth bidding wars!

Before you start reading the code walk-through, you should download the WebSync package and open the project found in the ‘\demos\WebSyncAuction\’ folder. Run the project and place some bids. Once you’ve got a functional overview of the demo the source code walk-through will be easier to follow.

The technology stack we’ve chosen for this demo is:

  • Visual Studio 2010 with .Net Framework 4.0
  • SqlServer Compact Edition 4.0 Database
  • EntityFramework for datalayer
  • WebSync for real time data updates
  • jQuery and jQuery UI for client side user interface

We’ll break down the demo into a few categories of development.

  1. Create a new Web Project
  2. Setup a basic auction data model and database with SqlCE 4 and EntityFramework
  3. Write some business logic to manage our auctions
  4. Write some WebSync events to handle subscriptions to our auction website
  5. Write a WebSync event to handle placing bids on auction items
  6. Setup our MasterPage and Default.aspx
  7. Write a JavaScript script client for the web page itself using jQuery and jQuery UI
  8. Testing the Auction app

Ok let’s get started!

Create a new Web Project in Visual Studio

To get started, let’s create a new “ASP.NET Web Application” in visual studio and name it “WebSyncAuction”.

  • Click File -> New -> Project…
  • Select “ASP.NET Web Application” from the list of available web project templates
  • Enter a project name like “WebSyncAuction”
  • Click Ok

To keep things simple for the purposes of this demo we are going to remove the Forms Authentication stuff from the project. They don’t hurt anything and you could certainly add authentication and login to this demo easily, but we want to start with a basic “pure” demo.

  • Delete the “Account” folder and all contents.
  • Delete the “About.aspx” page.
  • Delete Forms Authentication settings from the Web.Config file.
  • Delete login elements from the default MasterPage. (The login and logout navigation toolbar items.)
  • Delete “About” menu item from the default MasterPage.

Now we need to add some of the project references via Nuget.

  • Right click your project in the “Solution Explorer” within Visual Studio and select “Manage NuGet Packages…”
  • Search for ‘Microsoft.SqlServer.Compact’ and click Install. As of this tutorial, the current version is 4.0.8876.1
  • Search for ‘EntityFramework’ and click Install. As of this tutorial, the current version is 5.0.0.
  • Search for ‘jQuery’ and click Install. As of this tutorial, the current version is 1.9.0.
  • Search for ‘jQuery.UI.Combined’ and click Install. As of this tutorial, the current version is 1.10.0. (If you didn’t already add jQuery it will prompt you to do so)
  • Search for ‘jQuery.Templates’ and click Install. As of this tutorial, the current version is 0.1 Beta 1.

Now we need a few more references for this project which are not NuGet packages. The location of the WebSync assemblies will vary depending on your purchased license.

  • Right click your project in the “Solution Explorer” within Visual Studio and select “Add Reference”.
  • Click the “Browse” tab and locate your WebSync assemblies.
  • Add “FM.dll”, “FM.Server.dll”, “FM.WebSync.dll” and “FM.WebSync.Server.dll” to your project.

Setup a basic auction website data model and database with SqlCE 4 and EntityFramework

Before we can demo the WebSync part of an auction application, we need some basic foundational pieces in place. Let’s start by creating our database, building some data models and configuring EntityFramework.

  • Right click ‘App_Data’, select Add -> New Item. In the “Data” templates, select “SQL Server Compact 4.0 Local Database” and name your database “WebSyncAuctionDatabase.sdf”. Click “Add”.
  • Create two folders in your project, “Data” and “Migrations”
  • In your “Data” folder, add a class file named “Auction.cs”
  • In your “Data” folder, add a class file named “DemoDataContext.cs”
  • In your “Migrations” folder, add a class file named “Configuration.cs”

Now let’s create our Auction data models in our “Auction.cs” file. We’re going to create two data models: an “Auction” and an “AuctionBid”. Every “Auction” will maintain a historical list of all bids placed as a collection of “AuctionBid” records.

/// 
/// Each Auction is one item available for bidding on our website.
/// 
[DataContract]
public class Auction
{
    [Key]
    [DataMember(Name = "id")]
    public int Id { get; set; }

    [MaxLength(128)]
    [DataMember(Name = "description")]
    public string Description { get; set; }

    [MaxLength(128)]
    [DataMember(Name = "photoURL")]
    public string PhotoURL { get; set; }

    private DateTime _Expiration;
    [DataMember(Name = "expiration")]
    public DateTime Expiration
    {
        get
        {
            return new DateTime(_Expiration.Ticks, DateTimeKind.Utc);
        }
        set
        {
            if (value.Ticks == _Expiration.Ticks)
                return;

            _Expiration = new DateTime(value.Ticks, DateTimeKind.Utc);
        }
    }

    [DataMember(Name = "minimumBidIncrement")]
    public decimal MinimumBidIncrement { get; set; }

    [MaxLength(32)]
    [DataMember(Name = "currentBidUsername")]
    public string CurrentBidUsername { get; set; }

    [DataMember(Name = "currentBidPrice")]
    public decimal CurrentBidPrice { get; set; }

    /// 
    /// Collection of bid history records.
    /// 
    public virtual ICollection BidHistory { get; set; }
}

/// 
/// Each AuctionBid represents one user's bid on a particular Auction.
/// 
[DataContract]
public class AuctionBid
{
    [Key]
    [DataMember(Name = "id")]
    public int Id { get; set; }

    [DataMember(Name = "auctionId")]
    public int AuctionId { get; set; }
    public virtual Auction Auction { get; set; }

    [DataMember(Name = "timestamp")]
    public DateTime Timestamp { get; set; }

    [MaxLength(32)]
    [DataMember(Name = "username")]
    public string Username { get; set; }

    [DataMember(Name = "bidPrice")]
    public decimal BidPrice { get; set; }
}

Now that we have some data models in place, lets setup a new connection string to the web.config for your new database file:

<add name="WebSyncAuctionDB" connectionString="Data Source=|DataDirectory|WebSyncAuctionDatabase.sdf; Persist Security Info=False;" 

providerName="System.Data.SqlServerCe.4.0" />

Now we need to create out EntityFramework “DataContext” which will use our new connection string. We’ll add the DataContext to our “DemoDataContext.cs” file we created.

public class DemoDataContext : DbContext
{
    /// 
    /// Use the Database ConnectionString from our Web.Config called 'WebSyncAuctionDB'
    /// 
    public DemoDataContext()
        : base("name=WebSyncAuctionDB")  
    { }

    /// 
    /// Our collection of contacts which represents one physical table in the database.
    /// 
    public DbSet Auctions { get; set; }
    public DbSet AuctionBids { get; set; }
}

Now that we have some data models and a place to store them, we need to tell EntityFramework what behavior we want for database migrations. For the purposes of this demo, I want to EntityFramework to “seed” the database with some demo data anytime the application pool starts up. We’re going to build a custom “DbMigrationsConfiguration” in our “Configuration.cs” file we put in the Migrations project folder.

internal sealed class Configuration : DbMigrationsConfiguration
{
    public Configuration()
    {
        // Do NOT set this to true
        AutomaticMigrationsEnabled = false;
    }

    protected override void Seed(DemoDataContext context)
    {
        //  This method will be called after migrating to the latest version.

        //  You can use the DbSet.AddOrUpdate() helper extension method 
        //  to avoid creating duplicate seed data. E.g.
        //
        context.Auctions.AddOrUpdate(
            p => p.Description,
            new Auction {
                Description = "Mobile Phone",
                PhotoURL = "/images/mobilephone.png",
                Expiration = DateTime.UtcNow.AddMinutes(5).AddSeconds(53),
                MinimumBidIncrement = 1.25M,
                CurrentBidPrice = 1.00M,
                CurrentBidUsername = "Ben"
            },
            new Auction {
                Description = "Laptop",
                PhotoURL = "/images/laptop.png",
                Expiration = DateTime.UtcNow.AddMinutes(15).AddSeconds(2),
                MinimumBidIncrement = 10.00M,
                CurrentBidPrice = 100.00M
            },
            new Auction {
                Description = "Printer",
                PhotoURL = "/images/printer.png",
                Expiration = DateTime.UtcNow.AddMinutes(10).AddSeconds(7),
                MinimumBidIncrement = 2.5M,
                CurrentBidPrice = 15.00M
            }
        );
    }
}

Our data layer is almost complete; we just need to setup our configuration for EntityFramework to use our DemoDataContext and our custom DbMigrationsConfiguration class. These settings go in the web.config file.

First we need to declare the EntityFramework config section:

<configuration>
  <configSections>
    <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
    <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.4.0.0, 

Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
  </configSections>
</configuration>

Now we can add the actual EntityFramework configuration:

<entityFramework>
  <defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
    <parameters>
      <parameter value="v11.0" />
    </parameters>
  </defaultConnectionFactory>
  <contexts>
    <context type="WebSyncAuction.Data.DemoDataContext, WebSyncAuction">
      <databaseInitializer type="System.Data.Entity.MigrateDatabaseToLatestVersion`2[[WebSyncAuction.Data.DemoDataContext, WebSyncAuction],  

              [WebSyncAuction.Migrations.Configuration,  WebSyncAuction]], EntityFramework" />
    </context>
  </contexts>
</entityFramework>

It’s now time to get EntityFramework to create the migration which will create our database tables in the local SqlCE database. My personal preference is to disable automatic migrations (you saw that in our custom migration configuration) and instead create my migrations explicitly when I’m ready for it. To create a migration manually you need to open the “Package Manager Console” from Visual Studio’s Tools Menu -> Library Package Manager -> Package Manager Console. The package manager console will already be configured by NuGet when you installed the EntityFramework NuGet package so that you can execute some EntityFramework commands.

  • Open the package manager console and type “Add-Migration CreateAuctionDataTables” and press enter. EntityFramework will automatically build your migration for you and add it to your project.
  • After you’ve reviewed the migration for accuracy, you can type “Update-Database” and press enter to have EntityFramework apply all pending migrations to your database.

Congratulations, you now have a working data layer for your auction application! In the next section of this tutorial we will put it to work and create a couple business methods to work with our auction data.

Write some business logic to manage our auctions

Before we start writing our business logic, let’s deal with how and when to create an EntityFramework DataContext and when to destroy it. A best practice for web projects is to use one DataContext throughout each HttpRequest. Each HttpRequest typically represents one request or operation by the end user and logically matches with one transaction against the database by default. There are lots of ways to approach this, however this has worked well for us in many EntityFramework web applications. We will create an instance of our DataContext in the Global.asax ‘Application_BeginRequest()’ method and then dispose of it in our ‘Application_EndRequest’ method.

protected virtual void Application_BeginRequest()
{
    // We're going to create one EntityFramework data context to be used throughout 
    // this HttpRequest. We'll store it in the HttpContext so that it can be retrieved 
    // by our application API.
    DemoDataContext context = new DemoDataContext();
    context.Configuration.LazyLoadingEnabled = false;
    context.Configuration.ProxyCreationEnabled = false;
    HttpContext.Current.Items["_EntityContext"] = context;
}

protected virtual void Application_EndRequest()
{
    // Clean up our current context. EntityFramework data contexts grow in memory 
    // footprint as they are used and should not be kept around for longer than 
    // necessary.
    var entityContext = HttpContext.Current.Items["_EntityContext"] as DemoDataContext;
    if (entityContext != null)
        entityContext.Dispose();
}

Now that we have a new DataContext stored in the HttpContext for every HttpRequest, we can easily access it via an internal property getter. This is really just convenience as it keeps our Linq statements nice and readable. Ok, start by adding an “AuctionManager” class to your project’s “Data” folder. We will write our business logic in this class to list open auctions and process incoming bids.

public class AuctionsManager
{
    /// 
    /// A convenient way to grab our current entityframework data context 
    /// out of the HttpContext for use in the above CRUD methods for our 
    /// Auctions.
    /// 
    internal static DemoDataContext CurrentDataContext
    {
        get
        {
            DemoDataContext returnContext = null;

            //
            // This is created/disposed in Application_BeginRequest() and 
            // Application_EndRequest() when running in a web environment
            //
            // This is good practice with EntityFramework in a web 
            // environment as it ensures a healthy lifecycle for the data 
            // context.
            //
            if (HttpContext.Current != null)
            {
                returnContext = HttpContext.Current.Items["_EntityContext"] as DemoDataContext;
            }

            return returnContext;
        }
    }
}

Now that we have our “AuctionManager” class to hold our business logic, we’re going to create two methods for our auction application. First, let’s create a “ListActive” method to retrieve all active (non-expired) auctions. When we subscribe to the ‘/auctions’ channel we are really only interested in loading active (non-expired) auction items for display to the end user.

public static List ListActive()
{
    var now = DateTime.UtcNow;

    // Return only auctio Auctions with an expiration date in the future.
    return CurrentDataContext.Auctions
        .Where(auction => auction.Expiration > now)
        .ToList();
}

Next, let’s create a “ProcessBid” method. We will use this method in a WebSync event for handling incoming auction bids. The business logic in this method will also do some basic validation of the request to prevent bidding on expired auctions or bidding below the current minimum bid.

public static Auction ProcessBid(int auctionId, AuctionBid auctionBid)
{
    // Find the Auction we want to update in the database.
    var dbAuction = CurrentDataContext.Auctions.Find(auctionId);

    // Make sure Auction hasn't expired!
    if (dbAuction.Expiration < DateTime.UtcNow)
    {
        throw new Exception("Sorry, this auction has expired!");
    }

    // Make sure the new bid is greater than the current bid + minimum increment
    if (auctionBid.BidPrice >= (dbAuction.CurrentBidPrice + dbAuction.MinimumBidIncrement))
    {
        dbAuction.CurrentBidPrice = auctionBid.BidPrice;
        dbAuction.CurrentBidUsername = auctionBid.Username;
    }
    else
    {
        // If not, make sure its greater than the current bid (did they just fail to add the minimum increment or did another user outbid 

them?)
        throw new ArgumentException(string.Format("Your bid must exceed the previous bid of ${0} by this Auction's minimum bid increment of 

${1}.", dbAuction.CurrentBidPrice, dbAuction.MinimumBidIncrement));
    }

    // Save a Bid History record for this bid.
    var newAuctionBid = CurrentDataContext.AuctionBids.Create();
    newAuctionBid.AuctionId = dbAuction.Id;
    newAuctionBid.BidPrice = auctionBid.BidPrice;
    newAuctionBid.Username = auctionBid.Username;
    newAuctionBid.Timestamp = DateTime.UtcNow;
    CurrentDataContext.AuctionBids.Add(newAuctionBid);

    // Commit proposed DB changes.
    CurrentDataContext.SaveChanges();

    //
    // This is the magic WebSync part. If we get to this line of code 
    // then none of the above database code threw an exception and our 
    // auction was in fact updated. Therefor we can let the world 
    // know it was modified by publishing to the channel we planned 
    // to use to notify clients about changes to this auction.
    //
    WebSyncServer.Publish(string.Format("/auctions/{0}", dbAuction.Id), Json.Serialize(dbAuction));

    return dbAuction;
}

If the new bid was successfully saved, it will also be published via WebSync to immediately update all of our subscribed clients with the new current bid price and bidder name.

Write some WebSync events to handle subscriptions to our auction website

Now that we have all that basic stuff out of the way we can see how it comes together using WebSync events. The first set of WebSync events we are going to author will allow the WebSync client to subscribe to a list of open auctions and receive updates any time the bid price goes up.

This requires two WebSyncEvents, one BeforeSubscribe and one AfterSubscribe to customize the incoming subscriptions for the ‘/auctions’ channel and then return our initial set of data (currently active auctions). As the event names suggest a BeforeSubscribe event will fire before a client is subscribed to a channel and the AfterSubscribe event fires after the subscription is complete. During a BeforeSubscribe event you have the opportunity to modify the subscription request or cancel it completely. In our case we want to modify the subscription to include some extra channels for the client to receive updates about each auction we will return to it. In our AfterSubscribe event the client has already been subscribed to the set of channels we just constructed; now we just want to attach some extra data to the response that will be sent to the client.

[WebSyncEvent(EventType.BeforeSubscribe, "/auctions", FilterType.Template)]
public static void BeforeSubscribeAuctionsList(object sender, WebSyncEventArgs e)
{
    // ************************************************************
    // Retrieve the initial data records for this channel.
    // ************************************************************
    List results = AuctionsManager.ListActive();

    // ************************************************************
    // Save the retrieved initial data so that we can use it in 
    // another WebSync event, the AfterSubscribe event, and return 
    // this data to the client.
    // ************************************************************
    e.SetDynamicValue("Demo.Auctions", results);

    // ************************************************************
    // Replace the subscription channels list with channels 
    // appropriate to the rows of data we are returning in the 
    // MetaJson field.
    //
    // For each record returned, we are going to add another 
    // channel to this subscription to receive updates for that 
    // auction like a new bid price.
    // ************************************************************
    List channels = new List();
    channels.Add("/auctions");
    foreach (var Auction in results)
    {
        channels.Add(string.Format("/auctions/{0}", Auction.Id));
    }
    e.SubscribeInfo.Channels = channels.ToArray();
}
        
[WebSyncEvent(EventType.AfterSubscribe, "/auctions", FilterType.Template)]
public static void AfterSubscribeAuctionsList(object sender, WebSyncEventArgs e)
{
    // ************************************************************
    // Retrieve the initial data records for this channel.
    // ************************************************************
    var results = (List)e.GetDynamicValue("Demo.Auctions");

    // ************************************************************
    // Return our initial data in the MetaJson field.
    // ************************************************************
    e.MetaJson = Json.Serialize(new List(results));
}

You might have noticed that we retrieve our data in the BeforeSubscribe event but don’t actually attach it to the response until AfterSubscribe. This is the best practice to maintain data integrity. If we set this initial data as part of the response too early there is still opportunity for the subscribe request to fail and our data would not be appropriate for that type of response, so you need to provide the data in the AfterSubscribe event. However you need to build the customized list of channel subscriptions in the BeforeSubscribe event where you still have the opportunity to modify the subscription request before it is processed, so you needed to load the data there. WebSync 4 provides some convenient features for passing data between WebSyncEvents using the SetDynamicValue and GetDynamicValue methods available on the WebSyncEventArgs class.

Setup our MasterPage and Default.aspx

Before we start coding the JavaScript we need to setup the page with the necessary JavaScript refences for WebSync, jQuery, jQueryUI and any other JavaScript we need. We’ll put all our JavaScript references in the MasterPage. Then we’ll clean up the Default.aspx to remove anything we don’t need for this demo.

First lets include jQuery and jQueryUI plus the necessary CSS stylesheet for jQueryUI components:

<!-- Include jQuery UI CSS Stylesheets -->
<link href="~/Content/themes/base/jquery.ui.all.css" rel="stylesheet" type="text/css" />

<!-- Include jQuery JavaScript References -->
<script type="text/javascript" src="<%=ResolveClientUrl("~/Scripts/jquery-1.8.3.js")%>"></script>
<script type="text/javascript" src="<%=ResolveClientUrl("~/Scripts/jquery-ui-1.9.2.js")%>"></script>

Now let’s include some helpful JavaScript libraries:

<!-- Include jQuery extras -->
<script type="text/javascript" src="<%=ResolveClientUrl("~/Scripts/jquery.blockUI.js")%>"></script>
<script type="text/javascript" src="<%=ResolveClientUrl("~/Scripts/jQuery.tmpl.js")%>"></script>
<script type="text/javascript" src="<%=ResolveClientUrl("~/Scripts/jquery.noticeWriter.js")%>"></script>
<script type="text/javascript" src="<%=ResolveClientUrl("~/Scripts/moment.js")%>"></script>
  • The jQuery.blockUI plugin is used to block DOM elements with a translucent light box effect and a loading indicator. Particularly when you first launch the application in Visual Studio, the WebSync subscribe may take a few seconds while EntityFramework runs the migration and seeds or re-seeds the demo data in the database. The blockUI plugin keeps the auction container dimmed with a nice little loading method.
  • jQuery.tmpl is a templating plugin for jQuery to generate DOM elements given a data object. While it is officially deprecated, for the purposes of a simple demo app like this it will save us some time and simplify the JavaScript code. In a real production system you might choose another approach for building DOM elements with jQuery.
  • jQuery.noticeWriter is a little utility for injecting notices with jQueryUI CSS decoration into the DOM and doing a little animation to grab the end user’s attention.
  • Moment.js helps us work with dates/times so we can have a cool countdown timer on our auction listings.

Now let’s include the WebSync JavaScript library and its only dependency fm.js:

<!-- Include the FM Core JavaScript Library (required for WebSync4) -->
<script type="text/javascript" src="<%=ResolveClientUrl("~/Resources/fm/fm.js")%>"></script>
<!-- Include the WebSync JavaScript Library -->
<script type="text/javascript" src="<%=ResolveClientUrl("~/Resources/fm/fm.websync.js")%>"></script>
  • fm.js is where we keep functionality that is shared between multiple FrozenMountain libraries. This helps prevent duplicate code if you are using both WebSync and TheRest frameworks in your project.
  • Fm.websync.js is the WebSync client for JavaScript

While we are editing the MasterPage we also removed any extra menu items added automatically by Visual Studio. Now we can edit the Default.aspx page with a nice container div for us to put our auction items into:

<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>Welcome to the WebSync Auction Demo!</h2>
    <p>This is a demo using WebSync and jQuery to build a simple auction website.</p>
    <p>To read the full technical write-up explaining this demo visit <a href="http://www.frozenmountain.com/Blog/post/Build-an-Auction-

Website-with-WebSync.aspx" title="Building an Auction Website with WebSync - Frozen Mountain Blog">Building an Auction Website with 

WebSync</a> on our Blog.</p>

    <div id="auctionContainer">
        <!-- Embed our Auction Widget -->
        <script type="text/javascript" src="<%=ResolveClientUrl("~/Scripts/AuctionListing.js")%>"></script>
    </div>

</asp:Content>

Write a WebSync event to handle placing bids on auction items

So we’re now setup for client to subscribe to a list of auctions, but that’s not very interesting if we can’t also bid on those auctions and increase the current bid price. So let’s create a Server WebSyncEvent to handle those incoming bid requests.

[WebSync.WebSyncEvent(WebSync.EventType.BeforeService, "/auctions/{auctionId}", WebSync.FilterType.Template)]
public static void ServiceAuctionBid(object sender, WebSync.WebSyncEventArgs e)
{
    var auctionId = e.Match.GetParameter("auctionId");

    AuctionBid auctionPartial = Json.Deserialize(e.ServiceInfo.DataJson);

    try
    {
        // Try to process this bid request. This bid request could 
        // fail if the user is not bidding high enough or if the 
        // auction has expired.
        AuctionsManager.ProcessBid(auctionId, auctionPartial);
    }
    catch (Exception exc)
    {
        // Cancel this service request and send back an error msg.
        e.Cancel(exc.Message);
    }
}

Write a JavaScript script client for the web page itself using jQuery and jQuery UI

Add a new JavaScript file to your project’s “Scripts” folder and name it “AuctionListing.js”

Let’s start with a wireframe JavaScript component which will load when jQuery is ready.

(function () {

    if (window.WebSyncAuctionDemo && window.WebSyncAuctionDemo.AuctionListing) {
        return;
    } else {
        if (!window.WebSyncAuctionDemo) {
            window.WebSyncAuctionDemo = {};
        }
        if (!window.WebSyncAuctionDemo.AuctionListing) {
            window.WebSyncAuctionDemo.AuctionListing = {};
        }
    }
    var controller = window.WebSyncAuctionDemo.AuctionListing;

    controller.init = function (param) {
    };

    // Build any UI elements needed, dialogs, etc.
    controller.buildUI = function () {
    };


    // Don't fire up this controller until the page is ready.
    $(document).ready(function () {
        controller.init();

        controller.buildUI();
    }); //END $(document).ready()

})();

Before we go any further, let’s have a moment of silence for our friend Internet Explorer. Internet Explorer does not define “console” and therefor “console.log” if you do not have your developer tools open in the browser. This frequently causes working JavaScript to fail in IE due to logging statements. To keep things simple, I’m just going to abstract out the logging to a separate method in my JavaScript controller. That way you can easily swap out your logging for your favorite implementation. I’ll create a simple implementation, you’re welcome to switch it out to some DOM logging or a fancier logging framework. Just in case we need it you’ll notice I added an ‘enableLogging’ property on the JavaScript component.

// Just a config switch to turn on/off logging within this 
// custom WebSyncProxy implementation.
controller.enableLogging = true;

controller.log = function (logMessage) {
    // Only log when safely possible. IE doesn't define console 
    // if the developer tools are closed which breaks your app.
    if (controller.enableLogging && window.console && console.log) {
        console.log(logMessage);
    }
};

Before we get into the implementation details of WebSync, let’s setup a few more foundational pieces. The first thing I’m going to add to our JavaScript component is a couple jQuery templates to facilitate construction of the user interface.

// Define some templates for use with the listing. These could 
// also go in external files or your HTML markup, but we wanted 
// a self contained JavaScript file.
controller.templates = {}
controller.templates['auctionBaseTemplate'] =
        '
' + '' + '

' + '' + '' + '' + '' + '
' ; controller.templates['auctionBidDialog'] = '
' + '' + '
' + '
' + '
' + '
' + '
' + '
' + '
' ;

The first template represents the markup we will render for each active auction. The second template represents the markup we need for a bid dialog. Now we also want to hold references to some of things we are working with:

  • the auction container DOM element
  • a DOM element for displaying notices to the end user
  • a reference to our jQuery UI dialog after its created
  • a place to store a JavaScript timer to update our auction countdowns
  • a place to store our WebSync client instance.

Here’s the code I added to the JavaScript component to hold reference to all those things:

// Cache things we'll need to use more than once.
controller.auctionContainer = $('#auctionContainer');
controller.auctionNoticesContainer = null;
controller.auctionBidDialog = null;

controller.auctionEls = {};
controller.timer = null;

// A place to hold our WebSync client instance.
controller.websyncClient = null;

Now we are ready to setup WebSync and subscribe to our list of auctions! We’re going to do this in our JavaScript components init() method.

controller.init = function (param) {
    controller.log('Begin AuctionListing.init()');

    $("#auctionContainer").block();

    // Initialize WebSync
    controller.websyncClient = new fm.websync.client('websync.ashx');
    controller.websyncClient.connect({
        onSuccess: function (e) {
            controller.log('Connected to WebSync successfully.');
        },
        onFailure: function (e) {
            controller.log('Could not connect to WebSync (' + e.getException().message + ').');
        },
        onStreamFailure: function (e) {
            controller.log('Lost connection to WebSync! Reconnecting...');
        }
    });

    // Subscribe to new subscription set
    var subscribeArgs = {
        channel: '/auctions',
        onSuccess: function (e) {
            controller.log('Subscribed to master websync channel \'' + e.getChannel() + '\' successfully!');

            var initialAuctionData = e.getMeta();
            controller.log('Received ' + initialAuctionData.length + ' Active Auctions!');

            // Add each auction in turn
            for (auctionIndex in initialAuctionData) {
                controller.addAuction(initialAuctionData[auctionIndex]);
            }

            // Make the 'Bid Now' button pretty with jQuery UI
            $(".bidButton").button();

            controller.timer = setInterval(controller.setExpirationTick, 1000);
        },
        onFailure: function (e) {
            controller.log("Subscribe failure. " + e.getException().message);
        },
        onComplete: function (e) {
            window.setInterval(function () {
                $("#auctionContainer").unblock();
            }, 500);
        },
        onReceive: controller.onReceive
    };
    controller.websyncClient.subscribe(subscribeArgs);
};

// Updates the expiration time for all of the auction items currently on
// the page.
controller.setExpirationTick = function () {
    // Lets loop through each of our AuctionItems on our page and update 
    // the expiration countdown.
    for (key in controller.auctionEls)
        controller.auctionEls[key].trigger('tick');
};

controller.onReceive = function (e) {
};

If our WebSync subscribe was successful, we’re going to build our auction list and start a JavaScript timer to update our auctions’ countdowns in the user’s browser. The initial list of auctions was returned from our server side WebSyncEvent in the Meta property. This data can be retrieved within the onSuccess event handler with `e.getMeta()`. Let’s add a method to our JavaScript component that will take one Auction record and apply it to our jQuery template prepared earlier and then append the finished markup to our auction container element. We also need a method to wire up our auction events. Also note the timer above is simply firing a ‘tick’ event on each of our auctions. We’ll set up a handler for that tick event in our wireAuctionEvents() method so that the auction’s countdown timer is updated for every timer tick.

// Add a new auction to the auction container.
controller.addAuction = function (auctionRecord) {
    // Try to get a reference to the auction item if it's already been added.
    var auctionEl = controller.getCurrentAuctionItem(auctionRecord.id);

    // If it hasn't been added, create it.
    if (!auctionEl) {
        // Generate a new auction element using jQuery templates.
        auctionEl = $.tmpl(controller.templates['auctionBaseTemplate']);

        // Add the element to the global cache. This allows us to refer to it easily
        //     when performing an update later.
        controller.auctionEls[auctionRecord.id] = auctionEl;

        // Set the element's id attributes and attach the record to it using jQuery's
        //     data handlign features.
        auctionEl.attr('id', 'auctionItem-' + auctionRecord.id);
        auctionEl.data('auctionRecord', auctionRecord);

        // Add the new element to the auction container.
        auctionEl.appendTo(controller.auctionContainer);

        // Wire the events to the auction and then call an update.
        controller.wireAuctionEvents(auctionEl);
        controller.updateAuction(auctionRecord);
    }
    // If it's already added, just treat it like an update request.
    else {
        controller.updateAuction(auctionRecord);
    }
};

// Update an auction already in the auction container.
controller.updateAuction = function (auctionRecord) {
    // Try to get a reference to the auction item.
    var auctionEl = controller.getCurrentAuctionItem(auctionRecord.id);

    // If it exists, trigger its update method.
    if (auctionEl) {
        auctionEl.trigger('update', auctionRecord);
    }
};

// Set up events for a single auction element.
controller.wireAuctionEvents = function (auctionEl) {
    // Update event; causes the auction to refresh data.
    auctionEl.on('update', function (e, auctionRecord) {
        var el = $(this);

        var currentBid = auctionEl.data('auctionRecord').currentBidPrice;
        var newBid = auctionRecord.currentBidPrice;

        if (newBid != currentBid) {
            el.find('.currentBidPrice').stop(true, true).animate({
                color: '#cc0',
                fontSize: '24px'
            }).delay(3000).animate({
                color: '#161',
                fontSize: '20px'
            });
        }

        el.find('.description').text(auctionRecord.description);
        el.find('.expiration').text(controller.formatExpiration(auctionRecord.expiration));
        el.find('.currentBidPrice').text(controller.formatPrice(auctionRecord.currentBidPrice, 'USD'));
        el.find('.currentBidUsername').text(!!auctionRecord.currentBidUsername ? auctionRecord.currentBidUsername : '- no bids -');
        el.find('.photo').attr('src', auctionRecord.photoURL);

        el.data('auctionRecord', auctionRecord);
    });

    // Update event for the auction's timer; only updates the timer.
    auctionEl.on('tick', function (e) {
        var el = $(this);

        el.find('.expiration').text(controller.formatExpiration(el.data('auctionRecord').expiration));

        // If the auction is over, trigger the delete event.
        if (moment(el.data('auctionRecord').expiration) < moment())
            el.trigger('delete');
    });

    // Update event for the auction's timer; only updates the timer.
    auctionEl.on('delete', function (e) {
        var el = $(this);
        el.off();

        // Close the bid dialog if it's still up for the auction being deleted.
        var bidDialogData = controller.auctionBidDialog.data('auctionRecord');
        if (!!bidDialogData && bidDialogData['id'] == el.data('auctionRecord')['id'])
            controller.auctionBidDialog.dialog('close');

        // Disable the bid button.
        el.find('.bidButton').attr('disabled', 'disabled');

        // Fade all the text/etc to grey. We don't use the callback because
        //     it will run multiple times.
        el.find('*').animate({
            color: '#333'
        }, 1000, 'swing');

        // Nested animations.
        el.animate({
            backgroundColor: '#f55'
        }, 1000,
            function () {
                el.animate({
                    borderWidth: 0,
                    padding: 0,
                    opacity: 0.25,
                    width: 10
                }, 1000,
                    function () {
                        el.animate({
                            height: 0,
                            marginLeft: '-5px',
                            marginRight: '-5px',
                            marginTop: '150px',
                            paddingLeft: '5px',
                            width: 0
                        }, 1000, function () {
                            el.remove();
                        });
                    }
                );
            }
        );
    });

    // Click event for the bidding button.
    auctionEl.find('.bidButton').click(function (e) {
        e.preventDefault();

        // Update the auction bidding dialog with this element's record.
        controller.auctionBidDialog.trigger('update', auctionEl.data('auctionRecord'));

        // Open the dialog.
        controller.auctionBidDialog.dialog('open');
    });

};

Now that we have the ability to build our auction items list and update individual auction items, let’s handle incoming notifications from WebSync. For now we only really care about one kind of publication which are updates to any given auction items. These notifications were being published on channels that include the id for any given auction. So a channel would be like ‘/auctions/{auctionId}’. In the future we may add an administrative area to this demo and use other channels for new auctions (‘/auctions’) or to administratively deleted auctions (‘/auctions/ {auctionId}/delete’), but for now let’s just focus on auction updates.

controller.onReceive = function (e) {
    // Got a message from WebSync!
    var channel = e.getChannel(),
        channelSegments = channel.split("/"),
        auctionData = e.getData(),
        recordId = auctionData.id;

    // Figure out if this is a new, updated or deleted record.
    if (channelSegments.length === 2) {
        // For now, let's just log this as we haven't implemented 
        // an admin area or ability to add new auction yet.
        controller.log('New auction with id ' + recordId);

        // Maybe later we'll add a method to handle new auctions.
        //me.onReceiveNew(store, record, metaData);
    } else {
        // Updated or Deleted Record
        var deletedVerb = ((channelSegments.length > 3) && (channelSegments[3] == 'delete'));

        if (deletedVerb) {
            // For now, let's just log this as we haven't implemented 
            // an admin area or ability to delete auctions yet.
            controller.log('Delete auction id ' + recordId);

            // Maybe later we'll add a method to delete auctions.
            //me.onReceiveDeleted(store, record, metaData);
        } else {
            controller.log('Update auction id ' + recordId);
            controller.updateAuction(auctionData);
        }
    }
};

The important things to notice is our use of e.getChannel() to see what channel our message was received on, and our use of e.getData() to retrieve the data delivered in this notification. If this is an update notification we pass the notification data to our updateAuction() method as we probably have a new bid price and new high bidder name.

Now let’s build a bid dialog with jQuery UI and use WebSync to ‘Service’ our request to the server. We are going to use another jQuery template to render our dialog markup and wire up the necessary events to handle any user interaction with the bid dialog.

// Build any UI elements needed, dialogs, etc.
controller.buildUI = function () {
    // Create the inline messages container.
    controller.auctionNoticesContainer = $('
', { id: 'auctionNoticesContainer' }).appendTo(controller.auctionContainer); // Create a bidding dialog. controller.auctionBidDialog = $.tmpl(controller.templates['auctionBidDialog']); // Handles 'update' events, which update the bid dialog before opening. // Could also update it if new data is received while a dialog is // open. controller.auctionBidDialog.on('update', function (e, auctionRecord) { // Alias var dialog = controller.auctionBidDialog; // Add to data so we can use later when submitting bids. dialog.data('auctionRecord', auctionRecord); // Convert the bid price/increments to floating point numbers at two decimals. var bidPrice = parseFloat(auctionRecord.currentBidPrice), bidIncrement = parseFloat(auctionRecord.minimumBidIncrement); // Set the starting default bid. dialog.find('.bidAmount').attr('value', bidPrice + bidIncrement); }); // Inititalize the actual auction bid dialog. controller.auctionBidDialog = controller.auctionBidDialog.dialog({ autoOpen: false, draggable: false, modal: true, resizable: false, stack: true, title: 'Enter Your Bid', open: function (event, ui) { var dialog = $(this), dlgMsgContainer = dialog.children(".dlgMessages"), btnPlaceBid = dialog.parent().find("button:eq(1)"); // Make sure we don't have any old validation messages lingering // in the DOM (if we display the bid dialog again faster than an // old error message animation has faded it away) dlgMsgContainer.empty(); // Make sure the dialog will resize vertically if we inject a // validation message into the dialog DOM. dialog.css("height", "auto"); // Let's allow a bid to be submitted with the enter key for easy testing. dialog.keypress(function (e) { if (e.keyCode === $.ui.keyCode.ENTER) { //dialog.parent().find("button:eq(1)").trigger("click"); btnPlaceBid.trigger("click"); } }); // Let put focus on the place bid button by default. btnPlaceBid.focus(); }, buttons: [{ // Cancel button; closes the dialog. text: 'Cancel', click: function (e) { controller.auctionBidDialog.dialog('close'); } }, { // Confirm bid button; processes the bid. text: 'Confirm', click: function (e) { // Get all the information needed. var dialog = controller.auctionBidDialog, dlgMsgContainer = dialog.children(".dlgMessages"); $(dialog).parent().block(); var auctionRecord = dialog.data('auctionRecord'), bidName = dialog.find('.bidName'), bidNameValue = bidName.val(), bidAmount = dialog.find('.bidAmount'), bidAmountValue = parseFloat(bidAmount.val()); // First validate out input values, make sure we don't have a blank bid or blank name. if (!bidNameValue || bidNameValue.length === 0) { $(dialog).parent().unblock(); dlgMsgContainer.writeError("Invalid bidding name."); bidName.focus(); return false; } if (!bidAmountValue || isNaN(bidAmountValue)) { $(dialog).parent().unblock(); dlgMsgContainer.writeError("Invalid bid amount."); bidAmount.focus(); return false; } // Submit the bid with websync. controller.submitBid(auctionRecord.id, bidAmountValue, bidNameValue); } } ] }); };

The `submitBid()` method is where we using our WebSync client to “service” a request to the server. This is really just a plain old ajax POST, except WebSync gives us a lot of extra capability by using “channels” to map our POST request to server side WebSyncEvents instead of using URLs like you would with REST. You could do this using Frozen Mountain’s TheRest framework if you preferred the URL style mapping instead of WebSync channels, but I wanted to keep this example focused on one framework for now. If we get a lot of feedback from the community, maybe we’ll expand this demo further over time.

// Publishes a bid for the specific auction id, with a given price and
// under a specified bidder name.
controller.submitBid = function (auctionId, bidPrice, bidName) {
    // Send our new bid to the server!
    var serviceArgs = {
        channel: '/auctions/' + auctionId,
        data: {
            username: bidName,
            bidPrice: bidPrice
        },
        onComplete: function (e) {
            $(controller.auctionBidDialog).parent().unblock();
        },
        onSuccess: function (e) {
            controller.auctionBidDialog.dialog('close');
            controller.auctionNoticesContainer.writeAlert("Your bid was successful!");
        },
        onFailure: function (e) {
            var dlgMsgContainer = controller.auctionBidDialog.children(".dlgMessages");

            controller.auctionBidDialog.parent().unblock();
            dlgMsgContainer.writeError(e.getErrorMessage());
        }
    };

    controller.websyncClient.service(serviceArgs);
};

Testing the Auction App

Now that we have our auction demo complete, let’s do a little testing. In the real world, people will try everything on your auction website. So let’s try to simulate some of the ways people may interact with your website and make sure these conditions are handled appropriately.

  • Open multiple browsers (Chrome, Firefox, IE) and bid back and forth, watch them update. Each browser will cache the “Bid Name” you enter in the dialog in JavaScript. Place a bid from one browser with one name, then place a competing bid from another browser using a different name. Watch how both browsers update.
  • Try to perform overlapping bids, one will succeed and one will fail. Open the bid dialog in two browsers at the same time. Each browser should have the same bid price but from a different bid name. Now race! Try pressing the bid buttons as fast as you can. You’ll always have the first bid placed win and the second bidder will be told they bid too low.
  • Try to bid too low, it will fail. In our demo every auction has a current bid price, but also a minimum bid increment (take a look back at our demo data in the seed method). If you don’t bid high enough over the current bid price, your bid will fail.
  • Open the bid dialog, but wait for the auction to expire, then try to bid. For example open a the bid dialog, enter a valid bid, but then go for coffee break. The longest auction our demo has is just over 15 minutes. When you come back submit your bid. The bid will fail because the auction has expired while you were gone.

Hopefully this demo helps to illustrate the value of WebSync in real time applications. If you were to implement this auction demo with some kind of “frequent refresh” server polling mechanism you would be wasting bandwidth by returning the same data to the client often and wasting server resources processing those frequent incoming ajax requests on the server. WebSync reduces the wasted bandwidth and server load considerably so that you can handle a greater number of concurrent visitors with your server and saving you bandwidth costs with your host.

If you want to ask questions or discuss this demo, head on over to our google group!

Tags: , ,

websync

x.1.3 Released

by anton.venema 28. February 2013 01:07

WebSync 4.1.3, IceLink 2.1.3, and TheRest 2.1.3 have been released!

IceLink is the hero of this release announcement with full WebRTC audio/video support for iOS and Mac! We've also included a nifty new Pong demo for .NET that shows off how easy it is to build a multiplayer game using IceLink. The entire threading system in IceLink has been overhauled in this release to drastically improve performance. This is most noticeable on mobile devices, but even desktop applications will see a nice improvement.

We've also added a new "threadsPerCPU" config setting for WebSync that lets you crank up the thread-pool and maximize the use of your server resources. If you see a delay in message delivery under heavy load and have the server resources available, try increasing this from 1 (the default) to 2 or 3 to really turn up the heat.

What are you waiting for? Head on over to the Downloads page to pick up the latest SDKs.

Tags:

Guest Post: Electric Slide uses WebSync for multi-platform real time messaging

by Ben Swayne 19. February 2013 00:08

> The following is a guest post. Jim Phelan is using WebSync to power a multi-platform real-time presentation app. If you have something to share with the WebSync community, please feel free to Contact Us.

Electric Slide: Present Anywhere powered by WebSync

When we started building Electric Slide: Present Anywhere we knew that high performance, realtime messaging was essential to the app’s success. Since Electric Slide lets presenters show PowerPoint presentations, documents and videos in near realtime from iPhones and iPads to web browsers and TVs, fast messaging was of critical importance. We were aiming at the sub 200ms range for commands getting all the way from an iPad or iPhone to our server and then to the connected clients. We also needed something that had a wide array of libraries, since we needed the tool to work in our native iOS app as well as in pretty much any web browser.

In my previous role at Stream57 (now part of InterCall) we relied on Flash Media Server and RTMP for realtime messaging, but in the evolving world of the web Flash is clearly no longer an option. At one point we tried to roll our own comet style server, but that task was formidable. When we started looking at options for Electric Slide our criteria for a library were pretty clear: we needed a library that was compatible with nearly every browser, had an iOS native SDK that we could do some pretty intense multithreading with, performed well in high latency or low bandwidth situations, fit with our target architecture (.NET), was scalable in a cost effective manner, had low latency and implemented some sort of channel based publish / subscribe pattern.

WebSync ended up doing the job quite well. Our architecture uses a combination of WCF services and WebSync for transactional type requests and realtime events, respectively. Essentially anything that happens in a broadcast uses WebSync. We use WebSync to manage our pool of document conversion servers as well. Since we convert any PowerPoint, Office or PDF document to HTML5, we have a group of converter servers alongside a master conversion mediator that dishes out conversion jobs to the converters. Users view realtime progress of their conversion jobs in the app - a status that is updated in a separate WebSync channel.

Overall we’re very happy with WebSync. We can’t say enough about how good the support has been – we pretty much get an instant response to every inquiry. It’s also nice to know that once we’re ready to move to Windows Server 2012 we’ll be able to take advantage of full websocket support.

Jim Phelan is co-founder of Elucidate, makers of Electric Slide. For more information on the app, please check out www.electricslide.net

Tags:

IceLink and WebRTC

by anton.venema 16. January 2013 00:13

IceLink has always been fundamentally compatible with WebRTC. Since it's initial release, we have developed the peer connection negotiation algorithm to be completely interoperable with the open standard being developed by the team working on Google Chrome. By choosing to work off the same RFC specifications from day one, we have ensured that IceLink keeps as many options open as possible for you when it comes to interactions with third-party libraries.

But getting a WebRTC session going involves a lot more. In fact, you need twelve (!) additional components:

  • An audio capture engine that can read raw audio samples from the device microphone.
  • An audio render engine that can play back audio samples to the device speakers/headset.
  • A video capture engine that can grab raw images from the device camera.
  • A video render engine that can play back raw image to a visible on-screen container.
  • An audio encoding engine that can convert raw audio samples to compressed frames.
  • An audio decoding engine that can convert compressed frames back to raw audio samples.
  • A video encoding engine that can convert raw images to compressed frames.
  • A video decoding engine that can convert compressed frames back to raw images.
  • An audio packetizer that can convert compressed frames into a sequence of RTP packets.
  • An audio depacketizer that can convert a sequence of RTP packets back to compressed frames.
  • A video packetizer that can convert compressed frames into a sequence of RTP packets.
  • A video depacketizer that can convert a sequence of RTP packets back to compressed frames.

Basically, the sequence looks like this:

Audio:
Mic > Capture > Encode > Packetize > Network > Depacketize > Decode > Render > Speakers

Video:
Camera > Capture > Encode > Packetize > Network > Depacketize > Decode > Render > Screen

Phew! That's an exhaustive list, but it doesn't end there. The encoding, decoding, packetizing, and depacketizing have to line up exactly with other implementations if you plan to have them talk to each other. Unless the packet format and contents of the packet are designed the same way, you can't have cross-communication between libraries.

The good news for you is that we take care of all of this. Completely. You don't have to touch a video codec or worry about RTP packet formats or how to access the webcam from .NET. You just create a PeerConnectionHub and let us take care of the rest. That's the idea behind the WebRTC extension for IceLink, which is available for .NET and JavaScript at the time of this writing.

Other platforms are coming soon (mobile devices are a priority), including a drop-in Java .jar file for web browsers that will let you use native WebRTC functionality when supported and otherwise gracefully backoff to the Java plugin.

The latest downloads for IceLink and IceLink+WebSync include a WebRTC example that lets you fire up a video chat between a desktop application and a web browser with just a few (very) simple lines of code. Check it out and let us know what you think!

Tags:

WebSync 4.1.2, IceLink 2.1.2, TheRest 2.1.2

by anton.venema 15. January 2013 23:55

Some big updates for you! We have new releases of WebSync, IceLink, and TheRest fresh off the unit-testing grill.

You'll notice in this release that the download folder structure has changed quite a bit. Our old layout was well-intentioned, but had too much nesting that made it difficult to find exactly what you were looking for. The new layout should hopefully make it much easier for you to get what you need quickly and intuitively. Here's a quick break-down of the new structure:

Documentation/
  # all the docs are here, as before
Examples/
  {Platform}/    # i.e. JavaScript
    {ExampleName}/
      # example contents, including all dependencies
    {ExampleName}-2008.sln
    {ExampleName}-2010.sln
    {ExampleName}-2012.sln
Libraries/
  {Platform}/   # i.e. JavaScript
    # all libraries for the platform, including all dependencies

Solution files for Visual Studio versions that support the project types are included back to 2008 for those of you not yet using 2010 or 2012.

IceLink features strongly in this release with native .NET and JavaScript support for WebRTC (more on this later - other platforms coming soon!) as well as ConnectionHub and PeerConnectionHub classes to drastically simplify the initial offer/answer exchange and connection management necessary in most P2P applications.

For WebSync and TheRest, we have added a few requested features, fixed some miscellaneous bugs, and enhanced the documentation.

What are you waiting for? Head on over to the Downloads section and pick up the latest copy!

Tags:

WebSync On-Demand

by anton.venema 11. January 2013 01:27

We are currently experiencing issues with the WebSync On-Demand cluster. It does not appear to be a software issue, and we are working with data center staff to resolve the problem.

Please check back here for updates.

1:31pm PST: The problem appears to be related to a failing network card. We have temporarily disabled the problematic server so clients are not load balanced to it. Service should be fully operational while we investigate and resolve this issue.

Tags:

WS On-Demand Outage

by jerod.venema 18. December 2012 18:48

Hey folks, we're having a minor issue with our WSOD cluster this morning. We're working on it and will update here as soon as we have more info.

EDIT:

The outage has been resolved. Total downtime was 7 minutes, with another 15-20 minutes of sluggishness beforehand.

Tags:

on-demand-status