Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: offload mp4/ts probe to web worker #1117

Merged
merged 8 commits into from
Apr 22, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"lodash-compat": "^3.10.0",
"nomnoml": "^0.3.0",
"rollup": "^2.36.1",
"rollup-plugin-worker-factory": "0.5.4",
"rollup-plugin-worker-factory": "0.5.5",
"shelljs": "^0.8.4",
"sinon": "^8.1.1",
"url-toolkit": "^2.2.1",
Expand Down
194 changes: 108 additions & 86 deletions src/media-segment-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import videojs from 'video.js';
import { createTransferableMessage } from './bin-utils';
import { stringToArrayBuffer } from './util/string-to-array-buffer';
import { transmux } from './segment-transmuxer';
import { probeTsSegment } from './util/segment';
import mp4probe from 'mux.js/lib/mp4/probe';
import { segmentXhrHeaders } from './xhr';
import {workerCallback} from './util/worker-callback.js';
import {
detectContainerForBytes,
isLikelyFmp4MediaSegment
Expand Down Expand Up @@ -182,25 +181,34 @@ const handleInitSegmentResponse =
}, segment);
}

const tracks = mp4probe.tracks(segment.map.bytes);
workerCallback({
action: 'probeMp4Tracks',
data: segment.map.bytes,
transmuxer: segment.transmuxer,
callback: ({tracks, data}) => {
// transfer bytes back to us
segment.map.bytes = data;

tracks.forEach(function(track) {
segment.map.tracks = segment.map.tracks || {};
tracks.forEach(function(track) {
segment.map.tracks = segment.map.tracks || {};

// only support one track of each type for now
if (segment.map.tracks[track.type]) {
return;
}
// only support one track of each type for now
if (segment.map.tracks[track.type]) {
return;
}

segment.map.tracks[track.type] = track;
segment.map.tracks[track.type] = track;

if (typeof track.id === 'number' && track.timescale) {
segment.map.timescales = segment.map.timescales || {};
segment.map.timescales[track.id] = track.timescale;
}

});

if (typeof track.id === 'number' && track.timescale) {
segment.map.timescales = segment.map.timescales || {};
segment.map.timescales[track.id] = track.timescale;
return finishProcessingFn(null, segment);
}
});

return finishProcessingFn(null, segment);
};

/**
Expand Down Expand Up @@ -282,34 +290,7 @@ const transmuxAndNotify = ({
let videoStartFn = timingInfoFn.bind(null, segment, 'video', 'start');
const videoEndFn = timingInfoFn.bind(null, segment, 'video', 'end');

// Check to see if we are appending a full segment.
if (!isPartial && !segment.lastReachedChar) {
// In the full segment transmuxer, we don't yet have the ability to extract a "proper"
// start time. Meaning cached frame data may corrupt our notion of where this segment
// really starts. To get around this, full segment appends should probe for the info
// needed.
const probeResult = probeTsSegment(bytes, segment.baseStartTime);

if (probeResult) {
trackInfoFn(segment, {
hasAudio: probeResult.hasAudio,
hasVideo: probeResult.hasVideo,
isMuxed
});
trackInfoFn = null;

if (probeResult.hasAudio && !isMuxed) {
audioStartFn(probeResult.audioStart);
}
if (probeResult.hasVideo) {
videoStartFn(probeResult.videoStart);
}
audioStartFn = null;
videoStartFn = null;
}
}

transmux({
const finish = () => transmux({
bytes,
transmuxer: segment.transmuxer,
audioAppendStart: segment.audioAppendStart,
Expand Down Expand Up @@ -379,6 +360,47 @@ const transmuxAndNotify = ({
doneFn(null, segment, result);
}
});

// Check to see if we are appending a full segment.
if (!isPartial && !segment.lastReachedChar) {
// In the full segment transmuxer, we don't yet have the ability to extract a "proper"
// start time. Meaning cached frame data may corrupt our notion of where this segment
// really starts. To get around this, full segment appends should probe for the info
// needed.
workerCallback({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I'm curious about is now that this is asynchronous, the trackInfo, audioStart, and videoStart functions may come from different places (either from the probe or from the transmuxer). Due to cached data in the transmuxer, I wonder if this could lead to some different behavior due to race conditions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't send anything to the transmuxer until the probe comes back, so I wouldn't think so.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed that, you're right 👍

action: 'probeTs',
transmuxer: segment.transmuxer,
data: bytes,
baseStartTime: segment.baseStartTime,
callback: (data) => {
segment.bytes = bytes = data.data;

const probeResult = data.result;

if (probeResult) {
trackInfoFn(segment, {
hasAudio: probeResult.hasAudio,
hasVideo: probeResult.hasVideo,
isMuxed
});
trackInfoFn = null;

if (probeResult.hasAudio && !isMuxed) {
audioStartFn(probeResult.audioStart);
}
if (probeResult.hasVideo) {
videoStartFn(probeResult.videoStart);
}
audioStartFn = null;
videoStartFn = null;
}

finish();
}
});
} else {
finish();
}
};

const handleSegmentBytes = ({
Expand All @@ -396,7 +418,7 @@ const handleSegmentBytes = ({
dataFn,
doneFn
}) => {
const bytesAsUint8Array = new Uint8Array(bytes);
let bytesAsUint8Array = new Uint8Array(bytes);

// TODO:
// We should have a handler that fetches the number of bytes required
Expand Down Expand Up @@ -438,62 +460,62 @@ const handleSegmentBytes = ({
// Note that the start time returned by the probe reflects the baseMediaDecodeTime, as
// that is the true start of the segment (where the playback engine should begin
// decoding).
const timingInfo = mp4probe.startTime(segment.map.timescales, bytesAsUint8Array);

if (trackInfo.hasAudio && !trackInfo.isMuxed) {
timingInfoFn(segment, 'audio', 'start', timingInfo);
}

if (trackInfo.hasVideo) {
timingInfoFn(segment, 'video', 'start', timingInfo);
}

const finishLoading = (captions) => {
// if the track still has audio at this point it is only possible
// for it to be audio only. See `tracks.video && tracks.audio` if statement
// above.
// we make sure to use segment.bytes here as that
dataFn(segment, {data: bytes, type: trackInfo.hasAudio && !trackInfo.isMuxed ? 'audio' : 'video'});
dataFn(segment, {
data: bytesAsUint8Array,
type: trackInfo.hasAudio && !trackInfo.isMuxed ? 'audio' : 'video'
});
if (captions && captions.length) {
captionsFn(segment, captions);
}
doneFn(null, segment, {});
};

// Run through the CaptionParser in case there are captions.
// Initialize CaptionParser if it hasn't been yet
if (!tracks.video || !bytes.byteLength || !segment.transmuxer) {
finishLoading();
return;
}

const buffer = bytes instanceof ArrayBuffer ? bytes : bytes.buffer;
const byteOffset = bytes instanceof ArrayBuffer ? 0 : bytes.byteOffset;
const listenForCaptions = (event) => {
if (event.data.action !== 'mp4Captions') {
return;
}
segment.transmuxer.removeEventListener('message', listenForCaptions);

const data = event.data.data;

// transfer ownership of bytes back to us.
segment.bytes = bytes = new Uint8Array(data, data.byteOffset || 0, data.byteLength);
workerCallback({
action: 'probeMp4StartTime',
timescales: segment.map.timescales,
data: bytesAsUint8Array,
transmuxer: segment.transmuxer,
callback: ({data, startTime}) => {
// transfer bytes back to us
bytes = data.buffer;
segment.bytes = bytesAsUint8Array = data;

if (trackInfo.hasAudio && !trackInfo.isMuxed) {
timingInfoFn(segment, 'audio', 'start', startTime);
}

finishLoading(event.data.captions);
};
if (trackInfo.hasVideo) {
timingInfoFn(segment, 'video', 'start', startTime);
}

segment.transmuxer.addEventListener('message', listenForCaptions);
// Run through the CaptionParser in case there are captions.
// Initialize CaptionParser if it hasn't been yet
if (!tracks.video || !data.byteLength || !segment.transmuxer) {
finishLoading();
return;
}

// transfer ownership of bytes to worker.
segment.transmuxer.postMessage({
action: 'pushMp4Captions',
timescales: segment.map.timescales,
trackIds: [tracks.video.id],
data: buffer,
byteOffset,
byteLength: bytes.byteLength
}, [ buffer ]);
workerCallback({
action: 'pushMp4Captions',
endAction: 'mp4Captions',
transmuxer: segment.transmuxer,
data: bytesAsUint8Array,
timescales: segment.map.timescales,
trackIds: [tracks.video.id],
callback: (message) => {
// transfer bytes back to us
bytes = message.data.buffer;
segment.bytes = bytesAsUint8Array = message.data;
finishLoading(message.captions);
}
});
}
});
return;
}

Expand Down
65 changes: 65 additions & 0 deletions src/transmuxer-worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
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 mp4probe from 'mux.js/lib/mp4/probe';
import tsInspector from 'mux.js/lib/tools/ts-inspector.js';
import {
ONE_SECOND_IN_TS,
secondsToVideoTs,
videoTsToSeconds
} from 'mux.js/lib/utils/clock';
Expand Down Expand Up @@ -335,6 +338,68 @@ class MessageHandlers {
}, [segment.buffer]);
}

probeMp4StartTime({timescales, data}) {
const startTime = mp4probe.startTime(timescales, data);

this.self.postMessage({
action: 'probeMp4StartTime',
startTime,
data
}, [data.buffer]);
}

probeMp4Tracks({data}) {
const tracks = mp4probe.tracks(data);

this.self.postMessage({
action: 'probeMp4Tracks',
tracks,
data
}, [data.buffer]);
}

/**
* Probe an mpeg2-ts segment to determine the start time of the segment in it's
* internal "media time," as well as whether it contains video and/or audio.
*
* @private
* @param {Uint8Array} bytes - segment bytes
* @param {number} baseStartTime
* Relative reference timestamp used when adjusting frame timestamps for rollover.
* This value should be in seconds, as it's converted to a 90khz clock within the
* function body.
* @return {Object} The start time of the current segment in "media time" as well as
* whether it contains video and/or audio
*/
probeTs({data, baseStartTime}) {
const tsStartTime = (typeof baseStartTime === 'number' && !isNaN(baseStartTime)) ?
(baseStartTime * ONE_SECOND_IN_TS) :
void 0;
const timeInfo = tsInspector.inspect(data, tsStartTime);
let result = null;

if (timeInfo) {
result = {
// each type's time info comes back as an array of 2 times, start and end
hasVideo: timeInfo.video && timeInfo.video.length === 2 || false,
hasAudio: timeInfo.audio && timeInfo.audio.length === 2 || false
};

if (result.hasVideo) {
result.videoStart = timeInfo.video[0].ptsTime;
}
if (result.hasAudio) {
result.audioStart = timeInfo.audio[0].ptsTime;
}
}

this.self.postMessage({
action: 'probeTs',
result,
data
}, [data.buffer]);
}

clearAllMp4Captions() {
if (this.captionParser) {
this.captionParser.clearAllCaptions();
Expand Down
Loading