|
|
@@ -14,6 +14,7 @@
|
|
14
|
14
|
// Inspired by "MPEG Decoder in Java ME" by Nokia:
|
|
15
|
15
|
// http://www.developer.nokia.com/Community/Wiki/MPEG_decoder_in_Java_ME
|
|
16
|
16
|
|
|
|
17
|
+
|
|
17
|
18
|
var requestAnimFrame = (function(){
|
|
18
|
19
|
return window.requestAnimationFrame ||
|
|
19
|
20
|
window.webkitRequestAnimationFrame ||
|
|
|
@@ -35,12 +36,221 @@ var jsmpeg = window.jsmpeg = function( url, opts ) {
|
|
35
|
36
|
this.blockData = new Int32Array(64);
|
|
36
|
37
|
|
|
37
|
38
|
this.canvasContext = this.canvas.getContext('2d');
|
|
38
|
|
- this.load(url);
|
|
|
39
|
+
|
|
|
40
|
+ if( url instanceof WebSocket ) {
|
|
|
41
|
+ this.client = url;
|
|
|
42
|
+ this.client.onopen = this.initSocketClient.bind(this);
|
|
|
43
|
+ }
|
|
|
44
|
+ else {
|
|
|
45
|
+ this.load(url);
|
|
|
46
|
+ }
|
|
|
47
|
+};
|
|
|
48
|
+
|
|
|
49
|
+
|
|
|
50
|
+
|
|
|
51
|
+// ----------------------------------------------------------------------------
|
|
|
52
|
+// Streaming over WebSockets
|
|
|
53
|
+
|
|
|
54
|
+jsmpeg.prototype.waitForIntraFrame = true;
|
|
|
55
|
+jsmpeg.prototype.socketBufferSize = 512 * 1024; // 512kb each
|
|
|
56
|
+jsmpeg.prototype.initSocketClient = function( client ) {
|
|
|
57
|
+ this.buffer = new BitReader(new ArrayBuffer(this.socketBufferSize));
|
|
|
58
|
+
|
|
|
59
|
+ this.nextPictureBuffer = new BitReader(new ArrayBuffer(this.socketBufferSize));
|
|
|
60
|
+ this.nextPictureBuffer.writePos = 0;
|
|
|
61
|
+ this.nextPictureBuffer.chunkBegin = 0;
|
|
|
62
|
+ this.nextPictureBuffer.lastWriteBeforeWrap = 0;
|
|
|
63
|
+
|
|
|
64
|
+ this.client.binaryType = 'arraybuffer';
|
|
|
65
|
+ this.client.onmessage = this.receiveSocketMessage.bind(this);
|
|
|
66
|
+};
|
|
|
67
|
+
|
|
|
68
|
+jsmpeg.prototype.decodeSocketHeader = function( data ) {
|
|
|
69
|
+ // Custom header sent to all newly connected clients when streaming
|
|
|
70
|
+ // over websockets:
|
|
|
71
|
+ // struct { char magic[4] = "jsmp"; unsigned short width, height; };
|
|
|
72
|
+ if(
|
|
|
73
|
+ data[0] == SOCKET_MAGIC_BYTES.charCodeAt(0) &&
|
|
|
74
|
+ data[1] == SOCKET_MAGIC_BYTES.charCodeAt(1) &&
|
|
|
75
|
+ data[2] == SOCKET_MAGIC_BYTES.charCodeAt(2) &&
|
|
|
76
|
+ data[3] == SOCKET_MAGIC_BYTES.charCodeAt(3)
|
|
|
77
|
+ ) {
|
|
|
78
|
+ this.width = (data[4] * 256 + data[5]);
|
|
|
79
|
+ this.height = (data[6] * 256 + data[7]);
|
|
|
80
|
+ this.initBuffers();
|
|
|
81
|
+ }
|
|
|
82
|
+};
|
|
|
83
|
+
|
|
|
84
|
+jsmpeg.prototype.receiveSocketMessage = function( event ) {
|
|
|
85
|
+ var messageData = new Uint8Array(event.data);
|
|
|
86
|
+
|
|
|
87
|
+ if( !this.sequenceStarted ) {
|
|
|
88
|
+ this.decodeSocketHeader(messageData);
|
|
|
89
|
+ }
|
|
|
90
|
+
|
|
|
91
|
+ var current = this.buffer;
|
|
|
92
|
+ var next = this.nextPictureBuffer;
|
|
|
93
|
+
|
|
|
94
|
+ if( next.writePos + messageData.length > next.length ) {
|
|
|
95
|
+ next.lastWriteBeforeWrap = next.writePos;
|
|
|
96
|
+ next.writePos = 0;
|
|
|
97
|
+ next.index = 0;
|
|
|
98
|
+ }
|
|
|
99
|
+
|
|
|
100
|
+ next.bytes.set( messageData, next.writePos );
|
|
|
101
|
+ next.writePos += messageData.length;
|
|
|
102
|
+
|
|
|
103
|
+ var startCode = 0;
|
|
|
104
|
+ while( true ) {
|
|
|
105
|
+ startCode = next.findNextMPEGStartCode();
|
|
|
106
|
+ if(
|
|
|
107
|
+ startCode == BitReader.NOT_FOUND ||
|
|
|
108
|
+ ((next.index >> 3) > next.writePos)
|
|
|
109
|
+ ) {
|
|
|
110
|
+ // We reached the end with no picture found yet; move back a few bytes
|
|
|
111
|
+ // in case we are at the beginning of a start code and exit.
|
|
|
112
|
+ next.index = Math.max((next.writePos-3), 0) << 3;
|
|
|
113
|
+ return;
|
|
|
114
|
+ }
|
|
|
115
|
+ else if( startCode == START_PICTURE ) {
|
|
|
116
|
+ break;
|
|
|
117
|
+ }
|
|
|
118
|
+ }
|
|
|
119
|
+
|
|
|
120
|
+ // If we are still here, we found the next picture start code!
|
|
|
121
|
+
|
|
|
122
|
+
|
|
|
123
|
+
|
|
|
124
|
+ // Skip picture decoding until we find the first intra frame
|
|
|
125
|
+ if( this.waitForIntraFrame ) {
|
|
|
126
|
+ next.advance(10); // skip temporalReference
|
|
|
127
|
+ if( next.getBits(3) == PICTURE_TYPE_I ) {
|
|
|
128
|
+ this.waitForIntraFrame = false;
|
|
|
129
|
+ next.chunkBegin = (next.index-13) >> 3;
|
|
|
130
|
+ }
|
|
|
131
|
+ return;
|
|
|
132
|
+ }
|
|
|
133
|
+
|
|
|
134
|
+ // Last picture hasn't been decoded yet? Decode now but skip output
|
|
|
135
|
+ // before scheduling the next one
|
|
|
136
|
+ if( !this.currentPictureDecoded ) {
|
|
|
137
|
+ this.decodePicture(DECODE_SKIP_OUTPUT);
|
|
|
138
|
+ }
|
|
|
139
|
+
|
|
|
140
|
+
|
|
|
141
|
+ // Copy the picture chunk over to 'buffer' and schedule decoding.
|
|
|
142
|
+ var chunkEnd = ((next.index) >> 3);
|
|
|
143
|
+
|
|
|
144
|
+ if( chunkEnd > next.chunkBegin ) {
|
|
|
145
|
+ // Just copy the current picture chunk
|
|
|
146
|
+ current.bytes.set( next.bytes.subarray(next.chunkBegin, chunkEnd) );
|
|
|
147
|
+ current.writePos = chunkEnd - next.chunkBegin;
|
|
|
148
|
+ }
|
|
|
149
|
+ else {
|
|
|
150
|
+ // We wrapped the nextPictureBuffer around, so we have to copy the last part
|
|
|
151
|
+ // till the end, as well as from 0 to the current writePos
|
|
|
152
|
+ current.bytes.set( next.bytes.subarray(next.chunkBegin, next.lastWriteBeforeWrap) );
|
|
|
153
|
+ var written = next.lastWriteBeforeWrap - next.chunkBegin;
|
|
|
154
|
+ current.bytes.set( next.bytes.subarray(0, chunkEnd), written );
|
|
|
155
|
+ current.writePos = chunkEnd + written;
|
|
|
156
|
+ }
|
|
|
157
|
+
|
|
|
158
|
+ current.index = 0;
|
|
|
159
|
+ next.chunkBegin = chunkEnd;
|
|
|
160
|
+
|
|
|
161
|
+ // Decode!
|
|
|
162
|
+ this.currentPictureDecoded = false;
|
|
|
163
|
+ requestAnimFrame( this.scheduleDecoding.bind(this), this.canvas );
|
|
|
164
|
+};
|
|
|
165
|
+
|
|
|
166
|
+jsmpeg.prototype.scheduleDecoding = function() {
|
|
|
167
|
+ this.decodePicture();
|
|
|
168
|
+ this.currentPictureDecoded = true;
|
|
|
169
|
+};
|
|
|
170
|
+
|
|
|
171
|
+
|
|
|
172
|
+
|
|
|
173
|
+// ----------------------------------------------------------------------------
|
|
|
174
|
+// Recording from WebSockets
|
|
|
175
|
+
|
|
|
176
|
+jsmpeg.prototype.isRecording = false;
|
|
|
177
|
+jsmpeg.prototype.recorderWaitForIntraFrame = false;
|
|
|
178
|
+jsmpeg.prototype.recordedFrames = 0;
|
|
|
179
|
+jsmpeg.prototype.recordedSize = 0;
|
|
|
180
|
+jsmpeg.prototype.didStartRecordingCallback = null;
|
|
|
181
|
+
|
|
|
182
|
+jsmpeg.prototype.recordBuffers = [];
|
|
|
183
|
+
|
|
|
184
|
+jsmpeg.prototype.startRecording = function(callback) {
|
|
|
185
|
+ if( !this.client ) {
|
|
|
186
|
+ throw("Can't record when loading from file.");
|
|
|
187
|
+ return;
|
|
|
188
|
+ }
|
|
|
189
|
+
|
|
|
190
|
+ // Discard old buffers and set for recording
|
|
|
191
|
+ this.discardRecordBuffers();
|
|
|
192
|
+ this.isRecording = true;
|
|
|
193
|
+ this.recorderWaitForIntraFrame = true;
|
|
|
194
|
+ this.didStartRecordingCallback = callback || null;
|
|
|
195
|
+
|
|
|
196
|
+ // Fudge a simple Sequence Header for the MPEG file
|
|
|
197
|
+
|
|
|
198
|
+ // 3 bytes width & height, 12 bits each
|
|
|
199
|
+ var wh1 = (this.width >> 4),
|
|
|
200
|
+ wh2 = ((this.width & 0xf) << 4) | (this.height >> 8),
|
|
|
201
|
+ wh3 = (this.height & 0xff);
|
|
|
202
|
+
|
|
|
203
|
+ this.recordBuffers.push(new Uint8Array([
|
|
|
204
|
+ 0x00, 0x00, 0x01, 0xb3, // Sequence Start Code
|
|
|
205
|
+ wh1, wh2, wh3, // Width & height
|
|
|
206
|
+ 0x13, // aspect ratio & framerate
|
|
|
207
|
+ 0xff, 0xff, 0xe1, 0x58, // Meh. Bitrate and other boring stuff
|
|
|
208
|
+ 0x00, 0x00, 0x01, 0xb8, 0x00, 0x08, 0x00, // GOP
|
|
|
209
|
+ 0x00, 0x00, 0x00, 0x01, 0x00 // First Picture Start Code
|
|
|
210
|
+ ]));
|
|
39
|
211
|
};
|
|
|
212
|
+
|
|
|
213
|
+jsmpeg.prototype.recordFrameFromCurrentBuffer = function() {
|
|
|
214
|
+ if( !this.isRecording ) { return; }
|
|
|
215
|
+
|
|
|
216
|
+ if( this.recorderWaitForIntraFrame ) {
|
|
|
217
|
+ // Not an intra frame? Exit.
|
|
|
218
|
+ if( this.pictureCodingType != PICTURE_TYPE_I ) { return; }
|
|
|
219
|
+
|
|
|
220
|
+ // Start recording!
|
|
|
221
|
+ this.recorderWaitForIntraFrame = false;
|
|
|
222
|
+ if( this.didStartRecordingCallback ) {
|
|
|
223
|
+ this.didStartRecordingCallback( this );
|
|
|
224
|
+ }
|
|
|
225
|
+ }
|
|
|
226
|
+
|
|
|
227
|
+ this.recordedFrames++;
|
|
|
228
|
+ this.recordedSize += this.buffer.writePos;
|
|
|
229
|
+
|
|
|
230
|
+ // Copy the actual subrange for the current picture into a new Buffer
|
|
|
231
|
+ this.recordBuffers.push(new Uint8Array(this.buffer.bytes.subarray(0, this.buffer.writePos)));
|
|
|
232
|
+};
|
|
|
233
|
+
|
|
|
234
|
+jsmpeg.prototype.discardRecordBuffers = function() {
|
|
|
235
|
+ this.recordBuffers = [];
|
|
|
236
|
+ this.recordedFrames = 0;
|
|
|
237
|
+};
|
|
|
238
|
+
|
|
|
239
|
+jsmpeg.prototype.stopRecording = function() {
|
|
|
240
|
+ var blob = new Blob(this.recordBuffers, {type: 'video/mpeg'});
|
|
|
241
|
+ this.discardRecordBuffers();
|
|
|
242
|
+ this.isRecording = false;
|
|
|
243
|
+ return blob;
|
|
|
244
|
+};
|
|
|
245
|
+
|
|
|
246
|
+
|
|
|
247
|
+
|
|
|
248
|
+// ----------------------------------------------------------------------------
|
|
|
249
|
+// Loading via Ajax
|
|
40
|
250
|
|
|
41
|
251
|
jsmpeg.prototype.load = function( url ) {
|
|
42
|
252
|
this.url = url;
|
|
43
|
|
-
|
|
|
253
|
+
|
|
44
|
254
|
var request = new XMLHttpRequest();
|
|
45
|
255
|
var that = this;
|
|
46
|
256
|
request.onreadystatechange = function() {
|
|
|
@@ -99,10 +309,21 @@ jsmpeg.prototype.pause = function(file) {
|
|
99
|
309
|
};
|
|
100
|
310
|
|
|
101
|
311
|
jsmpeg.prototype.stop = function(file) {
|
|
102
|
|
- this.buffer.index = this.firstSequenceHeader;
|
|
|
312
|
+ if( this.buffer ) {
|
|
|
313
|
+ this.buffer.index = this.firstSequenceHeader;
|
|
|
314
|
+ }
|
|
103
|
315
|
this.playing = false;
|
|
|
316
|
+ if( this.client ) {
|
|
|
317
|
+ this.client.close();
|
|
|
318
|
+ this.client = null;
|
|
|
319
|
+ }
|
|
104
|
320
|
};
|
|
105
|
321
|
|
|
|
322
|
+
|
|
|
323
|
+
|
|
|
324
|
+// ----------------------------------------------------------------------------
|
|
|
325
|
+// Utilities
|
|
|
326
|
+
|
|
106
|
327
|
jsmpeg.prototype.readCode = function(codeTable) {
|
|
107
|
328
|
var state = 0;
|
|
108
|
329
|
do {
|
|
|
@@ -189,7 +410,25 @@ jsmpeg.prototype.decodeSequenceHeader = function() {
|
|
189
|
410
|
this.buffer.advance(4); // skip pixel aspect ratio
|
|
190
|
411
|
this.pictureRate = PICTURE_RATE[this.buffer.getBits(4)];
|
|
191
|
412
|
this.buffer.advance(18 + 1 + 10 + 1); // skip bitRate, marker, bufferSize and constrained bit
|
|
|
413
|
+
|
|
|
414
|
+ this.initBuffers();
|
|
|
415
|
+
|
|
|
416
|
+ if( this.buffer.getBits(1) ) { // load custom intra quant matrix?
|
|
|
417
|
+ for( var i = 0; i < 64; i++ ) {
|
|
|
418
|
+ this.customIntraQuantMatrix[ZIG_ZAG[i]] = this.buffer.getBits(8);
|
|
|
419
|
+ }
|
|
|
420
|
+ this.intraQuantMatrix = this.customIntraQuantMatrix;
|
|
|
421
|
+ }
|
|
192
|
422
|
|
|
|
423
|
+ if( this.buffer.getBits(1) ) { // load custom non intra quant matrix?
|
|
|
424
|
+ for( var i = 0; i < 64; i++ ) {
|
|
|
425
|
+ this.customNonIntraQuantMatrix[ZIG_ZAG[i]] = this.buffer.getBits(8);
|
|
|
426
|
+ }
|
|
|
427
|
+ this.nonIntraQuantMatrix = this.customNonIntraQuantMatrix;
|
|
|
428
|
+ }
|
|
|
429
|
+};
|
|
|
430
|
+
|
|
|
431
|
+jsmpeg.prototype.initBuffers = function() {
|
|
193
|
432
|
this.intraQuantMatrix = DEFAULT_INTRA_QUANT_MATRIX;
|
|
194
|
433
|
this.nonIntraQuantMatrix = DEFAULT_NON_INTRA_QUANT_MATRIX;
|
|
195
|
434
|
|
|
|
@@ -205,21 +444,6 @@ jsmpeg.prototype.decodeSequenceHeader = function() {
|
|
205
|
444
|
this.halfHeight = this.mbHeight << 3;
|
|
206
|
445
|
this.quarterSize = this.codedSize >> 2;
|
|
207
|
446
|
|
|
208
|
|
-
|
|
209
|
|
- if( this.buffer.getBits(1) ) { // load custom intra quant matrix?
|
|
210
|
|
- for( var i = 0; i < 64; i++ ) {
|
|
211
|
|
- this.customIntraQuantMatrix[ZIG_ZAG[i]] = this.buffer.getBits(8);
|
|
212
|
|
- }
|
|
213
|
|
- this.intraQuantMatrix = this.customIntraQuantMatrix;
|
|
214
|
|
- }
|
|
215
|
|
-
|
|
216
|
|
- if( this.buffer.getBits(1) ) { // load custom non intra quant matrix?
|
|
217
|
|
- for( var i = 0; i < 64; i++ ) {
|
|
218
|
|
- this.customNonIntraQuantMatrix[ZIG_ZAG[i]] = this.buffer.getBits(8);
|
|
219
|
|
- }
|
|
220
|
|
- this.nonIntraQuantMatrix = this.customNonIntraQuantMatrix;
|
|
221
|
|
- }
|
|
222
|
|
-
|
|
223
|
447
|
// Sequence already started? Don't allocate buffers again
|
|
224
|
448
|
if( this.sequenceStarted ) { return; }
|
|
225
|
449
|
this.sequenceStarted = true;
|
|
|
@@ -276,7 +500,7 @@ jsmpeg.prototype.forwardRSize = 0;
|
|
276
|
500
|
jsmpeg.prototype.forwardF = 0;
|
|
277
|
501
|
|
|
278
|
502
|
|
|
279
|
|
-jsmpeg.prototype.decodePicture = function() {
|
|
|
503
|
+jsmpeg.prototype.decodePicture = function(skipOutput) {
|
|
280
|
504
|
this.buffer.advance(10); // skip temporalReference
|
|
281
|
505
|
this.pictureCodingType = this.buffer.getBits(3);
|
|
282
|
506
|
this.buffer.advance(16); // skip vbv_delay
|
|
|
@@ -311,10 +535,15 @@ jsmpeg.prototype.decodePicture = function() {
|
|
311
|
535
|
|
|
312
|
536
|
// We found the next start code; rewind 32bits and let the main loop handle it.
|
|
313
|
537
|
this.buffer.rewind(32);
|
|
|
538
|
+
|
|
|
539
|
+ // Record this frame, if the recorder wants it
|
|
|
540
|
+ this.recordFrameFromCurrentBuffer();
|
|
314
|
541
|
|
|
315
|
542
|
|
|
316
|
|
- this.YCbCrToRGBA();
|
|
317
|
|
- this.canvasContext.putImageData(this.currentRGBA, 0, 0);
|
|
|
543
|
+ if( skipOutput != DECODE_SKIP_OUTPUT ) {
|
|
|
544
|
+ this.YCrCbToRGB();
|
|
|
545
|
+ this.canvasContext.putImageData(this.currentRGB, 0, 0);
|
|
|
546
|
+ }
|
|
318
|
547
|
|
|
319
|
548
|
// If this is a reference picutre then rotate the prediction pointers
|
|
320
|
549
|
if( this.pictureCodingType == PICTURE_TYPE_I || this.pictureCodingType == PICTURE_TYPE_P ) {
|
|
|
@@ -1110,6 +1339,8 @@ jsmpeg.prototype.IDCT = function() {
|
|
1110
|
1339
|
// VLC Tables and Constants
|
|
1111
|
1340
|
|
|
1112
|
1341
|
var
|
|
|
1342
|
+ SOCKET_MAGIC_BYTES = 'jsmp',
|
|
|
1343
|
+ DECODE_SKIP_OUTPUT = 1,
|
|
1113
|
1344
|
PICTURE_RATE = [
|
|
1114
|
1345
|
0.000, 23.976, 24.000, 25.000, 29.970, 30.000, 50.000, 59.940,
|
|
1115
|
1346
|
60.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000, 0.000
|
|
|
@@ -1804,13 +2035,14 @@ var MACROBLOCK_TYPE_TABLES = [
|
|
1804
|
2035
|
var BitReader = function(arrayBuffer) {
|
|
1805
|
2036
|
this.bytes = new Uint8Array(arrayBuffer);
|
|
1806
|
2037
|
this.length = this.bytes.length;
|
|
|
2038
|
+ this.writePos = this.bytes.length;
|
|
1807
|
2039
|
this.index = 0;
|
|
1808
|
2040
|
};
|
|
1809
|
2041
|
|
|
1810
|
2042
|
BitReader.NOT_FOUND = -1;
|
|
1811
|
2043
|
|
|
1812
|
2044
|
BitReader.prototype.findNextMPEGStartCode = function() {
|
|
1813
|
|
- for( var i = (this.index+7 >> 3); i < this.length; i++ ) {
|
|
|
2045
|
+ for( var i = (this.index+7 >> 3); i < this.writePos; i++ ) {
|
|
1814
|
2046
|
if(
|
|
1815
|
2047
|
this.bytes[i] == 0x00 &&
|
|
1816
|
2048
|
this.bytes[i+1] == 0x00 &&
|
|
|
@@ -1820,14 +2052,14 @@ BitReader.prototype.findNextMPEGStartCode = function() {
|
|
1820
|
2052
|
return this.bytes[i+3];
|
|
1821
|
2053
|
}
|
|
1822
|
2054
|
}
|
|
1823
|
|
- this.index = (this.length << 3);
|
|
|
2055
|
+ this.index = (this.writePos << 3);
|
|
1824
|
2056
|
return BitReader.NOT_FOUND;
|
|
1825
|
2057
|
};
|
|
1826
|
2058
|
|
|
1827
|
2059
|
BitReader.prototype.nextBytesAreStartCode = function() {
|
|
1828
|
2060
|
var i = (this.index+7 >> 3);
|
|
1829
|
2061
|
return (
|
|
1830
|
|
- i >= this.length || (
|
|
|
2062
|
+ i >= this.writePos || (
|
|
1831
|
2063
|
this.bytes[i] == 0x00 &&
|
|
1832
|
2064
|
this.bytes[i+1] == 0x00 &&
|
|
1833
|
2065
|
this.bytes[i+2] == 0x01
|