"use strict"; /* Copyright (C) 2014 by Jeremy P. White 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 . */ /*---------------------------------------------------------------------------- ** 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_FLAG_INTERLACED = [ 0x9A ] ; 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 Constants = { CLUSTER_SIMPLEBLOCK_FLAG_KEYFRAME : 1 << 7, OPUS_FREQUENCY : 48000, OPUS_CHANNELS : 2, SPICE_PLAYBACK_CODEC : 'audio/webm; codecs="opus"', MAX_CLUSTER_TIME : 1000, EXPECTED_PACKET_DURATION : 10, GAP_DETECTION_THRESHOLD : 50, 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 = Constants.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.flag_interlaced = 0; 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_u8_value(WEBM_FLAG_INTERLACED, this.flag_interlaced, 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_FLAG_INTERLACED.length + 1 + 1 + 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 = 2; // Arbitrary id; most likely makes no difference 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(Constants.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 Constants.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) { /* ** In general, we follow specifications found by looking here: ** https://www.webmproject.org/docs/container/ ** which points here: ** https://www.matroska.org/technical/specs/index.html ** and here: ** https://datatracker.ietf.org/doc/draft-ietf-cellar-matroska/ ** Our goal is to supply mandatory values, and note where we differ ** from the default. */ 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; // Different than default; we wish to force this.flag_lacing = 1; this.min_cache = 0; this.max_block_addition_id = 0; this.codec_id = "V_VP8"; this.codec_decode_all = 1; this.seek_pre_roll = 0; 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_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_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 ? Constants.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(); }, } export { Constants, webm_Audio as Audio, webm_Video as Video, webm_AudioTrackEntry as AudioTrackEntry, webm_VideoTrackEntry as VideoTrackEntry, webm_Tracks as Tracks, webm_Cluster as Cluster, webm_SimpleBlock as SimpleBlock, webm_Header as Header, };