diff --git a/demo/full/scripts/components/Options/AudioAdaptiveSettings.jsx b/demo/full/scripts/components/Options/AudioAdaptiveSettings.jsx index 21c05284be..8ee7fd3adf 100644 --- a/demo/full/scripts/components/Options/AudioAdaptiveSettings.jsx +++ b/demo/full/scripts/components/Options/AudioAdaptiveSettings.jsx @@ -75,6 +75,14 @@ function AudioAdaptiveSettings({ /> + + { + parseFloat(initialAudioBr) === 0 ? + "Starts loading the lowest audio bitrate" : + `Starts with an audio bandwidth estimate of ${initialAudioBr}` + + " bits per seconds." + } +
  • @@ -116,6 +124,14 @@ function AudioAdaptiveSettings({ > Do not limit + + { + !isMinAudioBrLimited || parseFloat(minAudioBr) <= 0 ? + "Not limiting the lowest audio bitrate reachable through the adaptive logic" : + "Limiting the lowest audio bitrate reachable through the adaptive " + + `logic to ${minAudioBr} bits per seconds` + } +
  • @@ -159,6 +175,14 @@ function AudioAdaptiveSettings({ Do not limit
    + + { + !isMaxAudioBrLimited || parseFloat(maxAudioBr) === Infinity ? + "Not limiting the highest audio bitrate reachable through the adaptive logic" : + "Limiting the highest audio bitrate reachable through the adaptive " + + `logic to ${maxAudioBr} bits per seconds` + } +
  • ); diff --git a/demo/full/scripts/components/Options/BufferOptions.jsx b/demo/full/scripts/components/Options/BufferOptions.jsx index c342deefa9..9d505f76cf 100644 --- a/demo/full/scripts/components/Options/BufferOptions.jsx +++ b/demo/full/scripts/components/Options/BufferOptions.jsx @@ -56,12 +56,12 @@ function BufferOptions({ const isNotLimited = getCheckBoxValue(evt.target); if (isNotLimited){ setMaxVideoBufferSizeLimit(false); - onMaxVideoBufferSizeInput(Infinity) + onMaxVideoBufferSizeInput(Infinity); } else { setMaxVideoBufferSizeLimit(true); - onMaxVideoBufferSizeInput(DEFAULT_VALUES.maxVideoBufferSize) + onMaxVideoBufferSizeInput(DEFAULT_VALUES.maxVideoBufferSize); } - } + }; return ( @@ -96,6 +96,10 @@ function BufferOptions({ /> + + Buffering around {wantedBufferAhead} second(s) ahead of the current + position +
  • @@ -123,7 +127,7 @@ function BufferOptions({ ariaLabel="Reset option to default value" title="Reset option to default value" onClick={() => { - setMaxVideoBufferSizeLimit(DEFAULT_VALUES.maxVideoBufferSize !== + setMaxVideoBufferSizeLimit(DEFAULT_VALUES.maxVideoBufferSize !== Infinity); onMaxVideoBufferSizeInput(DEFAULT_VALUES.maxVideoBufferSize); }} @@ -140,6 +144,14 @@ function BufferOptions({ > Do not limit + + { + parseFloat(maxVideoBufferSize) === Infinity || + !isMaxVideoBufferSizeLimited ? + "Not setting a size limit to the video buffer (relying only on the wantedBufferAhead option)" : + `Buffering at most around ${maxVideoBufferSize} kilobyte(s) on the video buffer` + } +
  • @@ -182,6 +194,14 @@ function BufferOptions({ > Do not limit + + { + parseFloat(maxBufferAhead) === Infinity || + !isMaxBufferAHeadLimited ? + "Not manually cleaning buffer far ahead of the current position" : + `Manually cleaning data ${maxBufferAhead} second(s) ahead of the current position` + } +
  • @@ -224,6 +244,14 @@ function BufferOptions({ > Do not limit + + { + parseFloat(maxBufferBehind) === Infinity || + !isMaxBufferBehindLimited ? + "Not manually cleaning buffer behind the current position" : + `Manually cleaning data ${maxBufferBehind} second(s) behind the current position` + } +
  • ); diff --git a/demo/full/scripts/components/Options/NetworkConfig.jsx b/demo/full/scripts/components/Options/NetworkConfig.jsx index 63ced55fe3..91a87e90e6 100644 --- a/demo/full/scripts/components/Options/NetworkConfig.jsx +++ b/demo/full/scripts/components/Options/NetworkConfig.jsx @@ -9,17 +9,24 @@ import DEFAULT_VALUES from "../../lib/defaultOptionsValues"; * @param {Object} props * @returns {Object} */ -function NetworkConfig({ +function RequestConfig({ segmentRetry, + segmentTimeout, manifestRetry, offlineRetry, + manifestTimeout, onSegmentRetryInput, + onSegmentTimeoutInput, onManifestRetryInput, onOfflineRetryInput, + onManifestTimeoutInput, }) { const [isSegmentRetryLimited, setSegmentRetryLimit] = useState( segmentRetry !== Infinity ); + const [isSegmentTimeoutLimited, setSegmentTimeoutLimit] = useState( + segmentTimeout !== -1 + ); const [isManifestRetryLimited, setManifestRetryLimit] = useState( manifestRetry !== Infinity ); @@ -27,6 +34,9 @@ function NetworkConfig({ offlineRetry !== Infinity ); + const [isManifestTimeoutLimited, setManifestTimeoutLimit] = useState( + manifestTimeout !== -1 + ); const onChangeLimitSegmentRetry = (evt) => { const isNotLimited = getCheckBoxValue(evt.target); if (isNotLimited) { @@ -38,6 +48,17 @@ function NetworkConfig({ } }; + const onChangeLimitSegmentTimeout = (evt) => { + const isNotLimited = getCheckBoxValue(evt.target); + if (isNotLimited) { + setSegmentTimeoutLimit(false); + onSegmentTimeoutInput("-1"); + } else { + setSegmentTimeoutLimit(true); + onSegmentTimeoutInput(DEFAULT_VALUES.segmentTimeout); + } + }; + const onChangeLimitManifestRetry = (evt) => { const isNotLimited = getCheckBoxValue(evt.target); if (isNotLimited) { @@ -60,6 +81,17 @@ function NetworkConfig({ } }; + const onChangeLimitManifestTimeout = (evt) => { + const isNotLimited = getCheckBoxValue(evt.target); + if (isNotLimited) { + setManifestTimeoutLimit(false); + onManifestTimeoutInput("-1"); + } else { + setManifestTimeoutLimit(true); + onManifestTimeoutInput(DEFAULT_VALUES.manifestTimeout); + } + }; + return (
  • @@ -103,7 +135,61 @@ function NetworkConfig({ > Do not limit + + {parseFloat(segmentRetry) === Infinity || !isSegmentRetryLimited ? + "Retry \"retryable\" segment requests with no limit" : + `Retry "retryable" segment requests at most ${segmentRetry} time(s)`} +
  • + +
  • +
    + + + onSegmentTimeoutInput(evt.target.value)} + value={segmentTimeout} + disabled={isSegmentTimeoutLimited === false} + className="optionInput" + /> +
    + + Do not limit + + + {parseFloat(segmentTimeout) === -1 || !isSegmentTimeoutLimited ? + "Perform segment requests without timeout" : + `Stop segment requests after ${segmentTimeout} millisecond(s)`} + +
  • +
  • @@ -145,6 +231,11 @@ function NetworkConfig({ > Do not limit + + {parseFloat(manifestRetry) === Infinity || !isManifestRetryLimited ? + "Retry \"retryable\" manifest requests with no limit" : + `Retry "retryable" manifest requests at most ${manifestRetry} time(s)`} +
  • @@ -187,9 +278,62 @@ function NetworkConfig({ > Do not limit + + {parseFloat(offlineRetry) === Infinity || !isOfflineRetryLimited ? + "Retry \"retryable\" requests when offline with no limit" : + `Retry "retryable" requests when offline at most ${offlineRetry} time(s)`} + +
  • + +
  • +
    + + + onManifestTimeoutInput(evt.target.value)} + value={manifestTimeout} + disabled={isManifestTimeoutLimited === false} + className="optionInput" + /> +
    + + Do not limit + + + {parseFloat(manifestTimeout) === -1 || !isManifestTimeoutLimited ? + "Perform manifest requests without timeout" : + `Stop manifest requests after ${manifestTimeout} millisecond(s)`} +
  • ); } -export default React.memo(NetworkConfig); +export default React.memo(RequestConfig); diff --git a/demo/full/scripts/components/Options/Playback.jsx b/demo/full/scripts/components/Options/Playback.jsx index af5f64ed5e..aa207dcc87 100644 --- a/demo/full/scripts/components/Options/Playback.jsx +++ b/demo/full/scripts/components/Options/Playback.jsx @@ -14,6 +14,21 @@ function TrackSwitch({ stopAtEnd, onStopAtEndClick, }) { + let manualBitrateSwitchingModeDesc; + switch (manualBrSwitchingMode) { + case "direct": + manualBitrateSwitchingModeDesc = + "Directly visible transition when a Representation is manually changed"; + break; + case "seamless": + manualBitrateSwitchingModeDesc = + "Smooth transition when a Representation is manually changed"; + break; + default: + manualBitrateSwitchingModeDesc = + "Unknown value"; + break; + } return (
  • @@ -26,6 +41,11 @@ function TrackSwitch({ > Auto Play + + {autoPlay ? + "Playing directly when the content is loaded." : + "Staying in pause when the content is loaded."} +
  • + + {manualBitrateSwitchingModeDesc} +
  • Stop At End + + {stopAtEnd ? + "Automatically stop when reaching the end of the content." : + "Don't stop when reaching the end of the content."} +
  • ); diff --git a/demo/full/scripts/components/Options/TrackSwitch.jsx b/demo/full/scripts/components/Options/TrackSwitch.jsx index ab8412feed..ac40bead45 100644 --- a/demo/full/scripts/components/Options/TrackSwitch.jsx +++ b/demo/full/scripts/components/Options/TrackSwitch.jsx @@ -14,6 +14,41 @@ function NetworkConfig({ onAudioTrackSwitchingModeChange, onCodecSwitchChange, }) { + let audioTrackSwitchingModeDescMsg; + switch (audioTrackSwitchingMode) { + case "reload": + audioTrackSwitchingModeDescMsg = + "Reloading when the audio track is changed"; + break; + case "direct": + audioTrackSwitchingModeDescMsg = + "Directly audible transition when the audio track is changed"; + break; + case "seamless": + audioTrackSwitchingModeDescMsg = + "Smooth transition when the audio track is changed"; + break; + default: + audioTrackSwitchingModeDescMsg = + "Unknown value"; + break; + } + + let onCodecSwitchDescMsg; + switch (onCodecSwitch) { + case "reload": + onCodecSwitchDescMsg = "Reloading buffers when the codec changes"; + break; + case "continue": + onCodecSwitchDescMsg = + "Keeping the same buffers even when the codec changes"; + break; + default: + onCodecSwitchDescMsg = + "Unknown value"; + break; + } + return (
  • @@ -27,6 +62,11 @@ function NetworkConfig({ > Fast Switching + + {enableFastSwitching ? + "Fast quality switch by replacing lower qualities in the buffer by higher ones when possible." : + "Not replacing lower qualities in the buffer by an higher one when possible."} +
  • + + {audioTrackSwitchingModeDescMsg} +
  • + + {onCodecSwitchDescMsg} +
  • ); diff --git a/demo/full/scripts/components/Options/VideoAdaptiveSettings.jsx b/demo/full/scripts/components/Options/VideoAdaptiveSettings.jsx index 6d3107ca21..83c7038869 100644 --- a/demo/full/scripts/components/Options/VideoAdaptiveSettings.jsx +++ b/demo/full/scripts/components/Options/VideoAdaptiveSettings.jsx @@ -79,6 +79,14 @@ function VideoAdaptiveSettings({ /> + + { + parseFloat(initialVideoBr) === 0 ? + "Starts loading the lowest video bitrate" : + `Starts with a video bandwidth estimate of ${initialVideoBr}` + + " bits per seconds." + } +
  • @@ -120,6 +128,14 @@ function VideoAdaptiveSettings({ > Do not limit + + { + !isMinVideoBrLimited || parseFloat(minVideoBr) <= 0 ? + "Not limiting the lowest video bitrate reachable through the adaptive logic" : + "Limiting the lowest video bitrate reachable through the adaptive " + + `logic to ${minVideoBr} bits per seconds` + } +
  • @@ -163,29 +179,51 @@ function VideoAdaptiveSettings({ Do not limit
    + + { + !isMaxVideoBrLimited || parseFloat(maxVideoBr) === Infinity ? + "Not limiting the highest video bitrate reachable through the adaptive logic" : + "Limiting the highest video bitrate reachable through the adaptive " + + `logic to ${maxVideoBr} bits per seconds` + } +
  • - - Limit Video Width - +
    + + Limit Video Width + + + {limitVideoWidth ? + "Limiting video width to the current +
  • - - Throttle Video Bitrate When Hidden - +
    + + Throttle Video Bitrate When Hidden + + + {throttleVideoBitrateWhenHidden ? + "Throttling the video bitrate when the page is hidden for a time" : + "Not throttling the video bitrate when the page is hidden for a time"} + +
  • ); diff --git a/demo/full/scripts/controllers/LogDisplayer.jsx b/demo/full/scripts/controllers/LogDisplayer.jsx deleted file mode 100644 index 1a8c481907..0000000000 --- a/demo/full/scripts/controllers/LogDisplayer.jsx +++ /dev/null @@ -1,258 +0,0 @@ -import React from "react"; -import { - filter, - skip, - Subject, - takeUntil, -} from "rxjs"; -import Button from "../components/Button.jsx"; - -const LogElement = ({ text, date }) => ( -
    - {date.toISOString() + " - " + text} -
    -); - -class LogDisplayer extends React.Component { - constructor(...args) { - super(...args); - this.state = { logs: [] }; - - // A weird React behavior obligates me to mutate a this._logs array instead - // of calling setState directly to allow multiple setState in a row before - // rendering. - // The case seen was that this.state.logs would not change right after - // setState, so the last addLog call would be the only one really considered - this._logs = []; - - // Only scroll to bottom if already scrolled to bottom - this.hasScrolledToBottom = true; - } - - addLog(text) { - this._logs = [...this._logs, { - text, - date: new Date(), - }]; - - this.setState({ logs: this._logs.slice() }); - } - - resetLogs() { - this._logs = []; - this.setState({ logs: [] }); - } - - componentDidMount() { - this.destructionSubject = new Subject(); - const { player } = this.props; - - player.$get("videoBitrateAuto").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - ).subscribe(vbAuto => { - const text = "Video Bitrate selection changed to " + - (vbAuto ? "automatic" : "manual"); - this.addLog(text); - }); - - player.$get("audioBitrateAuto").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - ).subscribe(abAuto => { - const text = "Audio Bitrate selection changed to " + - (abAuto ? "automatic" : "manual"); - this.addLog(text); - }); - - player.$get("videoBitrate").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - ).subscribe(vb => { - const text = "Video Bitrate changed to " + vb; - this.addLog(text); - }); - - player.$get("audioBitrate").pipe( - takeUntil(this.destructionSubject), - skip(1), // skip initial value - ).subscribe(ab => { - const text = "Audio Bitrate changed to " + ab; - this.addLog(text); - }); - - player.$get("error").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - filter(x => x), - ).subscribe(error => { - const message = error.message ? error.message : error; - const text = "The player encountered a fatal Error: " + message; - this.addLog(text); - }); - - player.$get("isLoading").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - filter(x => x), - ).subscribe(() => { - const text = "A new content is Loading."; - this.addLog(text); - }); - - player.$get("hasCurrentContent").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - filter(x => x), - ).subscribe(() => { - const text = "The new content has been loaded."; - this.addLog(text); - }); - - player.$get("isStopped").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - filter(x => x), - ).subscribe(() => { - const text = "The current content is stopped"; - this.addLog(text); - }); - - player.$get("hasEnded").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - filter(x => x), - ).subscribe(() => { - const text = "The current content has ended"; - this.addLog(text); - }); - - player.$get("isBuffering").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - ).subscribe((ib) => { - const text = ib ? - "The current content is buffering" : - "The current content is not buffering anymore"; - this.addLog(text); - }); - - player.$get("isSeeking").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - ).subscribe((ib) => { - const text = ib ? - "The current content is seeking" : - "The current content is not seeking anymore"; - this.addLog(text); - }); - - player.$get("availableLanguages").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - ).subscribe(() => { - const text = "The audio track list has changed"; - this.addLog(text); - }); - - player.$get("availableSubtitles").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - ).subscribe(() => { - const text = "The text track list has changed"; - this.addLog(text); - }); - - player.$get("availableVideoTracks").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - ).subscribe(() => { - const text = "The video track list has changed"; - this.addLog(text); - }); - - player.$get("availableAudioBitrates").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - ).subscribe(() => { - const text = "The audio bitrate list has changed"; - this.addLog(text); - }); - - player.$get("availableVideoBitrates").pipe( - skip(1), // skip initial value - takeUntil(this.destructionSubject), - ).subscribe(() => { - const text = "The video bitrate list has changed"; - this.addLog(text); - }); - - this.scrollToBottom(); - - const onScroll = () => { - if ( - this.element.scrollHeight - this.element.offsetHeight === - this.element.scrollTop - ) { - this.hasScrolledToBottom = true; - } else { - this.hasScrolledToBottom = false; - } - }; - - this.element.addEventListener("scroll", onScroll, { passive: true }); - this.destructionSubject.subscribe(() => - this.element.removeEventListener("scroll", onScroll)); - } - - scrollToBottom() { - if (this.hasScrolledToBottom) { - this.element.scrollTop = this.element.scrollHeight; - } - } - - componentDidUpdate() { - this.scrollToBottom(); - } - - componentWillUnmount() { - this.destructionSubject.next(); - this.destructionSubject.complete(); - } - - render() { - const { logs } = this.state; - - const logTexts = logs.map(({ text, date }, i) => - ); - - const clearLogs = () => this.resetLogs(); - return ( -
    -
    - Logs -
    -
    this.element = el} - > -
    -
    - ); - } -} - -export default React.memo(LogDisplayer); diff --git a/demo/full/scripts/controllers/Player.jsx b/demo/full/scripts/controllers/Player.jsx index 92f577bf14..0e166ef2c2 100644 --- a/demo/full/scripts/controllers/Player.jsx +++ b/demo/full/scripts/controllers/Player.jsx @@ -14,7 +14,6 @@ import ControlBar from "./ControlBar.jsx"; import ContentList from "./ContentList.jsx"; import Settings from "./Settings.jsx"; import ErrorDisplayer from "./ErrorDisplayer.jsx"; -import LogDisplayer from "./LogDisplayer.jsx"; import ChartsManager from "./charts/index.jsx"; import PlayerKnobsSettings from "./PlayerKnobsSettings.jsx"; import isEqual from "../lib/isEqual" @@ -238,7 +237,6 @@ function Player() { } - {player ? : null} ); diff --git a/demo/full/scripts/controllers/Settings.jsx b/demo/full/scripts/controllers/Settings.jsx index c0bcba8457..fc2c3c81fa 100644 --- a/demo/full/scripts/controllers/Settings.jsx +++ b/demo/full/scripts/controllers/Settings.jsx @@ -16,9 +16,7 @@ class Settings extends React.Component { constructor(...args) { super(...args); - this.state = { - ...defaultOptionsValues, - }; + this.state = Object.assign({}, defaultOptionsValues); } getOptions() { @@ -42,8 +40,10 @@ class Settings extends React.Component { onCodecSwitch, enableFastSwitching, segmentRetry, + segmentTimeout, manifestRetry, offlineRetry, + manifestTimeout, } = this.state; return { initOpts: { @@ -69,77 +69,108 @@ class Settings extends React.Component { enableFastSwitching, networkConfig: { segmentRetry: parseFloat(segmentRetry), + segmentRequestTimeout: parseFloat(segmentTimeout), manifestRetry: parseFloat(manifestRetry), + manifestRequestTimeout: parseFloat(manifestTimeout), offlineRetry: parseFloat(offlineRetry), }, }, }; } - onAutoPlayClick = (evt) => + onAutoPlayClick(evt) { this.setState({ autoPlay: getCheckBoxValue(evt.target) }); + } - onManualBrSwitchingModeChange = (value) => + onManualBrSwitchingModeChange(value) { this.setState({ manualBrSwitchingMode: value }); + } - onInitialVideoBrInput = (value) => + onInitialVideoBrInput(value) { this.setState({ initialVideoBr: value }); + } - onInitialAudioBrInput = (value) => + onInitialAudioBrInput(value) { this.setState({ initialAudioBr: value }); + } - onMinVideoBrInput = (value) => + onMinVideoBrInput(value) { this.setState({ minVideoBr: value }); + } - onMinAudioBrInput = (value) => + onMinAudioBrInput(value) { this.setState({ minAudioBr: value }); + } - onMaxVideoBrInput = (value) => + onMaxVideoBrInput(value) { this.setState({ maxVideoBr: value }); + } - onMaxAudioBrInput = (value) => + onMaxAudioBrInput(value) { this.setState({ maxAudioBr: value }); + } - onLimitVideoWidthClick = (evt) => + onLimitVideoWidthClick(evt) { this.setState({ limitVideoWidth: getCheckBoxValue(evt.target) }); + } - onThrottleVideoBitrateWhenHiddenClick = (evt) => + onThrottleVideoBitrateWhenHiddenClick(evt) { this.setState({ throttleVideoBitrateWhenHidden: getCheckBoxValue(evt.target), }); + } - onStopAtEndClick = (evt) => + onStopAtEndClick(evt) { this.setState({ stopAtEnd: getCheckBoxValue(evt.target) }); + } - onSegmentRetryInput = (value) => + onSegmentRetryInput(value) { this.setState({ segmentRetry: value }); + } + + onSegmentTimeoutInput(value) { + this.setState({ segmentTimeout: value }); + } - onManifestRetryInput = (value) => + onManifestRetryInput(value) { this.setState({ manifestRetry: value }); + } - onOfflineRetryInput = (value) => + onOfflineRetryInput(value) { this.setState({ offlineRetry: value }); + } + + onManifestTimeoutInput(value) { + this.setState({ manifestTimeout: value }); + } - onEnableFastSwitchingClick = (evt) => + onEnableFastSwitchingClick(evt) { this.setState({ enableFastSwitching: getCheckBoxValue(evt.target) }); + } - onAudioTrackSwitchingModeChange = (value) => + onAudioTrackSwitchingModeChange(value) { this.setState({ audioTrackSwitchingMode: value }); + } - onCodecSwitchChange = (value) => + onCodecSwitchChange(value) { this.setState({ onCodecSwitch: value }); + } - onWantedBufferAheadInput = (value) => + onWantedBufferAheadInput(value) { this.setState({ wantedBufferAhead: value }); - - onMaxVideoBufferSizeInput = (value) => + } + + onMaxVideoBufferSizeInput(value) { this.setState({ maxVideoBufferSize: value}); + } - onMaxBufferBehindInput = (value) => + onMaxBufferBehindInput(value) { this.setState({ maxBufferBehind: value }); + } - onMaxBufferAheadInput = (value) => + onMaxBufferAheadInput(value) { this.setState({ maxBufferAhead: value }); + } render() { const { @@ -155,8 +186,10 @@ class Settings extends React.Component { throttleVideoBitrateWhenHidden, stopAtEnd, segmentRetry, + segmentTimeout, manifestRetry, offlineRetry, + manifestTimeout, enableFastSwitching, audioTrackSwitchingMode, onCodecSwitch, @@ -175,33 +208,38 @@ class Settings extends React.Component { maxAudioBr, limitVideoWidth, throttleVideoBitrateWhenHidden, - onInitialVideoBrInput: this.onInitialVideoBrInput, - onInitialAudioBrInput: this.onInitialAudioBrInput, - onMinAudioBrInput: this.onMinAudioBrInput, - onMinVideoBrInput: this.onMinVideoBrInput, - onMaxAudioBrInput: this.onMaxAudioBrInput, - onMaxVideoBrInput: this.onMaxVideoBrInput, - onLimitVideoWidthClick: this.onLimitVideoWidthClick, + onInitialVideoBrInput: this.onInitialVideoBrInput.bind(this), + onInitialAudioBrInput: this.onInitialAudioBrInput.bind(this), + onMinAudioBrInput: this.onMinAudioBrInput.bind(this), + onMinVideoBrInput: this.onMinVideoBrInput.bind(this), + onMaxAudioBrInput: this.onMaxAudioBrInput.bind(this), + onMaxVideoBrInput: this.onMaxVideoBrInput.bind(this), + onLimitVideoWidthClick: this.onLimitVideoWidthClick.bind(this), onThrottleVideoBitrateWhenHiddenClick: - this.onThrottleVideoBitrateWhenHiddenClick, + this.onThrottleVideoBitrateWhenHiddenClick.bind(this), }; const networkConfig = { + manifestTimeout, segmentRetry, + segmentTimeout, manifestRetry, offlineRetry, - onSegmentRetryInput: this.onSegmentRetryInput, - onManifestRetryInput: this.onManifestRetryInput, - onOfflineRetryInput: this.onOfflineRetryInput, + onSegmentRetryInput: this.onSegmentRetryInput.bind(this), + onSegmentTimeoutInput: this.onSegmentTimeoutInput.bind(this), + onManifestRetryInput: this.onManifestRetryInput.bind(this), + onManifestTimeoutInput: this.onManifestTimeoutInput.bind(this), + onOfflineRetryInput: this.onOfflineRetryInput.bind(this), }; const trackSwitchModeConfig = { enableFastSwitching, audioTrackSwitchingMode, onCodecSwitch, - onEnableFastSwitchingClick: this.onEnableFastSwitchingClick, - onAudioTrackSwitchingModeChange: this.onAudioTrackSwitchingModeChange, - onCodecSwitchChange: this.onCodecSwitchChange, + onEnableFastSwitchingClick: this.onEnableFastSwitchingClick.bind(this), + onAudioTrackSwitchingModeChange: this + .onAudioTrackSwitchingModeChange.bind(this), + onCodecSwitchChange: this.onCodecSwitchChange.bind(this), }; if (!this.props.showOptions) { @@ -210,15 +248,24 @@ class Settings extends React.Component { return (
    +
    + Content options +
    +
    + Note: Those options won't be retroactively applied to + already-loaded contents +
    diff --git a/demo/full/scripts/lib/defaultOptionsValues.js b/demo/full/scripts/lib/defaultOptionsValues.js index 3fa4bb3063..45dd6496c5 100644 --- a/demo/full/scripts/lib/defaultOptionsValues.js +++ b/demo/full/scripts/lib/defaultOptionsValues.js @@ -13,6 +13,8 @@ const defaultOptionsValues = { segmentRetry: 4, manifestRetry: 4, offlineRetry: Infinity, + segmentTimeout: 30000, + manifestTimeout: 30000, enableFastSwitching: true, audioTrackSwitchingMode: "reload", onCodecSwitch: "continue", diff --git a/demo/full/scripts/modules/player/events.js b/demo/full/scripts/modules/player/events.js index 858a192238..00f16fdbaf 100644 --- a/demo/full/scripts/modules/player/events.js +++ b/demo/full/scripts/modules/player/events.js @@ -51,6 +51,9 @@ const linkPlayerEventsToState = (player, state, $destroy) => { // TODO Only active for content playback intervalObservable(POSITION_UPDATES_INTERVAL).pipe( map(() => { + if (player.getPlayerState() === "STOPPED") { + return {}; + } const position = player.getPosition(); const duration = player.getVideoDuration(); const videoTrack = player.getVideoTrack(); @@ -122,7 +125,7 @@ const linkPlayerEventsToState = (player, state, $destroy) => { stateUpdates.videoTrack = null; stateUpdates.currentTime = undefined; stateUpdates.wallClockDiff = undefined; - stateUpdates.bufferGap = undefined; + stateUpdates.bufferGap = 0; stateUpdates.bufferedData = null; stateUpdates.duration = undefined; stateUpdates.minimumPosition = undefined; diff --git a/demo/full/scripts/modules/player/index.js b/demo/full/scripts/modules/player/index.js index 2aa8c82d5d..8a657f2d18 100644 --- a/demo/full/scripts/modules/player/index.js +++ b/demo/full/scripts/modules/player/index.js @@ -77,7 +77,7 @@ const PLAYER = ({ $destroy, state }, initOpts) => { availableSubtitles: [], availableVideoBitrates: [], availableVideoTracks: [], - bufferGap: undefined, + bufferGap: 0, bufferedData: null, cannotLoadMetadata: false, currentTime: undefined, diff --git a/demo/full/styles/style.css b/demo/full/styles/style.css index e772e924fc..141adfb2bd 100644 --- a/demo/full/styles/style.css +++ b/demo/full/styles/style.css @@ -96,7 +96,7 @@ body { } .video-player-content { - max-width: 1000px; + max-width: 1050px; margin: auto; text-align: left; } @@ -425,6 +425,13 @@ body { margin: 5px; } +.option-desc { + font-weight: normal; + font-style: italic; + color: #242424; + font-size: 0.95em; +} + .choice-input-button { font-family: "icons", sans-serif; border: solid 1px #d1d1d1; @@ -641,6 +648,17 @@ body { width: auto; } +.settings-title { + text-align: center; + margin-bottom: 12px; + font-size: 1.5em; +} + +.settings-note { + font-style: italic; + text-align: center; +} + .volume { position: relative; display: flex; @@ -1212,7 +1230,7 @@ input:checked + .slider:before { } .loadVideooptions li { - padding: 5px 10px; + padding: 10px; border-top: dashed 1px black; display: flex; flex-direction: column; @@ -1251,7 +1269,9 @@ select { } .settingsWrapper { - margin: 10px 0; + border: 1px dashed #d1d1d1; + padding: 10px; + margin-top: 10px; } .featureWrapperWithSelectMode { diff --git a/doc/api/Player_Events.md b/doc/api/Player_Events.md index 07db1e9180..b5612790e1 100644 --- a/doc/api/Player_Events.md +++ b/doc/api/Player_Events.md @@ -112,8 +112,13 @@ This chapter describes events linked to the current audio, video or text track. _payload type_: `Array.` -Triggered when the currently available audio tracks change (e.g.: at the -beginning of the content, when period changes...). +Triggered when the currently available audio tracks might have changed (e.g.: at +the beginning of the content, when period changes...) for the currently-playing +Period. + +_The event might also rarely be emitted even if the list of available audio +tracks did not really change - as the RxPlayer might send it in situations where +there's a chance it had without thoroughly checking it._ The array emitted contains object describing each available audio track: @@ -152,8 +157,13 @@ This event only concerns the currently-playing Period. _payload type_: `Array.` -Triggered when the currently available video tracks change (e.g.: at the -beginning of the content, when period changes...). +Triggered when the currently available video tracks might change (e.g.: at the +beginning of the content, when period changes...) for the currently-playing +Period. + +_The event might also rarely be emitted even if the list of available video +tracks did not really change - as the RxPlayer might send it in situations where +there's a chance it had without thoroughly checking it._ The array emitted contains object describing each available video track: @@ -192,8 +202,13 @@ This event only concerns the currently-playing Period. _payload type_: `Array.` -Triggered when the currently available text tracks change (e.g.: at the -beginning of the content, when period changes...). +Triggered when the currently available text tracks might change (e.g.: at the +beginning of the content, when period changes...) for the currently-playing +Period. + +_The event might also rarely be emitted even if the list of available text +tracks did not really change - as the RxPlayer might send it in situations where +there's a chance it had without thoroughly checking it._ The array emitted contains object describing each available text track: diff --git a/src/core/api/public_api.ts b/src/core/api/public_api.ts index 01d9d9327a..e4129729bb 100644 --- a/src/core/api/public_api.ts +++ b/src/core/api/public_api.ts @@ -70,6 +70,7 @@ import { import areArraysOfNumbersEqual from "../../utils/are_arrays_of_numbers_equal"; import assert from "../../utils/assert"; import EventEmitter, { + IEventPayload, IListener, } from "../../utils/event_emitter"; import idGenerator from "../../utils/id_generator"; @@ -86,7 +87,9 @@ import createSharedReference, { IReadOnlySharedReference, ISharedReference, } from "../../utils/reference"; -import TaskCanceller from "../../utils/task_canceller"; +import TaskCanceller, { + CancellationSignal, +} from "../../utils/task_canceller"; import warnOnce from "../../utils/warn_once"; import { IABRThrottlers } from "../adaptive"; import { @@ -754,8 +757,14 @@ class Player extends EventEmitter { throw new Error("DirectFile feature not activated in your build."); } mediaElementTrackChoiceManager = - this._priv_initializeMediaElementTrackChoiceManager(defaultAudioTrack, - defaultTextTrack); + this._priv_initializeMediaElementTrackChoiceManager( + defaultAudioTrack, + defaultTextTrack, + currentContentCanceller.signal + ); + if (currentContentCanceller.isUsed) { + return; + } initializer = new features.directfile.initDirectFile({ autoPlay, keySystems, speed: this._priv_speed, @@ -2318,6 +2327,7 @@ class Player extends EventEmitter { return; // Event for another content } contentInfos.manifest = manifest; + const cancelSignal = contentInfos.currentContentCanceller.signal; this._priv_reloadingMetadata.manifest = manifest; const { initialAudioTrack, initialTextTrack } = contentInfos; @@ -2338,11 +2348,38 @@ class Player extends EventEmitter { contentInfos.trackChoiceManager .setPreferredVideoTracks(this._priv_preferredVideoTracks, true); - manifest.addEventListener("manifestUpdate", () => { + manifest.addEventListener("manifestUpdate", (updates) => { // Update the tracks chosen if it changed if (contentInfos.trackChoiceManager !== null) { contentInfos.trackChoiceManager.update(); } + const currentPeriod = this._priv_contentInfos?.currentPeriod ?? undefined; + const trackChoiceManager = this._priv_contentInfos?.trackChoiceManager; + if (currentPeriod === undefined || isNullOrUndefined(trackChoiceManager)) { + return; + } + for (const update of updates.updatedPeriods) { + if (update.period.id === currentPeriod.id) { + if (update.result.addedAdaptations.length > 0 || + update.result.removedAdaptations.length > 0) + { + // We might have new (or less) tracks, send events just to be sure + const audioTracks = trackChoiceManager.getAvailableAudioTracks(currentPeriod); + this._priv_triggerEventIfNotStopped("availableAudioTracksChange", + audioTracks ?? [], + cancelSignal); + const textTracks = trackChoiceManager.getAvailableTextTracks(currentPeriod); + this._priv_triggerEventIfNotStopped("availableTextTracksChange", + textTracks ?? [], + cancelSignal); + const videoTracks = trackChoiceManager.getAvailableVideoTracks(currentPeriod); + this._priv_triggerEventIfNotStopped("availableVideoTracksChange", + videoTracks ?? [], + cancelSignal); + } + } + return; + } }, contentInfos.currentContentCanceller.signal); } @@ -2362,42 +2399,68 @@ class Player extends EventEmitter { } contentInfos.currentPeriod = period; + const cancelSignal = contentInfos.currentContentCanceller.signal; if (this._priv_contentEventsMemory.periodChange !== period) { this._priv_contentEventsMemory.periodChange = period; - this.trigger("periodChange", period); + this._priv_triggerEventIfNotStopped("periodChange", period, cancelSignal); } - this.trigger("availableAudioTracksChange", this.getAvailableAudioTracks()); - this.trigger("availableTextTracksChange", this.getAvailableTextTracks()); - this.trigger("availableVideoTracksChange", this.getAvailableVideoTracks()); + this._priv_triggerEventIfNotStopped("availableAudioTracksChange", + this.getAvailableAudioTracks(), + cancelSignal); + this._priv_triggerEventIfNotStopped("availableTextTracksChange", + this.getAvailableTextTracks(), + cancelSignal); + this._priv_triggerEventIfNotStopped("availableVideoTracksChange", + this.getAvailableVideoTracks(), + cancelSignal); const trackChoiceManager = this._priv_contentInfos?.trackChoiceManager; // Emit intial events for the Period if (!isNullOrUndefined(trackChoiceManager)) { const audioTrack = trackChoiceManager.getChosenAudioTrack(period); + this._priv_triggerEventIfNotStopped("audioTrackChange", + audioTrack, + cancelSignal); const textTrack = trackChoiceManager.getChosenTextTrack(period); + this._priv_triggerEventIfNotStopped("textTrackChange", + textTrack, + cancelSignal); const videoTrack = trackChoiceManager.getChosenVideoTrack(period); - - this.trigger("audioTrackChange", audioTrack); - this.trigger("textTrackChange", textTrack); - this.trigger("videoTrackChange", videoTrack); + this._priv_triggerEventIfNotStopped("videoTrackChange", + videoTrack, + cancelSignal); } else { - this.trigger("audioTrackChange", null); - this.trigger("textTrackChange", null); - this.trigger("videoTrackChange", null); + this._priv_triggerEventIfNotStopped("audioTrackChange", null, cancelSignal); + this._priv_triggerEventIfNotStopped("textTrackChange", null, cancelSignal); + this._priv_triggerEventIfNotStopped("videoTrackChange", null, cancelSignal); } this._priv_triggerAvailableBitratesChangeEvent("availableAudioBitratesChange", - this.getAvailableAudioBitrates()); + this.getAvailableAudioBitrates(), + cancelSignal); + if (contentInfos.currentContentCanceller.isUsed) { + return; + } this._priv_triggerAvailableBitratesChangeEvent("availableVideoBitratesChange", - this.getAvailableVideoBitrates()); - + this.getAvailableVideoBitrates(), + cancelSignal); + if (contentInfos.currentContentCanceller.isUsed) { + return; + } const audioBitrate = this._priv_getCurrentRepresentations()?.audio?.bitrate ?? -1; - this._priv_triggerCurrentBitrateChangeEvent("audioBitrateChange", audioBitrate); + this._priv_triggerCurrentBitrateChangeEvent("audioBitrateChange", + audioBitrate, + cancelSignal); + if (contentInfos.currentContentCanceller.isUsed) { + return; + } const videoBitrate = this._priv_getCurrentRepresentations()?.video?.bitrate ?? -1; - this._priv_triggerCurrentBitrateChangeEvent("videoBitrateChange", videoBitrate); + this._priv_triggerCurrentBitrateChangeEvent("videoBitrateChange", + videoBitrate, + cancelSignal); } /** @@ -2543,6 +2606,7 @@ class Player extends EventEmitter { } const { trackChoiceManager } = contentInfos; + const cancelSignal = contentInfos.currentContentCanceller.signal; if (trackChoiceManager !== null && currentPeriod !== null && !isNullOrUndefined(period) && period.id === currentPeriod.id) @@ -2550,23 +2614,29 @@ class Player extends EventEmitter { switch (type) { case "audio": const audioTrack = trackChoiceManager.getChosenAudioTrack(currentPeriod); - this.trigger("audioTrackChange", audioTrack); + this._priv_triggerEventIfNotStopped("audioTrackChange", + audioTrack, + cancelSignal); const availableAudioBitrates = this.getAvailableAudioBitrates(); this._priv_triggerAvailableBitratesChangeEvent("availableAudioBitratesChange", - availableAudioBitrates); + availableAudioBitrates, + cancelSignal); break; case "text": const textTrack = trackChoiceManager.getChosenTextTrack(currentPeriod); - this.trigger("textTrackChange", textTrack); + this._priv_triggerEventIfNotStopped("textTrackChange", textTrack, cancelSignal); break; case "video": const videoTrack = trackChoiceManager.getChosenVideoTrack(currentPeriod); - this.trigger("videoTrackChange", videoTrack); + this._priv_triggerEventIfNotStopped("videoTrackChange", + videoTrack, + cancelSignal); const availableVideoBitrates = this.getAvailableVideoBitrates(); this._priv_triggerAvailableBitratesChangeEvent("availableVideoBitratesChange", - availableVideoBitrates); + availableVideoBitrates, + cancelSignal); break; } } @@ -2609,10 +2679,15 @@ class Player extends EventEmitter { currentPeriod !== null && currentPeriod.id === period.id) { + const cancelSignal = this._priv_contentInfos.currentContentCanceller.signal; if (type === "video") { - this._priv_triggerCurrentBitrateChangeEvent("videoBitrateChange", bitrate); + this._priv_triggerCurrentBitrateChangeEvent("videoBitrateChange", + bitrate, + cancelSignal); } else if (type === "audio") { - this._priv_triggerCurrentBitrateChangeEvent("audioBitrateChange", bitrate); + this._priv_triggerCurrentBitrateChangeEvent("audioBitrateChange", + bitrate, + cancelSignal); } } } @@ -2722,13 +2797,17 @@ class Player extends EventEmitter { * the previously stored value. * @param {string} event * @param {Array.} newVal + * @param {Object} currentContentCancelSignal */ private _priv_triggerAvailableBitratesChangeEvent( event : "availableAudioBitratesChange" | "availableVideoBitratesChange", - newVal : number[] + newVal : number[], + currentContentCancelSignal : CancellationSignal ) : void { const prevVal = this._priv_contentEventsMemory[event]; - if (prevVal === undefined || !areArraysOfNumbersEqual(newVal, prevVal)) { + if (!currentContentCancelSignal.isCancelled && + (prevVal === undefined || !areArraysOfNumbersEqual(newVal, prevVal))) + { this._priv_contentEventsMemory[event] = newVal; this.trigger(event, newVal); } @@ -2739,12 +2818,16 @@ class Player extends EventEmitter { * previously stored value. * @param {string} event * @param {number} newVal + * @param {Object} currentContentCancelSignal */ private _priv_triggerCurrentBitrateChangeEvent( event : "audioBitrateChange" | "videoBitrateChange", - newVal : number + newVal : number, + currentContentCancelSignal : CancellationSignal ) : void { - if (newVal !== this._priv_contentEventsMemory[event]) { + if (!currentContentCancelSignal.isCancelled && + newVal !== this._priv_contentEventsMemory[event]) + { this._priv_contentEventsMemory[event] = newVal; this.trigger(event, newVal); } @@ -2765,9 +2848,31 @@ class Player extends EventEmitter { return activeRepresentations[currentPeriod.id]; } + /** + * @param {string} evt + * @param {*} arg + * @param {Object} currentContentCancelSignal + */ + private _priv_triggerEventIfNotStopped( + evt : TEventName, + arg : IEventPayload, + currentContentCancelSignal : CancellationSignal + ) { + if (!currentContentCancelSignal.isCancelled) { + this.trigger(evt, arg); + } + } + + /** + * @param {Object} defaultAudioTrack + * @param {Object} defaultTextTrack + * @param {Object} cancelSignal + * @returns {Object} + */ private _priv_initializeMediaElementTrackChoiceManager( defaultAudioTrack : IAudioTrackPreference | null | undefined, - defaultTextTrack : ITextTrackPreference | null | undefined + defaultTextTrack : ITextTrackPreference | null | undefined, + cancelSignal : CancellationSignal ) : MediaElementTrackChoiceManager { assert(features.directfile !== null, "Initializing `MediaElementTrackChoiceManager` without Directfile feature"); @@ -2790,22 +2895,30 @@ class Player extends EventEmitter { mediaElementTrackChoiceManager .setPreferredVideoTracks(this._priv_preferredVideoTracks, true); - this.trigger("availableAudioTracksChange", - mediaElementTrackChoiceManager.getAvailableAudioTracks()); - this.trigger("availableVideoTracksChange", - mediaElementTrackChoiceManager.getAvailableVideoTracks()); - this.trigger("availableTextTracksChange", - mediaElementTrackChoiceManager.getAvailableTextTracks()); - - this.trigger("audioTrackChange", - mediaElementTrackChoiceManager.getChosenAudioTrack() - ?? null); - this.trigger("textTrackChange", - mediaElementTrackChoiceManager.getChosenTextTrack() - ?? null); - this.trigger("videoTrackChange", - mediaElementTrackChoiceManager.getChosenVideoTrack() - ?? null); + this._priv_triggerEventIfNotStopped( + "availableAudioTracksChange", + mediaElementTrackChoiceManager.getAvailableAudioTracks(), + cancelSignal); + this._priv_triggerEventIfNotStopped( + "availableVideoTracksChange", + mediaElementTrackChoiceManager.getAvailableVideoTracks(), + cancelSignal); + this._priv_triggerEventIfNotStopped( + "availableTextTracksChange", + mediaElementTrackChoiceManager.getAvailableTextTracks(), + cancelSignal); + this._priv_triggerEventIfNotStopped( + "audioTrackChange", + mediaElementTrackChoiceManager.getChosenAudioTrack() ?? null, + cancelSignal); + this._priv_triggerEventIfNotStopped( + "textTrackChange", + mediaElementTrackChoiceManager.getChosenTextTrack() ?? null, + cancelSignal); + this._priv_triggerEventIfNotStopped( + "videoTrackChange", + mediaElementTrackChoiceManager.getChosenVideoTrack() ?? null, + cancelSignal); mediaElementTrackChoiceManager .addEventListener("availableVideoTracksChange", (val) => diff --git a/src/core/stream/adaptation/adaptation_stream.ts b/src/core/stream/adaptation/adaptation_stream.ts index 75623fd593..ef41620758 100644 --- a/src/core/stream/adaptation/adaptation_stream.ts +++ b/src/core/stream/adaptation/adaptation_stream.ts @@ -269,6 +269,9 @@ export default function AdaptationStream( callbacks.addedSegment(segmentInfo); }, terminating() { + if (repStreamTerminatingCanceller.isUsed) { + return; // Already handled + } repStreamTerminatingCanceller.cancel(); return recursivelyCreateRepresentationStreams(false); }, diff --git a/src/manifest/__tests__/manifest.test.ts b/src/manifest/__tests__/manifest.test.ts index cda69eb08b..9faaa611ec 100644 --- a/src/manifest/__tests__/manifest.test.ts +++ b/src/manifest/__tests__/manifest.test.ts @@ -353,18 +353,18 @@ describe("Manifest - Manifest", () => { const fakePeriod = jest.fn((period) => ({ ...period, id: `foo${period.id}`, contentWarnings: [new Error(period.id)] })); - const fakeUpdatePeriodInPlace = jest.fn((oldPeriod, newPeriod) => { - Object.keys(oldPeriod).forEach(key => { - delete oldPeriod[key]; - }); - oldPeriod.id = newPeriod.id; - oldPeriod.start = newPeriod.start; - oldPeriod.adaptations = newPeriod.adaptations; - }); + const fakeReplacePeriodsRes = { + updatedPeriods: [], + addedPeriods: [], + removedPeriods: [], + }; + const fakeReplacePeriods = jest.fn(() => fakeReplacePeriodsRes); jest.mock("../period", () => ({ __esModule: true as const, default: fakePeriod })); - jest.mock("../update_period_in_place", () => ({ __esModule: true as const, - default: fakeUpdatePeriodInPlace })); + jest.mock("../update_periods", () => ({ + __esModule: true as const, + replacePeriods: fakeReplacePeriods, + })); const oldManifestArgs = { availabilityStartTime: 5, duration: 12, @@ -391,8 +391,6 @@ describe("Manifest - Manifest", () => { const mockTrigger = jest.spyOn(manifest, "trigger").mockImplementation(jest.fn()); - const [oldPeriod1, oldPeriod2] = manifest.periods; - const newAdaptations = {}; const newPeriod1 = { id: "foo0", start: 4, adaptations: {} }; const newPeriod2 = { id: "foo1", start: 12, adaptations: {} }; @@ -416,364 +414,15 @@ describe("Manifest - Manifest", () => { uris: ["url3", "url4"] }; manifest.replace(newManifest); - expect(manifest.adaptations).toEqual(newAdaptations); - expect(manifest.availabilityStartTime).toEqual(6); - expect(manifest.id).toEqual("fakeId"); - expect(manifest.isDynamic).toEqual(true); - expect(manifest.isLive).toEqual(true); - expect(manifest.lifetime).toEqual(14); - expect(manifest.contentWarnings).toEqual([new Error("c"), new Error("d")]); - expect(manifest.getMinimumSafePosition()).toEqual(40 - 5); - expect(manifest.getMaximumSafePosition()).toEqual(40); - expect(manifest.suggestedPresentationDelay).toEqual(100); - expect(manifest.uris).toEqual(["url3", "url4"]); - - expect(manifest.periods).toEqual([newPeriod1, newPeriod2]); - - expect(fakeUpdatePeriodInPlace).toHaveBeenCalledTimes(2); - expect(fakeUpdatePeriodInPlace).toHaveBeenCalledWith(oldPeriod1, newPeriod1, 0); - expect(fakeUpdatePeriodInPlace).toHaveBeenCalledWith(oldPeriod2, newPeriod2, 0); + expect(fakeReplacePeriods).toHaveBeenCalledTimes(1); + expect(fakeReplacePeriods) + .toHaveBeenCalledWith(manifest.periods, newManifest.periods); expect(mockTrigger).toHaveBeenCalledTimes(1); - expect(mockTrigger).toHaveBeenCalledWith("manifestUpdate", null); + expect(mockTrigger).toHaveBeenCalledWith("manifestUpdate", fakeReplacePeriodsRes); expect(fakeIdGenerator).toHaveBeenCalledTimes(2); expect(fakeGenerateNewId).toHaveBeenCalledTimes(1); expect(fakeLogger.info).not.toHaveBeenCalled(); expect(fakeLogger.warn).not.toHaveBeenCalled(); mockTrigger.mockRestore(); }); - - it("should prepend older Periods when calling `replace`", () => { - const oldManifestArgs = { availabilityStartTime: 5, - duration: 12, - id: "man", - isDynamic: false, - isLive: false, - lifetime: 13, - timeBounds: { minimumSafePosition: 0, - timeshiftDepth: null, - maximumTimeData: { - isLinear: false, - maximumSafePosition: 10, - time: 10, - } }, - contentWarnings: [new Error("a"), new Error("b")], - periods: [{ id: "1", start: 4, adaptations: {} }], - suggestedPresentationDelay: 99, - uris: ["url1", "url2"] }; - - const fakePeriod = jest.fn((period) => { - return { - ...period, - id: `foo${period.id}`, - contentWarnings: [new Error(period.id)], - }; - }); - const fakeUpdatePeriodInPlace = jest.fn((oldPeriod, newPeriod) => { - Object.keys(oldPeriod).forEach(key => { - delete oldPeriod[key]; - }); - oldPeriod.id = newPeriod.id; - oldPeriod.start = newPeriod.start; - oldPeriod.adaptations = newPeriod.adaptations; - oldPeriod.contentWarnings = newPeriod.contentWarnings; - }); - jest.mock("../period", () => ({ __esModule: true as const, - default: fakePeriod })); - jest.mock("../update_period_in_place", () => ({ - __esModule: true as const, - default: fakeUpdatePeriodInPlace, - })); - const Manifest = jest.requireActual("../manifest").default; - const manifest = new Manifest(oldManifestArgs, {}); - const [oldPeriod1] = manifest.periods; - - const mockTrigger = jest.spyOn(manifest, "trigger").mockImplementation(jest.fn()); - - const newPeriod1 = { id: "pre0", - start: 0, - adaptations: {}, - contentWarnings: [] }; - const newPeriod2 = { id: "pre1", - start: 2, - adaptations: {}, - contentWarnings: [] }; - const newPeriod3 = { id: "foo1", - start: 4, - adaptations: {}, - contentWarnings: [] }; - const newManifest = { adaptations: {}, - availabilityStartTime: 6, - id: "man2", - isDynamic: false, - isLive: true, - lifetime: 14, - contentWarnings: [ new Error("c"), - new Error("d") ], - suggestedPresentationDelay: 100, - periods: [ newPeriod1, - newPeriod2, - newPeriod3 ], - timeBounds: { minimumSafePosition: 0, - timeshiftDepth: null, - maximumTimeData: { isLinear: false, - maximumSafePosition: 10, - time: 10 } }, - uris: ["url3", "url4"] }; - - manifest.replace(newManifest); - - expect(manifest.periods).toEqual([newPeriod1, newPeriod2, newPeriod3]); - - expect(fakeUpdatePeriodInPlace).toHaveBeenCalledTimes(1); - expect(fakeUpdatePeriodInPlace).toHaveBeenCalledWith(oldPeriod1, newPeriod3, 0); - expect(mockTrigger).toHaveBeenCalledTimes(1); - expect(mockTrigger).toHaveBeenCalledWith("manifestUpdate", null); - expect(fakeIdGenerator).toHaveBeenCalledTimes(2); - expect(fakeGenerateNewId).toHaveBeenCalledTimes(1); - // expect(fakeLogger.info).toHaveBeenCalledTimes(2); - // expect(fakeLogger.info).toHaveBeenCalledWith( - // "Manifest: Adding new Period pre0 after update."); - // expect(fakeLogger.info).toHaveBeenCalledWith( - // "Manifest: Adding new Period pre1 after update."); - mockTrigger.mockRestore(); - }); - - it("should append newer Periods when calling `replace`", () => { - const oldManifestArgs = { availabilityStartTime: 5, - duration: 12, - id: "man", - isDynamic: false, - isLive: false, - lifetime: 13, - contentWarnings: [new Error("a"), new Error("b")], - periods: [{ id: "1" }], - suggestedPresentationDelay: 99, - timeBounds: { minimumSafePosition: 0, - timeshiftDepth: null, - maximumTimeData: { - isLinear: false, - maximumSafePosition: 10, - time: 10, - } }, - uris: ["url1", "url2"] }; - - const fakePeriod = jest.fn((period) => { - return { ...period, - id: `foo${period.id}`, - contentWarnings: [new Error(period.id)] }; - }); - const fakeUpdatePeriodInPlace = jest.fn((oldPeriod, newPeriod) => { - Object.keys(oldPeriod).forEach(key => { - delete oldPeriod[key]; - }); - oldPeriod.id = newPeriod.id; - }); - jest.mock("../period", () => ({ __esModule: true as const, - default: fakePeriod })); - jest.mock("../update_period_in_place", () => ({ - __esModule: true as const, - default: fakeUpdatePeriodInPlace, - })); - const Manifest = jest.requireActual("../manifest").default; - const manifest = new Manifest(oldManifestArgs, {}); - const [oldPeriod1] = manifest.periods; - - const mockTrigger = jest.spyOn(manifest, "trigger").mockImplementation(jest.fn()); - - const newPeriod1 = { id: "foo1" }; - const newPeriod2 = { id: "post0" }; - const newPeriod3 = { id: "post1" }; - const newManifest = { adaptations: {}, - availabilityStartTime: 6, - id: "man2", - isDynamic: false, - isLive: true, - lifetime: 14, - contentWarnings: [new Error("c"), new Error("d")], - suggestedPresentationDelay: 100, - periods: [newPeriod1, newPeriod2, newPeriod3], - timeBounds: { minimumSafePosition: 0, - timeshiftDepth: null, - maximumTimeData: { isLinear: false, - maximumSafePosition: 10, - time: 10 } }, - uris: ["url3", "url4"] }; - - manifest.replace(newManifest); - - expect(manifest.periods).toEqual([newPeriod1, newPeriod2, newPeriod3]); - - expect(fakeUpdatePeriodInPlace).toHaveBeenCalledTimes(1); - expect(fakeUpdatePeriodInPlace).toHaveBeenCalledWith(oldPeriod1, newPeriod1, 0); - expect(mockTrigger).toHaveBeenCalledTimes(1); - expect(mockTrigger).toHaveBeenCalledWith("manifestUpdate", null); - expect(fakeIdGenerator).toHaveBeenCalledTimes(2); - expect(fakeGenerateNewId).toHaveBeenCalledTimes(1); - // expect(fakeLogger.warn).toHaveBeenCalledTimes(1); - // expect(fakeLogger.warn) - // .toHaveBeenCalledWith("Manifest: Adding new Periods after update."); - mockTrigger.mockRestore(); - }); - - it("should replace different Periods when calling `replace`", () => { - const oldManifestArgs = { availabilityStartTime: 5, - duration: 12, - id: "man", - isDynamic: false, - isLive: false, - lifetime: 13, - contentWarnings: [new Error("a"), new Error("b")], - periods: [{ id: "1" }], - suggestedPresentationDelay: 99, - timeBounds: { minimumSafePosition: 0, - timeshiftDepth: null, - maximumTimeData: { - isLinear: false, - maximumSafePosition: 10, - time: 10, - } }, - uris: ["url1", "url2"] }; - - const fakePeriod = jest.fn((period) => { - return { ...period, - id: `foo${period.id}`, - contentWarnings: [new Error(period.id)] }; - }); - const fakeUpdatePeriodInPlace = jest.fn((oldPeriod, newPeriod) => { - Object.keys(oldPeriod).forEach(key => { - delete oldPeriod[key]; - }); - oldPeriod.id = newPeriod.id; - }); - jest.mock("../period", () => ({ __esModule: true as const, - default: fakePeriod })); - jest.mock("../update_period_in_place", () => ({ - __esModule: true as const, - default: fakeUpdatePeriodInPlace, - })); - const Manifest = jest.requireActual("../manifest").default; - const manifest = new Manifest(oldManifestArgs, {}); - - const mockTrigger = jest.spyOn(manifest, "trigger").mockImplementation(jest.fn()); - - const newPeriod1 = { id: "diff0" }; - const newPeriod2 = { id: "diff1" }; - const newPeriod3 = { id: "diff2" }; - const newManifest = { adaptations: {}, - availabilityStartTime: 6, - id: "man2", - isDynamic: false, - isLive: true, - lifetime: 14, - contentWarnings: [new Error("c"), new Error("d")], - suggestedPresentationDelay: 100, - periods: [newPeriod1, newPeriod2, newPeriod3], - timeBounds: { minimumSafePosition: 0, - timeshiftDepth: null, - maximumTimeData: { isLinear: false, - maximumSafePosition: 10, - time: 10 } }, - uris: ["url3", "url4"] }; - - manifest.replace(newManifest); - - expect(manifest.periods).toEqual([newPeriod1, newPeriod2, newPeriod3]); - - expect(fakeUpdatePeriodInPlace).not.toHaveBeenCalled(); - expect(mockTrigger).toHaveBeenCalledTimes(1); - expect(mockTrigger).toHaveBeenCalledWith("manifestUpdate", null); - expect(fakeIdGenerator).toHaveBeenCalledTimes(2); - expect(fakeGenerateNewId).toHaveBeenCalledTimes(1); - // expect(fakeLogger.info).toHaveBeenCalledTimes(4); - mockTrigger.mockRestore(); - }); - - it("should merge overlapping Periods when calling `replace`", () => { - const oldManifestArgs = { availabilityStartTime: 5, - duration: 12, - id: "man", - isDynamic: false, - isLive: false, - lifetime: 13, - contentWarnings: [new Error("a"), new Error("b")], - periods: [{ id: "1", start: 2 }, - { id: "2", start: 4 }, - { id: "3", start: 6 }], - timeBounds: { minimumSafePosition: 0, - timeshiftDepth: null, - maximumTimeData: { - isLinear: false, - maximumSafePosition: 10, - time: 10, - } }, - suggestedPresentationDelay: 99, - uris: ["url1", "url2"] }; - - const fakePeriod = jest.fn((period) => { - return { ...period, - id: `foo${period.id}`, - contentWarnings: [new Error(period.id)] }; - }); - const fakeUpdatePeriodInPlace = jest.fn((oldPeriod, newPeriod) => { - Object.keys(oldPeriod).forEach(key => { - delete oldPeriod[key]; - }); - oldPeriod.id = newPeriod.id; - oldPeriod.start = newPeriod.start; - }); - jest.mock("../period", () => ({ __esModule: true as const, - default: fakePeriod })); - jest.mock("../update_period_in_place", () => ({ - __esModule: true as const, - default: fakeUpdatePeriodInPlace, - })); - const Manifest = jest.requireActual("../manifest").default; - const manifest = new Manifest(oldManifestArgs, {}); - const [oldPeriod1, oldPeriod2] = manifest.periods; - - const mockTrigger = jest.spyOn(manifest, "trigger").mockImplementation(jest.fn()); - - const newPeriod1 = { id: "pre0", start: 0 }; - const newPeriod2 = { id: "foo1", start: 2 }; - const newPeriod3 = { id: "diff0", start: 3 }; - const newPeriod4 = { id: "foo2", start: 4 }; - const newPeriod5 = { id: "post0", start: 5 }; - const newManifest = { adaptations: {}, - availabilityStartTime: 6, - id: "man2", - isDynamic: false, - isLive: true, - lifetime: 14, - contentWarnings: [new Error("c"), new Error("d")], - suggestedPresentationDelay: 100, - periods: [ newPeriod1, - newPeriod2, - newPeriod3, - newPeriod4, - newPeriod5 ], - timeBounds: { minimumSafePosition: 0, - timeshiftDepth: null, - maximumTimeData: { isLinear: false, - maximumSafePosition: 10, - time: 10 } }, - uris: ["url3", "url4"] }; - - manifest.replace(newManifest); - - expect(manifest.periods).toEqual([ newPeriod1, - newPeriod2, - newPeriod3, - newPeriod4, - newPeriod5 ]); - - expect(fakeUpdatePeriodInPlace).toHaveBeenCalledTimes(2); - expect(fakeUpdatePeriodInPlace).toHaveBeenCalledWith(oldPeriod1, newPeriod2, 0); - expect(fakeUpdatePeriodInPlace).toHaveBeenCalledWith(oldPeriod2, newPeriod4, 0); - expect(mockTrigger).toHaveBeenCalledTimes(1); - expect(mockTrigger).toHaveBeenCalledWith("manifestUpdate", null); - expect(fakeIdGenerator).toHaveBeenCalledTimes(2); - expect(fakeGenerateNewId).toHaveBeenCalledTimes(1); - // expect(fakeLogger.info).toHaveBeenCalledTimes(5); - mockTrigger.mockRestore(); - }); }); diff --git a/src/manifest/__tests__/update_period_in_place.test.ts b/src/manifest/__tests__/update_period_in_place.test.ts index f611128c67..d09606c5ed 100644 --- a/src/manifest/__tests__/update_period_in_place.test.ts +++ b/src/manifest/__tests__/update_period_in_place.test.ts @@ -155,14 +155,17 @@ describe("Manifest - updatePeriodInPlace", () => { it("should fully update the first Period given by the second one in a full update", () => { /* eslint-enable max-len */ const oldVideoAdaptation1 = { contentWarnings: [], + type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1, oldVideoRepresentation2] }; const oldVideoAdaptation2 = { contentWarnings: [], + type: "video", id: "ada-video-2", representations: [oldVideoRepresentation3, oldVideoRepresentation4] }; const oldAudioAdaptation = { contentWarnings: [], + type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation] }; const oldPeriod = { @@ -180,14 +183,17 @@ describe("Manifest - updatePeriodInPlace", () => { }, }; const newVideoAdaptation1 = { contentWarnings: [], + type: "video", id: "ada-video-1", representations: [newVideoRepresentation1, newVideoRepresentation2] }; const newVideoAdaptation2 = { contentWarnings: [], + type: "video", id: "ada-video-2", representations: [newVideoRepresentation3, newVideoRepresentation4] }; const newAudioAdaptation = { contentWarnings: [], + type: "audio", id: "ada-audio-1", representations: [newAudioRepresentation] }; const newPeriod = { @@ -209,9 +215,40 @@ describe("Manifest - updatePeriodInPlace", () => { const newPeriodAdaptations = jest.spyOn(newPeriod, "getAdaptations"); const mockLog = jest.spyOn(log, "warn"); - updatePeriodInPlace(oldPeriod as unknown as Period, - newPeriod as unknown as Period, - MANIFEST_UPDATE_TYPE.Full); + const res = updatePeriodInPlace(oldPeriod as unknown as Period, + newPeriod as unknown as Period, + MANIFEST_UPDATE_TYPE.Full); + + expect(res).toEqual({ addedAdaptations: [], + removedAdaptations: [], + updatedAdaptations: [ + { + adaptation: oldVideoAdaptation1, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldVideoRepresentation1, + oldVideoRepresentation2, + ], + }, + { + adaptation: oldVideoAdaptation2, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldVideoRepresentation3, + oldVideoRepresentation4, + ], + }, + { + adaptation: oldAudioAdaptation, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldAudioRepresentation, + ], + }, + ] }); expect(oldPeriod.start).toEqual(500); expect(oldPeriod.end).toEqual(520); @@ -260,7 +297,7 @@ describe("Manifest - updatePeriodInPlace", () => { expect(mockNewVideoRepresentation4Update).not.toHaveBeenCalled(); expect(mockNewAudioRepresentationUpdate).not.toHaveBeenCalled(); - expect(mockLog).not.toHaveBeenCalled(); + expect(mockLog).toHaveBeenCalledTimes(0); mockLog.mockRestore(); }); @@ -269,14 +306,17 @@ describe("Manifest - updatePeriodInPlace", () => { /* eslint-enable max-len */ const oldVideoAdaptation1 = { contentWarnings: [], id: "ada-video-1", + type: "video", representations: [oldVideoRepresentation1, oldVideoRepresentation2] }; const oldVideoAdaptation2 = { contentWarnings: [], id: "ada-video-2", + type: "video", representations: [oldVideoRepresentation3, oldVideoRepresentation4] }; const oldAudioAdaptation = { contentWarnings: [], id: "ada-audio-1", + type: "audio", representations: [oldAudioRepresentation] }; const oldPeriod = { contentWarnings: [], @@ -294,14 +334,17 @@ describe("Manifest - updatePeriodInPlace", () => { }; const newVideoAdaptation1 = { contentWarnings: [], id: "ada-video-1", + type: "video", representations: [newVideoRepresentation1, newVideoRepresentation2] }; const newVideoAdaptation2 = { contentWarnings: [], id: "ada-video-2", + type: "video", representations: [newVideoRepresentation3, newVideoRepresentation4] }; const newAudioAdaptation = { contentWarnings: [], id: "ada-audio-1", + type: "audio", representations: [newAudioRepresentation] }; const newPeriod = { contentWarnings: [], @@ -322,9 +365,39 @@ describe("Manifest - updatePeriodInPlace", () => { const mockNewPeriodGetAdaptations = jest.spyOn(newPeriod, "getAdaptations"); const mockLog = jest.spyOn(log, "warn"); - updatePeriodInPlace(oldPeriod as unknown as Period, - newPeriod as unknown as Period, - MANIFEST_UPDATE_TYPE.Partial); + const res = updatePeriodInPlace(oldPeriod as unknown as Period, + newPeriod as unknown as Period, + MANIFEST_UPDATE_TYPE.Partial); + expect(res).toEqual({ addedAdaptations: [], + removedAdaptations: [], + updatedAdaptations: [ + { + adaptation: oldVideoAdaptation1, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldVideoRepresentation1, + oldVideoRepresentation2, + ], + }, + { + adaptation: oldVideoAdaptation2, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldVideoRepresentation3, + oldVideoRepresentation4, + ], + }, + { + adaptation: oldAudioAdaptation, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldAudioRepresentation, + ], + }, + ] }); expect(oldPeriod.start).toEqual(500); expect(oldPeriod.end).toEqual(520); @@ -373,16 +446,18 @@ describe("Manifest - updatePeriodInPlace", () => { expect(mockNewVideoRepresentation4Replace).not.toHaveBeenCalled(); expect(mockNewAudioRepresentationReplace).not.toHaveBeenCalled(); - expect(mockLog).not.toHaveBeenCalled(); + expect(mockLog).toHaveBeenCalledTimes(0); mockLog.mockRestore(); }); - it("should do nothing with new Adaptations", () => { + it("should add new Adaptations in Full mode", () => { const oldVideoAdaptation1 = { contentWarnings: [], + type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1, oldVideoRepresentation2] }; const oldAudioAdaptation = { contentWarnings: [], + type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation] }; const oldPeriod = { @@ -399,14 +474,17 @@ describe("Manifest - updatePeriodInPlace", () => { }; const newVideoAdaptation1 = { contentWarnings: [], id: "ada-video-1", + type: "video", representations: [newVideoRepresentation1, newVideoRepresentation2] }; const newVideoAdaptation2 = { contentWarnings: [], id: "ada-video-2", + type: "video", representations: [newVideoRepresentation3, newVideoRepresentation4] }; const newAudioAdaptation = { contentWarnings: [], id: "ada-audio-1", + type: "audio", representations: [newAudioRepresentation] }; const newPeriod = { contentWarnings: [], @@ -424,36 +502,144 @@ describe("Manifest - updatePeriodInPlace", () => { }; const mockLog = jest.spyOn(log, "warn"); - updatePeriodInPlace(oldPeriod as unknown as Period, - newPeriod as unknown as Period, - MANIFEST_UPDATE_TYPE.Full); - expect(mockLog).not.toHaveBeenCalled(); - expect(oldPeriod.adaptations.video).toHaveLength(1); - updatePeriodInPlace(oldPeriod as unknown as Period, - newPeriod as unknown as Period, - MANIFEST_UPDATE_TYPE.Partial); - expect(mockLog).not.toHaveBeenCalled(); - expect(oldPeriod.adaptations.video).toHaveLength(1); + const res = updatePeriodInPlace(oldPeriod as unknown as Period, + newPeriod as unknown as Period, + MANIFEST_UPDATE_TYPE.Full); + expect(res).toEqual({ addedAdaptations: [newVideoAdaptation2], + removedAdaptations: [], + updatedAdaptations: [ + { + adaptation: oldVideoAdaptation1, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldVideoRepresentation1, + oldVideoRepresentation2, + ], + }, + { + adaptation: oldAudioAdaptation, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldAudioRepresentation, + ], + }, + ] }); + expect(mockLog).toHaveBeenCalled(); + expect(mockLog).toHaveBeenNthCalledWith( + 1, + "Manifest: 1 new Adaptations found when merging." + ); + expect(oldPeriod.adaptations.video).toHaveLength(2); mockLog.mockRestore(); }); - it("should warn if an old Adaptation is not found", () => { + it("should add new Adaptations in Partial mode", () => { const oldVideoAdaptation1 = { contentWarnings: [], + type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1, oldVideoRepresentation2] }; - const oldVideoAdaptation2 = { contentWarnings: [], - id: "ada-video-2", - representations: [oldVideoRepresentation3, - oldVideoRepresentation4] }; const oldAudioAdaptation = { contentWarnings: [], + type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation] }; const oldPeriod = { + contentWarnings: [], + start: 5, + end: 15, + duration: 10, + adaptations: { video: [oldVideoAdaptation1], + audio: [oldAudioAdaptation] }, + getAdaptations() { + return [oldVideoAdaptation1, + oldAudioAdaptation]; + }, + }; + const newVideoAdaptation1 = { contentWarnings: [], + id: "ada-video-1", + type: "video", + representations: [newVideoRepresentation1, + newVideoRepresentation2] }; + const newVideoAdaptation2 = { contentWarnings: [], + id: "ada-video-2", + type: "video", + representations: [newVideoRepresentation3, + newVideoRepresentation4] }; + const newAudioAdaptation = { contentWarnings: [], + id: "ada-audio-1", + type: "audio", + representations: [newAudioRepresentation] }; + const newPeriod = { contentWarnings: [], start: 500, end: 520, duration: 20, + adaptations: { video: [newVideoAdaptation1, + newVideoAdaptation2], + audio: [newAudioAdaptation] }, + getAdaptations() { + return [newVideoAdaptation1, + newVideoAdaptation2, + newAudioAdaptation]; + }, + }; + + const mockLog = jest.spyOn(log, "warn"); + const res = updatePeriodInPlace(oldPeriod as unknown as Period, + newPeriod as unknown as Period, + MANIFEST_UPDATE_TYPE.Partial); + expect(res).toEqual({ addedAdaptations: [newVideoAdaptation2], + removedAdaptations: [], + updatedAdaptations: [ + { + adaptation: oldVideoAdaptation1, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldVideoRepresentation1, + oldVideoRepresentation2, + ], + }, + { + adaptation: oldAudioAdaptation, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldAudioRepresentation, + ], + }, + ] }); + expect(mockLog).toHaveBeenCalled(); + expect(mockLog).toHaveBeenNthCalledWith( + 1, + "Manifest: 1 new Adaptations found when merging." + ); + expect(oldPeriod.adaptations.video).toHaveLength(2); + mockLog.mockRestore(); + }); + + it("should remove unfound Adaptations in Full mode", () => { + const oldVideoAdaptation1 = { contentWarnings: [], + type: "video", + id: "ada-video-1", + representations: [oldVideoRepresentation1, + oldVideoRepresentation2] }; + const oldVideoAdaptation2 = { contentWarnings: [], + id: "ada-video-2", + type: "video", + representations: [newVideoRepresentation3, + newVideoRepresentation4] }; + const oldAudioAdaptation = { contentWarnings: [], + type: "audio", + id: "ada-audio-1", + representations: [oldAudioRepresentation] }; + const oldPeriod = { + contentWarnings: [], + start: 5, + end: 15, + duration: 10, adaptations: { video: [oldVideoAdaptation1, oldVideoAdaptation2], audio: [oldAudioAdaptation] }, @@ -465,49 +651,223 @@ describe("Manifest - updatePeriodInPlace", () => { }; const newVideoAdaptation1 = { contentWarnings: [], id: "ada-video-1", + type: "video", representations: [newVideoRepresentation1, newVideoRepresentation2] }; const newAudioAdaptation = { contentWarnings: [], id: "ada-audio-1", + type: "audio", representations: [newAudioRepresentation] }; const newPeriod = { + contentWarnings: [], + start: 500, + end: 520, + duration: 20, + adaptations: { video: [newVideoAdaptation1], + audio: [newAudioAdaptation] }, + getAdaptations() { + return [newVideoAdaptation1, + newAudioAdaptation]; + }, + }; + + const mockLog = jest.spyOn(log, "warn"); + const res = updatePeriodInPlace(oldPeriod as unknown as Period, + newPeriod as unknown as Period, + MANIFEST_UPDATE_TYPE.Full); + expect(res).toEqual({ addedAdaptations: [], + removedAdaptations: [oldVideoAdaptation2], + updatedAdaptations: [ + { + adaptation: oldVideoAdaptation1, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldVideoRepresentation1, + oldVideoRepresentation2, + ], + }, + { + adaptation: oldAudioAdaptation, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldAudioRepresentation, + ], + }, + ] }); + expect(mockLog).toHaveBeenCalled(); + expect(mockLog).toHaveBeenNthCalledWith( + 1, + "Manifest: Adaptation \"ada-video-2\" not found when merging." + ); + expect(oldPeriod.adaptations.video).toHaveLength(2); + mockLog.mockRestore(); + }); + + it("should remove unfound Adaptations in Partial mode", () => { + const oldVideoAdaptation1 = { contentWarnings: [], + type: "video", + id: "ada-video-1", + representations: [oldVideoRepresentation1, + oldVideoRepresentation2] }; + const oldVideoAdaptation2 = { contentWarnings: [], + id: "ada-video-2", + type: "video", + representations: [newVideoRepresentation3, + newVideoRepresentation4] }; + const oldAudioAdaptation = { contentWarnings: [], + type: "audio", + id: "ada-audio-1", + representations: [oldAudioRepresentation] }; + const oldPeriod = { contentWarnings: [], start: 5, end: 15, duration: 10, + adaptations: { video: [oldVideoAdaptation1, + oldVideoAdaptation2], + audio: [oldAudioAdaptation] }, + getAdaptations() { + return [oldVideoAdaptation1, + oldVideoAdaptation2, + oldAudioAdaptation]; + }, + }; + const newVideoAdaptation1 = { contentWarnings: [], + id: "ada-video-1", + type: "video", + representations: [newVideoRepresentation1, + newVideoRepresentation2] }; + const newAudioAdaptation = { contentWarnings: [], + id: "ada-audio-1", + type: "audio", + representations: [newAudioRepresentation] }; + const newPeriod = { + contentWarnings: [], + start: 500, + end: 520, + duration: 20, adaptations: { video: [newVideoAdaptation1], audio: [newAudioAdaptation] }, getAdaptations() { - return [newVideoAdaptation1, newAudioAdaptation]; + return [newVideoAdaptation1, + newAudioAdaptation]; }, }; const mockLog = jest.spyOn(log, "warn"); - updatePeriodInPlace(oldPeriod as unknown as Period, - newPeriod as unknown as Period, - MANIFEST_UPDATE_TYPE.Full); - expect(mockLog).toHaveBeenCalledTimes(1); - expect(mockLog).toHaveBeenCalledWith( + const res = updatePeriodInPlace(oldPeriod as unknown as Period, + newPeriod as unknown as Period, + MANIFEST_UPDATE_TYPE.Partial); + expect(res).toEqual({ addedAdaptations: [], + removedAdaptations: [oldVideoAdaptation2], + updatedAdaptations: [ + { + adaptation: oldVideoAdaptation1, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldVideoRepresentation1, + oldVideoRepresentation2, + ], + }, + { + adaptation: oldAudioAdaptation, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldAudioRepresentation, + ], + }, + ] }); + expect(mockLog).toHaveBeenCalled(); + expect(mockLog).toHaveBeenNthCalledWith( + 1, "Manifest: Adaptation \"ada-video-2\" not found when merging." ); expect(oldPeriod.adaptations.video).toHaveLength(2); - mockLog.mockClear(); - updatePeriodInPlace(oldPeriod as unknown as Period, - newPeriod as unknown as Period, - MANIFEST_UPDATE_TYPE.Partial); + mockLog.mockRestore(); + }); + + it("should add new Representations in Full mode", () => { + const oldVideoAdaptation1 = { contentWarnings: [], + type: "video", + id: "ada-video-1", + representations: [oldVideoRepresentation1] }; + const oldAudioAdaptation = { contentWarnings: [], + type: "audio", + id: "ada-audio-1", + representations: [oldAudioRepresentation] }; + const oldPeriod = { contentWarnings: [], + start: 5, + end: 15, + duration: 10, + adaptations: { video: [oldVideoAdaptation1], + audio: [oldAudioAdaptation] }, + getAdaptations() { return [oldVideoAdaptation1, + oldAudioAdaptation]; } }; + + const newVideoAdaptation1 = { contentWarnings: [], + id: "ada-video-1", + type: "video", + representations: [newVideoRepresentation1, + newVideoRepresentation2] }; + const newAudioAdaptation = { contentWarnings: [], + id: "ada-audio-1", + type: "audio", + representations: [newAudioRepresentation] }; + const newPeriod = { + contentWarnings: [], + start: 500, + end: 520, + duration: 20, + adaptations: { video: [newVideoAdaptation1], + audio: [newAudioAdaptation] }, + getAdaptations() { + return [newVideoAdaptation1, newAudioAdaptation]; + }, + }; + + const mockLog = jest.spyOn(log, "warn"); + const res = updatePeriodInPlace(oldPeriod as unknown as Period, + newPeriod as unknown as Period, + MANIFEST_UPDATE_TYPE.Full); + expect(res).toEqual({ addedAdaptations: [], + removedAdaptations: [], + updatedAdaptations: [ + { + adaptation: oldVideoAdaptation1, + addedRepresentations: [newVideoRepresentation2], + removedRepresentations: [], + updatedRepresentations: [ + oldVideoRepresentation1, + ], + }, + { + adaptation: oldAudioAdaptation, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldAudioRepresentation, + ], + }, + ] }); expect(mockLog).toHaveBeenCalledTimes(1); expect(mockLog).toHaveBeenCalledWith( - "Manifest: Adaptation \"ada-video-2\" not found when merging." + "Manifest: 1 new Representations found when merging." ); - expect(oldPeriod.adaptations.video).toHaveLength(2); + expect(oldVideoAdaptation1.representations).toHaveLength(2); mockLog.mockRestore(); }); - it("should do nothing with new Representations", () => { + it("should add new Representations in Partial mode", () => { const oldVideoAdaptation1 = { contentWarnings: [], + type: "video", id: "ada-video-1", representations: [oldVideoRepresentation1] }; const oldAudioAdaptation = { contentWarnings: [], + type: "audio", id: "ada-audio-1", representations: [oldAudioRepresentation] }; const oldPeriod = { contentWarnings: [], @@ -521,10 +881,12 @@ describe("Manifest - updatePeriodInPlace", () => { const newVideoAdaptation1 = { contentWarnings: [], id: "ada-video-1", + type: "video", representations: [newVideoRepresentation1, newVideoRepresentation2] }; const newAudioAdaptation = { contentWarnings: [], id: "ada-audio-1", + type: "audio", representations: [newAudioRepresentation] }; const newPeriod = { contentWarnings: [], @@ -539,26 +901,46 @@ describe("Manifest - updatePeriodInPlace", () => { }; const mockLog = jest.spyOn(log, "warn"); - updatePeriodInPlace(oldPeriod as unknown as Period, - newPeriod as unknown as Period, - MANIFEST_UPDATE_TYPE.Full); - expect(mockLog).not.toHaveBeenCalled(); - expect(oldVideoAdaptation1.representations).toHaveLength(1); - updatePeriodInPlace(oldPeriod as unknown as Period, - newPeriod as unknown as Period, - MANIFEST_UPDATE_TYPE.Partial); - expect(mockLog).not.toHaveBeenCalled(); - expect(oldVideoAdaptation1.representations).toHaveLength(1); + const res = updatePeriodInPlace(oldPeriod as unknown as Period, + newPeriod as unknown as Period, + MANIFEST_UPDATE_TYPE.Partial); + expect(res).toEqual({ addedAdaptations: [], + removedAdaptations: [], + updatedAdaptations: [ + { + adaptation: oldVideoAdaptation1, + addedRepresentations: [newVideoRepresentation2], + removedRepresentations: [], + updatedRepresentations: [ + oldVideoRepresentation1, + ], + }, + { + adaptation: oldAudioAdaptation, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldAudioRepresentation, + ], + }, + ] }); + expect(mockLog).toHaveBeenCalledTimes(1); + expect(mockLog).toHaveBeenCalledWith( + "Manifest: 1 new Representations found when merging." + ); + expect(oldVideoAdaptation1.representations).toHaveLength(2); mockLog.mockRestore(); }); - it("should warn if an old Representation is not found", () => { + it("should remove an old Representation that is not found in Full mode", () => { const oldVideoAdaptation1 = { contentWarnings: [], id: "ada-video-1", + type: "video", representations: [oldVideoRepresentation1, oldVideoRepresentation2] }; const oldAudioAdaptation = { contentWarnings: [], id: "ada-audio-1", + type: "audio", representations: [oldAudioRepresentation] }; const oldPeriod = { contentWarnings: [], start: 500, @@ -570,9 +952,11 @@ describe("Manifest - updatePeriodInPlace", () => { oldAudioAdaptation]; } }; const newVideoAdaptation1 = { contentWarnings: [], id: "ada-video-1", + type: "video", representations: [newVideoRepresentation1] }; const newAudioAdaptation = { contentWarnings: [], id: "ada-audio-1", + type: "audio", representations: [newAudioRepresentation] }; const newPeriod = { contentWarnings: [], start: 5, @@ -587,23 +971,104 @@ describe("Manifest - updatePeriodInPlace", () => { }; const mockLog = jest.spyOn(log, "warn"); - updatePeriodInPlace(oldPeriod as unknown as Period, - newPeriod as unknown as Period, - MANIFEST_UPDATE_TYPE.Full); + const res = updatePeriodInPlace(oldPeriod as unknown as Period, + newPeriod as unknown as Period, + MANIFEST_UPDATE_TYPE.Full); + expect(res).toEqual({ addedAdaptations: [], + removedAdaptations: [], + updatedAdaptations: [ + { + adaptation: oldVideoAdaptation1, + addedRepresentations: [], + removedRepresentations: [oldVideoRepresentation2], + updatedRepresentations: [ + oldVideoRepresentation1, + ], + }, + { + adaptation: oldAudioAdaptation, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldAudioRepresentation, + ], + }, + ] }); expect(mockLog).toHaveBeenCalledTimes(1); expect(mockLog).toHaveBeenCalledWith( "Manifest: Representation \"rep-video-2\" not found when merging." ); - expect(oldVideoAdaptation1.representations).toHaveLength(2); - mockLog.mockClear(); - updatePeriodInPlace(oldPeriod as unknown as Period, - newPeriod as unknown as Period, - MANIFEST_UPDATE_TYPE.Partial); + expect(oldVideoAdaptation1.representations).toHaveLength(1); + mockLog.mockRestore(); + }); + + it("should remove an old Representation that is not found in Partial mode", () => { + const oldVideoAdaptation1 = { contentWarnings: [], + id: "ada-video-1", + type: "video", + representations: [oldVideoRepresentation1, + oldVideoRepresentation2] }; + const oldAudioAdaptation = { contentWarnings: [], + id: "ada-audio-1", + type: "audio", + representations: [oldAudioRepresentation] }; + const oldPeriod = { contentWarnings: [], + start: 500, + end: 520, + duration: 20, + adaptations: { video: [oldVideoAdaptation1], + audio: [oldAudioAdaptation] }, + getAdaptations() { return [oldVideoAdaptation1, + oldAudioAdaptation]; } }; + const newVideoAdaptation1 = { contentWarnings: [], + id: "ada-video-1", + type: "video", + representations: [newVideoRepresentation1] }; + const newAudioAdaptation = { contentWarnings: [], + id: "ada-audio-1", + type: "audio", + representations: [newAudioRepresentation] }; + const newPeriod = { contentWarnings: [], + start: 5, + end: 15, + duration: 10, + adaptations: { video: [newVideoAdaptation1], + audio: [newAudioAdaptation], + }, + getAdaptations() { + return [newVideoAdaptation1, newAudioAdaptation]; + }, + }; + + const mockLog = jest.spyOn(log, "warn"); + const res = updatePeriodInPlace(oldPeriod as unknown as Period, + newPeriod as unknown as Period, + MANIFEST_UPDATE_TYPE.Partial); + expect(res).toEqual({ addedAdaptations: [], + removedAdaptations: [], + updatedAdaptations: [ + { + adaptation: oldVideoAdaptation1, + addedRepresentations: [], + removedRepresentations: [oldVideoRepresentation2], + updatedRepresentations: [ + oldVideoRepresentation1, + ], + }, + { + adaptation: oldAudioAdaptation, + addedRepresentations: [], + removedRepresentations: [], + updatedRepresentations: [ + oldAudioRepresentation, + ], + }, + ] }); expect(mockLog).toHaveBeenCalledTimes(1); expect(mockLog).toHaveBeenCalledWith( "Manifest: Representation \"rep-video-2\" not found when merging." ); - expect(oldVideoAdaptation1.representations).toHaveLength(2); + expect(oldVideoAdaptation1.representations).toHaveLength(1); mockLog.mockRestore(); }); }); diff --git a/src/manifest/__tests__/update_periods.test.ts b/src/manifest/__tests__/update_periods.test.ts index 10869937d4..c4f0b018c3 100644 --- a/src/manifest/__tests__/update_periods.test.ts +++ b/src/manifest/__tests__/update_periods.test.ts @@ -27,6 +27,12 @@ const MANIFEST_UPDATE_TYPE = { Partial: 1, }; +const fakeUpdatePeriodInPlaceRes = { + updatedAdaptations: [], + removedAdaptations: [], + addedAdaptations: [], +}; + describe("Manifest - replacePeriods", () => { beforeEach(() => { jest.resetModules(); @@ -37,7 +43,9 @@ describe("Manifest - replacePeriods", () => { // old periods : p1, p2 // new periods : p2 it("should remove old period", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace, @@ -50,7 +58,14 @@ describe("Manifest - replacePeriods", () => { { id: "p2" }, ] as any; const replacePeriods = jest.requireActual("../update_periods").replacePeriods; - replacePeriods(oldPeriods, newPeriods); + const res = replacePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [], + removedPeriods: [{ id: "p1" }], + updatedPeriods: [ + { period: { id: "p2" }, result: fakeUpdatePeriodInPlaceRes }, + ], + }); expect(oldPeriods.length).toBe(1); expect(oldPeriods[0].id).toBe("p2"); expect(fakeUpdatePeriodInPlace).toHaveBeenCalledTimes(1); @@ -65,7 +80,9 @@ describe("Manifest - replacePeriods", () => { // old periods : p1 // new periods : p1, p2 it("should add new period", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace, @@ -78,7 +95,14 @@ describe("Manifest - replacePeriods", () => { { id: "p3" }, ] as any; const replacePeriods = jest.requireActual("../update_periods").replacePeriods; - replacePeriods(oldPeriods, newPeriods); + const res = replacePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [{ id: "p3" }], + removedPeriods: [], + updatedPeriods: [ + { period: { id: "p2" }, result: fakeUpdatePeriodInPlaceRes }, + ], + }); expect(oldPeriods.length).toBe(2); expect(oldPeriods[0].id).toBe("p2"); expect(oldPeriods[1].id).toBe("p3"); @@ -94,7 +118,9 @@ describe("Manifest - replacePeriods", () => { // old periods: p1 // new periods: p2 it("should replace period", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace, @@ -106,7 +132,12 @@ describe("Manifest - replacePeriods", () => { { id: "p2" }, ] as any; const replacePeriods = jest.requireActual("../update_periods").replacePeriods; - replacePeriods(oldPeriods, newPeriods); + const res = replacePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [{ id: "p2" }], + removedPeriods: [{ id: "p1" }], + updatedPeriods: [], + }); expect(oldPeriods.length).toBe(1); expect(oldPeriods[0].id).toBe("p2"); expect(fakeUpdatePeriodInPlace).toHaveBeenCalledTimes(0); @@ -117,7 +148,9 @@ describe("Manifest - replacePeriods", () => { // old periods: p0, p1, p2 // new periods: p1, a, b, p2, p3 it("should handle more complex period replacement", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace, @@ -135,7 +168,19 @@ describe("Manifest - replacePeriods", () => { { id: "p3" }, ] as any; const replacePeriods = jest.requireActual("../update_periods").replacePeriods; - replacePeriods(oldPeriods, newPeriods); + const res = replacePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [ + { id: "a" }, + { id: "b" }, + { id: "p3" }, + ], + removedPeriods: [{ id: "p0" }], + updatedPeriods: [ + { period: { id: "p1" }, result: fakeUpdatePeriodInPlaceRes }, + { period: { id: "p2", start: 0 }, result: fakeUpdatePeriodInPlaceRes }, + ], + }); expect(oldPeriods.length).toBe(5); expect(oldPeriods[0].id).toBe("p1"); @@ -159,7 +204,9 @@ describe("Manifest - replacePeriods", () => { // old periods : p2 // new periods : p1, p2 it("should add new period before", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace, @@ -172,7 +219,16 @@ describe("Manifest - replacePeriods", () => { { id: "p2" }, ] as any; const replacePeriods = jest.requireActual("../update_periods").replacePeriods; - replacePeriods(oldPeriods, newPeriods); + const res = replacePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [ + { id: "p1" }, + ], + removedPeriods: [], + updatedPeriods: [ + { period: { id: "p2" }, result: fakeUpdatePeriodInPlaceRes }, + ], + }); expect(oldPeriods.length).toBe(2); expect(oldPeriods[0].id).toBe("p1"); expect(oldPeriods[1].id).toBe("p2"); @@ -188,7 +244,9 @@ describe("Manifest - replacePeriods", () => { // old periods : p1, p2 // new periods : No periods it("should remove all periods", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace, @@ -199,7 +257,15 @@ describe("Manifest - replacePeriods", () => { ] as any; const newPeriods = [] as any; const replacePeriods = jest.requireActual("../update_periods").replacePeriods; - replacePeriods(oldPeriods, newPeriods); + const res = replacePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [], + removedPeriods: [ + { id: "p1" }, + { id: "p2" }, + ], + updatedPeriods: [], + }); expect(oldPeriods.length).toBe(0); expect(fakeUpdatePeriodInPlace).toHaveBeenCalledTimes(0); }); @@ -209,7 +275,9 @@ describe("Manifest - replacePeriods", () => { // old periods : No periods // new periods : p1, p2 it("should add all periods to empty array", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace, @@ -220,7 +288,15 @@ describe("Manifest - replacePeriods", () => { { id: "p2" }, ] as any; const replacePeriods = jest.requireActual("../update_periods").replacePeriods; - replacePeriods(oldPeriods, newPeriods); + const res = replacePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [ + { id: "p1" }, + { id: "p2" }, + ], + removedPeriods: [], + updatedPeriods: [], + }); expect(oldPeriods.length).toBe(2); expect(oldPeriods[0].id).toBe("p1"); expect(oldPeriods[1].id).toBe("p2"); @@ -228,7 +304,7 @@ describe("Manifest - replacePeriods", () => { }); }); -describe("updatePeriods", () => { +describe("Manifest - updatePeriods", () => { beforeEach(() => { jest.resetModules(); }); @@ -238,7 +314,9 @@ describe("updatePeriods", () => { // old periods : p1, p2 // new periods : p2 it("should not remove old period", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace, @@ -248,7 +326,14 @@ describe("updatePeriods", () => { const newPeriods = [ { id: "p2", start: 60 } ] as any; const updatePeriods = jest.requireActual("../update_periods").updatePeriods; - updatePeriods(oldPeriods, newPeriods); + const res = updatePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [], + removedPeriods: [], + updatedPeriods: [ + { period: { id: "p2", start: 60 }, result: fakeUpdatePeriodInPlaceRes }, + ], + }); expect(oldPeriods.length).toBe(2); expect(oldPeriods[0].id).toBe("p1"); expect(fakeUpdatePeriodInPlace).toHaveBeenCalledTimes(1); @@ -263,7 +348,9 @@ describe("updatePeriods", () => { // old periods : p1 // new periods : p1, p2 it("should add new period", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace, @@ -272,7 +359,14 @@ describe("updatePeriods", () => { const newPeriods = [ { id: "p2", start: 60, end: 80 }, { id: "p3", start: 80 } ] as any; const updatePeriods = jest.requireActual("../update_periods").updatePeriods; - updatePeriods(oldPeriods, newPeriods); + const res = updatePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [{ id: "p3", start: 80 }], + removedPeriods: [], + updatedPeriods: [ + { period: { id: "p2", start: 60 }, result: fakeUpdatePeriodInPlaceRes }, + ], + }); expect(oldPeriods.length).toBe(2); expect(oldPeriods[0].id).toBe("p2"); expect(oldPeriods[1].id).toBe("p3"); @@ -289,7 +383,9 @@ describe("updatePeriods", () => { // old periods: p1 // new periods: p3 it("should throw when encountering two distant Periods", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace, @@ -326,11 +422,14 @@ describe("updatePeriods", () => { // old periods: p0, p1, p2 // new periods: p1, a, b, p2, p3 it("should handle more complex period replacement", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace })); const oldPeriods = [ { id: "p0", start: 50, end: 60 }, - { id: "p1", start: 60, end: 70 }, + { id: "p1", start: 60, end: 69 }, + { id: "p1.5", start: 69, end: 70 }, { id: "p2", start: 70 } ] as any; const newPeriods = [ { id: "p1", start: 60, end: 65 }, { id: "a", start: 65, end: 68 }, @@ -338,7 +437,27 @@ describe("updatePeriods", () => { { id: "p2", start: 70, end: 80 }, { id: "p3", start: 80 } ] as any; const updatePeriods = jest.requireActual("../update_periods").updatePeriods; - updatePeriods(oldPeriods, newPeriods); + const res = updatePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [ + { id: "a", start: 65, end: 68 }, + { id: "b", start: 68, end: 70 }, + { id: "p3", start: 80 }, + ], + removedPeriods: [ + { id: "p1.5", start: 69, end: 70 }, + ], + updatedPeriods: [ + { + period: { id: "p1", start: 60, end: 69 }, + result: fakeUpdatePeriodInPlaceRes, + }, + { + period: { id: "p2", start: 70 }, + result: fakeUpdatePeriodInPlaceRes, + }, + ], + }); expect(oldPeriods.length).toBe(6); expect(oldPeriods[0].id).toBe("p0"); @@ -347,12 +466,17 @@ describe("updatePeriods", () => { expect(oldPeriods[3].id).toBe("b"); expect(oldPeriods[4].id).toBe("p2"); expect(oldPeriods[5].id).toBe("p3"); - expect(fakeUpdatePeriodInPlace).toHaveBeenCalledTimes(1); + expect(fakeUpdatePeriodInPlace).toHaveBeenCalledTimes(2); expect(fakeUpdatePeriodInPlace) .toHaveBeenNthCalledWith(1, - { id: "p1", start: 60, end: 70 }, + { id: "p1", start: 60, end: 69 }, { id: "p1", start: 60, end: 65 }, MANIFEST_UPDATE_TYPE.Partial); + expect(fakeUpdatePeriodInPlace) + .toHaveBeenNthCalledWith(2, + { id: "p2", start: 70 }, + { id: "p2", start: 70, end: 80 }, + MANIFEST_UPDATE_TYPE.Full); }); // Case 5 : @@ -360,7 +484,9 @@ describe("updatePeriods", () => { // old periods : p2 // new periods : p1, p2 it("should throw when the first period is not encountered", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace })); const oldPeriods = [ { id: "p2", start: 70 } ] as any; @@ -397,13 +523,20 @@ describe("updatePeriods", () => { // old periods : p1, p2 // new periods : No periods it("should keep old periods if no new Period is available", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace })); const oldPeriods = [ { id: "p1" }, { id: "p2" } ] as any; const newPeriods = [] as any; const updatePeriods = jest.requireActual("../update_periods").updatePeriods; - updatePeriods(oldPeriods, newPeriods); + const res = updatePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [], + removedPeriods: [], + updatedPeriods: [], + }); expect(oldPeriods.length).toBe(2); expect(oldPeriods[0].id).toBe("p1"); expect(oldPeriods[1].id).toBe("p2"); @@ -415,13 +548,23 @@ describe("updatePeriods", () => { // old periods : No periods // new periods : p1, p2 it("should set only new Periods if none were available before", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace })); const oldPeriods = [] as any; const newPeriods = [ { id: "p1" }, { id: "p2" } ] as any; const updatePeriods = jest.requireActual("../update_periods").updatePeriods; - updatePeriods(oldPeriods, newPeriods); + const res = updatePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [ + { id: "p1" }, + { id: "p2" }, + ], + removedPeriods: [], + updatedPeriods: [], + }); expect(oldPeriods.length).toBe(2); expect(oldPeriods[0].id).toBe("p1"); expect(oldPeriods[1].id).toBe("p2"); @@ -433,7 +576,9 @@ describe("updatePeriods", () => { // old periods : p0, p1 // new periods : p4, p5 it("should throw if the new periods come strictly after", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace })); const updatePeriods = jest.requireActual("../update_periods").updatePeriods; @@ -470,7 +615,9 @@ describe("updatePeriods", () => { // old periods: p1 // new periods: p2 it("should concatenate consecutive periods", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace, @@ -479,7 +626,14 @@ describe("updatePeriods", () => { const newPeriods = [ { id: "p2", start: 60, end: 80 } ] as any; const updatePeriods = jest.requireActual("../update_periods").updatePeriods; - updatePeriods(oldPeriods, newPeriods); + const res = updatePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [ + { id: "p2", start: 60, end: 80 }, + ], + removedPeriods: [], + updatedPeriods: [], + }); expect(oldPeriods.length).toBe(2); expect(oldPeriods[0].id).toBe("p1"); expect(oldPeriods[1].id).toBe("p2"); @@ -493,7 +647,9 @@ describe("updatePeriods", () => { /* eslint-disable max-len */ it("should throw when encountering two completely different Periods with the same start", () => { /* eslint-enable max-len */ - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace, @@ -530,7 +686,9 @@ describe("updatePeriods", () => { // old periods: p0, p1, p2 // new periods: p1, p2, p3 it("should handle more complex period replacement", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace })); const oldPeriods = [ { id: "p0", start: 50, end: 60 }, @@ -540,7 +698,24 @@ describe("updatePeriods", () => { { id: "p2", start: 65, end: 80 }, { id: "p3", start: 80 } ] as any; const updatePeriods = jest.requireActual("../update_periods").updatePeriods; - updatePeriods(oldPeriods, newPeriods); + const res = updatePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [ + { id: "p3", start: 80 }, + ], + removedPeriods: [ + ], + updatedPeriods: [ + { + period: { id: "p1", start: 60, end: 70 }, + result: fakeUpdatePeriodInPlaceRes, + }, + { + period: { id: "p2", start: 70 }, + result: fakeUpdatePeriodInPlaceRes, + }, + ], + }); expect(oldPeriods.length).toBe(4); expect(oldPeriods[0].id).toBe("p0"); @@ -565,7 +740,9 @@ describe("updatePeriods", () => { // old periods: p0, p1, p2, p3 // new periods: p1, p3 it("should handle more complex period replacement", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace })); const oldPeriods = [ { id: "p0", start: 50, end: 60 }, @@ -575,7 +752,24 @@ describe("updatePeriods", () => { const newPeriods = [ { id: "p1", start: 60, end: 70 }, { id: "p3", start: 80 } ] as any; const updatePeriods = jest.requireActual("../update_periods").updatePeriods; - updatePeriods(oldPeriods, newPeriods); + const res = updatePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [ + ], + removedPeriods: [ + { id: "p2", start: 70, end: 80 }, + ], + updatedPeriods: [ + { + period: { id: "p1", start: 60, end: 70 }, + result: fakeUpdatePeriodInPlaceRes, + }, + { + period: { id: "p3", start: 80 }, + result: fakeUpdatePeriodInPlaceRes, + }, + ], + }); expect(oldPeriods.length).toBe(3); expect(oldPeriods[0].id).toBe("p0"); @@ -599,7 +793,9 @@ describe("updatePeriods", () => { // old periods: p0, p1, p2, p3, p4 // new periods: p1, p3 it("should remove periods not included in the new Periods", () => { - const fakeUpdatePeriodInPlace = jest.fn(() => { return; }); + const fakeUpdatePeriodInPlace = jest.fn(() => { + return fakeUpdatePeriodInPlaceRes; + }); jest.mock("../update_period_in_place", () => ({ __esModule: true as const, default: fakeUpdatePeriodInPlace })); const oldPeriods = [ { id: "p0", start: 50, end: 60 }, @@ -610,7 +806,25 @@ describe("updatePeriods", () => { const newPeriods = [ { id: "p1", start: 60, end: 70 }, { id: "p3", start: 80, end: 90 } ] as any; const updatePeriods = jest.requireActual("../update_periods").updatePeriods; - updatePeriods(oldPeriods, newPeriods); + const res = updatePeriods(oldPeriods, newPeriods); + expect(res).toEqual({ + addedPeriods: [ + ], + removedPeriods: [ + { id: "p2", start: 70, end: 80 }, + { id: "p4", start: 90 }, + ], + updatedPeriods: [ + { + period: { id: "p1", start: 60, end: 70 }, + result: fakeUpdatePeriodInPlaceRes, + }, + { + period: { id: "p3", start: 80, end: 90 }, + result: fakeUpdatePeriodInPlaceRes, + }, + ], + }); expect(oldPeriods.length).toBe(3); expect(oldPeriods[0].id).toBe("p0"); diff --git a/src/manifest/manifest.ts b/src/manifest/manifest.ts index 0894bd2f71..6df93f3fd3 100644 --- a/src/manifest/manifest.ts +++ b/src/manifest/manifest.ts @@ -36,6 +36,7 @@ import { MANIFEST_UPDATE_TYPE, } from "./types"; import { + IPeriodsUpdateResult, replacePeriods, updatePeriods, } from "./update_periods"; @@ -104,7 +105,7 @@ export interface IDecipherabilityUpdateElement { manifest : Manifest; /** Events emitted by a `Manifest` instance */ export interface IManifestEvents { /** The Manifest has been updated */ - manifestUpdate : null; + manifestUpdate : IPeriodsUpdateResult; /** Some Representation's decipherability status has been updated */ decipherabilityUpdate : IDecipherabilityUpdateElement[]; } @@ -726,14 +727,15 @@ export default class Manifest extends EventEmitter { this.transport = newManifest.transport; this.publishTime = newManifest.publishTime; + let updatedPeriodsResult; if (updateType === MANIFEST_UPDATE_TYPE.Full) { this._timeBounds = newManifest._timeBounds; this.uris = newManifest.uris; - replacePeriods(this.periods, newManifest.periods); + updatedPeriodsResult = replacePeriods(this.periods, newManifest.periods); } else { this._timeBounds.maximumTimeData = newManifest._timeBounds.maximumTimeData; this.updateUrl = newManifest.uris[0]; - updatePeriods(this.periods, newManifest.periods); + updatedPeriodsResult = updatePeriods(this.periods, newManifest.periods); // Partial updates do not remove old Periods. // This can become a memory problem when playing a content long enough. @@ -758,7 +760,7 @@ export default class Manifest extends EventEmitter { // Let's trigger events at the end, as those can trigger side-effects. // We do not want the current Manifest object to be incomplete when those // happen. - this.trigger("manifestUpdate", null); + this.trigger("manifestUpdate", updatedPeriodsResult); } } diff --git a/src/manifest/update_period_in_place.ts b/src/manifest/update_period_in_place.ts index 2cb8889f75..b6ff88b4cf 100644 --- a/src/manifest/update_period_in_place.ts +++ b/src/manifest/update_period_in_place.ts @@ -15,8 +15,10 @@ */ import log from "../log"; -import arrayFind from "../utils/array_find"; +import arrayFindIndex from "../utils/array_find_index"; +import Adaptation from "./adaptation"; import Period from "./period"; +import Representation from "./representation"; import { MANIFEST_UPDATE_TYPE } from "./types"; /** @@ -24,11 +26,19 @@ import { MANIFEST_UPDATE_TYPE } from "./types"; * the Manifest). * @param {Object} oldPeriod * @param {Object} newPeriod + * @param {number} updateType + * @returns {Object} */ -export default function updatePeriodInPlace(oldPeriod : Period, - newPeriod : Period, - updateType : MANIFEST_UPDATE_TYPE) : void -{ +export default function updatePeriodInPlace( + oldPeriod : Period, + newPeriod : Period, + updateType : MANIFEST_UPDATE_TYPE +) : IUpdatedPeriodResult { + const res : IUpdatedPeriodResult = { + updatedAdaptations: [], + removedAdaptations: [], + addedAdaptations: [], + }; oldPeriod.start = newPeriod.start; oldPeriod.end = newPeriod.end; oldPeriod.duration = newPeriod.duration; @@ -39,26 +49,43 @@ export default function updatePeriodInPlace(oldPeriod : Period, for (let j = 0; j < oldAdaptations.length; j++) { const oldAdaptation = oldAdaptations[j]; - const newAdaptation = arrayFind(newAdaptations, - a => a.id === oldAdaptation.id); - if (newAdaptation === undefined) { + const newAdaptationIdx = arrayFindIndex(newAdaptations, + a => a.id === oldAdaptation.id); + + if (newAdaptationIdx === -1) { log.warn("Manifest: Adaptation \"" + oldAdaptations[j].id + "\" not found when merging."); + const [removed] = oldAdaptations.splice(j, 1); + j--; + res.removedAdaptations.push(removed); } else { - const oldRepresentations = oldAdaptations[j].representations; - const newRepresentations = newAdaptation.representations; + const [newAdaptation] = newAdaptations.splice(newAdaptationIdx, 1); + const updatedRepresentations : Representation[] = []; + const addedRepresentations : Representation[] = []; + const removedRepresentations : Representation[] = []; + res.updatedAdaptations.push({ adaptation: oldAdaptation, + updatedRepresentations, + addedRepresentations, + removedRepresentations }); + + const oldRepresentations = oldAdaptation.representations; + const newRepresentations = newAdaptation.representations.slice(); for (let k = 0; k < oldRepresentations.length; k++) { const oldRepresentation = oldRepresentations[k]; - const newRepresentation = - arrayFind(newRepresentations, - representation => representation.id === oldRepresentation.id); + const newRepresentationIdx = arrayFindIndex(newRepresentations, representation => + representation.id === oldRepresentation.id); - if (newRepresentation === undefined) { + if (newRepresentationIdx === -1) { log.warn(`Manifest: Representation "${oldRepresentations[k].id}" ` + "not found when merging."); + const [removed] = oldRepresentations.splice(k, 1); + k--; + removedRepresentations.push(removed); } else { + const [newRepresentation] = newRepresentations.splice(newRepresentationIdx, 1); + updatedRepresentations.push(oldRepresentation); oldRepresentation.cdnMetadata = newRepresentation.cdnMetadata; if (updateType === MANIFEST_UPDATE_TYPE.Full) { oldRepresentation.index._replace(newRepresentation.index); @@ -67,6 +94,49 @@ export default function updatePeriodInPlace(oldPeriod : Period, } } } + + if (newRepresentations.length > 0) { + log.warn(`Manifest: ${newRepresentations.length} new Representations ` + + "found when merging."); + oldAdaptation.representations.push(...newRepresentations); + addedRepresentations.push(...newRepresentations); + } } } + if (newAdaptations.length > 0) { + log.warn(`Manifest: ${newAdaptations.length} new Adaptations ` + + "found when merging."); + for (const adap of newAdaptations) { + const prevAdaps = oldPeriod.adaptations[adap.type]; + if (prevAdaps === undefined) { + oldPeriod.adaptations[adap.type] = [adap]; + } else { + prevAdaps.push(adap); + } + res.addedAdaptations.push(adap); + } + } + return res; +} + +/** + * Object describing the updates performed by `updatePeriodInPlace` on a single + * Period. + */ +export interface IUpdatedPeriodResult { + /** Information on Adaptations that have been updated. */ + updatedAdaptations : Array<{ + /** The concerned Adaptation. */ + adaptation: Adaptation; + /** Representations that have been updated. */ + updatedRepresentations : Representation[]; + /** Representations that have been removed from the Adaptation. */ + removedRepresentations : Representation[]; + /** Representations that have been added to the Adaptation. */ + addedRepresentations : Representation[]; + }>; + /** Adaptation that have been removed from the Period. */ + removedAdaptations : Adaptation[]; + /** Adaptation that have been added to the Period. */ + addedAdaptations : Adaptation[]; } diff --git a/src/manifest/update_periods.ts b/src/manifest/update_periods.ts index 13f8427582..45734e7f03 100644 --- a/src/manifest/update_periods.ts +++ b/src/manifest/update_periods.ts @@ -19,18 +19,26 @@ import log from "../log"; import arrayFindIndex from "../utils/array_find_index"; import Period from "./period"; import { MANIFEST_UPDATE_TYPE } from "./types"; -import updatePeriodInPlace from "./update_period_in_place"; +import updatePeriodInPlace, { + IUpdatedPeriodResult, +} from "./update_period_in_place"; /** * Update old periods by adding new periods and removing * not available ones. * @param {Array.} oldPeriods * @param {Array.} newPeriods + * @returns {Object} */ export function replacePeriods( oldPeriods: Period[], newPeriods: Period[] -) : void { +) : IPeriodsUpdateResult { + const res : IPeriodsUpdateResult = { + updatedPeriods: [], + addedPeriods: [], + removedPeriods: [], + }; let firstUnhandledPeriodIdx = 0; for (let i = 0; i < newPeriods.length; i++) { const newPeriod = newPeriods[i]; @@ -41,29 +49,35 @@ export function replacePeriods( oldPeriod = oldPeriods[j]; } if (oldPeriod != null) { - updatePeriodInPlace(oldPeriod, newPeriod, MANIFEST_UPDATE_TYPE.Full); + const result = updatePeriodInPlace(oldPeriod, newPeriod, MANIFEST_UPDATE_TYPE.Full); + res.updatedPeriods.push({ period: oldPeriod, result }); const periodsToInclude = newPeriods.slice(firstUnhandledPeriodIdx, i); const nbrOfPeriodsToRemove = j - firstUnhandledPeriodIdx; - oldPeriods.splice(firstUnhandledPeriodIdx, - nbrOfPeriodsToRemove, - ...periodsToInclude); + const removed = oldPeriods.splice(firstUnhandledPeriodIdx, + nbrOfPeriodsToRemove, + ...periodsToInclude); + res.removedPeriods.push(...removed); + res.addedPeriods.push(...periodsToInclude); firstUnhandledPeriodIdx = i + 1; } } if (firstUnhandledPeriodIdx > oldPeriods.length) { log.error("Manifest: error when updating Periods"); - return; + return res; } if (firstUnhandledPeriodIdx < oldPeriods.length) { - oldPeriods.splice(firstUnhandledPeriodIdx, - oldPeriods.length - firstUnhandledPeriodIdx); + const removed = oldPeriods.splice(firstUnhandledPeriodIdx, + oldPeriods.length - firstUnhandledPeriodIdx); + res.removedPeriods.push(...removed); } const remainingNewPeriods = newPeriods.slice(firstUnhandledPeriodIdx, newPeriods.length); if (remainingNewPeriods.length > 0) { oldPeriods.push(...remainingNewPeriods); + res.addedPeriods.push(...remainingNewPeriods); } + return res; } /** @@ -71,17 +85,24 @@ export function replacePeriods( * not available ones. * @param {Array.} oldPeriods * @param {Array.} newPeriods + * @returns {Object} */ export function updatePeriods( oldPeriods: Period[], newPeriods: Period[] -) : void { +) : IPeriodsUpdateResult { + const res : IPeriodsUpdateResult = { + updatedPeriods: [], + addedPeriods: [], + removedPeriods: [], + }; if (oldPeriods.length === 0) { oldPeriods.splice(0, 0, ...newPeriods); - return; + res.addedPeriods.push(...newPeriods); + return res; } if (newPeriods.length === 0) { - return; + return res; } const oldLastPeriod = oldPeriods[oldPeriods.length - 1]; if (oldLastPeriod.start < newPeriods[0].start) { @@ -90,8 +111,11 @@ export function updatePeriods( "Cannot perform partial update: not enough data"); } oldPeriods.push(...newPeriods); - return; + res.addedPeriods.push(...newPeriods); + return res; } + + /** Index, in `oldPeriods` of the first element of `newPeriods` */ const indexOfNewFirstPeriod = arrayFindIndex(oldPeriods, ({ id }) => id === newPeriods[0].id); if (indexOfNewFirstPeriod < 0) { @@ -100,10 +124,14 @@ export function updatePeriods( } // The first updated Period can only be a partial part - updatePeriodInPlace(oldPeriods[indexOfNewFirstPeriod], - newPeriods[0], - MANIFEST_UPDATE_TYPE.Partial); + const updateRes = updatePeriodInPlace(oldPeriods[indexOfNewFirstPeriod], + newPeriods[0], + MANIFEST_UPDATE_TYPE.Partial); + res.updatedPeriods.push({ period: oldPeriods[indexOfNewFirstPeriod], + result: updateRes }); + // Search each consecutive elements of `newPeriods` - after the initial one already + // processed - in `oldPeriods`, removing and adding unfound Periods in the process let prevIndexOfNewPeriod = indexOfNewFirstPeriod + 1; for (let i = 1; i < newPeriods.length; i++) { const newPeriod = newPeriods[i]; @@ -114,28 +142,60 @@ export function updatePeriods( break; // end the loop } } - if (indexOfNewPeriod < 0) { - oldPeriods.splice(prevIndexOfNewPeriod, - oldPeriods.length - prevIndexOfNewPeriod, - ...newPeriods.slice(i, newPeriods.length)); - return; - } - if (indexOfNewPeriod > prevIndexOfNewPeriod) { - oldPeriods.splice(prevIndexOfNewPeriod, - indexOfNewPeriod - prevIndexOfNewPeriod); - indexOfNewPeriod = prevIndexOfNewPeriod; - } + if (indexOfNewPeriod < 0) { // Next element of `newPeriods` not found: insert it + let toRemoveUntil = -1; + for (let j = prevIndexOfNewPeriod; j < oldPeriods.length; j++) { + if (newPeriod.start < oldPeriods[j].start) { + toRemoveUntil = j; + break; // end the loop + } + } + const nbElementsToRemove = toRemoveUntil - prevIndexOfNewPeriod; + const removed = oldPeriods.splice(prevIndexOfNewPeriod, + nbElementsToRemove, + newPeriod); + res.addedPeriods.push(newPeriod); + res.removedPeriods.push(...removed); + } else { + if (indexOfNewPeriod > prevIndexOfNewPeriod) { + // Some old periods were not found: remove + log.warn("Manifest: old Periods not found in new when updating, removing"); + const removed = oldPeriods.splice(prevIndexOfNewPeriod, + indexOfNewPeriod - prevIndexOfNewPeriod); + res.removedPeriods.push(...removed); + indexOfNewPeriod = prevIndexOfNewPeriod; + } - // Later Periods can be fully replaced - updatePeriodInPlace(oldPeriods[indexOfNewPeriod], - newPeriod, - MANIFEST_UPDATE_TYPE.Full); + // Later Periods can be fully replaced + const result = updatePeriodInPlace(oldPeriods[indexOfNewPeriod], + newPeriod, + MANIFEST_UPDATE_TYPE.Full); + res.updatedPeriods.push({ period: oldPeriods[indexOfNewPeriod], result }); + } prevIndexOfNewPeriod++; } if (prevIndexOfNewPeriod < oldPeriods.length) { - oldPeriods.splice(prevIndexOfNewPeriod, - oldPeriods.length - prevIndexOfNewPeriod); + log.warn("Manifest: Ending Periods not found in new when updating, removing"); + const removed = oldPeriods.splice(prevIndexOfNewPeriod, + oldPeriods.length - prevIndexOfNewPeriod); + res.removedPeriods.push(...removed); } + return res; +} + +/** Object describing a Manifest update at the Periods level. */ +export interface IPeriodsUpdateResult { + /** Information on Periods that have been updated. */ + updatedPeriods : Array<{ + /** The concerned Period. */ + period : Period; + /** The updates performed. */ + result : IUpdatedPeriodResult; + }>; + /** Periods that have been added. */ + addedPeriods : Period[]; + /** Periods that have been removed. */ + removedPeriods : Period[]; } diff --git a/src/utils/event_emitter.ts b/src/utils/event_emitter.ts index 6fb603bcd6..ec4250f040 100644 --- a/src/utils/event_emitter.ts +++ b/src/utils/event_emitter.ts @@ -28,12 +28,14 @@ export interface IEventEmitter { } // Type of the argument in the listener's callback -type IArgs = TEventRecord[TEventName]; // Type of the listener function -export type IListener = (args: IArgs) => void; +export type IListener< + TEventRecord, + TEventName extends keyof TEventRecord +> = (args: IEventPayload) => void; type IListeners = { [P in keyof TEventRecord]? : Array> @@ -127,7 +129,7 @@ export default class EventEmitter implements IEventEmitter { */ protected trigger( evt : TEventName, - arg : IArgs + arg : IEventPayload ) : void { const listeners = this._listeners[evt]; if (!Array.isArray(listeners)) { diff --git a/tests/memory/index.js b/tests/memory/index.js index 0e16aa4b92..8a32f43db4 100644 --- a/tests/memory/index.js +++ b/tests/memory/index.js @@ -217,6 +217,7 @@ describe("Memory tests", () => { player.seekTo(0); } else { player.seekTo(20); + seekToBeginning = true; } const bitrateIdx = iterationIdx % videoBitrates.length; player.setVideoBitrate(videoBitrates[bitrateIdx]);