Skip to content

Commit

Permalink
Add option to parse pssh from media segment to make in-band key rotat…
Browse files Browse the repository at this point in the history
…ion work on Xbox one, which does not seem to be watching for pssh changes. Is enabled with the config field drm.parseInbandPsshEnabled.
  • Loading branch information
caridley committed Sep 9, 2022
1 parent c147da7 commit 01e04d4
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 5 deletions.
1 change: 1 addition & 0 deletions demo/common/message_ids.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ shakaDemo.MessageIds = {
NUMBER_NONZERO_INTEGER_WARNING: 'DEMO_NUMBER_NONZERO_INTEGER_WARNING',
NUMBER_OF_PARALLEL_DOWNLOADS: 'DEMO_NUMBER_OF_PARALLEL_DOWNLOADS',
OFFLINE_SECTION_HEADER: 'DEMO_OFFLINE_SECTION_HEADER',
PARSE_INBAND_PSSH_ENABLED: 'DEMO_PARSE_INBAND_PSSH_ENABLED',
PREFER_FORCED_SUBS: 'DEMO_PREFER_FORCED_SUBS',
PREFER_NATIVE_HLS: 'DEMO_PREFER_NATIVE_HLS',
REBUFFERING_GOAL: 'DEMO_REBUFFERING_GOAL',
Expand Down
4 changes: 3 additions & 1 deletion demo/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ shakaDemo.Config = class {
'drm.updateExpirationTime',
/* canBeDecimal= */ true,
/* canBeZero= */ false,
/* canBeUnset= */ true);
/* canBeUnset= */ true)
.addBoolInput_(MessageIds.PARSE_INBAND_PSSH_ENABLED,
'drm.parseInbandPsshEnabled');
const advanced = shakaDemoMain.getConfiguration().drm.advanced || {};
const addDRMAdvancedField = (name, valueName, suggestions) => {
// All advanced fields of a given type are set at once.
Expand Down
1 change: 1 addition & 0 deletions demo/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
"DEMO_NUMBER_NONZERO_INTEGER_WARNING": "Must be a positive, nonzero integer.",
"DEMO_NUMBER_OF_PARALLEL_DOWNLOADS": "Number of parallel downloads",
"DEMO_OBSERVE_QUALITY_CHANGES": "Observe media quality changes",
"DEMO_PARSE_INBAND_PSSH_ENABLED": "Parse inband 'pssh' from media segments",
"DEMO_OFFLINE": "Downloadable",
"DEMO_OFFLINE_SEARCH": "Filters for assets that can be stored offline.",
"DEMO_OFFLINE_SECTION_HEADER": "Offline",
Expand Down
4 changes: 4 additions & 0 deletions demo/locales/source.json
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,10 @@
"description": "The label on a field that allows users to provide a full mime type for a custom asset.",
"message": "Full Mime Type for Playing Media Playlists Directly"
},
"DEMO_PARSE_INBAND_PSSH_ENABLED": {
"description": "The name of a configuration value.",
"message": "Parse inband 'pssh' from media segments"
},
"DEMO_PLAY": {
"description": "A button to play the attached asset.",
"message": "Play"
Expand Down
9 changes: 7 additions & 2 deletions externs/shaka/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,8 @@ shaka.extern.AdvancedDrmConfiguration;
* logLicenseExchange: boolean,
* updateExpirationTime: number,
* preferredKeySystems: !Array.<string>,
* keySystemsMapping: !Object.<string, string>
* keySystemsMapping: !Object.<string, string>,
* parseInbandPsshEnabled: boolean
* }}
*
* @property {shaka.extern.RetryParameters} retryParameters
Expand Down Expand Up @@ -675,7 +676,11 @@ shaka.extern.AdvancedDrmConfiguration;
* Specifies the priorties of available DRM key systems.
* @property {Object.<string, string>} keySystemsMapping
* A map of key system name to key system name.
*
* @property {boolean} parseInbandPsshEnabled
* <i>Defaults to false</i><br>
* When true parse DRM init data from pssh boxes in media segments and ignore
* 'encrypted' events.
* This is required when using in-band key rotation on Xbox One.
* @exportDoc
*/
shaka.extern.DrmConfiguration;
Expand Down
53 changes: 52 additions & 1 deletion lib/media/drm_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ goog.require('shaka.util.FakeEvent');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.Iterables');
goog.require('shaka.util.Lazy');
goog.require('shaka.util.ManifestParserUtils');
goog.require('shaka.util.MapUtils');
goog.require('shaka.util.MimeUtils');
goog.require('shaka.util.Platform');
goog.require('shaka.util.Pssh');
goog.require('shaka.util.PublicPromise');
goog.require('shaka.util.StreamUtils');
goog.require('shaka.util.StringUtils');
Expand Down Expand Up @@ -497,7 +499,11 @@ shaka.media.DrmEngine = class {

// Explicit init data for any one stream or an offline session is
// sufficient to suppress 'encrypted' events for all streams.
if (!manifestInitData && !this.offlineSessionIds_.length) {
// Also suppress 'encrypted' events when parsing in-band ppsh
// from media segments because that serves the same purpose as the
// 'encrypted' events.
if (!manifestInitData && !this.offlineSessionIds_.length &&
!this.config_.parseInbandPsshEnabled) {
this.eventManager_.listen(
this.video_, 'encrypted', (e) => this.onEncryptedEvent_(e));
}
Expand Down Expand Up @@ -668,6 +674,14 @@ shaka.media.DrmEngine = class {
}
}

// If there are pre-existing sessions that have all been loaded
// then reset the allSessionsLoaded_ promise, which can now be
// used to wait for new sesssions to be loaded
if (this.activeSessions_.size > 0 && this.areAllSessionsLoaded_()) {
this.allSessionsLoaded_.resolve();
this.allSessionsLoaded_ = new shaka.util.PublicPromise();
this.allSessionsLoaded_.catch(() => {});
}
this.createSession(initDataType, initData,
this.currentDrmInfo_.sessionType);
}
Expand Down Expand Up @@ -2327,6 +2341,43 @@ shaka.media.DrmEngine = class {
}
}
}

/**
* Parse pssh from a media segment and announce new initData
*
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
* @param {!BufferSource} mediaSegment
* @return {!Promise<void>}
*/
parseInbandPssh(contentType, mediaSegment) {
if (!this.config_.parseInbandPsshEnabled) {
return Promise.resolve();
}

const ContentType = shaka.util.ManifestParserUtils.ContentType;
if (![ContentType.AUDIO, ContentType.VIDEO].includes(contentType)) {
return Promise.resolve();
}

const pssh = new shaka.util.Pssh(
shaka.util.BufferUtils.toUint8(mediaSegment));

let totalLength = 0;
for (const data of pssh.data) {
totalLength += data.length;
}
if (totalLength == 0) {
return Promise.resolve();
}
const combinedData = new Uint8Array(totalLength);
let pos = 0;
for (const data of pssh.data) {
combinedData.set(data, pos);
pos += data.length;
}
this.newInitData('cenc', combinedData);
return this.allSessionsLoaded_;
}
};


Expand Down
10 changes: 9 additions & 1 deletion lib/media/streaming_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -1591,6 +1591,8 @@ shaka.media.StreamingEngine = class {
shaka.log.v1(logPrefix, 'appending init segment');
const hasClosedCaptions = mediaState.stream.closedCaptions &&
mediaState.stream.closedCaptions.size > 0;
await this.playerInterface_.beforeAppendSegment(
mediaState.type, initSegment);
await this.playerInterface_.mediaSourceEngine.appendBuffer(
mediaState.type, initSegment, /* reference= */ null,
hasClosedCaptions);
Expand Down Expand Up @@ -1643,6 +1645,7 @@ shaka.media.StreamingEngine = class {

const seeked = mediaState.seeked;
mediaState.seeked = false;
await this.playerInterface_.beforeAppendSegment(mediaState.type, segment);
await this.playerInterface_.mediaSourceEngine.appendBuffer(
mediaState.type,
segment,
Expand Down Expand Up @@ -2026,7 +2029,9 @@ shaka.media.StreamingEngine = class {
* onManifestUpdate: function(),
* onSegmentAppended: function(number, number,
* !shaka.util.ManifestParserUtils.ContentType),
* onInitSegmentAppended: function(!number,!shaka.media.InitSegmentReference)
* onInitSegmentAppended: function(!number,!shaka.media.InitSegmentReference),
* beforeAppendSegment: function(
* shaka.util.ManifestParserUtils.ContentType,!BufferSource):Promise
* }}
*
* @property {function():number} getPresentationTime
Expand Down Expand Up @@ -2056,6 +2061,9 @@ shaka.media.StreamingEngine = class {
* @property
* {function(!number, !shaka.media.InitSegmentReference)} onInitSegmentAppended
* Called when an init segment is appended to a MediaSource.
* @property {!function(shaka.util.ManifestParserUtils.ContentType,
* !BufferSource):Promise} beforeAppendSegment
* A function called just before appending to the source buffer.
*/
shaka.media.StreamingEngine.PlayerInterface;

Expand Down
3 changes: 3 additions & 0 deletions lib/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -2969,6 +2969,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
this.qualityObserver_.addMediaQualityChange(mediaQuality, position);
}
},
beforeAppendSegment: (contentType, segment) => {
return this.drmEngine_.parseInbandPssh(contentType, segment);
},
};

return new shaka.media.StreamingEngine(this.manifest_, playerInterface);
Expand Down
1 change: 1 addition & 0 deletions lib/util/player_configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ shaka.util.PlayerConfiguration = class {
updateExpirationTime: 1,
preferredKeySystems: [],
keySystemsMapping: {},
parseInbandPsshEnabled: false,
};

const manifest = {
Expand Down
1 change: 1 addition & 0 deletions lib/util/pssh.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ shaka.util.Pssh = class {

new shaka.util.Mp4Parser()
.box('moov', shaka.util.Mp4Parser.children)
.box('moof', shaka.util.Mp4Parser.children)
.fullBox('pssh', (box) => this.parsePsshBox_(box))
.parse(psshBox);

Expand Down
61 changes: 61 additions & 0 deletions test/media/drm_engine_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,14 @@ describe('DrmEngine', () => {
'encrypted', jasmine.any(Function), jasmine.anything());
});

it('is not listened for if parseInbandPsshEnabled is true', async () => {
config.parseInbandPsshEnabled = true;
drmEngine.configure(config);
await initAndAttach();
expect(mockVideo.addEventListener).not.toHaveBeenCalledWith(
'encrypted', jasmine.any(Function), jasmine.anything());
});

it('triggers the creation of a session', async () => {
await initAndAttach();
const initData1 = new Uint8Array(1);
Expand Down Expand Up @@ -2355,6 +2363,59 @@ describe('DrmEngine', () => {
}
});

describe('parseInbandPssh', () => {
const WIDEVINE_PSSH =
'00000028' + // atom size
'70737368' + // atom type='pssh'
'00000000' + // v0, flags=0
'edef8ba979d64acea3c827dcd51d21ed' + // system id (Widevine)
'00000008' + // data size
'0102030405060708'; // data

const PLAYREADY_PSSH =
'00000028' + // atom size
'70737368' + // atom type 'pssh'
'00000000' + // v0, flags=0
'9a04f07998404286ab92e65be0885f95' + // system id (PlayReady)
'00000008' + // data size
'0102030405060708'; // data

const SEGMENT =
'00000058' + // atom size = 28x + 28x + 8x
'6d6f6f66' + // atom type 'moof'
WIDEVINE_PSSH +
PLAYREADY_PSSH;

const binarySegment = shaka.util.Uint8ArrayUtils.fromHex(SEGMENT);

it('calls newInitData when enabled', async () => {
config.parseInbandPsshEnabled = true;
await initAndAttach();

/** @type {!jasmine.Spy} */
const newInitDataSpy = jasmine.createSpy('newInitData');
drmEngine.newInitData = shaka.test.Util.spyFunc(newInitDataSpy);

await drmEngine.parseInbandPssh(
shaka.util.ManifestParserUtils.ContentType.VIDEO, binarySegment);
const expectedInitData = shaka.util.Uint8ArrayUtils.fromHex(
WIDEVINE_PSSH + PLAYREADY_PSSH);
expect(newInitDataSpy).toHaveBeenCalledWith('cenc', expectedInitData);
});

it('does not call newInitData when disabled', async () => {
config.parseInbandPsshEnabled = false;
await initAndAttach();

/** @type {!jasmine.Spy} */
const newInitDataSpy = jasmine.createSpy('newInitData');
drmEngine.newInitData = shaka.test.Util.spyFunc(newInitDataSpy);
await drmEngine.parseInbandPssh(
shaka.util.ManifestParserUtils.ContentType.VIDEO, binarySegment);
expect(newInitDataSpy).not.toHaveBeenCalled();
});
});

async function initAndAttach() {
const variants = manifest.variants;
await drmEngine.initForPlayback(variants, manifest.offlineSessionIds);
Expand Down
1 change: 1 addition & 0 deletions test/media/streaming_engine_integration.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ describe('StreamingEngine', () => {
onManifestUpdate: () => {},
onSegmentAppended: () => playhead.notifyOfBufferingChange(),
onInitSegmentAppended: () => {},
beforeAppendSegment: () => Promise.resolve(),
};
streamingEngine = new shaka.media.StreamingEngine(
/** @type {shaka.extern.Manifest} */(manifest), playerInterface);
Expand Down
29 changes: 29 additions & 0 deletions test/media/streaming_engine_unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ describe('StreamingEngine', () => {
let getBandwidthEstimate;
/** @type {!shaka.media.StreamingEngine} */
let streamingEngine;
/** @type {!jasmine.Spy} */
let beforeAppendSegment;

/** @type {function(function(), number)} */
let realSetTimeout;
Expand Down Expand Up @@ -429,9 +431,14 @@ describe('StreamingEngine', () => {
onEvent = jasmine.createSpy('onEvent');
onManifestUpdate = jasmine.createSpy('onManifestUpdate');
onSegmentAppended = jasmine.createSpy('onSegmentAppended');
beforeAppendSegment = jasmine.createSpy('beforeAppendSegment');
getBandwidthEstimate = jasmine.createSpy('getBandwidthEstimate');
getBandwidthEstimate.and.returnValue(1e3);

beforeAppendSegment.and.callFake((segment) => {
return Promise.resolve();
});

if (!config) {
config = shaka.util.PlayerConfiguration.createDefault().streaming;
config.rebufferingGoal = 2;
Expand All @@ -453,6 +460,7 @@ describe('StreamingEngine', () => {
onManifestUpdate: Util.spyFunc(onManifestUpdate),
onSegmentAppended: Util.spyFunc(onSegmentAppended),
onInitSegmentAppended: () => {},
beforeAppendSegment: Util.spyFunc(beforeAppendSegment),
};
streamingEngine = new shaka.media.StreamingEngine(
/** @type {shaka.extern.Manifest} */(manifest), playerInterface);
Expand Down Expand Up @@ -3546,6 +3554,27 @@ describe('StreamingEngine', () => {
});
});

describe('beforeAppendSegment', () => {
it('is called before appending media segment', async () => {
setupVod();
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
createStreamingEngine();
beforeAppendSegment.and.callFake((segment) => {
return shaka.test.Util.shortDelay();
});
streamingEngine.switchVariant(variant);
streamingEngine.switchTextStream(textStream);
await streamingEngine.start();
// Simulate time passing.
playing = true;
await Util.fakeEventLoop(10);
expect(beforeAppendSegment).toHaveBeenCalledWith(
ContentType.AUDIO, segmentData[ContentType.AUDIO].initSegments[0]);
expect(beforeAppendSegment).toHaveBeenCalledWith(
ContentType.AUDIO, segmentData[ContentType.AUDIO].segments[0]);
});
});

/**
* Slides the segment availability window forward by 1 second.
*/
Expand Down

0 comments on commit 01e04d4

Please sign in to comment.