deb-spice-html5/webm.js
Jeremy White dc5fd91f0c Revise sound packet time sequencing for a more recent Firefox.
Around version 45, Firefox started being very particular about the
time stamps put into the Opus stream.  The time stamps from the Spice server are
somewhat irregular.  They mostly arrive every 10 ms, but sometimes it is 11, or sometimes
with two time stamps the same in a row.  The previous logic resulted in fuzzy and/or
distorted audio streams in Firefox in a row.

Thus, we end up with an inelegant hack.  Essentially, we force every packet to have
a 10ms time delta, unless there is an obvious gap in time stream, in which case we
will resync.

This replaces logic that mitigated only the duplicated time packets.

The long term solution would appear to be 'sequence' mode, but I cannot get
Firefox to use that mode (and MDN suggests that for codecs such as VP8 with time
stamps in line, that Firefox will not accept it).
2016-10-03 09:37:56 -05:00

648 lines
21 KiB
JavaScript

"use strict";
/*
Copyright (C) 2014 by Jeremy P. White <jwhite@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_VIDEO = [ 0xE0 ] ;
var WEBM_PIXEL_WIDTH = [ 0xB0 ] ;
var WEBM_PIXEL_HEIGHT = [ 0xBA ] ;
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 EXPECTED_PACKET_DURATION = 10;
var GAP_DETECTION_THRESHOLD = 50;
var SPICE_VP8_CODEC = 'video/webm; codecs="vp8"';
/*----------------------------------------------------------------------------
** 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;
},
}
function webm_Video(width, height)
{
this.id = WEBM_VIDEO;
this.width = width;
this.height = height;
}
webm_Video.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_u16_value(WEBM_PIXEL_WIDTH, this.width, dv, at)
at = EBML_write_u16_value(WEBM_PIXEL_HEIGHT, this.height, dv, at)
return at;
},
buffer_size: function()
{
return this.id.length + 8 +
WEBM_PIXEL_WIDTH.length + 1 + 2 +
WEBM_PIXEL_HEIGHT.length + 1 + 2;
},
}
/* ---------------------------
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_AudioTrackEntry()
{
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_AudioTrackEntry.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_VideoTrackEntry(width, height)
{
this.id = WEBM_TRACK_ENTRY;
this.number = 1;
this.uid = 1;
this.type = 1; // Video
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 = "V_VP8";
this.video = new webm_Video(width, height);
}
webm_VideoTrackEntry.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 = this.video.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_FLAG_ENABLED.length + 1 + 1 +
WEBM_FLAG_DEFAULT.length + 1 + 1 +
WEBM_FLAG_FORCED.length + 1 + 1 +
WEBM_FLAG_LACING.length + 1 + 1 +
WEBM_CODEC_ID.length + this.codec_id.length + 1 +
WEBM_MIN_CACHE.length + 1 + 1 +
WEBM_MAX_BLOCK_ADDITION_ID.length + 1 + 1 +
WEBM_CODEC_DECODE_ALL.length + 1 + 1 +
WEBM_CODEC_DELAY.length + 1 + 4 +
WEBM_SEEK_PRE_ROLL.length + 1 + 4 +
WEBM_TRACK_TYPE.length + 1 + 1 +
this.video.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();
}
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);
return at;
},
buffer_size: function()
{
return this.ebml.buffer_size() +
this.segment.buffer_size() +
this.info.buffer_size();
},
}