diff --git a/README.md b/README.md index 82988de..131cdd8 100644 --- a/README.md +++ b/README.md @@ -18,15 +18,17 @@ + 支持 RTSP 拉流(拉取摄像头或其他流媒体服务器资源) + 支持 RTSP TCP、UDP、Multicast 播放 + 支持 H264+AAC H5播放,包括: + + WSP: [html5_rtsp_player](https://github.com/Streamedian/html5_rtsp_player) + + Websocket-RTSP(实验): 播放端修改自[html5_rtsp_player](https://github.com/Streamedian/html5_rtsp_player) + HTTP-FLV + Websocket-FLV + HTTP-HLS - + Websocket-RTSP(实验): 实时性更好 + 支持 H265+AAC H5播放(实验,需自行寻找播放软件),包括: + HTTP-FLV + Websocket-FLV + 支持流媒体用户推拉权限管理 + 业务系统集成 RestfulAPI ++ 支持 user 和 routetable 提供者插件:仅支持 linux 和 mac ## 文档 + [Quick Start](/docs/quickstart.md) diff --git a/demos/wsp/free.player.1.8.js b/demos/wsp/free.player.1.8.js new file mode 100644 index 0000000..bce5dd4 --- /dev/null +++ b/demos/wsp/free.player.1.8.js @@ -0,0 +1,9466 @@ +(function () { +'use strict'; + +// ERROR=0, WARN=1, LOG=2, DEBUG=3 +const LogLevel = { + Error: 0, + Warn: 1, + Log: 2, + Debug: 3 +}; + +let DEFAULT_LOG_LEVEL = LogLevel.Debug; + +function setDefaultLogLevel(level) { + DEFAULT_LOG_LEVEL = level; +} +class Logger { + constructor(level = DEFAULT_LOG_LEVEL, tag) { + this.tag = tag; + this.setLevel(level); + } + + setLevel(level) { + this.level = level; + } + + static get level_map() { return { + [LogLevel.Debug]:'log', + [LogLevel.Log]:'log', + [LogLevel.Warn]:'warn', + [LogLevel.Error]:'error' + }}; + + _log(lvl, args) { + args = Array.prototype.slice.call(args); + if (this.tag) { + args.unshift(`[${this.tag}]`); + } + if (this.level>=lvl) console[Logger.level_map[lvl]].apply(console, args); + } + log(){ + this._log(LogLevel.Log, arguments); + } + debug(){ + this._log(LogLevel.Debug, arguments); + } + error(){ + this._log(LogLevel.Error, arguments); + } + warn(){ + this._log(LogLevel.Warn, arguments); + } +} + +const taggedLoggers = new Map(); +function getTagged(tag) { + if (!taggedLoggers.has(tag)) { + taggedLoggers.set(tag, new Logger(DEFAULT_LOG_LEVEL, tag)); + } + return taggedLoggers.get(tag); +} +const Log = new Logger(); + +// export * from 'bp_logger'; + +class Url { + static parse(url) { + let ret = {}; + + let regex = /^([^:]+):\/\/([^\/]+)(.*)$/; //protocol, login, urlpath + let result = regex.exec(url); + + if (!result) { + throw new Error("bad url"); + } + + ret.full = url; + ret.protocol = result[1]; + ret.urlpath = result[3]; + + let parts = ret.urlpath.split('/'); + ret.basename = parts.pop().split(/\?|#/)[0]; + ret.basepath = parts.join('/'); + + let loginSplit = result[2].split('@'); + let hostport = loginSplit[0].split(':'); + let userpass = [ null, null ]; + if (loginSplit.length === 2) { + userpass = loginSplit[0].split(':'); + hostport = loginSplit[1].split(':'); + } + + ret.user = userpass[0]; + ret.pass = userpass[1]; + ret.host = hostport[0]; + ret.auth = (ret.user && ret.pass) ? `${ret.user}:${ret.pass}` : ''; + + ret.port = (null == hostport[1]) ? Url.protocolDefaultPort(ret.protocol) : hostport[1]; + ret.portDefined = (null != hostport[1]); + ret.location = `${ret.host}:${ret.port}`; + + if (ret.protocol == 'unix') { + ret.socket = ret.port; + ret.port = undefined; + } + + return ret; + } + + static full(parsed) { + return `${parsed.protocol}://${parsed.location}/${parsed.urlpath}`; + } + + static isAbsolute(url) { + return /^[^:]+:\/\//.test(url); + } + + static protocolDefaultPort(protocol) { + switch (protocol) { + case 'rtsp': return 554; + case 'http': return 80; + case 'https': return 443; + } + + return 0; + } +} + +const listener = Symbol("event_listener"); +const listeners = Symbol("event_listeners"); + +class DestructibleEventListener { + constructor(eventListener) { + this[listener] = eventListener; + this[listeners] = new Map(); + } + + clear() { + if (this[listeners]) { + for (let entry of this[listeners]) { + for (let fn of entry[1]) { + this[listener].removeEventListener(entry[0], fn); + } + } + } + this[listeners].clear(); + } + + destroy() { + this.clear(); + this[listeners] = null; + } + + on(event, selector, fn) { + if (fn == undefined) { + fn = selector; + selector = null; + } + if (selector) { + return this.addEventListener(event, (e) => { + if (e.target.matches(selector)) { + fn(e); + } + }); + } else { + return this.addEventListener(event, fn); + } + } + + addEventListener(event, fn) { + if (!this[listeners].has(event)) { + this[listeners].set(event, new Set()); + } + this[listeners].get(event).add(fn); + this[listener].addEventListener(event, fn, false); + return fn; + } + + removeEventListener(event, fn) { + this[listener].removeEventListener(event, fn, false); + if (this[listeners].has(event)) { + //this[listeners].set(event, new Set()); + let ev = this[listeners].get(event); + ev.delete(fn); + if (!ev.size) { + this[listeners].delete(event); + } + } + } + + dispatchEvent(event) { + if (this[listener]) { + this[listener].dispatchEvent(event); + } + } +} + +class EventEmitter { + constructor(element=null) { + this[listener] = new DestructibleEventListener(element || document.createElement('div')); + } + + clear() { + if (this[listener]) { + this[listener].clear(); + } + } + + destroy() { + if (this[listener]) { + this[listener].destroy(); + this[listener] = null; + } + } + + on(event, selector, fn) { + if (this[listener]) { + return this[listener].on(event, selector, fn); + } + return null; + } + + addEventListener(event, fn) { + if (this[listener]) { + return this[listener].addEventListener(event, fn, false); + } + return null; + } + + removeEventListener(event, fn) { + if (this[listener]) { + this[listener].removeEventListener(event, fn, false); + } + } + + dispatchEvent(event, data) { + if (this[listener]) { + this[listener].dispatchEvent(new CustomEvent(event, {detail: data})); + } + } +} + +class EventSourceWrapper { + constructor(eventSource) { + this.eventSource = eventSource; + this[listeners] = new Map(); + } + + on(event, selector, fn) { + if (!this[listeners].has(event)) { + this[listeners].set(event, new Set()); + } + let listener = this.eventSource.on(event, selector, fn); + if (listener) { + this[listeners].get(event).add(listener); + } + } + + off(event, fn){ + this.eventSource.removeEventListener(event, fn); + } + + clear() { + this.eventSource.clear(); + this[listeners].clear(); + } + + destroy() { + this.eventSource.clear(); + this[listeners] = null; + this.eventSource = null; + } +} + +// export * from 'bp_event'; + +/** + * Generate MP4 Box + * got from: https://github.com/dailymotion/hls.js + */ + +class MP4 { + static init() { + MP4.types = { + avc1: [], // codingname + avcC: [], + btrt: [], + dinf: [], + dref: [], + esds: [], + ftyp: [], + hdlr: [], + mdat: [], + mdhd: [], + mdia: [], + mfhd: [], + minf: [], + moof: [], + moov: [], + mp4a: [], + mvex: [], + mvhd: [], + sdtp: [], + stbl: [], + stco: [], + stsc: [], + stsd: [], + stsz: [], + stts: [], + tfdt: [], + tfhd: [], + traf: [], + trak: [], + trun: [], + trex: [], + tkhd: [], + vmhd: [], + smhd: [] + }; + + var i; + for (i in MP4.types) { + if (MP4.types.hasOwnProperty(i)) { + MP4.types[i] = [ + i.charCodeAt(0), + i.charCodeAt(1), + i.charCodeAt(2), + i.charCodeAt(3) + ]; + } + } + + var videoHdlr = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x56, 0x69, 0x64, 0x65, + 0x6f, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' + ]); + + var audioHdlr = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun' + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x53, 0x6f, 0x75, 0x6e, + 0x64, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' + ]); + + MP4.HDLR_TYPES = { + 'video': videoHdlr, + 'audio': audioHdlr + }; + + var dref = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01, // entry_count + 0x00, 0x00, 0x00, 0x0c, // entry_size + 0x75, 0x72, 0x6c, 0x20, // 'url' type + 0x00, // version 0 + 0x00, 0x00, 0x01 // entry_flags + ]); + + var stco = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00 // entry_count + ]); + + MP4.STTS = MP4.STSC = MP4.STCO = stco; + + MP4.STSZ = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // sample_size + 0x00, 0x00, 0x00, 0x00, // sample_count + ]); + MP4.VMHD = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x01, // flags + 0x00, 0x00, // graphicsmode + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00 // opcolor + ]); + MP4.SMHD = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, // balance + 0x00, 0x00 // reserved + ]); + + MP4.STSD = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01]);// entry_count + + var majorBrand = new Uint8Array([105,115,111,109]); // isom + var avc1Brand = new Uint8Array([97,118,99,49]); // avc1 + var minorVersion = new Uint8Array([0, 0, 0, 1]); + + MP4.FTYP = MP4.box(MP4.types.ftyp, majorBrand, minorVersion, majorBrand, avc1Brand); + MP4.DINF = MP4.box(MP4.types.dinf, MP4.box(MP4.types.dref, dref)); + } + + static box(type, ...payload) { + var size = 8, + i = payload.length, + len = i, + result; + // calculate the total size we need to allocate + while (i--) { + size += payload[i].byteLength; + } + result = new Uint8Array(size); + result[0] = (size >> 24) & 0xff; + result[1] = (size >> 16) & 0xff; + result[2] = (size >> 8) & 0xff; + result[3] = size & 0xff; + result.set(type, 4); + // copy the payload into the result + for (i = 0, size = 8; i < len; ++i) { + // copy payload[i] array @ offset size + result.set(payload[i], size); + size += payload[i].byteLength; + } + return result; + } + + static hdlr(type) { + return MP4.box(MP4.types.hdlr, MP4.HDLR_TYPES[type]); + } + + static mdat(data) { + return MP4.box(MP4.types.mdat, data); + } + + static mdhd(timescale, duration) { + return MP4.box(MP4.types.mdhd, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x02, // creation_time + 0x00, 0x00, 0x00, 0x03, // modification_time + (timescale >> 24) & 0xFF, + (timescale >> 16) & 0xFF, + (timescale >> 8) & 0xFF, + timescale & 0xFF, // timescale + (duration >> 24), + (duration >> 16) & 0xFF, + (duration >> 8) & 0xFF, + duration & 0xFF, // duration + 0x55, 0xc4, // 'und' language (undetermined) + 0x00, 0x00 + ])); + } + + static mdia(track) { + return MP4.box(MP4.types.mdia, MP4.mdhd(track.timescale, track.duration), MP4.hdlr(track.type), MP4.minf(track)); + } + + static mfhd(sequenceNumber) { + return MP4.box(MP4.types.mfhd, new Uint8Array([ + 0x00, + 0x00, 0x00, 0x00, // flags + (sequenceNumber >> 24), + (sequenceNumber >> 16) & 0xFF, + (sequenceNumber >> 8) & 0xFF, + sequenceNumber & 0xFF, // sequence_number + ])); + } + + static minf(track) { + if (track.type === 'audio') { + return MP4.box(MP4.types.minf, MP4.box(MP4.types.smhd, MP4.SMHD), MP4.DINF, MP4.stbl(track)); + } else { + return MP4.box(MP4.types.minf, MP4.box(MP4.types.vmhd, MP4.VMHD), MP4.DINF, MP4.stbl(track)); + } + } + + static moof(sn, baseMediaDecodeTime, track) { + return MP4.box(MP4.types.moof, MP4.mfhd(sn), MP4.traf(track,baseMediaDecodeTime)); + } + /** + * @param tracks... (optional) {array} the tracks associated with this movie + */ + static moov(tracks, duration, timescale) { + var + i = tracks.length, + boxes = []; + + while (i--) { + boxes[i] = MP4.trak(tracks[i]); + } + + return MP4.box.apply(null, [MP4.types.moov, MP4.mvhd(timescale, duration)].concat(boxes).concat(MP4.mvex(tracks))); + } + + static mvex(tracks) { + var + i = tracks.length, + boxes = []; + + while (i--) { + boxes[i] = MP4.trex(tracks[i]); + } + return MP4.box.apply(null, [MP4.types.mvex].concat(boxes)); + } + + static mvhd(timescale,duration) { + var + bytes = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01, // creation_time + 0x00, 0x00, 0x00, 0x02, // modification_time + (timescale >> 24) & 0xFF, + (timescale >> 16) & 0xFF, + (timescale >> 8) & 0xFF, + timescale & 0xFF, // timescale + (duration >> 24) & 0xFF, + (duration >> 16) & 0xFF, + (duration >> 8) & 0xFF, + duration & 0xFF, // duration + 0x00, 0x01, 0x00, 0x00, // 1.0 rate + 0x01, 0x00, // 1.0 volume + 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0xff, 0xff, 0xff, 0xff // next_track_ID + ]); + return MP4.box(MP4.types.mvhd, bytes); + } + + static sdtp(track) { + var + samples = track.samples || [], + bytes = new Uint8Array(4 + samples.length), + flags, + i; + // leave the full box header (4 bytes) all zero + // write the sample table + for (i = 0; i < samples.length; i++) { + flags = samples[i].flags; + bytes[i + 4] = (flags.dependsOn << 4) | + (flags.isDependedOn << 2) | + (flags.hasRedundancy); + } + + return MP4.box(MP4.types.sdtp, bytes); + } + + static stbl(track) { + return MP4.box(MP4.types.stbl, MP4.stsd(track), MP4.box(MP4.types.stts, MP4.STTS), MP4.box(MP4.types.stsc, MP4.STSC), MP4.box(MP4.types.stsz, MP4.STSZ), MP4.box(MP4.types.stco, MP4.STCO)); + } + + static avc1(track) { + var sps = [], pps = [], i, data, len; + // assemble the SPSs + + for (i = 0; i < track.sps.length; i++) { + data = track.sps[i]; + len = data.byteLength; + sps.push((len >>> 8) & 0xFF); + sps.push((len & 0xFF)); + sps = sps.concat(Array.prototype.slice.call(data)); // SPS + } + + // assemble the PPSs + for (i = 0; i < track.pps.length; i++) { + data = track.pps[i]; + len = data.byteLength; + pps.push((len >>> 8) & 0xFF); + pps.push((len & 0xFF)); + pps = pps.concat(Array.prototype.slice.call(data)); + } + + var avcc = MP4.box(MP4.types.avcC, new Uint8Array([ + 0x01, // version + sps[3], // profile + sps[4], // profile compat + sps[5], // level + 0xfc | 3, // lengthSizeMinusOne, hard-coded to 4 bytes + 0xE0 | track.sps.length // 3bit reserved (111) + numOfSequenceParameterSets + ].concat(sps).concat([ + track.pps.length // numOfPictureParameterSets + ]).concat(pps))), // "PPS" + width = track.width, + height = track.height; + //console.log('avcc:' + Hex.hexDump(avcc)); + return MP4.box(MP4.types.avc1, new Uint8Array([ + 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // data_reference_index + 0x00, 0x00, // pre_defined + 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // pre_defined + (width >> 8) & 0xFF, + width & 0xff, // width + (height >> 8) & 0xFF, + height & 0xff, // height + 0x00, 0x48, 0x00, 0x00, // horizresolution + 0x00, 0x48, 0x00, 0x00, // vertresolution + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // frame_count + 0x12, + 0x62, 0x69, 0x6E, 0x65, //binelpro.ru + 0x6C, 0x70, 0x72, 0x6F, + 0x2E, 0x72, 0x75, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, // compressorname + 0x00, 0x18, // depth = 24 + 0x11, 0x11]), // pre_defined = -1 + avcc, + MP4.box(MP4.types.btrt, new Uint8Array([ + 0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB + 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate + 0x00, 0x2d, 0xc6, 0xc0])) // avgBitrate + ); + } + + static esds(track) { + var configlen = track.config.byteLength; + let data = new Uint8Array(26+configlen+3); + data.set([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + + 0x03, // descriptor_type + 0x17+configlen, // length + 0x00, 0x01, //es_id + 0x00, // stream_priority + + 0x04, // descriptor_type + 0x0f+configlen, // length + 0x40, //codec : mpeg4_audio + 0x15, // stream_type + 0x00, 0x00, 0x00, // buffer_size + 0x00, 0x00, 0x00, 0x00, // maxBitrate + 0x00, 0x00, 0x00, 0x00, // avgBitrate + + 0x05, // descriptor_type + configlen + ]); + data.set(track.config, 26); + data.set([0x06, 0x01, 0x02], 26+configlen); + // return new Uint8Array([ + // 0x00, // version 0 + // 0x00, 0x00, 0x00, // flags + // + // 0x03, // descriptor_type + // 0x17+configlen, // length + // 0x00, 0x01, //es_id + // 0x00, // stream_priority + // + // 0x04, // descriptor_type + // 0x0f+configlen, // length + // 0x40, //codec : mpeg4_audio + // 0x15, // stream_type + // 0x00, 0x00, 0x00, // buffer_size + // 0x00, 0x00, 0x00, 0x00, // maxBitrate + // 0x00, 0x00, 0x00, 0x00, // avgBitrate + // + // 0x05 // descriptor_type + // ].concat([configlen]).concat(track.config).concat([0x06, 0x01, 0x02])); // GASpecificConfig)); // length + audio config descriptor + return data; + } + + static mp4a(track) { + var audiosamplerate = track.audiosamplerate; + return MP4.box(MP4.types.mp4a, new Uint8Array([ + 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // data_reference_index + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, track.channelCount, // channelcount + 0x00, 0x10, // sampleSize:16bits + 0x00, 0x00, // pre_defined + 0x00, 0x00, // reserved2 + (audiosamplerate >> 8) & 0xFF, + audiosamplerate & 0xff, // + 0x00, 0x00]), + MP4.box(MP4.types.esds, MP4.esds(track))); + } + + static stsd(track) { + if (track.type === 'audio') { + return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp4a(track)); + } else { + return MP4.box(MP4.types.stsd, MP4.STSD, MP4.avc1(track)); + } + } + + static tkhd(track) { + var id = track.id, + duration = track.duration, + width = track.width, + height = track.height, + volume = track.volume; + return MP4.box(MP4.types.tkhd, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x07, // flags + 0x00, 0x00, 0x00, 0x00, // creation_time + 0x00, 0x00, 0x00, 0x00, // modification_time + (id >> 24) & 0xFF, + (id >> 16) & 0xFF, + (id >> 8) & 0xFF, + id & 0xFF, // track_ID + 0x00, 0x00, 0x00, 0x00, // reserved + (duration >> 24), + (duration >> 16) & 0xFF, + (duration >> 8) & 0xFF, + duration & 0xFF, // duration + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, // layer + 0x00, 0x00, // alternate_group + (volume>>0)&0xff, (((volume%1)*10)>>0)&0xff, // track volume // FIXME + 0x00, 0x00, // reserved + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix + (width >> 8) & 0xFF, + width & 0xFF, + 0x00, 0x00, // width + (height >> 8) & 0xFF, + height & 0xFF, + 0x00, 0x00 // height + ])); + } + + static traf(track,baseMediaDecodeTime) { + var sampleDependencyTable = MP4.sdtp(track), + id = track.id; + return MP4.box(MP4.types.traf, + MP4.box(MP4.types.tfhd, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + (id >> 24), + (id >> 16) & 0XFF, + (id >> 8) & 0XFF, + (id & 0xFF) // track_ID + ])), + MP4.box(MP4.types.tfdt, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + (baseMediaDecodeTime >>24), + (baseMediaDecodeTime >> 16) & 0XFF, + (baseMediaDecodeTime >> 8) & 0XFF, + (baseMediaDecodeTime & 0xFF) // baseMediaDecodeTime + ])), + MP4.trun(track, + sampleDependencyTable.length + + 16 + // tfhd + 16 + // tfdt + 8 + // traf header + 16 + // mfhd + 8 + // moof header + 8), // mdat header + sampleDependencyTable); + } + + /** + * Generate a track box. + * @param track {object} a track definition + * @return {Uint8Array} the track box + */ + static trak(track) { + track.duration = track.duration || 0xffffffff; + return MP4.box(MP4.types.trak, MP4.tkhd(track), MP4.mdia(track)); + } + + static trex(track) { + var id = track.id; + return MP4.box(MP4.types.trex, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + (id >> 24), + (id >> 16) & 0XFF, + (id >> 8) & 0XFF, + (id & 0xFF), // track_ID + 0x00, 0x00, 0x00, 0x01, // default_sample_description_index + 0x00, 0x00, 0x00, 0x00, // default_sample_duration + 0x00, 0x00, 0x00, 0x00, // default_sample_size + 0x00, 0x01, 0x00, 0x01 // default_sample_flags + ])); + } + + static trun(track, offset) { + var samples= track.samples || [], + len = samples.length, + arraylen = 12 + (16 * len), + array = new Uint8Array(arraylen), + i,sample,duration,size,flags,cts; + offset += 8 + arraylen; + array.set([ + 0x00, // version 0 + 0x00, 0x0f, 0x01, // flags + (len >>> 24) & 0xFF, + (len >>> 16) & 0xFF, + (len >>> 8) & 0xFF, + len & 0xFF, // sample_count + (offset >>> 24) & 0xFF, + (offset >>> 16) & 0xFF, + (offset >>> 8) & 0xFF, + offset & 0xFF // data_offset + ],0); + for (i = 0; i < len; i++) { + sample = samples[i]; + duration = sample.duration; + size = sample.size; + flags = sample.flags; + cts = sample.cts; + array.set([ + (duration >>> 24) & 0xFF, + (duration >>> 16) & 0xFF, + (duration >>> 8) & 0xFF, + duration & 0xFF, // sample_duration + (size >>> 24) & 0xFF, + (size >>> 16) & 0xFF, + (size >>> 8) & 0xFF, + size & 0xFF, // sample_size + (flags.isLeading << 2) | flags.dependsOn, + (flags.isDependedOn << 6) | + (flags.hasRedundancy << 4) | + (flags.paddingValue << 1) | + flags.isNonSync, + flags.degradPrio & 0xF0 << 8, + flags.degradPrio & 0x0F, // sample_flags + (cts >>> 24) & 0xFF, + (cts >>> 16) & 0xFF, + (cts >>> 8) & 0xFF, + cts & 0xFF // sample_composition_time_offset + ],12+16*i); + } + return MP4.box(MP4.types.trun, array); + } + + static initSegment(tracks, duration, timescale) { + if (!MP4.types) { + MP4.init(); + } + var movie = MP4.moov(tracks, duration, timescale), result; + result = new Uint8Array(MP4.FTYP.byteLength + movie.byteLength); + result.set(MP4.FTYP); + result.set(movie, MP4.FTYP.byteLength); + return result; + } +} + +//import {MP4Inspect} from '../iso-bmff/mp4-inspector.js'; + +const LOG_TAG = "mse"; +const Log$1 = getTagged(LOG_TAG); + +class MSEBuffer { + constructor(parent, codec) { + this.mediaSource = parent.mediaSource; + this.players = parent.players; + this.cleaning = false; + this.parent = parent; + this.queue = []; + this.cleanResolvers = []; + this.codec = codec; + this.cleanRanges = []; + + Log$1.debug(`Use codec: ${codec}`); + + this.sourceBuffer = this.mediaSource.addSourceBuffer(codec); + this.eventSource = new EventEmitter(this.sourceBuffer); + + this.eventSource.addEventListener('updatestart', (e)=> { + // this.updating = true; + // Log.debug('update start'); + if (this.cleaning) { + Log$1.debug(`${this.codec} cleaning start`); + } + }); + + this.eventSource.addEventListener('update', (e)=> { + // this.updating = true; + if (this.cleaning) { + Log$1.debug(`${this.codec} cleaning update`); + } + }); + + this.eventSource.addEventListener('updateend', (e)=> { + // Log.debug('update end'); + // this.updating = false; + if (this.cleaning) { + Log$1.debug(`${this.codec} cleaning end`); + + try { + if (this.sourceBuffer.buffered.length && this.players[0].currentTime < this.sourceBuffer.buffered.start(0)) { + this.players[0].currentTime = this.sourceBuffer.buffered.start(0); + } + } catch (e) { + // TODO: do something? + } + while (this.cleanResolvers.length) { + let resolver = this.cleanResolvers.shift(); + resolver(); + } + this.cleaning = false; + + if (this.cleanRanges.length) { + this.doCleanup(); + return; + } + } else { + // Log.debug(`buffered: ${this.sourceBuffer.buffered.end(0)}, current ${this.players[0].currentTime}`); + } + this.feedNext(); + }); + + this.eventSource.addEventListener('error', (e)=> { + Log$1.debug(`Source buffer error: ${this.mediaSource.readyState}`); + if (this.mediaSource.sourceBuffers.length) { + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } + this.parent.eventSource.dispatchEvent('error'); + }); + + this.eventSource.addEventListener('abort', (e)=> { + Log$1.debug(`Source buffer aborted: ${this.mediaSource.readyState}`); + if (this.mediaSource.sourceBuffers.length) { + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } + this.parent.eventSource.dispatchEvent('error'); + }); + + if (!this.sourceBuffer.updating) { + this.feedNext(); + } + // TODO: cleanup every hour for live streams + } + + destroy() { + this.eventSource.destroy(); + this.clear(); + this.queue = []; + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } + + clear() { + this.queue = []; + let promises = []; + for (let i=0; i< this.sourceBuffer.buffered.length; ++i) { + // TODO: await remove + this.cleaning = true; + promises.push(new Promise((resolve, reject)=>{ + this.cleanResolvers.push(resolve); + if (!this.sourceBuffer.updating) { + this.sourceBuffer.remove(this.sourceBuffer.buffered.start(i), this.sourceBuffer.buffered.end(i)); + resolve(); + } else { + this.sourceBuffer.onupdateend = () => { + if (this.sourceBuffer) { + this.sourceBuffer.remove(this.sourceBuffer.buffered.start(i), this.sourceBuffer.buffered.end(i)); + } + resolve(); + }; + } + })); + } + return Promise.all(promises); + } + + setLive(is_live) { + this.is_live = is_live; + } + + feedNext() { + // Log.debug("feed next ", this.sourceBuffer.updating); + if (!this.sourceBuffer.updating && !this.cleaning && this.queue.length) { + this.doAppend(this.queue.shift()); + // TODO: if is live and current position > 1hr => clean all and restart + } + } + + doCleanup() { + if (!this.cleanRanges.length) { + this.cleaning = false; + this.feedNext(); + return; + } + let range = this.cleanRanges.shift(); + Log$1.debug(`${this.codec} remove range [${range[0]} - ${range[1]}). + \nUpdating: ${this.sourceBuffer.updating} + `); + this.cleaning = true; + this.sourceBuffer.remove(range[0], range[1]); + } + + initCleanup() { + if (this.sourceBuffer.buffered.length && !this.sourceBuffer.updating && !this.cleaning) { + Log$1.debug(`${this.codec} cleanup`); + let removeBound = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length-1) - 2; + + for (let i=0; i< this.sourceBuffer.buffered.length; ++i) { + let removeStart = this.sourceBuffer.buffered.start(i); + let removeEnd = this.sourceBuffer.buffered.end(i); + if ((this.players[0].currentTime <= removeStart) || (removeBound <= removeStart)) continue; + + if ((removeBound <= removeEnd) && (removeBound >= removeStart)) { + Log$1.debug(`Clear [${removeStart}, ${removeBound}), leave [${removeBound}, ${removeEnd}]`); + removeEnd = removeBound; + if (removeEnd!=removeStart) { + this.cleanRanges.push([removeStart, removeEnd]); + } + continue; // Do not cleanup buffered range after current position + } + this.cleanRanges.push([removeStart, removeEnd]); + } + + this.doCleanup(); + + // let bufferStart = this.sourceBuffer.buffered.start(0); + // let removeEnd = this.sourceBuffer.buffered.start(0) + (this.sourceBuffer.buffered.end(0) - this.sourceBuffer.buffered.start(0))/2; + // if (this.players[0].currentTime < removeEnd) { + // this.players[0].currentTime = removeEnd; + // } + // let removeEnd = Math.max(this.players[0].currentTime - 3, this.sourceBuffer.buffered.end(0) - 3); + // + // if (removeEnd < bufferStart) { + // removeEnd = this.sourceBuffer.buffered.start(0) + (this.sourceBuffer.buffered.end(0) - this.sourceBuffer.buffered.start(0))/2; + // if (this.players[0].currentTime < removeEnd) { + // this.players[0].currentTime = removeEnd; + // } + // } + + // if (removeEnd > bufferStart && (removeEnd - bufferStart > 0.5 )) { + // // try { + // Log.debug(`${this.codec} remove range [${bufferStart} - ${removeEnd}). + // \nBuffered end: ${this.sourceBuffer.buffered.end(0)} + // \nUpdating: ${this.sourceBuffer.updating} + // `); + // this.cleaning = true; + // this.sourceBuffer.remove(bufferStart, removeEnd); + // // } catch (e) { + // // // TODO: implement + // // Log.error(e); + // // } + // } else { + // this.feedNext(); + // } + } else { + this.feedNext(); + } + } + + doAppend(data) { + // console.log(MP4Inspect.mp4toJSON(data)); + let err = this.players[0].error; + if (err) { + Log$1.error(`Error occured: ${MSE.ErrorNotes[err.code]}`); + try { + this.players.forEach((video)=>{video.stop();}); + this.mediaSource.endOfStream(); + } catch (e){ + + } + this.parent.eventSource.dispatchEvent('error'); + } else { + try { + this.sourceBuffer.appendBuffer(data); + } catch (e) { + if (e.name === 'QuotaExceededError') { + Log$1.debug(`${this.codec} quota fail`); + this.queue.unshift(data); + this.initCleanup(); + return; + } + + // reconnect on fail + Log$1.error(`Error occured while appending buffer. ${e.name}: ${e.message}`); + this.parent.eventSource.dispatchEvent('error'); + } + } + + } + + feed(data) { + this.queue = this.queue.concat(data); + // Log.debug(this.sourceBuffer.updating, this.updating, this.queue.length); + if (this.sourceBuffer && !this.sourceBuffer.updating && !this.cleaning) { + // Log.debug('enq feed'); + this.feedNext(); + } + } +} + +class MSE { + // static CODEC_AVC_BASELINE = "avc1.42E01E"; + // static CODEC_AVC_MAIN = "avc1.4D401E"; + // static CODEC_AVC_HIGH = "avc1.64001E"; + // static CODEC_VP8 = "vp8"; + // static CODEC_AAC = "mp4a.40.2"; + // static CODEC_VORBIS = "vorbis"; + // static CODEC_THEORA = "theora"; + + static get ErrorNotes() {return { + [MediaError.MEDIA_ERR_ABORTED]: 'fetching process aborted by user', + [MediaError.MEDIA_ERR_NETWORK]: 'error occurred when downloading', + [MediaError.MEDIA_ERR_DECODE]: 'error occurred when decoding', + [MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED]: 'audio/video not supported' + }}; + + static isSupported(codecs) { + return (window.MediaSource && window.MediaSource.isTypeSupported(`video/mp4; codecs="${codecs.join(',')}"`)); + } + + constructor (players) { + this.players = players; + const playing = this.players.map((video, idx) => { + video.onplaying = function () { + playing[idx] = true; + }; + video.onpause = function () { + playing[idx] = false; + }; + return !video.paused; + }); + this.playing = playing; + this.mediaSource = new MediaSource(); + this.eventSource = new EventEmitter(this.mediaSource); + this.reset(); + } + + destroy() { + this.reset(); + this.eventSource.destroy(); + this.mediaSource = null; + this.eventSource = null; + } + + play() { + this.players.forEach((video, idx)=>{ + if (video.paused && !this.playing[idx]) { + Log$1.debug(`player ${idx}: play`); + video.play(); + } + }); + } + + setLive(is_live) { + for (let idx in this.buffers) { + this.buffers[idx].setLive(is_live); + } + this.is_live = is_live; + } + + resetBuffers() { + this.players.forEach((video, idx)=>{ + if (!video.paused && this.playing[idx]) { + video.pause(); + video.currentTime = 0; + } + }); + + let promises = []; + for (let buffer of this.buffers.values()) { + promises.push(buffer.clear()); + } + return Promise.all(promises).then(()=>{ + this.mediaSource.endOfStream(); + this.mediaSource.duration = 0; + this.mediaSource.clearLiveSeekableRange(); + this.play(); + }); + } + + clear() { + this.reset(); + this.players.forEach((video)=>{video.src = URL.createObjectURL(this.mediaSource);}); + + return this.setupEvents(); + } + + setupEvents() { + this.eventSource.clear(); + this.resolved = false; + this.mediaReady = new Promise((resolve, reject)=> { + this._sourceOpen = ()=> { + Log$1.debug(`Media source opened: ${this.mediaSource.readyState}`); + if (!this.resolved) { + this.resolved = true; + resolve(); + } + }; + this._sourceEnded = ()=>{ + Log$1.debug(`Media source ended: ${this.mediaSource.readyState}`); + }; + this._sourceClose = ()=>{ + Log$1.debug(`Media source closed: ${this.mediaSource.readyState}`); + if (this.resolved) { + this.eventSource.dispatchEvent('sourceclosed'); + } + }; + this.eventSource.addEventListener('sourceopen', this._sourceOpen); + this.eventSource.addEventListener('sourceended', this._sourceEnded); + this.eventSource.addEventListener('sourceclose', this._sourceClose); + }); + return this.mediaReady; + } + + reset() { + this.ready = false; + for (let track in this.buffers) { + this.buffers[track].destroy(); + delete this.buffers[track]; + } + if (this.mediaSource.readyState == 'open') { + this.mediaSource.duration = 0; + this.mediaSource.endOfStream(); + } + this.updating = false; + this.resolved = false; + this.buffers = {}; + // this.players.forEach((video)=>{video.src = URL.createObjectURL(this.mediaSource)}); + // TODO: remove event listeners for existing media source + // this.setupEvents(); + // this.clear(); + } + + setCodec(track, mimeCodec) { + return this.mediaReady.then(()=>{ + this.buffers[track] = new MSEBuffer(this, mimeCodec); + this.buffers[track].setLive(this.is_live); + }); + } + + feed(track, data) { + if (this.buffers[track]) { + this.buffers[track].feed(data); + } + } +} + +const Log$2 = getTagged('remuxer:base'); +let track_id = 1; +class BaseRemuxer { + + static get MP4_TIMESCALE() { return 90000;} + + // TODO: move to ts parser + // static PTSNormalize(value, reference) { + // + // let offset; + // if (reference === undefined) { + // return value; + // } + // if (reference < value) { + // // - 2^33 + // offset = -8589934592; + // } else { + // // + 2^33 + // offset = 8589934592; + // } + // /* PTS is 33bit (from 0 to 2^33 -1) + // if diff between value and reference is bigger than half of the amplitude (2^32) then it means that + // PTS looping occured. fill the gap */ + // while (Math.abs(value - reference) > 4294967296) { + // value += offset; + // } + // return value; + // } + + static getTrackID() { + return track_id++; + } + + constructor(timescale, scaleFactor, params) { + this.timeOffset = 0; + this.timescale = timescale; + this.scaleFactor = scaleFactor; + this.readyToDecode = false; + this.samples = []; + this.seq = 1; + this.tsAlign = 1; + } + + scaled(timestamp) { + return timestamp / this.scaleFactor; + } + + unscaled(timestamp) { + return timestamp * this.scaleFactor; + } + + remux(unit) { + if (unit) { + this.samples.push({ + unit: unit, + pts: unit.pts, + dts: unit.dts + }); + return true; + } + return false; + } + + static toMS(timestamp) { + return timestamp/90; + } + + setConfig(config) { + + } + + insertDscontinuity() { + this.samples.push(null); + } + + init(initPTS, initDTS, shouldInitialize=true) { + this.initPTS = Math.min(initPTS, this.samples[0].dts /*- this.unscaled(this.timeOffset)*/); + this.initDTS = Math.min(initDTS, this.samples[0].dts /*- this.unscaled(this.timeOffset)*/); + Log$2.debug(`Initial pts=${this.initPTS} dts=${this.initDTS} offset=${this.unscaled(this.timeOffset)}`); + this.initialized = shouldInitialize; + } + + flush() { + this.seq++; + this.mp4track.len = 0; + this.mp4track.samples = []; + } + + static dtsSortFunc(a,b) { + return (a.dts-b.dts); + } + + getPayloadBase(sampleFunction, setupSample) { + if (!this.readyToDecode || !this.initialized || !this.samples.length) return null; + this.samples.sort(BaseRemuxer.dtsSortFunc); + return true; + // + // let payload = new Uint8Array(this.mp4track.len); + // let offset = 0; + // let samples=this.mp4track.samples; + // let mp4Sample, lastDTS, pts, dts; + // + // while (this.samples.length) { + // let sample = this.samples.shift(); + // if (sample === null) { + // // discontinuity + // this.nextDts = undefined; + // break; + // } + // + // let unit = sample.unit; + // + // pts = Math.round((sample.pts - this.initDTS)/this.tsAlign)*this.tsAlign; + // dts = Math.round((sample.dts - this.initDTS)/this.tsAlign)*this.tsAlign; + // // ensure DTS is not bigger than PTS + // dts = Math.min(pts, dts); + // + // // sampleFunction(pts, dts); // TODO: + // + // // mp4Sample = setupSample(unit, pts, dts); // TODO: + // + // payload.set(unit.getData(), offset); + // offset += unit.getSize(); + // + // samples.push(mp4Sample); + // lastDTS = dts; + // } + // if (!samples.length) return null; + // + // // samplesPostFunction(samples); // TODO: + // + // return new Uint8Array(payload.buffer, 0, this.mp4track.len); + } +} + +const Log$3 = getTagged("remuxer:aac"); +// TODO: asm.js +class AACRemuxer extends BaseRemuxer { + + constructor(timescale, scaleFactor = 1, params={}) { + super(timescale, scaleFactor); + + this.codecstring=MSE.CODEC_AAC; + this.units = []; + this.initDTS = undefined; + this.nextAacPts = undefined; + this.lastPts = 0; + this.firstDTS = 0; + this.firstPTS = 0; + this.duration = params.duration || 1; + this.initialized = false; + + this.mp4track={ + id:BaseRemuxer.getTrackID(), + type: 'audio', + fragmented:true, + channelCount:0, + audiosamplerate: this.timescale, + duration: 0, + timescale: this.timescale, + volume: 1, + samples: [], + config: '', + len: 0 + }; + if (params.config) { + this.setConfig(params.config); + } + } + + setConfig(config) { + this.mp4track.channelCount = config.channels; + this.mp4track.audiosamplerate = config.samplerate; + if (!this.mp4track.duration) { + this.mp4track.duration = (this.duration?this.duration:1)*config.samplerate; + } + this.mp4track.timescale = config.samplerate; + this.mp4track.config = config.config; + this.mp4track.codec = config.codec; + this.timescale = config.samplerate; + this.scaleFactor = BaseRemuxer.MP4_TIMESCALE / config.samplerate; + this.expectedSampleDuration = 1024 * this.scaleFactor; + this.readyToDecode = true; + } + + remux(aac) { + if (super.remux.call(this, aac)) { + this.mp4track.len += aac.getSize(); + } + } + + getPayload() { + if (!this.readyToDecode || !this.samples.length) return null; + this.samples.sort(function(a, b) { + return (a.dts-b.dts); + }); + + let payload = new Uint8Array(this.mp4track.len); + let offset = 0; + let samples=this.mp4track.samples; + let mp4Sample, lastDTS, pts, dts; + + while (this.samples.length) { + let sample = this.samples.shift(); + if (sample === null) { + // discontinuity + this.nextDts = undefined; + break; + } + let unit = sample.unit; + pts = sample.pts - this.initDTS; + dts = sample.dts - this.initDTS; + + if (lastDTS === undefined) { + if (this.nextDts) { + let delta = Math.round(this.scaled(pts - this.nextAacPts)); + // if fragment are contiguous, or delta less than 600ms, ensure there is no overlap/hole between fragments + if (/*contiguous || */Math.abs(delta) < 600) { + // log delta + if (delta) { + if (delta > 0) { + Log$3.log(`${delta} ms hole between AAC samples detected,filling it`); + // if we have frame overlap, overlapping for more than half a frame duraion + } else if (delta < -12) { + // drop overlapping audio frames... browser will deal with it + Log$3.log(`${(-delta)} ms overlapping between AAC samples detected, drop frame`); + this.mp4track.len -= unit.getSize(); + continue; + } + // set DTS to next DTS + pts = dts = this.nextAacPts; + } + } + } + // remember first PTS of our aacSamples, ensure value is positive + this.firstDTS = Math.max(0, dts); + } + + mp4Sample = { + size: unit.getSize(), + cts: 0, + duration:1024, + flags: { + isLeading: 0, + isDependedOn: 0, + hasRedundancy: 0, + degradPrio: 0, + dependsOn: 1 + } + }; + + payload.set(unit.getData(), offset); + offset += unit.getSize(); + samples.push(mp4Sample); + lastDTS = dts; + } + if (!samples.length) return null; + this.nextDts =pts+this.expectedSampleDuration; + return new Uint8Array(payload.buffer, 0, this.mp4track.len); + } +} +//test.bundle.js:42 [remuxer:h264] skip frame from the past at DTS=18397972271140676 with expected DTS=18397998040950484 + +/** + * Parser for exponential Golomb codes, a variable-bitwidth number encoding scheme used by h264. +*/ +// TODO: asm.js +class ExpGolomb { + + constructor(data) { + this.data = data; + // the number of bytes left to examine in this.data + this.bytesAvailable = this.data.byteLength; + // the current word being examined + this.word = 0; // :uint + // the number of bits left to examine in the current word + this.bitsAvailable = 0; // :uint + } + + // ():void + loadWord() { + var + position = this.data.byteLength - this.bytesAvailable, + workingBytes = new Uint8Array(4), + availableBytes = Math.min(4, this.bytesAvailable); + if (availableBytes === 0) { + throw new Error('no bytes available'); + } + workingBytes.set(this.data.subarray(position, position + availableBytes)); + this.word = new DataView(workingBytes.buffer, workingBytes.byteOffset, workingBytes.byteLength).getUint32(0); + // track the amount of this.data that has been processed + this.bitsAvailable = availableBytes * 8; + this.bytesAvailable -= availableBytes; + } + + // (count:int):void + skipBits(count) { + var skipBytes; // :int + if (this.bitsAvailable > count) { + this.word <<= count; + this.bitsAvailable -= count; + } else { + count -= this.bitsAvailable; + skipBytes = count >> 3; + count -= (skipBytes << 3); + this.bytesAvailable -= skipBytes; + this.loadWord(); + this.word <<= count; + this.bitsAvailable -= count; + } + } + + // (size:int):uint + readBits(size) { + var + bits = Math.min(this.bitsAvailable, size), // :uint + valu = this.word >>> (32 - bits); // :uint + if (size > 32) { + Log.error('Cannot read more than 32 bits at a time'); + } + this.bitsAvailable -= bits; + if (this.bitsAvailable > 0) { + this.word <<= bits; + } else if (this.bytesAvailable > 0) { + this.loadWord(); + } + bits = size - bits; + if (bits > 0) { + return valu << bits | this.readBits(bits); + } else { + return valu; + } + } + + // ():uint + skipLZ() { + var leadingZeroCount; // :uint + for (leadingZeroCount = 0; leadingZeroCount < this.bitsAvailable; ++leadingZeroCount) { + if (0 !== (this.word & (0x80000000 >>> leadingZeroCount))) { + // the first bit of working word is 1 + this.word <<= leadingZeroCount; + this.bitsAvailable -= leadingZeroCount; + return leadingZeroCount; + } + } + // we exhausted word and still have not found a 1 + this.loadWord(); + return leadingZeroCount + this.skipLZ(); + } + + // ():void + skipUEG() { + this.skipBits(1 + this.skipLZ()); + } + + // ():void + skipEG() { + this.skipBits(1 + this.skipLZ()); + } + + // ():uint + readUEG() { + var clz = this.skipLZ(); // :uint + return this.readBits(clz + 1) - 1; + } + + // ():int + readEG() { + var valu = this.readUEG(); // :int + if (0x01 & valu) { + // the number is odd if the low order bit is set + return (1 + valu) >>> 1; // add 1 to make it even, and divide by 2 + } else { + return -1 * (valu >>> 1); // divide by two then make it negative + } + } + + // Some convenience functions + // :Boolean + readBoolean() { + return 1 === this.readBits(1); + } + + // ():int + readUByte() { + return this.readBits(8); + } + + // ():int + readUShort() { + return this.readBits(16); + } + // ():int + readUInt() { + return this.readBits(32); + } +} + +// TODO: asm.js + +function appendByteArray(buffer1, buffer2) { + let tmp = new Uint8Array((buffer1.byteLength|0) + (buffer2.byteLength|0)); + tmp.set(buffer1, 0); + tmp.set(buffer2, buffer1.byteLength|0); + return tmp; +} + + +function base64ToArrayBuffer(base64) { + var binary_string = window.atob(base64); + var len = binary_string.length; + var bytes = new Uint8Array( len ); + for (var i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes.buffer; +} + +function hexToByteArray(hex) { + let len = hex.length >> 1; + var bufView = new Uint8Array(len); + for (var i = 0; i < len; i++) { + bufView[i] = parseInt(hex.substr(i<<1,2),16); + } + return bufView; +} + + + +function bitSlice(bytearray, start=0, end=bytearray.byteLength*8) { + let byteLen = Math.ceil((end-start)/8); + let res = new Uint8Array(byteLen); + let startByte = start >>> 3; // /8 + let endByte = (end>>>3) - 1; // /8 + let bitOffset = start & 0x7; // %8 + let nBitOffset = 8 - bitOffset; + let endOffset = 8 - end & 0x7; // %8 + for (let i=0; i> nBitOffset; + if (i == endByte-1 && endOffset < 8) { + tail >>= endOffset; + tail <<= endOffset; + } + } + res[i]=(bytearray[startByte+i]< 0; --i) { + + /* Shift result one left to make room for another bit, + then add the next bit on the stream. */ + result = ((result|0) << 1) | (((this.byte|0) >> (8 - (++this.bitpos))) & 0x01); + if ((this.bitpos|0)>=8) { + this.byte = this.src.getUint8(++this.bytepos); + this.bitpos &= 0x7; + } + } + + return result; + } + skipBits(length) { + this.bitpos += (length|0) & 0x7; // %8 + this.bytepos += (length|0) >>> 3; // *8 + if (this.bitpos > 7) { + this.bitpos &= 0x7; + ++this.bytepos; + } + + if (!this.finished()) { + this.byte = this.src.getUint8(this.bytepos); + return 0; + } else { + return this.bytepos-this.src.byteLength-this.src.bitpos; + } + } + + finished() { + return this.bytepos >= this.src.byteLength; + } +} + +class NALU { + + static get NDR() {return 1;} + static get SLICE_PART_A() {return 2;} + static get SLICE_PART_B() {return 3;} + static get SLICE_PART_C() {return 4;} + static get IDR() {return 5;} + static get SEI() {return 6;} + static get SPS() {return 7;} + static get PPS() {return 8;} + static get DELIMITER() {return 9;} + static get EOSEQ() {return 10;} + static get EOSTR() {return 11;} + static get FILTER() {return 12;} + static get STAP_A() {return 24;} + static get STAP_B() {return 25;} + static get FU_A() {return 28;} + static get FU_B() {return 29;} + + static get TYPES() {return { + [NALU.IDR]: 'IDR', + [NALU.SEI]: 'SEI', + [NALU.SPS]: 'SPS', + [NALU.PPS]: 'PPS', + [NALU.NDR]: 'NDR' + }}; + + static type(nalu) { + if (nalu.ntype in NALU.TYPES) { + return NALU.TYPES[nalu.ntype]; + } else { + return 'UNKNOWN'; + } + } + + constructor(ntype, nri, data, dts, pts) { + + this.data = data; + this.ntype = ntype; + this.nri = nri; + this.dts = dts; + this.pts = pts ? pts : this.dts; + this.sliceType = null; + } + + appendData(idata) { + this.data = appendByteArray(this.data, idata); + } + + toString() { + return `${NALU.type(this)}(${this.data.byteLength}): NRI: ${this.getNri()}, PTS: ${this.pts}, DTS: ${this.dts}`; + } + + getNri() { + return this.nri >> 5; + } + + type() { + return this.ntype; + } + + isKeyframe() { + return this.ntype === NALU.IDR || this.sliceType === 7; + } + + getSize() { + return 4 + 1 + this.data.byteLength; + } + + getData() { + let header = new Uint8Array(5 + this.data.byteLength); + let view = new DataView(header.buffer); + view.setUint32(0, this.data.byteLength + 1); + view.setUint8(4, (0x0 & 0x80) | (this.nri & 0x60) | (this.ntype & 0x1F)); + header.set(this.data, 5); + return header; + } +} + +class H264Parser { + + constructor(remuxer) { + this.remuxer = remuxer; + this.track = remuxer.mp4track; + this.firstFound = false; + } + + msToScaled(timestamp) { + return (timestamp - this.remuxer.timeOffset) * this.remuxer.scaleFactor; + } + + parseSPS(sps) { + var config = H264Parser.readSPS(new Uint8Array(sps)); + + this.track.width = config.width; + this.track.height = config.height; + this.track.sps = [new Uint8Array(sps)]; + // this.track.timescale = this.remuxer.timescale; + // this.track.duration = this.remuxer.timescale; // TODO: extract duration for non-live client + this.track.codec = 'avc1.'; + + let codecarray = new DataView(sps.buffer, sps.byteOffset+1, 4); + for (let i = 0; i < 3; ++i) { + var h = codecarray.getUint8(i).toString(16); + if (h.length < 2) { + h = '0' + h; + } + this.track.codec += h; + } + } + + parsePPS(pps) { + this.track.pps = [new Uint8Array(pps)]; + } + + parseNAL(unit) { + if (!unit) return false; + + let push = null; + // console.log(unit.toString()); + switch (unit.type()) { + case NALU.NDR: + case NALU.IDR: + unit.sliceType = H264Parser.parceSliceHeader(unit.data); + if (unit.isKeyframe() && !this.firstFound) { + this.firstFound = true; + } + if (this.firstFound) { + push = true; + } else { + push = false; + } + break; + case NALU.PPS: + push = false; + if (!this.track.pps) { + this.parsePPS(unit.getData().subarray(4)); + if (!this.remuxer.readyToDecode && this.track.pps && this.track.sps) { + this.remuxer.readyToDecode = true; + } + } + break; + case NALU.SPS: + push = false; + if(!this.track.sps) { + this.parseSPS(unit.getData().subarray(4)); + if (!this.remuxer.readyToDecode && this.track.pps && this.track.sps) { + this.remuxer.readyToDecode = true; + } + } + break; + case NALU.SEI: + push = false; + let data = new DataView(unit.data.buffer, unit.data.byteOffset, unit.data.byteLength); + let byte_idx = 0; + let pay_type = data.getUint8(byte_idx); + ++byte_idx; + let pay_size = 0; + let sz = data.getUint8(byte_idx); + ++byte_idx; + while (sz === 255) { + pay_size+=sz; + sz = data.getUint8(byte_idx); + ++byte_idx; + } + pay_size+=sz; + + let uuid = unit.data.subarray(byte_idx, byte_idx+16); + byte_idx+=16; + console.log(`PT: ${pay_type}, PS: ${pay_size}, UUID: ${Array.from(uuid).map(function(i) { + return ('0' + i.toString(16)).slice(-2); + }).join('')}`); + // debugger; + break; + case NALU.EOSEQ: + case NALU.EOSTR: + push = false; + default: + } + if (push === null && unit.getNri() > 0 ) { + push=true; + } + return push; + } + + static parceSliceHeader(data) { + let decoder = new ExpGolomb(data); + let first_mb = decoder.readUEG(); + let slice_type = decoder.readUEG(); + let ppsid = decoder.readUEG(); + let frame_num = decoder.readUByte(); + // console.log(`first_mb: ${first_mb}, slice_type: ${slice_type}, ppsid: ${ppsid}, frame_num: ${frame_num}`); + return slice_type; + } + + /** + * Advance the ExpGolomb decoder past a scaling list. The scaling + * list is optionally transmitted as part of a sequence parameter + * set and is not relevant to transmuxing. + * @param decoder {ExpGolomb} exp golomb decoder + * @param count {number} the number of entries in this scaling list + * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 + */ + static skipScalingList(decoder, count) { + let lastScale = 8, + nextScale = 8, + deltaScale; + for (let j = 0; j < count; j++) { + if (nextScale !== 0) { + deltaScale = decoder.readEG(); + nextScale = (lastScale + deltaScale + 256) % 256; + } + lastScale = (nextScale === 0) ? lastScale : nextScale; + } + } + + /** + * Read a sequence parameter set and return some interesting video + * properties. A sequence parameter set is the H264 metadata that + * describes the properties of upcoming video frames. + * @param data {Uint8Array} the bytes of a sequence parameter set + * @return {object} an object with configuration parsed from the + * sequence parameter set, including the dimensions of the + * associated video frames. + */ + static readSPS(data) { + let decoder = new ExpGolomb(data); + let frameCropLeftOffset = 0, + frameCropRightOffset = 0, + frameCropTopOffset = 0, + frameCropBottomOffset = 0, + sarScale = 1, + profileIdc,profileCompat,levelIdc, + numRefFramesInPicOrderCntCycle, picWidthInMbsMinus1, + picHeightInMapUnitsMinus1, + frameMbsOnlyFlag, + scalingListCount; + decoder.readUByte(); + profileIdc = decoder.readUByte(); // profile_idc + profileCompat = decoder.readBits(5); // constraint_set[0-4]_flag, u(5) + decoder.skipBits(3); // reserved_zero_3bits u(3), + levelIdc = decoder.readUByte(); //level_idc u(8) + decoder.skipUEG(); // seq_parameter_set_id + // some profiles have more optional data we don't need + if (profileIdc === 100 || + profileIdc === 110 || + profileIdc === 122 || + profileIdc === 244 || + profileIdc === 44 || + profileIdc === 83 || + profileIdc === 86 || + profileIdc === 118 || + profileIdc === 128) { + var chromaFormatIdc = decoder.readUEG(); + if (chromaFormatIdc === 3) { + decoder.skipBits(1); // separate_colour_plane_flag + } + decoder.skipUEG(); // bit_depth_luma_minus8 + decoder.skipUEG(); // bit_depth_chroma_minus8 + decoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag + if (decoder.readBoolean()) { // seq_scaling_matrix_present_flag + scalingListCount = (chromaFormatIdc !== 3) ? 8 : 12; + for (let i = 0; i < scalingListCount; ++i) { + if (decoder.readBoolean()) { // seq_scaling_list_present_flag[ i ] + if (i < 6) { + H264Parser.skipScalingList(decoder, 16); + } else { + H264Parser.skipScalingList(decoder, 64); + } + } + } + } + } + decoder.skipUEG(); // log2_max_frame_num_minus4 + var picOrderCntType = decoder.readUEG(); + if (picOrderCntType === 0) { + decoder.readUEG(); //log2_max_pic_order_cnt_lsb_minus4 + } else if (picOrderCntType === 1) { + decoder.skipBits(1); // delta_pic_order_always_zero_flag + decoder.skipEG(); // offset_for_non_ref_pic + decoder.skipEG(); // offset_for_top_to_bottom_field + numRefFramesInPicOrderCntCycle = decoder.readUEG(); + for(let i = 0; i < numRefFramesInPicOrderCntCycle; ++i) { + decoder.skipEG(); // offset_for_ref_frame[ i ] + } + } + decoder.skipUEG(); // max_num_ref_frames + decoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag + picWidthInMbsMinus1 = decoder.readUEG(); + picHeightInMapUnitsMinus1 = decoder.readUEG(); + frameMbsOnlyFlag = decoder.readBits(1); + if (frameMbsOnlyFlag === 0) { + decoder.skipBits(1); // mb_adaptive_frame_field_flag + } + decoder.skipBits(1); // direct_8x8_inference_flag + if (decoder.readBoolean()) { // frame_cropping_flag + frameCropLeftOffset = decoder.readUEG(); + frameCropRightOffset = decoder.readUEG(); + frameCropTopOffset = decoder.readUEG(); + frameCropBottomOffset = decoder.readUEG(); + } + if (decoder.readBoolean()) { + // vui_parameters_present_flag + if (decoder.readBoolean()) { + // aspect_ratio_info_present_flag + let sarRatio; + const aspectRatioIdc = decoder.readUByte(); + switch (aspectRatioIdc) { + case 1: sarRatio = [1,1]; break; + case 2: sarRatio = [12,11]; break; + case 3: sarRatio = [10,11]; break; + case 4: sarRatio = [16,11]; break; + case 5: sarRatio = [40,33]; break; + case 6: sarRatio = [24,11]; break; + case 7: sarRatio = [20,11]; break; + case 8: sarRatio = [32,11]; break; + case 9: sarRatio = [80,33]; break; + case 10: sarRatio = [18,11]; break; + case 11: sarRatio = [15,11]; break; + case 12: sarRatio = [64,33]; break; + case 13: sarRatio = [160,99]; break; + case 14: sarRatio = [4,3]; break; + case 15: sarRatio = [3,2]; break; + case 16: sarRatio = [2,1]; break; + case 255: { + sarRatio = [decoder.readUByte() << 8 | decoder.readUByte(), decoder.readUByte() << 8 | decoder.readUByte()]; + break; + } + } + if (sarRatio) { + sarScale = sarRatio[0] / sarRatio[1]; + } + } + if (decoder.readBoolean()) {decoder.skipBits(1);} + + if (decoder.readBoolean()) { + decoder.skipBits(4); + if (decoder.readBoolean()) { + decoder.skipBits(24); + } + } + if (decoder.readBoolean()) { + decoder.skipUEG(); + decoder.skipUEG(); + } + if (decoder.readBoolean()) { + let unitsInTick = decoder.readUInt(); + let timeScale = decoder.readUInt(); + let fixedFrameRate = decoder.readBoolean(); + let frameDuration = timeScale/(2*unitsInTick); + console.log(`timescale: ${timeScale}; unitsInTick: ${unitsInTick}; fixedFramerate: ${fixedFrameRate}; avgFrameDuration: ${frameDuration}`); + } + } + return { + width: Math.ceil((((picWidthInMbsMinus1 + 1) * 16) - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale), + height: ((2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16) - ((frameMbsOnlyFlag? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset)) + }; + } + + static readSliceType(decoder) { + // skip NALu type + decoder.readUByte(); + // discard first_mb_in_slice + decoder.readUEG(); + // return slice_type + return decoder.readUEG(); + } +} + +const Log$4 = getTagged("remuxer:h264"); +// TODO: asm.js +class H264Remuxer extends BaseRemuxer { + + constructor(timescale, scaleFactor=1, params={}) { + super(timescale, scaleFactor); + + this.nextDts = undefined; + this.readyToDecode = false; + this.initialized = false; + + this.firstDTS=0; + this.firstPTS=0; + this.lastDTS=undefined; + this.lastSampleDuration = 0; + this.lastDurations = []; + // this.timescale = 90000; + this.tsAlign = Math.round(this.timescale/60); + + this.mp4track={ + id:BaseRemuxer.getTrackID(), + type: 'video', + len:0, + fragmented:true, + sps:'', + pps:'', + width:0, + height:0, + timescale: timescale, + duration: timescale, + samples: [] + }; + this.samples = []; + this.lastGopDTS = -99999999999999; + this.gop=[]; + this.firstUnit = true; + + this.h264 = new H264Parser(this); + + if (params.sps) { + let arr = new Uint8Array(params.sps); + if ((arr[0] & 0x1f) === 7) { + this.setSPS(arr); + } else { + Log$4.warn("bad SPS in SDP"); + } + } + if (params.pps) { + let arr = new Uint8Array(params.pps); + if ((arr[0] & 0x1f) === 8) { + this.setPPS(arr); + } else { + Log$4.warn("bad PPS in SDP"); + } + } + + if (this.mp4track.pps && this.mp4track.sps) { + this.readyToDecode = true; + } + } + + _scaled(timestamp) { + return timestamp >>> this.scaleFactor; + } + + _unscaled(timestamp) { + return timestamp << this.scaleFactor; + } + + setSPS(sps) { + this.h264.parseSPS(sps); + } + + setPPS(pps) { + this.h264.parsePPS(pps); + } + + remux(nalu) { + if (this.lastGopDTS < nalu.dts) { + this.gop.sort(BaseRemuxer.dtsSortFunc); + for (let unit of this.gop) { + // if (this.firstUnit) { + // unit.ntype = 5;//NALU.IDR; + // this.firstUnit = false; + // } + if (super.remux.call(this, unit)) { + this.mp4track.len += unit.getSize(); + } + } + this.gop = []; + this.lastGopDTS = nalu.dts; + } + if (this.h264.parseNAL(nalu)) { + this.gop.push(nalu); + } + } + + getPayload() { + if (!this.getPayloadBase()) { + return null; + } + + let payload = new Uint8Array(this.mp4track.len); + let offset = 0; + let samples=this.mp4track.samples; + let mp4Sample, lastDTS, pts, dts; + + + // Log.debug(this.samples.map((e)=>{ + // return Math.round((e.dts - this.initDTS)); + // })); + + // let minDuration = Number.MAX_SAFE_INTEGER; + while (this.samples.length) { + let sample = this.samples.shift(); + if (sample === null) { + // discontinuity + this.nextDts = undefined; + break; + } + + let unit = sample.unit; + + pts = sample.pts- this.initDTS; // /*Math.round(*/(sample.pts - this.initDTS)/*/this.tsAlign)*this.tsAlign*/; + dts = sample.dts - this.initDTS; ///*Math.round(*/(sample.dts - this.initDTS)/*/this.tsAlign)*this.tsAlign*/; + // ensure DTS is not bigger than PTS + dts = Math.min(pts,dts); + // if not first AVC sample of video track, normalize PTS/DTS with previous sample value + // and ensure that sample duration is positive + if (lastDTS !== undefined) { + let sampleDuration = this.scaled(dts - lastDTS); + // Log.debug(`Sample duration: ${sampleDuration}`); + if (sampleDuration < 0) { + Log$4.log(`invalid AVC sample duration at PTS/DTS: ${pts}/${dts}|lastDTS: ${lastDTS}:${sampleDuration}`); + this.mp4track.len -= unit.getSize(); + continue; + } + // minDuration = Math.min(sampleDuration, minDuration); + this.lastDurations.push(sampleDuration); + if (this.lastDurations.length > 100) { + this.lastDurations.shift(); + } + mp4Sample.duration = sampleDuration; + } else { + if (this.nextDts) { + let delta = dts - this.nextDts; + // if fragment are contiguous, or delta less than 600ms, ensure there is no overlap/hole between fragments + if (/*contiguous ||*/ Math.abs(Math.round(BaseRemuxer.toMS(delta))) < 600) { + + if (delta) { + // set DTS to next DTS + // Log.debug(`Video/PTS/DTS adjusted: ${pts}->${Math.max(pts - delta, this.nextDts)}/${dts}->${this.nextDts},delta:${delta}`); + dts = this.nextDts; + // offset PTS as well, ensure that PTS is smaller or equal than new DTS + pts = Math.max(pts - delta, dts); + } + } else { + if (delta < 0) { + Log$4.log(`skip frame from the past at DTS=${dts} with expected DTS=${this.nextDts}`); + this.mp4track.len -= unit.getSize(); + continue; + } + } + } + // remember first DTS of our avcSamples, ensure value is positive + this.firstDTS = Math.max(0, dts); + } + + mp4Sample = { + size: unit.getSize(), + duration: 0, + cts: this.scaled(pts - dts), + flags: { + isLeading: 0, + isDependedOn: 0, + hasRedundancy: 0, + degradPrio: 0 + } + }; + let flags = mp4Sample.flags; + if (sample.unit.isKeyframe() === true) { + // the current sample is a key frame + flags.dependsOn = 2; + flags.isNonSync = 0; + } else { + flags.dependsOn = 1; + flags.isNonSync = 1; + } + + payload.set(unit.getData(), offset); + offset += unit.getSize(); + + samples.push(mp4Sample); + lastDTS = dts; + } + + if (!samples.length) return null; + + let avgDuration = this.lastDurations.reduce(function(a, b) { return (a|0) + (b|0); }, 0) / (this.lastDurations.length||1)|0; + if (samples.length >= 2) { + this.lastSampleDuration = avgDuration; + mp4Sample.duration = avgDuration; + } else { + mp4Sample.duration = this.lastSampleDuration; + } + + if(samples.length && (!this.nextDts || navigator.userAgent.toLowerCase().indexOf('chrome') > -1)) { + let flags = samples[0].flags; + // chrome workaround, mark first sample as being a Random Access Point to avoid sourcebuffer append issue + // https://code.google.com/p/chromium/issues/detail?id=229412 + flags.dependsOn = 2; + flags.isNonSync = 0; + } + + // next AVC sample DTS should be equal to last sample DTS + last sample duration + this.nextDts = dts + this.unscaled(this.lastSampleDuration); + // Log.debug(`next dts: ${this.nextDts}, last duration: ${this.lastSampleDuration}, last dts: ${dts}`); + + return new Uint8Array(payload.buffer, 0, this.mp4track.len); + } +} + +class PayloadType { + static get H264() {return 1;} + static get AAC() {return 2;} + + static get map() {return { + [PayloadType.H264]: 'video', + [PayloadType.AAC]: 'audio' + }}; + + static get string_map() {return { + H264: PayloadType.H264, + AAC: PayloadType.AAC, + 'MP4A-LATM': PayloadType.AAC, + 'MPEG4-GENERIC': PayloadType.AAC + }} +} + +const LOG_TAG$1 = "remuxer"; +const Log$5 = getTagged(LOG_TAG$1); + +class Remuxer { + static get TrackConverters() {return { + [PayloadType.H264]: H264Remuxer, + [PayloadType.AAC]: AACRemuxer + }}; + + static get TrackScaleFactor() {return { + [PayloadType.H264]: 1,//4, + [PayloadType.AAC]: 0 + }}; + + static get TrackTimescale() {return { + [PayloadType.H264]: 90000,//22500, + [PayloadType.AAC]: 0 + }}; + + constructor(mediaElement) { + this.mse = new MSE([mediaElement]); + this.eventSource = new EventEmitter(); + this.mseEventSource = new EventSourceWrapper(this.mse.eventSource); + this.mse_ready = true; + + this.reset(); + + this.errorListener = this.mseClose.bind(this); + this.closeListener = this.mseClose.bind(this); + + this.eventSource.addEventListener('ready', this.init.bind(this)); + } + + initMSEHandlers() { + this.mseEventSource.on('error', this.errorListener); + this.mseEventSource.on('sourceclosed', this.closeListener); + } + + async reset() { + this.tracks = {}; + this.initialized = false; + this.initSegments = {}; + this.codecs = []; + this.streams = {}; + this.enabled = false; + await this.mse.clear(); + this.initMSEHandlers(); + } + + destroy() { + this.mseEventSource.destroy(); + this.mse.destroy(); + this.mse = null; + + this.detachClient(); + + this.eventSource.destroy(); + } + + onTracks(tracks) { + Log$5.debug(`ontracks: `, tracks.detail); + // store available track types + for (let track of tracks.detail) { + this.tracks[track.type] = new Remuxer.TrackConverters[track.type](Remuxer.TrackTimescale[track.type], Remuxer.TrackScaleFactor[track.type], track.params); + if (track.offset) { + this.tracks[track.type].timeOffset = track.offset; + } + if (track.duration) { + this.tracks[track.type].mp4track.duration = track.duration*(this.tracks[track.type].timescale || Remuxer.TrackTimescale[track.type]); + this.tracks[track.type].duration = track.duration; + } else { + this.tracks[track.type].duration = 1; + } + + // this.tracks[track.type].duration + } + this.mse.setLive(!this.client.seekable); + } + + setTimeOffset(timeOffset, track) { + if (this.tracks[track.type]) { + this.tracks[track.type].timeOffset = timeOffset;///this.tracks[track.type].scaleFactor; + } + } + + init() { + let tracks = []; + this.codecs = []; + let initmse = []; + let initPts = Infinity; + let initDts = Infinity; + for (let track_type in this.tracks) { + let track = this.tracks[track_type]; + if (!MSE.isSupported([track.mp4track.codec])) { + throw new Error(`${track.mp4track.type} codec ${track.mp4track.codec} is not supported`); + } + tracks.push(track.mp4track); + this.codecs.push(track.mp4track.codec); + track.init(initPts, initDts/*, false*/); + // initPts = Math.min(track.initPTS, initPts); + // initDts = Math.min(track.initDTS, initDts); + } + + for (let track_type in this.tracks) { + let track = this.tracks[track_type]; + //track.init(initPts, initDts); + this.initSegments[track_type] = MP4.initSegment([track.mp4track], track.duration*track.timescale, track.timescale); + initmse.push(this.initMSE(track_type, track.mp4track.codec)); + } + this.initialized = true; + return Promise.all(initmse).then(()=>{ + //this.mse.play(); + this.enabled = true; + }); + + } + + initMSE(track_type, codec) { + if (MSE.isSupported(this.codecs)) { + return this.mse.setCodec(track_type, `${PayloadType.map[track_type]}/mp4; codecs="${codec}"`).then(()=>{ + this.mse.feed(track_type, this.initSegments[track_type]); + // this.mse.play(); + // this.enabled = true; + }); + } else { + throw new Error('Codecs are not supported'); + } + } + + mseClose() { + // this.mse.clear(); + this.client.stop(); + this.eventSource.dispatchEvent('stopped'); + } + + flush() { + this.onSamples(); + + if (!this.initialized) { + // Log.debug(`Initialize...`); + if (Object.keys(this.tracks).length) { + for (let track_type in this.tracks) { + if (!this.tracks[track_type].readyToDecode || !this.tracks[track_type].samples.length) return; + Log$5.debug(`Init MSE for track ${this.tracks[track_type].mp4track.type}`); + } + this.eventSource.dispatchEvent('ready'); + } + } else { + + for (let track_type in this.tracks) { + let track = this.tracks[track_type]; + let pay = track.getPayload(); + if (pay && pay.byteLength) { + this.mse.feed(track_type, [MP4.moof(track.seq, track.scaled(track.firstDTS), track.mp4track), MP4.mdat(pay)]); + track.flush(); + } + } + } + } + + onSamples(ev) { + // TODO: check format + // let data = ev.detail; + // if (this.tracks[data.pay] && this.client.sampleQueues[data.pay].length) { + // console.log(`video ${data.units[0].dts}`); + for (let qidx in this.client.sampleQueues) { + let queue = this.client.sampleQueues[qidx]; + while (queue.length) { + let units = queue.shift(); + for (let chunk of units) { + this.tracks[qidx].remux(chunk); + } + } + } + // } + } + + onAudioConfig(ev) { + if (this.tracks[ev.detail.pay]) { + this.tracks[ev.detail.pay].setConfig(ev.detail.config); + } + } + + attachClient(client) { + this.detachClient(); + this.client = client; + this.clientEventSource = new EventSourceWrapper(this.client.eventSource); + this.clientEventSource.on('samples', this.samplesListener); + this.clientEventSource.on('audio_config', this.audioConfigListener); + this.clientEventSource.on('tracks', this.onTracks.bind(this)); + this.clientEventSource.on('flush', this.flush.bind(this)); + this.clientEventSource.on('clear', ()=>{ + this.reset(); + this.mse.clear().then(()=>{ + //this.mse.play(); + this.initMSEHandlers(); + }); + }); + } + + detachClient() { + if (this.client) { + this.clientEventSource.destroy(); + // this.client.eventSource.removeEventListener('samples', this.onSamples.bind(this)); + // this.client.eventSource.removeEventListener('audio_config', this.onAudioConfig.bind(this)); + // // TODO: clear other listeners + // this.client.eventSource.removeEventListener('clear', this._clearListener); + // this.client.eventSource.removeEventListener('tracks', this._tracksListener); + // this.client.eventSource.removeEventListener('flush', this._flushListener); + this.client = null; + } + } +} + +class State { + constructor(name, stateMachine) { + this.stateMachine = stateMachine; + this.transitions = new Set(); + this.name = name; + } + + + activate() { + return Promise.resolve(null); + } + + finishTransition() {} + + failHandler() {} + + deactivate() { + return Promise.resolve(null); + } +} + +class StateMachine { + constructor() { + this.storage = {}; + this.currentState = null; + this.states = new Map(); + } + + addState(name, {activate, finishTransition, deactivate}) { + let state = new State(name, this); + if (activate) state.activate = activate; + if (finishTransition) state.finishTransition = finishTransition; + if (deactivate) state.deactivate = deactivate; + this.states.set(name, state); + return this; + } + + addTransition(fromName, toName){ + if (!this.states.has(fromName)) { + throw ReferenceError(`No such state: ${fromName} while connecting to ${toName}`); + } + if (!this.states.has(toName)) { + throw ReferenceError(`No such state: ${toName} while connecting from ${fromName}`); + } + this.states.get(fromName).transitions.add(toName); + return this; + } + + _promisify(res) { + let promise; + try { + promise = res; + if (!promise.then) { + promise = Promise.resolve(promise); + } + } catch (e) { + promise = Promise.reject(e); + } + return promise; + } + + transitionTo(stateName) { + if (this.currentState == null) { + let state = this.states.get(stateName); + return this._promisify(state.activate.call(this)) + .then((data)=> { + this.currentState = state; + return data; + }).then(state.finishTransition.bind(this)).catch((e)=>{ + state.failHandler(); + throw e; + }); + } + if (this.currentState.name == stateName) return Promise.resolve(); + if (this.currentState.transitions.has(stateName)) { + let state = this.states.get(stateName); + return this._promisify(state.deactivate.call(this)) + .then(state.activate.bind(this)).then((data)=> { + this.currentState = state; + return data; + }).then(state.finishTransition.bind(this)).catch((e)=>{ + state.failHandler(); + throw e; + }); + } else { + return Promise.reject(`No such transition: ${this.currentState.name} to ${stateName}`); + } + } + +} + +// export * from 'bp_statemachine'; + +const Log$6 = getTagged("parser:sdp"); + +class SDPParser { + constructor() { + this.version = -1; + this.origin = null; + this.sessionName = null; + this.timing = null; + this.sessionBlock = {}; + this.media = {}; + this.tracks = {}; + this.mediaMap = {}; + } + + parse(content) { + // Log.debug(content); + return new Promise((resolve, reject) => { + var dataString = content; + var success = true; + var currentMediaBlock = this.sessionBlock; + + // TODO: multiple audio/video tracks + + for (let line of dataString.split("\n")) { + line = line.replace(/\r/, ''); + if (0 === line.length) { + /* Empty row (last row perhaps?), skip to next */ + continue; + } + + switch (line.charAt(0)) { + case 'v': + if (-1 !== this.version) { + Log$6.log('Version present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseVersion(line); + break; + + case 'o': + if (null !== this.origin) { + Log$6.log('Origin present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseOrigin(line); + break; + + case 's': + if (null !== this.sessionName) { + Log$6.log('Session Name present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseSessionName(line); + break; + + case 't': + if (null !== this.timing) { + Log$6.log('Timing present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseTiming(line); + break; + + case 'm': + if (null !== currentMediaBlock && this.sessionBlock !== currentMediaBlock) { + /* Complete previous block and store it */ + this.media[currentMediaBlock.type] = currentMediaBlock; + } + + /* A wild media block appears */ + currentMediaBlock = {}; + currentMediaBlock.rtpmap = {}; + this._parseMediaDescription(line, currentMediaBlock); + break; + + case 'a': + SDPParser._parseAttribute(line, currentMediaBlock); + break; + + default: + Log$6.log('Ignored unknown SDP directive: ' + line); + break; + } + + if (!success) { + reject(); + return; + } + } + + this.media[currentMediaBlock.type] = currentMediaBlock; + + success ? resolve() : reject(); + }); + } + + _parseVersion(line) { + let matches = line.match(/^v=([0-9]+)$/); + if (!matches || !matches.length) { + Log$6.log('\'v=\' (Version) formatted incorrectly: ' + line); + return false; + } + + this.version = matches[1]; + if (0 != this.version) { + Log$6.log('Unsupported SDP version:' + this.version); + return false; + } + + return true; + } + + _parseOrigin(line) { + let matches = line.match(/^o=([^ ]+) (-?[0-9]+) (-?[0-9]+) (IN) (IP4|IP6) ([^ ]+)$/); + if (!matches || !matches.length) { + Log$6.log('\'o=\' (Origin) formatted incorrectly: ' + line); + return false; + } + + this.origin = {}; + this.origin.username = matches[1]; + this.origin.sessionid = matches[2]; + this.origin.sessionversion = matches[3]; + this.origin.nettype = matches[4]; + this.origin.addresstype = matches[5]; + this.origin.unicastaddress = matches[6]; + + return true; + } + + _parseSessionName(line) { + let matches = line.match(/^s=([^\r\n]+)$/); + if (!matches || !matches.length) { + Log$6.log('\'s=\' (Session Name) formatted incorrectly: ' + line); + return false; + } + + this.sessionName = matches[1]; + + return true; + } + + _parseTiming(line) { + let matches = line.match(/^t=([0-9]+) ([0-9]+)$/); + if (!matches || !matches.length) { + Log$6.log('\'t=\' (Timing) formatted incorrectly: ' + line); + return false; + } + + this.timing = {}; + this.timing.start = matches[1]; + this.timing.stop = matches[2]; + + return true; + } + + _parseMediaDescription(line, media) { + let matches = line.match(/^m=([^ ]+) ([^ ]+) ([^ ]+)[ ]/); + if (!matches || !matches.length) { + Log$6.log('\'m=\' (Media) formatted incorrectly: ' + line); + return false; + } + + media.type = matches[1]; + media.port = matches[2]; + media.proto = matches[3]; + media.fmt = line.substr(matches[0].length).split(' ').map(function (fmt, index, array) { + return parseInt(fmt); + }); + + for (let fmt of media.fmt) { + this.mediaMap[fmt] = media; + } + + return true; + } + + static _parseAttribute(line, media) { + if (null === media) { + /* Not in a media block, can't be bothered parsing attributes for session */ + return true; + } + + var matches; + /* Used for some cases of below switch-case */ + var separator = line.indexOf(':'); + var attribute = line.substr(0, (-1 === separator) ? 0x7FFFFFFF : separator); + /* 0x7FF.. is default */ + + switch (attribute) { + case 'a=recvonly': + case 'a=sendrecv': + case 'a=sendonly': + case 'a=inactive': + media.mode = line.substr('a='.length); + break; + case 'a=range': + matches = line.match(/^a=range:\s*([a-zA-Z-]+)=([0-9.]+|now)\s*-\s*([0-9.]*)$/); + media.range = [Number(matches[2] == "now" ? -1 : matches[2]), Number(matches[3]), matches[1]]; + break; + case 'a=control': + media.control = line.substr('a=control:'.length); + break; + + case 'a=rtpmap': + matches = line.match(/^a=rtpmap:(\d+) (.*)$/); + if (null === matches) { + Log$6.log('Could not parse \'rtpmap\' of \'a=\''); + return false; + } + + var payload = parseInt(matches[1]); + media.rtpmap[payload] = {}; + + var attrs = matches[2].split('/'); + media.rtpmap[payload].name = attrs[0].toUpperCase(); + media.rtpmap[payload].clock = attrs[1]; + if (undefined !== attrs[2]) { + media.rtpmap[payload].encparams = attrs[2]; + } + media.ptype = PayloadType.string_map[attrs[0].toUpperCase()]; + + break; + + case 'a=fmtp': + matches = line.match(/^a=fmtp:(\d+) (.*)$/); + if (0 === matches.length) { + Log$6.log('Could not parse \'fmtp\' of \'a=\''); + return false; + } + + media.fmtp = {}; + for (var param of matches[2].split(';')) { + var idx = param.indexOf('='); + media.fmtp[param.substr(0, idx).toLowerCase().trim()] = param.substr(idx + 1).trim(); + } + break; + } + + return true; + } + + getSessionBlock() { + return this.sessionBlock; + } + + hasMedia(mediaType) { + return this.media[mediaType] != undefined; + } + + getMediaBlock(mediaType) { + return this.media[mediaType]; + } + + getMediaBlockByPayloadType(pt) { + // for (var m in this.media) { + // if (-1 !== this.media[m].fmt.indexOf(pt)) { + // return this.media[m]; + // } + // } + return this.mediaMap[pt] || null; + + //ErrorManager.dispatchError(826, [pt], true); + // Log.error(`failed to find media with payload type ${pt}`); + // + // return null; + } + + getMediaBlockList() { + var res = []; + for (var m in this.media) { + res.push(m); + } + + return res; + } +} + +const LOG_TAG$2 = "rtsp:stream"; +const Log$7 = getTagged(LOG_TAG$2); + +class RTSPStream { + + constructor(client, track) { + this.state = null; + this.client = client; + this.track = track; + this.rtpChannel = 1; + + this.stopKeepAlive(); + this.keepaliveInterval = null; + this.keepaliveTime = 30000; + } + + reset() { + this.stopKeepAlive(); + this.client.forgetRTPChannel(this.rtpChannel); + this.client = null; + this.track = null; + } + + start() { + return this.sendSetup();//.then(this.sendPlay.bind(this)); + } + + stop() { + return this.sendTeardown(); + } + + getSetupURL(track) { + let sessionBlock = this.client.sdp.getSessionBlock(); + if (Url.isAbsolute(track.control)) { + return track.control; + } else if (Url.isAbsolute(`${sessionBlock.control}${track.control}`)) { + return `${sessionBlock.control}${track.control}`; + } else if (Url.isAbsolute(`${this.client.contentBase}${track.control}`)) { + /* Should probably check session level control before this */ + return `${this.client.contentBase}${track.control}`; + } + else {//need return default + return track.control; + } + Log$7.error('Can\'t determine track URL from ' + + 'block.control:' + track.control + ', ' + + 'session.control:' + sessionBlock.control + ', and ' + + 'content-base:' + this.client.contentBase); + } + + getControlURL() { + let ctrl = this.client.sdp.getSessionBlock().control; + if (Url.isAbsolute(ctrl)) { + return ctrl; + } else if (!ctrl || '*' === ctrl) { + return this.client.contentBase; + } else { + return `${this.client.contentBase}${ctrl}`; + } + } + + sendKeepalive() { + if (this.client.methods.includes('GET_PARAMETER')) { + return this.client.sendRequest('GET_PARAMETER', this.getSetupURL(this.track), { + 'Session': this.session + }); + } else { + return this.client.sendRequest('OPTIONS', '*'); + } + } + + stopKeepAlive() { + clearInterval(this.keepaliveInterval); + } + + startKeepAlive() { + this.keepaliveInterval = setInterval(() => { + this.sendKeepalive().catch((e) => { + Log$7.error(e); + if (e instanceof RTSPError) { + if (Number(e.data.parsed.code) == 501) { + return; + } + } + this.client.reconnect(); + }); + }, this.keepaliveTime); + } + + sendRequest(_cmd, _params = {}) { + let params = {}; + if (this.session) { + params['Session'] = this.session; + } + Object.assign(params, _params); + return this.client.sendRequest(_cmd, this.getControlURL(), params); + } + + sendSetup() { + this.state = RTSPClientSM.STATE_SETUP; + this.rtpChannel = this.client.interleaveChannelIndex; + let interleavedChannels = this.client.interleaveChannelIndex++ + "-" + this.client.interleaveChannelIndex++; + return this.client.sendRequest('SETUP', this.getSetupURL(this.track), { + 'Transport': `RTP/AVP/TCP;unicast;interleaved=${interleavedChannels}`, + 'Date': new Date().toUTCString() + }).then((_data) => { + this.session = _data.headers['session']; + let transport = _data.headers['transport']; + if (transport) { + let interleaved = transport.match(/interleaved=([0-9]+)-([0-9]+)/)[1]; + if (interleaved) { + this.rtpChannel = Number(interleaved); + } + } + let sessionParamsChunks = this.session.split(';').slice(1); + let sessionParams = {}; + for (let chunk of sessionParamsChunks) { + let kv = chunk.split('='); + sessionParams[kv[0]]=kv[1]; + } + if (sessionParams['timeout']) { + this.keepaliveInterval = Number(sessionParams['timeout']) * 500; // * 1000 / 2 + } + /*if (!/RTP\/AVP\/TCP;unicast;interleaved=/.test(_data.headers["transport"])) { + // TODO: disconnect stream and notify client + throw new Error("Connection broken"); + }*/ + this.client.useRTPChannel(this.rtpChannel); + this.startKeepAlive(); + return {track: this.track, data: _data}; + }); + } +} + +/* + * JavaScript MD5 + * https://github.com/blueimp/JavaScript-MD5 + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + * + * Based on + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + + +/* +* Add integers, wrapping at 2^32. This uses 16-bit operations internally +* to work around bugs in some JS interpreters. +*/ +function safeAdd(x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF) +} + +/* +* Bitwise rotate a 32-bit number to the left. +*/ +function bitRotateLeft(num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)) +} + +/* +* These functions implement the four basic operations the algorithm uses. +*/ +function md5cmn(q, a, b, x, s, t) { + return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b) +} +function md5ff(a, b, c, d, x, s, t) { + return md5cmn((b & c) | ((~b) & d), a, b, x, s, t) +} +function md5gg(a, b, c, d, x, s, t) { + return md5cmn((b & d) | (c & (~d)), a, b, x, s, t) +} +function md5hh(a, b, c, d, x, s, t) { + return md5cmn(b ^ c ^ d, a, b, x, s, t) +} +function md5ii(a, b, c, d, x, s, t) { + return md5cmn(c ^ (b | (~d)), a, b, x, s, t) +} + +/* +* Calculate the MD5 of an array of little-endian words, and a bit length. +*/ +function binlMD5(x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << (len % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var i; + var olda; + var oldb; + var oldc; + var oldd; + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + for (i = 0; i < x.length; i += 16) { + olda = a; + oldb = b; + oldc = c; + oldd = d; + + a = md5ff(a, b, c, d, x[i], 7, -680876936); + d = md5ff(d, a, b, c, x[i + 1], 12, -389564586); + c = md5ff(c, d, a, b, x[i + 2], 17, 606105819); + b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330); + a = md5ff(a, b, c, d, x[i + 4], 7, -176418897); + d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426); + c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341); + b = md5ff(b, c, d, a, x[i + 7], 22, -45705983); + a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416); + d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417); + c = md5ff(c, d, a, b, x[i + 10], 17, -42063); + b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162); + a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682); + d = md5ff(d, a, b, c, x[i + 13], 12, -40341101); + c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290); + b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329); + + a = md5gg(a, b, c, d, x[i + 1], 5, -165796510); + d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632); + c = md5gg(c, d, a, b, x[i + 11], 14, 643717713); + b = md5gg(b, c, d, a, x[i], 20, -373897302); + a = md5gg(a, b, c, d, x[i + 5], 5, -701558691); + d = md5gg(d, a, b, c, x[i + 10], 9, 38016083); + c = md5gg(c, d, a, b, x[i + 15], 14, -660478335); + b = md5gg(b, c, d, a, x[i + 4], 20, -405537848); + a = md5gg(a, b, c, d, x[i + 9], 5, 568446438); + d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690); + c = md5gg(c, d, a, b, x[i + 3], 14, -187363961); + b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501); + a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467); + d = md5gg(d, a, b, c, x[i + 2], 9, -51403784); + c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473); + b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734); + + a = md5hh(a, b, c, d, x[i + 5], 4, -378558); + d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463); + c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562); + b = md5hh(b, c, d, a, x[i + 14], 23, -35309556); + a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060); + d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353); + c = md5hh(c, d, a, b, x[i + 7], 16, -155497632); + b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640); + a = md5hh(a, b, c, d, x[i + 13], 4, 681279174); + d = md5hh(d, a, b, c, x[i], 11, -358537222); + c = md5hh(c, d, a, b, x[i + 3], 16, -722521979); + b = md5hh(b, c, d, a, x[i + 6], 23, 76029189); + a = md5hh(a, b, c, d, x[i + 9], 4, -640364487); + d = md5hh(d, a, b, c, x[i + 12], 11, -421815835); + c = md5hh(c, d, a, b, x[i + 15], 16, 530742520); + b = md5hh(b, c, d, a, x[i + 2], 23, -995338651); + + a = md5ii(a, b, c, d, x[i], 6, -198630844); + d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415); + c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905); + b = md5ii(b, c, d, a, x[i + 5], 21, -57434055); + a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571); + d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606); + c = md5ii(c, d, a, b, x[i + 10], 15, -1051523); + b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799); + a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359); + d = md5ii(d, a, b, c, x[i + 15], 10, -30611744); + c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380); + b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649); + a = md5ii(a, b, c, d, x[i + 4], 6, -145523070); + d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379); + c = md5ii(c, d, a, b, x[i + 2], 15, 718787259); + b = md5ii(b, c, d, a, x[i + 9], 21, -343485551); + + a = safeAdd(a, olda); + b = safeAdd(b, oldb); + c = safeAdd(c, oldc); + d = safeAdd(d, oldd); + } + return [a, b, c, d] +} + +/* +* Convert an array of little-endian words to a string +*/ +function binl2rstr(input) { + var i; + var output = ''; + var length32 = input.length * 32; + for (i = 0; i < length32; i += 8) { + output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF); + } + return output +} + +/* +* Convert a raw string to an array of little-endian words +* Characters >255 have their high-byte silently ignored. +*/ +function rstr2binl(input) { + var i; + var output = []; + output[(input.length >> 2) - 1] = undefined; + for (i = 0; i < output.length; i += 1) { + output[i] = 0; + } + var length8 = input.length * 8; + for (i = 0; i < length8; i += 8) { + output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32); + } + return output +} + +/* +* Calculate the MD5 of a raw string +*/ +function rstrMD5(s) { + return binl2rstr(binlMD5(rstr2binl(s), s.length * 8)) +} + +/* +* Calculate the HMAC-MD5, of a key and some data (raw strings) +*/ +function rstrHMACMD5(key, data) { + var i; + var bkey = rstr2binl(key); + var ipad = []; + var opad = []; + var hash; + ipad[15] = opad[15] = undefined; + if (bkey.length > 16) { + bkey = binlMD5(bkey, key.length * 8); + } + for (i = 0; i < 16; i += 1) { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + hash = binlMD5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); + return binl2rstr(binlMD5(opad.concat(hash), 512 + 128)) +} + +/* +* Convert a raw string to a hex string +*/ +function rstr2hex(input) { + var hexTab = '0123456789abcdef'; + var output = ''; + var x; + var i; + for (i = 0; i < input.length; i += 1) { + x = input.charCodeAt(i); + output += hexTab.charAt((x >>> 4) & 0x0F) + + hexTab.charAt(x & 0x0F); + } + return output +} + +/* +* Encode a string as utf-8 +*/ +function str2rstrUTF8(input) { + return unescape(encodeURIComponent(input)) +} + +/* +* Take string arguments and return either raw or hex encoded strings +*/ +function rawMD5(s) { + return rstrMD5(str2rstrUTF8(s)) +} +function hexMD5(s) { + return rstr2hex(rawMD5(s)) +} +function rawHMACMD5(k, d) { + return rstrHMACMD5(str2rstrUTF8(k), str2rstrUTF8(d)) +} +function hexHMACMD5(k, d) { + return rstr2hex(rawHMACMD5(k, d)) +} + +function md5(string, key, raw) { + if (!key) { + if (!raw) { + return hexMD5(string) + } + return rawMD5(string) + } + if (!raw) { + return hexHMACMD5(key, string) + } + return rawHMACMD5(key, string) +} + +// TODO: asm.js +class RTP { + constructor(pkt/*uint8array*/, sdp) { + let bytes = new DataView(pkt.buffer, pkt.byteOffset, pkt.byteLength); + + this.version = bytes.getUint8(0) >>> 6; + this.padding = bytes.getUint8(0) & 0x20 >>> 5; + this.has_extension = bytes.getUint8(0) & 0x10 >>> 4; + this.csrc = bytes.getUint8(0) & 0x0F; + this.marker = bytes.getUint8(1) >>> 7; + this.pt = bytes.getUint8(1) & 0x7F; + this.sequence = bytes.getUint16(2) ; + this.timestamp = bytes.getUint32(4); + this.ssrc = bytes.getUint32(8); + this.csrcs = []; + + let pktIndex=12; + if (this.csrc>0) { + this.csrcs.push(bytes.getUint32(pktIndex)); + pktIndex+=4; + } + if (this.has_extension==1) { + this.extension = bytes.getUint16(pktIndex); + this.ehl = bytes.getUint16(pktIndex+2); + pktIndex+=4; + this.header_data = pkt.slice(pktIndex, this.ehl); + pktIndex += this.ehl; + } + + this.headerLength = pktIndex; + let padLength = 0; + if (this.padding) { + padLength = bytes.getUint8(pkt.byteLength-1); + } + + // this.bodyLength = pkt.byteLength-this.headerLength-padLength; + + this.media = sdp.getMediaBlockByPayloadType(this.pt); + if (null === this.media) { + Log.log(`Media description for payload type: ${this.pt} not provided.`); + } else { + this.type = this.media.ptype;//PayloadType.string_map[this.media.rtpmap[this.media.fmt[0]].name]; + } + + this.data = pkt.subarray(pktIndex); + // this.timestamp = 1000 * (this.timestamp / this.media.rtpmap[this.pt].clock); + // console.log(this); + } + getPayload() { + return this.data; + } + + getTimestampMS() { + return this.timestamp; //1000 * (this.timestamp / this.media.rtpmap[this.pt].clock); + } + + toString() { + return "RTP(" + + "version:" + this.version + ", " + + "padding:" + this.padding + ", " + + "has_extension:" + this.has_extension + ", " + + "csrc:" + this.csrc + ", " + + "marker:" + this.marker + ", " + + "pt:" + this.pt + ", " + + "sequence:" + this.sequence + ", " + + "timestamp:" + this.timestamp + ", " + + "ssrc:" + this.ssrc + ")"; + } + + isVideo(){return this.media.type == 'video';} + isAudio(){return this.media.type == 'audio';} + + +} + +class RTPFactory { + constructor(sdp) { + this.tsOffsets={}; + for (let pay in sdp.media) { + for (let pt of sdp.media[pay].fmt) { + this.tsOffsets[pt] = {last: 0, overflow: 0}; + } + } + } + + build(pkt/*uint8array*/, sdp) { + let rtp = new RTP(pkt, sdp); + + let tsOffset = this.tsOffsets[rtp.pt]; + if (tsOffset) { + rtp.timestamp += tsOffset.overflow; + if (tsOffset.last && Math.abs(rtp.timestamp - tsOffset.last) > 0x7fffffff) { + console.log(`\nlast ts: ${tsOffset.last}\n + new ts: ${rtp.timestamp}\n + new ts adjusted: ${rtp.timestamp+0xffffffff}\n + last overflow: ${tsOffset.overflow}\n + new overflow: ${tsOffset.overflow+0xffffffff}\n + `); + tsOffset.overflow += 0xffffffff; + rtp.timestamp += 0xffffffff; + } + /*if (rtp.timestamp>0xffffffff) { + console.log(`ts: ${rtp.timestamp}, seq: ${rtp.sequence}`); + }*/ + tsOffset.last = rtp.timestamp; + } + + return rtp; + } +} + +class RTSPMessage { + static get RTSP_1_0() {return "RTSP/1.0";} + + constructor(_rtsp_version) { + this.version = _rtsp_version; + } + + build(_cmd, _host, _params={}, _payload=null) { + let requestString = `${_cmd} ${_host} ${this.version}\r\n`; + for (let param in _params) { + requestString+=`${param}: ${_params[param]}\r\n`; + } + // TODO: binary payload + if (_payload) { + requestString+=`Content-Length: ${_payload.length}\r\n`; + } + requestString+='\r\n'; + if (_payload) { + requestString+=_payload; + } + return requestString; + } + + parse(_data) { + let lines = _data.split('\r\n'); + let parsed = { + headers:{}, + body:null, + code: 0, + statusLine: '' + }; + + let match; + [match, parsed.code, parsed.statusLine] = lines[0].match(new RegExp(`${this.version}[ ]+([0-9]{3})[ ]+(.*)`)); + parsed.code = Number(parsed.code); + let lineIdx = 1; + + while (lines[lineIdx]) { + let [k,v] = lines[lineIdx].split(/:(.+)/); + parsed.headers[k.toLowerCase()] = v.trim(); + lineIdx++; + } + + parsed.body = lines.slice(lineIdx).join('\n\r'); + + return parsed; + } + +} + +const MessageBuilder = new RTSPMessage(RTSPMessage.RTSP_1_0); + +// TODO: asm.js +class NALUAsm { + + constructor() { + this.fragmented_nalu = null; + } + + + static parseNALHeader(hdr) { + return { + nri: hdr & 0x60, + type: hdr & 0x1F + } + } + + parseSingleNALUPacket(rawData, header, dts, pts) { + return new NALU(header.type, header.nri, rawData.subarray(0), dts, pts); + } + + parseAggregationPacket(rawData, header, dts, pts) { + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + let nal_start_idx = 0; + let don = null; + if (NALU.STAP_B === header.type) { + don = data.getUint16(nal_start_idx); + nal_start_idx += 2; + } + let ret = []; + while (nal_start_idx < data.byteLength) { + let size = data.getUint16(nal_start_idx); + nal_start_idx += 2; + let header = NALUAsm.parseNALHeader(data.getInt8(nal_start_idx)); + nal_start_idx++; + let nalu = this.parseSingleNALUPacket(rawData.subarray(nal_start_idx, nal_start_idx+size), header, dts, pts); + if (nalu !== null) { + ret.push(nalu); + } + nal_start_idx+=size; + } + return ret; + } + + parseFragmentationUnit(rawData, header, dts, pts) { + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + let nal_start_idx = 0; + let fu_header = data.getUint8(nal_start_idx); + let is_start = (fu_header & 0x80) >>> 7; + let is_end = (fu_header & 0x40) >>> 6; + let payload_type = fu_header & 0x1F; + let ret = null; + + nal_start_idx++; + let don = 0; + if (NALU.FU_B === header.type) { + don = data.getUint16(nal_start_idx); + nal_start_idx += 2; + } + + if (is_start) { + this.fragmented_nalu = new NALU(payload_type, header.nri, rawData.subarray(nal_start_idx), dts, pts); + } + if (this.fragmented_nalu && this.fragmented_nalu.ntype === payload_type) { + if (!is_start) { + this.fragmented_nalu.appendData(rawData.subarray(nal_start_idx)); + } + if (is_end) { + ret = this.fragmented_nalu; + this.fragmented_nalu = null; + return ret; + } + } + return null; + } + + onNALUFragment(rawData, dts, pts) { + + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + + let header = NALUAsm.parseNALHeader(data.getUint8(0)); + + let nal_start_idx = 1; + + let unit = null; + if (header.type > 0 && header.type < 24) { + unit = this.parseSingleNALUPacket(rawData.subarray(nal_start_idx), header, dts, pts); + } else if (NALU.FU_A === header.type || NALU.FU_B === header.type) { + unit = this.parseFragmentationUnit(rawData.subarray(nal_start_idx), header, dts, pts); + } else if (NALU.STAP_A === header.type || NALU.STAP_B === header.type) { + return this.parseAggregationPacket(rawData.subarray(nal_start_idx), header, dts, pts); + } else { + /* 30 - 31 is undefined, ignore those (RFC3984). */ + Log.log('Undefined NAL unit, type: ' + header.type); + return null; + } + if (unit) { + return [unit]; + } + return null; + } +} + +class AACFrame { + + constructor(data, dts, pts) { + this.dts = dts; + this.pts = pts ? pts : this.dts; + + this.data=data;//.subarray(offset); + } + + getData() { + return this.data; + } + + getSize() { + return this.data.byteLength; + } +} + +// import {AACParser} from "../parsers/aac.js"; +// TODO: asm.js +class AACAsm { + constructor() { + this.config = null; + } + + onAACFragment(pkt) { + let rawData = pkt.getPayload(); + if (!pkt.media) { + return null; + } + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + + let sizeLength = Number(pkt.media.fmtp['sizelength'] || 0); + let indexLength = Number(pkt.media.fmtp['indexlength'] || 0); + let indexDeltaLength = Number(pkt.media.fmtp['indexdeltalength'] || 0); + let CTSDeltaLength = Number(pkt.media.fmtp['ctsdeltalength'] || 0); + let DTSDeltaLength = Number(pkt.media.fmtp['dtsdeltalength'] || 0); + let RandomAccessIndication = Number(pkt.media.fmtp['randomaccessindication'] || 0); + let StreamStateIndication = Number(pkt.media.fmtp['streamstateindication'] || 0); + let AuxiliaryDataSizeLength = Number(pkt.media.fmtp['auxiliarydatasizelength'] || 0); + + let configHeaderLength = + sizeLength + Math.max(indexLength, indexDeltaLength) + CTSDeltaLength + DTSDeltaLength + + RandomAccessIndication + StreamStateIndication + AuxiliaryDataSizeLength; + + + let auHeadersLengthPadded = 0; + let offset = 0; + let ts = (Math.round(pkt.getTimestampMS()/1024) << 10) * 90000 / this.config.samplerate; + if (0 !== configHeaderLength) { + /* The AU header section is not empty, read it from payload */ + let auHeadersLengthInBits = data.getUint16(0); // Always 2 octets, without padding + auHeadersLengthPadded = 2 + (auHeadersLengthInBits>>>3) + ((auHeadersLengthInBits & 0x7)?1:0); // Add padding + + //this.config = AACParser.parseAudioSpecificConfig(new Uint8Array(rawData, 0 , auHeadersLengthPadded)); + // TODO: parse config + let frames = []; + let frameOffset=0; + let bits = new BitArray(rawData.subarray(2 + offset)); + let cts = 0; + let dts = 0; + for (let offset=0; offset{ + if (this.connected) { + while (this.transport.dataQueue.length) { + this.onData(this.transport.dataQueue.pop()); + } + } + }; + this._onConnect = this.onConnected.bind(this); + this._onDisconnect = this.onDisconnected.bind(this); + } + + static streamType() { + return null; + } + + destroy() { + this.detachTransport(); + } + + attachTransport(transport) { + if (this.transport) { + this.detachTransport(); + } + this.transport = transport; + this.transport.eventSource.addEventListener('data', this._onData); + this.transport.eventSource.addEventListener('connected', this._onConnect); + this.transport.eventSource.addEventListener('disconnected', this._onDisconnect); + } + + detachTransport() { + if (this.transport) { + this.transport.eventSource.removeEventListener('data', this._onData); + this.transport.eventSource.removeEventListener('connected', this._onConnect); + this.transport.eventSource.removeEventListener('disconnected', this._onDisconnect); + this.transport = null; + } + } + reset() { + + } + + start() { + Log.log('Client started'); + this.paused = false; + // this.startStreamFlush(); + } + + stop() { + Log.log('Client paused'); + this.paused = true; + // this.stopStreamFlush(); + } + + seek(timeOffset) { + + } + + setSource(source) { + this.stop(); + this.endpoint = source; + this.sourceUrl = source.urlpath; + } + + startStreamFlush() { + this.flushInterval = setInterval(()=>{ + if (!this.paused) { + this.eventSource.dispatchEvent('flush'); + } + }, this.options.flush); + } + + stopStreamFlush() { + clearInterval(this.flushInterval); + } + + onData(data) { + + } + + onConnected() { + if (!this.seekable) { + this.transport.dataQueue = []; + this.eventSource.dispatchEvent('clear'); + } + this.connected = true; + } + + onDisconnected() { + this.connected = false; + } + + queryCredentials() { + return Promise.resolve(); + } + + setCredentials(user, password) { + this.endpoint.user = user; + this.endpoint.pass = password; + this.endpoint.auth = `${user}:${password}`; + } +} + +class AACParser { + static get SampleRates() {return [ + 96000, 88200, + 64000, 48000, + 44100, 32000, + 24000, 22050, + 16000, 12000, + 11025, 8000, + 7350];} + + // static Profile = [ + // 0: Null + // 1: AAC Main + // 2: AAC LC (Low Complexity) + // 3: AAC SSR (Scalable Sample Rate) + // 4: AAC LTP (Long Term Prediction) + // 5: SBR (Spectral Band Replication) + // 6: AAC Scalable + // ] + + static parseAudioSpecificConfig(bytesOrBits) { + let config; + if (bytesOrBits.byteLength) { // is byteArray + config = new BitArray(bytesOrBits); + } else { + config = bytesOrBits; + } + + let bitpos = config.bitpos+(config.src.byteOffset+config.bytepos)*8; + let prof = config.readBits(5); + this.codec = `mp4a.40.${prof}`; + let sfi = config.readBits(4); + if (sfi == 0xf) config.skipBits(24); + let channels = config.readBits(4); + + return { + config: bitSlice(new Uint8Array(config.src.buffer), bitpos, bitpos+16), + codec: `mp4a.40.${prof}`, + samplerate: AACParser.SampleRates[sfi], + channels: channels + } + } + + static parseStreamMuxConfig(bytes) { + // ISO_IEC_14496-3 Part 3 Audio. StreamMuxConfig + let config = new BitArray(bytes); + + if (!config.readBits(1)) { + config.skipBits(14); + return AACParser.parseAudioSpecificConfig(config); + } + } +} + +const LOG_TAG$3 = "rtsp:session"; +const Log$8 = getTagged(LOG_TAG$3); + +class RTSPSession { + + constructor(client, sessionId) { + this.state = null; + this.client = client; + this.sessionId = sessionId; + this.url = this.getControlURL(); + } + + reset() { + this.client = null; + } + + start() { + return this.sendPlay(); + } + + stop() { + return this.sendTeardown(); + } + + getControlURL() { + let ctrl = this.client.sdp.getSessionBlock().control; + if (Url.isAbsolute(ctrl)) { + return ctrl; + } else if (!ctrl || '*' === ctrl) { + return this.client.contentBase; + } else { + return `${this.client.contentBase}${ctrl}`; + } + } + + sendRequest(_cmd, _params = {}) { + let params = {}; + if (this.sessionId) { + params['Session'] = this.sessionId; + } + Object.assign(params, _params); + return this.client.sendRequest(_cmd, this.getControlURL(), params); + } + + async sendPlay(pos = 0) { + this.state = RTSPClientSM.STATE_PLAY; + let params = {}; + let range = this.client.sdp.sessionBlock.range; + if (range) { + // TODO: seekable + if (range[0] == -1) { + range[0] = 0;// Do not handle now at the moment + } + // params['Range'] = `${range[2]}=${range[0]}-`; + } + let data = await this.sendRequest('PLAY', params); + this.state = RTSPClientSM.STATE_PLAYING; + return {data: data}; + } + + async sendPause() { + if (!this.client.supports("PAUSE")) { + return; + } + this.state = RTSPClientSM.STATE_PAUSE; + await this.sendRequest("PAUSE"); + this.state = RTSPClientSM.STATE_PAUSED; + } + + async sendTeardown() { + if (this.state != RTSPClientSM.STATE_TEARDOWN) { + this.state = RTSPClientSM.STATE_TEARDOWN; + await this.sendRequest("TEARDOWN"); + Log$8.log('RTSPClient: STATE_TEARDOWN'); + ///this.client.connection.disconnect(); + // TODO: Notify client + } + } +} + +// import {RTP} from './rtp/rtp'; +const LOG_TAG$4 = "client:rtsp"; +const Log$9 = getTagged(LOG_TAG$4); + + + +class RTSPClient extends BaseClient { + constructor(options={flush: 200}) { + super(options); + this.clientSM = new RTSPClientSM(this); + this.clientSM.ontracks = (tracks) => { + this.eventSource.dispatchEvent('tracks', tracks); + this.startStreamFlush(); + }; + this.sampleQueues={}; + } + + static streamType() { + return 'rtsp'; + } + + setSource(url) { + super.setSource(url); + this.clientSM.setSource(url); + } + attachTransport(transport) { + super.attachTransport(transport); + this.clientSM.transport = transport; + } + + detachTransport() { + super.detachTransport(); + this.clientSM.transport = null; + } + + reset() { + super.reset(); + this.sampleQueues={}; + } + + destroy() { + this.clientSM.destroy(); + return super.destroy(); + } + + start() { + super.start(); + if (this.transport) { + return this.transport.ready.then(() => { + return this.clientSM.start(); + }); + } else { + return Promise.reject("no transport attached"); + } + } + + stop() { + super.stop(); + return this.clientSM.stop(); + } + + onData(data) { + this.clientSM.onData(data); + } + + onConnected() { + this.clientSM.onConnected(); + super.onConnected(); + } + + onDisconnected() { + super.onDisconnected(); + this.clientSM.onDisconnected(); + } +} + +class AuthError extends Error { + constructor(msg) { + super(msg); + } +} + +class RTSPError extends Error { + constructor(data) { + super(data.msg); + this.data = data; + } +} + +class RTSPClientSM extends StateMachine { + static get USER_AGENT() {return 'SFRtsp 0.3';} + static get STATE_INITIAL() {return 1 << 0;} + static get STATE_OPTIONS() {return 1 << 1;} + static get STATE_DESCRIBE () {return 1 << 2;} + static get STATE_SETUP() {return 1 << 3;} + static get STATE_STREAMS() {return 1 << 4;} + static get STATE_TEARDOWN() {return 1 << 5;} + static get STATE_PLAY() {return 1 << 6;} + static get STATE_PLAYING() {return 1 << 7;} + static get STATE_PAUSE() {return 1 << 8;} + static get STATE_PAUSED() {return 1 << 9;} + // static STATE_PAUSED = 1 << 6; + + constructor(parent) { + super(); + + this.parent = parent; + this.transport = null; + this.payParser = new RTPPayloadParser(); + this.rtp_channels = new Set(); + this.sessions = {}; + this.ontracks = null; + + this.addState(RTSPClientSM.STATE_INITIAL,{ + }).addState(RTSPClientSM.STATE_OPTIONS, { + activate: this.sendOptions, + finishTransition: this.onOptions + }).addState(RTSPClientSM.STATE_DESCRIBE, { + activate: this.sendDescribe, + finishTransition: this.onDescribe + }).addState(RTSPClientSM.STATE_SETUP, { + activate: this.sendSetup, + finishTransition: this.onSetup + }).addState(RTSPClientSM.STATE_STREAMS, { + + }).addState(RTSPClientSM.STATE_TEARDOWN, { + activate: ()=>{ + this.started = false; + }, + finishTransition: ()=>{ + return this.transitionTo(RTSPClientSM.STATE_INITIAL) + } + }).addTransition(RTSPClientSM.STATE_INITIAL, RTSPClientSM.STATE_OPTIONS) + .addTransition(RTSPClientSM.STATE_INITIAL, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_OPTIONS, RTSPClientSM.STATE_DESCRIBE) + .addTransition(RTSPClientSM.STATE_DESCRIBE, RTSPClientSM.STATE_SETUP) + .addTransition(RTSPClientSM.STATE_SETUP, RTSPClientSM.STATE_STREAMS) + .addTransition(RTSPClientSM.STATE_TEARDOWN, RTSPClientSM.STATE_INITIAL) + // .addTransition(RTSPClientSM.STATE_STREAMS, RTSPClientSM.STATE_PAUSED) + // .addTransition(RTSPClientSM.STATE_PAUSED, RTSPClientSM.STATE_STREAMS) + .addTransition(RTSPClientSM.STATE_STREAMS, RTSPClientSM.STATE_TEARDOWN) + // .addTransition(RTSPClientSM.STATE_PAUSED, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_SETUP, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_DESCRIBE, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_OPTIONS, RTSPClientSM.STATE_TEARDOWN); + + this.reset(); + + this.shouldReconnect = false; + + // TODO: remove listeners + // this.connection.eventSource.addEventListener('connected', ()=>{ + // if (this.shouldReconnect) { + // this.reconnect(); + // } + // }); + // this.connection.eventSource.addEventListener('disconnected', ()=>{ + // if (this.started) { + // this.shouldReconnect = true; + // } + // }); + // this.connection.eventSource.addEventListener('data', (data)=>{ + // let channel = new DataView(data).getUint8(1); + // if (this.rtp_channels.has(channel)) { + // this.onRTP({packet: new Uint8Array(data, 4), type: channel}); + // } + // + // }); + } + + destroy() { + this.parent = null; + } + + setSource(url) { + this.reset(); + this.endpoint = url; + this.url = `${url.protocol}://${url.location}${url.urlpath}`; + } + + onConnected() { + if (this.rtpFactory) { + this.rtpFactory = null; + } + if (this.shouldReconnect) { + this.start(); + } + } + + async onDisconnected() { + this.reset(); + this.shouldReconnect = true; + await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); + await this.transitionTo(RTSPClientSM.STATE_INITIAL); + } + + start() { + if (this.currentState.name !== RTSPClientSM.STATE_STREAMS) { + return this.transitionTo(RTSPClientSM.STATE_OPTIONS); + } else { + // TODO: seekable + let promises = []; + for (let session in this.sessions) { + promises.push(this.sessions[session].sendPlay()); + } + return Promise.all(promises); + } + } + + onData(data) { + let channel = data[1]; + if (this.rtp_channels.has(channel)) { + this.onRTP({packet: data.subarray(4), type: channel}); + } + } + + useRTPChannel(channel) { + this.rtp_channels.add(channel); + } + + forgetRTPChannel(channel) { + this.rtp_channels.delete(channel); + } + + stop() { + this.shouldReconnect = false; + let promises = []; + for (let session in this.sessions) { + promises.push(this.sessions[session].sendPause()); + } + return Promise.all(promises); + // this.mse = null; + } + + async reset() { + this.authenticator = ''; + this.methods = []; + this.tracks = []; + this.rtpBuffer={}; + for (let stream in this.streams) { + this.streams[stream].reset(); + } + for (let session in this.sessions) { + this.sessions[session].reset(); + } + this.streams={}; + this.sessions={}; + this.contentBase = ""; + if (this.currentState) { + if (this.currentState.name != RTSPClientSM.STATE_INITIAL) { + await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); + await this.transitionTo(RTSPClientSM.STATE_INITIAL); + } + } else { + await this.transitionTo(RTSPClientSM.STATE_INITIAL); + } + this.sdp = null; + this.interleaveChannelIndex = 0; + this.session = null; + this.timeOffset = {}; + this.lastTimestamp = {}; + } + + async reconnect() { + //this.parent.eventSource.dispatchEvent('clear'); + await this.reset(); + if (this.currentState.name != RTSPClientSM.STATE_INITIAL) { + await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); + return this.transitionTo(RTSPClientSM.STATE_OPTIONS); + } else { + return this.transitionTo(RTSPClientSM.STATE_OPTIONS); + } + } + + supports(method) { + return this.methods.includes(method) + } + + parse(_data) { + Log$9.debug(_data.payload); + let d=_data.payload.split('\r\n\r\n'); + let parsed = MessageBuilder.parse(d[0]); + let len = Number(parsed.headers['content-length']); + if (len) { + let d=_data.payload.split('\r\n\r\n'); + parsed.body = d[1]; + } else { + parsed.body=""; + } + return parsed + } + + sendRequest(_cmd, _host, _params={}, _payload=null) { + this.cSeq++; + Object.assign(_params, { + CSeq: this.cSeq, + 'User-Agent': RTSPClientSM.USER_AGENT + }); + if (this.authenticator) { + _params['Authorization'] = this.authenticator(_cmd); + } + return this.send(MessageBuilder.build(_cmd, _host, _params, _payload), _cmd).catch((e)=>{ + if ((e instanceof AuthError) && !_params['Authorization'] ) { + return this.sendRequest(_cmd, _host, _params, _payload); + } else { + throw e; + } + }); + } + + async send(_data, _method) { + if (this.transport) { + try { + await this.transport.ready; + } catch(e) { + this.onDisconnected(); + throw e; + } + Log$9.debug(_data); + let response = await this.transport.send(_data); + let parsed = this.parse(response); + // TODO: parse status codes + if (parsed.code == 401 /*&& !this.authenticator */) { + Log$9.debug(parsed.headers['www-authenticate']); + let auth = parsed.headers['www-authenticate']; + let method = auth.substring(0, auth.indexOf(' ')); + auth = auth.substr(method.length+1); + let chunks = auth.split(','); + + let ep = this.parent.endpoint; + if (!ep.user || !ep.pass) { + try { + await this.parent.queryCredentials.call(this.parent); + } catch (e) { + throw new AuthError(); + } + } + + if (method.toLowerCase() == 'digest') { + let parsedChunks = {}; + for (let chunk of chunks) { + let c = chunk.trim(); + let [k,v] = c.split('='); + parsedChunks[k] = v.substr(1, v.length-2); + } + this.authenticator = (_method)=>{ + let ep = this.parent.endpoint; + let ha1 = md5(`${ep.user}:${parsedChunks.realm}:${ep.pass}`); + let ha2 = md5(`${_method}:${this.url}`); + let response = md5(`${ha1}:${parsedChunks.nonce}:${ha2}`); + let tail=''; // TODO: handle other params + return `Digest username="${ep.user}", realm="${parsedChunks.realm}", nonce="${parsedChunks.nonce}", uri="${this.url}", response="${response}"${tail}`; + }; + } else { + this.authenticator = ()=>{return `Basic ${btoa(this.parent.endpoint.auth)}`;}; + } + + throw new AuthError(parsed); + } + if (parsed.code >= 300) { + Log$9.error(parsed.statusLine); + throw new RTSPError({msg: `RTSP error: ${parsed.code} ${parsed.statusLine}`, parsed: parsed}); + } + return parsed; + } else { + return Promise.reject("No transport attached"); + } + } + + sendOptions() { + this.reset(); + this.started = true; + this.cSeq = 0; + return this.sendRequest('OPTIONS', '*', {}); + } + + onOptions(data) { + this.methods = data.headers['public'].split(',').map((e)=>e.trim()); + this.transitionTo(RTSPClientSM.STATE_DESCRIBE); + } + + sendDescribe() { + return this.sendRequest('DESCRIBE', this.url, { + 'Accept': 'application/sdp' + }).then((data)=>{ + this.sdp = new SDPParser(); + return this.sdp.parse(data.body).catch(()=>{ + throw new Error("Failed to parse SDP"); + }).then(()=>{return data;}); + }); + } + + onDescribe(data) { + this.contentBase = data.headers['content-base'] || this.url;// `${this.endpoint.protocol}://${this.endpoint.location}${this.endpoint.urlpath}/`; + this.tracks = this.sdp.getMediaBlockList(); + this.rtpFactory = new RTPFactory(this.sdp); + + Log$9.log('SDP contained ' + this.tracks.length + ' track(s). Calling SETUP for each.'); + + if (data.headers['session']) { + this.session = data.headers['session']; + } + + if (!this.tracks.length) { + throw new Error("No tracks in SDP"); + } + + this.transitionTo(RTSPClientSM.STATE_SETUP); + } + + sendSetup() { + let streams=[]; + + // TODO: select first video and first audio tracks + for (let track_type of this.tracks) { + Log$9.log("setup track: "+track_type); + // if (track_type=='audio') continue; + // if (track_type=='video') continue; + let track = this.sdp.getMediaBlock(track_type); + if (!PayloadType.string_map[track.rtpmap[track.fmt[0]].name]) continue; + + this.streams[track_type] = new RTSPStream(this, track); + let setupPromise = this.streams[track_type].start(); + this.parent.sampleQueues[PayloadType.string_map[track.rtpmap[track.fmt[0]].name]]=[]; + this.rtpBuffer[track.fmt[0]]=[]; + streams.push(setupPromise.then(({track, data})=>{ + this.timeOffset[track.fmt[0]] = 0; + try { + let rtp_info = data.headers["rtp-info"].split(';'); + for (let chunk of rtp_info) { + let [key, val] = chunk.split("="); + if (key === "rtptime") { + this.timeOffset[track.fmt[0]] = 0;//Number(val); + } + } + } catch (e) { + // new Date().getTime(); + } + let params = { + timescale: 0, + scaleFactor: 0 + }; + if (track.fmtp['sprop-parameter-sets']) { + let sps_pps = track.fmtp['sprop-parameter-sets'].split(','); + params = { + sps:base64ToArrayBuffer(sps_pps[0]), + pps:base64ToArrayBuffer(sps_pps[1]) + }; + } else if (track.fmtp['config']) { + let config = track.fmtp['config']; + this.has_config = track.fmtp['cpresent']!='0'; + let generic = track.rtpmap[track.fmt[0]].name == 'MPEG4-GENERIC'; + if (generic) { + params={config: + AACParser.parseAudioSpecificConfig(hexToByteArray(config)) + }; + this.payParser.aacparser.setConfig(params.config); + } else if (config) { + // todo: parse audio specific config for mpeg4-generic + params={config: + AACParser.parseStreamMuxConfig(hexToByteArray(config)) + }; + this.payParser.aacparser.setConfig(params.config); + } + } + params.duration = this.sdp.sessionBlock.range?this.sdp.sessionBlock.range[1]-this.sdp.sessionBlock.range[0]:1; + this.parent.seekable = (params.duration > 1); + let res = { + track: track, + offset: this.timeOffset[track.fmt[0]], + type: PayloadType.string_map[track.rtpmap[track.fmt[0]].name], + params: params, + duration: params.duration + }; + console.log(res, this.timeOffset); + let session = data.headers.session.split(';')[0]; + if (!this.sessions[session]) { + this.sessions[session] = new RTSPSession(this, session); + } + return res; + })); + } + return Promise.all(streams).then((tracks)=>{ + let sessionPromises = []; + for (let session in this.sessions) { + sessionPromises.push(this.sessions[session].start()); + } + return Promise.all(sessionPromises).then(()=>{ + if (this.ontracks) { + this.ontracks(tracks); + } + }) + }).catch((e)=>{ + console.error(e); + this.stop(); + this.reset(); + }); + } + + onSetup() { + this.transitionTo(RTSPClientSM.STATE_STREAMS); + } + + onRTP(_data) { + if (!this.rtpFactory) return; + + let rtp = this.rtpFactory.build(_data.packet, this.sdp); + if (!rtp.type) { + return; + } + + if (this.timeOffset[rtp.pt] === undefined) { + //console.log(rtp.pt, this.timeOffset[rtp.pt]); + this.rtpBuffer[rtp.pt].push(rtp); + return; + } + + if (this.lastTimestamp[rtp.pt] === undefined) { + this.lastTimestamp[rtp.pt] = rtp.timestamp-this.timeOffset[rtp.pt]; + } + + let queue = this.rtpBuffer[rtp.pt]; + queue.push(rtp); + + while (queue.length) { + let rtp = queue.shift(); + + rtp.timestamp = rtp.timestamp-this.timeOffset[rtp.pt]-this.lastTimestamp[rtp.pt]; + // TODO: overflow + // if (rtp.timestamp < 0) { + // rtp.timestamp = (rtp.timestamp + Number.MAX_SAFE_INTEGER) % 0x7fffffff; + // } + if (rtp.media) { + let pay = this.payParser.parse(rtp); + if (pay) { + // if (typeof pay == typeof []) { + this.parent.sampleQueues[rtp.type].push(pay); + // } else { + // this.parent.sampleQueues[rtp.type].push([pay]); + // } + } + } + } + // this.remuxer.feedRTP(); + } +} + +// ASN.1 JavaScript decoder +// Copyright (c) 2008-2013 Lapo Luchini + +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +/*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ +/*global oids */ + +var hardLimit = 100; +var ellipsis = "\u2026"; +var DOM = { + tag: function (tagName, className) { + var t = document.createElement(tagName); + t.className = className; + return t; + }, + text: function (str) { + return document.createTextNode(str); + } + }; + +class Stream { + static get hexDigits() { + return "0123456789ABCDEF"; + }; + + static get reTime() { + return /^((?:1[89]|2\d)?\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/; + } + + + constructor(enc, pos) { + if (enc instanceof Stream) { + this.enc = enc.enc; + this.pos = enc.pos; + } else { + this.enc = enc; + this.pos = pos; + } + } + + get(pos) { + if (pos === undefined) + pos = this.pos++; + if (pos >= this.enc.length) + throw 'Requesting byte offset ' + pos + ' on a stream of length ' + this.enc.length; + return this.enc[pos]; + }; + + hexByte(b) { + return Stream.hexDigits.charAt((b >> 4) & 0xF) + Stream.hexDigits.charAt(b & 0xF); + }; + + hexDump(start, end, raw) { + var s = ""; + for (var i = start; i < end; ++i) { + s += this.hexByte(this.get(i)); + if (raw !== true) + switch (i & 0xF) { + case 0x7: + s += " "; + break; + case 0xF: + s += "\n"; + break; + default: + s += " "; + } + } + return s; + }; + + parseStringISO(start, end) { + var s = ""; + for (var i = start; i < end; ++i) + s += String.fromCharCode(this.get(i)); + return s; + }; + + parseStringUTF(start, end) { + var s = ""; + for (var i = start; i < end;) { + var c = this.get(i++); + if (c < 128) + s += String.fromCharCode(c); + else if ((c > 191) && (c < 224)) + s += String.fromCharCode(((c & 0x1F) << 6) | (this.get(i++) & 0x3F)); + else + s += String.fromCharCode(((c & 0x0F) << 12) | ((this.get(i++) & 0x3F) << 6) | (this.get(i++) & 0x3F)); + } + return s; + }; + + parseStringBMP(start, end) { + var str = ""; + for (var i = start; i < end; i += 2) { + var high_byte = this.get(i); + var low_byte = this.get(i + 1); + str += String.fromCharCode((high_byte << 8) + low_byte); + } + + return str; + }; + + parseTime(start, end) { + var s = this.parseStringISO(start, end), + m = Stream.reTime.exec(s); + if (!m) + return "Unrecognized time: " + s; + s = m[1] + "-" + m[2] + "-" + m[3] + " " + m[4]; + if (m[5]) { + s += ":" + m[5]; + if (m[6]) { + s += ":" + m[6]; + if (m[7]) + s += "." + m[7]; + } + } + if (m[8]) { + s += " UTC"; + if (m[8] != 'Z') { + s += m[8]; + if (m[9]) + s += ":" + m[9]; + } + } + return s; + }; + + parseInteger(start, end) { + //TODO support negative numbers + var len = end - start; + if (len > 4) { + len <<= 3; + var s = this.get(start); + if (s === 0) + len -= 8; + else + while (s < 128) { + s <<= 1; + --len; + } + return "(" + len + " bit)"; + } + var n = 0; + for (var i = start; i < end; ++i) + n = (n << 8) | this.get(i); + return n; + }; + + parseBitString(start, end) { + var unusedBit = this.get(start), + lenBit = ((end - start - 1) << 3) - unusedBit, + s = "(" + lenBit + " bit)"; + if (lenBit <= 20) { + var skip = unusedBit; + s += " "; + for (var i = end - 1; i > start; --i) { + var b = this.get(i); + for (var j = skip; j < 8; ++j) + s += (b >> j) & 1 ? "1" : "0"; + skip = 0; + } + } + return s; + }; + + parseOctetString(start, end) { + var len = end - start, + s = "(" + len + " byte) "; + if (len > hardLimit) + end = start + hardLimit; + for (var i = start; i < end; ++i) + s += this.hexByte(this.get(i)); //TODO: also try Latin1? + if (len > hardLimit) + s += ellipsis; + return s; + }; + + parseOID(start, end) { + var s = '', + n = 0, + bits = 0; + for (var i = start; i < end; ++i) { + var v = this.get(i); + n = (n << 7) | (v & 0x7F); + bits += 7; + if (!(v & 0x80)) { // finished + if (s === '') { + var m = n < 80 ? n < 40 ? 0 : 1 : 2; + s = m + "." + (n - m * 40); + } else + s += "." + ((bits >= 31) ? "bigint" : n); + n = bits = 0; + } + } + return s; + }; +} + +class ASN1 { + static get reSeemsASCII() { + return /^[ -~]+$/; + } + + constructor(stream, header, length, tag, sub) { + this.stream = stream; + this.header = header; + this.length = length; + this.tag = tag; + this.sub = sub; + } + + typeName() { + if (this.tag === undefined) + return "unknown"; + var tagClass = this.tag >> 6, + tagConstructed = (this.tag >> 5) & 1, + tagNumber = this.tag & 0x1F; + switch (tagClass) { + case 0: // universal + switch (tagNumber) { + case 0x00: + return "EOC"; + case 0x01: + return "BOOLEAN"; + case 0x02: + return "INTEGER"; + case 0x03: + return "BIT_STRING"; + case 0x04: + return "OCTET_STRING"; + case 0x05: + return "NULL"; + case 0x06: + return "OBJECT_IDENTIFIER"; + case 0x07: + return "ObjectDescriptor"; + case 0x08: + return "EXTERNAL"; + case 0x09: + return "REAL"; + case 0x0A: + return "ENUMERATED"; + case 0x0B: + return "EMBEDDED_PDV"; + case 0x0C: + return "UTF8String"; + case 0x10: + return "SEQUENCE"; + case 0x11: + return "SET"; + case 0x12: + return "NumericString"; + case 0x13: + return "PrintableString"; // ASCII subset + case 0x14: + return "TeletexString"; // aka T61String + case 0x15: + return "VideotexString"; + case 0x16: + return "IA5String"; // ASCII + case 0x17: + return "UTCTime"; + case 0x18: + return "GeneralizedTime"; + case 0x19: + return "GraphicString"; + case 0x1A: + return "VisibleString"; // ASCII subset + case 0x1B: + return "GeneralString"; + case 0x1C: + return "UniversalString"; + case 0x1E: + return "BMPString"; + default: + return "Universal_" + tagNumber.toString(16); + } + case 1: + return "Application_" + tagNumber.toString(16); + case 2: + return "[" + tagNumber + "]"; // Context + case 3: + return "Private_" + tagNumber.toString(16); + } + } + + content() { + if (this.tag === undefined) + return null; + var tagClass = this.tag >> 6, + tagNumber = this.tag & 0x1F, + content = this.posContent(), + len = Math.abs(this.length); + if (tagClass !== 0) { // universal + if (this.sub !== null) + return "(" + this.sub.length + " elem)"; + //TODO: TRY TO PARSE ASCII STRING + var s = this.stream.parseStringISO(content, content + Math.min(len, hardLimit)); + if (ASN1.reSeemsASCII.test(s)) + return s.substring(0, 2 * hardLimit) + ((s.length > 2 * hardLimit) ? ellipsis : ""); + else + return this.stream.parseOctetString(content, content + len); + } + switch (tagNumber) { + case 0x01: // BOOLEAN + return (this.stream.get(content) === 0) ? "false" : "true"; + case 0x02: // INTEGER + return this.stream.parseInteger(content, content + len); + case 0x03: // BIT_STRING + return this.sub ? "(" + this.sub.length + " elem)" : + this.stream.parseBitString(content, content + len); + case 0x04: // OCTET_STRING + return this.sub ? "(" + this.sub.length + " elem)" : + this.stream.parseOctetString(content, content + len); + //case 0x05: // NULL + case 0x06: // OBJECT_IDENTIFIER + return this.stream.parseOID(content, content + len); + //case 0x07: // ObjectDescriptor + //case 0x08: // EXTERNAL + //case 0x09: // REAL + //case 0x0A: // ENUMERATED + //case 0x0B: // EMBEDDED_PDV + case 0x10: // SEQUENCE + case 0x11: // SET + return "(" + this.sub.length + " elem)"; + case 0x0C: // UTF8String + return this.stream.parseStringUTF(content, content + len); + case 0x12: // NumericString + case 0x13: // PrintableString + case 0x14: // TeletexString + case 0x15: // VideotexString + case 0x16: // IA5String + //case 0x19: // GraphicString + case 0x1A: // VisibleString + //case 0x1B: // GeneralString + //case 0x1C: // UniversalString + return this.stream.parseStringISO(content, content + len); + case 0x1E: // BMPString + return this.stream.parseStringBMP(content, content + len); + case 0x17: // UTCTime + case 0x18: // GeneralizedTime + return this.stream.parseTime(content, content + len); + } + return null; + }; + + toString() { + return this.typeName() + "@" + this.stream.pos + "[header:" + this.header + ",length:" + this.length + ",sub:" + ((this.sub === null) ? 'null' : this.sub.length) + "]"; + }; + + print(indent) { + if (indent === undefined) indent = ''; + document.writeln(indent + this); + if (this.sub !== null) { + indent += ' '; + for (var i = 0, max = this.sub.length; i < max; ++i) + this.sub[i].print(indent); + } + }; + + toPrettyString(indent) { + if (indent === undefined) indent = ''; + var s = indent + this.typeName() + " @" + this.stream.pos; + if (this.length >= 0) + s += "+"; + s += this.length; + if (this.tag & 0x20) + s += " (constructed)"; + else if (((this.tag == 0x03) || (this.tag == 0x04)) && (this.sub !== null)) + s += " (encapsulates)"; + s += "\n"; + if (this.sub !== null) { + indent += ' '; + for (var i = 0, max = this.sub.length; i < max; ++i) + s += this.sub[i].toPrettyString(indent); + } + return s; + }; + + toDOM() { + var node = DOM.tag("div", "node"); + node.asn1 = this; + var head = DOM.tag("div", "head"); + var s = this.typeName().replace(/_/g, " "); + head.innerHTML = s; + var content = this.content(); + if (content !== null) { + content = String(content).replace(/"; + s += "Length: " + this.header + "+"; + if (this.length >= 0) + s += this.length; + else + s += (-this.length) + " (undefined)"; + if (this.tag & 0x20) + s += "
(constructed)"; + else if (((this.tag == 0x03) || (this.tag == 0x04)) && (this.sub !== null)) + s += "
(encapsulates)"; + //TODO if (this.tag == 0x03) s += "Unused bits: " + if (content !== null) { + s += "
Value:
" + content + ""; + if ((typeof oids === 'object') && (this.tag == 0x06)) { + var oid = oids[content]; + if (oid) { + if (oid.d) s += "
" + oid.d; + if (oid.c) s += "
" + oid.c; + if (oid.w) s += "
(warning!)"; + } + } + } + value.innerHTML = s; + node.appendChild(value); + var sub = DOM.tag("div", "sub"); + if (this.sub !== null) { + for (var i = 0, max = this.sub.length; i < max; ++i) + sub.appendChild(this.sub[i].toDOM()); + } + node.appendChild(sub); + head.onclick = function () { + node.className = (node.className == "node collapsed") ? "node" : "node collapsed"; + }; + return node; + }; + + posStart() { + return this.stream.pos; + }; + + posContent() { + return this.stream.pos + this.header; + }; + + posEnd() { + return this.stream.pos + this.header + Math.abs(this.length); + }; + + fakeHover(current) { + this.node.className += " hover"; + if (current) + this.head.className += " hover"; + }; + + fakeOut(current) { + var re = / ?hover/; + this.node.className = this.node.className.replace(re, ""); + if (current) + this.head.className = this.head.className.replace(re, ""); + }; + + toHexDOM_sub(node, className, stream, start, end) { + if (start >= end) + return; + var sub = DOM.tag("span", className); + sub.appendChild(DOM.text( + stream.hexDump(start, end))); + node.appendChild(sub); + }; + + toHexDOM(root) { + var node = DOM.tag("span", "hex"); + if (root === undefined) root = node; + this.head.hexNode = node; + this.head.onmouseover = function () { + this.hexNode.className = "hexCurrent"; + }; + this.head.onmouseout = function () { + this.hexNode.className = "hex"; + }; + node.asn1 = this; + node.onmouseover = function () { + var current = !root.selected; + if (current) { + root.selected = this.asn1; + this.className = "hexCurrent"; + } + this.asn1.fakeHover(current); + }; + node.onmouseout = function () { + var current = (root.selected == this.asn1); + this.asn1.fakeOut(current); + if (current) { + root.selected = null; + this.className = "hex"; + } + }; + this.toHexDOM_sub(node, "tag", this.stream, this.posStart(), this.posStart() + 1); + this.toHexDOM_sub(node, (this.length >= 0) ? "dlen" : "ulen", this.stream, this.posStart() + 1, this.posContent()); + if (this.sub === null) + node.appendChild(DOM.text( + this.stream.hexDump(this.posContent(), this.posEnd()))); + else if (this.sub.length > 0) { + var first = this.sub[0]; + var last = this.sub[this.sub.length - 1]; + this.toHexDOM_sub(node, "intro", this.stream, this.posContent(), first.posStart()); + for (var i = 0, max = this.sub.length; i < max; ++i) + node.appendChild(this.sub[i].toHexDOM(root)); + this.toHexDOM_sub(node, "outro", this.stream, last.posEnd(), this.posEnd()); + } + return node; + }; + + toHexString(root) { + return this.stream.hexDump(this.posStart(), this.posEnd(), true); + }; + +} + +ASN1.decodeLength = function (stream) { + var buf = stream.get(), + len = buf & 0x7F; + if (len == buf) + return len; + if (len > 3) + throw "Length over 24 bits not supported at position " + (stream.pos - 1); + if (len === 0) + return -1; // undefined + buf = 0; + for (var i = 0; i < len; ++i) + buf = (buf << 8) | stream.get(); + return buf; +}; +ASN1.hasContent = function (tag, len, stream) { + if (tag & 0x20) // constructed + return true; + if ((tag < 0x03) || (tag > 0x04)) + return false; + var p = new Stream(stream); + if (tag == 0x03) p.get(); // BitString unused bits, must be in [0, 7] + var subTag = p.get(); + if ((subTag >> 6) & 0x01) // not (universal or context) + return false; + try { + var subLength = ASN1.decodeLength(p); + return ((p.pos - stream.pos) + subLength == len); + } catch (exception) { + return false; + } +}; +ASN1.decode = function (stream) { + if (!(stream instanceof Stream)) + stream = new Stream(stream, 0); + var streamStart = new Stream(stream), + tag = stream.get(), + len = ASN1.decodeLength(stream), + header = stream.pos - streamStart.pos, + sub = null; + if (ASN1.hasContent(tag, len, stream)) { + // it has content, so we decode it + var start = stream.pos; + if (tag == 0x03) stream.get(); // skip BitString unused bits, must be in [0, 7] + sub = []; + if (len >= 0) { + // definite length + var end = start + len; + while (stream.pos < end) + sub[sub.length] = ASN1.decode(stream); + if (stream.pos != end) + throw "Content size is not correct for container starting at offset " + start; + } else { + // undefined length + try { + for (; ;) { + var s = ASN1.decode(stream); + if (s.tag === 0) + break; + sub[sub.length] = s; + } + len = start - stream.pos; + } catch (e) { + throw "Exception while decoding undefined length content: " + e; + } + } + } else + stream.pos += len; // skip content + return new ASN1(streamStart, header, len, tag, sub); +}; +ASN1.test = function () { + var test = [ + {value: [0x27], expected: 0x27}, + {value: [0x81, 0xC9], expected: 0xC9}, + {value: [0x83, 0xFE, 0xDC, 0xBA], expected: 0xFEDCBA} + ]; + for (var i = 0, max = test.length; i < max; ++i) { + var pos = 0, + stream = new Stream(test[i].value, 0), + res = ASN1.decodeLength(stream); + if (res != test[i].expected) + document.write("In test[" + i + "] expected " + test[i].expected + " got " + res + "\n"); + } +}; + +// prng4.js - uses Arcfour as a PRNG + +class Arcfour { + constructor() { + this.i = 0; + this.j = 0; + this.S = []; + } +} + +// Initialize arcfour context from key, an array of ints, each from [0..255] +function ARC4init(key) { + var i, j, t; + for(i = 0; i < 256; ++i) + this.S[i] = i; + j = 0; + for(i = 0; i < 256; ++i) { + j = (j + this.S[i] + key[i % key.length]) & 255; + t = this.S[i]; + this.S[i] = this.S[j]; + this.S[j] = t; + } + this.i = 0; + this.j = 0; +} + +function ARC4next() { + var t; + this.i = (this.i + 1) & 255; + this.j = (this.j + this.S[this.i]) & 255; + t = this.S[this.i]; + this.S[this.i] = this.S[this.j]; + this.S[this.j] = t; + return this.S[(t + this.S[this.i]) & 255]; +} + +Arcfour.prototype.init = ARC4init; +Arcfour.prototype.next = ARC4next; + +// Plug in your RNG constructor here +function prng_newstate() { + return new Arcfour(); +} + +// Pool size must be a multiple of 4 and greater than 32. +// An array of bytes the size of the pool will be passed to init() +var rng_psize = 256; + +// Random number generator - requires a PRNG backend, e.g. prng4.js +var rng_state; +var rng_pool; +var rng_pptr; + +// Initialize the pool with junk if needed. +if(rng_pool == null) { + rng_pool = new Array(); + rng_pptr = 0; + var t; + if(window.crypto && window.crypto.getRandomValues) { + // Extract entropy (2048 bits) from RNG if available + var z = new Uint32Array(256); + window.crypto.getRandomValues(z); + for (t = 0; t < z.length; ++t) + rng_pool[rng_pptr++] = z[t] & 255; + } + + // Use mouse events for entropy, if we do not have enough entropy by the time + // we need it, entropy will be generated by Math.random. + var onMouseMoveListener = function(ev) { + this.count = this.count || 0; + if (this.count >= 256 || rng_pptr >= rng_psize) { + if (window.removeEventListener) + window.removeEventListener("mousemove", onMouseMoveListener, false); + else if (window.detachEvent) + window.detachEvent("onmousemove", onMouseMoveListener); + return; + } + try { + var mouseCoordinates = ev.x + ev.y; + rng_pool[rng_pptr++] = mouseCoordinates & 255; + this.count += 1; + } catch (e) { + // Sometimes Firefox will deny permission to access event properties for some reason. Ignore. + } + }; + if (window.addEventListener) + window.addEventListener("mousemove", onMouseMoveListener, false); + else if (window.attachEvent) + window.attachEvent("onmousemove", onMouseMoveListener); + +} + +function rng_get_byte() { + if(rng_state == null) { + rng_state = prng_newstate(); + // At this point, we may not have collected enough entropy. If not, fall back to Math.random + while (rng_pptr < rng_psize) { + var random = Math.floor(65536 * Math.random()); + rng_pool[rng_pptr++] = random & 255; + } + rng_state.init(rng_pool); + for(rng_pptr = 0; rng_pptr < rng_pool.length; ++rng_pptr) + rng_pool[rng_pptr] = 0; + rng_pptr = 0; + } + // TODO: allow reseeding after first request + return rng_state.next(); +} + +function rng_get_bytes(ba) { + var i; + for(i = 0; i < ba.length; ++i) ba[i] = rng_get_byte(); +} + +class SecureRandom { + constructor(){} +} + +SecureRandom.prototype.nextBytes = rng_get_bytes; + +// Copyright (c) 2005 Tom Wu +// All Rights Reserved. +// See "LICENSE" for details. + +// Basic JavaScript BN library - subset useful for RSA encryption. + +// Bits per digit +var dbits; + +// JavaScript engine analysis +var canary = 0xdeadbeefcafe; +var j_lm = ((canary&0xffffff)==0xefcafe); + +// (public) Constructor +class BigInteger { + constructor(a,b,c) { + if (a != null) + if ("number" == typeof a) this.fromNumber(a, b, c); + else if (b == null && "string" != typeof a) this.fromString(a, 256); + else this.fromString(a, b); + } +} + +// return new, unset BigInteger +function nbi() { return new BigInteger(null); } + +// am: Compute w_j += (x*this_i), propagate carries, +// c is initial carry, returns final carry. +// c < 3*dvalue, x < 2*dvalue, this_i < dvalue +// We need to select the fastest one that works in this environment. + +// am1: use a single mult and divide to get the high bits, +// max digit bits should be 26 because +// max internal value = 2*dvalue^2-2*dvalue (< 2^53) +function am1(i,x,w,j,c,n) { + while(--n >= 0) { + var v = x*this[i++]+w[j]+c; + c = Math.floor(v/0x4000000); + w[j++] = v&0x3ffffff; + } + return c; +} +// am2 avoids a big mult-and-extract completely. +// Max digit bits should be <= 30 because we do bitwise ops +// on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) +function am2(i,x,w,j,c,n) { + var xl = x&0x7fff, xh = x>>15; + while(--n >= 0) { + var l = this[i]&0x7fff; + var h = this[i++]>>15; + var m = xh*l+h*xl; + l = xl*l+((m&0x7fff)<<15)+w[j]+(c&0x3fffffff); + c = (l>>>30)+(m>>>15)+xh*h+(c>>>30); + w[j++] = l&0x3fffffff; + } + return c; +} +// Alternately, set max digit bits to 28 since some +// browsers slow down when dealing with 32-bit numbers. +function am3(i,x,w,j,c,n) { + var xl = x&0x3fff, xh = x>>14; + while(--n >= 0) { + var l = this[i]&0x3fff; + var h = this[i++]>>14; + var m = xh*l+h*xl; + l = xl*l+((m&0x3fff)<<14)+w[j]+c; + c = (l>>28)+(m>>14)+xh*h; + w[j++] = l&0xfffffff; + } + return c; +} +if(j_lm && (navigator.appName == "Microsoft Internet Explorer")) { + BigInteger.prototype.am = am2; + dbits = 30; +} +else if(j_lm && (navigator.appName != "Netscape")) { + BigInteger.prototype.am = am1; + dbits = 26; +} +else { // Mozilla/Netscape seems to prefer am3 + BigInteger.prototype.am = am3; + dbits = 28; +} + +BigInteger.prototype.DB = dbits; +BigInteger.prototype.DM = ((1<= 0; --i) r[i] = this[i]; + r.t = this.t; + r.s = this.s; +} + +// (protected) set from integer value x, -DV <= x < DV +function bnpFromInt(x) { + this.t = 1; + this.s = (x<0)?-1:0; + if(x > 0) this[0] = x; + else if(x < -1) this[0] = x+this.DV; + else this.t = 0; +} + +// return bigint initialized to value +function nbv(i) { var r = nbi(); r.fromInt(i); return r; } + +// (protected) set from string and radix +function bnpFromString(s,b) { + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 256) k = 8; // byte array + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else { this.fromRadix(s,b); return; } + this.t = 0; + this.s = 0; + var i = s.length, mi = false, sh = 0; + while(--i >= 0) { + var x = (k==8)?s[i]&0xff:intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-") mi = true; + continue; + } + mi = false; + if(sh == 0) + this[this.t++] = x; + else if(sh+k > this.DB) { + this[this.t-1] |= (x&((1<<(this.DB-sh))-1))<>(this.DB-sh)); + } + else + this[this.t-1] |= x<= this.DB) sh -= this.DB; + } + if(k == 8 && (s[0]&0x80) != 0) { + this.s = -1; + if(sh > 0) this[this.t-1] |= ((1<<(this.DB-sh))-1)< 0 && this[this.t-1] == c) --this.t; +} + +// (public) return string representation in given radix +function bnToString(b) { + if(this.s < 0) return "-"+this.negate().toString(b); + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else return this.toRadix(b); + var km = (1< 0) { + if(p < this.DB && (d = this[i]>>p) > 0) { m = true; r = int2char(d); } + while(i >= 0) { + if(p < k) { + d = (this[i]&((1<>(p+=this.DB-k); + } + else { + d = (this[i]>>(p-=k))&km; + if(p <= 0) { p += this.DB; --i; } + } + if(d > 0) m = true; + if(m) r += int2char(d); + } + } + return m?r:"0"; +} + +// (public) -this +function bnNegate() { var r = nbi(); BigInteger.ZERO.subTo(this,r); return r; } + +// (public) |this| +function bnAbs() { return (this.s<0)?this.negate():this; } + +// (public) return + if this > a, - if this < a, 0 if equal +function bnCompareTo(a) { + var r = this.s-a.s; + if(r != 0) return r; + var i = this.t; + r = i-a.t; + if(r != 0) return (this.s<0)?-r:r; + while(--i >= 0) if((r=this[i]-a[i]) != 0) return r; + return 0; +} + +// returns bit length of the integer x +function nbits(x) { + var r = 1, t; + if((t=x>>>16) != 0) { x = t; r += 16; } + if((t=x>>8) != 0) { x = t; r += 8; } + if((t=x>>4) != 0) { x = t; r += 4; } + if((t=x>>2) != 0) { x = t; r += 2; } + if((t=x>>1) != 0) { x = t; r += 1; } + return r; +} + +// (public) return the number of bits in "this" +function bnBitLength() { + if(this.t <= 0) return 0; + return this.DB*(this.t-1)+nbits(this[this.t-1]^(this.s&this.DM)); +} + +// (protected) r = this << n*DB +function bnpDLShiftTo(n,r) { + var i; + for(i = this.t-1; i >= 0; --i) r[i+n] = this[i]; + for(i = n-1; i >= 0; --i) r[i] = 0; + r.t = this.t+n; + r.s = this.s; +} + +// (protected) r = this >> n*DB +function bnpDRShiftTo(n,r) { + for(var i = n; i < this.t; ++i) r[i-n] = this[i]; + r.t = Math.max(this.t-n,0); + r.s = this.s; +} + +// (protected) r = this << n +function bnpLShiftTo(n,r) { + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<= 0; --i) { + r[i+ds+1] = (this[i]>>cbs)|c; + c = (this[i]&bm)<= 0; --i) r[i] = 0; + r[ds] = c; + r.t = this.t+ds+1; + r.s = this.s; + r.clamp(); +} + +// (protected) r = this >> n +function bnpRShiftTo(n,r) { + r.s = this.s; + var ds = Math.floor(n/this.DB); + if(ds >= this.t) { r.t = 0; return; } + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<>bs; + for(var i = ds+1; i < this.t; ++i) { + r[i-ds-1] |= (this[i]&bm)<>bs; + } + if(bs > 0) r[this.t-ds-1] |= (this.s&bm)<>= this.DB; + } + if(a.t < this.t) { + c -= a.s; + while(i < this.t) { + c += this[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c -= a[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c -= a.s; + } + r.s = (c<0)?-1:0; + if(c < -1) r[i++] = this.DV+c; + else if(c > 0) r[i++] = c; + r.t = i; + r.clamp(); +} + +// (protected) r = this * a, r != this,a (HAC 14.12) +// "this" should be the larger one if appropriate. +function bnpMultiplyTo(a,r) { + var x = this.abs(), y = a.abs(); + var i = x.t; + r.t = i+y.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < y.t; ++i) r[i+x.t] = x.am(0,y[i],r,i,0,x.t); + r.s = 0; + r.clamp(); + if(this.s != a.s) BigInteger.ZERO.subTo(r,r); +} + +// (protected) r = this^2, r != this (HAC 14.16) +function bnpSquareTo(r) { + var x = this.abs(); + var i = r.t = 2*x.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < x.t-1; ++i) { + var c = x.am(i,x[i],r,2*i,0,1); + if((r[i+x.t]+=x.am(i+1,2*x[i],r,2*i+1,c,x.t-i-1)) >= x.DV) { + r[i+x.t] -= x.DV; + r[i+x.t+1] = 1; + } + } + if(r.t > 0) r[r.t-1] += x.am(i,x[i],r,2*i,0,1); + r.s = 0; + r.clamp(); +} + +// (protected) divide this by m, quotient and remainder to q, r (HAC 14.20) +// r != q, this != m. q or r may be null. +function bnpDivRemTo(m,q,r) { + var pm = m.abs(); + if(pm.t <= 0) return; + var pt = this.abs(); + if(pt.t < pm.t) { + if(q != null) q.fromInt(0); + if(r != null) this.copyTo(r); + return; + } + if(r == null) r = nbi(); + var y = nbi(), ts = this.s, ms = m.s; + var nsh = this.DB-nbits(pm[pm.t-1]); // normalize modulus + if(nsh > 0) { pm.lShiftTo(nsh,y); pt.lShiftTo(nsh,r); } + else { pm.copyTo(y); pt.copyTo(r); } + var ys = y.t; + var y0 = y[ys-1]; + if(y0 == 0) return; + var yt = y0*(1<1)?y[ys-2]>>this.F2:0); + var d1 = this.FV/yt, d2 = (1<= 0) { + r[r.t++] = 1; + r.subTo(t,r); + } + BigInteger.ONE.dlShiftTo(ys,t); + t.subTo(y,y); // "negative" y so we can replace sub with am later + while(y.t < ys) y[y.t++] = 0; + while(--j >= 0) { + // Estimate quotient digit + var qd = (r[--i]==y0)?this.DM:Math.floor(r[i]*d1+(r[i-1]+e)*d2); + if((r[i]+=y.am(0,qd,r,j,0,ys)) < qd) { // Try it out + y.dlShiftTo(j,t); + r.subTo(t,r); + while(r[i] < --qd) r.subTo(t,r); + } + } + if(q != null) { + r.drShiftTo(ys,q); + if(ts != ms) BigInteger.ZERO.subTo(q,q); + } + r.t = ys; + r.clamp(); + if(nsh > 0) r.rShiftTo(nsh,r); // Denormalize remainder + if(ts < 0) BigInteger.ZERO.subTo(r,r); +} + +// (public) this mod a +function bnMod(a) { + var r = nbi(); + this.abs().divRemTo(a,null,r); + if(this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r,r); + return r; +} + +// Modular reduction using "classic" algorithm +class Classic{ + constructor(m){ + this.m = m; + } +} +function cConvert(x) { + if(x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); + else return x; +} +function cRevert(x) { return x; } +function cReduce(x) { x.divRemTo(this.m,null,x); } +function cMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } +function cSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +Classic.prototype.convert = cConvert; +Classic.prototype.revert = cRevert; +Classic.prototype.reduce = cReduce; +Classic.prototype.mulTo = cMulTo; +Classic.prototype.sqrTo = cSqrTo; + +// (protected) return "-1/this % 2^DB"; useful for Mont. reduction +// justification: +// xy == 1 (mod m) +// xy = 1+km +// xy(2-xy) = (1+km)(1-km) +// x[y(2-xy)] = 1-k^2m^2 +// x[y(2-xy)] == 1 (mod m^2) +// if y is 1/x mod m, then y(2-xy) is 1/x mod m^2 +// should reduce x and y(2-xy) by m^2 at each step to keep size bounded. +// JS multiply "overflows" differently from C/C++, so care is needed here. +function bnpInvDigit() { + if(this.t < 1) return 0; + var x = this[0]; + if((x&1) == 0) return 0; + var y = x&3; // y == 1/x mod 2^2 + y = (y*(2-(x&0xf)*y))&0xf; // y == 1/x mod 2^4 + y = (y*(2-(x&0xff)*y))&0xff; // y == 1/x mod 2^8 + y = (y*(2-(((x&0xffff)*y)&0xffff)))&0xffff; // y == 1/x mod 2^16 + // last step - calculate inverse mod DV directly; + // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints + y = (y*(2-x*y%this.DV))%this.DV; // y == 1/x mod 2^dbits + // we really want the negative inverse, and -DV < y < DV + return (y>0)?this.DV-y:-y; +} + +// Montgomery reduction +class Montgomery { + constructor(m) { + this.m = m; + this.mp = m.invDigit(); + this.mpl = this.mp & 0x7fff; + this.mph = this.mp >> 15; + this.um = (1 << (m.DB - 15)) - 1; + this.mt2 = 2 * m.t; + } +} + +// xR mod m +function montConvert(x) { + var r = nbi(); + x.abs().dlShiftTo(this.m.t,r); + r.divRemTo(this.m,null,r); + if(x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r,r); + return r; +} + +// x/R mod m +function montRevert(x) { + var r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; +} + +// x = x/R mod m (HAC 14.32) +function montReduce(x) { + while(x.t <= this.mt2) // pad x so am has enough room later + x[x.t++] = 0; + for(var i = 0; i < this.m.t; ++i) { + // faster way of calculating u0 = x[i]*mp mod DV + var j = x[i]&0x7fff; + var u0 = (j*this.mpl+(((j*this.mph+(x[i]>>15)*this.mpl)&this.um)<<15))&x.DM; + // use am to combine the multiply-shift-add into one call + j = i+this.m.t; + x[j] += this.m.am(0,u0,x,i,0,this.m.t); + // propagate carry + while(x[j] >= x.DV) { x[j] -= x.DV; x[++j]++; } + } + x.clamp(); + x.drShiftTo(this.m.t,x); + if(x.compareTo(this.m) >= 0) x.subTo(this.m,x); +} + +// r = "x^2/R mod m"; x != r +function montSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +// r = "xy/R mod m"; x,y != r +function montMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + +Montgomery.prototype.convert = montConvert; +Montgomery.prototype.revert = montRevert; +Montgomery.prototype.reduce = montReduce; +Montgomery.prototype.mulTo = montMulTo; +Montgomery.prototype.sqrTo = montSqrTo; + +// (protected) true iff this is even +function bnpIsEven() { return ((this.t>0)?(this[0]&1):this.s) == 0; } + +// (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79) +function bnpExp(e,z) { + if(e > 0xffffffff || e < 1) return BigInteger.ONE; + var r = nbi(), r2 = nbi(), g = z.convert(this), i = nbits(e)-1; + g.copyTo(r); + while(--i >= 0) { + z.sqrTo(r,r2); + if((e&(1< 0) z.mulTo(r2,g,r); + else { var t = r; r = r2; r2 = t; } + } + return z.revert(r); +} + +// (public) this^e % m, 0 <= e < 2^32 +function bnModPowInt(e,m) { + var z; + if(e < 256 || m.isEven()) z = new Classic(m); else z = new Montgomery(m); + return this.exp(e,z); +} + +// protected +BigInteger.prototype.copyTo = bnpCopyTo; +BigInteger.prototype.fromInt = bnpFromInt; +BigInteger.prototype.fromString = bnpFromString; +BigInteger.prototype.clamp = bnpClamp; +BigInteger.prototype.dlShiftTo = bnpDLShiftTo; +BigInteger.prototype.drShiftTo = bnpDRShiftTo; +BigInteger.prototype.lShiftTo = bnpLShiftTo; +BigInteger.prototype.rShiftTo = bnpRShiftTo; +BigInteger.prototype.subTo = bnpSubTo; +BigInteger.prototype.multiplyTo = bnpMultiplyTo; +BigInteger.prototype.squareTo = bnpSquareTo; +BigInteger.prototype.divRemTo = bnpDivRemTo; +BigInteger.prototype.invDigit = bnpInvDigit; +BigInteger.prototype.isEven = bnpIsEven; +BigInteger.prototype.exp = bnpExp; + +// public +BigInteger.prototype.toString = bnToString; +BigInteger.prototype.negate = bnNegate; +BigInteger.prototype.abs = bnAbs; +BigInteger.prototype.compareTo = bnCompareTo; +BigInteger.prototype.bitLength = bnBitLength; +BigInteger.prototype.mod = bnMod; +BigInteger.prototype.modPowInt = bnModPowInt; + +// "constants" +BigInteger.ZERO = nbv(0); +BigInteger.ONE = nbv(1); + +// Copyright (c) 2005-2009 Tom Wu +// All Rights Reserved. +// See "LICENSE" for details. + +// Extended JavaScript BN functions, required for RSA private ops. + +// Version 1.1: new BigInteger("0", 10) returns "proper" zero +// Version 1.2: square() API, isProbablePrime fix + +// (public) +function bnClone() { var r = nbi(); this.copyTo(r); return r; } + +// (public) return value as integer +function bnIntValue() { + if(this.s < 0) { + if(this.t == 1) return this[0]-this.DV; + else if(this.t == 0) return -1; + } + else if(this.t == 1) return this[0]; + else if(this.t == 0) return 0; + // assumes 16 < DB < 32 + return ((this[1]&((1<<(32-this.DB))-1))<>24; } + +// (public) return value as short (assumes DB>=16) +function bnShortValue() { return (this.t==0)?this.s:(this[0]<<16)>>16; } + +// (protected) return x s.t. r^x < DV +function bnpChunkSize(r) { return Math.floor(Math.LN2*this.DB/Math.log(r)); } + +// (public) 0 if this == 0, 1 if this > 0 +function bnSigNum() { + if(this.s < 0) return -1; + else if(this.t <= 0 || (this.t == 1 && this[0] <= 0)) return 0; + else return 1; +} + +// (protected) convert to radix string +function bnpToRadix(b) { + if(b == null) b = 10; + if(this.signum() == 0 || b < 2 || b > 36) return "0"; + var cs = this.chunkSize(b); + var a = Math.pow(b,cs); + var d = nbv(a), y = nbi(), z = nbi(), r = ""; + this.divRemTo(d,y,z); + while(y.signum() > 0) { + r = (a+z.intValue()).toString(b).substr(1) + r; + y.divRemTo(d,y,z); + } + return z.intValue().toString(b) + r; +} + +// (protected) convert from radix string +function bnpFromRadix(s,b) { + this.fromInt(0); + if(b == null) b = 10; + var cs = this.chunkSize(b); + var d = Math.pow(b,cs), mi = false, j = 0, w = 0; + for(var i = 0; i < s.length; ++i) { + var x = intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-" && this.signum() == 0) mi = true; + continue; + } + w = b*w+x; + if(++j >= cs) { + this.dMultiply(d); + this.dAddOffset(w,0); + j = 0; + w = 0; + } + } + if(j > 0) { + this.dMultiply(Math.pow(b,j)); + this.dAddOffset(w,0); + } + if(mi) BigInteger.ZERO.subTo(this,this); +} + +// (protected) alternate constructor +function bnpFromNumber(a,b,c) { + if("number" == typeof b) { + // new BigInteger(int,int,RNG) + if(a < 2) this.fromInt(1); + else { + this.fromNumber(a,c); + if(!this.testBit(a-1)) // force MSB set + this.bitwiseTo(BigInteger.ONE.shiftLeft(a-1),op_or,this); + if(this.isEven()) this.dAddOffset(1,0); // force odd + while(!this.isProbablePrime(b)) { + this.dAddOffset(2,0); + if(this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a-1),this); + } + } + } + else { + // new BigInteger(int,RNG) + var x = [], t = a&7; + x.length = (a>>3)+1; + b.nextBytes(x); + if(t > 0) x[0] &= ((1< 0) { + if(p < this.DB && (d = this[i]>>p) != (this.s&this.DM)>>p) + r[k++] = d|(this.s<<(this.DB-p)); + while(i >= 0) { + if(p < 8) { + d = (this[i]&((1<>(p+=this.DB-8); + } + else { + d = (this[i]>>(p-=8))&0xff; + if(p <= 0) { p += this.DB; --i; } + } + if((d&0x80) != 0) d |= -256; + if(k == 0 && (this.s&0x80) != (d&0x80)) ++k; + if(k > 0 || d != this.s) r[k++] = d; + } + } + return r; +} + +function bnEquals(a) { return(this.compareTo(a)==0); } +function bnMin(a) { return(this.compareTo(a)<0)?this:a; } +function bnMax(a) { return(this.compareTo(a)>0)?this:a; } + +// (protected) r = this op a (bitwise) +function bnpBitwiseTo(a,op,r) { + var i, f, m = Math.min(a.t,this.t); + for(i = 0; i < m; ++i) r[i] = op(this[i],a[i]); + if(a.t < this.t) { + f = a.s&this.DM; + for(i = m; i < this.t; ++i) r[i] = op(this[i],f); + r.t = this.t; + } + else { + f = this.s&this.DM; + for(i = m; i < a.t; ++i) r[i] = op(f,a[i]); + r.t = a.t; + } + r.s = op(this.s,a.s); + r.clamp(); +} + +// (public) this & a +function op_and(x,y) { return x&y; } +function bnAnd(a) { var r = nbi(); this.bitwiseTo(a,op_and,r); return r; } + +// (public) this | a +function op_or(x,y) { return x|y; } +function bnOr(a) { var r = nbi(); this.bitwiseTo(a,op_or,r); return r; } + +// (public) this ^ a +function op_xor(x,y) { return x^y; } +function bnXor(a) { var r = nbi(); this.bitwiseTo(a,op_xor,r); return r; } + +// (public) this & ~a +function op_andnot(x,y) { return x&~y; } +function bnAndNot(a) { var r = nbi(); this.bitwiseTo(a,op_andnot,r); return r; } + +// (public) ~this +function bnNot() { + var r = nbi(); + for(var i = 0; i < this.t; ++i) r[i] = this.DM&~this[i]; + r.t = this.t; + r.s = ~this.s; + return r; +} + +// (public) this << n +function bnShiftLeft(n) { + var r = nbi(); + if(n < 0) this.rShiftTo(-n,r); else this.lShiftTo(n,r); + return r; +} + +// (public) this >> n +function bnShiftRight(n) { + var r = nbi(); + if(n < 0) this.lShiftTo(-n,r); else this.rShiftTo(n,r); + return r; +} + +// return index of lowest 1-bit in x, x < 2^31 +function lbit(x) { + if(x == 0) return -1; + var r = 0; + if((x&0xffff) == 0) { x >>= 16; r += 16; } + if((x&0xff) == 0) { x >>= 8; r += 8; } + if((x&0xf) == 0) { x >>= 4; r += 4; } + if((x&3) == 0) { x >>= 2; r += 2; } + if((x&1) == 0) ++r; + return r; +} + +// (public) returns index of lowest 1-bit (or -1 if none) +function bnGetLowestSetBit() { + for(var i = 0; i < this.t; ++i) + if(this[i] != 0) return i*this.DB+lbit(this[i]); + if(this.s < 0) return this.t*this.DB; + return -1; +} + +// return number of 1 bits in x +function cbit(x) { + var r = 0; + while(x != 0) { x &= x-1; ++r; } + return r; +} + +// (public) return number of set bits +function bnBitCount() { + var r = 0, x = this.s&this.DM; + for(var i = 0; i < this.t; ++i) r += cbit(this[i]^x); + return r; +} + +// (public) true iff nth bit is set +function bnTestBit(n) { + var j = Math.floor(n/this.DB); + if(j >= this.t) return(this.s!=0); + return((this[j]&(1<<(n%this.DB)))!=0); +} + +// (protected) this op (1<>= this.DB; + } + if(a.t < this.t) { + c += a.s; + while(i < this.t) { + c += this[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c += a[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += a.s; + } + r.s = (c<0)?-1:0; + if(c > 0) r[i++] = c; + else if(c < -1) r[i++] = this.DV+c; + r.t = i; + r.clamp(); +} + +// (public) this + a +function bnAdd(a) { var r = nbi(); this.addTo(a,r); return r; } + +// (public) this - a +function bnSubtract(a) { var r = nbi(); this.subTo(a,r); return r; } + +// (public) this * a +function bnMultiply(a) { var r = nbi(); this.multiplyTo(a,r); return r; } + +// (public) this^2 +function bnSquare() { var r = nbi(); this.squareTo(r); return r; } + +// (public) this / a +function bnDivide(a) { var r = nbi(); this.divRemTo(a,r,null); return r; } + +// (public) this % a +function bnRemainder(a) { var r = nbi(); this.divRemTo(a,null,r); return r; } + +// (public) [this/a,this%a] +function bnDivideAndRemainder(a) { + var q = nbi(), r = nbi(); + this.divRemTo(a,q,r); + return new Array(q,r); +} + +// (protected) this *= n, this >= 0, 1 < n < DV +function bnpDMultiply(n) { + this[this.t] = this.am(0,n-1,this,0,0,this.t); + ++this.t; + this.clamp(); +} + +// (protected) this += n << w words, this >= 0 +function bnpDAddOffset(n,w) { + if(n == 0) return; + while(this.t <= w) this[this.t++] = 0; + this[w] += n; + while(this[w] >= this.DV) { + this[w] -= this.DV; + if(++w >= this.t) this[this.t++] = 0; + ++this[w]; + } +} + +// A "null" reducer +function NullExp() {} +function nNop(x) { return x; } +function nMulTo(x,y,r) { x.multiplyTo(y,r); } +function nSqrTo(x,r) { x.squareTo(r); } + +NullExp.prototype.convert = nNop; +NullExp.prototype.revert = nNop; +NullExp.prototype.mulTo = nMulTo; +NullExp.prototype.sqrTo = nSqrTo; + +// (public) this^e +function bnPow(e) { return this.exp(e,new NullExp()); } + +// (protected) r = lower n words of "this * a", a.t <= n +// "this" should be the larger one if appropriate. +function bnpMultiplyLowerTo(a,n,r) { + var i = Math.min(this.t+a.t,n); + r.s = 0; // assumes a,this >= 0 + r.t = i; + while(i > 0) r[--i] = 0; + var j; + for(j = r.t-this.t; i < j; ++i) r[i+this.t] = this.am(0,a[i],r,i,0,this.t); + for(j = Math.min(a.t,n); i < j; ++i) this.am(0,a[i],r,i,0,n-i); + r.clamp(); +} + +// (protected) r = "this * a" without lower n words, n > 0 +// "this" should be the larger one if appropriate. +function bnpMultiplyUpperTo(a,n,r) { + --n; + var i = r.t = this.t+a.t-n; + r.s = 0; // assumes a,this >= 0 + while(--i >= 0) r[i] = 0; + for(i = Math.max(n-this.t,0); i < a.t; ++i) + r[this.t+i-n] = this.am(n-i,a[i],r,0,0,this.t+i-n); + r.clamp(); + r.drShiftTo(1,r); +} + +// Barrett modular reduction +function Barrett(m) { + // setup Barrett + this.r2 = nbi(); + this.q3 = nbi(); + BigInteger.ONE.dlShiftTo(2*m.t,this.r2); + this.mu = this.r2.divide(m); + this.m = m; +} + +function barrettConvert(x) { + if(x.s < 0 || x.t > 2*this.m.t) return x.mod(this.m); + else if(x.compareTo(this.m) < 0) return x; + else { var r = nbi(); x.copyTo(r); this.reduce(r); return r; } +} + +function barrettRevert(x) { return x; } + +// x = x mod m (HAC 14.42) +function barrettReduce(x) { + x.drShiftTo(this.m.t-1,this.r2); + if(x.t > this.m.t+1) { x.t = this.m.t+1; x.clamp(); } + this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3); + this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2); + while(x.compareTo(this.r2) < 0) x.dAddOffset(1,this.m.t+1); + x.subTo(this.r2,x); + while(x.compareTo(this.m) >= 0) x.subTo(this.m,x); +} + +// r = x^2 mod m; x != r +function barrettSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + +// r = x*y mod m; x,y != r +function barrettMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + +Barrett.prototype.convert = barrettConvert; +Barrett.prototype.revert = barrettRevert; +Barrett.prototype.reduce = barrettReduce; +Barrett.prototype.mulTo = barrettMulTo; +Barrett.prototype.sqrTo = barrettSqrTo; + +// (public) this^e % m (HAC 14.85) +function bnModPow(e,m) { + var i = e.bitLength(), k, r = nbv(1), z; + if(i <= 0) return r; + else if(i < 18) k = 1; + else if(i < 48) k = 3; + else if(i < 144) k = 4; + else if(i < 768) k = 5; + else k = 6; + if(i < 8) + z = new Classic(m); + else if(m.isEven()) + z = new Barrett(m); + else + z = new Montgomery(m); + + // precomputation + var g = [], n = 3, k1 = k-1, km = (1< 1) { + var g2 = nbi(); + z.sqrTo(g[1],g2); + while(n <= km) { + g[n] = nbi(); + z.mulTo(g2,g[n-2],g[n]); + n += 2; + } + } + + var j = e.t-1, w, is1 = true, r2 = nbi(), t; + i = nbits(e[j])-1; + while(j >= 0) { + if(i >= k1) w = (e[j]>>(i-k1))&km; + else { + w = (e[j]&((1<<(i+1))-1))<<(k1-i); + if(j > 0) w |= e[j-1]>>(this.DB+i-k1); + } + + n = k; + while((w&1) == 0) { w >>= 1; --n; } + if((i -= n) < 0) { i += this.DB; --j; } + if(is1) { // ret == 1, don't bother squaring or multiplying it + g[w].copyTo(r); + is1 = false; + } + else { + while(n > 1) { z.sqrTo(r,r2); z.sqrTo(r2,r); n -= 2; } + if(n > 0) z.sqrTo(r,r2); else { t = r; r = r2; r2 = t; } + z.mulTo(r2,g[w],r); + } + + while(j >= 0 && (e[j]&(1< 0) { + x.rShiftTo(g,x); + y.rShiftTo(g,y); + } + while(x.signum() > 0) { + if((i = x.getLowestSetBit()) > 0) x.rShiftTo(i,x); + if((i = y.getLowestSetBit()) > 0) y.rShiftTo(i,y); + if(x.compareTo(y) >= 0) { + x.subTo(y,x); + x.rShiftTo(1,x); + } + else { + y.subTo(x,y); + y.rShiftTo(1,y); + } + } + if(g > 0) y.lShiftTo(g,y); + return y; +} + +// (protected) this % n, n < 2^26 +function bnpModInt(n) { + if(n <= 0) return 0; + var d = this.DV%n, r = (this.s<0)?n-1:0; + if(this.t > 0) + if(d == 0) r = this[0]%n; + else for(var i = this.t-1; i >= 0; --i) r = (d*r+this[i])%n; + return r; +} + +// (public) 1/this % m (HAC 14.61) +function bnModInverse(m) { + var ac = m.isEven(); + if((this.isEven() && ac) || m.signum() == 0) return BigInteger.ZERO; + var u = m.clone(), v = this.clone(); + var a = nbv(1), b = nbv(0), c = nbv(0), d = nbv(1); + while(u.signum() != 0) { + while(u.isEven()) { + u.rShiftTo(1,u); + if(ac) { + if(!a.isEven() || !b.isEven()) { a.addTo(this,a); b.subTo(m,b); } + a.rShiftTo(1,a); + } + else if(!b.isEven()) b.subTo(m,b); + b.rShiftTo(1,b); + } + while(v.isEven()) { + v.rShiftTo(1,v); + if(ac) { + if(!c.isEven() || !d.isEven()) { c.addTo(this,c); d.subTo(m,d); } + c.rShiftTo(1,c); + } + else if(!d.isEven()) d.subTo(m,d); + d.rShiftTo(1,d); + } + if(u.compareTo(v) >= 0) { + u.subTo(v,u); + if(ac) a.subTo(c,a); + b.subTo(d,b); + } + else { + v.subTo(u,v); + if(ac) c.subTo(a,c); + d.subTo(b,d); + } + } + if(v.compareTo(BigInteger.ONE) != 0) return BigInteger.ZERO; + if(d.compareTo(m) >= 0) return d.subtract(m); + if(d.signum() < 0) d.addTo(m,d); else return d; + if(d.signum() < 0) return d.add(m); else return d; +} + +var lowprimes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997]; +var lplim = (1<<26)/lowprimes[lowprimes.length-1]; + +// (public) test primality with certainty >= 1-.5^t +function bnIsProbablePrime(t) { + var i, x = this.abs(); + if(x.t == 1 && x[0] <= lowprimes[lowprimes.length-1]) { + for(i = 0; i < lowprimes.length; ++i) + if(x[0] == lowprimes[i]) return true; + return false; + } + if(x.isEven()) return false; + i = 1; + while(i < lowprimes.length) { + var m = lowprimes[i], j = i+1; + while(j < lowprimes.length && m < lplim) m *= lowprimes[j++]; + m = x.modInt(m); + while(i < j) if(m%lowprimes[i++] == 0) return false; + } + return x.millerRabin(t); +} + +// (protected) true if probably prime (HAC 4.24, Miller-Rabin) +function bnpMillerRabin(t) { + var n1 = this.subtract(BigInteger.ONE); + var k = n1.getLowestSetBit(); + if(k <= 0) return false; + var r = n1.shiftRight(k); + t = (t+1)>>1; + if(t > lowprimes.length) t = lowprimes.length; + var a = nbi(); + for(var i = 0; i < t; ++i) { + //Pick bases at random, instead of starting at 2 + a.fromInt(lowprimes[Math.floor(Math.random()*lowprimes.length)]); + var y = a.modPow(r,this); + if(y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) { + var j = 1; + while(j++ < k && y.compareTo(n1) != 0) { + y = y.modPowInt(2,this); + if(y.compareTo(BigInteger.ONE) == 0) return false; + } + if(y.compareTo(n1) != 0) return false; + } + } + return true; +} + +// protected +BigInteger.prototype.chunkSize = bnpChunkSize; +BigInteger.prototype.toRadix = bnpToRadix; +BigInteger.prototype.fromRadix = bnpFromRadix; +BigInteger.prototype.fromNumber = bnpFromNumber; +BigInteger.prototype.bitwiseTo = bnpBitwiseTo; +BigInteger.prototype.changeBit = bnpChangeBit; +BigInteger.prototype.addTo = bnpAddTo; +BigInteger.prototype.dMultiply = bnpDMultiply; +BigInteger.prototype.dAddOffset = bnpDAddOffset; +BigInteger.prototype.multiplyLowerTo = bnpMultiplyLowerTo; +BigInteger.prototype.multiplyUpperTo = bnpMultiplyUpperTo; +BigInteger.prototype.modInt = bnpModInt; +BigInteger.prototype.millerRabin = bnpMillerRabin; + +// public +BigInteger.prototype.clone = bnClone; +BigInteger.prototype.intValue = bnIntValue; +BigInteger.prototype.byteValue = bnByteValue; +BigInteger.prototype.shortValue = bnShortValue; +BigInteger.prototype.signum = bnSigNum; +BigInteger.prototype.toByteArray = bnToByteArray; +BigInteger.prototype.equals = bnEquals; +BigInteger.prototype.min = bnMin; +BigInteger.prototype.max = bnMax; +BigInteger.prototype.and = bnAnd; +BigInteger.prototype.or = bnOr; +BigInteger.prototype.xor = bnXor; +BigInteger.prototype.andNot = bnAndNot; +BigInteger.prototype.not = bnNot; +BigInteger.prototype.shiftLeft = bnShiftLeft; +BigInteger.prototype.shiftRight = bnShiftRight; +BigInteger.prototype.getLowestSetBit = bnGetLowestSetBit; +BigInteger.prototype.bitCount = bnBitCount; +BigInteger.prototype.testBit = bnTestBit; +BigInteger.prototype.setBit = bnSetBit; +BigInteger.prototype.clearBit = bnClearBit; +BigInteger.prototype.flipBit = bnFlipBit; +BigInteger.prototype.add = bnAdd; +BigInteger.prototype.subtract = bnSubtract; +BigInteger.prototype.multiply = bnMultiply; +BigInteger.prototype.divide = bnDivide; +BigInteger.prototype.remainder = bnRemainder; +BigInteger.prototype.divideAndRemainder = bnDivideAndRemainder; +BigInteger.prototype.modPow = bnModPow; +BigInteger.prototype.modInverse = bnModInverse; +BigInteger.prototype.pow = bnPow; +BigInteger.prototype.gcd = bnGCD; +BigInteger.prototype.isProbablePrime = bnIsProbablePrime; + +// JSBN-specific extension +BigInteger.prototype.square = bnSquare; + +// BigInteger interfaces not implemented in jsbn: + +// BigInteger(int signum, byte[] magnitude) +// double doubleValue() +// float floatValue() +// int hashCode() +// long longValue() +// static BigInteger valueOf(long val) + +// Version 1.1: support utf-8 encoding in pkcs1pad2 + +// convert a (hex) string to a bignum object + +function parseBigInt(str,r) { + return new BigInteger(str,r); +} + +// PKCS#1 (type 2, random) pad input string s to n bytes, and return a bigint +function pkcs1pad2(s,n) { + if(n < s.length + 11) { // TODO: fix for utf-8 + console.error("Message too long for RSA"); + return null; + } + var ba = []; + var i = s.length - 1; + while(i >= 0 && n > 0) { + var c = s.charCodeAt(i--); + if(c < 128) { // encode using utf-8 + ba[--n] = c; + } + else if((c > 127) && (c < 2048)) { + ba[--n] = (c & 63) | 128; + ba[--n] = (c >> 6) | 192; + } + else { + ba[--n] = (c & 63) | 128; + ba[--n] = ((c >> 6) & 63) | 128; + ba[--n] = (c >> 12) | 224; + } + } + ba[--n] = 0; + var rng = new SecureRandom(); + var x = []; + while(n > 2) { // random non-zero pad + x[0] = 0; + while(x[0] == 0) rng.nextBytes(x); + ba[--n] = x[0]; + } + ba[--n] = 2; + ba[--n] = 0; + return new BigInteger(ba); +} + +// "empty" RSA key constructor +class RSAKey { + constructor() { + this.n = null; + this.e = 0; + this.d = null; + this.p = null; + this.q = null; + this.dmp1 = null; + this.dmq1 = null; + this.coeff = null; + } +} + +// Set the public key fields N and e from hex strings +function RSASetPublic(N,E) { + if(N != null && E != null && N.length > 0 && E.length > 0) { + this.n = parseBigInt(N,16); + this.e = parseInt(E,16); + } + else + console.error("Invalid RSA public key"); +} + +// Perform raw public operation on "x": return x^e (mod n) +function RSADoPublic(x) { + return x.modPowInt(this.e, this.n); +} + +// Return the PKCS#1 RSA encryption of "text" as an even-length hex string +function RSAEncrypt(text) { + var m = pkcs1pad2(text,(this.n.bitLength()+7)>>3); + if(m == null) return null; + var c = this.doPublic(m); + if(c == null) return null; + var h = c.toString(16); + if((h.length & 1) == 0) return h; else return "0" + h; +} + +// Return the PKCS#1 RSA encryption of "text" as a Base64-encoded string +//function RSAEncryptB64(text) { +// var h = this.encrypt(text); +// if(h) return hex2b64(h); else return null; +//} + +// protected +RSAKey.prototype.doPublic = RSADoPublic; + +// public +RSAKey.prototype.setPublic = RSASetPublic; +RSAKey.prototype.encrypt = RSAEncrypt; +//RSAKey.prototype.encrypt_b64 = RSAEncryptB64; + +// Version 1.1: support utf-8 decoding in pkcs1unpad2 + +// Undo PKCS#1 (type 2, random) padding and, if valid, return the plaintext + +function pkcs1unpad2(d,n) { + var b = d.toByteArray(); + var i = 0; + while(i < b.length && b[i] == 0) ++i; + if(b.length-i != n-1 || b[i] != 2) + return null; + ++i; + while(b[i] != 0) + if(++i >= b.length) return null; + var ret = ""; + while(++i < b.length) { + var c = b[i] & 255; + if(c < 128) { // utf-8 decode + ret += String.fromCharCode(c); + } + else if((c > 191) && (c < 224)) { + ret += String.fromCharCode(((c & 31) << 6) | (b[i+1] & 63)); + ++i; + } + else { + ret += String.fromCharCode(((c & 15) << 12) | ((b[i+1] & 63) << 6) | (b[i+2] & 63)); + i += 2; + } + } + return ret; +} + +// Set the private key fields N, e, and d from hex strings +function RSASetPrivate(N,E,D) { + if(N != null && E != null && N.length > 0 && E.length > 0) { + this.n = parseBigInt(N,16); + this.e = parseInt(E,16); + this.d = parseBigInt(D,16); + } + else + console.error("Invalid RSA private key"); +} + +// Set the private key fields N, e, d and CRT params from hex strings +function RSASetPrivateEx(N,E,D,P,Q,DP,DQ,C) { + if(N != null && E != null && N.length > 0 && E.length > 0) { + this.n = parseBigInt(N,16); + this.e = parseInt(E,16); + this.d = parseBigInt(D,16); + this.p = parseBigInt(P,16); + this.q = parseBigInt(Q,16); + this.dmp1 = parseBigInt(DP,16); + this.dmq1 = parseBigInt(DQ,16); + this.coeff = parseBigInt(C,16); + } + else + console.error("Invalid RSA private key"); +} + +// Generate a new random private key B bits long, using public expt E +function RSAGenerate(B,E) { + var rng = new SecureRandom(); + var qs = B>>1; + this.e = parseInt(E,16); + var ee = new BigInteger(E,16); + for(;;) { + for(;;) { + this.p = new BigInteger(B-qs,1,rng); + if(this.p.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) == 0 && this.p.isProbablePrime(10)) break; + } + for(;;) { + this.q = new BigInteger(qs,1,rng); + if(this.q.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) == 0 && this.q.isProbablePrime(10)) break; + } + if(this.p.compareTo(this.q) <= 0) { + var t = this.p; + this.p = this.q; + this.q = t; + } + var p1 = this.p.subtract(BigInteger.ONE); + var q1 = this.q.subtract(BigInteger.ONE); + var phi = p1.multiply(q1); + if(phi.gcd(ee).compareTo(BigInteger.ONE) == 0) { + this.n = this.p.multiply(this.q); + this.d = ee.modInverse(phi); + this.dmp1 = this.d.mod(p1); + this.dmq1 = this.d.mod(q1); + this.coeff = this.q.modInverse(this.p); + break; + } + } +} + +// Perform raw private operation on "x": return x^d (mod n) +function RSADoPrivate(x) { + if(this.p == null || this.q == null) + return x.modPow(this.d, this.n); + + // TODO: re-calculate any missing CRT params + var xp = x.mod(this.p).modPow(this.dmp1, this.p); + var xq = x.mod(this.q).modPow(this.dmq1, this.q); + + while(xp.compareTo(xq) < 0) + xp = xp.add(this.p); + return xp.subtract(xq).multiply(this.coeff).mod(this.p).multiply(this.q).add(xq); +} + +// Return the PKCS#1 RSA decryption of "ctext". +// "ctext" is an even-length hex string and the output is a plain string. +function RSADecrypt(ctext) { + var c = parseBigInt(ctext, 16); + var m = this.doPrivate(c); + if(m == null) return null; + return pkcs1unpad2(m, (this.n.bitLength()+7)>>3); +} + +// Return the PKCS#1 RSA decryption of "ctext". +// "ctext" is a Base64-encoded string and the output is a plain string. +//function RSAB64Decrypt(ctext) { +// var h = b64tohex(ctext); +// if(h) return this.decrypt(h); else return null; +//} + +// protected +RSAKey.prototype.doPrivate = RSADoPrivate; + +// public +RSAKey.prototype.setPrivate = RSASetPrivate; +RSAKey.prototype.setPrivateEx = RSASetPrivateEx; +RSAKey.prototype.generate = RSAGenerate; +RSAKey.prototype.decrypt = RSADecrypt; +//RSAKey.prototype.b64_decrypt = RSAB64Decrypt; + +// Base64 JavaScript decoder +// Copyright (c) 2008-2013 Lapo Luchini + +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +/*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ + +const Base64 = {}; +let decoder; + +Base64.decode = function (a) { + var i; + if (decoder === undefined) { + var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", + ignore = "= \f\n\r\t\u00A0\u2028\u2029"; + decoder = []; + for (i = 0; i < 64; ++i) + decoder[b64.charAt(i)] = i; + for (i = 0; i < ignore.length; ++i) + decoder[ignore.charAt(i)] = -1; + } + var out = []; + var bits = 0, char_count = 0; + for (i = 0; i < a.length; ++i) { + var c = a.charAt(i); + if (c == '=') + break; + c = decoder[c]; + if (c == -1) + continue; + if (c === undefined) + throw 'Illegal character at offset ' + i; + bits |= c; + if (++char_count >= 4) { + out[out.length] = (bits >> 16); + out[out.length] = (bits >> 8) & 0xFF; + out[out.length] = bits & 0xFF; + bits = 0; + char_count = 0; + } else { + bits <<= 6; + } + } + switch (char_count) { + case 1: + throw "Base64 encoding incomplete: at least 2 bits missing"; + case 2: + out[out.length] = (bits >> 10); + break; + case 3: + out[out.length] = (bits >> 16); + out[out.length] = (bits >> 8) & 0xFF; + break; + } + return out; +}; + +Base64.re = /-----BEGIN [^-]+-----([A-Za-z0-9+\/=\s]+)-----END [^-]+-----|begin-base64[^\n]+\n([A-Za-z0-9+\/=\s]+)====/; +Base64.unarmor = function (a) { + var m = Base64.re.exec(a); + if (m) { + if (m[1]) + a = m[1]; + else if (m[2]) + a = m[2]; + else + throw "RegExp out of sync"; + } + return Base64.decode(a); +}; + +// Hex JavaScript decoder +// Copyright (c) 2008-2013 Lapo Luchini + +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +/*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ +const Hex = {}; +let decoder$1; + +Hex.decode = function(a) { + var i; + if (decoder$1 === undefined) { + var hex = "0123456789ABCDEF", + ignore = " \f\n\r\t\u00A0\u2028\u2029"; + decoder$1 = []; + for (i = 0; i < 16; ++i) + decoder$1[hex.charAt(i)] = i; + hex = hex.toLowerCase(); + for (i = 10; i < 16; ++i) + decoder$1[hex.charAt(i)] = i; + for (i = 0; i < ignore.length; ++i) + decoder$1[ignore.charAt(i)] = -1; + } + var out = [], + bits = 0, + char_count = 0; + for (i = 0; i < a.length; ++i) { + var c = a.charAt(i); + if (c == '=') + break; + c = decoder$1[c]; + if (c == -1) + continue; + if (c === undefined) + throw 'Illegal character at offset ' + i; + bits |= c; + if (++char_count >= 2) { + out[out.length] = bits; + bits = 0; + char_count = 0; + } else { + bits <<= 4; + } + } + if (char_count) + throw "Hex encoding incomplete: 4 bits missing"; + return out; +}; + +/*! asn1-1.0.2.js (c) 2013 Kenji Urushima | kjur.github.com/jsrsasign/license + */ + +const JSX = /*window.JSX || */{}; +JSX.env = JSX.env || {}; + +var L = JSX; +var OP = Object.prototype; +var FUNCTION_TOSTRING = '[object Function]'; +var ADD = ["toString", "valueOf"]; + +JSX.env.parseUA = function(agent) { + + var numberify = function(s) { + var c = 0; + return parseFloat(s.replace(/\./g, function() { + return (c++ == 1) ? '' : '.'; + })); + }, + + nav = navigator, + o = { + ie: 0, + opera: 0, + gecko: 0, + webkit: 0, + chrome: 0, + mobile: null, + air: 0, + ipad: 0, + iphone: 0, + ipod: 0, + ios: null, + android: 0, + webos: 0, + caja: nav && nav.cajaVersion, + secure: false, + os: null + + }, + + ua = agent || (navigator && navigator.userAgent), + loc = window && window.location, + href = loc && loc.href, + m; + + o.secure = href && (href.toLowerCase().indexOf("https") === 0); + + if (ua) { + + if ((/windows|win32/i).test(ua)) { + o.os = 'windows'; + } else if ((/macintosh/i).test(ua)) { + o.os = 'macintosh'; + } else if ((/rhino/i).test(ua)) { + o.os = 'rhino'; + } + if ((/KHTML/).test(ua)) { + o.webkit = 1; + } + m = ua.match(/AppleWebKit\/([^\s]*)/); + if (m && m[1]) { + o.webkit = numberify(m[1]); + if (/ Mobile\//.test(ua)) { + o.mobile = 'Apple'; // iPhone or iPod Touch + m = ua.match(/OS ([^\s]*)/); + if (m && m[1]) { + m = numberify(m[1].replace('_', '.')); + } + o.ios = m; + o.ipad = o.ipod = o.iphone = 0; + m = ua.match(/iPad|iPod|iPhone/); + if (m && m[0]) { + o[m[0].toLowerCase()] = o.ios; + } + } else { + m = ua.match(/NokiaN[^\/]*|Android \d\.\d|webOS\/\d\.\d/); + if (m) { + o.mobile = m[0]; + } + if (/webOS/.test(ua)) { + o.mobile = 'WebOS'; + m = ua.match(/webOS\/([^\s]*);/); + if (m && m[1]) { + o.webos = numberify(m[1]); + } + } + if (/ Android/.test(ua)) { + o.mobile = 'Android'; + m = ua.match(/Android ([^\s]*);/); + if (m && m[1]) { + o.android = numberify(m[1]); + } + } + } + m = ua.match(/Chrome\/([^\s]*)/); + if (m && m[1]) { + o.chrome = numberify(m[1]); // Chrome + } else { + m = ua.match(/AdobeAIR\/([^\s]*)/); + if (m) { + o.air = m[0]; // Adobe AIR 1.0 or better + } + } + } + if (!o.webkit) { + m = ua.match(/Opera[\s\/]([^\s]*)/); + if (m && m[1]) { + o.opera = numberify(m[1]); + m = ua.match(/Version\/([^\s]*)/); + if (m && m[1]) { + o.opera = numberify(m[1]); // opera 10+ + } + m = ua.match(/Opera Mini[^;]*/); + if (m) { + o.mobile = m[0]; // ex: Opera Mini/2.0.4509/1316 + } + } else { // not opera or webkit + m = ua.match(/MSIE\s([^;]*)/); + if (m && m[1]) { + o.ie = numberify(m[1]); + } else { // not opera, webkit, or ie + m = ua.match(/Gecko\/([^\s]*)/); + if (m) { + o.gecko = 1; // Gecko detected, look for revision + m = ua.match(/rv:([^\s\)]*)/); + if (m && m[1]) { + o.gecko = numberify(m[1]); + } + } + } + } + } + } + return o; +}; + +JSX.env.ua = JSX.env.parseUA(); + +JSX.isFunction = function(o) { + return (typeof o === 'function') || OP.toString.apply(o) === FUNCTION_TOSTRING; +}; + +JSX._IEEnumFix = (JSX.env.ua.ie) ? function(r, s) { + var i, fname, f; + for (i=0;iMIT License + */ + +/** + * kjur's class library name space + *

+ * This name space provides following name spaces: + *

    + *
  • {@link KJUR.asn1} - ASN.1 primitive hexadecimal encoder
  • + *
  • {@link KJUR.asn1.x509} - ASN.1 structure for X.509 certificate and CRL
  • + *
  • {@link KJUR.crypto} - Java Cryptographic Extension(JCE) style MessageDigest/Signature + * class and utilities
  • + *
+ *

+ * NOTE: Please ignore method summary and document of this namespace. This caused by a bug of jsdoc2. + * @name KJUR + * @namespace kjur's class library name space + */ +// if (typeof KJUR == "undefined" || !KJUR) +const KJUR = {}; + +/** + * kjur's ASN.1 class library name space + *

+ * This is ITU-T X.690 ASN.1 DER encoder class library and + * class structure and methods is very similar to + * org.bouncycastle.asn1 package of + * well known BouncyCaslte Cryptography Library. + * + *

PROVIDING ASN.1 PRIMITIVES

+ * Here are ASN.1 DER primitive classes. + *
    + *
  • {@link KJUR.asn1.DERBoolean}
  • + *
  • {@link KJUR.asn1.DERInteger}
  • + *
  • {@link KJUR.asn1.DERBitString}
  • + *
  • {@link KJUR.asn1.DEROctetString}
  • + *
  • {@link KJUR.asn1.DERNull}
  • + *
  • {@link KJUR.asn1.DERObjectIdentifier}
  • + *
  • {@link KJUR.asn1.DERUTF8String}
  • + *
  • {@link KJUR.asn1.DERNumericString}
  • + *
  • {@link KJUR.asn1.DERPrintableString}
  • + *
  • {@link KJUR.asn1.DERTeletexString}
  • + *
  • {@link KJUR.asn1.DERIA5String}
  • + *
  • {@link KJUR.asn1.DERUTCTime}
  • + *
  • {@link KJUR.asn1.DERGeneralizedTime}
  • + *
  • {@link KJUR.asn1.DERSequence}
  • + *
  • {@link KJUR.asn1.DERSet}
  • + *
+ * + *

OTHER ASN.1 CLASSES

+ *
    + *
  • {@link KJUR.asn1.ASN1Object}
  • + *
  • {@link KJUR.asn1.DERAbstractString}
  • + *
  • {@link KJUR.asn1.DERAbstractTime}
  • + *
  • {@link KJUR.asn1.DERAbstractStructured}
  • + *
  • {@link KJUR.asn1.DERTaggedObject}
  • + *
+ *

+ * NOTE: Please ignore method summary and document of this namespace. This caused by a bug of jsdoc2. + * @name KJUR.asn1 + * @namespace + */ +if (typeof KJUR.asn1 == "undefined" || !KJUR.asn1) KJUR.asn1 = {}; + +/** + * ASN1 utilities class + * @name KJUR.asn1.ASN1Util + * @classs ASN1 utilities class + * @since asn1 1.0.2 + */ +KJUR.asn1.ASN1Util = new function() { + this.integerToByteHex = function(i) { + var h = i.toString(16); + if ((h.length % 2) == 1) h = '0' + h; + return h; + }; + this.bigIntToMinTwosComplementsHex = function(bigIntegerValue) { + var h = bigIntegerValue.toString(16); + if (h.substr(0, 1) != '-') { + if (h.length % 2 == 1) { + h = '0' + h; + } else { + if (! h.match(/^[0-7]/)) { + h = '00' + h; + } + } + } else { + var hPos = h.substr(1); + var xorLen = hPos.length; + if (xorLen % 2 == 1) { + xorLen += 1; + } else { + if (! h.match(/^[0-7]/)) { + xorLen += 2; + } + } + var hMask = ''; + for (var i = 0; i < xorLen; i++) { + hMask += 'f'; + } + var biMask = new BigInteger(hMask, 16); + var biNeg = biMask.xor(bigIntegerValue).add(BigInteger.ONE); + h = biNeg.toString(16).replace(/^-/, ''); + } + return h; + }; + /** + * get PEM string from hexadecimal data and header string + * @name getPEMStringFromHex + * @memberOf KJUR.asn1.ASN1Util + * @function + * @param {String} dataHex hexadecimal string of PEM body + * @param {String} pemHeader PEM header string (ex. 'RSA PRIVATE KEY') + * @return {String} PEM formatted string of input data + * @description + * @example + * var pem = KJUR.asn1.ASN1Util.getPEMStringFromHex('616161', 'RSA PRIVATE KEY'); + * // value of pem will be: + * -----BEGIN PRIVATE KEY----- + * YWFh + * -----END PRIVATE KEY----- + */ + this.getPEMStringFromHex = function(dataHex, pemHeader) { + var dataWA = CryptoJS.enc.Hex.parse(dataHex); + var dataB64 = CryptoJS.enc.Base64.stringify(dataWA); + var pemBody = dataB64.replace(/(.{64})/g, "$1\r\n"); + pemBody = pemBody.replace(/\r\n$/, ''); + return "-----BEGIN " + pemHeader + "-----\r\n" + + pemBody + + "\r\n-----END " + pemHeader + "-----\r\n"; + }; +}; + +// ******************************************************************** +// Abstract ASN.1 Classes +// ******************************************************************** + +// ******************************************************************** + +/** + * base class for ASN.1 DER encoder object + * @name KJUR.asn1.ASN1Object + * @class base class for ASN.1 DER encoder object + * @property {Boolean} isModified flag whether internal data was changed + * @property {String} hTLV hexadecimal string of ASN.1 TLV + * @property {String} hT hexadecimal string of ASN.1 TLV tag(T) + * @property {String} hL hexadecimal string of ASN.1 TLV length(L) + * @property {String} hV hexadecimal string of ASN.1 TLV value(V) + * @description + */ +KJUR.asn1.ASN1Object = function() { + var hV = ''; + + /** + * get hexadecimal ASN.1 TLV length(L) bytes from TLV value(V) + * @name getLengthHexFromValue + * @memberOf KJUR.asn1.ASN1Object + * @function + * @return {String} hexadecimal string of ASN.1 TLV length(L) + */ + this.getLengthHexFromValue = function() { + if (typeof this.hV == "undefined" || this.hV == null) { + throw "this.hV is null or undefined."; + } + if (this.hV.length % 2 == 1) { + throw "value hex must be even length: n=" + hV.length + ",v=" + this.hV; + } + var n = this.hV.length / 2; + var hN = n.toString(16); + if (hN.length % 2 == 1) { + hN = "0" + hN; + } + if (n < 128) { + return hN; + } else { + var hNlen = hN.length / 2; + if (hNlen > 15) { + throw "ASN.1 length too long to represent by 8x: n = " + n.toString(16); + } + var head = 128 + hNlen; + return head.toString(16) + hN; + } + }; + + /** + * get hexadecimal string of ASN.1 TLV bytes + * @name getEncodedHex + * @memberOf KJUR.asn1.ASN1Object + * @function + * @return {String} hexadecimal string of ASN.1 TLV + */ + this.getEncodedHex = function() { + if (this.hTLV == null || this.isModified) { + this.hV = this.getFreshValueHex(); + this.hL = this.getLengthHexFromValue(); + this.hTLV = this.hT + this.hL + this.hV; + this.isModified = false; + //console.error("first time: " + this.hTLV); + } + return this.hTLV; + }; + + /** + * get hexadecimal string of ASN.1 TLV value(V) bytes + * @name getValueHex + * @memberOf KJUR.asn1.ASN1Object + * @function + * @return {String} hexadecimal string of ASN.1 TLV value(V) bytes + */ + this.getValueHex = function() { + this.getEncodedHex(); + return this.hV; + }; + + this.getFreshValueHex = function() { + return ''; + }; +}; + +// == BEGIN DERAbstractString ================================================ +/** + * base class for ASN.1 DER string classes + * @name KJUR.asn1.DERAbstractString + * @class base class for ASN.1 DER string classes + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @property {String} s internal string of value + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • str - specify initial ASN.1 value(V) by a string
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERAbstractString = function(params) { + KJUR.asn1.DERAbstractString.superclass.constructor.call(this); + this.getString = function() { + return this.s; + }; + + /** + * set value by a string + * @name setString + * @memberOf KJUR.asn1.DERAbstractString + * @function + * @param {String} newS value by a string to set + */ + this.setString = function(newS) { + this.hTLV = null; + this.isModified = true; + this.s = newS; + this.hV = stohex(this.s); + }; + + /** + * set value by a hexadecimal string + * @name setStringHex + * @memberOf KJUR.asn1.DERAbstractString + * @function + * @param {String} newHexString value by a hexadecimal string to set + */ + this.setStringHex = function(newHexString) { + this.hTLV = null; + this.isModified = true; + this.s = null; + this.hV = newHexString; + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['str'] != "undefined") { + this.setString(params['str']); + } else if (typeof params['hex'] != "undefined") { + this.setStringHex(params['hex']); + } + } +}; +JSX.extend(KJUR.asn1.DERAbstractString, KJUR.asn1.ASN1Object); +// == END DERAbstractString ================================================ + +// == BEGIN DERAbstractTime ================================================== +/** + * base class for ASN.1 DER Generalized/UTCTime class + * @name KJUR.asn1.DERAbstractTime + * @class base class for ASN.1 DER Generalized/UTCTime class + * @param {Array} params associative array of parameters (ex. {'str': '130430235959Z'}) + * @extends KJUR.asn1.ASN1Object + * @description + * @see KJUR.asn1.ASN1Object - superclass + */ +KJUR.asn1.DERAbstractTime = function(params) { + KJUR.asn1.DERAbstractTime.superclass.constructor.call(this); + this.localDateToUTC = function(d) { + utc = d.getTime() + (d.getTimezoneOffset() * 60000); + var utcDate = new Date(utc); + return utcDate; + }; + + this.formatDate = function(dateObject, type) { + var pad = this.zeroPadding; + var d = this.localDateToUTC(dateObject); + var year = String(d.getFullYear()); + if (type == 'utc') year = year.substr(2, 2); + var month = pad(String(d.getMonth() + 1), 2); + var day = pad(String(d.getDate()), 2); + var hour = pad(String(d.getHours()), 2); + var min = pad(String(d.getMinutes()), 2); + var sec = pad(String(d.getSeconds()), 2); + return year + month + day + hour + min + sec + 'Z'; + }; + + this.zeroPadding = function(s, len) { + if (s.length >= len) return s; + return new Array(len - s.length + 1).join('0') + s; + }; + + // --- PUBLIC METHODS -------------------- + /** + * get string value of this string object + * @name getString + * @memberOf KJUR.asn1.DERAbstractTime + * @function + * @return {String} string value of this time object + */ + this.getString = function() { + return this.s; + }; + + /** + * set value by a string + * @name setString + * @memberOf KJUR.asn1.DERAbstractTime + * @function + * @param {String} newS value by a string to set such like "130430235959Z" + */ + this.setString = function(newS) { + this.hTLV = null; + this.isModified = true; + this.s = newS; + this.hV = stohex(this.s); + }; + + /** + * set value by a Date object + * @name setByDateValue + * @memberOf KJUR.asn1.DERAbstractTime + * @function + * @param {Integer} year year of date (ex. 2013) + * @param {Integer} month month of date between 1 and 12 (ex. 12) + * @param {Integer} day day of month + * @param {Integer} hour hours of date + * @param {Integer} min minutes of date + * @param {Integer} sec seconds of date + */ + this.setByDateValue = function(year, month, day, hour, min, sec) { + var dateObject = new Date(Date.UTC(year, month - 1, day, hour, min, sec, 0)); + this.setByDate(dateObject); + }; + + this.getFreshValueHex = function() { + return this.hV; + }; +}; +JSX.extend(KJUR.asn1.DERAbstractTime, KJUR.asn1.ASN1Object); +// == END DERAbstractTime ================================================== + +// == BEGIN DERAbstractStructured ============================================ +/** + * base class for ASN.1 DER structured class + * @name KJUR.asn1.DERAbstractStructured + * @class base class for ASN.1 DER structured class + * @property {Array} asn1Array internal array of ASN1Object + * @extends KJUR.asn1.ASN1Object + * @description + * @see KJUR.asn1.ASN1Object - superclass + */ +KJUR.asn1.DERAbstractStructured = function(params) { + KJUR.asn1.DERAbstractString.superclass.constructor.call(this); + this.setByASN1ObjectArray = function(asn1ObjectArray) { + this.hTLV = null; + this.isModified = true; + this.asn1Array = asn1ObjectArray; + }; + + /** + * append an ASN1Object to internal array + * @name appendASN1Object + * @memberOf KJUR.asn1.DERAbstractStructured + * @function + * @param {ASN1Object} asn1Object to add + */ + this.appendASN1Object = function(asn1Object) { + this.hTLV = null; + this.isModified = true; + this.asn1Array.push(asn1Object); + }; + + this.asn1Array = new Array(); + if (typeof params != "undefined") { + if (typeof params['array'] != "undefined") { + this.asn1Array = params['array']; + } + } +}; +JSX.extend(KJUR.asn1.DERAbstractStructured, KJUR.asn1.ASN1Object); + + +// ******************************************************************** +// ASN.1 Object Classes +// ******************************************************************** + +// ******************************************************************** +/** + * class for ASN.1 DER Boolean + * @name KJUR.asn1.DERBoolean + * @class class for ASN.1 DER Boolean + * @extends KJUR.asn1.ASN1Object + * @description + * @see KJUR.asn1.ASN1Object - superclass + */ +KJUR.asn1.DERBoolean = function() { + KJUR.asn1.DERBoolean.superclass.constructor.call(this); + this.hT = "01"; + this.hTLV = "0101ff"; +}; +JSX.extend(KJUR.asn1.DERBoolean, KJUR.asn1.ASN1Object); + +// ******************************************************************** +/** + * class for ASN.1 DER Integer + * @name KJUR.asn1.DERInteger + * @class class for ASN.1 DER Integer + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • int - specify initial ASN.1 value(V) by integer value
  • + *
  • bigint - specify initial ASN.1 value(V) by BigInteger object
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERInteger = function(params) { + KJUR.asn1.DERInteger.superclass.constructor.call(this); + this.hT = "02"; + + /** + * set value by Tom Wu's BigInteger object + * @name setByBigInteger + * @memberOf KJUR.asn1.DERInteger + * @function + * @param {BigInteger} bigIntegerValue to set + */ + this.setByBigInteger = function(bigIntegerValue) { + this.hTLV = null; + this.isModified = true; + this.hV = KJUR.asn1.ASN1Util.bigIntToMinTwosComplementsHex(bigIntegerValue); + }; + + /** + * set value by integer value + * @name setByInteger + * @memberOf KJUR.asn1.DERInteger + * @function + * @param {Integer} integer value to set + */ + this.setByInteger = function(intValue) { + var bi = new BigInteger(String(intValue), 10); + this.setByBigInteger(bi); + }; + + /** + * set value by integer value + * @name setValueHex + * @memberOf KJUR.asn1.DERInteger + * @function + * @param {String} hexadecimal string of integer value + * @description + *
+ * NOTE: Value shall be represented by minimum octet length of + * two's complement representation. + */ + this.setValueHex = function(newHexString) { + this.hV = newHexString; + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['bigint'] != "undefined") { + this.setByBigInteger(params['bigint']); + } else if (typeof params['int'] != "undefined") { + this.setByInteger(params['int']); + } else if (typeof params['hex'] != "undefined") { + this.setValueHex(params['hex']); + } + } +}; +JSX.extend(KJUR.asn1.DERInteger, KJUR.asn1.ASN1Object); + +// ******************************************************************** +/** + * class for ASN.1 DER encoded BitString primitive + * @name KJUR.asn1.DERBitString + * @class class for ASN.1 DER encoded BitString primitive + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • bin - specify binary string (ex. '10111')
  • + *
  • array - specify array of boolean (ex. [true,false,true,true])
  • + *
  • hex - specify hexadecimal string of ASN.1 value(V) including unused bits
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERBitString = function(params) { + KJUR.asn1.DERBitString.superclass.constructor.call(this); + this.hT = "03"; + + /** + * set ASN.1 value(V) by a hexadecimal string including unused bits + * @name setHexValueIncludingUnusedBits + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {String} newHexStringIncludingUnusedBits + */ + this.setHexValueIncludingUnusedBits = function(newHexStringIncludingUnusedBits) { + this.hTLV = null; + this.isModified = true; + this.hV = newHexStringIncludingUnusedBits; + }; + + /** + * set ASN.1 value(V) by unused bit and hexadecimal string of value + * @name setUnusedBitsAndHexValue + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {Integer} unusedBits + * @param {String} hValue + */ + this.setUnusedBitsAndHexValue = function(unusedBits, hValue) { + if (unusedBits < 0 || 7 < unusedBits) { + throw "unused bits shall be from 0 to 7: u = " + unusedBits; + } + var hUnusedBits = "0" + unusedBits; + this.hTLV = null; + this.isModified = true; + this.hV = hUnusedBits + hValue; + }; + + /** + * set ASN.1 DER BitString by binary string + * @name setByBinaryString + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {String} binaryString binary value string (i.e. '10111') + * @description + * Its unused bits will be calculated automatically by length of + * 'binaryValue'.
+ * NOTE: Trailing zeros '0' will be ignored. + */ + this.setByBinaryString = function(binaryString) { + binaryString = binaryString.replace(/0+$/, ''); + var unusedBits = 8 - binaryString.length % 8; + if (unusedBits == 8) unusedBits = 0; + for (var i = 0; i <= unusedBits; i++) { + binaryString += '0'; + } + var h = ''; + for (var i = 0; i < binaryString.length - 1; i += 8) { + var b = binaryString.substr(i, 8); + var x = parseInt(b, 2).toString(16); + if (x.length == 1) x = '0' + x; + h += x; + } + this.hTLV = null; + this.isModified = true; + this.hV = '0' + unusedBits + h; + }; + + /** + * set ASN.1 TLV value(V) by an array of boolean + * @name setByBooleanArray + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {array} booleanArray array of boolean (ex. [true, false, true]) + * @description + * NOTE: Trailing falses will be ignored. + */ + this.setByBooleanArray = function(booleanArray) { + var s = ''; + for (var i = 0; i < booleanArray.length; i++) { + if (booleanArray[i] == true) { + s += '1'; + } else { + s += '0'; + } + } + this.setByBinaryString(s); + }; + + /** + * generate an array of false with specified length + * @name newFalseArray + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {Integer} nLength length of array to generate + * @return {array} array of boolean faluse + * @description + * This static method may be useful to initialize boolean array. + */ + this.newFalseArray = function(nLength) { + var a = new Array(nLength); + for (var i = 0; i < nLength; i++) { + a[i] = false; + } + return a; + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['hex'] != "undefined") { + this.setHexValueIncludingUnusedBits(params['hex']); + } else if (typeof params['bin'] != "undefined") { + this.setByBinaryString(params['bin']); + } else if (typeof params['array'] != "undefined") { + this.setByBooleanArray(params['array']); + } + } +}; +JSX.extend(KJUR.asn1.DERBitString, KJUR.asn1.ASN1Object); + +// ******************************************************************** +/** + * class for ASN.1 DER OctetString + * @name KJUR.asn1.DEROctetString + * @class class for ASN.1 DER OctetString + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ +KJUR.asn1.DEROctetString = function(params) { + KJUR.asn1.DEROctetString.superclass.constructor.call(this, params); + this.hT = "04"; +}; +JSX.extend(KJUR.asn1.DEROctetString, KJUR.asn1.DERAbstractString); + +// ******************************************************************** +/** + * class for ASN.1 DER Null + * @name KJUR.asn1.DERNull + * @class class for ASN.1 DER Null + * @extends KJUR.asn1.ASN1Object + * @description + * @see KJUR.asn1.ASN1Object - superclass + */ +KJUR.asn1.DERNull = function() { + KJUR.asn1.DERNull.superclass.constructor.call(this); + this.hT = "05"; + this.hTLV = "0500"; +}; +JSX.extend(KJUR.asn1.DERNull, KJUR.asn1.ASN1Object); + +// ******************************************************************** +/** + * class for ASN.1 DER ObjectIdentifier + * @name KJUR.asn1.DERObjectIdentifier + * @class class for ASN.1 DER ObjectIdentifier + * @param {Array} params associative array of parameters (ex. {'oid': '2.5.4.5'}) + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • oid - specify initial ASN.1 value(V) by a oid string (ex. 2.5.4.13)
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERObjectIdentifier = function(params) { + var itox = function(i) { + var h = i.toString(16); + if (h.length == 1) h = '0' + h; + return h; + }; + var roidtox = function(roid) { + var h = ''; + var bi = new BigInteger(roid, 10); + var b = bi.toString(2); + var padLen = 7 - b.length % 7; + if (padLen == 7) padLen = 0; + var bPad = ''; + for (var i = 0; i < padLen; i++) bPad += '0'; + b = bPad + b; + for (var i = 0; i < b.length - 1; i += 7) { + var b8 = b.substr(i, 7); + if (i != b.length - 7) b8 = '1' + b8; + h += itox(parseInt(b8, 2)); + } + return h; + }; + + KJUR.asn1.DERObjectIdentifier.superclass.constructor.call(this); + this.hT = "06"; + + /** + * set value by a hexadecimal string + * @name setValueHex + * @memberOf KJUR.asn1.DERObjectIdentifier + * @function + * @param {String} newHexString hexadecimal value of OID bytes + */ + this.setValueHex = function(newHexString) { + this.hTLV = null; + this.isModified = true; + this.s = null; + this.hV = newHexString; + }; + + /** + * set value by a OID string + * @name setValueOidString + * @memberOf KJUR.asn1.DERObjectIdentifier + * @function + * @param {String} oidString OID string (ex. 2.5.4.13) + */ + this.setValueOidString = function(oidString) { + if (! oidString.match(/^[0-9.]+$/)) { + throw "malformed oid string: " + oidString; + } + var h = ''; + var a = oidString.split('.'); + var i0 = parseInt(a[0]) * 40 + parseInt(a[1]); + h += itox(i0); + a.splice(0, 2); + for (var i = 0; i < a.length; i++) { + h += roidtox(a[i]); + } + this.hTLV = null; + this.isModified = true; + this.s = null; + this.hV = h; + }; + + /** + * set value by a OID name + * @name setValueName + * @memberOf KJUR.asn1.DERObjectIdentifier + * @function + * @param {String} oidName OID name (ex. 'serverAuth') + * @since 1.0.1 + * @description + * OID name shall be defined in 'KJUR.asn1.x509.OID.name2oidList'. + * Otherwise raise error. + */ + this.setValueName = function(oidName) { + if (typeof KJUR.asn1.x509.OID.name2oidList[oidName] != "undefined") { + var oid = KJUR.asn1.x509.OID.name2oidList[oidName]; + this.setValueOidString(oid); + } else { + throw "DERObjectIdentifier oidName undefined: " + oidName; + } + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['oid'] != "undefined") { + this.setValueOidString(params['oid']); + } else if (typeof params['hex'] != "undefined") { + this.setValueHex(params['hex']); + } else if (typeof params['name'] != "undefined") { + this.setValueName(params['name']); + } + } +}; +JSX.extend(KJUR.asn1.DERObjectIdentifier, KJUR.asn1.ASN1Object); + +// ******************************************************************** +/** + * class for ASN.1 DER UTF8String + * @name KJUR.asn1.DERUTF8String + * @class class for ASN.1 DER UTF8String + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ +KJUR.asn1.DERUTF8String = function(params) { + KJUR.asn1.DERUTF8String.superclass.constructor.call(this, params); + this.hT = "0c"; +}; +JSX.extend(KJUR.asn1.DERUTF8String, KJUR.asn1.DERAbstractString); + +// ******************************************************************** +/** + * class for ASN.1 DER NumericString + * @name KJUR.asn1.DERNumericString + * @class class for ASN.1 DER NumericString + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ +KJUR.asn1.DERNumericString = function(params) { + KJUR.asn1.DERNumericString.superclass.constructor.call(this, params); + this.hT = "12"; +}; +JSX.extend(KJUR.asn1.DERNumericString, KJUR.asn1.DERAbstractString); + +// ******************************************************************** +/** + * class for ASN.1 DER PrintableString + * @name KJUR.asn1.DERPrintableString + * @class class for ASN.1 DER PrintableString + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ +KJUR.asn1.DERPrintableString = function(params) { + KJUR.asn1.DERPrintableString.superclass.constructor.call(this, params); + this.hT = "13"; +}; +JSX.extend(KJUR.asn1.DERPrintableString, KJUR.asn1.DERAbstractString); + +// ******************************************************************** +/** + * class for ASN.1 DER TeletexString + * @name KJUR.asn1.DERTeletexString + * @class class for ASN.1 DER TeletexString + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ +KJUR.asn1.DERTeletexString = function(params) { + KJUR.asn1.DERTeletexString.superclass.constructor.call(this, params); + this.hT = "14"; +}; +JSX.extend(KJUR.asn1.DERTeletexString, KJUR.asn1.DERAbstractString); + +// ******************************************************************** +/** + * class for ASN.1 DER IA5String + * @name KJUR.asn1.DERIA5String + * @class class for ASN.1 DER IA5String + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ +KJUR.asn1.DERIA5String = function(params) { + KJUR.asn1.DERIA5String.superclass.constructor.call(this, params); + this.hT = "16"; +}; +JSX.extend(KJUR.asn1.DERIA5String, KJUR.asn1.DERAbstractString); + +// ******************************************************************** +/** + * class for ASN.1 DER UTCTime + * @name KJUR.asn1.DERUTCTime + * @class class for ASN.1 DER UTCTime + * @param {Array} params associative array of parameters (ex. {'str': '130430235959Z'}) + * @extends KJUR.asn1.DERAbstractTime + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • str - specify initial ASN.1 value(V) by a string (ex.'130430235959Z')
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
  • date - specify Date object.
  • + *
+ * NOTE: 'params' can be omitted. + *

EXAMPLES

+ * @example + * var d1 = new KJUR.asn1.DERUTCTime(); + * d1.setString('130430125959Z'); + * + * var d2 = new KJUR.asn1.DERUTCTime({'str': '130430125959Z'}); + * + * var d3 = new KJUR.asn1.DERUTCTime({'date': new Date(Date.UTC(2015, 0, 31, 0, 0, 0, 0))}); + */ +KJUR.asn1.DERUTCTime = function(params) { + KJUR.asn1.DERUTCTime.superclass.constructor.call(this, params); + this.hT = "17"; + + /** + * set value by a Date object + * @name setByDate + * @memberOf KJUR.asn1.DERUTCTime + * @function + * @param {Date} dateObject Date object to set ASN.1 value(V) + */ + this.setByDate = function(dateObject) { + this.hTLV = null; + this.isModified = true; + this.date = dateObject; + this.s = this.formatDate(this.date, 'utc'); + this.hV = stohex(this.s); + }; + + if (typeof params != "undefined") { + if (typeof params['str'] != "undefined") { + this.setString(params['str']); + } else if (typeof params['hex'] != "undefined") { + this.setStringHex(params['hex']); + } else if (typeof params['date'] != "undefined") { + this.setByDate(params['date']); + } + } +}; +JSX.extend(KJUR.asn1.DERUTCTime, KJUR.asn1.DERAbstractTime); + +// ******************************************************************** +/** + * class for ASN.1 DER GeneralizedTime + * @name KJUR.asn1.DERGeneralizedTime + * @class class for ASN.1 DER GeneralizedTime + * @param {Array} params associative array of parameters (ex. {'str': '20130430235959Z'}) + * @extends KJUR.asn1.DERAbstractTime + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • str - specify initial ASN.1 value(V) by a string (ex.'20130430235959Z')
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
  • date - specify Date object.
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERGeneralizedTime = function(params) { + KJUR.asn1.DERGeneralizedTime.superclass.constructor.call(this, params); + this.hT = "18"; + + /** + * set value by a Date object + * @name setByDate + * @memberOf KJUR.asn1.DERGeneralizedTime + * @function + * @param {Date} dateObject Date object to set ASN.1 value(V) + * @example + * When you specify UTC time, use 'Date.UTC' method like this:
+ * var o = new DERUTCTime(); + * var date = new Date(Date.UTC(2015, 0, 31, 23, 59, 59, 0)); #2015JAN31 23:59:59 + * o.setByDate(date); + */ + this.setByDate = function(dateObject) { + this.hTLV = null; + this.isModified = true; + this.date = dateObject; + this.s = this.formatDate(this.date, 'gen'); + this.hV = stohex(this.s); + }; + + if (typeof params != "undefined") { + if (typeof params['str'] != "undefined") { + this.setString(params['str']); + } else if (typeof params['hex'] != "undefined") { + this.setStringHex(params['hex']); + } else if (typeof params['date'] != "undefined") { + this.setByDate(params['date']); + } + } +}; +JSX.extend(KJUR.asn1.DERGeneralizedTime, KJUR.asn1.DERAbstractTime); + +// ******************************************************************** +/** + * class for ASN.1 DER Sequence + * @name KJUR.asn1.DERSequence + * @class class for ASN.1 DER Sequence + * @extends KJUR.asn1.DERAbstractStructured + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • array - specify array of ASN1Object to set elements of content
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERSequence = function(params) { + KJUR.asn1.DERSequence.superclass.constructor.call(this, params); + this.hT = "30"; + this.getFreshValueHex = function() { + var h = ''; + for (var i = 0; i < this.asn1Array.length; i++) { + var asn1Obj = this.asn1Array[i]; + h += asn1Obj.getEncodedHex(); + } + this.hV = h; + return this.hV; + }; +}; +JSX.extend(KJUR.asn1.DERSequence, KJUR.asn1.DERAbstractStructured); + +// ******************************************************************** +/** + * class for ASN.1 DER Set + * @name KJUR.asn1.DERSet + * @class class for ASN.1 DER Set + * @extends KJUR.asn1.DERAbstractStructured + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • array - specify array of ASN1Object to set elements of content
  • + *
+ * NOTE: 'params' can be omitted. + */ +KJUR.asn1.DERSet = function(params) { + KJUR.asn1.DERSet.superclass.constructor.call(this, params); + this.hT = "31"; + this.getFreshValueHex = function() { + var a = new Array(); + for (var i = 0; i < this.asn1Array.length; i++) { + var asn1Obj = this.asn1Array[i]; + a.push(asn1Obj.getEncodedHex()); + } + a.sort(); + this.hV = a.join(''); + return this.hV; + }; +}; +JSX.extend(KJUR.asn1.DERSet, KJUR.asn1.DERAbstractStructured); + +// ******************************************************************** +/** + * class for ASN.1 DER TaggedObject + * @name KJUR.asn1.DERTaggedObject + * @class class for ASN.1 DER TaggedObject + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * Parameter 'tagNoNex' is ASN.1 tag(T) value for this object. + * For example, if you find '[1]' tag in a ASN.1 dump, + * 'tagNoHex' will be 'a1'. + *
+ * As for optional argument 'params' for constructor, you can specify *ANY* of + * following properties: + *
    + *
  • explicit - specify true if this is explicit tag otherwise false + * (default is 'true').
  • + *
  • tag - specify tag (default is 'a0' which means [0])
  • + *
  • obj - specify ASN1Object which is tagged
  • + *
+ * @example + * d1 = new KJUR.asn1.DERUTF8String({'str':'a'}); + * d2 = new KJUR.asn1.DERTaggedObject({'obj': d1}); + * hex = d2.getEncodedHex(); + */ +KJUR.asn1.DERTaggedObject = function(params) { + KJUR.asn1.DERTaggedObject.superclass.constructor.call(this); + this.hT = "a0"; + this.hV = ''; + this.isExplicit = true; + this.asn1Object = null; + + /** + * set value by an ASN1Object + * @name setString + * @memberOf KJUR.asn1.DERTaggedObject + * @function + * @param {Boolean} isExplicitFlag flag for explicit/implicit tag + * @param {Integer} tagNoHex hexadecimal string of ASN.1 tag + * @param {ASN1Object} asn1Object ASN.1 to encapsulate + */ + this.setASN1Object = function(isExplicitFlag, tagNoHex, asn1Object) { + this.hT = tagNoHex; + this.isExplicit = isExplicitFlag; + this.asn1Object = asn1Object; + if (this.isExplicit) { + this.hV = this.asn1Object.getEncodedHex(); + this.hTLV = null; + this.isModified = true; + } else { + this.hV = null; + this.hTLV = asn1Object.getEncodedHex(); + this.hTLV = this.hTLV.replace(/^../, tagNoHex); + this.isModified = false; + } + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['tag'] != "undefined") { + this.hT = params['tag']; + } + if (typeof params['explicit'] != "undefined") { + this.isExplicit = params['explicit']; + } + if (typeof params['obj'] != "undefined") { + this.asn1Object = params['obj']; + this.setASN1Object(this.isExplicit, this.hT, this.asn1Object); + } + } +}; +JSX.extend(KJUR.asn1.DERTaggedObject, KJUR.asn1.ASN1Object); + +var b64map="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +var b64pad="="; + +function hex2b64(h) { + var i; + var c; + var ret = ""; + for(i = 0; i+3 <= h.length; i+=3) { + c = parseInt(h.substring(i,i+3),16); + ret += b64map.charAt(c >> 6) + b64map.charAt(c & 63); + } + if(i+1 == h.length) { + c = parseInt(h.substring(i,i+1),16); + ret += b64map.charAt(c << 2); + } + else if(i+2 == h.length) { + c = parseInt(h.substring(i,i+2),16); + ret += b64map.charAt(c >> 2) + b64map.charAt((c & 3) << 4); + } + while((ret.length & 3) > 0) ret += b64pad; + return ret; +} + +// convert a base64 string to hex +function b64tohex(s) { + var ret = ""; + var i; + var k = 0; // b64 state, 0-3 + var slop; + for(i = 0; i < s.length; ++i) { + if(s.charAt(i) == b64pad) break; + let v = b64map.indexOf(s.charAt(i)); + if(v < 0) continue; + if(k == 0) { + ret += int2char(v >> 2); + slop = v & 3; + k = 1; + } + else if(k == 1) { + ret += int2char((slop << 2) | (v >> 4)); + slop = v & 0xf; + k = 2; + } + else if(k == 2) { + ret += int2char(slop); + ret += int2char(v >> 2); + slop = v & 3; + k = 3; + } + else { + ret += int2char((slop << 2) | (v >> 4)); + ret += int2char(v & 0xf); + k = 0; + } + } + if(k == 1) + ret += int2char(slop << 2); + return ret; +} + +// convert a base64 string to a byte/number array + +/** + * Retrieve the hexadecimal value (as a string) of the current ASN.1 element + * @returns {string} + * @public + */ +ASN1.prototype.getHexStringValue = function () { + var hexString = this.toHexString(); + var offset = this.header * 2; + var length = this.length * 2; + return hexString.substr(offset, length); +}; + +/** + * Method to parse a pem encoded string containing both a public or private key. + * The method will translate the pem encoded string in a der encoded string and + * will parse private key and public key parameters. This method accepts public key + * in the rsaencryption pkcs #1 format (oid: 1.2.840.113549.1.1.1). + * + * @todo Check how many rsa formats use the same format of pkcs #1. + * + * The format is defined as: + * PublicKeyInfo ::= SEQUENCE { + * algorithm AlgorithmIdentifier, + * PublicKey BIT STRING + * } + * Where AlgorithmIdentifier is: + * AlgorithmIdentifier ::= SEQUENCE { + * algorithm OBJECT IDENTIFIER, the OID of the enc algorithm + * parameters ANY DEFINED BY algorithm OPTIONAL (NULL for PKCS #1) + * } + * and PublicKey is a SEQUENCE encapsulated in a BIT STRING + * RSAPublicKey ::= SEQUENCE { + * modulus INTEGER, -- n + * publicExponent INTEGER -- e + * } + * it's possible to examine the structure of the keys obtained from openssl using + * an asn.1 dumper as the one used here to parse the components: http://lapo.it/asn1js/ + * @argument {string} pem the pem encoded string, can include the BEGIN/END header/footer + * @private + */ +RSAKey.prototype.parseKey = function (pem) { + try { + var modulus = 0; + var public_exponent = 0; + var reHex = /^\s*(?:[0-9A-Fa-f][0-9A-Fa-f]\s*)+$/; + var der = reHex.test(pem) ? Hex.decode(pem) : Base64.unarmor(pem); + var asn1 = ASN1.decode(der); + + //Fixes a bug with OpenSSL 1.0+ private keys + if(asn1.sub.length === 3){ + asn1 = asn1.sub[2].sub[0]; + } + if (asn1.sub.length === 9) { + + // Parse the private key. + modulus = asn1.sub[1].getHexStringValue(); //bigint + this.n = parseBigInt(modulus, 16); + + public_exponent = asn1.sub[2].getHexStringValue(); //int + this.e = parseInt(public_exponent, 16); + + var private_exponent = asn1.sub[3].getHexStringValue(); //bigint + this.d = parseBigInt(private_exponent, 16); + + var prime1 = asn1.sub[4].getHexStringValue(); //bigint + this.p = parseBigInt(prime1, 16); + + var prime2 = asn1.sub[5].getHexStringValue(); //bigint + this.q = parseBigInt(prime2, 16); + + var exponent1 = asn1.sub[6].getHexStringValue(); //bigint + this.dmp1 = parseBigInt(exponent1, 16); + + var exponent2 = asn1.sub[7].getHexStringValue(); //bigint + this.dmq1 = parseBigInt(exponent2, 16); + + var coefficient = asn1.sub[8].getHexStringValue(); //bigint + this.coeff = parseBigInt(coefficient, 16); + + } + else if (asn1.sub.length === 2) { + + // Parse the public key. + var bit_string = asn1.sub[1]; + var sequence = bit_string.sub[0]; + + modulus = sequence.sub[0].getHexStringValue(); + this.n = parseBigInt(modulus, 16); + public_exponent = sequence.sub[1].getHexStringValue(); + this.e = parseInt(public_exponent, 16); + + } + else { + return false; + } + return true; + } + catch (ex) { + return false; + } +}; + +/** + * Translate rsa parameters in a hex encoded string representing the rsa key. + * + * The translation follow the ASN.1 notation : + * RSAPrivateKey ::= SEQUENCE { + * version Version, + * modulus INTEGER, -- n + * publicExponent INTEGER, -- e + * privateExponent INTEGER, -- d + * prime1 INTEGER, -- p + * prime2 INTEGER, -- q + * exponent1 INTEGER, -- d mod (p1) + * exponent2 INTEGER, -- d mod (q-1) + * coefficient INTEGER, -- (inverse of q) mod p + * } + * @returns {string} DER Encoded String representing the rsa private key + * @private + */ +RSAKey.prototype.getPrivateBaseKey = function () { + var options = { + 'array': [ + new KJUR.asn1.DERInteger({'int': 0}), + new KJUR.asn1.DERInteger({'bigint': this.n}), + new KJUR.asn1.DERInteger({'int': this.e}), + new KJUR.asn1.DERInteger({'bigint': this.d}), + new KJUR.asn1.DERInteger({'bigint': this.p}), + new KJUR.asn1.DERInteger({'bigint': this.q}), + new KJUR.asn1.DERInteger({'bigint': this.dmp1}), + new KJUR.asn1.DERInteger({'bigint': this.dmq1}), + new KJUR.asn1.DERInteger({'bigint': this.coeff}) + ] + }; + var seq = new KJUR.asn1.DERSequence(options); + return seq.getEncodedHex(); +}; + +/** + * base64 (pem) encoded version of the DER encoded representation + * @returns {string} pem encoded representation without header and footer + * @public + */ +RSAKey.prototype.getPrivateBaseKeyB64 = function () { + return hex2b64(this.getPrivateBaseKey()); +}; + +/** + * Translate rsa parameters in a hex encoded string representing the rsa public key. + * The representation follow the ASN.1 notation : + * PublicKeyInfo ::= SEQUENCE { + * algorithm AlgorithmIdentifier, + * PublicKey BIT STRING + * } + * Where AlgorithmIdentifier is: + * AlgorithmIdentifier ::= SEQUENCE { + * algorithm OBJECT IDENTIFIER, the OID of the enc algorithm + * parameters ANY DEFINED BY algorithm OPTIONAL (NULL for PKCS #1) + * } + * and PublicKey is a SEQUENCE encapsulated in a BIT STRING + * RSAPublicKey ::= SEQUENCE { + * modulus INTEGER, -- n + * publicExponent INTEGER -- e + * } + * @returns {string} DER Encoded String representing the rsa public key + * @private + */ +RSAKey.prototype.getPublicBaseKey = function () { + var options = { + 'array': [ + new KJUR.asn1.DERObjectIdentifier({'oid': '1.2.840.113549.1.1.1'}), //RSA Encryption pkcs #1 oid + new KJUR.asn1.DERNull() + ] + }; + var first_sequence = new KJUR.asn1.DERSequence(options); + + options = { + 'array': [ + new KJUR.asn1.DERInteger({'bigint': this.n}), + new KJUR.asn1.DERInteger({'int': this.e}) + ] + }; + var second_sequence = new KJUR.asn1.DERSequence(options); + + options = { + 'hex': '00' + second_sequence.getEncodedHex() + }; + var bit_string = new KJUR.asn1.DERBitString(options); + + options = { + 'array': [ + first_sequence, + bit_string + ] + }; + var seq = new KJUR.asn1.DERSequence(options); + return seq.getEncodedHex(); +}; + +/** + * base64 (pem) encoded version of the DER encoded representation + * @returns {string} pem encoded representation without header and footer + * @public + */ +RSAKey.prototype.getPublicBaseKeyB64 = function () { + return hex2b64(this.getPublicBaseKey()); +}; + +/** + * wrap the string in block of width chars. The default value for rsa keys is 64 + * characters. + * @param {string} str the pem encoded string without header and footer + * @param {Number} [width=64] - the length the string has to be wrapped at + * @returns {string} + * @private + */ +RSAKey.prototype.wordwrap = function (str, width) { + width = width || 64; + if (!str) { + return str; + } + var regex = '(.{1,' + width + '})( +|$\n?)|(.{1,' + width + '})'; + return str.match(RegExp(regex, 'g')).join('\n'); +}; + +/** + * Retrieve the pem encoded private key + * @returns {string} the pem encoded private key with header/footer + * @public + */ +RSAKey.prototype.getPrivateKey = function () { + var key = "-----BEGIN RSA PRIVATE KEY-----\n"; + key += this.wordwrap(this.getPrivateBaseKeyB64()) + "\n"; + key += "-----END RSA PRIVATE KEY-----"; + return key; +}; + +/** + * Retrieve the pem encoded public key + * @returns {string} the pem encoded public key with header/footer + * @public + */ +RSAKey.prototype.getPublicKey = function () { + var key = "-----BEGIN PUBLIC KEY-----\n"; + key += this.wordwrap(this.getPublicBaseKeyB64()) + "\n"; + key += "-----END PUBLIC KEY-----"; + return key; +}; + +/** + * Check if the object contains the necessary parameters to populate the rsa modulus + * and public exponent parameters. + * @param {Object} [obj={}] - An object that may contain the two public key + * parameters + * @returns {boolean} true if the object contains both the modulus and the public exponent + * properties (n and e) + * @todo check for types of n and e. N should be a parseable bigInt object, E should + * be a parseable integer number + * @private + */ +RSAKey.prototype.hasPublicKeyProperty = function (obj) { + obj = obj || {}; + return ( + obj.hasOwnProperty('n') && + obj.hasOwnProperty('e') + ); +}; + +/** + * Check if the object contains ALL the parameters of an RSA key. + * @param {Object} [obj={}] - An object that may contain nine rsa key + * parameters + * @returns {boolean} true if the object contains all the parameters needed + * @todo check for types of the parameters all the parameters but the public exponent + * should be parseable bigint objects, the public exponent should be a parseable integer number + * @private + */ +RSAKey.prototype.hasPrivateKeyProperty = function (obj) { + obj = obj || {}; + return ( + obj.hasOwnProperty('n') && + obj.hasOwnProperty('e') && + obj.hasOwnProperty('d') && + obj.hasOwnProperty('p') && + obj.hasOwnProperty('q') && + obj.hasOwnProperty('dmp1') && + obj.hasOwnProperty('dmq1') && + obj.hasOwnProperty('coeff') + ); +}; + +/** + * Parse the properties of obj in the current rsa object. Obj should AT LEAST + * include the modulus and public exponent (n, e) parameters. + * @param {Object} obj - the object containing rsa parameters + * @private + */ +RSAKey.prototype.parsePropertiesFrom = function (obj) { + this.n = obj.n; + this.e = obj.e; + + if (obj.hasOwnProperty('d')) { + this.d = obj.d; + this.p = obj.p; + this.q = obj.q; + this.dmp1 = obj.dmp1; + this.dmq1 = obj.dmq1; + this.coeff = obj.coeff; + } +}; + +/** + * Create a new JSEncryptRSAKey that extends Tom Wu's RSA key object. + * This object is just a decorator for parsing the key parameter + * @param {string|Object} key - The key in string format, or an object containing + * the parameters needed to build a RSAKey object. + * @constructor + */ +class JSEncryptRSAKey extends RSAKey { + constructor(key) { + // Call the super constructor. + super(); + // If a key key was provided. + if (key) { + // If this is a string... + if (typeof key === 'string') { + this.parseKey(key); + } + else if ( + this.hasPrivateKeyProperty(key) || + this.hasPublicKeyProperty(key) + ) { + // Set the values for the key. + this.parsePropertiesFrom(key); + } + } + } +} + +/** + * + * @param {Object} [options = {}] - An object to customize JSEncrypt behaviour + * possible parameters are: + * - default_key_size {number} default: 1024 the key size in bit + * - default_public_exponent {string} default: '010001' the hexadecimal representation of the public exponent + * - log {boolean} default: false whether log warn/error or not + * @constructor + */ +class JSEncrypt{ + constructor (options) { + options = options || {}; + this.default_key_size = parseInt(options.default_key_size) || 1024; + this.default_public_exponent = options.default_public_exponent || '010001'; //65537 default openssl public exponent for rsa key type + this.log = options.log || false; + // The private and public key. + this.key = null; + } +} + +/** + * Method to set the rsa key parameter (one method is enough to set both the public + * and the private key, since the private key contains the public key paramenters) + * Log a warning if logs are enabled + * @param {Object|string} key the pem encoded string or an object (with or without header/footer) + * @public + */ +JSEncrypt.prototype.setKey = function (key) { + if (this.log && this.key) { + console.warn('A key was already set, overriding existing.'); + } + this.key = new JSEncryptRSAKey(key); +}; + +/** + * Proxy method for setKey, for api compatibility + * @see setKey + * @public + */ +JSEncrypt.prototype.setPrivateKey = function (privkey) { + // Create the key. + this.setKey(privkey); +}; + +/** + * Proxy method for setKey, for api compatibility + * @see setKey + * @public + */ +JSEncrypt.prototype.setPublicKey = function (pubkey) { + // Sets the public key. + this.setKey(pubkey); +}; + +/** + * Proxy method for RSAKey object's decrypt, decrypt the string using the private + * components of the rsa key object. Note that if the object was not set will be created + * on the fly (by the getKey method) using the parameters passed in the JSEncrypt constructor + * @param {string} string base64 encoded crypted string to decrypt + * @return {string} the decrypted string + * @public + */ +JSEncrypt.prototype.decrypt = function (string) { + // Return the decrypted string. + try { + return this.getKey().decrypt(b64tohex(string)); + } + catch (ex) { + return false; + } +}; + +/** + * Proxy method for RSAKey object's encrypt, encrypt the string using the public + * components of the rsa key object. Note that if the object was not set will be created + * on the fly (by the getKey method) using the parameters passed in the JSEncrypt constructor + * @param {string} string the string to encrypt + * @return {string} the encrypted string encoded in base64 + * @public + */ +JSEncrypt.prototype.encrypt = function (string) { + // Return the encrypted string. + try { + return hex2b64(this.getKey().encrypt(string)); + } + catch (ex) { + return false; + } +}; + +/** + * Getter for the current JSEncryptRSAKey object. If it doesn't exists a new object + * will be created and returned + * @param {callback} [cb] the callback to be called if we want the key to be generated + * in an async fashion + * @returns {JSEncryptRSAKey} the JSEncryptRSAKey object + * @public + */ +JSEncrypt.prototype.getKey = function (cb) { + // Only create new if it does not exist. + if (!this.key) { + // Get a new private key. + this.key = new JSEncryptRSAKey(); + if (cb && {}.toString.call(cb) === '[object Function]') { + this.key.generateAsync(this.default_key_size, this.default_public_exponent, cb); + return; + } + // Generate the key. + this.key.generate(this.default_key_size, this.default_public_exponent); + } + return this.key; +}; + +/** + * Returns the pem encoded representation of the private key + * If the key doesn't exists a new key will be created + * @returns {string} pem encoded representation of the private key WITH header and footer + * @public + */ +JSEncrypt.prototype.getPrivateKey = function () { + // Return the private representation of this key. + return this.getKey().getPrivateKey(); +}; + +/** + * Returns the pem encoded representation of the private key + * If the key doesn't exists a new key will be created + * @returns {string} pem encoded representation of the private key WITHOUT header and footer + * @public + */ +JSEncrypt.prototype.getPrivateKeyB64 = function () { + // Return the private representation of this key. + return this.getKey().getPrivateBaseKeyB64(); +}; + + +/** + * Returns the pem encoded representation of the public key + * If the key doesn't exists a new key will be created + * @returns {string} pem encoded representation of the public key WITH header and footer + * @public + */ +JSEncrypt.prototype.getPublicKey = function () { + // Return the private representation of this key. + return this.getKey().getPublicKey(); +}; + +/** + * Returns the pem encoded representation of the public key + * If the key doesn't exists a new key will be created + * @returns {string} pem encoded representation of the public key WITHOUT header and footer + * @public + */ +JSEncrypt.prototype.getPublicKeyB64 = function () { + // Return the private representation of this key. + return this.getKey().getPublicBaseKeyB64(); +}; + +// export * from 'jsencrypt'; + +class BaseTransport { + constructor(endpoint, stream_type, config={}) { + this.stream_type = stream_type; + this.endpoint = endpoint; + this.eventSource = new EventEmitter(); + this.dataQueue = []; + } + + static canTransfer(stream_type) { + return BaseTransport.streamTypes().includes(stream_type); + } + + static streamTypes() { + return []; + } + + destroy() { + this.eventSource.destroy(); + } + + connect() { + // TO be impemented + } + + disconnect() { + // TO be impemented + } + + reconnect() { + return this.disconnect().then(()=>{ + return this.connect(); + }); + } + + setEndpoint(endpoint) { + this.endpoint = endpoint; + return this.reconnect(); + } + + send(data) { + // TO be impemented + // return this.prepare(data).send(); + } + + prepare(data) { + // TO be impemented + // return new Request(data); + } + + // onData(type, data) { + // this.eventSource.dispatchEvent(type, data); + // } +} + +const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + +//navigator.hardwareConcurrency || 3; + +const LOG_TAG$5 = "transport:ws"; +const Log$10 = getTagged(LOG_TAG$5); +class WebsocketTransport extends BaseTransport { + constructor(endpoint, stream_type, options={ + socket:`${location.protocol.replace('http', 'ws')}//${location.host}/ws/`, + workers: 1 + }) { + super(endpoint, stream_type); + this.proxies = []; + this.currentProxy = 0; + this.workers = 1; + this.socket_url = options.socket; + this.ready = this.connect(); + } + + destroy() { + return this.disconnect().then(()=>{ + return super.destroy(); + }); + + } + + static canTransfer(stream_type) { + return WebsocketTransport.streamTypes().includes(stream_type); + } + + static streamTypes() { + return ['hls', 'rtsp']; + } + + connect() { + return this.disconnect().then(()=>{ + let promises = []; + // TODO: get mirror list + for (let i=0; i { + this.eventSource.dispatchEvent('disconnected', {code: e.code, reason: e.reason}); + // TODO: only reconnect on demand + if ([1000, 1006, 1013, 1011].includes(e.code)) { + setTimeout(()=> { + if (this.ready && this.ready.reject) { + this.ready.reject(); + } + this.ready = this.connect(); + }, 3000); + } + }); + + proxy.set_data_handler((data)=> { + this.dataQueue.push(new Uint8Array(data)); + this.eventSource.dispatchEvent('data'); + }); + + promises.push(proxy.connect().then(()=> { + this.eventSource.dispatchEvent('connected'); + }).catch((e)=> { + this.eventSource.dispatchEvent('error'); + throw new Error(e); + })); + this.proxies.push(proxy); + } + return Promise.all(promises); + }); + } + + disconnect() { + let promises = []; + for (let i=0; i{}; + this.disconnect_handler = ()=>{}; + this.builder = new WSPProtocol(WSPProtocol.V1_1); + this.awaitingPromises = {}; + this.seq = 0; + this.encryptor = new JSEncrypt(); + } + + set_data_handler(handler) { + this.data_handler = handler; + } + + set_disconnect_handler(handler) { + this.disconnect_handler = handler; + } + + close() { + Log$10.log('closing connection'); + return new Promise((resolve)=>{ + this.ctrlChannel.onclose = ()=>{ + if (this.dataChannel) { + this.dataChannel.onclose = ()=>{ + Log$10.log('closed'); + resolve(); + }; + this.dataChannel.close(); + } else { + Log$10.log('closed'); + resolve(); + } + }; + this.ctrlChannel.close(); + }); + } + + onDisconnect(){ + this.ctrlChannel.onclose=null; + this.ctrlChannel.close(); + if (this.dataChannel) { + this.dataChannel.onclose = null; + this.dataChannel.close(); + } + this.disconnect_handler(this); + } + + initDataChannel(channel_id) { + return new Promise((resolve, reject)=>{ + this.dataChannel = new WebSocket(this.url, WebSocketProxy.CHN_DATA); + this.dataChannel.binaryType = 'arraybuffer'; + this.dataChannel.onopen = ()=>{ + let msg = this.builder.build(WSPProtocol.CMD_JOIN, { + channel: channel_id + }); + Log$10.debug(msg); + this.dataChannel.send(msg); + }; + this.dataChannel.onmessage = (ev)=>{ + Log$10.debug(`[data]\r\n${ev.data}`); + let res = WSPProtocol.parse(ev.data); + if (!res) { + return reject(); + } + + this.dataChannel.onmessage=(e)=>{ + // Log.debug('got data'); + if (this.data_handler) { + this.data_handler(e.data); + } + }; + resolve(); + }; + this.dataChannel.onerror = (e)=>{ + Log$10.error(`[data] ${e.type}`); + this.dataChannel.close(); + }; + this.dataChannel.onclose = (e)=>{ + Log$10.error(`[data] ${e.type}. code: ${e.code}, reason: ${e.reason || 'unknown reason'}`); + this.onDisconnect(e); + }; + }); + } + + connect() { + this.encryptionKey = null; + return new Promise((resolve, reject)=>{ + this.ctrlChannel = new WebSocket(this.url, WebSocketProxy.CHN_CONTROL); + + this.connected = false; + + this.ctrlChannel.onopen = ()=>{ + let headers = { + proto: this.stream_type + }; + if (this.endpoint.socket) { + headers.socket = this.endpoint.socket; + } else { + Object.assign(headers, { + host: this.endpoint.host, + port: this.endpoint.port + }); + } + let msg = this.builder.build(WSPProtocol.CMD_INIT, headers); + Log$10.debug(msg); + this.ctrlChannel.send(msg); + }; + + this.ctrlChannel.onmessage = (ev)=>{ + Log$10.debug(`[ctrl]\r\n${ev.data}`); + + let res = WSPProtocol.parse(ev.data); + if (!res) { + return reject(); + } + + if (res.code >= 300) { + Log$10.error(`[ctrl]\r\n${res.code}: ${res.msg}`); + return reject(); + } + this.ctrlChannel.onmessage = (e)=> { + let res = WSPProtocol.parse(e.data); + Log$10.debug(`[ctrl]\r\n${e.data}`); + if (res.data.seq in this.awaitingPromises) { + if (res.code < 300) { + this.awaitingPromises[res.data.seq].resolve(res); + } else { + this.awaitingPromises[res.data.seq].reject(res); + } + delete this.awaitingPromises[res.data.seq]; + } + }; + this.encryptionKey = res.data.pubkey || null; + if (this.encryptionKey) { + this.encryptor.setPublicKey(this.encryptionKey); + // TODO: check errors + } + this.initDataChannel(res.data.channel).then(resolve).catch(reject); + }; + + this.ctrlChannel.onerror = (e)=>{ + Log$10.error(`[ctrl] ${e.type}`); + this.ctrlChannel.close(); + }; + this.ctrlChannel.onclose = (e)=>{ + Log$10.error(`[ctrl] ${e.type}. code: ${e.code} ${e.reason || 'unknown reason'}`); + this.onDisconnect(e); + }; + }); + } + + encrypt(msg) { + if (this.encryptionKey) { + let crypted = this.encryptor.encrypt(msg); + if (crypted === false) { + throw new Error("Encryption failed. Stopping") + } + return crypted; + } + return msg; + } + + send(payload) { + if (this.ctrlChannel.readyState != WebSocket.OPEN) { + this.close(); + // .then(this.connect.bind(this)); + // return; + throw new Error('disconnected'); + } + // Log.debug(payload); + let data = { + contentLength: payload.length, + seq: ++WSPProtocol.seq + }; + return { + seq:data.seq, + promise: new Promise((resolve, reject)=>{ + this.awaitingPromises[data.seq] = {resolve, reject}; + let msg = this.builder.build(WSPProtocol.CMD_WRAP, data, payload); + Log$10.debug(msg); + this.ctrlChannel.send(this.encrypt(msg)); + })}; + } +} + +const Log$11 = getTagged('wsp'); + +class StreamType$1 { + static get HLS() {return 'hls';} + static get RTSP() {return 'rtsp';} + + static isSupported(type) { + return [StreamType$1.HLS, StreamType$1.RTSP].includes(type); + } + + static fromUrl(url) { + let parsed; + try { + parsed = Url.parse(url); + } catch (e) { + return null; + } + switch (parsed.protocol) { + case 'rtsp': + return StreamType$1.RTSP; + case 'http': + case 'https': + if (url.indexOf('.m3u8')>=0) { + return StreamType$1.HLS; + } else { + return null; + } + default: + return null; + } + } + + static fromMime(mime) { + switch (mime) { + case 'application/x-rtsp': + return StreamType$1.RTSP; + case 'application/vnd.apple.mpegurl': + case 'application/x-mpegurl': + return StreamType$1.HLS; + default: + return null; + } + } +} + +class WSPlayer { + + constructor(node, opts) { + if (typeof node == typeof '') { + this.player = document.getElementById(node); + } else { + this.player = node; + } + + let modules = opts.modules || { + client: RTSPClient, + transport: { + constructor: WebsocketTransport + } + }; + this.errorHandler = opts.errorHandler || null; + this.queryCredentials = opts.queryCredentials || null; + + this.modules = {}; + for (let module of modules) { + let transport = module.transport || WebsocketTransport; + let client = module.client || RTSPClient; + if (transport.constructor.canTransfer(client.streamType())) { + this.modules[client.streamType()] = { + client: client, + transport: transport + }; + } else { + Log$11.warn(`Client stream type ${client.streamType()} is incompatible with transport types [${transport.streamTypes().join(', ')}]. Skip`); + } + } + + this.type = StreamType$1.RTSP; + this.url = null; + if (opts.url && opts.type) { + this.url = opts.url; + this.type = opts.type; + } else { + if (!this._checkSource(this.player)) { + for (let i=0; i{ + if (!this.isPlaying()) { + this.client.start(); + } + }, false); + + this.player.addEventListener('pause', ()=>{ + this.client.stop(); + }, false); + } + + // TODO: check native support + + isPlaying() { + return !(this.player.paused || this.client.paused); + } + + static canPlayWithModules(mimeType, modules) { + + let filteredModules = {}; + for (let module of modules) { + let transport = module.transport || WebsocketTransport; + let client = module.client || RTSPClient; + if (transport.canTransfer(client.streamType())) { + filteredModules[client.streamType()] = true; + } + } + + for (let type in filteredModules) { + if (type == StreamType$1.fromMime(mimeType)) { + return true; + } + } + return false; + } + + /// TODO: deprecate it? + static canPlay(resource) { + return StreamType$1.fromMime(resource.type) || StreamType$1.fromUrl(resource.src); + } + + canPlayUrl(src) { + let type = StreamType$1.fromUrl(src); + return (type in this.modules); + } + + _checkSource(src) { + if (!src.dataset['ignore'] && src.src && !this.player.canPlayType(src.type) && (StreamType$1.fromMime(src.type) || StreamType$1.fromUrl(src.src))) { + this.url = src.src; + this.type = src.type ? StreamType$1.fromMime(src.type) : StreamType$1.fromUrl(src.src); + return true; + } + return false; + } + + async setSource(url, type) { + if (this.transport) { + if (this.client) { + await this.client.detachTransport(); + } + await this.transport.destroy(); + } + try { + this.endpoint = Url.parse(url); + } catch (e) { + return; + } + this.url = url; + let transport = this.modules[type].transport; + this.transport = new transport.constructor(this.endpoint, this.type, transport.options); + + + let lastType = this.type; + this.type = (StreamType$1.isSupported(type)?type:false) || StreamType$1.fromMime(type); + if (!this.type) { + throw new Error("Bad stream type"); + } + + if (lastType!=this.type || !this.client) { + if (this.client) { + await this.client.destroy(); + } + let client = this.modules[type].client; + this.client = new client(); + } else { + this.client.reset(); + } + + if (this.queryCredentials) { + this.client.queryCredentials = this.queryCredentials; + } + if (this.remuxer) { + this.remuxer.destroy(); + this.remuxer = null; + } + this.remuxer = new Remuxer(this.player); + this.remuxer.attachClient(this.client); + + this.client.attachTransport(this.transport); + this.client.setSource(this.endpoint); + + if (this.player.autoplay) { + this.start(); + } + } + + start() { + if (this.client) { + this.client.start().catch((e)=>{ + if (this.errorHandler) { + this.errorHandler(e); + } + }); + } + } + + stop() { + if (this.client) { + this.client.stop(); + } + } + + async destroy() { + if (this.transport) { + if (this.client) { + await this.client.detachTransport(); + } + await this.transport.destroy(); + } + if (this.client) { + await this.client.destroy(); + } + if (this.remuxer) { + this.remuxer.destroy(); + this.remuxer = null; + } + } + +} + +setDefaultLogLevel(LogLevel.Error); +getTagged("transport:ws").setLevel(LogLevel.Error); +getTagged("client:rtsp").setLevel(LogLevel.Debug); +getTagged("mse").setLevel(LogLevel.Debug); + +window.Streamedian = { + logger(tag) { + return getTagged(tag) + }, + player(node, opts) { + if (!opts.socket) { + throw new Error("socket parameter is not set"); + } + let _options = { + modules: [ + { + client: RTSPClient, + transport: { + constructor: WebsocketTransport, + options: { + socket: opts.socket + } + } + } + ], + errorHandler(e) { + alert(`Failed to start player: ${e.message}`); + }, + queryCredentials(client) { + return new Promise((resolve, reject) => { + let c = prompt('input credentials in format user:password'); + if (c) { + client.setCredentials.apply(client, c.split(':')); + resolve(); + } else { + reject(); + } + }); + } + }; + return new WSPlayer(node, _options); + } +}; + +}()); +//# sourceMappingURL=streamedian.min.js.map diff --git a/demos/wsp/index.html b/demos/wsp/index.html new file mode 100755 index 0000000..284995f --- /dev/null +++ b/demos/wsp/index.html @@ -0,0 +1,252 @@ + + + + + RTSP player example(based streamedian) + + + +
+
+ + +
+
+

Enter your rtsp link to the stream, for example: "rtsp://localhost:1554/test/live1"

+

If need token,for example: "rtsp://localhost:1554/test/live1?token=4df8f5d5d680385cb07c2e354dd0f3f3"

+
+ +
+ + 120sec. +
+ +
+

Change buffer duration

+
+ + + +
+
+ Playback rate:  + + live +
+
+ +
+
+ +

View HTML5 RTSP video player log

+
+ + + + +

+ + + + + + + + + + diff --git a/demos/wsp/streamedian_1_8_5_3.min.js b/demos/wsp/streamedian_1_8_5_3.min.js new file mode 100755 index 0000000..ff44b26 --- /dev/null +++ b/demos/wsp/streamedian_1_8_5_3.min.js @@ -0,0 +1,9799 @@ +(function () { + 'use strict'; + + // ERROR=0, WARN=1, LOG=2, DEBUG=3 + const LogLevel = { + Error: 0, + Warn: 1, + Log: 2, + Debug: 3 + }; + + let DEFAULT_LOG_LEVEL = LogLevel.Debug; + + function setDefaultLogLevel(level) { + DEFAULT_LOG_LEVEL = level; + } + class Logger { + constructor(level = DEFAULT_LOG_LEVEL, tag) { + this.tag = tag; + this.setLevel(level); + } + + setLevel(level) { + this.level = level; + } + + static get level_map() { return { + [LogLevel.Debug]:'log', + [LogLevel.Log]:'log', + [LogLevel.Warn]:'warn', + [LogLevel.Error]:'error' + }}; + + _log(lvl, args) { + args = Array.prototype.slice.call(args); + if (this.tag) { + args.unshift(`[${this.tag}]`); + } + if (this.level>=lvl) console[Logger.level_map[lvl]].apply(console, args); + } + log(){ + this._log(LogLevel.Log, arguments); + } + debug(){ + this._log(LogLevel.Debug, arguments); + } + error(){ + this._log(LogLevel.Error, arguments); + } + warn(){ + this._log(LogLevel.Warn, arguments); + } + } + + const taggedLoggers = new Map(); + function getTagged(tag) { + if (!taggedLoggers.has(tag)) { + taggedLoggers.set(tag, new Logger(DEFAULT_LOG_LEVEL, tag)); + } + return taggedLoggers.get(tag); + } + const Log = new Logger(); + + // export * from 'bp_logger'; + + class Url { + static parse(url) { + let ret = {}; + + let urlparts = decodeURI(url).split(' '); + url = urlparts.shift(); + ret.client = urlparts.join(' '); + + let regex = /^([^:]+):\/\/([^\/]+)(.*)$/; //protocol, login, urlpath + let result = regex.exec(url); + + if (!result) { + throw new Error("bad url"); + } + + ret.full = url; + ret.protocol = result[1]; + ret.urlpath = result[3]; + + let parts = ret.urlpath.split('/'); + ret.basename = parts.pop().split(/\?|#/)[0]; + ret.basepath = parts.join('/'); + + let loginSplit = result[2].split('@'); + let hostport = loginSplit[0].split(':'); + let userpass = [ null, null ]; + if (loginSplit.length === 2) { + userpass = loginSplit[0].split(':'); + hostport = loginSplit[1].split(':'); + } + + ret.user = userpass[0]; + ret.pass = userpass[1]; + ret.host = hostport[0]; + ret.auth = (ret.user && ret.pass) ? `${ret.user}:${ret.pass}` : ''; + + ret.port = (null == hostport[1]) ? Url.protocolDefaultPort(ret.protocol) : hostport[1]; + ret.portDefined = (null != hostport[1]); + ret.location = `${ret.host}:${ret.port}`; + + if (ret.protocol == 'unix') { + ret.socket = ret.port; + ret.port = undefined; + } + + return ret; + } + + static full(parsed) { + return `${parsed.protocol}://${parsed.location}/${parsed.urlpath}`; + } + + static isAbsolute(url) { + return /^[^:]+:\/\//.test(url); + } + + static protocolDefaultPort(protocol) { + switch (protocol) { + case 'rtsp': return 554; + case 'http': return 80; + case 'https': return 443; + } + + return 0; + } + } + + const listener = Symbol("event_listener"); + const listeners = Symbol("event_listeners"); + + class DestructibleEventListener { + constructor(eventListener) { + this[listener] = eventListener; + this[listeners] = new Map(); + } + + clear() { + if (this[listeners]) { + for (let entry of this[listeners]) { + for (let fn of entry[1]) { + this[listener].removeEventListener(entry[0], fn); + } + } } + this[listeners].clear(); + } + + destroy() { + this.clear(); + this[listeners] = null; + } + + on(event, selector, fn) { + if (fn == undefined) { + fn = selector; + selector = null; + } + if (selector) { + return this.addEventListener(event, (e) => { + if (e.target.matches(selector)) { + fn(e); + } + }); + } else { + return this.addEventListener(event, fn); + } + } + + addEventListener(event, fn) { + if (!this[listeners].has(event)) { + this[listeners].set(event, new Set()); + } + this[listeners].get(event).add(fn); + this[listener].addEventListener(event, fn, false); + return fn; + } + + removeEventListener(event, fn) { + this[listener].removeEventListener(event, fn, false); + if (this[listeners].has(event)) { + //this[listeners].set(event, new Set()); + let ev = this[listeners].get(event); + ev.delete(fn); + if (!ev.size) { + this[listeners].delete(event); + } + } + } + + dispatchEvent(event) { + if (this[listener]) { + this[listener].dispatchEvent(event); + } + } + } + + class EventEmitter { + constructor(element=null) { + this[listener] = new DestructibleEventListener(element || document.createElement('div')); + } + + clear() { + if (this[listener]) { + this[listener].clear(); + } + } + + destroy() { + if (this[listener]) { + this[listener].destroy(); + this[listener] = null; + } + } + + on(event, selector, fn) { + if (this[listener]) { + return this[listener].on(event, selector, fn); + } + return null; + } + + addEventListener(event, fn) { + if (this[listener]) { + return this[listener].addEventListener(event, fn, false); + } + return null; + } + + removeEventListener(event, fn) { + if (this[listener]) { + this[listener].removeEventListener(event, fn, false); + } + } + + dispatchEvent(event, data) { + if (this[listener]) { + this[listener].dispatchEvent(new CustomEvent(event, {detail: data})); + } + } + } + + class EventSourceWrapper { + constructor(eventSource) { + this.eventSource = eventSource; + this[listeners] = new Map(); + } + + on(event, selector, fn) { + if (!this[listeners].has(event)) { + this[listeners].set(event, new Set()); + } + let listener = this.eventSource.on(event, selector, fn); + if (listener) { + this[listeners].get(event).add(listener); + } + } + + off(event, fn){ + this.eventSource.removeEventListener(event, fn); + } + + clear() { + this.eventSource.clear(); + this[listeners].clear(); + } + + destroy() { + this.eventSource.clear(); + this[listeners] = null; + this.eventSource = null; + } + } + + // export * from 'bp_event'; + + /** + * Generate MP4 Box + * got from: https://github.com/dailymotion/hls.js + */ + + class MP4 { + static init() { + MP4.types = { + avc1: [], // codingname + avcC: [], + btrt: [], + dinf: [], + dref: [], + esds: [], + ftyp: [], + hdlr: [], + mdat: [], + mdhd: [], + mdia: [], + mfhd: [], + minf: [], + moof: [], + moov: [], + mp4a: [], + mvex: [], + mvhd: [], + sdtp: [], + stbl: [], + stco: [], + stsc: [], + stsd: [], + stsz: [], + stts: [], + tfdt: [], + tfhd: [], + traf: [], + trak: [], + trun: [], + trex: [], + tkhd: [], + vmhd: [], + smhd: [] + }; + + var i; + for (i in MP4.types) { + if (MP4.types.hasOwnProperty(i)) { + MP4.types[i] = [ + i.charCodeAt(0), + i.charCodeAt(1), + i.charCodeAt(2), + i.charCodeAt(3) + ]; + } + } + + var videoHdlr = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x56, 0x69, 0x64, 0x65, + 0x6f, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' + ]); + + var audioHdlr = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun' + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x53, 0x6f, 0x75, 0x6e, + 0x64, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' + ]); + + MP4.HDLR_TYPES = { + 'video': videoHdlr, + 'audio': audioHdlr + }; + + var dref = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01, // entry_count + 0x00, 0x00, 0x00, 0x0c, // entry_size + 0x75, 0x72, 0x6c, 0x20, // 'url' type + 0x00, // version 0 + 0x00, 0x00, 0x01 // entry_flags + ]); + + var stco = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00 // entry_count + ]); + + MP4.STTS = MP4.STSC = MP4.STCO = stco; + + MP4.STSZ = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x00, // sample_size + 0x00, 0x00, 0x00, 0x00, // sample_count + ]); + MP4.VMHD = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x01, // flags + 0x00, 0x00, // graphicsmode + 0x00, 0x00, + 0x00, 0x00, + 0x00, 0x00 // opcolor + ]); + MP4.SMHD = new Uint8Array([ + 0x00, // version + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, // balance + 0x00, 0x00 // reserved + ]); + + MP4.STSD = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01]);// entry_count + + var majorBrand = new Uint8Array([105,115,111,109]); // isom + var avc1Brand = new Uint8Array([97,118,99,49]); // avc1 + var minorVersion = new Uint8Array([0, 0, 0, 1]); + + MP4.FTYP = MP4.box(MP4.types.ftyp, majorBrand, minorVersion, majorBrand, avc1Brand); + MP4.DINF = MP4.box(MP4.types.dinf, MP4.box(MP4.types.dref, dref)); + } + + static box(type, ...payload) { + var size = 8, + i = payload.length, + len = i, + result; + // calculate the total size we need to allocate + while (i--) { + size += payload[i].byteLength; + } + result = new Uint8Array(size); + result[0] = (size >> 24) & 0xff; + result[1] = (size >> 16) & 0xff; + result[2] = (size >> 8) & 0xff; + result[3] = size & 0xff; + result.set(type, 4); + // copy the payload into the result + for (i = 0, size = 8; i < len; ++i) { + // copy payload[i] array @ offset size + result.set(payload[i], size); + size += payload[i].byteLength; + } + return result; + } + + static hdlr(type) { + return MP4.box(MP4.types.hdlr, MP4.HDLR_TYPES[type]); + } + + static mdat(data) { + return MP4.box(MP4.types.mdat, data); + } + + static mdhd(timescale, duration) { + return MP4.box(MP4.types.mdhd, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x02, // creation_time + 0x00, 0x00, 0x00, 0x03, // modification_time + (timescale >> 24) & 0xFF, + (timescale >> 16) & 0xFF, + (timescale >> 8) & 0xFF, + timescale & 0xFF, // timescale + (duration >> 24), + (duration >> 16) & 0xFF, + (duration >> 8) & 0xFF, + duration & 0xFF, // duration + 0x55, 0xc4, // 'und' language (undetermined) + 0x00, 0x00 + ])); + } + + static mdia(track) { + return MP4.box(MP4.types.mdia, MP4.mdhd(track.timescale, track.duration), MP4.hdlr(track.type), MP4.minf(track)); + } + + static mfhd(sequenceNumber) { + return MP4.box(MP4.types.mfhd, new Uint8Array([ + 0x00, + 0x00, 0x00, 0x00, // flags + (sequenceNumber >> 24), + (sequenceNumber >> 16) & 0xFF, + (sequenceNumber >> 8) & 0xFF, + sequenceNumber & 0xFF, // sequence_number + ])); + } + + static minf(track) { + if (track.type === 'audio') { + return MP4.box(MP4.types.minf, MP4.box(MP4.types.smhd, MP4.SMHD), MP4.DINF, MP4.stbl(track)); + } else { + return MP4.box(MP4.types.minf, MP4.box(MP4.types.vmhd, MP4.VMHD), MP4.DINF, MP4.stbl(track)); + } + } + + static moof(sn, baseMediaDecodeTime, track) { + return MP4.box(MP4.types.moof, MP4.mfhd(sn), MP4.traf(track,baseMediaDecodeTime)); + } + /** + * @param tracks... (optional) {array} the tracks associated with this movie + */ + static moov(tracks, duration, timescale) { + var + i = tracks.length, + boxes = []; + + while (i--) { + boxes[i] = MP4.trak(tracks[i]); + } + + return MP4.box.apply(null, [MP4.types.moov, MP4.mvhd(timescale, duration)].concat(boxes).concat(MP4.mvex(tracks))); + } + + static mvex(tracks) { + var + i = tracks.length, + boxes = []; + + while (i--) { + boxes[i] = MP4.trex(tracks[i]); + } + return MP4.box.apply(null, [MP4.types.mvex].concat(boxes)); + } + + static mvhd(timescale,duration) { + var + bytes = new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + 0x00, 0x00, 0x00, 0x01, // creation_time + 0x00, 0x00, 0x00, 0x02, // modification_time + (timescale >> 24) & 0xFF, + (timescale >> 16) & 0xFF, + (timescale >> 8) & 0xFF, + timescale & 0xFF, // timescale + (duration >> 24) & 0xFF, + (duration >> 16) & 0xFF, + (duration >> 8) & 0xFF, + duration & 0xFF, // duration + 0x00, 0x01, 0x00, 0x00, // 1.0 rate + 0x01, 0x00, // 1.0 volume + 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // pre_defined + 0xff, 0xff, 0xff, 0xff // next_track_ID + ]); + return MP4.box(MP4.types.mvhd, bytes); + } + + static sdtp(track) { + var + samples = track.samples || [], + bytes = new Uint8Array(4 + samples.length), + flags, + i; + // leave the full box header (4 bytes) all zero + // write the sample table + for (i = 0; i < samples.length; i++) { + flags = samples[i].flags; + bytes[i + 4] = (flags.dependsOn << 4) | + (flags.isDependedOn << 2) | + (flags.hasRedundancy); + } + + return MP4.box(MP4.types.sdtp, bytes); + } + + static stbl(track) { + return MP4.box(MP4.types.stbl, MP4.stsd(track), MP4.box(MP4.types.stts, MP4.STTS), MP4.box(MP4.types.stsc, MP4.STSC), MP4.box(MP4.types.stsz, MP4.STSZ), MP4.box(MP4.types.stco, MP4.STCO)); + } + + static avc1(track) { + var sps = [], pps = [], i, data, len; + // assemble the SPSs + + for (i = 0; i < track.sps.length; i++) { + data = track.sps[i]; + len = data.byteLength; + sps.push((len >>> 8) & 0xFF); + sps.push((len & 0xFF)); + sps = sps.concat(Array.prototype.slice.call(data)); // SPS + } + + // assemble the PPSs + for (i = 0; i < track.pps.length; i++) { + data = track.pps[i]; + len = data.byteLength; + pps.push((len >>> 8) & 0xFF); + pps.push((len & 0xFF)); + pps = pps.concat(Array.prototype.slice.call(data)); + } + + var avcc = MP4.box(MP4.types.avcC, new Uint8Array([ + 0x01, // version + sps[3], // profile + sps[4], // profile compat + sps[5], // level + 0xfc | 3, // lengthSizeMinusOne, hard-coded to 4 bytes + 0xE0 | track.sps.length // 3bit reserved (111) + numOfSequenceParameterSets + ].concat(sps).concat([ + track.pps.length // numOfPictureParameterSets + ]).concat(pps))), // "PPS" + width = track.width, + height = track.height; + //console.log('avcc:' + Hex.hexDump(avcc)); + return MP4.box(MP4.types.avc1, new Uint8Array([ + 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // data_reference_index + 0x00, 0x00, // pre_defined + 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // pre_defined + (width >> 8) & 0xFF, + width & 0xff, // width + (height >> 8) & 0xFF, + height & 0xff, // height + 0x00, 0x48, 0x00, 0x00, // horizresolution + 0x00, 0x48, 0x00, 0x00, // vertresolution + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // frame_count + 0x12, + 0x62, 0x69, 0x6E, 0x65, //binelpro.ru + 0x6C, 0x70, 0x72, 0x6F, + 0x2E, 0x72, 0x75, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, // compressorname + 0x00, 0x18, // depth = 24 + 0x11, 0x11]), // pre_defined = -1 + avcc, + MP4.box(MP4.types.btrt, new Uint8Array([ + 0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB + 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate + 0x00, 0x2d, 0xc6, 0xc0])) // avgBitrate + ); + } + + static esds(track) { + var configlen = track.config.byteLength; + let data = new Uint8Array(26+configlen+3); + data.set([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + + 0x03, // descriptor_type + 0x17+configlen, // length + 0x00, 0x01, //es_id + 0x00, // stream_priority + + 0x04, // descriptor_type + 0x0f+configlen, // length + 0x40, //codec : mpeg4_audio + 0x15, // stream_type + 0x00, 0x00, 0x00, // buffer_size + 0x00, 0x00, 0x00, 0x00, // maxBitrate + 0x00, 0x00, 0x00, 0x00, // avgBitrate + + 0x05, // descriptor_type + configlen + ]); + data.set(track.config, 26); + data.set([0x06, 0x01, 0x02], 26+configlen); + // return new Uint8Array([ + // 0x00, // version 0 + // 0x00, 0x00, 0x00, // flags + // + // 0x03, // descriptor_type + // 0x17+configlen, // length + // 0x00, 0x01, //es_id + // 0x00, // stream_priority + // + // 0x04, // descriptor_type + // 0x0f+configlen, // length + // 0x40, //codec : mpeg4_audio + // 0x15, // stream_type + // 0x00, 0x00, 0x00, // buffer_size + // 0x00, 0x00, 0x00, 0x00, // maxBitrate + // 0x00, 0x00, 0x00, 0x00, // avgBitrate + // + // 0x05 // descriptor_type + // ].concat([configlen]).concat(track.config).concat([0x06, 0x01, 0x02])); // GASpecificConfig)); // length + audio config descriptor + return data; + } + + static mp4a(track) { + var audiosamplerate = track.audiosamplerate; + return MP4.box(MP4.types.mp4a, new Uint8Array([ + 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, 0x00, // reserved + 0x00, 0x01, // data_reference_index + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, track.channelCount, // channelcount + 0x00, 0x10, // sampleSize:16bits + 0x00, 0x00, // pre_defined + 0x00, 0x00, // reserved2 + (audiosamplerate >> 8) & 0xFF, + audiosamplerate & 0xff, // + 0x00, 0x00]), + MP4.box(MP4.types.esds, MP4.esds(track))); + } + + static stsd(track) { + if (track.type === 'audio') { + return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp4a(track)); + } else { + return MP4.box(MP4.types.stsd, MP4.STSD, MP4.avc1(track)); + } + } + + static tkhd(track) { + var id = track.id, + duration = track.duration, + width = track.width, + height = track.height, + volume = track.volume; + return MP4.box(MP4.types.tkhd, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x07, // flags + 0x00, 0x00, 0x00, 0x00, // creation_time + 0x00, 0x00, 0x00, 0x00, // modification_time + (id >> 24) & 0xFF, + (id >> 16) & 0xFF, + (id >> 8) & 0xFF, + id & 0xFF, // track_ID + 0x00, 0x00, 0x00, 0x00, // reserved + (duration >> 24), + (duration >> 16) & 0xFF, + (duration >> 8) & 0xFF, + duration & 0xFF, // duration + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, // reserved + 0x00, 0x00, // layer + 0x00, 0x00, // alternate_group + (volume>>0)&0xff, (((volume%1)*10)>>0)&0xff, // track volume // FIXME + 0x00, 0x00, // reserved + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix + (width >> 8) & 0xFF, + width & 0xFF, + 0x00, 0x00, // width + (height >> 8) & 0xFF, + height & 0xFF, + 0x00, 0x00 // height + ])); + } + + static traf(track,baseMediaDecodeTime) { + var sampleDependencyTable = MP4.sdtp(track), + id = track.id; + return MP4.box(MP4.types.traf, + MP4.box(MP4.types.tfhd, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + (id >> 24), + (id >> 16) & 0XFF, + (id >> 8) & 0XFF, + (id & 0xFF) // track_ID + ])), + MP4.box(MP4.types.tfdt, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + (baseMediaDecodeTime >>24), + (baseMediaDecodeTime >> 16) & 0XFF, + (baseMediaDecodeTime >> 8) & 0XFF, + (baseMediaDecodeTime & 0xFF) // baseMediaDecodeTime + ])), + MP4.trun(track, + sampleDependencyTable.length + + 16 + // tfhd + 16 + // tfdt + 8 + // traf header + 16 + // mfhd + 8 + // moof header + 8), // mdat header + sampleDependencyTable); + } + + /** + * Generate a track box. + * @param track {object} a track definition + * @return {Uint8Array} the track box + */ + static trak(track) { + track.duration = track.duration || 0xffffffff; + return MP4.box(MP4.types.trak, MP4.tkhd(track), MP4.mdia(track)); + } + + static trex(track) { + var id = track.id; + return MP4.box(MP4.types.trex, new Uint8Array([ + 0x00, // version 0 + 0x00, 0x00, 0x00, // flags + (id >> 24), + (id >> 16) & 0XFF, + (id >> 8) & 0XFF, + (id & 0xFF), // track_ID + 0x00, 0x00, 0x00, 0x01, // default_sample_description_index + 0x00, 0x00, 0x00, 0x00, // default_sample_duration + 0x00, 0x00, 0x00, 0x00, // default_sample_size + 0x00, 0x01, 0x00, 0x01 // default_sample_flags + ])); + } + + static trun(track, offset) { + var samples= track.samples || [], + len = samples.length, + arraylen = 12 + (16 * len), + array = new Uint8Array(arraylen), + i,sample,duration,size,flags,cts; + offset += 8 + arraylen; + array.set([ + 0x00, // version 0 + 0x00, 0x0f, 0x01, // flags + (len >>> 24) & 0xFF, + (len >>> 16) & 0xFF, + (len >>> 8) & 0xFF, + len & 0xFF, // sample_count + (offset >>> 24) & 0xFF, + (offset >>> 16) & 0xFF, + (offset >>> 8) & 0xFF, + offset & 0xFF // data_offset + ],0); + for (i = 0; i < len; i++) { + sample = samples[i]; + duration = sample.duration; + size = sample.size; + flags = sample.flags; + cts = sample.cts; + array.set([ + (duration >>> 24) & 0xFF, + (duration >>> 16) & 0xFF, + (duration >>> 8) & 0xFF, + duration & 0xFF, // sample_duration + (size >>> 24) & 0xFF, + (size >>> 16) & 0xFF, + (size >>> 8) & 0xFF, + size & 0xFF, // sample_size + (flags.isLeading << 2) | flags.dependsOn, + (flags.isDependedOn << 6) | + (flags.hasRedundancy << 4) | + (flags.paddingValue << 1) | + flags.isNonSync, + flags.degradPrio & 0xF0 << 8, + flags.degradPrio & 0x0F, // sample_flags + (cts >>> 24) & 0xFF, + (cts >>> 16) & 0xFF, + (cts >>> 8) & 0xFF, + cts & 0xFF // sample_composition_time_offset + ],12+16*i); + } + return MP4.box(MP4.types.trun, array); + } + + static initSegment(tracks, duration, timescale) { + if (!MP4.types) { + MP4.init(); + } + var movie = MP4.moov(tracks, duration, timescale), result; + result = new Uint8Array(MP4.FTYP.byteLength + movie.byteLength); + result.set(MP4.FTYP); + result.set(movie, MP4.FTYP.byteLength); + return result; + } + } + + //import {MP4Inspect} from '../iso-bmff/mp4-inspector.js'; + + const LOG_TAG = "mse"; + const Log$1 = getTagged(LOG_TAG); + + class MSEBuffer { + constructor(parent, codec) { + this.mediaSource = parent.mediaSource; + this.players = parent.players; + this.cleaning = false; + this.parent = parent; + this.queue = []; + this.cleanResolvers = []; + this.codec = codec; + this.cleanRanges = []; + this.updatesToCleanup = 0; + this.firstMoveToBufferStart = true; + + Log$1.debug(`Use codec: ${codec}`); + + this.sourceBuffer = this.mediaSource.addSourceBuffer(codec); + this.eventSource = new EventEmitter(this.sourceBuffer); + + this.eventSource.addEventListener('updatestart', (e)=> { + // this.updating = true; + // Log.debug('update start'); + if (this.cleaning) { + Log$1.debug(`${this.codec} cleaning start`); + } + }); + + this.eventSource.addEventListener('update', (e)=> { + // this.updating = true; + if (this.cleaning) { + Log$1.debug(`${this.codec} cleaning update`); + } + }); + + this.eventSource.addEventListener('updateend', (e)=> { + // Log.debug('update end'); + // this.updating = false; + if (this.cleaning) { + Log$1.debug(`${this.codec} cleaning end`); + + try { + if (this.sourceBuffer.buffered.length && this.players[0].currentTime < this.sourceBuffer.buffered.start(0)) { + this.players[0].currentTime = this.sourceBuffer.buffered.start(0); + } + } catch (e) { + // TODO: do something? + } + while (this.cleanResolvers.length) { + let resolver = this.cleanResolvers.shift(); + resolver(); + } + this.cleaning = false; + + if (this.cleanRanges.length) { + this.doCleanup(); + return; + } + } + + // cleanup buffer after 100 updates + this.updatesToCleanup++; + if(this.updatesToCleanup > 100){ + this.cleanupBuffer(); + this.updatesToCleanup = 0; + } + + this.feedNext(); + }); + + this.eventSource.addEventListener('error', (e)=> { + Log$1.debug(`Source buffer error: ${this.mediaSource.readyState}`); + if (this.mediaSource.sourceBuffers.length) { + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } + this.parent.eventSource.dispatchEvent('error'); + }); + + this.eventSource.addEventListener('abort', (e)=> { + Log$1.debug(`Source buffer aborted: ${this.mediaSource.readyState}`); + if (this.mediaSource.sourceBuffers.length) { + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } + this.parent.eventSource.dispatchEvent('error'); + }); + + if (!this.sourceBuffer.updating) { + this.feedNext(); + } + // TODO: cleanup every hour for live streams + } + + cleanupBuffer(){ + if(this.sourceBuffer.buffered.length && !this.sourceBuffer.updating){ + let currentPlayTime = this.players[0].currentTime; + let startBuffered = this.sourceBuffer.buffered.start(0); + let endBuffered = this.sourceBuffer.buffered.end(0); + let bufferedDuration = endBuffered - startBuffered; + let removeEnd = endBuffered - this.parent.bufferDuration; + + if((removeEnd > 0) && (bufferedDuration > this.parent.bufferDuration) && (currentPlayTime > startBuffered) && + (currentPlayTime > removeEnd)){ + try { + Log$1.debug("Remove media segments", startBuffered, removeEnd); + this.sourceBuffer.remove(startBuffered, removeEnd); + } + catch (e){ + Log$1.warn("Failed to cleanup buffer"); + } + } + } + } + + destroy() { + this.eventSource.destroy(); + this.clear(); + this.queue = []; + this.mediaSource.removeSourceBuffer(this.sourceBuffer); + } + + clear() { + this.queue = []; + let promises = []; + for (let i=0; i< this.sourceBuffer.buffered.length; ++i) { + // TODO: await remove + this.cleaning = true; + promises.push(new Promise((resolve, reject)=>{ + this.cleanResolvers.push(resolve); + if (!this.sourceBuffer.updating) { + this.sourceBuffer.remove(this.sourceBuffer.buffered.start(i), this.sourceBuffer.buffered.end(i)); + resolve(); + } else { + this.sourceBuffer.onupdateend = () => { + if (this.sourceBuffer) { + this.sourceBuffer.remove(this.sourceBuffer.buffered.start(i), this.sourceBuffer.buffered.end(i)); + } + resolve(); + }; + } + })); + } + return Promise.all(promises); + } + + setLive(is_live) { + this.is_live = is_live; + } + + feedNext() { + // Log.debug("feed next ", this.sourceBuffer.updating); + if (!this.sourceBuffer.updating && !this.cleaning && this.queue.length) { + this.doAppend(this.queue.shift()); + } + } + + doCleanup() { + if (!this.cleanRanges.length) { + this.cleaning = false; + this.feedNext(); + return; + } + let range = this.cleanRanges.shift(); + Log$1.debug(`${this.codec} remove range [${range[0]} - ${range[1]}). + \nUpdating: ${this.sourceBuffer.updating} + `); + this.cleaning = true; + this.sourceBuffer.remove(range[0], range[1]); + } + + initCleanup() { + if (this.sourceBuffer.buffered.length && !this.sourceBuffer.updating && !this.cleaning) { + Log$1.debug(`${this.codec} cleanup`); + let removeBound = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length-1) - 2; + + for (let i=0; i< this.sourceBuffer.buffered.length; ++i) { + let removeStart = this.sourceBuffer.buffered.start(i); + let removeEnd = this.sourceBuffer.buffered.end(i); + if ((this.players[0].currentTime <= removeStart) || (removeBound <= removeStart)) continue; + + if ((removeBound <= removeEnd) && (removeBound >= removeStart)) { + Log$1.debug(`Clear [${removeStart}, ${removeBound}), leave [${removeBound}, ${removeEnd}]`); + removeEnd = removeBound; + if (removeEnd!=removeStart) { + this.cleanRanges.push([removeStart, removeEnd]); + } + continue; // Do not cleanup buffered range after current position + } + this.cleanRanges.push([removeStart, removeEnd]); + } + + this.doCleanup(); + + // let bufferStart = this.sourceBuffer.buffered.start(0); + // let removeEnd = this.sourceBuffer.buffered.start(0) + (this.sourceBuffer.buffered.end(0) - this.sourceBuffer.buffered.start(0))/2; + // if (this.players[0].currentTime < removeEnd) { + // this.players[0].currentTime = removeEnd; + // } + // let removeEnd = Math.max(this.players[0].currentTime - 3, this.sourceBuffer.buffered.end(0) - 3); + // + // if (removeEnd < bufferStart) { + // removeEnd = this.sourceBuffer.buffered.start(0) + (this.sourceBuffer.buffered.end(0) - this.sourceBuffer.buffered.start(0))/2; + // if (this.players[0].currentTime < removeEnd) { + // this.players[0].currentTime = removeEnd; + // } + // } + + // if (removeEnd > bufferStart && (removeEnd - bufferStart > 0.5 )) { + // // try { + // Log.debug(`${this.codec} remove range [${bufferStart} - ${removeEnd}). + // \nBuffered end: ${this.sourceBuffer.buffered.end(0)} + // \nUpdating: ${this.sourceBuffer.updating} + // `); + // this.cleaning = true; + // this.sourceBuffer.remove(bufferStart, removeEnd); + // // } catch (e) { + // // // TODO: implement + // // Log.error(e); + // // } + // } else { + // this.feedNext(); + // } + } else { + this.feedNext(); + } + } + + doAppend(data) { + // console.log(MP4Inspect.mp4toJSON(data)); + let err = this.players[0].error; + if (err) { + Log$1.error(`Error occured: ${MSE.ErrorNotes[err.code]}`); + try { + this.players.forEach((video)=>{video.stop();}); + this.mediaSource.endOfStream(); + } catch (e){ + + } + this.parent.eventSource.dispatchEvent('error'); + } else { + try { + this.sourceBuffer.appendBuffer(data); + if (this.firstMoveToBufferStart && this.sourceBuffer.buffered.length) { + this.players[0].currentTime = this.sourceBuffer.buffered.start(0); + if (this.players[0].autoPlay) { + this.players[0].start(); + } + this.firstMoveToBufferStart = false; + } + } catch (e) { + if (e.name === 'QuotaExceededError') { + Log$1.debug(`${this.codec} quota fail`); + this.queue.unshift(data); + this.initCleanup(); + return; + } + + // reconnect on fail + Log$1.error(`Error occured while appending buffer. ${e.name}: ${e.message}`); + this.parent.eventSource.dispatchEvent('error'); + } + } + + } + + feed(data) { + this.queue = this.queue.concat(data); + // Log.debug(this.sourceBuffer.updating, this.updating, this.queue.length); + if (this.sourceBuffer && !this.sourceBuffer.updating && !this.cleaning) { + // Log.debug('enq feed'); + this.feedNext(); + } + } + } + + class MSE { + // static CODEC_AVC_BASELINE = "avc1.42E01E"; + // static CODEC_AVC_MAIN = "avc1.4D401E"; + // static CODEC_AVC_HIGH = "avc1.64001E"; + // static CODEC_VP8 = "vp8"; + // static CODEC_AAC = "mp4a.40.2"; + // static CODEC_VORBIS = "vorbis"; + // static CODEC_THEORA = "theora"; + + static get ErrorNotes() {return { + [MediaError.MEDIA_ERR_ABORTED]: 'fetching process aborted by user', + [MediaError.MEDIA_ERR_NETWORK]: 'error occurred when downloading', + [MediaError.MEDIA_ERR_DECODE]: 'error occurred when decoding', + [MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED]: 'audio/video not supported' + }}; + + static isSupported(codecs) { + return (window.MediaSource && window.MediaSource.isTypeSupported(`video/mp4; codecs="${codecs.join(',')}"`)); + } + + constructor (players) { + this.players = players; + const playing = this.players.map((video, idx) => { + video.onplaying = function () { + playing[idx] = true; + }; + video.onpause = function () { + playing[idx] = false; + }; + return !video.paused; + }); + this.playing = playing; + this.mediaSource = new MediaSource(); + this.eventSource = new EventEmitter(this.mediaSource); + this.reset(); + } + + set bufferDuration(buffDuration){ + this.bufferDuration_ = buffDuration; + } + + get bufferDuration(){ + return this.bufferDuration_; + } + + destroy() { + this.reset(); + this.eventSource.destroy(); + this.mediaSource = null; + this.eventSource = null; + } + + play() { + this.players.forEach((video, idx)=>{ + if (video.paused && !this.playing[idx]) { + Log$1.debug(`player ${idx}: play`); + video.play(); + } + }); + } + + setLive(is_live) { + for (let idx in this.buffers) { + this.buffers[idx].setLive(is_live); + } + this.is_live = is_live; + } + + resetBuffers() { + this.players.forEach((video, idx)=>{ + if (!video.paused && this.playing[idx]) { + video.pause(); + video.currentTime = 0; + } + }); + + let promises = []; + for (let buffer of this.buffers.values()) { + promises.push(buffer.clear()); + } + return Promise.all(promises).then(()=>{ + this.mediaSource.endOfStream(); + this.mediaSource.duration = 0; + this.mediaSource.clearLiveSeekableRange(); + this.play(); + }); + } + + clear() { + this.reset(); + this.players.forEach((video)=>{video.src = URL.createObjectURL(this.mediaSource);}); + + return this.setupEvents(); + } + + setupEvents() { + this.eventSource.clear(); + this.resolved = false; + this.mediaReady = new Promise((resolve, reject)=> { + this._sourceOpen = ()=> { + Log$1.debug(`Media source opened: ${this.mediaSource.readyState}`); + if (!this.resolved) { + this.resolved = true; + resolve(); + } + }; + this._sourceEnded = ()=>{ + Log$1.debug(`Media source ended: ${this.mediaSource.readyState}`); + }; + this._sourceClose = ()=>{ + Log$1.debug(`Media source closed: ${this.mediaSource.readyState}`); + if (this.resolved) { + this.eventSource.dispatchEvent('sourceclosed'); + } + }; + this.eventSource.addEventListener('sourceopen', this._sourceOpen); + this.eventSource.addEventListener('sourceended', this._sourceEnded); + this.eventSource.addEventListener('sourceclose', this._sourceClose); + }); + return this.mediaReady; + } + + reset() { + this.ready = false; + for (let track in this.buffers) { + this.buffers[track].destroy(); + delete this.buffers[track]; + } + if (this.mediaSource.readyState == 'open') { + this.mediaSource.duration = 0; + this.mediaSource.endOfStream(); + } + this.updating = false; + this.resolved = false; + this.buffers = {}; + // this.players.forEach((video)=>{video.src = URL.createObjectURL(this.mediaSource)}); + // TODO: remove event listeners for existing media source + // this.setupEvents(); + // this.clear(); + } + + setCodec(track, mimeCodec) { + return this.mediaReady.then(()=>{ + this.buffers[track] = new MSEBuffer(this, mimeCodec); + this.buffers[track].setLive(this.is_live); + }); + } + + feed(track, data) { + if (this.buffers[track]) { + this.buffers[track].feed(data); + } + } + } + + const Log$2 = getTagged('remuxer:base'); + let track_id = 1; + class BaseRemuxer { + + static get MP4_TIMESCALE() { return 90000;} + + // TODO: move to ts parser + // static PTSNormalize(value, reference) { + // + // let offset; + // if (reference === undefined) { + // return value; + // } + // if (reference < value) { + // // - 2^33 + // offset = -8589934592; + // } else { + // // + 2^33 + // offset = 8589934592; + // } + // /* PTS is 33bit (from 0 to 2^33 -1) + // if diff between value and reference is bigger than half of the amplitude (2^32) then it means that + // PTS looping occured. fill the gap */ + // while (Math.abs(value - reference) > 4294967296) { + // value += offset; + // } + // return value; + // } + + static getTrackID() { + return track_id++; + } + + constructor(timescale, scaleFactor, params) { + this.timeOffset = 0; + this.timescale = timescale; + this.scaleFactor = scaleFactor; + this.readyToDecode = false; + this.samples = []; + this.seq = 1; + this.tsAlign = 1; + } + + scaled(timestamp) { + return timestamp / this.scaleFactor; + } + + unscaled(timestamp) { + return timestamp * this.scaleFactor; + } + + remux(unit) { + if (unit) { + this.samples.push({ + unit: unit, + pts: unit.pts, + dts: unit.dts + }); + return true; + } + return false; + } + + static toMS(timestamp) { + return timestamp/90; + } + + setConfig(config) { + + } + + insertDscontinuity() { + this.samples.push(null); + } + + init(initPTS, initDTS, shouldInitialize=true) { + this.initPTS = Math.min(initPTS, this.samples[0].dts /*- this.unscaled(this.timeOffset)*/); + this.initDTS = Math.min(initDTS, this.samples[0].dts /*- this.unscaled(this.timeOffset)*/); + Log$2.debug(`Initial pts=${this.initPTS} dts=${this.initDTS} offset=${this.unscaled(this.timeOffset)}`); + this.initialized = shouldInitialize; + } + + flush() { + this.seq++; + this.mp4track.len = 0; + this.mp4track.samples = []; + } + + static dtsSortFunc(a,b) { + return (a.dts-b.dts); + } + + static groupByDts(gop) { + const groupBy = (xs, key) => { + return xs.reduce((rv, x) => { + (rv[x[key]] = rv[x[key]] || []).push(x); + return rv; + }, {}); + }; + return groupBy(gop, 'dts'); + } + + getPayloadBase(sampleFunction, setupSample) { + if (!this.readyToDecode || !this.initialized || !this.samples.length) return null; + this.samples.sort(BaseRemuxer.dtsSortFunc); + return true; + // + // let payload = new Uint8Array(this.mp4track.len); + // let offset = 0; + // let samples=this.mp4track.samples; + // let mp4Sample, lastDTS, pts, dts; + // + // while (this.samples.length) { + // let sample = this.samples.shift(); + // if (sample === null) { + // // discontinuity + // this.nextDts = undefined; + // break; + // } + // + // let unit = sample.unit; + // + // pts = Math.round((sample.pts - this.initDTS)/this.tsAlign)*this.tsAlign; + // dts = Math.round((sample.dts - this.initDTS)/this.tsAlign)*this.tsAlign; + // // ensure DTS is not bigger than PTS + // dts = Math.min(pts, dts); + // + // // sampleFunction(pts, dts); // TODO: + // + // // mp4Sample = setupSample(unit, pts, dts); // TODO: + // + // payload.set(unit.getData(), offset); + // offset += unit.getSize(); + // + // samples.push(mp4Sample); + // lastDTS = dts; + // } + // if (!samples.length) return null; + // + // // samplesPostFunction(samples); // TODO: + // + // return new Uint8Array(payload.buffer, 0, this.mp4track.len); + } + } + + const Log$3 = getTagged("remuxer:aac"); + // TODO: asm.js + class AACRemuxer extends BaseRemuxer { + + constructor(timescale, scaleFactor = 1, params={}) { + super(timescale, scaleFactor); + + this.codecstring=MSE.CODEC_AAC; + this.units = []; + this.initDTS = undefined; + this.nextAacPts = undefined; + this.lastPts = 0; + this.firstDTS = 0; + this.firstPTS = 0; + this.duration = params.duration || 1; + this.initialized = false; + + this.mp4track={ + id:BaseRemuxer.getTrackID(), + type: 'audio', + fragmented:true, + channelCount:0, + audiosamplerate: this.timescale, + duration: 0, + timescale: this.timescale, + volume: 1, + samples: [], + config: '', + len: 0 + }; + if (params.config) { + this.setConfig(params.config); + } + } + + setConfig(config) { + this.mp4track.channelCount = config.channels; + this.mp4track.audiosamplerate = config.samplerate; + if (!this.mp4track.duration) { + this.mp4track.duration = (this.duration?this.duration:1)*config.samplerate; + } + this.mp4track.timescale = config.samplerate; + this.mp4track.config = config.config; + this.mp4track.codec = config.codec; + this.timescale = config.samplerate; + this.scaleFactor = BaseRemuxer.MP4_TIMESCALE / config.samplerate; + this.expectedSampleDuration = 1024 * this.scaleFactor; + this.readyToDecode = true; + } + + remux(aac) { + if (super.remux.call(this, aac)) { + this.mp4track.len += aac.getSize(); + } + } + + getPayload() { + if (!this.readyToDecode || !this.samples.length) return null; + this.samples.sort(function(a, b) { + return (a.dts-b.dts); + }); + + let payload = new Uint8Array(this.mp4track.len); + let offset = 0; + let samples=this.mp4track.samples; + let mp4Sample, lastDTS, pts, dts; + + while (this.samples.length) { + let sample = this.samples.shift(); + if (sample === null) { + // discontinuity + this.nextDts = undefined; + break; + } + let unit = sample.unit; + pts = sample.pts - this.initDTS; + dts = sample.dts - this.initDTS; + + if (lastDTS === undefined) { + if (this.nextDts) { + let delta = Math.round(this.scaled(pts - this.nextAacPts)); + // if fragment are contiguous, or delta less than 600ms, ensure there is no overlap/hole between fragments + if (/*contiguous || */Math.abs(delta) < 600) { + // log delta + if (delta) { + if (delta > 0) { + Log$3.log(`${delta} ms hole between AAC samples detected,filling it`); + // if we have frame overlap, overlapping for more than half a frame duraion + } else if (delta < -12) { + // drop overlapping audio frames... browser will deal with it + Log$3.log(`${(-delta)} ms overlapping between AAC samples detected, drop frame`); + this.mp4track.len -= unit.getSize(); + continue; + } + // set DTS to next DTS + pts = dts = this.nextAacPts; + } + } + } + // remember first PTS of our aacSamples, ensure value is positive + this.firstDTS = Math.max(0, dts); + } + + mp4Sample = { + size: unit.getSize(), + cts: 0, + duration:1024, + flags: { + isLeading: 0, + isDependedOn: 0, + hasRedundancy: 0, + degradPrio: 0, + dependsOn: 1 + } + }; + + payload.set(unit.getData(), offset); + offset += unit.getSize(); + samples.push(mp4Sample); + lastDTS = dts; + } + if (!samples.length) return null; + this.nextDts =pts+this.expectedSampleDuration; + return new Uint8Array(payload.buffer, 0, this.mp4track.len); + } + } + //test.bundle.js:42 [remuxer:h264] skip frame from the past at DTS=18397972271140676 with expected DTS=18397998040950484 + + /** + * Parser for exponential Golomb codes, a variable-bitwidth number encoding scheme used by h264. + */ + + class ExpGolomb { + + constructor(data) { + this.data = data; + // the number of bytes left to examine in this.data + this.bytesAvailable = this.data.byteLength; + // the current word being examined + this.word = 0; // :uint + // the number of bits left to examine in the current word + this.bitsAvailable = 0; // :uint + } + + // ():void + loadWord() { + var + position = this.data.byteLength - this.bytesAvailable, + workingBytes = new Uint8Array(4), + availableBytes = Math.min(4, this.bytesAvailable); + if (availableBytes === 0) { + throw new Error('no bytes available'); + } + workingBytes.set(this.data.subarray(position, position + availableBytes)); + this.word = new DataView(workingBytes.buffer, workingBytes.byteOffset, workingBytes.byteLength).getUint32(0); + // track the amount of this.data that has been processed + this.bitsAvailable = availableBytes * 8; + this.bytesAvailable -= availableBytes; + } + + // (count:int):void + skipBits(count) { + var skipBytes; // :int + if (this.bitsAvailable > count) { + this.word <<= count; + this.bitsAvailable -= count; + } else { + count -= this.bitsAvailable; + skipBytes = count >> 3; + count -= (skipBytes << 3); + this.bytesAvailable -= skipBytes; + this.loadWord(); + this.word <<= count; + this.bitsAvailable -= count; + } + } + + // (size:int):uint + readBits(size) { + var + bits = Math.min(this.bitsAvailable, size), // :uint + valu = this.word >>> (32 - bits); // :uint + if (size > 32) { + Log.error('Cannot read more than 32 bits at a time'); + } + this.bitsAvailable -= bits; + if (this.bitsAvailable > 0) { + this.word <<= bits; + } else if (this.bytesAvailable > 0) { + this.loadWord(); + } + bits = size - bits; + if (bits > 0) { + return valu << bits | this.readBits(bits); + } else { + return valu; + } + } + + // ():uint + skipLZ() { + var leadingZeroCount; // :uint + for (leadingZeroCount = 0; leadingZeroCount < this.bitsAvailable; ++leadingZeroCount) { + if (0 !== (this.word & (0x80000000 >>> leadingZeroCount))) { + // the first bit of working word is 1 + this.word <<= leadingZeroCount; + this.bitsAvailable -= leadingZeroCount; + return leadingZeroCount; + } + } + // we exhausted word and still have not found a 1 + this.loadWord(); + return leadingZeroCount + this.skipLZ(); + } + + // ():void + skipUEG() { + this.skipBits(1 + this.skipLZ()); + } + + // ():void + skipEG() { + this.skipBits(1 + this.skipLZ()); + } + + // ():uint + readUEG() { + var clz = this.skipLZ(); // :uint + return this.readBits(clz + 1) - 1; + } + + // ():int + readEG() { + var valu = this.readUEG(); // :int + if (0x01 & valu) { + // the number is odd if the low order bit is set + return (1 + valu) >>> 1; // add 1 to make it even, and divide by 2 + } else { + return -1 * (valu >>> 1); // divide by two then make it negative + } + } + + // Some convenience functions + // :Boolean + readBoolean() { + return 1 === this.readBits(1); + } + + // ():int + readUByte() { + return this.readBits(8); + } + + // ():int + readUShort() { + return this.readBits(16); + } + // ():int + readUInt() { + return this.readBits(32); + } + } + + // TODO: asm.js + + function appendByteArray(buffer1, buffer2) { + let tmp = new Uint8Array((buffer1.byteLength|0) + (buffer2.byteLength|0)); + tmp.set(buffer1, 0); + tmp.set(buffer2, buffer1.byteLength|0); + return tmp; + } + function base64ToArrayBuffer(base64) { + var binary_string = window.atob(base64); + var len = binary_string.length; + var bytes = new Uint8Array( len ); + for (var i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes.buffer; + } + + function hexToByteArray(hex) { + let len = hex.length >> 1; + var bufView = new Uint8Array(len); + for (var i = 0; i < len; i++) { + bufView[i] = parseInt(hex.substr(i<<1,2),16); + } + return bufView; + } + + function bitSlice(bytearray, start=0, end=bytearray.byteLength*8) { + let byteLen = Math.ceil((end-start)/8); + let res = new Uint8Array(byteLen); + let startByte = start >>> 3; // /8 + let endByte = (end>>>3) - 1; // /8 + let bitOffset = start & 0x7; // %8 + let nBitOffset = 8 - bitOffset; + let endOffset = 8 - end & 0x7; // %8 + for (let i=0; i> nBitOffset; + if (i == endByte-1 && endOffset < 8) { + tail >>= endOffset; + tail <<= endOffset; + } + } + res[i]=(bytearray[startByte+i]< 0; --i) { + + /* Shift result one left to make room for another bit, + then add the next bit on the stream. */ + result = ((result|0) << 1) | (((this.byte|0) >> (8 - (++this.bitpos))) & 0x01); + if ((this.bitpos|0)>=8) { + this.byte = this.src.getUint8(++this.bytepos); + this.bitpos &= 0x7; + } + } + + return result; + } + skipBits(length) { + this.bitpos += (length|0) & 0x7; // %8 + this.bytepos += (length|0) >>> 3; // *8 + if (this.bitpos > 7) { + this.bitpos &= 0x7; + ++this.bytepos; + } + + if (!this.finished()) { + this.byte = this.src.getUint8(this.bytepos); + return 0; + } else { + return this.bytepos-this.src.byteLength-this.src.bitpos; + } + } + + finished() { + return this.bytepos >= this.src.byteLength; + } + } + + class NALU { + + static get NDR() {return 1;} + static get SLICE_PART_A() {return 2;} + static get SLICE_PART_B() {return 3;} + static get SLICE_PART_C() {return 4;} + static get IDR() {return 5;} + static get SEI() {return 6;} + static get SPS() {return 7;} + static get PPS() {return 8;} + static get DELIMITER() {return 9;} + static get EOSEQ() {return 10;} + static get EOSTR() {return 11;} + static get FILTER() {return 12;} + static get STAP_A() {return 24;} + static get STAP_B() {return 25;} + static get FU_A() {return 28;} + static get FU_B() {return 29;} + + static get TYPES() {return { + [NALU.IDR]: 'IDR', + [NALU.SEI]: 'SEI', + [NALU.SPS]: 'SPS', + [NALU.PPS]: 'PPS', + [NALU.NDR]: 'NDR' + }}; + + static type(nalu) { + if (nalu.ntype in NALU.TYPES) { + return NALU.TYPES[nalu.ntype]; + } else { + return 'UNKNOWN'; + } + } + + constructor(ntype, nri, data, dts, pts) { + + this.data = data; + this.ntype = ntype; + this.nri = nri; + this.dts = dts; + this.pts = pts ? pts : this.dts; + this.sliceType = null; + } + + appendData(idata) { + this.data = appendByteArray(this.data, idata); + } + + toString() { + return `${NALU.type(this)}(${this.data.byteLength}): NRI: ${this.getNri()}, PTS: ${this.pts}, DTS: ${this.dts}`; + } + + getNri() { + return this.nri >> 5; + } + + type() { + return this.ntype; + } + + isKeyframe() { + return this.ntype === NALU.IDR || this.sliceType === 7; + } + + getSize() { + return 4 + 1 + this.data.byteLength; + } + + getData() { + let header = new Uint8Array(5 + this.data.byteLength); + let view = new DataView(header.buffer); + view.setUint32(0, this.data.byteLength + 1); + view.setUint8(4, (0x0 & 0x80) | (this.nri & 0x60) | (this.ntype & 0x1F)); + header.set(this.data, 5); + return header; + } + } + + class H264Parser { + + constructor(remuxer) { + this.remuxer = remuxer; + this.track = remuxer.mp4track; + this.firstFound = false; + } + + msToScaled(timestamp) { + return (timestamp - this.remuxer.timeOffset) * this.remuxer.scaleFactor; + } + + parseSPS(sps) { + var config = H264Parser.readSPS(new Uint8Array(sps)); + + this.track.width = config.width; + this.track.height = config.height; + this.track.sps = [new Uint8Array(sps)]; + // this.track.timescale = this.remuxer.timescale; + // this.track.duration = this.remuxer.timescale; // TODO: extract duration for non-live client + this.track.codec = 'avc1.'; + + let codecarray = new DataView(sps.buffer, sps.byteOffset+1, 4); + for (let i = 0; i < 3; ++i) { + var h = codecarray.getUint8(i).toString(16); + if (h.length < 2) { + h = '0' + h; + } + this.track.codec += h; + } + } + + parsePPS(pps) { + this.track.pps = [new Uint8Array(pps)]; + } + + parseNAL(unit) { + if (!unit) return false; + + let push = null; + // console.log(unit.toString()); + switch (unit.type()) { + case NALU.NDR: + case NALU.IDR: + unit.sliceType = H264Parser.parceSliceHeader(unit.data); + if (unit.isKeyframe() && !this.firstFound) { + this.firstFound = true; + } + if (this.firstFound) { + push = true; + } else { + push = false; + } + break; + case NALU.PPS: + push = false; + if (!this.track.pps) { + this.parsePPS(unit.getData().subarray(4)); + if (!this.remuxer.readyToDecode && this.track.pps && this.track.sps) { + this.remuxer.readyToDecode = true; + } + } + break; + case NALU.SPS: + push = false; + + if (!this.firstFound) { + if (!navigator.vendor.match(/apple/i)) { + if (!navigator.platform.match(/linux/i)) { + this.firstFound = true; + push = true; + } + } + } + if(!this.track.sps) { + this.parseSPS(unit.getData().subarray(4)); + if (!this.remuxer.readyToDecode && this.track.pps && this.track.sps) { + this.remuxer.readyToDecode = true; + } + } + break; + case NALU.SEI: + push = false; + let data = new DataView(unit.data.buffer, unit.data.byteOffset, unit.data.byteLength); + let byte_idx = 0; + let pay_type = data.getUint8(byte_idx); + ++byte_idx; + let pay_size = 0; + let sz = data.getUint8(byte_idx); + ++byte_idx; + while (sz === 255) { + pay_size+=sz; + sz = data.getUint8(byte_idx); + ++byte_idx; + } + pay_size+=sz; + + let uuid = unit.data.subarray(byte_idx, byte_idx+16); + byte_idx+=16; + console.log(`PT: ${pay_type}, PS: ${pay_size}, UUID: ${Array.from(uuid).map(function(i) { + return ('0' + i.toString(16)).slice(-2); + }).join('')}`); + // debugger; + break; + case NALU.EOSEQ: + case NALU.EOSTR: + push = false; + default: + } + if (push === null && unit.getNri() > 0 ) { + push=true; + } + return push; + } + + static parceSliceHeader(data) { + let decoder = new ExpGolomb(data); + let first_mb = decoder.readUEG(); + let slice_type = decoder.readUEG(); + let ppsid = decoder.readUEG(); + let frame_num = decoder.readUByte(); + // console.log(`first_mb: ${first_mb}, slice_type: ${slice_type}, ppsid: ${ppsid}, frame_num: ${frame_num}`); + return slice_type; + } + + /** + * Advance the ExpGolomb decoder past a scaling list. The scaling + * list is optionally transmitted as part of a sequence parameter + * set and is not relevant to transmuxing. + * @param decoder {ExpGolomb} exp golomb decoder + * @param count {number} the number of entries in this scaling list + * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 + */ + static skipScalingList(decoder, count) { + let lastScale = 8, + nextScale = 8, + deltaScale; + for (let j = 0; j < count; j++) { + if (nextScale !== 0) { + deltaScale = decoder.readEG(); + nextScale = (lastScale + deltaScale + 256) % 256; + } + lastScale = (nextScale === 0) ? lastScale : nextScale; + } + } + + /** + * Read a sequence parameter set and return some interesting video + * properties. A sequence parameter set is the H264 metadata that + * describes the properties of upcoming video frames. + * @param data {Uint8Array} the bytes of a sequence parameter set + * @return {object} an object with configuration parsed from the + * sequence parameter set, including the dimensions of the + * associated video frames. + */ + static readSPS(data) { + let decoder = new ExpGolomb(data); + let frameCropLeftOffset = 0, + frameCropRightOffset = 0, + frameCropTopOffset = 0, + frameCropBottomOffset = 0, + sarScale = 1, + profileIdc,profileCompat,levelIdc, + numRefFramesInPicOrderCntCycle, picWidthInMbsMinus1, + picHeightInMapUnitsMinus1, + frameMbsOnlyFlag, + scalingListCount; + decoder.readUByte(); + profileIdc = decoder.readUByte(); // profile_idc + profileCompat = decoder.readBits(5); // constraint_set[0-4]_flag, u(5) + decoder.skipBits(3); // reserved_zero_3bits u(3), + levelIdc = decoder.readUByte(); //level_idc u(8) + decoder.skipUEG(); // seq_parameter_set_id + // some profiles have more optional data we don't need + if (profileIdc === 100 || + profileIdc === 110 || + profileIdc === 122 || + profileIdc === 244 || + profileIdc === 44 || + profileIdc === 83 || + profileIdc === 86 || + profileIdc === 118 || + profileIdc === 128) { + var chromaFormatIdc = decoder.readUEG(); + if (chromaFormatIdc === 3) { + decoder.skipBits(1); // separate_colour_plane_flag + } + decoder.skipUEG(); // bit_depth_luma_minus8 + decoder.skipUEG(); // bit_depth_chroma_minus8 + decoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag + if (decoder.readBoolean()) { // seq_scaling_matrix_present_flag + scalingListCount = (chromaFormatIdc !== 3) ? 8 : 12; + for (let i = 0; i < scalingListCount; ++i) { + if (decoder.readBoolean()) { // seq_scaling_list_present_flag[ i ] + if (i < 6) { + H264Parser.skipScalingList(decoder, 16); + } else { + H264Parser.skipScalingList(decoder, 64); + } + } + } + } + } + decoder.skipUEG(); // log2_max_frame_num_minus4 + var picOrderCntType = decoder.readUEG(); + if (picOrderCntType === 0) { + decoder.readUEG(); //log2_max_pic_order_cnt_lsb_minus4 + } else if (picOrderCntType === 1) { + decoder.skipBits(1); // delta_pic_order_always_zero_flag + decoder.skipEG(); // offset_for_non_ref_pic + decoder.skipEG(); // offset_for_top_to_bottom_field + numRefFramesInPicOrderCntCycle = decoder.readUEG(); + for(let i = 0; i < numRefFramesInPicOrderCntCycle; ++i) { + decoder.skipEG(); // offset_for_ref_frame[ i ] + } + } + decoder.skipUEG(); // max_num_ref_frames + decoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag + picWidthInMbsMinus1 = decoder.readUEG(); + picHeightInMapUnitsMinus1 = decoder.readUEG(); + frameMbsOnlyFlag = decoder.readBits(1); + if (frameMbsOnlyFlag === 0) { + decoder.skipBits(1); // mb_adaptive_frame_field_flag + } + decoder.skipBits(1); // direct_8x8_inference_flag + if (decoder.readBoolean()) { // frame_cropping_flag + frameCropLeftOffset = decoder.readUEG(); + frameCropRightOffset = decoder.readUEG(); + frameCropTopOffset = decoder.readUEG(); + frameCropBottomOffset = decoder.readUEG(); + } + if (decoder.readBoolean()) { + // vui_parameters_present_flag + if (decoder.readBoolean()) { + // aspect_ratio_info_present_flag + let sarRatio; + const aspectRatioIdc = decoder.readUByte(); + switch (aspectRatioIdc) { + case 1: sarRatio = [1,1]; break; + case 2: sarRatio = [12,11]; break; + case 3: sarRatio = [10,11]; break; + case 4: sarRatio = [16,11]; break; + case 5: sarRatio = [40,33]; break; + case 6: sarRatio = [24,11]; break; + case 7: sarRatio = [20,11]; break; + case 8: sarRatio = [32,11]; break; + case 9: sarRatio = [80,33]; break; + case 10: sarRatio = [18,11]; break; + case 11: sarRatio = [15,11]; break; + case 12: sarRatio = [64,33]; break; + case 13: sarRatio = [160,99]; break; + case 14: sarRatio = [4,3]; break; + case 15: sarRatio = [3,2]; break; + case 16: sarRatio = [2,1]; break; + case 255: { + sarRatio = [decoder.readUByte() << 8 | decoder.readUByte(), decoder.readUByte() << 8 | decoder.readUByte()]; + break; + } + } + if (sarRatio) { + sarScale = sarRatio[0] / sarRatio[1]; + } + } + if (decoder.readBoolean()) {decoder.skipBits(1);} + + if (decoder.readBoolean()) { + decoder.skipBits(4); + if (decoder.readBoolean()) { + decoder.skipBits(24); + } + } + if (decoder.readBoolean()) { + decoder.skipUEG(); + decoder.skipUEG(); + } + if (decoder.readBoolean()) { + let unitsInTick = decoder.readUInt(); + let timeScale = decoder.readUInt(); + let fixedFrameRate = decoder.readBoolean(); + let frameDuration = timeScale/(2*unitsInTick); + console.log(`timescale: ${timeScale}; unitsInTick: ${unitsInTick}; fixedFramerate: ${fixedFrameRate}; avgFrameDuration: ${frameDuration}`); + } + } + return { + width: Math.ceil((((picWidthInMbsMinus1 + 1) * 16) - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale), + height: ((2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16) - ((frameMbsOnlyFlag? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset)) + }; + } + + static readSliceType(decoder) { + // skip NALu type + decoder.readUByte(); + // discard first_mb_in_slice + decoder.readUEG(); + // return slice_type + return decoder.readUEG(); + } + } + + const Log$4 = getTagged("remuxer:h264"); + // TODO: asm.js + class H264Remuxer extends BaseRemuxer { + + constructor(timescale, scaleFactor=1, params={}) { + super(timescale, scaleFactor); + + this.nextDts = undefined; + this.readyToDecode = false; + this.initialized = false; + + this.firstDTS=0; + this.firstPTS=0; + this.lastDTS=undefined; + this.lastSampleDuration = 0; + this.lastDurations = []; + // this.timescale = 90000; + this.tsAlign = Math.round(this.timescale/60); + + this.mp4track={ + id:BaseRemuxer.getTrackID(), + type: 'video', + len:0, + fragmented:true, + sps:'', + pps:'', + width:0, + height:0, + timescale: timescale, + duration: timescale, + samples: [] + }; + this.samples = []; + this.lastGopDTS = -99999999999999; + this.gop=[]; + this.firstUnit = true; + + this.h264 = new H264Parser(this); + + if (params.sps) { + let arr = new Uint8Array(params.sps); + if ((arr[0] & 0x1f) === 7) { + this.setSPS(arr); + } else { + Log$4.warn("bad SPS in SDP"); + } + } + if (params.pps) { + let arr = new Uint8Array(params.pps); + if ((arr[0] & 0x1f) === 8) { + this.setPPS(arr); + } else { + Log$4.warn("bad PPS in SDP"); + } + } + + if (this.mp4track.pps && this.mp4track.sps) { + this.readyToDecode = true; + } + } + + _scaled(timestamp) { + return timestamp >>> this.scaleFactor; + } + + _unscaled(timestamp) { + return timestamp << this.scaleFactor; + } + + setSPS(sps) { + this.h264.parseSPS(sps); + } + + setPPS(pps) { + this.h264.parsePPS(pps); + } + + remux(nalu) { + if (this.lastGopDTS < nalu.dts) { + this.gop.sort(BaseRemuxer.dtsSortFunc); + + if (this.gop.length > 1) { + // Aggregate multi-slices which belong to one frame + const groupedGop = BaseRemuxer.groupByDts(this.gop); + this.gop = Object.values(groupedGop).map(group => { + return group.reduce((preUnit, curUnit) => { + const naluData = curUnit.getData(); + naluData.set(new Uint8Array([0x0, 0x0, 0x0, 0x1])); + preUnit.appendData(naluData); + return preUnit; + }); + }); + } + + for (let unit of this.gop) { + // if (this.firstUnit) { + // unit.ntype = 5;//NALU.IDR; + // this.firstUnit = false; + // } + if (super.remux.call(this, unit)) { + this.mp4track.len += unit.getSize(); + } + } + this.gop = []; + this.lastGopDTS = nalu.dts; + } + if (this.h264.parseNAL(nalu)) { + this.gop.push(nalu); + } + } + + getPayload() { + if (!this.getPayloadBase()) { + return null; + } + + let payload = new Uint8Array(this.mp4track.len); + let offset = 0; + let samples=this.mp4track.samples; + let mp4Sample, lastDTS, pts, dts; + + + // Log.debug(this.samples.map((e)=>{ + // return Math.round((e.dts - this.initDTS)); + // })); + + // let minDuration = Number.MAX_SAFE_INTEGER; + while (this.samples.length) { + let sample = this.samples.shift(); + if (sample === null) { + // discontinuity + this.nextDts = undefined; + break; + } + + let unit = sample.unit; + + pts = sample.pts- this.initDTS; // /*Math.round(*/(sample.pts - this.initDTS)/*/this.tsAlign)*this.tsAlign*/; + dts = sample.dts - this.initDTS; ///*Math.round(*/(sample.dts - this.initDTS)/*/this.tsAlign)*this.tsAlign*/; + // ensure DTS is not bigger than PTS + dts = Math.min(pts,dts); + // if not first AVC sample of video track, normalize PTS/DTS with previous sample value + // and ensure that sample duration is positive + if (lastDTS !== undefined) { + let sampleDuration = this.scaled(dts - lastDTS); + // Log.debug(`Sample duration: ${sampleDuration}`); + if (sampleDuration < 0) { + Log$4.log(`invalid AVC sample duration at PTS/DTS: ${pts}/${dts}|lastDTS: ${lastDTS}:${sampleDuration}`); + this.mp4track.len -= unit.getSize(); + continue; + } + // minDuration = Math.min(sampleDuration, minDuration); + this.lastDurations.push(sampleDuration); + if (this.lastDurations.length > 100) { + this.lastDurations.shift(); + } + mp4Sample.duration = sampleDuration; + } else { + if (this.nextDts) { + let delta = dts - this.nextDts; + // if fragment are contiguous, or delta less than 600ms, ensure there is no overlap/hole between fragments + if (/*contiguous ||*/ Math.abs(Math.round(BaseRemuxer.toMS(delta))) < 600) { + + if (delta) { + // set DTS to next DTS + // Log.debug(`Video/PTS/DTS adjusted: ${pts}->${Math.max(pts - delta, this.nextDts)}/${dts}->${this.nextDts},delta:${delta}`); + dts = this.nextDts; + // offset PTS as well, ensure that PTS is smaller or equal than new DTS + pts = Math.max(pts - delta, dts); + } + } else { + if (delta < 0) { + Log$4.log(`skip frame from the past at DTS=${dts} with expected DTS=${this.nextDts}`); + this.mp4track.len -= unit.getSize(); + continue; + } + } + } + // remember first DTS of our avcSamples, ensure value is positive + this.firstDTS = Math.max(0, dts); + } + + mp4Sample = { + size: unit.getSize(), + duration: 0, + cts: this.scaled(pts - dts), + flags: { + isLeading: 0, + isDependedOn: 0, + hasRedundancy: 0, + degradPrio: 0 + } + }; + let flags = mp4Sample.flags; + if (sample.unit.isKeyframe() === true) { + // the current sample is a key frame + flags.dependsOn = 2; + flags.isNonSync = 0; + } else { + flags.dependsOn = 1; + flags.isNonSync = 1; + } + + payload.set(unit.getData(), offset); + offset += unit.getSize(); + + samples.push(mp4Sample); + lastDTS = dts; + } + + if (!samples.length) return null; + + let avgDuration = this.lastDurations.reduce(function(a, b) { return (a|0) + (b|0); }, 0) / (this.lastDurations.length||1)|0; + if (samples.length >= 2) { + this.lastSampleDuration = avgDuration; + mp4Sample.duration = avgDuration; + } else { + mp4Sample.duration = this.lastSampleDuration; + } + + if(samples.length && (!this.nextDts || navigator.userAgent.toLowerCase().indexOf('chrome') > -1)) { + let flags = samples[0].flags; + // chrome workaround, mark first sample as being a Random Access Point to avoid sourcebuffer append issue + // https://code.google.com/p/chromium/issues/detail?id=229412 + flags.dependsOn = 2; + flags.isNonSync = 0; + } + + // next AVC sample DTS should be equal to last sample DTS + last sample duration + this.nextDts = dts + this.unscaled(this.lastSampleDuration); + // Log.debug(`next dts: ${this.nextDts}, last duration: ${this.lastSampleDuration}, last dts: ${dts}`); + + return new Uint8Array(payload.buffer, 0, this.mp4track.len); + } + } + + class PayloadType { + static get H264() {return 1;} + static get AAC() {return 2;} + + static get map() {return { + [PayloadType.H264]: 'video', + [PayloadType.AAC]: 'audio' + }}; + + static get string_map() {return { + H264: PayloadType.H264, + AAC: PayloadType.AAC, + 'MP4A-LATM': PayloadType.AAC, + 'MPEG4-GENERIC': PayloadType.AAC + }} + } + + const LOG_TAG$1 = "remuxer"; + const Log$5 = getTagged(LOG_TAG$1); + + class Remuxer { + static get TrackConverters() {return { + [PayloadType.H264]: H264Remuxer, + [PayloadType.AAC]: AACRemuxer + }}; + + static get TrackScaleFactor() {return { + [PayloadType.H264]: 1,//4, + [PayloadType.AAC]: 0 + }}; + + static get TrackTimescale() {return { + [PayloadType.H264]: 90000,//22500, + [PayloadType.AAC]: 0 + }}; + + constructor(mediaElement) { + this.mse = new MSE([mediaElement]); + this.eventSource = new EventEmitter(); + this.mseEventSource = new EventSourceWrapper(this.mse.eventSource); + this.mse_ready = true; + + this.reset(); + + this.errorListener = this.mseClose.bind(this); + this.closeListener = this.mseClose.bind(this); + this.errorDecodeListener = this.mseErrorDecode.bind(this); + + this.eventSource.addEventListener('ready', this.init.bind(this)); + } + + initMSEHandlers() { + this.mseEventSource.on('error', this.errorListener); + this.mseEventSource.on('sourceclosed', this.closeListener); + this.mseEventSource.on('errordecode', this.errorDecodeListener); + } + + async reset() { + this.tracks = {}; + this.initialized = false; + this.initSegments = {}; + this.codecs = []; + this.streams = {}; + this.enabled = false; + await this.mse.clear(); + this.initMSEHandlers(); + } + + destroy() { + this.mseEventSource.destroy(); + this.mse.destroy(); + this.mse = null; + + this.detachClient(); + + this.eventSource.destroy(); + } + + onTracks(tracks) { + Log$5.debug(`ontracks: `, tracks.detail); + // store available track types + for (let track of tracks.detail) { + this.tracks[track.type] = new Remuxer.TrackConverters[track.type](Remuxer.TrackTimescale[track.type], Remuxer.TrackScaleFactor[track.type], track.params); + if (track.offset) { + this.tracks[track.type].timeOffset = track.offset; + } + if (track.duration) { + this.tracks[track.type].mp4track.duration = track.duration*(this.tracks[track.type].timescale || Remuxer.TrackTimescale[track.type]); + this.tracks[track.type].duration = track.duration; + } else { + this.tracks[track.type].duration = 1; + } + + // this.tracks[track.type].duration + } + this.mse.setLive(!this.client.seekable); + } + + setTimeOffset(timeOffset, track) { + if (this.tracks[track.type]) { + this.tracks[track.type].timeOffset = timeOffset;///this.tracks[track.type].scaleFactor; + } + } + + get MSE(){ + return this.mse; + } + + init() { + let tracks = []; + this.codecs = []; + let initmse = []; + let initPts = Infinity; + let initDts = Infinity; + for (let track_type in this.tracks) { + let track = this.tracks[track_type]; + if (!MSE.isSupported([track.mp4track.codec])) { + throw new Error(`${track.mp4track.type} codec ${track.mp4track.codec} is not supported`); + } + tracks.push(track.mp4track); + this.codecs.push(track.mp4track.codec); + track.init(initPts, initDts/*, false*/); + // initPts = Math.min(track.initPTS, initPts); + // initDts = Math.min(track.initDTS, initDts); + } + + for (let track_type in this.tracks) { + let track = this.tracks[track_type]; + //track.init(initPts, initDts); + this.initSegments[track_type] = MP4.initSegment([track.mp4track], track.duration*track.timescale, track.timescale); + initmse.push(this.initMSE(track_type, track.mp4track.codec)); + } + this.initialized = true; + return Promise.all(initmse).then(()=>{ + //this.mse.play(); + this.enabled = true; + }); + + } + + initMSE(track_type, codec) { + if (MSE.isSupported(this.codecs)) { + return this.mse.setCodec(track_type, `${PayloadType.map[track_type]}/mp4; codecs="${codec}"`).then(()=>{ + this.mse.feed(track_type, this.initSegments[track_type]); + // this.mse.play(); + // this.enabled = true; + }); + } else { + throw new Error('Codecs are not supported'); + } + } + + mseClose() { + // this.mse.clear(); + this.client.stop(); + this.eventSource.dispatchEvent('stopped'); + } + + mseErrorDecode() { + if(this.tracks[2]) { + console.warn(this.tracks[2].mp4track.type); + this.mse.buffers[2].destroy(); + delete this.tracks[2]; + } + } + + flush() { + this.onSamples(); + + if (!this.initialized) { + // Log.debug(`Initialize...`); + if (Object.keys(this.tracks).length) { + for (let track_type in this.tracks) { + if (!this.tracks[track_type].readyToDecode || !this.tracks[track_type].samples.length) return; + Log$5.debug(`Init MSE for track ${this.tracks[track_type].mp4track.type}`); + } + this.eventSource.dispatchEvent('ready'); + } + } else { + for (let track_type in this.tracks) { + let track = this.tracks[track_type]; + let pay = track.getPayload(); + if (pay && pay.byteLength) { + this.mse.feed(track_type, [MP4.moof(track.seq, track.scaled(track.firstDTS), track.mp4track), MP4.mdat(pay)]); + track.flush(); + } + } + } + } + + onSamples(ev) { + // TODO: check format + // let data = ev.detail; + // if (this.tracks[data.pay] && this.client.sampleQueues[data.pay].length) { + // console.log(`video ${data.units[0].dts}`); + for (let qidx in this.client.sampleQueues) { + let queue = this.client.sampleQueues[qidx]; + while (queue.length) { + let units = queue.shift(); + if(units){ + for (let chunk of units) { + if(this.tracks[qidx]) { + this.tracks[qidx].remux(chunk); + } + } + } + } + } + // } + } + + onAudioConfig(ev) { + if (this.tracks[ev.detail.pay]) { + this.tracks[ev.detail.pay].setConfig(ev.detail.config); + } + } + + attachClient(client) { + this.detachClient(); + this.client = client; + this.clientEventSource = new EventSourceWrapper(this.client.eventSource); + this.clientEventSource.on('samples', this.samplesListener); + this.clientEventSource.on('audio_config', this.audioConfigListener); + this.clientEventSource.on('tracks', this.onTracks.bind(this)); + this.clientEventSource.on('flush', this.flush.bind(this)); + this.clientEventSource.on('clear', ()=>{ + this.reset(); + this.mse.clear().then(()=>{ + //this.mse.play(); + this.initMSEHandlers(); + }); + }); + } + + detachClient() { + if (this.client) { + this.clientEventSource.destroy(); + // this.client.eventSource.removeEventListener('samples', this.onSamples.bind(this)); + // this.client.eventSource.removeEventListener('audio_config', this.onAudioConfig.bind(this)); + // // TODO: clear other listeners + // this.client.eventSource.removeEventListener('clear', this._clearListener); + // this.client.eventSource.removeEventListener('tracks', this._tracksListener); + // this.client.eventSource.removeEventListener('flush', this._flushListener); + this.client = null; + } + } + } + + class State { + constructor(name, stateMachine) { + this.stateMachine = stateMachine; + this.transitions = new Set(); + this.name = name; + } + + + activate() { + return Promise.resolve(null); + } + + finishTransition() {} + + failHandler() {} + + deactivate() { + return Promise.resolve(null); + } + } + + class StateMachine { + constructor() { + this.storage = {}; + this.currentState = null; + this.states = new Map(); + } + + addState(name, {activate, finishTransition, deactivate}) { + let state = new State(name, this); + if (activate) state.activate = activate; + if (finishTransition) state.finishTransition = finishTransition; + if (deactivate) state.deactivate = deactivate; + this.states.set(name, state); + return this; + } + + addTransition(fromName, toName){ + if (!this.states.has(fromName)) { + throw ReferenceError(`No such state: ${fromName} while connecting to ${toName}`); + } + if (!this.states.has(toName)) { + throw ReferenceError(`No such state: ${toName} while connecting from ${fromName}`); + } + this.states.get(fromName).transitions.add(toName); + return this; + } + + _promisify(res) { + let promise; + try { + promise = res; + if (!promise.then) { + promise = Promise.resolve(promise); + } + } catch (e) { + promise = Promise.reject(e); + } + return promise; + } + + transitionTo(stateName) { + if (this.currentState == null) { + let state = this.states.get(stateName); + return this._promisify(state.activate.call(this)) + .then((data)=> { + this.currentState = state; + return data; + }).then(state.finishTransition.bind(this)).catch((e)=>{ + state.failHandler(); + throw e; + }); + } + if (this.currentState.name == stateName) return Promise.resolve(); + if (this.currentState.transitions.has(stateName)) { + let state = this.states.get(stateName); + return this._promisify(state.deactivate.call(this)) + .then(state.activate.bind(this)).then((data)=> { + this.currentState = state; + return data; + }).then(state.finishTransition.bind(this)).catch((e)=>{ + state.failHandler(); + throw e; + }); + } else { + return Promise.reject(`No such transition: ${this.currentState.name} to ${stateName}`); + } + } + + } + + // export * from 'bp_statemachine'; + + const Log$6 = getTagged("parser:sdp"); + + class SDPParser { + constructor() { + this.version = -1; + this.origin = null; + this.sessionName = null; + this.timing = null; + this.sessionBlock = {}; + this.media = {}; + this.tracks = {}; + this.mediaMap = {}; + } + + parse(content) { + // Log.debug(content); + return new Promise((resolve, reject) => { + var dataString = content; + var success = true; + var currentMediaBlock = this.sessionBlock; + + // TODO: multiple audio/video tracks + + for (let line of dataString.split("\n")) { + line = line.replace(/\r/, ''); + if (0 === line.length) { + /* Empty row (last row perhaps?), skip to next */ + continue; + } + + switch (line.charAt(0)) { + case 'v': + if (-1 !== this.version) { + Log$6.log('Version present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseVersion(line); + break; + + case 'o': + if (null !== this.origin) { + Log$6.log('Origin present multiple times in SDP'); + reject(); + return false; + } + this._parseOrigin(line); + success = true; // parsing is optional + break; + + case 's': + if (null !== this.sessionName) { + Log$6.log('Session Name present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseSessionName(line); + break; + + case 't': + if (null !== this.timing) { + Log$6.log('Timing present multiple times in SDP'); + reject(); + return false; + } + success = success && this._parseTiming(line); + break; + + case 'm': + if (null !== currentMediaBlock && this.sessionBlock !== currentMediaBlock) { + /* Complete previous block and store it */ + this.media[currentMediaBlock.type] = currentMediaBlock; + } + + /* A wild media block appears */ + currentMediaBlock = {}; + currentMediaBlock.rtpmap = {}; + this._parseMediaDescription(line, currentMediaBlock); + break; + + case 'a': + SDPParser._parseAttribute(line, currentMediaBlock); + break; + + default: + Log$6.log('Ignored unknown SDP directive: ' + line); + break; + } + + if (!success) { + reject(); + return; + } + } + + this.media[currentMediaBlock.type] = currentMediaBlock; + + success ? resolve() : reject(); + }); + } + + _parseVersion(line) { + let matches = line.match(/^v=([0-9]+)$/); + if (!matches || !matches.length) { + Log$6.log('\'v=\' (Version) formatted incorrectly: ' + line); + return false; + } + + this.version = matches[1]; + if (0 != this.version) { + Log$6.log('Unsupported SDP version:' + this.version); + return false; + } + + return true; + } + + _parseOrigin(line) { + let matches = line.match(/^o=([^ ]+) (-?[0-9]+) (-?[0-9]+) (IN) (IP4|IP6) ([^ ]+)$/); + if (!matches || !matches.length) { + Log$6.log('\'o=\' (Origin) formatted incorrectly: ' + line); + return false; + } + + this.origin = {}; + this.origin.username = matches[1]; + this.origin.sessionid = matches[2]; + this.origin.sessionversion = matches[3]; + this.origin.nettype = matches[4]; + this.origin.addresstype = matches[5]; + this.origin.unicastaddress = matches[6]; + + return true; + } + + _parseSessionName(line) { + let matches = line.match(/^s=([^\r\n]+)$/); + if (!matches || !matches.length) { + Log$6.log('\'s=\' (Session Name) formatted incorrectly: ' + line); + return false; + } + + this.sessionName = matches[1]; + + return true; + } + + _parseTiming(line) { + let matches = line.match(/^t=([0-9]+) ([0-9]+)$/); + if (!matches || !matches.length) { + Log$6.log('\'t=\' (Timing) formatted incorrectly: ' + line); + return false; + } + + this.timing = {}; + this.timing.start = matches[1]; + this.timing.stop = matches[2]; + + return true; + } + + _parseMediaDescription(line, media) { + let matches = line.match(/^m=([^ ]+) ([^ ]+) ([^ ]+)[ ]/); + if (!matches || !matches.length) { + Log$6.log('\'m=\' (Media) formatted incorrectly: ' + line); + return false; + } + + media.type = matches[1]; + media.port = matches[2]; + media.proto = matches[3]; + media.fmt = line.substr(matches[0].length).split(' ').map(function (fmt, index, array) { + return parseInt(fmt); + }); + + for (let fmt of media.fmt) { + this.mediaMap[fmt] = media; + } + + return true; + } + + static _parseAttribute(line, media) { + if (null === media) { + /* Not in a media block, can't be bothered parsing attributes for session */ + return true; + } + + var matches; + /* Used for some cases of below switch-case */ + var separator = line.indexOf(':'); + var attribute = line.substr(0, (-1 === separator) ? 0x7FFFFFFF : separator); + /* 0x7FF.. is default */ + + switch (attribute) { + case 'a=recvonly': + case 'a=sendrecv': + case 'a=sendonly': + case 'a=inactive': + media.mode = line.substr('a='.length); + break; + case 'a=range': + matches = line.match(/^a=range:\s*([a-zA-Z-]+)=([0-9TZtz.]+|now)\s*-\s*([0-9TZtz.]*)$/); + media.range = [Number(matches[2] == "now" ? -1 : matches[2]), Number(matches[3]), matches[1]]; + break; + case 'a=control': + media.control = line.substr('a=control:'.length); + break; + + case 'a=rtpmap': + matches = line.match(/^a=rtpmap:(\d+) (.*)$/); + if (null === matches) { + Log$6.log('Could not parse \'rtpmap\' of \'a=\''); + return false; + } + + var payload = parseInt(matches[1]); + media.rtpmap[payload] = {}; + + var attrs = matches[2].split('/'); + media.rtpmap[payload].name = attrs[0].toUpperCase(); + media.rtpmap[payload].clock = attrs[1]; + if (undefined !== attrs[2]) { + media.rtpmap[payload].encparams = attrs[2]; + } + media.ptype = PayloadType.string_map[attrs[0].toUpperCase()]; + + break; + + case 'a=fmtp': + matches = line.match(/^a=fmtp:(\d+) (.*)$/); + if (!matches || 0 === matches.length) { + Log$6.log('Could not parse \'fmtp\' of \'a=\''); + return false; + } + + media.fmtp = {}; + for (var param of matches[2].split(';')) { + var idx = param.indexOf('='); + media.fmtp[param.substr(0, idx).toLowerCase().trim()] = param.substr(idx + 1).trim(); + } + break; + } + + return true; + } + + getSessionBlock() { + return this.sessionBlock; + } + + hasMedia(mediaType) { + return this.media[mediaType] != undefined; + } + + getMediaBlock(mediaType) { + return this.media[mediaType]; + } + + getMediaBlockByPayloadType(pt) { + // for (var m in this.media) { + // if (-1 !== this.media[m].fmt.indexOf(pt)) { + // return this.media[m]; + // } + // } + return this.mediaMap[pt] || null; + + //ErrorManager.dispatchError(826, [pt], true); + // Log.error(`failed to find media with payload type ${pt}`); + // + // return null; + } + + getMediaBlockList() { + var res = []; + for (var m in this.media) { + res.push(m); + } + + return res; + } + } + + const LOG_TAG$2 = "rtsp:stream"; + const Log$7 = getTagged(LOG_TAG$2); + + class RTSPStream { + + constructor(client, track) { + this.state = null; + this.client = client; + this.track = track; + this.rtpChannel = 1; + + this.stopKeepAlive(); + this.keepaliveInterval = null; + this.keepaliveTime = 30000; + } + + reset() { + this.stopKeepAlive(); + this.client.forgetRTPChannel(this.rtpChannel); + this.client = null; + this.track = null; + } + + start(lastSetupPromise = null) { + if (lastSetupPromise != null) { + // if a setup was already made, use the same session + return lastSetupPromise.then((obj) => this.sendSetup(obj.session)) + } else { + return this.sendSetup(); + } + } + + stop() { + return this.sendTeardown(); + } + + getSetupURL(track) { + let sessionBlock = this.client.sdp.getSessionBlock(); + if (Url.isAbsolute(track.control)) { + return track.control; + } else if (Url.isAbsolute(`${sessionBlock.control}${track.control}`)) { + return `${sessionBlock.control}${track.control}`; + } else if (Url.isAbsolute(`${this.client.contentBase}${track.control}`)) { + /* Should probably check session level control before this */ + return `${this.client.contentBase}${track.control}`; + } + else {//need return default + return track.control; + } + Log$7.error('Can\'t determine track URL from ' + + 'block.control:' + track.control + ', ' + + 'session.control:' + sessionBlock.control + ', and ' + + 'content-base:' + this.client.contentBase); + } + + getControlURL() { + let ctrl = this.client.sdp.getSessionBlock().control; + if (Url.isAbsolute(ctrl)) { + return ctrl; + } else if (!ctrl || '*' === ctrl) { + return this.client.contentBase; + } else { + return `${this.client.contentBase}${ctrl}`; + } + } + + sendKeepalive() { + if (this.client.methods.includes('GET_PARAMETER')) { + return this.client.sendRequest('GET_PARAMETER', this.getSetupURL(this.track), { + 'Session': this.session + }); + } else { + return this.client.sendRequest('OPTIONS', '*'); + } + } + + stopKeepAlive() { + clearInterval(this.keepaliveInterval); + } + + startKeepAlive() { + this.keepaliveInterval = setInterval(() => { + this.sendKeepalive().catch((e) => { + Log$7.error(e); + if (e instanceof RTSPError) { + if (Number(e.data.parsed.code) == 501) { + return; + } + } + this.client.reconnect(); + }); + }, this.keepaliveTime); + } + + sendRequest(_cmd, _params = {}) { + let params = {}; + if (this.session) { + params['Session'] = this.session; + } + Object.assign(params, _params); + return this.client.sendRequest(_cmd, this.getControlURL(), params); + } + + sendSetup(session = null) { + this.state = RTSPClientSM.STATE_SETUP; + this.rtpChannel = this.client.interleaveChannelIndex; + let interleavedChannels = this.client.interleaveChannelIndex++ + "-" + this.client.interleaveChannelIndex++; + let params = { + 'Transport': `RTP/AVP/TCP;unicast;interleaved=${interleavedChannels}`, + 'Date': new Date().toUTCString() + }; + if(session){ + params.Session = session; + } + return this.client.sendRequest('SETUP', this.getSetupURL(this.track), params).then((_data) => { + this.session = _data.headers['session'].split(';'); + let transport = _data.headers['transport']; + if (transport) { + let interleaved = transport.match(/interleaved=([0-9]+)-([0-9]+)/)[1]; + if (interleaved) { + this.rtpChannel = Number(interleaved); + } + } + + let sessionParamsChunks = this.session.slice(1); + let sessionParams = {}; + for (let chunk of sessionParamsChunks) { + let kv = chunk.split('='); + sessionParams[kv[0]] = kv[1]; + } + if (sessionParams['timeout']) { + this.keepaliveInterval = Number(sessionParams['timeout']) * 500; // * 1000 / 2 + } + /*if (!/RTP\/AVP\/TCP;unicast;interleaved=/.test(_data.headers["transport"])) { + // TODO: disconnect stream and notify client + throw new Error("Connection broken"); + }*/ + this.client.useRTPChannel(this.rtpChannel); + this.startKeepAlive(); + return {track: this.track, data: _data, session: this.session[0]}; + }); + } + } + + /* + * JavaScript MD5 + * https://github.com/blueimp/JavaScript-MD5 + * + * Copyright 2011, Sebastian Tschan + * https://blueimp.net + * + * Licensed under the MIT license: + * https://opensource.org/licenses/MIT + * + * Based on + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + + + /* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ + function safeAdd(x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF) + } + + /* + * Bitwise rotate a 32-bit number to the left. + */ + function bitRotateLeft(num, cnt) { + return (num << cnt) | (num >>> (32 - cnt)) + } + + /* + * These functions implement the four basic operations the algorithm uses. + */ + function md5cmn(q, a, b, x, s, t) { + return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b) + } + function md5ff(a, b, c, d, x, s, t) { + return md5cmn((b & c) | ((~b) & d), a, b, x, s, t) + } + function md5gg(a, b, c, d, x, s, t) { + return md5cmn((b & d) | (c & (~d)), a, b, x, s, t) + } + function md5hh(a, b, c, d, x, s, t) { + return md5cmn(b ^ c ^ d, a, b, x, s, t) + } + function md5ii(a, b, c, d, x, s, t) { + return md5cmn(c ^ (b | (~d)), a, b, x, s, t) + } + + /* + * Calculate the MD5 of an array of little-endian words, and a bit length. + */ + function binlMD5(x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << (len % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var i; + var olda; + var oldb; + var oldc; + var oldd; + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + for (i = 0; i < x.length; i += 16) { + olda = a; + oldb = b; + oldc = c; + oldd = d; + + a = md5ff(a, b, c, d, x[i], 7, -680876936); + d = md5ff(d, a, b, c, x[i + 1], 12, -389564586); + c = md5ff(c, d, a, b, x[i + 2], 17, 606105819); + b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330); + a = md5ff(a, b, c, d, x[i + 4], 7, -176418897); + d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426); + c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341); + b = md5ff(b, c, d, a, x[i + 7], 22, -45705983); + a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416); + d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417); + c = md5ff(c, d, a, b, x[i + 10], 17, -42063); + b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162); + a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682); + d = md5ff(d, a, b, c, x[i + 13], 12, -40341101); + c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290); + b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329); + + a = md5gg(a, b, c, d, x[i + 1], 5, -165796510); + d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632); + c = md5gg(c, d, a, b, x[i + 11], 14, 643717713); + b = md5gg(b, c, d, a, x[i], 20, -373897302); + a = md5gg(a, b, c, d, x[i + 5], 5, -701558691); + d = md5gg(d, a, b, c, x[i + 10], 9, 38016083); + c = md5gg(c, d, a, b, x[i + 15], 14, -660478335); + b = md5gg(b, c, d, a, x[i + 4], 20, -405537848); + a = md5gg(a, b, c, d, x[i + 9], 5, 568446438); + d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690); + c = md5gg(c, d, a, b, x[i + 3], 14, -187363961); + b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501); + a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467); + d = md5gg(d, a, b, c, x[i + 2], 9, -51403784); + c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473); + b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734); + + a = md5hh(a, b, c, d, x[i + 5], 4, -378558); + d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463); + c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562); + b = md5hh(b, c, d, a, x[i + 14], 23, -35309556); + a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060); + d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353); + c = md5hh(c, d, a, b, x[i + 7], 16, -155497632); + b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640); + a = md5hh(a, b, c, d, x[i + 13], 4, 681279174); + d = md5hh(d, a, b, c, x[i], 11, -358537222); + c = md5hh(c, d, a, b, x[i + 3], 16, -722521979); + b = md5hh(b, c, d, a, x[i + 6], 23, 76029189); + a = md5hh(a, b, c, d, x[i + 9], 4, -640364487); + d = md5hh(d, a, b, c, x[i + 12], 11, -421815835); + c = md5hh(c, d, a, b, x[i + 15], 16, 530742520); + b = md5hh(b, c, d, a, x[i + 2], 23, -995338651); + + a = md5ii(a, b, c, d, x[i], 6, -198630844); + d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415); + c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905); + b = md5ii(b, c, d, a, x[i + 5], 21, -57434055); + a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571); + d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606); + c = md5ii(c, d, a, b, x[i + 10], 15, -1051523); + b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799); + a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359); + d = md5ii(d, a, b, c, x[i + 15], 10, -30611744); + c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380); + b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649); + a = md5ii(a, b, c, d, x[i + 4], 6, -145523070); + d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379); + c = md5ii(c, d, a, b, x[i + 2], 15, 718787259); + b = md5ii(b, c, d, a, x[i + 9], 21, -343485551); + + a = safeAdd(a, olda); + b = safeAdd(b, oldb); + c = safeAdd(c, oldc); + d = safeAdd(d, oldd); + } + return [a, b, c, d] + } + + /* + * Convert an array of little-endian words to a string + */ + function binl2rstr(input) { + var i; + var output = ''; + var length32 = input.length * 32; + for (i = 0; i < length32; i += 8) { + output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF); + } + return output + } + + /* + * Convert a raw string to an array of little-endian words + * Characters >255 have their high-byte silently ignored. + */ + function rstr2binl(input) { + var i; + var output = []; + output[(input.length >> 2) - 1] = undefined; + for (i = 0; i < output.length; i += 1) { + output[i] = 0; + } + var length8 = input.length * 8; + for (i = 0; i < length8; i += 8) { + output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32); + } + return output + } + + /* + * Calculate the MD5 of a raw string + */ + function rstrMD5(s) { + return binl2rstr(binlMD5(rstr2binl(s), s.length * 8)) + } + + /* + * Calculate the HMAC-MD5, of a key and some data (raw strings) + */ + function rstrHMACMD5(key, data) { + var i; + var bkey = rstr2binl(key); + var ipad = []; + var opad = []; + var hash; + ipad[15] = opad[15] = undefined; + if (bkey.length > 16) { + bkey = binlMD5(bkey, key.length * 8); + } + for (i = 0; i < 16; i += 1) { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + hash = binlMD5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); + return binl2rstr(binlMD5(opad.concat(hash), 512 + 128)) + } + + /* + * Convert a raw string to a hex string + */ + function rstr2hex(input) { + var hexTab = '0123456789abcdef'; + var output = ''; + var x; + var i; + for (i = 0; i < input.length; i += 1) { + x = input.charCodeAt(i); + output += hexTab.charAt((x >>> 4) & 0x0F) + + hexTab.charAt(x & 0x0F); + } + return output + } + + /* + * Encode a string as utf-8 + */ + function str2rstrUTF8(input) { + return unescape(encodeURIComponent(input)) + } + + /* + * Take string arguments and return either raw or hex encoded strings + */ + function rawMD5(s) { + return rstrMD5(str2rstrUTF8(s)) + } + function hexMD5(s) { + return rstr2hex(rawMD5(s)) + } + function rawHMACMD5(k, d) { + return rstrHMACMD5(str2rstrUTF8(k), str2rstrUTF8(d)) + } + function hexHMACMD5(k, d) { + return rstr2hex(rawHMACMD5(k, d)) + } + + function md5(string, key, raw) { + if (!key) { + if (!raw) { + return hexMD5(string) + } + return rawMD5(string) + } + if (!raw) { + return hexHMACMD5(key, string) + } + return rawHMACMD5(key, string) + } + + // TODO: asm.js + class RTP { + constructor(pkt/*uint8array*/, sdp) { + let bytes = new DataView(pkt.buffer, pkt.byteOffset, pkt.byteLength); + + this.version = bytes.getUint8(0) >>> 6; + this.padding = bytes.getUint8(0) & 0x20 >>> 5; + this.has_extension = bytes.getUint8(0) & 0x10 >>> 4; + this.csrc = bytes.getUint8(0) & 0x0F; + this.marker = bytes.getUint8(1) >>> 7; + this.pt = bytes.getUint8(1) & 0x7F; + this.sequence = bytes.getUint16(2) ; + this.timestamp = bytes.getUint32(4); + this.ssrc = bytes.getUint32(8); + this.csrcs = []; + + let pktIndex=12; + if (this.csrc>0) { + this.csrcs.push(bytes.getUint32(pktIndex)); + pktIndex+=4; + } + if (this.has_extension==1) { + this.extension = bytes.getUint16(pktIndex); + this.ehl = bytes.getUint16(pktIndex+2); + pktIndex+=4; + this.header_data = pkt.slice(pktIndex, this.ehl); + pktIndex += this.ehl; + } + + this.headerLength = pktIndex; + let padLength = 0; + if (this.padding) { + padLength = bytes.getUint8(pkt.byteLength-1); + } + + // this.bodyLength = pkt.byteLength-this.headerLength-padLength; + + this.media = sdp.getMediaBlockByPayloadType(this.pt); + if (null === this.media) { + Log.log(`Media description for payload type: ${this.pt} not provided.`); + } else { + this.type = this.media.ptype;//PayloadType.string_map[this.media.rtpmap[this.media.fmt[0]].name]; + } + + this.data = pkt.subarray(pktIndex); + // this.timestamp = 1000 * (this.timestamp / this.media.rtpmap[this.pt].clock); + // console.log(this); + } + getPayload() { + return this.data; + } + + getTimestampMS() { + return this.timestamp; //1000 * (this.timestamp / this.media.rtpmap[this.pt].clock); + } + + toString() { + return "RTP(" + + "version:" + this.version + ", " + + "padding:" + this.padding + ", " + + "has_extension:" + this.has_extension + ", " + + "csrc:" + this.csrc + ", " + + "marker:" + this.marker + ", " + + "pt:" + this.pt + ", " + + "sequence:" + this.sequence + ", " + + "timestamp:" + this.timestamp + ", " + + "ssrc:" + this.ssrc + ")"; + } + + isVideo(){return this.media.type == 'video';} + isAudio(){return this.media.type == 'audio';} + + + } + + class RTPFactory { + constructor(sdp) { + this.tsOffsets={}; + for (let pay in sdp.media) { + for (let pt of sdp.media[pay].fmt) { + this.tsOffsets[pt] = {last: 0, overflow: 0}; + } + } + } + + build(pkt/*uint8array*/, sdp) { + let rtp = new RTP(pkt, sdp); + + let tsOffset = this.tsOffsets[rtp.pt]; + if (tsOffset) { + rtp.timestamp += tsOffset.overflow; + if (tsOffset.last && Math.abs(rtp.timestamp - tsOffset.last) > 0x7fffffff) { + console.log(`\nlast ts: ${tsOffset.last}\n + new ts: ${rtp.timestamp}\n + new ts adjusted: ${rtp.timestamp+0xffffffff}\n + last overflow: ${tsOffset.overflow}\n + new overflow: ${tsOffset.overflow+0xffffffff}\n + `); + tsOffset.overflow += 0xffffffff; + rtp.timestamp += 0xffffffff; + } + /*if (rtp.timestamp>0xffffffff) { + console.log(`ts: ${rtp.timestamp}, seq: ${rtp.sequence}`); + }*/ + tsOffset.last = rtp.timestamp; + } + + return rtp; + } + } + + class RTSPMessage { + static get RTSP_1_0() {return "RTSP/1.0";} + + constructor(_rtsp_version) { + this.version = _rtsp_version; + } + + build(_cmd, _host, _params={}, _payload=null) { + let requestString = `${_cmd} ${_host} ${this.version}\r\n`; + for (let param in _params) { + requestString+=`${param}: ${_params[param]}\r\n`; + } + // TODO: binary payload + if (_payload) { + requestString+=`Content-Length: ${_payload.length}\r\n`; + } + requestString+='\r\n'; + if (_payload) { + requestString+=_payload; + } + return requestString; + } + + parse(_data) { + let lines = _data.split('\r\n'); + let parsed = { + headers:{}, + body:null, + code: 0, + statusLine: '' + }; + + let match; + [match, parsed.code, parsed.statusLine] = lines[0].match(new RegExp(`${this.version}[ ]+([0-9]{3})[ ]+(.*)`)); + parsed.code = Number(parsed.code); + let lineIdx = 1; + + while (lines[lineIdx]) { + let [k,v] = lines[lineIdx].split(/:(.+)/); + parsed.headers[k.toLowerCase()] = v.trim(); + lineIdx++; + } + + parsed.body = lines.slice(lineIdx).join('\n\r'); + + return parsed; + } + + } + + const MessageBuilder = new RTSPMessage(RTSPMessage.RTSP_1_0); + + // TODO: asm.js + class NALUAsm { + + constructor() { + this.fragmented_nalu = null; + } + + + static parseNALHeader(hdr) { + return { + nri: hdr & 0x60, + type: hdr & 0x1F + } + } + + parseSingleNALUPacket(rawData, header, dts, pts) { + return new NALU(header.type, header.nri, rawData.subarray(0), dts, pts); + } + + parseAggregationPacket(rawData, header, dts, pts) { + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + let nal_start_idx = 0; + let don = null; + if (NALU.STAP_B === header.type) { + don = data.getUint16(nal_start_idx); + nal_start_idx += 2; + } + let ret = []; + while (nal_start_idx < data.byteLength) { + let size = data.getUint16(nal_start_idx); + nal_start_idx += 2; + let header = NALUAsm.parseNALHeader(data.getInt8(nal_start_idx)); + nal_start_idx++; + let nalu = this.parseSingleNALUPacket(rawData.subarray(nal_start_idx, nal_start_idx+size), header, dts, pts); + if (nalu !== null) { + ret.push(nalu); + } + nal_start_idx+=size; + } + return ret; + } + + parseFragmentationUnit(rawData, header, dts, pts) { + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + let nal_start_idx = 0; + let fu_header = data.getUint8(nal_start_idx); + let is_start = (fu_header & 0x80) >>> 7; + let is_end = (fu_header & 0x40) >>> 6; + let payload_type = fu_header & 0x1F; + let ret = null; + + nal_start_idx++; + let don = 0; + if (NALU.FU_B === header.type) { + don = data.getUint16(nal_start_idx); + nal_start_idx += 2; + } + + if (is_start) { + this.fragmented_nalu = new NALU(payload_type, header.nri, rawData.subarray(nal_start_idx), dts, pts); + } + if (this.fragmented_nalu && this.fragmented_nalu.ntype === payload_type) { + if (!is_start) { + this.fragmented_nalu.appendData(rawData.subarray(nal_start_idx)); + } + if (is_end) { + ret = this.fragmented_nalu; + this.fragmented_nalu = null; + return ret; + } + } + return null; + } + + onNALUFragment(rawData, dts, pts) { + + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + + let header = NALUAsm.parseNALHeader(data.getUint8(0)); + + let nal_start_idx = 1; + + let unit = null; + if (header.type > 0 && header.type < 24) { + unit = this.parseSingleNALUPacket(rawData.subarray(nal_start_idx), header, dts, pts); + } else if (NALU.FU_A === header.type || NALU.FU_B === header.type) { + unit = this.parseFragmentationUnit(rawData.subarray(nal_start_idx), header, dts, pts); + } else if (NALU.STAP_A === header.type || NALU.STAP_B === header.type) { + return this.parseAggregationPacket(rawData.subarray(nal_start_idx), header, dts, pts); + } else { + /* 30 - 31 is undefined, ignore those (RFC3984). */ + Log.log('Undefined NAL unit, type: ' + header.type); + return null; + } + if (unit) { + return [unit]; + } + return null; + } + } + + class AACFrame { + + constructor(data, dts, pts) { + this.dts = dts; + this.pts = pts ? pts : this.dts; + + this.data=data;//.subarray(offset); + } + + getData() { + return this.data; + } + + getSize() { + return this.data.byteLength; + } + } + + // import {AACParser} from "../parsers/aac.js"; + // TODO: asm.js + class AACAsm { + constructor() { + this.config = null; + } + + onAACFragment(pkt) { + let rawData = pkt.getPayload(); + if (!pkt.media) { + return null; + } + let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); + + let sizeLength = Number(pkt.media.fmtp['sizelength'] || 0); + let indexLength = Number(pkt.media.fmtp['indexlength'] || 0); + let indexDeltaLength = Number(pkt.media.fmtp['indexdeltalength'] || 0); + let CTSDeltaLength = Number(pkt.media.fmtp['ctsdeltalength'] || 0); + let DTSDeltaLength = Number(pkt.media.fmtp['dtsdeltalength'] || 0); + let RandomAccessIndication = Number(pkt.media.fmtp['randomaccessindication'] || 0); + let StreamStateIndication = Number(pkt.media.fmtp['streamstateindication'] || 0); + let AuxiliaryDataSizeLength = Number(pkt.media.fmtp['auxiliarydatasizelength'] || 0); + + let configHeaderLength = + sizeLength + Math.max(indexLength, indexDeltaLength) + CTSDeltaLength + DTSDeltaLength + + RandomAccessIndication + StreamStateIndication + AuxiliaryDataSizeLength; + + + let auHeadersLengthPadded = 0; + let offset = 0; + let ts = (Math.round(pkt.getTimestampMS()/1024) << 10) * 90000 / this.config.samplerate; + if (0 !== configHeaderLength) { + /* The AU header section is not empty, read it from payload */ + let auHeadersLengthInBits = data.getUint16(0); // Always 2 octets, without padding + auHeadersLengthPadded = 2 + (auHeadersLengthInBits>>>3) + ((auHeadersLengthInBits & 0x7)?1:0); // Add padding + + //this.config = AACParser.parseAudioSpecificConfig(new Uint8Array(rawData, 0 , auHeadersLengthPadded)); + // TODO: parse config + let frames = []; + let frameOffset=0; + let bits = new BitArray(rawData.subarray(2 + offset)); + let cts = 0; + let dts = 0; + for (let offset=0; offset{ + if (this.connected) { + while (this.transport.dataQueue.length) { + this.onData(this.transport.dataQueue.pop()); + } + } + }; + this._onConnect = this.onConnected.bind(this); + this._onDisconnect = this.onDisconnected.bind(this); + } + + static streamType() { + return null; + } + + destroy() { + this.detachTransport(); + } + + attachTransport(transport) { + if (this.transport) { + this.detachTransport(); + } + this.transport = transport; + this.transport.eventSource.addEventListener('data', this._onData); + this.transport.eventSource.addEventListener('connected', this._onConnect); + this.transport.eventSource.addEventListener('disconnected', this._onDisconnect); + } + + detachTransport() { + if (this.transport) { + this.transport.eventSource.removeEventListener('data', this._onData); + this.transport.eventSource.removeEventListener('connected', this._onConnect); + this.transport.eventSource.removeEventListener('disconnected', this._onDisconnect); + this.transport = null; + } + } + reset() { + + } + + start() { + Log.log('Client started'); + this.paused = false; + // this.startStreamFlush(); + } + + stop() { + Log.log('Client paused'); + this.paused = true; + // this.stopStreamFlush(); + } + + seek(timeOffset) { + + } + + setSource(source) { + this.stop(); + this.endpoint = source; + this.sourceUrl = source.urlpath; + } + + startStreamFlush() { + this.flushInterval = setInterval(()=>{ + if (!this.paused) { + this.eventSource.dispatchEvent('flush'); + } + }, this.options.flush); + } + + stopStreamFlush() { + clearInterval(this.flushInterval); + } + + onData(data) { + + } + + onConnected() { + if (!this.seekable) { + this.transport.dataQueue = []; + this.eventSource.dispatchEvent('clear'); + } + this.connected = true; + } + + onDisconnected() { + this.connected = false; + } + + queryCredentials() { + return Promise.resolve(); + } + + setCredentials(user, password) { + this.endpoint.user = user; + this.endpoint.pass = password; + this.endpoint.auth = `${user}:${password}`; + } + } + + class AACParser { + static get SampleRates() {return [ + 96000, 88200, + 64000, 48000, + 44100, 32000, + 24000, 22050, + 16000, 12000, + 11025, 8000, + 7350];} + + // static Profile = [ + // 0: Null + // 1: AAC Main + // 2: AAC LC (Low Complexity) + // 3: AAC SSR (Scalable Sample Rate) + // 4: AAC LTP (Long Term Prediction) + // 5: SBR (Spectral Band Replication) + // 6: AAC Scalable + // ] + + static parseAudioSpecificConfig(bytesOrBits) { + let config; + if (bytesOrBits.byteLength) { // is byteArray + config = new BitArray(bytesOrBits); + } else { + config = bytesOrBits; + } + + let bitpos = config.bitpos+(config.src.byteOffset+config.bytepos)*8; + let prof = config.readBits(5); + this.codec = `mp4a.40.${prof}`; + let sfi = config.readBits(4); + if (sfi == 0xf) config.skipBits(24); + let channels = config.readBits(4); + + return { + config: bitSlice(new Uint8Array(config.src.buffer), bitpos, bitpos+16), + codec: `mp4a.40.${prof}`, + samplerate: AACParser.SampleRates[sfi], + channels: channels + } + } + + static parseStreamMuxConfig(bytes) { + // ISO_IEC_14496-3 Part 3 Audio. StreamMuxConfig + let config = new BitArray(bytes); + + if (!config.readBits(1)) { + config.skipBits(14); + return AACParser.parseAudioSpecificConfig(config); + } + } + } + + const LOG_TAG$3 = "rtsp:session"; + const Log$8 = getTagged(LOG_TAG$3); + + class RTSPSession { + + constructor(client, sessionId) { + this.state = null; + this.client = client; + this.sessionId = sessionId; + this.url = this.getControlURL(); + } + + reset() { + this.client = null; + } + + start() { + return this.sendPlay(); + } + + stop() { + return this.sendTeardown(); + } + + getControlURL() { + let ctrl = this.client.sdp.getSessionBlock().control; + if (Url.isAbsolute(ctrl)) { + return ctrl; + } else if (!ctrl || '*' === ctrl) { + return this.client.contentBase; + } else { + return `${this.client.contentBase}${ctrl}`; + } + } + + sendRequest(_cmd, _params = {}) { + let params = {}; + if (this.sessionId) { + params['Session'] = this.sessionId; + } + Object.assign(params, _params); + return this.client.sendRequest(_cmd, this.getControlURL(), params); + } + + async sendPlay(pos = 0) { + this.state = RTSPClientSM.STATE_PLAY; + let params = {}; + let range = this.client.sdp.sessionBlock.range; + if (range) { + // TODO: seekable + if (range[0] == -1) { + range[0] = 0;// Do not handle now at the moment + } + // params['Range'] = `${range[2]}=${range[0]}-`; + } + let data = await this.sendRequest('PLAY', params); + this.state = RTSPClientSM.STATE_PLAYING; + return {data: data}; + } + + async sendPause() { + if (!this.client.supports("PAUSE")) { + return; + } + this.state = RTSPClientSM.STATE_PAUSE; + await this.sendRequest("PAUSE"); + this.state = RTSPClientSM.STATE_PAUSED; + } + + async sendTeardown() { + if (this.state != RTSPClientSM.STATE_TEARDOWN) { + this.state = RTSPClientSM.STATE_TEARDOWN; + await this.sendRequest("TEARDOWN"); + Log$8.log('RTSPClient: STATE_TEARDOWN'); + ///this.client.connection.disconnect(); + // TODO: Notify client + } + } + } + + const LOG_TAG$4 = "client:rtsp"; + const Log$9 = getTagged(LOG_TAG$4); + + class RTSPClient extends BaseClient { + constructor(options={flush: 200}) { + super(options); + this.clientSM = new RTSPClientSM(this); + this.clientSM.ontracks = (tracks) => { + this.eventSource.dispatchEvent('tracks', tracks); + this.startStreamFlush(); + }; + this.sampleQueues={}; + } + + static streamType() { + return 'rtsp'; + } + + setSource(url) { + super.setSource(url); + this.clientSM.setSource(url); + } + attachTransport(transport) { + super.attachTransport(transport); + this.clientSM.transport = transport; + } + + detachTransport() { + super.detachTransport(); + this.clientSM.transport = null; + } + + reset() { + super.reset(); + this.sampleQueues={}; + } + + destroy() { + this.clientSM.destroy(); + return super.destroy(); + } + + start() { + super.start(); + if (this.transport) { + return this.transport.ready.then(() => { + return this.clientSM.start(); + }); + } else { + return Promise.reject("no transport attached"); + } + } + + stop() { + super.stop(); + return this.clientSM.stop(); + } + + onData(data) { + this.clientSM.onData(data); + } + + onConnected() { + this.clientSM.onConnected(); + super.onConnected(); + } + + onDisconnected() { + super.onDisconnected(); + this.clientSM.onDisconnected(); + } + } + + class AuthError extends Error { + constructor(msg) { + super(msg); + } + } + + class RTSPError extends Error { + constructor(data) { + super(data.msg); + this.data = data; + } + } + + class RTSPClientSM extends StateMachine { + static get USER_AGENT() {return 'SFRtsp 0.3';} + static get STATE_INITIAL() {return 1 << 0;} + static get STATE_OPTIONS() {return 1 << 1;} + static get STATE_DESCRIBE () {return 1 << 2;} + static get STATE_SETUP() {return 1 << 3;} + static get STATE_STREAMS() {return 1 << 4;} + static get STATE_TEARDOWN() {return 1 << 5;} + static get STATE_PLAY() {return 1 << 6;} + static get STATE_PLAYING() {return 1 << 7;} + static get STATE_PAUSE() {return 1 << 8;} + static get STATE_PAUSED() {return 1 << 9;} + // static STATE_PAUSED = 1 << 6; + + constructor(parent) { + super(); + + this.parent = parent; + this.transport = null; + this.payParser = new RTPPayloadParser(); + this.rtp_channels = new Set(); + this.sessions = {}; + this.ontracks = null; + + this.addState(RTSPClientSM.STATE_INITIAL,{ + }).addState(RTSPClientSM.STATE_OPTIONS, { + activate: this.sendOptions, + finishTransition: this.onOptions + }).addState(RTSPClientSM.STATE_DESCRIBE, { + activate: this.sendDescribe, + finishTransition: this.onDescribe + }).addState(RTSPClientSM.STATE_SETUP, { + activate: this.sendSetup, + finishTransition: this.onSetup + }).addState(RTSPClientSM.STATE_STREAMS, { + + }).addState(RTSPClientSM.STATE_TEARDOWN, { + activate: ()=>{ + this.started = false; + }, + finishTransition: ()=>{ + return this.transitionTo(RTSPClientSM.STATE_INITIAL) + } + }).addTransition(RTSPClientSM.STATE_INITIAL, RTSPClientSM.STATE_OPTIONS) + .addTransition(RTSPClientSM.STATE_INITIAL, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_OPTIONS, RTSPClientSM.STATE_DESCRIBE) + .addTransition(RTSPClientSM.STATE_DESCRIBE, RTSPClientSM.STATE_SETUP) + .addTransition(RTSPClientSM.STATE_SETUP, RTSPClientSM.STATE_STREAMS) + .addTransition(RTSPClientSM.STATE_TEARDOWN, RTSPClientSM.STATE_INITIAL) + // .addTransition(RTSPClientSM.STATE_STREAMS, RTSPClientSM.STATE_PAUSED) + // .addTransition(RTSPClientSM.STATE_PAUSED, RTSPClientSM.STATE_STREAMS) + .addTransition(RTSPClientSM.STATE_STREAMS, RTSPClientSM.STATE_TEARDOWN) + // .addTransition(RTSPClientSM.STATE_PAUSED, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_SETUP, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_DESCRIBE, RTSPClientSM.STATE_TEARDOWN) + .addTransition(RTSPClientSM.STATE_OPTIONS, RTSPClientSM.STATE_TEARDOWN); + + this.reset(); + + this.shouldReconnect = false; + + // TODO: remove listeners + // this.connection.eventSource.addEventListener('connected', ()=>{ + // if (this.shouldReconnect) { + // this.reconnect(); + // } + // }); + // this.connection.eventSource.addEventListener('disconnected', ()=>{ + // if (this.started) { + // this.shouldReconnect = true; + // } + // }); + // this.connection.eventSource.addEventListener('data', (data)=>{ + // let channel = new DataView(data).getUint8(1); + // if (this.rtp_channels.has(channel)) { + // this.onRTP({packet: new Uint8Array(data, 4), type: channel}); + // } + // + // }); + } + + destroy() { + this.parent = null; + } + + setSource(url) { + this.reset(); + this.endpoint = url; + this.url = `${url.protocol}://${url.location}${url.urlpath}`; + } + + onConnected() { + if (this.rtpFactory) { + this.rtpFactory = null; + } + if (this.shouldReconnect) { + this.start(); + } + } + + async onDisconnected() { + this.reset(); + this.shouldReconnect = true; + await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); + await this.transitionTo(RTSPClientSM.STATE_INITIAL); + } + + start() { + if (this.currentState.name !== RTSPClientSM.STATE_STREAMS) { + return this.transitionTo(RTSPClientSM.STATE_OPTIONS); + } else { + // TODO: seekable + let promises = []; + for (let session in this.sessions) { + promises.push(this.sessions[session].sendPlay()); + } + return Promise.all(promises); + } + } + + onData(data) { + let channel = data[1]; + if (this.rtp_channels.has(channel)) { + this.onRTP({packet: data.subarray(4), type: channel}); + } + } + + useRTPChannel(channel) { + this.rtp_channels.add(channel); + } + + forgetRTPChannel(channel) { + this.rtp_channels.delete(channel); + } + + stop() { + this.shouldReconnect = false; + let promises = []; + for (let session in this.sessions) { + promises.push(this.sessions[session].sendPause()); + } + return Promise.all(promises); + // this.mse = null; + } + + async reset() { + this.authenticator = ''; + this.methods = []; + this.tracks = []; + this.rtpBuffer={}; + for (let stream in this.streams) { + this.streams[stream].reset(); + } + for (let session in this.sessions) { + this.sessions[session].reset(); + } + this.streams={}; + this.sessions={}; + this.contentBase = ""; + if (this.currentState) { + if (this.currentState.name != RTSPClientSM.STATE_INITIAL) { + await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); + await this.transitionTo(RTSPClientSM.STATE_INITIAL); + } + } else { + await this.transitionTo(RTSPClientSM.STATE_INITIAL); + } + this.sdp = null; + this.interleaveChannelIndex = 0; + this.session = null; + this.timeOffset = {}; + this.lastTimestamp = {}; + } + + async reconnect() { + //this.parent.eventSource.dispatchEvent('clear'); + await this.reset(); + if (this.currentState.name != RTSPClientSM.STATE_INITIAL) { + await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); + return this.transitionTo(RTSPClientSM.STATE_OPTIONS); + } else { + return this.transitionTo(RTSPClientSM.STATE_OPTIONS); + } + } + + supports(method) { + return this.methods.includes(method) + } + + parse(_data) { + Log$9.debug(_data.payload); + let d=_data.payload.split('\r\n\r\n'); + let parsed = MessageBuilder.parse(d[0]); + let len = Number(parsed.headers['content-length']); + if (len) { + let d=_data.payload.split('\r\n\r\n'); + parsed.body = d[1]; + } else { + parsed.body=""; + } + return parsed + } + + sendRequest(_cmd, _host, _params={}, _payload=null) { + this.cSeq++; + Object.assign(_params, { + CSeq: this.cSeq, + 'User-Agent': RTSPClientSM.USER_AGENT + }); + if (this.authenticator) { + _params['Authorization'] = this.authenticator(_cmd); + } + return this.send(MessageBuilder.build(_cmd, _host, _params, _payload), _cmd).catch((e)=>{ + if ((e instanceof AuthError) && !_params['Authorization'] ) { + return this.sendRequest(_cmd, _host, _params, _payload); + } else { + throw e; + } + }); + } + + async send(_data, _method) { + if (this.transport) { + try { + await this.transport.ready; + } catch(e) { + this.onDisconnected(); + throw e; + } + Log$9.debug(_data); + let response = await this.transport.send(_data); + let parsed = this.parse(response); + // TODO: parse status codes + if (parsed.code == 401 /*&& !this.authenticator */) { + Log$9.debug(parsed.headers['www-authenticate']); + let auth = parsed.headers['www-authenticate']; + let method = auth.substring(0, auth.indexOf(' ')); + auth = auth.substr(method.length+1); + let chunks = auth.split(','); + + let ep = this.parent.endpoint; + if (!ep.user || !ep.pass) { + try { + await this.parent.queryCredentials.call(this.parent); + } catch (e) { + throw new AuthError(); + } + } + + if (method.toLowerCase() == 'digest') { + let parsedChunks = {}; + for (let chunk of chunks) { + let c = chunk.trim(); + let [k,v] = c.split('='); + parsedChunks[k] = v.substr(1, v.length-2); + } + this.authenticator = (_method)=>{ + let ep = this.parent.endpoint; + let ha1 = md5(`${ep.user}:${parsedChunks.realm}:${ep.pass}`); + let ha2 = md5(`${_method}:${this.url}`); + let response = md5(`${ha1}:${parsedChunks.nonce}:${ha2}`); + let tail=''; // TODO: handle other params + return `Digest username="${ep.user}", realm="${parsedChunks.realm}", nonce="${parsedChunks.nonce}", uri="${this.url}", response="${response}"${tail}`; + }; + } else { + this.authenticator = ()=>{return `Basic ${btoa(this.parent.endpoint.auth)}`;}; + } + + throw new AuthError(parsed); + } + if (parsed.code >= 300) { + Log$9.error(parsed.statusLine); + this.parent.options.errorHandler(new RTSPError({msg: `RTSP error: ${parsed.code} ${parsed.statusLine}`, parsed: parsed})); + } + return parsed; + } else { + return Promise.reject("No transport attached"); + } + } + + sendOptions() { + this.reset(); + this.started = true; + this.cSeq = 0; + return this.sendRequest('OPTIONS', this.url, {}); + } + + onOptions(data) { + this.methods = data.headers['public'].split(',').map((e)=>e.trim()); + this.transitionTo(RTSPClientSM.STATE_DESCRIBE); + } + + sendDescribe() { + return this.sendRequest('DESCRIBE', this.url, { + 'Accept': 'application/sdp' + }).then((data)=>{ + this.sdp = new SDPParser(); + return this.sdp.parse(data.body).catch(()=>{ + throw new Error("Failed to parse SDP"); + }).then(()=>{return data;}); + }); + } + + onDescribe(data) { + this.contentBase = data.headers['content-base'] || this.url;// `${this.endpoint.protocol}://${this.endpoint.location}${this.endpoint.urlpath}/`; + this.tracks = this.sdp.getMediaBlockList(); + this.rtpFactory = new RTPFactory(this.sdp); + + Log$9.log('SDP contained ' + this.tracks.length + ' track(s). Calling SETUP for each.'); + + if (data.headers['session']) { + this.session = data.headers['session']; + } + + if (!this.tracks.length) { + throw new Error("No tracks in SDP"); + } + + this.transitionTo(RTSPClientSM.STATE_SETUP); + } + + sendSetup() { + let streams=[]; + let lastPromise = null; + + // TODO: select first video and first audio tracks + for (let track_type of this.tracks) { + Log$9.log("setup track: "+track_type); + // if (track_type=='audio') continue; + // if (track_type=='video') continue; + let track = this.sdp.getMediaBlock(track_type); + // If payload type is defined in the specification then "rtpmap" may be not specified + if (!track.rtpmap[track.fmt[0]]) { + Log$9.warn(`Pyload type "${track.fmt[0]}" is not supported`); + continue; + } + + // If payload type is dynamic then check it encoding name + if (!PayloadType.string_map[track.rtpmap[track.fmt[0]].name]) { + Log$9.warn(`Pyload type "${track.rtpmap[track.fmt[0]].name}" is not supported`); + continue; + } + + this.streams[track_type] = new RTSPStream(this, track); + let setupPromise = this.streams[track_type].start(lastPromise); + lastPromise = setupPromise; + this.parent.sampleQueues[PayloadType.string_map[track.rtpmap[track.fmt[0]].name]]=[]; + this.rtpBuffer[track.fmt[0]]=[]; + streams.push(setupPromise.then(({track, data})=>{ + this.timeOffset[track.fmt[0]] = 0; + try { + let rtp_info = data.headers["rtp-info"].split(';'); + for (let chunk of rtp_info) { + let [key, val] = chunk.split("="); + if (key === "rtptime") { + this.timeOffset[track.fmt[0]] = 0;//Number(val); + } + } + } catch (e) { + // new Date().getTime(); + } + let params = { + timescale: 0, + scaleFactor: 0 + }; + if (track.fmtp) { + if (track.fmtp['sprop-parameter-sets']) { + let sps_pps = track.fmtp['sprop-parameter-sets'].split(','); + params = { + sps:base64ToArrayBuffer(sps_pps[0]), + pps:base64ToArrayBuffer(sps_pps[1]) + }; + } else if (track.fmtp['config']) { + let config = track.fmtp['config']; + this.has_config = track.fmtp['cpresent']!='0'; + let generic = track.rtpmap[track.fmt[0]].name == 'MPEG4-GENERIC'; + if (generic) { + params={config: + AACParser.parseAudioSpecificConfig(hexToByteArray(config)) + }; + this.payParser.aacparser.setConfig(params.config); + } else if (config) { + // todo: parse audio specific config for mpeg4-generic + params={config: + AACParser.parseStreamMuxConfig(hexToByteArray(config)) + }; + this.payParser.aacparser.setConfig(params.config); + } + } + } + params.duration = this.sdp.sessionBlock.range?this.sdp.sessionBlock.range[1]-this.sdp.sessionBlock.range[0]:1; + this.parent.seekable = (params.duration > 1); + let res = { + track: track, + offset: this.timeOffset[track.fmt[0]], + type: PayloadType.string_map[track.rtpmap[track.fmt[0]].name], + params: params, + duration: params.duration + }; + console.log(res, this.timeOffset); + let session = data.headers.session.split(';')[0]; + if (!this.sessions[session]) { + this.sessions[session] = new RTSPSession(this, session); + } + return res; + })); + } + return Promise.all(streams).then((tracks)=>{ + let sessionPromises = []; + for (let session in this.sessions) { + sessionPromises.push(this.sessions[session].start()); + } + return Promise.all(sessionPromises).then(()=>{ + if (this.ontracks) { + this.ontracks(tracks); + } + }) + }).catch((e)=>{ + console.error(e); + this.stop(); + this.reset(); + }); + } + + onSetup() { + this.transitionTo(RTSPClientSM.STATE_STREAMS); + } + + onRTP(_data) { + if (!this.rtpFactory) return; + + let rtp = this.rtpFactory.build(_data.packet, this.sdp); + if (!rtp.type) { + return; + } + + if (this.timeOffset[rtp.pt] === undefined) { + //console.log(rtp.pt, this.timeOffset[rtp.pt]); + this.rtpBuffer[rtp.pt].push(rtp); + return; + } + + if (this.lastTimestamp[rtp.pt] === undefined) { + this.lastTimestamp[rtp.pt] = rtp.timestamp-this.timeOffset[rtp.pt]; + } + + let queue = this.rtpBuffer[rtp.pt]; + queue.push(rtp); + + while (queue.length) { + let rtp = queue.shift(); + + rtp.timestamp = rtp.timestamp-this.timeOffset[rtp.pt]-this.lastTimestamp[rtp.pt]; + // TODO: overflow + // if (rtp.timestamp < 0) { + // rtp.timestamp = (rtp.timestamp + Number.MAX_SAFE_INTEGER) % 0x7fffffff; + // } + if (rtp.media) { + let pay = this.payParser.parse(rtp); + if (pay) { + // if (typeof pay == typeof []) { + this.parent.sampleQueues[rtp.type].push(pay); + // } else { + // this.parent.sampleQueues[rtp.type].push([pay]); + // } + } + } + } + // this.remuxer.feedRTP(); + } + } + + // ASN.1 JavaScript decoder + // Copyright (c) 2008-2013 Lapo Luchini + + // Permission to use, copy, modify, and/or distribute this software for any + // purpose with or without fee is hereby granted, provided that the above + // copyright notice and this permission notice appear in all copies. + // + // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + /*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ + /*global oids */ + + var hardLimit = 100, + ellipsis = "\u2026", + DOM = { + tag: function (tagName, className) { + var t = document.createElement(tagName); + t.className = className; + return t; + }, + text: function (str) { + return document.createTextNode(str); + } + }; + + class Stream { + static get hexDigits() { + return "0123456789ABCDEF"; + }; + + static get reTime() { + return /^((?:1[89]|2\d)?\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|[-+](?:[0]\d|1[0-2])([0-5]\d)?)?$/; + } + + + constructor(enc, pos) { + if (enc instanceof Stream) { + this.enc = enc.enc; + this.pos = enc.pos; + } else { + this.enc = enc; + this.pos = pos; + } + } + + get(pos) { + if (pos === undefined) + pos = this.pos++; + if (pos >= this.enc.length) + throw 'Requesting byte offset ' + pos + ' on a stream of length ' + this.enc.length; + return this.enc[pos]; + }; + + hexByte(b) { + return Stream.hexDigits.charAt((b >> 4) & 0xF) + Stream.hexDigits.charAt(b & 0xF); + }; + + hexDump(start, end, raw) { + var s = ""; + for (var i = start; i < end; ++i) { + s += this.hexByte(this.get(i)); + if (raw !== true) + switch (i & 0xF) { + case 0x7: + s += " "; + break; + case 0xF: + s += "\n"; + break; + default: + s += " "; + } + } + return s; + }; + + parseStringISO(start, end) { + var s = ""; + for (var i = start; i < end; ++i) + s += String.fromCharCode(this.get(i)); + return s; + }; + + parseStringUTF(start, end) { + var s = ""; + for (var i = start; i < end;) { + var c = this.get(i++); + if (c < 128) + s += String.fromCharCode(c); + else if ((c > 191) && (c < 224)) + s += String.fromCharCode(((c & 0x1F) << 6) | (this.get(i++) & 0x3F)); + else + s += String.fromCharCode(((c & 0x0F) << 12) | ((this.get(i++) & 0x3F) << 6) | (this.get(i++) & 0x3F)); + } + return s; + }; + + parseStringBMP(start, end) { + var str = ""; + for (var i = start; i < end; i += 2) { + var high_byte = this.get(i); + var low_byte = this.get(i + 1); + str += String.fromCharCode((high_byte << 8) + low_byte); + } + + return str; + }; + + parseTime(start, end) { + var s = this.parseStringISO(start, end), + m = Stream.reTime.exec(s); + if (!m) + return "Unrecognized time: " + s; + s = m[1] + "-" + m[2] + "-" + m[3] + " " + m[4]; + if (m[5]) { + s += ":" + m[5]; + if (m[6]) { + s += ":" + m[6]; + if (m[7]) + s += "." + m[7]; + } + } + if (m[8]) { + s += " UTC"; + if (m[8] != 'Z') { + s += m[8]; + if (m[9]) + s += ":" + m[9]; + } + } + return s; + }; + + parseInteger(start, end) { + //TODO support negative numbers + var len = end - start; + if (len > 4) { + len <<= 3; + var s = this.get(start); + if (s === 0) + len -= 8; + else + while (s < 128) { + s <<= 1; + --len; + } + return "(" + len + " bit)"; + } + var n = 0; + for (var i = start; i < end; ++i) + n = (n << 8) | this.get(i); + return n; + }; + + parseBitString(start, end) { + var unusedBit = this.get(start), + lenBit = ((end - start - 1) << 3) - unusedBit, + s = "(" + lenBit + " bit)"; + if (lenBit <= 20) { + var skip = unusedBit; + s += " "; + for (var i = end - 1; i > start; --i) { + var b = this.get(i); + for (var j = skip; j < 8; ++j) + s += (b >> j) & 1 ? "1" : "0"; + skip = 0; + } + } + return s; + }; + + parseOctetString(start, end) { + var len = end - start, + s = "(" + len + " byte) "; + if (len > hardLimit) + end = start + hardLimit; + for (var i = start; i < end; ++i) + s += this.hexByte(this.get(i)); //TODO: also try Latin1? + if (len > hardLimit) + s += ellipsis; + return s; + }; + + parseOID(start, end) { + var s = '', + n = 0, + bits = 0; + for (var i = start; i < end; ++i) { + var v = this.get(i); + n = (n << 7) | (v & 0x7F); + bits += 7; + if (!(v & 0x80)) { // finished + if (s === '') { + var m = n < 80 ? n < 40 ? 0 : 1 : 2; + s = m + "." + (n - m * 40); + } else + s += "." + ((bits >= 31) ? "bigint" : n); + n = bits = 0; + } + } + return s; + }; + } + + class ASN1 { + static get reSeemsASCII() { + return /^[ -~]+$/; + } + + constructor(stream, header, length, tag, sub) { + this.stream = stream; + this.header = header; + this.length = length; + this.tag = tag; + this.sub = sub; + } + + typeName() { + if (this.tag === undefined) + return "unknown"; + var tagClass = this.tag >> 6, + tagConstructed = (this.tag >> 5) & 1, + tagNumber = this.tag & 0x1F; + switch (tagClass) { + case 0: // universal + switch (tagNumber) { + case 0x00: + return "EOC"; + case 0x01: + return "BOOLEAN"; + case 0x02: + return "INTEGER"; + case 0x03: + return "BIT_STRING"; + case 0x04: + return "OCTET_STRING"; + case 0x05: + return "NULL"; + case 0x06: + return "OBJECT_IDENTIFIER"; + case 0x07: + return "ObjectDescriptor"; + case 0x08: + return "EXTERNAL"; + case 0x09: + return "REAL"; + case 0x0A: + return "ENUMERATED"; + case 0x0B: + return "EMBEDDED_PDV"; + case 0x0C: + return "UTF8String"; + case 0x10: + return "SEQUENCE"; + case 0x11: + return "SET"; + case 0x12: + return "NumericString"; + case 0x13: + return "PrintableString"; // ASCII subset + case 0x14: + return "TeletexString"; // aka T61String + case 0x15: + return "VideotexString"; + case 0x16: + return "IA5String"; // ASCII + case 0x17: + return "UTCTime"; + case 0x18: + return "GeneralizedTime"; + case 0x19: + return "GraphicString"; + case 0x1A: + return "VisibleString"; // ASCII subset + case 0x1B: + return "GeneralString"; + case 0x1C: + return "UniversalString"; + case 0x1E: + return "BMPString"; + default: + return "Universal_" + tagNumber.toString(16); + } + case 1: + return "Application_" + tagNumber.toString(16); + case 2: + return "[" + tagNumber + "]"; // Context + case 3: + return "Private_" + tagNumber.toString(16); + } + } + + content() { + if (this.tag === undefined) + return null; + var tagClass = this.tag >> 6, + tagNumber = this.tag & 0x1F, + content = this.posContent(), + len = Math.abs(this.length); + if (tagClass !== 0) { // universal + if (this.sub !== null) + return "(" + this.sub.length + " elem)"; + //TODO: TRY TO PARSE ASCII STRING + var s = this.stream.parseStringISO(content, content + Math.min(len, hardLimit)); + if (ASN1.reSeemsASCII.test(s)) + return s.substring(0, 2 * hardLimit) + ((s.length > 2 * hardLimit) ? ellipsis : ""); + else + return this.stream.parseOctetString(content, content + len); + } + switch (tagNumber) { + case 0x01: // BOOLEAN + return (this.stream.get(content) === 0) ? "false" : "true"; + case 0x02: // INTEGER + return this.stream.parseInteger(content, content + len); + case 0x03: // BIT_STRING + return this.sub ? "(" + this.sub.length + " elem)" : + this.stream.parseBitString(content, content + len); + case 0x04: // OCTET_STRING + return this.sub ? "(" + this.sub.length + " elem)" : + this.stream.parseOctetString(content, content + len); + //case 0x05: // NULL + case 0x06: // OBJECT_IDENTIFIER + return this.stream.parseOID(content, content + len); + //case 0x07: // ObjectDescriptor + //case 0x08: // EXTERNAL + //case 0x09: // REAL + //case 0x0A: // ENUMERATED + //case 0x0B: // EMBEDDED_PDV + case 0x10: // SEQUENCE + case 0x11: // SET + return "(" + this.sub.length + " elem)"; + case 0x0C: // UTF8String + return this.stream.parseStringUTF(content, content + len); + case 0x12: // NumericString + case 0x13: // PrintableString + case 0x14: // TeletexString + case 0x15: // VideotexString + case 0x16: // IA5String + //case 0x19: // GraphicString + case 0x1A: // VisibleString + //case 0x1B: // GeneralString + //case 0x1C: // UniversalString + return this.stream.parseStringISO(content, content + len); + case 0x1E: // BMPString + return this.stream.parseStringBMP(content, content + len); + case 0x17: // UTCTime + case 0x18: // GeneralizedTime + return this.stream.parseTime(content, content + len); + } + return null; + }; + + toString() { + return this.typeName() + "@" + this.stream.pos + "[header:" + this.header + ",length:" + this.length + ",sub:" + ((this.sub === null) ? 'null' : this.sub.length) + "]"; + }; + + print(indent) { + if (indent === undefined) indent = ''; + document.writeln(indent + this); + if (this.sub !== null) { + indent += ' '; + for (var i = 0, max = this.sub.length; i < max; ++i) + this.sub[i].print(indent); + } + }; + + toPrettyString(indent) { + if (indent === undefined) indent = ''; + var s = indent + this.typeName() + " @" + this.stream.pos; + if (this.length >= 0) + s += "+"; + s += this.length; + if (this.tag & 0x20) + s += " (constructed)"; + else if (((this.tag == 0x03) || (this.tag == 0x04)) && (this.sub !== null)) + s += " (encapsulates)"; + s += "\n"; + if (this.sub !== null) { + indent += ' '; + for (var i = 0, max = this.sub.length; i < max; ++i) + s += this.sub[i].toPrettyString(indent); + } + return s; + }; + + toDOM() { + var node = DOM.tag("div", "node"); + node.asn1 = this; + var head = DOM.tag("div", "head"); + var s = this.typeName().replace(/_/g, " "); + head.innerHTML = s; + var content = this.content(); + if (content !== null) { + content = String(content).replace(/"; + s += "Length: " + this.header + "+"; + if (this.length >= 0) + s += this.length; + else + s += (-this.length) + " (undefined)"; + if (this.tag & 0x20) + s += "
(constructed)"; + else if (((this.tag == 0x03) || (this.tag == 0x04)) && (this.sub !== null)) + s += "
(encapsulates)"; + //TODO if (this.tag == 0x03) s += "Unused bits: " + if (content !== null) { + s += "
Value:
" + content + ""; + if ((typeof oids === 'object') && (this.tag == 0x06)) { + var oid = oids[content]; + if (oid) { + if (oid.d) s += "
" + oid.d; + if (oid.c) s += "
" + oid.c; + if (oid.w) s += "
(warning!)"; + } + } + } + value.innerHTML = s; + node.appendChild(value); + var sub = DOM.tag("div", "sub"); + if (this.sub !== null) { + for (var i = 0, max = this.sub.length; i < max; ++i) + sub.appendChild(this.sub[i].toDOM()); + } + node.appendChild(sub); + head.onclick = function () { + node.className = (node.className == "node collapsed") ? "node" : "node collapsed"; + }; + return node; + }; + + posStart() { + return this.stream.pos; + }; + + posContent() { + return this.stream.pos + this.header; + }; + + posEnd() { + return this.stream.pos + this.header + Math.abs(this.length); + }; + + fakeHover(current) { + this.node.className += " hover"; + if (current) + this.head.className += " hover"; + }; + + fakeOut(current) { + var re = / ?hover/; + this.node.className = this.node.className.replace(re, ""); + if (current) + this.head.className = this.head.className.replace(re, ""); + }; + + toHexDOM_sub(node, className, stream, start, end) { + if (start >= end) + return; + var sub = DOM.tag("span", className); + sub.appendChild(DOM.text( + stream.hexDump(start, end))); + node.appendChild(sub); + }; + + toHexDOM(root) { + var node = DOM.tag("span", "hex"); + if (root === undefined) root = node; + this.head.hexNode = node; + this.head.onmouseover = function () { + this.hexNode.className = "hexCurrent"; + }; + this.head.onmouseout = function () { + this.hexNode.className = "hex"; + }; + node.asn1 = this; + node.onmouseover = function () { + var current = !root.selected; + if (current) { + root.selected = this.asn1; + this.className = "hexCurrent"; + } + this.asn1.fakeHover(current); + }; + node.onmouseout = function () { + var current = (root.selected == this.asn1); + this.asn1.fakeOut(current); + if (current) { + root.selected = null; + this.className = "hex"; + } + }; + this.toHexDOM_sub(node, "tag", this.stream, this.posStart(), this.posStart() + 1); + this.toHexDOM_sub(node, (this.length >= 0) ? "dlen" : "ulen", this.stream, this.posStart() + 1, this.posContent()); + if (this.sub === null) + node.appendChild(DOM.text( + this.stream.hexDump(this.posContent(), this.posEnd()))); + else if (this.sub.length > 0) { + var first = this.sub[0]; + var last = this.sub[this.sub.length - 1]; + this.toHexDOM_sub(node, "intro", this.stream, this.posContent(), first.posStart()); + for (var i = 0, max = this.sub.length; i < max; ++i) + node.appendChild(this.sub[i].toHexDOM(root)); + this.toHexDOM_sub(node, "outro", this.stream, last.posEnd(), this.posEnd()); + } + return node; + }; + + toHexString(root) { + return this.stream.hexDump(this.posStart(), this.posEnd(), true); + }; + + } + + ASN1.decodeLength = function (stream) { + var buf = stream.get(), + len = buf & 0x7F; + if (len == buf) + return len; + if (len > 3) + throw "Length over 24 bits not supported at position " + (stream.pos - 1); + if (len === 0) + return -1; // undefined + buf = 0; + for (var i = 0; i < len; ++i) + buf = (buf << 8) | stream.get(); + return buf; + }; + ASN1.hasContent = function (tag, len, stream) { + if (tag & 0x20) // constructed + return true; + if ((tag < 0x03) || (tag > 0x04)) + return false; + var p = new Stream(stream); + if (tag == 0x03) p.get(); // BitString unused bits, must be in [0, 7] + var subTag = p.get(); + if ((subTag >> 6) & 0x01) // not (universal or context) + return false; + try { + var subLength = ASN1.decodeLength(p); + return ((p.pos - stream.pos) + subLength == len); + } catch (exception) { + return false; + } + }; + ASN1.decode = function (stream) { + if (!(stream instanceof Stream)) + stream = new Stream(stream, 0); + var streamStart = new Stream(stream), + tag = stream.get(), + len = ASN1.decodeLength(stream), + header = stream.pos - streamStart.pos, + sub = null; + if (ASN1.hasContent(tag, len, stream)) { + // it has content, so we decode it + var start = stream.pos; + if (tag == 0x03) stream.get(); // skip BitString unused bits, must be in [0, 7] + sub = []; + if (len >= 0) { + // definite length + var end = start + len; + while (stream.pos < end) + sub[sub.length] = ASN1.decode(stream); + if (stream.pos != end) + throw "Content size is not correct for container starting at offset " + start; + } else { + // undefined length + try { + for (; ;) { + var s = ASN1.decode(stream); + if (s.tag === 0) + break; + sub[sub.length] = s; + } + len = start - stream.pos; + } catch (e) { + throw "Exception while decoding undefined length content: " + e; + } + } + } else + stream.pos += len; // skip content + return new ASN1(streamStart, header, len, tag, sub); + }; + ASN1.test = function () { + var test = [ + {value: [0x27], expected: 0x27}, + {value: [0x81, 0xC9], expected: 0xC9}, + {value: [0x83, 0xFE, 0xDC, 0xBA], expected: 0xFEDCBA} + ]; + for (var i = 0, max = test.length; i < max; ++i) { + var stream = new Stream(test[i].value, 0), + res = ASN1.decodeLength(stream); + if (res != test[i].expected) + document.write("In test[" + i + "] expected " + test[i].expected + " got " + res + "\n"); + } + }; + + // prng4.js - uses Arcfour as a PRNG + + class Arcfour { + constructor() { + this.i = 0; + this.j = 0; + this.S = []; + } + } + + // Initialize arcfour context from key, an array of ints, each from [0..255] + function ARC4init(key) { + var i, j, t; + for(i = 0; i < 256; ++i) + this.S[i] = i; + j = 0; + for(i = 0; i < 256; ++i) { + j = (j + this.S[i] + key[i % key.length]) & 255; + t = this.S[i]; + this.S[i] = this.S[j]; + this.S[j] = t; + } + this.i = 0; + this.j = 0; + } + + function ARC4next() { + var t; + this.i = (this.i + 1) & 255; + this.j = (this.j + this.S[this.i]) & 255; + t = this.S[this.i]; + this.S[this.i] = this.S[this.j]; + this.S[this.j] = t; + return this.S[(t + this.S[this.i]) & 255]; + } + + Arcfour.prototype.init = ARC4init; + Arcfour.prototype.next = ARC4next; + + // Plug in your RNG constructor here + function prng_newstate() { + return new Arcfour(); + } + + // Pool size must be a multiple of 4 and greater than 32. + // An array of bytes the size of the pool will be passed to init() + var rng_psize = 256; + + // Random number generator - requires a PRNG backend, e.g. prng4.js + var rng_state; + var rng_pool; + var rng_pptr; + + // Initialize the pool with junk if needed. + if(rng_pool == null) { + rng_pool = new Array(); + rng_pptr = 0; + var t; + if(window.crypto && window.crypto.getRandomValues) { + // Extract entropy (2048 bits) from RNG if available + var z = new Uint32Array(256); + window.crypto.getRandomValues(z); + for (t = 0; t < z.length; ++t) + rng_pool[rng_pptr++] = z[t] & 255; + } + + // Use mouse events for entropy, if we do not have enough entropy by the time + // we need it, entropy will be generated by Math.random. + var onMouseMoveListener = function(ev) { + this.count = this.count || 0; + if (this.count >= 256 || rng_pptr >= rng_psize) { + if (window.removeEventListener) + window.removeEventListener("mousemove", onMouseMoveListener, false); + else if (window.detachEvent) + window.detachEvent("onmousemove", onMouseMoveListener); + return; + } + try { + var mouseCoordinates = ev.x + ev.y; + rng_pool[rng_pptr++] = mouseCoordinates & 255; + this.count += 1; + } catch (e) { + // Sometimes Firefox will deny permission to access event properties for some reason. Ignore. + } + }; + if (window.addEventListener) + window.addEventListener("mousemove", onMouseMoveListener, false); + else if (window.attachEvent) + window.attachEvent("onmousemove", onMouseMoveListener); + + } + + function rng_get_byte() { + if(rng_state == null) { + rng_state = prng_newstate(); + // At this point, we may not have collected enough entropy. If not, fall back to Math.random + while (rng_pptr < rng_psize) { + var random = Math.floor(65536 * Math.random()); + rng_pool[rng_pptr++] = random & 255; + } + rng_state.init(rng_pool); + for(rng_pptr = 0; rng_pptr < rng_pool.length; ++rng_pptr) + rng_pool[rng_pptr] = 0; + rng_pptr = 0; + } + // TODO: allow reseeding after first request + return rng_state.next(); + } + + function rng_get_bytes(ba) { + var i; + for(i = 0; i < ba.length; ++i) ba[i] = rng_get_byte(); + } + + class SecureRandom { + constructor(){} + } + + SecureRandom.prototype.nextBytes = rng_get_bytes; + + // Copyright (c) 2005 Tom Wu + // All Rights Reserved. + // See "LICENSE" for details. + + // Basic JavaScript BN library - subset useful for RSA encryption. + + // Bits per digit + var dbits; + + // (public) Constructor + class BigInteger { + constructor(a,b,c) { + if (a != null) + if ("number" == typeof a) this.fromNumber(a, b, c); + else if (b == null && "string" != typeof a) this.fromString(a, 256); + else this.fromString(a, b); + } + } + + // return new, unset BigInteger + function nbi() { return new BigInteger(null); } + + // am: Compute w_j += (x*this_i), propagate carries, + // c is initial carry, returns final carry. + // c < 3*dvalue, x < 2*dvalue, this_i < dvalue + // We need to select the fastest one that works in this environment. + + // am1: use a single mult and divide to get the high bits, + // max digit bits should be 26 because + // max internal value = 2*dvalue^2-2*dvalue (< 2^53) + function am1(i,x,w,j,c,n) { + while(--n >= 0) { + var v = x*this[i++]+w[j]+c; + c = Math.floor(v/0x4000000); + w[j++] = v&0x3ffffff; + } + return c; + } + // am2 avoids a big mult-and-extract completely. + // Max digit bits should be <= 30 because we do bitwise ops + // on values up to 2*hdvalue^2-hdvalue-1 (< 2^31) + function am2(i,x,w,j,c,n) { + var xl = x&0x7fff, xh = x>>15; + while(--n >= 0) { + var l = this[i]&0x7fff; + var h = this[i++]>>15; + var m = xh*l+h*xl; + l = xl*l+((m&0x7fff)<<15)+w[j]+(c&0x3fffffff); + c = (l>>>30)+(m>>>15)+xh*h+(c>>>30); + w[j++] = l&0x3fffffff; + } + return c; + } + // Alternately, set max digit bits to 28 since some + // browsers slow down when dealing with 32-bit numbers. + function am3(i,x,w,j,c,n) { + var xl = x&0x3fff, xh = x>>14; + while(--n >= 0) { + var l = this[i]&0x3fff; + var h = this[i++]>>14; + var m = xh*l+h*xl; + l = xl*l+((m&0x3fff)<<14)+w[j]+c; + c = (l>>28)+(m>>14)+xh*h; + w[j++] = l&0xfffffff; + } + return c; + } + if(navigator.appName == "Microsoft Internet Explorer") { + BigInteger.prototype.am = am2; + dbits = 30; + } + else if(navigator.appName != "Netscape") { + BigInteger.prototype.am = am1; + dbits = 26; + } + else { // Mozilla/Netscape seems to prefer am3 + BigInteger.prototype.am = am3; + dbits = 28; + } + + BigInteger.prototype.DB = dbits; + BigInteger.prototype.DM = ((1<= 0; --i) r[i] = this[i]; + r.t = this.t; + r.s = this.s; + } + + // (protected) set from integer value x, -DV <= x < DV + function bnpFromInt(x) { + this.t = 1; + this.s = (x<0)?-1:0; + if(x > 0) this[0] = x; + else if(x < -1) this[0] = x+this.DV; + else this.t = 0; + } + + // return bigint initialized to value + function nbv(i) { var r = nbi(); r.fromInt(i); return r; } + + // (protected) set from string and radix + function bnpFromString(s,b) { + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 256) k = 8; // byte array + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else { this.fromRadix(s,b); return; } + this.t = 0; + this.s = 0; + var i = s.length, mi = false, sh = 0; + while(--i >= 0) { + var x = (k==8)?s[i]&0xff:intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-") mi = true; + continue; + } + mi = false; + if(sh == 0) + this[this.t++] = x; + else if(sh+k > this.DB) { + this[this.t-1] |= (x&((1<<(this.DB-sh))-1))<>(this.DB-sh)); + } + else + this[this.t-1] |= x<= this.DB) sh -= this.DB; + } + if(k == 8 && (s[0]&0x80) != 0) { + this.s = -1; + if(sh > 0) this[this.t-1] |= ((1<<(this.DB-sh))-1)< 0 && this[this.t-1] == c) --this.t; + } + + // (public) return string representation in given radix + function bnToString(b) { + if(this.s < 0) return "-"+this.negate().toString(b); + var k; + if(b == 16) k = 4; + else if(b == 8) k = 3; + else if(b == 2) k = 1; + else if(b == 32) k = 5; + else if(b == 4) k = 2; + else return this.toRadix(b); + var km = (1< 0) { + if(p < this.DB && (d = this[i]>>p) > 0) { m = true; r = int2char(d); } + while(i >= 0) { + if(p < k) { + d = (this[i]&((1<>(p+=this.DB-k); + } + else { + d = (this[i]>>(p-=k))&km; + if(p <= 0) { p += this.DB; --i; } + } + if(d > 0) m = true; + if(m) r += int2char(d); + } + } + return m?r:"0"; + } + + // (public) -this + function bnNegate() { var r = nbi(); BigInteger.ZERO.subTo(this,r); return r; } + + // (public) |this| + function bnAbs() { return (this.s<0)?this.negate():this; } + + // (public) return + if this > a, - if this < a, 0 if equal + function bnCompareTo(a) { + var r = this.s-a.s; + if(r != 0) return r; + var i = this.t; + r = i-a.t; + if(r != 0) return (this.s<0)?-r:r; + while(--i >= 0) if((r=this[i]-a[i]) != 0) return r; + return 0; + } + + // returns bit length of the integer x + function nbits(x) { + var r = 1, t; + if((t=x>>>16) != 0) { x = t; r += 16; } + if((t=x>>8) != 0) { x = t; r += 8; } + if((t=x>>4) != 0) { x = t; r += 4; } + if((t=x>>2) != 0) { x = t; r += 2; } + if((t=x>>1) != 0) { x = t; r += 1; } + return r; + } + + // (public) return the number of bits in "this" + function bnBitLength() { + if(this.t <= 0) return 0; + return this.DB*(this.t-1)+nbits(this[this.t-1]^(this.s&this.DM)); + } + + // (protected) r = this << n*DB + function bnpDLShiftTo(n,r) { + var i; + for(i = this.t-1; i >= 0; --i) r[i+n] = this[i]; + for(i = n-1; i >= 0; --i) r[i] = 0; + r.t = this.t+n; + r.s = this.s; + } + + // (protected) r = this >> n*DB + function bnpDRShiftTo(n,r) { + for(var i = n; i < this.t; ++i) r[i-n] = this[i]; + r.t = Math.max(this.t-n,0); + r.s = this.s; + } + + // (protected) r = this << n + function bnpLShiftTo(n,r) { + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<= 0; --i) { + r[i+ds+1] = (this[i]>>cbs)|c; + c = (this[i]&bm)<= 0; --i) r[i] = 0; + r[ds] = c; + r.t = this.t+ds+1; + r.s = this.s; + r.clamp(); + } + + // (protected) r = this >> n + function bnpRShiftTo(n,r) { + r.s = this.s; + var ds = Math.floor(n/this.DB); + if(ds >= this.t) { r.t = 0; return; } + var bs = n%this.DB; + var cbs = this.DB-bs; + var bm = (1<>bs; + for(var i = ds+1; i < this.t; ++i) { + r[i-ds-1] |= (this[i]&bm)<>bs; + } + if(bs > 0) r[this.t-ds-1] |= (this.s&bm)<>= this.DB; + } + if(a.t < this.t) { + c -= a.s; + while(i < this.t) { + c += this[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c -= a[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c -= a.s; + } + r.s = (c<0)?-1:0; + if(c < -1) r[i++] = this.DV+c; + else if(c > 0) r[i++] = c; + r.t = i; + r.clamp(); + } + + // (protected) r = this * a, r != this,a (HAC 14.12) + // "this" should be the larger one if appropriate. + function bnpMultiplyTo(a,r) { + var x = this.abs(), y = a.abs(); + var i = x.t; + r.t = i+y.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < y.t; ++i) r[i+x.t] = x.am(0,y[i],r,i,0,x.t); + r.s = 0; + r.clamp(); + if(this.s != a.s) BigInteger.ZERO.subTo(r,r); + } + + // (protected) r = this^2, r != this (HAC 14.16) + function bnpSquareTo(r) { + var x = this.abs(); + var i = r.t = 2*x.t; + while(--i >= 0) r[i] = 0; + for(i = 0; i < x.t-1; ++i) { + var c = x.am(i,x[i],r,2*i,0,1); + if((r[i+x.t]+=x.am(i+1,2*x[i],r,2*i+1,c,x.t-i-1)) >= x.DV) { + r[i+x.t] -= x.DV; + r[i+x.t+1] = 1; + } + } + if(r.t > 0) r[r.t-1] += x.am(i,x[i],r,2*i,0,1); + r.s = 0; + r.clamp(); + } + + // (protected) divide this by m, quotient and remainder to q, r (HAC 14.20) + // r != q, this != m. q or r may be null. + function bnpDivRemTo(m,q,r) { + var pm = m.abs(); + if(pm.t <= 0) return; + var pt = this.abs(); + if(pt.t < pm.t) { + if(q != null) q.fromInt(0); + if(r != null) this.copyTo(r); + return; + } + if(r == null) r = nbi(); + var y = nbi(), ts = this.s, ms = m.s; + var nsh = this.DB-nbits(pm[pm.t-1]); // normalize modulus + if(nsh > 0) { pm.lShiftTo(nsh,y); pt.lShiftTo(nsh,r); } + else { pm.copyTo(y); pt.copyTo(r); } + var ys = y.t; + var y0 = y[ys-1]; + if(y0 == 0) return; + var yt = y0*(1<1)?y[ys-2]>>this.F2:0); + var d1 = this.FV/yt, d2 = (1<= 0) { + r[r.t++] = 1; + r.subTo(t,r); + } + BigInteger.ONE.dlShiftTo(ys,t); + t.subTo(y,y); // "negative" y so we can replace sub with am later + while(y.t < ys) y[y.t++] = 0; + while(--j >= 0) { + // Estimate quotient digit + var qd = (r[--i]==y0)?this.DM:Math.floor(r[i]*d1+(r[i-1]+e)*d2); + if((r[i]+=y.am(0,qd,r,j,0,ys)) < qd) { // Try it out + y.dlShiftTo(j,t); + r.subTo(t,r); + while(r[i] < --qd) r.subTo(t,r); + } + } + if(q != null) { + r.drShiftTo(ys,q); + if(ts != ms) BigInteger.ZERO.subTo(q,q); + } + r.t = ys; + r.clamp(); + if(nsh > 0) r.rShiftTo(nsh,r); // Denormalize remainder + if(ts < 0) BigInteger.ZERO.subTo(r,r); + } + + // (public) this mod a + function bnMod(a) { + var r = nbi(); + this.abs().divRemTo(a,null,r); + if(this.s < 0 && r.compareTo(BigInteger.ZERO) > 0) a.subTo(r,r); + return r; + } + + // Modular reduction using "classic" algorithm + class Classic{ + constructor(m){ + this.m = m; + } + } + function cConvert(x) { + if(x.s < 0 || x.compareTo(this.m) >= 0) return x.mod(this.m); + else return x; + } + function cRevert(x) { return x; } + function cReduce(x) { x.divRemTo(this.m,null,x); } + function cMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + function cSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + + Classic.prototype.convert = cConvert; + Classic.prototype.revert = cRevert; + Classic.prototype.reduce = cReduce; + Classic.prototype.mulTo = cMulTo; + Classic.prototype.sqrTo = cSqrTo; + + // (protected) return "-1/this % 2^DB"; useful for Mont. reduction + // justification: + // xy == 1 (mod m) + // xy = 1+km + // xy(2-xy) = (1+km)(1-km) + // x[y(2-xy)] = 1-k^2m^2 + // x[y(2-xy)] == 1 (mod m^2) + // if y is 1/x mod m, then y(2-xy) is 1/x mod m^2 + // should reduce x and y(2-xy) by m^2 at each step to keep size bounded. + // JS multiply "overflows" differently from C/C++, so care is needed here. + function bnpInvDigit() { + if(this.t < 1) return 0; + var x = this[0]; + if((x&1) == 0) return 0; + var y = x&3; // y == 1/x mod 2^2 + y = (y*(2-(x&0xf)*y))&0xf; // y == 1/x mod 2^4 + y = (y*(2-(x&0xff)*y))&0xff; // y == 1/x mod 2^8 + y = (y*(2-(((x&0xffff)*y)&0xffff)))&0xffff; // y == 1/x mod 2^16 + // last step - calculate inverse mod DV directly; + // assumes 16 < DB <= 32 and assumes ability to handle 48-bit ints + y = (y*(2-x*y%this.DV))%this.DV; // y == 1/x mod 2^dbits + // we really want the negative inverse, and -DV < y < DV + return (y>0)?this.DV-y:-y; + } + + // Montgomery reduction + class Montgomery { + constructor(m) { + this.m = m; + this.mp = m.invDigit(); + this.mpl = this.mp & 0x7fff; + this.mph = this.mp >> 15; + this.um = (1 << (m.DB - 15)) - 1; + this.mt2 = 2 * m.t; + } + } + + // xR mod m + function montConvert(x) { + var r = nbi(); + x.abs().dlShiftTo(this.m.t,r); + r.divRemTo(this.m,null,r); + if(x.s < 0 && r.compareTo(BigInteger.ZERO) > 0) this.m.subTo(r,r); + return r; + } + + // x/R mod m + function montRevert(x) { + var r = nbi(); + x.copyTo(r); + this.reduce(r); + return r; + } + + // x = x/R mod m (HAC 14.32) + function montReduce(x) { + while(x.t <= this.mt2) // pad x so am has enough room later + x[x.t++] = 0; + for(var i = 0; i < this.m.t; ++i) { + // faster way of calculating u0 = x[i]*mp mod DV + var j = x[i]&0x7fff; + var u0 = (j*this.mpl+(((j*this.mph+(x[i]>>15)*this.mpl)&this.um)<<15))&x.DM; + // use am to combine the multiply-shift-add into one call + j = i+this.m.t; + x[j] += this.m.am(0,u0,x,i,0,this.m.t); + // propagate carry + while(x[j] >= x.DV) { x[j] -= x.DV; x[++j]++; } + } + x.clamp(); + x.drShiftTo(this.m.t,x); + if(x.compareTo(this.m) >= 0) x.subTo(this.m,x); + } + + // r = "x^2/R mod m"; x != r + function montSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + + // r = "xy/R mod m"; x,y != r + function montMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + + Montgomery.prototype.convert = montConvert; + Montgomery.prototype.revert = montRevert; + Montgomery.prototype.reduce = montReduce; + Montgomery.prototype.mulTo = montMulTo; + Montgomery.prototype.sqrTo = montSqrTo; + + // (protected) true iff this is even + function bnpIsEven() { return ((this.t>0)?(this[0]&1):this.s) == 0; } + + // (protected) this^e, e < 2^32, doing sqr and mul with "r" (HAC 14.79) + function bnpExp(e,z) { + if(e > 0xffffffff || e < 1) return BigInteger.ONE; + var r = nbi(), r2 = nbi(), g = z.convert(this), i = nbits(e)-1; + g.copyTo(r); + while(--i >= 0) { + z.sqrTo(r,r2); + if((e&(1< 0) z.mulTo(r2,g,r); + else { var t = r; r = r2; r2 = t; } + } + return z.revert(r); + } + + // (public) this^e % m, 0 <= e < 2^32 + function bnModPowInt(e,m) { + var z; + if(e < 256 || m.isEven()) z = new Classic(m); else z = new Montgomery(m); + return this.exp(e,z); + } + + // protected + BigInteger.prototype.copyTo = bnpCopyTo; + BigInteger.prototype.fromInt = bnpFromInt; + BigInteger.prototype.fromString = bnpFromString; + BigInteger.prototype.clamp = bnpClamp; + BigInteger.prototype.dlShiftTo = bnpDLShiftTo; + BigInteger.prototype.drShiftTo = bnpDRShiftTo; + BigInteger.prototype.lShiftTo = bnpLShiftTo; + BigInteger.prototype.rShiftTo = bnpRShiftTo; + BigInteger.prototype.subTo = bnpSubTo; + BigInteger.prototype.multiplyTo = bnpMultiplyTo; + BigInteger.prototype.squareTo = bnpSquareTo; + BigInteger.prototype.divRemTo = bnpDivRemTo; + BigInteger.prototype.invDigit = bnpInvDigit; + BigInteger.prototype.isEven = bnpIsEven; + BigInteger.prototype.exp = bnpExp; + + // public + BigInteger.prototype.toString = bnToString; + BigInteger.prototype.negate = bnNegate; + BigInteger.prototype.abs = bnAbs; + BigInteger.prototype.compareTo = bnCompareTo; + BigInteger.prototype.bitLength = bnBitLength; + BigInteger.prototype.mod = bnMod; + BigInteger.prototype.modPowInt = bnModPowInt; + + // "constants" + BigInteger.ZERO = nbv(0); + BigInteger.ONE = nbv(1); + + // Copyright (c) 2005-2009 Tom Wu + // (public) + function bnClone() { var r = nbi(); this.copyTo(r); return r; } + + // (public) return value as integer + function bnIntValue() { + if(this.s < 0) { + if(this.t == 1) return this[0]-this.DV; + else if(this.t == 0) return -1; + } + else if(this.t == 1) return this[0]; + else if(this.t == 0) return 0; + // assumes 16 < DB < 32 + return ((this[1]&((1<<(32-this.DB))-1))<>24; } + + // (public) return value as short (assumes DB>=16) + function bnShortValue() { return (this.t==0)?this.s:(this[0]<<16)>>16; } + + // (protected) return x s.t. r^x < DV + function bnpChunkSize(r) { return Math.floor(Math.LN2*this.DB/Math.log(r)); } + + // (public) 0 if this == 0, 1 if this > 0 + function bnSigNum() { + if(this.s < 0) return -1; + else if(this.t <= 0 || (this.t == 1 && this[0] <= 0)) return 0; + else return 1; + } + + // (protected) convert to radix string + function bnpToRadix(b) { + if(b == null) b = 10; + if(this.signum() == 0 || b < 2 || b > 36) return "0"; + var cs = this.chunkSize(b); + var a = Math.pow(b,cs); + var d = nbv(a), y = nbi(), z = nbi(), r = ""; + this.divRemTo(d,y,z); + while(y.signum() > 0) { + r = (a+z.intValue()).toString(b).substr(1) + r; + y.divRemTo(d,y,z); + } + return z.intValue().toString(b) + r; + } + + // (protected) convert from radix string + function bnpFromRadix(s,b) { + this.fromInt(0); + if(b == null) b = 10; + var cs = this.chunkSize(b); + var d = Math.pow(b,cs), mi = false, j = 0, w = 0; + for(var i = 0; i < s.length; ++i) { + var x = intAt(s,i); + if(x < 0) { + if(s.charAt(i) == "-" && this.signum() == 0) mi = true; + continue; + } + w = b*w+x; + if(++j >= cs) { + this.dMultiply(d); + this.dAddOffset(w,0); + j = 0; + w = 0; + } + } + if(j > 0) { + this.dMultiply(Math.pow(b,j)); + this.dAddOffset(w,0); + } + if(mi) BigInteger.ZERO.subTo(this,this); + } + + // (protected) alternate constructor + function bnpFromNumber(a,b,c) { + if("number" == typeof b) { + // new BigInteger(int,int,RNG) + if(a < 2) this.fromInt(1); + else { + this.fromNumber(a,c); + if(!this.testBit(a-1)) // force MSB set + this.bitwiseTo(BigInteger.ONE.shiftLeft(a-1),op_or,this); + if(this.isEven()) this.dAddOffset(1,0); // force odd + while(!this.isProbablePrime(b)) { + this.dAddOffset(2,0); + if(this.bitLength() > a) this.subTo(BigInteger.ONE.shiftLeft(a-1),this); + } + } + } + else { + // new BigInteger(int,RNG) + var x = [], t = a&7; + x.length = (a>>3)+1; + b.nextBytes(x); + if(t > 0) x[0] &= ((1< 0) { + if(p < this.DB && (d = this[i]>>p) != (this.s&this.DM)>>p) + r[k++] = d|(this.s<<(this.DB-p)); + while(i >= 0) { + if(p < 8) { + d = (this[i]&((1<>(p+=this.DB-8); + } + else { + d = (this[i]>>(p-=8))&0xff; + if(p <= 0) { p += this.DB; --i; } + } + if((d&0x80) != 0) d |= -256; + if(k == 0 && (this.s&0x80) != (d&0x80)) ++k; + if(k > 0 || d != this.s) r[k++] = d; + } + } + return r; + } + + function bnEquals(a) { return(this.compareTo(a)==0); } + function bnMin(a) { return (this.compareTo(a)<0)?this:a; } + function bnMax(a) { return (this.compareTo(a)>0)?this:a; } + + // (protected) r = this op a (bitwise) + function bnpBitwiseTo(a,op,r) { + var i, f, m = Math.min(a.t,this.t); + for(i = 0; i < m; ++i) r[i] = op(this[i],a[i]); + if(a.t < this.t) { + f = a.s&this.DM; + for(i = m; i < this.t; ++i) r[i] = op(this[i],f); + r.t = this.t; + } + else { + f = this.s&this.DM; + for(i = m; i < a.t; ++i) r[i] = op(f,a[i]); + r.t = a.t; + } + r.s = op(this.s,a.s); + r.clamp(); + } + + // (public) this & a + function op_and(x,y) { return x&y; } + function bnAnd(a) { var r = nbi(); this.bitwiseTo(a,op_and,r); return r; } + + // (public) this | a + function op_or(x,y) { return x|y; } + function bnOr(a) { var r = nbi(); this.bitwiseTo(a,op_or,r); return r; } + + // (public) this ^ a + function op_xor(x,y) { return x^y; } + function bnXor(a) { var r = nbi(); this.bitwiseTo(a,op_xor,r); return r; } + + // (public) this & ~a + function op_andnot(x,y) { return x&~y; } + function bnAndNot(a) { var r = nbi(); this.bitwiseTo(a,op_andnot,r); return r; } + + // (public) ~this + function bnNot() { + var r = nbi(); + for(var i = 0; i < this.t; ++i) r[i] = this.DM&~this[i]; + r.t = this.t; + r.s = ~this.s; + return r; + } + + // (public) this << n + function bnShiftLeft(n) { + var r = nbi(); + if(n < 0) this.rShiftTo(-n,r); else this.lShiftTo(n,r); + return r; + } + + // (public) this >> n + function bnShiftRight(n) { + var r = nbi(); + if(n < 0) this.lShiftTo(-n,r); else this.rShiftTo(n,r); + return r; + } + + // return index of lowest 1-bit in x, x < 2^31 + function lbit(x) { + if(x == 0) return -1; + var r = 0; + if((x&0xffff) == 0) { x >>= 16; r += 16; } + if((x&0xff) == 0) { x >>= 8; r += 8; } + if((x&0xf) == 0) { x >>= 4; r += 4; } + if((x&3) == 0) { x >>= 2; r += 2; } + if((x&1) == 0) ++r; + return r; + } + + // (public) returns index of lowest 1-bit (or -1 if none) + function bnGetLowestSetBit() { + for(var i = 0; i < this.t; ++i) + if(this[i] != 0) return i*this.DB+lbit(this[i]); + if(this.s < 0) return this.t*this.DB; + return -1; + } + + // return number of 1 bits in x + function cbit(x) { + var r = 0; + while(x != 0) { x &= x-1; ++r; } + return r; + } + + // (public) return number of set bits + function bnBitCount() { + var r = 0, x = this.s&this.DM; + for(var i = 0; i < this.t; ++i) r += cbit(this[i]^x); + return r; + } + + // (public) true iff nth bit is set + function bnTestBit(n) { + var j = Math.floor(n/this.DB); + if(j >= this.t) return(this.s!=0); + return((this[j]&(1<<(n%this.DB)))!=0); + } + + // (protected) this op (1<>= this.DB; + } + if(a.t < this.t) { + c += a.s; + while(i < this.t) { + c += this[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += this.s; + } + else { + c += this.s; + while(i < a.t) { + c += a[i]; + r[i++] = c&this.DM; + c >>= this.DB; + } + c += a.s; + } + r.s = (c<0)?-1:0; + if(c > 0) r[i++] = c; + else if(c < -1) r[i++] = this.DV+c; + r.t = i; + r.clamp(); + } + + // (public) this + a + function bnAdd(a) { var r = nbi(); this.addTo(a,r); return r; } + + // (public) this - a + function bnSubtract(a) { var r = nbi(); this.subTo(a,r); return r; } + + // (public) this * a + function bnMultiply(a) { var r = nbi(); this.multiplyTo(a,r); return r; } + + // (public) this^2 + function bnSquare() { var r = nbi(); this.squareTo(r); return r; } + + // (public) this / a + function bnDivide(a) { var r = nbi(); this.divRemTo(a,r,null); return r; } + + // (public) this % a + function bnRemainder(a) { var r = nbi(); this.divRemTo(a,null,r); return r; } + + // (public) [this/a,this%a] + function bnDivideAndRemainder(a) { + var q = nbi(), r = nbi(); + this.divRemTo(a,q,r); + return new Array(q,r); + } + + // (protected) this *= n, this >= 0, 1 < n < DV + function bnpDMultiply(n) { + this[this.t] = this.am(0,n-1,this,0,0,this.t); + ++this.t; + this.clamp(); + } + + // (protected) this += n << w words, this >= 0 + function bnpDAddOffset(n,w) { + if(n == 0) return; + while(this.t <= w) this[this.t++] = 0; + this[w] += n; + while(this[w] >= this.DV) { + this[w] -= this.DV; + if(++w >= this.t) this[this.t++] = 0; + ++this[w]; + } + } + + // A "null" reducer + function NullExp() {} + function nNop(x) { return x; } + function nMulTo(x,y,r) { x.multiplyTo(y,r); } + function nSqrTo(x,r) { x.squareTo(r); } + + NullExp.prototype.convert = nNop; + NullExp.prototype.revert = nNop; + NullExp.prototype.mulTo = nMulTo; + NullExp.prototype.sqrTo = nSqrTo; + + // (public) this^e + function bnPow(e) { return this.exp(e,new NullExp()); } + + // (protected) r = lower n words of "this * a", a.t <= n + // "this" should be the larger one if appropriate. + function bnpMultiplyLowerTo(a,n,r) { + var i = Math.min(this.t+a.t,n); + r.s = 0; // assumes a,this >= 0 + r.t = i; + while(i > 0) r[--i] = 0; + var j; + for(j = r.t-this.t; i < j; ++i) r[i+this.t] = this.am(0,a[i],r,i,0,this.t); + for(j = Math.min(a.t,n); i < j; ++i) this.am(0,a[i],r,i,0,n-i); + r.clamp(); + } + + // (protected) r = "this * a" without lower n words, n > 0 + // "this" should be the larger one if appropriate. + function bnpMultiplyUpperTo(a,n,r) { + --n; + var i = r.t = this.t+a.t-n; + r.s = 0; // assumes a,this >= 0 + while(--i >= 0) r[i] = 0; + for(i = Math.max(n-this.t,0); i < a.t; ++i) + r[this.t+i-n] = this.am(n-i,a[i],r,0,0,this.t+i-n); + r.clamp(); + r.drShiftTo(1,r); + } + + // Barrett modular reduction + function Barrett(m) { + // setup Barrett + this.r2 = nbi(); + this.q3 = nbi(); + BigInteger.ONE.dlShiftTo(2*m.t,this.r2); + this.mu = this.r2.divide(m); + this.m = m; + } + + function barrettConvert(x) { + if(x.s < 0 || x.t > 2*this.m.t) return x.mod(this.m); + else if(x.compareTo(this.m) < 0) return x; + else { var r = nbi(); x.copyTo(r); this.reduce(r); return r; } + } + + function barrettRevert(x) { return x; } + + // x = x mod m (HAC 14.42) + function barrettReduce(x) { + x.drShiftTo(this.m.t-1,this.r2); + if(x.t > this.m.t+1) { x.t = this.m.t+1; x.clamp(); } + this.mu.multiplyUpperTo(this.r2,this.m.t+1,this.q3); + this.m.multiplyLowerTo(this.q3,this.m.t+1,this.r2); + while(x.compareTo(this.r2) < 0) x.dAddOffset(1,this.m.t+1); + x.subTo(this.r2,x); + while(x.compareTo(this.m) >= 0) x.subTo(this.m,x); + } + + // r = x^2 mod m; x != r + function barrettSqrTo(x,r) { x.squareTo(r); this.reduce(r); } + + // r = x*y mod m; x,y != r + function barrettMulTo(x,y,r) { x.multiplyTo(y,r); this.reduce(r); } + + Barrett.prototype.convert = barrettConvert; + Barrett.prototype.revert = barrettRevert; + Barrett.prototype.reduce = barrettReduce; + Barrett.prototype.mulTo = barrettMulTo; + Barrett.prototype.sqrTo = barrettSqrTo; + + // (public) this^e % m (HAC 14.85) + function bnModPow(e,m) { + var i = e.bitLength(), k, r = nbv(1), z; + if(i <= 0) return r; + else if(i < 18) k = 1; + else if(i < 48) k = 3; + else if(i < 144) k = 4; + else if(i < 768) k = 5; + else k = 6; + if(i < 8) + z = new Classic(m); + else if(m.isEven()) + z = new Barrett(m); + else + z = new Montgomery(m); + + // precomputation + var g = [], n = 3, k1 = k-1, km = (1< 1) { + var g2 = nbi(); + z.sqrTo(g[1],g2); + while(n <= km) { + g[n] = nbi(); + z.mulTo(g2,g[n-2],g[n]); + n += 2; + } + } + + var j = e.t-1, w, is1 = true, r2 = nbi(), t; + i = nbits(e[j])-1; + while(j >= 0) { + if(i >= k1) w = (e[j]>>(i-k1))&km; + else { + w = (e[j]&((1<<(i+1))-1))<<(k1-i); + if(j > 0) w |= e[j-1]>>(this.DB+i-k1); + } + + n = k; + while((w&1) == 0) { w >>= 1; --n; } + if((i -= n) < 0) { i += this.DB; --j; } + if(is1) { // ret == 1, don't bother squaring or multiplying it + g[w].copyTo(r); + is1 = false; + } + else { + while(n > 1) { z.sqrTo(r,r2); z.sqrTo(r2,r); n -= 2; } + if(n > 0) z.sqrTo(r,r2); else { t = r; r = r2; r2 = t; } + z.mulTo(r2,g[w],r); + } + + while(j >= 0 && (e[j]&(1< 0) { + x.rShiftTo(g,x); + y.rShiftTo(g,y); + } + while(x.signum() > 0) { + if((i = x.getLowestSetBit()) > 0) x.rShiftTo(i,x); + if((i = y.getLowestSetBit()) > 0) y.rShiftTo(i,y); + if(x.compareTo(y) >= 0) { + x.subTo(y,x); + x.rShiftTo(1,x); + } + else { + y.subTo(x,y); + y.rShiftTo(1,y); + } + } + if(g > 0) y.lShiftTo(g,y); + return y; + } + + // (protected) this % n, n < 2^26 + function bnpModInt(n) { + if(n <= 0) return 0; + var d = this.DV%n, r = (this.s<0)?n-1:0; + if(this.t > 0) + if(d == 0) r = this[0]%n; + else for(var i = this.t-1; i >= 0; --i) r = (d*r+this[i])%n; + return r; + } + + // (public) 1/this % m (HAC 14.61) + function bnModInverse(m) { + var ac = m.isEven(); + if((this.isEven() && ac) || m.signum() == 0) return BigInteger.ZERO; + var u = m.clone(), v = this.clone(); + var a = nbv(1), b = nbv(0), c = nbv(0), d = nbv(1); + while(u.signum() != 0) { + while(u.isEven()) { + u.rShiftTo(1,u); + if(ac) { + if(!a.isEven() || !b.isEven()) { a.addTo(this,a); b.subTo(m,b); } + a.rShiftTo(1,a); + } + else if(!b.isEven()) b.subTo(m,b); + b.rShiftTo(1,b); + } + while(v.isEven()) { + v.rShiftTo(1,v); + if(ac) { + if(!c.isEven() || !d.isEven()) { c.addTo(this,c); d.subTo(m,d); } + c.rShiftTo(1,c); + } + else if(!d.isEven()) d.subTo(m,d); + d.rShiftTo(1,d); + } + if(u.compareTo(v) >= 0) { + u.subTo(v,u); + if(ac) a.subTo(c,a); + b.subTo(d,b); + } + else { + v.subTo(u,v); + if(ac) c.subTo(a,c); + d.subTo(b,d); + } + } + if(v.compareTo(BigInteger.ONE) != 0) return BigInteger.ZERO; + if(d.compareTo(m) >= 0) return d.subtract(m); + if(d.signum() < 0) d.addTo(m,d); else return d; + if(d.signum() < 0) return d.add(m); else return d; + } + + var lowprimes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443,449,457,461,463,467,479,487,491,499,503,509,521,523,541,547,557,563,569,571,577,587,593,599,601,607,613,617,619,631,641,643,647,653,659,661,673,677,683,691,701,709,719,727,733,739,743,751,757,761,769,773,787,797,809,811,821,823,827,829,839,853,857,859,863,877,881,883,887,907,911,919,929,937,941,947,953,967,971,977,983,991,997]; + var lplim = (1<<26)/lowprimes[lowprimes.length-1]; + + // (public) test primality with certainty >= 1-.5^t + function bnIsProbablePrime(t) { + var i, x = this.abs(); + if(x.t == 1 && x[0] <= lowprimes[lowprimes.length-1]) { + for(i = 0; i < lowprimes.length; ++i) + if(x[0] == lowprimes[i]) return true; + return false; + } + if(x.isEven()) return false; + i = 1; + while(i < lowprimes.length) { + var m = lowprimes[i], j = i+1; + while(j < lowprimes.length && m < lplim) m *= lowprimes[j++]; + m = x.modInt(m); + while(i < j) if(m%lowprimes[i++] == 0) return false; + } + return x.millerRabin(t); + } + + // (protected) true if probably prime (HAC 4.24, Miller-Rabin) + function bnpMillerRabin(t) { + var n1 = this.subtract(BigInteger.ONE); + var k = n1.getLowestSetBit(); + if(k <= 0) return false; + var r = n1.shiftRight(k); + t = (t+1)>>1; + if(t > lowprimes.length) t = lowprimes.length; + var a = nbi(); + for(var i = 0; i < t; ++i) { + //Pick bases at random, instead of starting at 2 + a.fromInt(lowprimes[Math.floor(Math.random()*lowprimes.length)]); + var y = a.modPow(r,this); + if(y.compareTo(BigInteger.ONE) != 0 && y.compareTo(n1) != 0) { + var j = 1; + while(j++ < k && y.compareTo(n1) != 0) { + y = y.modPowInt(2,this); + if(y.compareTo(BigInteger.ONE) == 0) return false; + } + if(y.compareTo(n1) != 0) return false; + } + } + return true; + } + + // protected + BigInteger.prototype.chunkSize = bnpChunkSize; + BigInteger.prototype.toRadix = bnpToRadix; + BigInteger.prototype.fromRadix = bnpFromRadix; + BigInteger.prototype.fromNumber = bnpFromNumber; + BigInteger.prototype.bitwiseTo = bnpBitwiseTo; + BigInteger.prototype.changeBit = bnpChangeBit; + BigInteger.prototype.addTo = bnpAddTo; + BigInteger.prototype.dMultiply = bnpDMultiply; + BigInteger.prototype.dAddOffset = bnpDAddOffset; + BigInteger.prototype.multiplyLowerTo = bnpMultiplyLowerTo; + BigInteger.prototype.multiplyUpperTo = bnpMultiplyUpperTo; + BigInteger.prototype.modInt = bnpModInt; + BigInteger.prototype.millerRabin = bnpMillerRabin; + + // public + BigInteger.prototype.clone = bnClone; + BigInteger.prototype.intValue = bnIntValue; + BigInteger.prototype.byteValue = bnByteValue; + BigInteger.prototype.shortValue = bnShortValue; + BigInteger.prototype.signum = bnSigNum; + BigInteger.prototype.toByteArray = bnToByteArray; + BigInteger.prototype.equals = bnEquals; + BigInteger.prototype.min = bnMin; + BigInteger.prototype.max = bnMax; + BigInteger.prototype.and = bnAnd; + BigInteger.prototype.or = bnOr; + BigInteger.prototype.xor = bnXor; + BigInteger.prototype.andNot = bnAndNot; + BigInteger.prototype.not = bnNot; + BigInteger.prototype.shiftLeft = bnShiftLeft; + BigInteger.prototype.shiftRight = bnShiftRight; + BigInteger.prototype.getLowestSetBit = bnGetLowestSetBit; + BigInteger.prototype.bitCount = bnBitCount; + BigInteger.prototype.testBit = bnTestBit; + BigInteger.prototype.setBit = bnSetBit; + BigInteger.prototype.clearBit = bnClearBit; + BigInteger.prototype.flipBit = bnFlipBit; + BigInteger.prototype.add = bnAdd; + BigInteger.prototype.subtract = bnSubtract; + BigInteger.prototype.multiply = bnMultiply; + BigInteger.prototype.divide = bnDivide; + BigInteger.prototype.remainder = bnRemainder; + BigInteger.prototype.divideAndRemainder = bnDivideAndRemainder; + BigInteger.prototype.modPow = bnModPow; + BigInteger.prototype.modInverse = bnModInverse; + BigInteger.prototype.pow = bnPow; + BigInteger.prototype.gcd = bnGCD; + BigInteger.prototype.isProbablePrime = bnIsProbablePrime; + + // JSBN-specific extension + BigInteger.prototype.square = bnSquare; + + // BigInteger interfaces not implemented in jsbn: + + // BigInteger(int signum, byte[] magnitude) + // double doubleValue() + // float floatValue() + // int hashCode() + // long longValue() + // static BigInteger valueOf(long val) + + // Version 1.1: support utf-8 encoding in pkcs1pad2 + + function parseBigInt(str,r) { + return new BigInteger(str,r); + } + + // PKCS#1 (type 2, random) pad input string s to n bytes, and return a bigint + function pkcs1pad2(s,n) { + if(n < s.length + 11) { // TODO: fix for utf-8 + console.error("Message too long for RSA"); + return null; + } + var ba = []; + var i = s.length - 1; + while(i >= 0 && n > 0) { + var c = s.charCodeAt(i--); + if(c < 128) { // encode using utf-8 + ba[--n] = c; + } + else if((c > 127) && (c < 2048)) { + ba[--n] = (c & 63) | 128; + ba[--n] = (c >> 6) | 192; + } + else { + ba[--n] = (c & 63) | 128; + ba[--n] = ((c >> 6) & 63) | 128; + ba[--n] = (c >> 12) | 224; + } + } + ba[--n] = 0; + var rng = new SecureRandom(); + var x = []; + while(n > 2) { // random non-zero pad + x[0] = 0; + while(x[0] == 0) rng.nextBytes(x); + ba[--n] = x[0]; + } + ba[--n] = 2; + ba[--n] = 0; + return new BigInteger(ba); + } + + // "empty" RSA key constructor + class RSAKey { + constructor() { + this.n = null; + this.e = 0; + this.d = null; + this.p = null; + this.q = null; + this.dmp1 = null; + this.dmq1 = null; + this.coeff = null; + } + } + + // Set the public key fields N and e from hex strings + function RSASetPublic(N,E) { + if(N != null && E != null && N.length > 0 && E.length > 0) { + this.n = parseBigInt(N,16); + this.e = parseInt(E,16); + } + else + console.error("Invalid RSA public key"); + } + + // Perform raw public operation on "x": return x^e (mod n) + function RSADoPublic(x) { + return x.modPowInt(this.e, this.n); + } + + // Return the PKCS#1 RSA encryption of "text" as an even-length hex string + function RSAEncrypt(text) { + var m = pkcs1pad2(text,(this.n.bitLength()+7)>>3); + if(m == null) return null; + var c = this.doPublic(m); + if(c == null) return null; + var h = c.toString(16); + if((h.length & 1) == 0) return h; else return "0" + h; + } + + // Return the PKCS#1 RSA encryption of "text" as a Base64-encoded string + //function RSAEncryptB64(text) { + // var h = this.encrypt(text); + // if(h) return hex2b64(h); else return null; + //} + + // protected + RSAKey.prototype.doPublic = RSADoPublic; + + // public + RSAKey.prototype.setPublic = RSASetPublic; + RSAKey.prototype.encrypt = RSAEncrypt; + //RSAKey.prototype.encrypt_b64 = RSAEncryptB64; + + // Version 1.1: support utf-8 decoding in pkcs1unpad2 + + function pkcs1unpad2(d,n) { + var b = d.toByteArray(); + var i = 0; + while(i < b.length && b[i] == 0) ++i; + if(b.length-i != n-1 || b[i] != 2) + return null; + ++i; + while(b[i] != 0) + if(++i >= b.length) return null; + var ret = ""; + while(++i < b.length) { + var c = b[i] & 255; + if(c < 128) { // utf-8 decode + ret += String.fromCharCode(c); + } + else if((c > 191) && (c < 224)) { + ret += String.fromCharCode(((c & 31) << 6) | (b[i+1] & 63)); + ++i; + } + else { + ret += String.fromCharCode(((c & 15) << 12) | ((b[i+1] & 63) << 6) | (b[i+2] & 63)); + i += 2; + } + } + return ret; + } + + // Set the private key fields N, e, and d from hex strings + function RSASetPrivate(N,E,D) { + if(N != null && E != null && N.length > 0 && E.length > 0) { + this.n = parseBigInt(N,16); + this.e = parseInt(E,16); + this.d = parseBigInt(D,16); + } + else + console.error("Invalid RSA private key"); + } + + // Set the private key fields N, e, d and CRT params from hex strings + function RSASetPrivateEx(N,E,D,P,Q,DP,DQ,C) { + if(N != null && E != null && N.length > 0 && E.length > 0) { + this.n = parseBigInt(N,16); + this.e = parseInt(E,16); + this.d = parseBigInt(D,16); + this.p = parseBigInt(P,16); + this.q = parseBigInt(Q,16); + this.dmp1 = parseBigInt(DP,16); + this.dmq1 = parseBigInt(DQ,16); + this.coeff = parseBigInt(C,16); + } + else + console.error("Invalid RSA private key"); + } + + // Generate a new random private key B bits long, using public expt E + function RSAGenerate(B,E) { + var rng = new SecureRandom(); + var qs = B>>1; + this.e = parseInt(E,16); + var ee = new BigInteger(E,16); + for(;;) { + for(;;) { + this.p = new BigInteger(B-qs,1,rng); + if(this.p.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) == 0 && this.p.isProbablePrime(10)) break; + } + for(;;) { + this.q = new BigInteger(qs,1,rng); + if(this.q.subtract(BigInteger.ONE).gcd(ee).compareTo(BigInteger.ONE) == 0 && this.q.isProbablePrime(10)) break; + } + if(this.p.compareTo(this.q) <= 0) { + var t = this.p; + this.p = this.q; + this.q = t; + } + var p1 = this.p.subtract(BigInteger.ONE); + var q1 = this.q.subtract(BigInteger.ONE); + var phi = p1.multiply(q1); + if(phi.gcd(ee).compareTo(BigInteger.ONE) == 0) { + this.n = this.p.multiply(this.q); + this.d = ee.modInverse(phi); + this.dmp1 = this.d.mod(p1); + this.dmq1 = this.d.mod(q1); + this.coeff = this.q.modInverse(this.p); + break; + } + } + } + + // Perform raw private operation on "x": return x^d (mod n) + function RSADoPrivate(x) { + if(this.p == null || this.q == null) + return x.modPow(this.d, this.n); + + // TODO: re-calculate any missing CRT params + var xp = x.mod(this.p).modPow(this.dmp1, this.p); + var xq = x.mod(this.q).modPow(this.dmq1, this.q); + + while(xp.compareTo(xq) < 0) + xp = xp.add(this.p); + return xp.subtract(xq).multiply(this.coeff).mod(this.p).multiply(this.q).add(xq); + } + + // Return the PKCS#1 RSA decryption of "ctext". + // "ctext" is an even-length hex string and the output is a plain string. + function RSADecrypt(ctext) { + var c = parseBigInt(ctext, 16); + var m = this.doPrivate(c); + if(m == null) return null; + return pkcs1unpad2(m, (this.n.bitLength()+7)>>3); + } + + // Return the PKCS#1 RSA decryption of "ctext". + // "ctext" is a Base64-encoded string and the output is a plain string. + //function RSAB64Decrypt(ctext) { + // var h = b64tohex(ctext); + // if(h) return this.decrypt(h); else return null; + //} + + // protected + RSAKey.prototype.doPrivate = RSADoPrivate; + + // public + RSAKey.prototype.setPrivate = RSASetPrivate; + RSAKey.prototype.setPrivateEx = RSASetPrivateEx; + RSAKey.prototype.generate = RSAGenerate; + RSAKey.prototype.decrypt = RSADecrypt; + //RSAKey.prototype.b64_decrypt = RSAB64Decrypt; + + // Base64 JavaScript decoder + // Copyright (c) 2008-2013 Lapo Luchini + + // Permission to use, copy, modify, and/or distribute this software for any + // purpose with or without fee is hereby granted, provided that the above + // copyright notice and this permission notice appear in all copies. + // + // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + /*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ + + const Base64 = {}; + let decoder; + + Base64.decode = function (a) { + var i; + if (decoder === undefined) { + var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", + ignore = "= \f\n\r\t\u00A0\u2028\u2029"; + decoder = []; + for (i = 0; i < 64; ++i) + decoder[b64.charAt(i)] = i; + for (i = 0; i < ignore.length; ++i) + decoder[ignore.charAt(i)] = -1; + } + var out = []; + var bits = 0, char_count = 0; + for (i = 0; i < a.length; ++i) { + var c = a.charAt(i); + if (c == '=') + break; + c = decoder[c]; + if (c == -1) + continue; + if (c === undefined) + throw 'Illegal character at offset ' + i; + bits |= c; + if (++char_count >= 4) { + out[out.length] = (bits >> 16); + out[out.length] = (bits >> 8) & 0xFF; + out[out.length] = bits & 0xFF; + bits = 0; + char_count = 0; + } else { + bits <<= 6; + } + } + switch (char_count) { + case 1: + throw "Base64 encoding incomplete: at least 2 bits missing"; + case 2: + out[out.length] = (bits >> 10); + break; + case 3: + out[out.length] = (bits >> 16); + out[out.length] = (bits >> 8) & 0xFF; + break; + } + return out; + }; + + Base64.re = /-----BEGIN [^-]+-----([A-Za-z0-9+\/=\s]+)-----END [^-]+-----|begin-base64[^\n]+\n([A-Za-z0-9+\/=\s]+)====/; + Base64.unarmor = function (a) { + var m = Base64.re.exec(a); + if (m) { + if (m[1]) + a = m[1]; + else if (m[2]) + a = m[2]; + else + throw "RegExp out of sync"; + } + return Base64.decode(a); + }; + + // Hex JavaScript decoder + // Copyright (c) 2008-2013 Lapo Luchini + + // Permission to use, copy, modify, and/or distribute this software for any + // purpose with or without fee is hereby granted, provided that the above + // copyright notice and this permission notice appear in all copies. + // + // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + /*jshint browser: true, strict: true, immed: true, latedef: true, undef: true, regexdash: false */ + const Hex = {}; + let decoder$1; + + Hex.decode = function(a) { + var i; + if (decoder$1 === undefined) { + var hex = "0123456789ABCDEF", + ignore = " \f\n\r\t\u00A0\u2028\u2029"; + decoder$1 = []; + for (i = 0; i < 16; ++i) + decoder$1[hex.charAt(i)] = i; + hex = hex.toLowerCase(); + for (i = 10; i < 16; ++i) + decoder$1[hex.charAt(i)] = i; + for (i = 0; i < ignore.length; ++i) + decoder$1[ignore.charAt(i)] = -1; + } + var out = [], + bits = 0, + char_count = 0; + for (i = 0; i < a.length; ++i) { + var c = a.charAt(i); + if (c == '=') + break; + c = decoder$1[c]; + if (c == -1) + continue; + if (c === undefined) + throw 'Illegal character at offset ' + i; + bits |= c; + if (++char_count >= 2) { + out[out.length] = bits; + bits = 0; + char_count = 0; + } else { + bits <<= 4; + } + } + if (char_count) + throw "Hex encoding incomplete: 4 bits missing"; + return out; + }; + + /*! asn1-1.0.2.js (c) 2013 Kenji Urushima | kjur.github.com/jsrsasign/license + */ + + const JSX = /*window.JSX || */{}; + JSX.env = JSX.env || {}; + + var L = JSX, OP = Object.prototype, FUNCTION_TOSTRING = '[object Function]',ADD = ["toString", "valueOf"]; + + JSX.env.parseUA = function(agent) { + + var numberify = function(s) { + var c = 0; + return parseFloat(s.replace(/\./g, function() { + return (c++ == 1) ? '' : '.'; + })); + }, + + nav = navigator, + o = { + ie: 0, + opera: 0, + gecko: 0, + webkit: 0, + chrome: 0, + mobile: null, + air: 0, + ipad: 0, + iphone: 0, + ipod: 0, + ios: null, + android: 0, + webos: 0, + caja: nav && nav.cajaVersion, + secure: false, + os: null + + }, + + ua = agent || (navigator && navigator.userAgent), + loc = window && window.location, + href = loc && loc.href, + m; + + o.secure = href && (href.toLowerCase().indexOf("https") === 0); + + if (ua) { + + if ((/windows|win32/i).test(ua)) { + o.os = 'windows'; + } else if ((/macintosh/i).test(ua)) { + o.os = 'macintosh'; + } else if ((/rhino/i).test(ua)) { + o.os = 'rhino'; + } + if ((/KHTML/).test(ua)) { + o.webkit = 1; + } + m = ua.match(/AppleWebKit\/([^\s]*)/); + if (m && m[1]) { + o.webkit = numberify(m[1]); + if (/ Mobile\//.test(ua)) { + o.mobile = 'Apple'; // iPhone or iPod Touch + m = ua.match(/OS ([^\s]*)/); + if (m && m[1]) { + m = numberify(m[1].replace('_', '.')); + } + o.ios = m; + o.ipad = o.ipod = o.iphone = 0; + m = ua.match(/iPad|iPod|iPhone/); + if (m && m[0]) { + o[m[0].toLowerCase()] = o.ios; + } + } else { + m = ua.match(/NokiaN[^\/]*|Android \d\.\d|webOS\/\d\.\d/); + if (m) { + o.mobile = m[0]; + } + if (/webOS/.test(ua)) { + o.mobile = 'WebOS'; + m = ua.match(/webOS\/([^\s]*);/); + if (m && m[1]) { + o.webos = numberify(m[1]); + } + } + if (/ Android/.test(ua)) { + o.mobile = 'Android'; + m = ua.match(/Android ([^\s]*);/); + if (m && m[1]) { + o.android = numberify(m[1]); + } + } + } + m = ua.match(/Chrome\/([^\s]*)/); + if (m && m[1]) { + o.chrome = numberify(m[1]); // Chrome + } else { + m = ua.match(/AdobeAIR\/([^\s]*)/); + if (m) { + o.air = m[0]; // Adobe AIR 1.0 or better + } + } + } + if (!o.webkit) { + m = ua.match(/Opera[\s\/]([^\s]*)/); + if (m && m[1]) { + o.opera = numberify(m[1]); + m = ua.match(/Version\/([^\s]*)/); + if (m && m[1]) { + o.opera = numberify(m[1]); // opera 10+ + } + m = ua.match(/Opera Mini[^;]*/); + if (m) { + o.mobile = m[0]; // ex: Opera Mini/2.0.4509/1316 + } + } else { // not opera or webkit + m = ua.match(/MSIE\s([^;]*)/); + if (m && m[1]) { + o.ie = numberify(m[1]); + } else { // not opera, webkit, or ie + m = ua.match(/Gecko\/([^\s]*)/); + if (m) { + o.gecko = 1; // Gecko detected, look for revision + m = ua.match(/rv:([^\s\)]*)/); + if (m && m[1]) { + o.gecko = numberify(m[1]); + } + } + } + } + } + } + return o; + }; + + JSX.env.ua = JSX.env.parseUA(); + + JSX.isFunction = function(o) { + return (typeof o === 'function') || OP.toString.apply(o) === FUNCTION_TOSTRING; + }; + + JSX._IEEnumFix = (JSX.env.ua.ie) ? function(r, s) { + var i, fname, f; + for (i=0;iMIT License + */ + + /** + * kjur's class library name space + *

+ * This name space provides following name spaces: + *

    + *
  • {@link KJUR.asn1} - ASN.1 primitive hexadecimal encoder
  • + *
  • {@link KJUR.asn1.x509} - ASN.1 structure for X.509 certificate and CRL
  • + *
  • {@link KJUR.crypto} - Java Cryptographic Extension(JCE) style MessageDigest/Signature + * class and utilities
  • + *
+ *

+ * NOTE: Please ignore method summary and document of this namespace. This caused by a bug of jsdoc2. + * @name KJUR + * @namespace kjur's class library name space + */ + // if (typeof KJUR == "undefined" || !KJUR) + const KJUR = {}; + + /** + * kjur's ASN.1 class library name space + *

+ * This is ITU-T X.690 ASN.1 DER encoder class library and + * class structure and methods is very similar to + * org.bouncycastle.asn1 package of + * well known BouncyCaslte Cryptography Library. + * + *

PROVIDING ASN.1 PRIMITIVES

+ * Here are ASN.1 DER primitive classes. + *
    + *
  • {@link KJUR.asn1.DERBoolean}
  • + *
  • {@link KJUR.asn1.DERInteger}
  • + *
  • {@link KJUR.asn1.DERBitString}
  • + *
  • {@link KJUR.asn1.DEROctetString}
  • + *
  • {@link KJUR.asn1.DERNull}
  • + *
  • {@link KJUR.asn1.DERObjectIdentifier}
  • + *
  • {@link KJUR.asn1.DERUTF8String}
  • + *
  • {@link KJUR.asn1.DERNumericString}
  • + *
  • {@link KJUR.asn1.DERPrintableString}
  • + *
  • {@link KJUR.asn1.DERTeletexString}
  • + *
  • {@link KJUR.asn1.DERIA5String}
  • + *
  • {@link KJUR.asn1.DERUTCTime}
  • + *
  • {@link KJUR.asn1.DERGeneralizedTime}
  • + *
  • {@link KJUR.asn1.DERSequence}
  • + *
  • {@link KJUR.asn1.DERSet}
  • + *
+ * + *

OTHER ASN.1 CLASSES

+ *
    + *
  • {@link KJUR.asn1.ASN1Object}
  • + *
  • {@link KJUR.asn1.DERAbstractString}
  • + *
  • {@link KJUR.asn1.DERAbstractTime}
  • + *
  • {@link KJUR.asn1.DERAbstractStructured}
  • + *
  • {@link KJUR.asn1.DERTaggedObject}
  • + *
+ *

+ * NOTE: Please ignore method summary and document of this namespace. This caused by a bug of jsdoc2. + * @name KJUR.asn1 + * @namespace + */ + if (typeof KJUR.asn1 == "undefined" || !KJUR.asn1) KJUR.asn1 = {}; + + /** + * ASN1 utilities class + * @name KJUR.asn1.ASN1Util + * @classs ASN1 utilities class + * @since asn1 1.0.2 + */ + KJUR.asn1.ASN1Util = new function() { + this.integerToByteHex = function(i) { + var h = i.toString(16); + if ((h.length % 2) == 1) h = '0' + h; + return h; + }; + this.bigIntToMinTwosComplementsHex = function(bigIntegerValue) { + var h = bigIntegerValue.toString(16); + if (h.substr(0, 1) != '-') { + if (h.length % 2 == 1) { + h = '0' + h; + } else { + if (! h.match(/^[0-7]/)) { + h = '00' + h; + } + } + } else { + var hPos = h.substr(1); + var xorLen = hPos.length; + if (xorLen % 2 == 1) { + xorLen += 1; + } else { + if (! h.match(/^[0-7]/)) { + xorLen += 2; + } + } + var hMask = ''; + for (var i = 0; i < xorLen; i++) { + hMask += 'f'; + } + var biMask = new BigInteger(hMask, 16); + var biNeg = biMask.xor(bigIntegerValue).add(BigInteger.ONE); + h = biNeg.toString(16).replace(/^-/, ''); + } + return h; + }; + /** + * get PEM string from hexadecimal data and header string + * @name getPEMStringFromHex + * @memberOf KJUR.asn1.ASN1Util + * @function + * @param {String} dataHex hexadecimal string of PEM body + * @param {String} pemHeader PEM header string (ex. 'RSA PRIVATE KEY') + * @return {String} PEM formatted string of input data + * @description + * @example + * var pem = KJUR.asn1.ASN1Util.getPEMStringFromHex('616161', 'RSA PRIVATE KEY'); + * // value of pem will be: + * -----BEGIN PRIVATE KEY----- + * YWFh + * -----END PRIVATE KEY----- + */ + this.getPEMStringFromHex = function(dataHex, pemHeader) { + var dataWA = CryptoJS.enc.Hex.parse(dataHex); + var dataB64 = CryptoJS.enc.Base64.stringify(dataWA); + var pemBody = dataB64.replace(/(.{64})/g, "$1\r\n"); + pemBody = pemBody.replace(/\r\n$/, ''); + return "-----BEGIN " + pemHeader + "-----\r\n" + + pemBody + + "\r\n-----END " + pemHeader + "-----\r\n"; + }; + }; + + // ******************************************************************** + // Abstract ASN.1 Classes + // ******************************************************************** + + // ******************************************************************** + + /** + * base class for ASN.1 DER encoder object + * @name KJUR.asn1.ASN1Object + * @class base class for ASN.1 DER encoder object + * @property {Boolean} isModified flag whether internal data was changed + * @property {String} hTLV hexadecimal string of ASN.1 TLV + * @property {String} hT hexadecimal string of ASN.1 TLV tag(T) + * @property {String} hL hexadecimal string of ASN.1 TLV length(L) + * @property {String} hV hexadecimal string of ASN.1 TLV value(V) + * @description + */ + KJUR.asn1.ASN1Object = function() { + var hV = ''; + + /** + * get hexadecimal ASN.1 TLV length(L) bytes from TLV value(V) + * @name getLengthHexFromValue + * @memberOf KJUR.asn1.ASN1Object + * @function + * @return {String} hexadecimal string of ASN.1 TLV length(L) + */ + this.getLengthHexFromValue = function() { + if (typeof this.hV == "undefined" || this.hV == null) { + throw "this.hV is null or undefined."; + } + if (this.hV.length % 2 == 1) { + throw "value hex must be even length: n=" + hV.length + ",v=" + this.hV; + } + var n = this.hV.length / 2; + var hN = n.toString(16); + if (hN.length % 2 == 1) { + hN = "0" + hN; + } + if (n < 128) { + return hN; + } else { + var hNlen = hN.length / 2; + if (hNlen > 15) { + throw "ASN.1 length too long to represent by 8x: n = " + n.toString(16); + } + var head = 128 + hNlen; + return head.toString(16) + hN; + } + }; + + /** + * get hexadecimal string of ASN.1 TLV bytes + * @name getEncodedHex + * @memberOf KJUR.asn1.ASN1Object + * @function + * @return {String} hexadecimal string of ASN.1 TLV + */ + this.getEncodedHex = function() { + if (this.hTLV == null || this.isModified) { + this.hV = this.getFreshValueHex(); + this.hL = this.getLengthHexFromValue(); + this.hTLV = this.hT + this.hL + this.hV; + this.isModified = false; + //console.error("first time: " + this.hTLV); + } + return this.hTLV; + }; + + /** + * get hexadecimal string of ASN.1 TLV value(V) bytes + * @name getValueHex + * @memberOf KJUR.asn1.ASN1Object + * @function + * @return {String} hexadecimal string of ASN.1 TLV value(V) bytes + */ + this.getValueHex = function() { + this.getEncodedHex(); + return this.hV; + }; + + this.getFreshValueHex = function() { + return ''; + }; + }; + + // == BEGIN DERAbstractString ================================================ + /** + * base class for ASN.1 DER string classes + * @name KJUR.asn1.DERAbstractString + * @class base class for ASN.1 DER string classes + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @property {String} s internal string of value + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • str - specify initial ASN.1 value(V) by a string
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
+ * NOTE: 'params' can be omitted. + */ + KJUR.asn1.DERAbstractString = function(params) { + KJUR.asn1.DERAbstractString.superclass.constructor.call(this); + + /** + * get string value of this string object + * @name getString + * @memberOf KJUR.asn1.DERAbstractString + * @function + * @return {String} string value of this string object + */ + this.getString = function() { + return this.s; + }; + + /** + * set value by a string + * @name setString + * @memberOf KJUR.asn1.DERAbstractString + * @function + * @param {String} newS value by a string to set + */ + this.setString = function(newS) { + this.hTLV = null; + this.isModified = true; + this.s = newS; + this.hV = stohex(this.s); + }; + + /** + * set value by a hexadecimal string + * @name setStringHex + * @memberOf KJUR.asn1.DERAbstractString + * @function + * @param {String} newHexString value by a hexadecimal string to set + */ + this.setStringHex = function(newHexString) { + this.hTLV = null; + this.isModified = true; + this.s = null; + this.hV = newHexString; + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['str'] != "undefined") { + this.setString(params['str']); + } else if (typeof params['hex'] != "undefined") { + this.setStringHex(params['hex']); + } + } + }; + JSX.extend(KJUR.asn1.DERAbstractString, KJUR.asn1.ASN1Object); + // == END DERAbstractString ================================================ + + // == BEGIN DERAbstractTime ================================================== + /** + * base class for ASN.1 DER Generalized/UTCTime class + * @name KJUR.asn1.DERAbstractTime + * @class base class for ASN.1 DER Generalized/UTCTime class + * @param {Array} params associative array of parameters (ex. {'str': '130430235959Z'}) + * @extends KJUR.asn1.ASN1Object + * @description + * @see KJUR.asn1.ASN1Object - superclass + */ + KJUR.asn1.DERAbstractTime = function(params) { + KJUR.asn1.DERAbstractTime.superclass.constructor.call(this); + + // --- PRIVATE METHODS -------------------- + this.localDateToUTC = function(d) { + utc = d.getTime() + (d.getTimezoneOffset() * 60000); + var utcDate = new Date(utc); + return utcDate; + }; + + this.formatDate = function(dateObject, type) { + var pad = this.zeroPadding; + var d = this.localDateToUTC(dateObject); + var year = String(d.getFullYear()); + if (type == 'utc') year = year.substr(2, 2); + var month = pad(String(d.getMonth() + 1), 2); + var day = pad(String(d.getDate()), 2); + var hour = pad(String(d.getHours()), 2); + var min = pad(String(d.getMinutes()), 2); + var sec = pad(String(d.getSeconds()), 2); + return year + month + day + hour + min + sec + 'Z'; + }; + + this.zeroPadding = function(s, len) { + if (s.length >= len) return s; + return new Array(len - s.length + 1).join('0') + s; + }; + + // --- PUBLIC METHODS -------------------- + /** + * get string value of this string object + * @name getString + * @memberOf KJUR.asn1.DERAbstractTime + * @function + * @return {String} string value of this time object + */ + this.getString = function() { + return this.s; + }; + + /** + * set value by a string + * @name setString + * @memberOf KJUR.asn1.DERAbstractTime + * @function + * @param {String} newS value by a string to set such like "130430235959Z" + */ + this.setString = function(newS) { + this.hTLV = null; + this.isModified = true; + this.s = newS; + this.hV = stohex(this.s); + }; + + /** + * set value by a Date object + * @name setByDateValue + * @memberOf KJUR.asn1.DERAbstractTime + * @function + * @param {Integer} year year of date (ex. 2013) + * @param {Integer} month month of date between 1 and 12 (ex. 12) + * @param {Integer} day day of month + * @param {Integer} hour hours of date + * @param {Integer} min minutes of date + * @param {Integer} sec seconds of date + */ + this.setByDateValue = function(year, month, day, hour, min, sec) { + var dateObject = new Date(Date.UTC(year, month - 1, day, hour, min, sec, 0)); + this.setByDate(dateObject); + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + }; + JSX.extend(KJUR.asn1.DERAbstractTime, KJUR.asn1.ASN1Object); + // == END DERAbstractTime ================================================== + + // == BEGIN DERAbstractStructured ============================================ + /** + * base class for ASN.1 DER structured class + * @name KJUR.asn1.DERAbstractStructured + * @class base class for ASN.1 DER structured class + * @property {Array} asn1Array internal array of ASN1Object + * @extends KJUR.asn1.ASN1Object + * @description + * @see KJUR.asn1.ASN1Object - superclass + */ + KJUR.asn1.DERAbstractStructured = function(params) { + KJUR.asn1.DERAbstractString.superclass.constructor.call(this); + + /** + * set value by array of ASN1Object + * @name setByASN1ObjectArray + * @memberOf KJUR.asn1.DERAbstractStructured + * @function + * @param {array} asn1ObjectArray array of ASN1Object to set + */ + this.setByASN1ObjectArray = function(asn1ObjectArray) { + this.hTLV = null; + this.isModified = true; + this.asn1Array = asn1ObjectArray; + }; + + /** + * append an ASN1Object to internal array + * @name appendASN1Object + * @memberOf KJUR.asn1.DERAbstractStructured + * @function + * @param {ASN1Object} asn1Object to add + */ + this.appendASN1Object = function(asn1Object) { + this.hTLV = null; + this.isModified = true; + this.asn1Array.push(asn1Object); + }; + + this.asn1Array = new Array(); + if (typeof params != "undefined") { + if (typeof params['array'] != "undefined") { + this.asn1Array = params['array']; + } + } + }; + JSX.extend(KJUR.asn1.DERAbstractStructured, KJUR.asn1.ASN1Object); + + + // ******************************************************************** + // ASN.1 Object Classes + // ******************************************************************** + + // ******************************************************************** + /** + * class for ASN.1 DER Boolean + * @name KJUR.asn1.DERBoolean + * @class class for ASN.1 DER Boolean + * @extends KJUR.asn1.ASN1Object + * @description + * @see KJUR.asn1.ASN1Object - superclass + */ + KJUR.asn1.DERBoolean = function() { + KJUR.asn1.DERBoolean.superclass.constructor.call(this); + this.hT = "01"; + this.hTLV = "0101ff"; + }; + JSX.extend(KJUR.asn1.DERBoolean, KJUR.asn1.ASN1Object); + + // ******************************************************************** + /** + * class for ASN.1 DER Integer + * @name KJUR.asn1.DERInteger + * @class class for ASN.1 DER Integer + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • int - specify initial ASN.1 value(V) by integer value
  • + *
  • bigint - specify initial ASN.1 value(V) by BigInteger object
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
+ * NOTE: 'params' can be omitted. + */ + KJUR.asn1.DERInteger = function(params) { + KJUR.asn1.DERInteger.superclass.constructor.call(this); + this.hT = "02"; + + /** + * set value by Tom Wu's BigInteger object + * @name setByBigInteger + * @memberOf KJUR.asn1.DERInteger + * @function + * @param {BigInteger} bigIntegerValue to set + */ + this.setByBigInteger = function(bigIntegerValue) { + this.hTLV = null; + this.isModified = true; + this.hV = KJUR.asn1.ASN1Util.bigIntToMinTwosComplementsHex(bigIntegerValue); + }; + + /** + * set value by integer value + * @name setByInteger + * @memberOf KJUR.asn1.DERInteger + * @function + * @param {Integer} integer value to set + */ + this.setByInteger = function(intValue) { + var bi = new BigInteger(String(intValue), 10); + this.setByBigInteger(bi); + }; + + /** + * set value by integer value + * @name setValueHex + * @memberOf KJUR.asn1.DERInteger + * @function + * @param {String} hexadecimal string of integer value + * @description + *
+ * NOTE: Value shall be represented by minimum octet length of + * two's complement representation. + */ + this.setValueHex = function(newHexString) { + this.hV = newHexString; + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['bigint'] != "undefined") { + this.setByBigInteger(params['bigint']); + } else if (typeof params['int'] != "undefined") { + this.setByInteger(params['int']); + } else if (typeof params['hex'] != "undefined") { + this.setValueHex(params['hex']); + } + } + }; + JSX.extend(KJUR.asn1.DERInteger, KJUR.asn1.ASN1Object); + + // ******************************************************************** + /** + * class for ASN.1 DER encoded BitString primitive + * @name KJUR.asn1.DERBitString + * @class class for ASN.1 DER encoded BitString primitive + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • bin - specify binary string (ex. '10111')
  • + *
  • array - specify array of boolean (ex. [true,false,true,true])
  • + *
  • hex - specify hexadecimal string of ASN.1 value(V) including unused bits
  • + *
+ * NOTE: 'params' can be omitted. + */ + KJUR.asn1.DERBitString = function(params) { + KJUR.asn1.DERBitString.superclass.constructor.call(this); + this.hT = "03"; + + /** + * set ASN.1 value(V) by a hexadecimal string including unused bits + * @name setHexValueIncludingUnusedBits + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {String} newHexStringIncludingUnusedBits + */ + this.setHexValueIncludingUnusedBits = function(newHexStringIncludingUnusedBits) { + this.hTLV = null; + this.isModified = true; + this.hV = newHexStringIncludingUnusedBits; + }; + + /** + * set ASN.1 value(V) by unused bit and hexadecimal string of value + * @name setUnusedBitsAndHexValue + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {Integer} unusedBits + * @param {String} hValue + */ + this.setUnusedBitsAndHexValue = function(unusedBits, hValue) { + if (unusedBits < 0 || 7 < unusedBits) { + throw "unused bits shall be from 0 to 7: u = " + unusedBits; + } + var hUnusedBits = "0" + unusedBits; + this.hTLV = null; + this.isModified = true; + this.hV = hUnusedBits + hValue; + }; + + /** + * set ASN.1 DER BitString by binary string + * @name setByBinaryString + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {String} binaryString binary value string (i.e. '10111') + * @description + * Its unused bits will be calculated automatically by length of + * 'binaryValue'.
+ * NOTE: Trailing zeros '0' will be ignored. + */ + this.setByBinaryString = function(binaryString) { + binaryString = binaryString.replace(/0+$/, ''); + var unusedBits = 8 - binaryString.length % 8; + if (unusedBits == 8) unusedBits = 0; + for (var i = 0; i <= unusedBits; i++) { + binaryString += '0'; + } + var h = ''; + for (var i = 0; i < binaryString.length - 1; i += 8) { + var b = binaryString.substr(i, 8); + var x = parseInt(b, 2).toString(16); + if (x.length == 1) x = '0' + x; + h += x; + } + this.hTLV = null; + this.isModified = true; + this.hV = '0' + unusedBits + h; + }; + + /** + * set ASN.1 TLV value(V) by an array of boolean + * @name setByBooleanArray + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {array} booleanArray array of boolean (ex. [true, false, true]) + * @description + * NOTE: Trailing falses will be ignored. + */ + this.setByBooleanArray = function(booleanArray) { + var s = ''; + for (var i = 0; i < booleanArray.length; i++) { + if (booleanArray[i] == true) { + s += '1'; + } else { + s += '0'; + } + } + this.setByBinaryString(s); + }; + + /** + * generate an array of false with specified length + * @name newFalseArray + * @memberOf KJUR.asn1.DERBitString + * @function + * @param {Integer} nLength length of array to generate + * @return {array} array of boolean faluse + * @description + * This static method may be useful to initialize boolean array. + */ + this.newFalseArray = function(nLength) { + var a = new Array(nLength); + for (var i = 0; i < nLength; i++) { + a[i] = false; + } + return a; + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['hex'] != "undefined") { + this.setHexValueIncludingUnusedBits(params['hex']); + } else if (typeof params['bin'] != "undefined") { + this.setByBinaryString(params['bin']); + } else if (typeof params['array'] != "undefined") { + this.setByBooleanArray(params['array']); + } + } + }; + JSX.extend(KJUR.asn1.DERBitString, KJUR.asn1.ASN1Object); + + // ******************************************************************** + /** + * class for ASN.1 DER OctetString + * @name KJUR.asn1.DEROctetString + * @class class for ASN.1 DER OctetString + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ + KJUR.asn1.DEROctetString = function(params) { + KJUR.asn1.DEROctetString.superclass.constructor.call(this, params); + this.hT = "04"; + }; + JSX.extend(KJUR.asn1.DEROctetString, KJUR.asn1.DERAbstractString); + + // ******************************************************************** + /** + * class for ASN.1 DER Null + * @name KJUR.asn1.DERNull + * @class class for ASN.1 DER Null + * @extends KJUR.asn1.ASN1Object + * @description + * @see KJUR.asn1.ASN1Object - superclass + */ + KJUR.asn1.DERNull = function() { + KJUR.asn1.DERNull.superclass.constructor.call(this); + this.hT = "05"; + this.hTLV = "0500"; + }; + JSX.extend(KJUR.asn1.DERNull, KJUR.asn1.ASN1Object); + + // ******************************************************************** + /** + * class for ASN.1 DER ObjectIdentifier + * @name KJUR.asn1.DERObjectIdentifier + * @class class for ASN.1 DER ObjectIdentifier + * @param {Array} params associative array of parameters (ex. {'oid': '2.5.4.5'}) + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • oid - specify initial ASN.1 value(V) by a oid string (ex. 2.5.4.13)
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
+ * NOTE: 'params' can be omitted. + */ + KJUR.asn1.DERObjectIdentifier = function(params) { + var itox = function(i) { + var h = i.toString(16); + if (h.length == 1) h = '0' + h; + return h; + }; + var roidtox = function(roid) { + var h = ''; + var bi = new BigInteger(roid, 10); + var b = bi.toString(2); + var padLen = 7 - b.length % 7; + if (padLen == 7) padLen = 0; + var bPad = ''; + for (var i = 0; i < padLen; i++) bPad += '0'; + b = bPad + b; + for (var i = 0; i < b.length - 1; i += 7) { + var b8 = b.substr(i, 7); + if (i != b.length - 7) b8 = '1' + b8; + h += itox(parseInt(b8, 2)); + } + return h; + }; + + KJUR.asn1.DERObjectIdentifier.superclass.constructor.call(this); + this.hT = "06"; + + /** + * set value by a hexadecimal string + * @name setValueHex + * @memberOf KJUR.asn1.DERObjectIdentifier + * @function + * @param {String} newHexString hexadecimal value of OID bytes + */ + this.setValueHex = function(newHexString) { + this.hTLV = null; + this.isModified = true; + this.s = null; + this.hV = newHexString; + }; + + /** + * set value by a OID string + * @name setValueOidString + * @memberOf KJUR.asn1.DERObjectIdentifier + * @function + * @param {String} oidString OID string (ex. 2.5.4.13) + */ + this.setValueOidString = function(oidString) { + if (! oidString.match(/^[0-9.]+$/)) { + throw "malformed oid string: " + oidString; + } + var h = ''; + var a = oidString.split('.'); + var i0 = parseInt(a[0]) * 40 + parseInt(a[1]); + h += itox(i0); + a.splice(0, 2); + for (var i = 0; i < a.length; i++) { + h += roidtox(a[i]); + } + this.hTLV = null; + this.isModified = true; + this.s = null; + this.hV = h; + }; + + /** + * set value by a OID name + * @name setValueName + * @memberOf KJUR.asn1.DERObjectIdentifier + * @function + * @param {String} oidName OID name (ex. 'serverAuth') + * @since 1.0.1 + * @description + * OID name shall be defined in 'KJUR.asn1.x509.OID.name2oidList'. + * Otherwise raise error. + */ + this.setValueName = function(oidName) { + if (typeof KJUR.asn1.x509.OID.name2oidList[oidName] != "undefined") { + var oid = KJUR.asn1.x509.OID.name2oidList[oidName]; + this.setValueOidString(oid); + } else { + throw "DERObjectIdentifier oidName undefined: " + oidName; + } + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['oid'] != "undefined") { + this.setValueOidString(params['oid']); + } else if (typeof params['hex'] != "undefined") { + this.setValueHex(params['hex']); + } else if (typeof params['name'] != "undefined") { + this.setValueName(params['name']); + } + } + }; + JSX.extend(KJUR.asn1.DERObjectIdentifier, KJUR.asn1.ASN1Object); + + // ******************************************************************** + /** + * class for ASN.1 DER UTF8String + * @name KJUR.asn1.DERUTF8String + * @class class for ASN.1 DER UTF8String + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ + KJUR.asn1.DERUTF8String = function(params) { + KJUR.asn1.DERUTF8String.superclass.constructor.call(this, params); + this.hT = "0c"; + }; + JSX.extend(KJUR.asn1.DERUTF8String, KJUR.asn1.DERAbstractString); + + // ******************************************************************** + /** + * class for ASN.1 DER NumericString + * @name KJUR.asn1.DERNumericString + * @class class for ASN.1 DER NumericString + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ + KJUR.asn1.DERNumericString = function(params) { + KJUR.asn1.DERNumericString.superclass.constructor.call(this, params); + this.hT = "12"; + }; + JSX.extend(KJUR.asn1.DERNumericString, KJUR.asn1.DERAbstractString); + + // ******************************************************************** + /** + * class for ASN.1 DER PrintableString + * @name KJUR.asn1.DERPrintableString + * @class class for ASN.1 DER PrintableString + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ + KJUR.asn1.DERPrintableString = function(params) { + KJUR.asn1.DERPrintableString.superclass.constructor.call(this, params); + this.hT = "13"; + }; + JSX.extend(KJUR.asn1.DERPrintableString, KJUR.asn1.DERAbstractString); + + // ******************************************************************** + /** + * class for ASN.1 DER TeletexString + * @name KJUR.asn1.DERTeletexString + * @class class for ASN.1 DER TeletexString + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ + KJUR.asn1.DERTeletexString = function(params) { + KJUR.asn1.DERTeletexString.superclass.constructor.call(this, params); + this.hT = "14"; + }; + JSX.extend(KJUR.asn1.DERTeletexString, KJUR.asn1.DERAbstractString); + + // ******************************************************************** + /** + * class for ASN.1 DER IA5String + * @name KJUR.asn1.DERIA5String + * @class class for ASN.1 DER IA5String + * @param {Array} params associative array of parameters (ex. {'str': 'aaa'}) + * @extends KJUR.asn1.DERAbstractString + * @description + * @see KJUR.asn1.DERAbstractString - superclass + */ + KJUR.asn1.DERIA5String = function(params) { + KJUR.asn1.DERIA5String.superclass.constructor.call(this, params); + this.hT = "16"; + }; + JSX.extend(KJUR.asn1.DERIA5String, KJUR.asn1.DERAbstractString); + + // ******************************************************************** + /** + * class for ASN.1 DER UTCTime + * @name KJUR.asn1.DERUTCTime + * @class class for ASN.1 DER UTCTime + * @param {Array} params associative array of parameters (ex. {'str': '130430235959Z'}) + * @extends KJUR.asn1.DERAbstractTime + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • str - specify initial ASN.1 value(V) by a string (ex.'130430235959Z')
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
  • date - specify Date object.
  • + *
+ * NOTE: 'params' can be omitted. + *

EXAMPLES

+ * @example + * var d1 = new KJUR.asn1.DERUTCTime(); + * d1.setString('130430125959Z'); + * + * var d2 = new KJUR.asn1.DERUTCTime({'str': '130430125959Z'}); + * + * var d3 = new KJUR.asn1.DERUTCTime({'date': new Date(Date.UTC(2015, 0, 31, 0, 0, 0, 0))}); + */ + KJUR.asn1.DERUTCTime = function(params) { + KJUR.asn1.DERUTCTime.superclass.constructor.call(this, params); + this.hT = "17"; + + /** + * set value by a Date object + * @name setByDate + * @memberOf KJUR.asn1.DERUTCTime + * @function + * @param {Date} dateObject Date object to set ASN.1 value(V) + */ + this.setByDate = function(dateObject) { + this.hTLV = null; + this.isModified = true; + this.date = dateObject; + this.s = this.formatDate(this.date, 'utc'); + this.hV = stohex(this.s); + }; + + if (typeof params != "undefined") { + if (typeof params['str'] != "undefined") { + this.setString(params['str']); + } else if (typeof params['hex'] != "undefined") { + this.setStringHex(params['hex']); + } else if (typeof params['date'] != "undefined") { + this.setByDate(params['date']); + } + } + }; + JSX.extend(KJUR.asn1.DERUTCTime, KJUR.asn1.DERAbstractTime); + + // ******************************************************************** + /** + * class for ASN.1 DER GeneralizedTime + * @name KJUR.asn1.DERGeneralizedTime + * @class class for ASN.1 DER GeneralizedTime + * @param {Array} params associative array of parameters (ex. {'str': '20130430235959Z'}) + * @extends KJUR.asn1.DERAbstractTime + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • str - specify initial ASN.1 value(V) by a string (ex.'20130430235959Z')
  • + *
  • hex - specify initial ASN.1 value(V) by a hexadecimal string
  • + *
  • date - specify Date object.
  • + *
+ * NOTE: 'params' can be omitted. + */ + KJUR.asn1.DERGeneralizedTime = function(params) { + KJUR.asn1.DERGeneralizedTime.superclass.constructor.call(this, params); + this.hT = "18"; + + /** + * set value by a Date object + * @name setByDate + * @memberOf KJUR.asn1.DERGeneralizedTime + * @function + * @param {Date} dateObject Date object to set ASN.1 value(V) + * @example + * When you specify UTC time, use 'Date.UTC' method like this:
+ * var o = new DERUTCTime(); + * var date = new Date(Date.UTC(2015, 0, 31, 23, 59, 59, 0)); #2015JAN31 23:59:59 + * o.setByDate(date); + */ + this.setByDate = function(dateObject) { + this.hTLV = null; + this.isModified = true; + this.date = dateObject; + this.s = this.formatDate(this.date, 'gen'); + this.hV = stohex(this.s); + }; + + if (typeof params != "undefined") { + if (typeof params['str'] != "undefined") { + this.setString(params['str']); + } else if (typeof params['hex'] != "undefined") { + this.setStringHex(params['hex']); + } else if (typeof params['date'] != "undefined") { + this.setByDate(params['date']); + } + } + }; + JSX.extend(KJUR.asn1.DERGeneralizedTime, KJUR.asn1.DERAbstractTime); + + // ******************************************************************** + /** + * class for ASN.1 DER Sequence + * @name KJUR.asn1.DERSequence + * @class class for ASN.1 DER Sequence + * @extends KJUR.asn1.DERAbstractStructured + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • array - specify array of ASN1Object to set elements of content
  • + *
+ * NOTE: 'params' can be omitted. + */ + KJUR.asn1.DERSequence = function(params) { + KJUR.asn1.DERSequence.superclass.constructor.call(this, params); + this.hT = "30"; + this.getFreshValueHex = function() { + var h = ''; + for (var i = 0; i < this.asn1Array.length; i++) { + var asn1Obj = this.asn1Array[i]; + h += asn1Obj.getEncodedHex(); + } + this.hV = h; + return this.hV; + }; + }; + JSX.extend(KJUR.asn1.DERSequence, KJUR.asn1.DERAbstractStructured); + + // ******************************************************************** + /** + * class for ASN.1 DER Set + * @name KJUR.asn1.DERSet + * @class class for ASN.1 DER Set + * @extends KJUR.asn1.DERAbstractStructured + * @description + *
+ * As for argument 'params' for constructor, you can specify one of + * following properties: + *
    + *
  • array - specify array of ASN1Object to set elements of content
  • + *
+ * NOTE: 'params' can be omitted. + */ + KJUR.asn1.DERSet = function(params) { + KJUR.asn1.DERSet.superclass.constructor.call(this, params); + this.hT = "31"; + this.getFreshValueHex = function() { + var a = new Array(); + for (var i = 0; i < this.asn1Array.length; i++) { + var asn1Obj = this.asn1Array[i]; + a.push(asn1Obj.getEncodedHex()); + } + a.sort(); + this.hV = a.join(''); + return this.hV; + }; + }; + JSX.extend(KJUR.asn1.DERSet, KJUR.asn1.DERAbstractStructured); + + // ******************************************************************** + /** + * class for ASN.1 DER TaggedObject + * @name KJUR.asn1.DERTaggedObject + * @class class for ASN.1 DER TaggedObject + * @extends KJUR.asn1.ASN1Object + * @description + *
+ * Parameter 'tagNoNex' is ASN.1 tag(T) value for this object. + * For example, if you find '[1]' tag in a ASN.1 dump, + * 'tagNoHex' will be 'a1'. + *
+ * As for optional argument 'params' for constructor, you can specify *ANY* of + * following properties: + *
    + *
  • explicit - specify true if this is explicit tag otherwise false + * (default is 'true').
  • + *
  • tag - specify tag (default is 'a0' which means [0])
  • + *
  • obj - specify ASN1Object which is tagged
  • + *
+ * @example + * d1 = new KJUR.asn1.DERUTF8String({'str':'a'}); + * d2 = new KJUR.asn1.DERTaggedObject({'obj': d1}); + * hex = d2.getEncodedHex(); + */ + KJUR.asn1.DERTaggedObject = function(params) { + KJUR.asn1.DERTaggedObject.superclass.constructor.call(this); + this.hT = "a0"; + this.hV = ''; + this.isExplicit = true; + this.asn1Object = null; + + /** + * set value by an ASN1Object + * @name setString + * @memberOf KJUR.asn1.DERTaggedObject + * @function + * @param {Boolean} isExplicitFlag flag for explicit/implicit tag + * @param {Integer} tagNoHex hexadecimal string of ASN.1 tag + * @param {ASN1Object} asn1Object ASN.1 to encapsulate + */ + this.setASN1Object = function(isExplicitFlag, tagNoHex, asn1Object) { + this.hT = tagNoHex; + this.isExplicit = isExplicitFlag; + this.asn1Object = asn1Object; + if (this.isExplicit) { + this.hV = this.asn1Object.getEncodedHex(); + this.hTLV = null; + this.isModified = true; + } else { + this.hV = null; + this.hTLV = asn1Object.getEncodedHex(); + this.hTLV = this.hTLV.replace(/^../, tagNoHex); + this.isModified = false; + } + }; + + this.getFreshValueHex = function() { + return this.hV; + }; + + if (typeof params != "undefined") { + if (typeof params['tag'] != "undefined") { + this.hT = params['tag']; + } + if (typeof params['explicit'] != "undefined") { + this.isExplicit = params['explicit']; + } + if (typeof params['obj'] != "undefined") { + this.asn1Object = params['obj']; + this.setASN1Object(this.isExplicit, this.hT, this.asn1Object); + } + } + }; + JSX.extend(KJUR.asn1.DERTaggedObject, KJUR.asn1.ASN1Object); + + var b64map="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var b64pad="="; + + function hex2b64(h) { + var i; + var c; + var ret = ""; + for(i = 0; i+3 <= h.length; i+=3) { + c = parseInt(h.substring(i,i+3),16); + ret += b64map.charAt(c >> 6) + b64map.charAt(c & 63); + } + if(i+1 == h.length) { + c = parseInt(h.substring(i,i+1),16); + ret += b64map.charAt(c << 2); + } + else if(i+2 == h.length) { + c = parseInt(h.substring(i,i+2),16); + ret += b64map.charAt(c >> 2) + b64map.charAt((c & 3) << 4); + } + while((ret.length & 3) > 0) ret += b64pad; + return ret; + } + + // convert a base64 string to hex + function b64tohex(s) { + var ret = ""; + var i; + var k = 0; // b64 state, 0-3 + var slop; + for(i = 0; i < s.length; ++i) { + if(s.charAt(i) == b64pad) break; + let v = b64map.indexOf(s.charAt(i)); + if(v < 0) continue; + if(k == 0) { + ret += int2char(v >> 2); + slop = v & 3; + k = 1; + } + else if(k == 1) { + ret += int2char((slop << 2) | (v >> 4)); + slop = v & 0xf; + k = 2; + } + else if(k == 2) { + ret += int2char(slop); + ret += int2char(v >> 2); + slop = v & 3; + k = 3; + } + else { + ret += int2char((slop << 2) | (v >> 4)); + ret += int2char(v & 0xf); + k = 0; + } + } + if(k == 1) + ret += int2char(slop << 2); + return ret; + } + + /** + * Retrieve the hexadecimal value (as a string) of the current ASN.1 element + * @returns {string} + * @public + */ + ASN1.prototype.getHexStringValue = function () { + var hexString = this.toHexString(); + var offset = this.header * 2; + var length = this.length * 2; + return hexString.substr(offset, length); + }; + + /** + * Method to parse a pem encoded string containing both a public or private key. + * The method will translate the pem encoded string in a der encoded string and + * will parse private key and public key parameters. This method accepts public key + * in the rsaencryption pkcs #1 format (oid: 1.2.840.113549.1.1.1). + * + * @todo Check how many rsa formats use the same format of pkcs #1. + * + * The format is defined as: + * PublicKeyInfo ::= SEQUENCE { + * algorithm AlgorithmIdentifier, + * PublicKey BIT STRING + * } + * Where AlgorithmIdentifier is: + * AlgorithmIdentifier ::= SEQUENCE { + * algorithm OBJECT IDENTIFIER, the OID of the enc algorithm + * parameters ANY DEFINED BY algorithm OPTIONAL (NULL for PKCS #1) + * } + * and PublicKey is a SEQUENCE encapsulated in a BIT STRING + * RSAPublicKey ::= SEQUENCE { + * modulus INTEGER, -- n + * publicExponent INTEGER -- e + * } + * it's possible to examine the structure of the keys obtained from openssl using + * an asn.1 dumper as the one used here to parse the components: http://lapo.it/asn1js/ + * @argument {string} pem the pem encoded string, can include the BEGIN/END header/footer + * @private + */ + RSAKey.prototype.parseKey = function (pem) { + try { + var modulus = 0; + var public_exponent = 0; + var reHex = /^\s*(?:[0-9A-Fa-f][0-9A-Fa-f]\s*)+$/; + var der = reHex.test(pem) ? Hex.decode(pem) : Base64.unarmor(pem); + var asn1 = ASN1.decode(der); + + //Fixes a bug with OpenSSL 1.0+ private keys + if(asn1.sub.length === 3){ + asn1 = asn1.sub[2].sub[0]; + } + if (asn1.sub.length === 9) { + + // Parse the private key. + modulus = asn1.sub[1].getHexStringValue(); //bigint + this.n = parseBigInt(modulus, 16); + + public_exponent = asn1.sub[2].getHexStringValue(); //int + this.e = parseInt(public_exponent, 16); + + var private_exponent = asn1.sub[3].getHexStringValue(); //bigint + this.d = parseBigInt(private_exponent, 16); + + var prime1 = asn1.sub[4].getHexStringValue(); //bigint + this.p = parseBigInt(prime1, 16); + + var prime2 = asn1.sub[5].getHexStringValue(); //bigint + this.q = parseBigInt(prime2, 16); + + var exponent1 = asn1.sub[6].getHexStringValue(); //bigint + this.dmp1 = parseBigInt(exponent1, 16); + + var exponent2 = asn1.sub[7].getHexStringValue(); //bigint + this.dmq1 = parseBigInt(exponent2, 16); + + var coefficient = asn1.sub[8].getHexStringValue(); //bigint + this.coeff = parseBigInt(coefficient, 16); + + } + else if (asn1.sub.length === 2) { + + // Parse the public key. + var bit_string = asn1.sub[1]; + var sequence = bit_string.sub[0]; + + modulus = sequence.sub[0].getHexStringValue(); + this.n = parseBigInt(modulus, 16); + public_exponent = sequence.sub[1].getHexStringValue(); + this.e = parseInt(public_exponent, 16); + + } + else { + return false; + } + return true; + } + catch (ex) { + return false; + } + }; + + /** + * Translate rsa parameters in a hex encoded string representing the rsa key. + * + * The translation follow the ASN.1 notation : + * RSAPrivateKey ::= SEQUENCE { + * version Version, + * modulus INTEGER, -- n + * publicExponent INTEGER, -- e + * privateExponent INTEGER, -- d + * prime1 INTEGER, -- p + * prime2 INTEGER, -- q + * exponent1 INTEGER, -- d mod (p1) + * exponent2 INTEGER, -- d mod (q-1) + * coefficient INTEGER, -- (inverse of q) mod p + * } + * @returns {string} DER Encoded String representing the rsa private key + * @private + */ + RSAKey.prototype.getPrivateBaseKey = function () { + var options = { + 'array': [ + new KJUR.asn1.DERInteger({'int': 0}), + new KJUR.asn1.DERInteger({'bigint': this.n}), + new KJUR.asn1.DERInteger({'int': this.e}), + new KJUR.asn1.DERInteger({'bigint': this.d}), + new KJUR.asn1.DERInteger({'bigint': this.p}), + new KJUR.asn1.DERInteger({'bigint': this.q}), + new KJUR.asn1.DERInteger({'bigint': this.dmp1}), + new KJUR.asn1.DERInteger({'bigint': this.dmq1}), + new KJUR.asn1.DERInteger({'bigint': this.coeff}) + ] + }; + var seq = new KJUR.asn1.DERSequence(options); + return seq.getEncodedHex(); + }; + + /** + * base64 (pem) encoded version of the DER encoded representation + * @returns {string} pem encoded representation without header and footer + * @public + */ + RSAKey.prototype.getPrivateBaseKeyB64 = function () { + return hex2b64(this.getPrivateBaseKey()); + }; + + /** + * Translate rsa parameters in a hex encoded string representing the rsa public key. + * The representation follow the ASN.1 notation : + * PublicKeyInfo ::= SEQUENCE { + * algorithm AlgorithmIdentifier, + * PublicKey BIT STRING + * } + * Where AlgorithmIdentifier is: + * AlgorithmIdentifier ::= SEQUENCE { + * algorithm OBJECT IDENTIFIER, the OID of the enc algorithm + * parameters ANY DEFINED BY algorithm OPTIONAL (NULL for PKCS #1) + * } + * and PublicKey is a SEQUENCE encapsulated in a BIT STRING + * RSAPublicKey ::= SEQUENCE { + * modulus INTEGER, -- n + * publicExponent INTEGER -- e + * } + * @returns {string} DER Encoded String representing the rsa public key + * @private + */ + RSAKey.prototype.getPublicBaseKey = function () { + var options = { + 'array': [ + new KJUR.asn1.DERObjectIdentifier({'oid': '1.2.840.113549.1.1.1'}), //RSA Encryption pkcs #1 oid + new KJUR.asn1.DERNull() + ] + }; + var first_sequence = new KJUR.asn1.DERSequence(options); + + options = { + 'array': [ + new KJUR.asn1.DERInteger({'bigint': this.n}), + new KJUR.asn1.DERInteger({'int': this.e}) + ] + }; + var second_sequence = new KJUR.asn1.DERSequence(options); + + options = { + 'hex': '00' + second_sequence.getEncodedHex() + }; + var bit_string = new KJUR.asn1.DERBitString(options); + + options = { + 'array': [ + first_sequence, + bit_string + ] + }; + var seq = new KJUR.asn1.DERSequence(options); + return seq.getEncodedHex(); + }; + + /** + * base64 (pem) encoded version of the DER encoded representation + * @returns {string} pem encoded representation without header and footer + * @public + */ + RSAKey.prototype.getPublicBaseKeyB64 = function () { + return hex2b64(this.getPublicBaseKey()); + }; + + /** + * wrap the string in block of width chars. The default value for rsa keys is 64 + * characters. + * @param {string} str the pem encoded string without header and footer + * @param {Number} [width=64] - the length the string has to be wrapped at + * @returns {string} + * @private + */ + RSAKey.prototype.wordwrap = function (str, width) { + width = width || 64; + if (!str) { + return str; + } + var regex = '(.{1,' + width + '})( +|$\n?)|(.{1,' + width + '})'; + return str.match(RegExp(regex, 'g')).join('\n'); + }; + + /** + * Retrieve the pem encoded private key + * @returns {string} the pem encoded private key with header/footer + * @public + */ + RSAKey.prototype.getPrivateKey = function () { + var key = "-----BEGIN RSA PRIVATE KEY-----\n"; + key += this.wordwrap(this.getPrivateBaseKeyB64()) + "\n"; + key += "-----END RSA PRIVATE KEY-----"; + return key; + }; + + /** + * Retrieve the pem encoded public key + * @returns {string} the pem encoded public key with header/footer + * @public + */ + RSAKey.prototype.getPublicKey = function () { + var key = "-----BEGIN PUBLIC KEY-----\n"; + key += this.wordwrap(this.getPublicBaseKeyB64()) + "\n"; + key += "-----END PUBLIC KEY-----"; + return key; + }; + + /** + * Check if the object contains the necessary parameters to populate the rsa modulus + * and public exponent parameters. + * @param {Object} [obj={}] - An object that may contain the two public key + * parameters + * @returns {boolean} true if the object contains both the modulus and the public exponent + * properties (n and e) + * @todo check for types of n and e. N should be a parseable bigInt object, E should + * be a parseable integer number + * @private + */ + RSAKey.prototype.hasPublicKeyProperty = function (obj) { + obj = obj || {}; + return ( + obj.hasOwnProperty('n') && + obj.hasOwnProperty('e') + ); + }; + + /** + * Check if the object contains ALL the parameters of an RSA key. + * @param {Object} [obj={}] - An object that may contain nine rsa key + * parameters + * @returns {boolean} true if the object contains all the parameters needed + * @todo check for types of the parameters all the parameters but the public exponent + * should be parseable bigint objects, the public exponent should be a parseable integer number + * @private + */ + RSAKey.prototype.hasPrivateKeyProperty = function (obj) { + obj = obj || {}; + return ( + obj.hasOwnProperty('n') && + obj.hasOwnProperty('e') && + obj.hasOwnProperty('d') && + obj.hasOwnProperty('p') && + obj.hasOwnProperty('q') && + obj.hasOwnProperty('dmp1') && + obj.hasOwnProperty('dmq1') && + obj.hasOwnProperty('coeff') + ); + }; + + /** + * Parse the properties of obj in the current rsa object. Obj should AT LEAST + * include the modulus and public exponent (n, e) parameters. + * @param {Object} obj - the object containing rsa parameters + * @private + */ + RSAKey.prototype.parsePropertiesFrom = function (obj) { + this.n = obj.n; + this.e = obj.e; + + if (obj.hasOwnProperty('d')) { + this.d = obj.d; + this.p = obj.p; + this.q = obj.q; + this.dmp1 = obj.dmp1; + this.dmq1 = obj.dmq1; + this.coeff = obj.coeff; + } + }; + + /** + * Create a new JSEncryptRSAKey that extends Tom Wu's RSA key object. + * This object is just a decorator for parsing the key parameter + * @param {string|Object} key - The key in string format, or an object containing + * the parameters needed to build a RSAKey object. + * @constructor + */ + class JSEncryptRSAKey extends RSAKey { + constructor(key) { + // Call the super constructor. + super(); + // If a key key was provided. + if (key) { + // If this is a string... + if (typeof key === 'string') { + this.parseKey(key); + } + else if ( + this.hasPrivateKeyProperty(key) || + this.hasPublicKeyProperty(key) + ) { + // Set the values for the key. + this.parsePropertiesFrom(key); + } + } + } + } + + /** + * + * @param {Object} [options = {}] - An object to customize JSEncrypt behaviour + * possible parameters are: + * - default_key_size {number} default: 1024 the key size in bit + * - default_public_exponent {string} default: '010001' the hexadecimal representation of the public exponent + * - log {boolean} default: false whether log warn/error or not + * @constructor + */ + class JSEncrypt{ + constructor (options) { + options = options || {}; + this.default_key_size = parseInt(options.default_key_size) || 1024; + this.default_public_exponent = options.default_public_exponent || '010001'; //65537 default openssl public exponent for rsa key type + this.log = options.log || false; + // The private and public key. + this.key = null; + } + } + + /** + * Method to set the rsa key parameter (one method is enough to set both the public + * and the private key, since the private key contains the public key paramenters) + * Log a warning if logs are enabled + * @param {Object|string} key the pem encoded string or an object (with or without header/footer) + * @public + */ + JSEncrypt.prototype.setKey = function (key) { + if (this.log && this.key) { + console.warn('A key was already set, overriding existing.'); + } + this.key = new JSEncryptRSAKey(key); + }; + + /** + * Proxy method for setKey, for api compatibility + * @see setKey + * @public + */ + JSEncrypt.prototype.setPrivateKey = function (privkey) { + // Create the key. + this.setKey(privkey); + }; + + /** + * Proxy method for setKey, for api compatibility + * @see setKey + * @public + */ + JSEncrypt.prototype.setPublicKey = function (pubkey) { + // Sets the public key. + this.setKey(pubkey); + }; + + /** + * Proxy method for RSAKey object's decrypt, decrypt the string using the private + * components of the rsa key object. Note that if the object was not set will be created + * on the fly (by the getKey method) using the parameters passed in the JSEncrypt constructor + * @param {string} string base64 encoded crypted string to decrypt + * @return {string} the decrypted string + * @public + */ + JSEncrypt.prototype.decrypt = function (string) { + // Return the decrypted string. + try { + return this.getKey().decrypt(b64tohex(string)); + } + catch (ex) { + return false; + } + }; + + /** + * Proxy method for RSAKey object's encrypt, encrypt the string using the public + * components of the rsa key object. Note that if the object was not set will be created + * on the fly (by the getKey method) using the parameters passed in the JSEncrypt constructor + * @param {string} string the string to encrypt + * @return {string} the encrypted string encoded in base64 + * @public + */ + JSEncrypt.prototype.encrypt = function (string) { + // Return the encrypted string. + try { + return hex2b64(this.getKey().encrypt(string)); + } + catch (ex) { + return false; + } + }; + + /** + * Getter for the current JSEncryptRSAKey object. If it doesn't exists a new object + * will be created and returned + * @param {callback} [cb] the callback to be called if we want the key to be generated + * in an async fashion + * @returns {JSEncryptRSAKey} the JSEncryptRSAKey object + * @public + */ + JSEncrypt.prototype.getKey = function (cb) { + // Only create new if it does not exist. + if (!this.key) { + // Get a new private key. + this.key = new JSEncryptRSAKey(); + if (cb && {}.toString.call(cb) === '[object Function]') { + this.key.generateAsync(this.default_key_size, this.default_public_exponent, cb); + return; + } + // Generate the key. + this.key.generate(this.default_key_size, this.default_public_exponent); + } + return this.key; + }; + + /** + * Returns the pem encoded representation of the private key + * If the key doesn't exists a new key will be created + * @returns {string} pem encoded representation of the private key WITH header and footer + * @public + */ + JSEncrypt.prototype.getPrivateKey = function () { + // Return the private representation of this key. + return this.getKey().getPrivateKey(); + }; + + /** + * Returns the pem encoded representation of the private key + * If the key doesn't exists a new key will be created + * @returns {string} pem encoded representation of the private key WITHOUT header and footer + * @public + */ + JSEncrypt.prototype.getPrivateKeyB64 = function () { + // Return the private representation of this key. + return this.getKey().getPrivateBaseKeyB64(); + }; + + + /** + * Returns the pem encoded representation of the public key + * If the key doesn't exists a new key will be created + * @returns {string} pem encoded representation of the public key WITH header and footer + * @public + */ + JSEncrypt.prototype.getPublicKey = function () { + // Return the private representation of this key. + return this.getKey().getPublicKey(); + }; + + /** + * Returns the pem encoded representation of the public key + * If the key doesn't exists a new key will be created + * @returns {string} pem encoded representation of the public key WITHOUT header and footer + * @public + */ + JSEncrypt.prototype.getPublicKeyB64 = function () { + // Return the private representation of this key. + return this.getKey().getPublicBaseKeyB64(); + }; + + // export * from 'jsencrypt'; + + class BaseTransport { + constructor(endpoint, stream_type, config={}) { + this.stream_type = stream_type; + this.endpoint = endpoint; + this.eventSource = new EventEmitter(); + this.dataQueue = []; + } + + static canTransfer(stream_type) { + return BaseTransport.streamTypes().includes(stream_type); + } + + static streamTypes() { + return []; + } + + destroy() { + this.eventSource.destroy(); + } + + connect() { + // TO be impemented + } + + disconnect() { + // TO be impemented + } + + reconnect() { + return this.disconnect().then(()=>{ + return this.connect(); + }); + } + + setEndpoint(endpoint) { + this.endpoint = endpoint; + return this.reconnect(); + } + + send(data) { + // TO be impemented + // return this.prepare(data).send(); + } + + prepare(data) { + // TO be impemented + // return new Request(data); + } + + // onData(type, data) { + // this.eventSource.dispatchEvent(type, data); + // } + } + + const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + //navigator.hardwareConcurrency || 3; + + function SMediaError(data) { + if(data instanceof SMediaError) { + return data; + } + + if (typeof data === 'number') { + this.code = data; + } else if (typeof value === 'string') { + this.message = data; + } + + if (!this.message) { + this.message = SMediaError.defaultMessages[this.code] || ''; + } + } + + SMediaError.prototype.code = 0; + SMediaError.prototype.message = ''; + + SMediaError.errorTypes = [ + 'MEDIA_ERR_CUSTOM', + 'MEDIA_ERR_ABORTED', + 'MEDIA_ERR_NETWORK', + 'MEDIA_ERR_DECODE', + 'MEDIA_ERR_SRC_NOT_SUPPORTED', + 'MEDIA_ERR_ENCRYPTED', + 'MEDIA_ERR_TRANSPORT' + ]; + + SMediaError.defaultMessages = { + 1: 'The fetching of the associated resource was aborted by the user\'s request.', + 2: 'Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.', + 3: 'Despite having previously been determined to be usable, an error occurred while trying to decode the media resource, resulting in an error.', + 4: 'The associated resource or media provider object (such as a MediaStream) has been found to be unsuitable.', + 5: 'The media is encrypted and we do not have the keys to decrypt it.', + 6: 'Transport error' + }; + + for (let errIndex = 0; errIndex < SMediaError.errorTypes.length; errIndex++) { + SMediaError[SMediaError.errorTypes[errIndex]] = errIndex; + SMediaError.prototype[SMediaError.errorTypes[errIndex]] = errIndex; + } + + const LOG_TAG$5 = "transport:ws"; + const Log$a = getTagged(LOG_TAG$5); + const LogI = getTagged("info"); + + class WebsocketTransport extends BaseTransport { + constructor(endpoint, stream_type, options={ + socket:`${location.protocol.replace('http', 'ws')}//${location.host}/ws/`, + workers: 1 + }) { + super(endpoint, stream_type); + this.proxies = []; + this.currentProxy = 0; + this.workers = 1; + this.socket_url = options.socket; + this.ready = this.connect(); + } + + destroy() { + return this.disconnect().then(()=>{ + return super.destroy(); + }); + + } + + static canTransfer(stream_type) { + return WebsocketTransport.streamTypes().includes(stream_type); + } + + static streamTypes() { + return ['hls', 'rtsp']; + } + + connect() { + return this.disconnect().then(()=>{ + let promises = []; + // TODO: get mirror list + for (let i=0; i{ + this.eventSource.dispatchEvent('info', info); + }); + + proxy.set_error_handler((error)=>{ + this.eventSource.dispatchEvent('error', error); + }); + proxy.set_disconnect_handler((error)=> { + this.eventSource.dispatchEvent('disconnected', {code: error.code, reason: error.reason}); + // TODO: only reconnect on demand + if ([1000, 1006, 1013, 1011].includes(error.code)) { + setTimeout(()=> { + if (this.ready && this.ready.reject) { + this.ready.reject(); + } + this.ready = this.connect(); + }, 3000); + } + }); + + proxy.set_data_handler((data)=> { + this.dataQueue.push(new Uint8Array(data)); + this.eventSource.dispatchEvent('data'); + }); + + promises.push(proxy.connect().then(()=> { + this.eventSource.dispatchEvent('connected'); + }).catch((e)=> { + this.eventSource.dispatchEvent('error'); + throw new Error(e); + })); + this.proxies.push(proxy); + } + return Promise.all(promises); + }); + } + + disconnect() { + let promises = []; + for (let i=0; i{}; + this.error_handler = ()=>{}; + this.disconnect_handler = ()=>{}; + this.builder = new WSPProtocol(WSPProtocol.V1_1); + this.awaitingPromises = {}; + this.seq = 0; + this.encryptor = new JSEncrypt(); + this.info_handler = ()=>{}; + } + + set_error_handler(handler){ + this.error_handler = handler; + } + + set_data_handler(handler) { + this.data_handler = handler; + } + + set_disconnect_handler(handler) { + this.disconnect_handler = handler; + } + + set_info_handler(handler){ + this.info_handler = handler; + } + + close() { + Log$a.log('closing connection'); + return new Promise((resolve)=>{ + this.ctrlChannel.onclose = ()=>{ + if (this.dataChannel) { + this.dataChannel.onclose = ()=>{ + Log$a.log('closed'); + resolve(); + }; + this.dataChannel.close(); + } else { + Log$a.log('closed'); + resolve(); + } + }; + this.ctrlChannel.close(); + }); + } + + onDisconnect(error){ + this.ctrlChannel.onclose=null; + this.ctrlChannel.close(); + if (this.dataChannel) { + this.dataChannel.onclose = null; + this.dataChannel.close(); + } + this.disconnect_handler(error); + if(error.code === WSPProtocol.WCC_INVALID_DOMAIN){ + let err = new SMediaError(SMediaError.MEDIA_ERR_TRANSPORT); + err.message = "Invalid Domain (credentials)"; + Log$a.error("Invalid domain (credentials)"); + this.error(err); + } + } + + initDataChannel(channel_id) { + return new Promise((resolve, reject)=>{ + this.dataChannel = new WebSocket(this.url, WebSocketProxy.CHN_DATA); + this.dataChannel.binaryType = 'arraybuffer'; + this.dataChannel.onopen = ()=>{ + let msg = this.builder.build(WSPProtocol.CMD_JOIN, { + channel: channel_id + }); + Log$a.debug(msg); + this.dataChannel.send(msg); + }; + this.dataChannel.onmessage = (ev)=>{ + Log$a.debug(`[data]\r\n${ev.data}`); + let res = WSPProtocol.parse(ev.data); + if (!res) { + return reject(); + } + + this.dataChannel.onmessage=(e)=>{ + // Log.debug('got data'); + if (this.data_handler) { + this.data_handler(e.data); + } + }; + resolve(); + }; + this.dataChannel.onerror = (e)=>{ + this.dataChannel.close(); + this.error(SMediaError.MEDIA_ERR_TRANSPORT); + }; + this.dataChannel.onclose = (e)=>{ + Log$a.error(`[data] ${e.type}. code: ${e.code}, reason: ${e.reason || 'unknown reason'}`); + this.onDisconnect(e); + }; + }); + } + + error(err){ + if (err !== undefined) { + this.error_ = new SMediaError(err); + if (this.error_handler){ + this.error_handler(this.error_ ); + } + } + return this.error_; + } + + onProxyCommandResponse(res){ + LogI.setLevel(4); + + let command = res.data.command; + let jsonObj = JSON.parse(res.payload); + + if (command === WSPProtocol.CMD_GET_INFO && jsonObj && jsonObj.info) + { + LogI.log("------------- Info ---------------------"); + + let infoObj = jsonObj.info; + let licenseInfo = infoObj.license; + let expiresAt = licenseInfo.expiresAt; + let requestedDomain = infoObj.requestedDomain; + let clients = infoObj.clients; + + LogI.log("License expires at : ", expiresAt); + LogI.log("Requested domain : ", requestedDomain); + + if (clients) { + LogI.log("------------- Source list --------------"); + for (let client in clients) { + LogI.log("Client: ", client); + if(clients[client]) { + clients[client].forEach((src) => { + LogI.log(" ", src.description, ":", src.url); + }); + } else { + LogI.log(" Client sources not found"); + } + } + this.info_handler(infoObj); + } + + LogI.log("-----------------------------------------"); + } + } + + connect() { + this.encryptionKey = null; + return new Promise((resolve, reject)=>{ + this.ctrlChannel = new WebSocket(this.url, WebSocketProxy.CHN_CONTROL); + + this.connected = false; + + this.ctrlChannel.onopen = ()=>{ + let headers = { + proto: this.stream_type + }; + if (this.endpoint.socket) { + headers.socket = this.endpoint.socket; + } else { + Object.assign(headers, { + host: this.endpoint.host, + port: this.endpoint.port, + client: this.endpoint.client + }); + } + let msgLicense = this.builder.build(WSPProtocol.CMD_GET_INFO, headers); + Log$a.debug(msgLicense); + this.ctrlChannel.send(msgLicense); + + let msg = this.builder.build(WSPProtocol.CMD_INIT, headers); + Log$a.debug(msg); + this.ctrlChannel.send(msg); + }; + + this.ctrlChannel.onmessage = (ev)=>{ + Log$a.debug(`[ctrl]\r\n${ev.data}`); + + let res = WSPProtocol.parse(ev.data); + if (!res) { + return reject(); + } + if (res.data && res.data.command && res.data.type && res.payload){ + this.onProxyCommandResponse(res); + return; + } + + if (res.code >= 300) { + Log$a.error(`[ctrl]\r\n${res.code}: ${res.msg}`); + return reject(); + } + this.ctrlChannel.onmessage = (e)=> { + let res = WSPProtocol.parse(e.data); + Log$a.debug(`[ctrl]\r\n${e.data}`); + if (res.data.seq in this.awaitingPromises) { + if (res.code < 300) { + this.awaitingPromises[res.data.seq].resolve(res); + } else { + this.awaitingPromises[res.data.seq].reject(res); + } + delete this.awaitingPromises[res.data.seq]; + } + }; + this.encryptionKey = res.data.pubkey || null; + if (this.encryptionKey) { + this.encryptor.setPublicKey(this.encryptionKey); + // TODO: check errors + } + this.initDataChannel(res.data.channel).then(resolve).catch(reject); + }; + + this.ctrlChannel.onerror = (e)=>{ + Log$a.error(`[ctrl] ${e.type}`); + this.error(SMediaError.MEDIA_ERR_TRANSPORT); + this.ctrlChannel.close(); + }; + this.ctrlChannel.onclose = (e)=>{ + Log$a.error(`[ctrl] ${e.type}. code: ${e.code} ${e.reason || 'unknown reason'}`); + this.onDisconnect(e); + }; + }); + } + + encrypt(msg) { + if (this.encryptionKey) { + let crypted = this.encryptor.encrypt(msg); + if (crypted === false) { + this.error(SMediaError.MEDIA_ERR_ENCRYPTED); + return; + } + return crypted; + } + return msg; + } + + send(payload) { + if (this.ctrlChannel.readyState != WebSocket.OPEN) { + this.close(); + this.error(SMediaError.MEDIA_ERR_TRANSPORT); + return; + } + // Log.debug(payload); + let data = { + contentLength: payload.length, + seq: ++WSPProtocol.seq + }; + return { + seq:data.seq, + promise: new Promise((resolve, reject)=>{ + this.awaitingPromises[data.seq] = {resolve, reject}; + let msg = this.builder.build(WSPProtocol.CMD_WRAP, data, payload); + Log$a.debug(msg); + this.ctrlChannel.send(this.encrypt(msg)); + })}; + } + } + + const Log$b = getTagged('wsp'); + + class StreamType$1 { + static get HLS() {return 'hls';} + static get RTSP() {return 'rtsp';} + + static isSupported(type) { + return [StreamType$1.HLS, StreamType$1.RTSP].includes(type); + } + + static fromUrl(url) { + let parsed; + try { + parsed = Url.parse(url); + } catch (e) { + return null; + } + switch (parsed.protocol) { + case 'rtsp': + return StreamType$1.RTSP; + case 'http': + case 'https': + if (url.indexOf('.m3u8')>=0) { + return StreamType$1.HLS; + } else { + return null; + } + default: + return null; + } + } + + static fromMime(mime) { + switch (mime) { + case 'application/x-rtsp': + return StreamType$1.RTSP; + case 'application/vnd.apple.mpegurl': + case 'application/x-mpegurl': + return StreamType$1.HLS; + default: + return null; + } + } + } + + class WSPlayer { + + constructor(node, opts) { + if (typeof node == typeof '') { + this.player = document.getElementById(node); + } else { + this.player = node; + } + + let modules = opts.modules || { + client: RTSPClient, + transport: { + constructor: WebsocketTransport + } + }; + this.errorHandler = opts.errorHandler || null; + this.infoHandler = opts.infoHandler || null; + this.queryCredentials = opts.queryCredentials || null; + + this.bufferDuration_ = opts.bufferDuration || 120; + if(isNaN(this.bufferDuration_) || (this.bufferDuration_ <= 0)){ + Log$b.warn("Expected number type for bufferDuration"); + this.bufferDuration_ = 120; + } + + this.modules = {}; + for (let module of modules) { + let transport = module.transport || WebsocketTransport; + let client = module.client || RTSPClient; + if (transport.constructor.canTransfer(client.streamType())) { + this.modules[client.streamType()] = { + client: client, + transport: transport + }; + } else { + Log$b.warn(`Client stream type ${client.streamType()} is incompatible with transport types [${transport.streamTypes().join(', ')}]. Skip`); + } + } + + this.type = StreamType$1.RTSP; + this.url = null; + if (opts.url && opts.type) { + this.url = opts.url; + this.type = opts.type; + } else { + if (!this._checkSource(this.player)) { + for (let i=0; i{ + if (!this.isPlaying()) { + this.client.start(); + } + }, false); + + this.player.addEventListener('pause', ()=>{ + this.client.stop(); + }, false); + + this.player.addEventListener('seeking', ()=>{ + if(this.player.buffered.length) { + let bStart = this.player.buffered.start(0); + let bEnd = this.player.buffered.end(0); + let bDuration = bEnd - bStart; + + if (bDuration > 0 && (this.player.currentTime < bStart || this.player.currentTime > bEnd)) { + if(this.player.currentTime < bStart){ + this.player.currentTime = bStart; + } + else{ + this.player.currentTime = bEnd - 1; + } + } + } + }, false); + + this.player.addEventListener('abort', () => { + // disconnect the transport when the player is closed + this.client.stop(); + this.transport.disconnect().then(() => { + this.client.destroy(); + }); + }, false); + + this.redirectNativeMediaErrors = opts.hasOwnProperty('redirectNativeMediaErrors') ? + opts.redirectNativeMediaErrors : true; + + if(this.redirectNativeMediaErrors) { + this.player.addEventListener('error', () => { + this.error(this.player.error.code); + }, false); + } + } + + // TODO: check native support + + isPlaying() { + return !(this.player.paused || this.client.paused); + } + + static canPlayWithModules(mimeType, modules) { + + let filteredModules = {}; + for (let module of modules) { + let transport = module.transport || WebsocketTransport; + let client = module.client || RTSPClient; + if (transport.canTransfer(client.streamType())) { + filteredModules[client.streamType()] = true; + } + } + + for (let type in filteredModules) { + if (type == StreamType$1.fromMime(mimeType)) { + return true; + } + } + return false; + } + + /// TODO: deprecate it? + static canPlay(resource) { + return StreamType$1.fromMime(resource.type) || StreamType$1.fromUrl(resource.src); + } + + canPlayUrl(src) { + let type = StreamType$1.fromUrl(src); + return (type in this.modules); + } + + _checkSource(src) { + if (!src.dataset['ignore'] && src.src && !this.player.canPlayType(src.type) && (StreamType$1.fromMime(src.type) || StreamType$1.fromUrl(src.src))) { + this.url = src.src; + this.type = src.type ? StreamType$1.fromMime(src.type) : StreamType$1.fromUrl(src.src); + return true; + } + return false; + } + + async setSource(url, type) { + if (this.transport) { + if (this.client) { + await this.client.detachTransport(); + } + await this.transport.destroy(); + } + try { + this.endpoint = Url.parse(url); + } catch (e) { + this.error(SMediaError.MEDIA_ERR_SRC_NOT_SUPPORTED); + return; + } + + this.url = url; + let transport = this.modules[type].transport; + this.transport = new transport.constructor(this.endpoint, this.type, transport.options); + this.transport.eventSource.addEventListener('error', (errorEvent)=>{ + this.error(errorEvent.detail); + }); + this.transport.eventSource.addEventListener('info', (infoEvent)=>{ + this.info(infoEvent.detail); + }); + + let lastType = this.type; + this.type = (StreamType$1.isSupported(type)?type:false) || StreamType$1.fromMime(type); + if (!this.type) { + this.error(SMediaError.MEDIA_ERR_SRC_NOT_SUPPORTED); + return; + } + + if (lastType!=this.type || !this.client) { + if (this.client) { + await this.client.destroy(); + } + let client = this.modules[type].client; + let opts = {errorHandler: this.errorHandler, flush: 200}; + this.client = new client(opts); + } else { + this.client.reset(); + } + + if (this.queryCredentials) { + this.client.queryCredentials = this.queryCredentials; + } + if (this.remuxer) { + this.remuxer.destroy(); + this.remuxer = null; + } + this.remuxer = new Remuxer(this.player); + this.remuxer.MSE.bufferDuration = this.bufferDuration_; + this.remuxer.attachClient(this.client); + + this.client.attachTransport(this.transport); + this.client.setSource(this.endpoint); + + if (this.player.autoplay) { + this.start(); + } + } + + set bufferDuration(duration){ + if(this.remuxer && this.remuxer.MSE) { + this.bufferDuration_ = duration; this.remuxer.MSE.bufferDuration = duration; + } + } + + get bufferDuration(){ + if(this.remuxer) + return this.remuxer.MSE.bufferDuration; + else + return undefined; + } + + error(err){ + if (err !== undefined) { + this.error_ = new SMediaError(err); + if (this.errorHandler){ + Log$b.error(this.error_.message); + this.errorHandler(this.error_); + } + } + return this.error_; + } + + info(inf){ + if (inf !== undefined) { + if (this.infoHandler){ + this.infoHandler(inf); + } + } + } + + start() { + if (this.client) { + this.client.start().catch((e)=>{ + if (this.errorHandler) { + this.errorHandler(e); + } + }); + } + } + + stop() { + if (this.client) { + this.client.stop(); + } + } + + async destroy() { + if (this.transport) { + if (this.client) { + await this.client.detachTransport(); + } + await this.transport.destroy(); + } + if (this.client) { + await this.client.destroy(); + } + if (this.remuxer) { + this.remuxer.destroy(); + this.remuxer = null; + } + } + + } + + setDefaultLogLevel(LogLevel.Error); + getTagged("transport:ws").setLevel(LogLevel.Error); + getTagged("client:rtsp").setLevel(LogLevel.Debug); + getTagged("mse").setLevel(LogLevel.Debug); + + window.Streamedian = { + logger(tag) { + return getTagged(tag) + }, + player(node, opts) { + if (!opts.socket) { + throw new Error("socket parameter is not set"); + } + + let _options = { + modules: [ + { + client: RTSPClient, + transport: { + constructor: WebsocketTransport, + options: { + socket: opts.socket + } + } + } + ], + errorHandler(e) { + if(opts.errorHandler) { + opts.errorHandler(e); + } else { + alert(`Failed to start player: ${e.message}`); + } + }, + infoHandler(inf) { + if(opts.infoHandler) { + opts.infoHandler(inf); + } + }, + redirectNativeMediaErrors: opts.redirectNativeMediaErrors, + bufferDuration : opts.bufferDuration, + + queryCredentials(client) { + return new Promise((resolve, reject) => { + let c = prompt('input credentials in format user:password'); + if (c) { + client.setCredentials.apply(client, c.split(':')); + resolve(); + } else { + reject(); + } + }); + } + }; + return new WSPlayer(node, _options); + } + }; + +}()); +//# sourceMappingURL=streamedian.min.js.map diff --git a/demos/wsp/style.css b/demos/wsp/style.css new file mode 100755 index 0000000..dac210c --- /dev/null +++ b/demos/wsp/style.css @@ -0,0 +1,28 @@ +body { + max-width: 720px; + margin: 50px auto; +} + +#test_video { + width: 720px; +} + +.controls { + display: flex; + justify-content: space-around; + align-items: center; +} +input.input, .form-inline .input-group>.form-control { + width: 300px; +} +.logs { + overflow: auto; + width: 720px; + height: 150px; + padding: 5px; + border-top: solid 1px gray; + border-bottom: solid 1px gray; +} +button { + margin: 5px +} \ No newline at end of file diff --git a/docs/apis.md b/docs/apis.md index 8d04cc6..fd60004 100755 --- a/docs/apis.md +++ b/docs/apis.md @@ -70,6 +70,9 @@ rtsp.active | number | 当前活跃连接数 | flv| object| flv连接信息 | flv.total|number|累计总链接数 | flv.active | number | 当前活跃连接数 | +wsp| object| wsp连接信息 | +wsp.total|number|累计总链接数 | +wsp.active | number | 当前活跃连接数 | extra | object | 运行时内存等信息 | #### 1.2.1 示例 @@ -98,6 +101,10 @@ curl http://localhost:1554/api/v1/runtime "flv": { "total": 0, "active": 0 + }, + "wsp": { + "total": 0, + "active": 0 } } ``` diff --git a/docs/quickstart.md b/docs/quickstart.md index 8bacc38..ced7eec 100755 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -52,31 +52,36 @@ ffplay -rtsp_transport tcp rtsp://localhost:1554/hr/door/video1 -fflags nobuffe rtsp://localhost:1554/hr/door/video1 请求在服务器内自动变成去拉取rtsp://admin:admin@192.168.110.145:1554/door/video1。 -### 3.2 使用websocket-rtsp +### 3.2 使用wsp +打开demo地址:http://localhost:1554/demos/wsp + +输入:rtsp://localhost:1554/group/door 即可访问。 + +### 3.3 使用websocket-rtsp 打开demo地址:http://localhost:1554/demos/rtsp 输入:ws://localhost:1554/ws/group/door 即可访问。 -### 3.3 使用http-flv访问 +### 3.4 使用http-flv访问 打开demo地址:http://localhost:1554/demos/flv 输入:http://locaolhost:1554/streams/group/door.flv 即可访问。 **注意:**由于 Chrome 对长连接的流限制为6个,因此如果使用 Chrome 打开更多建议使用websocket-flv。 -### 3.4 使用 websocket-flv访问 +### 3.5 使用 websocket-flv访问 打开demo地址:http://localhost:1554/demos/flv 输入:ws://locaolhost:1554/ws/group/door.flv 即可访问。 -### 3.5 使用 http-hls访问 +### 3.6 使用 http-hls访问 由于 iOS的Safari不支持上述任何http访问模式,请使用 http-hls 在浏览器输入: http://localhost:1554/streams/group/door.m3m8 即可访问。 **注意:** 由于http-hls的段文件默认被放在内存中,占用大量的内存;如系统内存不足,请配置存储路径。 -### 3.6 访问 h265 flv +### 3.7 访问 h265 flv 打开demo地址:http://localhost:1554/demos/flv265 输入:http://locaolhost:1554/streams/group/door.flv 即可访问。 diff --git a/network/websocket/websocket.go b/network/websocket/websocket.go index a1b3c73..8cfc168 100755 --- a/network/websocket/websocket.go +++ b/network/websocket/websocket.go @@ -67,7 +67,7 @@ const ( // The default upgrader to use var upgrader = &websocket.Upgrader{ - Subprotocols: []string{"rtsp"}, + Subprotocols: []string{"rtsp", "control", "data"}, CheckOrigin: func(r *http.Request) bool { return true }, // ReadBufferSize: 64 * 1024, WriteBufferSize: 64 * 1024, } diff --git a/service/apis.go b/service/apis.go index 8d580ab..d670499 100755 --- a/service/apis.go +++ b/service/apis.go @@ -189,6 +189,7 @@ func (s *Service) onGetRuntime(w http.ResponseWriter, r *http.Request, pathParam Streams sccc `json:"streams"` Rtsp stats.ConnsSample `json:"rtsp"` Flv stats.ConnsSample `json:"flv"` + Wsp stats.ConnsSample `json:"wsp"` Extra *stats.Runtime `json:"extra,omitempty"` } sc, cc := media.Count() @@ -199,6 +200,7 @@ func (s *Service) onGetRuntime(w http.ResponseWriter, r *http.Request, pathParam Streams: sccc{sc, cc}, Rtsp: stats.RtspConns.GetSample(), Flv: stats.FlvConns.GetSample(), + Wsp: stats.WspConns.GetSample(), } params := r.URL.Query() diff --git a/service/service.go b/service/service.go index 4573e7b..8af2cc6 100755 --- a/service/service.go +++ b/service/service.go @@ -22,6 +22,7 @@ import ( "github.com/cnotch/ipchub/provider/auth" "github.com/cnotch/ipchub/provider/route" "github.com/cnotch/ipchub/service/rtsp" + "github.com/cnotch/ipchub/service/wsp" "github.com/cnotch/scheduler" "github.com/cnotch/xlog" "github.com/emitter-io/address" @@ -36,6 +37,7 @@ type Service struct { tlsusing bool http *http.Server rtsp *tcp.Server + wsp *tcp.Server tokens *auth.TokenManager } @@ -48,6 +50,7 @@ func NewService(ctx context.Context, l *xlog.Logger) (s *Service, err error) { logger: l, http: new(http.Server), rtsp: new(tcp.Server), + wsp: new(tcp.Server), tokens: new(auth.TokenManager), } @@ -78,7 +81,8 @@ func NewService(ctx context.Context, l *xlog.Logger) (s *Service, err error) { // 设置 rtsp AcceptHandler s.rtsp.OnAccept = rtsp.CreateAcceptHandler() - + // 设置 wsp AcceptHandler + s.wsp.OnAccept = wsp.CreateAcceptHandler() // 启动定时存储拉流信息 scheduler.PeriodFunc(time.Minute*5, time.Minute*5, func() { route.Flush() diff --git a/service/streamapis.go b/service/streamapis.go index 9ed1f90..1f3d834 100755 --- a/service/streamapis.go +++ b/service/streamapis.go @@ -38,6 +38,12 @@ func (s *Service) onWebSocketRequest(w http.ResponseWriter, r *http.Request) { return } + if ws.Subprotocol() == "control" || ws.Subprotocol() == "data" { + // 代理访问 + s.wsp.OnAccept(ws) + return + } + if ext == ".flv" { go flv.ConsumeByWebsocket(s.logger, streamPath, r.RemoteAddr, ws) return diff --git a/service/wsp/protocol.go b/service/wsp/protocol.go new file mode 100755 index 0000000..390e704 --- /dev/null +++ b/service/wsp/protocol.go @@ -0,0 +1,170 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package wsp + +import ( + "bytes" + "fmt" + "io" + "strings" + "sync" + "unicode" + + "github.com/cnotch/ipchub/utils/scan" + "github.com/cnotch/xlog" +) + +const ( + wspProto = "WSP/1.1" // WSP协议版本 + prefixBody = "\r\n\r\n" // Header和Body分割符 +) + +// WSP 协议命令 +const ( + CmdInit = "INIT" // 初始化建立通道 + CmdJoin = "JOIN" // 数据通道使用 + CmdWrap = "WRAP" // 包装其他协议的命令 + CmdGetInfo = "GET_INFO" // 获取客户及license信息 +) + +// WSP 协议字段 +const ( + FieldProto = "proto" // 初始化的协议 如:rtsp + FieldSeq = "seq" // 命令序列 + FieldHost = "host" // 需要代理服务访问的远端host + FieldPort = "port" // 需要代理服务访问的远端port + FieldClient = "client" // 客户信息 + FieldChannel = "channel" // 数据通道编号,相当于一个session + FieldSocket = "socket" // 代替上面的host和port +) + +type badStringError struct { + what string + str string +} + +func (e *badStringError) Error() string { return fmt.Sprintf("%s %q", e.what, e.str) } + +// Request WSP 协议请求 +type Request struct { + Cmd string + Header map[string]string + Body string +} + +var ( + spacePair = scan.NewPair(' ', + func(r rune) bool { + return unicode.IsSpace(r) + }) + + validCmds = map[string]bool{ + CmdGetInfo: true, + CmdInit: true, + CmdJoin: true, + CmdWrap: true, + } + + bspool = &sync.Pool{ + New: func() interface{} { + return make([]byte, 8*1024) + }, + } + buffers = sync.Pool{ + New: func() interface{} { + return bytes.NewBuffer(make([]byte, 0, 1024*2)) + }, + } +) + +// DecodeStringRequest 解码字串请求 +func DecodeStringRequest(input string) (*Request, error) { + index := strings.Index(input, prefixBody) + if index < 0 { + return nil, &badStringError{"malformed WSP request,missing '\\r\\n\\r\\n'", input} + } + + req := &Request{ + Body: input[index+4:], + Header: make(map[string]string, 4), + } + + scanner := scan.Line + + // 先取首行 + tailing, substr, ok := scanner.Scan(input[:index]) + if !ok { + return nil, &badStringError{"malformed WSP request first line", substr} + } + proto, cmd, ok := spacePair.Scan(substr) + if proto != wspProto { + return nil, &badStringError{"malformed WSP request proto ", proto} + } + if _, ok := validCmds[cmd]; !ok { + return nil, &badStringError{"malformed WSP request command ", cmd} + } + req.Cmd = cmd + + // 循环取header + for ok { + tailing, substr, ok = scanner.Scan(tailing) + k, v, found := scan.ColonPair.Scan(substr) + if found { + req.Header[k] = v + } + } + + return req, nil +} + +// DecodeRequest 解码请求 +func DecodeRequest(r io.Reader, logger *xlog.Logger) (*Request, error) { + buf := bspool.Get().([]byte) + defer bspool.Put(buf) + + n, err := r.Read(buf) + if n == 0 && err == nil { // 上一个报文结束,再读一次 + n, err = r.Read(buf) + } + + if err != nil { + return nil, err + } + + input := string(buf[:n]) + logger.Debugf("wsp <<<=== \r\n%s", input) + + return DecodeStringRequest(input) +} + +// IsWrap 是否是包装协议,如果是,可以从Body提取被包装的协议 +func (req *Request) IsWrap() bool { + return req.Cmd == CmdWrap +} + +// ResponseOK 响应请求成功 +func (req *Request) ResponseOK(buf *bytes.Buffer, header map[string]string, payload string) { + req.ResponseTo(buf, 200, "OK", header, payload) +} + +// ResponseTo 响应请求到buf +func (req *Request) ResponseTo(buf *bytes.Buffer, statusCode int, statusText string, header map[string]string, payload string) { + // 写首行 + buf.WriteString(fmt.Sprintf("%s %d %s\r\n", wspProto, statusCode, statusText)) + // 写头 + header[FieldSeq] = req.Header[FieldSeq] + for k, v := range header { + buf.WriteString(k) + buf.WriteString(": ") + buf.WriteString(v) + buf.WriteString("\r\n") + } + // 写header和body分割 + buf.WriteString("\r\n") + // 写payload + if len(payload) > 0 { + buf.WriteString(payload) + } +} diff --git a/service/wsp/protocol_test.go b/service/wsp/protocol_test.go new file mode 100755 index 0000000..081c8cb --- /dev/null +++ b/service/wsp/protocol_test.go @@ -0,0 +1,69 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package wsp + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/cnotch/xlog" +) + +func TestDecodeRequest(t *testing.T) { + var reqStr = "WSP/1.1 GET_INFO\r\nproto: rtsp\r\nhost: 192.168.1.1\r\nport: 554\r\nclient: \r\nseq: 1\r\n\r\n" + t.Run("decode", func(t *testing.T) { + r := bytes.NewBufferString(reqStr) + got, err := DecodeRequest(r, xlog.L()) + if err != nil { + t.Errorf("DecodeRequest() error = %v", err) + return + } + assert.Equal(t, CmdGetInfo, got.Cmd) + assert.Equal(t, "1", got.Header[FieldSeq]) + }) + +} + +func TestRequest_ResponseOK(t *testing.T) { + respStr1 := "WSP/1.1 200 OK\r\nchannel: 334\r\nseq: 1\r\n\r\n" + respStr2 := "WSP/1.1 404 NOT FOUND\r\nchannel: 334\r\nseq: 1\r\n\r\n123" + t.Run("no payload", func(t *testing.T) { + req := &Request{ + Header: make(map[string]string), + } + req.Header[FieldSeq] = "1" + buf := &bytes.Buffer{} + header := make(map[string]string) + header[FieldChannel] = "334" + req.ResponseOK(buf, header, "") + resp := buf.String() + assert.Equal(t, respStr1, resp) + }) + t.Run("payload", func(t *testing.T) { + req := &Request{ + Header: make(map[string]string), + } + req.Header[FieldSeq] = "1" + buf := &bytes.Buffer{} + header := make(map[string]string) + header[FieldChannel] = "334" + req.ResponseTo(buf, 404, "NOT FOUND", header, "123") + resp := buf.String() + assert.Equal(t, respStr2, resp) + }) + +} + +func Benchmark_DecodeRequest(b *testing.B) { + var reqStr = "WSP/1.1 GET_INFO\r\nproto: rtsp\r\nhost: 192.168.1.1\r\nport: 554\r\nclient: \r\nseq: 1\r\n\r\n" + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + got, _ := DecodeStringRequest(reqStr) + _ = got + } + }) +} diff --git a/service/wsp/session.go b/service/wsp/session.go new file mode 100755 index 0000000..30cbfc6 --- /dev/null +++ b/service/wsp/session.go @@ -0,0 +1,461 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package wsp + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net/url" + "runtime/debug" + "strings" + "sync" + "time" + + "github.com/cnotch/ipchub/config" + "github.com/cnotch/ipchub/media" + "github.com/cnotch/ipchub/network/websocket" + "github.com/cnotch/ipchub/provider/security" + "github.com/cnotch/ipchub/service/rtsp" + "github.com/cnotch/ipchub/stats" + "github.com/cnotch/xlog" + "github.com/pixelbender/go-sdp/sdp" +) + +const ( + statusInit = iota + statusReady + statusPlaying +) +const ( + rtspURLPrefix = "rtsp://" // RTSP地址前缀 +) + +// Session RTSP 会话 +type Session struct { + // 创建时设置 + svr *Server + channelID string + logger *xlog.Logger + closed bool + paused bool + lsession string // 本地会话标识 + timeout time.Duration + conn websocket.Conn + lockW sync.Mutex + dataChannel websocket.Conn + + // DESCRIBE 后设置 + url *url.URL + path string + rawSdp string + sdp *sdp.Session + aControl string + vControl string + aCodec string + vCodec string + + // Setup 后设置 + transport rtsp.RTPTransport + + // 启动流媒体传输后设置 + status int // session状态 + source *media.Stream + cid *media.CID +} + +func newSession(svr *Server, conn websocket.Conn, channelID string) *Session { + + session := &Session{ + svr: svr, + channelID: channelID, + lsession: security.NewID().Base64(), + timeout: config.NetTimeout() * time.Duration(2), + conn: conn, + transport: rtsp.RTPTransport{ + Mode: rtsp.PlaySession, // 默认为播放 + Type: rtsp.RTPUnknownTrans, + }, + status: statusInit, + } + + for i := 0; i < 4; i++ { + session.transport.Channels[i] = -1 + session.transport.ClientPorts[i] = -1 + } + session.logger = svr.logger.With(xlog.Fields( + xlog.F("channel", channelID), + xlog.F("path", conn.Path()))) + + return session +} + +// 设置rtp数据通道 +func (s *Session) setDataChannel(dc websocket.Conn) { + s.lockW.Lock() + s.dataChannel = dc + s.lockW.Unlock() +} + +// Addr Session地址 +func (s *Session) Addr() string { + return s.conn.RemoteAddr().String() +} + +// Consume 消费媒体包 +func (s *Session) Consume(p Pack) { + if s.closed || s.paused { + return + } + + buf := buffers.Get().(*bytes.Buffer) + buf.Reset() + defer buffers.Put(buf) + p2 := p.(*rtsp.RTPPack) + p2.Write(buf, s.transport.Channels[:]) + + var err error + s.lockW.Lock() + if s.dataChannel != nil { + _, err = s.dataChannel.Write(buf.Bytes()) + } + s.lockW.Unlock() + + if err != nil { + s.logger.Errorf("send pack error = %v , close socket", err) + s.Close() + return + } +} + +// Close 关闭会话 +func (s *Session) Close() error { + if s.closed { + return nil + } + + s.closed = true + s.paused = false + s.conn.Close() + s.lockW.Lock() + if s.dataChannel != nil { + s.dataChannel.Close() + } + s.lockW.Unlock() + return nil +} + +func (s *Session) process() { + var err error + defer func() { + if r := recover(); r != nil { + s.logger.Errorf("wsp channel panic, %v \n %s", r, debug.Stack()) + } + + if err != nil { + if err == io.EOF { // 如果客户端断开提醒 + s.logger.Warn("websocket disconnect actively") + } else if !s.closed { // 如果主动关闭,不提示 + s.logger.Error(err.Error()) + } + } + + // 删除通道 + s.svr.sessions.Delete(s.channelID) + // 停止消费 + if s.cid != nil { + s.source.StopConsume(*s.cid) + s.cid = nil + s.source = nil + } + // 关闭连接 + s.Close() + + // 重置到初始状态 + s.conn = nil + s.dataChannel = nil + s.status = statusInit + stats.WspConns.Release() + s.logger.Info("close wsp channel") + }() + + s.logger.Info("open wsp channel") + + stats.WspConns.Add() // 增加一个 RTSP 连接计数 + + for !s.closed { + deadLine := time.Time{} + if s.timeout > 0 { + deadLine = time.Now().Add(s.timeout) + } + if err = s.conn.SetReadDeadline(deadLine); err != nil { + break + } + + var req *Request + req, err = DecodeRequest(s.conn, s.logger) + if err != nil { + break + } + + if req.Cmd != CmdWrap { + s.logger.Error("must is WRAP command request") + break + } + + // 从包装命令中提取 rtsp 请求 + var rtspReq *rtsp.Request + rtspReq, err = rtsp.ReadRequest(bufio.NewReader(bytes.NewBufferString(req.Body))) + if err != nil { + break + } + + // 处理请求,并获得响应 + rtspResp := s.onRequest(rtspReq) + + // 发送响应 + buf := buffers.Get().(*bytes.Buffer) + buf.Reset() + defer buffers.Put(buf) + req.ResponseOK(buf, map[string]string{FieldChannel: s.channelID}, "") + rtspResp.Write(buf) + _, err = s.conn.Write(buf.Bytes()) + if err != nil { + break + } + + s.logger.Debugf("wsp ===>>>\r\n%s", buf.String()) + + // 关闭通道 + if rtspReq.Method == rtsp.MethodTeardown { + break + } + } +} + +func (s *Session) onRequest(req *rtsp.Request) *rtsp.Response { + resp := s.newResponse(rtsp.StatusOK, req) + // 预处理 + continueProcess := s.onPreprocess(resp, req) + if !continueProcess { + return resp + } + + switch req.Method { + case rtsp.MethodDescribe: + s.onDescribe(resp, req) + case rtsp.MethodSetup: + s.onSetup(resp, req) + case rtsp.MethodPlay: + s.onPlay(resp, req) + case rtsp.MethodPause: + s.onPause(resp, req) + default: + // 状态不支持的方法 + resp.StatusCode = rtsp.StatusMethodNotValidInThisState + } + return resp +} + +func (s *Session) onDescribe(resp *rtsp.Response, req *rtsp.Request) { + + // TODO: 检查 accept 中的类型是否包含 sdp + s.url = req.URL + s.path = s.conn.Path() // 使用websocket路径 + // s.path = utils.CanonicalPath(req.URL.Path) + stream := media.GetOrCreate(s.path) + if stream == nil { + resp.StatusCode = rtsp.StatusNotFound + return + } + + // 从流中取 sdp + sdpRaw := stream.Sdp() + if len(sdpRaw) == 0 { + resp.StatusCode = rtsp.StatusNotFound + return + } + err := s.parseSdp(sdpRaw) + if err != nil { // TODO:需要更好的处理方式 + resp.StatusCode = rtsp.StatusNotFound + return + } + + resp.Header.Set(rtsp.FieldContentType, "application/sdp") + resp.Body = s.rawSdp +} + +func (s *Session) onSetup(resp *rtsp.Response, req *rtsp.Request) { + // a=control:streamid=1 + // a=control:rtsp://192.168.1.165/trackID=1 + // a=control:?ctype=video + setupURL := &url.URL{} + *setupURL = *req.URL + if setupURL.Port() == "" { + setupURL.Host = fmt.Sprintf("%s:554", setupURL.Host) + } + setupPath := setupURL.String() + + //setupPath = setupPath[strings.LastIndex(setupPath, "/")+1:] + vPath := getControlPath(s.vControl) + if vPath == "" { + resp.StatusCode = rtsp.StatusInternalServerError + resp.Status = "Invalid VControl" + return + } + + aPath := getControlPath(s.aControl) + + ts := req.Header.Get(rtsp.FieldTransport) + resp.Header.Set(rtsp.FieldTransport, ts) // 先回写transport + + // 检查控制路径 + chindex := -1 + if setupPath == aPath || (aPath != "" && strings.LastIndex(setupPath, aPath) == len(setupPath)-len(aPath)) { + chindex = int(rtsp.ChannelAudio) + } else if setupPath == vPath || (vPath != "" && strings.LastIndex(setupPath, vPath) == len(setupPath)-len(vPath)) { + chindex = int(rtsp.ChannelVideo) + } else { // 找不到被 Setup 的资源 + resp.StatusCode = rtsp.StatusInternalServerError + resp.Status = fmt.Sprintf("SETUP Unkown control:%s", setupPath) + return + } + + err := s.transport.ParseTransport(chindex, ts) + if err != nil { + resp.StatusCode = rtsp.StatusInvalidParameter + resp.Status = err.Error() + return + } + + // 检查必须是play模式 + if rtsp.PlaySession != s.transport.Mode { + resp.StatusCode = rtsp.StatusInvalidParameter + resp.Status = "can't setup as record" + return + } + + if s.transport.Type != rtsp.RTPTCPUnicast { // 需要修改回复的transport + resp.StatusCode = rtsp.StatusUnsupportedTransport + resp.Status = "websocket only support tcp unicast" + return + } + + if s.status < statusReady { // 初始状态切换到Ready + s.status = statusReady + } +} + +func (s *Session) onPlay(resp *rtsp.Response, req *rtsp.Request) { + if s.status == statusPlaying { + s.paused = false + return + } + + stream := media.GetOrCreate(s.path) + if stream == nil { + resp.StatusCode = rtsp.StatusNotFound + return + } + + resp.Header.Set(rtsp.FieldRange, req.Header.Get(rtsp.FieldRange)) + if s.cid == nil { + s.source = stream + // cid := stream.StartConsume(s) + cid := stream.StartConsumeNoGopCache(s, media.RTPPacket, "wsp") + s.cid = &cid + } + s.status = statusPlaying + s.paused = false + return +} + +func (s *Session) onPause(resp *rtsp.Response, req *rtsp.Request) { + if s.status == statusPlaying { + s.paused = true + } +} + +func (s *Session) onPreprocess(resp *rtsp.Response, req *rtsp.Request) (continueProcess bool) { + // Options + if req.Method == rtsp.MethodOptions { + resp.Header.Set(rtsp.FieldPublic, "DESCRIBE, SETUP, TEARDOWN, PLAY, OPTIONS, ANNOUNCE") + return false + } + + // 关闭请求 + if req.Method == rtsp.MethodTeardown { + return false + } + + // 检查状态下的方法 + switch s.status { + case statusReady: + continueProcess = req.Method == rtsp.MethodSetup || + req.Method == rtsp.MethodPlay + case statusPlaying: + continueProcess = (req.Method == rtsp.MethodPlay || + req.Method == rtsp.MethodPause) + default: + continueProcess = !(req.Method == rtsp.MethodPlay || + req.Method == rtsp.MethodRecord) + } + + if !continueProcess { + resp.StatusCode = rtsp.StatusMethodNotValidInThisState + return false + } + + return true +} + +func (s *Session) newResponse(code int, req *rtsp.Request) *rtsp.Response { + resp := &rtsp.Response{ + StatusCode: code, + Header: make(rtsp.Header), + Request: req, + } + + resp.Header.Set(rtsp.FieldCSeq, req.Header.Get(rtsp.FieldCSeq)) + resp.Header.Set(rtsp.FieldSession, s.lsession) + return resp +} + +func (s *Session) parseSdp(rawSdp string) (err error) { + // 从流中取 sdp + s.rawSdp = rawSdp + // 解析 + s.sdp, err = sdp.ParseString(s.rawSdp) + if err != nil { + return + } + + for _, media := range s.sdp.Media { + switch media.Type { + case "video": + s.vControl = media.Attributes.Get("control") + s.vCodec = media.Format[0].Name + case "audio": + s.aControl = media.Attributes.Get("control") + s.aCodec = media.Format[0].Name + } + } + return +} + +func getControlPath(ctrl string) (path string) { + if len(ctrl) >= len(rtspURLPrefix) && strings.EqualFold(ctrl[:len(rtspURLPrefix)], rtspURLPrefix) { + ctrlURL, err := url.Parse(ctrl) + if err != nil { + return + } + if ctrlURL.Port() == "" { + ctrlURL.Host = fmt.Sprintf("%s:554", ctrlURL.Hostname()) + } + return ctrlURL.String() + } + return ctrl +} diff --git a/service/wsp/types.go b/service/wsp/types.go new file mode 100644 index 0000000..500ab54 --- /dev/null +++ b/service/wsp/types.go @@ -0,0 +1,10 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package wsp + +import "github.com/cnotch/ipchub/av/format" + +// Pack . +type Pack = format.Packet diff --git a/service/wsp/wsp.go b/service/wsp/wsp.go new file mode 100755 index 0000000..b94f209 --- /dev/null +++ b/service/wsp/wsp.go @@ -0,0 +1,124 @@ +// Copyright (c) 2019,CAOHONGJU All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package wsp + +import ( + "bytes" + "net" + "sync" + + "github.com/cnotch/ipchub/network/websocket" + "github.com/cnotch/ipchub/provider/security" + "github.com/cnotch/xlog" + "github.com/kelindar/tcp" +) + +// Server https://github.com/Streamedian/html5_rtsp_player 客户端配套的服务器 +type Server struct { + logger *xlog.Logger + + sessions sync.Map +} + +// CreateAcceptHandler 创建连接接入处理器 +func CreateAcceptHandler() tcp.OnAccept { + svr := &Server{ + logger: xlog.L(), + } + return svr.onAcceptConn +} + +// onAcceptConn 当新连接接入时触发 +func (svr *Server) onAcceptConn(c net.Conn) { + wsc := c.(websocket.Conn) + if wsc.Subprotocol() == "control" { + go svr.handshakeControlChannel(wsc) + } else { + go svr.handshakeDataChannel(wsc) + } +} + +func (svr *Server) handshakeControlChannel(wsc websocket.Conn) { + svr.logger.Info("wsp control channel handshake.") + wsc = wsc.TextTransport() + for { + req, err := DecodeRequest(wsc, svr.logger) + if err != nil { + svr.logger.Error(err.Error()) + wsc.Close() + break + } + + if req.Cmd == CmdGetInfo { + continue + } + + if req.Cmd != CmdInit { + svr.logger.Errorf("wsp control channel handshake failed, malformed WSP request command: %s.", req.Cmd) + wsc.Close() + break + } + + // 初始化 + channelID := security.NewID().String() + buf := buffers.Get().(*bytes.Buffer) + buf.Reset() + defer buffers.Put(buf) + req.ResponseOK(buf, map[string]string{FieldChannel: channelID}, "") + _, err = wsc.Write(buf.Bytes()) + if err != nil { + svr.logger.Error(err.Error()) + wsc.Close() + break + } + session := newSession(svr, wsc, channelID) + svr.sessions.Store(channelID, session) + svr.logger.Debugf("wsp ===>>> \r\n%s", buf.String()) + go session.process() + break + } +} + +func (svr *Server) handshakeDataChannel(wsc websocket.Conn) { + tc := wsc.TextTransport() + req, err := DecodeRequest(tc, svr.logger) + if err != nil { + svr.logger.Error(err.Error()) + tc.Close() + return + } + + channelID := req.Header[FieldChannel] + code := 200 + text := "OK" + var session *Session + si, ok := svr.sessions.Load(channelID) + if ok { + session = si.(*Session) + } else { + code = 404 + text = "NOT FOUND" + } + + buf := buffers.Get().(*bytes.Buffer) + buf.Reset() + defer buffers.Put(buf) + req.ResponseTo(buf, code, text, map[string]string{}, "") + _, err = tc.Write(buf.Bytes()) + if err != nil { + svr.logger.Error(err.Error()) + tc.Close() + return + } + + svr.logger.Debugf("wsp ===>>> \r\n%s", buf.String()) + if session == nil { + tc.Close() + return + } + + // 添加到session + session.setDataChannel(wsc) +} diff --git a/stats/conns.go b/stats/conns.go index 0f79084..a473b3e 100755 --- a/stats/conns.go +++ b/stats/conns.go @@ -12,6 +12,7 @@ import ( var ( RtspConns = NewConns() // RTSP连接统计 FlvConns = NewConns() // flv连接统计 + WspConns = NewConns() // WSP连接统计 ) // ConnsSample 连接计数采样