You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
363 lines
11 KiB
363 lines
11 KiB
import {
|
|
useFakeEnvironment,
|
|
useFakeMediaSource,
|
|
createPlayer,
|
|
openMediaSource,
|
|
standardXHRResponse,
|
|
} from '../../test/test-helpers';
|
|
import {Hls} from '../../src/videojs-http-streaming.js';
|
|
|
|
let simulationDefaults = {
|
|
// number of seconds of video in each segment
|
|
segmentDuration: 10,
|
|
// number of milliseconds to delay the first byte
|
|
roundTripDelay: 70,
|
|
// throughput of the "backend" system (decryption, transmuxing, and appending)
|
|
// - for MSE, >150mbps is easily achievable
|
|
// - for Flash, the value is closer to 5mbps on a good day (and good computer)
|
|
throughput: 500000000,
|
|
manifestLatency: 120
|
|
};
|
|
|
|
// the number of seconds it takes for a single bit to be
|
|
// transmitted from the client to the server, or vice-versa
|
|
const propagationDelay = 0.5;
|
|
|
|
// send a mock playlist response
|
|
const playlistResponse = (request, simulationParams) => {
|
|
let match = request.url.match(/(\d+)-(\d+)/);
|
|
let maxBitrate = match[1];
|
|
let avgBitrate = match[2];
|
|
|
|
let response =
|
|
'#EXTM3U\n' +
|
|
'#EXT-X-PLAYLIST-TYPE:VOD\n' +
|
|
`#EXT-X-TARGETDURATION:${simulationParams.segmentDuration}\n`;
|
|
|
|
for (let i = 0; i < simulationParams.segmentCount; i++) {
|
|
response += `#EXTINF:${simulationParams.segmentDuration},\n`;
|
|
response += `${maxBitrate}-${avgBitrate}-${i}.ts\n`;
|
|
}
|
|
response += '#EXT-X-ENDLIST\n';
|
|
|
|
return response;
|
|
};
|
|
|
|
const clearSourceBufferUpdates = (sourceBuffer) => {
|
|
while (sourceBuffer.updates_.length) {
|
|
let update = sourceBuffer.updates_.pop();
|
|
if (update.append || update.remove) {
|
|
sourceBuffer.trigger('updateend');
|
|
}
|
|
}
|
|
};
|
|
|
|
// run the simulation
|
|
const runSimulation = function(options, done) {
|
|
let networkTrace = options.networkTrace;
|
|
let traceDurationInMs = networkTrace.reduce((acc, t) => acc + t[1], 0);
|
|
let simulationParams = Object.create(simulationDefaults);
|
|
simulationParams.segmentCount = Math.floor(traceDurationInMs / 1000 / simulationParams.segmentDuration);
|
|
|
|
// If segments are provided, switch into "simulate movie mode"
|
|
if (options.segments) {
|
|
let key = Object.keys(options.segments)[0];
|
|
if (key && Array.isArray(options.segments[key])) {
|
|
simulationParams.segmentCount = Math.min(simulationParams.segmentCount, options.segments[key].length);
|
|
simulationParams.dontCountNullSegments = true;
|
|
}
|
|
}
|
|
simulationParams.duration = simulationParams.segmentCount * simulationParams.segmentDuration;
|
|
simulationParams.durationInMs = simulationParams.duration * 1000;
|
|
|
|
Hls.GOAL_BUFFER_LENGTH = options.goalBufferLength;
|
|
Hls.BANDWIDTH_VARIANCE = options.bandwidthVariance;
|
|
Hls.BUFFER_LOW_WATER_LINE = options.bufferLowWaterLine;
|
|
|
|
// SETUP
|
|
let results = {
|
|
bandwidth: [],
|
|
effectiveBandwidth: [],
|
|
playlists: [],
|
|
buffered: [],
|
|
options: options
|
|
};
|
|
let env = useFakeEnvironment();
|
|
|
|
// Sinon 1.10.2 handles abort incorrectly (triggering the error event)
|
|
// Later versions fixed this but broke the ability to set the response
|
|
// to an arbitrary object (in our case, a typed array).
|
|
XMLHttpRequest.prototype = Object.create(XMLHttpRequest.prototype);
|
|
XMLHttpRequest.prototype.abort = function abort() {
|
|
this.aborted = true;
|
|
this.response = this.responseText = '';
|
|
this.errorFlag = true;
|
|
this.requestHeaders = {};
|
|
this.responseHeaders = {};
|
|
|
|
if (this.readyState > 0 /*FakeXMLHttpRequest.UNSENT*/ && this.sendFlag) {
|
|
this.readyStateChange(4); /*FakeXMLHttpRequest.DONE*/
|
|
this.sendFlag = false;
|
|
}
|
|
|
|
this.readyState = 0; /*FakeXMLHttpRequest.UNSENT;*/
|
|
};
|
|
|
|
let clock = env.clock;
|
|
let requests = env.requests;
|
|
let mse = useFakeMediaSource();
|
|
let buffered = 0;
|
|
let currentTime = 0;
|
|
let player = window.player = createPlayer();
|
|
let poptions = player.options();
|
|
poptions.vhs.debug = true;
|
|
player.options(poptions);
|
|
document.querySelector('#qunit-fixture').style = 'display: none;';
|
|
player.src({
|
|
src: 'http://example.com/master.m3u8',
|
|
type: 'application/x-mpegurl'
|
|
});
|
|
openMediaSource(player, clock);
|
|
|
|
// run next tick so that Flash doesn't swallow exceptions
|
|
let master = '#EXTM3U\n';
|
|
options.playlists.forEach((bandwidths) => {
|
|
master += `#EXT-X-STREAM-INF:BANDWIDTH=${bandwidths[0]},AVERAGE-BANDWIDTH=${bandwidths[1]}\n`;
|
|
master += `playlist-${bandwidths[0]}-${bandwidths[1]}.m3u8\n`;
|
|
});
|
|
|
|
// simulate buffered and currentTime during playback
|
|
let getBuffer = (buff) => {
|
|
return videojs.createTimeRange(0, currentTime + buffered);
|
|
};
|
|
player.tech_.buffered = getBuffer;
|
|
|
|
player.tech(true).vhs.playbackWatcher_.dispose();
|
|
|
|
Object.defineProperty(player.tech_, 'time_', {
|
|
get: () => currentTime
|
|
});
|
|
|
|
// respond to the playlist requests
|
|
let masterRequest = requests.shift();
|
|
masterRequest.respond(200, null, master);
|
|
|
|
let playlistRequest = requests.shift();
|
|
playlistRequest.respond(200, null, playlistResponse(playlistRequest, simulationParams));
|
|
|
|
let sourceBuffer = player.tech(true).vhs.mediaSource.sourceBuffers[0];
|
|
Object.defineProperty(sourceBuffer, 'buffered', {
|
|
get: getBuffer
|
|
});
|
|
|
|
// record the measured bandwidth for the playlist requests
|
|
results.effectiveBandwidth.push({
|
|
time: 0,
|
|
bandwidth: player.tech(true).vhs.bandwidth
|
|
});
|
|
player.tech(true).vhs.masterPlaylistController_.mainSegmentLoader_.segmentMetadataTrack_ = null;
|
|
player.play();
|
|
|
|
let t = 0;
|
|
let i = 0;
|
|
let s = 0;
|
|
let tInSeconds = 0;
|
|
let segmentRequest = null;
|
|
let segmentSize = null;
|
|
let segmentDownloaded = 0;
|
|
let segmentStartTime;
|
|
let segmentDelay = 0;
|
|
let segmentMaxBitrate;
|
|
let segmentAvgBitrate;
|
|
|
|
console.log('Simulation Started for', simulationParams.duration, 'seconds');
|
|
console.time('Simulation Ended');
|
|
|
|
// advance time and collect simulation results
|
|
while (t < simulationParams.durationInMs && i < networkTrace.length && s < simulationParams.segmentCount) {
|
|
let intervalParams = {
|
|
bytesTotal: networkTrace[i][0],
|
|
bytesRemaining: networkTrace[i][0],
|
|
// in milliseconds
|
|
timeTotal: networkTrace[i][1],
|
|
timeRemaining: networkTrace[i][1],
|
|
// in bits per second
|
|
bandwidth: Math.round((networkTrace[i][0] * 8) / (networkTrace[i][1] / 1000)),
|
|
bytesPerMs: networkTrace[i][0] / networkTrace[i][1]
|
|
};
|
|
|
|
results.bandwidth.push({
|
|
time: tInSeconds,
|
|
bandwidth: intervalParams.bandwidth
|
|
});
|
|
|
|
for (let j = 1; j <= intervalParams.timeTotal; j++) {
|
|
clock.tick(1);
|
|
t += 1;
|
|
tInSeconds = t / 1000;
|
|
|
|
// simulate playback
|
|
if (!player.paused() && buffered > 0 && buffered < j / 1000) {
|
|
// Then buffered becomes zero and current time can only advance by
|
|
// the buffer duration
|
|
currentTime += buffered;
|
|
buffered = 0;
|
|
results.buffered.push({
|
|
time: tInSeconds,
|
|
buffered: buffered
|
|
});
|
|
intervalParams.timeRemaining -= j;
|
|
}
|
|
|
|
if (!segmentRequest && requests.length) {
|
|
let request = requests.shift();
|
|
|
|
// playlist responses
|
|
if (/\.m3u8$/.test(request.url)) {
|
|
setTimeout(() => {
|
|
// for simplicity, playlist responses have zero trasmission time
|
|
request.respond(200, null, playlistResponse(request, simulationParams));
|
|
}, simulationParams.manifestLatency);
|
|
continue;
|
|
}
|
|
|
|
let bitrates = request.url.match(/(\d+)-(\d+)-(\d+)/);
|
|
|
|
segmentRequest = request;
|
|
segmentMaxBitrate = +bitrates[1];
|
|
segmentAvgBitrate = +bitrates[2];
|
|
segmentDelay += simulationParams.roundTripDelay;
|
|
segmentDownloaded = 0;
|
|
if (Array.isArray(options.segments[segmentMaxBitrate]) &&
|
|
Number.isFinite(options.segments[segmentMaxBitrate][bitrates[3]])) {
|
|
segmentSize = options.segments[segmentMaxBitrate][bitrates[3]];
|
|
} else {
|
|
segmentSize = Math.ceil((segmentAvgBitrate * simulationParams.segmentDuration) / 8);
|
|
}
|
|
}
|
|
|
|
if (segmentRequest) {
|
|
if (segmentDelay <= 0) {
|
|
segmentDownloaded += intervalParams.bytesPerMs;
|
|
} else {
|
|
segmentDelay -= 1;
|
|
if (segmentDelay === 0) {
|
|
segmentStartTime = tInSeconds;
|
|
}
|
|
}
|
|
|
|
if (segmentRequest.timedout) {
|
|
results.playlists.push({
|
|
start: segmentStartTime,
|
|
end: tInSeconds,
|
|
duration: simulationParams.segmentDuration,
|
|
bitrate: segmentMaxBitrate,
|
|
timedout: true
|
|
});
|
|
results.effectiveBandwidth.push({
|
|
time: (segmentStartTime + tInSeconds) / 2,
|
|
bandwidth: player.tech(true).vhs.bandwidth
|
|
});
|
|
segmentDelay = 0;
|
|
segmentRequest = null;
|
|
segmentSize = null;
|
|
console.error("Request for segment timedout");
|
|
continue;
|
|
}
|
|
|
|
if (segmentRequest.aborted) {
|
|
results.playlists.push({
|
|
start: segmentStartTime,
|
|
end: tInSeconds,
|
|
duration: simulationParams.segmentDuration,
|
|
bitrate: segmentMaxBitrate,
|
|
aborted: true
|
|
});
|
|
results.effectiveBandwidth.push({
|
|
time: (segmentStartTime + tInSeconds) / 2,
|
|
bandwidth: player.tech(true).vhs.bandwidth
|
|
});
|
|
segmentDelay = 0;
|
|
segmentRequest = null;
|
|
segmentSize = null;
|
|
console.error("Request for segment aborted");
|
|
continue;
|
|
}
|
|
|
|
if (segmentDownloaded > segmentSize) {
|
|
if (!currentTime ||
|
|
player.tech(true).vhs.masterPlaylistController_.mainSegmentLoader_.mediaIndex !== null) {
|
|
buffered += simulationParams.segmentDuration;
|
|
s++;
|
|
} else {
|
|
if (!simulationParams.dontCountNullSegments) {
|
|
s++;
|
|
}
|
|
}
|
|
|
|
segmentRequest.response = new Uint8Array(segmentSize);
|
|
segmentRequest.respond(200, null, '');
|
|
|
|
console.log('Request for', segmentRequest.uri, 'complete');
|
|
setTimeout((fore) => {
|
|
clearSourceBufferUpdates(sourceBuffer);
|
|
}, Math.round(segmentSize / (simulationParams.throughput / 8) * 1000), segmentRequest);
|
|
|
|
results.playlists.push({
|
|
start: segmentStartTime,
|
|
end: tInSeconds,
|
|
duration: simulationParams.segmentDuration,
|
|
bitrate: segmentMaxBitrate
|
|
});
|
|
results.effectiveBandwidth.push({
|
|
time: (segmentStartTime + tInSeconds) / 2,
|
|
bandwidth: player.tech(true).vhs.bandwidth
|
|
});
|
|
|
|
segmentRequest = null;
|
|
segmentSize = null;
|
|
} else if (t % 250 === 0) {
|
|
segmentRequest.dispatchEvent({
|
|
type: 'progress',
|
|
lengthComputable: true,
|
|
target: segmentRequest,
|
|
loaded: segmentDownloaded,
|
|
total: segmentSize
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
results.buffered.push({
|
|
time: tInSeconds,
|
|
buffered: buffered
|
|
});
|
|
|
|
let periodInSeconds = intervalParams.timeRemaining / 1000;
|
|
|
|
// simulate playback
|
|
if (!player.paused() && buffered > 0) {
|
|
if (buffered < periodInSeconds) {
|
|
// Then buffered becomes zero and current time can only advance by
|
|
// the buffer duration
|
|
currentTime += buffered;
|
|
buffered = 0;
|
|
} else {
|
|
buffered -= periodInSeconds;
|
|
currentTime += periodInSeconds;
|
|
}
|
|
player.trigger('timeupdate');
|
|
}
|
|
i += 1;
|
|
}
|
|
console.timeEnd('Simulation Ended');
|
|
|
|
player.dispose();
|
|
mse.restore();
|
|
env.restore();
|
|
|
|
console.log(results);
|
|
done(null, results);
|
|
};
|
|
|
|
export default runSimulation;
|