transmuxer.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. import AACDemuxer from './audio/aacdemuxer';
  2. import { AC3Demuxer } from './audio/ac3-demuxer';
  3. import MP3Demuxer from './audio/mp3demuxer';
  4. import Decrypter from '../crypt/decrypter';
  5. import MP4Demuxer from '../demux/mp4demuxer';
  6. import TSDemuxer from '../demux/tsdemuxer';
  7. import { ErrorDetails, ErrorTypes } from '../errors';
  8. import { Events } from '../events';
  9. import MP4Remuxer from '../remux/mp4-remuxer';
  10. import PassThroughRemuxer from '../remux/passthrough-remuxer';
  11. import { PlaylistLevelType } from '../types/loader';
  12. import {
  13. getAesModeFromFullSegmentMethod,
  14. isFullSegmentEncryption,
  15. } from '../utils/encryption-methods-util';
  16. import type { HlsConfig } from '../config';
  17. import type { HlsEventEmitter } from '../events';
  18. import type { DecryptData } from '../loader/level-key';
  19. import type { Demuxer, DemuxerResult, KeyData } from '../types/demuxer';
  20. import type { Remuxer } from '../types/remuxer';
  21. import type { ChunkMetadata, TransmuxerResult } from '../types/transmuxer';
  22. import type { TypeSupported } from '../utils/codecs';
  23. import type { ILogger } from '../utils/logger';
  24. import type { RationalTimestamp } from '../utils/timescale-conversion';
  25. let now: () => number;
  26. // performance.now() not available on WebWorker, at least on Safari Desktop
  27. try {
  28. now = self.performance.now.bind(self.performance);
  29. } catch (err) {
  30. now = Date.now;
  31. }
  32. type MuxConfig =
  33. | { demux: typeof MP4Demuxer; remux: typeof PassThroughRemuxer }
  34. | { demux: typeof TSDemuxer; remux: typeof MP4Remuxer }
  35. | { demux: typeof AC3Demuxer; remux: typeof MP4Remuxer }
  36. | { demux: typeof AACDemuxer; remux: typeof MP4Remuxer }
  37. | { demux: typeof MP3Demuxer; remux: typeof MP4Remuxer };
  38. const muxConfig: MuxConfig[] = [
  39. { demux: MP4Demuxer, remux: PassThroughRemuxer },
  40. { demux: TSDemuxer, remux: MP4Remuxer },
  41. { demux: AACDemuxer, remux: MP4Remuxer },
  42. { demux: MP3Demuxer, remux: MP4Remuxer },
  43. ];
  44. if (__USE_M2TS_ADVANCED_CODECS__) {
  45. muxConfig.splice(2, 0, { demux: AC3Demuxer, remux: MP4Remuxer });
  46. }
  47. export default class Transmuxer {
  48. private asyncResult: boolean = false;
  49. private logger: ILogger;
  50. private observer: HlsEventEmitter;
  51. private typeSupported: TypeSupported;
  52. private config: HlsConfig;
  53. private id: PlaylistLevelType;
  54. private demuxer?: Demuxer;
  55. private remuxer?: Remuxer;
  56. private decrypter?: Decrypter;
  57. private probe!: Function;
  58. private decryptionPromise: Promise<TransmuxerResult> | null = null;
  59. private transmuxConfig!: TransmuxConfig;
  60. private currentTransmuxState!: TransmuxState;
  61. constructor(
  62. observer: HlsEventEmitter,
  63. typeSupported: TypeSupported,
  64. config: HlsConfig,
  65. vendor: string,
  66. id: PlaylistLevelType,
  67. logger: ILogger,
  68. ) {
  69. this.observer = observer;
  70. this.typeSupported = typeSupported;
  71. this.config = config;
  72. this.id = id;
  73. this.logger = logger;
  74. }
  75. configure(transmuxConfig: TransmuxConfig) {
  76. this.transmuxConfig = transmuxConfig;
  77. if (this.decrypter) {
  78. this.decrypter.reset();
  79. }
  80. }
  81. push(
  82. data: ArrayBuffer,
  83. decryptdata: DecryptData | null,
  84. chunkMeta: ChunkMetadata,
  85. state?: TransmuxState,
  86. ): TransmuxerResult | Promise<TransmuxerResult> {
  87. const stats = chunkMeta.transmuxing;
  88. stats.executeStart = now();
  89. let uintData: Uint8Array<ArrayBuffer> = new Uint8Array(data);
  90. const { currentTransmuxState, transmuxConfig } = this;
  91. if (state) {
  92. this.currentTransmuxState = state;
  93. }
  94. const {
  95. contiguous,
  96. discontinuity,
  97. trackSwitch,
  98. accurateTimeOffset,
  99. timeOffset,
  100. initSegmentChange,
  101. } = state || currentTransmuxState;
  102. const {
  103. audioCodec,
  104. videoCodec,
  105. defaultInitPts,
  106. duration,
  107. initSegmentData,
  108. } = transmuxConfig;
  109. const keyData = getEncryptionType(uintData, decryptdata);
  110. if (keyData && isFullSegmentEncryption(keyData.method)) {
  111. const decrypter = this.getDecrypter();
  112. const aesMode = getAesModeFromFullSegmentMethod(keyData.method);
  113. // Software decryption is synchronous; webCrypto is not
  114. if (decrypter.isSync()) {
  115. // Software decryption is progressive. Progressive decryption may not return a result on each call. Any cached
  116. // data is handled in the flush() call
  117. let decryptedData = decrypter.softwareDecrypt(
  118. uintData,
  119. keyData.key.buffer,
  120. keyData.iv.buffer,
  121. aesMode,
  122. );
  123. // For Low-Latency HLS Parts, decrypt in place, since part parsing is expected on push progress
  124. const loadingParts = chunkMeta.part > -1;
  125. if (loadingParts) {
  126. const data = decrypter.flush();
  127. decryptedData = data ? data.buffer : data;
  128. }
  129. if (!decryptedData) {
  130. stats.executeEnd = now();
  131. return emptyResult(chunkMeta);
  132. }
  133. uintData = new Uint8Array(decryptedData);
  134. } else {
  135. this.asyncResult = true;
  136. this.decryptionPromise = decrypter
  137. .webCryptoDecrypt(
  138. uintData,
  139. keyData.key.buffer,
  140. keyData.iv.buffer,
  141. aesMode,
  142. )
  143. .then((decryptedData): TransmuxerResult => {
  144. // Calling push here is important; if flush() is called while this is still resolving, this ensures that
  145. // the decrypted data has been transmuxed
  146. const result = this.push(
  147. decryptedData,
  148. null,
  149. chunkMeta,
  150. ) as TransmuxerResult;
  151. this.decryptionPromise = null;
  152. return result;
  153. });
  154. return this.decryptionPromise;
  155. }
  156. }
  157. const resetMuxers = this.needsProbing(discontinuity, trackSwitch);
  158. if (resetMuxers) {
  159. const error = this.configureTransmuxer(uintData);
  160. if (error) {
  161. this.logger.warn(`[transmuxer] ${error.message}`);
  162. this.observer.emit(Events.ERROR, Events.ERROR, {
  163. type: ErrorTypes.MEDIA_ERROR,
  164. details: ErrorDetails.FRAG_PARSING_ERROR,
  165. fatal: false,
  166. error,
  167. reason: error.message,
  168. });
  169. stats.executeEnd = now();
  170. return emptyResult(chunkMeta);
  171. }
  172. }
  173. if (discontinuity || trackSwitch || initSegmentChange || resetMuxers) {
  174. this.resetInitSegment(
  175. initSegmentData,
  176. audioCodec,
  177. videoCodec,
  178. duration,
  179. decryptdata,
  180. );
  181. }
  182. if (discontinuity || initSegmentChange || resetMuxers) {
  183. this.resetInitialTimestamp(defaultInitPts);
  184. }
  185. if (!contiguous) {
  186. this.resetContiguity();
  187. }
  188. const result = this.transmux(
  189. uintData,
  190. keyData,
  191. timeOffset,
  192. accurateTimeOffset,
  193. chunkMeta,
  194. );
  195. this.asyncResult = isPromise(result);
  196. const currentState = this.currentTransmuxState;
  197. currentState.contiguous = true;
  198. currentState.discontinuity = false;
  199. currentState.trackSwitch = false;
  200. stats.executeEnd = now();
  201. return result;
  202. }
  203. // Due to data caching, flush calls can produce more than one TransmuxerResult (hence the Array type)
  204. flush(
  205. chunkMeta: ChunkMetadata,
  206. ): TransmuxerResult[] | Promise<TransmuxerResult[]> {
  207. const stats = chunkMeta.transmuxing;
  208. stats.executeStart = now();
  209. const { decrypter, currentTransmuxState, decryptionPromise } = this;
  210. if (decryptionPromise) {
  211. this.asyncResult = true;
  212. // Upon resolution, the decryption promise calls push() and returns its TransmuxerResult up the stack. Therefore
  213. // only flushing is required for async decryption
  214. return decryptionPromise.then(() => {
  215. return this.flush(chunkMeta);
  216. });
  217. }
  218. const transmuxResults: TransmuxerResult[] = [];
  219. const { timeOffset } = currentTransmuxState;
  220. if (decrypter) {
  221. // The decrypter may have data cached, which needs to be demuxed. In this case we'll have two TransmuxResults
  222. // This happens in the case that we receive only 1 push call for a segment (either for non-progressive downloads,
  223. // or for progressive downloads with small segments)
  224. const decryptedData = decrypter.flush();
  225. if (decryptedData) {
  226. // Push always returns a TransmuxerResult if decryptdata is null
  227. transmuxResults.push(
  228. this.push(decryptedData.buffer, null, chunkMeta) as TransmuxerResult,
  229. );
  230. }
  231. }
  232. const { demuxer, remuxer } = this;
  233. if (!demuxer || !remuxer) {
  234. // If probing failed, then Hls.js has been given content its not able to handle
  235. stats.executeEnd = now();
  236. const emptyResults = [emptyResult(chunkMeta)];
  237. if (this.asyncResult) {
  238. return Promise.resolve(emptyResults);
  239. }
  240. return emptyResults;
  241. }
  242. const demuxResultOrPromise = demuxer.flush(timeOffset);
  243. if (isPromise(demuxResultOrPromise)) {
  244. this.asyncResult = true;
  245. // Decrypt final SAMPLE-AES samples
  246. return demuxResultOrPromise.then((demuxResult) => {
  247. this.flushRemux(transmuxResults, demuxResult, chunkMeta);
  248. return transmuxResults;
  249. });
  250. }
  251. this.flushRemux(transmuxResults, demuxResultOrPromise, chunkMeta);
  252. if (this.asyncResult) {
  253. return Promise.resolve(transmuxResults);
  254. }
  255. return transmuxResults;
  256. }
  257. private flushRemux(
  258. transmuxResults: TransmuxerResult[],
  259. demuxResult: DemuxerResult,
  260. chunkMeta: ChunkMetadata,
  261. ) {
  262. const { audioTrack, videoTrack, id3Track, textTrack } = demuxResult;
  263. const { accurateTimeOffset, timeOffset } = this.currentTransmuxState;
  264. this.logger.log(
  265. `[transmuxer.ts]: Flushed ${this.id} sn: ${chunkMeta.sn}${
  266. chunkMeta.part > -1 ? ' part: ' + chunkMeta.part : ''
  267. } of ${this.id === PlaylistLevelType.MAIN ? 'level' : 'track'} ${chunkMeta.level}`,
  268. );
  269. const remuxResult = this.remuxer!.remux(
  270. audioTrack,
  271. videoTrack,
  272. id3Track,
  273. textTrack,
  274. timeOffset,
  275. accurateTimeOffset,
  276. true,
  277. this.id,
  278. );
  279. transmuxResults.push({
  280. remuxResult,
  281. chunkMeta,
  282. });
  283. chunkMeta.transmuxing.executeEnd = now();
  284. }
  285. resetInitialTimestamp(defaultInitPts: RationalTimestamp | null) {
  286. const { demuxer, remuxer } = this;
  287. if (!demuxer || !remuxer) {
  288. return;
  289. }
  290. demuxer.resetTimeStamp(defaultInitPts);
  291. remuxer.resetTimeStamp(defaultInitPts);
  292. }
  293. resetContiguity() {
  294. const { demuxer, remuxer } = this;
  295. if (!demuxer || !remuxer) {
  296. return;
  297. }
  298. demuxer.resetContiguity();
  299. remuxer.resetNextTimestamp();
  300. }
  301. resetInitSegment(
  302. initSegmentData: Uint8Array | undefined,
  303. audioCodec: string | undefined,
  304. videoCodec: string | undefined,
  305. trackDuration: number,
  306. decryptdata: DecryptData | null,
  307. ) {
  308. const { demuxer, remuxer } = this;
  309. if (!demuxer || !remuxer) {
  310. return;
  311. }
  312. demuxer.resetInitSegment(
  313. initSegmentData,
  314. audioCodec,
  315. videoCodec,
  316. trackDuration,
  317. );
  318. remuxer.resetInitSegment(
  319. initSegmentData,
  320. audioCodec,
  321. videoCodec,
  322. decryptdata,
  323. );
  324. }
  325. destroy(): void {
  326. if (this.demuxer) {
  327. this.demuxer.destroy();
  328. this.demuxer = undefined;
  329. }
  330. if (this.remuxer) {
  331. this.remuxer.destroy();
  332. this.remuxer = undefined;
  333. }
  334. }
  335. private transmux(
  336. data: Uint8Array,
  337. keyData: KeyData | null,
  338. timeOffset: number,
  339. accurateTimeOffset: boolean,
  340. chunkMeta: ChunkMetadata,
  341. ): TransmuxerResult | Promise<TransmuxerResult> {
  342. let result: TransmuxerResult | Promise<TransmuxerResult>;
  343. if (keyData && keyData.method === 'SAMPLE-AES') {
  344. result = this.transmuxSampleAes(
  345. data,
  346. keyData,
  347. timeOffset,
  348. accurateTimeOffset,
  349. chunkMeta,
  350. );
  351. } else {
  352. result = this.transmuxUnencrypted(
  353. data,
  354. timeOffset,
  355. accurateTimeOffset,
  356. chunkMeta,
  357. );
  358. }
  359. return result;
  360. }
  361. private transmuxUnencrypted(
  362. data: Uint8Array,
  363. timeOffset: number,
  364. accurateTimeOffset: boolean,
  365. chunkMeta: ChunkMetadata,
  366. ): TransmuxerResult {
  367. const { audioTrack, videoTrack, id3Track, textTrack } = (
  368. this.demuxer as Demuxer
  369. ).demux(data, timeOffset, false, !this.config.progressive);
  370. const remuxResult = this.remuxer!.remux(
  371. audioTrack,
  372. videoTrack,
  373. id3Track,
  374. textTrack,
  375. timeOffset,
  376. accurateTimeOffset,
  377. false,
  378. this.id,
  379. );
  380. return {
  381. remuxResult,
  382. chunkMeta,
  383. };
  384. }
  385. private transmuxSampleAes(
  386. data: Uint8Array,
  387. decryptData: KeyData,
  388. timeOffset: number,
  389. accurateTimeOffset: boolean,
  390. chunkMeta: ChunkMetadata,
  391. ): Promise<TransmuxerResult> {
  392. return (this.demuxer as Demuxer)
  393. .demuxSampleAes(data, decryptData, timeOffset)
  394. .then((demuxResult) => {
  395. const remuxResult = this.remuxer!.remux(
  396. demuxResult.audioTrack,
  397. demuxResult.videoTrack,
  398. demuxResult.id3Track,
  399. demuxResult.textTrack,
  400. timeOffset,
  401. accurateTimeOffset,
  402. false,
  403. this.id,
  404. );
  405. return {
  406. remuxResult,
  407. chunkMeta,
  408. };
  409. });
  410. }
  411. private configureTransmuxer(data: Uint8Array): void | Error {
  412. const { config, observer, typeSupported } = this;
  413. // probe for content type
  414. let mux;
  415. for (let i = 0, len = muxConfig.length; i < len; i++) {
  416. if (muxConfig[i].demux?.probe(data, this.logger)) {
  417. mux = muxConfig[i];
  418. break;
  419. }
  420. }
  421. if (!mux) {
  422. return new Error('Failed to find demuxer by probing fragment data');
  423. }
  424. // so let's check that current remuxer and demuxer are still valid
  425. const demuxer = this.demuxer;
  426. const remuxer = this.remuxer;
  427. const Remuxer: MuxConfig['remux'] = mux.remux;
  428. const Demuxer: MuxConfig['demux'] = mux.demux;
  429. if (!remuxer || !(remuxer instanceof Remuxer)) {
  430. this.remuxer = new Remuxer(observer, config, typeSupported, this.logger);
  431. }
  432. if (!demuxer || !(demuxer instanceof Demuxer)) {
  433. this.demuxer = new Demuxer(observer, config, typeSupported, this.logger);
  434. this.probe = Demuxer.probe;
  435. }
  436. }
  437. private needsProbing(discontinuity: boolean, trackSwitch: boolean): boolean {
  438. // in case of continuity change, or track switch
  439. // we might switch from content type (AAC container to TS container, or TS to fmp4 for example)
  440. return !this.demuxer || !this.remuxer || discontinuity || trackSwitch;
  441. }
  442. private getDecrypter(): Decrypter {
  443. let decrypter = this.decrypter;
  444. if (!decrypter) {
  445. decrypter = this.decrypter = new Decrypter(this.config);
  446. }
  447. return decrypter;
  448. }
  449. }
  450. function getEncryptionType(
  451. data: Uint8Array,
  452. decryptData: DecryptData | null,
  453. ): KeyData | null {
  454. let encryptionType: KeyData | null = null;
  455. if (
  456. data.byteLength > 0 &&
  457. decryptData?.key != null &&
  458. decryptData.iv !== null &&
  459. decryptData.method != null
  460. ) {
  461. encryptionType = decryptData as KeyData;
  462. }
  463. return encryptionType;
  464. }
  465. const emptyResult = (chunkMeta): TransmuxerResult => ({
  466. remuxResult: {},
  467. chunkMeta,
  468. });
  469. export function isPromise<T>(p: Promise<T> | any): p is Promise<T> {
  470. return 'then' in p && p.then instanceof Function;
  471. }
  472. export class TransmuxConfig {
  473. public audioCodec?: string;
  474. public videoCodec?: string;
  475. public initSegmentData?: Uint8Array;
  476. public duration: number;
  477. public defaultInitPts: RationalTimestamp | null;
  478. constructor(
  479. audioCodec: string | undefined,
  480. videoCodec: string | undefined,
  481. initSegmentData: Uint8Array | undefined,
  482. duration: number,
  483. defaultInitPts?: RationalTimestamp,
  484. ) {
  485. this.audioCodec = audioCodec;
  486. this.videoCodec = videoCodec;
  487. this.initSegmentData = initSegmentData;
  488. this.duration = duration;
  489. this.defaultInitPts = defaultInitPts || null;
  490. }
  491. }
  492. export class TransmuxState {
  493. public discontinuity: boolean;
  494. public contiguous: boolean;
  495. public accurateTimeOffset: boolean;
  496. public trackSwitch: boolean;
  497. public timeOffset: number;
  498. public initSegmentChange: boolean;
  499. constructor(
  500. discontinuity: boolean,
  501. contiguous: boolean,
  502. accurateTimeOffset: boolean,
  503. trackSwitch: boolean,
  504. timeOffset: number,
  505. initSegmentChange: boolean,
  506. ) {
  507. this.discontinuity = discontinuity;
  508. this.contiguous = contiguous;
  509. this.accurateTimeOffset = accurateTimeOffset;
  510. this.trackSwitch = trackSwitch;
  511. this.timeOffset = timeOffset;
  512. this.initSegmentChange = initSegmentChange;
  513. }
  514. }