8475. Building Course Player with Node.js and Socket.IONode.js, Socket.IO, and jQuery
Build a course player with Socket.IO.
Build a realtime web application to play course recordings with Socket.IO, HTML5 Canvas and jQuery based on Node.js.
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 Project
Create new Node.js app named course-player-socketio
.
$ mkdir course-player-socketio
$ cd course-player-socketio
$ npm init
2.2 Installing Packages
Install express
and socket.io
locally.
$ npm install express --save
$ npm install socket.io --save
2.3 Data Model
Create file ‘model/index.js
’.
function Index(timestamp, grid, offset, length) {
this.timestamp = timestamp;
this.grid = grid;
this.offset = offset;
this.length = length;
this.row = function() {
return this.grid >> 4;
}
this.col = function() {
return this.grid & 0xf;
}
}
module.exports = Index;
Create file ‘model/screenimage.js
’.
function ScreenImage(row, col, imagestream) {
this.row = row;
this.col = col;
this.imagestream = imagestream;
}
module.exports = ScreenImage;
Create file ‘model/wbdata.js
’.
function WBData(second, wblines, wbevents) {
this.second = second;
this.wblines = wblines;
this.wbevents = wbevents;
}
module.exports = WBData;
Create file ‘model/wbevent.js
’.
function WBEvent(timestamp, reserved, x, y) {
this.timestamp = timestamp;
this.reserved = reserved;
this.x = x;
this.y = y;
}
module.exports = WBEvent;
Create file ‘model/wbline.js
’.
function WBLine(x0, y0, x1, y1, color, reserved) {
this.x0 = x0;
this.y0 = y0;
this.x1 = x1;
this.y1 = y1;
this.color = color;
this.reserved = reserved;
}
module.exports = WBLine;
2.4 File Api(Server side)
Create file ‘api/fileapi.js
’.
var fs = require('fs');
var zlib = require('zlib');
var Index = require('../model/index');
var ScreenImage = require('../model/screenimage');
var WBLine = require('../model/wbline');
var WBEvent = require('../model/wbevent');
var MAX_ROW_NO = 8;
var MAX_COL_NO = 8;
exports.getIndexFile = function(originalFile, unzippedFile) {
// unzip the index file if it doesn't exist
if (!fs.existsSync(unzippedFile)) {
unzipIndexFile(originalFile, unzippedFile);
}
// read the unzipped file to buffer
return fs.readFileSync(unzippedFile);
};
exports.unzipIndexFile = function(originalFile, unzippedFile) {
var buffer = fs.readFileSync(originalFile);
var inflate = zlib.inflateSync(buffer);
fs.writeFileSync(unzippedFile, inflate);
};
exports.getIndexArray = function(buffer){
var arr = [];
var ix = 0;
var pos = 0;
while (pos < buffer.length) {
arr[ix] = new Index(buffer.readUInt16LE(pos), buffer.readInt8(pos+2), buffer.readInt32LE(pos+3), buffer.readUInt32LE(pos+7));
ix++;
pos = pos + 11;
}
for (var j = 0; j < arr.length; j++) {
if (arr[j].offset == -1 && j > 0) {
arr[j].offset = arr[j - 1].offset;
arr[j].length = arr[j - 1].length;
}
}
// sort by timestamp and grid
arr.sort((a, b) => {
var compare = a.timestamp - b.timestamp;
if (compare == 0) {
compare = a.grid - b.grid;
}
return compare;
});
return arr;
};
exports.getSSIndex = function(hm, indexarr, second) {
var foundset = [];
for(var i = 0; i < MAX_ROW_NO * MAX_COL_NO; i++) {
foundset[i] = false;
}
var res = [];
var index = 0;
var firstItem = 0;
var firstSecond = second;
for (; firstSecond >= 0; firstSecond--) {
if(hm[firstSecond]) {
firstItem = hm[firstSecond];
break;
}
}
while (firstItem < indexarr.length && indexarr[firstItem].timestamp == firstSecond) {
firstItem++;
}
if (firstItem > 0) {
for (var i = firstItem - 1; i >= 0; i--) {
var row = indexarr[i].grid >> 4;
var col = indexarr[i].grid & 0xf;
var value = row * MAX_ROW_NO + col;
if (!foundset[value]) {
foundset[value] = true;
res[index]=indexarr[i];
index++;
}
if (res.length == MAX_ROW_NO * MAX_COL_NO) {
break;
}
}
}
return res;
};
exports.getSSData = function(imagedatafile, imageindex) {
var res = [];
var index = 0;
var fd = fs.openSync(imagedatafile, 'r');
var i = 0;
for (i = 0; i < imageindex.length; i++) {
var imageobj = imageindex[i];
var row = imageobj.grid >> 4;
var col = imageobj.grid & 0xf;
var offset = imageindex[i].offset;
var length = imageindex[i].length;
var buffer = new Buffer(length);
fs.readSync(fd, buffer, 0, length, offset);
// image in base64 format
var image = "data:image/png;base64," + buffer.toString('base64');
res[index]= new ScreenImage(row, col, image);
index++;
}
return JSON.stringify(res);
};
exports.getWBIndex = function(indexarr) {
var res = [];
for (var i = 0; i < indexarr.length; i++) {
if(!res[indexarr[i].timestamp]) {
res[indexarr[i].timestamp] = i;
}
}
return res;
};
exports.getWBImageData = function(wbImageDataFile, wbImageIndex, indexList, second) {
var res = [];
var indeximage;
var minutes = Math.floor(second / 60);
if (wbImageIndex[minutes]) {
indeximage = indexList[wbImageIndex[minutes]];
}
if (indeximage && indeximage.length>0) {
var fd = fs.openSync(wbImageDataFile, 'r');
var length = indeximage.length;
var buffer = new Buffer(length);
fs.readSync(fd, buffer, 0, length, indeximage.offset);
var ix = 0;
var pos = 0;
while (pos < buffer.length) {
res[ix] = new WBLine(buffer.readUInt16LE(pos), buffer.readUInt16LE(pos+2), buffer.readUInt16LE(pos+4), buffer.readUInt16LE(pos+6),buffer.readInt16LE(pos+8), buffer.readUInt16LE(pos+10));
ix++;
pos = pos + 12;
}
}
return res;
};
exports.getWBSequenceData = function(wbSequenceDataFile, wbSequenceIndex, indexList, second) {
var res = [];
var indexsequence;
var minutes = Math.floor(second / 60);
if (wbSequenceIndex[minutes]) {
indexsequence = indexList[wbSequenceIndex[minutes]];
}
if (indexsequence && indexsequence.length>0) {
var fd = fs.openSync(wbSequenceDataFile, 'r');
var length = indexsequence.length;
var buffer = new Buffer(length);
fs.readSync(fd, buffer, 0, length, indexsequence.offset);
var ix = 0;
var pos = 0;
while (pos < buffer.length) {
res[ix] = new WBEvent(buffer.readUInt16LE(pos), buffer.readUInt16LE(pos+2), buffer.readInt16LE(pos+4), buffer.readInt16LE(pos+6));
ix++;
pos = pos + 8;
}
}
return res;
};
The following points need to be noted about the above code.
- Use native files system module
fs
provided by Node.js to read data from local files. Notice, we usezlib
to decompress the index files. And use the index to get offset and length. Then, use them to read small parts of the data from data file instead of reading the whole file. - For Screenshot, read the decompressed index file
ScreenShot/High/unzippedindex.pak
to get the index list. Then, get offset and length of index to read image data by time(in second) fromScreenShot/High/1.pak
. - Whiteboard has two parts, one is the static lines
VectorImage
, another is dynamic drawing eventsVectorSequence
. To get data for Whiteboard’s lines, first, read the decompressed index fileWB/1/VectorImage/unzippedindex.pak
to get the index list. Then, get offset and length of index to read line data by time(in second) fromWB/1/VectorImage/1.pak
. The same operations to get Whiteboard’s events.
2.5 Course Api(Server side)
Create file ‘api/courseapi.js
’.
var path = require("path");
var WBData = require('../model/wbdata');
var fileApi = require('./fileapi');
const ssIndexFile = path.join(__dirname, '../204304/ScreenShot/High/package.pak');
const unzippedSsIndexFile = path.join(__dirname, '../204304/ScreenShot/High/unzippedindex.pak');
const ssScreenshotDataFile = path.join(__dirname, '../204304/ScreenShot/High/1.pak');
const wbImageIndexFile = path.join(__dirname, '../204304/WB/1/VectorImage/package.pak');
const unzippedWbImageIndexFile = path.join(__dirname, '../204304/WB/1/VectorImage/unzippedindex.pak');
const wbImageDataFile = path.join(__dirname, '../204304/WB/1/VectorImage/1.pak');
const wbSequenceIndexFile = path.join(__dirname, '../204304/WB/1/VectorSequence/package.pak');
const unzippedWbSequenceIndexFile = path.join(__dirname, '../204304/WB/1/VectorSequence/unzippedindex.pak');
const wbSequenceDataFile = path.join(__dirname, '../204304/WB/1/VectorSequence/1.pak');
// Screenshot Cache
var ssIndexArray = null;
var ssHashmap = [];
// Whiteboard Cache
var wbImageIndexArray = null;
var wbImageIndex = null;
var wbSequenceIndexArray = null;
var wbSequenceIndex = null;
exports.getScreenshotData = function(second) {
if (ssIndexArray===null) {
var buffer = fileApi.getIndexFile(ssIndexFile, unzippedSsIndexFile);
ssIndexArray = fileApi.getIndexArray(buffer);
ssHashmap = [];
for (var i = 0; i < ssIndexArray.length; i++)
{
if(!ssHashmap[ssIndexArray[i].timestamp]) {
ssHashmap[ssIndexArray[i].timestamp] = i;
}
}
}
var ssIndex = fileApi.getSSIndex(ssHashmap, ssIndexArray, second);
return fileApi.getSSData(ssScreenshotDataFile, ssIndex);
};
exports.getWhiteBoardData = function(second) {
// get lines
var lines = this.getWBImageData(second);
// get events
var events = this.getWBSequenceData(second);
// combine them to whiteboard data
var res = new WBData(second, lines, events);
return JSON.stringify(res);
};
exports.getWBImageData = function(second) {
if (wbImageIndex===null) {
var buffer = fileApi.getIndexFile(wbImageIndexFile, unzippedWbImageIndexFile);
wbImageIndexArray = fileApi.getIndexArray(buffer);
wbImageIndex = fileApi.getWBIndex(wbImageIndexArray);
}
return fileApi.getWBImageData(wbImageDataFile, wbImageIndex, wbImageIndexArray, second);
};
exports.getWBSequenceData = function(second) {
if (wbSequenceIndex===null) {
var buffer = fileApi.getIndexFile(wbSequenceIndexFile, unzippedWbSequenceIndexFile);
wbSequenceIndexArray = fileApi.getIndexArray(buffer);
wbSequenceIndex = fileApi.getWBIndex(wbSequenceIndexArray);
}
return fileApi.getWBSequenceData(wbSequenceDataFile, wbSequenceIndex, wbSequenceIndexArray, second);
};
The following points need to be noted about the above code.
- Define constants for paths of data files.
- Use
getScreenshotData()
to get the Screenshot data by second. - Use
getWhiteBoardData()
to get the Whiteboard data by second. - Use local variables to
cache
index files to improve performance.
2.6 Server(Server side)
Create file ‘server.js
’.
var http = require('http');
var path = require('path');
var express = require('express');
var courseApi = require('./apis/courseapi');
var app = express();
var server = http.createServer(app);
var io = require('socket.io').listen(server);
io.sockets.on('connection', function(socket) {
socket.on('updatetime', function(data) {
console.log('server.updatetime:' + data.second);
// Get data for Screenshot
var ssdata = courseApi.getScreenshotData(data.second);
// Get data for Whiteboard
var wbdata = courseApi.getWhiteBoardData(data.second);
// Notify client through emit with data
socket.emit('playCourse', {ssdata: ssdata, wbdata:wbdata});
});
});
var staticPath = __dirname;
app.use(express.static(staticPath));
server.listen(12103, function() {
console.log('Server is listening at http://localhost:12103');
});
function tick () {
var dt = new Date();
dt = dt.toUTCString();
io.sockets.send(dt);
}
setInterval(tick, 1000);
The following points need to be noted about the above code.
- Setup web server with
express
at port12103
. - Create a timer to repeatedly notify the client of the server time.
- Open socket connection with
Socket.IO
, monitoringupdatetime
event. - Once receive the time(data.second) from client, fetch course data for screenshot and whiteboard. Then, emit
playCourse
event to send data back to client.
2.7 Home Page(Client Side)
Create file ‘index.html
’. It is the default page for this app.
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="/socket.io/socket.io.js"></script>
<script type="text/javascript" src="client/player.js"></script>
<script type="text/javascript">
var socket = io.connect();
// Get server time
socket.on('message', function (time) {
document.getElementById('time').innerHTML = time;
});
// Get course data by second from server
socket.on('playCourse', function (data) {
drawScreenshot(data.ssdata, $('#workingss'), $('#playerss'));
drawWhiteboard(data.wbdata, $('#workingwb'), $('#playerwb'));
});
</script>
<link href="/assets/css/bootstrap.min.css" rel="stylesheet" type="text/css" />
<link href="/assets/css/Site.css" rel="stylesheet" type="text/css" />
<script src="/public/scripts/jquery-1.10.2.min.js"></script>
<script src="/public/scripts/bootstrap.min.js"></script>
<!--jquery slider bar-->
<link rel="stylesheet" href="http://code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css">
<script src="http://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
</head>
<body>
<div>
<h1>Course Player</h1>
<p>Built with <a href='https://socket.io/'>Socket.IO</a>, <a href='https://nodejs.org'>Node.js</a> and <a href='https://jquery.com/'>jQuery</a></p>
<p>Current server time is: <span id="time"></span></p>
</div>
<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>
<canvas id="workingss" style="display:none" width="500" height="300"></canvas>
<canvas id="workingwb" style="display:none" width="500" height="300"></canvas>
</div>
<div>
<footer className="container-fluid text-center">
<p>© 2016 jojozhuang.github.io, All rights reserved.</p>
</footer>
</div>
<!--Add script to update the page and send messages.-->
<script type="text/javascript">
$(function () {
// 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";
var interval = null;
$("#btnplay").click(function () {
playstate = playCourse(playstate, $("#btnplay"), $("#processbar"), $("#currenttime"), $('#workingss'), $('#playerss'), $('#workingwb'), $('#playerwb'));
});
});
</script>
</body>
</html>
The following points need to be noted about the above code.
- Import
'/socket.io/socket.io.js'
and callio.connect()
to create socket connection. - Monitor
message
event and get the server time. - Create
Video
,Screenshot
andWhiteboard
with canvas and jQuery slider bar. The slider bar is to simulate progress bar of the video player. We define two canvas controlsplayerss
andworkingss
for screenshot.workingss
is invisible. We draw images first on the working canvas. Then, draw the entire image on theplayerss
canvas for only one time to avoid flashing. Same for whiteboard. - The max value of slider bar is 4 * 60 * 60 - 30 * 60 = 12600 seconds, since each course lasts 3 and half hours.
- Monitor
playCourse
event and get the data from server. - Use
drawScreenshot(data)
anddrawWhiteboard(data)
to draw screenshot and whiteboard. - For the jQuery slider bar, use
slide
event to update the time when user is dragging the slider bar. And usestop
event to update the time when user finishes dragging. Meanwhile, callclearScreenshot()
andclearWhiteboard()
methods to clear both screenshot and whiteboard.
2.8 Player(Client Side)
Create file ‘client/player.js
’. All the functions of course player are defined here.
function playCourse(playstate, btnplay, processbar, currenttime, workingss, ss, workingwb, wb) {
if (playstate == 'stopped') {
interval = setInterval(function () {
processbar.slider("value", processbar.slider("value") + 1);
currenttime.val(getReadableTimeText(processbar.slider("value")));
socket.emit('updatetime', {
second: processbar.slider("value")
});
}, 1000);
btnplay.prop('value', 'Stop');
playstate = "playing";
} else if (playstate == 'playing') {
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');
}
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, 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;
}
The following points need to be noted about the above code.
- Use
playCourse()
to start or stop the player. When player is started, we setup a timer to increment the time by second and emitupdatetime
event to notify server. - Use
drawScreenshot(ssdata, workingss, ss)
to draw images on screenshot canvas. Notice, for each screenshot, there is a maximum number of 64 images for each-time drawing. There will be fewer images if some of them are not changed. We draw the images one by one on the hidden working canvas. Then, draw the screenshot canvas with the entire working canvas. Thus, we can prevent canvas from flashing during drawing. - Use
drawWhiteboard(wbdata, workingwb, wb)
to draw lines and events on whiteboard canvas with the given color, width, and position. Notice, we draw them first on the working canvas. Then, draw the whiteboard canvas with the entire working canvas. Thus, we can prevent canvas from flashing during drawing.
2.9 Others
1) Decompress file.
The data files for screenshot and whiteboard are compressed. We use zlib
to decompress them. Generally, there are two encoding formats for compression, Gzip and Inflate. Here, we use the Inflate
method of zlib. In addition, there are two approaches to decompress files, asynchronous and synchronous, see the below sample codes. For this course player, we use the synchronous approach.
Asynchronous approach.
var inflate = zlib.createInflateSync();
var input = fs.createReadStream(originalFile);
var output = fs.createWriteStream(unzippedFile);
/*output.on('finish', function(){
console.log("finish");
};*/
input.pipe(inflate).pipe(output);
Synchronous approach.
var buffer = fs.readFileSync(originalFile);
var inflate = zlib.inflateSync(buffer);
fs.writeFileSync(unzippedFile, inflate);
2) Read buffer.
let buffer = new Buffer([3,0,51,2,0,0,0,212,0,0,0])
var pos = 0;
console.log(buffer.readUInt16LE(0)); // print 3
pos = pos+2;
console.log(buffer.readInt8(2)); // print 51
pos= pos+1;
console.log(buffer.readInt32LE(3)); //print 2
pos = pos+4;
console.log(buffer.readUInt32LE(7)); // print 212
3) Image in Base64 format.
Append data:image/png;base64
to image data and set it to src of html image control to diaplay it.
2.10 Final Project Structure
Notice, folder 204304
contains the data files for screenshot and whiteboard.
3. Running and Testing
Start the app.
$ npm start
View the course player at http://localhost:12103/ in chrome. 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.
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.
You can drag the slider bar to move forward or backward.
4. Conclusion
4.1 Easy to Implement
If you are familiar with Node.js and javascript, it is not too difficult to develop such real time online application.
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 system performance at server side.
4.3 Cross-platform
This player is web based, the only required application on client’s machine 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.