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