Home Reference Source

src/demux/transmuxer-interface.ts

  1. import * as work from 'webworkify-webpack';
  2. import { Events } from '../events';
  3. import Transmuxer, {
  4. TransmuxConfig,
  5. TransmuxState,
  6. isPromise,
  7. } from '../demux/transmuxer';
  8. import { logger } from '../utils/logger';
  9. import { ErrorTypes, ErrorDetails } from '../errors';
  10. import { getMediaSource } from '../utils/mediasource-helper';
  11. import { EventEmitter } from 'eventemitter3';
  12. import Fragment, { Part } from '../loader/fragment';
  13. import type { ChunkMetadata, TransmuxerResult } from '../types/transmuxer';
  14. import type Hls from '../hls';
  15. import type { HlsEventEmitter } from '../events';
  16. import type { PlaylistLevelType } from '../types/loader';
  17.  
  18. const MediaSource = getMediaSource() || { isTypeSupported: () => false };
  19.  
  20. export default class TransmuxerInterface {
  21. private hls: Hls;
  22. private id: PlaylistLevelType;
  23. private observer: HlsEventEmitter;
  24. private frag: Fragment | null = null;
  25. private part: Part | null = null;
  26. private worker: any;
  27. private onwmsg?: Function;
  28. private transmuxer: Transmuxer | null = null;
  29. private onTransmuxComplete: (transmuxResult: TransmuxerResult) => void;
  30. private onFlush: (chunkMeta: ChunkMetadata) => void;
  31.  
  32. constructor(
  33. hls: Hls,
  34. id: PlaylistLevelType,
  35. onTransmuxComplete: (transmuxResult: TransmuxerResult) => void,
  36. onFlush: (chunkMeta: ChunkMetadata) => void
  37. ) {
  38. this.hls = hls;
  39. this.id = id;
  40. this.onTransmuxComplete = onTransmuxComplete;
  41. this.onFlush = onFlush;
  42.  
  43. const config = hls.config;
  44.  
  45. const forwardMessage = (ev, data) => {
  46. data = data || {};
  47. data.frag = this.frag;
  48. data.id = this.id;
  49. hls.trigger(ev, data);
  50. };
  51.  
  52. // forward events to main thread
  53. this.observer = new EventEmitter() as HlsEventEmitter;
  54. this.observer.on(Events.FRAG_DECRYPTED, forwardMessage);
  55. this.observer.on(Events.ERROR, forwardMessage);
  56.  
  57. const typeSupported = {
  58. mp4: MediaSource.isTypeSupported('video/mp4'),
  59. mpeg: MediaSource.isTypeSupported('audio/mpeg'),
  60. mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"'),
  61. };
  62. // navigator.vendor is not always available in Web Worker
  63. // refer to https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/navigator
  64. const vendor = navigator.vendor;
  65. if (config.enableWorker && typeof Worker !== 'undefined') {
  66. logger.log('demuxing in webworker');
  67. let worker;
  68. try {
  69. worker = this.worker = work(
  70. require.resolve('../demux/transmuxer-worker.ts')
  71. );
  72. this.onwmsg = this.onWorkerMessage.bind(this);
  73. worker.addEventListener('message', this.onwmsg);
  74. worker.onerror = (event) => {
  75. hls.trigger(Events.ERROR, {
  76. type: ErrorTypes.OTHER_ERROR,
  77. details: ErrorDetails.INTERNAL_EXCEPTION,
  78. fatal: true,
  79. event: 'demuxerWorker',
  80. err: {
  81. message:
  82. event.message +
  83. ' (' +
  84. event.filename +
  85. ':' +
  86. event.lineno +
  87. ')',
  88. },
  89. });
  90. };
  91. worker.postMessage({
  92. cmd: 'init',
  93. typeSupported: typeSupported,
  94. vendor: vendor,
  95. id: id,
  96. config: JSON.stringify(config),
  97. });
  98. } catch (err) {
  99. logger.warn('Error in worker:', err);
  100. logger.error(
  101. 'Error while initializing DemuxerWorker, fallback to inline'
  102. );
  103. if (worker) {
  104. // revoke the Object URL that was used to create transmuxer worker, so as not to leak it
  105. self.URL.revokeObjectURL(worker.objectURL);
  106. }
  107. this.transmuxer = new Transmuxer(
  108. this.observer,
  109. typeSupported,
  110. config,
  111. vendor
  112. );
  113. this.worker = null;
  114. }
  115. } else {
  116. this.transmuxer = new Transmuxer(
  117. this.observer,
  118. typeSupported,
  119. config,
  120. vendor
  121. );
  122. }
  123. }
  124.  
  125. destroy(): void {
  126. const w = this.worker;
  127. if (w) {
  128. w.removeEventListener('message', this.onwmsg);
  129. w.terminate();
  130. this.worker = null;
  131. } else {
  132. const transmuxer = this.transmuxer;
  133. if (transmuxer) {
  134. transmuxer.destroy();
  135. this.transmuxer = null;
  136. }
  137. }
  138. const observer = this.observer;
  139. if (observer) {
  140. observer.removeAllListeners();
  141. }
  142. // @ts-ignore
  143. this.observer = null;
  144. }
  145.  
  146. push(
  147. data: ArrayBuffer,
  148. initSegmentData: Uint8Array | undefined,
  149. audioCodec: string | undefined,
  150. videoCodec: string | undefined,
  151. frag: Fragment,
  152. part: Part | null,
  153. duration: number,
  154. accurateTimeOffset: boolean,
  155. chunkMeta: ChunkMetadata,
  156. defaultInitPTS?: number
  157. ): void {
  158. chunkMeta.transmuxing.start = self.performance.now();
  159. const { transmuxer, worker } = this;
  160. const timeOffset = part ? part.start : frag.start;
  161. const decryptdata = frag.decryptdata;
  162. const lastFrag = this.frag;
  163.  
  164. const discontinuity = !(lastFrag && frag.cc === lastFrag.cc);
  165. const trackSwitch = !(lastFrag && chunkMeta.level === lastFrag.level);
  166. const snDiff = lastFrag ? chunkMeta.sn - (lastFrag.sn as number) : -1;
  167. const partDiff = this.part ? chunkMeta.part - this.part.index : 1;
  168. const contiguous =
  169. !trackSwitch && (snDiff === 1 || (snDiff === 0 && partDiff === 1));
  170. const now = self.performance.now();
  171.  
  172. if (trackSwitch || snDiff || frag.stats.parsing.start === 0) {
  173. frag.stats.parsing.start = now;
  174. }
  175. if (part && (partDiff || !contiguous)) {
  176. part.stats.parsing.start = now;
  177. }
  178. const state = new TransmuxState(
  179. discontinuity,
  180. contiguous,
  181. accurateTimeOffset,
  182. trackSwitch,
  183. timeOffset
  184. );
  185. if (!contiguous || discontinuity) {
  186. logger.log(`[transmuxer-interface, ${frag.type}]: Starting new transmux session for sn: ${chunkMeta.sn} p: ${chunkMeta.part} level: ${chunkMeta.level} id: ${chunkMeta.id}
  187. discontinuity: ${discontinuity}
  188. trackSwitch: ${trackSwitch}
  189. contiguous: ${contiguous}
  190. accurateTimeOffset: ${accurateTimeOffset}
  191. timeOffset: ${timeOffset}`);
  192. const config = new TransmuxConfig(
  193. audioCodec,
  194. videoCodec,
  195. initSegmentData,
  196. duration,
  197. defaultInitPTS
  198. );
  199. this.configureTransmuxer(config);
  200. }
  201.  
  202. this.frag = frag;
  203. this.part = part;
  204.  
  205. // Frags with sn of 'initSegment' are not transmuxed
  206. if (worker) {
  207. // post fragment payload as transferable objects for ArrayBuffer (no copy)
  208. worker.postMessage(
  209. {
  210. cmd: 'demux',
  211. data,
  212. decryptdata,
  213. chunkMeta,
  214. state,
  215. },
  216. data instanceof ArrayBuffer ? [data] : []
  217. );
  218. } else if (transmuxer) {
  219. const transmuxResult = transmuxer.push(
  220. data,
  221. decryptdata,
  222. chunkMeta,
  223. state
  224. );
  225. if (isPromise(transmuxResult)) {
  226. transmuxResult.then((data) => {
  227. this.handleTransmuxComplete(data);
  228. });
  229. } else {
  230. this.handleTransmuxComplete(transmuxResult as TransmuxerResult);
  231. }
  232. }
  233. }
  234.  
  235. flush(chunkMeta: ChunkMetadata) {
  236. chunkMeta.transmuxing.start = self.performance.now();
  237. const { transmuxer, worker } = this;
  238. if (worker) {
  239. worker.postMessage({
  240. cmd: 'flush',
  241. chunkMeta,
  242. });
  243. } else if (transmuxer) {
  244. const transmuxResult = transmuxer.flush(chunkMeta);
  245. if (isPromise(transmuxResult)) {
  246. transmuxResult.then((data) => {
  247. this.handleFlushResult(data, chunkMeta);
  248. });
  249. } else {
  250. this.handleFlushResult(
  251. transmuxResult as Array<TransmuxerResult>,
  252. chunkMeta
  253. );
  254. }
  255. }
  256. }
  257.  
  258. private handleFlushResult(
  259. results: Array<TransmuxerResult>,
  260. chunkMeta: ChunkMetadata
  261. ) {
  262. results.forEach((result) => {
  263. this.handleTransmuxComplete(result);
  264. });
  265. this.onFlush(chunkMeta);
  266. }
  267.  
  268. private onWorkerMessage(ev: any): void {
  269. const data = ev.data;
  270. const hls = this.hls;
  271. switch (data.event) {
  272. case 'init': {
  273. // revoke the Object URL that was used to create transmuxer worker, so as not to leak it
  274. self.URL.revokeObjectURL(this.worker.objectURL);
  275. break;
  276. }
  277.  
  278. case 'transmuxComplete': {
  279. this.handleTransmuxComplete(data.data);
  280. break;
  281. }
  282.  
  283. case 'flush': {
  284. this.onFlush(data.data);
  285. break;
  286. }
  287.  
  288. /* falls through */
  289. default: {
  290. data.data = data.data || {};
  291. data.data.frag = this.frag;
  292. data.data.id = this.id;
  293. hls.trigger(data.event, data.data);
  294. break;
  295. }
  296. }
  297. }
  298.  
  299. private configureTransmuxer(config: TransmuxConfig) {
  300. const { worker, transmuxer } = this;
  301. if (worker) {
  302. worker.postMessage({
  303. cmd: 'configure',
  304. config,
  305. });
  306. } else if (transmuxer) {
  307. transmuxer.configure(config);
  308. }
  309. }
  310.  
  311. private handleTransmuxComplete(result: TransmuxerResult) {
  312. result.chunkMeta.transmuxing.end = self.performance.now();
  313. this.onTransmuxComplete(result);
  314. }
  315. }