Home Reference Source

src/demux/transmuxer-interface.ts

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