HTML 5 client

Introduction

In this tutorial, you'll learn how to build a Rednet-based rendering server and how you can communicate with it from a HTML 5 page running in a browser.

The tutorial running in a browser.

Supporting HTML browsers is quite an advantage for rendering technologies as it enables the usage of advanced rendering capabilities on any device and any platform.

Even if this tutorial is not really complex, it involves some technologies which can be new to you. You'll need to set up some basic HTML pages to communicate with the Rednet rendering server. Those web pages will be a mix of classic HTML 5 and some JavaScript.

So, before continuing, we strongly encourage you to keep you up-to-date with those technologies. Here is a website which has proved to be very useful: http://www.w3schools.com.

To test the tutorial, first run Rednet/BuildImage/Win64/HTML5Client. Then open the Rednet/Buildimage/Resources/HTML5Client/index.html page with your favourite web browser.

The rendering server

The rendering server is responsible for managing input connection requests from HTML clients and streaming renderings of an animated 3d scene.

Network setup

As My first peer "usual", the server starts by initializing Rednet and running a peer:

if( RNET::System::Initialize( host_name, host_adapters ) != RED_OK )
{
  printf( "Error: REDnet failed to initialize.\n" );
  return RED_FAIL;
}

// Force REDnet to use a Loopback adapter. This ensures that all the
// REDnet communications will stay local without using any hardware network
// interface. This is for the HTML part of the tutorial that will use
// localhost as the address of the rendering server.
for( unsigned int a = 0; a < host_adapters.size(); ++a )
{
  if( host_adapters[a].IsLoopback() )
  {
    adapter = &host_adapters[a];
    RNET::System::SetAdapter( *adapter );
    break;
  }
}
if( adapter == NULL )
{
  printf( "Error: REDnet failed to find a loopback adapter.\n" );
  return RED_FAIL;
}

// Display the information returned by REDnet.
printf( "\n" );
printf( "Host           : %s\n", host_name.Buffer() );

// Display the adapter name and IP address.
printf( "Network adapter: %s\n", adapter->GetDescription().Buffer() );
printf( "IP address     : %s\n", adapter->GetAddress().ToString().Buffer() );
printf( "\n" );


// 2. Now, we can create REDnet peers to start communicating:
// ----------------------------------------------------------

// The REDnet object creation process must go through the 
// RNET::Factory class.
peer = RED::Factory::CreatePeer();
if( peer == NULL )
{
  printf( "Error: REDnet failed to create the peer.\n" );
  RNET::System::Shutdown();
  return RED_FAIL;
}

// The peer has been created but is not running yet. We need
// to set it up explicitly.

// As any other object in REDsdk, you must query interfaces to raw objects
// before using them in a specialized way.
RNET::IPeer* ipeer = peer->As< RNET::IPeer >();

// We want to detect connections from HTML 5 clients. So, we set two callbacks,
// one for incoming connection requests and another one for disconnections.
ipeer->SetOnConnectionReady( OnConnectionReady, peer );
ipeer->SetOnConnectionClosed( OnConnectionClosed, peer );

// The client can change the quality of the image being sent. We need to setup
// an additional callback to process incoming messages from the clients.
ipeer->SetOnDataReceived( OnDataReceived, peer );

// Start the peer.
if( ipeer->Start( RED::Object::GetIDFromString( "HTML5Server" ),
                  1,
                  "",
                  1 ) != RED_OK )
{
  printf( "Error: REDnet failed to start the peer.\n" );
  RNET::System::Shutdown();
  return RED_FAIL;
}

We setup three callbacks: one for new connections, one for closed connections and another one for received data. This lets the program to manage the list of connected clients. In this tutorial, we force the number of simultaneously connected clients to 1, but it could be any other value.

3d setup

For the sake of simplicity, we chose to create a very simple 3d setup for this tutorial. It's made of a single rotating torus over a gradient background. We'll not cover in depth the code to setup the RED engine as it's fully detailed in many places in the Redsdk documentation.

To summarize:

and may not be refreshed correctly

retrieve the rendered pixels easily

The scene graph contains an additional transformation node which let us easily animate the torus by modifying only a matrix.

Main loop

The main loop of the program animates the torus and send updated images to connected clients.

The torus animation is computed using Euler angles which are derived from measured elapsed time. In order to avoid animation discontinuities using matrices, we use quaternions instead.

float e = (float)anim.MSElapsed();
float angle = 3.f * e / 1700.f;

// We use quaternions to avoid singularities in the sequential
// Euler rotation.

// Compute the quaternion coefficients from interpolated Euler angles.
float c_a = cosf( angle );
float s_a = sinf( angle );
float c_b = cosf( angle * 0.25f );
float s_b = sinf( angle * 0.25f );
float c_c = cosf( angle * 0.07f );
float s_c = sinf( angle * 0.07f );

float x = c_a * c_b * c_c + s_a * s_b * s_c;
float y = s_a * c_b * c_c + c_a * s_b * s_c;
float z = c_a * s_b * c_c + s_a * c_b * s_c;
float w = c_a * c_b * s_c + s_a * s_b * c_c;

// Normalize the quaternion.
float s = 1.f / sqrt( x * x + y * y + z * z + w * w );
x *= s;
y *= s;
z *= s;
w *= s;

// Convert the quaternion to a rotation matrix.
float m[16];
m[0]  = x * x + y * y - z * z - w * w;
m[1]  = 2.f * ( y * z + x * w );
m[2]  = 2.f * ( y * w - x * z );
m[3]  = 0.f;

m[4]  = 2.f * ( y * z - x * w );
m[5]  = x * x - y * y + z * z - w * w;
m[6]  = 2.f * ( x * y + z * w );
m[7]  = 0.f;

m[8]  = 2.f * ( x * z + y * w );
m[9]  = 2.f * ( z * w - x * y );
m[10] = x * x - y * y - z * z + w * w;
m[11] = 0.f;

m[12] = 0.f;
m[13] = 0.f;
m[14] = 0.f;
m[15] = 1.f;

mat.SetColumnMajorMatrix( m );
RED::ITransformShape* itrans = red_trans->As< RED::ITransformShape >();
RC_TEST( itrans->SetMatrix( &mat, iresmgr->GetState() ) );

Once the torus matrix has been updated and the transaction has been closed, the image is rendered into the auxiliary buffer:

// Render the scene.
RC_TEST( iwin->FrameDrawing() );

We can now pass the RED render image to the peer to send its content to the connected client:

// Send the rendered image if somebody's connected.
RNET::IPeer* ipeer = peer->As< RNET::IPeer >();
if( ipeer->IsConnectionAlive( connection ) )
{
  unsigned int sent;
  if( ipeer->SendImage( sent, quality, connection, red_render_image, 0 ) != RED_OK )
    app::Close();
}

The HTML 5 page

Introduction

Now, it's time to write the client side of the tutorial: the HTML 5 page. This page creates a drawing area in JavaScript (called a canvas) and set up a web socket with the rendering server.

The web socket will be used for bidirectional communications with the rendering server while the canvas will be used to display received images.

HTML 5 canvas

The canvas is created using HTML commands:

<canvas id="frame_buffer" width="640" height="480" style="border: 1px solid black;">
</canvas>

We give it a name ("frame_buffer"), a fixed size (640 x 480) and set a one pixel black border all around.

The rest of the code will be JavaScript code.

JavaScript code

The JavaScript code in a HTML page is enclosed between two tags:

<script>
  [..]
</script>

We set a JavaScript callback to be called on the loading of the web page to set up everything we need:

window.addEventListener( "load", init, false );

The JavaScript callback named init will be automatically called when the page will be loaded. Here is the code for that callback:

function init() 
{
  // Check web socket support.
  if( ( "WebSocket" in window ) == false )
  {
    alert( "Your browser does not fully support web sockets. Try updating it with the latest version." );
    return;
  }
  
  // Set global vars.
  canvas = document.getElementById( "frame_buffer" );
  ctx = canvas.getContext( "2d" );
  
  // Open the connection with the rendering server.
  ws_create();
}

We start by checking web socket support. Web sockets are a quite new feature designed with HTML 5. All major browsers do support it now, but we still need to perform that check in case somebody didn't update his browser recently.

We continue by retrieving the access to the canvas. Finally, we open the connection with the rendering server:

function ws_create()
{
  // Create a web socket and connect it to our rendering server.
  ws = new WebSocket( "ws://localhost:18000/stream" );

  ws.binaryType = "arraybuffer";

  // Websocket callbacks.
  ws.onopen = ws_onopen;
  ws.onclose = ws_onclose;
  ws.onmessage = ws_onmessage;

  // During the page lifetime, we need to periodically send some kind of echo messages
  // to the connected server. This is just to ensure that our connection doesn't get 
  // closed due to a wrong time out detection.
  //
  // Set the 'alive' message frequency (every 3sec).
  ws_heartbeat_cb = window.setInterval( ws_heartbeat, 3000 );

  // Create a new image for our fresh socket: it will receive the content
  // of images sent by the rendering server.
  img = new Image();
  
  // Set the image callback to automatically refresh the canvas content when a new
  // image has been received.
  img.onload = image_onload;
}

It's very simple to create a web socket in JavaScript. You just need to supply the URL to the destination and set needed callbacks. Here, the URL of the dispatcher is ws://localhost:18000/stream. So, we just write:

ws = new WebSocket( "ws://localhost:18000/stream" );
ws.binaryType = "arraybuffer";

to create the web socket.

To track changes, we set two callbacks:

// Web socket callbacks.
ws.onclose = ws_onclose;
ws.onmessage = ws_onmessage;

Finally, we create a JavaScript image to store received frames. By setting its onload callback , we get automatically called when a new complete image is available:

function image_onload()
{
  // Draw the image.
  ctx.drawImage( img, 0, 0 );
}

The remaining code takes place in the ws_onmessage callback. When data are received through the web socket, that callback is automatically called. Here, we assume that only image are sent by the rendering server. In fact, any kind of binary or text data can be sent/received through web sockets as you'll see when dealing with the image quality parameter.

function ws_onmessage( msg )
{
  // Consider we received a frame.
  img.src = msg.data;
}      

The received data are stored in msg. Rednet sends images which are already ready to display by the browser. Then, we only have to set the received image to the JavaScript image variable and draw it into our canvas to update the browser display.

Assigning a new content to a JavaScript image has the effect of calling the image onload callback automatically if set. In our case, the image_onload function seen above will be called as soon as one of the

img.src = ...

line gets evaluated.

Communicating with the server

To illustrate communications from the HTML page towards the rendering server, we added the possibility to change the quality of the image compression right from the HTML page.

We need an input field and an update button to change the quality value:

<div align="center">
  Image quality in [0,100]: <input type="text" value="100" id="image_quality" /><input type="button" value="Update" onclick="onImageQuality()" />
</div>

Each time the update button is clicked, the onImageQuality JavaScript callback is called.

function onImageQuality()
{
  value = document.getElementById( "image_quality" ).value;
  if( value < 0 ) value = 0;
  else if( value > 100 ) value = 100;
  
  document.getElementById( "image_quality" ).value = value;
    
  // Send the new image quality value to the rendering server.
  ws.send( "image_quality:" + value );
}

This callback retrieves the value of the quality parameter, check its bounds and send it to the rendering server using the web socket. Data are sent in text form which facilitate their encoding in the HTML page and their decoding on the server side.

Here is the rendering server code for the data reception callback:

RED_RC OnDataReceived( int                   iConnection,
                        const RNET::IMessage& iMessage,
                        void*                 ioUserData )
{
  if( iMessage.Size() == 0 )
    return RED_OK;

  if( iMessage.GetType() == RNET::MST_DATA_WEBSOCKET )
  {
    // Get the content of the message.
    char* buffer;
    int size;
    iMessage.GetData( buffer, size );

    // Check if we know the type of message.
    if( strncmp( buffer, "image_quality:", 14 ) == 0 )
    {
      // Image quality setting message.
      quality = (float)atoi( buffer + 14 ) / 100.f;
      quality = REDMax<float>( 0.f, REDMin<float>( 1.f, quality ) );
    }
  }

  return RED_OK;
}

The iMessage object contains the data received from the page. When receiving data through a web socket, the message type is set to RNET::MST_DATA_WEBSOCKET. The data themselves can be either text or binary depending on the web socket settings on the web page side. We know by design that the page only sends us text data, so we check for the "image_quality:" string and then extract the new value for the quality parameter on success.

The image quality has been set to 5. The image is a lot smaller in size on disk (and therefore takes less time to transmit) but looks quite blocky.