Просмотр исходного кода

Added support for streaming/recording over websockets

Dominic Szablewski 12 лет назад
Родитель
Сommit
75aaddccbf
1 измененных файлов: 256 добавлений и 24 удалений
  1. 256 24
      jsmpg.js

+ 256 - 24
jsmpg.js Просмотреть файл

@@ -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