Explorar el Código

Added support for streaming/recording over websockets

Dominic Szablewski hace 12 años
padre
commit
75aaddccbf
Se han modificado 1 ficheros con 256 adiciones y 24 borrados
  1. 256 24
      jsmpg.js

+ 256 - 24
jsmpg.js Ver fichero

14
 // Inspired by "MPEG Decoder in Java ME" by Nokia:
14
 // Inspired by "MPEG Decoder in Java ME" by Nokia:
15
 // http://www.developer.nokia.com/Community/Wiki/MPEG_decoder_in_Java_ME
15
 // http://www.developer.nokia.com/Community/Wiki/MPEG_decoder_in_Java_ME
16
 
16
 
17
+
17
 var requestAnimFrame = (function(){
18
 var requestAnimFrame = (function(){
18
 	return window.requestAnimationFrame ||
19
 	return window.requestAnimationFrame ||
19
 		window.webkitRequestAnimationFrame ||
20
 		window.webkitRequestAnimationFrame ||
35
 	this.blockData = new Int32Array(64);
36
 	this.blockData = new Int32Array(64);
36
 
37
 
37
 	this.canvasContext = this.canvas.getContext('2d');
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
 jsmpeg.prototype.load = function( url ) {
251
 jsmpeg.prototype.load = function( url ) {
42
 	this.url = url;
252
 	this.url = url;
43
-	
253
+
44
 	var request = new XMLHttpRequest();
254
 	var request = new XMLHttpRequest();
45
 	var that = this;
255
 	var that = this;
46
 	request.onreadystatechange = function() {		
256
 	request.onreadystatechange = function() {		
99
 };
309
 };
100
 
310
 
101
 jsmpeg.prototype.stop = function(file) {
311
 jsmpeg.prototype.stop = function(file) {
102
-	this.buffer.index = this.firstSequenceHeader;
312
+	if( this.buffer ) {
313
+		this.buffer.index = this.firstSequenceHeader;
314
+	}
103
 	this.playing = false;
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
 jsmpeg.prototype.readCode = function(codeTable) {
327
 jsmpeg.prototype.readCode = function(codeTable) {
107
 	var state = 0;
328
 	var state = 0;
108
 	do {
329
 	do {
189
 	this.buffer.advance(4); // skip pixel aspect ratio
410
 	this.buffer.advance(4); // skip pixel aspect ratio
190
 	this.pictureRate = PICTURE_RATE[this.buffer.getBits(4)];
411
 	this.pictureRate = PICTURE_RATE[this.buffer.getBits(4)];
191
 	this.buffer.advance(18 + 1 + 10 + 1); // skip bitRate, marker, bufferSize and constrained bit
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
 	this.intraQuantMatrix = DEFAULT_INTRA_QUANT_MATRIX;
432
 	this.intraQuantMatrix = DEFAULT_INTRA_QUANT_MATRIX;
194
 	this.nonIntraQuantMatrix = DEFAULT_NON_INTRA_QUANT_MATRIX;
433
 	this.nonIntraQuantMatrix = DEFAULT_NON_INTRA_QUANT_MATRIX;
195
 	
434
 	
205
 	this.halfHeight = this.mbHeight << 3;
444
 	this.halfHeight = this.mbHeight << 3;
206
 	this.quarterSize = this.codedSize >> 2;
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
 	// Sequence already started? Don't allocate buffers again
447
 	// Sequence already started? Don't allocate buffers again
224
 	if( this.sequenceStarted ) { return; }
448
 	if( this.sequenceStarted ) { return; }
225
 	this.sequenceStarted = true;
449
 	this.sequenceStarted = true;
276
 jsmpeg.prototype.forwardF = 0;
500
 jsmpeg.prototype.forwardF = 0;
277
 
501
 
278
 
502
 
279
-jsmpeg.prototype.decodePicture = function() {
503
+jsmpeg.prototype.decodePicture = function(skipOutput) {
280
 	this.buffer.advance(10); // skip temporalReference
504
 	this.buffer.advance(10); // skip temporalReference
281
 	this.pictureCodingType = this.buffer.getBits(3);
505
 	this.pictureCodingType = this.buffer.getBits(3);
282
 	this.buffer.advance(16); // skip vbv_delay
506
 	this.buffer.advance(16); // skip vbv_delay
311
 	
535
 	
312
 	// We found the next start code; rewind 32bits and let the main loop handle it.
536
 	// We found the next start code; rewind 32bits and let the main loop handle it.
313
 	this.buffer.rewind(32);
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
 	// If this is a reference picutre then rotate the prediction pointers
548
 	// If this is a reference picutre then rotate the prediction pointers
320
 	if( this.pictureCodingType == PICTURE_TYPE_I || this.pictureCodingType == PICTURE_TYPE_P ) {
549
 	if( this.pictureCodingType == PICTURE_TYPE_I || this.pictureCodingType == PICTURE_TYPE_P ) {
1110
 // VLC Tables and Constants
1339
 // VLC Tables and Constants
1111
 
1340
 
1112
 var
1341
 var
1342
+	SOCKET_MAGIC_BYTES = 'jsmp',
1343
+	DECODE_SKIP_OUTPUT = 1,
1113
 	PICTURE_RATE = [
1344
 	PICTURE_RATE = [
1114
 		0.000, 23.976, 24.000, 25.000, 29.970, 30.000, 50.000, 59.940,
1345
 		0.000, 23.976, 24.000, 25.000, 29.970, 30.000, 50.000, 59.940,
1115
 		60.000,  0.000,  0.000,  0.000,  0.000,  0.000,  0.000,  0.000
1346
 		60.000,  0.000,  0.000,  0.000,  0.000,  0.000,  0.000,  0.000
1804
 var BitReader = function(arrayBuffer) {
2035
 var BitReader = function(arrayBuffer) {
1805
 	this.bytes = new Uint8Array(arrayBuffer);
2036
 	this.bytes = new Uint8Array(arrayBuffer);
1806
 	this.length = this.bytes.length;
2037
 	this.length = this.bytes.length;
2038
+	this.writePos = this.bytes.length;
1807
 	this.index = 0;
2039
 	this.index = 0;
1808
 };
2040
 };
1809
 
2041
 
1810
 BitReader.NOT_FOUND = -1;
2042
 BitReader.NOT_FOUND = -1;
1811
 
2043
 
1812
 BitReader.prototype.findNextMPEGStartCode = function() {	
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
 		if(
2046
 		if(
1815
 			this.bytes[i] == 0x00 &&
2047
 			this.bytes[i] == 0x00 &&
1816
 			this.bytes[i+1] == 0x00 &&
2048
 			this.bytes[i+1] == 0x00 &&
1820
 			return this.bytes[i+3];
2052
 			return this.bytes[i+3];
1821
 		}
2053
 		}
1822
 	}
2054
 	}
1823
-	this.index = (this.length << 3);
2055
+	this.index = (this.writePos << 3);
1824
 	return BitReader.NOT_FOUND;
2056
 	return BitReader.NOT_FOUND;
1825
 };
2057
 };
1826
 
2058
 
1827
 BitReader.prototype.nextBytesAreStartCode = function() {
2059
 BitReader.prototype.nextBytesAreStartCode = function() {
1828
 	var i = (this.index+7 >> 3);
2060
 	var i = (this.index+7 >> 3);
1829
 	return (
2061
 	return (
1830
-		i >= this.length || (
2062
+		i >= this.writePos || (
1831
 			this.bytes[i] == 0x00 && 
2063
 			this.bytes[i] == 0x00 && 
1832
 			this.bytes[i+1] == 0x00 &&
2064
 			this.bytes[i+1] == 0x00 &&
1833
 			this.bytes[i+2] == 0x01
2065
 			this.bytes[i+2] == 0x01