Browse Source

run decryption in a webworker (#972)

* run decryption in a webworker
* drop support for IE10
pull/6/head
Matthew Neil 9 years ago
committed by GitHub
parent
commit
9929effca9
  1. 4
      README.md
  2. 3
      package.json
  3. 33
      src/bin-utils.js
  4. 42
      src/decrypter-worker.js
  5. 19
      src/master-playlist-controller.js
  6. 44
      src/segment-loader.js
  7. 5
      src/videojs-contrib-hls.js
  8. 19
      test/segment-loader.test.js
  9. 12
      test/videojs-contrib-hls.test.js

4
README.md

@ -42,6 +42,7 @@ Maintenance Status: Stable
- [In-Band Metadata](#in-band-metadata)
- [Hosting Considerations](#hosting-considerations)
- [Known Issues](#known-issues)
- [IE10 and Below](#ie10-and-below)
- [IE11](#ie11)
- [Fragmented MP4 Support](#fragmented-mp4-support)
- [Testing](#testing)
@ -452,6 +453,9 @@ and most CDNs should have no trouble turning CORS on for your account.
Issues that are currenty know about with workarounds. If you want to
help find a solution that would be appreciated!
### IE10 and Below
As of version 5.0.0, IE10 and below are no longer supported.
### IE11
In some IE11 setups there are issues working with its native HTML
SourceBuffers functionality. This leads to various issues, such as

3
package.json

@ -96,7 +96,8 @@
"url-toolkit": "^1.0.4",
"video.js": "^5.15.1",
"videojs-contrib-media-sources": "^4.1.4",
"videojs-swf": "^5.0.2"
"videojs-swf": "^5.0.2",
"webworkify": "1.0.2"
},
"devDependencies": {
"babel": "^5.8.0",

33
src/bin-utils.js

@ -30,6 +30,36 @@ const formatAsciiString = function(e) {
return '.';
};
/**
* Creates an object for sending to a web worker modifying properties that are TypedArrays
* into a new object with seperated properties for the buffer, byteOffset, and byteLength.
*
* @param {Object} message
* Object of properties and values to send to the web worker
* @return {Object}
* Modified message with TypedArray values expanded
* @function createTransferableMessage
*/
const createTransferableMessage = function(message) {
const transferable = {};
Object.keys(message).forEach((key) => {
const value = message[key];
if (ArrayBuffer.isView(value)) {
transferable[key] = {
bytes: value.buffer,
byteOffset: value.byteOffset,
byteLength: value.byteLength
};
} else {
transferable[key] = value;
}
});
return transferable;
};
/**
* utils to help dump binary data to the console
*/
@ -59,7 +89,8 @@ const utils = {
result += textRange(ranges, i) + ' ';
}
return result;
}
},
createTransferableMessage
};
export default utils;

42
src/decrypter-worker.js

@ -0,0 +1,42 @@
import window from 'global/window';
import {Decrypter} from 'aes-decrypter';
import { createTransferableMessage } from './bin-utils';
/**
* Our web worker interface so that things can talk to aes-decrypter
* that will be running in a web worker. the scope is passed to this by
* webworkify.
*
* @param {Object} self
* the scope for the web worker
*/
const Worker = function(self) {
self.onmessage = function(event) {
const data = event.data;
const encrypted = new Uint8Array(data.encrypted.bytes,
data.encrypted.byteOffset,
data.encrypted.byteLength);
const key = new Uint32Array(data.key.bytes,
data.key.byteOffset,
data.key.byteLength / 4);
const iv = new Uint32Array(data.iv.bytes,
data.iv.byteOffset,
data.iv.byteLength / 4);
/* eslint-disable no-new, handle-callback-err */
new Decrypter(encrypted,
key,
iv,
function(err, bytes) {
window.postMessage(createTransferableMessage({
source: data.source,
decrypted: bytes
}), [bytes.buffer]);
});
/* eslint-enable */
};
};
export default (self) => {
return new Worker(self);
};

19
src/master-playlist-controller.js

@ -8,6 +8,8 @@ import videojs from 'video.js';
import AdCueTags from './ad-cue-tags';
import SyncController from './sync-controller';
import { translateLegacyCodecs } from 'videojs-contrib-media-sources/es5/codec-utils';
import worker from 'webworkify';
import Decrypter from './decrypter-worker';
// 5 minute blacklist
const BLACKLIST_DURATION = 5 * 60 * 1000;
@ -235,6 +237,8 @@ export class MasterPlaylistController extends videojs.EventTarget {
this.syncController_ = new SyncController();
this.decrypter_ = worker(Decrypter);
let segmentLoaderOptions = {
hls: this.hls_,
mediaSource: this.mediaSource,
@ -244,7 +248,9 @@ export class MasterPlaylistController extends videojs.EventTarget {
setCurrentTime: (a) => this.tech_.setCurrentTime(a),
hasPlayed: () => this.hasPlayed_(),
bandwidth,
syncController: this.syncController_
syncController: this.syncController_,
decrypter: this.decrypter_,
loaderType: 'main'
};
// setup playlist loaders
@ -256,7 +262,17 @@ export class MasterPlaylistController extends videojs.EventTarget {
// combined audio/video or just video when alternate audio track is selected
this.mainSegmentLoader_ = new SegmentLoader(segmentLoaderOptions);
// alternate audio track
segmentLoaderOptions.loaderType = 'audio';
this.audioSegmentLoader_ = new SegmentLoader(segmentLoaderOptions);
this.decrypter_.onmessage = (event) => {
if (event.data.source === 'main') {
this.mainSegmentLoader_.handleDecrypted_(event.data);
} else if (event.data.source === 'audio') {
this.audiosegmentloader_.handleDecrypted_(event.data);
}
};
this.setupSegmentLoaderListeners_();
this.masterPlaylistLoader_.start();
@ -968,6 +984,7 @@ export class MasterPlaylistController extends videojs.EventTarget {
* that it controls
*/
dispose() {
this.decrypter_.terminate();
this.masterPlaylistLoader_.dispose();
this.mainSegmentLoader_.dispose();

44
src/segment-loader.js

@ -4,9 +4,9 @@
import {getMediaInfoForTime_ as getMediaInfoForTime} from './playlist';
import videojs from 'video.js';
import SourceUpdater from './source-updater';
import {Decrypter} from 'aes-decrypter';
import Config from './config';
import window from 'global/window';
import { createTransferableMessage } from './bin-utils';
// in ms
const CHECK_BUFFER_DELAY = 500;
@ -130,6 +130,7 @@ export default class SegmentLoader extends videojs.EventTarget {
this.setCurrentTime_ = settings.setCurrentTime;
this.mediaSource_ = settings.mediaSource;
this.hls_ = settings.hls;
this.loaderType_ = settings.loaderType;
// private instance variables
this.checkBufferTimeout_ = null;
@ -145,6 +146,8 @@ export default class SegmentLoader extends videojs.EventTarget {
this.activeInitSegmentId_ = null;
this.initSegments_ = {};
this.decrypter_ = settings.decrypter;
// Manages the tracking and generation of sync-points, mappings
// between a time in the display time and a segment index within
// a playlist
@ -926,21 +929,40 @@ export default class SegmentLoader extends videojs.EventTarget {
if (segment.key) {
// this is an encrypted segment
// incrementally decrypt the segment
/* eslint-disable no-new, handle-callback-err */
new Decrypter(segmentInfo.encryptedBytes,
segment.key.bytes,
segment.key.iv,
(function(err, bytes) {
// err always null
segmentInfo.bytes = bytes;
this.handleSegment_();
}).bind(this));
/* eslint-enable */
this.decrypter_.postMessage(createTransferableMessage({
source: this.loaderType_,
encrypted: segmentInfo.encryptedBytes,
key: segment.key.bytes,
iv: segment.key.iv
}), [
segmentInfo.encryptedBytes.buffer,
segment.key.bytes.buffer
]);
} else {
this.handleSegment_();
}
}
/**
* Handles response from the decrypter and attaches the decrypted bytes to the pending
* segment
*
* @param {Object} data
* Response from decrypter
* @method handleDecrypted_
*/
handleDecrypted_(data) {
const segmentInfo = this.pendingSegment_;
const decrypted = data.decrypted;
if (segmentInfo) {
segmentInfo.bytes = new Uint8Array(decrypted.bytes,
decrypted.byteOffset,
decrypted.byteLength);
}
this.handleSegment_();
}
/**
* append a decrypted segement to the SourceBuffer through a SourceUpdater
*

5
src/videojs-contrib-hls.js

@ -753,6 +753,11 @@ Hls.comparePlaylistResolution = function(left, right) {
};
HlsSourceHandler.canPlayType = function(type) {
// No support for IE 10 or below
if (videojs.browser.IE_VERSION && videojs.browser.IE_VERSION <= 10) {
return false;
}
let mpegurlRE = /^(audio|video|application)\/(x-|vnd\.apple\.)?mpegurl/i;
// favor native HLS support if it's available

19
test/segment-loader.test.js

@ -11,11 +11,14 @@ import {
} from './test-helpers.js';
import sinon from 'sinon';
import SyncController from '../src/sync-controller';
import Decrypter from '../src/decrypter-worker';
import worker from 'webworkify';
let currentTime;
let mediaSource;
let loader;
let syncController;
let decrypter;
QUnit.module('Segment Loader', {
beforeEach(assert) {
@ -38,6 +41,7 @@ QUnit.module('Segment Loader', {
mediaSource = new videojs.MediaSource();
mediaSource.trigger('sourceopen');
syncController = new SyncController();
decrypter = worker(Decrypter);
loader = new SegmentLoader({
hls: this.fakeHls,
currentTime() {
@ -47,14 +51,20 @@ QUnit.module('Segment Loader', {
seeking: () => false,
hasPlayed: () => true,
mediaSource,
syncController
syncController,
decrypter,
loaderType: 'main'
});
decrypter.onmessage = (event) => {
loader.handleDecrypted_(event.data);
};
},
afterEach() {
this.env.restore();
this.mse.restore();
this.timescale.restore();
this.startTime.restore();
decrypter.terminate();
}
});
@ -970,9 +980,13 @@ function(assert) {
QUnit.test('segment with key has decrypted bytes appended during processing', function(assert) {
let keyRequest;
let segmentRequest;
let done = assert.async();
// stop processing so we can examine segment info
loader.handleSegment_ = function() {};
loader.handleSegment_ = function() {
assert.ok(loader.pendingSegment_.bytes, 'decrypted bytes in segment');
done();
};
loader.playlist(playlistWithDuration(10, {isEncrypted: true}));
loader.mimeType(this.mimeType);
@ -993,7 +1007,6 @@ QUnit.test('segment with key has decrypted bytes appended during processing', fu
this.clock.tick(1);
// Allow the decrypter's async stream to run the callback
this.clock.tick(1);
assert.ok(loader.pendingSegment_.bytes, 'decrypted bytes in segment');
// verify stats
assert.equal(loader.mediaBytesTransferred, 8, '8 bytes');

12
test/videojs-contrib-hls.test.js

@ -95,6 +95,7 @@ QUnit.module('HLS', {
// save and restore browser detection for the Firefox-specific tests
this.old.IS_FIREFOX = videojs.browser.IS_FIREFOX;
this.old.IE_VERSION = videojs.browser.IE_VERSION;
this.standardXHRResponse = (request, data) => {
standardXHRResponse(request, data);
@ -120,6 +121,7 @@ QUnit.module('HLS', {
videojs.Hls.supportsNativeHls = this.old.NativeHlsSupport;
videojs.Hls.Decrypter = this.old.Decrypt;
videojs.browser.IS_FIREFOX = this.old.IS_FIREFOX;
videojs.browser.IE_VERSION = this.old.IE_VERSION;
this.player.dispose();
}
@ -1466,6 +1468,16 @@ QUnit.test('the source handler supports HLS mime types', function(assert) {
});
});
QUnit.test('source handler does not support sources when IE 10 or below', function(assert) {
videojs.browser.IE_VERSION = 10;
['html5', 'flash'].forEach(function(techName) {
assert.ok(!HlsSourceHandler(techName).canHandleSource({
type: 'application/x-mpegURL'
}), 'does not support when browser is IE10');
});
});
QUnit.test('fires loadstart manually if Flash is used', function(assert) {
videojs.HlsHandler.prototype.setupQualityLevels_ = () => {};
let tech = new (videojs.getTech('Flash'))({});

Loading…
Cancel
Save