Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion demo/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,37 @@ if (demoConfig) {

const hlsjsDefaults = {
debug: true,
enableWorker: true,
enableWorker: false,
lowLatencyMode: true,
backBufferLength: 60 * 1.5,
emeEnabled: true,
drmSystems: {
"com.microsoft.playready": {
licenseUrl: "https://shield-drm.imggaming.com/api/v2/license"
}
},
drmSystemOptions: {
"videoRobustness": "3000",
"audioRobustness": "3000"
},
licenseXhrSetup: async function(xhr) {
const res = await fetch("https://shield-api.imggaming.com/admin/v1/ovp/dice/client/dce.sandbox/action/sign_test_content_token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ttl_seconds: 300,
claims: {
eid: "786df1e4-9a75-4052-8625-204ba23b2bae",
aid: "00000000-0000-0000-0000-000000000000",
did: "00000000-0000-0000-0000-000000000000",
def: "uhd2"
}
})
});
const { token } = await res.json();
xhr.setRequestHeader("Authorization", "Bearer " + token);
xhr.setRequestHeader("X-DRM-INFO", btoa(JSON.stringify({ system: "com.microsoft.playready" })));
}
};

let enableStreaming = getDemoConfigPropOrDefault('enableStreaming', true);
Expand Down
3 changes: 3 additions & 0 deletions src/controller/base-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,7 @@
return data;
})
.then((data: FragLoadedData) => {
console.log('$$$ _loadInitSegment - loaded init segment, checking if decryption is needed', data);

Check warning on line 649 in src/controller/base-stream-controller.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
const { hls } = this;
const { frag, payload } = data;
const decryptData = frag.decryptdata;
Expand Down Expand Up @@ -689,9 +690,11 @@
},
});
data.payload = decryptedData;
console.log('$$$ _loadInitSegment - calling this.completeInitSegmentLoad with decrypted data', data);

Check warning on line 693 in src/controller/base-stream-controller.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
return this.completeInitSegmentLoad(data);
});
}
console.log('$$$ _loadInitSegment - calling this.completeInitSegmentLoad with non-decrypted data', data);

Check warning on line 697 in src/controller/base-stream-controller.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
return this.completeInitSegmentLoad(data);
})
.catch((reason) => {
Expand Down
36 changes: 30 additions & 6 deletions src/controller/eme-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,12 @@ class EMEController extends Logger implements ComponentAPI {

private getLicenseServerUrl(keySystem: KeySystems): string | undefined {
const { drmSystems, widevineLicenseUrl } = this.config;
const keySystemConfiguration = drmSystems?.[keySystem];
// const keySystemConfiguration = drmSystems?.[keySystem];
const keySystemConfiguration =
drmSystems?.[keySystem] ??
(keySystem === KeySystems.PLAYREADY_RECOMMENDATION
? drmSystems?.[KeySystems.PLAYREADY]
: undefined);

if (keySystemConfiguration) {
return keySystemConfiguration.licenseUrl;
Expand All @@ -162,8 +167,12 @@ class EMEController extends Logger implements ComponentAPI {

private getServerCertificateUrl(keySystem: KeySystems): string | void {
const { drmSystems } = this.config;
const keySystemConfiguration = drmSystems?.[keySystem];

// const keySystemConfiguration = drmSystems?.[keySystem];
const keySystemConfiguration =
drmSystems?.[keySystem] ??
(keySystem === KeySystems.PLAYREADY_RECOMMENDATION
? drmSystems?.[KeySystems.PLAYREADY]
: undefined);
if (keySystemConfiguration) {
return keySystemConfiguration.serverCertificateUrl;
} else {
Expand Down Expand Up @@ -439,7 +448,13 @@ class EMEController extends Logger implements ComponentAPI {
.filter(
(value) => !!value && keySystemsInConfig.indexOf(value) !== -1,
) as any as KeySystems[];

// Chrome on Windows registers PlayReady as the recommendation variant.
// When the standard key system is in the attempt list, add the recommendation
// variant as an immediate fallback so attemptKeySystemAccess tries it next.
// if (keySystemsToAttempt.indexOf(KeySystems.PLAYREADY) !== -1) {
// const idx = keySystemsToAttempt.indexOf(KeySystems.PLAYREADY);
// keySystemsToAttempt.splice(idx + 1, 0, KeySystems.PLAYREADY_RECOMMENDATION);
// }
return this.selectKeySystem(keySystemsToAttempt);
}

Expand Down Expand Up @@ -570,7 +585,7 @@ class EMEController extends Logger implements ComponentAPI {
const keySystemsToAttempt = keySystem
? [keySystem]
: getKeySystemsForConfig(this.config);
return this.attemptKeySystemAccess(keySystemsToAttempt);
return this.getKeySystemSelectionPromise(keySystemsToAttempt);
}
return mediaKeySessionContext;
}
Expand All @@ -593,6 +608,15 @@ class EMEController extends Logger implements ComponentAPI {
})}`,
);
}
// Add recommendation variant as fallback if base PlayReady is in the list
const playreadyIdx = keySystemsToAttempt.indexOf(KeySystems.PLAYREADY);
if (
playreadyIdx !== -1 &&
keySystemsToAttempt.indexOf(KeySystems.PLAYREADY_RECOMMENDATION) === -1
) {
keySystemsToAttempt = keySystemsToAttempt.slice();
keySystemsToAttempt.splice(playreadyIdx + 1, 0, KeySystems.PLAYREADY_RECOMMENDATION);
}
return this.attemptKeySystemAccess(keySystemsToAttempt);
}

Expand Down Expand Up @@ -1332,7 +1356,7 @@ class EMEController extends Logger implements ComponentAPI {

this.setupLicenseXHR(xhr, url, keySessionContext, licenseChallenge)
.then(({ xhr, licenseChallenge }) => {
if (keySessionContext.keySystem == KeySystems.PLAYREADY) {
if (keySessionContext.keySystem == KeySystems.PLAYREADY || keySessionContext.keySystem == KeySystems.PLAYREADY_RECOMMENDATION) {
licenseChallenge = this.unpackPlayReadyKeyMessage(
xhr,
licenseChallenge,
Expand Down
28 changes: 27 additions & 1 deletion src/demux/transmuxer-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,34 @@
chunkMeta.transmuxing.start = self.performance.now();
const { instanceNo, transmuxer } = this;
const timeOffset = part ? part.start : frag.start;
console.log('$$$ TransmuxerInterface push', {

Check warning on line 201 in src/demux/transmuxer-interface.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
id: this.id,
sn: chunkMeta.sn,
part: chunkMeta.part,
level: chunkMeta.level,
timeOffset,
accurateTimeOffset,
});
// TODO: push "clear-lead" decrypt data for unencrypted fragments in streams with encrypted ones
const decryptdata = frag.decryptdata;
// const decryptdata = frag.decryptdata;

// For clear-lead segments (frag.decryptdata is null), still pass the PlayReady
// key from the first encrypted fragment so generateInitSegment can call
// fakeEncryption and mark the SourceBuffer pipeline as encrypted from the start.
// PlayReady LevelKey.key is null so transmuxer.push won't attempt AES decryption
// on the clear segment data — only the init segment processing is affected.
let decryptdata = frag.decryptdata;
if (decryptdata == null) {
const levelDetails = this.hls.levels[frag.level]?.details;
const encryptedFrag = levelDetails?.encryptedFragments?.[0];
if (encryptedFrag?.levelkeys) {
decryptdata =
Object.values(encryptedFrag.levelkeys).find(
(k) => k?.isCommonEncryption,
) ?? null;
}
}

const lastFrag = this.frag;

const discontinuity = !(lastFrag && frag.cc === lastFrag.cc);
Expand Down
4 changes: 4 additions & 0 deletions src/loader/fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,10 @@ export class Fragment extends BaseSegment {
return this._decryptdata;
}

set decryptdata(value: LevelKey | null) {
this._decryptdata = value;
}

get end(): number {
return this.start + this.duration;
}
Expand Down
1 change: 1 addition & 0 deletions src/remux/mp4-remuxer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
}

resetInitSegment() {
console.log('$$$ MP4Remuxer - resetInitSegment called with audioCodec');

Check warning on line 136 in src/remux/mp4-remuxer.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
this.log('ISGenerated flag reset');
this.ISGenerated = false;
this.videoTrackConfig = undefined;
Expand Down
36 changes: 31 additions & 5 deletions src/remux/passthrough-remuxer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import { ElementaryStreamTypes } from '../loader/fragment';
import { getCodecCompatibleName } from '../utils/codecs';
import { type ILogger, Logger } from '../utils/logger';
import { patchEncyptionData } from '../utils/mp4-tools';
import { getSampleData, parseInitSegment } from '../utils/mp4-tools';
import { fakeEncryption, patchEncyptionData } from '../utils/mp4-tools';
import { findBox, getSampleData, parseInitSegment, patchTencIsProtected } from '../utils/mp4-tools';
import type { HlsConfig } from '../config';
import type { HlsEventEmitter } from '../events';
import type { DecryptData } from '../loader/level-key';
Expand All @@ -29,6 +29,7 @@

class PassThroughRemuxer extends Logger implements Remuxer {
private emitInitSegment: boolean = false;
private encryptedInitPatched = false;
private audioCodec?: string;
private videoCodec?: string;
private initData?: InitData;
Expand Down Expand Up @@ -73,10 +74,13 @@
videoCodec: string | undefined,
decryptdata: DecryptData | null,
) {
console.log('$$$ PassThroughRemuxer - resetInitSegment called with audioCodec', audioCodec, 'videoCodec', videoCodec, 'decryptdata', JSON.stringify(decryptdata));

Check warning on line 77 in src/remux/passthrough-remuxer.ts

View workflow job for this annotation

GitHub Actions / build

Unexpected console statement
this.audioCodec = audioCodec;
this.videoCodec = videoCodec;
// if (decryptdata) decryptdata.keyFormat = "com.microsoft.playready.recommendation";
this.generateInitSegment(initSegment, decryptdata);
this.emitInitSegment = true;
this.encryptedInitPatched = false;
}

private generateInitSegment(
Expand All @@ -89,11 +93,14 @@
this.initData = undefined;
return;
}
const { audio, video } = (this.initData = parseInitSegment(initSegment));

if (decryptdata) {
patchEncyptionData(initSegment, decryptdata);
const { audio, video } = parseInitSegment(initSegment);
if (!audio?.encrypted && !video?.encrypted) {
initSegment = fakeEncryption(initSegment);
}
initSegment = patchEncyptionData(initSegment, decryptdata) ?? initSegment;
} else {
const { audio, video } = parseInitSegment(initSegment);
const eitherTrack = audio || video;
if (eitherTrack?.encrypted) {
this.warn(
Expand All @@ -102,6 +109,8 @@
}
}

const { audio, video } = (this.initData = parseInitSegment(initSegment));

// Get codec from initSegment
if (audio) {
audioCodec = getParsedTrackCodec(
Expand Down Expand Up @@ -179,6 +188,23 @@
// The binary segment data is added to the videoTrack in the mp4demuxer. We don't check to see if the data is only
// audio or video (or both); adding it to video was an arbitrary choice.
const data = videoTrack.samples;

if (!this.encryptedInitPatched && findBox(data, ['moof', 'traf', 'senc']).length > 0) {
this.encryptedInitPatched = true;
if (this.initTracks) {
const seen = new Set<Uint8Array>();
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
for (const track of Object.values(this.initTracks)) {
if (track?.initSegment && !seen.has(track.initSegment)) {
seen.add(track.initSegment);
patchTencIsProtected(track.initSegment as Uint8Array<ArrayBuffer>, true);
}
}
this.emitInitSegment = true;
}
}


if (!data.length) {
return result;
}
Expand Down
9 changes: 6 additions & 3 deletions src/utils/mediakeys-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const enum KeySystems {
CLEARKEY = 'org.w3.clearkey',
FAIRPLAY = 'com.apple.fps',
PLAYREADY = 'com.microsoft.playready',
PLAYREADY_RECOMMENDATION = 'com.microsoft.playready.recommendation',
WIDEVINE = 'com.widevine.alpha',
}

Expand Down Expand Up @@ -67,7 +68,8 @@ export function keySystemDomainToKeySystemFormat(
case KeySystems.FAIRPLAY:
return KeySystemFormats.FAIRPLAY;
case KeySystems.PLAYREADY:
return KeySystemFormats.PLAYREADY;
case KeySystems.PLAYREADY_RECOMMENDATION:
return KeySystemFormats.PLAYREADY;
case KeySystems.WIDEVINE:
return KeySystemFormats.WIDEVINE;
case KeySystems.CLEARKEY:
Expand Down Expand Up @@ -122,6 +124,7 @@ export function getSupportedMediaKeySystemConfigurations(
break;
case KeySystems.WIDEVINE:
case KeySystems.PLAYREADY:
case KeySystems.PLAYREADY_RECOMMENDATION:
initDataTypes = ['cenc'];
break;
case KeySystems.CLEARKEY:
Expand Down Expand Up @@ -152,12 +155,12 @@ function createMediaKeySystemConfigurations(
drmSystemOptions.sessionType || 'temporary',
],
audioCapabilities: audioCodecs.map((codec) => ({
contentType: `audio/mp4; codecs=${codec}`,
contentType: `audio/mp4; codecs="${codec}"`,
robustness: drmSystemOptions.audioRobustness || '',
encryptionScheme: drmSystemOptions.audioEncryptionScheme || null,
})),
videoCapabilities: videoCodecs.map((codec) => ({
contentType: `video/mp4; codecs=${codec}`,
contentType: `video/mp4; codecs="${codec}"`,
robustness: drmSystemOptions.videoRobustness || '',
encryptionScheme: drmSystemOptions.videoEncryptionScheme || null,
})),
Expand Down
Loading
Loading