8474. Building Course Player with SignalR and ASP.NET
SignalR, ASP.NET, and jQuery


Build a course player with SignalR.

Build a realtime web application to play course recordings with SignalR, HTML5 Canvas and jQuery based on ASP.NET MVC.

1. Course Player

A course player consists of three components: video, screenshot and whiteboard.

  • Video is captured by a camera during the lecturing time. It is in mp4 format.
  • Screenshot is captured from computer monitor shared by teachers. It contains handouts and materials for the course. Screenshot are actually images.
  • Whiteboard is captured from special pens and brushes. Any operation on the board, such as writing, drawing or brushing is recorded.

Check the posting Introduction of Course Player to learn the details.

2. Course Player Project

2.1 Creating New Projects

In Visual Studio, create a ‘Web Application’ project named CoursePlayer.SignalR, and create another ‘Class’ project named CoursePlayer.Core.

2.2 Core Project

Reuse the Course Player Core project, copy all files except the interface file ‘IFileHelper’ from ‘Johnny.Portfolio.CoursePlayer.Core’. image Project ‘Johnny.Portfolio.CoursePlayer.Core’ was created for the portfolio Course Player Xamarin. Check the posting Building Course Player with Xamarin for more details.

In CourseApi.cs, we define two methods. One is for fetching the data of screenshot, another is for fetching the data of whiteboard.

public static List<SSImage> GetScreenshotData(int second) { }
public static WBData GetWhiteboardData(int second) { }

When reading the data from course files, we need to decompress them first. Here, I’m using SharpZipLib library, visit https://github.com/icsharpcode/SharpZipLib for more details. image

2.3 Web Project

Open the web project, it looks as follows. image 1) Install package Microsoft.AspNet.SignalR and its dependency through NuGet Package Manager. To enable SignalR in your application, create a class named Startup. Right click on /App_Start folder, choose ‘New Item’, then select Web->General->OWIN Startup class. image image Update its content as follows.

using Microsoft.Owin;
using Owin;

[assembly: OwinStartup(typeof(CoursePlayer.SignalR.Startup))]

namespace CoursePlayer.SignalR
{
    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            app.MapSignalR();
        }
    }
}

2) Update /Content/Site.css to style the whole website.

body {
    padding-top: 50px;
    padding-bottom: 20px;
}

/* Set padding to keep content from hitting the edges */
.body-content {
    padding-left: 15px;
    padding-right: 15px;
}

/* Set width on the form input elements since they're 100% wide by default */
input,
select,
textarea {
    max-width: 280px;
}

canvas {
    background: #fff;
    margin: 20px auto;
    border: 5px solid #E8E8E8;
    display: block;
}

table, th, td {
    /*border: 1px solid black;*/
    padding: 0px;
    margin: 0px;
}

canvas, video {
    margin: 0;
    padding: 0;
}

3) Disable bundling by commenting out the ‘RegisterBundles()’ method in Global.asax.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace CoursePlayer.SignalR
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            //BundleConfig.RegisterBundles(BundleTable.Bundles); // comment out
        }
    }
}

4) Create a javascript in ‘/Scripts’ folder named player.js.

function playCourse(hub, playerHub, playstate, btnplay, processbar, currenttime, videoplayer, workingss, ss, workingwb, wb) {
    if (playstate == 'stopped') {
        hub.start().done(function () {
            playerHub.server.joinGroup($("#groupName").val());
            interval = setInterval(function () {
                processbar.slider("value", processbar.slider("value") + 1);
                currenttime.val(getReadableTimeText(processbar.slider("value")));
                playerHub.server.updateTime($("#groupName").val(), $("#processbar").slider("value"));
            }, 1000);
            btnplay.prop('value', 'Stop');
            playstate = "playing";
            if (videoplayer) {
                console.log('play video go')
                videoplayer.play();
            }
        });
    } else if (playstate == 'playing') {
        hub.stop($("#groupName").val());
        processbar.slider("value", 0);
        currenttime.val(getReadableTimeText(processbar.slider("value")));
        playstate = "stopped";
        // stop the interval
        clearInterval(interval);
        // clear screenshot and whiteboard
        clearScreenshot(workingss, ss);
        clearWhiteboard(workingwb, wb);
        btnplay.prop('value', 'Play');
        if (videoplayer) {
            videoplayer.currentTime(0);
            videoplayer.pause();
        }
    }
    return playstate;
}

function drawScreenshot(ssdata, workingss, ss) {
    var left, top, width, height = 0;
    var imageList = JSON.parse(ssdata);

    console.log(imageList.length);
    for (var i = 0; i < imageList.length; i++) {
        left = workingss.width() / 8 * imageList[i].Col;
        top = workingss.height() / 8 * imageList[i].Row;
        width = workingss.width() / 8;
        height = workingss.height() / 8;
        drawImageOnCanvas(workingss, left, top, width, height, "data:image/png;base64," + imageList[i].ImageStream);
    }
    // draw entire working canvas to screenshot canvas
    var ctxss = ss[0].getContext('2d')
    ctxss.drawImage(workingss[0], 0, 0);
}

function drawImageOnCanvas(workingss, left, top, width, height, image) {
    var ctx = workingss[0].getContext("2d");
    var img = new Image();
    img.onload = function () {
        ctx.drawImage(img, left, top, width, height);
    }
    img.src = image;
}

function drawWhiteboard(wbdata, workingwb, wb) {
    var lastPoint;
    var currentColor = -10;
    var currentWidth = 1;
    var ctxwb = workingwb[0].getContext('2d');
    var xRate = workingwb.width() / 9600;
    var yRate = workingwb.height() / 4800;
    var wbobj = JSON.parse(wbdata);
    if (wbobj.WBLines) {
        for (var i = 0; i < wbobj.WBLines.length; i++) {
            var line = wbobj.WBLines[i];
            drawLine(ctxwb, getColor(line.Color), getWidth(line.Color), line.X0, line.Y0, line.X1, line.Y1, xRate, yRate);
        }
        var mywb = wb[0].getContext('2d');
        mywb.drawImage(workingwb[0], 0, 0);
    }
    if (wbobj.WBEvents) {
        var endMilliseconds = wbobj.Second * 1000 % 60000;
        for (var i = 0; i < endMilliseconds; i++) {
            for (var j = 0; j < wbobj.WBEvents.length; j++) {
                var event = wbobj.WBEvents[j];
                if (event && event.TimeStamp == i) {
                    if (event.X >= 0) {
                        if (!lastPoint) {
                            lastPoint = event;
                        } else {
                            drawLine(ctxwb, getColor(currentColor), currentWidth, lastPoint.X, lastPoint.Y, event.X, event.Y, xRate, yRate);
                            lastPoint = event;
                        }
                    } else {
                        switch (event.X) {
                            case -100: //Pen Up
                                currentColor = -8;
                                lastPoint = null;
                                break;
                            case -200: //Clear event
                                clearWhiteboard();
                                lastPoint = null;
                                break;
                            default:
                                currentColor = event.X;
                                currentWidth = getWidth(currentColor);
                                break;
                        }
                        lastPoint = null;
                    }
                }
            }
        }
        var mywb = wb[0].getContext('2d');
        mywb.drawImage(workingwb[0], 0, 0);
    }
}

function drawLine(ctxwb, color, width, x0, y0, x1, y1, xRate, yRate) {
    ctxwb.fillStyle = "solid";
    ctxwb.beginPath();
    ctxwb.strokeStyle = color;
    ctxwb.lineWidth = width;
    ctxwb.moveTo(x0 * xRate, y0 * yRate);
    ctxwb.lineTo(x1 * xRate, y1 * yRate);
    ctxwb.closePath();
    ctxwb.stroke();
}

function getColor(color) {
    switch (color) {
        case -1:
            return '#FF0000';
        case -2:
            return '#0000FF';
        case -3:
            return '#00FF00';
        case -8:
            return '#000000';
        case -9:
            return '#FFFFFF';
        case -10:
            return '#FFFFFF';
        default:
            return '#FFFFFF';
    }
}

function getWidth(color) {
    switch (color) {
        case -1:
            return 1;
        case -2:
            return 1;
        case -3:
            return 1;
        case -8:
            return 1;
        case -9:
            return 8 * 10 / 12;
        case -10:
            return 39 * 10 / 12;
        default:
            return 1;
    }
}

function clearScreenshot(workingss, ss) {
    // reset screen
    var ctxworkingss = workingss[0].getContext('2d');
    ctxworkingss.clearRect(0, 0, workingss.width(), workingss.height());
    var ctxss = ss[0].getContext('2d');
    ctxss.clearRect(0, 0, ss.width(), ss.height());
}

function clearWhiteboard(workingwb, wb) {
    // reset whiteboard
    var ctxworkingwb = workingwb[0].getContext('2d');
    ctxworkingwb.clearRect(0, 0, workingwb.width(), workingwb.height());
    var ctxwb = wb[0].getContext('2d');
    ctxwb.clearRect(0, 0, wb.width(), wb.height());
}

function getReadableTimeText(totalseconds) {
    var hours, minutes, seconds = 0;
    seconds = totalseconds % 60;
    hours = Math.floor(totalseconds / (60 * 60));
    minutes = Math.floor((totalseconds - hours * 60 * 60) / 60);

    var outh, outm, outs = "";
    outh = hours < 10 ? "0" + hours : hours;
    outm = minutes < 10 ? "0" + minutes : minutes;
    outs = seconds < 10 ? "0" + seconds : seconds;

    return outh + ":" + outm + ":" + outs;
}

Actually, this file is copied from the portfolio ‘Course Player(Socket.IO)’.

5) Remove the auto generated About() and Contact() methods from HomeController.cs. And remove their views About.cshtml and Contact.cshtml in /Views/Home/. Update file /Views/Home/Index.cshtml as follows.

@{
    ViewBag.Title = "Home";
}

<h2>Home</h2>
<h4>This is the demo page for SignalR</h4>
<ul>
  <li><a href="http://www.asp.net/signalr/overview/getting-started/tutorial-getting-started-with-signalr">Tutorial: Getting Started with SignalR 2</a></li>
  <li><a href="http://signalr.net/">http://signalr.net/</a></li>
  <li><a href="http://www.asp.net/signalr">SignalR on ASP.NET</a></li>
  <li><a href="https://jqueryui.com/slider/#rangemax">jQuery Slider Bar</a></li>
  <li><a href="http://videojs.com/">Javascript Video Controller</a></li>
</ul>

<ul>
    <li><a href="~/sliderbar.html">Jquery SliderBar Example</a></li>
    <li><a href="~/videojs.html">VideoJs Example</a></li>
</ul>

Update the layout file /Views/Shared/_Layout.cshtml as follows.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - Demo of SignalR</title>
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
    <link href="~/Content/bootstrap.min.css" rel="stylesheet" type="text/css" />
    <script src="~/Scripts/modernizr-2.6.2.js"></script>
    <script type="text/javascript" src="~/scripts/player.js"></script>
    <style type="text/css">
        .playercontainer {
            background-color: #99CCFF;
            border: thick solid #808080;
        }
    </style>
    <!--Script references. -->
    <!--Reference the jQuery library. -->
    <script src="~/Scripts/jquery-1.10.2.min.js"></script>
    <script src="~/Scripts/bootstrap.min.js"></script>
    <!--Reference the SignalR library. -->
    <script src="~/Scripts/jquery.signalR-2.2.2.min.js"></script>
    <!--Reference the autogenerated SignalR hub script. -->
    <script src="signalr/hubs"></script>
    <script src="~/Scripts/jquery.event.drag-2.2.js"></script>
    <!--jquery slider bar-->
    <link rel="stylesheet" href="//code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css">
    <script src="//code.jquery.com/ui/1.11.4/jquery-ui.js"></script>

</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("Home", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("Dummy Player", "Index", "DummyPlayer", routeValues: null, htmlAttributes: null)</li>
                </ul>
            </div>
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>

</body>
</html>

6) Create a controller named DummyPlayerController.

using System.Web.Mvc;

namespace CoursePlayer.SignalR.Controllers
{
    public class DummyPlayerController : Controller
    {
        // GET: Dummy Player
        public ActionResult Index()
        {
            return View();
        }
    }
}

Create view for this controller in folder /Views/DummyPlayer with the name Index.cshtml.


@{
    ViewBag.Title = "Dummy Player";
}

<h2>Course</h2>
<div class="playercontainer">
    <table style="width:100%;" align="center">
        <tr>
            <td align="left"><label for="currenttime">Current Time:</label><input type="text" id="currenttime" readonly style="border:0; color:#f6931f; font-weight:bold;"></td>
            <td colspan="2" align="right"><input type="button" id="btnplay" value="Play" /></td>
            <td align="right"><label for="total">Total Time:</label><input type="text" id="total" readonly style="border:0; color:#f6931f; font-weight:bold;"></td>
        </tr>
        <tr><td colspan="4"><div id="processbar" style="margin-top:10px"></div></td></tr>
        <tr><td colspan="2" align="left"><canvas id="playerss" width="500" height="300" style="margin-top:10px"></canvas></td><td colspan="2" align="right"><canvas id="playerwb" width="500" height="300" style="margin-top:10px"></canvas></td></tr>
    </table>
    <input type="hidden" id="groupName" value="grpjohnny" />
    <canvas id="workingss" style="display:none" width="500" height="300"></canvas>
    <canvas id="workingwb" style="display:none" width="500" height="300"></canvas>
</div>

<style type="text/css">
    #draw {
        border: 1px solid #AAA;
        background: #EEE;
    }
</style>
<!--Add script to update the page and send messages.-->
<script type="text/javascript">
    $(function () {
        $("#groupName").val("johnnygrp" + Math.floor((Math.random() * 1000) + 1));
        var hub = $.connection.hub;
        // Declare a proxy to reference the hub.
        var playerHub = $.connection.playerHub;
        console.log(playerHub);
        //draw screenshot      
        playerHub.client.broadcastDrawScreenshot = function (ssdata) {
            //console.log("ssdata:" + ssdata)
            drawScreenshot(ssdata, $('#workingss'), $('#playerss'));
        };
        playerHub.client.broadcastDrawWhiteboard = function (wbdata) {
            //console.log("wbdata:" + wbdata)
            drawWhiteboard(wbdata, $('#workingwb'), $('#playerwb'));
        };
        // use jquery slider control to create process bar
        $("#processbar").slider({
            range: "max",
            min: 0,
            max: 4 * 60 * 60 - 30 * 60,
            value: 0,
            slide: function (event, ui) {
                $("#currenttime").val(getReadableTimeText(ui.value));
            },
            stop: function (event, ui) {
                $("#currenttime").val(getReadableTimeText(ui.value));
                clearScreenshot($('#workingss'), $('#playerss'));
                clearWhiteboard($('#workingwb'), $('#playerwb'));
            }
        });
        $("#currenttime").val(getReadableTimeText($("#processbar").slider("value")));
        $("#total").val(getReadableTimeText(4 * 60 * 60 - 30 * 60));

        // play course and emit time to server
        var playstate = "stopped";
        $("#btnplay").click(function () {
            playstate = playCourse(hub, playerHub, playstate, $("#btnplay"), $("#processbar"), $("#currenttime"), null, $('#workingss'), $('#playerss'), $('#workingwb'), $('#playerwb'));
        });
    });
</script>

7) Include folder /204304 to the project. This folder contains all of the data files for screenshot and whiteboard of one course recording. image 8) Create a model class ScreenImage in folder /Models with following content.

namespace CoursePlayer.SignalR.Models
{
    public class ScreenImage
    {
        public int Row { get; set; }
        public int Col { get; set; }
        public string ImageStream { get; set; }

    }
}

Create folder named /SignalR, then create class named PlayerHub. This hub class read data for the course and send back to client.

using CoursePlayer.Core;
using CoursePlayer.Core.Models;
using CoursePlayer.SignalR.Models;
using Microsoft.AspNet.SignalR;
using System;
using System.Collections.Generic;
using System.Web.Script.Serialization;

namespace CoursePlayer.SignalR
{
    public class PlayerHub : Hub
    {
        public void JoinGroup(string groupName)
        {
            Groups.Add(Context.ConnectionId, groupName);
        }

        public void UpdateTime(string group, string second)
        {
            int currenttime = Convert.ToInt32(second);
            List<SSImage> images = CourseApi.GetScreenshotData(currenttime);
            List<ScreenImage> list = new List<ScreenImage>();

            // convert image from byte[] to base64 string.
            foreach (SSImage item in images)
            {
                if (item.Image == null)
                {
                    continue;
                }
                list.Add(new ScreenImage { Row = item.Row, Col = item.Col, ImageStream = Convert.ToBase64String(item.Image) });
            }
            JavaScriptSerializer jss = new JavaScriptSerializer();
            Clients.Group(group).broadcastDrawScreenshot(jss.Serialize(list));

            WBData wbData = CourseApi.GetWhiteboardData(currenttime);
            JavaScriptSerializer jss2 = new JavaScriptSerializer();
            Clients.Group(group).broadcastDrawWhiteboard(jss2.Serialize(wbData));
        }
    }
}

2.4 Running and Testing

Start the web project. Home page contains some useful information related to SignalR. image Switch to ‘Dummy Player’. On the top of the player, there is the slider bar and a Play button. There are two canvases below the slider bar. The left one is for screenshot and the right one is for whiteboard. image Click the Play button, the slider bar begins to move and the current time will increment in seconds. Meanwhile, the screenshot and whiteboard canvas show the content simultaneously. image You can drag the slider bar to move forward or backward. image

3. Enhancement with Video Player

Enhance the dummy player by replacing the slider bar with video player.

3.1 Reference for Video Control

Add the referencse of the video player into layout file /Views/Shared/_Layout.cshtml.

<!--http://videojs.com/-->
<link href="http://vjs.zencdn.net/5.0.2/video-js.css" rel="stylesheet">
<script src="http://vjs.zencdn.net/ie8/1.1.0/videojs-ie8.min.js"></script>
<script src="http://vjs.zencdn.net/5.0.2/video.js"></script>

3.2 Controller and View

Create a new controller named CoursePlayerController.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace CoursePlayer.SignalR.Controllers
{
    public class CoursePlayerController : Controller
    {
        // GET: CoursePlayer
        public ActionResult Index()
        {
            return View();
        }
    }
}

Create view for this controller in folder /Views/CoursePlayer with the name Index.cshtml.


@{
    ViewBag.Title = "Course";
}

<h2>Course Player</h2>
<div class="chatcontainer">
    <table width="100%">
        <tr>
            <td><label for="currenttime">Current Time:</label><input type="text" id="currenttime" readonly style="border:0; color:#f6931f; font-weight:bold;"></td>
            <td><input type="button" id="btnplay" value="Play" /></td>
            <td align="right"><label for="total">Total Time:</label><input type="text" id="total" readonly style="border:0; color:#f6931f; font-weight:bold;"></td>
        </tr>
        <tr>
            <td colspan="3"><div id="processbar" style="margin-top:10px"></div></td>
        </tr>
    </table>
    <table width="100%" style="margin-top:20px">
        <tr>
            <td rowspan="2" width="50%">
                <video id="videoplayer" class="video-js vjs-default-skin" controls preload="none" width="530" height="690" data-setup="{}">
                    <source src="http://localhost:22962/lecture.mp4" type="video/mp4">
                    <track kind="captions" src="../shared/example-captions.vtt" srclang="en" label="English"></track>
                    <!-- Tracks need an ending tag thanks to IE9 -->
                    <track kind="subtitles" src="../shared/example-captions.vtt" srclang="en" label="English"></track>
                    <!-- Tracks need an ending tag thanks to IE9 -->
                    <p class="vjs-no-js">To view this video please enable JavaScript, and consider upgrading to a web browser that <a href="http://videojs.com/html5-video-support/" target="_blank">supports HTML5 video</a></p>
                </video>
            </td>
            <td width="50%"><canvas id="playerss" width="500" height="330"></canvas></td>
        </tr>
        <tr>
            <td><canvas id="playerwb" width="500" height="330"></canvas></td>
        </tr>
    </table>
    <input type="hidden" id="groupName" value="grpjohnny" />
    <canvas id="workingss" style="display:none" width="500" height="300"></canvas>
    <canvas id="workingwb" style="display:none" width="500" height="300"></canvas>
</div>

<style type="text/css">
    #draw {
        border: 1px solid #AAA;
        background: #EEE;
    }
</style>

<!--Add script to update the page and send messages.-->
<script type="text/javascript">
    $(function () {
        $("#groupName").val("johnnygrp" + Math.floor((Math.random() * 1000) + 1));
        var videoplayer = videojs('videoplayer');
        var hub = $.connection.hub;
        // Declare a proxy to reference the hub.
        var playerHub = $.connection.playerHub;
        console.log(playerHub);
        //draw screenshot
        playerHub.client.broadcastDrawScreenshot = function (ssdata) {
            //console.log("ssdata:" + ssdata)
            drawScreenshot(ssdata, $('#workingss'), $('#playerss'));
        };
        playerHub.client.broadcastDrawWhiteboard = function (wbdata) {
            //console.log("wbdata:" + wbdata)
            drawWhiteboard(wbdata, $('#workingwb'), $('#playerwb'));
        };
        // use jquery slider control to create process bar
        $("#processbar").slider({
            range: "max",
            min: 0,
            max: 4 * 60 * 60 - 30 * 60,
            value: 0,
            slide: function (event, ui) {
                $("#currenttime").val(getReadableTimeText(ui.value));
            },
            stop: function (event, ui) {
                $("#currenttime").val(getReadableTimeText(ui.value));
                clearScreenshot($('#workingss'), $('#playerss'));
                clearWhiteboard($('#workingwb'), $('#playerwb'));
                //videoplayer.currentTime(ui.value);
            }
        });
        $("#currenttime").val(getReadableTimeText($("#processbar").slider("value")));
        $("#total").val(getReadableTimeText(4 * 60 * 60 - 30 * 60));

        // play course and emit time to server
        var playstate = "stopped";
        $("#btnplay").click(function () {
            playstate = playCourse(hub, playerHub, playstate, $("#btnplay"), $("#processbar"), $("#currenttime"), videoplayer, $('#workingss'), $('#playerss'), $('#workingwb'), $('#playerwb'));
        });
    });
</script>

Add link for this new view in layout file.

<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li>@Html.ActionLink("Dummy Player", "Index", "DummyPlayer", routeValues: null, htmlAttributes: null)</li>
        <li>@Html.ActionLink("Course Player", "Index", "CoursePlayer", routeValues: null, htmlAttributes: null)</li>
    </ul>
</div>

3.3 Final Project Structure

image Notice, folder /204304 contains the data files for screenshot and whiteboard.

3.4 Running and Testing

Start the web project and switch to ‘Course Player’. On the top of the player, there is the slider bar and a Play button. There are two canvases below the slider bar. The upper one is for screenshot and the lower one is for whiteboard. And there is a video player at the left side. image Click the Play button, the slider bar begins to move and the current time will increment in seconds. Meanwhile, the screenshot and whiteboard canvas show the content simultaneously. image You can drag the slider bar to move forward or backward. image13

4. Conclusion

4.1 Easy to Implement

If you are familiar with C# and ASP.NET, it is really easy to develop such real time online application. Of course, you need write some javascript code to use SignalR at the client side.

4.2 Low Bandwidth Consumption

Communication occurs only when necessary. Unlike traditional web application, WebSocket makes the web application react at real time. This improve the user experience at client side and the system performance at server side.

4.3 Cross-platform(For customers/students)

This player is web based, the only required tool on client’s machine to watch the recording is a web browser(eg. Google Chrome). Besides, this course player is based on HTML5, so it can be accessed in different web browsers and on different platforms. No need to install extra plugin in web browser, such as flash player or Silverlight.

4.4 Cross-platform(For developer)

For developer, since this WebSocket based player is a cross-platform application, it is a better solution than other platform specific solutions. Compared with the Flash player and Silverlight player, this SignalR player is simple and easy to maintain, since there is only one copy of the code.

4.5 Reusable

The core module(CoursePlayer.Core) of this application is shared with Xamarin Course Player, which is another portfolio of mine. That is a cross-platform solution for mobile development. image

This means, we have the cross-platform solution for developing applications by only using C#.

  • First, use Xamarin to develop mobile apps for iOS and Android platform.
  • Second, use ASP.NET and SignalR to develop web application for different web browsers and platforms.
  • Technically, the core module can be shared and reused by mobile and web application, even more, it can be shared with winform applications.
  • Two parts cannot be reused, one is the UI, web(html) and mobile(native UI) are obviously different. And another is file operation, reading/writing file on windows/ios/linux platform varies apparently. However, the business logics are same, which can be reused.

5. Source Files

6. References