浏览代码

Add progressive loading via Range-Requests; remove fetch/ReadableStream loading

Dominic Szablewski 9 年前
父节点
当前提交
e06577fad2
共有 2 个文件被更改,包括 227 次插入123 次删除
  1. 2 1
      README.md
  2. 225 122
      jsmpg.js

+ 2 - 1
README.md 查看文件

@@ -20,7 +20,8 @@ The `file` argument accepts a URL to a .mpg file or a (yet unconnected) WebSocke
20 20
 The `options` argument to the `jsmpeg()` supports the following properties:
21 21
 
22 22
 - `benchmark` whether to log benchmark results to the browser's console
23
-- `progressive` whether to start playback as soon as the first frames have been loaded. Uses the new `fetch` API if available. Default `true`.
23
+- `progressive` whether to start playback as soon as the first chunks have been loaded. Uses HTTP range-requests. Default `false`.
24
+- `progressiveThrottled` whether to throttle downloading chunks until they're needed for playback. Requires `progressive`; default `false`.
24 25
 - `canvas` the HTML Canvas element to use; jsmpeg will create its own Canvas element if none is provided
25 26
 - `autoplay` whether playback should start automatically after loading
26 27
 - `loop` whether playback is looped

+ 225 - 122
jsmpg.js 查看文件

@@ -11,7 +11,6 @@ var requestAnimFrame = (function(){
11 11
 
12 12
 var jsmpeg = window.jsmpeg = function( url, opts ) {
13 13
 	opts = opts || {};
14
-	this.progressive = (opts.progressive !== false);
15 14
 	this.benchmark = !!opts.benchmark;
16 15
 	this.canvas = opts.canvas || document.createElement('canvas');
17 16
 	this.autoplay = !!opts.autoplay;
@@ -22,6 +21,11 @@ var jsmpeg = window.jsmpeg = function( url, opts ) {
22 21
 	this.externalDecodeCallback = opts.ondecodeframe || null;
23 22
 	this.externalFinishedCallback = opts.onfinished || null;
24 23
 
24
+	this.progressive = !!opts.progressive;
25
+	this.progressiveThrottled = !!opts.progressiveThrottled;
26
+	this.progressiveChunkSize = opts.progressiveChunkSize || 256*1024;
27
+	this.progressiveChunkSizeMax = 4 * 1024 * 1024;
28
+
25 29
 	this.customIntraQuantMatrix = new Uint8Array(64);
26 30
 	this.customNonIntraQuantMatrix = new Uint8Array(64);
27 31
 	this.blockData = new Int32Array(64);
@@ -40,6 +44,9 @@ var jsmpeg = window.jsmpeg = function( url, opts ) {
40 44
 		this.client = url;
41 45
 		this.client.onopen = this.initSocketClient.bind(this);
42 46
 	}
47
+	else if (this.progressive) {
48
+		this.beginProgressiveLoad(url);
49
+	}
43 50
 	else {
44 51
 		this.load(url);
45 52
 	}
@@ -258,129 +265,29 @@ jsmpeg.prototype.currentFrame = -1;
258 265
 jsmpeg.prototype.currentTime = 0;
259 266
 jsmpeg.prototype.frameCount = 0;
260 267
 jsmpeg.prototype.duration = 0;
261
-jsmpeg.prototype.progressiveMinSize = 128 * 1024;
262
-
263
-
264
-jsmpeg.prototype.fetchReaderPump = function(reader) {
265
-	var that = this;
266
-	reader.read().then(function (result) {
267
-		that.fetchReaderReceive(reader, result);
268
-	});
269
-};
270
-
271
-jsmpeg.prototype.fetchReaderReceive = function(reader, result) {
272
-	if( result.done ) {
273
-		if( this.seekable ) {
274
-			var currentBufferPos = this.buffer.index;
275
-			this.collectIntraFrames();
276
-			this.buffer.index = currentBufferPos;
277
-		}
278
-
279
-		this.duration = this.frameCount / this.pictureRate;
280
-		this.lastFrameIndex = this.buffer.writePos << 3;
281
-		return;
282
-	}
283
-
284
-	this.buffer.bytes.set(result.value, this.buffer.writePos);
285
-	this.buffer.writePos += result.value.byteLength;
286
-
287
-	// Find the last picture start code - we have to be careful not trying
288
-	// to decode any frames that aren't fully loaded yet.
289
-	this.lastFrameIndex =  this.findLastPictureStartCode();
290
-
291
-	// Initialize the sequence headers and start playback if we have enough data
292
-	// (at least 128kb) 
293
-	if( !this.sequenceStarted && this.buffer.writePos >= this.progressiveMinSize ) {
294
-		this.findStartCode(START_SEQUENCE);
295
-		this.firstSequenceHeader = this.buffer.index;
296
-		this.decodeSequenceHeader();
297
-
298
-		// Load the first frame
299
-		this.nextFrame();
300
-
301
-		if( this.autoplay ) {
302
-			this.play();
303
-		}
304
-
305
-		if( this.externalLoadCallback ) {
306
-			this.externalLoadCallback(this);
307
-		}
308
-	}
309
-
310
-	// If the player starved previously, restart playback now
311
-	else if( this.sequenceStarted && this.wantsToPlay && !this.playing ) {
312
-		this.play();
313
-	}
314
-
315
-	// Not enough data to start playback yet - show loading progress
316
-	else if( !this.sequenceStarted ) {
317
-		var status = {loaded: this.buffer.writePos, total: this.progressiveMinSize};
318
-		if( this.gl ) {
319
-			this.updateLoaderGL(status);
320
-		}
321
-		else {
322
-			this.updateLoader2D(status);
323
-		}
324
-	}
325
-
326
-	this.fetchReaderPump(reader);
327
-};
328
-
329
-jsmpeg.prototype.findLastPictureStartCode = function() {
330
-	var bufferBytes = this.buffer.bytes;
331
-	for( var i = this.buffer.writePos; i > 3; i-- ) {
332
-		if(
333
-			bufferBytes[i] == START_PICTURE &&
334
-			bufferBytes[i-1] == 0x01 &&
335
-			bufferBytes[i-2] == 0x00 &&
336
-			bufferBytes[i-3] == 0x00			
337
-		) {
338
-			return (i-3) << 3;
339
-		}
340
-	}
341
-	return 0;
342
-};
343
-
268
+	
344 269
 jsmpeg.prototype.load = function( url ) {
345 270
 	this.url = url;
346 271
 
272
+	var request = new XMLHttpRequest();
347 273
 	var that = this;
348
-	if( 
349
-		this.progressive && 
350
-		window.fetch && 
351
-		window.ReadableByteStream
352
-	) {
353
-		var reqHeaders = new Headers();
354
-		reqHeaders.append('Content-Type', 'video/mpeg');
355
-		fetch(url, {headers: reqHeaders}).then(function (res) {
356
-			var contentLength = res.headers.get('Content-Length');
357
-			var reader = res.body.getReader();
358
-
359
-			that.buffer = new BitReader(new ArrayBuffer(contentLength));
360
-			that.buffer.writePos = 0;
361
-			that.fetchReaderPump(reader);
362
-        });
363
-	}
364
-	else {
365
-		var request = new XMLHttpRequest();
366
-		request.onreadystatechange = function() {
367
-			if( request.readyState === request.DONE && request.status === 200 ) {
368
-				that.loadCallback(request.response);
369
-			}
370
-		};
274
+	request.onreadystatechange = function() {
275
+		if( request.readyState == request.DONE && request.status == 200 ) {
276
+			that.loadCallback(request.response);
277
+		}
278
+	};
371 279
 
372
-		request.onprogress = this.gl
373
-			? this.updateLoaderGL.bind(this)
374
-			: this.updateLoader2D.bind(this);
280
+	request.onprogress = this.gl 
281
+		? this.updateLoaderGL.bind(this) 
282
+		: this.updateLoader2D.bind(this);
375 283
 
376
-		request.open('GET', url);
377
-		request.responseType = 'arraybuffer';
378
-		request.send();
379
-	}
284
+	request.open('GET', url);
285
+	request.responseType = "arraybuffer";
286
+	request.send();
380 287
 };
381 288
 
382 289
 jsmpeg.prototype.updateLoader2D = function( ev ) {
383
-	var
290
+	var 
384 291
 		p = ev.loaded / ev.total,
385 292
 		w = this.canvas.width,
386 293
 		h = this.canvas.height,
@@ -397,8 +304,7 @@ jsmpeg.prototype.updateLoaderGL = function( ev ) {
397 304
 	gl.uniform1f(gl.getUniformLocation(this.loadingProgram, 'loaded'), (ev.loaded / ev.total));
398 305
 	gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
399 306
 };
400
-
401
-
307
+	
402 308
 jsmpeg.prototype.loadCallback = function(file) {
403 309
 	this.buffer = new BitReader(file);
404 310
 
@@ -406,7 +312,7 @@ jsmpeg.prototype.loadCallback = function(file) {
406 312
 		this.collectIntraFrames();
407 313
 		this.buffer.index = 0;
408 314
 	}
409
-
315
+	
410 316
 	this.findStartCode(START_SEQUENCE);
411 317
 	this.firstSequenceHeader = this.buffer.index;
412 318
 	this.decodeSequenceHeader();
@@ -416,7 +322,7 @@ jsmpeg.prototype.loadCallback = function(file) {
416 322
 
417 323
 	// Load the first frame
418 324
 	this.nextFrame();
419
-
325
+	
420 326
 	if( this.autoplay ) {
421 327
 		this.play();
422 328
 	}
@@ -478,9 +384,18 @@ jsmpeg.prototype.seekToTime = function(time, seekExact) {
478 384
 
479 385
 jsmpeg.prototype.play = function() {
480 386
 	if( this.playing ) { return; }
387
+	if( this.progressive ) {
388
+		this.wantsToPlay = true;
389
+		this.attemptToPlay();
390
+	}
391
+	else {
392
+		this._playNow();
393
+	}
394
+};
395
+
396
+jsmpeg.prototype._playNow = function() {
481 397
 	this.targetTime = this.now();
482 398
 	this.playing = true;
483
-	this.wantsToPlay = true;
484 399
 	this.scheduleNextFrame();
485 400
 };
486 401
 
@@ -491,6 +406,8 @@ jsmpeg.prototype.pause = function() {
491 406
 
492 407
 jsmpeg.prototype.stop = function() {
493 408
 	this.currentFrame = -1;
409
+	this.currentTime = 0;
410
+	this.wantsToPlay = false;
494 411
 	if( this.buffer ) {
495 412
 		this.buffer.index = this.firstSequenceHeader;
496 413
 	}
@@ -499,7 +416,187 @@ jsmpeg.prototype.stop = function() {
499 416
 		this.client.close();
500 417
 		this.client = null;
501 418
 	}
502
-	this.wantsToPlay = false;
419
+};
420
+
421
+
422
+
423
+// ----------------------------------------------------------------------------
424
+// Progressive loading via AJAX
425
+
426
+jsmpeg.prototype.beginProgressiveLoad = function( url ) {
427
+	this.url = url;
428
+	
429
+	this.progressiveLoadPositon = 0;
430
+	this.fileSize = 0;
431
+
432
+	var request = new XMLHttpRequest();
433
+	var that = this;
434
+	request.onreadystatechange = function() {
435
+		if( request.readyState === request.DONE ) {
436
+			that.fileSize = parseInt(request.getResponseHeader("Content-Length"));
437
+			that.buffer = new BitReader(new ArrayBuffer(that.fileSize));
438
+			that.buffer.writePos = 0;
439
+			that.loadNextChunk();
440
+		}
441
+	};
442
+	request.open('HEAD', url);
443
+	request.send();
444
+};
445
+
446
+jsmpeg.prototype.maybeLoadNextChunk = function() {
447
+	if( 
448
+		!this.chunkIsLoading && 
449
+		(this.buffer.index >> 3) > this.nextChunkLoadAt &&
450
+		this.progressiveLoadFails < 5
451
+	) {
452
+		this.loadNextChunk();
453
+	}
454
+};
455
+
456
+jsmpeg.prototype.loadNextChunk = function() {
457
+	var that = this;
458
+	var start = this.buffer.writePos,
459
+		end = Math.min(this.buffer.writePos + that.progressiveChunkSize-1, this.fileSize-1);
460
+	
461
+	if( start >= this.fileSize ) {
462
+		return;
463
+	}
464
+	
465
+	this.chunkIsLoading = true;
466
+	this.chunkLoadStart = Date.now();
467
+	var request = new XMLHttpRequest();
468
+
469
+	request.onreadystatechange = function() {		
470
+		if( 
471
+			request.readyState === request.DONE && 
472
+			request.status > 200 && request.status < 300
473
+		) {
474
+			that.progressiveLoadFails = 0;
475
+			that.progressiveLoadCallback(request.response);
476
+		}
477
+		else if( request.readyState === request.DONE ) {
478
+			// retry
479
+			that.chunkIsLoading = false;
480
+			that.progressiveLoadFails++;
481
+			that.maybeLoadNextChunk();
482
+		}
483
+	};
484
+	
485
+	if( start === 0 ) {
486
+		request.onprogress = this.gl 
487
+			? this.updateLoaderGL.bind(this) 
488
+			: this.updateLoader2D.bind(this);
489
+	}
490
+
491
+	request.open('GET', this.url+'?'+start+"-"+end);
492
+	request.setRequestHeader("Range", "bytes="+start+"-"+end);
493
+	request.responseType = "arraybuffer";
494
+	request.send();
495
+};
496
+
497
+jsmpeg.prototype.canPlayThrough = false;
498
+
499
+jsmpeg.prototype.progressiveLoadCallback = function(data) {
500
+	this.chunkIsLoading = false;
501
+	var isFirstChunk = (this.buffer.writePos === 0);
502
+	
503
+	var bytes = new Uint8Array(data);
504
+	this.buffer.bytes.set(bytes, this.buffer.writePos);
505
+	this.buffer.writePos += bytes.length;
506
+	
507
+	if( isFirstChunk ) {
508
+		this.findStartCode(START_SEQUENCE);
509
+		this.firstSequenceHeader = this.buffer.index;
510
+		this.decodeSequenceHeader();
511
+	}
512
+		
513
+	var loadTime = (Date.now() - this.chunkLoadStart)/1000;	
514
+	
515
+	// Throttled loading and playback did start already? Calculate when 
516
+	// the next chunk needs to be loaded
517
+	var playIndex = (this.buffer.index >> 3);
518
+	var bytesPerSecondLoaded = bytes.length / loadTime;
519
+	var bytesPerSecondPlayed = 0;
520
+
521
+	if( this.currentTime > 0 ) {
522
+		bytesPerSecondPlayed = playIndex / this.currentTime;
523
+	}
524
+	else {
525
+		// Playback didn't start - we need to count the frames we got and estimate the bytes per second
526
+		var currentIndex = this.buffer.index;
527
+		this.buffer.index = (this.buffer.writePos - bytes.length) << 3;
528
+		var frames;
529
+		for( frames = 0; this.findStartCode(START_PICTURE) !== BitReader.NOT_FOUND; frames++ ) {}
530
+		this.buffer.index = currentIndex;
531
+		bytesPerSecondPlayed = bytes.length/(frames / this.pictureRate);
532
+	}
533
+	
534
+	
535
+	var remainingTimeToPlay = (this.buffer.writePos - playIndex) / bytesPerSecondPlayed;
536
+	var remainingTimeToLoad = (this.fileSize - this.buffer.writePos) / bytesPerSecondLoaded;
537
+	var totalRemainingPlayTime = (this.fileSize-playIndex) / bytesPerSecondPlayed;
538
+	this.canPlayThrough = totalRemainingPlayTime > remainingTimeToLoad;
539
+
540
+	// Do we have a lot more remaining play time than we typically need for loading? 
541
+	// -> Increase the chunk size for the next load
542
+	if( remainingTimeToPlay > loadTime * 8 ) {
543
+		this.progressiveChunkSize = Math.min(this.progressiveChunkSize * 2, this.progressiveChunkSizeMax);
544
+	}
545
+	
546
+	// Start loading at the latest when only one chunk is left to play, but subtract twice as
547
+	// much bytes as the next chunk will take to load than it will to play
548
+	if( this.progressiveThrottled && this.canPlayThrough ) {
549
+		this.nextChunkLoadAt = this.buffer.writePos 
550
+			- this.progressiveChunkSize * 2
551
+			- this.progressiveChunkSize * (bytesPerSecondPlayed / bytesPerSecondLoaded) * 4;
552
+	}
553
+	else {
554
+		this.nextChunkLoadAt = 0;
555
+	}
556
+
557
+	if( this.buffer.writePos >= this.fileSize ) {
558
+		// All loaded. We don't need to schedule another load
559
+		this.lastFrameIndex = this.buffer.writePos << 3;
560
+		this.canPlayThrough = true;
561
+
562
+		if( this.seekable ) {
563
+			var currentBufferPos = this.buffer.index;
564
+			this.buffer.index = 0;
565
+			this.collectIntraFrames();
566
+			this.buffer.index = currentBufferPos;
567
+		}
568
+		if( this.externalLoadCallback ) {
569
+			this.externalLoadCallback(this);
570
+		}
571
+	}
572
+	else {
573
+	 	this.lastFrameIndex =  this.findLastPictureStartCode();
574
+	 	this.maybeLoadNextChunk();
575
+	 }
576
+	
577
+	this.attemptToPlay();
578
+};
579
+
580
+jsmpeg.prototype.findLastPictureStartCode = function() {
581
+	var bufferBytes = this.buffer.bytes;
582
+	for( var i = this.buffer.writePos; i > 3; i-- ) {
583
+		if(
584
+			bufferBytes[i] == START_PICTURE &&
585
+			bufferBytes[i-1] == 0x01 &&
586
+			bufferBytes[i-2] == 0x00 &&
587
+			bufferBytes[i-3] == 0x00			
588
+		) {
589
+			return (i-3) << 3;
590
+		}
591
+	}
592
+	return 0;
593
+};
594
+
595
+jsmpeg.prototype.attemptToPlay = function() {
596
+	if( this.playing || !this.wantsToPlay || !this.canPlayThrough ) { 
597
+		return;
598
+	}
599
+	this._playNow();
503 600
 };
504 601
 
505 602
 
@@ -559,7 +656,7 @@ jsmpeg.prototype.nextFrame = function() {
559 656
 	var frameStart = this.now();
560 657
 	while(true) {
561 658
 		var code = this.buffer.findNextMPEGStartCode();
562
-
659
+		
563 660
 		if( code === START_SEQUENCE ) {
564 661
 			this.decodeSequenceHeader();
565 662
 		}
@@ -567,8 +664,10 @@ jsmpeg.prototype.nextFrame = function() {
567 664
 			if( this.progressive && this.buffer.index >= this.lastFrameIndex ) {
568 665
 				// Starved
569 666
 				this.playing = false;
667
+				this.maybeLoadNextChunk();
570 668
 				return;
571 669
 			}
670
+			
572 671
 			if( this.playing ) {
573 672
 				this.scheduleNextFrame();
574 673
 			}
@@ -739,6 +838,10 @@ jsmpeg.prototype.decodePicture = function(skipOutput) {
739 838
 	this.currentFrame++;
740 839
 	this.currentTime = this.currentFrame / this.pictureRate;
741 840
 
841
+	if( this.progressive ) {
842
+		this.maybeLoadNextChunk();
843
+	}
844
+
742 845
 	this.buffer.advance(10); // skip temporalReference
743 846
 	this.pictureCodingType = this.buffer.getBits(3);
744 847
 	this.buffer.advance(16); // skip vbv_delay