├── .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 | 
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 += `
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 |
--------------------------------------------------------------------------------