Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix in-band key rotation on Xbox One #4478

Merged
merged 7 commits into from
Oct 4, 2022
Merged
1 change: 1 addition & 0 deletions demo/common/message_ids.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,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 @@ -132,7 +132,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 @@ -169,6 +169,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 @@ -703,6 +703,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
8 changes: 7 additions & 1 deletion externs/shaka/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,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 @@ -693,6 +694,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 true on Xbox One, and false for all other browsers.</i><br>
* When true parse DRM init data from pssh boxes in media segments and ignore
joeyparrish marked this conversation as resolved.
Show resolved Hide resolved
* 'encrypted' events.
* This is required when using in-band key rotation on Xbox One.
*
joeyparrish marked this conversation as resolved.
Show resolved Hide resolved
* @exportDoc
*/
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 @@ -1639,6 +1639,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 @@ -1712,6 +1714,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 @@ -2166,7 +2169,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 @@ -2196,6 +2201,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 @@ -2977,6 +2977,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
4 changes: 4 additions & 0 deletions lib/util/player_configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ shaka.util.PlayerConfiguration = class {
updateExpirationTime: 1,
preferredKeySystems: [],
keySystemsMapping: {},
// The Xbox One browser does not detect DRM key changes signalled by a
// change in the PSSH in media segments. We need to parse PSSH from media
// segments to detect key changes.
parseInbandPsshEnabled: shaka.util.Platform.isXboxOne(),
};

const manifest = {
Expand Down
3 changes: 2 additions & 1 deletion lib/util/pssh.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ 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);

if (this.data.length == 0) {
shaka.log.warning('No pssh box found!');
shaka.log.v2('No pssh box found!');
}
}

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 @@ -250,6 +250,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 @@ -3639,6 +3647,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