Skip to content

Commit

Permalink
Fix SAMPLE-AES streaming
Browse files Browse the repository at this point in the history
Follow up from #3384
Fixes a regression introduced in 6836ef3
  • Loading branch information
Rob Walch committed Jan 18, 2021
1 parent c3918e2 commit ce542fd
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 71 deletions.
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
40 changes: 22 additions & 18 deletions src/demux/sample-aes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,11 @@ class SampleAesDecrypter {
});
}

// 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 +44,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 +61,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 @@ -150,18 +148,24 @@ class SampleAesDecrypter {
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
39 changes: 17 additions & 22 deletions src/demux/tsdemuxer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ class TSDemuxer implements Demuxer {
private readonly config: HlsConfig;
private typeSupported: TypeSupported;

private sampleAes: any = null;
private sampleAes: SampleAesDecrypter | null = null;
private pmtParsed: boolean = false;
private contiguous: boolean = false;
private audioCodec!: string;
Expand Down Expand Up @@ -411,10 +411,10 @@ class TSDemuxer implements Demuxer {
};
}

flush() {
flush(): DemuxerResult | Promise<DemuxerResult> {
const { remainderData } = this;
this.remainderData = null;
let result;
let result: DemuxerResult;
if (remainderData) {
result = this.demux(remainderData, -1, false, true);
} else {
Expand All @@ -426,6 +426,9 @@ class TSDemuxer implements Demuxer {
};
}
this.extractRemainingSamples(result);
if (this.sampleAes) {
return this.decrypt(result, this.sampleAes);
}
return result;
}

Expand Down Expand Up @@ -484,36 +487,28 @@ class TSDemuxer implements Demuxer {
keyData,
this.discardEPB
));
return new Promise((resolve) => {
this.decrypt(
demuxResult.audioTrack,
demuxResult.avcTrack,
sampleAes
).then(() => {
resolve(demuxResult);
});
});
return this.decrypt(demuxResult, sampleAes);
}

decrypt(
audioTrack: DemuxedAudioTrack,
videoTrack: DemuxedVideoTrack,
demuxResult: DemuxerResult,
sampleAes: SampleAesDecrypter
): Promise<void> {
): Promise<DemuxerResult> {
return new Promise((resolve) => {
const { audioTrack, avcTrack } = demuxResult;
if (audioTrack.samples && audioTrack.isAAC) {
sampleAes.decryptAacSamples(audioTrack.samples, 0, () => {
if (videoTrack.samples) {
sampleAes.decryptAvcSamples(videoTrack.samples, 0, 0, () => {
resolve();
if (avcTrack.samples) {
sampleAes.decryptAvcSamples(avcTrack.samples, 0, 0, () => {
resolve(demuxResult);
});
} else {
resolve();
resolve(demuxResult);
}
});
} else if (videoTrack.samples) {
sampleAes.decryptAvcSamples(videoTrack.samples, 0, 0, () => {
resolve();
} else if (avcTrack.samples) {
sampleAes.decryptAvcSamples(avcTrack.samples, 0, 0, () => {
resolve(demuxResult);
});
}
});
Expand Down
2 changes: 1 addition & 1 deletion src/types/demuxer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface Demuxer {
keyData: KeyData,
timeOffset: number
): Promise<DemuxerResult>;
flush(timeOffset?: number): DemuxerResult;
flush(timeOffset?: number): DemuxerResult | Promise<DemuxerResult>;
destroy(): void;
resetInitSegment(
audioCodec: string | undefined,
Expand Down

0 comments on commit ce542fd

Please sign in to comment.