Parcourir la source

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

Dominic Szablewski il y a 9 ans
Parent
révision
e06577fad2
2 fichiers modifiés avec 227 ajouts et 123 suppressions
  1. 2 1
      README.md
  2. 225 122
      jsmpg.js

+ 2 - 1
README.md Voir le fichier

20
 The `options` argument to the `jsmpeg()` supports the following properties:
20
 The `options` argument to the `jsmpeg()` supports the following properties:
21
 
21
 
22
 - `benchmark` whether to log benchmark results to the browser's console
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
 - `canvas` the HTML Canvas element to use; jsmpeg will create its own Canvas element if none is provided
25
 - `canvas` the HTML Canvas element to use; jsmpeg will create its own Canvas element if none is provided
25
 - `autoplay` whether playback should start automatically after loading
26
 - `autoplay` whether playback should start automatically after loading
26
 - `loop` whether playback is looped
27
 - `loop` whether playback is looped

+ 225 - 122
jsmpg.js Voir le fichier

11
 
11
 
12
 var jsmpeg = window.jsmpeg = function( url, opts ) {
12
 var jsmpeg = window.jsmpeg = function( url, opts ) {
13
 	opts = opts || {};
13
 	opts = opts || {};
14
-	this.progressive = (opts.progressive !== false);
15
 	this.benchmark = !!opts.benchmark;
14
 	this.benchmark = !!opts.benchmark;
16
 	this.canvas = opts.canvas || document.createElement('canvas');
15
 	this.canvas = opts.canvas || document.createElement('canvas');
17
 	this.autoplay = !!opts.autoplay;
16
 	this.autoplay = !!opts.autoplay;
22
 	this.externalDecodeCallback = opts.ondecodeframe || null;
21
 	this.externalDecodeCallback = opts.ondecodeframe || null;
23
 	this.externalFinishedCallback = opts.onfinished || null;
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
 	this.customIntraQuantMatrix = new Uint8Array(64);
29
 	this.customIntraQuantMatrix = new Uint8Array(64);
26
 	this.customNonIntraQuantMatrix = new Uint8Array(64);
30
 	this.customNonIntraQuantMatrix = new Uint8Array(64);
27
 	this.blockData = new Int32Array(64);
31
 	this.blockData = new Int32Array(64);
40
 		this.client = url;
44
 		this.client = url;
41
 		this.client.onopen = this.initSocketClient.bind(this);
45
 		this.client.onopen = this.initSocketClient.bind(this);
42
 	}
46
 	}
47
+	else if (this.progressive) {
48
+		this.beginProgressiveLoad(url);
49
+	}
43
 	else {
50
 	else {
44
 		this.load(url);
51
 		this.load(url);
45
 	}
52
 	}
258
 jsmpeg.prototype.currentTime = 0;
265
 jsmpeg.prototype.currentTime = 0;
259
 jsmpeg.prototype.frameCount = 0;
266
 jsmpeg.prototype.frameCount = 0;
260
 jsmpeg.prototype.duration = 0;
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
 jsmpeg.prototype.load = function( url ) {
269
 jsmpeg.prototype.load = function( url ) {
345
 	this.url = url;
270
 	this.url = url;
346
 
271
 
272
+	var request = new XMLHttpRequest();
347
 	var that = this;
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
 jsmpeg.prototype.updateLoader2D = function( ev ) {
289
 jsmpeg.prototype.updateLoader2D = function( ev ) {
383
-	var
290
+	var 
384
 		p = ev.loaded / ev.total,
291
 		p = ev.loaded / ev.total,
385
 		w = this.canvas.width,
292
 		w = this.canvas.width,
386
 		h = this.canvas.height,
293
 		h = this.canvas.height,
397
 	gl.uniform1f(gl.getUniformLocation(this.loadingProgram, 'loaded'), (ev.loaded / ev.total));
304
 	gl.uniform1f(gl.getUniformLocation(this.loadingProgram, 'loaded'), (ev.loaded / ev.total));
398
 	gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
305
 	gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
399
 };
306
 };
400
-
401
-
307
+	
402
 jsmpeg.prototype.loadCallback = function(file) {
308
 jsmpeg.prototype.loadCallback = function(file) {
403
 	this.buffer = new BitReader(file);
309
 	this.buffer = new BitReader(file);
404
 
310
 
406
 		this.collectIntraFrames();
312
 		this.collectIntraFrames();
407
 		this.buffer.index = 0;
313
 		this.buffer.index = 0;
408
 	}
314
 	}
409
-
315
+	
410
 	this.findStartCode(START_SEQUENCE);
316
 	this.findStartCode(START_SEQUENCE);
411
 	this.firstSequenceHeader = this.buffer.index;
317
 	this.firstSequenceHeader = this.buffer.index;
412
 	this.decodeSequenceHeader();
318
 	this.decodeSequenceHeader();
416
 
322
 
417
 	// Load the first frame
323
 	// Load the first frame
418
 	this.nextFrame();
324
 	this.nextFrame();
419
-
325
+	
420
 	if( this.autoplay ) {
326
 	if( this.autoplay ) {
421
 		this.play();
327
 		this.play();
422
 	}
328
 	}
478
 
384
 
479
 jsmpeg.prototype.play = function() {
385
 jsmpeg.prototype.play = function() {
480
 	if( this.playing ) { return; }
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
 	this.targetTime = this.now();
397
 	this.targetTime = this.now();
482
 	this.playing = true;
398
 	this.playing = true;
483
-	this.wantsToPlay = true;
484
 	this.scheduleNextFrame();
399
 	this.scheduleNextFrame();
485
 };
400
 };
486
 
401
 
491
 
406
 
492
 jsmpeg.prototype.stop = function() {
407
 jsmpeg.prototype.stop = function() {
493
 	this.currentFrame = -1;
408
 	this.currentFrame = -1;
409
+	this.currentTime = 0;
410
+	this.wantsToPlay = false;
494
 	if( this.buffer ) {
411
 	if( this.buffer ) {
495
 		this.buffer.index = this.firstSequenceHeader;
412
 		this.buffer.index = this.firstSequenceHeader;
496
 	}
413
 	}
499
 		this.client.close();
416
 		this.client.close();
500
 		this.client = null;
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
 	var frameStart = this.now();
656
 	var frameStart = this.now();
560
 	while(true) {
657
 	while(true) {
561
 		var code = this.buffer.findNextMPEGStartCode();
658
 		var code = this.buffer.findNextMPEGStartCode();
562
-
659
+		
563
 		if( code === START_SEQUENCE ) {
660
 		if( code === START_SEQUENCE ) {
564
 			this.decodeSequenceHeader();
661
 			this.decodeSequenceHeader();
565
 		}
662
 		}
567
 			if( this.progressive && this.buffer.index >= this.lastFrameIndex ) {
664
 			if( this.progressive && this.buffer.index >= this.lastFrameIndex ) {
568
 				// Starved
665
 				// Starved
569
 				this.playing = false;
666
 				this.playing = false;
667
+				this.maybeLoadNextChunk();
570
 				return;
668
 				return;
571
 			}
669
 			}
670
+			
572
 			if( this.playing ) {
671
 			if( this.playing ) {
573
 				this.scheduleNextFrame();
672
 				this.scheduleNextFrame();
574
 			}
673
 			}
739
 	this.currentFrame++;
838
 	this.currentFrame++;
740
 	this.currentTime = this.currentFrame / this.pictureRate;
839
 	this.currentTime = this.currentFrame / this.pictureRate;
741
 
840
 
841
+	if( this.progressive ) {
842
+		this.maybeLoadNextChunk();
843
+	}
844
+
742
 	this.buffer.advance(10); // skip temporalReference
845
 	this.buffer.advance(10); // skip temporalReference
743
 	this.pictureCodingType = this.buffer.getBits(3);
846
 	this.pictureCodingType = this.buffer.getBits(3);
744
 	this.buffer.advance(16); // skip vbv_delay
847
 	this.buffer.advance(16); // skip vbv_delay