/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.offline.OfflineScheme');
goog.require('goog.asserts');
goog.require('shaka.net.NetworkingEngine');
goog.require('shaka.offline.OfflineUri');
goog.require('shaka.offline.Storage');
goog.require('shaka.offline.StorageMuxer');
goog.require('shaka.util.AbortableOperation');
goog.require('shaka.util.Error');
goog.require('shaka.util.Timer');
/**
* @summary A plugin that handles requests for offline content.
* @export
*/
shaka.offline.OfflineScheme = class {
/**
* @param {string} uri
* @param {shaka.extern.Request} request
* @param {shaka.net.NetworkingEngine.RequestType} requestType
* @param {shaka.extern.ProgressUpdated} progressUpdated Called when a
* progress event happened.
* @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
* @export
*/
static plugin(uri, request, requestType, progressUpdated) {
const offlineUri = shaka.offline.OfflineUri.parse(uri);
if (offlineUri && offlineUri.isManifest()) {
return shaka.offline.OfflineScheme.getManifest_(uri, request);
}
if (offlineUri && offlineUri.isSegment()) {
return shaka.offline.OfflineScheme.getSegment_(
offlineUri.key(), offlineUri, request);
}
if (offlineUri && offlineUri.isPendingSegment()) {
return shaka.offline.OfflineScheme.getPendingSegment_(
offlineUri, request);
}
return shaka.util.AbortableOperation.failed(
new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.NETWORK,
shaka.util.Error.Code.MALFORMED_OFFLINE_URI,
uri));
}
/**
* @param {string} uri
* @param {shaka.extern.Request} request
* @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
* @private
*/
static getManifest_(uri, request) {
/** @type {shaka.extern.Response} */
const response = {
uri: uri,
originalUri: uri,
data: new ArrayBuffer(0),
headers: {'content-type': 'application/x-offline-manifest'},
originalRequest: request,
};
return shaka.util.AbortableOperation.completed(response);
}
/**
* @param {number} id
* @param {!shaka.offline.OfflineUri} uri
* @param {shaka.extern.Request} request
* @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
* @private
*/
static getSegment_(id, uri, request) {
goog.asserts.assert(
uri.isSegment(),
'Only segment uri\'s should be given to getSegment');
/** @type {!shaka.offline.StorageMuxer} */
const muxer = new shaka.offline.StorageMuxer();
return shaka.util.AbortableOperation.completed(undefined)
.chain(() => muxer.init())
.chain(() => muxer.getCell(uri.mechanism(), uri.cell()))
.chain((cell) => cell.getSegments([uri.key()]))
.chain((segments) => {
const segment = segments[0];
return {
uri: uri,
data: segment.data,
headers: {},
originalRequest: request,
};
})
.finally(() => muxer.destroy());
}
/**
* Waits for a segment of content that is being downloaded to be stored, and
* returns it.
*
* @param {!shaka.offline.OfflineUri} uri
* @param {shaka.extern.Request} request
* @return {!shaka.extern.IAbortableOperation.<shaka.extern.Response>}
* @private
*/
static getPendingSegment_(uri, request) {
const pendingSegment = uri.pendingSegment();
goog.asserts.assert(pendingSegment, 'Not a pending segment uri');
const contentUri = uri.manifestOfPendingSegment().toString();
let aborted = false;
/** @type {?function()} */
let stopWaiting = null;
const getSegment = async () => {
/** @type {!shaka.offline.StorageMuxer} */
const muxer = new shaka.offline.StorageMuxer();
try {
await muxer.init();
const cell = await muxer.getCell(uri.mechanism(), uri.cell());
/* eslint-disable no-await-in-loop */
for (;;) {
if (aborted) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.PLAYER,
shaka.util.Error.Code.OPERATION_ABORTED);
}
// Check this before reading the manifest. A download assigns all
// the segments it stored to the manifest before it ends, so if it
// has ended by the time the manifest is read, the manifest has them.
const downloading =
await shaka.offline.Storage.isDownloading(contentUri);
const manifests = await cell.getManifests([uri.key()]);
const key = shaka.offline.OfflineScheme.getStoredKey_(
manifests[0], pendingSegment, uri);
if (key != null) {
const segments = await cell.getSegments([key]);
/** @type {shaka.extern.Response} */
const response = {
uri: uri.toString(),
originalUri: uri.toString(),
data: segments[0].data,
headers: {},
originalRequest: request,
};
return response;
}
if (!downloading) {
throw new shaka.util.Error(
shaka.util.Error.Severity.RECOVERABLE,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.SEGMENT_NOT_STORED,
contentUri);
}
await new Promise((resolve) => {
if (aborted) {
// Aborted while reading the manifest.
resolve();
return;
}
const timer = new shaka.util.Timer(resolve);
stopWaiting = () => {
timer.stop();
resolve();
};
timer.tickAfter(
shaka.offline.OfflineScheme.PENDING_SEGMENT_POLL_INTERVAL_);
});
stopWaiting = null;
}
/* eslint-enable no-await-in-loop */
} finally {
await muxer.destroy();
}
};
return new shaka.util.AbortableOperation(getSegment(), () => {
aborted = true;
if (stopWaiting) {
stopWaiting();
}
return Promise.resolve();
});
}
/**
* @param {shaka.extern.ManifestDB} manifestDB
* @param {?shaka.offline.OfflineUri.PendingSegment} pendingSegment
* @param {!shaka.offline.OfflineUri} uri
* @return {?number} The key the segment was stored with, or null if it has
* not been stored yet.
* @private
*/
static getStoredKey_(manifestDB, pendingSegment, uri) {
goog.asserts.assert(pendingSegment, 'Not a pending segment uri');
const stream = manifestDB.streams.find((streamDB) => {
return streamDB.id == pendingSegment.streamId;
});
const segment = stream ? stream.segments[pendingSegment.position] : null;
if (!segment) {
throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.STORAGE,
shaka.util.Error.Code.REQUESTED_ITEM_NOT_FOUND,
uri.toString());
}
if (pendingSegment.isInit) {
return segment.pendingInitSegmentRefId ? null : segment.initSegmentKey;
}
return segment.pendingSegmentRefId ? null : segment.dataKey;
}
};
/**
* How often to check whether a segment that is being downloaded has been
* stored, in seconds.
*
* @const {number}
* @private
*/
shaka.offline.OfflineScheme.PENDING_SEGMENT_POLL_INTERVAL_ = 0.5;
shaka.net.NetworkingEngine.registerScheme(
'offline', shaka.offline.OfflineScheme.plugin);