github.com/cnotch/ipchub@v1.1.0/demos/rtsp/rtsp.dev.js (about) 1 (function () { 2 'use strict'; 3 4 // ERROR=0, WARN=1, LOG=2, DEBUG=3 5 const LogLevel = { 6 Error: 0, 7 Warn: 1, 8 Log: 2, 9 Debug: 3 10 }; 11 12 let DEFAULT_LOG_LEVEL = LogLevel.Debug; 13 14 function setDefaultLogLevel(level) { 15 DEFAULT_LOG_LEVEL = level; 16 } 17 class Logger { 18 constructor(level = DEFAULT_LOG_LEVEL, tag) { 19 this.tag = tag; 20 this.setLevel(level); 21 } 22 23 setLevel(level) { 24 this.level = level; 25 } 26 27 static get level_map() { return { 28 [LogLevel.Debug]:'log', 29 [LogLevel.Log]:'log', 30 [LogLevel.Warn]:'warn', 31 [LogLevel.Error]:'error' 32 }}; 33 34 _log(lvl, args) { 35 args = Array.prototype.slice.call(args); 36 if (this.tag) { 37 args.unshift(`[${this.tag}]`); 38 } 39 if (this.level>=lvl) console[Logger.level_map[lvl]].apply(console, args); 40 } 41 log(){ 42 this._log(LogLevel.Log, arguments); 43 } 44 debug(){ 45 this._log(LogLevel.Debug, arguments); 46 } 47 error(){ 48 this._log(LogLevel.Error, arguments); 49 } 50 warn(){ 51 this._log(LogLevel.Warn, arguments); 52 } 53 } 54 55 const taggedLoggers = new Map(); 56 function getTagged(tag) { 57 if (!taggedLoggers.has(tag)) { 58 taggedLoggers.set(tag, new Logger(DEFAULT_LOG_LEVEL, tag)); 59 } 60 return taggedLoggers.get(tag); 61 } 62 const Log = new Logger(); 63 64 class Url { 65 static parse(url) { 66 let ret = {}; 67 68 let regex = /^([^:]+):\/\/([^\/]+)(.*)$/; //protocol, login, urlpath 69 let result = regex.exec(url); 70 71 if (!result) { 72 throw new Error("bad url"); 73 } 74 75 ret.full = url; 76 ret.protocol = result[1]; 77 ret.urlpath = result[3]; 78 79 let parts = ret.urlpath.split('/'); 80 ret.basename = parts.pop().split(/\?|#/)[0]; 81 ret.basepath = parts.join('/'); 82 83 let loginSplit = result[2].split('@'); 84 let hostport = loginSplit[0].split(':'); 85 let userpass = [ null, null ]; 86 if (loginSplit.length === 2) { 87 userpass = loginSplit[0].split(':'); 88 hostport = loginSplit[1].split(':'); 89 } 90 91 ret.user = userpass[0]; 92 ret.pass = userpass[1]; 93 ret.host = hostport[0]; 94 ret.auth = (ret.user && ret.pass) ? `${ret.user}:${ret.pass}` : ''; 95 96 ret.port = (null == hostport[1]) ? Url.protocolDefaultPort(ret.protocol) : hostport[1]; 97 ret.portDefined = (null != hostport[1]); 98 ret.location = `${ret.host}:${ret.port}`; 99 100 if (ret.protocol == 'unix') { 101 ret.socket = ret.port; 102 ret.port = undefined; 103 } 104 105 return ret; 106 } 107 108 static full(parsed) { 109 return `${parsed.protocol}://${parsed.location}/${parsed.urlpath}`; 110 } 111 112 static isAbsolute(url) { 113 return /^[^:]+:\/\//.test(url); 114 } 115 116 static protocolDefaultPort(protocol) { 117 switch (protocol) { 118 case 'rtsp': return 554; 119 case 'http': return 80; 120 case 'https': return 443; 121 } 122 123 return 0; 124 } 125 } 126 127 const listener = Symbol("event_listener"); 128 const listeners = Symbol("event_listeners"); 129 130 class DestructibleEventListener { 131 constructor(eventListener) { 132 this[listener] = eventListener; 133 this[listeners] = new Map(); 134 } 135 136 clear() { 137 if (this[listeners]) { 138 for (let entry of this[listeners]) { 139 for (let fn of entry[1]) { 140 this[listener].removeEventListener(entry[0], fn); 141 } 142 } } 143 this[listeners].clear(); 144 } 145 146 destroy() { 147 this.clear(); 148 this[listeners] = null; 149 } 150 151 on(event, selector, fn) { 152 if (fn == undefined) { 153 fn = selector; 154 selector = null; 155 } 156 if (selector) { 157 return this.addEventListener(event, (e) => { 158 if (e.target.matches(selector)) { 159 fn(e); 160 } 161 }); 162 } else { 163 return this.addEventListener(event, fn); 164 } 165 } 166 167 addEventListener(event, fn) { 168 if (!this[listeners].has(event)) { 169 this[listeners].set(event, new Set()); 170 } 171 this[listeners].get(event).add(fn); 172 this[listener].addEventListener(event, fn, false); 173 return fn; 174 } 175 176 removeEventListener(event, fn) { 177 this[listener].removeEventListener(event, fn, false); 178 if (this[listeners].has(event)) { 179 //this[listeners].set(event, new Set()); 180 let ev = this[listeners].get(event); 181 ev.delete(fn); 182 if (!ev.size) { 183 this[listeners].delete(event); 184 } 185 } 186 } 187 188 dispatchEvent(event) { 189 if (this[listener]) { 190 this[listener].dispatchEvent(event); 191 } 192 } 193 } 194 195 class EventEmitter { 196 constructor(element=null) { 197 this[listener] = new DestructibleEventListener(element || document.createElement('div')); 198 } 199 200 clear() { 201 if (this[listener]) { 202 this[listener].clear(); 203 } 204 } 205 206 destroy() { 207 if (this[listener]) { 208 this[listener].destroy(); 209 this[listener] = null; 210 } 211 } 212 213 on(event, selector, fn) { 214 if (this[listener]) { 215 return this[listener].on(event, selector, fn); 216 } 217 return null; 218 } 219 220 addEventListener(event, fn) { 221 if (this[listener]) { 222 return this[listener].addEventListener(event, fn, false); 223 } 224 return null; 225 } 226 227 removeEventListener(event, fn) { 228 if (this[listener]) { 229 this[listener].removeEventListener(event, fn, false); 230 } 231 } 232 233 dispatchEvent(event, data) { 234 if (this[listener]) { 235 this[listener].dispatchEvent(new CustomEvent(event, {detail: data})); 236 } 237 } 238 } 239 240 class EventSourceWrapper { 241 constructor(eventSource) { 242 this.eventSource = eventSource; 243 this[listeners] = new Map(); 244 } 245 246 on(event, selector, fn) { 247 if (!this[listeners].has(event)) { 248 this[listeners].set(event, new Set()); 249 } 250 let listener = this.eventSource.on(event, selector, fn); 251 if (listener) { 252 this[listeners].get(event).add(listener); 253 } 254 } 255 256 off(event, fn){ 257 this.eventSource.removeEventListener(event, fn); 258 } 259 260 clear() { 261 this.eventSource.clear(); 262 this[listeners].clear(); 263 } 264 265 destroy() { 266 this.eventSource.clear(); 267 this[listeners] = null; 268 this.eventSource = null; 269 } 270 } 271 272 /** 273 * Generate MP4 Box 274 * got from: https://github.com/dailymotion/hls.js 275 */ 276 277 class MP4 { 278 static init() { 279 MP4.types = { 280 avc1: [], // codingname 281 avcC: [], 282 btrt: [], 283 dinf: [], 284 dref: [], 285 esds: [], 286 ftyp: [], 287 hdlr: [], 288 mdat: [], 289 mdhd: [], 290 mdia: [], 291 mfhd: [], 292 minf: [], 293 moof: [], 294 moov: [], 295 mp4a: [], 296 mvex: [], 297 mvhd: [], 298 sdtp: [], 299 stbl: [], 300 stco: [], 301 stsc: [], 302 stsd: [], 303 stsz: [], 304 stts: [], 305 tfdt: [], 306 tfhd: [], 307 traf: [], 308 trak: [], 309 trun: [], 310 trex: [], 311 tkhd: [], 312 vmhd: [], 313 smhd: [] 314 }; 315 316 var i; 317 for (i in MP4.types) { 318 if (MP4.types.hasOwnProperty(i)) { 319 MP4.types[i] = [ 320 i.charCodeAt(0), 321 i.charCodeAt(1), 322 i.charCodeAt(2), 323 i.charCodeAt(3) 324 ]; 325 } 326 } 327 328 var videoHdlr = new Uint8Array([ 329 0x00, // version 0 330 0x00, 0x00, 0x00, // flags 331 0x00, 0x00, 0x00, 0x00, // pre_defined 332 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' 333 0x00, 0x00, 0x00, 0x00, // reserved 334 0x00, 0x00, 0x00, 0x00, // reserved 335 0x00, 0x00, 0x00, 0x00, // reserved 336 0x56, 0x69, 0x64, 0x65, 337 0x6f, 0x48, 0x61, 0x6e, 338 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' 339 ]); 340 341 var audioHdlr = new Uint8Array([ 342 0x00, // version 0 343 0x00, 0x00, 0x00, // flags 344 0x00, 0x00, 0x00, 0x00, // pre_defined 345 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun' 346 0x00, 0x00, 0x00, 0x00, // reserved 347 0x00, 0x00, 0x00, 0x00, // reserved 348 0x00, 0x00, 0x00, 0x00, // reserved 349 0x53, 0x6f, 0x75, 0x6e, 350 0x64, 0x48, 0x61, 0x6e, 351 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' 352 ]); 353 354 MP4.HDLR_TYPES = { 355 'video': videoHdlr, 356 'audio': audioHdlr 357 }; 358 359 var dref = new Uint8Array([ 360 0x00, // version 0 361 0x00, 0x00, 0x00, // flags 362 0x00, 0x00, 0x00, 0x01, // entry_count 363 0x00, 0x00, 0x00, 0x0c, // entry_size 364 0x75, 0x72, 0x6c, 0x20, // 'url' type 365 0x00, // version 0 366 0x00, 0x00, 0x01 // entry_flags 367 ]); 368 369 var stco = new Uint8Array([ 370 0x00, // version 371 0x00, 0x00, 0x00, // flags 372 0x00, 0x00, 0x00, 0x00 // entry_count 373 ]); 374 375 MP4.STTS = MP4.STSC = MP4.STCO = stco; 376 377 MP4.STSZ = new Uint8Array([ 378 0x00, // version 379 0x00, 0x00, 0x00, // flags 380 0x00, 0x00, 0x00, 0x00, // sample_size 381 0x00, 0x00, 0x00, 0x00, // sample_count 382 ]); 383 MP4.VMHD = new Uint8Array([ 384 0x00, // version 385 0x00, 0x00, 0x01, // flags 386 0x00, 0x00, // graphicsmode 387 0x00, 0x00, 388 0x00, 0x00, 389 0x00, 0x00 // opcolor 390 ]); 391 MP4.SMHD = new Uint8Array([ 392 0x00, // version 393 0x00, 0x00, 0x00, // flags 394 0x00, 0x00, // balance 395 0x00, 0x00 // reserved 396 ]); 397 398 MP4.STSD = new Uint8Array([ 399 0x00, // version 0 400 0x00, 0x00, 0x00, // flags 401 0x00, 0x00, 0x00, 0x01]);// entry_count 402 403 var majorBrand = new Uint8Array([105,115,111,109]); // isom 404 var avc1Brand = new Uint8Array([97,118,99,49]); // avc1 405 var minorVersion = new Uint8Array([0, 0, 0, 1]); 406 407 MP4.FTYP = MP4.box(MP4.types.ftyp, majorBrand, minorVersion, majorBrand, avc1Brand); 408 MP4.DINF = MP4.box(MP4.types.dinf, MP4.box(MP4.types.dref, dref)); 409 } 410 411 static box(type, ...payload) { 412 var size = 8, 413 i = payload.length, 414 len = i, 415 result; 416 // calculate the total size we need to allocate 417 while (i--) { 418 size += payload[i].byteLength; 419 } 420 result = new Uint8Array(size); 421 result[0] = (size >> 24) & 0xff; 422 result[1] = (size >> 16) & 0xff; 423 result[2] = (size >> 8) & 0xff; 424 result[3] = size & 0xff; 425 result.set(type, 4); 426 // copy the payload into the result 427 for (i = 0, size = 8; i < len; ++i) { 428 // copy payload[i] array @ offset size 429 result.set(payload[i], size); 430 size += payload[i].byteLength; 431 } 432 return result; 433 } 434 435 static hdlr(type) { 436 return MP4.box(MP4.types.hdlr, MP4.HDLR_TYPES[type]); 437 } 438 439 static mdat(data) { 440 return MP4.box(MP4.types.mdat, data); 441 } 442 443 static mdhd(timescale, duration) { 444 return MP4.box(MP4.types.mdhd, new Uint8Array([ 445 0x00, // version 0 446 0x00, 0x00, 0x00, // flags 447 0x00, 0x00, 0x00, 0x02, // creation_time 448 0x00, 0x00, 0x00, 0x03, // modification_time 449 (timescale >> 24) & 0xFF, 450 (timescale >> 16) & 0xFF, 451 (timescale >> 8) & 0xFF, 452 timescale & 0xFF, // timescale 453 (duration >> 24), 454 (duration >> 16) & 0xFF, 455 (duration >> 8) & 0xFF, 456 duration & 0xFF, // duration 457 0x55, 0xc4, // 'und' language (undetermined) 458 0x00, 0x00 459 ])); 460 } 461 462 static mdia(track) { 463 return MP4.box(MP4.types.mdia, MP4.mdhd(track.timescale, track.duration), MP4.hdlr(track.type), MP4.minf(track)); 464 } 465 466 static mfhd(sequenceNumber) { 467 return MP4.box(MP4.types.mfhd, new Uint8Array([ 468 0x00, 469 0x00, 0x00, 0x00, // flags 470 (sequenceNumber >> 24), 471 (sequenceNumber >> 16) & 0xFF, 472 (sequenceNumber >> 8) & 0xFF, 473 sequenceNumber & 0xFF, // sequence_number 474 ])); 475 } 476 477 static minf(track) { 478 if (track.type === 'audio') { 479 return MP4.box(MP4.types.minf, MP4.box(MP4.types.smhd, MP4.SMHD), MP4.DINF, MP4.stbl(track)); 480 } else { 481 return MP4.box(MP4.types.minf, MP4.box(MP4.types.vmhd, MP4.VMHD), MP4.DINF, MP4.stbl(track)); 482 } 483 } 484 485 static moof(sn, baseMediaDecodeTime, track) { 486 return MP4.box(MP4.types.moof, MP4.mfhd(sn), MP4.traf(track,baseMediaDecodeTime)); 487 } 488 /** 489 * @param tracks... (optional) {array} the tracks associated with this movie 490 */ 491 static moov(tracks, duration, timescale) { 492 var 493 i = tracks.length, 494 boxes = []; 495 496 while (i--) { 497 boxes[i] = MP4.trak(tracks[i]); 498 } 499 500 return MP4.box.apply(null, [MP4.types.moov, MP4.mvhd(timescale, duration)].concat(boxes).concat(MP4.mvex(tracks))); 501 } 502 503 static mvex(tracks) { 504 var 505 i = tracks.length, 506 boxes = []; 507 508 while (i--) { 509 boxes[i] = MP4.trex(tracks[i]); 510 } 511 return MP4.box.apply(null, [MP4.types.mvex].concat(boxes)); 512 } 513 514 static mvhd(timescale,duration) { 515 var 516 bytes = new Uint8Array([ 517 0x00, // version 0 518 0x00, 0x00, 0x00, // flags 519 0x00, 0x00, 0x00, 0x01, // creation_time 520 0x00, 0x00, 0x00, 0x02, // modification_time 521 (timescale >> 24) & 0xFF, 522 (timescale >> 16) & 0xFF, 523 (timescale >> 8) & 0xFF, 524 timescale & 0xFF, // timescale 525 (duration >> 24) & 0xFF, 526 (duration >> 16) & 0xFF, 527 (duration >> 8) & 0xFF, 528 duration & 0xFF, // duration 529 0x00, 0x01, 0x00, 0x00, // 1.0 rate 530 0x01, 0x00, // 1.0 volume 531 0x00, 0x00, // reserved 532 0x00, 0x00, 0x00, 0x00, // reserved 533 0x00, 0x00, 0x00, 0x00, // reserved 534 0x00, 0x01, 0x00, 0x00, 535 0x00, 0x00, 0x00, 0x00, 536 0x00, 0x00, 0x00, 0x00, 537 0x00, 0x00, 0x00, 0x00, 538 0x00, 0x01, 0x00, 0x00, 539 0x00, 0x00, 0x00, 0x00, 540 0x00, 0x00, 0x00, 0x00, 541 0x00, 0x00, 0x00, 0x00, 542 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix 543 0x00, 0x00, 0x00, 0x00, 544 0x00, 0x00, 0x00, 0x00, 545 0x00, 0x00, 0x00, 0x00, 546 0x00, 0x00, 0x00, 0x00, 547 0x00, 0x00, 0x00, 0x00, 548 0x00, 0x00, 0x00, 0x00, // pre_defined 549 0xff, 0xff, 0xff, 0xff // next_track_ID 550 ]); 551 return MP4.box(MP4.types.mvhd, bytes); 552 } 553 554 static sdtp(track) { 555 var 556 samples = track.samples || [], 557 bytes = new Uint8Array(4 + samples.length), 558 flags, 559 i; 560 // leave the full box header (4 bytes) all zero 561 // write the sample table 562 for (i = 0; i < samples.length; i++) { 563 flags = samples[i].flags; 564 bytes[i + 4] = (flags.dependsOn << 4) | 565 (flags.isDependedOn << 2) | 566 (flags.hasRedundancy); 567 } 568 569 return MP4.box(MP4.types.sdtp, bytes); 570 } 571 572 static stbl(track) { 573 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)); 574 } 575 576 static avc1(track) { 577 var sps = [], pps = [], i, data, len; 578 // assemble the SPSs 579 580 for (i = 0; i < track.sps.length; i++) { 581 data = track.sps[i]; 582 len = data.byteLength; 583 sps.push((len >>> 8) & 0xFF); 584 sps.push((len & 0xFF)); 585 sps = sps.concat(Array.prototype.slice.call(data)); // SPS 586 } 587 588 // assemble the PPSs 589 for (i = 0; i < track.pps.length; i++) { 590 data = track.pps[i]; 591 len = data.byteLength; 592 pps.push((len >>> 8) & 0xFF); 593 pps.push((len & 0xFF)); 594 pps = pps.concat(Array.prototype.slice.call(data)); 595 } 596 597 var avcc = MP4.box(MP4.types.avcC, new Uint8Array([ 598 0x01, // version 599 sps[3], // profile 600 sps[4], // profile compat 601 sps[5], // level 602 0xfc | 3, // lengthSizeMinusOne, hard-coded to 4 bytes 603 0xE0 | track.sps.length // 3bit reserved (111) + numOfSequenceParameterSets 604 ].concat(sps).concat([ 605 track.pps.length // numOfPictureParameterSets 606 ]).concat(pps))), // "PPS" 607 width = track.width, 608 height = track.height; 609 //console.log('avcc:' + Hex.hexDump(avcc)); 610 return MP4.box(MP4.types.avc1, new Uint8Array([ 611 0x00, 0x00, 0x00, // reserved 612 0x00, 0x00, 0x00, // reserved 613 0x00, 0x01, // data_reference_index 614 0x00, 0x00, // pre_defined 615 0x00, 0x00, // reserved 616 0x00, 0x00, 0x00, 0x00, 617 0x00, 0x00, 0x00, 0x00, 618 0x00, 0x00, 0x00, 0x00, // pre_defined 619 (width >> 8) & 0xFF, 620 width & 0xff, // width 621 (height >> 8) & 0xFF, 622 height & 0xff, // height 623 0x00, 0x48, 0x00, 0x00, // horizresolution 624 0x00, 0x48, 0x00, 0x00, // vertresolution 625 0x00, 0x00, 0x00, 0x00, // reserved 626 0x00, 0x01, // frame_count 627 0x12, 628 0x62, 0x69, 0x6E, 0x65, //binelpro.ru 629 0x6C, 0x70, 0x72, 0x6F, 630 0x2E, 0x72, 0x75, 0x00, 631 0x00, 0x00, 0x00, 0x00, 632 0x00, 0x00, 0x00, 0x00, 633 0x00, 0x00, 0x00, 0x00, 634 0x00, 0x00, 0x00, 0x00, 635 0x00, 0x00, 0x00, // compressorname 636 0x00, 0x18, // depth = 24 637 0x11, 0x11]), // pre_defined = -1 638 avcc, 639 MP4.box(MP4.types.btrt, new Uint8Array([ 640 0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB 641 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate 642 0x00, 0x2d, 0xc6, 0xc0])) // avgBitrate 643 ); 644 } 645 646 static esds(track) { 647 var configlen = track.config.byteLength; 648 let data = new Uint8Array(26+configlen+3); 649 data.set([ 650 0x00, // version 0 651 0x00, 0x00, 0x00, // flags 652 653 0x03, // descriptor_type 654 0x17+configlen, // length 655 0x00, 0x01, //es_id 656 0x00, // stream_priority 657 658 0x04, // descriptor_type 659 0x0f+configlen, // length 660 0x40, //codec : mpeg4_audio 661 0x15, // stream_type 662 0x00, 0x00, 0x00, // buffer_size 663 0x00, 0x00, 0x00, 0x00, // maxBitrate 664 0x00, 0x00, 0x00, 0x00, // avgBitrate 665 666 0x05, // descriptor_type 667 configlen 668 ]); 669 data.set(track.config, 26); 670 data.set([0x06, 0x01, 0x02], 26+configlen); 671 // return new Uint8Array([ 672 // 0x00, // version 0 673 // 0x00, 0x00, 0x00, // flags 674 // 675 // 0x03, // descriptor_type 676 // 0x17+configlen, // length 677 // 0x00, 0x01, //es_id 678 // 0x00, // stream_priority 679 // 680 // 0x04, // descriptor_type 681 // 0x0f+configlen, // length 682 // 0x40, //codec : mpeg4_audio 683 // 0x15, // stream_type 684 // 0x00, 0x00, 0x00, // buffer_size 685 // 0x00, 0x00, 0x00, 0x00, // maxBitrate 686 // 0x00, 0x00, 0x00, 0x00, // avgBitrate 687 // 688 // 0x05 // descriptor_type 689 // ].concat([configlen]).concat(track.config).concat([0x06, 0x01, 0x02])); // GASpecificConfig)); // length + audio config descriptor 690 return data; 691 } 692 693 static mp4a(track) { 694 var audiosamplerate = track.audiosamplerate; 695 return MP4.box(MP4.types.mp4a, new Uint8Array([ 696 0x00, 0x00, 0x00, // reserved 697 0x00, 0x00, 0x00, // reserved 698 0x00, 0x01, // data_reference_index 699 0x00, 0x00, 0x00, 0x00, 700 0x00, 0x00, 0x00, 0x00, // reserved 701 0x00, track.channelCount, // channelcount 702 0x00, 0x10, // sampleSize:16bits 703 0x00, 0x00, // pre_defined 704 0x00, 0x00, // reserved2 705 (audiosamplerate >> 8) & 0xFF, 706 audiosamplerate & 0xff, // 707 0x00, 0x00]), 708 MP4.box(MP4.types.esds, MP4.esds(track))); 709 } 710 711 static stsd(track) { 712 if (track.type === 'audio') { 713 return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp4a(track)); 714 } else { 715 return MP4.box(MP4.types.stsd, MP4.STSD, MP4.avc1(track)); 716 } 717 } 718 719 static tkhd(track) { 720 var id = track.id, 721 duration = track.duration, 722 width = track.width, 723 height = track.height, 724 volume = track.volume; 725 return MP4.box(MP4.types.tkhd, new Uint8Array([ 726 0x00, // version 0 727 0x00, 0x00, 0x07, // flags 728 0x00, 0x00, 0x00, 0x00, // creation_time 729 0x00, 0x00, 0x00, 0x00, // modification_time 730 (id >> 24) & 0xFF, 731 (id >> 16) & 0xFF, 732 (id >> 8) & 0xFF, 733 id & 0xFF, // track_ID 734 0x00, 0x00, 0x00, 0x00, // reserved 735 (duration >> 24), 736 (duration >> 16) & 0xFF, 737 (duration >> 8) & 0xFF, 738 duration & 0xFF, // duration 739 0x00, 0x00, 0x00, 0x00, 740 0x00, 0x00, 0x00, 0x00, // reserved 741 0x00, 0x00, // layer 742 0x00, 0x00, // alternate_group 743 (volume>>0)&0xff, (((volume%1)*10)>>0)&0xff, // track volume // FIXME 744 0x00, 0x00, // reserved 745 0x00, 0x01, 0x00, 0x00, 746 0x00, 0x00, 0x00, 0x00, 747 0x00, 0x00, 0x00, 0x00, 748 0x00, 0x00, 0x00, 0x00, 749 0x00, 0x01, 0x00, 0x00, 750 0x00, 0x00, 0x00, 0x00, 751 0x00, 0x00, 0x00, 0x00, 752 0x00, 0x00, 0x00, 0x00, 753 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix 754 (width >> 8) & 0xFF, 755 width & 0xFF, 756 0x00, 0x00, // width 757 (height >> 8) & 0xFF, 758 height & 0xFF, 759 0x00, 0x00 // height 760 ])); 761 } 762 763 static traf(track,baseMediaDecodeTime) { 764 var sampleDependencyTable = MP4.sdtp(track), 765 id = track.id; 766 return MP4.box(MP4.types.traf, 767 MP4.box(MP4.types.tfhd, new Uint8Array([ 768 0x00, // version 0 769 0x00, 0x00, 0x00, // flags 770 (id >> 24), 771 (id >> 16) & 0XFF, 772 (id >> 8) & 0XFF, 773 (id & 0xFF) // track_ID 774 ])), 775 MP4.box(MP4.types.tfdt, new Uint8Array([ 776 0x00, // version 0 777 0x00, 0x00, 0x00, // flags 778 (baseMediaDecodeTime >>24), 779 (baseMediaDecodeTime >> 16) & 0XFF, 780 (baseMediaDecodeTime >> 8) & 0XFF, 781 (baseMediaDecodeTime & 0xFF) // baseMediaDecodeTime 782 ])), 783 MP4.trun(track, 784 sampleDependencyTable.length + 785 16 + // tfhd 786 16 + // tfdt 787 8 + // traf header 788 16 + // mfhd 789 8 + // moof header 790 8), // mdat header 791 sampleDependencyTable); 792 } 793 794 /** 795 * Generate a track box. 796 * @param track {object} a track definition 797 * @return {Uint8Array} the track box 798 */ 799 static trak(track) { 800 track.duration = track.duration || 0xffffffff; 801 return MP4.box(MP4.types.trak, MP4.tkhd(track), MP4.mdia(track)); 802 } 803 804 static trex(track) { 805 var id = track.id; 806 return MP4.box(MP4.types.trex, new Uint8Array([ 807 0x00, // version 0 808 0x00, 0x00, 0x00, // flags 809 (id >> 24), 810 (id >> 16) & 0XFF, 811 (id >> 8) & 0XFF, 812 (id & 0xFF), // track_ID 813 0x00, 0x00, 0x00, 0x01, // default_sample_description_index 814 0x00, 0x00, 0x00, 0x00, // default_sample_duration 815 0x00, 0x00, 0x00, 0x00, // default_sample_size 816 0x00, 0x01, 0x00, 0x01 // default_sample_flags 817 ])); 818 } 819 820 static trun(track, offset) { 821 var samples= track.samples || [], 822 len = samples.length, 823 arraylen = 12 + (16 * len), 824 array = new Uint8Array(arraylen), 825 i,sample,duration,size,flags,cts; 826 offset += 8 + arraylen; 827 array.set([ 828 0x00, // version 0 829 0x00, 0x0f, 0x01, // flags 830 (len >>> 24) & 0xFF, 831 (len >>> 16) & 0xFF, 832 (len >>> 8) & 0xFF, 833 len & 0xFF, // sample_count 834 (offset >>> 24) & 0xFF, 835 (offset >>> 16) & 0xFF, 836 (offset >>> 8) & 0xFF, 837 offset & 0xFF // data_offset 838 ],0); 839 for (i = 0; i < len; i++) { 840 sample = samples[i]; 841 duration = sample.duration; 842 size = sample.size; 843 flags = sample.flags; 844 cts = sample.cts; 845 array.set([ 846 (duration >>> 24) & 0xFF, 847 (duration >>> 16) & 0xFF, 848 (duration >>> 8) & 0xFF, 849 duration & 0xFF, // sample_duration 850 (size >>> 24) & 0xFF, 851 (size >>> 16) & 0xFF, 852 (size >>> 8) & 0xFF, 853 size & 0xFF, // sample_size 854 (flags.isLeading << 2) | flags.dependsOn, 855 (flags.isDependedOn << 6) | 856 (flags.hasRedundancy << 4) | 857 (flags.paddingValue << 1) | 858 flags.isNonSync, 859 flags.degradPrio & 0xF0 << 8, 860 flags.degradPrio & 0x0F, // sample_flags 861 (cts >>> 24) & 0xFF, 862 (cts >>> 16) & 0xFF, 863 (cts >>> 8) & 0xFF, 864 cts & 0xFF // sample_composition_time_offset 865 ],12+16*i); 866 } 867 return MP4.box(MP4.types.trun, array); 868 } 869 870 static initSegment(tracks, duration, timescale) { 871 if (!MP4.types) { 872 MP4.init(); 873 } 874 var movie = MP4.moov(tracks, duration, timescale), result; 875 result = new Uint8Array(MP4.FTYP.byteLength + movie.byteLength); 876 result.set(MP4.FTYP); 877 result.set(movie, MP4.FTYP.byteLength); 878 return result; 879 } 880 } 881 882 //import {MP4Inspect} from '../iso-bmff/mp4-inspector.js'; 883 884 const LOG_TAG = "mse"; 885 const Log$1 = getTagged(LOG_TAG); 886 887 class MSEBuffer { 888 constructor(parent, codec) { 889 this.mediaSource = parent.mediaSource; 890 this.players = parent.players; 891 this.cleaning = false; 892 this.parent = parent; 893 this.queue = []; 894 this.cleanResolvers = []; 895 this.codec = codec; 896 this.cleanRanges = []; 897 898 Log$1.debug(`Use codec: ${codec}`); 899 900 this.sourceBuffer = this.mediaSource.addSourceBuffer(codec); 901 this.eventSource = new EventEmitter(this.sourceBuffer); 902 903 this.eventSource.addEventListener('updatestart', (e)=> { 904 // this.updating = true; 905 // Log.debug('update start'); 906 if (this.cleaning) { 907 Log$1.debug(`${this.codec} cleaning start`); 908 } 909 }); 910 911 this.eventSource.addEventListener('update', (e)=> { 912 // this.updating = true; 913 if (this.cleaning) { 914 Log$1.debug(`${this.codec} cleaning update`); 915 } 916 }); 917 918 this.eventSource.addEventListener('updateend', (e)=> { 919 // Log.debug('update end'); 920 // this.updating = false; 921 if (this.cleaning) { 922 Log$1.debug(`${this.codec} cleaning end`); 923 924 try { 925 if (this.sourceBuffer.buffered.length && this.players[0].currentTime < this.sourceBuffer.buffered.start(0)) { 926 this.players[0].currentTime = this.sourceBuffer.buffered.start(0); 927 } 928 } catch (e) { 929 // TODO: do something? 930 } 931 while (this.cleanResolvers.length) { 932 let resolver = this.cleanResolvers.shift(); 933 resolver(); 934 } 935 this.cleaning = false; 936 937 if (this.cleanRanges.length) { 938 this.doCleanup(); 939 return; 940 } 941 } 942 this.feedNext(); 943 }); 944 945 this.eventSource.addEventListener('error', (e)=> { 946 Log$1.debug(`Source buffer error: ${this.mediaSource.readyState}`); 947 if (this.mediaSource.sourceBuffers.length) { 948 this.mediaSource.removeSourceBuffer(this.sourceBuffer); 949 } 950 this.parent.eventSource.dispatchEvent('error'); 951 }); 952 953 this.eventSource.addEventListener('abort', (e)=> { 954 Log$1.debug(`Source buffer aborted: ${this.mediaSource.readyState}`); 955 if (this.mediaSource.sourceBuffers.length) { 956 this.mediaSource.removeSourceBuffer(this.sourceBuffer); 957 } 958 this.parent.eventSource.dispatchEvent('error'); 959 }); 960 961 if (!this.sourceBuffer.updating) { 962 this.feedNext(); 963 } 964 // TODO: cleanup every hour for live streams 965 } 966 967 destroy() { 968 this.eventSource.destroy(); 969 this.clear(); 970 this.queue = []; 971 this.mediaSource.removeSourceBuffer(this.sourceBuffer); 972 } 973 974 clear() { 975 this.queue = []; 976 let promises = []; 977 for (let i=0; i< this.sourceBuffer.buffered.length; ++i) { 978 // TODO: await remove 979 this.cleaning = true; 980 promises.push(new Promise((resolve, reject)=>{ 981 this.cleanResolvers.push(resolve); 982 if (!this.sourceBuffer.updating) { 983 this.sourceBuffer.remove(this.sourceBuffer.buffered.start(i), this.sourceBuffer.buffered.end(i)); 984 resolve(); 985 } else { 986 this.sourceBuffer.onupdateend = () => { 987 if (this.sourceBuffer) { 988 this.sourceBuffer.remove(this.sourceBuffer.buffered.start(i), this.sourceBuffer.buffered.end(i)); 989 } 990 resolve(); 991 }; 992 } 993 })); 994 } 995 return Promise.all(promises); 996 } 997 998 setLive(is_live) { 999 this.is_live = is_live; 1000 } 1001 1002 feedNext() { 1003 // Log.debug("feed next ", this.sourceBuffer.updating); 1004 if (!this.sourceBuffer.updating && !this.cleaning && this.queue.length) { 1005 this.doAppend(this.queue.shift()); 1006 // TODO: if is live and current position > 1hr => clean all and restart 1007 } 1008 } 1009 1010 doCleanup() { 1011 if (!this.cleanRanges.length) { 1012 this.cleaning = false; 1013 this.feedNext(); 1014 return; 1015 } 1016 let range = this.cleanRanges.shift(); 1017 Log$1.debug(`${this.codec} remove range [${range[0]} - ${range[1]}). 1018 \nUpdating: ${this.sourceBuffer.updating} 1019 `); 1020 this.cleaning = true; 1021 this.sourceBuffer.remove(range[0], range[1]); 1022 } 1023 1024 initCleanup() { 1025 if (this.sourceBuffer.buffered.length && !this.sourceBuffer.updating && !this.cleaning) { 1026 Log$1.debug(`${this.codec} cleanup`); 1027 let removeBound = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length-1) - 2; 1028 1029 for (let i=0; i< this.sourceBuffer.buffered.length; ++i) { 1030 let removeStart = this.sourceBuffer.buffered.start(i); 1031 let removeEnd = this.sourceBuffer.buffered.end(i); 1032 if ((this.players[0].currentTime <= removeStart) || (removeBound <= removeStart)) continue; 1033 1034 if ((removeBound <= removeEnd) && (removeBound >= removeStart)) { 1035 Log$1.debug(`Clear [${removeStart}, ${removeBound}), leave [${removeBound}, ${removeEnd}]`); 1036 removeEnd = removeBound; 1037 if (removeEnd!=removeStart) { 1038 this.cleanRanges.push([removeStart, removeEnd]); 1039 } 1040 continue; // Do not cleanup buffered range after current position 1041 } 1042 this.cleanRanges.push([removeStart, removeEnd]); 1043 } 1044 1045 this.doCleanup(); 1046 1047 // let bufferStart = this.sourceBuffer.buffered.start(0); 1048 // let removeEnd = this.sourceBuffer.buffered.start(0) + (this.sourceBuffer.buffered.end(0) - this.sourceBuffer.buffered.start(0))/2; 1049 // if (this.players[0].currentTime < removeEnd) { 1050 // this.players[0].currentTime = removeEnd; 1051 // } 1052 // let removeEnd = Math.max(this.players[0].currentTime - 3, this.sourceBuffer.buffered.end(0) - 3); 1053 // 1054 // if (removeEnd < bufferStart) { 1055 // removeEnd = this.sourceBuffer.buffered.start(0) + (this.sourceBuffer.buffered.end(0) - this.sourceBuffer.buffered.start(0))/2; 1056 // if (this.players[0].currentTime < removeEnd) { 1057 // this.players[0].currentTime = removeEnd; 1058 // } 1059 // } 1060 1061 // if (removeEnd > bufferStart && (removeEnd - bufferStart > 0.5 )) { 1062 // // try { 1063 // Log.debug(`${this.codec} remove range [${bufferStart} - ${removeEnd}). 1064 // \nBuffered end: ${this.sourceBuffer.buffered.end(0)} 1065 // \nUpdating: ${this.sourceBuffer.updating} 1066 // `); 1067 // this.cleaning = true; 1068 // this.sourceBuffer.remove(bufferStart, removeEnd); 1069 // // } catch (e) { 1070 // // // TODO: implement 1071 // // Log.error(e); 1072 // // } 1073 // } else { 1074 // this.feedNext(); 1075 // } 1076 } else { 1077 this.feedNext(); 1078 } 1079 } 1080 1081 doAppend(data) { 1082 // console.log(MP4Inspect.mp4toJSON(data)); 1083 let err = this.players[0].error; 1084 if (err) { 1085 Log$1.error(`Error occured: ${MSE.ErrorNotes[err.code]}`); 1086 try { 1087 this.players.forEach((video)=>{video.stop();}); 1088 this.mediaSource.endOfStream(); 1089 } catch (e){ 1090 1091 } 1092 this.parent.eventSource.dispatchEvent('error'); 1093 } else { 1094 try { 1095 this.sourceBuffer.appendBuffer(data); 1096 } catch (e) { 1097 if (e.name === 'QuotaExceededError') { 1098 Log$1.debug(`${this.codec} quota fail`); 1099 this.queue.unshift(data); 1100 this.initCleanup(); 1101 return; 1102 } 1103 1104 // reconnect on fail 1105 Log$1.error(`Error occured while appending buffer. ${e.name}: ${e.message}`); 1106 this.parent.eventSource.dispatchEvent('error'); 1107 } 1108 } 1109 1110 } 1111 1112 feed(data) { 1113 this.queue = this.queue.concat(data); 1114 // Log.debug(this.sourceBuffer.updating, this.updating, this.queue.length); 1115 if (this.sourceBuffer && !this.sourceBuffer.updating && !this.cleaning) { 1116 // Log.debug('enq feed'); 1117 this.feedNext(); 1118 } 1119 } 1120 } 1121 1122 class MSE { 1123 // static CODEC_AVC_BASELINE = "avc1.42E01E"; 1124 // static CODEC_AVC_MAIN = "avc1.4D401E"; 1125 // static CODEC_AVC_HIGH = "avc1.64001E"; 1126 // static CODEC_VP8 = "vp8"; 1127 // static CODEC_AAC = "mp4a.40.2"; 1128 // static CODEC_VORBIS = "vorbis"; 1129 // static CODEC_THEORA = "theora"; 1130 1131 static get ErrorNotes() {return { 1132 [MediaError.MEDIA_ERR_ABORTED]: 'fetching process aborted by user', 1133 [MediaError.MEDIA_ERR_NETWORK]: 'error occurred when downloading', 1134 [MediaError.MEDIA_ERR_DECODE]: 'error occurred when decoding', 1135 [MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED]: 'audio/video not supported' 1136 }}; 1137 1138 static isSupported(codecs) { 1139 return (window.MediaSource && window.MediaSource.isTypeSupported(`video/mp4; codecs="${codecs.join(',')}"`)); 1140 } 1141 1142 constructor (players) { 1143 this.players = players; 1144 const playing = this.players.map((video, idx) => { 1145 video.onplaying = function () { 1146 playing[idx] = true; 1147 }; 1148 video.onpause = function () { 1149 playing[idx] = false; 1150 }; 1151 return !video.paused; 1152 }); 1153 this.playing = playing; 1154 this.mediaSource = new MediaSource(); 1155 this.eventSource = new EventEmitter(this.mediaSource); 1156 this.reset(); 1157 } 1158 1159 destroy() { 1160 this.reset(); 1161 this.eventSource.destroy(); 1162 this.mediaSource = null; 1163 this.eventSource = null; 1164 } 1165 1166 play() { 1167 this.players.forEach((video, idx)=>{ 1168 if (video.paused && !this.playing[idx]) { 1169 Log$1.debug(`player ${idx}: play`); 1170 video.play(); 1171 } 1172 }); 1173 } 1174 1175 setLive(is_live) { 1176 for (let idx in this.buffers) { 1177 this.buffers[idx].setLive(is_live); 1178 } 1179 this.is_live = is_live; 1180 } 1181 1182 resetBuffers() { 1183 this.players.forEach((video, idx)=>{ 1184 if (!video.paused && this.playing[idx]) { 1185 video.pause(); 1186 video.currentTime = 0; 1187 } 1188 }); 1189 1190 let promises = []; 1191 for (let buffer of this.buffers.values()) { 1192 promises.push(buffer.clear()); 1193 } 1194 return Promise.all(promises).then(()=>{ 1195 this.mediaSource.endOfStream(); 1196 this.mediaSource.duration = 0; 1197 this.mediaSource.clearLiveSeekableRange(); 1198 this.play(); 1199 }); 1200 } 1201 1202 clear() { 1203 this.reset(); 1204 this.players.forEach((video)=>{video.src = URL.createObjectURL(this.mediaSource);}); 1205 1206 return this.setupEvents(); 1207 } 1208 1209 setupEvents() { 1210 this.eventSource.clear(); 1211 this.resolved = false; 1212 this.mediaReady = new Promise((resolve, reject)=> { 1213 this._sourceOpen = ()=> { 1214 Log$1.debug(`Media source opened: ${this.mediaSource.readyState}`); 1215 if (!this.resolved) { 1216 this.resolved = true; 1217 resolve(); 1218 } 1219 }; 1220 this._sourceEnded = ()=>{ 1221 Log$1.debug(`Media source ended: ${this.mediaSource.readyState}`); 1222 }; 1223 this._sourceClose = ()=>{ 1224 Log$1.debug(`Media source closed: ${this.mediaSource.readyState}`); 1225 if (this.resolved) { 1226 this.eventSource.dispatchEvent('sourceclosed'); 1227 } 1228 }; 1229 this.eventSource.addEventListener('sourceopen', this._sourceOpen); 1230 this.eventSource.addEventListener('sourceended', this._sourceEnded); 1231 this.eventSource.addEventListener('sourceclose', this._sourceClose); 1232 }); 1233 return this.mediaReady; 1234 } 1235 1236 reset() { 1237 this.ready = false; 1238 for (let track in this.buffers) { 1239 this.buffers[track].destroy(); 1240 delete this.buffers[track]; 1241 } 1242 if (this.mediaSource.readyState == 'open') { 1243 this.mediaSource.duration = 0; 1244 this.mediaSource.endOfStream(); 1245 } 1246 this.updating = false; 1247 this.resolved = false; 1248 this.buffers = {}; 1249 // this.players.forEach((video)=>{video.src = URL.createObjectURL(this.mediaSource)}); 1250 // TODO: remove event listeners for existing media source 1251 // this.setupEvents(); 1252 // this.clear(); 1253 } 1254 1255 setCodec(track, mimeCodec) { 1256 return this.mediaReady.then(()=>{ 1257 this.buffers[track] = new MSEBuffer(this, mimeCodec); 1258 this.buffers[track].setLive(this.is_live); 1259 }); 1260 } 1261 1262 feed(track, data) { 1263 if (this.buffers[track]) { 1264 this.buffers[track].feed(data); 1265 } 1266 } 1267 } 1268 1269 const Log$2 = getTagged('remuxer:base'); 1270 let track_id = 1; 1271 class BaseRemuxer { 1272 1273 static get MP4_TIMESCALE() { return 90000;} 1274 1275 // TODO: move to ts parser 1276 // static PTSNormalize(value, reference) { 1277 // 1278 // let offset; 1279 // if (reference === undefined) { 1280 // return value; 1281 // } 1282 // if (reference < value) { 1283 // // - 2^33 1284 // offset = -8589934592; 1285 // } else { 1286 // // + 2^33 1287 // offset = 8589934592; 1288 // } 1289 // /* PTS is 33bit (from 0 to 2^33 -1) 1290 // if diff between value and reference is bigger than half of the amplitude (2^32) then it means that 1291 // PTS looping occured. fill the gap */ 1292 // while (Math.abs(value - reference) > 4294967296) { 1293 // value += offset; 1294 // } 1295 // return value; 1296 // } 1297 1298 static getTrackID() { 1299 return track_id++; 1300 } 1301 1302 constructor(timescale, scaleFactor, params) { 1303 this.timeOffset = 0; 1304 this.timescale = timescale; 1305 this.scaleFactor = scaleFactor; 1306 this.readyToDecode = false; 1307 this.samples = []; 1308 this.seq = 1; 1309 this.tsAlign = 1; 1310 } 1311 1312 scaled(timestamp) { 1313 return timestamp / this.scaleFactor; 1314 } 1315 1316 unscaled(timestamp) { 1317 return timestamp * this.scaleFactor; 1318 } 1319 1320 remux(unit) { 1321 if (unit) { 1322 this.samples.push({ 1323 unit: unit, 1324 pts: unit.pts, 1325 dts: unit.dts 1326 }); 1327 return true; 1328 } 1329 return false; 1330 } 1331 1332 static toMS(timestamp) { 1333 return timestamp/90; 1334 } 1335 1336 setConfig(config) { 1337 1338 } 1339 1340 insertDscontinuity() { 1341 this.samples.push(null); 1342 } 1343 1344 init(initPTS, initDTS, shouldInitialize=true) { 1345 this.initPTS = Math.min(initPTS, this.samples[0].dts /*- this.unscaled(this.timeOffset)*/); 1346 this.initDTS = Math.min(initDTS, this.samples[0].dts /*- this.unscaled(this.timeOffset)*/); 1347 Log$2.debug(`Initial pts=${this.initPTS} dts=${this.initDTS} offset=${this.unscaled(this.timeOffset)}`); 1348 this.initialized = shouldInitialize; 1349 } 1350 1351 flush() { 1352 this.seq++; 1353 this.mp4track.len = 0; 1354 this.mp4track.samples = []; 1355 } 1356 1357 static dtsSortFunc(a,b) { 1358 return (a.dts-b.dts); 1359 } 1360 1361 static groupByDts(gop) { 1362 const groupBy = (xs, key) => { 1363 return xs.reduce((rv, x) => { 1364 (rv[x[key]] = rv[x[key]] || []).push(x); 1365 return rv; 1366 }, {}); 1367 }; 1368 return groupBy(gop, 'dts'); 1369 } 1370 1371 getPayloadBase(sampleFunction, setupSample) { 1372 if (!this.readyToDecode || !this.initialized || !this.samples.length) return null; 1373 this.samples.sort(BaseRemuxer.dtsSortFunc); 1374 return true; 1375 // 1376 // let payload = new Uint8Array(this.mp4track.len); 1377 // let offset = 0; 1378 // let samples=this.mp4track.samples; 1379 // let mp4Sample, lastDTS, pts, dts; 1380 // 1381 // while (this.samples.length) { 1382 // let sample = this.samples.shift(); 1383 // if (sample === null) { 1384 // // discontinuity 1385 // this.nextDts = undefined; 1386 // break; 1387 // } 1388 // 1389 // let unit = sample.unit; 1390 // 1391 // pts = Math.round((sample.pts - this.initDTS)/this.tsAlign)*this.tsAlign; 1392 // dts = Math.round((sample.dts - this.initDTS)/this.tsAlign)*this.tsAlign; 1393 // // ensure DTS is not bigger than PTS 1394 // dts = Math.min(pts, dts); 1395 // 1396 // // sampleFunction(pts, dts); // TODO: 1397 // 1398 // // mp4Sample = setupSample(unit, pts, dts); // TODO: 1399 // 1400 // payload.set(unit.getData(), offset); 1401 // offset += unit.getSize(); 1402 // 1403 // samples.push(mp4Sample); 1404 // lastDTS = dts; 1405 // } 1406 // if (!samples.length) return null; 1407 // 1408 // // samplesPostFunction(samples); // TODO: 1409 // 1410 // return new Uint8Array(payload.buffer, 0, this.mp4track.len); 1411 } 1412 } 1413 1414 const Log$3 = getTagged("remuxer:aac"); 1415 // TODO: asm.js 1416 class AACRemuxer extends BaseRemuxer { 1417 1418 constructor(timescale, scaleFactor = 1, params={}) { 1419 super(timescale, scaleFactor); 1420 1421 this.codecstring=MSE.CODEC_AAC; 1422 this.units = []; 1423 this.initDTS = undefined; 1424 this.nextAacPts = undefined; 1425 this.lastPts = 0; 1426 this.firstDTS = 0; 1427 this.firstPTS = 0; 1428 this.duration = params.duration || 1; 1429 this.initialized = false; 1430 1431 this.mp4track={ 1432 id:BaseRemuxer.getTrackID(), 1433 type: 'audio', 1434 fragmented:true, 1435 channelCount:0, 1436 audiosamplerate: this.timescale, 1437 duration: 0, 1438 timescale: this.timescale, 1439 volume: 1, 1440 samples: [], 1441 config: '', 1442 len: 0 1443 }; 1444 if (params.config) { 1445 this.setConfig(params.config); 1446 } 1447 } 1448 1449 setConfig(config) { 1450 this.mp4track.channelCount = config.channels; 1451 this.mp4track.audiosamplerate = config.samplerate; 1452 if (!this.mp4track.duration) { 1453 this.mp4track.duration = (this.duration?this.duration:1)*config.samplerate; 1454 } 1455 this.mp4track.timescale = config.samplerate; 1456 this.mp4track.config = config.config; 1457 this.mp4track.codec = config.codec; 1458 this.timescale = config.samplerate; 1459 this.scaleFactor = BaseRemuxer.MP4_TIMESCALE / config.samplerate; 1460 this.expectedSampleDuration = 1024 * this.scaleFactor; 1461 this.readyToDecode = true; 1462 } 1463 1464 remux(aac) { 1465 if (super.remux.call(this, aac)) { 1466 this.mp4track.len += aac.getSize(); 1467 } 1468 } 1469 1470 getPayload() { 1471 if (!this.readyToDecode || !this.samples.length) return null; 1472 this.samples.sort(function(a, b) { 1473 return (a.dts-b.dts); 1474 }); 1475 1476 let payload = new Uint8Array(this.mp4track.len); 1477 let offset = 0; 1478 let samples=this.mp4track.samples; 1479 let mp4Sample, lastDTS, pts, dts; 1480 1481 while (this.samples.length) { 1482 let sample = this.samples.shift(); 1483 if (sample === null) { 1484 // discontinuity 1485 this.nextDts = undefined; 1486 break; 1487 } 1488 let unit = sample.unit; 1489 pts = sample.pts - this.initDTS; 1490 dts = sample.dts - this.initDTS; 1491 1492 if (lastDTS === undefined) { 1493 if (this.nextDts) { 1494 let delta = Math.round(this.scaled(pts - this.nextAacPts)); 1495 // if fragment are contiguous, or delta less than 600ms, ensure there is no overlap/hole between fragments 1496 if (/*contiguous || */Math.abs(delta) < 600) { 1497 // log delta 1498 if (delta) { 1499 if (delta > 0) { 1500 Log$3.log(`${delta} ms hole between AAC samples detected,filling it`); 1501 // if we have frame overlap, overlapping for more than half a frame duraion 1502 } else if (delta < -12) { 1503 // drop overlapping audio frames... browser will deal with it 1504 Log$3.log(`${(-delta)} ms overlapping between AAC samples detected, drop frame`); 1505 this.mp4track.len -= unit.getSize(); 1506 continue; 1507 } 1508 // set DTS to next DTS 1509 pts = dts = this.nextAacPts; 1510 } 1511 } 1512 } 1513 // remember first PTS of our aacSamples, ensure value is positive 1514 this.firstDTS = Math.max(0, dts); 1515 } 1516 1517 mp4Sample = { 1518 size: unit.getSize(), 1519 cts: 0, 1520 duration:1024, 1521 flags: { 1522 isLeading: 0, 1523 isDependedOn: 0, 1524 hasRedundancy: 0, 1525 degradPrio: 0, 1526 dependsOn: 1 1527 } 1528 }; 1529 1530 payload.set(unit.getData(), offset); 1531 offset += unit.getSize(); 1532 samples.push(mp4Sample); 1533 lastDTS = dts; 1534 } 1535 if (!samples.length) return null; 1536 this.nextDts =pts+this.expectedSampleDuration; 1537 return new Uint8Array(payload.buffer, 0, this.mp4track.len); 1538 } 1539 } 1540 //test.bundle.js:42 [remuxer:h264] skip frame from the past at DTS=18397972271140676 with expected DTS=18397998040950484 1541 1542 /** 1543 * Parser for exponential Golomb codes, a variable-bitwidth number encoding scheme used by h264. 1544 */ 1545 1546 class ExpGolomb { 1547 1548 constructor(data) { 1549 this.data = data; 1550 // the number of bytes left to examine in this.data 1551 this.bytesAvailable = this.data.byteLength; 1552 // the current word being examined 1553 this.word = 0; // :uint 1554 // the number of bits left to examine in the current word 1555 this.bitsAvailable = 0; // :uint 1556 } 1557 1558 // ():void 1559 loadWord() { 1560 var 1561 position = this.data.byteLength - this.bytesAvailable, 1562 workingBytes = new Uint8Array(4), 1563 availableBytes = Math.min(4, this.bytesAvailable); 1564 if (availableBytes === 0) { 1565 throw new Error('no bytes available'); 1566 } 1567 workingBytes.set(this.data.subarray(position, position + availableBytes)); 1568 this.word = new DataView(workingBytes.buffer, workingBytes.byteOffset, workingBytes.byteLength).getUint32(0); 1569 // track the amount of this.data that has been processed 1570 this.bitsAvailable = availableBytes * 8; 1571 this.bytesAvailable -= availableBytes; 1572 } 1573 1574 // (count:int):void 1575 skipBits(count) { 1576 var skipBytes; // :int 1577 if (this.bitsAvailable > count) { 1578 this.word <<= count; 1579 this.bitsAvailable -= count; 1580 } else { 1581 count -= this.bitsAvailable; 1582 skipBytes = count >> 3; 1583 count -= (skipBytes << 3); 1584 this.bytesAvailable -= skipBytes; 1585 this.loadWord(); 1586 this.word <<= count; 1587 this.bitsAvailable -= count; 1588 } 1589 } 1590 1591 // (size:int):uint 1592 readBits(size) { 1593 var 1594 bits = Math.min(this.bitsAvailable, size), // :uint 1595 valu = this.word >>> (32 - bits); // :uint 1596 if (size > 32) { 1597 Log.error('Cannot read more than 32 bits at a time'); 1598 } 1599 this.bitsAvailable -= bits; 1600 if (this.bitsAvailable > 0) { 1601 this.word <<= bits; 1602 } else if (this.bytesAvailable > 0) { 1603 this.loadWord(); 1604 } 1605 bits = size - bits; 1606 if (bits > 0) { 1607 return valu << bits | this.readBits(bits); 1608 } else { 1609 return valu; 1610 } 1611 } 1612 1613 // ():uint 1614 skipLZ() { 1615 var leadingZeroCount; // :uint 1616 for (leadingZeroCount = 0; leadingZeroCount < this.bitsAvailable; ++leadingZeroCount) { 1617 if (0 !== (this.word & (0x80000000 >>> leadingZeroCount))) { 1618 // the first bit of working word is 1 1619 this.word <<= leadingZeroCount; 1620 this.bitsAvailable -= leadingZeroCount; 1621 return leadingZeroCount; 1622 } 1623 } 1624 // we exhausted word and still have not found a 1 1625 this.loadWord(); 1626 return leadingZeroCount + this.skipLZ(); 1627 } 1628 1629 // ():void 1630 skipUEG() { 1631 this.skipBits(1 + this.skipLZ()); 1632 } 1633 1634 // ():void 1635 skipEG() { 1636 this.skipBits(1 + this.skipLZ()); 1637 } 1638 1639 // ():uint 1640 readUEG() { 1641 var clz = this.skipLZ(); // :uint 1642 return this.readBits(clz + 1) - 1; 1643 } 1644 1645 // ():int 1646 readEG() { 1647 var valu = this.readUEG(); // :int 1648 if (0x01 & valu) { 1649 // the number is odd if the low order bit is set 1650 return (1 + valu) >>> 1; // add 1 to make it even, and divide by 2 1651 } else { 1652 return -1 * (valu >>> 1); // divide by two then make it negative 1653 } 1654 } 1655 1656 // Some convenience functions 1657 // :Boolean 1658 readBoolean() { 1659 return 1 === this.readBits(1); 1660 } 1661 1662 // ():int 1663 readUByte() { 1664 return this.readBits(8); 1665 } 1666 1667 // ():int 1668 readUShort() { 1669 return this.readBits(16); 1670 } 1671 // ():int 1672 readUInt() { 1673 return this.readBits(32); 1674 } 1675 } 1676 1677 // TODO: asm.js 1678 1679 function appendByteArray(buffer1, buffer2) { 1680 let tmp = new Uint8Array((buffer1.byteLength|0) + (buffer2.byteLength|0)); 1681 tmp.set(buffer1, 0); 1682 tmp.set(buffer2, buffer1.byteLength|0); 1683 return tmp; 1684 } 1685 function base64ToArrayBuffer(base64) { 1686 var binary_string = window.atob(base64); 1687 var len = binary_string.length; 1688 var bytes = new Uint8Array( len ); 1689 for (var i = 0; i < len; i++) { 1690 bytes[i] = binary_string.charCodeAt(i); 1691 } 1692 return bytes.buffer; 1693 } 1694 1695 function hexToByteArray(hex) { 1696 let len = hex.length >> 1; 1697 var bufView = new Uint8Array(len); 1698 for (var i = 0; i < len; i++) { 1699 bufView[i] = parseInt(hex.substr(i<<1,2),16); 1700 } 1701 return bufView; 1702 } 1703 1704 function bitSlice(bytearray, start=0, end=bytearray.byteLength*8) { 1705 let byteLen = Math.ceil((end-start)/8); 1706 let res = new Uint8Array(byteLen); 1707 let startByte = start >>> 3; // /8 1708 let endByte = (end>>>3) - 1; // /8 1709 let bitOffset = start & 0x7; // %8 1710 let nBitOffset = 8 - bitOffset; 1711 let endOffset = 8 - end & 0x7; // %8 1712 for (let i=0; i<byteLen; ++i) { 1713 let tail = 0; 1714 if (i<endByte) { 1715 tail = bytearray[startByte+i+1] >> nBitOffset; 1716 if (i == endByte-1 && endOffset < 8) { 1717 tail >>= endOffset; 1718 tail <<= endOffset; 1719 } 1720 } 1721 res[i]=(bytearray[startByte+i]<<bitOffset) | tail; 1722 } 1723 return res; 1724 } 1725 1726 class BitArray { 1727 1728 constructor(src) { 1729 this.src = new DataView(src.buffer, src.byteOffset, src.byteLength); 1730 this.bitpos = 0; 1731 this.byte = this.src.getUint8(0); /* This should really be undefined, uint wont allow it though */ 1732 this.bytepos = 0; 1733 } 1734 1735 readBits(length) { 1736 if (32 < (length|0) || 0 === (length|0)) { 1737 /* To big for an uint */ 1738 throw new Error("too big"); 1739 } 1740 1741 let result = 0; 1742 for (let i = length; i > 0; --i) { 1743 1744 /* Shift result one left to make room for another bit, 1745 then add the next bit on the stream. */ 1746 result = ((result|0) << 1) | (((this.byte|0) >> (8 - (++this.bitpos))) & 0x01); 1747 if ((this.bitpos|0)>=8) { 1748 this.byte = this.src.getUint8(++this.bytepos); 1749 this.bitpos &= 0x7; 1750 } 1751 } 1752 1753 return result; 1754 } 1755 skipBits(length) { 1756 this.bitpos += (length|0) & 0x7; // %8 1757 this.bytepos += (length|0) >>> 3; // *8 1758 if (this.bitpos > 7) { 1759 this.bitpos &= 0x7; 1760 ++this.bytepos; 1761 } 1762 1763 if (!this.finished()) { 1764 this.byte = this.src.getUint8(this.bytepos); 1765 return 0; 1766 } else { 1767 return this.bytepos-this.src.byteLength-this.src.bitpos; 1768 } 1769 } 1770 1771 finished() { 1772 return this.bytepos >= this.src.byteLength; 1773 } 1774 } 1775 1776 class NALU { 1777 1778 static get NDR() {return 1;} 1779 static get SLICE_PART_A() {return 2;} 1780 static get SLICE_PART_B() {return 3;} 1781 static get SLICE_PART_C() {return 4;} 1782 static get IDR() {return 5;} 1783 static get SEI() {return 6;} 1784 static get SPS() {return 7;} 1785 static get PPS() {return 8;} 1786 static get DELIMITER() {return 9;} 1787 static get EOSEQ() {return 10;} 1788 static get EOSTR() {return 11;} 1789 static get FILTER() {return 12;} 1790 static get STAP_A() {return 24;} 1791 static get STAP_B() {return 25;} 1792 static get FU_A() {return 28;} 1793 static get FU_B() {return 29;} 1794 1795 static get TYPES() {return { 1796 [NALU.IDR]: 'IDR', 1797 [NALU.SEI]: 'SEI', 1798 [NALU.SPS]: 'SPS', 1799 [NALU.PPS]: 'PPS', 1800 [NALU.NDR]: 'NDR' 1801 }}; 1802 1803 static type(nalu) { 1804 if (nalu.ntype in NALU.TYPES) { 1805 return NALU.TYPES[nalu.ntype]; 1806 } else { 1807 return 'UNKNOWN'; 1808 } 1809 } 1810 1811 constructor(ntype, nri, data, dts, pts) { 1812 1813 this.data = data; 1814 this.ntype = ntype; 1815 this.nri = nri; 1816 this.dts = dts; 1817 this.pts = pts ? pts : this.dts; 1818 this.sliceType = null; 1819 } 1820 1821 appendData(idata) { 1822 this.data = appendByteArray(this.data, idata); 1823 } 1824 1825 toString() { 1826 return `${NALU.type(this)}(${this.data.byteLength}): NRI: ${this.getNri()}, PTS: ${this.pts}, DTS: ${this.dts}`; 1827 } 1828 1829 getNri() { 1830 return this.nri >> 5; 1831 } 1832 1833 type() { 1834 return this.ntype; 1835 } 1836 1837 isKeyframe() { 1838 return this.ntype === NALU.IDR || this.sliceType === 7; 1839 } 1840 1841 getSize() { 1842 return 4 + 1 + this.data.byteLength; 1843 } 1844 1845 getData() { 1846 let header = new Uint8Array(5 + this.data.byteLength); 1847 let view = new DataView(header.buffer); 1848 view.setUint32(0, this.data.byteLength + 1); 1849 view.setUint8(4, (0x0 & 0x80) | (this.nri & 0x60) | (this.ntype & 0x1F)); 1850 header.set(this.data, 5); 1851 return header; 1852 } 1853 } 1854 1855 class H264Parser { 1856 1857 constructor(remuxer) { 1858 this.remuxer = remuxer; 1859 this.track = remuxer.mp4track; 1860 this.firstFound = false; 1861 } 1862 1863 msToScaled(timestamp) { 1864 return (timestamp - this.remuxer.timeOffset) * this.remuxer.scaleFactor; 1865 } 1866 1867 parseSPS(sps) { 1868 var config = H264Parser.readSPS(new Uint8Array(sps)); 1869 1870 this.track.width = config.width; 1871 this.track.height = config.height; 1872 this.track.sps = [new Uint8Array(sps)]; 1873 // this.track.timescale = this.remuxer.timescale; 1874 // this.track.duration = this.remuxer.timescale; // TODO: extract duration for non-live client 1875 this.track.codec = 'avc1.'; 1876 1877 let codecarray = new DataView(sps.buffer, sps.byteOffset+1, 4); 1878 for (let i = 0; i < 3; ++i) { 1879 var h = codecarray.getUint8(i).toString(16); 1880 if (h.length < 2) { 1881 h = '0' + h; 1882 } 1883 this.track.codec += h; 1884 } 1885 } 1886 1887 parsePPS(pps) { 1888 this.track.pps = [new Uint8Array(pps)]; 1889 } 1890 1891 parseNAL(unit) { 1892 if (!unit) return false; 1893 1894 let push = null; 1895 // console.log(unit.toString()); 1896 switch (unit.type()) { 1897 case NALU.NDR: 1898 case NALU.IDR: 1899 unit.sliceType = H264Parser.parceSliceHeader(unit.data); 1900 if (unit.isKeyframe() && !this.firstFound) { 1901 this.firstFound = true; 1902 } 1903 if (this.firstFound) { 1904 push = true; 1905 } else { 1906 push = false; 1907 } 1908 break; 1909 case NALU.PPS: 1910 push = false; 1911 if (!this.track.pps) { 1912 this.parsePPS(unit.getData().subarray(4)); 1913 if (!this.remuxer.readyToDecode && this.track.pps && this.track.sps) { 1914 this.remuxer.readyToDecode = true; 1915 } 1916 } 1917 break; 1918 case NALU.SPS: 1919 push = false; 1920 if(!this.track.sps) { 1921 this.parseSPS(unit.getData().subarray(4)); 1922 if (!this.remuxer.readyToDecode && this.track.pps && this.track.sps) { 1923 this.remuxer.readyToDecode = true; 1924 } 1925 } 1926 break; 1927 case NALU.SEI: 1928 push = false; 1929 let data = new DataView(unit.data.buffer, unit.data.byteOffset, unit.data.byteLength); 1930 let byte_idx = 0; 1931 let pay_type = data.getUint8(byte_idx); 1932 ++byte_idx; 1933 let pay_size = 0; 1934 let sz = data.getUint8(byte_idx); 1935 ++byte_idx; 1936 while (sz === 255) { 1937 pay_size+=sz; 1938 sz = data.getUint8(byte_idx); 1939 ++byte_idx; 1940 } 1941 pay_size+=sz; 1942 1943 let uuid = unit.data.subarray(byte_idx, byte_idx+16); 1944 byte_idx+=16; 1945 console.log(`PT: ${pay_type}, PS: ${pay_size}, UUID: ${Array.from(uuid).map(function(i) { 1946 return ('0' + i.toString(16)).slice(-2); 1947 }).join('')}`); 1948 // debugger; 1949 break; 1950 case NALU.EOSEQ: 1951 case NALU.EOSTR: 1952 push = false; 1953 default: 1954 } 1955 if (push === null && unit.getNri() > 0 ) { 1956 push=true; 1957 } 1958 return push; 1959 } 1960 1961 static parceSliceHeader(data) { 1962 let decoder = new ExpGolomb(data); 1963 let first_mb = decoder.readUEG(); 1964 let slice_type = decoder.readUEG(); 1965 let ppsid = decoder.readUEG(); 1966 let frame_num = decoder.readUByte(); 1967 // console.log(`first_mb: ${first_mb}, slice_type: ${slice_type}, ppsid: ${ppsid}, frame_num: ${frame_num}`); 1968 return slice_type; 1969 } 1970 1971 /** 1972 * Advance the ExpGolomb decoder past a scaling list. The scaling 1973 * list is optionally transmitted as part of a sequence parameter 1974 * set and is not relevant to transmuxing. 1975 * @param decoder {ExpGolomb} exp golomb decoder 1976 * @param count {number} the number of entries in this scaling list 1977 * @see Recommendation ITU-T H.264, Section 7.3.2.1.1.1 1978 */ 1979 static skipScalingList(decoder, count) { 1980 let lastScale = 8, 1981 nextScale = 8, 1982 deltaScale; 1983 for (let j = 0; j < count; j++) { 1984 if (nextScale !== 0) { 1985 deltaScale = decoder.readEG(); 1986 nextScale = (lastScale + deltaScale + 256) % 256; 1987 } 1988 lastScale = (nextScale === 0) ? lastScale : nextScale; 1989 } 1990 } 1991 1992 /** 1993 * Read a sequence parameter set and return some interesting video 1994 * properties. A sequence parameter set is the H264 metadata that 1995 * describes the properties of upcoming video frames. 1996 * @param data {Uint8Array} the bytes of a sequence parameter set 1997 * @return {object} an object with configuration parsed from the 1998 * sequence parameter set, including the dimensions of the 1999 * associated video frames. 2000 */ 2001 static readSPS(data) { 2002 let decoder = new ExpGolomb(data); 2003 let frameCropLeftOffset = 0, 2004 frameCropRightOffset = 0, 2005 frameCropTopOffset = 0, 2006 frameCropBottomOffset = 0, 2007 sarScale = 1, 2008 profileIdc,profileCompat,levelIdc, 2009 numRefFramesInPicOrderCntCycle, picWidthInMbsMinus1, 2010 picHeightInMapUnitsMinus1, 2011 frameMbsOnlyFlag, 2012 scalingListCount; 2013 decoder.readUByte(); 2014 profileIdc = decoder.readUByte(); // profile_idc 2015 profileCompat = decoder.readBits(5); // constraint_set[0-4]_flag, u(5) 2016 decoder.skipBits(3); // reserved_zero_3bits u(3), 2017 levelIdc = decoder.readUByte(); //level_idc u(8) 2018 decoder.skipUEG(); // seq_parameter_set_id 2019 // some profiles have more optional data we don't need 2020 if (profileIdc === 100 || 2021 profileIdc === 110 || 2022 profileIdc === 122 || 2023 profileIdc === 244 || 2024 profileIdc === 44 || 2025 profileIdc === 83 || 2026 profileIdc === 86 || 2027 profileIdc === 118 || 2028 profileIdc === 128) { 2029 var chromaFormatIdc = decoder.readUEG(); 2030 if (chromaFormatIdc === 3) { 2031 decoder.skipBits(1); // separate_colour_plane_flag 2032 } 2033 decoder.skipUEG(); // bit_depth_luma_minus8 2034 decoder.skipUEG(); // bit_depth_chroma_minus8 2035 decoder.skipBits(1); // qpprime_y_zero_transform_bypass_flag 2036 if (decoder.readBoolean()) { // seq_scaling_matrix_present_flag 2037 scalingListCount = (chromaFormatIdc !== 3) ? 8 : 12; 2038 for (let i = 0; i < scalingListCount; ++i) { 2039 if (decoder.readBoolean()) { // seq_scaling_list_present_flag[ i ] 2040 if (i < 6) { 2041 H264Parser.skipScalingList(decoder, 16); 2042 } else { 2043 H264Parser.skipScalingList(decoder, 64); 2044 } 2045 } 2046 } 2047 } 2048 } 2049 decoder.skipUEG(); // log2_max_frame_num_minus4 2050 var picOrderCntType = decoder.readUEG(); 2051 if (picOrderCntType === 0) { 2052 decoder.readUEG(); //log2_max_pic_order_cnt_lsb_minus4 2053 } else if (picOrderCntType === 1) { 2054 decoder.skipBits(1); // delta_pic_order_always_zero_flag 2055 decoder.skipEG(); // offset_for_non_ref_pic 2056 decoder.skipEG(); // offset_for_top_to_bottom_field 2057 numRefFramesInPicOrderCntCycle = decoder.readUEG(); 2058 for(let i = 0; i < numRefFramesInPicOrderCntCycle; ++i) { 2059 decoder.skipEG(); // offset_for_ref_frame[ i ] 2060 } 2061 } 2062 decoder.skipUEG(); // max_num_ref_frames 2063 decoder.skipBits(1); // gaps_in_frame_num_value_allowed_flag 2064 picWidthInMbsMinus1 = decoder.readUEG(); 2065 picHeightInMapUnitsMinus1 = decoder.readUEG(); 2066 frameMbsOnlyFlag = decoder.readBits(1); 2067 if (frameMbsOnlyFlag === 0) { 2068 decoder.skipBits(1); // mb_adaptive_frame_field_flag 2069 } 2070 decoder.skipBits(1); // direct_8x8_inference_flag 2071 if (decoder.readBoolean()) { // frame_cropping_flag 2072 frameCropLeftOffset = decoder.readUEG(); 2073 frameCropRightOffset = decoder.readUEG(); 2074 frameCropTopOffset = decoder.readUEG(); 2075 frameCropBottomOffset = decoder.readUEG(); 2076 } 2077 if (decoder.readBoolean()) { 2078 // vui_parameters_present_flag 2079 if (decoder.readBoolean()) { 2080 // aspect_ratio_info_present_flag 2081 let sarRatio; 2082 const aspectRatioIdc = decoder.readUByte(); 2083 switch (aspectRatioIdc) { 2084 case 1: sarRatio = [1,1]; break; 2085 case 2: sarRatio = [12,11]; break; 2086 case 3: sarRatio = [10,11]; break; 2087 case 4: sarRatio = [16,11]; break; 2088 case 5: sarRatio = [40,33]; break; 2089 case 6: sarRatio = [24,11]; break; 2090 case 7: sarRatio = [20,11]; break; 2091 case 8: sarRatio = [32,11]; break; 2092 case 9: sarRatio = [80,33]; break; 2093 case 10: sarRatio = [18,11]; break; 2094 case 11: sarRatio = [15,11]; break; 2095 case 12: sarRatio = [64,33]; break; 2096 case 13: sarRatio = [160,99]; break; 2097 case 14: sarRatio = [4,3]; break; 2098 case 15: sarRatio = [3,2]; break; 2099 case 16: sarRatio = [2,1]; break; 2100 case 255: { 2101 sarRatio = [decoder.readUByte() << 8 | decoder.readUByte(), decoder.readUByte() << 8 | decoder.readUByte()]; 2102 break; 2103 } 2104 } 2105 if (sarRatio) { 2106 sarScale = sarRatio[0] / sarRatio[1]; 2107 } 2108 } 2109 if (decoder.readBoolean()) {decoder.skipBits(1);} 2110 2111 if (decoder.readBoolean()) { 2112 decoder.skipBits(4); 2113 if (decoder.readBoolean()) { 2114 decoder.skipBits(24); 2115 } 2116 } 2117 if (decoder.readBoolean()) { 2118 decoder.skipUEG(); 2119 decoder.skipUEG(); 2120 } 2121 if (decoder.readBoolean()) { 2122 let unitsInTick = decoder.readUInt(); 2123 let timeScale = decoder.readUInt(); 2124 let fixedFrameRate = decoder.readBoolean(); 2125 let frameDuration = timeScale/(2*unitsInTick); 2126 console.log(`timescale: ${timeScale}; unitsInTick: ${unitsInTick}; fixedFramerate: ${fixedFrameRate}; avgFrameDuration: ${frameDuration}`); 2127 } 2128 } 2129 return { 2130 width: Math.ceil((((picWidthInMbsMinus1 + 1) * 16) - frameCropLeftOffset * 2 - frameCropRightOffset * 2) * sarScale), 2131 height: ((2 - frameMbsOnlyFlag) * (picHeightInMapUnitsMinus1 + 1) * 16) - ((frameMbsOnlyFlag? 2 : 4) * (frameCropTopOffset + frameCropBottomOffset)) 2132 }; 2133 } 2134 2135 static readSliceType(decoder) { 2136 // skip NALu type 2137 decoder.readUByte(); 2138 // discard first_mb_in_slice 2139 decoder.readUEG(); 2140 // return slice_type 2141 return decoder.readUEG(); 2142 } 2143 } 2144 2145 const Log$4 = getTagged("remuxer:h264"); 2146 // TODO: asm.js 2147 class H264Remuxer extends BaseRemuxer { 2148 2149 constructor(timescale, scaleFactor=1, params={}) { 2150 super(timescale, scaleFactor); 2151 2152 this.nextDts = undefined; 2153 this.readyToDecode = false; 2154 this.initialized = false; 2155 2156 this.firstDTS=0; 2157 this.firstPTS=0; 2158 this.lastDTS=undefined; 2159 this.lastSampleDuration = 0; 2160 this.lastDurations = []; 2161 // this.timescale = 90000; 2162 this.tsAlign = Math.round(this.timescale/60); 2163 2164 this.mp4track={ 2165 id:BaseRemuxer.getTrackID(), 2166 type: 'video', 2167 len:0, 2168 fragmented:true, 2169 sps:'', 2170 pps:'', 2171 width:0, 2172 height:0, 2173 timescale: timescale, 2174 duration: timescale, 2175 samples: [] 2176 }; 2177 this.samples = []; 2178 this.lastGopDTS = -99999999999999; 2179 this.gop=[]; 2180 this.firstUnit = true; 2181 2182 this.h264 = new H264Parser(this); 2183 2184 if (params.sps) { 2185 let arr = new Uint8Array(params.sps); 2186 if ((arr[0] & 0x1f) === 7) { 2187 this.setSPS(arr); 2188 } else { 2189 Log$4.warn("bad SPS in SDP"); 2190 } 2191 } 2192 if (params.pps) { 2193 let arr = new Uint8Array(params.pps); 2194 if ((arr[0] & 0x1f) === 8) { 2195 this.setPPS(arr); 2196 } else { 2197 Log$4.warn("bad PPS in SDP"); 2198 } 2199 } 2200 2201 if (this.mp4track.pps && this.mp4track.sps) { 2202 this.readyToDecode = true; 2203 } 2204 } 2205 2206 _scaled(timestamp) { 2207 return timestamp >>> this.scaleFactor; 2208 } 2209 2210 _unscaled(timestamp) { 2211 return timestamp << this.scaleFactor; 2212 } 2213 2214 setSPS(sps) { 2215 this.h264.parseSPS(sps); 2216 } 2217 2218 setPPS(pps) { 2219 this.h264.parsePPS(pps); 2220 } 2221 2222 remux(nalu) { 2223 if (this.lastGopDTS < nalu.dts) { 2224 this.gop.sort(BaseRemuxer.dtsSortFunc); 2225 2226 if (this.gop.length > 1) { 2227 // Aggregate multi-slices which belong to one frame 2228 const groupedGop = BaseRemuxer.groupByDts(this.gop); 2229 this.gop = Object.values(groupedGop).map(group => { 2230 return group.reduce((preUnit, curUnit) => { 2231 const naluData = curUnit.getData(); 2232 naluData.set(new Uint8Array([0x0, 0x0, 0x0, 0x1])); 2233 preUnit.appendData(naluData); 2234 return preUnit; 2235 }); 2236 }); 2237 } 2238 2239 for (let unit of this.gop) { 2240 // if (this.firstUnit) { 2241 // unit.ntype = 5;//NALU.IDR; 2242 // this.firstUnit = false; 2243 // } 2244 if (super.remux.call(this, unit)) { 2245 this.mp4track.len += unit.getSize(); 2246 } 2247 } 2248 this.gop = []; 2249 this.lastGopDTS = nalu.dts; 2250 } 2251 if (this.h264.parseNAL(nalu)) { 2252 this.gop.push(nalu); 2253 } 2254 } 2255 2256 getPayload() { 2257 if (!this.getPayloadBase()) { 2258 return null; 2259 } 2260 2261 let payload = new Uint8Array(this.mp4track.len); 2262 let offset = 0; 2263 let samples=this.mp4track.samples; 2264 let mp4Sample, lastDTS, pts, dts; 2265 2266 2267 // Log.debug(this.samples.map((e)=>{ 2268 // return Math.round((e.dts - this.initDTS)); 2269 // })); 2270 2271 // let minDuration = Number.MAX_SAFE_INTEGER; 2272 while (this.samples.length) { 2273 let sample = this.samples.shift(); 2274 if (sample === null) { 2275 // discontinuity 2276 this.nextDts = undefined; 2277 break; 2278 } 2279 2280 let unit = sample.unit; 2281 2282 pts = sample.pts- this.initDTS; // /*Math.round(*/(sample.pts - this.initDTS)/*/this.tsAlign)*this.tsAlign*/; 2283 dts = sample.dts - this.initDTS; ///*Math.round(*/(sample.dts - this.initDTS)/*/this.tsAlign)*this.tsAlign*/; 2284 // ensure DTS is not bigger than PTS 2285 dts = Math.min(pts,dts); 2286 // if not first AVC sample of video track, normalize PTS/DTS with previous sample value 2287 // and ensure that sample duration is positive 2288 if (lastDTS !== undefined) { 2289 let sampleDuration = this.scaled(dts - lastDTS); 2290 // Log.debug(`Sample duration: ${sampleDuration}`); 2291 if (sampleDuration < 0) { 2292 Log$4.log(`invalid AVC sample duration at PTS/DTS: ${pts}/${dts}|lastDTS: ${lastDTS}:${sampleDuration}`); 2293 this.mp4track.len -= unit.getSize(); 2294 continue; 2295 } 2296 // minDuration = Math.min(sampleDuration, minDuration); 2297 this.lastDurations.push(sampleDuration); 2298 if (this.lastDurations.length > 100) { 2299 this.lastDurations.shift(); 2300 } 2301 mp4Sample.duration = sampleDuration; 2302 } else { 2303 if (this.nextDts) { 2304 let delta = dts - this.nextDts; 2305 // if fragment are contiguous, or delta less than 600ms, ensure there is no overlap/hole between fragments 2306 if (/*contiguous ||*/ Math.abs(Math.round(BaseRemuxer.toMS(delta))) < 600) { 2307 2308 if (delta) { 2309 // set DTS to next DTS 2310 // Log.debug(`Video/PTS/DTS adjusted: ${pts}->${Math.max(pts - delta, this.nextDts)}/${dts}->${this.nextDts},delta:${delta}`); 2311 dts = this.nextDts; 2312 // offset PTS as well, ensure that PTS is smaller or equal than new DTS 2313 pts = Math.max(pts - delta, dts); 2314 } 2315 } else { 2316 if (delta < 0) { 2317 Log$4.log(`skip frame from the past at DTS=${dts} with expected DTS=${this.nextDts}`); 2318 this.mp4track.len -= unit.getSize(); 2319 continue; 2320 } 2321 } 2322 } 2323 // remember first DTS of our avcSamples, ensure value is positive 2324 this.firstDTS = Math.max(0, dts); 2325 } 2326 2327 mp4Sample = { 2328 size: unit.getSize(), 2329 duration: 0, 2330 cts: this.scaled(pts - dts), 2331 flags: { 2332 isLeading: 0, 2333 isDependedOn: 0, 2334 hasRedundancy: 0, 2335 degradPrio: 0 2336 } 2337 }; 2338 let flags = mp4Sample.flags; 2339 if (sample.unit.isKeyframe() === true) { 2340 // the current sample is a key frame 2341 flags.dependsOn = 2; 2342 flags.isNonSync = 0; 2343 } else { 2344 flags.dependsOn = 1; 2345 flags.isNonSync = 1; 2346 } 2347 2348 payload.set(unit.getData(), offset); 2349 offset += unit.getSize(); 2350 2351 samples.push(mp4Sample); 2352 lastDTS = dts; 2353 } 2354 2355 if (!samples.length) return null; 2356 2357 let avgDuration = this.lastDurations.reduce(function(a, b) { return (a|0) + (b|0); }, 0) / (this.lastDurations.length||1)|0; 2358 if (samples.length >= 2) { 2359 this.lastSampleDuration = avgDuration; 2360 mp4Sample.duration = avgDuration; 2361 } else { 2362 mp4Sample.duration = this.lastSampleDuration; 2363 } 2364 2365 if(samples.length && (!this.nextDts || navigator.userAgent.toLowerCase().indexOf('chrome') > -1)) { 2366 let flags = samples[0].flags; 2367 // chrome workaround, mark first sample as being a Random Access Point to avoid sourcebuffer append issue 2368 // https://code.google.com/p/chromium/issues/detail?id=229412 2369 flags.dependsOn = 2; 2370 flags.isNonSync = 0; 2371 } 2372 2373 // next AVC sample DTS should be equal to last sample DTS + last sample duration 2374 this.nextDts = dts + this.unscaled(this.lastSampleDuration); 2375 // Log.debug(`next dts: ${this.nextDts}, last duration: ${this.lastSampleDuration}, last dts: ${dts}`); 2376 2377 return new Uint8Array(payload.buffer, 0, this.mp4track.len); 2378 } 2379 } 2380 2381 class PayloadType { 2382 static get H264() {return 1;} 2383 static get AAC() {return 2;} 2384 2385 static get map() {return { 2386 [PayloadType.H264]: 'video', 2387 [PayloadType.AAC]: 'audio' 2388 }}; 2389 2390 static get string_map() {return { 2391 H264: PayloadType.H264, 2392 AAC: PayloadType.AAC, 2393 'MP4A-LATM': PayloadType.AAC, 2394 'MPEG4-GENERIC': PayloadType.AAC 2395 }} 2396 } 2397 2398 const LOG_TAG$1 = "remuxer"; 2399 const Log$5 = getTagged(LOG_TAG$1); 2400 2401 class Remuxer { 2402 static get TrackConverters() {return { 2403 [PayloadType.H264]: H264Remuxer, 2404 [PayloadType.AAC]: AACRemuxer 2405 }}; 2406 2407 static get TrackScaleFactor() {return { 2408 [PayloadType.H264]: 1,//4, 2409 [PayloadType.AAC]: 0 2410 }}; 2411 2412 static get TrackTimescale() {return { 2413 [PayloadType.H264]: 90000,//22500, 2414 [PayloadType.AAC]: 0 2415 }}; 2416 2417 constructor(mediaElement) { 2418 this.mse = new MSE([mediaElement]); 2419 this.eventSource = new EventEmitter(); 2420 this.mseEventSource = new EventSourceWrapper(this.mse.eventSource); 2421 this.mse_ready = true; 2422 2423 this.reset(); 2424 2425 this.errorListener = this.mseClose.bind(this); 2426 this.closeListener = this.mseClose.bind(this); 2427 this.errorDecodeListener = this.mseErrorDecode.bind(this); 2428 2429 this.eventSource.addEventListener('ready', this.init.bind(this)); 2430 } 2431 2432 initMSEHandlers() { 2433 this.mseEventSource.on('error', this.errorListener); 2434 this.mseEventSource.on('sourceclosed', this.closeListener); 2435 this.mseEventSource.on('errordecode', this.errorDecodeListener); 2436 } 2437 2438 async reset() { 2439 this.tracks = {}; 2440 this.initialized = false; 2441 this.initSegments = {}; 2442 this.codecs = []; 2443 this.streams = {}; 2444 this.enabled = false; 2445 await this.mse.clear(); 2446 this.initMSEHandlers(); 2447 } 2448 2449 destroy() { 2450 this.mseEventSource.destroy(); 2451 this.mse.destroy(); 2452 this.mse = null; 2453 2454 this.detachClient(); 2455 2456 this.eventSource.destroy(); 2457 } 2458 2459 onTracks(tracks) { 2460 Log$5.debug(`ontracks: `, tracks.detail); 2461 // store available track types 2462 for (let track of tracks.detail) { 2463 this.tracks[track.type] = new Remuxer.TrackConverters[track.type](Remuxer.TrackTimescale[track.type], Remuxer.TrackScaleFactor[track.type], track.params); 2464 if (track.offset) { 2465 this.tracks[track.type].timeOffset = track.offset; 2466 } 2467 if (track.duration) { 2468 this.tracks[track.type].mp4track.duration = track.duration*(this.tracks[track.type].timescale || Remuxer.TrackTimescale[track.type]); 2469 this.tracks[track.type].duration = track.duration; 2470 } else { 2471 this.tracks[track.type].duration = 1; 2472 } 2473 2474 // this.tracks[track.type].duration 2475 } 2476 this.mse.setLive(!this.client.seekable); 2477 } 2478 2479 setTimeOffset(timeOffset, track) { 2480 if (this.tracks[track.type]) { 2481 this.tracks[track.type].timeOffset = timeOffset;///this.tracks[track.type].scaleFactor; 2482 } 2483 } 2484 2485 init() { 2486 let tracks = []; 2487 this.codecs = []; 2488 let initmse = []; 2489 let initPts = Infinity; 2490 let initDts = Infinity; 2491 for (let track_type in this.tracks) { 2492 let track = this.tracks[track_type]; 2493 if (!MSE.isSupported([track.mp4track.codec])) { 2494 throw new Error(`${track.mp4track.type} codec ${track.mp4track.codec} is not supported`); 2495 } 2496 tracks.push(track.mp4track); 2497 this.codecs.push(track.mp4track.codec); 2498 track.init(initPts, initDts/*, false*/); 2499 // initPts = Math.min(track.initPTS, initPts); 2500 // initDts = Math.min(track.initDTS, initDts); 2501 } 2502 2503 for (let track_type in this.tracks) { 2504 let track = this.tracks[track_type]; 2505 //track.init(initPts, initDts); 2506 this.initSegments[track_type] = MP4.initSegment([track.mp4track], track.duration*track.timescale, track.timescale); 2507 initmse.push(this.initMSE(track_type, track.mp4track.codec)); 2508 } 2509 this.initialized = true; 2510 return Promise.all(initmse).then(()=>{ 2511 //this.mse.play(); 2512 this.enabled = true; 2513 }); 2514 2515 } 2516 2517 initMSE(track_type, codec) { 2518 if (MSE.isSupported(this.codecs)) { 2519 return this.mse.setCodec(track_type, `${PayloadType.map[track_type]}/mp4; codecs="${codec}"`).then(()=>{ 2520 this.mse.feed(track_type, this.initSegments[track_type]); 2521 // this.mse.play(); 2522 // this.enabled = true; 2523 }); 2524 } else { 2525 throw new Error('Codecs are not supported'); 2526 } 2527 } 2528 2529 mseClose() { 2530 // this.mse.clear(); 2531 this.client.stop(); 2532 this.eventSource.dispatchEvent('stopped'); 2533 } 2534 2535 mseErrorDecode() { 2536 if(this.tracks[2]) { 2537 console.warn(this.tracks[2].mp4track.type); 2538 this.mse.buffers[2].destroy(); 2539 delete this.tracks[2]; 2540 } 2541 } 2542 2543 flush() { 2544 this.onSamples(); 2545 2546 if (!this.initialized) { 2547 // Log.debug(`Initialize...`); 2548 if (Object.keys(this.tracks).length) { 2549 for (let track_type in this.tracks) { 2550 if (!this.tracks[track_type].readyToDecode || !this.tracks[track_type].samples.length) return; 2551 Log$5.debug(`Init MSE for track ${this.tracks[track_type].mp4track.type}`); 2552 } 2553 this.eventSource.dispatchEvent('ready'); 2554 } 2555 } else { 2556 for (let track_type in this.tracks) { 2557 let track = this.tracks[track_type]; 2558 let pay = track.getPayload(); 2559 if (pay && pay.byteLength) { 2560 this.mse.feed(track_type, [MP4.moof(track.seq, track.scaled(track.firstDTS), track.mp4track), MP4.mdat(pay)]); 2561 track.flush(); 2562 } 2563 } 2564 } 2565 } 2566 2567 onSamples(ev) { 2568 // TODO: check format 2569 // let data = ev.detail; 2570 // if (this.tracks[data.pay] && this.client.sampleQueues[data.pay].length) { 2571 // console.log(`video ${data.units[0].dts}`); 2572 for (let qidx in this.client.sampleQueues) { 2573 let queue = this.client.sampleQueues[qidx]; 2574 while (queue.length) { 2575 let units = queue.shift(); 2576 if(units){ 2577 for (let chunk of units) { 2578 if(this.tracks[qidx]) { 2579 this.tracks[qidx].remux(chunk); 2580 } 2581 } 2582 } else { 2583 if (!this.initialized) { 2584 delete this.tracks[qidx]; 2585 } 2586 } 2587 } 2588 } 2589 // } 2590 } 2591 2592 onAudioConfig(ev) { 2593 if (this.tracks[ev.detail.pay]) { 2594 this.tracks[ev.detail.pay].setConfig(ev.detail.config); 2595 } 2596 } 2597 2598 attachClient(client) { 2599 this.detachClient(); 2600 this.client = client; 2601 this.clientEventSource = new EventSourceWrapper(this.client.eventSource); 2602 this.clientEventSource.on('samples', this.samplesListener); 2603 this.clientEventSource.on('audio_config', this.audioConfigListener); 2604 this.clientEventSource.on('tracks', this.onTracks.bind(this)); 2605 this.clientEventSource.on('flush', this.flush.bind(this)); 2606 this.clientEventSource.on('clear', ()=>{ 2607 this.reset(); 2608 this.mse.clear().then(()=>{ 2609 //this.mse.play(); 2610 this.initMSEHandlers(); 2611 }); 2612 }); 2613 } 2614 2615 detachClient() { 2616 if (this.client) { 2617 this.clientEventSource.destroy(); 2618 // this.client.eventSource.removeEventListener('samples', this.onSamples.bind(this)); 2619 // this.client.eventSource.removeEventListener('audio_config', this.onAudioConfig.bind(this)); 2620 // // TODO: clear other listeners 2621 // this.client.eventSource.removeEventListener('clear', this._clearListener); 2622 // this.client.eventSource.removeEventListener('tracks', this._tracksListener); 2623 // this.client.eventSource.removeEventListener('flush', this._flushListener); 2624 this.client = null; 2625 } 2626 } 2627 } 2628 2629 class BaseTransport { 2630 constructor(endpoint, stream_type, config={}) { 2631 this.stream_type = stream_type; 2632 this.endpoint = endpoint; 2633 this.eventSource = new EventEmitter(); 2634 this.dataQueue = []; 2635 } 2636 2637 static canTransfer(stream_type) { 2638 return BaseTransport.streamTypes().includes(stream_type); 2639 } 2640 2641 static streamTypes() { 2642 return []; 2643 } 2644 2645 destroy() { 2646 this.eventSource.destroy(); 2647 } 2648 2649 connect() { 2650 // TO be impemented 2651 } 2652 2653 disconnect() { 2654 // TO be impemented 2655 } 2656 2657 reconnect() { 2658 return this.disconnect().then(()=>{ 2659 return this.connect(); 2660 }); 2661 } 2662 2663 setEndpoint(endpoint) { 2664 this.endpoint = endpoint; 2665 return this.reconnect(); 2666 } 2667 2668 send(data) { 2669 // TO be impemented 2670 // return this.prepare(data).send(); 2671 } 2672 2673 prepare(data) { 2674 // TO be impemented 2675 // return new Request(data); 2676 } 2677 2678 // onData(type, data) { 2679 // this.eventSource.dispatchEvent(type, data); 2680 // } 2681 } 2682 2683 const LOG_TAG$2 = "transport:ws"; 2684 const Log$6 = getTagged(LOG_TAG$2); 2685 2686 function bytesToString(data) { 2687 let len = data.length; 2688 let dataString = ""; 2689 for (let i = 0; i < len; i++) { 2690 dataString += String.fromCharCode(data[i]); 2691 } 2692 return dataString 2693 } 2694 2695 class WebsocketTransport extends BaseTransport { 2696 constructor(endpoint, stream_type, options={ 2697 socket:`${location.protocol.replace('http', 'ws')}//${location.host}/ws/`, 2698 workers: 1 2699 }) { 2700 super(endpoint, stream_type); 2701 this.socket_url = options.socket; 2702 2703 this.wssocket = undefined; 2704 this.awaitingPromises = []; 2705 2706 this.ready = this.connect(); 2707 } 2708 2709 destroy() { 2710 return this.disconnect().then(()=>{ 2711 return super.destroy(); 2712 }); 2713 2714 } 2715 2716 static canTransfer(stream_type) { 2717 return WebsocketTransport.streamTypes().includes(stream_type); 2718 } 2719 2720 static streamTypes() { 2721 return ['rtsp']; 2722 } 2723 2724 _connect() { 2725 return new Promise((resolve, reject)=>{ 2726 this.wssocket = new WebSocket(this.socket_url, 'rtsp'); 2727 this.wssocket.binaryType = 'arraybuffer'; 2728 2729 this.wssocket.onopen = ()=>{ 2730 Log$6.log('websocket opened'); 2731 this.eventSource.dispatchEvent('connected'); 2732 resolve(); 2733 }; 2734 2735 this.wssocket.onmessage = (ev)=>{ 2736 let data = new Uint8Array(ev.data); 2737 let prefix = data[0]; 2738 if (prefix == 36) { // rtpData 2739 this.dataQueue.push(data); 2740 this.eventSource.dispatchEvent('data'); 2741 } else if (prefix == 82){ // rtsp response 2742 let rtspResp = bytesToString(data); 2743 if (this.awaitingPromises.length) { 2744 this.awaitingPromises.shift().resolve(rtspResp); 2745 } 2746 } else{ 2747 Log$6.warn('无效包', prefix); 2748 } 2749 }; 2750 2751 this.wssocket.onerror = (e)=>{ 2752 Log$6.error(`${e.type}. code: ${e.code}, reason: ${e.reason || 'unknown reason'}`); 2753 this.wssocket.close(); 2754 this.eventSource.dispatchEvent('error', {code: e.code, reason: e.reason || 'unknown reason'}); 2755 }; 2756 2757 this.wssocket.onclose = (e)=>{ 2758 Log$6.log(`${e.type}. code: ${e.code}, reason: ${e.reason || 'unknown reason'}`); 2759 this.wssocket.onclose = null; 2760 this.wssocket.close(); 2761 this.eventSource.dispatchEvent('disconnected', {code: e.code, reason: e.reason || 'unknown reason'}); 2762 }; 2763 }); 2764 } 2765 2766 connect() { 2767 return this.disconnect().then((resolve, reject)=>{ 2768 return this._connect() 2769 }); 2770 } 2771 2772 disconnect() { 2773 return new Promise((resolve, reject)=>{ 2774 if (this.wssocket){ 2775 this.wssocket.onclose = ()=>{ 2776 Log$6.log('websocket closed'); 2777 }; 2778 this.wssocket.close(); 2779 resolve(); 2780 } 2781 else{ 2782 resolve(); 2783 } 2784 }); 2785 } 2786 2787 send(_data) { 2788 if (!this.wssocket) { 2789 throw new Error('websocekt disconnected'); 2790 } 2791 2792 return new Promise((resolve, reject)=>{ 2793 this.awaitingPromises.push({resolve:resolve, reject:reject}); 2794 this.wssocket.send(_data); 2795 }); 2796 } 2797 } 2798 2799 class State { 2800 constructor(name, stateMachine) { 2801 this.stateMachine = stateMachine; 2802 this.transitions = new Set(); 2803 this.name = name; 2804 } 2805 2806 2807 activate() { 2808 return Promise.resolve(null); 2809 } 2810 2811 finishTransition() {} 2812 2813 failHandler() {} 2814 2815 deactivate() { 2816 return Promise.resolve(null); 2817 } 2818 } 2819 2820 class StateMachine { 2821 constructor() { 2822 this.storage = {}; 2823 this.currentState = null; 2824 this.states = new Map(); 2825 } 2826 2827 addState(name, {activate, finishTransition, deactivate}) { 2828 let state = new State(name, this); 2829 if (activate) state.activate = activate; 2830 if (finishTransition) state.finishTransition = finishTransition; 2831 if (deactivate) state.deactivate = deactivate; 2832 this.states.set(name, state); 2833 return this; 2834 } 2835 2836 addTransition(fromName, toName){ 2837 if (!this.states.has(fromName)) { 2838 throw ReferenceError(`No such state: ${fromName} while connecting to ${toName}`); 2839 } 2840 if (!this.states.has(toName)) { 2841 throw ReferenceError(`No such state: ${toName} while connecting from ${fromName}`); 2842 } 2843 this.states.get(fromName).transitions.add(toName); 2844 return this; 2845 } 2846 2847 _promisify(res) { 2848 let promise; 2849 try { 2850 promise = res; 2851 if (!promise.then) { 2852 promise = Promise.resolve(promise); 2853 } 2854 } catch (e) { 2855 promise = Promise.reject(e); 2856 } 2857 return promise; 2858 } 2859 2860 transitionTo(stateName) { 2861 if (this.currentState == null) { 2862 let state = this.states.get(stateName); 2863 return this._promisify(state.activate.call(this)) 2864 .then((data)=> { 2865 this.currentState = state; 2866 return data; 2867 }).then(state.finishTransition.bind(this)).catch((e)=>{ 2868 state.failHandler(); 2869 throw e; 2870 }); 2871 } 2872 if (this.currentState.name == stateName) return Promise.resolve(); 2873 if (this.currentState.transitions.has(stateName)) { 2874 let state = this.states.get(stateName); 2875 return this._promisify(state.deactivate.call(this)) 2876 .then(state.activate.bind(this)).then((data)=> { 2877 this.currentState = state; 2878 return data; 2879 }).then(state.finishTransition.bind(this)).catch((e)=>{ 2880 state.failHandler(); 2881 throw e; 2882 }); 2883 } else { 2884 return Promise.reject(`No such transition: ${this.currentState.name} to ${stateName}`); 2885 } 2886 } 2887 2888 } 2889 2890 const Log$7 = getTagged("parser:sdp"); 2891 2892 class SDPParser { 2893 constructor() { 2894 this.version = -1; 2895 this.origin = null; 2896 this.sessionName = null; 2897 this.timing = null; 2898 this.sessionBlock = {}; 2899 this.media = {}; 2900 this.tracks = {}; 2901 this.mediaMap = {}; 2902 } 2903 2904 parse(content) { 2905 // Log.debug(content); 2906 return new Promise((resolve, reject) => { 2907 var dataString = content; 2908 var success = true; 2909 var currentMediaBlock = this.sessionBlock; 2910 2911 // TODO: multiple audio/video tracks 2912 2913 for (let line of dataString.split("\n")) { 2914 line = line.replace(/\r/, ''); 2915 if (0 === line.length) { 2916 /* Empty row (last row perhaps?), skip to next */ 2917 continue; 2918 } 2919 2920 switch (line.charAt(0)) { 2921 case 'v': 2922 if (-1 !== this.version) { 2923 Log$7.log('Version present multiple times in SDP'); 2924 reject(); 2925 return false; 2926 } 2927 success = success && this._parseVersion(line); 2928 break; 2929 2930 case 'o': 2931 if (null !== this.origin) { 2932 Log$7.log('Origin present multiple times in SDP'); 2933 reject(); 2934 return false; 2935 } 2936 success = success && this._parseOrigin(line); 2937 break; 2938 2939 case 's': 2940 if (null !== this.sessionName) { 2941 Log$7.log('Session Name present multiple times in SDP'); 2942 reject(); 2943 return false; 2944 } 2945 success = success && this._parseSessionName(line); 2946 break; 2947 2948 case 't': 2949 if (null !== this.timing) { 2950 Log$7.log('Timing present multiple times in SDP'); 2951 reject(); 2952 return false; 2953 } 2954 success = success && this._parseTiming(line); 2955 break; 2956 2957 case 'm': 2958 if (null !== currentMediaBlock && this.sessionBlock !== currentMediaBlock) { 2959 /* Complete previous block and store it */ 2960 this.media[currentMediaBlock.type] = currentMediaBlock; 2961 } 2962 2963 /* A wild media block appears */ 2964 currentMediaBlock = {}; 2965 currentMediaBlock.rtpmap = {}; 2966 this._parseMediaDescription(line, currentMediaBlock); 2967 break; 2968 2969 case 'a': 2970 SDPParser._parseAttribute(line, currentMediaBlock); 2971 break; 2972 2973 default: 2974 Log$7.log('Ignored unknown SDP directive: ' + line); 2975 break; 2976 } 2977 2978 if (!success) { 2979 reject(); 2980 return; 2981 } 2982 } 2983 2984 this.media[currentMediaBlock.type] = currentMediaBlock; 2985 2986 success ? resolve() : reject(); 2987 }); 2988 } 2989 2990 _parseVersion(line) { 2991 let matches = line.match(/^v=([0-9]+)$/); 2992 if (!matches || !matches.length) { 2993 Log$7.log('\'v=\' (Version) formatted incorrectly: ' + line); 2994 return false; 2995 } 2996 2997 this.version = matches[1]; 2998 if (0 != this.version) { 2999 Log$7.log('Unsupported SDP version:' + this.version); 3000 return false; 3001 } 3002 3003 return true; 3004 } 3005 3006 _parseOrigin(line) { 3007 let matches = line.match(/^o=([^ ]+) (-?[0-9]+) (-?[0-9]+) (IN) (IP4|IP6) ([^ ]+)$/); 3008 if (!matches || !matches.length) { 3009 Log$7.log('\'o=\' (Origin) formatted incorrectly: ' + line); 3010 return false; 3011 } 3012 3013 this.origin = {}; 3014 this.origin.username = matches[1]; 3015 this.origin.sessionid = matches[2]; 3016 this.origin.sessionversion = matches[3]; 3017 this.origin.nettype = matches[4]; 3018 this.origin.addresstype = matches[5]; 3019 this.origin.unicastaddress = matches[6]; 3020 3021 return true; 3022 } 3023 3024 _parseSessionName(line) { 3025 let matches = line.match(/^s=([^\r\n]+)$/); 3026 if (!matches || !matches.length) { 3027 Log$7.log('\'s=\' (Session Name) formatted incorrectly: ' + line); 3028 return false; 3029 } 3030 3031 this.sessionName = matches[1]; 3032 3033 return true; 3034 } 3035 3036 _parseTiming(line) { 3037 let matches = line.match(/^t=([0-9]+) ([0-9]+)$/); 3038 if (!matches || !matches.length) { 3039 Log$7.log('\'t=\' (Timing) formatted incorrectly: ' + line); 3040 return false; 3041 } 3042 3043 this.timing = {}; 3044 this.timing.start = matches[1]; 3045 this.timing.stop = matches[2]; 3046 3047 return true; 3048 } 3049 3050 _parseMediaDescription(line, media) { 3051 let matches = line.match(/^m=([^ ]+) ([^ ]+) ([^ ]+)[ ]/); 3052 if (!matches || !matches.length) { 3053 Log$7.log('\'m=\' (Media) formatted incorrectly: ' + line); 3054 return false; 3055 } 3056 3057 media.type = matches[1]; 3058 media.port = matches[2]; 3059 media.proto = matches[3]; 3060 media.fmt = line.substr(matches[0].length).split(' ').map(function (fmt, index, array) { 3061 return parseInt(fmt); 3062 }); 3063 3064 for (let fmt of media.fmt) { 3065 this.mediaMap[fmt] = media; 3066 } 3067 3068 return true; 3069 } 3070 3071 static _parseAttribute(line, media) { 3072 if (null === media) { 3073 /* Not in a media block, can't be bothered parsing attributes for session */ 3074 return true; 3075 } 3076 3077 var matches; 3078 /* Used for some cases of below switch-case */ 3079 var separator = line.indexOf(':'); 3080 var attribute = line.substr(0, (-1 === separator) ? 0x7FFFFFFF : separator); 3081 /* 0x7FF.. is default */ 3082 3083 switch (attribute) { 3084 case 'a=recvonly': 3085 case 'a=sendrecv': 3086 case 'a=sendonly': 3087 case 'a=inactive': 3088 media.mode = line.substr('a='.length); 3089 break; 3090 case 'a=range': 3091 matches = line.match(/^a=range:\s*([a-zA-Z-]+)=([0-9.]+|now)\s*-\s*([0-9.]*)$/); 3092 media.range = [Number(matches[2] == "now" ? -1 : matches[2]), Number(matches[3]), matches[1]]; 3093 break; 3094 case 'a=control': 3095 media.control = line.substr('a=control:'.length); 3096 break; 3097 3098 case 'a=rtpmap': 3099 matches = line.match(/^a=rtpmap:(\d+) (.*)$/); 3100 if (null === matches) { 3101 Log$7.log('Could not parse \'rtpmap\' of \'a=\''); 3102 return false; 3103 } 3104 3105 var payload = parseInt(matches[1]); 3106 media.rtpmap[payload] = {}; 3107 3108 var attrs = matches[2].split('/'); 3109 media.rtpmap[payload].name = attrs[0].toUpperCase(); 3110 media.rtpmap[payload].clock = attrs[1]; 3111 if (undefined !== attrs[2]) { 3112 media.rtpmap[payload].encparams = attrs[2]; 3113 } 3114 media.ptype = PayloadType.string_map[attrs[0].toUpperCase()]; 3115 3116 break; 3117 3118 case 'a=fmtp': 3119 matches = line.match(/^a=fmtp:(\d+) (.*)$/); 3120 if (0 === matches.length) { 3121 Log$7.log('Could not parse \'fmtp\' of \'a=\''); 3122 return false; 3123 } 3124 3125 media.fmtp = {}; 3126 for (var param of matches[2].split(';')) { 3127 var idx = param.indexOf('='); 3128 media.fmtp[param.substr(0, idx).toLowerCase().trim()] = param.substr(idx + 1).trim(); 3129 } 3130 break; 3131 } 3132 3133 return true; 3134 } 3135 3136 getSessionBlock() { 3137 return this.sessionBlock; 3138 } 3139 3140 hasMedia(mediaType) { 3141 return this.media[mediaType] != undefined; 3142 } 3143 3144 getMediaBlock(mediaType) { 3145 return this.media[mediaType]; 3146 } 3147 3148 getMediaBlockByPayloadType(pt) { 3149 // for (var m in this.media) { 3150 // if (-1 !== this.media[m].fmt.indexOf(pt)) { 3151 // return this.media[m]; 3152 // } 3153 // } 3154 return this.mediaMap[pt] || null; 3155 3156 //ErrorManager.dispatchError(826, [pt], true); 3157 // Log.error(`failed to find media with payload type ${pt}`); 3158 // 3159 // return null; 3160 } 3161 3162 getMediaBlockList() { 3163 var res = []; 3164 for (var m in this.media) { 3165 res.push(m); 3166 } 3167 3168 return res; 3169 } 3170 } 3171 3172 const LOG_TAG$3 = "rtsp:stream"; 3173 const Log$8 = getTagged(LOG_TAG$3); 3174 3175 class RTSPStream { 3176 3177 constructor(client, track) { 3178 this.state = null; 3179 this.client = client; 3180 this.track = track; 3181 this.rtpChannel = 1; 3182 3183 this.stopKeepAlive(); 3184 this.keepaliveInterval = null; 3185 this.keepaliveTime = 30000; 3186 } 3187 3188 reset() { 3189 this.stopKeepAlive(); 3190 this.client.forgetRTPChannel(this.rtpChannel); 3191 this.client = null; 3192 this.track = null; 3193 } 3194 3195 start(lastSetupPromise = null) { 3196 if (lastSetupPromise != null) { 3197 // if a setup was already made, use the same session 3198 return lastSetupPromise.then((obj) => this.sendSetup(obj.session)) 3199 } else { 3200 return this.sendSetup(); 3201 } 3202 } 3203 3204 stop() { 3205 return this.sendTeardown(); 3206 } 3207 3208 getSetupURL(track) { 3209 let sessionBlock = this.client.sdp.getSessionBlock(); 3210 if (Url.isAbsolute(track.control)) { 3211 return track.control; 3212 } else if (Url.isAbsolute(`${sessionBlock.control}${track.control}`)) { 3213 return `${sessionBlock.control}${track.control}`; 3214 } else if (Url.isAbsolute(`${this.client.contentBase}${track.control}`)) { 3215 /* Should probably check session level control before this */ 3216 return `${this.client.contentBase}${track.control}`; 3217 } 3218 else {//need return default 3219 return track.control; 3220 } 3221 Log$8.error('Can\'t determine track URL from ' + 3222 'block.control:' + track.control + ', ' + 3223 'session.control:' + sessionBlock.control + ', and ' + 3224 'content-base:' + this.client.contentBase); 3225 } 3226 3227 getControlURL() { 3228 let ctrl = this.client.sdp.getSessionBlock().control; 3229 if (Url.isAbsolute(ctrl)) { 3230 return ctrl; 3231 } else if (!ctrl || '*' === ctrl) { 3232 return this.client.contentBase; 3233 } else { 3234 return `${this.client.contentBase}${ctrl}`; 3235 } 3236 } 3237 3238 sendKeepalive() { 3239 if (this.client.methods.includes('GET_PARAMETER')) { 3240 return this.client.sendRequest('GET_PARAMETER', this.getSetupURL(this.track), { 3241 'Session': this.session 3242 }); 3243 } else { 3244 return this.client.sendRequest('OPTIONS', '*'); 3245 } 3246 } 3247 3248 stopKeepAlive() { 3249 clearInterval(this.keepaliveInterval); 3250 } 3251 3252 startKeepAlive() { 3253 this.keepaliveInterval = setInterval(() => { 3254 this.sendKeepalive().catch((e) => { 3255 // Log.error(e); 3256 if (e instanceof RTSPError) { 3257 if (Number(e.data.parsed.code) == 501) { 3258 return; 3259 } 3260 } 3261 this.client.reconnect(); 3262 }); 3263 }, this.keepaliveTime); 3264 } 3265 3266 sendRequest(_cmd, _params = {}) { 3267 let params = {}; 3268 if (this.session) { 3269 params['Session'] = this.session; 3270 } 3271 Object.assign(params, _params); 3272 return this.client.sendRequest(_cmd, this.getControlURL(), params); 3273 } 3274 3275 sendSetup(session = null) { 3276 this.state = RTSPClientSM.STATE_SETUP; 3277 this.rtpChannel = this.client.interleaveChannelIndex; 3278 let interleavedChannels = this.client.interleaveChannelIndex++ + "-" + this.client.interleaveChannelIndex++; 3279 let params = { 3280 'Transport': `RTP/AVP/TCP;unicast;interleaved=${interleavedChannels}`, 3281 'Date': new Date().toUTCString() 3282 }; 3283 if(session){ 3284 params.Session = session; 3285 } 3286 return this.client.sendRequest('SETUP', this.getSetupURL(this.track), params).then((_data) => { 3287 this.session = _data.headers['session']; 3288 let transport = _data.headers['transport']; 3289 if (transport) { 3290 let interleaved = transport.match(/interleaved=([0-9]+)-([0-9]+)/)[1]; 3291 if (interleaved) { 3292 this.rtpChannel = Number(interleaved); 3293 } 3294 } 3295 let sessionParamsChunks = this.session.split(';').slice(1); 3296 let sessionParams = {}; 3297 for (let chunk of sessionParamsChunks) { 3298 let kv = chunk.split('='); 3299 sessionParams[kv[0]] = kv[1]; 3300 } 3301 if (sessionParams['timeout']) { 3302 this.keepaliveInterval = Number(sessionParams['timeout']) * 500; // * 1000 / 2 3303 } 3304 /*if (!/RTP\/AVP\/TCP;unicast;interleaved=/.test(_data.headers["transport"])) { 3305 // TODO: disconnect stream and notify client 3306 throw new Error("Connection broken"); 3307 }*/ 3308 this.client.useRTPChannel(this.rtpChannel); 3309 this.startKeepAlive(); 3310 return {track: this.track, data: _data, session: this.session}; 3311 }); 3312 } 3313 } 3314 3315 /* 3316 * JavaScript MD5 3317 * https://github.com/blueimp/JavaScript-MD5 3318 * 3319 * Copyright 2011, Sebastian Tschan 3320 * https://blueimp.net 3321 * 3322 * Licensed under the MIT license: 3323 * https://opensource.org/licenses/MIT 3324 * 3325 * Based on 3326 * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message 3327 * Digest Algorithm, as defined in RFC 1321. 3328 * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 3329 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 3330 * Distributed under the BSD License 3331 * See http://pajhome.org.uk/crypt/md5 for more info. 3332 */ 3333 3334 3335 /* 3336 * Add integers, wrapping at 2^32. This uses 16-bit operations internally 3337 * to work around bugs in some JS interpreters. 3338 */ 3339 function safeAdd(x, y) { 3340 var lsw = (x & 0xFFFF) + (y & 0xFFFF); 3341 var msw = (x >> 16) + (y >> 16) + (lsw >> 16); 3342 return (msw << 16) | (lsw & 0xFFFF) 3343 } 3344 3345 /* 3346 * Bitwise rotate a 32-bit number to the left. 3347 */ 3348 function bitRotateLeft(num, cnt) { 3349 return (num << cnt) | (num >>> (32 - cnt)) 3350 } 3351 3352 /* 3353 * These functions implement the four basic operations the algorithm uses. 3354 */ 3355 function md5cmn(q, a, b, x, s, t) { 3356 return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b) 3357 } 3358 function md5ff(a, b, c, d, x, s, t) { 3359 return md5cmn((b & c) | ((~b) & d), a, b, x, s, t) 3360 } 3361 function md5gg(a, b, c, d, x, s, t) { 3362 return md5cmn((b & d) | (c & (~d)), a, b, x, s, t) 3363 } 3364 function md5hh(a, b, c, d, x, s, t) { 3365 return md5cmn(b ^ c ^ d, a, b, x, s, t) 3366 } 3367 function md5ii(a, b, c, d, x, s, t) { 3368 return md5cmn(c ^ (b | (~d)), a, b, x, s, t) 3369 } 3370 3371 /* 3372 * Calculate the MD5 of an array of little-endian words, and a bit length. 3373 */ 3374 function binlMD5(x, len) { 3375 /* append padding */ 3376 x[len >> 5] |= 0x80 << (len % 32); 3377 x[(((len + 64) >>> 9) << 4) + 14] = len; 3378 3379 var i; 3380 var olda; 3381 var oldb; 3382 var oldc; 3383 var oldd; 3384 var a = 1732584193; 3385 var b = -271733879; 3386 var c = -1732584194; 3387 var d = 271733878; 3388 3389 for (i = 0; i < x.length; i += 16) { 3390 olda = a; 3391 oldb = b; 3392 oldc = c; 3393 oldd = d; 3394 3395 a = md5ff(a, b, c, d, x[i], 7, -680876936); 3396 d = md5ff(d, a, b, c, x[i + 1], 12, -389564586); 3397 c = md5ff(c, d, a, b, x[i + 2], 17, 606105819); 3398 b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330); 3399 a = md5ff(a, b, c, d, x[i + 4], 7, -176418897); 3400 d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426); 3401 c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341); 3402 b = md5ff(b, c, d, a, x[i + 7], 22, -45705983); 3403 a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416); 3404 d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417); 3405 c = md5ff(c, d, a, b, x[i + 10], 17, -42063); 3406 b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162); 3407 a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682); 3408 d = md5ff(d, a, b, c, x[i + 13], 12, -40341101); 3409 c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290); 3410 b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329); 3411 3412 a = md5gg(a, b, c, d, x[i + 1], 5, -165796510); 3413 d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632); 3414 c = md5gg(c, d, a, b, x[i + 11], 14, 643717713); 3415 b = md5gg(b, c, d, a, x[i], 20, -373897302); 3416 a = md5gg(a, b, c, d, x[i + 5], 5, -701558691); 3417 d = md5gg(d, a, b, c, x[i + 10], 9, 38016083); 3418 c = md5gg(c, d, a, b, x[i + 15], 14, -660478335); 3419 b = md5gg(b, c, d, a, x[i + 4], 20, -405537848); 3420 a = md5gg(a, b, c, d, x[i + 9], 5, 568446438); 3421 d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690); 3422 c = md5gg(c, d, a, b, x[i + 3], 14, -187363961); 3423 b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501); 3424 a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467); 3425 d = md5gg(d, a, b, c, x[i + 2], 9, -51403784); 3426 c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473); 3427 b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734); 3428 3429 a = md5hh(a, b, c, d, x[i + 5], 4, -378558); 3430 d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463); 3431 c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562); 3432 b = md5hh(b, c, d, a, x[i + 14], 23, -35309556); 3433 a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060); 3434 d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353); 3435 c = md5hh(c, d, a, b, x[i + 7], 16, -155497632); 3436 b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640); 3437 a = md5hh(a, b, c, d, x[i + 13], 4, 681279174); 3438 d = md5hh(d, a, b, c, x[i], 11, -358537222); 3439 c = md5hh(c, d, a, b, x[i + 3], 16, -722521979); 3440 b = md5hh(b, c, d, a, x[i + 6], 23, 76029189); 3441 a = md5hh(a, b, c, d, x[i + 9], 4, -640364487); 3442 d = md5hh(d, a, b, c, x[i + 12], 11, -421815835); 3443 c = md5hh(c, d, a, b, x[i + 15], 16, 530742520); 3444 b = md5hh(b, c, d, a, x[i + 2], 23, -995338651); 3445 3446 a = md5ii(a, b, c, d, x[i], 6, -198630844); 3447 d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415); 3448 c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905); 3449 b = md5ii(b, c, d, a, x[i + 5], 21, -57434055); 3450 a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571); 3451 d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606); 3452 c = md5ii(c, d, a, b, x[i + 10], 15, -1051523); 3453 b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799); 3454 a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359); 3455 d = md5ii(d, a, b, c, x[i + 15], 10, -30611744); 3456 c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380); 3457 b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649); 3458 a = md5ii(a, b, c, d, x[i + 4], 6, -145523070); 3459 d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379); 3460 c = md5ii(c, d, a, b, x[i + 2], 15, 718787259); 3461 b = md5ii(b, c, d, a, x[i + 9], 21, -343485551); 3462 3463 a = safeAdd(a, olda); 3464 b = safeAdd(b, oldb); 3465 c = safeAdd(c, oldc); 3466 d = safeAdd(d, oldd); 3467 } 3468 return [a, b, c, d] 3469 } 3470 3471 /* 3472 * Convert an array of little-endian words to a string 3473 */ 3474 function binl2rstr(input) { 3475 var i; 3476 var output = ''; 3477 var length32 = input.length * 32; 3478 for (i = 0; i < length32; i += 8) { 3479 output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF); 3480 } 3481 return output 3482 } 3483 3484 /* 3485 * Convert a raw string to an array of little-endian words 3486 * Characters >255 have their high-byte silently ignored. 3487 */ 3488 function rstr2binl(input) { 3489 var i; 3490 var output = []; 3491 output[(input.length >> 2) - 1] = undefined; 3492 for (i = 0; i < output.length; i += 1) { 3493 output[i] = 0; 3494 } 3495 var length8 = input.length * 8; 3496 for (i = 0; i < length8; i += 8) { 3497 output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32); 3498 } 3499 return output 3500 } 3501 3502 /* 3503 * Calculate the MD5 of a raw string 3504 */ 3505 function rstrMD5(s) { 3506 return binl2rstr(binlMD5(rstr2binl(s), s.length * 8)) 3507 } 3508 3509 /* 3510 * Calculate the HMAC-MD5, of a key and some data (raw strings) 3511 */ 3512 function rstrHMACMD5(key, data) { 3513 var i; 3514 var bkey = rstr2binl(key); 3515 var ipad = []; 3516 var opad = []; 3517 var hash; 3518 ipad[15] = opad[15] = undefined; 3519 if (bkey.length > 16) { 3520 bkey = binlMD5(bkey, key.length * 8); 3521 } 3522 for (i = 0; i < 16; i += 1) { 3523 ipad[i] = bkey[i] ^ 0x36363636; 3524 opad[i] = bkey[i] ^ 0x5C5C5C5C; 3525 } 3526 hash = binlMD5(ipad.concat(rstr2binl(data)), 512 + data.length * 8); 3527 return binl2rstr(binlMD5(opad.concat(hash), 512 + 128)) 3528 } 3529 3530 /* 3531 * Convert a raw string to a hex string 3532 */ 3533 function rstr2hex(input) { 3534 var hexTab = '0123456789abcdef'; 3535 var output = ''; 3536 var x; 3537 var i; 3538 for (i = 0; i < input.length; i += 1) { 3539 x = input.charCodeAt(i); 3540 output += hexTab.charAt((x >>> 4) & 0x0F) + 3541 hexTab.charAt(x & 0x0F); 3542 } 3543 return output 3544 } 3545 3546 /* 3547 * Encode a string as utf-8 3548 */ 3549 function str2rstrUTF8(input) { 3550 return unescape(encodeURIComponent(input)) 3551 } 3552 3553 /* 3554 * Take string arguments and return either raw or hex encoded strings 3555 */ 3556 function rawMD5(s) { 3557 return rstrMD5(str2rstrUTF8(s)) 3558 } 3559 function hexMD5(s) { 3560 return rstr2hex(rawMD5(s)) 3561 } 3562 function rawHMACMD5(k, d) { 3563 return rstrHMACMD5(str2rstrUTF8(k), str2rstrUTF8(d)) 3564 } 3565 function hexHMACMD5(k, d) { 3566 return rstr2hex(rawHMACMD5(k, d)) 3567 } 3568 3569 function md5(string, key, raw) { 3570 if (!key) { 3571 if (!raw) { 3572 return hexMD5(string) 3573 } 3574 return rawMD5(string) 3575 } 3576 if (!raw) { 3577 return hexHMACMD5(key, string) 3578 } 3579 return rawHMACMD5(key, string) 3580 } 3581 3582 // TODO: asm.js 3583 class RTP { 3584 constructor(pkt/*uint8array*/, sdp) { 3585 let bytes = new DataView(pkt.buffer, pkt.byteOffset, pkt.byteLength); 3586 3587 this.version = bytes.getUint8(0) >>> 6; 3588 this.padding = bytes.getUint8(0) & 0x20 >>> 5; 3589 this.has_extension = bytes.getUint8(0) & 0x10 >>> 4; 3590 this.csrc = bytes.getUint8(0) & 0x0F; 3591 this.marker = bytes.getUint8(1) >>> 7; 3592 this.pt = bytes.getUint8(1) & 0x7F; 3593 this.sequence = bytes.getUint16(2) ; 3594 this.timestamp = bytes.getUint32(4); 3595 this.ssrc = bytes.getUint32(8); 3596 this.csrcs = []; 3597 3598 let pktIndex=12; 3599 if (this.csrc>0) { 3600 this.csrcs.push(bytes.getUint32(pktIndex)); 3601 pktIndex+=4; 3602 } 3603 if (this.has_extension==1) { 3604 this.extension = bytes.getUint16(pktIndex); 3605 this.ehl = bytes.getUint16(pktIndex+2); 3606 pktIndex+=4; 3607 this.header_data = pkt.slice(pktIndex, this.ehl); 3608 pktIndex += this.ehl; 3609 } 3610 3611 this.headerLength = pktIndex; 3612 let padLength = 0; 3613 if (this.padding) { 3614 padLength = bytes.getUint8(pkt.byteLength-1); 3615 } 3616 3617 // this.bodyLength = pkt.byteLength-this.headerLength-padLength; 3618 3619 this.media = sdp.getMediaBlockByPayloadType(this.pt); 3620 if (null === this.media) { 3621 Log.log(`Media description for payload type: ${this.pt} not provided.`); 3622 } else { 3623 this.type = this.media.ptype;//PayloadType.string_map[this.media.rtpmap[this.media.fmt[0]].name]; 3624 } 3625 3626 this.data = pkt.subarray(pktIndex); 3627 // this.timestamp = 1000 * (this.timestamp / this.media.rtpmap[this.pt].clock); 3628 // console.log(this); 3629 } 3630 getPayload() { 3631 return this.data; 3632 } 3633 3634 getTimestampMS() { 3635 return this.timestamp; //1000 * (this.timestamp / this.media.rtpmap[this.pt].clock); 3636 } 3637 3638 toString() { 3639 return "RTP(" + 3640 "version:" + this.version + ", " + 3641 "padding:" + this.padding + ", " + 3642 "has_extension:" + this.has_extension + ", " + 3643 "csrc:" + this.csrc + ", " + 3644 "marker:" + this.marker + ", " + 3645 "pt:" + this.pt + ", " + 3646 "sequence:" + this.sequence + ", " + 3647 "timestamp:" + this.timestamp + ", " + 3648 "ssrc:" + this.ssrc + ")"; 3649 } 3650 3651 isVideo(){return this.media.type == 'video';} 3652 isAudio(){return this.media.type == 'audio';} 3653 3654 3655 } 3656 3657 class RTPFactory { 3658 constructor(sdp) { 3659 this.tsOffsets={}; 3660 for (let pay in sdp.media) { 3661 for (let pt of sdp.media[pay].fmt) { 3662 this.tsOffsets[pt] = {last: 0, overflow: 0}; 3663 } 3664 } 3665 } 3666 3667 build(pkt/*uint8array*/, sdp) { 3668 let rtp = new RTP(pkt, sdp); 3669 3670 let tsOffset = this.tsOffsets[rtp.pt]; 3671 if (tsOffset) { 3672 rtp.timestamp += tsOffset.overflow; 3673 if (tsOffset.last && Math.abs(rtp.timestamp - tsOffset.last) > 0x7fffffff) { 3674 console.log(`\nlast ts: ${tsOffset.last}\n 3675 new ts: ${rtp.timestamp}\n 3676 new ts adjusted: ${rtp.timestamp+0xffffffff}\n 3677 last overflow: ${tsOffset.overflow}\n 3678 new overflow: ${tsOffset.overflow+0xffffffff}\n 3679 `); 3680 tsOffset.overflow += 0xffffffff; 3681 rtp.timestamp += 0xffffffff; 3682 } 3683 /*if (rtp.timestamp>0xffffffff) { 3684 console.log(`ts: ${rtp.timestamp}, seq: ${rtp.sequence}`); 3685 }*/ 3686 tsOffset.last = rtp.timestamp; 3687 } 3688 3689 return rtp; 3690 } 3691 } 3692 3693 class RTSPMessage { 3694 static get RTSP_1_0() {return "RTSP/1.0";} 3695 3696 constructor(_rtsp_version) { 3697 this.version = _rtsp_version; 3698 } 3699 3700 build(_cmd, _host, _params={}, _payload=null) { 3701 let requestString = `${_cmd} ${_host} ${this.version}\r\n`; 3702 for (let param in _params) { 3703 requestString+=`${param}: ${_params[param]}\r\n`; 3704 } 3705 // TODO: binary payload 3706 if (_payload) { 3707 requestString+=`Content-Length: ${_payload.length}\r\n`; 3708 } 3709 requestString+='\r\n'; 3710 if (_payload) { 3711 requestString+=_payload; 3712 } 3713 return requestString; 3714 } 3715 3716 parse(_data) { 3717 let lines = _data.split('\r\n'); 3718 let parsed = { 3719 headers:{}, 3720 body:null, 3721 code: 0, 3722 statusLine: '' 3723 }; 3724 3725 let match; 3726 [match, parsed.code, parsed.statusLine] = lines[0].match(new RegExp(`${this.version}[ ]+([0-9]{3})[ ]+(.*)`)); 3727 parsed.code = Number(parsed.code); 3728 let lineIdx = 1; 3729 3730 while (lines[lineIdx]) { 3731 let [k,v] = lines[lineIdx].split(/:(.+)/); 3732 parsed.headers[k.toLowerCase()] = v.trim(); 3733 lineIdx++; 3734 } 3735 3736 parsed.body = lines.slice(lineIdx).join('\n\r'); 3737 3738 return parsed; 3739 } 3740 3741 } 3742 3743 const MessageBuilder = new RTSPMessage(RTSPMessage.RTSP_1_0); 3744 3745 // TODO: asm.js 3746 class NALUAsm { 3747 3748 constructor() { 3749 this.fragmented_nalu = null; 3750 } 3751 3752 3753 static parseNALHeader(hdr) { 3754 return { 3755 nri: hdr & 0x60, 3756 type: hdr & 0x1F 3757 } 3758 } 3759 3760 parseSingleNALUPacket(rawData, header, dts, pts) { 3761 return new NALU(header.type, header.nri, rawData.subarray(0), dts, pts); 3762 } 3763 3764 parseAggregationPacket(rawData, header, dts, pts) { 3765 let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); 3766 let nal_start_idx = 0; 3767 let don = null; 3768 if (NALU.STAP_B === header.type) { 3769 don = data.getUint16(nal_start_idx); 3770 nal_start_idx += 2; 3771 } 3772 let ret = []; 3773 while (nal_start_idx < data.byteLength) { 3774 let size = data.getUint16(nal_start_idx); 3775 nal_start_idx += 2; 3776 let header = NALUAsm.parseNALHeader(data.getInt8(nal_start_idx)); 3777 nal_start_idx++; 3778 let nalu = this.parseSingleNALUPacket(rawData.subarray(nal_start_idx, nal_start_idx+size), header, dts, pts); 3779 if (nalu !== null) { 3780 ret.push(nalu); 3781 } 3782 nal_start_idx+=size; 3783 } 3784 return ret; 3785 } 3786 3787 parseFragmentationUnit(rawData, header, dts, pts) { 3788 let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); 3789 let nal_start_idx = 0; 3790 let fu_header = data.getUint8(nal_start_idx); 3791 let is_start = (fu_header & 0x80) >>> 7; 3792 let is_end = (fu_header & 0x40) >>> 6; 3793 let payload_type = fu_header & 0x1F; 3794 let ret = null; 3795 3796 nal_start_idx++; 3797 let don = 0; 3798 if (NALU.FU_B === header.type) { 3799 don = data.getUint16(nal_start_idx); 3800 nal_start_idx += 2; 3801 } 3802 3803 if (is_start) { 3804 this.fragmented_nalu = new NALU(payload_type, header.nri, rawData.subarray(nal_start_idx), dts, pts); 3805 } 3806 if (this.fragmented_nalu && this.fragmented_nalu.ntype === payload_type) { 3807 if (!is_start) { 3808 this.fragmented_nalu.appendData(rawData.subarray(nal_start_idx)); 3809 } 3810 if (is_end) { 3811 ret = this.fragmented_nalu; 3812 this.fragmented_nalu = null; 3813 return ret; 3814 } 3815 } 3816 return null; 3817 } 3818 3819 onNALUFragment(rawData, dts, pts) { 3820 3821 let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); 3822 3823 let header = NALUAsm.parseNALHeader(data.getUint8(0)); 3824 3825 let nal_start_idx = 1; 3826 3827 let unit = null; 3828 if (header.type > 0 && header.type < 24) { 3829 unit = this.parseSingleNALUPacket(rawData.subarray(nal_start_idx), header, dts, pts); 3830 } else if (NALU.FU_A === header.type || NALU.FU_B === header.type) { 3831 unit = this.parseFragmentationUnit(rawData.subarray(nal_start_idx), header, dts, pts); 3832 } else if (NALU.STAP_A === header.type || NALU.STAP_B === header.type) { 3833 return this.parseAggregationPacket(rawData.subarray(nal_start_idx), header, dts, pts); 3834 } else { 3835 /* 30 - 31 is undefined, ignore those (RFC3984). */ 3836 Log.log('Undefined NAL unit, type: ' + header.type); 3837 return null; 3838 } 3839 if (unit) { 3840 return [unit]; 3841 } 3842 return null; 3843 } 3844 } 3845 3846 class AACFrame { 3847 3848 constructor(data, dts, pts) { 3849 this.dts = dts; 3850 this.pts = pts ? pts : this.dts; 3851 3852 this.data=data;//.subarray(offset); 3853 } 3854 3855 getData() { 3856 return this.data; 3857 } 3858 3859 getSize() { 3860 return this.data.byteLength; 3861 } 3862 } 3863 3864 // import {AACParser} from "../parsers/aac.js"; 3865 // TODO: asm.js 3866 class AACAsm { 3867 constructor() { 3868 this.config = null; 3869 } 3870 3871 onAACFragment(pkt) { 3872 let rawData = pkt.getPayload(); 3873 if (!pkt.media) { 3874 return null; 3875 } 3876 let data = new DataView(rawData.buffer, rawData.byteOffset, rawData.byteLength); 3877 3878 let sizeLength = Number(pkt.media.fmtp['sizelength'] || 0); 3879 let indexLength = Number(pkt.media.fmtp['indexlength'] || 0); 3880 let indexDeltaLength = Number(pkt.media.fmtp['indexdeltalength'] || 0); 3881 let CTSDeltaLength = Number(pkt.media.fmtp['ctsdeltalength'] || 0); 3882 let DTSDeltaLength = Number(pkt.media.fmtp['dtsdeltalength'] || 0); 3883 let RandomAccessIndication = Number(pkt.media.fmtp['randomaccessindication'] || 0); 3884 let StreamStateIndication = Number(pkt.media.fmtp['streamstateindication'] || 0); 3885 let AuxiliaryDataSizeLength = Number(pkt.media.fmtp['auxiliarydatasizelength'] || 0); 3886 3887 let configHeaderLength = 3888 sizeLength + Math.max(indexLength, indexDeltaLength) + CTSDeltaLength + DTSDeltaLength + 3889 RandomAccessIndication + StreamStateIndication + AuxiliaryDataSizeLength; 3890 3891 3892 let auHeadersLengthPadded = 0; 3893 let offset = 0; 3894 let ts = (Math.round(pkt.getTimestampMS()/1024) << 10) * 90000 / this.config.samplerate; 3895 if (0 !== configHeaderLength) { 3896 /* The AU header section is not empty, read it from payload */ 3897 let auHeadersLengthInBits = data.getUint16(0); // Always 2 octets, without padding 3898 auHeadersLengthPadded = 2 + (auHeadersLengthInBits>>>3) + ((auHeadersLengthInBits & 0x7)?1:0); // Add padding 3899 3900 //this.config = AACParser.parseAudioSpecificConfig(new Uint8Array(rawData, 0 , auHeadersLengthPadded)); 3901 // TODO: parse config 3902 let frames = []; 3903 let frameOffset=0; 3904 let bits = new BitArray(rawData.subarray(2 + offset)); 3905 let cts = 0; 3906 let dts = 0; 3907 for (let offset=0; offset<auHeadersLengthInBits;) { 3908 let size = bits.readBits(sizeLength); 3909 let idx = bits.readBits(offset?indexDeltaLength:indexLength); 3910 offset+=sizeLength+(offset?indexDeltaLength:indexLength)/*+2*/; 3911 if (/*ctsPresent &&*/ CTSDeltaLength) { 3912 let ctsPresent = bits.readBits(1); 3913 cts = bits.readBits(CTSDeltaLength); 3914 offset+=CTSDeltaLength; 3915 } 3916 if (/*dtsPresent && */DTSDeltaLength) { 3917 let dtsPresent = bits.readBits(1); 3918 dts = bits.readBits(DTSDeltaLength); 3919 offset+=CTSDeltaLength; 3920 } 3921 if (RandomAccessIndication) { 3922 bits.skipBits(1); 3923 offset+=1; 3924 } 3925 if (StreamStateIndication) { 3926 bits.skipBits(StreamStateIndication); 3927 offset+=StreamStateIndication; 3928 } 3929 frames.push(new AACFrame(rawData.subarray(auHeadersLengthPadded + frameOffset, auHeadersLengthPadded + frameOffset + size), ts+dts, ts+cts)); 3930 frameOffset+=size; 3931 } 3932 return frames; 3933 } else { 3934 let aacData = rawData.subarray(auHeadersLengthPadded); 3935 while (true) { 3936 if (aacData[offset] !=255) break; 3937 ++offset; 3938 } 3939 ++offset; 3940 return [new AACFrame(rawData.subarray(auHeadersLengthPadded+offset), ts)]; 3941 } 3942 } 3943 } 3944 3945 class RTPPayloadParser { 3946 3947 constructor() { 3948 this.h264parser = new RTPH264Parser(); 3949 this.aacparser = new RTPAACParser(); 3950 } 3951 3952 parse(rtp) { 3953 if (rtp.media.type=='video') { 3954 return this.h264parser.parse(rtp); 3955 } else if (rtp.media.type == 'audio') { 3956 return this.aacparser.parse(rtp); 3957 } 3958 return null; 3959 } 3960 } 3961 3962 class RTPH264Parser { 3963 constructor() { 3964 this.naluasm = new NALUAsm(); 3965 } 3966 3967 parse(rtp) { 3968 return this.naluasm.onNALUFragment(rtp.getPayload(), rtp.getTimestampMS()); 3969 } 3970 } 3971 3972 class RTPAACParser { 3973 3974 constructor() { 3975 this.scale = 1; 3976 this.asm = new AACAsm(); 3977 } 3978 3979 setConfig(conf) { 3980 this.asm.config = conf; 3981 } 3982 3983 parse(rtp) { 3984 return this.asm.onAACFragment(rtp); 3985 } 3986 } 3987 3988 class BaseClient { 3989 constructor(options={flush: 100}) { 3990 this.options = options; 3991 this.eventSource = new EventEmitter(); 3992 3993 Object.defineProperties(this, { 3994 sourceUrl: {value: null, writable: true}, // TODO: getter with validator 3995 paused: {value: true, writable: true}, 3996 seekable: {value: false, writable: true}, 3997 connected: {value: false, writable: true} 3998 }); 3999 4000 this._onData = ()=>{ 4001 if (this.connected) { 4002 while (this.transport.dataQueue.length) { 4003 this.onData(this.transport.dataQueue.pop()); 4004 } 4005 } 4006 }; 4007 this._onConnect = this.onConnected.bind(this); 4008 this._onDisconnect = this.onDisconnected.bind(this); 4009 } 4010 4011 static streamType() { 4012 return null; 4013 } 4014 4015 destroy() { 4016 this.detachTransport(); 4017 } 4018 4019 attachTransport(transport) { 4020 if (this.transport) { 4021 this.detachTransport(); 4022 } 4023 this.transport = transport; 4024 this.transport.eventSource.addEventListener('data', this._onData); 4025 this.transport.eventSource.addEventListener('connected', this._onConnect); 4026 this.transport.eventSource.addEventListener('disconnected', this._onDisconnect); 4027 } 4028 4029 detachTransport() { 4030 if (this.transport) { 4031 this.transport.eventSource.removeEventListener('data', this._onData); 4032 this.transport.eventSource.removeEventListener('connected', this._onConnect); 4033 this.transport.eventSource.removeEventListener('disconnected', this._onDisconnect); 4034 this.transport = null; 4035 } 4036 } 4037 reset() { 4038 4039 } 4040 4041 start() { 4042 Log.log('Client started'); 4043 this.paused = false; 4044 // this.startStreamFlush(); 4045 } 4046 4047 stop() { 4048 Log.log('Client paused'); 4049 this.paused = true; 4050 // this.stopStreamFlush(); 4051 } 4052 4053 seek(timeOffset) { 4054 4055 } 4056 4057 setSource(source) { 4058 this.stop(); 4059 this.endpoint = source; 4060 this.sourceUrl = source.urlpath; 4061 } 4062 4063 startStreamFlush() { 4064 this.flushInterval = setInterval(()=>{ 4065 if (!this.paused) { 4066 this.eventSource.dispatchEvent('flush'); 4067 } 4068 }, this.options.flush); 4069 } 4070 4071 stopStreamFlush() { 4072 clearInterval(this.flushInterval); 4073 } 4074 4075 onData(data) { 4076 4077 } 4078 4079 onConnected() { 4080 if (!this.seekable) { 4081 this.transport.dataQueue = []; 4082 this.eventSource.dispatchEvent('clear'); 4083 } 4084 this.connected = true; 4085 } 4086 4087 onDisconnected() { 4088 this.connected = false; 4089 } 4090 4091 queryCredentials() { 4092 return Promise.resolve(); 4093 } 4094 4095 setCredentials(user, password) { 4096 this.endpoint.user = user; 4097 this.endpoint.pass = password; 4098 this.endpoint.auth = `${user}:${password}`; 4099 } 4100 } 4101 4102 class AACParser { 4103 static get SampleRates() {return [ 4104 96000, 88200, 4105 64000, 48000, 4106 44100, 32000, 4107 24000, 22050, 4108 16000, 12000, 4109 11025, 8000, 4110 7350];} 4111 4112 // static Profile = [ 4113 // 0: Null 4114 // 1: AAC Main 4115 // 2: AAC LC (Low Complexity) 4116 // 3: AAC SSR (Scalable Sample Rate) 4117 // 4: AAC LTP (Long Term Prediction) 4118 // 5: SBR (Spectral Band Replication) 4119 // 6: AAC Scalable 4120 // ] 4121 4122 static parseAudioSpecificConfig(bytesOrBits) { 4123 let config; 4124 if (bytesOrBits.byteLength) { // is byteArray 4125 config = new BitArray(bytesOrBits); 4126 } else { 4127 config = bytesOrBits; 4128 } 4129 4130 let bitpos = config.bitpos+(config.src.byteOffset+config.bytepos)*8; 4131 let prof = config.readBits(5); 4132 this.codec = `mp4a.40.${prof}`; 4133 let sfi = config.readBits(4); 4134 if (sfi == 0xf) config.skipBits(24); 4135 let channels = config.readBits(4); 4136 4137 return { 4138 config: bitSlice(new Uint8Array(config.src.buffer), bitpos, bitpos+16), 4139 codec: `mp4a.40.${prof}`, 4140 samplerate: AACParser.SampleRates[sfi], 4141 channels: channels 4142 } 4143 } 4144 4145 static parseStreamMuxConfig(bytes) { 4146 // ISO_IEC_14496-3 Part 3 Audio. StreamMuxConfig 4147 let config = new BitArray(bytes); 4148 4149 if (!config.readBits(1)) { 4150 config.skipBits(14); 4151 return AACParser.parseAudioSpecificConfig(config); 4152 } 4153 } 4154 } 4155 4156 const LOG_TAG$4 = "rtsp:session"; 4157 const Log$9 = getTagged(LOG_TAG$4); 4158 4159 class RTSPSession { 4160 4161 constructor(client, sessionId) { 4162 this.state = null; 4163 this.client = client; 4164 this.sessionId = sessionId; 4165 this.url = this.getControlURL(); 4166 } 4167 4168 reset() { 4169 this.client = null; 4170 } 4171 4172 start() { 4173 return this.sendPlay(); 4174 } 4175 4176 stop() { 4177 return this.sendTeardown(); 4178 } 4179 4180 getControlURL() { 4181 let ctrl = this.client.sdp.getSessionBlock().control; 4182 if (Url.isAbsolute(ctrl)) { 4183 return ctrl; 4184 } else if (!ctrl || '*' === ctrl) { 4185 return this.client.contentBase; 4186 } else { 4187 return `${this.client.contentBase}${ctrl}`; 4188 } 4189 } 4190 4191 sendRequest(_cmd, _params = {}) { 4192 let params = {}; 4193 if (this.sessionId) { 4194 params['Session'] = this.sessionId; 4195 } 4196 Object.assign(params, _params); 4197 return this.client.sendRequest(_cmd, this.getControlURL(), params); 4198 } 4199 4200 async sendPlay(pos = 0) { 4201 this.state = RTSPClientSM.STATE_PLAY; 4202 let params = {}; 4203 let range = this.client.sdp.sessionBlock.range; 4204 if (range) { 4205 // TODO: seekable 4206 if (range[0] == -1) { 4207 range[0] = 0;// Do not handle now at the moment 4208 } 4209 // params['Range'] = `${range[2]}=${range[0]}-`; 4210 } 4211 let data = await this.sendRequest('PLAY', params); 4212 this.state = RTSPClientSM.STATE_PLAYING; 4213 return {data: data}; 4214 } 4215 4216 async sendPause() { 4217 if (!this.client.supports("PAUSE")) { 4218 return; 4219 } 4220 this.state = RTSPClientSM.STATE_PAUSE; 4221 await this.sendRequest("PAUSE"); 4222 this.state = RTSPClientSM.STATE_PAUSED; 4223 } 4224 4225 async sendTeardown() { 4226 if (this.state != RTSPClientSM.STATE_TEARDOWN) { 4227 this.state = RTSPClientSM.STATE_TEARDOWN; 4228 await this.sendRequest("TEARDOWN"); 4229 Log$9.log('RTSPClient: STATE_TEARDOWN'); 4230 ///this.client.connection.disconnect(); 4231 // TODO: Notify client 4232 } 4233 } 4234 } 4235 4236 const LOG_TAG$5 = "client:rtsp"; 4237 const Log$a = getTagged(LOG_TAG$5); 4238 4239 class RTSPClient extends BaseClient { 4240 constructor(options={flush: 200}) { 4241 super(options); 4242 this.clientSM = new RTSPClientSM(this); 4243 this.clientSM.ontracks = (tracks) => { 4244 this.eventSource.dispatchEvent('tracks', tracks); 4245 this.startStreamFlush(); 4246 }; 4247 this.sampleQueues={}; 4248 } 4249 4250 static streamType() { 4251 return 'rtsp'; 4252 } 4253 4254 setSource(url) { 4255 super.setSource(url); 4256 this.clientSM.setSource(url); 4257 } 4258 attachTransport(transport) { 4259 super.attachTransport(transport); 4260 this.clientSM.transport = transport; 4261 } 4262 4263 detachTransport() { 4264 super.detachTransport(); 4265 this.clientSM.transport = null; 4266 } 4267 4268 reset() { 4269 super.reset(); 4270 this.sampleQueues={}; 4271 } 4272 4273 destroy() { 4274 this.clientSM.destroy(); 4275 return super.destroy(); 4276 } 4277 4278 start() { 4279 super.start(); 4280 if (this.transport) { 4281 return this.transport.ready.then(() => { 4282 return this.clientSM.start(); 4283 }); 4284 } else { 4285 return Promise.reject("no transport attached"); 4286 } 4287 } 4288 4289 stop() { 4290 super.stop(); 4291 return this.clientSM.stop(); 4292 } 4293 4294 onData(data) { 4295 this.clientSM.onData(data); 4296 } 4297 4298 onConnected() { 4299 this.clientSM.onConnected(); 4300 super.onConnected(); 4301 } 4302 4303 onDisconnected() { 4304 super.onDisconnected(); 4305 this.clientSM.onDisconnected(); 4306 } 4307 } 4308 4309 class AuthError extends Error { 4310 constructor(msg) { 4311 super(msg); 4312 } 4313 } 4314 4315 class RTSPError extends Error { 4316 constructor(data) { 4317 super(data.msg); 4318 this.data = data; 4319 } 4320 } 4321 4322 class RTSPClientSM extends StateMachine { 4323 static get USER_AGENT() {return 'SFRtsp 0.3';} 4324 static get STATE_INITIAL() {return 1 << 0;} 4325 static get STATE_OPTIONS() {return 1 << 1;} 4326 static get STATE_DESCRIBE () {return 1 << 2;} 4327 static get STATE_SETUP() {return 1 << 3;} 4328 static get STATE_STREAMS() {return 1 << 4;} 4329 static get STATE_TEARDOWN() {return 1 << 5;} 4330 static get STATE_PLAY() {return 1 << 6;} 4331 static get STATE_PLAYING() {return 1 << 7;} 4332 static get STATE_PAUSE() {return 1 << 8;} 4333 static get STATE_PAUSED() {return 1 << 9;} 4334 // static STATE_PAUSED = 1 << 6; 4335 4336 constructor(parent) { 4337 super(); 4338 4339 this.parent = parent; 4340 this.transport = null; 4341 this.payParser = new RTPPayloadParser(); 4342 this.rtp_channels = new Set(); 4343 this.sessions = {}; 4344 this.ontracks = null; 4345 4346 this.addState(RTSPClientSM.STATE_INITIAL,{ 4347 }).addState(RTSPClientSM.STATE_OPTIONS, { 4348 activate: this.sendOptions, 4349 finishTransition: this.onOptions 4350 }).addState(RTSPClientSM.STATE_DESCRIBE, { 4351 activate: this.sendDescribe, 4352 finishTransition: this.onDescribe 4353 }).addState(RTSPClientSM.STATE_SETUP, { 4354 activate: this.sendSetup, 4355 finishTransition: this.onSetup 4356 }).addState(RTSPClientSM.STATE_STREAMS, { 4357 4358 }).addState(RTSPClientSM.STATE_TEARDOWN, { 4359 activate: ()=>{ 4360 this.started = false; 4361 }, 4362 finishTransition: ()=>{ 4363 return this.transitionTo(RTSPClientSM.STATE_INITIAL) 4364 } 4365 }).addTransition(RTSPClientSM.STATE_INITIAL, RTSPClientSM.STATE_OPTIONS) 4366 .addTransition(RTSPClientSM.STATE_INITIAL, RTSPClientSM.STATE_TEARDOWN) 4367 .addTransition(RTSPClientSM.STATE_OPTIONS, RTSPClientSM.STATE_DESCRIBE) 4368 .addTransition(RTSPClientSM.STATE_DESCRIBE, RTSPClientSM.STATE_SETUP) 4369 .addTransition(RTSPClientSM.STATE_SETUP, RTSPClientSM.STATE_STREAMS) 4370 .addTransition(RTSPClientSM.STATE_TEARDOWN, RTSPClientSM.STATE_INITIAL) 4371 // .addTransition(RTSPClientSM.STATE_STREAMS, RTSPClientSM.STATE_PAUSED) 4372 // .addTransition(RTSPClientSM.STATE_PAUSED, RTSPClientSM.STATE_STREAMS) 4373 .addTransition(RTSPClientSM.STATE_STREAMS, RTSPClientSM.STATE_TEARDOWN) 4374 // .addTransition(RTSPClientSM.STATE_PAUSED, RTSPClientSM.STATE_TEARDOWN) 4375 .addTransition(RTSPClientSM.STATE_SETUP, RTSPClientSM.STATE_TEARDOWN) 4376 .addTransition(RTSPClientSM.STATE_DESCRIBE, RTSPClientSM.STATE_TEARDOWN) 4377 .addTransition(RTSPClientSM.STATE_OPTIONS, RTSPClientSM.STATE_TEARDOWN); 4378 4379 this.reset(); 4380 4381 this.shouldReconnect = false; 4382 4383 // TODO: remove listeners 4384 // this.connection.eventSource.addEventListener('connected', ()=>{ 4385 // if (this.shouldReconnect) { 4386 // this.reconnect(); 4387 // } 4388 // }); 4389 // this.connection.eventSource.addEventListener('disconnected', ()=>{ 4390 // if (this.started) { 4391 // this.shouldReconnect = true; 4392 // } 4393 // }); 4394 // this.connection.eventSource.addEventListener('data', (data)=>{ 4395 // let channel = new DataView(data).getUint8(1); 4396 // if (this.rtp_channels.has(channel)) { 4397 // this.onRTP({packet: new Uint8Array(data, 4), type: channel}); 4398 // } 4399 // 4400 // }); 4401 } 4402 4403 destroy() { 4404 this.parent = null; 4405 } 4406 4407 setSource(url) { 4408 this.reset(); 4409 this.endpoint = url; 4410 this.url = `${url.protocol}://${url.location}${url.urlpath}`; 4411 } 4412 4413 onConnected() { 4414 if (this.rtpFactory) { 4415 this.rtpFactory = null; 4416 } 4417 if (this.shouldReconnect) { 4418 this.start(); 4419 } 4420 } 4421 4422 async onDisconnected() { 4423 this.reset(); 4424 this.shouldReconnect = true; 4425 await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); 4426 await this.transitionTo(RTSPClientSM.STATE_INITIAL); 4427 } 4428 4429 start() { 4430 if (this.currentState.name !== RTSPClientSM.STATE_STREAMS) { 4431 return this.transitionTo(RTSPClientSM.STATE_OPTIONS); 4432 } else { 4433 // TODO: seekable 4434 let promises = []; 4435 for (let session in this.sessions) { 4436 promises.push(this.sessions[session].sendPlay()); 4437 } 4438 return Promise.all(promises); 4439 } 4440 } 4441 4442 onData(data) { 4443 let channel = data[1]; 4444 if (this.rtp_channels.has(channel)) { 4445 this.onRTP({packet: data.subarray(4), type: channel}); 4446 } 4447 } 4448 4449 useRTPChannel(channel) { 4450 this.rtp_channels.add(channel); 4451 } 4452 4453 forgetRTPChannel(channel) { 4454 this.rtp_channels.delete(channel); 4455 } 4456 4457 stop() { 4458 this.shouldReconnect = false; 4459 let promises = []; 4460 for (let session in this.sessions) { 4461 promises.push(this.sessions[session].sendPause()); 4462 } 4463 return Promise.all(promises); 4464 // this.mse = null; 4465 } 4466 4467 async reset() { 4468 this.authenticator = ''; 4469 this.methods = []; 4470 this.tracks = []; 4471 this.rtpBuffer={}; 4472 for (let stream in this.streams) { 4473 this.streams[stream].reset(); 4474 } 4475 for (let session in this.sessions) { 4476 this.sessions[session].reset(); 4477 } 4478 this.streams={}; 4479 this.sessions={}; 4480 this.contentBase = ""; 4481 if (this.currentState) { 4482 if (this.currentState.name != RTSPClientSM.STATE_INITIAL) { 4483 await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); 4484 await this.transitionTo(RTSPClientSM.STATE_INITIAL); 4485 } 4486 } else { 4487 await this.transitionTo(RTSPClientSM.STATE_INITIAL); 4488 } 4489 this.sdp = null; 4490 this.interleaveChannelIndex = 0; 4491 this.session = null; 4492 this.timeOffset = {}; 4493 this.lastTimestamp = {}; 4494 } 4495 4496 async reconnect() { 4497 //this.parent.eventSource.dispatchEvent('clear'); 4498 await this.reset(); 4499 if (this.currentState.name != RTSPClientSM.STATE_INITIAL) { 4500 await this.transitionTo(RTSPClientSM.STATE_TEARDOWN); 4501 return this.transitionTo(RTSPClientSM.STATE_OPTIONS); 4502 } else { 4503 return this.transitionTo(RTSPClientSM.STATE_OPTIONS); 4504 } 4505 } 4506 4507 supports(method) { 4508 return this.methods.includes(method) 4509 } 4510 4511 parse(_data) { 4512 Log$a.debug(_data); 4513 let d=_data.split('\r\n\r\n'); 4514 let parsed = MessageBuilder.parse(d[0]); 4515 let len = Number(parsed.headers['content-length']); 4516 if (len) { 4517 parsed.body = d[1]; 4518 } else { 4519 parsed.body=""; 4520 } 4521 return parsed 4522 } 4523 4524 sendRequest(_cmd, _host, _params={}, _payload=null) { 4525 this.cSeq++; 4526 Object.assign(_params, { 4527 CSeq: this.cSeq, 4528 'User-Agent': RTSPClientSM.USER_AGENT 4529 }); 4530 if (this.authenticator) { 4531 _params['Authorization'] = this.authenticator(_cmd); 4532 } 4533 return this.send(MessageBuilder.build(_cmd, _host, _params, _payload), _cmd).catch((e)=>{ 4534 if ((e instanceof AuthError) && !_params['Authorization'] ) { 4535 return this.sendRequest(_cmd, _host, _params, _payload); 4536 } else { 4537 throw e; 4538 } 4539 }); 4540 } 4541 4542 async send(_data, _method) { 4543 if (this.transport) { 4544 try { 4545 await this.transport.ready; 4546 } catch(e) { 4547 this.onDisconnected(); 4548 throw e; 4549 } 4550 Log$a.debug(_data); 4551 let response = await this.transport.send(_data); 4552 let parsed = this.parse(response); 4553 // TODO: parse status codes 4554 if (parsed.code == 401 /*&& !this.authenticator */) { 4555 Log$a.debug(parsed.headers['www-authenticate']); 4556 let auth = parsed.headers['www-authenticate']; 4557 let method = auth.substring(0, auth.indexOf(' ')); 4558 auth = auth.substr(method.length+1); 4559 let chunks = auth.split(','); 4560 4561 let ep = this.parent.endpoint; 4562 if (!ep.user || !ep.pass) { 4563 try { 4564 await this.parent.queryCredentials.call(this.parent); 4565 } catch (e) { 4566 throw new AuthError(); 4567 } 4568 } 4569 4570 if (method.toLowerCase() == 'digest') { 4571 let parsedChunks = {}; 4572 for (let chunk of chunks) { 4573 let c = chunk.trim(); 4574 let [k,v] = c.split('='); 4575 parsedChunks[k] = v.substr(1, v.length-2); 4576 } 4577 this.authenticator = (_method)=>{ 4578 let ep = this.parent.endpoint; 4579 let ha1 = md5(`${ep.user}:${parsedChunks.realm}:${ep.pass}`); 4580 let ha2 = md5(`${_method}:${this.url}`); 4581 let response = md5(`${ha1}:${parsedChunks.nonce}:${ha2}`); 4582 let tail=''; // TODO: handle other params 4583 return `Digest username="${ep.user}", realm="${parsedChunks.realm}", nonce="${parsedChunks.nonce}", uri="${this.url}", response="${response}"${tail}`; 4584 }; 4585 } else { 4586 this.authenticator = ()=>{return `Basic ${btoa(this.parent.endpoint.auth)}`;}; 4587 } 4588 4589 throw new AuthError(parsed); 4590 } 4591 if (parsed.code >= 300) { 4592 Log$a.error(parsed.statusLine); 4593 throw new RTSPError({msg: `RTSP error: ${parsed.code} ${parsed.statusLine}`, parsed: parsed}); 4594 } 4595 return parsed; 4596 } else { 4597 return Promise.reject("No transport attached"); 4598 } 4599 } 4600 4601 sendOptions() { 4602 this.reset(); 4603 this.started = true; 4604 this.cSeq = 0; 4605 return this.sendRequest('OPTIONS', '*', {}); 4606 } 4607 4608 onOptions(data) { 4609 this.methods = data.headers['public'].split(',').map((e)=>e.trim()); 4610 this.transitionTo(RTSPClientSM.STATE_DESCRIBE); 4611 } 4612 4613 sendDescribe() { 4614 return this.sendRequest('DESCRIBE', this.url, { 4615 'Accept': 'application/sdp' 4616 }).then((data)=>{ 4617 this.sdp = new SDPParser(); 4618 return this.sdp.parse(data.body).catch(()=>{ 4619 throw new Error("Failed to parse SDP"); 4620 }).then(()=>{return data;}); 4621 }); 4622 } 4623 4624 onDescribe(data) { 4625 this.contentBase = data.headers['content-base'] || this.url;// `${this.endpoint.protocol}://${this.endpoint.location}${this.endpoint.urlpath}/`; 4626 this.tracks = this.sdp.getMediaBlockList(); 4627 this.rtpFactory = new RTPFactory(this.sdp); 4628 4629 Log$a.log('SDP contained ' + this.tracks.length + ' track(s). Calling SETUP for each.'); 4630 4631 if (data.headers['session']) { 4632 this.session = data.headers['session']; 4633 } 4634 4635 if (!this.tracks.length) { 4636 throw new Error("No tracks in SDP"); 4637 } 4638 4639 this.transitionTo(RTSPClientSM.STATE_SETUP); 4640 } 4641 4642 sendSetup() { 4643 let streams=[]; 4644 let lastPromise = null; 4645 4646 // TODO: select first video and first audio tracks 4647 for (let track_type of this.tracks) { 4648 Log$a.log("setup track: "+track_type); 4649 // if (track_type=='audio') continue; 4650 // if (track_type=='video') continue; 4651 let track = this.sdp.getMediaBlock(track_type); 4652 if (!PayloadType.string_map[track.rtpmap[track.fmt[0]].name]) continue; 4653 4654 this.streams[track_type] = new RTSPStream(this, track); 4655 let setupPromise = this.streams[track_type].start(lastPromise); 4656 lastPromise = setupPromise; 4657 this.parent.sampleQueues[PayloadType.string_map[track.rtpmap[track.fmt[0]].name]]=[]; 4658 this.rtpBuffer[track.fmt[0]]=[]; 4659 streams.push(setupPromise.then(({track, data})=>{ 4660 this.timeOffset[track.fmt[0]] = 0; 4661 try { 4662 let rtp_info = data.headers["rtp-info"].split(';'); 4663 for (let chunk of rtp_info) { 4664 let [key, val] = chunk.split("="); 4665 if (key === "rtptime") { 4666 this.timeOffset[track.fmt[0]] = 0;//Number(val); 4667 } 4668 } 4669 } catch (e) { 4670 // new Date().getTime(); 4671 } 4672 let params = { 4673 timescale: 0, 4674 scaleFactor: 0 4675 }; 4676 if (track.fmtp['sprop-parameter-sets']) { 4677 let sps_pps = track.fmtp['sprop-parameter-sets'].split(','); 4678 params = { 4679 sps:base64ToArrayBuffer(sps_pps[0]), 4680 pps:base64ToArrayBuffer(sps_pps[1]) 4681 }; 4682 } else if (track.fmtp['config']) { 4683 let config = track.fmtp['config']; 4684 this.has_config = track.fmtp['cpresent']!='0'; 4685 let generic = track.rtpmap[track.fmt[0]].name == 'MPEG4-GENERIC'; 4686 if (generic) { 4687 params={config: 4688 AACParser.parseAudioSpecificConfig(hexToByteArray(config)) 4689 }; 4690 this.payParser.aacparser.setConfig(params.config); 4691 } else if (config) { 4692 // todo: parse audio specific config for mpeg4-generic 4693 params={config: 4694 AACParser.parseStreamMuxConfig(hexToByteArray(config)) 4695 }; 4696 this.payParser.aacparser.setConfig(params.config); 4697 } 4698 } 4699 params.duration = this.sdp.sessionBlock.range?this.sdp.sessionBlock.range[1]-this.sdp.sessionBlock.range[0]:1; 4700 this.parent.seekable = (params.duration > 1); 4701 let res = { 4702 track: track, 4703 offset: this.timeOffset[track.fmt[0]], 4704 type: PayloadType.string_map[track.rtpmap[track.fmt[0]].name], 4705 params: params, 4706 duration: params.duration 4707 }; 4708 console.log(res, this.timeOffset); 4709 let session = data.headers.session.split(';')[0]; 4710 if (!this.sessions[session]) { 4711 this.sessions[session] = new RTSPSession(this, session); 4712 } 4713 return res; 4714 })); 4715 } 4716 return Promise.all(streams).then((tracks)=>{ 4717 let sessionPromises = []; 4718 for (let session in this.sessions) { 4719 sessionPromises.push(this.sessions[session].start()); 4720 } 4721 return Promise.all(sessionPromises).then(()=>{ 4722 if (this.ontracks) { 4723 this.ontracks(tracks); 4724 } 4725 }) 4726 }).catch((e)=>{ 4727 console.error(e); 4728 this.stop(); 4729 this.reset(); 4730 }); 4731 } 4732 4733 onSetup() { 4734 this.transitionTo(RTSPClientSM.STATE_STREAMS); 4735 } 4736 4737 onRTP(_data) { 4738 if (!this.rtpFactory) return; 4739 4740 let rtp = this.rtpFactory.build(_data.packet, this.sdp); 4741 if (!rtp.type) { 4742 return; 4743 } 4744 4745 if (this.timeOffset[rtp.pt] === undefined) { 4746 //console.log(rtp.pt, this.timeOffset[rtp.pt]); 4747 this.rtpBuffer[rtp.pt].push(rtp); 4748 return; 4749 } 4750 4751 if (this.lastTimestamp[rtp.pt] === undefined) { 4752 this.lastTimestamp[rtp.pt] = rtp.timestamp-this.timeOffset[rtp.pt]; 4753 } 4754 4755 let queue = this.rtpBuffer[rtp.pt]; 4756 queue.push(rtp); 4757 4758 while (queue.length) { 4759 let rtp = queue.shift(); 4760 4761 rtp.timestamp = rtp.timestamp-this.timeOffset[rtp.pt]-this.lastTimestamp[rtp.pt]; 4762 // TODO: overflow 4763 // if (rtp.timestamp < 0) { 4764 // rtp.timestamp = (rtp.timestamp + Number.MAX_SAFE_INTEGER) % 0x7fffffff; 4765 // } 4766 if (rtp.media) { 4767 let pay = this.payParser.parse(rtp); 4768 if (pay) { 4769 // if (typeof pay == typeof []) { 4770 this.parent.sampleQueues[rtp.type].push(pay); 4771 // } else { 4772 // this.parent.sampleQueues[rtp.type].push([pay]); 4773 // } 4774 } 4775 } 4776 } 4777 // this.remuxer.feedRTP(); 4778 } 4779 } 4780 4781 const Log$b = getTagged('wsplayer'); 4782 4783 class WSPlayer { 4784 4785 constructor(node, opts) { 4786 if (typeof node == typeof '') { 4787 this.videoElement = document.getElementById(node); 4788 } else { 4789 this.videoElement = node; 4790 } 4791 4792 this.eventSource = new EventEmitter(); 4793 this.type = "rtsp"; 4794 4795 // TODO: opts 处理 4796 // this.videoElement.addEventListener('play', ()=>{ 4797 // if (!this.isPlaying()) { 4798 // this.client.start(); 4799 // } 4800 // }, false); 4801 4802 this.videoElement.addEventListener('abort', () => { 4803 this.unload(); 4804 }, false); 4805 } 4806 4807 isPlaying() { 4808 return !(this.player.paused || this.client.paused); 4809 } 4810 4811 // 加载并启动 4812 async load(wsurl) { 4813 await this.unload(); 4814 4815 this.url = wsurl; 4816 try { 4817 this.endpoint = Url.parse(wsurl); 4818 this.endpoint.protocol="rtsp"; 4819 } catch (e) { 4820 return; 4821 } 4822 this.transport = new WebsocketTransport(this.endpoint, this.type, {socket:wsurl}); 4823 this.client = new RTSPClient(); 4824 this.remuxer = new Remuxer(this.videoElement); 4825 4826 this.remuxer.attachClient(this.client); 4827 4828 this.client.attachTransport(this.transport); 4829 this.client.setSource(this.endpoint); 4830 4831 // 确保video元素可用 4832 this.videoElement.src = "rtsp://placehold"; 4833 this.client.start().catch((e)=>{ 4834 this.errorHandler(e); 4835 }); 4836 } 4837 4838 // 卸载 4839 async unload() { 4840 if (this.remuxer) { 4841 this.remuxer.destroy(); 4842 this.remuxer = null; 4843 } 4844 if (this.client) { 4845 this.client.stop(); 4846 await this.client.destroy(); 4847 this.client = null; 4848 } 4849 if (this.transport) { 4850 await this.transport.destroy(); 4851 this.transport = null; 4852 } 4853 // 重置video元素 4854 this.videoElement.src = "rtsp://placehold"; 4855 } 4856 4857 errorHandler(ev) { 4858 this.eventSource.dispatchEvent('error', ev); 4859 } 4860 } 4861 4862 setDefaultLogLevel(LogLevel.Debug); 4863 getTagged("transport:ws").setLevel(LogLevel.Debug); 4864 getTagged("client:rtsp").setLevel(LogLevel.Debug); 4865 getTagged("mse").setLevel(LogLevel.Debug); 4866 4867 // factory method 4868 // node = video element 4869 function createPlayer(node, optionalConfig) { 4870 return new WSPlayer(node, optionalConfig) 4871 } 4872 4873 // interfaces 4874 let rtspjs = {}; 4875 rtspjs.createPlayer = createPlayer; 4876 rtspjs.getLogger = getTagged; 4877 window.rtspjs = rtspjs; 4878 4879 }());