Home Reference Source

src/demux/transmuxer.ts

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