[Spice-devel] [spice-html5] Add support for audio streams using the Opus encoding.

Jeremy White jwhite at codeweavers.com
Tue May 20 11:03:31 PDT 2014


Requires a browser with MediaSource extension support, and
Opus support for the source buffers.  In practice, that is
Chrome and Firefox.

Signed-off-by: Jeremy White <jwhite at codeweavers.com>
---
 enums.js     |   20 +++
 main.js      |    2 +
 playback.js  |  278 +++++++++++++++++++++++++++++
 spice.html   |    2 +
 spiceconn.js |    5 +
 spicemsg.js  |   63 +++++++
 webm.js      |  553 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 7 files changed, 923 insertions(+)
 create mode 100644 playback.js
 create mode 100644 webm.js

diff --git a/enums.js b/enums.js
index fa541a4..7d41226 100644
--- a/enums.js
+++ b/enums.js
@@ -148,6 +148,26 @@ var SPICE_MSG_CURSOR_TRAIL              = 106;
 var SPICE_MSG_CURSOR_INVAL_ONE          = 107;
 var SPICE_MSG_CURSOR_INVAL_ALL          = 108;
 
+var SPICE_MSG_PLAYBACK_DATA             = 101;
+var SPICE_MSG_PLAYBACK_MODE             = 102;
+var SPICE_MSG_PLAYBACK_START            = 103;
+var SPICE_MSG_PLAYBACK_STOP             = 104;
+var SPICE_MSG_PLAYBACK_VOLUME           = 105;
+var SPICE_MSG_PLAYBACK_MUTE             = 106;
+var SPICE_MSG_PLAYBACK_LATENCY          = 107;
+
+var SPICE_PLAYBACK_CAP_CELT_0_5_1       = 0;
+var SPICE_PLAYBACK_CAP_VOLUME           = 1;
+var SPICE_PLAYBACK_CAP_LATENCY          = 2;
+var SPICE_PLAYBACK_CAP_OPUS             = 3;
+
+var SPICE_AUDIO_DATA_MODE_INVALID       = 0;
+var SPICE_AUDIO_DATA_MODE_RAW           = 1;
+var SPICE_AUDIO_DATA_MODE_CELT_0_5_1    = 2;
+var SPICE_AUDIO_DATA_MODE_OPUS          = 3;
+
+var SPICE_AUDIO_FMT_INVALID             = 0;
+var SPICE_AUDIO_FMT_S16                 = 1;
 
 var SPICE_CHANNEL_MAIN                  = 1;
 var SPICE_CHANNEL_DISPLAY               = 2;
diff --git a/main.js b/main.js
index 6b4e4cc..3656a8d 100644
--- a/main.js
+++ b/main.js
@@ -129,6 +129,8 @@ SpiceMainConn.prototype.process_channel_message = function(msg)
             }
             else if (chans.channels[i].type == SPICE_CHANNEL_CURSOR)
                 this.cursor = new SpiceCursorConn(conn);
+            else if (chans.channels[i].type == SPICE_CHANNEL_PLAYBACK)
+                this.cursor = new SpicePlaybackConn(conn);
             else
             {
                 this.log_err("Channel type " + chans.channels[i].type + " unknown.");
diff --git a/playback.js b/playback.js
new file mode 100644
index 0000000..7209fbe
--- /dev/null
+++ b/playback.js
@@ -0,0 +1,278 @@
+"use strict";
+/*
+   Copyright (C) 2014 by Jeremy P. White <jwhite at codeweavers.com>
+
+   This file is part of spice-html5.
+
+   spice-html5 is free software: you can redistribute it and/or modify
+   it under the terms of the GNU Lesser General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   spice-html5 is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public License
+   along with spice-html5.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+/*----------------------------------------------------------------------------
+**  SpicePlaybackConn
+**      Drive the Spice Playback channel (sound out)
+**--------------------------------------------------------------------------*/
+function SpicePlaybackConn()
+{
+    SpiceConn.apply(this, arguments);
+
+    this.queue = new Array();
+    this.append_okay = false;
+    this.start_time = 0;
+    this.skip_until = 0;
+    this.gap_time = 0;
+}
+
+SpicePlaybackConn.prototype = Object.create(SpiceConn.prototype);
+SpicePlaybackConn.prototype.process_channel_message = function(msg)
+{
+    if (!!!window.MediaSource)
+    {
+        this.log_err('MediaSource API is not available');
+        return false;
+    }
+
+    if (msg.type == SPICE_MSG_PLAYBACK_START)
+    {
+        var start = new SpiceMsgPlaybackStart(msg.data);
+
+        DEBUG > 0 && console.log("PlaybackStart; frequency " + start.frequency);
+
+        if (start.frequency != OPUS_FREQUENCY)
+        {
+            this.log_err('This player cannot handle frequency ' + start.frequency);
+            return false;
+        }
+
+        if (start.channels != OPUS_CHANNELS)
+        {
+            this.log_err('This player cannot handle ' + start.channels + ' channels');
+            return false;
+        }
+
+        if (start.format != SPICE_AUDIO_FMT_S16)
+        {
+            this.log_err('This player cannot format ' + start.format);
+            return false;
+        }
+
+        if (! this.source_buffer)
+        {
+            this.media_source = new MediaSource();
+            this.media_source.spiceconn = this;
+
+            this.audio = document.createElement("audio");
+            this.audio.setAttribute('autoplay', true);
+            this.audio.src = window.URL.createObjectURL(this.media_source);
+            document.getElementById(this.parent.screen_id).appendChild(this.audio);
+
+            this.media_source.addEventListener('sourceopen', handle_source_open, false);
+            this.media_source.addEventListener('sourceended', handle_source_ended, false);
+            this.media_source.addEventListener('sourceclosed', handle_source_closed, false);
+
+            this.bytes_written = 0;
+
+            return true;
+        }
+    }
+
+    if (msg.type == SPICE_MSG_PLAYBACK_DATA)
+    {
+        var data = new SpiceMsgPlaybackData(msg.data);
+
+        // If this packet has the same time as the last, just bump up by one.
+        if (this.last_data_time && data.time <= this.last_data_time)
+        {
+            // FIXME - this is arguably wrong.  But delaying the transmission was worse,
+            //          in initial testing.  Could use more research.
+            DEBUG > 1 && console.log("Hacking time of " + data.time + " to " + this.last_data_time + 1);
+            data.time = this.last_data_time + 1;
+        }
+
+        /* Gap detection:  If there has been a delay since our last packet, then audio must
+             have paused.  Handling that gets tricky.  In Chrome, you can seek forward,
+             but you cannot in Firefox.  And seeking forward in Chrome is nice, as it keeps
+             Chrome from being overly cautious in it's buffer strategy.
+
+             So we do two things.  First, we seek forward.  Second, we compute how much of a gap
+             there would have been, and essentially eliminate it.
+        */
+        if (this.last_data_time && data.time >= (this.last_data_time + GAP_DETECTION_THRESHOLD))
+        {
+            this.skip_until = data.time;
+            this.gap_time = (data.time - this.start_time) - 
+              (this.source_buffer.buffered.end(this.source_buffer.buffered.end.length - 1) * 1000.0).toFixed(0);
+        }
+
+        this.last_data_time = data.time;
+
+
+        DEBUG > 1 && console.log("PlaybackData; time " + data.time + "; length " + data.data.byteLength);
+
+        if (! this.source_buffer)
+            return true;
+
+        if (this.start_time == 0)
+            this.start_playback(data);
+
+        else if (data.time - this.cluster_time >= MAX_CLUSTER_TIME || this.skip_until > 0)
+            this.new_cluster(data);
+
+        else
+            this.simple_block(data, false);
+
+        if (this.skip_until > 0)
+        {
+            this.audio.currentTime = (this.skip_until - this.start_time - this.gap_time) / 1000.0;
+            this.skip_until = 0;
+        }
+
+        if (this.audio.paused)
+            this.audio.play();
+
+        return true;
+    }
+
+    if (msg.type == SPICE_MSG_PLAYBACK_MODE)
+    {
+        var mode = new SpiceMsgPlaybackMode(msg.data);
+        if (mode.mode != SPICE_AUDIO_DATA_MODE_OPUS)
+        {
+            this.log_err('This player cannot handle mode ' + mode.mode);
+            delete this.source_buffer;
+        }
+        return true;
+    }
+
+    if (msg.type == SPICE_MSG_PLAYBACK_STOP)
+    {
+        return true;
+    }
+
+    return false;
+}
+
+SpicePlaybackConn.prototype.start_playback = function(data)
+{
+    this.start_time = data.time;
+
+    var h = new webm_Header();
+
+    var mb = new ArrayBuffer(h.buffer_size())
+
+    this.bytes_written = h.to_buffer(mb);
+
+    this.source_buffer.addEventListener('error', handle_sourcebuffer_error, false);
+    this.source_buffer.addEventListener('updateend', handle_append_buffer_done, false);
+    playback_append_buffer(this, mb);
+
+    this.new_cluster(data);
+}
+
+SpicePlaybackConn.prototype.new_cluster = function(data)
+{
+    this.cluster_time = data.time;
+
+    var c = new webm_Cluster(data.time - this.start_time - this.gap_time);
+
+    var mb = new ArrayBuffer(c.buffer_size());
+    this.bytes_written += c.to_buffer(mb);
+
+    if (this.append_okay)
+        playback_append_buffer(this, mb);
+    else
+        this.queue.push(mb);
+
+    this.simple_block(data, true);
+}
+
+SpicePlaybackConn.prototype.simple_block = function(data, keyframe)
+{
+    var sb = new webm_SimpleBlock(data.time - this.cluster_time, data.data, keyframe);
+    var mb = new ArrayBuffer(sb.buffer_size());
+
+    this.bytes_written += sb.to_buffer(mb);
+
+    if (this.append_okay)
+        playback_append_buffer(this, mb);
+    else
+        this.queue.push(mb);
+}
+
+function handle_source_open(e)
+{
+    var p = this.spiceconn;
+
+    if (p.source_buffer)
+        return;
+
+    p.source_buffer = this.addSourceBuffer(SPICE_PLAYBACK_CODEC);
+    if (! p.source_buffer)
+    {
+        p.log_err('Codec ' + SPICE_PLAYBACK_CODEC + ' not available.');
+        return;
+    }
+    p.source_buffer.spiceconn = p;
+    p.source_buffer.mode = "segments";
+
+    // FIXME - Experimentation with segments and sequences was unsatisfying.
+    //         Switching to sequence did not solve our gap problem,
+    //         but the browsers didn't fully support the time seek capability
+    //         we would expect to gain from 'segments'.
+    //         Segments worked at the time of this patch, so segments it is for now.
+
+}
+
+function handle_source_ended(e)
+{
+    var p = this.spiceconn;
+    p.log_err('Audio source unexpectedly ended.');
+}
+
+function handle_source_closed(e)
+{
+    var p = this.spiceconn;
+    p.log_err('Audio source unexpectedly closed.');
+}
+
+function handle_append_buffer_done(b)
+{
+    var p = this.spiceconn;
+    if (p.queue.length > 0)
+    {
+        var mb = p.queue.shift();
+        playback_append_buffer(p, mb);
+    }
+    else
+        p.append_okay = true;
+
+}
+
+function handle_sourcebuffer_error(e)
+{
+    var p = this.spiceconn;
+    p.log_err('source_buffer error ' + e.message);
+}
+
+function playback_append_buffer(p, b)
+{
+    try
+    {
+        p.source_buffer.appendBuffer(b);
+        p.append_okay = false;
+    }
+    catch (e)
+    {
+        p.log_err("Error invoking appendBuffer: " + e.message);
+    }
+}
diff --git a/spice.html b/spice.html
index bd8d1ab..3b7929c 100644
--- a/spice.html
+++ b/spice.html
@@ -44,6 +44,8 @@
         <script src="display.js"></script> 
         <script src="main.js"></script> 
         <script src="inputs.js"></script> 
+        <script src="webm.js"></script>
+        <script src="playback.js"></script>
         <script src="simulatecursor.js"></script>
         <script src="cursor.js"></script> 
         <script src="thirdparty/jsbn.js"></script>
diff --git a/spiceconn.js b/spiceconn.js
index 318e9ae..81bc301 100644
--- a/spiceconn.js
+++ b/spiceconn.js
@@ -121,6 +121,11 @@ SpiceConn.prototype =
             (1 << SPICE_COMMON_CAP_MINI_HEADER)
             );
 
+        if (msg.channel_type == SPICE_CHANNEL_PLAYBACK)
+            msg.channel_caps.push(
+                (1 << SPICE_PLAYBACK_CAP_OPUS)
+            );
+
         hdr.size = msg.buffer_size();
 
         var mb = new ArrayBuffer(hdr.buffer_size() + msg.buffer_size());
diff --git a/spicemsg.js b/spicemsg.js
index de39aec..78371bc 100644
--- a/spicemsg.js
+++ b/spicemsg.js
@@ -608,6 +608,69 @@ SpiceMsgCursorInit.prototype =
     },
 }
 
+function SpiceMsgPlaybackData(a, at)
+{
+    this.from_buffer(a, at);
+}
+
+SpiceMsgPlaybackData.prototype =
+{
+    from_buffer: function(a, at, mb)
+    {
+        at = at || 0;
+        var dv = new SpiceDataView(a);
+        this.time = dv.getUint32(at, true); at += 4;
+        if (a.byteLength > at)
+        {
+            this.data = a.slice(at);
+            at += this.data.byteLength;
+        }
+        return at;
+    },
+}
+
+function SpiceMsgPlaybackMode(a, at)
+{
+    this.from_buffer(a, at);
+}
+
+SpiceMsgPlaybackMode.prototype =
+{
+    from_buffer: function(a, at, mb)
+    {
+        at = at || 0;
+        var dv = new SpiceDataView(a);
+        this.time = dv.getUint32(at, true); at += 4;
+        this.mode = dv.getUint16(at, true); at += 2;
+        if (a.byteLength > at)
+        {
+            this.data = a.slice(at);
+            at += this.data.byteLength;
+        }
+        return at;
+    },
+}
+
+function SpiceMsgPlaybackStart(a, at)
+{
+    this.from_buffer(a, at);
+}
+
+SpiceMsgPlaybackStart.prototype =
+{
+    from_buffer: function(a, at, mb)
+    {
+        at = at || 0;
+        var dv = new SpiceDataView(a);
+        this.channels = dv.getUint32(at, true); at += 4;
+        this.format = dv.getUint16(at, true); at += 2;
+        this.frequency = dv.getUint32(at, true); at += 4;
+        this.time = dv.getUint32(at, true); at += 4;
+        return at;
+    },
+}
+
+
 
 function SpiceMsgCursorSet(a, at)
 {
diff --git a/webm.js b/webm.js
new file mode 100644
index 0000000..35cbc07
--- /dev/null
+++ b/webm.js
@@ -0,0 +1,553 @@
+"use strict";
+/*
+   Copyright (C) 2014 by Jeremy P. White <jwhite at codeweavers.com>
+
+   This file is part of spice-html5.
+
+   spice-html5 is free software: you can redistribute it and/or modify
+   it under the terms of the GNU Lesser General Public License as published by
+   the Free Software Foundation, either version 3 of the License, or
+   (at your option) any later version.
+
+   spice-html5 is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU Lesser General Public License for more details.
+
+   You should have received a copy of the GNU Lesser General Public License
+   along with spice-html5.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+
+/*----------------------------------------------------------------------------
+**  EBML identifiers
+**--------------------------------------------------------------------------*/
+var EBML_HEADER =                           [ 0x1a, 0x45, 0xdf, 0xa3 ];
+var EBML_HEADER_VERSION =                   [ 0x42, 0x86 ];
+var EBML_HEADER_READ_VERSION =              [ 0x42, 0xf7 ];
+var EBML_HEADER_MAX_ID_LENGTH =             [ 0x42, 0xf2 ];
+var EBML_HEADER_MAX_SIZE_LENGTH =           [ 0x42, 0xf3 ];
+var EBML_HEADER_DOC_TYPE =                  [ 0x42, 0x82 ];
+var EBML_HEADER_DOC_TYPE_VERSION =          [ 0x42, 0x87 ];
+var EBML_HEADER_DOC_TYPE_READ_VERSION =     [ 0x42, 0x85 ];
+
+var WEBM_SEGMENT_HEADER =                   [ 0x18, 0x53, 0x80, 0x67 ];
+var WEBM_SEGMENT_INFORMATION =              [ 0x15, 0x49, 0xA9, 0x66 ];
+
+var WEBM_TIMECODE_SCALE =                   [ 0x2A, 0xD7, 0xB1 ];
+var WEBM_MUXING_APP =                       [ 0x4D, 0x80 ];
+var WEBM_WRITING_APP =                      [ 0x57, 0x41 ];
+
+var WEBM_SEEK_HEAD =                        [ 0x11, 0x4D, 0x9B, 0x74 ];
+var WEBM_SEEK =                             [ 0x4D, 0xBB ];
+var WEBM_SEEK_ID =                          [ 0x53, 0xAB ];
+var WEBM_SEEK_POSITION =                    [ 0x53, 0xAC ];
+
+var WEBM_TRACKS =                           [ 0x16, 0x54, 0xAE, 0x6B ];
+var WEBM_TRACK_ENTRY =                      [ 0xAE ];
+var WEBM_TRACK_NUMBER =                     [ 0xD7 ];
+var WEBM_TRACK_UID =                        [ 0x73, 0xC5 ];
+var WEBM_TRACK_TYPE =                       [ 0x83 ];
+var WEBM_FLAG_ENABLED =                     [ 0xB9 ];
+var WEBM_FLAG_DEFAULT =                     [ 0x88 ];
+var WEBM_FLAG_FORCED =                      [ 0x55, 0xAA ];
+var WEBM_FLAG_LACING =                      [ 0x9C ];
+var WEBM_MIN_CACHE =                        [ 0x6D, 0xE7 ];
+
+var WEBM_MAX_BLOCK_ADDITION_ID =            [ 0x55, 0xEE ];
+var WEBM_CODEC_DECODE_ALL =                 [ 0xAA ];
+var WEBM_SEEK_PRE_ROLL =                    [ 0x56, 0xBB ];
+var WEBM_CODEC_DELAY =                      [ 0x56, 0xAA ];
+var WEBM_CODEC_PRIVATE =                    [ 0x63, 0xA2 ];
+var WEBM_CODEC_ID =                         [ 0x86 ];
+
+var WEBM_AUDIO =                            [ 0xE1 ] ;
+var WEBM_SAMPLING_FREQUENCY =               [ 0xB5 ] ;
+var WEBM_CHANNELS =                         [ 0x9F ] ;
+
+var WEBM_CLUSTER =                          [ 0x1F, 0x43, 0xB6, 0x75 ];
+var WEBM_TIME_CODE =                        [ 0xE7 ] ;
+var WEBM_SIMPLE_BLOCK =                     [ 0xA3 ] ;
+
+/*----------------------------------------------------------------------------
+**  Various OPUS / Webm constants
+**--------------------------------------------------------------------------*/
+var CLUSTER_SIMPLEBLOCK_FLAG_KEYFRAME       = 1 << 7;
+
+var OPUS_FREQUENCY                          = 48000;
+var OPUS_CHANNELS                           = 2;
+
+var SPICE_PLAYBACK_CODEC                    = 'audio/webm; codecs="opus"';
+var MAX_CLUSTER_TIME                        = 1000;
+
+var GAP_DETECTION_THRESHOLD                 = 50;
+
+/*----------------------------------------------------------------------------
+**  EBML utility functions
+**      These classes can create the binary representation of a webm file
+**--------------------------------------------------------------------------*/
+function EBML_write_u1_data_len(len, dv, at)
+{
+    var b = 0x80 | len;
+    dv.setUint8(at, b);
+    return at + 1;
+}
+
+function EBML_write_u8_value(id, val, dv, at)
+{
+    at = EBML_write_array(id, dv, at);
+    at = EBML_write_u1_data_len(1, dv, at);
+    dv.setUint8(at, val);
+    return at + 1;
+}
+
+function EBML_write_u32_value(id, val, dv, at)
+{
+    at = EBML_write_array(id, dv, at);
+    at = EBML_write_u1_data_len(4, dv, at);
+    dv.setUint32(at, val);
+    return at + 4;
+}
+
+function EBML_write_u16_value(id, val, dv, at)
+{
+    at = EBML_write_array(id, dv, at);
+    at = EBML_write_u1_data_len(2, dv, at);
+    dv.setUint16(at, val);
+    return at + 2;
+}
+
+function EBML_write_float_value(id, val, dv, at)
+{
+    at = EBML_write_array(id, dv, at);
+    at = EBML_write_u1_data_len(4, dv, at);
+    dv.setFloat32(at, val);
+    return at + 4;
+}
+
+
+
+function EBML_write_u64_data_len(len, dv, at)
+{
+    /* Javascript doesn't do 64 bit ints, so this cheats and
+        just has a max of 32 bits.  Fine for our purposes */
+    dv.setUint8(at++, 0x01);
+    dv.setUint8(at++, 0x00);
+    dv.setUint8(at++, 0x00);
+    dv.setUint8(at++, 0x00);
+    var val = len & 0xFFFFFFFF;
+    for (var shift = 24; shift >= 0; shift -= 8)
+        dv.setUint8(at++, val >> shift);
+    return at;
+}
+
+function EBML_write_array(arr, dv, at)
+{
+    for (var i = 0; i < arr.length; i++)
+        dv.setUint8(at + i, arr[i]);
+    return at + arr.length;
+}
+
+function EBML_write_string(str, dv, at)
+{
+    for (var i = 0; i < str.length; i++)
+        dv.setUint8(at + i, str.charCodeAt(i));
+    return at + str.length;
+}
+
+function EBML_write_data(id, data, dv, at)
+{
+    at = EBML_write_array(id, dv, at);
+    if (data.length < 127)
+        at = EBML_write_u1_data_len(data.length, dv, at);
+    else
+        at = EBML_write_u64_data_len(data.length, dv, at);
+    if ((typeof data) == "string")
+        at = EBML_write_string(data, dv, at);
+    else
+        at = EBML_write_array(data, dv, at);
+    return at;
+}
+
+/*----------------------------------------------------------------------------
+**  Webm objects
+**      These classes can create the binary representation of a webm file
+**--------------------------------------------------------------------------*/
+function EBMLHeader()
+{
+    this.id = EBML_HEADER;
+    this.Version = 1;
+    this.ReadVersion = 1;
+    this.MaxIDLength = 4;
+    this.MaxSizeLength = 8;
+    this.DocType = "webm";
+    this.DocTypeVersion = 2;  /* Not well specified by the WebM guys, but functionally required for Firefox */
+    this.DocTypeReadVersion = 2;
+}
+
+EBMLHeader.prototype =
+{
+    to_buffer: function(a, at)
+    {
+        at = at || 0;
+        var dv = new DataView(a);
+
+        at = EBML_write_array(this.id, dv, at);
+        at = EBML_write_u64_data_len(0x1f, dv, at);
+        at = EBML_write_u8_value(EBML_HEADER_VERSION, this.Version, dv, at);
+        at = EBML_write_u8_value(EBML_HEADER_READ_VERSION, this.ReadVersion, dv, at);
+        at = EBML_write_u8_value(EBML_HEADER_MAX_ID_LENGTH, this.MaxIDLength, dv, at);
+        at = EBML_write_u8_value(EBML_HEADER_MAX_SIZE_LENGTH, this.MaxSizeLength, dv, at);
+        at = EBML_write_data(EBML_HEADER_DOC_TYPE, this.DocType, dv, at);
+        at = EBML_write_u8_value(EBML_HEADER_DOC_TYPE_VERSION, this.DocTypeVersion, dv, at);
+        at = EBML_write_u8_value(EBML_HEADER_DOC_TYPE_READ_VERSION, this.DocTypeReadVersion, dv, at);
+
+        return at;
+    },
+    buffer_size: function()
+    {
+        return 0x1f + 8 + this.id.length;
+    },
+}
+
+function webm_Segment()
+{
+    this.id = WEBM_SEGMENT_HEADER;
+}
+
+webm_Segment.prototype =
+{
+    to_buffer: function(a, at)
+    {
+        at = at || 0;
+        var dv = new DataView(a);
+
+        at = EBML_write_array(this.id, dv, at);
+        dv.setUint8(at++, 0xff);
+        return at;
+    },
+    buffer_size: function()
+    {
+        return this.id.length + 1;
+    },
+}
+
+function webm_SegmentInformation()
+{
+    this.id = WEBM_SEGMENT_INFORMATION;
+    this.timecode_scale = 1000000; /* 1 ms */
+    this.muxing_app = "spice";
+    this.writing_app = "spice-html5";
+
+}
+
+webm_SegmentInformation.prototype =
+{
+    to_buffer: function(a, at)
+    {
+        at = at || 0;
+        var dv = new DataView(a);
+
+        at = EBML_write_array(this.id, dv, at);
+        at = EBML_write_u64_data_len(this.buffer_size() - 8 - this.id.length, dv, at);
+        at = EBML_write_u32_value(WEBM_TIMECODE_SCALE, this.timecode_scale, dv, at);
+        at = EBML_write_data(WEBM_MUXING_APP, this.muxing_app, dv, at);
+        at = EBML_write_data(WEBM_WRITING_APP, this.writing_app, dv, at);
+        return at;
+    },
+    buffer_size: function()
+    {
+        return this.id.length + 8 +
+                 WEBM_TIMECODE_SCALE.length + 1 + 4 +
+                 WEBM_MUXING_APP.length + 1 + this.muxing_app.length +
+                 WEBM_WRITING_APP.length + 1 + this.writing_app.length;
+    },
+}
+
+function webm_Audio(frequency)
+{
+    this.id = WEBM_AUDIO;
+    this.sampling_frequency = frequency;
+    this.channels = OPUS_CHANNELS;
+}
+
+webm_Audio.prototype =
+{
+    to_buffer: function(a, at)
+    {
+        at = at || 0;
+        var dv = new DataView(a);
+        at = EBML_write_array(this.id, dv, at);
+        at = EBML_write_u64_data_len(this.buffer_size() - 8 - this.id.length, dv, at);
+        at = EBML_write_u8_value(WEBM_CHANNELS, this.channels, dv, at);
+        at = EBML_write_float_value(WEBM_SAMPLING_FREQUENCY, this.sampling_frequency, dv, at);
+        return at;
+    },
+    buffer_size: function()
+    {
+        return this.id.length + 8 +
+            WEBM_SAMPLING_FREQUENCY.length + 1 + 4 +
+            WEBM_CHANNELS.length + 1 + 1;
+    },
+}
+
+
+/* ---------------------------
+   SeekHead not currently used.  Hopefully not needed.
+*/
+function webm_Seek(seekid, pos)
+{
+    this.id = WEBM_SEEK;
+    this.pos = pos;
+    this.seekid = seekid;
+}
+
+webm_Seek.prototype =
+{
+    to_buffer: function(a, at)
+    {
+        at = at || 0;
+        var dv = new DataView(a);
+        at = EBML_write_array(this.id, dv, at);
+        at = EBML_write_u1_data_len(this.buffer_size() - 1 - this.id.length, dv, at);
+
+        at = EBML_write_data(WEBM_SEEK_ID, this.seekid, dv, at)
+        at = EBML_write_u16_value(WEBM_SEEK_POSITION, this.pos, dv, at)
+
+        return at;
+    },
+    buffer_size: function()
+    {
+        return this.id.length + 1 +
+                WEBM_SEEK_ID.length + 1 + this.seekid.length +
+                WEBM_SEEK_POSITION.length + 1 + 2;
+    },
+}
+function webm_SeekHead(info_pos, track_pos)
+{
+    this.id = WEBM_SEEK_HEAD;
+    this.info = new webm_Seek(WEBM_SEGMENT_INFORMATION, info_pos);
+    this.track = new webm_Seek(WEBM_TRACKS, track_pos);
+}
+
+webm_SeekHead.prototype =
+{
+    to_buffer: function(a, at)
+    {
+        at = at || 0;
+        var dv = new DataView(a);
+        at = EBML_write_array(this.id, dv, at);
+        at = EBML_write_u64_data_len(this.buffer_size() - 8 - this.id.length, dv, at);
+
+        at = this.info.to_buffer(a, at);
+        at = this.track.to_buffer(a, at);
+
+        return at;
+    },
+    buffer_size: function()
+    {
+        return this.id.length + 8 +
+                this.info.buffer_size() +
+                this.track.buffer_size();
+    },
+}
+
+/* -------------------------------
+   End of Seek Head
+*/
+
+function webm_TrackEntry()
+{
+    this.id = WEBM_TRACK_ENTRY;
+    this.number = 1;
+    this.uid = 1;
+    this.type = 2; // Audio
+    this.flag_enabled = 1;
+    this.flag_default = 1;
+    this.flag_forced = 1;
+    this.flag_lacing = 0;
+    this.min_cache = 0; // fixme - check
+    this.max_block_addition_id = 0;
+    this.codec_decode_all = 0; // fixme - check
+    this.seek_pre_roll = 0; // 80000000; // fixme - check
+    this.codec_delay =   80000000; // Must match codec_private.preskip
+    this.codec_id = "A_OPUS";
+    this.audio = new webm_Audio(OPUS_FREQUENCY);
+
+    // See:  http://tools.ietf.org/html/draft-terriberry-oggopus-01
+    this.codec_private = [ 0x4f, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64,  // OpusHead
+                           0x01, // Version
+                           OPUS_CHANNELS,
+                           0x00, 0x0F, // Preskip - 3840 samples - should be 8ms at 48kHz
+                           0x80, 0xbb, 0x00, 0x00,  // 48000
+                           0x00, 0x00, // Output gain
+                           0x00  // Channel mapping family
+                           ];
+}
+
+webm_TrackEntry.prototype =
+{
+    to_buffer: function(a, at)
+    {
+        at = at || 0;
+        var dv = new DataView(a);
+        at = EBML_write_array(this.id, dv, at);
+        at = EBML_write_u64_data_len(this.buffer_size() - 8 - this.id.length, dv, at);
+        at = EBML_write_u8_value(WEBM_TRACK_NUMBER, this.number, dv, at);
+        at = EBML_write_u8_value(WEBM_TRACK_UID, this.uid, dv, at);
+        at = EBML_write_u8_value(WEBM_FLAG_ENABLED, this.flag_enabled, dv, at);
+        at = EBML_write_u8_value(WEBM_FLAG_DEFAULT, this.flag_default, dv, at);
+        at = EBML_write_u8_value(WEBM_FLAG_FORCED, this.flag_forced, dv, at);
+        at = EBML_write_u8_value(WEBM_FLAG_LACING, this.flag_lacing, dv, at);
+        at = EBML_write_data(WEBM_CODEC_ID, this.codec_id, dv, at);
+        at = EBML_write_u8_value(WEBM_MIN_CACHE, this.min_cache, dv, at);
+        at = EBML_write_u8_value(WEBM_MAX_BLOCK_ADDITION_ID, this.max_block_addition_id, dv, at);
+        at = EBML_write_u8_value(WEBM_CODEC_DECODE_ALL, this.codec_decode_all, dv, at);
+        at = EBML_write_u32_value(WEBM_CODEC_DELAY, this.codec_delay, dv, at);
+        at = EBML_write_u32_value(WEBM_SEEK_PRE_ROLL, this.seek_pre_roll, dv, at);
+        at = EBML_write_u8_value(WEBM_TRACK_TYPE, this.type, dv, at);
+        at = EBML_write_data(WEBM_CODEC_PRIVATE, this.codec_private, dv, at);
+
+        at = this.audio.to_buffer(a, at);
+        return at;
+    },
+    buffer_size: function()
+    {
+        return this.id.length + 8 +
+            WEBM_TRACK_NUMBER.length + 1 + 1 +
+            WEBM_TRACK_UID.length + 1 + 1 +
+            WEBM_TRACK_TYPE.length + 1 + 1 +
+            WEBM_FLAG_ENABLED.length + 1 + 1 +
+            WEBM_FLAG_DEFAULT.length + 1 + 1 +
+            WEBM_FLAG_FORCED.length + 1 + 1 +
+            WEBM_FLAG_LACING.length + 1 + 1 +
+            WEBM_MIN_CACHE.length + 1 + 1 +
+            WEBM_MAX_BLOCK_ADDITION_ID.length + 1 + 1 +
+            WEBM_CODEC_DECODE_ALL.length + 1 + 1 +
+            WEBM_SEEK_PRE_ROLL.length + 1 + 4 +
+            WEBM_CODEC_DELAY.length + 1 + 4 +
+            WEBM_CODEC_ID.length + this.codec_id.length + 1 +
+            WEBM_CODEC_PRIVATE.length + 1 + this.codec_private.length +
+            this.audio.buffer_size();
+    },
+}
+function webm_Tracks(entry)
+{
+    this.id = WEBM_TRACKS;
+    this.track_entry = entry;
+}
+
+webm_Tracks.prototype =
+{
+    to_buffer: function(a, at)
+    {
+        at = at || 0;
+        var dv = new DataView(a);
+        at = EBML_write_array(this.id, dv, at);
+        at = EBML_write_u64_data_len(this.buffer_size() - 8 - this.id.length, dv, at);
+        at = this.track_entry.to_buffer(a, at);
+        return at;
+    },
+    buffer_size: function()
+    {
+        return this.id.length + 8 +
+                 this.track_entry.buffer_size();
+    },
+}
+
+function webm_Cluster(timecode, data)
+{
+    this.id = WEBM_CLUSTER;
+    this.timecode = timecode;
+    this.data = data;
+}
+
+webm_Cluster.prototype =
+{
+    to_buffer: function(a, at)
+    {
+        at = at || 0;
+        var dv = new DataView(a);
+        at = EBML_write_array(this.id, dv, at);
+        dv.setUint8(at++, 0xff);
+        at = EBML_write_u32_value(WEBM_TIME_CODE, this.timecode, dv, at);
+        return at;
+    },
+    buffer_size: function()
+    {
+        return this.id.length + 1 +
+                 WEBM_TIME_CODE.length + 1 + 4;
+    },
+}
+
+function webm_SimpleBlock(timecode, data, keyframe)
+{
+    this.id = WEBM_SIMPLE_BLOCK;
+    this.timecode = timecode;
+    this.data = data;
+    this.keyframe = keyframe;
+}
+
+webm_SimpleBlock.prototype =
+{
+    to_buffer: function(a, at)
+    {
+        at = at || 0;
+        var dv = new DataView(a);
+        at = EBML_write_array(this.id, dv, at);
+        at = EBML_write_u64_data_len(this.data.byteLength + 4, dv, at);
+        at = EBML_write_u1_data_len(1, dv, at); // Track #
+        dv.setUint16(at, this.timecode); at += 2; // timecode - relative to cluster
+        dv.setUint8(at, this.keyframe ? CLUSTER_SIMPLEBLOCK_FLAG_KEYFRAME : 0); at += 1;  // flags
+
+        // FIXME - There should be a better way to copy
+        var u8 = new Uint8Array(this.data);
+        for (var i = 0; i < this.data.byteLength; i++)
+            dv.setUint8(at++, u8[i]);
+
+        return at;
+    },
+    buffer_size: function()
+    {
+        return this.id.length + 8 +
+                 1 + 2 + 1 +
+                 this.data.byteLength;
+    },
+}
+
+function webm_Header()
+{
+    this.ebml = new EBMLHeader;
+    this.segment = new webm_Segment;
+    this.seek_head = new webm_SeekHead(0, 0);
+
+    this.seek_head.info.pos = this.segment.buffer_size() + this.seek_head.buffer_size();
+
+    this.info = new webm_SegmentInformation;
+
+    this.seek_head.track.pos = this.seek_head.info.pos + this.info.buffer_size();
+
+    this.track_entry = new webm_TrackEntry;
+    this.tracks = new webm_Tracks(this.track_entry);
+}
+
+webm_Header.prototype =
+{
+    to_buffer: function(a, at)
+    {
+        at = at || 0;
+        at = this.ebml.to_buffer(a, at);
+        at = this.segment.to_buffer(a, at);
+        at = this.info.to_buffer(a, at);
+        at = this.tracks.to_buffer(a, at);
+
+        return at;
+    },
+    buffer_size: function()
+    {
+        return this.ebml.buffer_size() +
+               this.segment.buffer_size() +
+               this.info.buffer_size() +
+               this.tracks.buffer_size();
+    },
+}
-- 
1.7.10.4



More information about the Spice-devel mailing list