Skip to content

Commit

Permalink
Feat(player): Added events for download lifecycle.
Browse files Browse the repository at this point in the history
This adds two new player events, 'downloadheadersreceived' and
'downloadfailed', to allow users to measure network performance
in greater detail.

Issue shaka-project#3533

Change-Id: I33a3bd411d815e926d4bea2184e8d3ea69e2bb49
  • Loading branch information
theodab committed Jul 29, 2021
1 parent 38cfc23 commit 7893b77
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 63 deletions.
21 changes: 17 additions & 4 deletions externs/shaka/net.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,15 +139,17 @@ shaka.extern.Response;
* @typedef {!function(string,
* shaka.extern.Request,
* shaka.net.NetworkingEngine.RequestType,
* shaka.extern.ProgressUpdated):
* shaka.extern.ProgressUpdated,
* shaka.extern.HeadersReceived):
* !shaka.extern.IAbortableOperation.<shaka.extern.Response>}
* @description
* Defines a plugin that handles a specific scheme.
*
* The functions accepts four parameters, uri string, request, request type,
* and a progressUpdated function. The progressUpdated function can be ignored
* by plugins that do not have this information, but it will always be provided
* by NetworkingEngine.
* a progressUpdated function, and a headersReceived function. The
* progressUpdated and headersReceived functions can be ignored by plugins that
* do not have this information, but it will always be provided by
* NetworkingEngine.
*
* @exportDoc
*/
Expand All @@ -171,6 +173,17 @@ shaka.extern.SchemePlugin;
shaka.extern.ProgressUpdated;


/**
* @typedef {function(!Object.<string, string>)}
*
* @description
* A callback function to handle headers received events through networking
* engine in player.
* The first argument is the headers object of the response.
*/
shaka.extern.HeadersReceived;


/**
* Defines a filter for requests. This filter takes the request and modifies
* it before it is sent to the scheme plugin.
Expand Down
38 changes: 27 additions & 11 deletions lib/net/http_fetch_plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ shaka.net.HttpFetchPlugin = class {
* @param {shaka.net.NetworkingEngine.RequestType} requestType
* @param {shaka.extern.ProgressUpdated} progressUpdated Called when a
* progress event happened.
* @param {shaka.extern.HeadersReceived} headersReceived Called when the
* headers for the download are received, but before the body is.
* @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
* @export
*/
static parse(uri, request, requestType, progressUpdated) {
static parse(uri, request, requestType, progressUpdated, headersReceived) {
const headers = new shaka.net.HttpFetchPlugin.Headers_();
shaka.util.MapUtils.asMap(request.headers).forEach((value, key) => {
headers.append(key, value);
Expand All @@ -55,7 +57,7 @@ shaka.net.HttpFetchPlugin = class {
};

const pendingRequest = shaka.net.HttpFetchPlugin.request_(
uri, requestType, init, abortStatus, progressUpdated,
uri, requestType, init, abortStatus, progressUpdated, headersReceived,
request.streamDataCallback);

/** @type {!shaka.util.AbortableOperation} */
Expand Down Expand Up @@ -92,12 +94,13 @@ shaka.net.HttpFetchPlugin = class {
* @param {!RequestInit} init
* @param {shaka.net.HttpFetchPlugin.AbortStatus} abortStatus
* @param {shaka.extern.ProgressUpdated} progressUpdated
* @param {shaka.extern.HeadersReceived} headersReceived
* @param {?function(BufferSource):!Promise} streamDataCallback
* @return {!Promise<!shaka.extern.Response>}
* @private
*/
static async request_(uri, requestType, init, abortStatus, progressUpdated,
streamDataCallback) {
headersReceived, streamDataCallback) {
const fetch = shaka.net.HttpFetchPlugin.fetch_;
const ReadableStream = shaka.net.HttpFetchPlugin.ReadableStream_;
let response;
Expand All @@ -113,6 +116,10 @@ shaka.net.HttpFetchPlugin = class {
// headers are available. The download itself isn't done until the promise
// for retrieving the data (arrayBuffer, blob, etc) has resolved.
response = await fetch(uri, init);
// At this point in the process, we have the headers of the response, but
// not the body yet.
headersReceived(shaka.net.HttpFetchPlugin.headersToGenericObject_(
response.headers));
// Getting the reader in this way allows us to observe the process of
// downloading the body, instead of just waiting for an opaque promise to
// resolve.
Expand Down Expand Up @@ -194,19 +201,28 @@ shaka.net.HttpFetchPlugin = class {
}
}

const headers = {};
/** @type {Headers} */
const responseHeaders = response.headers;
responseHeaders.forEach((value, key) => {
// Since Edge incorrectly return the header with a leading new line
// character ('\n'), we trim the header here.
headers[key.trim()] = value;
});
const headers = shaka.net.HttpFetchPlugin.headersToGenericObject_(
response.headers);

return shaka.net.HttpPluginUtils.makeResponse(
headers, arrayBuffer, response.status, uri, response.url, requestType);
}

/**
* @param {!Headers} headers
* @return {!Object.<string, string>}
* @private
*/
static headersToGenericObject_(headers) {
const headersObj = {};
headers.forEach((value, key) => {
// Since Edge incorrectly return the header with a leading new line
// character ('\n'), we trim the header here.
headersObj[key.trim()] = value;
});
return headersObj;
}

/**
* Determine if the Fetch API is supported in the browser. Note: this is
* deliberately exposed as a method to allow the client app to use the same
Expand Down
50 changes: 36 additions & 14 deletions lib/net/http_xhr_plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ shaka.net.HttpXHRPlugin = class {
* @param {shaka.net.NetworkingEngine.RequestType} requestType
* @param {shaka.extern.ProgressUpdated} progressUpdated Called when a
* progress event happened.
* @param {shaka.extern.HeadersReceived} headersReceived Called when the
* headers for the download are received, but before the body is.
* @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
* @export
*/
static parse(uri, request, requestType, progressUpdated) {
static parse(uri, request, requestType, progressUpdated, headersReceived) {
const xhr = new shaka.net.HttpXHRPlugin.Xhr_();

// Last time stamp when we got a progress event.
Expand All @@ -48,23 +50,25 @@ shaka.net.HttpXHRPlugin = class {
shaka.util.Error.Code.OPERATION_ABORTED,
uri, requestType));
};
xhr.onload = (event) => {
const target = event.target;
goog.asserts.assert(target, 'XHR onload has no target!');
// Since Edge incorrectly return the header with a leading new
// line character ('\n'), we trim the header here.
const headerLines = target.getAllResponseHeaders().trim().split('\r\n');
const headers = {};
for (const header of headerLines) {
/** @type {!Array.<string>} */
const parts = header.split(': ');
headers[parts[0].toLowerCase()] = parts.slice(1).join(': ');
let calledHeadersReceived = false;
xhr.onreadystatechange = (event) => {
// See if the readyState is 2 ("HEADERS_RECEIVED").
if (xhr.readyState == 2 && !calledHeadersReceived) {
const headers = shaka.net.HttpXHRPlugin.headersToGenericObject_(xhr);
headersReceived(headers);
// Don't send out this event twice.
calledHeadersReceived = true;
}
};
xhr.onload = (event) => {
const headers = shaka.net.HttpXHRPlugin.headersToGenericObject_(xhr);
goog.asserts.assert(xhr.response instanceof ArrayBuffer,
'XHR should have a response by now!');
const xhrResponse = xhr.response;

try {
const response = shaka.net.HttpPluginUtils.makeResponse(headers,
target.response, target.status, uri, target.responseURL,
requestType);
xhrResponse, xhr.status, uri, xhr.responseURL, requestType);
resolve(response);
} catch (error) {
goog.asserts.assert(error instanceof shaka.util.Error,
Expand Down Expand Up @@ -116,6 +120,24 @@ shaka.net.HttpXHRPlugin = class {
return Promise.resolve();
});
}

/**
* @param {!XMLHttpRequest} xhr
* @return {!Object.<string, string>}
* @private
*/
static headersToGenericObject_(xhr) {
// Since Edge incorrectly return the header with a leading new
// line character ('\n'), we trim the header here.
const headerLines = xhr.getAllResponseHeaders().trim().split('\r\n');
const headers = {};
for (const header of headerLines) {
/** @type {!Array.<string>} */
const parts = header.split(': ');
headers[parts[0].toLowerCase()] = parts.slice(1).join(': ');
}
return headers;
}
};


Expand Down
99 changes: 81 additions & 18 deletions lib/net/networking_engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget {
* @param {function(number, number)=} onProgressUpdated Called when a progress
* event is triggered. Passed the duration, in milliseconds, that the
* request took, and the number of bytes transferred.
* @param {shaka.net.NetworkingEngine.OnHeadersReceived=} onHeadersReceived
* Called when the headers are received for a download.
* @param {shaka.net.NetworkingEngine.OnDownloadFailed=} onDownloadFailed
* Called when a download fails, for any reason.
*/
constructor(onProgressUpdated) {
constructor(onProgressUpdated, onHeadersReceived, onDownloadFailed) {
super();

/** @private {boolean} */
Expand All @@ -67,6 +71,12 @@ shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget {
/** @private {?function(number, number)} */
this.onProgressUpdated_ = onProgressUpdated || null;

/** @private {?shaka.net.NetworkingEngine.OnHeadersReceived} */
this.onHeadersReceived_ = onHeadersReceived || null;

/** @private {?shaka.net.NetworkingEngine.OnDownloadFailed} */
this.onDownloadFailed_ = onDownloadFailed || null;

/** @private {boolean} */
this.forceHTTPS_ = false;
}
Expand Down Expand Up @@ -447,6 +457,8 @@ shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget {

let aborted = false;

let headersReceivedCalled = false;

let startTimeMs;
const sendOperation = backoffOperation.chain(() => {
if (this.destroyed_) {
Expand All @@ -456,23 +468,27 @@ shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget {
startTimeMs = Date.now();
const segment = shaka.net.NetworkingEngine.RequestType.SEGMENT;

const requestPlugin = plugin(request.uris[index],
request,
type,
// The following function is passed to plugin.
(time, bytes, numBytesRemaining) => {
if (connectionTimer) {
connectionTimer.stop();
}
if (stallTimer) {
stallTimer.tickAfter(stallTimeoutMs / 1000);
}
if (this.onProgressUpdated_ && type == segment) {
this.onProgressUpdated_(time, bytes);
gotProgress = true;
numBytesRemainingObj.setBytes(numBytesRemaining);
}
});
const progressUpdated = (time, bytes, numBytesRemaining) => {
if (connectionTimer) {
connectionTimer.stop();
}
if (stallTimer) {
stallTimer.tickAfter(stallTimeoutMs / 1000);
}
if (this.onProgressUpdated_ && type == segment) {
this.onProgressUpdated_(time, bytes);
gotProgress = true;
numBytesRemainingObj.setBytes(numBytesRemaining);
}
};
const headersReceived = (headers) => {
if (this.onHeadersReceived_) {
this.onHeadersReceived_(headers, request, type);
}
headersReceivedCalled = true;
};
const requestPlugin = plugin(
request.uris[index], request, type, progressUpdated, headersReceived);

if (!progressSupport) {
return requestPlugin;
Expand Down Expand Up @@ -511,6 +527,13 @@ shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget {
response: response,
gotProgress: gotProgress,
};
if (!headersReceivedCalled) {
// The plugin did not call headersReceived, perhaps because it is not
// able to track that information. So, fire the event manually.
if (this.onHeadersReceived_) {
this.onHeadersReceived_(response.headers, request, type);
}
}

return responseAndGotProgress;
}, (error) => {
Expand All @@ -520,6 +543,17 @@ shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget {
if (stallTimer) {
stallTimer.stop();
}
if (this.onDownloadFailed_) {
let shakaError = null;
let httpResponseCode = 0;
if (error instanceof shaka.util.Error) {
shakaError = error;
if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS) {
httpResponseCode = /** @type {number} */ (error.data[1]);
}
}
this.onDownloadFailed_(request, shakaError, httpResponseCode, aborted);
}
if (this.destroyed_) {
return shaka.util.AbortableOperation.aborted();
}
Expand Down Expand Up @@ -763,3 +797,32 @@ shaka.net.NetworkingEngine.schemes_ = {};
* @private
*/
shaka.net.NetworkingEngine.ResponseAndGotProgress;


/**
* @typedef {function(
* !Object.<string, string>,
* !shaka.extern.Request,
* !shaka.net.NetworkingEngine.RequestType)}
*
* @description
* A callback function that passes the shaka.extern.HeadersReceived along to
* the player, plus some extra data.
* @export
*/
shaka.net.NetworkingEngine.OnHeadersReceived;


/**
* @typedef {function(
* !shaka.extern.Request,
* ?shaka.util.Error,
* number,
* boolean)}
*
* @description
* A callback function that notifies the player when a download fails, for any
* reason (e.g. even if the download was aborted).
* @export
*/
shaka.net.NetworkingEngine.OnDownloadFailed;
Loading

0 comments on commit 7893b77

Please sign in to comment.