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 SAMPLE-AES MPEG-TS streaming #3388

Merged
merged 2 commits into from
Jan 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 23 additions & 5 deletions src/crypt/decrypter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,21 @@ export default class Decrypter {
this.subtle =
browserCrypto.subtle ||
((browserCrypto as any).webkitSubtle as SubtleCrypto);
} else {
this.config.enableSoftwareAES = true;
}
} catch (e) {
/* no-op */
}
}
if (this.subtle === null) {
this.config.enableSoftwareAES = true;
}
}

isSync() {
public isSync() {
return this.config.enableSoftwareAES;
}

flush(): Uint8Array | void {
public flush(): Uint8Array | void {
const { currentResult } = this;
if (!currentResult) {
this.reset();
Expand All @@ -65,7 +66,7 @@ export default class Decrypter {
return data;
}

reset() {
public reset() {
this.currentResult = null;
this.currentIV = null;
this.remainderData = null;
Expand All @@ -74,6 +75,23 @@ export default class Decrypter {
}
}

public decrypt(
data: Uint8Array | ArrayBuffer,
key: ArrayBuffer,
iv: ArrayBuffer,
callback: (decryptedData: ArrayBuffer) => void
) {
if (this.config.enableSoftwareAES) {
this.softwareDecrypt(new Uint8Array(data), key, iv);
const decryptResult = this.flush();
if (decryptResult) {
callback(decryptResult.buffer);
}
} else {
this.webCryptoDecrypt(new Uint8Array(data), key, iv).then(callback);
}
}

public softwareDecrypt(
data: Uint8Array,
key: ArrayBuffer,
Expand Down
52 changes: 25 additions & 27 deletions src/demux/sample-aes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,24 @@ import type {
DemuxedVideoTrack,
KeyData,
} from '../types/demuxer';
import { discardEPB } from './tsdemuxer';

class SampleAesDecrypter {
private keyData: KeyData;
private discardEPB: (data: Uint8Array) => Uint8Array;
private decrypter: Decrypter;

constructor(
observer: HlsEventEmitter,
config: HlsConfig,
keyData: KeyData,
discardEPB: (data: Uint8Array) => Uint8Array
) {
constructor(observer: HlsEventEmitter, config: HlsConfig, keyData: KeyData) {
this.keyData = keyData;
this.discardEPB = discardEPB;
this.decrypter = new Decrypter(observer, config, {
removePKCS7Padding: false,
});
}

// TODO: Fix callback return type.
decryptBuffer(
encryptedData: Uint8Array | ArrayBufferLike,
callback: (decryptedData: any) => void
encryptedData: Uint8Array | ArrayBuffer,
callback: (decryptedData: ArrayBuffer) => void
) {
// TODO: `this.decrypter` is an instance of `Decrypter`, which has no function named decrypt!?
(this.decrypter as any).decrypt(
this.decrypter.decrypt(
encryptedData,
this.keyData.key.buffer,
this.keyData.iv.buffer,
Expand All @@ -46,7 +38,7 @@ class SampleAesDecrypter {
}

// AAC - encrypt all full 16 bytes blocks starting from offset 16
decryptAacSample(
private decryptAacSample(
samples: AudioSample[],
sampleIndex: number,
callback: () => void,
Expand All @@ -63,8 +55,8 @@ class SampleAesDecrypter {
);

const localthis = this;
this.decryptBuffer(encryptedBuffer, function (decryptedData) {
decryptedData = new Uint8Array(decryptedData);
this.decryptBuffer(encryptedBuffer, (decryptedBuffer: ArrayBuffer) => {
const decryptedData = new Uint8Array(decryptedBuffer);
curUnit.set(decryptedData, 16);

if (!sync) {
Expand Down Expand Up @@ -146,22 +138,28 @@ class SampleAesDecrypter {
curUnit: AvcSampleUnit,
sync: boolean
) {
const decodedData = this.discardEPB(curUnit.data);
const decodedData = discardEPB(curUnit.data);
const encryptedData = this.getAvcEncryptedData(decodedData);
const localthis = this;

this.decryptBuffer(encryptedData.buffer, function (decryptedData) {
curUnit.data = localthis.getAvcDecryptedUnit(decodedData, decryptedData);

if (!sync) {
localthis.decryptAvcSamples(
samples,
sampleIndex,
unitIndex + 1,
callback
this.decryptBuffer(
encryptedData.buffer,
function (decryptedBuffer: ArrayBuffer) {
curUnit.data = localthis.getAvcDecryptedUnit(
decodedData,
decryptedBuffer
);

if (!sync) {
localthis.decryptAvcSamples(
samples,
sampleIndex,
unitIndex + 1,
callback
);
}
}
});
);
}

decryptAvcSamples(
Expand Down
58 changes: 33 additions & 25 deletions src/demux/transmuxer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default class Transmuxer {
private vendor: any;
private demuxer?: Demuxer;
private remuxer?: Remuxer;
private decrypter: any;
private decrypter?: Decrypter;
private probe!: Function;
private decryptionPromise: Promise<TransmuxerResult> | null = null;
private transmuxConfig!: TransmuxConfig;
Expand Down Expand Up @@ -100,7 +100,7 @@ export default class Transmuxer {
if (config.enableSoftwareAES) {
// Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached
// data is handled in the flush() call
const decryptedData: ArrayBuffer = decrypter.softwareDecrypt(
const decryptedData = decrypter.softwareDecrypt(
uintData,
keyData.key.buffer,
keyData.iv.buffer
Expand Down Expand Up @@ -200,14 +200,7 @@ export default class Transmuxer {
const stats = chunkMeta.transmuxing;
stats.executeStart = now();

const {
decrypter,
cache,
currentTransmuxState,
decryptionPromise,
observer,
} = this;
const transmuxResults: Array<TransmuxerResult> = [];
const { decrypter, cache, currentTransmuxState, decryptionPromise } = this;

if (decryptionPromise) {
// Upon resolution, the decryption promise calls push() and returns its TransmuxerResult up the stack. Therefore
Expand All @@ -217,7 +210,8 @@ export default class Transmuxer {
});
}

const { accurateTimeOffset, timeOffset } = currentTransmuxState;
const transmuxResults: Array<TransmuxerResult> = [];
const { timeOffset } = currentTransmuxState;
if (decrypter) {
// The decrypter may have data cached, which needs to be demuxed. In this case we'll have two TransmuxResults
// This happens in the case that we receive only 1 push call for a segment (either for non-progressive downloads,
Expand All @@ -237,7 +231,7 @@ export default class Transmuxer {
if (!demuxer || !remuxer) {
// If probing failed, and each demuxer saw enough bytes to be able to probe, then Hls.js has been given content its not able to handle
if (bytesSeen >= minProbeByteLength) {
observer.emit(Events.ERROR, Events.ERROR, {
this.observer.emit(Events.ERROR, Events.ERROR, {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.FRAG_PARSING_ERROR,
fatal: true,
Expand All @@ -248,15 +242,28 @@ export default class Transmuxer {
return [emptyResult(chunkMeta)];
}

const { audioTrack, avcTrack, id3Track, textTrack } = demuxer.flush(
timeOffset
);
const demuxResultOrPromise = demuxer.flush(timeOffset);
if (isPromise(demuxResultOrPromise)) {
// Decrypt final SAMPLE-AES samples
return demuxResultOrPromise.then((demuxResult) => {
this.flushRemux(transmuxResults, demuxResult, chunkMeta);
return transmuxResults;
});
}

this.flushRemux(transmuxResults, demuxResultOrPromise, chunkMeta);
return transmuxResults;
}

private flushRemux(transmuxResults, demuxResult, chunkMeta) {
const { audioTrack, avcTrack, id3Track, textTrack } = demuxResult;
const { accurateTimeOffset, timeOffset } = this.currentTransmuxState;
logger.log(
`[transmuxer.ts]: Flushed fragment ${chunkMeta.sn}${
chunkMeta.part > -1 ? ' p: ' + chunkMeta.part : ''
} of level ${chunkMeta.level}`
);
const remuxResult = remuxer.remux(
const remuxResult = this.remuxer!.remux(
audioTrack,
avcTrack,
id3Track,
Expand All @@ -270,8 +277,7 @@ export default class Transmuxer {
chunkMeta,
});

stats.executeEnd = now();
return transmuxResults;
chunkMeta.transmuxing.executeEnd = now();
}

resetInitialTimestamp(defaultInitPts: number | undefined) {
Expand Down Expand Up @@ -367,7 +373,6 @@ export default class Transmuxer {
};
}

// TODO: Handle flush with Sample-AES
private transmuxSampleAes(
data: Uint8Array,
decryptData: KeyData,
Expand All @@ -377,18 +382,21 @@ export default class Transmuxer {
): Promise<TransmuxerResult> {
return (this.demuxer as Demuxer)
.demuxSampleAes(data, decryptData, timeOffset)
.then((demuxResult) => ({
remuxResult: this.remuxer!.remux(
.then((demuxResult) => {
const remuxResult = this.remuxer!.remux(
demuxResult.audioTrack,
demuxResult.avcTrack,
demuxResult.id3Track,
demuxResult.textTrack,
timeOffset,
accurateTimeOffset,
false
),
chunkMeta,
}));
);
return {
remuxResult,
chunkMeta,
};
});
}

private configureTransmuxer(
Expand Down Expand Up @@ -447,7 +455,7 @@ export default class Transmuxer {
return !this.demuxer || discontinuity || trackSwitch;
}

private getDecrypter() {
private getDecrypter(): Decrypter {
let decrypter = this.decrypter;
if (!decrypter) {
decrypter = this.decrypter = new Decrypter(this.observer, this.config);
Expand Down