deb-spice-html5/webm.js

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();
},
}