@l5i/dashjs
Version:
A reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers.
282 lines (247 loc) • 11.6 kB
JavaScript
/**
* The copyright in this software is being made available under the BSD License,
* included below. This software may be subject to other third party and contributor
* rights, including patent rights, and no such rights are granted under this license.
*
* Copyright (c) 2013, Dash Industry Forum.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
* * Neither the name of Dash Industry Forum nor the names of its
* contributors may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
import FactoryMaker from '../../core/FactoryMaker';
import BoxParser from '../utils/BoxParser';
/**
* @module FetchLoader
* @description Manages download of resources via HTTP using fetch.
* @param {Object} cfg - dependencies from parent
*/
function FetchLoader(cfg) {
cfg = cfg || {};
const requestModifier = cfg.requestModifier;
let instance;
function load(httpRequest) {
// Variables will be used in the callback functions
let firstProgress = true; /*jshint ignore:line*/
let needFailureReport = true; /*jshint ignore:line*/
let requestStartTime = new Date();
let lastTraceTime = requestStartTime; /*jshint ignore:line*/
let lastTraceReceivedCount = 0; /*jshint ignore:line*/
let request = httpRequest.request;
const headers = new Headers(); /*jshint ignore:line*/
if (request.range) {
headers.append('Range', 'bytes=' + request.range);
}
if (!request.requestStartDate) {
request.requestStartDate = requestStartTime;
}
if (requestModifier) {
// modifyRequestHeader expects a XMLHttpRequest object so,
// to keep backward compatibility, we should expose a setRequestHeader method
// TODO: Remove RequestModifier dependency on XMLHttpRequest object and define
// a more generic way to intercept/modify requests
requestModifier.modifyRequestHeader({
setRequestHeader: function (header, value) {
headers.append(header, value);
}
});
}
let abortController;
if (typeof window.AbortController === 'function') {
abortController = new AbortController(); /*jshint ignore:line*/
httpRequest.abortController = abortController;
}
const reqOptions = {
method: httpRequest.method,
headers: headers,
credentials: httpRequest.withCredentials ? 'include' : undefined,
signal: abortController ? abortController.signal : undefined
};
fetch(httpRequest.url, reqOptions).then(function (response) {
if (!httpRequest.response) {
httpRequest.response = {};
}
httpRequest.response.status = response.status;
httpRequest.response.statusText = response.statusText;
httpRequest.response.responseURL = response.url;
if (!response.ok) {
httpRequest.onerror();
}
let responseHeaders = '';
for (const key of response.headers.keys()) {
responseHeaders += key + ': ' + response.headers.get(key) + '\n';
}
httpRequest.response.responseHeaders = responseHeaders;
if (!response.body) {
// Fetch returning a ReadableStream response body is not currently supported by all browsers.
// Browser compatibility: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
// If it is not supported, returning the whole segment when it's ready (as xhr)
return response.arrayBuffer().then(function (buffer) {
httpRequest.response.response = buffer;
const event = {
loaded: buffer.byteLength,
total: buffer.byteLength
};
httpRequest.progress(event);
httpRequest.onload();
httpRequest.onend();
return;
});
}
const totalBytes = parseInt(response.headers.get('Content-Length'), 10);
let bytesReceived = 0;
let signaledFirstByte = false;
let remaining = new Uint8Array();
let offset = 0;
httpRequest.reader = response.body.getReader();
let downLoadedData = [];
const processResult = function ({ value, done }) {
if (done) {
if (remaining) {
// If there is pending data, call progress so network metrics
// are correctly generated
// Same structure as https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequestEventTarget/onprogress
httpRequest.progress({
loaded: bytesReceived,
total: isNaN(totalBytes) ? bytesReceived : totalBytes,
lengthComputable: true,
time: calculateDownloadedTime(downLoadedData, bytesReceived)
});
httpRequest.response.response = remaining.buffer;
}
httpRequest.onload();
httpRequest.onend();
return;
}
if (value && value.length > 0) {
remaining = concatTypedArray(remaining, value);
bytesReceived += value.length;
downLoadedData.push({
ts: Date.now(),
bytes: value.length
});
const boxesInfo = BoxParser().getInstance().findLastTopIsoBoxCompleted(['moov', 'mdat'], remaining, offset);
if (boxesInfo.found) {
const end = boxesInfo.lastCompletedOffset + boxesInfo.size;
// If we are going to pass full buffer, avoid copying it and pass
// complete buffer. Otherwise clone the part of the buffer that is completed
// and adjust remaining buffer. A clone is needed because ArrayBuffer of a typed-array
// keeps a reference to the original data
let data;
if (end === remaining.length) {
data = remaining;
remaining = new Uint8Array();
} else {
data = new Uint8Array(remaining.subarray(0, end));
remaining = remaining.subarray(end);
}
// Announce progress but don't track traces. Throughput measures are quite unstable
// when they are based in small amount of data
httpRequest.progress({
data: data.buffer,
lengthComputable: false,
noTrace: true
});
offset = 0;
} else {
offset = boxesInfo.lastCompletedOffset;
// Call progress so it generates traces that will be later used to know when the first byte
// were received
if (!signaledFirstByte) {
httpRequest.progress({
lengthComputable: false,
noTrace: true
});
signaledFirstByte = true;
}
}
}
read(httpRequest, processResult);
};
read(httpRequest, processResult);
})
.catch( function (e) {
if (httpRequest.onerror) {
httpRequest.onerror(e);
}
});
}
function read(httpRequest, processResult) {
httpRequest.reader.read()
.then(processResult)
.catch(function (e) {
if (httpRequest.onerror && httpRequest.response.status === 200) {
// Error, but response code is 200, trigger error
httpRequest.onerror(e);
}
});
}
function concatTypedArray(remaining, data) {
if (remaining.length === 0) {
return data;
}
const result = new Uint8Array(remaining.length + data.length);
result.set(remaining);
result.set(data, remaining.length);
return result;
}
function abort(request) {
if (request.abortController) {
// For firefox and edge
request.abortController.abort();
} else if (request.reader) {
// For Chrome
try {
request.reader.cancel();
} catch (e) {
// throw exceptions (TypeError) when reader was previously closed,
// for example, because a network issue
}
}
}
function calculateDownloadedTime(datum, bytesReceived) {
datum = datum.filter(data => data.bytes > ((bytesReceived / 4) / datum.length) );
if (datum.length > 1) {
let time = 0;
const avgTimeDistance = (datum[datum.length - 1].ts - datum[0].ts) / datum.length;
datum.forEach((data, index) => {
// To be counted the data has to be over a threshold
const next = datum[index + 1];
if (next) {
const distance = next.ts - data.ts;
time += distance < avgTimeDistance ? distance : 0;
}
});
return time;
}
return null;
}
instance = {
load: load,
abort: abort,
calculateDownloadedTime: calculateDownloadedTime
};
return instance;
}
FetchLoader.__dashjs_factory_name = 'FetchLoader';
const factory = FactoryMaker.getClassFactory(FetchLoader);
export default factory;