1
0
Fork 0
mirror of https://github.com/DanielnetoDotCom/YouPHPTube synced 2025-10-05 10:49:36 +02:00
This commit is contained in:
Daniel Neto 2024-02-08 10:08:03 -03:00
parent c940cd61ac
commit 59a20745e7
2101 changed files with 1312074 additions and 30292 deletions

View file

@ -28,6 +28,7 @@ import logger from './util/logger';
import {merge, createTimeRanges} from './util/vjs-compat';
import { addMetadata, createMetadataTrackIfNotExists, addDateRangeMetadata } from './util/text-tracks';
import ContentSteeringController from './content-steering-controller';
import { bufferToHexString } from './util/string.js';
const ABORT_EARLY_EXCLUSION_SECONDS = 10;
@ -235,12 +236,12 @@ export class PlaylistController extends videojs.EventTarget {
this.sourceUpdater_ = new SourceUpdater(this.mediaSource);
this.inbandTextTracks_ = {};
this.timelineChangeController_ = new TimelineChangeController();
this.keyStatusMap_ = new Map();
const segmentLoaderSettings = {
vhs: this.vhs_,
parse708captions: options.parse708captions,
useDtsForTimestampOffset: options.useDtsForTimestampOffset,
calculateTimestampOffsetForEachSegment: options.calculateTimestampOffsetForEachSegment,
captionServices,
mediaSource: this.mediaSource,
currentTime: this.tech_.currentTime.bind(this.tech_),
@ -404,7 +405,7 @@ export class PlaylistController extends videojs.EventTarget {
switchMedia_(playlist, cause, delay) {
const oldMedia = this.media();
const oldId = oldMedia && (oldMedia.id || oldMedia.uri);
const newId = playlist.id || playlist.uri;
const newId = playlist && (playlist.id || playlist.uri);
if (oldId && oldId !== newId) {
this.logger_(`switch media ${oldId} -> ${newId} from ${cause}`);
@ -608,6 +609,8 @@ export class PlaylistController extends videojs.EventTarget {
let updatedPlaylist = this.mainPlaylistLoader_.media();
if (!updatedPlaylist) {
// Add content steering listeners on first load and init.
this.attachContentSteeringListeners_();
this.initContentSteeringController_();
// exclude any variants that are not supported by the browser before selecting
// an initial media as the playlist selectors do not consider browser support
@ -671,15 +674,23 @@ export class PlaylistController extends videojs.EventTarget {
this.requestOptions_.timeout = requestTimeout;
}
this.mainPlaylistLoader_.load();
if (this.sourceType_ === 'dash') {
// we don't want to re-request the same hls playlist right after it was changed
this.mainPlaylistLoader_.load();
}
// TODO: Create a new event on the PlaylistLoader that signals
// that the segments have changed in some way and use that to
// update the SegmentLoader instead of doing it twice here and
// on `loadedplaylist`
this.mainSegmentLoader_.pause();
this.mainSegmentLoader_.playlist(media, this.requestOptions_);
this.mainSegmentLoader_.load();
if (this.waitingForFastQualityPlaylistReceived_) {
this.runFastQualitySwitch_();
} else {
this.mainSegmentLoader_.load();
}
this.tech_.trigger({
type: 'mediachange',
@ -741,7 +752,12 @@ export class PlaylistController extends videojs.EventTarget {
// that the segments have changed in some way and use that to
// update the SegmentLoader instead of doing it twice here and
// on `mediachange`
this.mainSegmentLoader_.pause();
this.mainSegmentLoader_.playlist(updatedPlaylist, this.requestOptions_);
if (this.waitingForFastQualityPlaylistReceived_) {
this.runFastQualitySwitch_();
}
this.updateDuration(!updatedPlaylist.endList);
// If the player isn't paused, ensure that the segment loader is running,
@ -956,39 +972,39 @@ export class PlaylistController extends videojs.EventTarget {
/**
* Re-tune playback quality level for the current player
* conditions. This will reset the main segment loader
* and the next segment position to the currentTime.
* This is good for manual quality changes.
* conditions. This method will perform destructive actions like removing
* already buffered content in order to readjust the currently active
* playlist quickly. This is good for manual quality changes
*
* @private
*/
fastQualityChange_(media = this.selectPlaylist()) {
if (media === this.mainPlaylistLoader_.media()) {
if (media && media === this.mainPlaylistLoader_.media()) {
this.logger_('skipping fastQualityChange because new media is same as old');
return;
}
this.switchMedia_(media, 'fast-quality');
// Reset main segment loader properties and next segment position information.
// Don't need to reset audio as it is reset when media changes.
// We resetLoaderProperties separately here as we want to fetch init segments if
// necessary and ensure we're not in an ended state when we switch playlists.
this.resetMainLoaderReplaceSegments();
// we would like to avoid race condition when we call fastQuality,
// reset everything and start loading segments from prev segments instead of new because new playlist is not received yet
this.waitingForFastQualityPlaylistReceived_ = true;
}
/**
* Sets the replaceUntil flag on the main segment soader to the buffered end
* and resets the main segment loaders properties.
*/
resetMainLoaderReplaceSegments() {
const buffered = this.tech_.buffered();
const bufferedEnd = buffered.end(buffered.length - 1);
runFastQualitySwitch_() {
this.waitingForFastQualityPlaylistReceived_ = false;
// Delete all buffered data to allow an immediate quality switch, then seek to give
// the browser a kick to remove any cached frames from the previous rendtion (.04 seconds
// ahead was roughly the minimum that will accomplish this across a variety of content
// in IE and Edge, but seeking in place is sufficient on all other browsers)
// Edge/IE bug: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/14600375/
// Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=651904
this.mainSegmentLoader_.pause();
this.mainSegmentLoader_.resetEverything(() => {
this.tech_.setCurrentTime(this.tech_.currentTime());
});
// Set the replace segments flag to the buffered end, this forces fetchAtBuffer
// on the main loader to remain, false after the resetLoader call, until we have
// replaced all content buffered ahead of the currentTime.
this.mainSegmentLoader_.replaceSegmentsUntil = bufferedEnd;
this.mainSegmentLoader_.resetLoaderProperties();
this.mainSegmentLoader_.resetLoader();
// don't need to reset audio as it is reset when media changes
}
/**
@ -1450,11 +1466,14 @@ export class PlaylistController extends videojs.EventTarget {
// cancel outstanding requests so we begin buffering at the new
// location
this.mainSegmentLoader_.pause();
this.mainSegmentLoader_.resetEverything();
if (this.mediaTypes_.AUDIO.activePlaylistLoader) {
this.audioSegmentLoader_.pause();
this.audioSegmentLoader_.resetEverything();
}
if (this.mediaTypes_.SUBTITLES.activePlaylistLoader) {
this.subtitleSegmentLoader_.pause();
this.subtitleSegmentLoader_.resetEverything();
}
@ -1691,6 +1710,7 @@ export class PlaylistController extends videojs.EventTarget {
this.mainPlaylistLoader_.dispose();
this.mainSegmentLoader_.dispose();
this.contentSteeringController_.dispose();
this.keyStatusMap_.clear();
if (this.loadOnPlay_) {
this.tech_.off('play', this.loadOnPlay_);
@ -2104,51 +2124,84 @@ export class PlaylistController extends videojs.EventTarget {
});
}
/**
* Utility for getting the pathway or service location from an HLS or DASH playlist.
*
* @param {Object} playlist for getting pathway from.
* @return the pathway attribute of a playlist
*/
pathwayAttribute_(playlist) {
return playlist.attributes['PATHWAY-ID'] || playlist.attributes.serviceLocation;
}
/**
* Initialize content steering listeners and apply the tag properties.
* Initialize available pathways and apply the tag properties.
*/
initContentSteeringController_() {
const initialMain = this.main();
const main = this.main();
if (!initialMain.contentSteering) {
if (!main.contentSteering) {
return;
}
const updateSteeringValues = (main) => {
for (const playlist of main.playlists) {
this.contentSteeringController_.addAvailablePathway(this.pathwayAttribute_(playlist));
}
this.contentSteeringController_.assignTagProperties(main.uri, main.contentSteering);
};
updateSteeringValues(initialMain);
this.contentSteeringController_.on('content-steering', this.excludeThenChangePathway_.bind(this));
// We need to ensure we update the content steering values when a new
// manifest is loaded in live DASH with content steering.
if (this.sourceType_ === 'dash') {
this.mainPlaylistLoader_.on('mediaupdatetimeout', () => {
this.mainPlaylistLoader_.refreshMedia_(this.mainPlaylistLoader_.media().id);
// clear past values
this.contentSteeringController_.abort();
this.contentSteeringController_.clearTTLTimeout_();
this.contentSteeringController_.clearAvailablePathways();
updateSteeringValues(this.main());
});
for (const playlist of main.playlists) {
this.contentSteeringController_.addAvailablePathway(this.pathwayAttribute_(playlist));
}
this.contentSteeringController_.assignTagProperties(main.uri, main.contentSteering);
// request the steering manifest immediately if queryBeforeStart is set.
if (this.contentSteeringController_.queryBeforeStart) {
// When queryBeforeStart is true, initial request should omit steering parameters.
this.contentSteeringController_.requestSteeringManifest(true);
return;
}
// otherwise start content steering after playback starts
this.tech_.one('canplay', () => {
this.contentSteeringController_.requestSteeringManifest();
});
}
// Do this at startup only, after that the steering requests are managed by the Content Steering class.
// DASH queryBeforeStart scenarios will be handled by the Content Steering class.
if (!this.contentSteeringController_.queryBeforeStart) {
this.tech_.one('canplay', () => {
this.contentSteeringController_.requestSteeringManifest();
/**
* Reset the content steering controller and re-init.
*/
resetContentSteeringController_() {
this.contentSteeringController_.clearAvailablePathways();
this.contentSteeringController_.dispose();
this.initContentSteeringController_();
}
/**
* Attaches the listeners for content steering.
*/
attachContentSteeringListeners_() {
this.contentSteeringController_.on('content-steering', this.excludeThenChangePathway_.bind(this));
if (this.sourceType_ === 'dash') {
this.mainPlaylistLoader_.on('loadedplaylist', () => {
const main = this.main();
// check if steering tag or pathways changed.
const didDashTagChange = this.contentSteeringController_.didDASHTagChange(main.uri, main.contentSteering);
const didPathwaysChange = () => {
const availablePathways = this.contentSteeringController_.getAvailablePathways();
const newPathways = [];
for (const playlist of main.playlists) {
const serviceLocation = playlist.attributes.serviceLocation;
if (serviceLocation) {
newPathways.push(serviceLocation);
if (!availablePathways.has(serviceLocation)) {
return true;
}
}
}
// If we have no new serviceLocations and previously had availablePathways
if (!newPathways.length && availablePathways.size) {
return true;
}
return false;
};
if (didDashTagChange || didPathwaysChange()) {
this.resetContentSteeringController_();
}
});
}
}
@ -2162,6 +2215,9 @@ export class PlaylistController extends videojs.EventTarget {
if (!currentPathway) {
return;
}
this.handlePathwayClones_();
const main = this.main();
const playlists = main.playlists;
const ids = new Set();
@ -2211,6 +2267,106 @@ export class PlaylistController extends videojs.EventTarget {
}
}
/**
* Add, update, or delete playlists and media groups for
* the pathway clones for HLS Content Steering.
*
* See https://datatracker.ietf.org/doc/draft-pantos-hls-rfc8216bis/
*
* NOTE: Pathway cloning does not currently support the `PER_VARIANT_URIS` and
* `PER_RENDITION_URIS` as we do not handle `STABLE-VARIANT-ID` or
* `STABLE-RENDITION-ID` values.
*/
handlePathwayClones_() {
const main = this.main();
const playlists = main.playlists;
const currentPathwayClones = this.contentSteeringController_.currentPathwayClones;
const nextPathwayClones = this.contentSteeringController_.nextPathwayClones;
const hasClones = (currentPathwayClones && currentPathwayClones.size) || (nextPathwayClones && nextPathwayClones.size);
if (!hasClones) {
return;
}
for (const [id, clone] of currentPathwayClones.entries()) {
const newClone = nextPathwayClones.get(id);
// Delete the old pathway clone.
if (!newClone) {
this.mainPlaylistLoader_.updateOrDeleteClone(clone);
this.contentSteeringController_.excludePathway(id);
}
}
for (const [id, clone] of nextPathwayClones.entries()) {
const oldClone = currentPathwayClones.get(id);
// Create a new pathway if it is a new pathway clone object.
if (!oldClone) {
const playlistsToClone = playlists.filter(p => {
return p.attributes['PATHWAY-ID'] === clone['BASE-ID'];
});
playlistsToClone.forEach((p) => {
this.mainPlaylistLoader_.addClonePathway(clone, p);
});
this.contentSteeringController_.addAvailablePathway(id);
continue;
}
// There have not been changes to the pathway clone object, so skip.
if (this.equalPathwayClones_(oldClone, clone)) {
continue;
}
// Update a preexisting cloned pathway.
// True is set for the update flag.
this.mainPlaylistLoader_.updateOrDeleteClone(clone, true);
this.contentSteeringController_.addAvailablePathway(id);
}
// Deep copy contents of next to current pathways.
this.contentSteeringController_.currentPathwayClones = new Map(JSON.parse(JSON.stringify([...nextPathwayClones])));
}
/**
* Determines whether two pathway clone objects are equivalent.
*
* @param {Object} a The first pathway clone object.
* @param {Object} b The second pathway clone object.
* @return {boolean} True if the pathway clone objects are equal, false otherwise.
*/
equalPathwayClones_(a, b) {
if (
a['BASE-ID'] !== b['BASE-ID'] ||
a.ID !== b.ID ||
a['URI-REPLACEMENT'].HOST !== b['URI-REPLACEMENT'].HOST
) {
return false;
}
const aParams = a['URI-REPLACEMENT'].PARAMS;
const bParams = b['URI-REPLACEMENT'].PARAMS;
// We need to iterate through both lists of params because one could be
// missing a parameter that the other has.
for (const p in aParams) {
if (aParams[p] !== bParams[p]) {
return false;
}
}
for (const p in bParams) {
if (aParams[p] !== bParams[p]) {
return false;
}
}
return true;
}
/**
* Changes the current playlists for audio, video and subtitles after a new pathway
* is chosen from content steering.
@ -2227,4 +2383,97 @@ export class PlaylistController extends videojs.EventTarget {
this.switchMedia_(nextPlaylist, 'content-steering');
}
/**
* Iterates through playlists and check their keyId set and compare with the
* keyStatusMap, only enable playlists that have a usable key. If the playlist
* has no keyId leave it enabled by default.
*/
excludeNonUsablePlaylistsByKeyId_() {
if (!this.mainPlaylistLoader_ || !this.mainPlaylistLoader_.main) {
return;
}
let nonUsableKeyStatusCount = 0;
const NON_USABLE = 'non-usable';
this.mainPlaylistLoader_.main.playlists.forEach((playlist) => {
const keyIdSet = this.mainPlaylistLoader_.getKeyIdSet(playlist);
// If the playlist doesn't have keyIDs lets not exclude it.
if (!keyIdSet || !keyIdSet.size) {
return;
}
keyIdSet.forEach((key) => {
const USABLE = 'usable';
const hasUsableKeyStatus = this.keyStatusMap_.has(key) && this.keyStatusMap_.get(key) === USABLE;
const nonUsableExclusion = playlist.lastExcludeReason_ === NON_USABLE && playlist.excludeUntil === Infinity;
if (!hasUsableKeyStatus) {
// Only exclude playlists that haven't already been excluded as non-usable.
if (playlist.excludeUntil !== Infinity && playlist.lastExcludeReason_ !== NON_USABLE) {
playlist.excludeUntil = Infinity;
playlist.lastExcludeReason_ = NON_USABLE;
this.logger_(`excluding playlist ${playlist.id} because the key ID ${key} doesn't exist in the keyStatusMap or is not ${USABLE}`);
}
// count all nonUsableKeyStatus
nonUsableKeyStatusCount++;
} else if (hasUsableKeyStatus && nonUsableExclusion) {
delete playlist.excludeUntil;
delete playlist.lastExcludeReason_;
this.logger_(`enabling playlist ${playlist.id} because key ID ${key} is ${USABLE}`);
}
});
});
// If for whatever reason every playlist has a non usable key status. Lets try re-including the SD renditions as a failsafe.
if (nonUsableKeyStatusCount >= this.mainPlaylistLoader_.main.playlists.length) {
this.mainPlaylistLoader_.main.playlists.forEach((playlist) => {
const isNonHD = playlist && playlist.attributes && playlist.attributes.RESOLUTION && playlist.attributes.RESOLUTION.height < 720;
const excludedForNonUsableKey = playlist.excludeUntil === Infinity && playlist.lastExcludeReason_ === NON_USABLE;
if (isNonHD && excludedForNonUsableKey) {
// Only delete the excludeUntil so we don't try and re-exclude these playlists.
delete playlist.excludeUntil;
videojs.log.warn(`enabling non-HD playlist ${playlist.id} because all playlists were excluded due to ${NON_USABLE} key IDs`);
}
});
}
}
/**
* Adds a keystatus to the keystatus map, tries to convert to string if necessary.
*
* @param {any} keyId the keyId to add a status for
* @param {string} status the status of the keyId
*/
addKeyStatus_(keyId, status) {
const isString = typeof keyId === 'string';
const keyIdHexString = isString ? keyId : bufferToHexString(keyId);
const formattedKeyIdString = keyIdHexString.slice(0, 32).toLowerCase();
this.logger_(`KeyStatus '${status}' with key ID ${formattedKeyIdString} added to the keyStatusMap`);
this.keyStatusMap_.set(formattedKeyIdString, status);
}
/**
* Utility function for adding key status to the keyStatusMap and filtering usable encrypted playlists.
*
* @param {any} keyId the keyId from the keystatuschange event
* @param {string} status the key status string
*/
updatePlaylistByKeyStatus(keyId, status) {
this.addKeyStatus_(keyId, status);
if (!this.waitingForFastQualityPlaylistReceived_) {
this.excludeNonUsableThenChangePlaylist_();
}
// Listen to loadedplaylist with a single listener and check for new contentProtection elements when a playlist is updated.
this.mainPlaylistLoader_.off('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this));
this.mainPlaylistLoader_.on('loadedplaylist', this.excludeNonUsableThenChangePlaylist_.bind(this));
}
excludeNonUsableThenChangePlaylist_() {
this.excludeNonUsablePlaylistsByKeyId_();
this.fastQualityChange_();
}
}