├── .gitignore ├── MIT-LICENSE.txt ├── README.md ├── bilibili.js ├── damoo.js ├── flashBlocker.js ├── flvdemux.js ├── http.js ├── index.js ├── jsonp.js ├── mama-hd.safariextension ├── Info.plist ├── bg.html └── script.js ├── mama-hd ├── background.js ├── icon128.png ├── icon48.png ├── manifest.json └── test.html ├── mediaSource.js ├── mp4mux.js ├── package.json ├── packcrx.sh ├── player.js ├── promise.js ├── screenshot-1280x800.png ├── screenshot.png ├── tudou.js ├── webpack.config.js ├── webpack.safari.config.js └── youku.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | npm-debug.log -------------------------------------------------------------------------------- /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 nareix https://github.com/nareix/mama-hd 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 妈妈再也不用担心我的 MacBook 发热了计划之 1080P 2 | 3 | 『[妈妈计划](https://github.com/zythum/mama2/)』用于解决在看视频网站时 MacBook 发(guang)热(gao)严(tai)重(duo)的问题,使用 video 来替换原来的 flash 播放器(对 Windows 下的 Flash 也有用) 4 | 5 | 本分支计划实现了用原生 video 实时转码播放 Bilibili/土豆/优酷 的 1080P flv 格式视频: 6 | 7 | ![](https://raw.githubusercontent.com/nareix/mama-hd/master/screenshot.png) 8 | 9 | # 使用 10 | 11 | [Chrome 商店](https://chrome.google.com/webstore/detail/mama-hd/hoihfdmeofbkbbjpieicemdhmjgfdihm?hl=zh-CN&gl=ID) 12 | 13 | 要求 Chrome 版本大于 48 14 | 15 | 打开一个视频页面,点击右上角的图标就可以播放了 16 | 17 | ⌘ + Enter 全屏 / ↑ ↓ 音量 / ← → 快进快退 / M 静音 / Space 暂停 / D 打开弹幕 18 | 19 | 非 Flv 视频建议使用[妈妈计划](https://github.com/zythum/mama2/) 20 | 21 | # 原理 22 | 23 | 使用 Media Source Extensions 播放 fmp4(Fragmented MP4),这种 mp4 可以随意取一个片段播放,不需要全局的索引信息,dash.js 就是基于它做的视频直播。而 Chrome 里也有速度很快的二进制操作(Uint8Array,底层是零拷贝),所以只要把 flv 在浏览器里面实时转换成 fmp4 就可以了。 24 | 25 | **经过实测,平均转码 10s 的视频只需要 20~40ms 左右(i5 2.9G),CPU 占用与播放相比可以忽略。** 26 | 27 | 弹幕用 canvas 实现,CPU 占用同样很低。 28 | 29 | # 使用同样原理的项目 30 | 31 | https://github.com/Bilibili/flv.js 32 | 33 | # 特性 34 | 35 | - [x] 支持 Bilibili 36 | - [x] 支持土豆/优酷 37 | - [x] 快速启动(不等待全部 flv metadata 加载完毕,只加载完第一段就开始播放) 38 | - [x] 优化进度条拖动 39 | - [ ] 支持 mp4demux(少量 B 站视频和搜狐视频是分段 mp4) 40 | - [ ] 支持视频下载 41 | - [x] 支持 Bilibili 弹幕 42 | - [x] 支持土豆弹幕 43 | - [ ] 优化转码速度 44 | 45 | # ChangeLog 46 | 47 | 0.91 48 | 49 | 修复了各种播放不能/卡死的 BUG,实现了弹幕 50 | 51 | # 感谢 52 | 53 | 妈妈计划(等测试稳定了求合并到主分支) 54 | 55 | you-get 56 | 57 | mux.js 58 | 59 | [Vczh]粉丝群 [咸鱼]Square 的建议 60 | 61 | @zsxsoft 的建议以及弹幕实现(https://github.com/zsxsoft/danmu-client) 62 | 63 | @jamesliu96 的弹幕实现(https://github.com/jamesliu96/Damoo) 64 | 65 | @cnbeining 的 B 站新解析(2016-8) 66 | 67 | ## License 68 | 69 | MIT 70 | -------------------------------------------------------------------------------- /bilibili.js: -------------------------------------------------------------------------------- 1 | 2 | var md5 = require('blueimp-md5'); 3 | const SECRETKEY_MINILOADER = '1c15888dc316e05a15fdd0a02ed6584f'; 4 | let interfaceUrl = (cid, ts) => `cid=${cid}&player=1&ts=${ts}`; 5 | let calcSign = (cid, ts) => md5(`${interfaceUrl(cid,ts)}${SECRETKEY_MINILOADER}`); 6 | 7 | exports.calcSign = calcSign; 8 | exports.testUrl = url => url.match('bilibili.com/') 9 | 10 | exports.getVideos = (url) => { 11 | return fetch(url, {credentials: 'include'}).then(res => res.text()).then(res => { 12 | let cid = res.match(/cid=(\d+)/); 13 | if (cid) 14 | return cid[1]; 15 | }).then(function(cid) { 16 | if (!cid) 17 | return; 18 | 19 | let ts = Math.ceil(Date.now()/1000) 20 | return fetch(`http://interface.bilibili.com/playurl?${interfaceUrl(cid,ts)}&sign=${calcSign(cid,ts)}`) 21 | .then(res => res.text()).then(res => { 22 | let parser = new DOMParser(); 23 | let doc = parser.parseFromString(res, 'text/xml'); 24 | let array = x => Array.prototype.slice.call(x); 25 | let duration = 0.0; 26 | array(doc.querySelectorAll('durl > length')).forEach(len => duration += +len.textContent); 27 | let src = array(doc.querySelectorAll('durl > url')).map(url => url.textContent ); 28 | return { 29 | duration: duration/1000.0, 30 | src: src, 31 | commentUrl: 'http://comment.bilibili.com/'+cid+'.xml', 32 | } 33 | }) 34 | }); 35 | } 36 | 37 | function pad(num, n) { 38 | if (num.length >= n) 39 | return num; 40 | return (Array(n).join(0) + num).slice(-n) 41 | } 42 | let colorDec2Hex = (x) => '#'+pad((x||0).toString(16), 6) 43 | exports.colorDec2Hex = colorDec2Hex; 44 | 45 | let getDamooRaw = url => { 46 | return fetch(url).then(res => res.text()).then(res => { 47 | let parser = new DOMParser(); 48 | let doc = parser.parseFromString(res, 'text/xml'); 49 | let array = x => Array.prototype.slice.call(x); 50 | return array(doc.querySelectorAll('i > d')).map((d, i) => { 51 | let p = d.getAttribute('p').split(','); 52 | if (p[5] == 2) 53 | return; 54 | let pos; 55 | switch (+p[1]) { 56 | case 4: pos = 'bottom'; break; 57 | case 5: pos = 'top'; break; 58 | } 59 | //console.log(p[1], d.textContent); 60 | return {time: parseFloat(p[0]), pos, color:colorDec2Hex(+p[3]), text: d.textContent}; 61 | }).filter(x => x).sort((a,b) => a.time-b.time); 62 | return arr; 63 | }) 64 | } 65 | exports.getDamooRaw = getDamooRaw; 66 | 67 | exports.getAllDamoo = (res) => { 68 | return getDamooRaw(res.commentUrl); 69 | } 70 | 71 | -------------------------------------------------------------------------------- /damoo.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Damoo - HTML5 Danmaku Engine v2.1.9 3 | * https://github.com/jamesliu96/Damoo 4 | * 5 | * Copyright (c) 2015-2016 James Liu 6 | * Released under the MIT license 7 | */ 8 | 9 | var Damoo = function({container, fontSize, fontFamily}) { 10 | fontFamily = fontFamily || 'Arial'; 11 | this.canvas = new Canvas(container, fontSize, fontFamily); 12 | this.thread = new Thread(() => Math.floor(container.offsetHeight/fontSize-4)); 13 | }; 14 | 15 | var _preload = function(d, f) { 16 | var cvs = document.createElement('canvas'); 17 | var ctx = cvs.getContext('2d'); 18 | ctx.font = f; 19 | cvs.width = ctx.measureText(d.text).width; 20 | cvs.height = f.size * 1.5; 21 | ctx.font = f; 22 | ctx.textAlign = "start"; 23 | ctx.textBaseline = "top"; 24 | let shadow = d.shadow || {color: '#000'}; 25 | if (shadow) { 26 | ctx.shadowOffsetX = 1; 27 | ctx.shadowOffsetY = 1; 28 | ctx.shadowColor = shadow.color; 29 | } 30 | ctx.fillStyle = "#fff"; 31 | ctx.fillStyle = d.color; 32 | ctx.fillText(d.text, 0, 0); 33 | return cvs; 34 | }; 35 | 36 | var _RAF = function(cb) { return setTimeout(cb, 1000/10) }; 37 | var _CAF = function(id) { clearTimeout(id); }; 38 | 39 | Damoo.prototype.curtime = function() { 40 | if (this._curtimeBase) { 41 | return this._curtimeBase+(Date.now()-this._curtimeStart)/1e3; 42 | } 43 | return Date.now()/1e3; 44 | } 45 | 46 | Damoo.prototype.emit = function(d) { 47 | if (!this.visible) 48 | return; 49 | 50 | if ("string" === typeof d) { 51 | d = { text: d }; 52 | } 53 | var cvs = _preload(d, this.canvas.font); 54 | 55 | var fixed; 56 | var index; 57 | if (d.pos == 'top') { 58 | fixed = true; 59 | index = this.thread.allocFixedIndex(1); 60 | } else if (d.pos == 'bottom') { 61 | fixed = true; 62 | index = this.thread.allocFixedIndex(-1); 63 | } else { 64 | index = this.thread.allocIndex(); 65 | } 66 | 67 | this.thread.push({ 68 | canvas: cvs, 69 | fixed, index, 70 | pos: d.pos, 71 | displaytime: d.time || (fixed ? 5 : 10), 72 | timestart: this.curtime(), 73 | y: this.canvas.font.size*index, 74 | }); 75 | return this; 76 | }; 77 | 78 | Damoo.prototype.render = function() { 79 | var time = this.curtime(); 80 | 81 | this.canvas.clear(); 82 | this.thread.forEach(d => { 83 | var elapsed = time-d.timestart; 84 | if (elapsed > d.displaytime) { 85 | this.thread.remove(d); 86 | return; 87 | } 88 | var x; 89 | if (d.fixed) { 90 | x = (this.canvas.width-d.canvas.width)/2; 91 | } else { 92 | var w = this.canvas.width+d.canvas.width; 93 | x = this.canvas.width-w*(elapsed/d.displaytime); 94 | } 95 | this.canvas.context.drawImage(d.canvas, x, d.y); 96 | }); 97 | this._afid = _RAF(() => this.render()); 98 | } 99 | 100 | Damoo.prototype.clear = function() { 101 | this.thread.empty(); 102 | }; 103 | 104 | Damoo.prototype.updateState = function() { 105 | if (this.playing && this.visible) { 106 | if (this._afid == null) { 107 | this.render(); 108 | } 109 | } else { 110 | if (this._afid) { 111 | _CAF(this._afid); 112 | this._afid = null; 113 | } 114 | } 115 | } 116 | 117 | Damoo.prototype.synctime = function(time) { 118 | this._curtimeBase = time; 119 | this._curtimeStart = Date.now(); 120 | } 121 | 122 | Damoo.prototype.suspend = function() { 123 | if (this.playing) { 124 | this.playing = false; 125 | this.updateState(); 126 | } 127 | }; 128 | 129 | Damoo.prototype.resume = function() { 130 | if (!this.playing) { 131 | this.playing = true; 132 | this.updateState(); 133 | } 134 | }; 135 | 136 | Damoo.prototype.show = function() { 137 | if (!this.visible) { 138 | this.visible = true; 139 | this.canvas.container.appendChild(this.canvas.layer); 140 | this.updateState(); 141 | } 142 | }; 143 | 144 | Damoo.prototype.hide = function() { 145 | if (this.visible) { 146 | this.visible = false; 147 | this.canvas.container.removeChild(this.canvas.layer); 148 | this.updateState(); 149 | } 150 | }; 151 | 152 | var Canvas = function(container, fontSize, fontFamily) { 153 | this.container = container; 154 | this.font = new Font(fontSize, fontFamily); 155 | this.layer = document.createElement('canvas'); 156 | this.layer.style.position = 'absolute'; 157 | this.layer.style.left = 0; 158 | this.layer.style.top = 0; 159 | this.context = this.layer.getContext('2d'); 160 | let resize = () => { 161 | this.width = container.offsetWidth; 162 | this.height = container.offsetHeight; 163 | this.layer.width = this.width; 164 | this.layer.height = this.height; 165 | } 166 | window.addEventListener('resize', resize); 167 | resize(); 168 | }; 169 | 170 | Canvas.prototype.clear = function() { 171 | this.context.clearRect(0, 0, this.width, this.height); 172 | }; 173 | 174 | var Font = function(s, f) { 175 | this.size = s; 176 | this.family = f || "sans-serif"; 177 | }; 178 | 179 | Font.prototype.toString = function() { 180 | return this.size + "px " + this.family; 181 | }; 182 | 183 | var Thread = function(rows) { 184 | this.rows = rows; 185 | this.empty(); 186 | }; 187 | 188 | Thread.prototype.allocFixedIndex = function(inc) { 189 | var n = this.rows(); 190 | if (inc > 0) { 191 | if (this.fixedTop > n) { 192 | this.fixedTop = 0; 193 | } 194 | return this.fixedTop++; 195 | } else { 196 | if (this.fixedBottom > n || this.fixedBottom < 0) { 197 | this.fixedBottom = n; 198 | } 199 | return this.fixedBottom--; 200 | } 201 | }; 202 | 203 | Thread.prototype.allocIndex = function() { 204 | if (this.index >= this.rows()) { 205 | this.index = 0; 206 | } 207 | return this.index++; 208 | }; 209 | 210 | Thread.prototype.push = function(d) { 211 | this.pool.add(d); 212 | }; 213 | 214 | Thread.prototype.forEach = function(fn) { 215 | this.pool.forEach(fn); 216 | }; 217 | 218 | Thread.prototype.remove = function(d) { 219 | if (d.pos == 'top') { 220 | if (d.index < this.fixedTop) { 221 | this.fixedTop = d.index; 222 | } 223 | } else if (d.pos == 'bottom') { 224 | if (d.index > this.fixedBottom) { 225 | this.fixedBottom = d.index; 226 | } 227 | } else { 228 | if (d.index < this.index) { 229 | this.index = d.index; 230 | } 231 | } 232 | this.pool.delete(d); 233 | }; 234 | 235 | Thread.prototype.empty = function() { 236 | this.index = 0; 237 | this.fixedTop = 0; 238 | this.fixedBottom = this.rows(); 239 | this.pool = new Set(); 240 | }; 241 | 242 | module.exports = Damoo; 243 | 244 | -------------------------------------------------------------------------------- /flashBlocker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 用于屏蔽页面上的所有flash 3 | */ 4 | var flashText = '
Flash
'; 5 | 6 | var count = 0; 7 | var flashBlocks = {}; 8 | //点击时间触发 9 | var click2ShowFlash = function(e){ 10 | var index = this.getAttribute('data-flash-index'); 11 | var flash = flashBlocks[index]; 12 | flash.setAttribute('data-flash-show','isshow'); 13 | this.parentNode.insertBefore(flash, this); 14 | this.parentNode.removeChild(this); 15 | this.removeEventListener('click', click2ShowFlash, false); 16 | }; 17 | 18 | var createAPlaceHolder = function(flash, width, height){ 19 | var index = count++; 20 | var style = document.defaultView.getComputedStyle(flash, null); 21 | var positionType = style.position; 22 | positionType = positionType === 'static' ? 'relative' : positionType; 23 | var margin = style['margin']; 24 | var display = style['display'] == 'inline' ? 'inline-block' : style['display']; 25 | var style = [ 26 | '', 27 | 'width:' + width +'px', 28 | 'height:' + height +'px', 29 | 'position:' + positionType, 30 | 'margin:' + margin, 31 | 'display:' + display, 32 | 'margin:0', 33 | 'padding:0', 34 | 'border:0', 35 | 'border-radius:1px', 36 | 'cursor:pointer', 37 | 'background:-webkit-linear-gradient(top, rgba(240,240,240,1)0%,rgba(220,220,220,1)100%)', 38 | '' 39 | ] 40 | flashBlocks[index] = flash; 41 | var placeHolder = document.createElement('div'); 42 | placeHolder.setAttribute('title', '点我还原Flash'); 43 | placeHolder.setAttribute('data-flash-index', '' + index); 44 | flash.parentNode.insertBefore(placeHolder, flash); 45 | flash.parentNode.removeChild(flash); 46 | placeHolder.addEventListener('click', click2ShowFlash, false); 47 | placeHolder.style.cssText += style.join(';'); 48 | placeHolder.innerHTML = flashText; 49 | return placeHolder; 50 | }; 51 | 52 | var parseFlash = function(target){ 53 | if(target instanceof HTMLObjectElement) { 54 | if(target.innerHTML.trim() == '') return; 55 | if(target.getAttribute("classid") && !/^java:/.test(target.getAttribute("classid"))) return; 56 | } else if(!(target instanceof HTMLEmbedElement)) return; 57 | 58 | var width = target.offsetWidth; 59 | var height = target.offsetHeight; 60 | 61 | if(width > 160 && height > 60){ 62 | createAPlaceHolder(target, width, height); 63 | } 64 | }; 65 | 66 | var handleBeforeLoadEvent = function(e){ 67 | var target = e.target 68 | if(target.getAttribute('data-flash-show') == 'isshow') return; 69 | parseFlash(target); 70 | }; 71 | 72 | module.exports = function() { 73 | var embeds = document.getElementsByTagName('embed'); 74 | var objects = document.getElementsByTagName('object'); 75 | for(var i=0,len=objects.length; i createAPlaceHolder(x, x.offsetWidth, x.offsetHeight)); 82 | } 83 | // document.addEventListener("beforeload", handleBeforeLoadEvent, true); 84 | -------------------------------------------------------------------------------- /flvdemux.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict' 3 | 4 | class ByteReader { 5 | constructor(buf) { 6 | this.buf = buf; 7 | this.pos = 0; 8 | } 9 | 10 | len() { 11 | return this.buf.byteLength-this.pos; 12 | } 13 | 14 | readBEUint(len) { 15 | if (this.pos >= this.buf.byteLength) 16 | throw new Error('EOF'); 17 | let v = 0; 18 | for (let i = this.pos; i < this.pos+len; i++) { 19 | v <<= 8; 20 | v |= this.buf[i]; 21 | } 22 | this.pos += len; 23 | return v; 24 | } 25 | 26 | readBEInt(len) { 27 | let i = this.readBEUint(len); 28 | let topbit = 1<<(len*8-1); 29 | if (i & topbit) { 30 | return -((topbit<<1)-i); 31 | } else { 32 | return i; 33 | } 34 | } 35 | 36 | readBuf(len) { 37 | if (this.pos >= this.buf.byteLength) 38 | throw new Error('EOF'); 39 | let b = this.buf.slice(this.pos, this.pos+len); 40 | this.pos += len; 41 | return b; 42 | } 43 | 44 | skip(len) { 45 | if (this.pos >= this.buf.byteLength) 46 | throw new Error('EOF'); 47 | this.pos += len; 48 | } 49 | } 50 | 51 | const TAG_SCRIPTDATA = 18; 52 | const TAG_AUDIO = 8; 53 | const TAG_VIDEO = 9; 54 | 55 | const AMF_NUMBER = 0x00; 56 | const AMF_BOOL = 0x01; 57 | const AMF_STRING = 0x02; 58 | const AMF_OBJECT = 0x03; 59 | const AMF_NULL = 0x05; 60 | const AMF_UNDEFINED = 0x06; 61 | const AMF_REFERENCE = 0x07; 62 | const AMF_MIXEDARRAY = 0x08; 63 | const AMF_OBJECT_END = 0x09; 64 | const AMF_ARRAY = 0x0a; 65 | const AMF_DATE = 0x0b; 66 | const AMF_LONG_STRING = 0x0c; 67 | 68 | let readAMFString = br => { 69 | let length = br.readBEUint(2); 70 | let buf = br.readBuf(length); 71 | return String.fromCharCode.apply(null, buf); 72 | } 73 | 74 | let readAMFObject = br => { 75 | let type = br.readBEUint(1); 76 | 77 | switch (type) { 78 | case AMF_NUMBER: { 79 | var b = br.readBuf(8); 80 | return new DataView(b.buffer).getFloat64(0); 81 | } 82 | 83 | case AMF_BOOL: { 84 | return br.readBEUint(1) != 0; 85 | } 86 | 87 | case AMF_STRING: { 88 | return readAMFString(br); 89 | } 90 | 91 | case AMF_OBJECT: { 92 | let map = {}; 93 | for (;;) { 94 | let str = readAMFString(br); 95 | if (str.length == 0) 96 | break; 97 | let obj = readAMFObject(br); 98 | map[str] = obj; 99 | } 100 | br.skip(1); 101 | return map; 102 | } 103 | 104 | case AMF_DATE: { 105 | br.skip(10); 106 | return; 107 | } 108 | 109 | case AMF_ARRAY: { 110 | let arr = []; 111 | let len = br.readBEUint(4); 112 | for (let i = 0; i < len; i++) { 113 | let obj = readAMFObject(br); 114 | arr.push(obj); 115 | } 116 | return arr; 117 | } 118 | 119 | case AMF_MIXEDARRAY: { 120 | let map = {}; 121 | br.skip(4); 122 | for (;;) { 123 | let str = readAMFString(br); 124 | if (str.length == 0) 125 | break; 126 | let obj = readAMFObject(br); 127 | map[str] = obj; 128 | } 129 | br.skip(1); 130 | return map; 131 | } 132 | } 133 | } 134 | 135 | let parseScriptData = uint8arr => { 136 | let br = new ByteReader(uint8arr); 137 | let type = br.readBEUint(1); 138 | let str = readAMFString(br); 139 | if (str == 'onMetaData') { 140 | return readAMFObject(br); 141 | } 142 | } 143 | 144 | let parseVideoPacket = (uint8arr, dts) => { 145 | let br = new ByteReader(uint8arr); 146 | let flags = br.readBEUint(1); 147 | let frameType = (flags>>4)&0xf; 148 | let codecId = flags&0xf; 149 | let pkt = {type:'video', dts:dts/1e3}; 150 | 151 | if (codecId == 7) { // h264 152 | let type = br.readBEUint(1); 153 | let cts = br.readBEInt(3); 154 | pkt.cts = cts/1e3; 155 | pkt.pts = dts+cts; 156 | if (type == 0) { 157 | // AVCDecoderConfigurationRecord 158 | pkt.AVCDecoderConfigurationRecord = br.readBuf(br.len()); 159 | } else if (type == 1) { 160 | // NALUs 161 | pkt.NALUs = br.readBuf(br.len()); 162 | pkt.isKeyFrame = frameType==1; 163 | } else if (type == 2) { 164 | //throw new Error('type=2'); 165 | } 166 | } 167 | return pkt; 168 | } 169 | 170 | let parseAudioPacket = (uint8arr, dts) => { 171 | let br = new ByteReader(uint8arr); 172 | let flags = br.readBEUint(1) 173 | let fmt = flags>>4; 174 | let pkt = {type: 'audio', dts:dts/1e3} 175 | if (fmt == 10) { 176 | // AAC 177 | let type = br.readBEUint(1); 178 | if (type == 0) { 179 | pkt.AudioSpecificConfig = br.readBuf(br.len()); 180 | pkt.sampleRate = [5500,11000,22000,44000][(flags>>2)&3]; 181 | pkt.sampleSize = [8,16][(flags>>1)&1]; 182 | pkt.channelCount = [1,2][(flags)&1]; 183 | } else if (type == 1) 184 | pkt.frame = br.readBuf(br.len()); 185 | } 186 | return pkt; 187 | }; 188 | 189 | let parseMediaSegment = uint8arr => { 190 | let br = new ByteReader(uint8arr); 191 | let packets = []; 192 | 193 | while (br.len() > 0) { 194 | let tagType = br.readBEUint(1); 195 | let dataSize = br.readBEUint(3); 196 | let dts = br.readBEUint(3); 197 | br.skip(4); 198 | let data = br.readBuf(dataSize); 199 | 200 | switch (tagType) { 201 | case TAG_SCRIPTDATA: 202 | break; 203 | 204 | case TAG_VIDEO: 205 | packets.push(parseVideoPacket(data, dts)); 206 | break; 207 | 208 | case TAG_AUDIO: 209 | packets.push(parseAudioPacket(data, dts)); 210 | break; 211 | 212 | default: 213 | //throw new Error(`unknown tag=${tagType}`); 214 | } 215 | br.skip(4); 216 | } 217 | 218 | return packets; 219 | } 220 | 221 | class InitSegmentParser { 222 | constructor() { 223 | let meta, firsta, firstv; 224 | this._readloop = (function *() { 225 | yield 5; 226 | let dataOffset = yield 4; 227 | yield dataOffset-9+4; 228 | 229 | for (;;) { 230 | let tagType = yield 1; 231 | let dataSize = yield 3; 232 | let timeStamp = yield 3; 233 | yield 4; 234 | let data = yield {len:dataSize}; 235 | if (tagType == TAG_SCRIPTDATA) { 236 | meta = parseScriptData(data); 237 | } else if (tagType == TAG_VIDEO && firstv == null) { 238 | firstv = parseVideoPacket(data); 239 | } else if (tagType == TAG_AUDIO && firsta == null) { 240 | firsta = parseAudioPacket(data); 241 | } 242 | if (meta && firsta && firstv) { 243 | return {meta,firstv,firsta}; 244 | } 245 | yield 4; 246 | } 247 | })(); 248 | this._next(); 249 | } 250 | 251 | _next() { 252 | let r = this._readloop.next(this._val); 253 | if (r.done) { 254 | this._done = r.value; 255 | } else { 256 | if (typeof(r.value) == 'number') { 257 | this._left = r.value; 258 | this._val = 0; 259 | } else { 260 | this._left = r.value.len; 261 | if (this._left > 1024*1024*16) 262 | throw new Error('buf too big') 263 | this._val = new Uint8Array(this._left); 264 | } 265 | } 266 | } 267 | 268 | push(input) { 269 | let pos = 0; 270 | while (!this._done && pos < input.byteLength) { 271 | if (typeof(this._val) == 'number') { 272 | while (this._left > 0 && pos < input.byteLength) { 273 | this._val <<= 8; 274 | this._val |= input[pos]; 275 | this._left--; 276 | pos++; 277 | } 278 | } else { 279 | while (this._left > 0 && pos < input.byteLength) { 280 | let len = Math.min(this._left, input.byteLength-pos); 281 | this._val.set(input.slice(pos,pos+len), this._val.byteLength-this._left); 282 | this._left -= len; 283 | pos += len; 284 | } 285 | } 286 | if (this._left == 0) { 287 | this._next(); 288 | } 289 | } 290 | return this._done; 291 | } 292 | } 293 | 294 | let parseInitSegment = uint8arr => { 295 | try { 296 | let br = new ByteReader(uint8arr); 297 | br.skip(5); 298 | let dataOffset = br.readBEUint(4); 299 | let skip = dataOffset-9+4; 300 | br.skip(skip); 301 | 302 | let meta; 303 | let firsta, firstv; 304 | 305 | for (let i = 0; i < 4; i++) { 306 | let tagType = br.readBEUint(1); 307 | let dataSize = br.readBEUint(3); 308 | let timeStamp = br.readBEUint(3); 309 | br.skip(4); 310 | let data = br.readBuf(dataSize); 311 | 312 | if (tagType == TAG_SCRIPTDATA) { 313 | meta = parseScriptData(data); 314 | } else if (tagType == TAG_VIDEO && firstv == null) { 315 | firstv = parseVideoPacket(data); 316 | } else if (tagType == TAG_AUDIO && firsta == null) { 317 | firsta = parseAudioPacket(data); 318 | } 319 | 320 | if (meta && firsta && firstv) { 321 | return {meta,firstv,firsta}; 322 | } 323 | br.skip(4); 324 | } 325 | } catch (e) { 326 | console.error(e.stack); 327 | } 328 | } 329 | 330 | exports.InitSegmentParser = InitSegmentParser; 331 | exports.parseInitSegment = parseInitSegment; 332 | exports.parseMediaSegment = parseMediaSegment; 333 | 334 | -------------------------------------------------------------------------------- /http.js: -------------------------------------------------------------------------------- 1 | 2 | exports.fetch = (url, opts) => { 3 | opts = opts || {}; 4 | var retries = opts.retries; 5 | 6 | if (retries) { 7 | return new Promise(function(resolve, reject) { 8 | var wrappedFetch = function(n) { 9 | fetch(url, opts).then(function(res) { 10 | if (!(res.status >= 200 && res.status < 300)) { 11 | if (n > 0) { 12 | setTimeout(function() { 13 | wrappedFetch(--n); 14 | }, 1000); 15 | } else { 16 | reject(new Error('try to death')); 17 | } 18 | } else { 19 | resolve(res); 20 | } 21 | }).catch(reject); 22 | } 23 | wrappedFetch(retries); 24 | }); 25 | } 26 | 27 | return fetch(url, opts); 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | // TODO 3 | // [OK] youku support 4 | // [OK] tudou support 5 | // [OK] video player shortcut 6 | // [OK] double buffered problem: http://www.bilibili.com/video/av4376362/index_3.html at 360.0s 7 | // discontinous audio problem: http://www.bilibili.com/video/av3067286/ at 97.806,108.19 8 | // discontinous audio problem: http://www.bilibili.com/video/av1965365/index_6.html at 51.806 9 | // [OK] fast start 10 | // [OK] open twice 11 | // [OK] http://www.bilibili.com/video/av3659561/index_57.html: Error: empty range, maybe video end 12 | // [OK] http://www.bilibili.com/video/av3659561/index_56.html: First segment too small 13 | // [OK] double buffered problem: http://www.bilibili.com/video/av4467810/ 14 | // [OK] double buffered problem: http://www.bilibili.com/video/av3791945/ 15 | // [[2122.957988,2162.946522],[2163.041988,2173.216033]] 16 | // [OK] video reset problem: http://www.bilibili.com/video/av314/ 17 | // [OK] video stuck problem: http://www.tudou.com/albumplay/-3O0GyT_JkQ/Az5cnjgva4k.html 16:11 18 | // [OK] InitSegment invalid: http://www.bilibili.com/video/av1753789 19 | // EOF error at index 67 http://www.bilibili.com/video/av4593775/ 20 | // EOF error at index 166,168 http://www.tudou.com/albumplay/92J2xqpSxWY/m4dBe7EG-7Q.html 21 | 22 | // Test needed for safari: 23 | // xhr cross origin, change referer header, pass arraybuffer efficiency, 24 | // mse playing 25 | 26 | 'use strict' 27 | 28 | let localhost = 'http://localhost:6060/' 29 | 30 | let mediaSource = require('./mediaSource'); 31 | let Nanobar = require('nanobar'); 32 | let bilibili = require('./bilibili'); 33 | let youku = require('./youku'); 34 | let tudou = require('./tudou'); 35 | let createPlayer = require('./player'); 36 | let flashBlocker = require('./flashBlocker'); 37 | let flvdemux = require('./flvdemux'); 38 | let FastDamoo = require('./damoo'); 39 | 40 | let nanobar = new Nanobar(); 41 | 42 | let style = document.createElement('style'); 43 | let themeColor = '#DF6558'; 44 | 45 | style.innerHTML = ` 46 | .nanobar .bar { 47 | background: ${themeColor}; 48 | } 49 | .nanobar { 50 | z-index: 1000001; 51 | left: 0px; 52 | top: 0px; 53 | } 54 | .mama-toolbar { 55 | position: absolute; 56 | z-index: 1; 57 | bottom: 20px; 58 | right: 20px; 59 | } 60 | 61 | .mama-toolbar svg { 62 | width: 17px; 63 | color: #fff; 64 | fill: currentColor; 65 | cursor: pointer; 66 | } 67 | 68 | .mama-toolbar { 69 | display: flex; 70 | padding: 5px; 71 | padding-left: 15px; 72 | padding-right: 15px; 73 | border-radius: 5px; 74 | align-items: center; 75 | background: #333; 76 | } 77 | 78 | .mama-toolbar input[type=range]:focus { 79 | outline: none; 80 | } 81 | 82 | .mama-toolbar .selected { 83 | color: ${themeColor}; 84 | } 85 | 86 | .mama-toolbar input[type=range] { 87 | -webkit-appearance: none; 88 | height: 9px; 89 | width: 75px; 90 | border-radius: 3px; 91 | margin: 0; 92 | margin-right: 8px; 93 | } 94 | 95 | .mama-toolbar input[type=range]::-webkit-slider-thumb { 96 | -webkit-appearance: none; 97 | height: 13px; 98 | width: 5px; 99 | background: ${themeColor}; 100 | border-radius: 1px; 101 | } 102 | ` 103 | 104 | document.head.appendChild(style); 105 | mediaSource.debug = true; 106 | 107 | let getSeeker = url => { 108 | let seekers = [bilibili, youku, tudou]; 109 | let found = seekers.filter(s => s.testUrl(url)); 110 | return found[0]; 111 | } 112 | 113 | let playVideo = res => { 114 | let player = createPlayer(); 115 | let media = mediaSource.bindVideo({ 116 | video:player.video, 117 | src:res.src, 118 | duration:res.duration, 119 | }); 120 | player.streams = media.streams; 121 | return {player, media}; 122 | } 123 | 124 | let handleDamoo = (vres, player, seeker, media) => { 125 | let mode; 126 | if (seeker.getAllDamoo) { 127 | mode = 'all'; 128 | } else if (seeker.getDamooProgressive) { 129 | mode = 'progressive'; 130 | } 131 | 132 | if (!mode) 133 | return; 134 | 135 | let damoos = []; 136 | 137 | (() => { 138 | if (mode == 'all') { 139 | return seeker.getAllDamoo(vres).then(res => { 140 | damoos = res; 141 | }); 142 | } else if (mode == 'progressive') { 143 | return new Promise((fulfill, reject) => { 144 | seeker.getDamooProgressive(vres, res => { 145 | damoos = damoos.concat(res); 146 | //console.log(`damoo: loaded n=${damoos.length}`); 147 | fulfill(); 148 | }) 149 | }); 150 | } 151 | })().then(() => { 152 | let video = player.video; 153 | let updating; 154 | let cur = 0; 155 | let emitter; 156 | 157 | let update = () => { 158 | let time = video.currentTime+1.0; 159 | if (cur < damoos.length && time > damoos[cur].time) { 160 | for (; cur < damoos.length && damoos[cur].time <= time; cur++) { 161 | let d = damoos[cur]; 162 | //console.log('damoo: emit', `${Math.floor(d.time/60)}:${Math.floor(d.time%60)}`, d.text); 163 | emitter.emit({text: d.text, pos: d.pos, shadow: {color: '#000'}, color: d.color}); 164 | } 165 | } 166 | updating = setTimeout(update, 1000); 167 | }; 168 | let stopUpdate = () => { 169 | if (updating) { 170 | clearTimeout(updating); 171 | updating = null; 172 | } 173 | } 174 | let startUpdate = () => { 175 | if (!updating) 176 | update(); 177 | } 178 | 179 | let resetCur = () => { 180 | let time; 181 | for (cur = 0; cur < damoos.length; cur++) { 182 | if (damoos[cur].time > video.currentTime) { 183 | time = damoos[cur].time; 184 | break; 185 | } 186 | } 187 | console.log(`damoo: cur=${cur}/${damoos.length} time=${time}`); 188 | } 189 | 190 | media.onSeek.push(() => { 191 | emitter.clear(); 192 | resetCur(); 193 | }) 194 | 195 | player.onResume.push(() => { 196 | if (emitter == null) { 197 | emitter = new FastDamoo({container:player.damoo, fontSize:20}); 198 | let setDamooOpts = () => { 199 | player.damoo.style.opacity = player.damooOpacity; 200 | if (player.damooEnabled) { 201 | emitter.show(); 202 | } else { 203 | emitter.hide(); 204 | } 205 | } 206 | player.onDamooOptsChange.push(() => setDamooOpts()); 207 | setDamooOpts(); 208 | } 209 | emitter.synctime(video.currentTime); 210 | emitter.resume() 211 | startUpdate(); 212 | }); 213 | player.onSuspend.push(() => { 214 | emitter.synctime(video.currentTime); 215 | emitter.suspend() 216 | stopUpdate(); 217 | }); 218 | 219 | }); 220 | } 221 | 222 | let playUrl = url => { 223 | return new Promise((fulfill, reject) => { 224 | let seeker = getSeeker(url) 225 | if (seeker) { 226 | flashBlocker(); 227 | nanobar.go(30); 228 | seeker.getVideos(url).then(res => { 229 | console.log('getVideosResult:', res); 230 | if (res) { 231 | let ctrl = playVideo(res); 232 | ctrl.player.onStarted.push(() => nanobar.go(100)); 233 | handleDamoo(res, ctrl.player, seeker, ctrl.media); 234 | nanobar.go(60) 235 | fulfill(ctrl); 236 | } else { 237 | throw new Error('getVideosResult: invalid') 238 | } 239 | }).catch(e => { 240 | nanobar.go(100); 241 | throw e; 242 | }); 243 | } else { 244 | throw new Error('seeker not found'); 245 | } 246 | }); 247 | } 248 | 249 | let cmd = {}; 250 | 251 | cmd.youku = youku; 252 | cmd.tudou = tudou; 253 | cmd.bilibili = bilibili; 254 | 255 | cmd.testDanmuLayer = () => { 256 | let danmu = createDamnuLayer(document.body); 257 | } 258 | 259 | cmd.fetchSingleFlvMediaSegments = (url, duration, indexStart, indexEnd) => { 260 | let streams = new mediaSource.Streams({ 261 | urls: [localhost+url], 262 | fakeDuration: duration, 263 | }); 264 | streams.onProbeProgress = (stream, i) => { 265 | if (i == 0) { 266 | streams.fetchMediaSegmentsByIndex(indexStart, indexEnd); 267 | } 268 | }; 269 | streams.probeOneByOne(); 270 | } 271 | 272 | cmd.playSingleFlv = (url, duration, pos) => { 273 | cmd.ctrl = playVideo({ 274 | src:[ 275 | localhost+url, 276 | ], 277 | duration, 278 | }); 279 | if (pos) 280 | setTimeout(() => cmd.ctrl.player.video.currentTime = pos, 500); 281 | } 282 | 283 | cmd.getVideos = url => { 284 | let seeker = getSeeker(url); 285 | if (!seeker) { 286 | console.log('seeker not found'); 287 | return; 288 | } 289 | seeker.getVideos(url).then(res => console.log(res)) 290 | } 291 | 292 | cmd.playUrl = playUrl; 293 | 294 | cmd.testXhr = () => { 295 | var xhr = new XMLHttpRequest(); 296 | xhr.open('GET', localhost+'projectindex-0.flv'); 297 | setTimeout(() => xhr.abort(), 100); 298 | xhr.onload = function(e) { 299 | console.log(this.status); 300 | console.log(this.response.length); 301 | } 302 | xhr.onerror = function() { 303 | console.log('onerror') 304 | } 305 | xhr.send(); 306 | } 307 | 308 | cmd.testWriteFile = () => { 309 | let errfunc = e => console.error(e); 310 | webkitRequestFileSystem(TEMPORARY, 1*1024*1024*1024, fs => { 311 | fs.root.getFile('tmp.bin', {create:true}, file => { 312 | file.createWriter(writer => { 313 | writer.onwrittend = () => console.log('write complete'); 314 | //writer.truncate(1024*1024); 315 | for (let i = 0; i < 1024*1024*10; i++) { 316 | let u8 = new Uint8Array([0x65,0x65,0x65,0x65]); 317 | writer.write(new Blob([u8])); 318 | } 319 | let a = document.createElement('a'); 320 | a.href = file.toURL(); 321 | a.download = 'a.txt'; 322 | document.body.appendChild(a); 323 | a.click(); 324 | }); 325 | }, errfunc); 326 | }, errfunc); 327 | } 328 | 329 | cmd.testfetch = () => { 330 | let dbp = console.log.bind(console) 331 | 332 | let parser = new flvdemux.InitSegmentParser(); 333 | let total = 0; 334 | let pump = reader => { 335 | return reader.read().then(res => { 336 | if (res.done) { 337 | dbp('parser: EOF'); 338 | return; 339 | } 340 | let chunk = res.value; 341 | total += chunk.byteLength; 342 | dbp(`parser: incoming ${chunk.byteLength}`); 343 | let done = parser.push(chunk); 344 | if (done) { 345 | dbp('parser: finished', done); 346 | reader.cancel(); 347 | return done; 348 | } else { 349 | return pump(reader); 350 | } 351 | }); 352 | } 353 | 354 | let headers = new Headers(); 355 | headers.append('Range', 'bytes=0-400000'); 356 | fetch(`http://27.221.48.172/youku/65723A1CDA44683D499698466F/030001290051222DE95D6C055EEB3EBFDE3F09-E65E-1E0A-218C-3CDFACC4F973.flv`, {headers}).then(res => pump(res.body.getReader())) 357 | .then(res => console.log(res)); 358 | } 359 | 360 | cmd.testInitSegment = () => { 361 | let dbp = console.log.bind(console) 362 | 363 | let meta; 364 | let fetchseg = seg => { 365 | return fetch(localhost+'test-fragmented.mp4', {headers: { 366 | Range: `bytes=${seg.offset}-${seg.offset+seg.size-1}`, 367 | }}).then(res=>res.arrayBuffer()); 368 | } 369 | 370 | fetch(localhost+'test-fragmented-manifest.json').then(res=>res.json()).then(res => { 371 | meta = res; 372 | dbp('meta', meta); 373 | }).then(res => { 374 | res = new Uint8Array(res); 375 | 376 | let mediaSource = new MediaSource(); 377 | let video = document.createElement('video'); 378 | document.body.appendChild(video); 379 | 380 | video.src = URL.createObjectURL(mediaSource); 381 | video.autoplay = true; 382 | 383 | video.addEventListener('loadedmetadata', () => { 384 | dbp('loadedmetadata', video.duration); 385 | }); 386 | 387 | let sourceBuffer; 388 | mediaSource.addEventListener('sourceopen', e => { 389 | dbp('sourceopen'); 390 | if (mediaSource.sourceBuffers.length > 0) 391 | return; 392 | let codecType = meta.type; 393 | sourceBuffer = mediaSource.addSourceBuffer(codecType); 394 | sourceBuffer.mode = 'sequence'; 395 | sourceBuffer.addEventListener('error', () => dbp('sourceBuffer: error')); 396 | sourceBuffer.addEventListener('abort', () => dbp('sourceBuffer: abort')); 397 | sourceBuffer.addEventListener('update', () => { 398 | dbp('sourceBuffer: update'); 399 | }) 400 | sourceBuffer.addEventListener('updateend', () => { 401 | let ranges = []; 402 | let buffered = sourceBuffer.buffered; 403 | for (let i = 0; i < buffered.length; i++) { 404 | ranges.push([buffered.start(i), buffered.end(i)]); 405 | } 406 | dbp('sourceBuffer: updateend'); 407 | dbp('buffered', JSON.stringify(ranges), 'duration', video.duration); 408 | }); 409 | fetchseg(meta.init).then(() => { 410 | sourceBuffer.appendBuffer(res); 411 | return fetchseg(meta.media[1]).then(res => { 412 | dbp(res.byteLength); 413 | sourceBuffer.appendBuffer(res); 414 | }); 415 | }); 416 | }) 417 | mediaSource.addEventListener('sourceended', () => dbp('mediaSource: sourceended')) 418 | mediaSource.addEventListener('sourceclose', () => dbp('mediaSource: sourceclose')) 419 | }).catch(e => { 420 | console.error(e); 421 | }); 422 | } 423 | 424 | cmd.testPlayer = () => { 425 | let player = createPlayer(); 426 | player.video.src = localhost+'projectindex.mp4'; 427 | player.video.muted = true; 428 | } 429 | //cmd.testPlayer(); 430 | 431 | cmd.testCanvasSpeed = () => { 432 | let canvas = document.createElement('canvas'); 433 | canvas.width = 1280; 434 | canvas.height = 800; 435 | let ctx = canvas.getContext('2d'); 436 | 437 | let line = []; 438 | for (let i = 0; i < 3; i++) { 439 | let c = document.createElement('canvas'); 440 | c.width = 100; 441 | c.height = 100; 442 | line.push(c); 443 | } 444 | console.log('canvas', canvas.width, canvas.height); 445 | 446 | var _RAF = window.requestAnimationFrame || 447 | window.mozRequestAnimationFrame || 448 | window.webkitRequestAnimationFrame || 449 | window.msRequestAnimationFrame || 450 | window.oRequestAnimationFrame || 451 | function(cb) { return setTimeout(cb, 17); }; 452 | 453 | setInterval(() => { 454 | line.forEach(c => { 455 | ctx.drawImage(c, 0, 0); 456 | console.log('draw'); 457 | }); 458 | }, 1000/24); 459 | } 460 | 461 | cmd.testCssTransition = () => { 462 | let freelist = []; 463 | const FONTSIZE = 25; 464 | const ROWS = 50; 465 | let currow = 0; 466 | 467 | let container = document.createElement('div'); 468 | container.style.width = '100%'; 469 | container.style.height = '100%'; 470 | document.body.appendChild(container); 471 | 472 | for (let i = 0; i < 200; i++) { 473 | let p = document.createElement('canvas'); 474 | p.width = 1; 475 | p.height = 1; 476 | p.style.position = 'absolute'; 477 | p.style.backgroundColor = 'transparent'; 478 | container.appendChild(p); 479 | freelist.push(p); 480 | } 481 | 482 | let emit = ({text, pos, color, shadow}) => { 483 | if (freelist.length == 0) 484 | return; 485 | 486 | currow++; 487 | if (currow > ROWS) 488 | currow = 0; 489 | 490 | color = color || '#fff'; 491 | shadow = shadow || {color: '#000'}; 492 | pos = pos || 'normal'; 493 | 494 | let p = freelist[0]; 495 | freelist = freelist.slice(1); 496 | 497 | let size = FONTSIZE; 498 | let ctx = p.getContext('2d'); 499 | ctx.font = `${size}px Arail`; 500 | p.width = ctx.measureText(text).width; 501 | p.height = size*1.5; 502 | 503 | ctx.font = `${size}px Arail`; 504 | ctx.fillStyle = color; 505 | ctx.textAlign = "start"; 506 | ctx.textBaseline = "top"; 507 | if (shadow) { 508 | ctx.shadowOffsetX = 1; 509 | ctx.shadowOffsetY = 1; 510 | ctx.shadowColor = shadow.color; 511 | } 512 | ctx.fillText(text, 0, 0); 513 | 514 | let time = 5; 515 | let movew = container.offsetWidth+p.width; 516 | 517 | p.style.top = `${currow*FONTSIZE}px`; 518 | 519 | if (pos == 'top') { 520 | p.style.left = `${(container.offsetWidth-p.width)/2}px`; 521 | } else { 522 | p.style.right = `${-p.width}px`; 523 | } 524 | 525 | p.style.display = 'block'; 526 | 527 | setTimeout(() => { 528 | p.style.display = 'none'; 529 | freelist.push(p); 530 | }, time*1000); 531 | 532 | if (pos == 'normal') { 533 | setTimeout(() => { 534 | p.style.transition = `transform ${time}s linear`; 535 | p.style.transform = `translate(-${movew}px,0)`; 536 | }, 50); 537 | } 538 | 539 | } 540 | 541 | emit({text:'哔哩哔哩哔哩哔哩哔哩哔哩哔哩哔哩哔哩哔哩哔哩哔哩哔哩哔哩'}); 542 | emit({text:'我占了中间位置', color:'#f00', pos:'top'}); 543 | //setInterval(reset, 2000); 544 | } 545 | 546 | cmd.testDamoo = () => { 547 | let div = document.createElement('div'); 548 | 549 | document.body.style.margin = '0'; 550 | let resize = () => { 551 | div.style.height = window.innerHeight+'px'; 552 | div.style.width = window.innerWidth+'px'; 553 | } 554 | window.addEventListener('resize', () => resize()); 555 | resize(); 556 | 557 | div.innerHTML = ` 558 |

Background

559 | `; 560 | div.style.background = '#eee'; 561 | document.body.appendChild(div); 562 | 563 | let dm = new FastDamoo({container:div, fontSize:20}); 564 | dm.show(); 565 | dm.resume(); 566 | dm.emit({text:'小小小的文字', color:'#000'}); 567 | dm.emit({text:'小小小的文字', color:'#000', pos:'bottom'}); 568 | dm.emit({text:'稍微长一点的文字2333333333333333333', color:'#000', pos:'top'}); 569 | 570 | document.body.addEventListener('keydown', (e) => { 571 | switch (e.code) { 572 | case "KeyR": { 573 | dm.resume(); 574 | } break; 575 | 576 | case "KeyP": { 577 | dm.suspend(); 578 | } break; 579 | 580 | case "KeyS": { 581 | dm.show(); 582 | } break; 583 | 584 | case "KeyH": { 585 | dm.hide(); 586 | } break; 587 | } 588 | }); 589 | 590 | let i = 0; 591 | let timer = setInterval(() => { 592 | i++; 593 | if (i > 300) { 594 | clearInterval(timer); 595 | return; 596 | } 597 | let text = '哔哩哔哩'; 598 | for (let i = 0; i < 4; i++) 599 | text = text+text; 600 | dm.emit({ 601 | text, 602 | color: '#f00', 603 | }); 604 | }, 10); 605 | } 606 | //cmd.testDamoo() 607 | 608 | if (location.href.substr(0,6) != 'chrome') { 609 | playUrl(location.href); 610 | } else { 611 | window.cmd = cmd; 612 | } 613 | 614 | -------------------------------------------------------------------------------- /jsonp.js: -------------------------------------------------------------------------------- 1 | 2 | var callbackPrefix = 'MAMA2_HTTP_JSONP_CALLBACK' 3 | var callbackCount = 0 4 | var timeoutDelay = 10000 5 | 6 | function callbackHandle () { 7 | return callbackPrefix + callbackCount++ 8 | } 9 | 10 | function jsonp(url, callbackKey) { 11 | return new Promise((fulfill, reject) => { 12 | callbackKey = callbackKey || 'callback' 13 | 14 | var _callbackHandle = callbackHandle() 15 | 16 | window[_callbackHandle] = function (rs) { 17 | clearTimeout(timeoutTimer) 18 | delete window[_callbackHandle] 19 | fulfill(rs) 20 | } 21 | 22 | var timeoutTimer = setTimeout(function () { 23 | delete window[_callbackHandle] 24 | reject(new Error('jsonp timeout')) 25 | }, timeoutDelay) 26 | 27 | var src = url + (url.indexOf('?') >= 0 ? '&' : '?') + callbackKey + '=' + _callbackHandle; 28 | fetch(src).then(res => res.text()).then(res => { 29 | eval(res) 30 | }) 31 | }) 32 | } 33 | 34 | module.exports = jsonp 35 | -------------------------------------------------------------------------------- /mama-hd.safariextension/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Author 6 | nareix 7 | Builder Version 8 | 10600.1.25 9 | CFBundleDisplayName 10 | mama-hd 11 | CFBundleIdentifier 12 | com.nareix.mama-hd 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleVersion 18 | 1 19 | Chrome 20 | 21 | Global Page 22 | bg.html 23 | Toolbar Items 24 | 25 | 26 | Command 27 | MAMA-HD 28 | Identifier 29 | MAMA-HD 30 | Image 31 | icon128.png 32 | Include By Default 33 | 34 | Label 35 | MAMA-HD 36 | Palette Label 37 | MAMA-HD 38 | Tool Tip 39 | MAMA-HD 40 | 41 | 42 | 43 | Content 44 | 45 | Scripts 46 | 47 | Start 48 | 49 | script.js 50 | 51 | 52 | 53 | Description 54 | 妈妈再也不用担心我的 Macbook 发热计划之 1080P 55 | DeveloperIdentifier 56 | 57 | ExtensionInfoDictionaryVersion 58 | 1.0 59 | Permissions 60 | 61 | Website Access 62 | 63 | Include Secure Pages 64 | 65 | Level 66 | All 67 | 68 | 69 | Website 70 | http://github.com/nareix/mama-hd/ 71 | 72 | 73 | -------------------------------------------------------------------------------- /mama-hd.safariextension/bg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MAMA2 6 | 7 | 8 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /mama-hd.safariextension/script.js: -------------------------------------------------------------------------------- 1 | 2 | safari.self.addEventListener("message", function(theMessageEvent){ 3 | if (theMessageEvent.name == 'MAMA-HD') { 4 | if (window === window.top) { 5 | (function(s){ 6 | s=document.body.appendChild(document.createElement('script')); 7 | s.src=safari.extension.baseURI+'bundle.js'; 8 | s.charset='UTF-8';}()) 9 | } 10 | } 11 | }, false); 12 | -------------------------------------------------------------------------------- /mama-hd/background.js: -------------------------------------------------------------------------------- 1 | chrome.browserAction.onClicked.addListener(function(){ 2 | chrome.tabs.executeScript(null, {file: "bundle.js"}); 3 | }); 4 | 5 | chrome.webRequest.onBeforeSendHeaders.addListener( 6 | function(details) { 7 | var sethdr = {}; 8 | for (var i = 0; i < details.requestHeaders.length; i++) { 9 | var header = details.requestHeaders[i]; 10 | if (header.name.substr(0,7) == 'sethdr-') { 11 | sethdr[header.name.substr(7)] = header.value; 12 | } 13 | } 14 | for (var i = 0; i < details.requestHeaders.length; i++) { 15 | var header = details.requestHeaders[i]; 16 | if (sethdr[header.name]) { 17 | header.value = sethdr[header.name]; 18 | delete sethdr[header.name]; 19 | } 20 | } 21 | for (var k in sethdr) { 22 | details.requestHeaders.push({name:k, value:sethdr[k]}); 23 | } 24 | return {requestHeaders: details.requestHeaders}; 25 | }, 26 | {urls: ["http://play.youku.com/*"]}, 27 | ["blocking", "requestHeaders"] 28 | ); 29 | 30 | -------------------------------------------------------------------------------- /mama-hd/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nareix/mama-hd/2ee17c121fda6749c0d720b51788c4c1447b44bf/mama-hd/icon128.png -------------------------------------------------------------------------------- /mama-hd/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nareix/mama-hd/2ee17c121fda6749c0d720b51788c4c1447b44bf/mama-hd/icon48.png -------------------------------------------------------------------------------- /mama-hd/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "MAMA-HD", 3 | "version": "0.924", 4 | "manifest_version": 2, 5 | "minimum_chrome_version": "48", 6 | "browser_action": { 7 | "default_icon": "icon128.png" 8 | }, 9 | "icons": { 10 | "128": "icon128.png" 11 | }, 12 | "background": { 13 | "scripts": ["background.js"], 14 | "persistent": true 15 | }, 16 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 17 | "permissions": [ 18 | "webRequest", 19 | "webRequestBlocking", 20 | "tabs", 21 | "*://*/*" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /mama-hd/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /mediaSource.js: -------------------------------------------------------------------------------- 1 | 2 | //TODO 3 | // [DONE] sourceBuffer: opeartion queue 4 | // [DONE] seek to keyframe problem 5 | // [DONE] rewrite fetchMediaSegment 6 | // seeking start seeking end cb, when seeking not end bufupdate not seek 7 | // [DONE] xhr retry 8 | 9 | 'use strict' 10 | 11 | let flvdemux = require('./flvdemux') 12 | let mp4mux = require('./mp4mux') 13 | let fetch = require('./http').fetch; 14 | 15 | let app = {} 16 | 17 | let dbp = console.log.bind(console); 18 | 19 | let concatUint8Array = function(list) { 20 | let len = 0; 21 | list.forEach(b => len += b.byteLength) 22 | let res = new Uint8Array(len); 23 | let off = 0; 24 | list.forEach(b => { 25 | res.set(b, off); 26 | off += b.byteLength; 27 | }) 28 | return res; 29 | } 30 | 31 | class Streams { 32 | constructor({urls,fakeDuration}) { 33 | if (fakeDuration == null) 34 | throw new Error('fakeDuration must set'); 35 | this.urls = urls; 36 | this.fakeDuration = fakeDuration; 37 | this.streams = []; 38 | this.duration = 0; 39 | this.keyframes = []; 40 | this.probeIdx = 0; 41 | } 42 | 43 | probeFirst() { 44 | return this.probeOneByOne(); 45 | } 46 | 47 | fetchInitSegment(url) { 48 | let parser = new flvdemux.InitSegmentParser(); 49 | let pump = reader => { 50 | return reader.read().then(res => { 51 | if (res.done) { 52 | //dbp('initsegparser: EOF'); 53 | return; 54 | } 55 | let chunk = res.value; 56 | //dbp(`initsegparser: incoming ${chunk.byteLength}`); 57 | let done = parser.push(chunk); 58 | if (done) { 59 | //dbp('initsegparser: finished', done); 60 | reader.cancel(); 61 | return done; 62 | } else { 63 | return pump(reader); 64 | } 65 | }); 66 | } 67 | return fetch(url, {headers: {Range: 'bytes=0-5000000'}, retries: 1024}).then(res => { 68 | return pump(res.body.getReader()) 69 | }); 70 | } 71 | 72 | probeOneByOne() { 73 | let url = this.urls[this.probeIdx]; 74 | return this.fetchInitSegment(url).then(flvhdr => { 75 | if (flvhdr == null) 76 | return Promise.reject(new Error('probe '+url+' failed')); 77 | let stream = flvhdr; 78 | 79 | this.streams.push(stream); 80 | stream.duration = stream.meta.duration; 81 | stream.timeStart = this.duration; 82 | stream.timeEnd = this.duration+stream.duration; 83 | stream.indexStart = this.keyframes.length; 84 | 85 | let keyframes = stream.meta.keyframes; 86 | keyframes.times.forEach((time, i) => { 87 | let last = i==keyframes.times.length-1; 88 | let entry = { 89 | timeStart: stream.timeStart+time, 90 | timeEnd: stream.timeStart+(last?stream.duration:keyframes.times[i+1]), 91 | urlIdx: this.probeIdx, 92 | rangeStart: keyframes.filepositions[i], 93 | rangeEnd: last?stream.meta.filesize:keyframes.filepositions[i+1], 94 | }; 95 | entry.duration = entry.timeEnd-entry.timeStart; 96 | entry.size = entry.rangeEnd-entry.rangeStart; 97 | this.keyframes.push(entry); 98 | }); 99 | this.duration += stream.duration; 100 | 101 | if (this.probeIdx == 0) { 102 | if (flvhdr.firstv.AVCDecoderConfigurationRecord == null) 103 | throw new Error('AVCDecoderConfigurationRecord not found'); 104 | if (flvhdr.firsta.AudioSpecificConfig == null) 105 | throw new Error('AudioSpecificConfig not found'); 106 | 107 | let record = flvhdr.firstv.AVCDecoderConfigurationRecord; 108 | dbp('probe:', `h264.profile=${record[1].toString(16)}`, 'meta', flvhdr); 109 | 110 | this.videoTrack = { 111 | type: 'video', 112 | id: 1, 113 | duration: Math.ceil(this.fakeDuration*mp4mux.timeScale), 114 | width: flvhdr.meta.width, 115 | height: flvhdr.meta.height, 116 | AVCDecoderConfigurationRecord: flvhdr.firstv.AVCDecoderConfigurationRecord, 117 | }; 118 | this.audioTrack = { 119 | type: 'audio', 120 | id: 2, 121 | duration: this.videoTrack.duration, 122 | channelcount: flvhdr.firsta.channelCount, 123 | samplerate: flvhdr.firsta.sampleRate, 124 | samplesize: flvhdr.firsta.sampleSize, 125 | AudioSpecificConfig: flvhdr.firsta.AudioSpecificConfig, 126 | }; 127 | } 128 | 129 | this.probeIdx++; 130 | dbp(`probe: got ${this.probeIdx}/${this.urls.length}`); 131 | 132 | if (this.onProbeProgress) 133 | this.onProbeProgress(stream, this.probeIdx-1); 134 | if (this.probeIdx < this.urls.length) { 135 | this.probeOneByOne(); 136 | } 137 | }); 138 | } 139 | 140 | findIndexByTime(time, opts) { 141 | if (time < 0 || time > this.duration) 142 | return; 143 | let minDiff = this.duration, best; 144 | this.keyframes.forEach((keyframe, i) => { 145 | let diff = time-keyframe.timeStart; 146 | let absDiff = Math.abs(diff); 147 | if (absDiff < minDiff) { 148 | minDiff = absDiff; 149 | best = i; 150 | } 151 | }); 152 | return best; 153 | 154 | let choose = 0; 155 | for (let i = 0; i < this.keyframes.length; i++) { 156 | let e = this.keyframes[i]; 157 | if (time <= e.timeEnd) { 158 | choose = i; 159 | break; 160 | } 161 | } 162 | return choose; 163 | } 164 | 165 | fetchMediaSegmentsByIndex(indexStart, indexEnd) { 166 | let ranges = []; 167 | let totalSize = 0; 168 | 169 | for (let i = indexStart; i <= indexEnd; i++) { 170 | let e = this.keyframes[i]; 171 | let url = this.urls[e.urlIdx]; 172 | let range; 173 | if (ranges.length == 0 || ranges[ranges.length-1].url != url) { 174 | range = {url, start:e.rangeStart, end:e.rangeEnd}; 175 | range.streamTimeBase = this.streams[e.urlIdx].timeStart; 176 | range.timeStart = e.timeStart; 177 | range.indexStart = i; 178 | ranges.push(range); 179 | } else { 180 | range = ranges[ranges.length-1]; 181 | } 182 | range.indexEnd = i; 183 | range.end = e.rangeEnd; 184 | range.timeEnd = e.timeEnd; 185 | range.duration = range.timeEnd-range.timeStart; 186 | totalSize += e.size; 187 | } 188 | 189 | if (ranges.length == 0) 190 | throw new Error('ranges.length = 0'); 191 | 192 | let timeStart = this.keyframes[indexStart].timeStart; 193 | let timeEnd = this.keyframes[indexEnd].timeEnd; 194 | dbp('fetch:', `index=[${indexStart},${indexEnd}] `+ 195 | `time=[${timeStart},${timeEnd}] size=${totalSize/1e6}M range.nr=${ranges.length}`); 196 | 197 | let resbuf = []; 198 | let fulfill; 199 | let xhr; 200 | 201 | let promise = new Promise((_fulfill, reject) => { 202 | fulfill = _fulfill; 203 | 204 | let request = i => { 205 | let range = ranges[i]; 206 | let {url,start,end} = range; 207 | dbp('fetch:', `bytes=[${start},${end}]`); 208 | xhr = new XMLHttpRequest(); 209 | xhr.open('GET', url); 210 | xhr.responseType = 'arraybuffer'; 211 | { 212 | let range; 213 | if (start || end) { 214 | range = 'bytes='; 215 | if (start) 216 | range += start; 217 | else 218 | range += '0'; 219 | range += '-' 220 | if (end) 221 | range += end-1; 222 | } 223 | if (range !== undefined) { 224 | xhr.setRequestHeader('Range', range); 225 | } 226 | } 227 | xhr.onerror = () => { 228 | setTimeout(() => request(i), 2000); 229 | } 230 | 231 | xhr.onload = () => { 232 | let segbuf = new Uint8Array(xhr.response); 233 | let cputimeStart = new Date().getTime(); 234 | let buf = this.transcodeMediaSegments(segbuf, range); 235 | let cputimeEnd = new Date().getTime(); 236 | dbp('transcode:', `[${range.indexStart},${range.indexEnd}]`, 'cputime(ms):', (cputimeEnd-cputimeStart), 237 | 'segbuf(MB)', segbuf.byteLength/1e6, 238 | 'videotime(s)', range.duration 239 | ); 240 | resbuf.push(buf); 241 | if (i+1 < ranges.length) { 242 | request(i+1); 243 | } else { 244 | fulfill(concatUint8Array(resbuf)); 245 | } 246 | } 247 | 248 | xhr.send(); 249 | } 250 | 251 | request(0); 252 | }); 253 | 254 | promise.cancel = () => { 255 | xhr.abort(); 256 | fulfill(); 257 | }; 258 | 259 | promise.timeStart = timeStart; 260 | promise.timeEnd = timeEnd; 261 | 262 | return promise; 263 | } 264 | 265 | getInitSegment() { 266 | return mp4mux.initSegment([this.videoTrack, this.audioTrack], this.fakeDuration*mp4mux.timeScale); 267 | } 268 | 269 | transcodeMediaSegments(segbuf, range) { 270 | let segpkts = flvdemux.parseMediaSegment(segbuf); 271 | 272 | let lastSample, lastDuration, duration; 273 | let videoTrack = this.videoTrack; 274 | let audioTrack = this.audioTrack; 275 | 276 | // baseMediaDecodeTime=firstpacket.time [video][video][video][video] nextkeyframe.time 277 | // baseMediaDecodeTime=firstpacket.time [audio][audio][audio][audio] keyframe.time+aac_total_duration 278 | 279 | if (this._lastTranscodeRangeEndIndex !== undefined && 280 | this._lastTranscodeRangeEndIndex+1 === range.indexStart) { 281 | audioTrack._firstTime = audioTrack._lastTime; 282 | videoTrack._firstTime = videoTrack._lastTime; 283 | } else { 284 | delete audioTrack._firstTime; 285 | delete videoTrack._firstTime; 286 | } 287 | this._lastTranscodeRangeEndIndex = range.indexEnd; 288 | 289 | videoTrack._mdatSize = 0; 290 | videoTrack.samples = []; 291 | audioTrack._mdatSize = 0; 292 | audioTrack.samples = []; 293 | 294 | lastSample = null; 295 | duration = 0; 296 | segpkts.filter(pkt => pkt.type == 'video' && pkt.NALUs).forEach((pkt, i) => { 297 | let sample = {}; 298 | sample._data = pkt.NALUs; 299 | sample._offset = videoTrack._mdatSize; 300 | sample.size = sample._data.byteLength; 301 | videoTrack._mdatSize += sample.size; 302 | 303 | if (videoTrack._firstTime === undefined) { 304 | videoTrack._firstTime = pkt.dts+range.streamTimeBase; 305 | } 306 | sample._dts = pkt.dts; 307 | sample.compositionTimeOffset = pkt.cts*mp4mux.timeScale; 308 | 309 | sample.flags = { 310 | isLeading: 0, 311 | dependsOn: 0, 312 | isDependedOn: 0, 313 | hasRedundancy: 0, 314 | paddingValue: 0, 315 | isNonSyncSample: pkt.isKeyFrame?0:1, 316 | degradationPriority: 0, 317 | }; 318 | 319 | if (lastSample) { 320 | let diff = sample._dts-lastSample._dts; 321 | lastSample.duration = diff*mp4mux.timeScale; 322 | duration += diff; 323 | } 324 | lastSample = sample; 325 | videoTrack.samples.push(sample); 326 | }); 327 | lastSample.duration = (range.duration-duration)*mp4mux.timeScale; 328 | videoTrack._lastTime = range.timeEnd; 329 | 330 | lastSample = null; 331 | duration = 0; 332 | segpkts.filter(pkt => pkt.type == 'audio' && pkt.frame).forEach((pkt, i) => { 333 | let sample = {}; 334 | sample._data = pkt.frame; 335 | sample._offset = audioTrack._mdatSize; 336 | sample.size = sample._data.byteLength; 337 | audioTrack._mdatSize += sample.size; 338 | 339 | //dbp('audiosample', pkt.dts, pkt.frame.byteLength); 340 | 341 | if (audioTrack._firstTime === undefined) { 342 | audioTrack._firstTime = pkt.dts+range.streamTimeBase; 343 | } 344 | sample._dts = pkt.dts; 345 | 346 | if (lastSample) { 347 | let diff = sample._dts-lastSample._dts; 348 | lastSample.duration = diff*mp4mux.timeScale; 349 | duration += diff; 350 | lastDuration = diff; 351 | } 352 | lastSample = sample; 353 | audioTrack.samples.push(sample); 354 | }); 355 | lastSample.duration = lastDuration*mp4mux.timeScale; 356 | audioTrack._lastTime = duration+lastDuration+audioTrack._firstTime; 357 | 358 | videoTrack.baseMediaDecodeTime = videoTrack._firstTime*mp4mux.timeScale; 359 | audioTrack.baseMediaDecodeTime = audioTrack._firstTime*mp4mux.timeScale; 360 | 361 | if (0) { 362 | let totdur = x => x.samples.reduce((val,e) => val+e.duration, 0); 363 | dbp('av.samplesCount',audioTrack.samples.length, videoTrack.samples.length); 364 | dbp('av.duration:', totdur(audioTrack)/mp4mux.timeScale,totdur(videoTrack)/mp4mux.timeScale); 365 | dbp('av.firstTime:', audioTrack._firstTime, videoTrack._firstTime); 366 | dbp('av.lastTime:', audioTrack._lastTime, videoTrack._lastTime); 367 | } 368 | 369 | let moof, _mdat, mdat; 370 | let list = []; 371 | 372 | moof = mp4mux.moof(0, [videoTrack]); 373 | _mdat = new Uint8Array(videoTrack._mdatSize); 374 | videoTrack.samples.forEach(sample => _mdat.set(sample._data, sample._offset)); 375 | mdat = mp4mux.mdat(_mdat); 376 | list = list.concat([moof, mdat]); 377 | 378 | moof = mp4mux.moof(0, [audioTrack]); 379 | _mdat = new Uint8Array(audioTrack._mdatSize); 380 | audioTrack.samples.forEach(sample => _mdat.set(sample._data, sample._offset)); 381 | mdat = mp4mux.mdat(_mdat); 382 | list = list.concat([moof, mdat]); 383 | 384 | return concatUint8Array(list); 385 | } 386 | } 387 | 388 | function debounce(start, interval) { 389 | var timer; 390 | return function() { 391 | var context = this, args = arguments; 392 | var later = function() { 393 | timer = null; 394 | start.apply(context, args); 395 | }; 396 | if (timer) { 397 | clearTimeout(timer); 398 | } else { 399 | start.apply(context, args); 400 | } 401 | timer = setTimeout(later, interval); 402 | }; 403 | }; 404 | 405 | function triggerPerNr(fn, nr) { 406 | let counter = 0; 407 | return () => { 408 | counter++; 409 | if (counter == nr) { 410 | counter = 0; 411 | fn(); 412 | } 413 | } 414 | } 415 | 416 | app.bindVideo = (opts) => { 417 | let video = opts.video; 418 | let streams = new Streams({urls:opts.src, fakeDuration:opts.duration}); 419 | let mediaSource = new MediaSource(); 420 | video.src = URL.createObjectURL(mediaSource); 421 | 422 | let self = {mediaSource, streams, onSeek: []}; 423 | 424 | let sourceBuffer; 425 | let sourceBufferOnUpdateend; 426 | 427 | let tryPrefetch; 428 | let clearBufferAndPrefetch; 429 | { 430 | let fetching; 431 | let pending = []; 432 | 433 | let doaction = fn => { 434 | if (sourceBuffer.updating) { 435 | pending.push(fn); 436 | } else fn() 437 | } 438 | 439 | sourceBufferOnUpdateend = () => { 440 | if (pending.length > 0) { 441 | dbp('updateend: do pending'); 442 | pending[0](); 443 | pending = pending.slice(1); 444 | } 445 | let buffered = sourceBuffer.buffered; 446 | } 447 | 448 | let fetchAndAppend = (time,duration) => { 449 | let indexStart = streams.findIndexByTime(time); 450 | if (indexStart == null) 451 | return; 452 | let indexEnd = indexStart; 453 | for (let i = indexStart; i < streams.keyframes.length; i++) { 454 | let e = streams.keyframes[i]; 455 | if (e.timeEnd > time+duration) { 456 | indexEnd = i; 457 | break; 458 | } 459 | } 460 | 461 | let sess = streams.fetchMediaSegmentsByIndex(indexStart, indexEnd); 462 | fetching = sess; 463 | sess.then(segbuf => { 464 | if (sess === fetching) { 465 | fetching = null; 466 | } 467 | if (segbuf) { 468 | doaction(() => sourceBuffer.appendBuffer(segbuf)); 469 | } 470 | }); 471 | } 472 | 473 | let stopFetching = () => { 474 | if (fetching) { 475 | fetching.cancel(); 476 | fetching = null; 477 | } 478 | } 479 | 480 | tryPrefetch = (duration=10) => { 481 | if (fetching || sourceBuffer.updating) 482 | return; 483 | 484 | let time; 485 | let buffered = sourceBuffer.buffered; 486 | if (buffered.length > 0) { 487 | time = buffered.end(buffered.length-1); 488 | } else { 489 | time = 0; 490 | } 491 | 492 | if (time < video.currentTime + 60.0) 493 | fetchAndAppend(time, duration); 494 | } 495 | 496 | clearBufferAndPrefetch = (duration=10) => { 497 | dbp('prefetch: clearBufferAndPrefetch'); 498 | 499 | if (sourceBuffer.updating) 500 | sourceBuffer.abort(); 501 | 502 | let time = video.currentTime; 503 | stopFetching(); 504 | 505 | sourceBuffer.remove(0, video.duration); 506 | if (time > streams.duration) { 507 | // wait probe done 508 | } else { 509 | fetchAndAppend(time, duration); 510 | } 511 | } 512 | } 513 | 514 | let currentTimeIsBuffered = () => { 515 | let buffered = sourceBuffer.buffered; 516 | if (buffered.length == 0) 517 | return; 518 | return video.currentTime >= buffered.start(0) && 519 | video.currentTime < buffered.end(buffered.length-1); 520 | }; 521 | 522 | streams.onProbeProgress = (stream, i) => { 523 | if (i > 0 && stream.timeStart <= video.currentTime && video.currentTime < stream.timeEnd) { 524 | dbp('onProbeProgress:', i, 'need prefetch'); 525 | clearBufferAndPrefetch(); 526 | } 527 | } 528 | 529 | video.addEventListener('seeking', debounce(() => { 530 | if (!currentTimeIsBuffered()) { 531 | dbp('seeking(not buffered):', video.currentTime); 532 | clearBufferAndPrefetch(); 533 | } else { 534 | dbp('seeking(buffered):', video.currentTime); 535 | } 536 | self.onSeek.forEach(x => x()); 537 | }, 200)); 538 | 539 | mediaSource.addEventListener('sourceended', () => dbp('mediaSource: sourceended')) 540 | mediaSource.addEventListener('sourceclose', () => dbp('mediaSource: sourceclose')) 541 | 542 | mediaSource.addEventListener('sourceopen', e => { 543 | if (mediaSource.sourceBuffers.length > 0) 544 | return; 545 | 546 | //sourceBuffer = mediaSource.addSourceBuffer('video/mp4; codecs="avc1.64001E, mp4a.40.2"'); 547 | let codecType = 'video/mp4; codecs="avc1.640029, mp4a.40.05"'; 548 | dbp('codec supported:', MediaSource.isTypeSupported(codecType)); 549 | sourceBuffer = mediaSource.addSourceBuffer(codecType); 550 | self.sourceBuffer = sourceBuffer; 551 | 552 | sourceBuffer.addEventListener('error', () => dbp('sourceBuffer: error')); 553 | sourceBuffer.addEventListener('abort', () => dbp('sourceBuffer: abort')); 554 | sourceBuffer.addEventListener('updateend', () => { 555 | //dbp('sourceBuffer: updateend') 556 | sourceBufferOnUpdateend(); 557 | }); 558 | 559 | sourceBuffer.addEventListener('update', () => { 560 | let ranges = []; 561 | let buffered = sourceBuffer.buffered; 562 | for (let i = 0; i < buffered.length; i++) { 563 | ranges.push([buffered.start(i), buffered.end(i)]); 564 | } 565 | dbp('bufupdate:', JSON.stringify(ranges), 'time', video.currentTime); 566 | 567 | if (buffered.length > 0) { 568 | if (video.currentTime < buffered.start(0) || 569 | video.currentTime > buffered.end(buffered.length-1)) 570 | { 571 | video.currentTime = buffered.start(0)+0.1; 572 | } 573 | } 574 | }); 575 | 576 | streams.probeFirst().then(() => { 577 | sourceBuffer.appendBuffer(streams.getInitSegment()); 578 | }); 579 | 580 | video.addEventListener('loadedmetadata', () => { 581 | tryPrefetch(5.0); 582 | setInterval(() => { 583 | tryPrefetch(); 584 | }, 1500); 585 | }); 586 | }); 587 | 588 | return self; 589 | } 590 | 591 | app.Streams = Streams; 592 | module.exports = app; 593 | 594 | -------------------------------------------------------------------------------- /mp4mux.js: -------------------------------------------------------------------------------- 1 | /** 2 | * mux.js 3 | * 4 | * Copyright (c) 2015 Brightcove 5 | * All rights reserved. 6 | * 7 | * Functions that generate fragmented MP4s suitable for use with Media 8 | * Source Extensions. 9 | */ 10 | 'use strict'; 11 | 12 | var FLAGS = {}; 13 | 14 | FLAGS.MOV_TFHD_BASE_DATA_OFFSET = 0x01 15 | FLAGS.MOV_TFHD_STSD_ID = 0x02 16 | FLAGS.MOV_TFHD_DEFAULT_DURATION = 0x08 17 | FLAGS.MOV_TFHD_DEFAULT_SIZE = 0x10 18 | FLAGS.MOV_TFHD_DEFAULT_FLAGS = 0x20 19 | FLAGS.MOV_TFHD_DURATION_IS_EMPTY = 0x010000 20 | FLAGS.MOV_TFHD_DEFAULT_BASE_IS_MOOF = 0x020000 21 | 22 | FLAGS.MOV_TRUN_DATA_OFFSET = 0x01 23 | FLAGS.MOV_TRUN_FIRST_SAMPLE_FLAGS = 0x04 24 | FLAGS.MOV_TRUN_SAMPLE_DURATION = 0x100 25 | FLAGS.MOV_TRUN_SAMPLE_SIZE = 0x200 26 | FLAGS.MOV_TRUN_SAMPLE_FLAGS = 0x400 27 | FLAGS.MOV_TRUN_SAMPLE_CTS = 0x800 28 | 29 | FLAGS.MOV_FRAG_SAMPLE_FLAG_DEGRADATION_PRIORITY_MASK = 0x0000ffff 30 | FLAGS.MOV_FRAG_SAMPLE_FLAG_IS_NON_SYNC = 0x00010000 31 | FLAGS.MOV_FRAG_SAMPLE_FLAG_PADDING_MASK = 0x000e0000 32 | FLAGS.MOV_FRAG_SAMPLE_FLAG_REDUNDANCY_MASK = 0x00300000 33 | FLAGS.MOV_FRAG_SAMPLE_FLAG_DEPENDED_MASK = 0x00c00000 34 | FLAGS.MOV_FRAG_SAMPLE_FLAG_DEPENDS_MASK = 0x03000000 35 | 36 | FLAGS.MOV_FRAG_SAMPLE_FLAG_DEPENDS_NO = 0x02000000 37 | FLAGS.MOV_FRAG_SAMPLE_FLAG_DEPENDS_YES = 0x01000000 38 | 39 | FLAGS.MOV_TKHD_FLAG_ENABLED = 0x0001 40 | FLAGS.MOV_TKHD_FLAG_IN_MOVIE = 0x0002 41 | FLAGS.MOV_TKHD_FLAG_IN_PREVIEW = 0x0004 42 | FLAGS.MOV_TKHD_FLAG_IN_POSTER = 0x0008 43 | 44 | var box, dinf, esds, ftyp, mdat, mfhd, minf, moof, moov, mvex, mvhd, trak, 45 | tkhd, mdia, mdhd, hdlr, sdtp, stbl, stsd, styp, traf, trex, trun, 46 | types, MAJOR_BRAND, MINOR_VERSION, AVC1_BRAND, VIDEO_HDLR, 47 | AUDIO_HDLR, HDLR_TYPES, VMHD, SMHD, DREF, STCO, STSC, STSZ, STTS; 48 | 49 | // pre-calculate constants 50 | (function() { 51 | var i; 52 | types = { 53 | avc1: [], // codingname 54 | avcC: [], 55 | btrt: [], 56 | dinf: [], 57 | dref: [], 58 | esds: [], 59 | ftyp: [], 60 | hdlr: [], 61 | mdat: [], 62 | mdhd: [], 63 | mdia: [], 64 | mfhd: [], 65 | minf: [], 66 | moof: [], 67 | moov: [], 68 | mp4a: [], // codingname 69 | mvex: [], 70 | mvhd: [], 71 | sdtp: [], 72 | smhd: [], 73 | stbl: [], 74 | stco: [], 75 | stsc: [], 76 | stsd: [], 77 | stsz: [], 78 | stts: [], 79 | styp: [], 80 | tfdt: [], 81 | tfhd: [], 82 | traf: [], 83 | trak: [], 84 | trun: [], 85 | trex: [], 86 | tkhd: [], 87 | vmhd: [] 88 | }; 89 | 90 | for (i in types) { 91 | if (types.hasOwnProperty(i)) { 92 | types[i] = [ 93 | i.charCodeAt(0), 94 | i.charCodeAt(1), 95 | i.charCodeAt(2), 96 | i.charCodeAt(3) 97 | ]; 98 | } 99 | } 100 | 101 | MAJOR_BRAND = new Uint8Array([ 102 | 'i'.charCodeAt(0), 103 | 's'.charCodeAt(0), 104 | 'o'.charCodeAt(0), 105 | 'm'.charCodeAt(0) 106 | ]); 107 | AVC1_BRAND = new Uint8Array([ 108 | 'a'.charCodeAt(0), 109 | 'v'.charCodeAt(0), 110 | 'c'.charCodeAt(0), 111 | '1'.charCodeAt(0) 112 | ]); 113 | MINOR_VERSION = new Uint8Array([0, 0, 0, 1]); 114 | VIDEO_HDLR = new Uint8Array([ 115 | 0x00, // version 0 116 | 0x00, 0x00, 0x00, // flags 117 | 0x00, 0x00, 0x00, 0x00, // pre_defined 118 | 0x76, 0x69, 0x64, 0x65, // handler_type: 'vide' 119 | 0x00, 0x00, 0x00, 0x00, // reserved 120 | 0x00, 0x00, 0x00, 0x00, // reserved 121 | 0x00, 0x00, 0x00, 0x00, // reserved 122 | 0x56, 0x69, 0x64, 0x65, 123 | 0x6f, 0x48, 0x61, 0x6e, 124 | 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler' 125 | ]); 126 | AUDIO_HDLR = new Uint8Array([ 127 | 0x00, // version 0 128 | 0x00, 0x00, 0x00, // flags 129 | 0x00, 0x00, 0x00, 0x00, // pre_defined 130 | 0x73, 0x6f, 0x75, 0x6e, // handler_type: 'soun' 131 | 0x00, 0x00, 0x00, 0x00, // reserved 132 | 0x00, 0x00, 0x00, 0x00, // reserved 133 | 0x00, 0x00, 0x00, 0x00, // reserved 134 | 0x53, 0x6f, 0x75, 0x6e, 135 | 0x64, 0x48, 0x61, 0x6e, 136 | 0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'SoundHandler' 137 | ]); 138 | HDLR_TYPES = { 139 | "video":VIDEO_HDLR, 140 | "audio": AUDIO_HDLR 141 | }; 142 | DREF = new Uint8Array([ 143 | 0x00, // version 0 144 | 0x00, 0x00, 0x00, // flags 145 | 0x00, 0x00, 0x00, 0x01, // entry_count 146 | 0x00, 0x00, 0x00, 0x0c, // entry_size 147 | 0x75, 0x72, 0x6c, 0x20, // 'url' type 148 | 0x00, // version 0 149 | 0x00, 0x00, 0x01 // entry_flags 150 | ]); 151 | SMHD = new Uint8Array([ 152 | 0x00, // version 153 | 0x00, 0x00, 0x00, // flags 154 | 0x00, 0x00, // balance, 0 means centered 155 | 0x00, 0x00 // reserved 156 | ]); 157 | STCO = new Uint8Array([ 158 | 0x00, // version 159 | 0x00, 0x00, 0x00, // flags 160 | 0x00, 0x00, 0x00, 0x00 // entry_count 161 | ]); 162 | STSC = STCO; 163 | STSZ = new Uint8Array([ 164 | 0x00, // version 165 | 0x00, 0x00, 0x00, // flags 166 | 0x00, 0x00, 0x00, 0x00, // sample_size 167 | 0x00, 0x00, 0x00, 0x00, // sample_count 168 | ]); 169 | STTS = STCO; 170 | VMHD = new Uint8Array([ 171 | 0x00, // version 172 | 0x00, 0x00, 0x01, // flags 173 | 0x00, 0x00, // graphicsmode 174 | 0x00, 0x00, 175 | 0x00, 0x00, 176 | 0x00, 0x00 // opcolor 177 | ]); 178 | })(); 179 | 180 | box = function(type) { 181 | var 182 | payload = [], 183 | size = 0, 184 | i, 185 | result, 186 | view; 187 | 188 | for (i = 1; i < arguments.length; i++) { 189 | payload.push(arguments[i]); 190 | } 191 | 192 | i = payload.length; 193 | 194 | // calculate the total size we need to allocate 195 | while (i--) { 196 | size += payload[i].byteLength; 197 | } 198 | result = new Uint8Array(size + 8); 199 | view = new DataView(result.buffer, result.byteOffset, result.byteLength); 200 | view.setUint32(0, result.byteLength); 201 | result.set(type, 4); 202 | 203 | // copy the payload into the result 204 | for (i = 0, size = 8; i < payload.length; i++) { 205 | result.set(payload[i], size); 206 | size += payload[i].byteLength; 207 | } 208 | return result; 209 | }; 210 | 211 | dinf = function() { 212 | return box(types.dinf, box(types.dref, DREF)); 213 | }; 214 | 215 | esds = function(track) { 216 | var AudioSpecificConfig; 217 | if (track.AudioSpecificConfig) 218 | AudioSpecificConfig = Array.prototype.slice.call(track.AudioSpecificConfig); 219 | else 220 | AudioSpecificConfig = [ 221 | (track.audioobjecttype << 3) | (track.samplingfrequencyindex >>> 1), 222 | (track.samplingfrequencyindex << 7) | (track.channelcount << 3), 223 | ]; 224 | 225 | // ISO/IEC 14496-3, AudioSpecificConfig 226 | // for samplingFrequencyIndex see ISO/IEC 13818-7:2006, 8.1.3.2.2, Table 35 227 | 228 | return box(types.esds, new Uint8Array([ 229 | 0x00, // version 230 | 0x00, 0x00, 0x00, // flags 231 | 232 | // ES_Descriptor 233 | 0x03, // tag, ES_DescrTag 234 | 0x17+AudioSpecificConfig.length, // length 235 | 0x00, 0x00, // ES_ID 236 | 0x00, // streamDependenceFlag, URL_flag, reserved, streamPriority 237 | 238 | // DecoderConfigDescriptor 239 | 0x04, // tag, DecoderConfigDescrTag 240 | 0x0f+AudioSpecificConfig.length, // length 241 | 0x40, // object type 242 | 0x15, // streamType 243 | 0x00, 0x06, 0x00, // bufferSizeDB 244 | 0x00, 0x00, 0xda, 0xc0, // maxBitrate 245 | 0x00, 0x00, 0xda, 0xc0, // avgBitrate 246 | 247 | // DecoderSpecificInfo 248 | 0x05, // tag, DecoderSpecificInfoTag 249 | AudioSpecificConfig.length, // length 250 | ].concat(AudioSpecificConfig).concat([ 251 | 0x06, 0x01, 0x02 // GASpecificConfig 252 | ]))); 253 | }; 254 | 255 | ftyp = function() { 256 | return box(types.ftyp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND, AVC1_BRAND); 257 | }; 258 | hdlr = function(type) { 259 | return box(types.hdlr, HDLR_TYPES[type]); 260 | }; 261 | mdat = function(data) { 262 | return box(types.mdat, data); 263 | }; 264 | 265 | mdhd = function(track) { 266 | var result = new Uint8Array([ 267 | 0x00, // version 0 268 | 0x00, 0x00, 0x00, // flags 269 | 0x00, 0x00, 0x00, 0x02, // creation_time 270 | 0x00, 0x00, 0x00, 0x03, // modification_time 271 | 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second 272 | 273 | (track.duration >>> 24) & 0xFF, 274 | (track.duration >>> 16) & 0xFF, 275 | (track.duration >>> 8) & 0xFF, 276 | track.duration & 0xFF, // duration 277 | 0x55, 0xc4, // 'und' language (undetermined) 278 | 0x00, 0x00 279 | ]); 280 | 281 | // Use the sample rate from the track metadata, when it is 282 | // defined. The sample rate can be parsed out of an ADTS header, for 283 | // instance. 284 | /*if (track.samplerate) { 285 | result[12] = (track.samplerate >>> 24) & 0xFF; 286 | result[13] = (track.samplerate >>> 16) & 0xFF; 287 | result[14] = (track.samplerate >>> 8) & 0xFF; 288 | result[15] = (track.samplerate) & 0xFF; 289 | }*/ 290 | 291 | return box(types.mdhd, result); 292 | }; 293 | 294 | mdia = function(track) { 295 | return box(types.mdia, mdhd(track), hdlr(track.type), minf(track)); 296 | }; 297 | 298 | mfhd = function(sequenceNumber) { 299 | return box(types.mfhd, new Uint8Array([ 300 | 0x00, 301 | 0x00, 0x00, 0x00, // flags 302 | (sequenceNumber & 0xFF000000) >> 24, 303 | (sequenceNumber & 0xFF0000) >> 16, 304 | (sequenceNumber & 0xFF00) >> 8, 305 | sequenceNumber & 0xFF, // sequence_number 306 | ])); 307 | }; 308 | 309 | minf = function(track) { 310 | return box(types.minf, 311 | track.type === 'video' ? box(types.vmhd, VMHD) : box(types.smhd, SMHD), 312 | dinf(), 313 | stbl(track)); 314 | }; 315 | 316 | moof = function(sequenceNumber, tracks) { 317 | var 318 | trackFragments = [], 319 | i = tracks.length; 320 | // build traf boxes for each track fragment 321 | while (i--) { 322 | trackFragments[i] = traf(tracks[i]); 323 | } 324 | return box.apply(null, [ 325 | types.moof, 326 | mfhd(sequenceNumber) 327 | ].concat(trackFragments)); 328 | }; 329 | 330 | /** 331 | * Returns a movie box. 332 | * @param tracks {array} the tracks associated with this movie 333 | * @see ISO/IEC 14496-12:2012(E), section 8.2.1 334 | */ 335 | moov = function(tracks, duration) { 336 | var 337 | i = tracks.length, 338 | boxes = []; 339 | 340 | while (i--) { 341 | boxes[i] = trak(tracks[i]); 342 | } 343 | 344 | return box.apply(null, [types.moov, mvhd(duration||0xffffffff)].concat(boxes).concat(mvex(tracks))); 345 | }; 346 | mvex = function(tracks) { 347 | var 348 | i = tracks.length, 349 | boxes = []; 350 | 351 | while (i--) { 352 | boxes[i] = trex(tracks[i]); 353 | } 354 | return box.apply(null, [types.mvex].concat(boxes)); 355 | }; 356 | mvhd = function(duration) { 357 | var 358 | bytes = new Uint8Array([ 359 | 0x00, // version 0 360 | 0x00, 0x00, 0x00, // flags 361 | 0x00, 0x00, 0x00, 0x01, // creation_time 362 | 0x00, 0x00, 0x00, 0x02, // modification_time 363 | 0x00, 0x01, 0x5f, 0x90, // timescale, 90,000 "ticks" per second 364 | (duration & 0xFF000000) >> 24, 365 | (duration & 0xFF0000) >> 16, 366 | (duration & 0xFF00) >> 8, 367 | duration & 0xFF, // duration 368 | 0x00, 0x01, 0x00, 0x00, // 1.0 rate 369 | 0x01, 0x00, // 1.0 volume 370 | 0x00, 0x00, // reserved 371 | 0x00, 0x00, 0x00, 0x00, // reserved 372 | 0x00, 0x00, 0x00, 0x00, // reserved 373 | 0x00, 0x01, 0x00, 0x00, 374 | 0x00, 0x00, 0x00, 0x00, 375 | 0x00, 0x00, 0x00, 0x00, 376 | 0x00, 0x00, 0x00, 0x00, 377 | 0x00, 0x01, 0x00, 0x00, 378 | 0x00, 0x00, 0x00, 0x00, 379 | 0x00, 0x00, 0x00, 0x00, 380 | 0x00, 0x00, 0x00, 0x00, 381 | 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix 382 | 0x00, 0x00, 0x00, 0x00, 383 | 0x00, 0x00, 0x00, 0x00, 384 | 0x00, 0x00, 0x00, 0x00, 385 | 0x00, 0x00, 0x00, 0x00, 386 | 0x00, 0x00, 0x00, 0x00, 387 | 0x00, 0x00, 0x00, 0x00, // pre_defined 388 | 0xff, 0xff, 0xff, 0xff // next_track_ID 389 | ]); 390 | return box(types.mvhd, bytes); 391 | }; 392 | 393 | sdtp = function(track) { 394 | var 395 | samples = track.samples || [], 396 | bytes = new Uint8Array(4 + samples.length), 397 | flags, 398 | i; 399 | 400 | // leave the full box header (4 bytes) all zero 401 | 402 | // write the sample table 403 | for (i = 0; i < samples.length; i++) { 404 | flags = samples[i].flags; 405 | 406 | bytes[i + 4] = (flags.dependsOn << 4) | 407 | (flags.isDependedOn << 2) | 408 | (flags.hasRedundancy); 409 | } 410 | 411 | return box(types.sdtp, 412 | bytes); 413 | }; 414 | 415 | stbl = function(track) { 416 | return box(types.stbl, 417 | stsd(track), 418 | box(types.stts, STTS), 419 | box(types.stsc, STSC), 420 | box(types.stsz, STSZ), 421 | box(types.stco, STCO)); 422 | }; 423 | 424 | (function() { 425 | var videoSample, audioSample; 426 | 427 | stsd = function(track) { 428 | 429 | return box(types.stsd, new Uint8Array([ 430 | 0x00, // version 0 431 | 0x00, 0x00, 0x00, // flags 432 | 0x00, 0x00, 0x00, 0x01 433 | ]), track.type === 'video' ? videoSample(track) : audioSample(track)); 434 | }; 435 | 436 | videoSample = function(track) { 437 | var fieldsBuf = new Uint8Array([ 438 | 0x00, 0x00, 0x00, 439 | 0x00, 0x00, 0x00, // reserved 440 | 0x00, 0x01, // data_reference_index 441 | 0x00, 0x00, // pre_defined 442 | 0x00, 0x00, // reserved 443 | 0x00, 0x00, 0x00, 0x00, 444 | 0x00, 0x00, 0x00, 0x00, 445 | 0x00, 0x00, 0x00, 0x00, // pre_defined 446 | (track.width & 0xff00) >> 8, 447 | track.width & 0xff, // width 448 | (track.height & 0xff00) >> 8, 449 | track.height & 0xff, // height 450 | 0x00, 0x48, 0x00, 0x00, // horizresolution 451 | 0x00, 0x48, 0x00, 0x00, // vertresolution 452 | 0x00, 0x00, 0x00, 0x00, // reserved 453 | 0x00, 0x01, // frame_count 454 | 0x13, 455 | 0x76, 0x69, 0x64, 0x65, 456 | 0x6f, 0x6a, 0x73, 0x2d, 457 | 0x63, 0x6f, 0x6e, 0x74, 458 | 0x72, 0x69, 0x62, 0x2d, 459 | 0x68, 0x6c, 0x73, 0x00, 460 | 0x00, 0x00, 0x00, 0x00, 461 | 0x00, 0x00, 0x00, 0x00, 462 | 0x00, 0x00, 0x00, // compressorname 463 | 0x00, 0x18, // depth = 24 464 | 0x11, 0x11 // pre_defined = -1 465 | ]); 466 | var avcCBox; 467 | 468 | if (track.AVCDecoderConfigurationRecord) { 469 | avcCBox = box(types.avcC, track.AVCDecoderConfigurationRecord); 470 | } else { 471 | var sps = track.sps || [], 472 | pps = track.pps || [], 473 | sequenceParameterSets = [], 474 | pictureParameterSets = [], 475 | i; 476 | 477 | // assemble the SPSs 478 | for (i = 0; i < sps.length; i++) { 479 | sequenceParameterSets.push((sps[i].byteLength & 0xFF00) >>> 8); 480 | sequenceParameterSets.push((sps[i].byteLength & 0xFF)); // sequenceParameterSetLength 481 | sequenceParameterSets = sequenceParameterSets.concat(Array.prototype.slice.call(sps[i])); // SPS 482 | } 483 | 484 | // assemble the PPSs 485 | for (i = 0; i < pps.length; i++) { 486 | pictureParameterSets.push((pps[i].byteLength & 0xFF00) >>> 8); 487 | pictureParameterSets.push((pps[i].byteLength & 0xFF)); 488 | pictureParameterSets = pictureParameterSets.concat(Array.prototype.slice.call(pps[i])); 489 | } 490 | 491 | avcCBox = box(types.avcC, new Uint8Array([ 492 | 0x01, // configurationVersion 493 | track.profileIdc, // AVCProfileIndication 494 | track.profileCompatibility, // profile_compatibility 495 | track.levelIdc, // AVCLevelIndication 496 | 0xff // lengthSizeMinusOne, hard-coded to 4 bytes 497 | ].concat([ 498 | sps.length // numOfSequenceParameterSets 499 | ]).concat(sequenceParameterSets).concat([ 500 | pps.length // numOfPictureParameterSets 501 | ]).concat(pictureParameterSets))) // "PPS" 502 | } 503 | 504 | return box(types.avc1, fieldsBuf, avcCBox, box(types.btrt, new Uint8Array([ 505 | 0x00, 0x1c, 0x9c, 0x80, // bufferSizeDB 506 | 0x00, 0x2d, 0xc6, 0xc0, // maxBitrate 507 | 0x00, 0x2d, 0xc6, 0xc0 // avgBitrate 508 | ]))); 509 | }; 510 | 511 | audioSample = function(track) { 512 | return box(types.mp4a, new Uint8Array([ 513 | 514 | // SampleEntry, ISO/IEC 14496-12 515 | 0x00, 0x00, 0x00, 516 | 0x00, 0x00, 0x00, // reserved 517 | 0x00, 0x01, // data_reference_index 518 | 519 | // AudioSampleEntry, ISO/IEC 14496-12 520 | 0x00, 0x00, 0x00, 0x00, // reserved 521 | 0x00, 0x00, 0x00, 0x00, // reserved 522 | (track.channelcount & 0xff00) >> 8, 523 | (track.channelcount & 0xff), // channelcount 524 | 525 | (track.samplesize & 0xff00) >> 8, 526 | (track.samplesize & 0xff), // samplesize 527 | 0x00, 0x00, // pre_defined 528 | 0x00, 0x00, // reserved 529 | 530 | (track.samplerate & 0xff00) >> 8, 531 | (track.samplerate & 0xff), 532 | 0x00, 0x00 // samplerate, 16.16 533 | 534 | // MP4AudioSampleEntry, ISO/IEC 14496-14 535 | ]), esds(track)); 536 | }; 537 | })(); 538 | 539 | styp = function() { 540 | return box(types.styp, MAJOR_BRAND, MINOR_VERSION, MAJOR_BRAND); 541 | }; 542 | 543 | tkhd = function(track) { 544 | var result = new Uint8Array([ 545 | 0x00, // version 0 546 | 0x00, 0x00, 0x07, // flags 547 | 0x00, 0x00, 0x00, 0x00, // creation_time 548 | 0x00, 0x00, 0x00, 0x00, // modification_time 549 | (track.id & 0xFF000000) >> 24, 550 | (track.id & 0xFF0000) >> 16, 551 | (track.id & 0xFF00) >> 8, 552 | track.id & 0xFF, // track_ID 553 | 0x00, 0x00, 0x00, 0x00, // reserved 554 | (track.duration & 0xFF000000) >> 24, 555 | (track.duration & 0xFF0000) >> 16, 556 | (track.duration & 0xFF00) >> 8, 557 | track.duration & 0xFF, // duration 558 | 0x00, 0x00, 0x00, 0x00, 559 | 0x00, 0x00, 0x00, 0x00, // reserved 560 | 0x00, 0x00, // layer 561 | 0x00, 0x00, // alternate_group 562 | 0x01, 0x00, // non-audio track volume 563 | 0x00, 0x00, // reserved 564 | 0x00, 0x01, 0x00, 0x00, 565 | 0x00, 0x00, 0x00, 0x00, 566 | 0x00, 0x00, 0x00, 0x00, 567 | 0x00, 0x00, 0x00, 0x00, 568 | 0x00, 0x01, 0x00, 0x00, 569 | 0x00, 0x00, 0x00, 0x00, 570 | 0x00, 0x00, 0x00, 0x00, 571 | 0x00, 0x00, 0x00, 0x00, 572 | 0x40, 0x00, 0x00, 0x00, // transformation: unity matrix 573 | (track.width & 0xFF00) >> 8, 574 | track.width & 0xFF, 575 | 0x00, 0x00, // width 576 | (track.height & 0xFF00) >> 8, 577 | track.height & 0xFF, 578 | 0x00, 0x00 // height 579 | ]); 580 | 581 | return box(types.tkhd, result); 582 | }; 583 | 584 | /** 585 | * Generate a track fragment (traf) box. A traf box collects metadata 586 | * about tracks in a movie fragment (moof) box. 587 | */ 588 | traf = function(track) { 589 | var trackFragmentHeader, trackFragmentDecodeTime, 590 | trackFragmentRun, sampleDependencyTable, dataOffset; 591 | 592 | // diff tfhd 593 | // his: video.flags=MOV_TFHD_DEFAULT_BASE_IS_MOOF|MOV_TFHD_DEFAULT_FLAGS 594 | // video.default_flags=MOV_FRAG_SAMPLE_FLAG_DEPENDS_YES|MOV_FRAG_SAMPLE_FLAG_IS_NON_SYNC 595 | // audio.flags=MOV_TFHD_DEFAULT_BASE_IS_MOOF 596 | // 597 | // mine: video.flags=MOV_TFHD_STSD_ID|MOV_TFHD_DEFAULT_DURATION|MOV_TFHD_DEFAULT_SIZE|MOV_TFHD_DEFAULT_FLAGS 598 | // audio.flags=MOV_TFHD_STSD_ID|MOV_TFHD_DEFAULT_DURATION|MOV_TFHD_DEFAULT_SIZE|MOV_TFHD_DEFAULT_FLAGS 599 | 600 | trackFragmentHeader = box(types.tfhd, new Uint8Array([ 601 | 0x00, // version 0 602 | 0x00, 0x00, 0x3a, 603 | (track.id & 0xFF000000) >> 24, 604 | (track.id & 0xFF0000) >> 16, 605 | (track.id & 0xFF00) >> 8, 606 | (track.id & 0xFF), // track_ID 607 | 0x00, 0x00, 0x00, 0x01, // sample_description_index 608 | 0x00, 0x00, 0x00, 0x00, // default_sample_duration 609 | 0x00, 0x00, 0x00, 0x00, // default_sample_size 610 | 0x00, 0x00, 0x00, 0x00 // default_sample_flags 611 | ])); 612 | 613 | trackFragmentDecodeTime = box(types.tfdt, new Uint8Array([ 614 | 0x00, // version 0 615 | 0x00, 0x00, 0x00, // flags 616 | // baseMediaDecodeTime 617 | (track.baseMediaDecodeTime >>> 24) & 0xFF, 618 | (track.baseMediaDecodeTime >>> 16) & 0xFF, 619 | (track.baseMediaDecodeTime >>> 8) & 0xFF, 620 | track.baseMediaDecodeTime & 0xFF 621 | ])); 622 | 623 | // the data offset specifies the number of bytes from the start of 624 | // the containing moof to the first payload byte of the associated 625 | // mdat 626 | dataOffset = (32 + // tfhd 627 | 16 + // tfdt 628 | 8 + // traf header 629 | 16 + // mfhd 630 | 8 + // moof header 631 | 8); // mdat header 632 | 633 | // audio tracks require less metadata 634 | if (track.type === 'audio') { 635 | trackFragmentRun = trun(track, dataOffset); 636 | return box(types.traf, 637 | trackFragmentHeader, 638 | trackFragmentDecodeTime, 639 | trackFragmentRun); 640 | } 641 | 642 | // video tracks should contain an independent and disposable samples 643 | // box (sdtp) 644 | // generate one and adjust offsets to match 645 | sampleDependencyTable = sdtp(track); 646 | trackFragmentRun = trun(track, sampleDependencyTable.length + dataOffset); 647 | 648 | return box(types.traf, 649 | trackFragmentHeader, 650 | trackFragmentDecodeTime, 651 | trackFragmentRun, 652 | sampleDependencyTable); 653 | }; 654 | 655 | /** 656 | * Generate a track box. 657 | * @param track {object} a track definition 658 | * @return {Uint8Array} the track box 659 | */ 660 | trak = function(track) { 661 | track.duration = track.duration || 0xffffffff; 662 | return box(types.trak, 663 | tkhd(track), 664 | mdia(track)); 665 | }; 666 | 667 | trex = function(track) { 668 | var result = new Uint8Array([ 669 | 0x00, // version 0 670 | 0x00, 0x00, 0x00, // flags 671 | (track.id & 0xFF000000) >> 24, 672 | (track.id & 0xFF0000) >> 16, 673 | (track.id & 0xFF00) >> 8, 674 | (track.id & 0xFF), // track_ID 675 | 0x00, 0x00, 0x00, 0x01, // default_sample_description_index 676 | 0x00, 0x00, 0x00, 0x00, // default_sample_duration 677 | 0x00, 0x00, 0x00, 0x00, // default_sample_size 678 | 0x00, 0x01, 0x00, 0x01 // default_sample_flags 679 | ]); 680 | // the last two bytes of default_sample_flags is the sample 681 | // degradation priority, a hint about the importance of this sample 682 | // relative to others. Lower the degradation priority for all sample 683 | // types other than video. 684 | if (track.type !== 'video') { 685 | result[result.length - 1] = 0x00; 686 | } 687 | 688 | return box(types.trex, result); 689 | }; 690 | 691 | (function() { 692 | var audioTrun, videoTrun, trunHeader; 693 | 694 | // This method assumes all samples are uniform. That is, if a 695 | // duration is present for the first sample, it will be present for 696 | // all subsequent samples. 697 | // see ISO/IEC 14496-12:2012, Section 8.8.8.1 698 | trunHeader = function(samples, offset) { 699 | var durationPresent = 0, sizePresent = 0, 700 | flagsPresent = 0, compositionTimeOffset = 0; 701 | 702 | // trun flag constants 703 | if (samples.length) { 704 | if (samples[0].duration !== undefined) { 705 | durationPresent = 0x1; 706 | } 707 | if (samples[0].size !== undefined) { 708 | sizePresent = 0x2; 709 | } 710 | if (samples[0].flags !== undefined) { 711 | flagsPresent = 0x4; 712 | } 713 | if (samples[0].compositionTimeOffset !== undefined) { 714 | compositionTimeOffset = 0x8; 715 | } 716 | } 717 | 718 | // diff trun 719 | // his: video.flags=MOV_TRUN(DATA_OFFSET|FIRST_SAMPLE_FLAGS|DURATION|SIZE) 720 | // audio.flags=MOV_TRUN(DATA_OFFSET|DURATION|SIZE) 721 | // video.firstsampleflags=MOV_FRAG_SAMPLE_FLAG_DEPENDS_NO 722 | // audio.firstsampleflags=0 723 | // 724 | // mine: video.flags=DATA_OFFSET|SAMPLE_FLAGS|DURATION|SIZE 725 | // audio.flags=DATA_OFFSET|DURATION|SIZE 726 | 727 | return [ 728 | 0x00, // version 0 729 | 0x00, 730 | durationPresent | sizePresent | flagsPresent | compositionTimeOffset, 731 | 0x01, // flags 732 | (samples.length & 0xFF000000) >>> 24, 733 | (samples.length & 0xFF0000) >>> 16, 734 | (samples.length & 0xFF00) >>> 8, 735 | samples.length & 0xFF, // sample_count 736 | (offset & 0xFF000000) >>> 24, 737 | (offset & 0xFF0000) >>> 16, 738 | (offset & 0xFF00) >>> 8, 739 | offset & 0xFF // data_offset 740 | ]; 741 | }; 742 | 743 | videoTrun = function(track, offset) { 744 | var bytes, samples, sample, i; 745 | 746 | samples = track.samples || []; 747 | offset += 8 + 12 + (16 * samples.length); 748 | 749 | bytes = trunHeader(samples, offset); 750 | 751 | for (i = 0; i < samples.length; i++) { 752 | sample = samples[i]; 753 | bytes = bytes.concat([ 754 | (sample.duration & 0xFF000000) >>> 24, 755 | (sample.duration & 0xFF0000) >>> 16, 756 | (sample.duration & 0xFF00) >>> 8, 757 | sample.duration & 0xFF, // sample_duration 758 | (sample.size & 0xFF000000) >>> 24, 759 | (sample.size & 0xFF0000) >>> 16, 760 | (sample.size & 0xFF00) >>> 8, 761 | sample.size & 0xFF, // sample_size 762 | (sample.flags.isLeading << 2) | sample.flags.dependsOn, 763 | (sample.flags.isDependedOn << 6) | 764 | (sample.flags.hasRedundancy << 4) | 765 | (sample.flags.paddingValue << 1) | 766 | sample.flags.isNonSyncSample, 767 | sample.flags.degradationPriority & 0xF0 << 8, 768 | sample.flags.degradationPriority & 0x0F, // sample_flags 769 | (sample.compositionTimeOffset & 0xFF000000) >>> 24, 770 | (sample.compositionTimeOffset & 0xFF0000) >>> 16, 771 | (sample.compositionTimeOffset & 0xFF00) >>> 8, 772 | sample.compositionTimeOffset & 0xFF // sample_composition_time_offset 773 | ]); 774 | } 775 | return box(types.trun, new Uint8Array(bytes)); 776 | }; 777 | 778 | audioTrun = function(track, offset) { 779 | var bytes, samples, sample, i; 780 | 781 | samples = track.samples || []; 782 | offset += 8 + 12 + (8 * samples.length); 783 | 784 | bytes = trunHeader(samples, offset); 785 | 786 | for (i = 0; i < samples.length; i++) { 787 | sample = samples[i]; 788 | bytes = bytes.concat([ 789 | (sample.duration & 0xFF000000) >>> 24, 790 | (sample.duration & 0xFF0000) >>> 16, 791 | (sample.duration & 0xFF00) >>> 8, 792 | sample.duration & 0xFF, // sample_duration 793 | (sample.size & 0xFF000000) >>> 24, 794 | (sample.size & 0xFF0000) >>> 16, 795 | (sample.size & 0xFF00) >>> 8, 796 | sample.size & 0xFF]); // sample_size 797 | } 798 | 799 | return box(types.trun, new Uint8Array(bytes)); 800 | }; 801 | 802 | trun = function(track, offset) { 803 | if (track.type === 'audio') { 804 | return audioTrun(track, offset); 805 | } else { 806 | return videoTrun(track, offset); 807 | } 808 | }; 809 | })(); 810 | 811 | module.exports = { 812 | FLAGS: FLAGS, 813 | ftyp: ftyp, 814 | mdat: mdat, 815 | moof: moof, 816 | moov: moov, 817 | timeScale: 90000, 818 | initSegment: function(tracks, duration) { 819 | var 820 | fileType = ftyp(), 821 | movie = moov(tracks, duration), 822 | result; 823 | 824 | result = new Uint8Array(fileType.byteLength + movie.byteLength); 825 | result.set(fileType); 826 | result.set(movie, fileType.byteLength); 827 | return result; 828 | } 829 | }; 830 | 831 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mama-hd", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/nareix/mama-hd.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/nareix/mama-hd/issues" 17 | }, 18 | "homepage": "https://github.com/nareix/mama-hd#readme", 19 | "dependencies": { 20 | "babel-plugin-transform-runtime": "^6.7.5", 21 | "fetch": "^1.0.1", 22 | "nanobar": "^0.4.1", 23 | "querystring": "^0.2.0", 24 | "blueimp-md5": "^2.3.0", 25 | "whatwg-fetch": "^1.0.0" 26 | }, 27 | "devDependencies": { 28 | "babel-core": "^6.7.7", 29 | "babel-loader": "^6.2.4", 30 | "babel-preset-es2015": "^6.6.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packcrx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | rm -rf mama-hd.crx mama-hd.zip 4 | webpack -d 5 | /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --pack-extension=`pwd`/mama-hd --pack-extension-key=`pwd`/mama-hd.pem 6 | zip mama-hd.zip -r mama-hd 7 | 8 | -------------------------------------------------------------------------------- /player.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = () => { 3 | let div = document.createElement('div'); 4 | let damoo = document.createElement('div'); 5 | let video = document.createElement('video'); 6 | 7 | let toolbar = document.createElement('div'); 8 | toolbar.className = 'mama-toolbar'; 9 | toolbar.innerHTML += `` 10 | toolbar.innerHTML += ` 13 | 15 | 16 | `; 17 | toolbar.style.display = 'none'; 18 | document.body.style.background = '#000'; 19 | 20 | div.appendChild(toolbar); 21 | div.appendChild(video); 22 | div.appendChild(damoo); 23 | 24 | div.style.background = '#000'; 25 | div.style.position = 'fixed'; 26 | div.style.top = '0px'; 27 | div.style.left = '0px'; 28 | div.style.zIndex = '1000000'; 29 | 30 | damoo.style.position = 'absolute'; 31 | damoo.style.pointerEvents = 'none'; 32 | damoo.style.overflow = 'hidden'; 33 | 34 | video.autoplay = true; 35 | video.controls = true; 36 | video.style.position = 'absolute'; 37 | video.style.display = 'none'; 38 | 39 | function debounce(start, end, interval) { 40 | var timer; 41 | return function() { 42 | var context = this, args = arguments; 43 | var later = function() { 44 | timer = null; 45 | end.apply(context, args); 46 | }; 47 | if (timer) { 48 | clearTimeout(timer); 49 | } else { 50 | start.apply(context, args); 51 | } 52 | timer = setTimeout(later, interval); 53 | }; 54 | }; 55 | 56 | div.addEventListener('mousemove', debounce(() => { 57 | div.style.cursor = 'default'; 58 | toolbar.style.display = 'flex'; 59 | }, () => { 60 | div.style.cursor = 'none'; 61 | toolbar.style.display = 'none'; 62 | }, 5000)); 63 | 64 | let self = { 65 | video, damoo, div, 66 | onStarted:[], onSuspend:[], onResume:[], 67 | damooEnabled:false, 68 | damooOpacity:1.0, 69 | onDamooOptsChange:[], 70 | }; 71 | 72 | let resize = () => { 73 | let windowRatio = window.innerHeight/window.innerWidth; 74 | let videoRatio = video.videoHeight/video.videoWidth; 75 | if (videoRatio > windowRatio) { 76 | let width = window.innerHeight/videoRatio; 77 | video.style.height = window.innerHeight+'px'; 78 | video.style.width = width+'px'; 79 | video.style.left = (window.innerWidth-width)/2+'px'; 80 | video.style.top = '0px'; 81 | } else { 82 | let height = window.innerWidth*videoRatio; 83 | video.style.width = window.innerWidth+'px'; 84 | video.style.height = height+'px'; 85 | video.style.top = (window.innerHeight-height)/2+'px'; 86 | video.style.left = '0px'; 87 | } 88 | damoo.style.width = video.style.width; 89 | damoo.style.height = video.style.height; 90 | damoo.style.top = video.style.top; 91 | damoo.style.left = video.style.left; 92 | div.style.height = window.innerHeight+'px'; 93 | div.style.width = window.innerWidth+'px'; 94 | } 95 | 96 | let onStarted = () => { 97 | video.style.display = 'block'; 98 | video.removeEventListener('canplay', onStarted); 99 | resize(); 100 | self.onStarted.forEach(cb => cb()); 101 | } 102 | video.addEventListener('canplay', onStarted); 103 | 104 | let toggleFullScreen = () => { 105 | if (!document.webkitFullscreenElement) { 106 | div.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT); 107 | } else { 108 | document.webkitCancelFullScreen(); 109 | } 110 | } 111 | 112 | let togglePlayPause; 113 | { 114 | let playing = false; 115 | let timer; 116 | 117 | let setToPaused = () => { 118 | if (playing) { 119 | playing = false; 120 | console.log('player: suspend'); 121 | self.onSuspend.forEach(x => x()); 122 | } 123 | } 124 | 125 | let setToPlaying = () => { 126 | if (!playing) { 127 | playing = true; 128 | console.log('player: resume'); 129 | self.onResume.forEach(x => x()); 130 | } 131 | } 132 | 133 | video.addEventListener('timeupdate', () => { 134 | if (video.paused) 135 | return; 136 | setToPlaying(); 137 | if (timer) { 138 | clearTimeout(timer); 139 | } 140 | timer = setTimeout(() => { 141 | setToPaused(); 142 | timer = null; 143 | }, 1000); 144 | }); 145 | 146 | togglePlayPause = () => { 147 | if (video.paused) { 148 | video.play(); 149 | } else { 150 | video.pause(); 151 | setToPaused(); 152 | } 153 | } 154 | } 155 | 156 | video.addEventListener('mousedown', () => { 157 | togglePlayPause(); 158 | }) 159 | 160 | let seekDelta = 5.0; 161 | let getSeekTime = (delta) => { 162 | let inc = delta>0?1:-1; 163 | let index = self.streams.findIndexByTime(video.currentTime); 164 | let keyframes = self.streams.keyframes; 165 | for (let i = index; i >= 0 && i < keyframes.length; i += inc) { 166 | let e = keyframes[i]; 167 | if (Math.abs(e.timeStart-video.currentTime) > Math.abs(delta)) { 168 | index = i; 169 | break; 170 | } 171 | } 172 | let time = self.streams.keyframes[index].timeStart; 173 | video.currentTime = time; 174 | }; 175 | let seekBack = () => { 176 | video.currentTime = getSeekTime(-seekDelta); 177 | } 178 | let seekForward = () => { 179 | video.currentTime = getSeekTime(seekDelta); 180 | } 181 | 182 | let volumeDelta = 0.2; 183 | let volumeUp = () => { 184 | if (video.muted) { 185 | video.muted = false; 186 | video.volume = 0.0; 187 | } 188 | video.volume = Math.min(1.0, video.volume+volumeDelta); 189 | } 190 | let volumeDown = () => { 191 | video.volume = Math.max(0, video.volume-volumeDelta); 192 | } 193 | 194 | let toggleMute = () => { 195 | video.muted = !video.muted; 196 | } 197 | 198 | let toggleDamoo; 199 | { 200 | let btn = toolbar.querySelector('.damoo'); 201 | let input = toolbar.querySelector('.damoo-input'); 202 | input.min = 0; 203 | input.max = 1; 204 | input.step = 0.01; 205 | input.value = self.damooOpacity; 206 | if (self.damooEnabled) { 207 | btn.classList.add('selected'); 208 | input.style.display = 'block'; 209 | } else { 210 | input.style.display = 'none'; 211 | } 212 | toggleDamoo = () => { 213 | btn.classList.toggle('selected'); 214 | self.damooEnabled = !self.damooEnabled; 215 | if (self.damooEnabled) { 216 | input.style.display = 'block'; 217 | } else { 218 | input.style.display = 'none'; 219 | } 220 | self.onDamooOptsChange.forEach(x => x()); 221 | } 222 | btn.addEventListener('click', () => { 223 | toggleDamoo(); 224 | }); 225 | input.addEventListener('change', () => { 226 | self.damooOpacity = input.value; 227 | self.onDamooOptsChange.forEach(x => x()); 228 | }); 229 | } 230 | 231 | document.body.addEventListener('keydown', (e) => { 232 | switch (e.code) { 233 | case "Space": { 234 | togglePlayPause(); 235 | e.preventDefault(); 236 | } break; 237 | 238 | case "ArrowUp": { 239 | volumeUp(); 240 | e.preventDefault(); 241 | } break; 242 | 243 | case "KeyD": { 244 | toggleDamoo(); 245 | e.preventDefault(); 246 | } break; 247 | 248 | case "KeyM": { 249 | toggleMute(); 250 | e.preventDefault(); 251 | } break; 252 | 253 | case "ArrowDown": { 254 | volumeDown(); 255 | e.preventDefault(); 256 | } break; 257 | 258 | case "ArrowLeft": { 259 | seekBack(); 260 | e.preventDefault(); 261 | } break; 262 | 263 | case "ArrowRight": { 264 | seekForward(); 265 | e.preventDefault(); 266 | } break; 267 | 268 | case "Enter": { 269 | if (e.metaKey || e.ctrlKey) { 270 | toggleFullScreen(); 271 | e.preventDefault(); 272 | } 273 | } break; 274 | } 275 | 276 | }); 277 | 278 | document.body.style.margin = 0; 279 | document.body.appendChild(div); 280 | resize(); 281 | window.addEventListener('resize', resize); 282 | 283 | return self; 284 | } 285 | 286 | -------------------------------------------------------------------------------- /promise.js: -------------------------------------------------------------------------------- 1 | 2 | exports.Cancellable = input => { 3 | let fulfill; 4 | let promise = new Promise((_fulfill, reject) => { 5 | fulfill = _fulfill; 6 | input.then(fulfill).catch(reject); 7 | }); 8 | promise.cancel = fulfill; 9 | return promise; 10 | } 11 | 12 | -------------------------------------------------------------------------------- /screenshot-1280x800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nareix/mama-hd/2ee17c121fda6749c0d720b51788c4c1447b44bf/screenshot-1280x800.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nareix/mama-hd/2ee17c121fda6749c0d720b51788c4c1447b44bf/screenshot.png -------------------------------------------------------------------------------- /tudou.js: -------------------------------------------------------------------------------- 1 | /* tudou 2 | * @朱一 3 | */ 4 | // TODO: 5 | // cannot play http://www.tudou.com/programs/view/TXBFQYX6F04/ missing vcode 6 | 7 | var youku = require('./youku') 8 | var bilibili = require('./bilibili') 9 | var querystring = require('querystring'); 10 | 11 | exports.testUrl = function (url) { 12 | return /tudou\.com/.test(url); 13 | } 14 | 15 | exports.getVideos = function (url) { 16 | return (() => { 17 | if (window.pageConfig && window.pageConfig.vcode && window.pageConfig.iid) { 18 | return Promise.resolve({vcode: window.pageConfig.vcode, iid: window.pageConfig.iid}); 19 | } 20 | return fetch(url, {credentials: 'include'}).then(res => res.text()).then(res => { 21 | var vcode = res.match(/vcode: '(\S+)'/); 22 | var iid = res.match(/iid: (\S+)/); 23 | if (vcode && iid) 24 | return {vcode:vcode[1], iid:iid[1]}; 25 | }) 26 | })().then(res => { 27 | if (res == null) 28 | throw new Error('vcode iid not found'); 29 | return youku.getVideosByVcode(res.vcode).then(yres => { 30 | yres.iid = res.iid; 31 | return yres; 32 | }); 33 | }) 34 | } 35 | 36 | let getDamooRaw = (id, params) => { 37 | //http://service.danmu.tudou.com/list?7122 38 | //FormData: uid=81677981&mcount=1&iid=132611501&type=1&ct=1001&mat=6 39 | //mat=minute at 40 | params.uid = params.uid || 0; 41 | params.mcount = params.mcount || 5; 42 | params.type = params.type || 1; 43 | params.ct = params.ct || 1001; 44 | var body = querystring.stringify(params); 45 | return fetch('http://service.danmu.tudou.com/list?'+id, { 46 | credentials: 'include', body, method: 'POST', 47 | headers: { 48 | 'Content-Type': 'application/x-www-form-urlencoded', 49 | }, 50 | }).then(res => res.json()).then(res => { 51 | if (!(res && res.result)) 52 | return; 53 | return res.result.map((x, i) => { 54 | let color; 55 | let pos; 56 | if (x.propertis) { 57 | try { 58 | let p = JSON.parse(x.propertis); 59 | color = bilibili.colorDec2Hex(p.color); 60 | if (color.length > 7) 61 | color = color.substr(0, 7); 62 | switch (p.pos) { 63 | case 6: pos = 'bottom'; break; 64 | case 4: pos = 'top'; break; 65 | } 66 | } catch (e) { 67 | } 68 | } 69 | return { 70 | text: x.content, 71 | time: x.playat/1000.0, 72 | color, pos, 73 | } 74 | }).sort((a,b) => a.time-b.time) 75 | }); 76 | } 77 | exports.getDamooRaw = getDamooRaw; 78 | 79 | exports.getDamooProgressive = (vres, cb) => { 80 | let get = minute => { 81 | let n = 1; 82 | getDamooRaw(1234, {iid: vres.iid, mat: minute, mcount: n}).then(res => { 83 | if (res && res.length > 0) { 84 | //console.log(`tudou: damoo loaded minute=[${minute},${minute+n}] n=${res.length}`); 85 | cb(res); 86 | } 87 | if (minute*60 < vres.duration) 88 | get(minute+n); 89 | }); 90 | } 91 | get(0); 92 | } 93 | 94 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: "./index.js", 3 | output: { 4 | path: __dirname, 5 | filename: "./mama-hd/bundle.js" 6 | }, 7 | module: { 8 | } 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /webpack.safari.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: [ 3 | "whatwg-fetch", 4 | "./index.js", 5 | ], 6 | output: { 7 | path: __dirname, 8 | filename: "./mama-hd.safariextension/bundle.js" 9 | }, 10 | module: { 11 | loaders: [{ 12 | test: /\.jsx?$/, 13 | exclude: /(node_modules|bower_components)/, 14 | loader: 'babel', // 'babel-loader' is also a legal name to reference 15 | query: { 16 | presets: ['es2015'], 17 | plugins: ['transform-runtime'], 18 | } 19 | }], 20 | } 21 | }; 22 | 23 | -------------------------------------------------------------------------------- /youku.js: -------------------------------------------------------------------------------- 1 | /* youku 2 | * @朱一 3 | */ 4 | 5 | 'use strict' 6 | 7 | var querystring = require('querystring'); 8 | 9 | exports.testUrl = function (url) { 10 | return url.match(/v\.youku\.com/) 11 | } 12 | 13 | function E(a, c) { 14 | for (var b = [], f = 0, i, e = "", h = 0; 256 > h; h++) b[h] = h; 15 | for (h = 0; 256 > h; h++) f = (f + b[h] + a.charCodeAt(h % a.length)) % 256, i = b[h], b[h] = b[f], b[f] = i; 16 | for (var q = f = h = 0; q < c.length; q++) h = (h + 1) % 256, f = (f + b[h]) % 256, i = b[h], b[h] = b[f], b[f] = i, e += String.fromCharCode(c.charCodeAt(q) ^ b[(b[h] + b[f]) % 256]); 17 | return e 18 | } 19 | 20 | function generate_ep(no,streamfileid,sid,token) { 21 | var number = no.toString(16).toUpperCase(); 22 | if (number.length == 1) { 23 | number = '0'+number; 24 | } 25 | var fcode2 = 'bf7e5f01'; 26 | var fileid = streamfileid.slice(0,8)+number+streamfileid.slice(10); 27 | var ep = encodeURIComponent(btoa(E(fcode2, sid+'_'+fileid+'_'+token))); 28 | return [fileid, ep]; 29 | } 30 | 31 | exports.testEncryptFuncs = function() { 32 | { 33 | let fn = (a,b) => E(a, atob(b)).split('_') 34 | console.log(fn("becaf9be","PgXWTwkWLrPa2fbJ9+JxWtGhuBQ01wnKWRs="),"9461488808682128ae179_4114") 35 | } 36 | 37 | { 38 | let assert = (r1, r2) => { 39 | console.log(r1[0]==r2[0],r1[1]==r2[1]); 40 | } 41 | assert(generate_ep(0,"03008002005715DFD766A500E68D4783E81E57-3E8D-DABF-8542-460ADBBC66A5","24614839104951215057d","1329"),["03008002005715DFD766A500E68D4783E81E57-3E8D-DABF-8542-460ADBBC66A5","cCaSG02FVccB5SfWjT8bZinicXBbXP4J9h%2BNgdJgALshT%2Bm67UilwJu2P%2FpCFowfelYCF%2BPy3tjmH0UTYfM2oRwQqz%2FaT%2Fro%2B%2FTh5alVxOF0FGtFdMumsVSfQDL4"]) 42 | } 43 | 44 | { 45 | console.log(querystring.parse("oip=1932302622&ep=cCaSG02FX84D5ifaij8bbn7jd3VZXP4J9h%2BNgdJgALshT%2Bm67UilwJu2P%2FpCFowfelYCF%2BPy3tjmH0UTYfM2oRwQqz%2FaT%2Fro%2B%2FTh5alVxOF0FGtFdMumsVSfQDH1&token=1314&yxon=1&ctype=12&ev=1&K=9f73bb3c4155957624129573")) 46 | console.log('mine',querystring.parse("ctype=12&ev=1&K=fb5cd30b897d0949261ef913&ep=cSaSG02FUcoC5yfZij8bZH%2FjIHMLXP4J9h%2BNgdJhALshT%2BnNnzrSxJXFS41CFv5oBid1Y5rzrNSTY0ARYfU2qG4Q2kqtSPrni4Ti5apWzZMAFxk2AMnTxVSaRDP3&oip=1932302622&token=4736&yxon=1")) 47 | } 48 | 49 | { 50 | let data = JSON.parse(`{"e":{"desc":"","provider":"play","code":0},"data":{"id":862768,"stream":[{"logo":"none","media_type":"standard","audio_lang":"default","subtitle_lang":"default","transfer_mode_org":"http","segs":[{"total_milliseconds_audio":"1795669","fileid":"030020010057230223FEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438","total_milliseconds_video":"1795667","key":"90e959ebddf813392412979a","size":"86371154"}],"stream_type":"3gphd","width":480,"transfer_mode":"http","size":86371154,"height":366,"milliseconds_video":1795667,"drm_type":"default","milliseconds_audio":1795669,"stream_fileid":"030020010057230223FEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438"},{"logo":"none","media_type":"standard","audio_lang":"default","subtitle_lang":"default","transfer_mode_org":"http","segs":[{"total_milliseconds_audio":"409600","fileid":"03000205005723027DFEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438","total_milliseconds_video":"409600","key":"37ec34a13b3d665b282b61be","size":"20591540"},{"total_milliseconds_audio":"409600","fileid":"03000205015723027DFEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438","total_milliseconds_video":"409600","key":"f6b9ef5afce65a04261efcac","size":"21394445"},{"total_milliseconds_audio":"362533","fileid":"03000205025723027DFEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438","total_milliseconds_video":"362533","key":"62e46a284c2d2ae32412979a","size":"19437517"},{"total_milliseconds_audio":"298400","fileid":"03000205035723027DFEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438","total_milliseconds_video":"298400","key":"1fa3c8fa48ce0e1f2412979a","size":"19868318"},{"total_milliseconds_audio":"315536","fileid":"03000205045723027DFEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438","total_milliseconds_video":"315534","key":"37cdf72dd0e395fe261efcac","size":"20442591"}],"stream_type":"flvhd","width":480,"transfer_mode":"http","size":101734411,"height":366,"milliseconds_video":1795667,"drm_type":"default","milliseconds_audio":1795669,"stream_fileid":"03000205005723027DFEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438"},{"logo":"none","media_type":"standard","audio_lang":"default","subtitle_lang":"default","transfer_mode_org":"http","segs":[{"total_milliseconds_audio":"395854","fileid":"030008050057230A77FEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438","total_milliseconds_video":"395854","key":"4e534452a6dfd9872412979a","size":"32024089"},{"total_milliseconds_audio":"391349","fileid":"030008050157230A77FEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438","total_milliseconds_video":"391349","key":"fb34cda7c5fc5268261efcac","size":"32844767"},{"total_milliseconds_audio":"374584","fileid":"030008050257230A77FEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438","total_milliseconds_video":"374583","key":"5a17bba933613284261efcac","size":"33922099"},{"total_milliseconds_audio":"333625","fileid":"030008050357230A77FEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438","total_milliseconds_video":"333625","key":"0ba87cd04ff5a9492412979a","size":"37678873"},{"total_milliseconds_audio":"300257","fileid":"030008050457230A77FEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438","total_milliseconds_video":"300174","key":"2331c14afeb54948261efcac","size":"35383393"}],"stream_type":"mp4hd","width":704,"transfer_mode":"http","size":171853221,"height":536,"milliseconds_video":1795585,"drm_type":"default","milliseconds_audio":1795669,"stream_fileid":"030008050057230A77FEB42D9B7D2FB424E317-3B01-8066-DABC-7C9B74ADE438"}],"security":{"encrypt_string":"EZIdNEWjiLVksbbEOeHLaC23yrK3W0Np4qoMg4Nijic=","ip":2746431115},"video":{"logo":["http://r2.ykimg.com/0541040857230A846A0A430458F07AAA","http://r2.ykimg.com/0542040857230A846A0A430458F07AAA","http://r2.ykimg.com/0543040857230A846A0A430458F07AAA"],"title":"video_id:3468941","source":53093,"encodeid":"CMzQ1MTA3Mg==","description":"","userid":765164847},"network":{"dma_code":"17816","area_code":"442000"}},"cost":0.007000000216066837}`); 51 | let info = {data12:data.data, data10:data.data}; 52 | console.log(info); 53 | extractFlvPath(info).then(res => console.log(res)); 54 | } 55 | } 56 | 57 | var extractFlvPath = exports.extractFlvPath = function(info) { 58 | var sorted = info.data10.stream.sort( 59 | (a,b) => a.height { 71 | var gres = generate_ep(no, stream.stream_fileid, sid, token); 72 | var fileid = gres[0]; 73 | var fileep = gres[1]; 74 | var q = querystring.stringify({ctype:12, ev:1, K:seg.key, ep:decodeURIComponent(fileep), oip:ip, token, yxon:1}); 75 | var container = { 76 | mp4hd3:'flv', hd3:'flv', mp4hd2:'flv', 77 | hd2:'flv', mp4hd:'mp4', mp4:'mp4', 78 | flvhd:'flv', flv:'flv', '3gphd':'3gp', 79 | }[stream.stream_type]; 80 | var url = `http://k.youku.com/player/getFlvPath/sid/${sid}_00/st/${container}/fileid/${fileid}?${q}`; 81 | return url; 82 | }); 83 | 84 | return Promise.all(urls.map(url => fetch(url).then(res => res.json()).then(r => r[0].server))) 85 | .then(urls => { 86 | return {src: urls, duration: stream.milliseconds_video/1000.0}; 87 | }); 88 | } 89 | 90 | var getVideosByVideoId = exports.getVideosByVideoId = function (vid) { 91 | //var headers = new Headers(); 92 | //headers.append('sethdr-Referer', 'http://static.youku.com/'); 93 | //headers.append('sethdr-Cookie', '__ysuid'+new Date().getTime()/1e3); 94 | return Promise.all([ 95 | fetch('http://play.youku.com/play/get.json?vid='+vid+'&ct=10', {credentials: 'include'}).then(res => res.json()), 96 | fetch('http://play.youku.com/play/get.json?vid='+vid+'&ct=12', {credentials: 'include'}).then(res => res.json()), 97 | ]).then(res => { 98 | var data10 = res[0].data; 99 | var data12 = res[1].data; 100 | console.log('youku:', 'data10', data10, 'data12', data12); 101 | return extractFlvPath({data10,data12}); 102 | }) 103 | } 104 | 105 | var getVideosByVcode = exports.getVideosByVcode = function (vcode) { 106 | return getVideosByUrl(`http://v.youku.com/v_show/id_${vcode}.html`); 107 | } 108 | 109 | var getVideosByUrl = exports.getVideosByUrl = function (url) { 110 | return fetch(url, {credentials: 'include'}).then(res => res.text()).then(res => { 111 | var parser = new DOMParser(); 112 | var doc = parser.parseFromString(res, 'text/html'); 113 | var scripts = Array.prototype.slice.call(doc.querySelectorAll('script')).map(script => script.textContent); 114 | var videoId = scripts.filter(x => x.match(/videoId:/)); 115 | if (videoId) { 116 | videoId = videoId[0].match(/videoId: *"(\d+)"/); 117 | if (videoId) 118 | return getVideosByVideoId(videoId[1]); 119 | } 120 | var videoId = scripts.filter(x => x.match(/var videoId =/)); 121 | if (videoId) { 122 | videoId = videoId[0].match(/videoId = '(\d+)'/); 123 | if (videoId) 124 | return getVideosByVideoId(videoId[1]); 125 | } 126 | }) 127 | } 128 | 129 | exports.getVideos = function (url) { 130 | if (window.videoId) 131 | return getVideosByVideoId(window.videoId); 132 | else 133 | return getVideosByUrl(url); 134 | } 135 | 136 | --------------------------------------------------------------------------------