Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonocasey committed May 2, 2019
1 parent 854422f commit 197bab7
Show file tree
Hide file tree
Showing 11 changed files with 465 additions and 23 deletions.
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ Video.js Compatibility: 6.0, 7.0
- [Source](#source)
- [List](#list)
- [withCredentials](#withcredentials)
- [handleManifestRedirects](#handlemanifestredirects)
- [useCueTags](#usecuetags)
- [overrideNative](#overridenative)
- [blacklistDuration](#blacklistduration)
Expand All @@ -51,6 +52,7 @@ Video.js Compatibility: 6.0, 7.0
- [allowSeeksWithinUnsafeLiveWindow](#allowseekswithinunsafelivewindow)
- [customTagParsers](#customtagparsers)
- [customTagMappers](#customtagmappers)
- [cacheEncryptionKeys](#cacheencryptionkeys)
- [Runtime Properties](#runtime-properties)
- [hls.playlists.master](#hlsplaylistsmaster)
- [hls.playlists.media](#hlsplaylistsmedia)
Expand Down Expand Up @@ -178,7 +180,7 @@ You can deploy a single HLS stream, code against the
regular HTML5 video APIs, and create a fast, high-quality video
experience across all the big web device categories.

Check out the [full documentation](docs/intro.md) for details on how HLS works
Check out the [full documentation](docs/README.md) for details on how HLS works
and advanced configuration. A description of the [adaptive switching
behavior](docs/bitrate-switching.md) is available, too.

Expand Down Expand Up @@ -258,6 +260,16 @@ is set to `true`.
See html5rocks's [article](http://www.html5rocks.com/en/tutorials/cors/)
for more info.

##### handleManifestRedirects
* Type: `boolean`
* Default: `false`
* can be used as a source option
* can be used as an initialization option

When the `handleManifestRedirects` property is set to `true`, manifest requests
which are redirected will have their URL updated to the new URL for future
requests.

##### useCueTags
* Type: `boolean`
* can be used as an initialization option
Expand Down Expand Up @@ -408,6 +420,14 @@ With `customTagParsers` you can pass an array of custom m3u8 tag parser objects.

Similar to `customTagParsers`, with `customTagMappers` you can pass an array of custom m3u8 tag mapper objects. See https://github.com/videojs/m3u8-parser#custom-parsers

##### cacheEncryptionKeys
* Type: `boolean`
* can be used as a source option
* can be used as an initialization option

This option forces the player to cache AES-128 encryption keys internally instead of requesting the key alongside every segment request.
This option defaults to `false`.

### Runtime Properties
Runtime properties are attached to the tech object when HLS is in
use. You can get a reference to the HLS source handler like this:
Expand Down
7 changes: 7 additions & 0 deletions src/bin-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ export const initSegmentId = function(initSegment) {
].join(',');
};

/**
* Returns a unique string identifier for a media segment key.
*/
export const segmentKeyId = function(key) {
return key.resolvedUri;
};

/**
* utils to help dump binary data to the console
*/
Expand Down
8 changes: 5 additions & 3 deletions src/media-segment-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -474,16 +474,18 @@ const decryptSegment = ({

decryptionWorker.addEventListener('message', decryptionHandler);

const keyBytes = segment.key.bytes.slice();

// this is an encrypted segment
// incrementally decrypt the segment
decryptionWorker.postMessage(createTransferableMessage({
source: segment.requestId,
encrypted: segment.encryptedBytes,
key: segment.key.bytes,
key: keyBytes,
iv: segment.key.iv
}), [
segment.encryptedBytes.buffer,
segment.key.bytes.buffer
keyBytes.buffer
]);
};

Expand Down Expand Up @@ -723,7 +725,7 @@ export const mediaSegmentRequest = ({
});

// optionally, request the decryption key
if (segment.key) {
if (segment.key && !segment.key.bytes) {
const keyRequestOptions = videojs.mergeOptions(xhrOptions, {
uri: segment.key.resolvedUri,
responseType: 'arraybuffer'
Expand Down
56 changes: 51 additions & 5 deletions src/segment-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Playlist from './playlist';
import videojs from 'video.js';
import Config from './config';
import window from 'global/window';
import { initSegmentId } from './bin-utils';
import { initSegmentId, segmentKeyId } from './bin-utils';
import { mediaSegmentRequest, REQUEST_ERRORS } from './media-segment-request';
import TransmuxWorker from 'worker!./transmuxer-worker.worker.js';
import segmentTransmuxer from './segment-transmuxer';
Expand Down Expand Up @@ -218,6 +218,11 @@ export default class SegmentLoader extends videojs.EventTarget {
// Fragmented mp4 playback
this.activeInitSegmentId_ = null;
this.initSegments_ = {};

// HLSe playback
this.cacheEncryptionKeys_ = settings.cacheEncryptionKeys;
this.keyCache_ = {};

// Fmp4 CaptionParser
this.captionParser_ = new CaptionParser();

Expand Down Expand Up @@ -450,6 +455,44 @@ export default class SegmentLoader extends videojs.EventTarget {
return storedMap || map;
}

/**
* Gets and sets key for the provided key
*
* @param {Object} key
* The key object representing the key to get or set
* @param {Boolean=} set
* If true, the key for the provided key should be saved
* @return {Object}
* Key object for desired key
*/
segmentKey(key, set = false) {
if (!key) {
return null;
}

const id = segmentKeyId(key);
let storedKey = this.keyCache_[id];

// TODO: We should use the HTTP Expires header to invalidate our cache per
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-6.2.3
if (this.cacheEncryptionKeys_ && set && !storedKey && key.bytes) {
this.keyCache_[id] = storedKey = {
resolvedUri: key.resolvedUri,
bytes: key.bytes
};
}

const result = {
resolvedUri: (storedKey || key).resolvedUri
};

if (storedKey) {
result.bytes = storedKey.bytes;
}

return result;
}

/**
* Returns true if all configuration required for loading is present, otherwise false.
*
Expand Down Expand Up @@ -1655,16 +1698,19 @@ export default class SegmentLoader extends videojs.EventTarget {
0, 0, 0, segmentInfo.mediaIndex + segmentInfo.playlist.mediaSequence
]);

simpleSegment.key = {
resolvedUri: segment.key.resolvedUri,
iv
};
simpleSegment.key = this.segmentKey(segment.key);
simpleSegment.key.iv = iv;
}

if (segment.map) {
simpleSegment.map = this.initSegmentForMap(segment.map);
}

// if this request included a segment key, save that data in the cache
if (simpleSegment.key) {
this.segmentKey(simpleSegment.key, true);
}

return simpleSegment;
}

Expand Down
4 changes: 3 additions & 1 deletion src/videojs-http-streaming.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ class HlsHandler extends Component {
this.options_.useBandwidthFromLocalStorage || false;
this.options_.customTagParsers = this.options_.customTagParsers || [];
this.options_.customTagMappers = this.options_.customTagMappers || [];
this.options_.cacheEncryptionKeys = this.options_.cacheEncryptionKeys || false;

if (typeof this.options_.blacklistDuration !== 'number') {
this.options_.blacklistDuration = 5 * 60;
Expand Down Expand Up @@ -443,7 +444,8 @@ class HlsHandler extends Component {
'smoothQualityChange',
'customTagParsers',
'customTagMappers',
'handleManifestRedirects'
'handleManifestRedirects',
'cacheEncryptionKeys'
].forEach((option) => {
if (typeof this.source_[option] !== 'undefined') {
this.options_[option] = this.source_[option];
Expand Down
4 changes: 4 additions & 0 deletions test/configuration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ const options = [{
return '#FOO';
}
}]
}, {
name: 'cacheEncryptionKeys',
default: false,
test: true
}];

const CONFIG_KEYS = Object.keys(Config);
Expand Down
35 changes: 35 additions & 0 deletions test/master-playlist-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,41 @@ QUnit.test('creates appropriate PlaylistLoader for sourceType', function(assert)
'created a dash playlist loader');
});

QUnit.test('passes options to SegmentLoader', function(assert) {
const options = {
url: 'test',
tech: this.player.tech_
};

let controller = new MasterPlaylistController(options);

assert.notOk(controller.mainSegmentLoader_.bandwidth, "bandwidth won't be set by default");
assert.notOk(controller.mainSegmentLoader_.sourceType_, "sourceType won't be set by default");
assert.notOk(controller.mainSegmentLoader_.cacheEncryptionKeys_, "cacheEncryptionKeys won't be set by default");

controller = new MasterPlaylistController(Object.assign({
bandwidth: 3,
cacheEncryptionKeys: true,
sourceType: 'fake-type'
}, options));

assert.strictEqual(
controller.mainSegmentLoader_.bandwidth,
3,
'bandwidth will be set'
);
assert.strictEqual(
controller.mainSegmentLoader_.sourceType_,
'fake-type',
'sourceType will be set'
);
assert.strictEqual(
controller.mainSegmentLoader_.cacheEncryptionKeys_,
true,
'cacheEncryptionKeys will be set'
);
});

QUnit.test('resets SegmentLoader when seeking out of buffer',
function(assert) {
let resets = 0;
Expand Down
53 changes: 52 additions & 1 deletion test/media-segment-request.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,6 @@ QUnit.test('the key response is converted to the correct format', function(asser
QUnit.test('segment with key has bytes decrypted', function(assert) {
const done = assert.async();

assert.expect(8);
mediaSegmentRequest({
xhr: this.xhr,
xhrOptions: this.xhrOptions,
Expand All @@ -375,6 +374,12 @@ QUnit.test('segment with key has bytes decrypted', function(assert) {
doneFn: (error, segmentData) => {
assert.notOk(error, 'there are no errors');
assert.ok(segmentData.bytes, 'decrypted bytes in segment');
assert.ok(segmentData.key.bytes, 'key bytes in segment');
assert.equal(
segmentData.key.bytes.buffer.byteLength,
16,
'key bytes are readable'
);

// verify stats
assert.equal(segmentData.stats.bytesReceived, 8, '8 bytes');
Expand All @@ -399,6 +404,52 @@ QUnit.test('segment with key has bytes decrypted', function(assert) {
this.clock.tick(100);
});

QUnit.test('segment with key bytes does not request key again', function(assert) {
const done = assert.async();

mediaSegmentRequest(
this.xhr,
this.xhrOptions,
this.realDecrypter,
this.noop,
{
resolvedUri: '0-test.ts',
key: {
resolvedUri: '0-key.php',
bytes: new Uint32Array([0, 2, 3, 1]),
iv: {
bytes: new Uint32Array([0, 0, 0, 1])
}
}
},
this.noop,
(error, segmentData) => {
assert.notOk(error, 'there are no errors');
assert.ok(segmentData.bytes, 'decrypted bytes in segment');
assert.ok(segmentData.key.bytes, 'key bytes in segment');
assert.equal(
segmentData.key.bytes.buffer.byteLength,
16,
'key bytes are readable'
);

// verify stats
assert.equal(segmentData.stats.bytesReceived, 8, '8 bytes');
done();
});

assert.equal(this.requests.length, 1, 'there is one request');
const segmentReq = this.requests.shift();

assert.equal(segmentReq.uri, '0-test.ts', 'the second request is for a segment');

segmentReq.response = new Uint8Array(8).buffer;
segmentReq.respond(200, null, '');

// Allow the decrypter to decrypt
this.clock.tick(100);
});

QUnit.test('key 404 calls back with error', function(assert) {
const done = assert.async();
let segmentReq;
Expand Down

0 comments on commit 197bab7

Please sign in to comment.