UNPKG

@videojs/http-streaming

Version:

Play back HLS and DASH with Video.js, even where it's not natively supported

454 lines (394 loc) 12.4 kB
/* global self */ /** * @file transmuxer-worker.js */ /** * videojs-contrib-media-sources * * Copyright (c) 2015 Brightcove * All rights reserved. * * Handles communication between the browser-world and the mux.js * transmuxer running inside of a WebWorker by exposing a simple * message-based interface to a Transmuxer object. */ import {Transmuxer as FullMux} from 'mux.js/lib/mp4/transmuxer'; import PartialMux from 'mux.js/lib/partial/transmuxer'; import CaptionParser from 'mux.js/lib/mp4/caption-parser'; import { secondsToVideoTs, videoTsToSeconds } from 'mux.js/lib/utils/clock'; const typeFromStreamString = (streamString) => { if (streamString === 'AudioSegmentStream') { return 'audio'; } return streamString === 'VideoSegmentStream' ? 'video' : ''; }; /** * Re-emits transmuxer events by converting them into messages to the * world outside the worker. * * @param {Object} transmuxer the transmuxer to wire events on * @private */ const wireFullTransmuxerEvents = function(self, transmuxer) { transmuxer.on('data', function(segment) { // transfer ownership of the underlying ArrayBuffer // instead of doing a copy to save memory // ArrayBuffers are transferable but generic TypedArrays are not // @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects) const initArray = segment.initSegment; segment.initSegment = { data: initArray.buffer, byteOffset: initArray.byteOffset, byteLength: initArray.byteLength }; const typedArray = segment.data; segment.data = typedArray.buffer; self.postMessage({ action: 'data', segment, byteOffset: typedArray.byteOffset, byteLength: typedArray.byteLength }, [segment.data]); }); transmuxer.on('done', function(data) { self.postMessage({ action: 'done' }); }); transmuxer.on('gopInfo', function(gopInfo) { self.postMessage({ action: 'gopInfo', gopInfo }); }); transmuxer.on('videoSegmentTimingInfo', function(timingInfo) { const videoSegmentTimingInfo = { start: { decode: videoTsToSeconds(timingInfo.start.dts), presentation: videoTsToSeconds(timingInfo.start.pts) }, end: { decode: videoTsToSeconds(timingInfo.end.dts), presentation: videoTsToSeconds(timingInfo.end.pts) }, baseMediaDecodeTime: videoTsToSeconds(timingInfo.baseMediaDecodeTime) }; if (timingInfo.prependedContentDuration) { videoSegmentTimingInfo.prependedContentDuration = videoTsToSeconds(timingInfo.prependedContentDuration); } self.postMessage({ action: 'videoSegmentTimingInfo', videoSegmentTimingInfo }); }); transmuxer.on('audioSegmentTimingInfo', function(timingInfo) { // Note that all times for [audio/video]SegmentTimingInfo events are in video clock const audioSegmentTimingInfo = { start: { decode: videoTsToSeconds(timingInfo.start.dts), presentation: videoTsToSeconds(timingInfo.start.pts) }, end: { decode: videoTsToSeconds(timingInfo.end.dts), presentation: videoTsToSeconds(timingInfo.end.pts) }, baseMediaDecodeTime: videoTsToSeconds(timingInfo.baseMediaDecodeTime) }; if (timingInfo.prependedContentDuration) { audioSegmentTimingInfo.prependedContentDuration = videoTsToSeconds(timingInfo.prependedContentDuration); } self.postMessage({ action: 'audioSegmentTimingInfo', audioSegmentTimingInfo }); }); transmuxer.on('id3Frame', function(id3Frame) { self.postMessage({ action: 'id3Frame', id3Frame }); }); transmuxer.on('caption', function(caption) { self.postMessage({ action: 'caption', caption }); }); transmuxer.on('trackinfo', function(trackInfo) { self.postMessage({ action: 'trackinfo', trackInfo }); }); transmuxer.on('audioTimingInfo', function(audioTimingInfo) { // convert to video TS since we prioritize video time over audio self.postMessage({ action: 'audioTimingInfo', audioTimingInfo: { start: videoTsToSeconds(audioTimingInfo.start), end: videoTsToSeconds(audioTimingInfo.end) } }); }); transmuxer.on('videoTimingInfo', function(videoTimingInfo) { self.postMessage({ action: 'videoTimingInfo', videoTimingInfo: { start: videoTsToSeconds(videoTimingInfo.start), end: videoTsToSeconds(videoTimingInfo.end) } }); }); }; const wirePartialTransmuxerEvents = function(self, transmuxer) { transmuxer.on('data', function(event) { // transfer ownership of the underlying ArrayBuffer // instead of doing a copy to save memory // ArrayBuffers are transferable but generic TypedArrays are not // @link https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Passing_data_by_transferring_ownership_(transferable_objects) const initSegment = { data: event.data.track.initSegment.buffer, byteOffset: event.data.track.initSegment.byteOffset, byteLength: event.data.track.initSegment.byteLength }; const boxes = { data: event.data.boxes.buffer, byteOffset: event.data.boxes.byteOffset, byteLength: event.data.boxes.byteLength }; const segment = { boxes, initSegment, type: event.type, sequence: event.data.sequence }; if (typeof event.data.videoFrameDts !== 'undefined') { segment.videoFrameDtsTime = videoTsToSeconds(event.data.videoFrameDts); } if (typeof event.data.videoFramePts !== 'undefined') { segment.videoFramePtsTime = videoTsToSeconds(event.data.videoFramePts); } self.postMessage({ action: 'data', segment }, [ segment.boxes.data, segment.initSegment.data ]); }); transmuxer.on('id3Frame', function(id3Frame) { self.postMessage({ action: 'id3Frame', id3Frame }); }); transmuxer.on('caption', function(caption) { self.postMessage({ action: 'caption', caption }); }); transmuxer.on('done', function(data) { self.postMessage({ action: 'done', type: typeFromStreamString(data) }); }); transmuxer.on('partialdone', function(data) { self.postMessage({ action: 'partialdone', type: typeFromStreamString(data) }); }); transmuxer.on('endedsegment', function(data) { self.postMessage({ action: 'endedSegment', type: typeFromStreamString(data) }); }); transmuxer.on('trackinfo', function(trackInfo) { self.postMessage({ action: 'trackinfo', trackInfo }); }); transmuxer.on('audioTimingInfo', function(audioTimingInfo) { // This can happen if flush is called when no // audio has been processed. This should be an // unusual case, but if it does occur should not // result in valid data being returned if (audioTimingInfo.start === null) { self.postMessage({ action: 'audioTimingInfo', audioTimingInfo }); return; } // convert to video TS since we prioritize video time over audio const timingInfoInSeconds = { start: videoTsToSeconds(audioTimingInfo.start) }; if (audioTimingInfo.end) { timingInfoInSeconds.end = videoTsToSeconds(audioTimingInfo.end); } self.postMessage({ action: 'audioTimingInfo', audioTimingInfo: timingInfoInSeconds }); }); transmuxer.on('videoTimingInfo', function(videoTimingInfo) { const timingInfoInSeconds = { start: videoTsToSeconds(videoTimingInfo.start) }; if (videoTimingInfo.end) { timingInfoInSeconds.end = videoTsToSeconds(videoTimingInfo.end); } self.postMessage({ action: 'videoTimingInfo', videoTimingInfo: timingInfoInSeconds }); }); }; /** * All incoming messages route through this hash. If no function exists * to handle an incoming message, then we ignore the message. * * @class MessageHandlers * @param {Object} options the options to initialize with */ class MessageHandlers { constructor(self, options) { this.options = options || {}; this.self = self; this.init(); } /** * initialize our web worker and wire all the events. */ init() { if (this.transmuxer) { this.transmuxer.dispose(); } this.transmuxer = this.options.handlePartialData ? new PartialMux(this.options) : new FullMux(this.options); if (this.options.handlePartialData) { wirePartialTransmuxerEvents(this.self, this.transmuxer); } else { wireFullTransmuxerEvents(this.self, this.transmuxer); } } pushMp4Captions(data) { if (!this.captionParser) { this.captionParser = new CaptionParser(); this.captionParser.init(); } const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); const parsed = this.captionParser.parse( segment, data.trackIds, data.timescales ); this.self.postMessage({ action: 'mp4Captions', captions: parsed && parsed.captions || [], data: segment.buffer }, [segment.buffer]); } clearAllMp4Captions() { if (this.captionParser) { this.captionParser.clearAllCaptions(); } } clearParsedMp4Captions() { if (this.captionParser) { this.captionParser.clearParsedCaptions(); } } /** * Adds data (a ts segment) to the start of the transmuxer pipeline for * processing. * * @param {ArrayBuffer} data data to push into the muxer */ push(data) { // Cast array buffer to correct type for transmuxer const segment = new Uint8Array(data.data, data.byteOffset, data.byteLength); this.transmuxer.push(segment); } /** * Recreate the transmuxer so that the next segment added via `push` * start with a fresh transmuxer. */ reset() { this.transmuxer.reset(); } /** * Set the value that will be used as the `baseMediaDecodeTime` time for the * next segment pushed in. Subsequent segments will have their `baseMediaDecodeTime` * set relative to the first based on the PTS values. * * @param {Object} data used to set the timestamp offset in the muxer */ setTimestampOffset(data) { const timestampOffset = data.timestampOffset || 0; this.transmuxer.setBaseMediaDecodeTime(Math.round(secondsToVideoTs(timestampOffset))); } setAudioAppendStart(data) { this.transmuxer.setAudioAppendStart(Math.ceil(secondsToVideoTs(data.appendStart))); } setRemux(data) { this.transmuxer.setRemux(data.remux); } /** * Forces the pipeline to finish processing the last segment and emit it's * results. * * @param {Object} data event data, not really used */ flush(data) { this.transmuxer.flush(); // transmuxed done action is fired after both audio/video pipelines are flushed self.postMessage({ action: 'done', type: 'transmuxed' }); } partialFlush(data) { this.transmuxer.partialFlush(); // transmuxed partialdone action is fired after both audio/video pipelines are flushed self.postMessage({ action: 'partialdone', type: 'transmuxed' }); } endTimeline() { this.transmuxer.endTimeline(); // transmuxed endedtimeline action is fired after both audio/video pipelines end their // timelines self.postMessage({ action: 'endedtimeline', type: 'transmuxed' }); } alignGopsWith(data) { this.transmuxer.alignGopsWith(data.gopsToAlignWith.slice()); } } /** * Our web worker interface so that things can talk to mux.js * that will be running in a web worker. the scope is passed to this by * webworkify. * * @param {Object} self the scope for the web worker */ self.onmessage = function(event) { if (event.data.action === 'init' && event.data.options) { this.messageHandlers = new MessageHandlers(self, event.data.options); return; } if (!this.messageHandlers) { this.messageHandlers = new MessageHandlers(self); } if (event.data && event.data.action && event.data.action !== 'init') { if (this.messageHandlers[event.data.action]) { this.messageHandlers[event.data.action](event.data); } } };