transmuxer-worker.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. /* global self */
  2. /**
  3. * @file transmuxer-worker.js
  4. */
  5. /**
  6. * videojs-contrib-media-sources
  7. *
  8. * Copyright (c) 2015 Brightcove
  9. * All rights reserved.
  10. *
  11. * Handles communication between the browser-world and the mux.js
  12. * transmuxer running inside of a WebWorker by exposing a simple
  13. * message-based interface to a Transmuxer object.
  14. */
  15. import {Transmuxer} from 'mux.js/lib/mp4/transmuxer';
  16. import CaptionParser from 'mux.js/lib/mp4/caption-parser';
  17. import mp4probe from 'mux.js/lib/mp4/probe';
  18. import tsInspector from 'mux.js/lib/tools/ts-inspector.js';
  19. import {
  20. ONE_SECOND_IN_TS,
  21. secondsToVideoTs,
  22. videoTsToSeconds
  23. } from 'mux.js/lib/utils/clock';
  24. /**
  25. * Re-emits transmuxer events by converting them into messages to the
  26. * world outside the worker.
  27. *
  28. * @param {Object} transmuxer the transmuxer to wire events on
  29. * @private
  30. */
  31. const wireTransmuxerEvents = function(self, transmuxer) {
  32. transmuxer.on('data', function(segment) {
  33. // transfer ownership of the underlying ArrayBuffer
  34. // instead of doing a copy to save memory
  35. // ArrayBuffers are transferable but generic TypedArrays are not
  36. // @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects)
  37. const initArray = segment.initSegment;
  38. segment.initSegment = {
  39. data: initArray.buffer,
  40. byteOffset: initArray.byteOffset,
  41. byteLength: initArray.byteLength
  42. };
  43. const typedArray = segment.data;
  44. segment.data = typedArray.buffer;
  45. self.postMessage({
  46. action: 'data',
  47. segment,
  48. byteOffset: typedArray.byteOffset,
  49. byteLength: typedArray.byteLength
  50. }, [segment.data]);
  51. });
  52. transmuxer.on('done', function(data) {
  53. self.postMessage({ action: 'done' });
  54. });
  55. transmuxer.on('gopInfo', function(gopInfo) {
  56. self.postMessage({
  57. action: 'gopInfo',
  58. gopInfo
  59. });
  60. });
  61. transmuxer.on('videoSegmentTimingInfo', function(timingInfo) {
  62. const videoSegmentTimingInfo = {
  63. start: {
  64. decode: videoTsToSeconds(timingInfo.start.dts),
  65. presentation: videoTsToSeconds(timingInfo.start.pts)
  66. },
  67. end: {
  68. decode: videoTsToSeconds(timingInfo.end.dts),
  69. presentation: videoTsToSeconds(timingInfo.end.pts)
  70. },
  71. baseMediaDecodeTime: videoTsToSeconds(timingInfo.baseMediaDecodeTime)
  72. };
  73. if (timingInfo.prependedContentDuration) {
  74. videoSegmentTimingInfo.prependedContentDuration = videoTsToSeconds(timingInfo.prependedContentDuration);
  75. }
  76. self.postMessage({
  77. action: 'videoSegmentTimingInfo',
  78. videoSegmentTimingInfo
  79. });
  80. });
  81. transmuxer.on('audioSegmentTimingInfo', function(timingInfo) {
  82. // Note that all times for [audio/video]SegmentTimingInfo events are in video clock
  83. const audioSegmentTimingInfo = {
  84. start: {
  85. decode: videoTsToSeconds(timingInfo.start.dts),
  86. presentation: videoTsToSeconds(timingInfo.start.pts)
  87. },
  88. end: {
  89. decode: videoTsToSeconds(timingInfo.end.dts),
  90. presentation: videoTsToSeconds(timingInfo.end.pts)
  91. },
  92. baseMediaDecodeTime: videoTsToSeconds(timingInfo.baseMediaDecodeTime)
  93. };
  94. if (timingInfo.prependedContentDuration) {
  95. audioSegmentTimingInfo.prependedContentDuration =
  96. videoTsToSeconds(timingInfo.prependedContentDuration);
  97. }
  98. self.postMessage({
  99. action: 'audioSegmentTimingInfo',
  100. audioSegmentTimingInfo
  101. });
  102. });
  103. transmuxer.on('id3Frame', function(id3Frame) {
  104. self.postMessage({
  105. action: 'id3Frame',
  106. id3Frame
  107. });
  108. });
  109. transmuxer.on('caption', function(caption) {
  110. self.postMessage({
  111. action: 'caption',
  112. caption
  113. });
  114. });
  115. transmuxer.on('trackinfo', function(trackInfo) {
  116. self.postMessage({
  117. action: 'trackinfo',
  118. trackInfo
  119. });
  120. });
  121. transmuxer.on('audioTimingInfo', function(audioTimingInfo) {
  122. // convert to video TS since we prioritize video time over audio
  123. self.postMessage({
  124. action: 'audioTimingInfo',
  125. audioTimingInfo: {
  126. start: videoTsToSeconds(audioTimingInfo.start),
  127. end: videoTsToSeconds(audioTimingInfo.end)
  128. }
  129. });
  130. });
  131. transmuxer.on('videoTimingInfo', function(videoTimingInfo) {
  132. self.postMessage({
  133. action: 'videoTimingInfo',
  134. videoTimingInfo: {
  135. start: videoTsToSeconds(videoTimingInfo.start),
  136. end: videoTsToSeconds(videoTimingInfo.end)
  137. }
  138. });
  139. });
  140. transmuxer.on('log', function(log) {
  141. self.postMessage({action: 'log', log});
  142. });
  143. };
  144. /**
  145. * All incoming messages route through this hash. If no function exists
  146. * to handle an incoming message, then we ignore the message.
  147. *
  148. * @class MessageHandlers
  149. * @param {Object} options the options to initialize with
  150. */
  151. class MessageHandlers {
  152. constructor(self, options) {
  153. this.options = options || {};
  154. this.self = self;
  155. this.init();
  156. }
  157. /**
  158. * initialize our web worker and wire all the events.
  159. */
  160. init() {
  161. if (this.transmuxer) {
  162. this.transmuxer.dispose();
  163. }
  164. this.transmuxer = new Transmuxer(this.options);
  165. wireTransmuxerEvents(this.self, this.transmuxer);
  166. }
  167. pushMp4Captions(data) {
  168. if (!this.captionParser) {
  169. this.captionParser = new CaptionParser();
  170. this.captionParser.init();
  171. }
  172. const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength);
  173. const parsed = this.captionParser.parse(
  174. segment,
  175. data.trackIds,
  176. data.timescales
  177. );
  178. this.self.postMessage({
  179. action: 'mp4Captions',
  180. captions: parsed && parsed.captions || [],
  181. logs: parsed && parsed.logs || [],
  182. data: segment.buffer
  183. }, [segment.buffer]);
  184. }
  185. probeMp4StartTime({timescales, data}) {
  186. const startTime = mp4probe.startTime(timescales, data);
  187. this.self.postMessage({
  188. action: 'probeMp4StartTime',
  189. startTime,
  190. data
  191. }, [data.buffer]);
  192. }
  193. probeMp4Tracks({data}) {
  194. const tracks = mp4probe.tracks(data);
  195. this.self.postMessage({
  196. action: 'probeMp4Tracks',
  197. tracks,
  198. data
  199. }, [data.buffer]);
  200. }
  201. /**
  202. * Probes an mp4 segment for EMSG boxes containing ID3 data.
  203. * https://aomediacodec.github.io/id3-emsg/
  204. *
  205. * @param {Uint8Array} data segment data
  206. * @param {number} offset segment start time
  207. * @return {Object[]} an array of ID3 frames
  208. */
  209. probeEmsgID3({data, offset}) {
  210. const id3Frames = mp4probe.getEmsgID3(data, offset);
  211. this.self.postMessage({
  212. action: 'probeEmsgID3',
  213. id3Frames,
  214. emsgData: data
  215. }, [data.buffer]);
  216. }
  217. /**
  218. * Probe an mpeg2-ts segment to determine the start time of the segment in it's
  219. * internal "media time," as well as whether it contains video and/or audio.
  220. *
  221. * @private
  222. * @param {Uint8Array} bytes - segment bytes
  223. * @param {number} baseStartTime
  224. * Relative reference timestamp used when adjusting frame timestamps for rollover.
  225. * This value should be in seconds, as it's converted to a 90khz clock within the
  226. * function body.
  227. * @return {Object} The start time of the current segment in "media time" as well as
  228. * whether it contains video and/or audio
  229. */
  230. probeTs({data, baseStartTime}) {
  231. const tsStartTime = (typeof baseStartTime === 'number' && !isNaN(baseStartTime)) ?
  232. (baseStartTime * ONE_SECOND_IN_TS) :
  233. void 0;
  234. const timeInfo = tsInspector.inspect(data, tsStartTime);
  235. let result = null;
  236. if (timeInfo) {
  237. result = {
  238. // each type's time info comes back as an array of 2 times, start and end
  239. hasVideo: timeInfo.video && timeInfo.video.length === 2 || false,
  240. hasAudio: timeInfo.audio && timeInfo.audio.length === 2 || false
  241. };
  242. if (result.hasVideo) {
  243. result.videoStart = timeInfo.video[0].ptsTime;
  244. }
  245. if (result.hasAudio) {
  246. result.audioStart = timeInfo.audio[0].ptsTime;
  247. }
  248. }
  249. this.self.postMessage({
  250. action: 'probeTs',
  251. result,
  252. data
  253. }, [data.buffer]);
  254. }
  255. clearAllMp4Captions() {
  256. if (this.captionParser) {
  257. this.captionParser.clearAllCaptions();
  258. }
  259. }
  260. clearParsedMp4Captions() {
  261. if (this.captionParser) {
  262. this.captionParser.clearParsedCaptions();
  263. }
  264. }
  265. /**
  266. * Adds data (a ts segment) to the start of the transmuxer pipeline for
  267. * processing.
  268. *
  269. * @param {ArrayBuffer} data data to push into the muxer
  270. */
  271. push(data) {
  272. // Cast array buffer to correct type for transmuxer
  273. const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength);
  274. this.transmuxer.push(segment);
  275. }
  276. /**
  277. * Recreate the transmuxer so that the next segment added via `push`
  278. * start with a fresh transmuxer.
  279. */
  280. reset() {
  281. this.transmuxer.reset();
  282. }
  283. /**
  284. * Set the value that will be used as the `baseMediaDecodeTime` time for the
  285. * next segment pushed in. Subsequent segments will have their `baseMediaDecodeTime`
  286. * set relative to the first based on the PTS values.
  287. *
  288. * @param {Object} data used to set the timestamp offset in the muxer
  289. */
  290. setTimestampOffset(data) {
  291. const timestampOffset = data.timestampOffset || 0;
  292. this.transmuxer.setBaseMediaDecodeTime(Math.round(secondsToVideoTs(timestampOffset)));
  293. }
  294. setAudioAppendStart(data) {
  295. this.transmuxer.setAudioAppendStart(Math.ceil(secondsToVideoTs(data.appendStart)));
  296. }
  297. setRemux(data) {
  298. this.transmuxer.setRemux(data.remux);
  299. }
  300. /**
  301. * Forces the pipeline to finish processing the last segment and emit it's
  302. * results.
  303. *
  304. * @param {Object} data event data, not really used
  305. */
  306. flush(data) {
  307. this.transmuxer.flush();
  308. // transmuxed done action is fired after both audio/video pipelines are flushed
  309. self.postMessage({
  310. action: 'done',
  311. type: 'transmuxed'
  312. });
  313. }
  314. endTimeline() {
  315. this.transmuxer.endTimeline();
  316. // transmuxed endedtimeline action is fired after both audio/video pipelines end their
  317. // timelines
  318. self.postMessage({
  319. action: 'endedtimeline',
  320. type: 'transmuxed'
  321. });
  322. }
  323. alignGopsWith(data) {
  324. this.transmuxer.alignGopsWith(data.gopsToAlignWith.slice());
  325. }
  326. }
  327. /**
  328. * Our web worker interface so that things can talk to mux.js
  329. * that will be running in a web worker. the scope is passed to this by
  330. * webworkify.
  331. *
  332. * @param {Object} self the scope for the web worker
  333. */
  334. self.onmessage = function(event) {
  335. if (event.data.action === 'init' && event.data.options) {
  336. this.messageHandlers = new MessageHandlers(self, event.data.options);
  337. return;
  338. }
  339. if (!this.messageHandlers) {
  340. this.messageHandlers = new MessageHandlers(self);
  341. }
  342. if (event.data && event.data.action && event.data.action !== 'init') {
  343. if (this.messageHandlers[event.data.action]) {
  344. this.messageHandlers[event.data.action](event.data);
  345. }
  346. }
  347. };