1
0
Fork 0
mirror of https://github.com/DanielnetoDotCom/YouPHPTube synced 2025-10-05 02:39:46 +02:00

Update npm

This commit is contained in:
Daniel Neto 2024-04-03 15:54:35 -03:00
parent 8341712d58
commit 1bd85100b9
5320 changed files with 58396 additions and 344722 deletions

View file

@ -1,3 +1,52 @@
<a name="3.12.0"></a>
# [3.12.0](https://github.com/videojs/http-streaming/compare/v3.11.3...v3.12.0) (2024-03-12)
### Features
* Custom Pixel Ratio ([#1497](https://github.com/videojs/http-streaming/issues/1497)) ([0e9d9d8](https://github.com/videojs/http-streaming/commit/0e9d9d8))
### Chores
* **demo:** Remove error on iOS on demo page ([#1493](https://github.com/videojs/http-streaming/issues/1493)) ([c50ba7e](https://github.com/videojs/http-streaming/commit/c50ba7e))
* update mux.js to v7.0.3 ([#1498](https://github.com/videojs/http-streaming/issues/1498)) ([bebcafd](https://github.com/videojs/http-streaming/commit/bebcafd))
<a name="3.11.3"></a>
## [3.11.3](https://github.com/videojs/http-streaming/compare/v3.11.2...v3.11.3) (2024-02-28)
### Bug Fixes
* fix repeated segments ([#1489](https://github.com/videojs/http-streaming/issues/1489)) ([ed8f6bd](https://github.com/videojs/http-streaming/commit/ed8f6bd))
<a name="3.11.2"></a>
## [3.11.2](https://github.com/videojs/http-streaming/compare/v3.11.1...v3.11.2) (2024-02-21)
### Reverts
* "fix: fix repeated segments issue during bandwidth update ([#1477](https://github.com/videojs/http-streaming/issues/1477))" ([#1488](https://github.com/videojs/http-streaming/issues/1488)) ([75f7b1a](https://github.com/videojs/http-streaming/commit/75f7b1a))
<a name="3.11.1"></a>
## [3.11.1](https://github.com/videojs/http-streaming/compare/v3.11.0...v3.11.1) (2024-02-12)
### Bug Fixes
* changeType on full codec change only ([#1474](https://github.com/videojs/http-streaming/issues/1474)) ([4e51778](https://github.com/videojs/http-streaming/commit/4e51778))
### Chores
* Replace old quality selector ([#1482](https://github.com/videojs/http-streaming/issues/1482)) ([64376db](https://github.com/videojs/http-streaming/commit/64376db))
<a name="3.11.0"></a>
# [3.11.0](https://github.com/videojs/http-streaming/compare/v3.10.0...v3.11.0) (2024-01-25)
### Features
* add request types ([#1479](https://github.com/videojs/http-streaming/issues/1479)) ([5b87f69](https://github.com/videojs/http-streaming/commit/5b87f69))
* error type enhancement ([#1478](https://github.com/videojs/http-streaming/issues/1478)) ([8f3a4d1](https://github.com/videojs/http-streaming/commit/8f3a4d1))
### Bug Fixes
* fix repeated segments issue during bandwidth update ([#1477](https://github.com/videojs/http-streaming/issues/1477)) ([823f072](https://github.com/videojs/http-streaming/commit/823f072))
<a name="3.10.0"></a>
# [3.10.0](https://github.com/videojs/http-streaming/compare/v3.9.1...v3.10.0) (2024-01-17)

View file

@ -48,6 +48,7 @@ Video.js Compatibility: 7.x, 8.x
- [enableLowInitialPlaylist](#enablelowinitialplaylist)
- [limitRenditionByPlayerDimensions](#limitrenditionbyplayerdimensions)
- [useDevicePixelRatio](#usedevicepixelratio)
- [customPixelRatio](#custompixelratio)
- [allowSeeksWithinUnsafeLiveWindow](#allowseekswithinunsafelivewindow)
- [customTagParsers](#customtagparsers)
- [customTagMappers](#customtagmappers)
@ -404,6 +405,18 @@ This setting is `true` by default.
If true, this will take the device pixel ratio into account when doing rendition switching. This means that if you have a player with the width of `540px` in a high density display with a device pixel ratio of 2, a rendition of `1080p` will be allowed.
This setting is `false` by default.
##### customPixelRatio
* Type: `number`
* can be used as an initialization option.
If set, this will take the initial player dimensions and multiply it by a custom ratio when the player automatically selects renditions. This means that if you have a player where the dimension is `540p`, with a custom pixel ratio of `2`, a rendition of `1080p` or a lower rendition closest to this value will be chosen. Additionally, if you have a player where the dimension is `540p`, with a custom pixel ratio of `0.5`, a rendition of `270p` or a lower rendition closest to this value will be chosen. When the custom pixel ratio is 0, the lowest available rendition will be selected.
It is worth noting that if the player dimension multiplied by the custom pixel ratio is greater than any available rendition resolution, a rendition will be selected based on bandwidth, and the player dimension will be disregarded.
`limitRenditionByPlayerDimensions` must be `true` in order for this feature to be enabled. This is the default value.
If `useDevicePixelRatio` is set to `true`, the custom pixel ratio will be prioritized and overwrite any previous pixel ratio.
##### allowSeeksWithinUnsafeLiveWindow
* Type: `boolean`
* can be used as a source option

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,7 @@
<link rel="icon" href="logo.svg">
<link href="node_modules/bootstrap/dist/css/bootstrap.css" rel="stylesheet">
<link href="node_modules/video.js/dist/video-js.css" rel="stylesheet">
<link href="node_modules/videojs-http-source-selector/dist/videojs-http-source-selector.css" rel="stylesheet">
<link href="node_modules/jb-videojs-hls-quality-selector/dist/videojs-hls-quality-selector.css" rel="stylesheet">
<style>
.form-check {
background-color: hsl(0, 0%, 90%);

View file

@ -1,6 +1,6 @@
{
"name": "@videojs/http-streaming",
"version": "3.10.0",
"version": "3.12.0",
"description": "Play back HLS and DASH with Video.js, even where it's not natively supported",
"main": "dist/videojs-http-streaming.cjs.js",
"module": "dist/videojs-http-streaming.es.js",
@ -63,11 +63,11 @@
"global": "^4.4.0",
"m3u8-parser": "^7.1.0",
"mpd-parser": "^1.3.0",
"mux.js": "7.0.2",
"mux.js": "7.0.3",
"video.js": "^7 || ^8"
},
"peerDependencies": {
"video.js": "^7 || ^8"
"video.js": "^8.11.0"
},
"devDependencies": {
"@babel/cli": "^7.21.0",
@ -77,6 +77,7 @@
"@videojs/generator-helpers": "~3.1.0",
"bootstrap": "^5.1.0",
"d3": "^3.4.8",
"jb-videojs-hls-quality-selector": "^2.0.2",
"jsdoc": "^3.6.11",
"karma": "^6.4.0",
"lodash": "^4.17.4",
@ -92,7 +93,6 @@
"videojs-generate-karma-config": "^8.0.1",
"videojs-generate-rollup-config": "^7.0.0",
"videojs-generator-verify": "~3.0.1",
"videojs-http-source-selector": "^1.1.6",
"videojs-standard": "^9.0.0",
"water-plant-uml": "^2.0.2"
},

View file

@ -438,7 +438,7 @@
var steeringManifestEl = document.querySelector('.steering-manifest');
player.one('loadedmetadata', function() {
var steeringController = player.tech_.vhs.playlistController_.contentSteeringController_;
var steeringController = player.tech_.vhs && player.tech_.vhs.playlistController_.contentSteeringController_;
if (!steeringController) {
return;
@ -559,7 +559,8 @@
'node_modules/video.js/dist/alt/video.core',
'node_modules/videojs-contrib-eme/dist/videojs-contrib-eme',
'node_modules/videojs-contrib-quality-levels/dist/videojs-contrib-quality-levels',
'node_modules/videojs-http-source-selector/dist/videojs-http-source-selector'
'node_modules/jb-videojs-hls-quality-selector/dist/jb-videojs-hls-quality-selector'
].map(function(url) {
return url + (event.target.checked ? '.min' : '') + '.js';
});
@ -594,8 +595,8 @@
player = window.player = window.videojs(videoEl, {
plugins: {
httpSourceSelector: {
default: 'auto'
hlsQualitySelector: {
displayCurrentQuality: true
}
},
liveui: stateEls.liveui.checked,

View file

@ -10,9 +10,9 @@ const files = [
'node_modules/videojs-contrib-eme/dist/videojs-contrib-eme.min.js',
'node_modules/videojs-contrib-quality-levels/dist/videojs-contrib-quality-levels.js',
'node_modules/videojs-contrib-quality-levels/dist/videojs-contrib-quality-levels.min.js',
'node_modules/videojs-http-source-selector/dist/videojs-http-source-selector.css',
'node_modules/videojs-http-source-selector/dist/videojs-http-source-selector.js',
'node_modules/videojs-http-source-selector/dist/videojs-http-source-selector.min.js',
'node_modules/jb-videojs-hls-quality-selector/dist/videojs-hls-quality-selector.css',
'node_modules/jb-videojs-hls-quality-selector/dist/jb-videojs-hls-quality-selector.js',
'node_modules/jb-videojs-hls-quality-selector/dist/jb-videojs-hls-quality-selector.min.js',
'node_modules/bootstrap/dist/js/bootstrap.js',
'node_modules/bootstrap/dist/css/bootstrap.css',
'node_modules/d3/d3.min.js',

View file

@ -171,7 +171,8 @@ export default class ContentSteeringController extends videojs.EventTarget {
}
this.request_ = this.xhr_({
uri
uri,
requestType: 'content-steering-manifest'
}, (error, errorInfo) => {
if (error) {
// If the client receives HTTP 410 Gone in response to a manifest request,

View file

@ -361,7 +361,8 @@ export default class DashPlaylistLoader extends EventTarget {
message: 'DASH request error at URL: ' + request.uri,
response: request.response,
// MEDIA_ERR_NETWORK
code: 2
code: 2,
metadata: err.metadata
};
if (startingState) {
this.state = startingState;
@ -390,6 +391,7 @@ export default class DashPlaylistLoader extends EventTarget {
const uri = resolveManifestRedirect(playlist.sidx.resolvedUri);
const fin = (err, request) => {
// TODO: add error metdata here once we create an error type in video.js
if (this.requestErrored_(err, request, startingState)) {
return;
}
@ -400,6 +402,10 @@ export default class DashPlaylistLoader extends EventTarget {
try {
sidx = parseSidx(toUint8(request.response).subarray(8));
} catch (e) {
e.metadata = {
errorType: videojs.Error.DashManifestSidxParsingError
};
// sidx parsing failed.
this.requestErrored_(e, request, startingState);
return;
@ -421,9 +427,11 @@ export default class DashPlaylistLoader extends EventTarget {
}
if (!container || container !== 'mp4') {
const sidxContainer = container || 'unknown';
return fin({
status: request.status,
message: `Unsupported ${container || 'unknown'} container type for sidx segment at URL: ${uri}`,
message: `Unsupported ${sidxContainer} container type for sidx segment at URL: ${uri}`,
// response is just bytes in this case
// but we really don't want to return that.
response: '',
@ -431,7 +439,11 @@ export default class DashPlaylistLoader extends EventTarget {
internal: true,
playlistExclusionDuration: Infinity,
// MEDIA_ERR_NETWORK
code: 2
code: 2,
metadata: {
errorType: videojs.Error.UnsupportedSidxContainer,
sidxContainer
}
}, request);
}
@ -636,7 +648,8 @@ export default class DashPlaylistLoader extends EventTarget {
requestMain_(cb) {
this.request = this.vhs_.xhr({
uri: this.mainPlaylistLoader_.srcUrl,
withCredentials: this.withCredentials
withCredentials: this.withCredentials,
requestType: 'dash-manifest'
}, (error, req) => {
if (this.requestErrored_(error, req)) {
if (this.state === 'HAVE_NOTHING') {
@ -695,7 +708,8 @@ export default class DashPlaylistLoader extends EventTarget {
this.request = this.vhs_.xhr({
uri: resolveUrl(this.mainPlaylistLoader_.srcUrl, utcTiming.value),
method: utcTiming.method,
withCredentials: this.withCredentials
withCredentials: this.withCredentials,
requestType: 'dash-clock-sync'
}, (error, req) => {
// disposed
if (!this.request) {

View file

@ -1,3 +1,4 @@
import videojs from 'video.js';
import { createTransferableMessage } from './bin-utils';
import { stringToArrayBuffer } from './util/string-to-array-buffer';
import { transmux } from './segment-transmuxer';
@ -159,11 +160,16 @@ const parseInitSegment = (segment, callback) => {
// only know how to parse mp4 init segments at the moment
if (type !== 'mp4') {
const uri = segment.map.resolvedUri || segment.map.uri;
const mediaType = type || 'unknown';
return callback({
internal: true,
message: `Found unsupported ${type || 'unknown'} container for initialization segment at URL: ${uri}`,
code: REQUEST_ERRORS.FAILURE
message: `Found unsupported ${mediaType} container for initialization segment at URL: ${uri}`,
code: REQUEST_ERRORS.FAILURE,
metadata: {
errorType: videojs.Error.UnsupportedMediaInitialization,
mediaType
}
});
}
@ -988,7 +994,8 @@ export const mediaSegmentRequest = ({
}
const keyRequestOptions = merge(xhrOptions, {
uri: segment.key.resolvedUri,
responseType: 'arraybuffer'
responseType: 'arraybuffer',
requestType: 'segment-key'
});
const keyRequestCallback = handleKeyResponse(segment, objects, finishProcessingFn);
const keyXhr = xhr(keyRequestOptions, keyRequestCallback);
@ -1003,7 +1010,8 @@ export const mediaSegmentRequest = ({
if (differentMapKey) {
const mapKeyRequestOptions = merge(xhrOptions, {
uri: segment.map.key.resolvedUri,
responseType: 'arraybuffer'
responseType: 'arraybuffer',
requestType: 'segment-key'
});
const mapKeyRequestCallback = handleKeyResponse(segment, [segment.map.key], finishProcessingFn);
const mapKeyXhr = xhr(mapKeyRequestOptions, mapKeyRequestCallback);
@ -1013,7 +1021,8 @@ export const mediaSegmentRequest = ({
const initSegmentOptions = merge(xhrOptions, {
uri: segment.map.resolvedUri,
responseType: 'arraybuffer',
headers: segmentXhrHeaders(segment.map)
headers: segmentXhrHeaders(segment.map),
requestType: 'segment-media-initialization'
});
const initSegmentRequestCallback = handleInitSegmentResponse({segment, finishProcessingFn});
const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback);
@ -1024,7 +1033,8 @@ export const mediaSegmentRequest = ({
const segmentRequestOptions = merge(xhrOptions, {
uri: segment.part && segment.part.resolvedUri || segment.resolvedUri,
responseType: 'arraybuffer',
headers: segmentXhrHeaders(segment)
headers: segmentXhrHeaders(segment),
requestType: 'segment'
});
const segmentRequestCallback = handleSegmentResponse({

View file

@ -446,7 +446,8 @@ export default class PlaylistLoader extends EventTarget {
this.request = this.vhs_.xhr({
uri,
withCredentials: this.withCredentials
withCredentials: this.withCredentials,
requestType: 'hls-playlist'
}, (error, req) => {
// disposed
if (!this.request) {
@ -484,7 +485,10 @@ export default class PlaylistLoader extends EventTarget {
status: xhr.status,
message: `HLS playlist request error at URL: ${uri}.`,
responseText: xhr.responseText,
code: (xhr.status >= 500) ? 4 : 2
code: (xhr.status >= 500) ? 4 : 2,
metadata: {
errorType: videojs.Error.HlsPlaylistRequestError
}
};
this.trigger('error');
@ -689,7 +693,8 @@ export default class PlaylistLoader extends EventTarget {
this.request = this.vhs_.xhr({
uri: playlist.resolvedUri,
withCredentials: this.withCredentials
withCredentials: this.withCredentials,
requestType: 'hls-playlist'
}, (error, req) => {
// disposed
if (!this.request) {
@ -835,7 +840,8 @@ export default class PlaylistLoader extends EventTarget {
// request the specified URL
this.request = this.vhs_.xhr({
uri: this.src,
withCredentials: this.withCredentials
withCredentials: this.withCredentials,
requestType: 'hls-playlist'
}, (error, req) => {
// disposed
if (!this.request) {
@ -851,7 +857,10 @@ export default class PlaylistLoader extends EventTarget {
message: `HLS playlist request error at URL: ${this.src}.`,
responseText: req.responseText,
// MEDIA_ERR_NETWORK
code: 2
code: 2,
metadata: {
errorType: videojs.Error.HlsPlaylistRequestError
}
};
if (this.state === 'HAVE_NOTHING') {
this.started = false;

View file

@ -364,7 +364,11 @@ export const TEST_ONLY_SIMPLE_SELECTOR = (newSimpleSelector) => {
* bandwidth variance
*/
export const lastBandwidthSelector = function() {
const pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1;
let pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1;
if (!isNaN(this.customPixelRatio)) {
pixelRatio = this.customPixelRatio;
}
return simpleSelector(
this.playlists.main,
@ -399,7 +403,11 @@ export const movingAverageBandwidthSelector = function(decay) {
}
return function() {
const pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1;
let pixelRatio = this.useDevicePixelRatio ? window.devicePixelRatio || 1 : 1;
if (!isNaN(this.customPixelRatio)) {
pixelRatio = this.customPixelRatio;
}
if (average < 0) {
average = this.systemBandwidth;

View file

@ -11,7 +11,7 @@ import segmentTransmuxer from './segment-transmuxer';
import { TIME_FUDGE_FACTOR, timeUntilRebuffer as timeUntilRebuffer_ } from './ranges';
import { minRebufferMaxBandwidthSelector } from './playlist-selectors';
import logger from './util/logger';
import { concatSegments } from './util/segment';
import {compactSegmentUrlDescription, concatSegments} from './util/segment';
import {
createCaptionsTrackIfNotExists,
addCaptionData,
@ -678,6 +678,18 @@ export default class SegmentLoader extends videojs.EventTarget {
}
}
/**
* TODO: Current sync controller consists of many hls-specific strategies
* media sequence sync is also hls-specific, and we would like to be protocol-agnostic on this level
* this should be a part of the sync-controller and sync controller should expect different strategy list based on the protocol.
*
* @return {MediaSequenceSync|null}
* @private
*/
get mediaSequenceSync_() {
return this.syncController_.getMediaSequenceSync(this.loaderType_);
}
createTransmuxer_() {
return segmentTransmuxer.createTransmuxer({
remux: false,
@ -815,6 +827,7 @@ export default class SegmentLoader extends videojs.EventTarget {
}
this.pendingSegment_ = null;
return this.error_;
}
@ -1033,8 +1046,14 @@ export default class SegmentLoader extends videojs.EventTarget {
}
this.logger_(`playlist update [${oldId} => ${newPlaylist.id || newPlaylist.uri}]`);
this.syncController_.updateMediaSequenceMap(newPlaylist, this.currentTime_(), this.loaderType_);
if (this.mediaSequenceSync_) {
this.mediaSequenceSync_.update(newPlaylist, this.currentTime_());
this.logger_(`Playlist update:
currentTime: ${this.currentTime_()}
bufferedEnd: ${lastBufferedEnd(this.buffered_())}
`, this.mediaSequenceSync_.diagnostics);
}
// in VOD, this is always a rendition switch (or we updated our syncInfo above)
// in LIVE, we always want to update with new playlists (including refreshes)
this.trigger('syncinfoupdate');
@ -1199,6 +1218,9 @@ export default class SegmentLoader extends videojs.EventTarget {
*/
resetLoader() {
this.fetchAtBuffer_ = false;
if (this.mediaSequenceSync_) {
this.mediaSequenceSync_.resetAppendedStatus();
}
this.resyncLoader();
}
@ -1215,7 +1237,11 @@ export default class SegmentLoader extends videojs.EventTarget {
this.partIndex = null;
this.syncPoint_ = null;
this.isPendingTimestampOffset_ = false;
this.shouldForceTimestampOffsetAfterResync_ = true;
// this is mainly to sync timing-info when switching between renditions with and without timestamp-rollover,
// so we don't want it for DASH
if (this.sourceType_ === 'hls') {
this.shouldForceTimestampOffsetAfterResync_ = true;
}
this.callQueue_ = [];
this.loadQueue_ = [];
this.metadataQueue_.id3 = [];
@ -1451,18 +1477,50 @@ export default class SegmentLoader extends videojs.EventTarget {
next.mediaIndex = this.mediaIndex + 1;
}
} else {
// Find the segment containing the end of the buffer or current time.
const {segmentIndex, startTime, partIndex} = Playlist.getMediaInfoForTime({
exactManifestTimings: this.exactManifestTimings,
playlist: this.playlist_,
currentTime: this.fetchAtBuffer_ ? bufferedEnd : this.currentTime_(),
startingPartIndex: this.syncPoint_.partIndex,
startingSegmentIndex: this.syncPoint_.segmentIndex,
startTime: this.syncPoint_.time
});
let segmentIndex; let partIndex; let startTime;
const targetTime = this.fetchAtBuffer_ ? bufferedEnd : this.currentTime_();
next.getMediaInfoForTime = this.fetchAtBuffer_ ?
`bufferedEnd ${bufferedEnd}` : `currentTime ${this.currentTime_()}`;
if (this.mediaSequenceSync_) {
this.logger_(`chooseNextRequest_ request after Quality Switch:
For TargetTime: ${targetTime}.
CurrentTime: ${this.currentTime_()}
BufferedEnd: ${bufferedEnd}
Fetch At Buffer: ${this.fetchAtBuffer_}
`, this.mediaSequenceSync_.diagnostics);
}
if (this.mediaSequenceSync_ && this.mediaSequenceSync_.isReliable) {
const syncInfo = this.getSyncInfoFromMediaSequenceSync_(targetTime);
if (!syncInfo) {
this.logger_('chooseNextRequest_ - no sync info found using media sequence sync');
// no match
return null;
}
this.logger_(`chooseNextRequest_ mediaSequence syncInfo (${syncInfo.start} --> ${syncInfo.end})`);
segmentIndex = syncInfo.segmentIndex;
partIndex = syncInfo.partIndex;
startTime = syncInfo.start;
} else {
this.logger_('chooseNextRequest_ - fallback to a regular segment selection algorithm, based on a syncPoint.');
// fallback
const mediaInfoForTime = Playlist.getMediaInfoForTime({
exactManifestTimings: this.exactManifestTimings,
playlist: this.playlist_,
currentTime: targetTime,
startingPartIndex: this.syncPoint_.partIndex,
startingSegmentIndex: this.syncPoint_.segmentIndex,
startTime: this.syncPoint_.time
});
segmentIndex = mediaInfoForTime.segmentIndex;
partIndex = mediaInfoForTime.partIndex;
startTime = mediaInfoForTime.startTime;
}
next.getMediaInfoForTime = this.fetchAtBuffer_ ? `bufferedEnd ${targetTime}` : `currentTime ${targetTime}`;
next.mediaIndex = segmentIndex;
next.startOfSegment = startTime;
next.partIndex = partIndex;
@ -1535,6 +1593,47 @@ export default class SegmentLoader extends videojs.EventTarget {
return this.generateSegmentInfo_(next);
}
getSyncInfoFromMediaSequenceSync_(targetTime) {
if (!this.mediaSequenceSync_) {
return null;
}
// we should pull the target time to the least available time if we drop out of sync for any reason
const finalTargetTime = Math.max(targetTime, this.mediaSequenceSync_.start);
if (targetTime !== finalTargetTime) {
this.logger_(`getSyncInfoFromMediaSequenceSync_. Pulled target time from ${targetTime} to ${finalTargetTime}`);
}
const mediaSequenceSyncInfo = this.mediaSequenceSync_.getSyncInfoForTime(finalTargetTime);
if (!mediaSequenceSyncInfo) {
// no match at all
return null;
}
if (!mediaSequenceSyncInfo.isAppended) {
// has a perfect match
return mediaSequenceSyncInfo;
}
// has match, but segment was already appended.
// attempt to auto-advance to the nearest next segment:
const nextMediaSequenceSyncInfo = this.mediaSequenceSync_.getSyncInfoForTime(mediaSequenceSyncInfo.end);
if (!nextMediaSequenceSyncInfo) {
// no match at all
return null;
}
if (nextMediaSequenceSyncInfo.isAppended) {
this.logger_('getSyncInfoFromMediaSequenceSync_: We encounter unexpected scenario where next media sequence sync info is also appended!');
}
// got match with the nearest next segment
return nextMediaSequenceSyncInfo;
}
generateSegmentInfo_(options) {
const {
independent,
@ -2268,7 +2367,10 @@ export default class SegmentLoader extends videojs.EventTarget {
`video buffer: ${timeRangesToArray(videoBuffered).join(', ')}, `);
this.error({
message: 'Quota exceeded error with append of a single segment of content',
excludeUntil: Infinity
excludeUntil: Infinity,
metadata: {
errorType: videojs.Error.SegmentExceedsSourceBufferQuota
}
});
this.trigger('error');
return;
@ -2323,14 +2425,19 @@ export default class SegmentLoader extends videojs.EventTarget {
}
this.logger_('Received non QUOTA_EXCEEDED_ERR on append', error);
this.error(`${type} append of ${bytes.length}b failed for segment ` +
`#${segmentInfo.mediaIndex} in playlist ${segmentInfo.playlist.id}`);
// If an append errors, we often can't recover.
// (see https://w3c.github.io/media-source/#sourcebuffer-append-error).
//
// Trigger a special error so that it can be handled separately from normal,
// recoverable errors.
this.error({
message: `${type} append of ${bytes.length}b failed for segment ` +
`#${segmentInfo.mediaIndex} in playlist ${segmentInfo.playlist.id}`,
metadata: {
errorType: videojs.Error.SegmentAppendError
}
});
this.trigger('appenderror');
}
@ -2484,7 +2591,9 @@ export default class SegmentLoader extends videojs.EventTarget {
segmentInfo.timeline > 0;
const isEndOfTimeline = isEndOfStream || (isWalkingForward && isDiscontinuity);
this.logger_(`Requesting ${segmentInfoString(segmentInfo)}`);
this.logger_(`Requesting
${compactSegmentUrlDescription(segmentInfo.uri)}
${segmentInfoString(segmentInfo)}`);
// If there's an init segment associated with this segment, but it is not cached (identified by a lack of bytes),
// then this init segment has never been seen before and should be appended.
@ -2811,7 +2920,10 @@ export default class SegmentLoader extends videojs.EventTarget {
if (!trackInfo) {
this.error({
message: 'No starting media returned, likely due to an unsupported media format.',
playlistExclusionDuration: Infinity
playlistExclusionDuration: Infinity,
metadata: {
errorType: videojs.Error.SegmentUnsupportedMediaFormat
}
});
this.trigger('error');
return;
@ -2892,7 +3004,10 @@ export default class SegmentLoader extends videojs.EventTarget {
if (illegalMediaSwitchError) {
this.error({
message: illegalMediaSwitchError,
playlistExclusionDuration: Infinity
playlistExclusionDuration: Infinity,
metadata: {
errorType: videojs.Error.SegmentSwitchError
}
});
this.trigger('error');
return true;
@ -3005,6 +3120,14 @@ export default class SegmentLoader extends videojs.EventTarget {
const segmentInfo = this.pendingSegment_;
if (segmentInfo.part && segmentInfo.part.syncInfo) {
// low-latency flow
segmentInfo.part.syncInfo.markAppended();
} else if (segmentInfo.segment.syncInfo) {
// normal flow
segmentInfo.segment.syncInfo.markAppended();
}
// Now that the end of the segment has been reached, we can set the end time. It's
// best to wait until all appends are done so we're sure that the primary media is
// finished (and we have its end time).

View file

@ -9,7 +9,7 @@ import {getMimeForCodec} from '@videojs/vhs-utils/es/codecs.js';
import window from 'global/window';
import toTitleCase from './util/to-title-case.js';
import { QUOTA_EXCEEDED_ERR } from './error-codes';
import {createTimeRanges} from './util/vjs-compat';
import {createTimeRanges, bufferedRangesToString} from './util/vjs-compat';
const bufferTypes = [
'video',
@ -275,7 +275,13 @@ const actions = {
}
// do not update codec if we don't need to.
if (sourceUpdater.codecs[type] === codec) {
// Only update if we change the codec base.
// For example, going from avc1.640028 to avc1.64001f does not require a changeType call.
const newCodecBase = codec.substring(0, codec.indexOf('.'));
const oldCodec = sourceUpdater.codecs[type];
const oldCodecBase = oldCodec.substring(0, oldCodec.indexOf('.'));
if (oldCodecBase === newCodecBase) {
return;
}
@ -308,6 +314,11 @@ const onUpdateend = (type, sourceUpdater) => (e) => {
// updateend events on source buffers. This does not appear to be in the spec. As such,
// if we encounter an updateend without a corresponding pending action from our queue
// for that source buffer type, process the next action.
const bufferedRangesForType = sourceUpdater[`${type}Buffered`]();
const descriptiveString = bufferedRangesToString(bufferedRangesForType);
sourceUpdater.logger_(`received "updateend" event for ${type} Source Buffer: `, descriptiveString);
if (sourceUpdater.queuePending[type]) {
const doneFn = sourceUpdater.queuePending[type].doneFn;

View file

@ -5,6 +5,7 @@
import {sumDurations, getPartsAndSegments} from './playlist';
import videojs from 'video.js';
import logger from './util/logger';
import MediaSequenceSync from './util/media-sequence-sync';
// The maximum gap allowed between two media sequence tags when trying to
// synchronize expired playlist segments.
@ -44,71 +45,27 @@ export const syncPointStrategies = [
* @param {string} type
*/
run: (syncController, playlist, duration, currentTimeline, currentTime, type) => {
if (!type) {
const mediaSequenceSync = syncController.getMediaSequenceSync(type);
if (!mediaSequenceSync) {
return null;
}
const mediaSequenceMap = syncController.getMediaSequenceMap(type);
if (!mediaSequenceMap || mediaSequenceMap.size === 0) {
if (!mediaSequenceSync.isReliable) {
return null;
}
if (playlist.mediaSequence === undefined || !Array.isArray(playlist.segments) || !playlist.segments.length) {
const syncInfo = mediaSequenceSync.getSyncInfoForTime(currentTime);
if (!syncInfo) {
return null;
}
let currentMediaSequence = playlist.mediaSequence;
let segmentIndex = 0;
for (const segment of playlist.segments) {
const range = mediaSequenceMap.get(currentMediaSequence);
if (!range) {
// unexpected case
// we expect this playlist to be the same playlist in the map
// just break from the loop and move forward to the next strategy
break;
}
if (currentTime >= range.start && currentTime < range.end) {
// we found segment
if (Array.isArray(segment.parts) && segment.parts.length) {
let currentPartStart = range.start;
let partIndex = 0;
for (const part of segment.parts) {
const start = currentPartStart;
const end = start + part.duration;
if (currentTime >= start && currentTime < end) {
return {
time: range.start,
segmentIndex,
partIndex
};
}
partIndex++;
currentPartStart = end;
}
}
// no parts found, return sync point for segment
return {
time: range.start,
segmentIndex,
partIndex: null
};
}
segmentIndex++;
currentMediaSequence++;
}
// we didn't find any segments for provided current time
return null;
return {
time: syncInfo.start,
partIndex: syncInfo.partIndex,
segmentIndex: syncInfo.segmentIndex
};
}
},
// Stategy "ProgramDateTime": We have a program-date-time tag in this playlist
@ -272,78 +229,25 @@ export default class SyncController extends videojs.EventTarget {
this.timelines = [];
this.discontinuities = [];
this.timelineToDatetimeMappings = {};
/**
* @type {Map<string, Map<number, { start: number, end: number }>>}
* @private
*/
this.mediaSequenceStorage_ = new Map();
// TODO: this map should be only available for HLS. Since only HLS has MediaSequence.
// For some reason this map helps with syncing between quality switch for MPEG-DASH as well.
// Moreover if we disable this map for MPEG-DASH - quality switch will be broken.
// MPEG-DASH should have its own separate sync strategy
this.mediaSequenceStorage_ = {
main: new MediaSequenceSync(),
audio: new MediaSequenceSync(),
vtt: new MediaSequenceSync()
};
this.logger_ = logger('SyncController');
}
/**
* Get media sequence map by type
*
* @param {string} type - segment loader type
* @return {Map<number, { start: number, end: number }> | undefined}
* @param {string} loaderType
* @return {MediaSequenceSync|null}
*/
getMediaSequenceMap(type) {
return this.mediaSequenceStorage_.get(type);
}
/**
* Update Media Sequence Map -> <MediaSequence, Range>
*
* @param {Object} playlist - parsed playlist
* @param {number} currentTime - current player's time
* @param {string} type - segment loader type
* @return {void}
*/
updateMediaSequenceMap(playlist, currentTime, type) {
// we should not process this playlist if it does not have mediaSequence or segments
if (playlist.mediaSequence === undefined || !Array.isArray(playlist.segments) || !playlist.segments.length) {
return;
}
const currentMap = this.getMediaSequenceMap(type);
const result = new Map();
let currentMediaSequence = playlist.mediaSequence;
let currentBaseTime;
if (!currentMap) {
// first playlist setup:
currentBaseTime = 0;
} else if (currentMap.has(playlist.mediaSequence)) {
// further playlists setup:
currentBaseTime = currentMap.get(playlist.mediaSequence).start;
} else {
// it seems like we have a gap between playlists, use current time as a fallback:
this.logger_(`MediaSequence sync for ${type} segment loader - received a gap between playlists.
Fallback base time to: ${currentTime}.
Received media sequence: ${currentMediaSequence}.
Current map: `, currentMap);
currentBaseTime = currentTime;
}
this.logger_(`MediaSequence sync for ${type} segment loader.
Received media sequence: ${currentMediaSequence}.
base time is ${currentBaseTime}
Current map: `, currentMap);
playlist.segments.forEach((segment) => {
const start = currentBaseTime;
const end = start + segment.duration;
const range = { start, end };
result.set(currentMediaSequence, range);
currentMediaSequence++;
currentBaseTime = end;
});
this.mediaSequenceStorage_.set(type, result);
getMediaSequenceSync(loaderType) {
return this.mediaSequenceStorage_[loaderType] || null;
}
/**
@ -436,8 +340,7 @@ Current map: `, currentMap);
playlist,
duration,
playlist.discontinuitySequence,
0,
'main'
0
);
// Without sync-points, there is not enough information to determine the expired time

View file

@ -0,0 +1,246 @@
import {compactSegmentUrlDescription} from './segment';
class SyncInfo {
/**
* @param {number} start - media sequence start
* @param {number} end - media sequence end
* @param {number} segmentIndex - index for associated segment
* @param {number|null} [partIndex] - index for associated part
* @param {boolean} [appended] - appended indicator
*
*/
constructor({start, end, segmentIndex, partIndex = null, appended = false}) {
this.start_ = start;
this.end_ = end;
this.segmentIndex_ = segmentIndex;
this.partIndex_ = partIndex;
this.appended_ = appended;
}
isInRange(targetTime) {
return targetTime >= this.start && targetTime < this.end;
}
markAppended() {
this.appended_ = true;
}
resetAppendedStatus() {
this.appended_ = false;
}
get isAppended() {
return this.appended_;
}
get start() {
return this.start_;
}
get end() {
return this.end_;
}
get segmentIndex() {
return this.segmentIndex_;
}
get partIndex() {
return this.partIndex_;
}
}
class SyncInfoData {
/**
*
* @param {SyncInfo} segmentSyncInfo - sync info for a given segment
* @param {Array<SyncInfo>} [partsSyncInfo] - sync infos for a list of parts for a given segment
*/
constructor(segmentSyncInfo, partsSyncInfo = []) {
this.segmentSyncInfo_ = segmentSyncInfo;
this.partsSyncInfo_ = partsSyncInfo;
}
get segmentSyncInfo() {
return this.segmentSyncInfo_;
}
get partsSyncInfo() {
return this.partsSyncInfo_;
}
get hasPartsSyncInfo() {
return this.partsSyncInfo_.length > 0;
}
resetAppendStatus() {
this.segmentSyncInfo_.resetAppendedStatus();
this.partsSyncInfo_.forEach((partSyncInfo) => partSyncInfo.resetAppendedStatus());
}
}
export default class MediaSequenceSync {
constructor() {
/**
* @type {Map<number, SyncInfoData>}
* @private
*/
this.storage_ = new Map();
this.diagnostics_ = '';
this.isReliable_ = false;
this.start_ = -Infinity;
this.end_ = Infinity;
}
get start() {
return this.start_;
}
get end() {
return this.end_;
}
get diagnostics() {
return this.diagnostics_;
}
get isReliable() {
return this.isReliable_;
}
resetAppendedStatus() {
this.storage_.forEach((syncInfoData) => syncInfoData.resetAppendStatus());
}
/**
* update sync storage
*
* @param {Object} playlist
* @param {number} currentTime
*
* @return {void}
*/
update(playlist, currentTime) {
const { mediaSequence, segments } = playlist;
this.isReliable_ = this.isReliablePlaylist_(mediaSequence, segments);
if (!this.isReliable_) {
return;
}
return this.updateStorage_(
segments,
mediaSequence,
this.calculateBaseTime_(mediaSequence, currentTime)
);
}
/**
* @param {number} targetTime
* @return {SyncInfo|null}
*/
getSyncInfoForTime(targetTime) {
for (const { segmentSyncInfo, partsSyncInfo } of this.storage_.values()) {
// Normal segment flow:
if (!partsSyncInfo.length) {
if (segmentSyncInfo.isInRange(targetTime)) {
return segmentSyncInfo;
}
} else {
// Low latency flow:
for (const partSyncInfo of partsSyncInfo) {
if (partSyncInfo.isInRange(targetTime)) {
return partSyncInfo;
}
}
}
}
return null;
}
updateStorage_(segments, startingMediaSequence, startingTime) {
const newStorage = new Map();
let newDiagnostics = '\n';
let currentStart = startingTime;
let currentMediaSequence = startingMediaSequence;
this.start_ = currentStart;
segments.forEach((segment, segmentIndex) => {
const prevSyncInfoData = this.storage_.get(currentMediaSequence);
const segmentStart = currentStart;
const segmentEnd = segmentStart + segment.duration;
const segmentIsAppended = Boolean(prevSyncInfoData &&
prevSyncInfoData.segmentSyncInfo &&
prevSyncInfoData.segmentSyncInfo.isAppended);
const segmentSyncInfo = new SyncInfo({
start: segmentStart,
end: segmentEnd,
appended: segmentIsAppended,
segmentIndex
});
segment.syncInfo = segmentSyncInfo;
let currentPartStart = currentStart;
const partsSyncInfo = (segment.parts || []).map((part, partIndex) => {
const partStart = currentPartStart;
const partEnd = currentPartStart + part.duration;
const partIsAppended = Boolean(prevSyncInfoData &&
prevSyncInfoData.partsSyncInfo &&
prevSyncInfoData.partsSyncInfo[partIndex] &&
prevSyncInfoData.partsSyncInfo[partIndex].isAppended);
const partSyncInfo = new SyncInfo({
start: partStart,
end: partEnd,
appended: partIsAppended,
segmentIndex,
partIndex
});
currentPartStart = partEnd;
newDiagnostics += `Media Sequence: ${currentMediaSequence}.${partIndex} | Range: ${partStart} --> ${partEnd} | Appended: ${partIsAppended}\n`;
part.syncInfo = partSyncInfo;
return partSyncInfo;
});
newStorage.set(currentMediaSequence, new SyncInfoData(segmentSyncInfo, partsSyncInfo));
newDiagnostics += `${compactSegmentUrlDescription(segment.resolvedUri)} | Media Sequence: ${currentMediaSequence} | Range: ${segmentStart} --> ${segmentEnd} | Appended: ${segmentIsAppended}\n`;
currentMediaSequence++;
currentStart = segmentEnd;
});
this.end_ = currentStart;
this.storage_ = newStorage;
this.diagnostics_ = newDiagnostics;
}
calculateBaseTime_(mediaSequence, fallback) {
if (!this.storage_.size) {
// Initial setup flow.
return 0;
}
if (this.storage_.has(mediaSequence)) {
// Normal flow.
return this.storage_.get(mediaSequence).segmentSyncInfo.start;
}
// Fallback flow.
// There is a gap between last recorded playlist and a new one received.
return fallback;
}
isReliablePlaylist_(mediaSequence, segments) {
return mediaSequence !== undefined && mediaSequence !== null && Array.isArray(segments) && segments.length;
}
}

View file

@ -21,3 +21,24 @@ export const concatSegments = (segmentObj) => {
return tempBuffer;
};
/**
* Example:
* https://host.com/path1/path2/path3/segment.ts?arg1=val1
* -->
* path3/segment.ts
*
* @param resolvedUri
* @return {string}
*/
export function compactSegmentUrlDescription(resolvedUri) {
try {
return new URL(resolvedUri)
.pathname
.split('/')
.slice(-2)
.join('/');
} catch (e) {
return '';
}
}

View file

@ -24,3 +24,27 @@ export function createTimeRanges(...args) {
return fn.apply(context, args);
}
/**
* Converts provided buffered ranges to a descriptive string
*
* @param {TimeRanges} buffered - received buffered time ranges
*
* @return {string} - descriptive string
*/
export function bufferedRangesToString(buffered) {
if (buffered.length === 0) {
return 'Buffered Ranges are empty';
}
let bufferedRangesStr = 'Buffered Ranges: \n';
for (let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
bufferedRangesStr += `${start} --> ${end}. Duration (${end - start})\n`;
}
return bufferedRangesStr;
}

View file

@ -738,6 +738,7 @@ class VhsHandler extends Component {
[
'withCredentials',
'useDevicePixelRatio',
'customPixelRatio',
'limitRenditionByPlayerDimensions',
'bandwidth',
'customTagParsers',
@ -761,6 +762,13 @@ class VhsHandler extends Component {
this.limitRenditionByPlayerDimensions = this.options_.limitRenditionByPlayerDimensions;
this.useDevicePixelRatio = this.options_.useDevicePixelRatio;
const customPixelRatio = this.options_.customPixelRatio;
// Ensure the custom pixel ratio is a number greater than or equal to 0
if (typeof customPixelRatio === 'number' && customPixelRatio >= 0) {
this.customPixelRatio = customPixelRatio;
}
}
// alias for public method to set options
setOptions(options = {}) {
@ -1088,7 +1096,10 @@ class VhsHandler extends Component {
this.logger_('error while creating EME key session', err);
this.player_.error({
message: 'Failed to initialize media keys for EME',
code: 3
code: 3,
metadata: {
errorType: videojs.Error.EMEKeySessionCreationError
}
});
});
}

View file

@ -37,8 +37,6 @@ export default class VTTSegmentLoader extends SegmentLoader {
this.subtitlesTrack_ = null;
this.loaderType_ = 'subtitle';
this.featuresNativeTextTracks_ = settings.featuresNativeTextTracks;
this.loadVttJs = settings.loadVttJs;
@ -314,7 +312,12 @@ export default class VTTSegmentLoader extends SegmentLoader {
this.loadVttJs()
.then(
() => this.segmentRequestFinished_(error, simpleSegment, result),
() => this.stopForError({ message: 'Error loading vtt.js' })
() => this.stopForError({
message: 'Error loading vtt.js',
metadata: {
errorType: videojs.Error.VttLoadError
}
})
);
return;
}
@ -325,7 +328,10 @@ export default class VTTSegmentLoader extends SegmentLoader {
this.parseVTTCues_(segmentInfo);
} catch (e) {
this.stopForError({
message: e.message
message: e.message,
metadata: {
errorType: videojs.Error.VttCueParsingError
}
});
return;
}

View file

@ -13,10 +13,6 @@ import videojs from 'video.js';
import window from 'global/window';
import {merge} from './util/vjs-compat';
const {
xhr: videojsXHR
} = videojs;
const callbackWrapper = function(request, error, response, callback) {
const reqResponse = request.responseType === 'arraybuffer' ? request.response : request.responseText;
@ -115,7 +111,7 @@ const xhrFactory = function() {
// Use the standard videojs.xhr() method unless `videojs.Vhs.xhr` has been overriden
// TODO: switch back to videojs.Vhs.xhr.name === 'XhrFunction' when we drop IE11
const xhrMethod = videojs.Vhs.xhr.original === true ? videojsXHR : videojs.Vhs.xhr;
const xhrMethod = videojs.Vhs.xhr.original === true ? videojs.xhr : videojs.Vhs.xhr;
// call all registered onRequest hooks, assign new options.
const beforeRequestOptions = callAllRequestHooks(_requestCallbackSet, options);