From a03a976eba88c7ec2a59b74482a6741be8eeab0e Mon Sep 17 00:00:00 2001 From: Johan Lautakoksi Date: Thu, 29 Jun 2023 00:49:19 +0200 Subject: [PATCH 01/17] feat: implematation of demux support for live mix --- engine/server.ts | 37 +- engine/session.js | 96 ++- engine/session_live.js | 1478 +++++++++++++++++++++++++++++++++---- engine/stream_switcher.js | 78 +- examples/demux.ts | 8 +- examples/livemix.ts | 54 +- package-lock.json | 17 +- package.json | 2 +- 8 files changed, 1544 insertions(+), 226 deletions(-) diff --git a/engine/server.ts b/engine/server.ts index f4c0c5a3..d165642a 100644 --- a/engine/server.ts +++ b/engine/server.ts @@ -433,7 +433,7 @@ export class ChannelEngine { try { await Promise.all(channels.map(channel => getSwitchStatusAndPerformSwitch(channel))); } catch (err) { - debug('Problem occured when updating streamSwitchers'); + debug('Problem occurred when updating streamSwitchers'); throw new Error (err); } @@ -474,9 +474,10 @@ export class ChannelEngine { useDemuxedAudio: options.useDemuxedAudio, dummySubtitleEndpoint: this.dummySubtitleEndpoint, subtitleSliceEndpoint: this.subtitleSliceEndpoint, - useVTTSubtitles: this.useVTTSubtitles, + useVTTSubtitles: false, cloudWatchMetrics: this.logCloudWatchMetrics, profile: channel.profile, + audioTracks: channel.audioTracks }, this.sessionLiveStore); sessionSwitchers[channel.id] = new StreamSwitcher({ @@ -751,15 +752,31 @@ export class ChannelEngine { } async _handleAudioManifest(req, res, next) { - debug(`req.url=${req.url}`); + debug(`x-playback-session-id=${req.headers["x-playback-session-id"]} req.url=${req.url}`); + debug(req.params); const session = sessions[req.params[2]]; - if (session) { + const sessionLive = sessionsLive[req.params[2]]; + if (session && sessionLive) { try { - const body = await session.getCurrentAudioManifestAsync( - req.params[0], - req.params[1], - req.headers["x-playback-session-id"] - ); + let body = null; + if (!this.streamSwitchManager) { + debug(`[${req.params[1]}]: Responding with VOD2Live manifest`); + body = await session.getCurrentAudioManifestAsync(req.params[0], req.params[1], req.headers["x-playback-session-id"]); + } else { + while (switcherStatus[req.params[2]] === null || switcherStatus[req.params[2]] === undefined) { + debug(`[${req.params[1]}]: (${switcherStatus[req.params[1]]}) Waiting for streamSwitcher to respond`); + await timer(500); + } + debug(`switcherStatus[${req.params[1]}]=[${switcherStatus[req.params[2]]}]`); + if (switcherStatus[req.params[2]]) { + debug(`[${req.params[2]}]: Responding with Live-stream manifest`); + body = await sessionLive.getCurrentAudioManifestAsync(req.params[0], req.params[1]); + } else { + debug(`[${req.params[2]}]: Responding with VOD2Live manifest`); + body = await session.getCurrentAudioManifestAsync(req.params[0], req.params[1], req.headers["x-playback-session-id"]); + } + } + res.sendRaw(200, Buffer.from(body, 'utf8'), { "Content-Type": "application/vnd.apple.mpegurl", "Access-Control-Allow-Origin": "*", @@ -771,7 +788,7 @@ export class ChannelEngine { next(this._gracefulErrorHandler(err)); } } else { - const err = new errs.NotFoundError('Invalid session'); + const err = new errs.NotFoundError('Invalid session(s)'); next(err); } } diff --git a/engine/session.js b/engine/session.js index f31b195e..540a9093 100644 --- a/engine/session.js +++ b/engine/session.js @@ -68,6 +68,10 @@ class Session { discSeq: null, mediaSeqOffset: null, transitionSegments: null, + audioSeq: null, + discAudioSeq: null, + audioSeqOffset: null, + transitionAudioSegments: null, reloadBehind: null, } this.isAllowedToClearVodCache = null; @@ -351,6 +355,17 @@ class Session { return null; } } + async getTruncatedVodAudioSegments(vodUri, duration) { + try { + const hlsVod = await this._truncateSlate(null, duration, vodUri); + let vodSegments = hlsVod.getAudioSegments(); + Object.keys(vodSegments).forEach((bw) => vodSegments[bw].unshift({ discontinuity: true, cue: { in: true } })); + return vodSegments; + } catch (exc) { + debug(`[${this._sessionId}]: Failed to generate truncated VOD!`); + return null; + } + } async setCurrentMediaSequenceSegments(segments, mSeqOffset, reloadBehind) { if (!this._sessionState) { @@ -470,7 +485,63 @@ class Session { } } - async setCurrentMediaAndDiscSequenceCount(_mediaSeq, _discSeq) { + async getCurrentAudioSequenceSegments(opts) { + if (!this._sessionState) { + throw new Error('Session not ready'); + } + const isLeader = await this._sessionStateStore.isLeader(this._instanceId); + if (isLeader) { + await this._sessionState.set("vodReloaded", 0); + } + + // Only read data from store if state is VOD_PLAYING + let state = await this.getSessionState(); + let tries = 12; + while (state !== SessionState.VOD_PLAYING && tries > 0) { + const waitTimeMs = 500; + debug(`[${this._sessionId}]: state=${state} - Waiting ${waitTimeMs}ms_${tries} until Leader has finished loading next vod.`); + await timer(waitTimeMs); + tries--; + state = await this.getSessionState(); + } + + const playheadState = { + vodMediaSeqAudio: null + } + if (opts && opts.targetMseq !== undefined) { + playheadState.vodMediaSeqAudio = opts.targetMseq; + } else { + playheadState.vodMediaSeqAudio = await this._playheadState.get("vodMediaSeqAudio"); + } + + // NOTE: Assume that VOD cache was already cleared in 'getCurrentMediaAndDiscSequenceCount()' + // and that we now have access to the correct vod cache + const currentVod = await this._sessionState.getCurrentVod(); + if (currentVod) { + try { + const audioSegments = currentVod.getLiveAudioSequenceSegments(playheadState.vodMediaSeqAudio); + + let audioSequenceValue = 0; + if (currentVod.sequenceAlwaysContainNewSegments) { + audioSequenceValue = currentVod.mediaSequenceValuesAudio[playheadState.vodMediaSeqAudio]; + debug(`[${this._sessionId}]: {${audioSequenceValue}}_{${currentVod.getLastSequenceMediaSequenceValueAudio()}}`); + } else { + audioSequenceValue = playheadState.vodMediaSeqAudio; + } + + debug(`[${this._sessionId}]: Requesting all audio segments from Media Sequence: ${playheadState.vodMediaSeqAudio}(${audioSequenceValue})_${currentVod.getLiveMediaSequencesCount("audio")}`); + return audioSegments; + } catch (err) { + logerror(this._sessionId, err); + await this._sessionState.clearCurrentVodCache(); // force reading up from shared store + throw new Error("Failed to get all current audio segments: " + JSON.stringify(playheadState)); + } + } else { + throw new Error("Engine not ready"); + } + } + + async setCurrentMediaAndDiscSequenceCount(_mediaSeq, _discSeq, _audioSeq, _discAudioSeq) { if (!this._sessionState) { throw new Error("Session not ready"); } @@ -479,6 +550,8 @@ class Session { this.switchDataForSession.mediaSeq = _mediaSeq; this.switchDataForSession.discSeq = _discSeq; + this.switchDataForSession.audioSeq = _audioSeq; + this.switchDataForSession.discAudioSeq = _discAudioSeq; } async getCurrentMediaAndDiscSequenceCount() { @@ -497,9 +570,10 @@ class Session { state = await this.getSessionState(); } - const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo"]); + const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo", "mediaSeqAudio","vodMediaSeqAudio"]); const discSeqOffset = await this._sessionState.get("discSeq"); - // TODO: support Audio too ^ + const discAudioSeqOffset = await this._sessionState.get("discSeqAudio"); + // Clear Vod Cache here when Switching to Live just to be safe... if (playheadState.vodMediaSeqVideo === 0) { @@ -520,12 +594,26 @@ class Session { mediaSequenceValue = playheadState.vodMediaSeqVideo; } const discSeqCount = discSeqOffset + currentVod.discontinuities[playheadState.vodMediaSeqVideo]; - + let discSeqCountAudio; + let audioSequenceValue; + if (this.use_demuxed_audio) { + discSeqCountAudio = discAudioSeqOffset + currentVod.discontinuitiesAudio[playheadState.vodMediaSeqAudio]; + if (currentVod.sequenceAlwaysContainNewSegments) { + audioSequenceValue = currentVod.mediaSequenceValuesAudio[playheadState.vodMediaSeqAudio]; + debug(`[${this._sessionId}]: seqIndex=${playheadState.vodMediaSeqAudio}_seqValue=${audioSequenceValue}`) + } else { + audioSequenceValue = playheadState.vodMediaSeqAudio; + } + } debug(`[${this._sessionId}]: MediaSeq: (${playheadState.mediaSeq}+${mediaSequenceValue}=${(playheadState.mediaSeq + mediaSequenceValue)}) and DiscSeq: (${discSeqCount}) requested `); + debug(`[${this._sessionId}]: AudioSeq: (${playheadState.mediaSeqAudio}+${audioSequenceValue}=${(playheadState.mediaSeqAudio + audioSequenceValue)}) and DiscSeq: (${discSeqCountAudio}) requested `); return { 'mediaSeq': (playheadState.mediaSeq + mediaSequenceValue), 'discSeq': discSeqCount, 'vodMediaSeqVideo': playheadState.vodMediaSeqVideo, + 'vodMediaSeqAudio': playheadState.vodMediaSeqAudio, + 'audioSeq': playheadState.mediaSeqAudio + audioSequenceValue, + 'discSeqAudio': discSeqCountAudio, }; } catch (err) { logerror(this._sessionId, err); diff --git a/engine/session_live.js b/engine/session_live.js index dd783a73..8b931197 100644 --- a/engine/session_live.js +++ b/engine/session_live.js @@ -26,6 +26,11 @@ const PlayheadState = Object.freeze({ IDLE: 4, }); +/** + * When we implement subtitle support in live-mix we should place it in its own file/or share it with audio + * we should also remove audio implementation when we implement subtitles from this file so we don't get at 4000 line long file. + */ + class SessionLive { constructor(config, sessionLiveStore) { this.sessionId = crypto.randomBytes(20).toString("hex"); @@ -35,15 +40,25 @@ class SessionLive { this.prevMediaSeqCount = 0; this.discSeqCount = 0; this.prevDiscSeqCount = 0; + this.audioSeqCount = 0; + this.prevAudioSeqCount = 0; + this.audioDiscSeqCount = 0; + this.prevAudioDiscSeqCount = 0; this.targetDuration = 0; this.masterManifestUri = null; this.vodSegments = {}; + this.vodAudioSegments = {}; this.mediaManifestURIs = {}; + this.audioManifestURIs = {}; this.liveSegQueue = {}; this.lastRequestedMediaSeqRaw = null; this.liveSourceM3Us = {}; + this.liveAudioSegQueue = {}; + this.lastRequestedAudioSeqRaw = null; + this.liveAudioSourceM3Us = {}; this.playheadState = PlayheadState.IDLE; this.liveSegsForFollowers = {}; + this.audioLiveSegsForFollowers = {}; this.timerCompensation = null; this.firstTime = true; this.allowedToSet = false; @@ -65,6 +80,9 @@ class SessionLive { if (config.profile) { this.sessionLiveProfile = config.profile; } + if (config.audioTracks) { + this.sessionAudioTracks = config.audioTracks; + } } } @@ -89,11 +107,17 @@ class SessionLive { await timer(resetDelay); await this.sessionLiveState.set("liveSegsForFollowers", null); await this.sessionLiveState.set("lastRequestedMediaSeqRaw", null); + await this.sessionLiveState.set("liveAudioSegsForFollowers", null); + await this.sessionLiveState.set("lastRequestedAudioSeqRaw", null); await this.sessionLiveState.set("transitSegs", null); + await this.sessionLiveState.set("transitSegsAudio", null); await this.sessionLiveState.set("firstCounts", { liveSourceMseqCount: null, + liveSourceAudioMseqCount: null, mediaSeqCount: null, + audioSeqCount: null, discSeqCount: null, + audioDiscSeqCount: null }); debug(`[${this.instanceId}][${this.sessionId}]: LEADER: SessionLive values in Store have now been reset!`); } @@ -112,14 +136,23 @@ class SessionLive { this.mediaSeqCount = 0; this.prevMediaSeqCount = 0; this.discSeqCount = 0; + this.audioSeqCount = 0; + this.prevAudioSeqCount = 0; + this.audioDiscSeqCount = 0; this.targetDuration = 0; this.masterManifestUri = null; this.vodSegments = {}; + this.vodAudioSegments = {}; this.mediaManifestURIs = {}; + this.audioManifestURIs = {}; this.liveSegQueue = {}; + this.liveAudioSegQueue = {}; this.lastRequestedMediaSeqRaw = null; + this.lastRequestedAudioSeqRaw = null; this.liveSourceM3Us = {}; + this.liveAudioSourceM3Us = {}; this.liveSegsForFollowers = {}; + this.audioLiveSegsForFollowers = {}; this.timerCompensation = null; this.firstTime = true; this.pushAmount = 0; @@ -155,6 +188,7 @@ class SessionLive { this.waitForPlayhead = true; const tsIncrementBegin = Date.now(); await this._loadAllMediaManifests(); + await this._loadAllAudioManifests(); const tsIncrementEnd = Date.now(); this.waitForPlayhead = false; @@ -220,6 +254,10 @@ class SessionLive { this._filterLiveProfiles(); debug(`[${this.sessionId}]: Filtered Live profiles! (${Object.keys(this.mediaManifestURIs).length}) profiles left!`); } + if (this.sessionAudioTracks) { + this._filterLiveAudioTracks(); + debug(`[${this.sessionId}]: Filtered Live audio tracks! (${Object.keys([Object.keys(this.audioManifestURIs)[0]]).length}) profiles left!`); + } } catch (err) { this.masterManifestUri = null; debug(`[${this.instanceId}][${this.sessionId}]: Failed to fetch Live Master Manifest! ${err}`); @@ -293,15 +331,84 @@ class SessionLive { debug(`[${this.sessionId}]: LEADER: I am adding 'transitSegs' to Store for future followers`); } } + async setCurrentAudioSequenceSegments(segments) { + if (segments === null) { + debug(`[${this.sessionId}]: No segments provided.`); + return false; + } + // Make it possible to add & share new segments + this.allowedToSet = true; + if (this._isEmpty(this.vodAudioSegments)) { + const groupIds = Object.keys(segments); + for (let i = 0; i < groupIds.length; i++) { + const groupId = groupIds[i]; + const langs = Object.keys(segments[groupId]); + for (let j = 0; j < langs.length; j++) { + const lang = langs[j]; + if (!this.vodAudioSegments[groupId]) { + this.vodAudioSegments[groupId] = {}; + } + if (!this.vodAudioSegments[groupId][lang]) { + this.vodAudioSegments[groupId][lang] = []; + } + + if (segments[groupId][lang][0].discontinuity) { + segments[groupId][lang].shift(); + } + let cueInExists = null; + for (let segIdx = 0; segIdx < segments[groupId][lang].length; segIdx++) { + const v2lSegment = segments[groupId][lang][segIdx]; + if (v2lSegment.cue) { + if (v2lSegment.cue["in"]) { + cueInExists = true; + } else { + cueInExists = false; + } + } + this.vodAudioSegments[groupId][lang].push(v2lSegment); + } + + const endIdx = segments[groupId][langs].length - 1; + if (!segments[groupId][lang][endIdx].discontinuity) { + const finalSegItem = { discontinuity: true }; + if (!cueInExists) { + finalSegItem["cue"] = { in: true }; + } + this.vodAudioSegments[groupId][lang].push(finalSegItem); + } else { + if (!cueInExists) { + segments[groupId][lang][endIdx]["cue"] = { in: true }; + } + } + } + } + } else { + debug(`[${this.sessionId}]: 'vodAudioSegments' not empty = Using 'transitSegs'`); + } + debug(`[${this.sessionId}]: Setting CurrentAudioSequenceSegments. First seg is: [${this.vodAudioSegments[Object.keys(this.vodAudioSegments)[0]][Object.keys(this.vodAudioSegments[Object.keys(this.vodAudioSegments)[0]])][0].uri}]`); + + const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (isLeader) { + //debug(`[${this.sessionId}]: LEADER: I am adding 'transitSegs'=${JSON.stringify(this.vodSegments)} to Store for future followers`); + await this.sessionLiveState.set("transitSegs", this.vodAudioSegments); + debug(`[${this.sessionId}]: LEADER: I am adding 'transitSegs' to Store for future followers`); + } + } - async setCurrentMediaAndDiscSequenceCount(mediaSeq, discSeq) { + async setCurrentMediaAndDiscSequenceCount(mediaSeq, discSeq, audioMediaSeq, audioDiscSeq) { if (mediaSeq === null || discSeq === null) { debug(`[${this.sessionId}]: No media or disc sequence provided`); return false; } - debug(`[${this.sessionId}]: Setting mediaSeqCount and discSeqCount to: [${mediaSeq}]:[${discSeq}]`); + if (this.useDemuxedAudio && (audioDiscSeq === null || audioDiscSeq === null)) { + debug(`[${this.sessionId}]: No media or disc sequence for audio provided`); + return false; + } + debug(`[${this.sessionId}]: Setting mediaSeqCount, discSeqCount, audioSeqCount and audioDiscSeqCount to: [${mediaSeq}]:[${discSeq}], [${audioMediaSeq}]:[${audioDiscSeq}]`); this.mediaSeqCount = mediaSeq; this.discSeqCount = discSeq; + this.audioSeqCount = audioMediaSeq; + this.audioDiscSeqCount = audioDiscSeq; // IN CASE: New/Respawned Node Joins the Live Party // Don't use what Session gave you. Use the Leaders number if it's available @@ -312,14 +419,21 @@ class SessionLive { liveSourceMseqCount: null, mediaSeqCount: null, discSeqCount: null, + liveSourceAudioMseqCount: null, + audioSeqCount: null, + audioDiscSeqCount: null, }; } if (isLeader) { liveCounts.discSeqCount = this.discSeqCount; + liveCounts.audioDiscSeqCount = this.audioDiscSeqCount; await this.sessionLiveState.set("firstCounts", liveCounts); } else { const leadersMediaSeqCount = liveCounts.mediaSeqCount; const leadersDiscSeqCount = liveCounts.discSeqCount; + const leadersAudioSeqCount = liveCounts.audioSeqCount; + const leadersAudioDiscSeqCount = liveCounts.audioDiscSeqCount; + if (leadersMediaSeqCount !== null) { this.mediaSeqCount = leadersMediaSeqCount; debug(`[${this.sessionId}]: Setting mediaSeqCount to: [${this.mediaSeqCount}]`); @@ -329,17 +443,35 @@ class SessionLive { this.vodSegments = transitSegs; } } + if (leadersAudioSeqCount !== null) { + this.audioSeqCount = leadersAudioSeqCount; + debug(`[${this.sessionId}]: Setting mediaSeqCount to: [${this.audioSeqCount}]`); + const transitAudioSegs = await this.sessionLiveState.get("transitAudioSegs"); + if (!this._isEmpty(transitAudioSegs)) { + debug(`[${this.sessionId}]: Getting and loading 'transitSegs'`); + this.vodAudioSegments = transitAudioSegs; + } + } if (leadersDiscSeqCount !== null) { this.discSeqCount = leadersDiscSeqCount; debug(`[${this.sessionId}]: Setting discSeqCount to: [${this.discSeqCount}]`); } + if (leadersAudioDiscSeqCount !== null) { + this.discAudioSeqCount = leadersAudioDiscSeqCount; + debug(`[${this.sessionId}]: Setting discSeqCount to: [${this.discAudioSeqCount}]`); + } } + return true; } async getTransitionalSegments() { return this.vodSegments; } + async getTransitionalAudioSegments() { + return this.vodAudioSegments; + } + async getCurrentMediaSequenceSegments() { /** * Might be possible that a follower sends segments to Session @@ -388,10 +520,64 @@ class SessionLive { }; } + async getCurrentAudioSequenceSegments() { + /** + * Might be possible that a follower sends segments to Session + * BEFORE Leader finished fetching new segs and sending segs himself. + * As long as Leader sends same segs to session as Follower even though Leader + * is trying to get new segs, it should be fine! + **/ + this.allowedToSet = false; + const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (!isLeader) { + const leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); + if (leadersAudioSeqRaw > this.lastRequestedAudioSeqRaw) { + this.lastRequestedMediaSeqRaw = leadersAudioSeqRaw; + this.liveAudioSegsForFollowers = await this.sessionLiveState.get("liveAudioSegsForFollowers"); + this._updateAudioLiveSegQueue(); + } + } + + let currentAudioSequenceSegments = {}; + let segmentCount = 0; + let increment = 0; + const groupIds = Object.keys(this.audioManifestURIs); + for (let i = 0; i < groupIds.length; i++) { + let groupId = groupIds[i]; + let langs = Object.keys(this.audioManifestURIs[groupIds[i]]); + for (let j = 0; j < langs.length; j++) { + + const liveTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.audioManifestURIs); + const vodTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.vodAudioSegments); + + // Remove segments and disc-tag if they are on top + if (this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].length > 0 && this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage][0].discontinuity) { + this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].shift(); + increment = 1; + } + + segmentCount = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].length; + currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = []; + // In case we switch back before we've depleted all transitional segments + currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].concat(this.liveAudioSegQueue[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage]); + currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage].push({ discontinuity: true, cue: { in: true } }); + debug(`[${this.sessionId}]: Getting current audio segments for ${groupId, langs[j]}`); + } + } + + this.discSeqCount += increment; + return { + currMseqSegs: currentAudioSequenceSegments, + segCount: segmentCount, + }; + } + async getCurrentMediaAndDiscSequenceCount() { return { mediaSeq: this.mediaSeqCount, discSeq: this.discSeqCount, + audioSeq: this.audioSeqCount, + audioDiscSeq: this.audioDiscSeqCount, }; } @@ -440,10 +626,38 @@ class SessionLive { return m3u8; } - // TODO: Implement this later async getCurrentAudioManifestAsync(audioGroupId, audioLanguage) { - debug(`[${this.sessionId}]: getCurrentAudioManifestAsync is NOT Implemented`); - return "Not Implemented"; + if (!this.sessionLiveState) { + throw new Error("SessionLive not ready"); + } + if (audioGroupId === null) { + debug(`[${this.sessionId}]: No audioGroupId provided`); + return null; + } + if (audioLanguage === null) { + debug(`[${this.sessionId}]: No audioLanguage provided`); + return null; + } + debug(`[${this.sessionId}]: ...Loading the selected Live Audio Manifest`); + let attempts = 10; + let m3u8 = null; + while (!m3u8 && attempts > 0) { + attempts--; + try { + m3u8 = await this._GenerateLiveAudioManifest(audioGroupId, audioLanguage); + if (!m3u8) { + debug(`[${this.sessionId}]: No audio manifest available yet, will try again after 1000ms`); + await timer(1000); + } + } catch (exc) { + throw new Error(`[${this.instanceId}][${this.sessionId}]: Failed to generate audio manifest. Live Session might have ended already. \n${exc}`); + } + } + if (!m3u8) { + throw new Error(`[${this.instanceId}][${this.sessionId}]: Failed to generate audio manifest after 10000ms`); + } + console.log("return meu8", m3u8) + return m3u8; } async getCurrentSubtitleManifestAsync(subtitleGroupId, subtitleLanguage) { @@ -495,8 +709,35 @@ class SessionLive { this.mediaManifestURIs[streamItemBW] = ""; } this.mediaManifestURIs[streamItemBW] = mediaManifestUri; + if (streamItem.get("audio") && this.useDemuxedAudio) { + let audioGroupId = streamItem.get("audio") + let audioGroupItems = m3u.items.MediaItem.filter((item) => { + return item.get("type") === "AUDIO" && item.get("group-id") === audioGroupId; + }); + // # Find all langs amongst the mediaItems that have this group id. + // # It extracts each mediaItems language attribute value. + // # ALSO initialize in this.audioSegments a lang. property who's value is an array [{seg1}, {seg2}, ...]. + if (!this.audioManifestURIs[audioGroupId]) { + this.audioManifestURIs[audioGroupId] = {} + } + + audioGroupItems.map((item) => { + let itemLang; + if (!item.get("language")) { + itemLang = item.get("name"); + } else { + itemLang = item.get("language"); + } + if (!this.audioManifestURIs[audioGroupId][itemLang]) { + this.audioManifestURIs[audioGroupId][itemLang] = "ehj" + } + const audioManifestUri = url.resolve(baseUrl, streamItem.get("uri")) + this.audioManifestURIs[audioGroupId][itemLang] = audioManifestUri; + }); + } } debug(`[${this.sessionId}]: All Live Media Manifest URIs have been collected. (${Object.keys(this.mediaManifestURIs).length}) profiles found!`); + debug(`[${this.sessionId}]: All Live Audio Manifest URIs have been collected. (${Object.keys(this.audioManifestURIs[Object.keys(this.audioManifestURIs)[0]]).length}) tracks found!`); resolve(); parser.on("error", (exc) => { debug(`Parser Error: ${JSON.stringify(exc)}`); @@ -539,6 +780,48 @@ class SessionLive { } } + _updateLiveAudioSegQueue() { + let followerGroupIds = Object.keys(this.liveAudioSegsForFollowers); + let followerLangs = Object.keys(Object.keys(this.liveAudioSegsForFollowers[followerGroupIds[0]])); + if (this.liveAudioSegsForFollowers[followerGroupIds[0]][followerLangs[0]].length === 0) { + debug(`[${this.sessionId}]: FOLLOWER: Error No Segments found at all.`); + } + const liveGroupIds = Object.keys(this.liveAudioSegsForFollowers); + const size = this.liveAudioSegsForFollowers[liveGroupIds[0]][Object.keys(this.liveAudioSegsForFollowers[liveGroupIds[0]])].length; + + // Push the New Live Segments to All Variants + for (let segIdx = 0; segIdx < size; segIdx++) { + for (let i = 0; i < liveGroupIds.length; i++) {x + const liveGroupId = liveGroupIds[i]; + const liveLangs = Object.keys(this.liveAudioSegsForFollowers[liveGroupId]) + for (let j = 0; j < liveLangs.length; j++) { + const liveLang = liveLangs[j]; + + const liveSegFromLeader = this.liveAudioSegsForFollowers[liveGroupId][liveLang][segIdx]; + if (!this.liveAudioSegQueue[liveGroupId]) { + this.liveAudioSegQueue[liveGroupId] = {}; + } + if (!this.liveAudioSegQueue[liveGroupId][liveLang]) { + this.liveAudioSegQueue[liveGroupId][liveLang] = []; + } + // Do not push duplicates + const liveSegURIs = this.liveAudioSegQueue[liveGroupId][liveLang].filter((seg) => seg.uri).map((seg) => seg.uri); + if (liveSegFromLeader.uri && liveSegURIs.includes(liveSegFromLeader.uri)) { + debug(`[${this.sessionId}]: FOLLOWER: Found duplicate live segment. Skip push! (${liveGroupId})`); + } else { + this.liveAudioSegQueue[liveGroupId][liveLang].push(liveSegFromLeader); + debug(`[${this.sessionId}]: FOLLOWER: Pushed segment (${liveSegFromLeader.uri ? liveSegFromLeader.uri : "Disc-tag"}) to 'liveAudioSegQueue' (${liveGroupId, liveLang})`); + } + } + } + } + // Remove older segments and update counts + const newTotalDuration = this._incrementAndShift("FOLLOWER"); + if (newTotalDuration) { + debug(`[${this.sessionId}]: FOLLOWER: New Adjusted Playlist Duration=${newTotalDuration}s`); + } + } + /** * This function adds new live segments to the node from which it can * generate new manifests from. Method for attaining new segments differ @@ -700,9 +983,9 @@ class SessionLive { if (!allStoredMediaSeqCounts.every((val, i, arr) => val === arr[0])) { debug(`[${this.sessionId}]: Live Mseq counts=[${allStoredMediaSeqCounts}]`); // Figure out what bw's are behind. - const higestMediaSeqCount = Math.max(...allStoredMediaSeqCounts); + const highestMediaSeqCount = Math.max(...allStoredMediaSeqCounts); bandwidthsToSkipOnRetry = Object.keys(this.liveSourceM3Us).filter((bw) => { - if (this.liveSourceM3Us[bw].mediaSeq === higestMediaSeqCount) { + if (this.liveSourceM3Us[bw].mediaSeq === highestMediaSeqCount) { return true; } return false; @@ -884,11 +1167,11 @@ class SessionLive { if (this.firstTime && this.allowedToSet) { // Buy some time for followers (NOT Respawned) to fetch their own L.S m3u8. await timer(1000); // maybe remove - const firstCounts = { - liveSourceMseqCount: this.lastRequestedMediaSeqRaw, - mediaSeqCount: this.prevMediaSeqCount, - discSeqCount: this.prevDiscSeqCount, - }; + let firstCounts = await this.sessionLiveState.get("firstCounts"); + firstCounts.liveSourceMseqCount = this.lastRequestedMediaSeqRaw; + firstCounts.mediaSeqCount = this.prevMediaSeqCount; + firstCounts.discSeqCount = this.prevDiscSeqCount; + debug(`[${this.sessionId}]: LEADER: I am adding 'firstCounts'=${JSON.stringify(firstCounts)} to Store for future followers`); await this.sessionLiveState.set("firstCounts", firstCounts); } @@ -903,116 +1186,552 @@ class SessionLive { return; } - _shiftSegments(opt) { - let _totalDur = 0; - let _segments = {}; - let _name = ""; - let _removedSegments = 0; - let _removedDiscontinuities = 0; + async _loadAllAudioManifests() { + debug(`[${this.sessionId}]: Attempting to load all audio manifest URIs in=${Object.keys(this.audioManifestURIs[Object.keys(this.audioManifestURIs)[0]])}`); + let currentMseqRaw = null; + // ------------------------------------- + // If I am a Follower-node then my job + // ends here, where I only read from store. + // ------------------------------------- + let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (!isLeader && this.lastRequestedMediaSeqRaw !== null) { + debug(`[${this.sessionId}]: FOLLOWER: Reading data from store!`); - if (opt && opt.totalDur) { - _totalDur = opt.totalDur; - } - if (opt && opt.segments) { - _segments = JSON.parse(JSON.stringify(opt.segments)); // clone it - } - if (opt && opt.name) { - _name = opt.name || "NONE"; - } - if (opt && opt.removedSegments) { - _removedSegments = opt.removedSegments; - } - if (opt && opt.removedDiscontinuities) { - _removedDiscontinuities = opt.removedDiscontinuities; - } - const bws = Object.keys(_segments); + let leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); - /* When Total Duration is past the Limit, start Shifting V2L|LIVE segments if found */ - while (_totalDur > TARGET_PLAYLIST_DURATION_SEC) { - // Skip loop if there are no more segments to remove... - if (_segments[bws[0]].length === 0) { - return { totalDuration: _totalDur, removedSegments: _removedSegments, removedDiscontinuities: _removedDiscontinuities, shiftedSegments: _segments }; + if (!leadersAudioSeqRaw < this.lastRequestedAudioSeqRaw && this.blockGenerateManifest) { + this.blockGenerateManifest = false; } - debug(`[${this.sessionId}]: ${_name}: (${_totalDur})s/(${TARGET_PLAYLIST_DURATION_SEC})s - Playlist Duration is Over the Target. Shift needed!`); - let timeToRemove = 0; - let incrementDiscSeqCount = false; - // Shift Segments for each variant... - for (let i = 0; i < bws.length; i++) { - let seg = _segments[bws[i]].shift(); - if (i === 0) { - debug(`[${this.sessionId}]: ${_name}: (${bws[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); - } - if (seg && seg.discontinuity) { - incrementDiscSeqCount = true; - if (_segments[bws[i]].length > 0) { - seg = _segments[bws[i]].shift(); - if (i === 0) { - debug(`[${this.sessionId}]: ${_name}: (${bws[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); - } + let attempts = 10; + // CHECK AGAIN CASE 1: Store Empty + while (!leadersAudioSeqRaw && attempts > 0) { + if (!leadersAudioSeqRaw) { + isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (isLeader) { + debug(`[${this.instanceId}]: I'm the new leader`); + return; } } - if (seg && seg.duration) { - timeToRemove = seg.duration; + + if (!this.allowedToSet) { + debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); + break; } + const segDur = this._getAnyFirstAudioSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; + const waitTimeMs = parseInt(segDur / 3, 10); + debug(`[${this.sessionId}]: FOLLOWER: Leader has not put anything in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`); + await timer(waitTimeMs); + this.timerCompensation = false; + leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); + attempts--; } - if (timeToRemove) { - _totalDur -= timeToRemove; - // Increment number of removed segments... - _removedSegments++; - } - if (incrementDiscSeqCount) { - // Update Session Live Discontinuity Sequence Count - _removedDiscontinuities++; + + if (!leadersAudioSeqRaw) { + debug(`[${this.instanceId}]: The leader is still alive`); + return; } - } - return { totalDuration: _totalDur, removedSegments: _removedSegments, removedDiscontinuities: _removedDiscontinuities, shiftedSegments: _segments }; - } - /** - * Shifts V2L or LIVE items if total segment duration (V2L+LIVE) are over the target duration. - * It will also update and increment SessionLive's MediaSeqCount and DiscSeqCount based - * on what was shifted. - * @param {string} instanceName Name of instance "LEADER" | "FOLLOWER" - * @returns {number} The new total duration in seconds - */ - _incrementAndShift(instanceName) { - if (!instanceName) { - instanceName = "UNKNOWN"; + let liveAudioSegsInStore = await this.sessionLiveState.get("liveAudioSegsForFollowers"); + attempts = 10; + // CHECK AGAIN CASE 2: Store Old + while ((leadersAudioSeqRaw <= this.lastRequestedAudioSeqRaw && attempts > 0) || (this._containsAudioSegment(this.liveAudioSegsForFollowers, liveAudioSegsInStore) && attempts > 0)) { + if (!this.allowedToSet) { + debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); + break; + } + if (leadersAudioSeqRaw <= this.lastRequestedMediaSeqRaw) { + isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (isLeader) { + debug(`[${this.instanceId}][${this.sessionId}]: I'm the new leader`); + return; + } + } + if (this._containsAudioSegment(this.liveAudioSegsForFollowers, liveAudioSegsInStore)) { + debug(`[${this.sessionId}]: FOLLOWER: _containsSegment=true,${leadersAudioSeqRaw},${this.lastRequestedAudioSeqRaw}`); + } + const segDur = this._getAnyFirstAudioSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; + const waitTimeMs = parseInt(segDur / 3, 10); + debug(`[${this.sessionId}]: FOLLOWER: Cannot find anything NEW in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`); + await timer(waitTimeMs); + this.timerCompensation = false; + leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); + liveAudioSegsInStore = await this.sessionLiveState.get("liveAudioSegsForFollowers"); + attempts--; + } + // FINALLY + if (leadersAudioSeqRaw <= this.lastRequestedAudioSeqRaw) { + debug(`[${this.instanceId}][${this.sessionId}]: The leader is still alive`); + return; + } + // Follower updates its manifest building blocks (segment holders & counts) + this.lastRequestedAudioSeqRaw = leadersAudioSeqRaw; + this.liveAudioSegsForFollowers = liveAudioSegsInStore; + debug(`[${this.sessionId}]: These are the segments from store: [${JSON.stringify(this.liveAudioSegsForFollowers)}]`); + this._updateLiveAudioSegQueue(); + return; } - const vodBws = Object.keys(this.vodSegments); - const liveBws = Object.keys(this.liveSegQueue); - let vodTotalDur = 0; - let liveTotalDur = 0; - let totalDur = 0; - let removedSegments = 0; - let removedDiscontinuities = 0; - // Calculate Playlist Total Duration - this.vodSegments[vodBws[0]].forEach((seg) => { - if (seg.duration) { - vodTotalDur += seg.duration; - } - }); - this.liveSegQueue[liveBws[0]].forEach((seg) => { - if (seg.duration) { - liveTotalDur += seg.duration; + // --------------------------------- + // FETCHING FROM LIVE-SOURCE - New Followers (once) & Leaders do this. + // --------------------------------- + let FETCH_ATTEMPTS = 10; + this.liveAudioSegsForFollowers = {}; + let groupLangToSkipOnRetry = []; + while (FETCH_ATTEMPTS > 0) { + if (isLeader) { + console.log(`[${this.sessionId}]: LEADER: Trying to fetch manifests for all groups and language\n Attempts left=[${FETCH_ATTEMPTS}]`); + } else { + debug(`[${this.sessionId}]: NEW FOLLOWER: Trying to fetch manifests for all groups and language\n Attempts left=[${FETCH_ATTEMPTS}]`); } - }); - totalDur = vodTotalDur + liveTotalDur; - debug(`[${this.sessionId}]: ${instanceName}: L2L dur->: ${liveTotalDur}s | V2L dur->: ${vodTotalDur}s | Total dur->: ${totalDur}s`); - /** --- SHIFT then INCREMENT --- **/ + if (!this.allowedToSet) { + debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Live-Source`); + break; + } - // Shift V2L Segments - const outputV2L = this._shiftSegments({ - name: instanceName, - totalDur: totalDur, - segments: this.vodSegments, - removedSegments: removedSegments, - removedDiscontinuities: removedDiscontinuities, - }); - // Update V2L Segments + // Reset Values Each Attempt + let livePromises = []; + let manifestList = []; + this.pushAmount = 0; + try { + if (groupLangToSkipOnRetry.length > 0) { + debug(`[${this.sessionId}]: (X) Skipping loadAudio promises for bws ${JSON.stringify(groupLangToSkipOnRetry)}`); + } + // Collect Live Source Requesting Promises + const groupIds = Object.keys(this.audioManifestURIs) + for (let i = 0; i < groupIds.length; i++) { + let groupId = groupIds[i]; + let langs = Object.keys(this.audioManifestURIs[groupId]); + for (let j = 0; j < langs.length; j++) { + const lang = langs[j]; + if (groupLangToSkipOnRetry.includes(groupId + lang)) { + continue; + } + livePromises.push(this._loadAudioManifest(groupId, lang)); + console.log(`[${this.sessionId}]: Pushed loadAudio promise for groupId,lang=[${groupId}, ${lang}]`); + } + } + // Fetch From Live Source + debug(`[${this.sessionId}]: Executing Promises I: Fetch From Live Audio Source`); + manifestList = await allSettled(livePromises); + livePromises = []; + } catch (err) { + debug(`[${this.sessionId}]: Promises I: FAILURE!\n${err}`); + return; + } + + // Handle if any promise got rejected + if (manifestList.some((result) => result.status === "rejected")) { + FETCH_ATTEMPTS--; + debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again in 1000ms...`); + await timer(1000); + continue; + } + + // Store the results locally + manifestList.forEach((variantItem) => { + const groupId = variantItem.value.groupId; + const lang = variantItem.value.lang; + if (!this.liveAudioSourceM3Us[groupId]) { + this.liveAudioSourceM3Us[groupId] = {}; + } + if (!this.liveAudioSourceM3Us[groupId][lang]) { + this.liveAudioSourceM3Us[groupId][lang] = {}; + } + this.liveAudioSourceM3Us[groupId][lang] = variantItem.value; + }); + + const allStoredAudioSeqCounts = [];//Object.keys(this.liveAudioSourceM3Us).map((variant) => this.liveSourceM3Us[variant].mediaSeq); + + const groupIds = Object.keys(this.liveAudioSourceM3Us) + for (let i = 0; i < groupIds.length; i++) { + const langs = Object.keys(this.liveAudioSourceM3Us[groupIds[i]]); + for (let j = 0; j < langs.length; j++) { + allStoredAudioSeqCounts.push(this.liveAudioSourceM3Us[groupIds[i]][langs[j]].mediaSeq); + } + } + // Handle if mediaSeqCounts are NOT synced up! + if (!allStoredAudioSeqCounts.every((val, i, arr) => val === arr[0])) { + debug(`[${this.sessionId}]: Live audio Mseq counts=[${allStoredAudioSeqCounts}]`); + // Figure out what group lang is behind. + const highestMediaSeqCount = Math.max(...allStoredAudioSeqCounts); + + const gi = Object.keys(this.liveAudioSourceM3Us) + for (let i = 0; i < gi.length; i++) { + const langs = Object.keys(this.liveAudioSourceM3Us[groupIds[i]]); + for (let j = 0; j < langs.length; j++) { + if (this.liveSourceM3Us[gi[i]][langs[j]].mediaSeq === highestMediaSeqCount) { + groupLangToSkipOnRetry.push(gi[i] + langs[j]) + } + } + } + + // Decrement fetch counter + FETCH_ATTEMPTS--; + // Calculate retry delay time. Default=1000 + let retryDelayMs = 1000; + if (Object.keys(this.liveAudioSegQueue).length > 0) { + const firstGroupId = Object.keys(this.liveAudioSegQueue)[0]; + const firstLang = Object.keys(this.liveAudioSegQueue[firstGroupId])[0]; + const lastIdx = this.liveAudioSegQueue[firstGroupId][firstLang].length - 1; + if (this.liveAudioSegQueue[firstGroupId][lastIdx].duration) { + retryDelayMs = this.liveAudioSegQueue[firstGroupId][lastIdx].duration * 1000 * 0.25; + } + } + // Wait a little before trying again + debug(`[${this.sessionId}]: ALERT! Live Source Data NOT in sync! Will try again after ${retryDelayMs}ms`); + await timer(retryDelayMs); + if (isLeader) { + this.timerCompensation = false; + } + continue; + } + + currentMseqRaw = allStoredAudioSeqCounts[0]; + + if (!isLeader) { + let leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); + let tries = 20; + + while ((!isLeader && !leadersFirstSeqCounts.liveSourceAudioMseqCount && tries > 0) || leadersFirstSeqCounts.liveAudioSourceMseqCount === 0) { + debug(`[${this.sessionId}]: NEW FOLLOWER: Waiting for LEADER to add 'firstCounts' in store! Will look again after 1000ms (tries left=${tries})`); + await timer(1000); + leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); + tries--; + // Might take over as Leader if Leader is not setting data due to being down. + isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (isLeader) { + debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); + } + } + + if (tries === 0) { + isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (isLeader) { + debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); + break; + } else { + debug(`[${this.sessionId}][${this.instanceId}]: The leader is still alive`); + leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); + if (!leadersFirstSeqCounts.liveSourceMseqCount) { + debug(`[${this.sessionId}][${this.instanceId}]: Could not find 'firstCounts' in store. Abort Executing Promises II & Returning to Playhead.`); + return; + } + } + } + + if (isLeader) { + debug(`[${this.sessionId}]: NEW LEADER: Original Leader went missing, I am retrying live source fetch...`); + await this.sessionLiveState.set("transitAudioSegs", this.vodAudioSegments); + debug(`[${this.sessionId}]: NEW LEADER: I am adding 'transitSegs' to Store for future followers`); + continue; + } + + // Respawners never do this, only starter followers. + // Edge Case: FOLLOWER transitioned from session with different segments from LEADER + if (leadersFirstSeqCounts.discSeqCount !== this.discSeqCount) { + this.discSeqCount = leadersFirstSeqCounts.discSeqCount; + } + if (leadersFirstSeqCounts.mediaAudioSeqCount !== this.mediaAudioSeqCount) { + this.mediaAudioSeqCount = leadersFirstSeqCounts.mediaAudioSeqCount; + debug( + `[${this.sessionId}]: FOLLOWER transitioned with wrong V2L segments, updating counts to [${this.mediaAudioSeqCount}][${this.discAudioSeqCount}], and reading 'transitSegs' from store` + ); + const transitSegs = await this.sessionLiveState.get("transitAudioSegs"); + if (!this._isEmpty(transitSegs)) { + this.vodAudioSegments = transitSegs; + } + } + + // Prepare to load segments... + debug(`[${this.instanceId}][${this.sessionId}]: Newest mseq from LIVE=${currentMseqRaw} First mseq in store=${leadersFirstSeqCounts.liveAudioSourceMseqCount}`); + if (currentMseqRaw === leadersFirstSeqCounts.liveAudioSourceMseqCount) { + this.pushAmount = 1; // Follower from start + } else { + // TODO: To support and account for past discontinuity tags in the Live Source stream, + // we will need to get the real 'current' discontinuity-sequence count from Leader somehow. + + // RESPAWNED NODES + this.pushAudioAmount = currentMseqRaw - leadersFirstSeqCounts.liveAudioSourceMseqCount + 1; + + const transitSegs = await this.sessionLiveState.get("transitAudioSegs"); + //debug(`[${this.sessionId}]: NEW FOLLOWER: I tried to get 'transitSegs'. This is what I found ${JSON.stringify(transitSegs)}`); + if (!this._isEmpty(transitSegs)) { + this.vodAudioSegments = transitSegs; + } + } + console.log(`[${this.sessionId}]: ...pushAmount=${this.pushAudioAmount}`); + } else { + // LEADER calculates pushAmount differently... + if (this.firstTime) { + this.pushAudioAmount = 1; // Leader from start + } else { + this.pushAudioAmount = currentMseqRaw - this.lastRequestedAudioSeqRaw; + debug(`[${this.sessionId}]: ...calculating pushAmount=${currentMseqRaw}-${this.lastRequestedAudioSeqRaw}=${this.pushAudioAmount}`); + } + debug(`[${this.sessionId}]: ...pushAmount=${this.pushAudioAmount}`); + break; + } + // Live Source Data is in sync, and LEADER & new FOLLOWER are in sync + break; + } + + if (FETCH_ATTEMPTS === 0) { + debug(`[${this.sessionId}]: Fetching from Live-Source did not work! Returning to Playhead Loop...`); + return; + } + + isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + // NEW FOLLOWER - Edge Case: One Instance is ahead of another. Read latest live segs from store + if (!isLeader) { + const leadersCurrentMseqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); + const counts = await this.sessionLiveState.get("firstCounts"); + const leadersFirstMseqRaw = counts.liveSourceAudioMseqCount; + if (leadersCurrentMseqRaw !== null && leadersCurrentMseqRaw > currentMseqRaw) { + // if leader never had any segs from prev mseq + if (leadersFirstMseqRaw !== null && leadersFirstMseqRaw === leadersCurrentMseqRaw) { + // Follower updates it's manifest ingedients (segment holders & counts) + this.lastRequestedAudioSeqRaw = leadersCurrentMseqRaw; + this.liveAudioSegsForFollowers = await this.sessionLiveState.get("liveAudioSegsForFollowers"); + debug(`[${this.sessionId}]: NEW FOLLOWER: Leader is ahead or behind me! Clearing Queue and Getting latest segments from store.`); + this._updateLiveAudioSegQueue(); + this.firstTime = false; + debug(`[${this.sessionId}]: Got all needed segments from live-source (read from store).\nWe are now able to build Live Manifest: [${this.mediaSeqCount}]`); + return; + } else if (leadersCurrentMseqRaw < this.lastRequestedAudioSeqRaw) { + // WE ARE A RESPAWN-NODE, and we are ahead of leader. + this.blockGenerateManifest = true; + } + } + } + if (this.allowedToSet) { + console.log("hej", this.audioManifestURIs) + // Collect and Push Segment-Extracting Promises + let pushPromises = []; + for (let i = 0; i < Object.keys(this.audioManifestURIs).length; i++) { + let groupId = Object.keys(this.audioManifestURIs)[i]; + let langs = Object.keys(this.audioManifestURIs[groupId]); + for (let j = 0; j < langs.length; j++) { + let lang = langs[j]; + // will add new segments to live seg queue + pushPromises.push(this._parseAudioManifest(this.liveAudioSourceM3Us[groupId][lang].M3U, this.audioManifestURIs[groupId][lang], groupId, lang, isLeader)); + debug(`[${this.sessionId}]: Pushed pushPromise for groupId=${groupId} & lang${lang}`); + } + } + // Segment Pushing + debug(`[${this.sessionId}]: Executing Promises II: Segment Pushing`); + await allSettled(pushPromises); + + // UPDATE COUNTS, & Shift Segments in vodSegments and liveSegQueue if needed. + const leaderORFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; + const newTotalDuration = this._incrementAndShift(leaderORFollower); // might need audio + if (newTotalDuration) { + debug(`[${this.sessionId}]: New Adjusted Playlist Duration=${newTotalDuration}s`); + } + } + + // ----------------------------------------------------- + // Leader writes to store so that Followers can read. + // ----------------------------------------------------- + if (isLeader) { + if (this.allowedToSet) { + const liveGroupIds = Object.keys(this.liveAudioSegsForFollowers); + const liveLangs = Object.keys(this.liveAudioSegsForFollowers[liveGroupIds[0]]); + const segListSize = this.liveAudioSegsForFollowers[liveGroupIds[0]][liveLangs[0]].length; + // Do not replace old data with empty data + if (segListSize > 0) { + debug(`[${this.sessionId}]: LEADER: Adding data to store!`); + await this.sessionLiveState.set("lastRequestedAudioSeqRaw", this.lastRequestedAudioSeqRaw); + await this.sessionLiveState.set("liveAudioSegsForFollowers", this.liveAudioSegsForFollowers); + } + } + + // [LASTLY]: LEADER does this for respawned-FOLLOWERS' sake. + if (this.firstTime && this.allowedToSet) { + // Buy some time for followers (NOT Respawned) to fetch their own L.S m3u8. + await timer(1000); // maybe remove + let firstCounts = await this.sessionLiveState.get("firstCounts"); + firstCounts.liveSourceAudioMseqCount = this.lastRequestedAudioSeqRaw; + firstCounts.audioSeqCount = this.prevAudioSeqCount; + firstCounts.discAudioSeqCount = this.prevAudioDiscSeqCount; + + debug(`[${this.sessionId}]: LEADER: I am adding 'firstCounts'=${JSON.stringify(firstCounts)} to Store for future followers`); + await this.sessionLiveState.set("firstCounts", firstCounts); + } + debug(`[${this.sessionId}]: LEADER: I am using segs from Mseq=${this.lastRequestedAudioSeqRaw}`); + } else { + debug(`[${this.sessionId}]: NEW FOLLOWER: I am using segs from Mseq=${this.lastRequestedAudioSeqRaw}`); + } + + this.firstTime = false; + debug(`[${this.sessionId}]: Got all needed segments from live-source (from all bandwidths).\nWe are now able to build Live Manifest: [${this.audioSeqCount}]`); + + return; + } + + _shiftSegments(opt) { + let _totalDur = 0; + let _segments = {}; + let _name = ""; + let _removedSegments = 0; + let _removedDiscontinuities = 0; + let _type = "VIDEO"; + + if (opt && opt.totalDur) { + _totalDur = opt.totalDur; + } + if (opt && opt.segments) { + _segments = JSON.parse(JSON.stringify(opt.segments)); // clone it + } + if (opt && opt.name) { + _name = opt.name || "NONE"; + } + if (opt && opt.removedSegments) { + _removedSegments = opt.removedSegments; + } + if (opt && opt.removedDiscontinuities) { + _removedDiscontinuities = opt.removedDiscontinuities; + } + if (opt && opt.type) { + _type = opt.type; + } + const bws = Object.keys(_segments); + + + /* When Total Duration is past the Limit, start Shifting V2L|LIVE segments if found */ + while (_totalDur > TARGET_PLAYLIST_DURATION_SEC) { + let result = null; + if (_type === "VIDEO") { + result = this._shiftMediaSegments(bws, _name, _segments, _totalDur); + } else { + result = this._shiftAudioSegments(bws, _name, _segments, _totalDur); + } + // Skip loop if there are no more segments to remove... + if (!result) { + return { totalDuration: _totalDur, removedSegments: _removedSegments, removedDiscontinuities: _removedDiscontinuities, shiftedSegments: _segments }; + } + debug(`[${this.sessionId}]: ${_name}: (${_totalDur})s/(${TARGET_PLAYLIST_DURATION_SEC})s - Playlist Duration is Over the Target. Shift needed!`); + _segments = result.segments; + if (result.timeToRemove) { + _totalDur -= result.timeToRemove; + // Increment number of removed segments... + _removedSegments++; + } + if (result.incrementDiscSeqCount) { + // Update Session Live Discontinuity Sequence Count + _removedDiscontinuities++; + } + } + return { totalDuration: _totalDur, removedSegments: _removedSegments, removedDiscontinuities: _removedDiscontinuities, shiftedSegments: _segments }; + } + + _shiftMediaSegments(bws, _name, _segments) { + if (_segments[bws[0]].length === 0) { + return null; + } + let timeToRemove = 0; + let incrementDiscSeqCount = false; + + // Shift Segments for each variant... + for (let i = 0; i < bws.length; i++) { + let seg = _segments[bws[i]].shift(); + if (i === 0) { + debug(`[${this.sessionId}]: ${_name}: (${bws[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); + } + if (seg && seg.discontinuity) { + incrementDiscSeqCount = true; + if (_segments[bws[i]].length > 0) { + seg = _segments[bws[i]].shift(); + if (i === 0) { + debug(`[${this.sessionId}]: ${_name}: (${bws[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); + } + } + } + if (seg && seg.duration) { + timeToRemove = seg.duration; + } + } + return { timeToRemove: timeToRemove, incrementDiscSeqCount: incrementDiscSeqCount, segments: _segments } + } + + _shiftAudioSegments(groupIds, _name, _segments) { + const firstLang = Object.keys(_segments[groupIds[0]])[0]; + if (_segments[groupIds[0]][firstLang].length === 0) { + return null; + } + let timeToRemove = 0; + let incrementDiscSeqCount = false; + + // Shift Segments for each variant... + for (let i = 0; i < groupIds.length; i++) { + const langs = Object.keys(_segments[groupIds[i]]); + for (let j = 0; j < langs.length; j++) { + let seg = _segments[groupIds[i]][langs[j]].shift(); + if (i === 0) { + debug(`[${this.sessionId}]: ${_name}: (${groupIds[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); + } + if (seg && seg.discontinuity) { + incrementDiscSeqCount = true; + if (_segments[groupIds[i]][langs[j]].length > 0) { + seg = _segments[groupIds[i]][langs[j]].shift(); + if (i === 0) { + debug(`[${this.sessionId}]: ${_name}: (${groupIds[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); + } + } + } + if (seg && seg.duration) { + timeToRemove = seg.duration; + } + } + } + return { timeToRemove: timeToRemove, incrementDiscSeqCount: incrementDiscSeqCount, segments: _segments } + } + + /** + * Shifts V2L or LIVE items if total segment duration (V2L+LIVE) are over the target duration. + * It will also update and increment SessionLive's MediaSeqCount and DiscSeqCount based + * on what was shifted. + * @param {string} instanceName Name of instance "LEADER" | "FOLLOWER" + * @returns {number} The new total duration in seconds + */ + _incrementAndShift(instanceName) { + if (!instanceName) { + instanceName = "UNKNOWN"; + } + const vodBws = Object.keys(this.vodSegments); + const liveBws = Object.keys(this.liveSegQueue); + let vodTotalDur = 0; + let liveTotalDur = 0; + let totalDur = 0; + let removedSegments = 0; + let removedDiscontinuities = 0; + + // Calculate Playlist Total Duration + this.vodSegments[vodBws[0]].forEach((seg) => { + if (seg.duration) { + vodTotalDur += seg.duration; + } + }); + this.liveSegQueue[liveBws[0]].forEach((seg) => { + if (seg.duration) { + liveTotalDur += seg.duration; + } + }); + totalDur = vodTotalDur + liveTotalDur; + debug(`[${this.sessionId}]: ${instanceName}: L2L dur->: ${liveTotalDur}s | V2L dur->: ${vodTotalDur}s | Total dur->: ${totalDur}s`); + + /** --- SHIFT then INCREMENT --- **/ + + // Shift V2L Segments + const outputV2L = this._shiftSegments({ + name: instanceName, + totalDur: totalDur, + segments: this.vodSegments, + removedSegments: removedSegments, + removedDiscontinuities: removedDiscontinuities, + }); + // Update V2L Segments this.vodSegments = outputV2L.shiftedSegments; // Update values totalDur = outputV2L.totalDuration; @@ -1048,28 +1767,156 @@ class SessionLive { if (this.discSeqCount !== this.prevDiscSeqCount) { debug(`[${this.sessionId}]: ${instanceName}: Incrementing Dseq Count from {${this.prevDiscSeqCount}} -> {${this.discSeqCount}}`); } - debug(`[${this.sessionId}]: ${instanceName}: Incrementing Mseq Count from [${this.prevMediaSeqCount}] -> [${this.mediaSeqCount}]`); - debug(`[${this.sessionId}]: ${instanceName}: Finished updating all Counts and Segment Queues!`); - return totalDur; + debug(`[${this.sessionId}]: ${instanceName}: Incrementing Mseq Count from [${this.prevMediaSeqCount}] -> [${this.mediaSeqCount}]`); + debug(`[${this.sessionId}]: ${instanceName}: Finished updating all Counts and Segment Queues!`); + return totalDur; + } + + _incrementAndShiftAudio(instanceName) { + if (!instanceName) { + instanceName = "UNKNOWN"; + } + const vodGroupId = Object.keys(this.vodAudioSegments)[0]; + const vodLanguage = Object.keys(vodGroupId)[0]; + const liveGroupId = Object.keys(this.liveAudioSegQueue)[0]; + const liveLanguage = Object.keys(liveGroupId)[0]; + let vodTotalDur = 0; + let liveTotalDur = 0; + let totalDur = 0; + let removedSegments = 0; + let removedDiscontinuities = 0; + + // Calculate Playlist Total Duration + this.vodAudioSegments[vodGroupId][vodLanguage].forEach((seg) => { + if (seg.duration) { + vodTotalDur += seg.duration; + } + }); + this.liveAudioSegQueue[liveGroupId][liveLanguage].forEach((seg) => { + if (seg.duration) { + liveTotalDur += seg.duration; + } + }); + totalDur = vodTotalDur + liveTotalDur; + debug(`[${this.sessionId}]: ${instanceName}: L2L dur->: ${liveTotalDur}s | V2L dur->: ${vodTotalDur}s | Total dur->: ${totalDur}s`); + + /** --- SHIFT then INCREMENT --- **/ + + // Shift V2L Segments + console.log("shift vodx") + const outputV2L = this._shiftSegments({ + name: instanceName, + totalDur: totalDur, + segments: this.vodAudioSegments, + removedSegments: removedSegments, + removedDiscontinuities: removedDiscontinuities, + type: "AUDIO", + }); + // Update V2L Segments + this.vodAudioSegments = outputV2L.shiftedSegments; + // Update values + totalDur = outputV2L.totalDuration; + removedSegments = outputV2L.removedSegments; + removedDiscontinuities = outputV2L.removedDiscontinuities; + // Shift LIVE Segments + console.log("shift live") + const outputLIVE = this._shiftSegments({ + name: instanceName, + totalDur: totalDur, + segments: this.liveAudioSegQueue, + removedSegments: removedSegments, + removedDiscontinuities: removedDiscontinuities, + type: "AUDIO", + }); + // Update LIVE Segments + this.liveAudioSegQueue = outputLIVE.shiftedSegments; + // Update values + totalDur = outputLIVE.totalDuration; + removedSegments = outputLIVE.removedSegments; + removedDiscontinuities = outputLIVE.removedDiscontinuities; + + // Update Session Live Discontinuity Sequence Count... + this.prevAudioDiscSeqCount = this.discAudioSeqCount; + this.discAudioSeqCount += removedDiscontinuities; + // Update Session Live Audio Sequence Count... + this.prevAudioSeqCount = this.audioSeqCount; + this.audioSeqCount += removedSegments; + if (this.restAmount) { + this.audioSeqCount += this.restAmount; + debug(`[${this.sessionId}]: ${instanceName}: Added restAmount=[${this.restAmount}] to 'mediaSeqCount'`); + this.restAmount = 0; + } + + if (this.discAudioSeqCount !== this.prevAudioDiscSeqCount) { + debug(`[${this.sessionId}]: ${instanceName}: Incrementing Dseq Count from {${this.prevAudioDiscSeqCount}} -> {${this.discAudioSeqCount}}`); + } + debug(`[${this.sessionId}]: ${instanceName}: Incrementing Mseq Count from [${this.prevAudioSeqCount}] -> [${this.audioSeqCount}]`); + debug(`[${this.sessionId}]: ${instanceName}: Finished updating all Counts and Segment Queues!`); + return totalDur; + } + + async _loadMediaManifest(bw) { + if (!this.sessionLiveState) { + throw new Error("SessionLive not ready"); + } + + const liveTargetBandwidth = this._findNearestBw(bw, Object.keys(this.mediaManifestURIs)); + debug(`[${this.sessionId}]: Requesting bw=(${bw}), Nearest Bandwidth is: ${liveTargetBandwidth}`); + // Get the target media manifest + const mediaManifestUri = this.mediaManifestURIs[liveTargetBandwidth]; + const parser = m3u8.createStream(); + const controller = new AbortController(); + const timeout = setTimeout(() => { + debug(`[${this.sessionId}]: Request Timeout! Aborting Request to ${mediaManifestUri}`); + controller.abort(); + }, FAIL_TIMEOUT); + + const response = await fetch(mediaManifestUri, { signal: controller.signal }); + try { + response.body.pipe(parser); + } catch (err) { + debug(`[${this.sessionId}]: Error when piping response to parser! ${JSON.stringify(err)}`); + return Promise.reject(err); + } finally { + clearTimeout(timeout); + } + return new Promise((resolve, reject) => { + parser.on("m3u", (m3u) => { + try { + const resolveObj = { + M3U: m3u, + mediaSeq: m3u.get("mediaSequence"), + bandwidth: liveTargetBandwidth, + }; + resolve(resolveObj); + } catch (exc) { + debug(`[${this.sessionId}]: Error when parsing latest manifest`); + reject(exc); + } + }); + parser.on("error", (exc) => { + debug(`Parser Error: ${JSON.stringify(exc)}`); + reject(exc); + }); + }); } - async _loadMediaManifest(bw) { + async _loadAudioManifest(groupId, lang) { if (!this.sessionLiveState) { throw new Error("SessionLive not ready"); } - - const liveTargetBandwidth = this._findNearestBw(bw, Object.keys(this.mediaManifestURIs)); - debug(`[${this.sessionId}]: Requesting bw=(${bw}), Nearest Bandwidth is: ${liveTargetBandwidth}`); + const liveTargetGroupLang = this._findAudioGroupAndLang(groupId, lang, this.audioManifestURIs); + debug(`[${this.sessionId}]: Requesting groupId=(${groupId}) & lang=(${lang}), Nearest match is: ${JSON.stringify(liveTargetGroupLang)}`); // Get the target media manifest - const mediaManifestUri = this.mediaManifestURIs[liveTargetBandwidth]; + const audioManifestUri = this.audioManifestURIs[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage]; const parser = m3u8.createStream(); const controller = new AbortController(); const timeout = setTimeout(() => { - debug(`[${this.sessionId}]: Request Timeout! Aborting Request to ${mediaManifestUri}`); + debug(`[${this.sessionId}]: Request Timeout! Aborting Request to ${audioManifestUri}`); controller.abort(); }, FAIL_TIMEOUT); - const response = await fetch(mediaManifestUri, { signal: controller.signal }); + const response = await fetch(audioManifestUri, { signal: controller.signal }); try { response.body.pipe(parser); } catch (err) { @@ -1084,7 +1931,8 @@ class SessionLive { const resolveObj = { M3U: m3u, mediaSeq: m3u.get("mediaSequence"), - bandwidth: liveTargetBandwidth, + groupId: liveTargetGroupLang.audioGroupId, + lang: liveTargetGroupLang.audioLanguage, }; resolve(resolveObj); } catch (exc) { @@ -1138,6 +1986,52 @@ class SessionLive { }); } + _parseAudioManifest(m3u, audioPlaylistUri, liveTargetGroupId, liveTargetLanguage, isLeader) { + return new Promise(async (resolve, reject) => { + try { + if (!this.liveAudioSegQueue[liveTargetGroupId]) { + this.liveAudioSegQueue[liveTargetGroupId] = {}; + } + if (!this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage]) { + this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage] = []; + } + if (!this.liveAudioSegsForFollowers[liveTargetGroupId]) { + this.liveAudioSegsForFollowers[liveTargetGroupId] = {}; + } + if (!this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage]) { + this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage] = []; + } + let baseUrl = ""; + const m = audioPlaylistUri.match(/^(.*)\/.*?$/); + if (m) { + baseUrl = m[1] + "/"; + } + + //debug(`[${this.sessionId}]: Current RAW Mseq: [${m3u.get("mediaSequence")}]`); + //debug(`[${this.sessionId}]: Previous RAW Mseq: [${this.lastRequestedMediaSeqRaw}]`); + + if (this.pushAmount >= 0) { + this.lastRequestedMediaSeqRaw = m3u.get("mediaSequence"); + } + this.targetDuration = m3u.get("targetDuration"); + let startIdx = m3u.items.PlaylistItem.length - this.pushAmount; + if (startIdx < 0) { + this.restAmount = startIdx * -1; + startIdx = 0; + } + if (audioPlaylistUri) { + // push segments + console.log("pushed segments"); + this._addLiveAudioSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetGroupId, liveTargetLanguage, isLeader); + } + resolve(); + } catch (exc) { + console.error("ERROR: " + exc); + reject(exc); + } + }); + } + /** * Collects 'new' PlaylistItems and converts them into custom SegmentItems, * then Pushes them to the LiveSegQueue for all variants. @@ -1229,6 +2123,93 @@ class SessionLive { } } + _addLiveAudioSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetGroupId, liveTargetLanguage, isLeader) { + console.log("adding kive to queue", playlistItems.length) + const leaderOrFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; + for (let i = startIdx; i < playlistItems.length; i++) { + console.log("hej") + let seg = {}; + let playlistItem = playlistItems[i]; + let segmentUri; + let cueData = null; + let daterangeData = null; + let attributes = playlistItem["attributes"].attributes; + if (playlistItem.properties.discontinuity) { + this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage].push({ discontinuity: true }); + this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage].push({ discontinuity: true }); + } + if ("cuein" in attributes) { + if (!cueData) { + cueData = {}; + } + cueData["in"] = true; + } + if ("cueout" in attributes) { + if (!cueData) { + cueData = {}; + } + cueData["out"] = true; + cueData["duration"] = attributes["cueout"]; + } + if ("cuecont" in attributes) { + if (!cueData) { + cueData = {}; + } + cueData["cont"] = true; + } + if ("scteData" in attributes) { + if (!cueData) { + cueData = {}; + } + cueData["scteData"] = attributes["scteData"]; + } + if ("assetData" in attributes) { + if (!cueData) { + cueData = {}; + } + cueData["assetData"] = attributes["assetData"]; + } + if ("daterange" in attributes) { + if (!daterangeData) { + daterangeData = {}; + } + let allDaterangeAttributes = Object.keys(attributes["daterange"]); + allDaterangeAttributes.forEach((attr) => { + if (attr.match(/DURATION$/)) { + daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]); + } else { + daterangeData[attr.toLowerCase()] = attributes["daterange"][attr]; + } + }); + } + console.log(playlistItem) + if (playlistItem.properties.uri) { + console.log("playlistitem hase uri") + if (playlistItem.properties.uri.match("^http")) { + segmentUri = playlistItem.properties.uri; + } else { + segmentUri = url.resolve(baseUrl, playlistItem.properties.uri); + } + seg["duration"] = playlistItem.properties.duration; + seg["uri"] = segmentUri; + seg["cue"] = cueData; + if (daterangeData) { + seg["daterange"] = daterangeData; + } + // Push new Live Segments! But do not push duplicates + const liveSegURIs = this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage].filter((seg) => seg.uri).map((seg) => seg.uri); + if (seg.uri && liveSegURIs.includes(seg.uri)) { + console.log(`[${this.sessionId}]: ${leaderOrFollower}: Found duplicate live segment. Skip push! (${liveTargetGroupId, liveTargetLanguage})`); + } else { + console.log("pushing lice seq queue", seg) + this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage].push(seg); + this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage].push(seg); + debug(`[${this.sessionId}]: ${leaderOrFollower}: Pushed segment (${seg.uri ? seg.uri : "Disc-tag"}) to 'liveSegQueue' (${liveTargetGroupId, liveTargetLanguage})`); + } + } + } + } + /* ---------------------- GENERATE MANIFEST @@ -1311,50 +2292,155 @@ class SessionLive { return m3u8; } + async _GenerateLiveAudioManifest(audioGroupId, audioLanguage) { + if (audioGroupId === null) { + throw new Error("No audioGroupId provided"); + } + if (audioLanguage === null) { + throw new Error("No audioLanguage provided"); + } + const liveTargetTrackIds = this._findAudioGroupAndLang(audioGroupId, audioLanguage, this.audioManifestURIs); + const vodTargetTrackIds = this._findAudioGroupAndLang(audioGroupId, audioLanguage, this.vodAudioSegments); + debug(`[${this.sessionId}]: Client requesting manifest for VodTrackInfo=(${JSON.stringify(vodTargetTrackIds)}). Nearest LiveTrackInfo=(${JSON.stringify(liveTargetTrackIds)})`); + + if (this.blockGenerateManifest) { + debug(`[${this.sessionId}]: FOLLOWER: Cannot Generate Audio Manifest! Waiting to sync-up with Leader...`); + return null; + } + + // Uncomment below to guarantee that node always return the most current m3u8, + // But it will cost an extra trip to store for every client request... + /* + // DO NOT GENERATE MANIFEST CASE: Node is NOT in sync with Leader. (Store has new segs, but node hasn't read them yet) + const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (!isLeader) { + let leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); + if (leadersMediaSeqRaw !== this.lastRequestedMediaSeqRaw) { + debug(`[${this.sessionId}]: FOLLOWER: Cannot Audio Generate Manifest! <${this.instanceId}> New segments need to be collected first!...`); + return null; + } + } + */ + + // DO NOT GENERATE MANIFEST CASE: Node has not found anything in store OR Node has not even check yet. + console.log(this.liveAudioSegQueue, "live audio") + if (Object.keys(this.liveAudioSegQueue).length === 0 || + (this.liveAudioSegQueue[liveTargetTrackIds.audioGroupId] && + this.liveAudioSegQueue[liveTargetTrackIds.audioGroupId][liveTargetTrackIds.audioLanguage] && + this.liveAudioSegQueue[liveTargetTrackIds.audioGroupId][liveTargetTrackIds.audioLanguage].length === 0) + ) { + debug(`[${this.sessionId}]: Cannot Generate Audio Manifest! <${this.instanceId}> Not yet collected ANY segments from Live Source...`); + return null; + } + + // DO NOT GENERATE MANIFEST CASE: Node is in the middle of gathering segs of all variants. + const groupIds = Object.keys(this.liveAudioSegQueue); + let segAmounts = []; + for (let i = 0; i < groupIds.length; i++) { + const groupId = groupIds[i]; + const langs = Object.keys(this.liveAudioSegQueue[groupId]); + for (let j = 0; j < langs.length; j++) { + const lang = langs[j]; + if (this.liveAudioSegQueue[groupId][lang].length !== 0) { + segAmounts.push(this.liveAudioSegQueue[groupId][lang].length); + } + } + + } + + if (!segAmounts.every((val, i, arr) => val === arr[0])) { + debug(`[${this.sessionId}]: Cannot Generate Manifest! <${this.instanceId}> Not yet collected ALL segments from Live Source...`); + return null; + } + + if (!this._isEmpty(this.liveAudioSegQueue) && this.liveAudioSegQueue[groupIds[0]][Object.keys(this.liveAudioSegQueue[groupIds[0]][0])].length !== 0) { + this.targetDuration = this._getMaxDuration(this.liveAudioSegQueue[groupIds[0]][Object.keys(this.liveAudioSegQueue[groupIds[0]][0])]); + } + + // Determine if VOD segments influence targetDuration + for (let i = 0; i < this.vodAudioSegments[vodTargetTrackIds.audioGroupId][vodTargetTrackIds.audioLanguage].length; i++) { + let vodSeg = this.vodAudioSegments[vodTargetTrackIds.audioGroupId][vodTargetTrackIds.audioLanguage][i]; + // Get max duration amongst segments + if (vodSeg.duration > this.targetDuration) { + this.targetDuration = vodSeg.duration; + } + } + + debug(`[${this.sessionId}]: Started Generating the Manifest File:[${this.mediaSeqCount}]...`); + let m3u8 = "#EXTM3U\n"; + m3u8 += "#EXT-X-VERSION:6\n"; + m3u8 += m3u8Header(this.instanceId); + m3u8 += "#EXT-X-INDEPENDENT-SEGMENTS\n"; + m3u8 += "#EXT-X-TARGETDURATION:" + Math.round(this.targetDuration) + "\n"; + m3u8 += "#EXT-X-MEDIA-SEQUENCE:" + this.mediaSeqCount + "\n"; + m3u8 += "#EXT-X-DISCONTINUITY-SEQUENCE:" + this.discSeqCount + "\n"; + if (Object.keys(this.vodAudioSegments).length !== 0) { + // Add transitional segments if there are any left. + debug(`[${this.sessionId}]: Adding a Total of (${this.vodAudioSegments[vodTargetTrackIds.audioGroupId][vodTargetTrackIds.audioLanguage].length}) VOD segments to manifest`); + m3u8 = this._setAudioManifestTags(this.vodAudioSegments, m3u8, vodTargetTrackIds); + // Add live-source segments + m3u8 = this._setAudioManifestTags(this.liveAudioSegQueue, m3u8, liveTargetTrackIds); + } + debug(`[${this.sessionId}]: Manifest Generation Complete!`); + return m3u8; + } _setMediaManifestTags(segments, m3u8, bw) { for (let i = 0; i < segments[bw].length; i++) { const seg = segments[bw][i]; - if (seg.discontinuity) { - m3u8 += "#EXT-X-DISCONTINUITY\n"; - } - if (seg.cue) { - if (seg.cue.out) { - if (seg.cue.scteData) { - m3u8 += "#EXT-OATCLS-SCTE35:" + seg.cue.scteData + "\n"; - } - if (seg.cue.assetData) { - m3u8 += "#EXT-X-ASSET:" + seg.cue.assetData + "\n"; - } - m3u8 += "#EXT-X-CUE-OUT:DURATION=" + seg.cue.duration + "\n"; + m3u8 += this._setTagsOnSegment(seg, m3u8) + } + return m3u8 + } + + _setAudioManifestTags(segments, m3u8, trackIds) { + for (let i = 0; i < segments[trackIds.audioGroupId][trackIds.audioLanguage].length; i++) { + const seg = segments[trackIds.audioGroupId][trackIds.audioLanguage][i]; + m3u8 += this._setTagsOnSegment(seg, m3u8) + } + return m3u8 + } + + _setTagsOnSegment(segment, m3u8) { + if (segment.discontinuity) { + m3u8 += "#EXT-X-DISCONTINUITY\n"; + } + if (segment.cue) { + if (segment.cue.out) { + if (segment.cue.scteData) { + m3u8 += "#EXT-OATCLS-SCTE35:" + segment.cue.scteData + "\n"; } - if (seg.cue.cont) { - if (seg.cue.scteData) { - m3u8 += "#EXT-X-CUE-OUT-CONT:ElapsedTime=" + seg.cue.cont + ",Duration=" + seg.cue.duration + ",SCTE35=" + seg.cue.scteData + "\n"; - } else { - m3u8 += "#EXT-X-CUE-OUT-CONT:" + seg.cue.cont + "/" + seg.cue.duration + "\n"; - } + if (segment.cue.assetData) { + m3u8 += "#EXT-X-ASSET:" + segment.cue.assetData + "\n"; } + m3u8 += "#EXT-X-CUE-OUT:DURATION=" + segment.cue.duration + "\n"; } - if (seg.datetime) { - m3u8 += `#EXT-X-PROGRAM-DATE-TIME:${seg.datetime}\n`; - } - if (seg.daterange) { - const dateRangeAttributes = Object.keys(seg.daterange) - .map((key) => daterangeAttribute(key, seg.daterange[key])) - .join(","); - if (!seg.datetime && seg.daterange["start-date"]) { - m3u8 += "#EXT-X-PROGRAM-DATE-TIME:" + seg.daterange["start-date"] + "\n"; + if (segment.cue.cont) { + if (segment.cue.scteData) { + m3u8 += "#EXT-X-CUE-OUT-CONT:ElapsedTime=" + segment.cue.cont + ",Duration=" + segment.cue.duration + ",SCTE35=" + segment.cue.scteData + "\n"; + } else { + m3u8 += "#EXT-X-CUE-OUT-CONT:" + segment.cue.cont + "/" + segment.cue.duration + "\n"; } - m3u8 += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n"; } - // Mimick logic used in hls-vodtolive - if (seg.cue && seg.cue.in) { - m3u8 += "#EXT-X-CUE-IN" + "\n"; - } - if (seg.uri) { - m3u8 += "#EXTINF:" + seg.duration.toFixed(3) + ",\n"; - m3u8 += seg.uri + "\n"; + } + if (segment.datetime) { + m3u8 += `#EXT-X-PROGRAM-DATE-TIME:${segment.datetime}\n`; + } + if (segment.daterange) { + const dateRangeAttributes = Object.keys(segment.daterange) + .map((key) => daterangeAttribute(key, segment.daterange[key])) + .join(","); + if (!segment.datetime && segment.daterange["start-date"]) { + m3u8 += "#EXT-X-PROGRAM-DATE-TIME:" + segment.daterange["start-date"] + "\n"; } + m3u8 += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n"; + } + // Mimick logic used in hls-vodtolive + if (segment.cue && segment.cue.in) { + m3u8 += "#EXT-X-CUE-IN" + "\n"; + } + if (segment.uri) { + m3u8 += "#EXTINF:" + segment.duration.toFixed(3) + ",\n"; + m3u8 += segment.uri + "\n"; } return m3u8; } @@ -1392,6 +2478,70 @@ class SessionLive { return null; } + _getFirstAudioGroupWithSegments(array) { + const audioGroupIds = Object.keys(array).filter((id) => { + let idLangs = Object.keys(array[id]).filter((lang) => { + return array[id][lang].length > 0; + }); + return idLangs.length > 0; + }); + if (audioGroupIds.length > 0) { + return audioGroupIds[0]; + } else { + return null; + } + } + + _getFirstAudioLanguageWithSegments(groupId, array) { + const langsWithSegments = Object.keys(array[groupId]).filter((lang) => { + return array[groupId][lang].length > 0; + }); + if (langsWithSegments.length > 0) { + return langsWithSegments[0]; + } else { + return null; + } + } + + _findAudioGroupsForLang(audioLanguage, segments) { + let trackInfos = [] + const groupIds = Object.keys(segments); + for (let i = 0; i < groupIds.length; i++) { + const groupId = groupIds[i]; + const langs = Object.keys(segments[groupId]); + for (let j = 0; j < langs.length; j++) { + const lang = langs[j]; + if (lang === audioLanguage) { + + trackInfos.push({ audioGroupId: groupId, audioLanguage: lang }) + break; + } + } + } + return trackInfos; + } + + _findAudioGroupAndLang(audioGroupId, audioLanguage, array) { + if (audioGroupId === null || !array[audioGroupId]) { + audioGroupId = this._getFirstAudioGroupWithSegments(array); + if (!audioGroupId) { + return []; + } + } + if (!array[audioGroupId][audioLanguage]) { + const fallbackLang = this._getFirstAudioLanguageWithSegments(audioGroupId, array); + return { + "audioGroupId": audioGroupId, + "audioLanguage": fallbackLang + }; + } + return { + "audioGroupId": audioGroupId, + "audioLanguage": audioLanguage + }; + + } + _getMaxDuration(segments) { if (!segments) { debug(`[${this.sessionId}]: ERROR segments is: ${segments}`); @@ -1421,6 +2571,28 @@ class SessionLive { newItem[bw] = this.mediaManifestURIs[bw]; }); this.mediaManifestURIs = newItem; + + + } + + _filterLiveAudioTracks() { + let audioTracks = this.sessionAudioTracks; + const toKeep = new Set(); + + let newItemsAudio = {}; + audioTracks.forEach((audioTrack) => { + let groupAndLangToKeep = this._findAudioGroupsForLang(audioTrack.language, this.audioManifestURIs); + toKeep.add(...groupAndLangToKeep); + }); + toKeep.forEach((trackInfo) => { + if (!newItemsAudio[trackInfo.audioGroupId]) { + newItemsAudio[trackInfo.audioGroupId] = {} + } + newItemsAudio[trackInfo.audioGroupId][trackInfo.audioLanguage] = this.audioManifestURIs[trackInfo.audioGroupId][trackInfo.audioLanguage]; + + }); + + this.audioManifestURIs = newItemsAudio; } _getAnyFirstSegmentDurationMs() { diff --git a/engine/stream_switcher.js b/engine/stream_switcher.js index 348777bf..5288026a 100644 --- a/engine/stream_switcher.js +++ b/engine/stream_switcher.js @@ -118,6 +118,7 @@ class StreamSwitcher { const segments = await this._loadPreroll(prerollUri); const prerollItem = { segments: segments, + audioSegments: segments, maxAge: tsNow + 30 * 60 * 1000, }; this.prerollsCache[this.sessionId] = prerollItem; @@ -225,6 +226,12 @@ class StreamSwitcher { let currLiveCounts = 0; let currVodSegments = null; let eventSegments = null; + + let liveAudioSegments = null; + let currVodAudioSegments = null; + let eventAudioSegments = null; + + let liveUri = null; switch (state) { @@ -234,19 +241,25 @@ class StreamSwitcher { this.eventId = scheduleObj.eventId; currVodCounts = await session.getCurrentMediaAndDiscSequenceCount(); currVodSegments = await session.getCurrentMediaSequenceSegments({ targetMseq: currVodCounts.vodMediaSeqVideo }); + currVodAudioSegments = await session.getCurrentAudioSequenceSegments({ targetMseq: currVodCounts.vodMediaSeqAudio }); // Insert preroll if available for current channel if (this.prerollsCache[this.sessionId]) { - const prerollSegments = this.prerollsCache[this.sessionId].segments; + const prerollSegments = this.prerollsCache[this.sessionId].segments;//audio segments???? this._insertTimedMetadata(prerollSegments, scheduleObj.timedMetadata || {}); currVodSegments = this._mergeSegments(prerollSegments, currVodSegments, false); + + // TODO add preroll audio support + // const prerollAudioSegments = this.prerollsCache[this.sessionId].audioSegments; + // currVodAudioSegments = this._mergeSegments(prerollAudioSegments, currVodAudioSegments, false); } // In risk that the SL-playhead might have updated some data after // we reset last time... we should Reset SessionLive before sending new data. await sessionLive.resetLiveStoreAsync(0); - await sessionLive.setCurrentMediaAndDiscSequenceCount(currVodCounts.mediaSeq, currVodCounts.discSeq); + await sessionLive.setCurrentMediaAndDiscSequenceCount(currVodCounts.mediaSeq, currVodCounts.discSeq, currVodCounts.audioSeq, currVodCounts.discSeqAudio); await sessionLive.setCurrentMediaSequenceSegments(currVodSegments); + await sessionLive.setCurrentAudioSequenceSegments(currVodAudioSegments); liveUri = await sessionLive.setLiveUri(scheduleObj.uri); if (!liveUri) { @@ -275,8 +288,10 @@ class StreamSwitcher { this.eventId = scheduleObj.eventId; currVodCounts = await session.getCurrentMediaAndDiscSequenceCount(); eventSegments = await session.getTruncatedVodSegments(scheduleObj.uri, scheduleObj.duration / 1000); + eventAudioSegments = await session.getTruncatedVodAudioSegments(scheduleObj.uri, scheduleObj.duration / 1000); - if (!eventSegments) { + + if (!eventSegments || (this.useDemuxedAudio && !eventAudioSegments)) { debug(`[${this.sessionId}]: [ ERROR Switching from V2L->VOD ]`); this.working = false; this.eventId = null; @@ -289,8 +304,9 @@ class StreamSwitcher { eventSegments = this._mergeSegments(prerollSegments, eventSegments, true); } - await session.setCurrentMediaAndDiscSequenceCount(currVodCounts.mediaSeq, currVodCounts.discSeq); + await session.setCurrentMediaAndDiscSequenceCount(currVodCounts.mediaSeq, currVodCounts.discSeq, currVodCounts.audioSeq, currVodCounts.audioDiscSeq); await session.setCurrentMediaSequenceSegments(eventSegments, 0, true); + await session.setCurrentAudioSequenceSegments(eventAudioSegments, 0, true); this.working = false; debug(`[${this.sessionId}]: [ Switched from V2L->VOD ]`); @@ -307,13 +323,14 @@ class StreamSwitcher { debug(`[${this.sessionId}]: [ INIT Switching from LIVE->V2L ]`); this.eventId = null; liveSegments = await sessionLive.getCurrentMediaSequenceSegments(); + liveAudioSegments = await sessionLive.getCurrentAudioSequenceSegments() liveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount(); if (scheduleObj && !scheduleObj.duration) { debug(`[${this.sessionId}]: Cannot switch VOD. No duration specified for schedule item: [${scheduleObj.assetId}]`); } - if (this._isEmpty(liveSegments.currMseqSegs)) { + if (this._isEmpty(liveSegments.currMseqSegs) || (this.useDemuxedAudio && this._isEmpty(liveAudioSegments.currMseqSegs))) { this.working = false; this.streamTypeLive = false; debug(`[${this.sessionId}]: [ Switched from LIVE->V2L ]`); @@ -325,10 +342,15 @@ class StreamSwitcher { const prerollSegments = this.prerollsCache[this.sessionId].segments; liveSegments.currMseqSegs = this._mergeSegments(prerollSegments, liveSegments.currMseqSegs, false); liveSegments.segCount += prerollSegments.length; + // AUDIO NOT YET IMPLEMENTED + /*const prerollAudioSegments = this.prerollsCache[this.sessionId].audioSegments; + liveAudioSegments.currMseqSegs = this._mergeSegments(prerollAudioSegments, liveSegments.currMseqSegs, false); + liveAudioSegments.segCount += prerollAudioSegments.length;*/ } - await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq, liveCounts.discSeq); + await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq, liveCounts.discSeq, liveCounts.audioSeq, liveCounts.audioDiscSeq); await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount); + await session.setCurrentAudioSequenceSegments(liveAudioSegments.currMseqSegs, liveAudioSegments.segCount) await sessionLive.resetSession(); sessionLive.resetLiveStoreAsync(RESET_DELAY); // In parallel @@ -341,7 +363,7 @@ class StreamSwitcher { this.streamTypeLive = false; this.working = false; this.eventId = null; - debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->V2L ]`); + debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->V2L ] ${err}`); throw new Error(err); } case SwitcherState.LIVE_TO_VOD: @@ -350,9 +372,11 @@ class StreamSwitcher { // TODO: Not yet fully tested/supported this.eventId = scheduleObj.eventId; liveSegments = await sessionLive.getCurrentMediaSequenceSegments(); + liveAudioSegments = await sessionLive.getCurrentAudioSequenceSegments(); liveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount(); eventSegments = await session.getTruncatedVodSegments(scheduleObj.uri, scheduleObj.duration / 1000); + if (!eventSegments) { debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->VOD ]`); this.streamTypeLive = false; @@ -361,8 +385,9 @@ class StreamSwitcher { return false; } - await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq - 1, liveCounts.discSeq - 1); + await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq - 1, liveCounts.discSeq - 1, liveCounts.audioSeq - 1, liveCounts.audioDiscSeq - 1); await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount); + await session.setCurrentAudioSequenceSegments(liveAudioSegments.currMseqSegs, liveAudioSegments.segCount); // Insert preroll, if available, for current channel if (this.prerollsCache[this.sessionId]) { @@ -373,7 +398,7 @@ class StreamSwitcher { await sessionLive.resetSession(); sessionLive.resetLiveStoreAsync(RESET_DELAY); // In parallel - + this.working = false; this.streamTypeLive = false; debug(`[${this.sessionId}]: Switched from LIVE->VOD`); @@ -391,6 +416,7 @@ class StreamSwitcher { // TODO: Not yet fully tested/supported this.eventId = scheduleObj.eventId; eventSegments = await sessionLive.getCurrentMediaSequenceSegments(); + eventAudioSegments = await sessionLive.getCurrentAudioSequenceSegments(); currLiveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount(); await sessionLive.resetSession(); @@ -403,8 +429,12 @@ class StreamSwitcher { eventSegments.currMseqSegs = this._mergeSegments(prerollSegments, eventSegments.currMseqSegs, false); } - await sessionLive.setCurrentMediaAndDiscSequenceCount(currLiveCounts.mediaSeq, currLiveCounts.discSeq); + const faild = await sessionLive.setCurrentMediaAndDiscSequenceCount(currLiveCounts.mediaSeq, currLiveCounts.discSeq, currLiveCounts.audioSeq, currLiveCounts.audioDiscSeq); + if (!faild) { + console.error("cound not set switch live-> live", currVodCounts.mediaSeq, currVodCounts.discSeq, currVodCounts.audioSeq, currVodCounts.audioDiscSeq) + } await sessionLive.setCurrentMediaSequenceSegments(eventSegments.currMseqSegs); + await sessionLive.setCurrentAudioSequenceSegments(eventAudioSegments.currMseqSegs); liveUri = await sessionLive.setLiveUri(scheduleObj.uri); if (!liveUri) { @@ -472,6 +502,7 @@ class StreamSwitcher { const prerollSegments = {}; const mediaM3UPlaylists = {}; const mediaURIs = {}; + const audioURIs = {}; try { const m3u = await this._fetchParseM3u8(uri); debug(`[${this.sessionId}]: ...Fetched a New Preroll Slate Master Manifest from:\n${uri}`); @@ -481,17 +512,44 @@ class StreamSwitcher { for (let i = 0; i < m3u.items.StreamItem.length; i++) { const streamItem = m3u.items.StreamItem[i]; const bw = streamItem.get("bandwidth"); + + const mediaUri = streamItem.get("uri"); if (mediaUri.match("^http")) { mediaURIs[bw] = mediaUri; } else { mediaURIs[bw] = new URL(mediaUri, uri).href; } + const audioGroupId = streamItem.get("audio") + if (audioGroupId) { + audioURIs[audioGroupId] = {}; + let audioGroupItems = m3u.items.MediaItem.filter((item) => { + return item.get("type") === "AUDIO" && item.get("group-id") === audioGroupId; + }); + let audioLanguages = audioGroupItems.map((item) => { + let itemLang; + if (!item.get("language")) { + itemLang = item.get("name"); + } else { + itemLang = item.get("language"); + } + audioURIs[audioGroupId][itemLang] = []; + }); + for(let j = 0; j < audioLanguages.length; j++) { + const mediaUri = streamItem.get("uri"); + if (mediaUri.match("^http")) { + audioURIs[audioGroupId][audioLanguages[j]] = mediaUri; + } else { + audioURIs[audioGroupId][audioLanguages[j]] = new URL(mediaUri, uri).href; + } + } + } } // Fetch and parse Media URIs const bandwidths = Object.keys(mediaURIs); const loadMediaPromises = []; + const loadAudioPromises = [];// TODO rest of preroll // Queue up... bandwidths.forEach((bw) => loadMediaPromises.push(this._fetchParseM3u8(mediaURIs[bw]))); // Execute... diff --git a/examples/demux.ts b/examples/demux.ts index 33197bbf..c6078980 100644 --- a/examples/demux.ts +++ b/examples/demux.ts @@ -26,11 +26,7 @@ class RefAssetManager implements IAssetManager { title: "Elephants dream", uri: "https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/index.m3u8", }, - { - id: 2, - title: "Test HLS Bird noises (1m10s)", - uri: "https://mtoczko.github.io/hls-test-streams/test-audio-pdt/playlist.m3u8", - }, + ], }; this.pos = { @@ -117,4 +113,4 @@ const engineOptions: ChannelEngineOpts = { const engine = new ChannelEngine(refAssetManager, engineOptions); engine.start(); -engine.listen(process.env.PORT || 8000); +engine.listen(process.env.PORT || 8001); diff --git a/examples/livemix.ts b/examples/livemix.ts index d5780648..3eb69e55 100644 --- a/examples/livemix.ts +++ b/examples/livemix.ts @@ -5,7 +5,7 @@ import { ChannelEngine, ChannelEngineOpts, IAssetManager, IChannelManager, IStreamSwitchManager, VodRequest, VodResponse, Channel, ChannelProfile, - Schedule + Schedule, AudioTracks } from "../index"; const { v4: uuidv4 } = require('uuid'); @@ -14,38 +14,14 @@ class RefAssetManager implements IAssetManager { private assets; private pos; constructor(opts?) { - if (process.env.TEST_CHANNELS) { - this.assets = {}; - this.pos = {}; - - const testChannelsCount = parseInt(process.env.TEST_CHANNELS, 10); - for (let i = 0; i < testChannelsCount; i++) { - const channelId = `${i + 1}`; - this.assets[channelId] = [ - { id: 1, title: "Tears of Steel", uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/master.m3u8" }, - { id: 2, title: "Unhinged Trailer", uri: "https://maitv-vod.lab.eyevinn.technology/UNHINGED_Trailer_2020.mp4/master.m3u8" }, - { id: 3, title: "Morbius Trailer", uri: "https://maitv-vod.lab.eyevinn.technology/MORBIUS_Trailer_2020.mp4/master.m3u8" }, - { id: 4, title: "TV Plus Joachim", uri: "https://maitv-vod.lab.eyevinn.technology/tvplus-ad-joachim.mov/master.m3u8" }, - { id: 5, title: "The Outpost Trailer", uri: "https://maitv-vod.lab.eyevinn.technology/THE_OUTPOST_Trailer_2020.mp4/master.m3u8" }, - { id: 6, title: "TV Plus Megha", uri: "https://maitv-vod.lab.eyevinn.technology/tvplus-ad-megha.mov/master.m3u8" }, - ]; - this.pos[channelId] = 2; - } - } else { this.assets = { '1': [ - { id: 1, title: "Tears of Steel", uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/master.m3u8" }, - { id: 2, title: "Morbius Trailer", uri: "https://maitv-vod.lab.eyevinn.technology/MORBIUS_Trailer_2020.mp4/master.m3u8" }, - { id: 3, title: "The Outpost Trailer", uri: "https://maitv-vod.lab.eyevinn.technology/THE_OUTPOST_Trailer_2020.mp4/master.m3u8" }, - { id: 4, title: "Unhinged Trailer", uri: "https://maitv-vod.lab.eyevinn.technology/UNHINGED_Trailer_2020.mp4/master.m3u8" }, - { id: 5, title: "TV Plus Megha", uri: "https://maitv-vod.lab.eyevinn.technology/tvplus-ad-megha.mov/master.m3u8" }, - { id: 6, title: "TV Plus Joachim", uri: "https://maitv-vod.lab.eyevinn.technology/tvplus-ad-joachim.mov/master.m3u8" }, + { id: 1, title: "Tears of Steel", uri: "https://mtoczko.github.io/hls-test-streams/test-audio-pdt/playlist.m3u8" }, ] }; this.pos = { - '1': 1 + '1': 0 }; - } } /* @param {Object} vodRequest @@ -69,7 +45,7 @@ class RefAssetManager implements IAssetManager { { pos: 0, duration: 15 * 1000, - url: "https://maitv-vod.lab.eyevinn.technology/ads/apotea-15s.mp4/master.m3u8" + url: "https://mtoczko.github.io/hls-test-streams/test-audio-pdt/playlist.m3u8" } ] }; @@ -78,7 +54,7 @@ class RefAssetManager implements IAssetManager { const vodResponse = { id: vod.id, title: vod.title, - uri: STITCH_ENDPOINT + "?payload=" + encodedPayload + uri: vod.uri }; resolve(vodResponse); } else { @@ -99,10 +75,10 @@ class RefChannelManager implements IChannelManager { if (process.env.TEST_CHANNELS) { const testChannelsCount = parseInt(process.env.TEST_CHANNELS, 10); for (let i = 0; i < testChannelsCount; i++) { - this.channels.push({ id: `${i + 1}`, profile: this._getProfile() }); + this.channels.push({ id: `${i + 1}`, profile: this._getProfile(), audioTracks: this._getAudioTracks() }); } } else { - this.channels = [{ id: "1", profile: this._getProfile() }]; + this.channels = [{ id: "1", profile: this._getProfile(), audioTracks: this._getAudioTracks() }]; } } @@ -117,6 +93,13 @@ class RefChannelManager implements IChannelManager { { bw: 742000, codecs: 'avc1.4d001f,mp4a.40.2', resolution: [480, 214] }, ] } + + _getAudioTracks(): AudioTracks[] { + return [ + { language: "en", name: "English", default: true }, + { language: "es", name: "Spanish", default: false }, + ]; + } } const StreamType = Object.freeze({ LIVE: 1, @@ -168,8 +151,8 @@ class StreamSwitchManager implements IStreamSwitchManager { type: StreamType.LIVE, start_time: startOffset, end_time: endTime, - uri: "https://d2fz24s2fts31b.cloudfront.net/out/v1/6484d7c664924b77893f9b4f63080e5d/manifest.m3u8", - }, + uri: "http://localhost:8001/channels/1/master.m3u8", + }/*, { eventId: this.generateID(), assetId: this.generateID(), @@ -179,7 +162,7 @@ class StreamSwitchManager implements IStreamSwitchManager { end_time: (endTime + 100*1000) + streamDuration, uri: "https://maitv-vod.lab.eyevinn.technology/COME_TO_DADDY_Trailer_2020.mp4/master.m3u8", duration: streamDuration, - }); + }*/); } resolve(this.schedule[channelId]); } else { @@ -199,8 +182,9 @@ const engineOptions: ChannelEngineOpts = { channelManager: refChannelManager, streamSwitchManager: refStreamSwitchManager, defaultSlateUri: "https://maitv-vod.lab.eyevinn.technology/slate-consuo.mp4/master.m3u8", - slateRepetitions: 10, + slateRepetitions: 10, redisUrl: process.env.REDIS_URL, + useDemuxedAudio: true, }; const engine = new ChannelEngine(refAssetManager, engineOptions); diff --git a/package-lock.json b/package-lock.json index 1210c1e1..2d399e3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@eyevinn/hls-repeat": "^0.2.0", "@eyevinn/hls-truncate": "^0.2.0", - "@eyevinn/hls-vodtolive": "^3.0.0", + "@eyevinn/hls-vodtolive": "3.1.0", "@eyevinn/m3u8": "^0.5.3", "abort-controller": "^3.0.0", "debug": "^3.2.7", @@ -34,6 +34,9 @@ "node": ">=14 <20" } }, + "@eyevinn/master": { + "extraneous": true + }, "node_modules/@eyevinn/hls-repeat": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@eyevinn/hls-repeat/-/hls-repeat-0.2.0.tgz", @@ -96,9 +99,9 @@ } }, "node_modules/@eyevinn/hls-vodtolive": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@eyevinn/hls-vodtolive/-/hls-vodtolive-3.0.0.tgz", - "integrity": "sha512-TR5PrAwybam4nhtGifFyq1sx32UIIjByiaZhJKZ5m+vWno9auuFfPW0nxxiA72u1LR51EzX410TlU2L7rrgHUA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eyevinn/hls-vodtolive/-/hls-vodtolive-3.1.0.tgz", + "integrity": "sha512-/NOh+pq1ZAgtHSMDoUZEu2x/JPzHGuIHHPaasyfviWKwUdB/H3ReVB1V+23aXNbRX1JnlOLJLdUph6ZXe7QjMQ==", "dependencies": { "@eyevinn/m3u8": "^0.5.6", "abort-controller": "^3.0.0", @@ -2621,9 +2624,9 @@ } }, "@eyevinn/hls-vodtolive": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@eyevinn/hls-vodtolive/-/hls-vodtolive-3.0.0.tgz", - "integrity": "sha512-TR5PrAwybam4nhtGifFyq1sx32UIIjByiaZhJKZ5m+vWno9auuFfPW0nxxiA72u1LR51EzX410TlU2L7rrgHUA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eyevinn/hls-vodtolive/-/hls-vodtolive-3.1.0.tgz", + "integrity": "sha512-/NOh+pq1ZAgtHSMDoUZEu2x/JPzHGuIHHPaasyfviWKwUdB/H3ReVB1V+23aXNbRX1JnlOLJLdUph6ZXe7QjMQ==", "requires": { "@eyevinn/m3u8": "^0.5.6", "abort-controller": "^3.0.0", diff --git a/package.json b/package.json index 64e9f88d..edd7f33f 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "dependencies": { "@eyevinn/hls-repeat": "^0.2.0", "@eyevinn/hls-truncate": "^0.2.0", - "@eyevinn/hls-vodtolive": "^3.0.0", + "@eyevinn/hls-vodtolive": "3.1.0", "@eyevinn/m3u8": "^0.5.3", "abort-controller": "^3.0.0", "debug": "^3.2.7", From 7bcfcdbd8a85f2f945c0a3afd922af6a58741f41 Mon Sep 17 00:00:00 2001 From: Johan Lautakoksi Date: Fri, 11 Aug 2023 09:01:04 +0200 Subject: [PATCH 02/17] bug fixes, preroll implementation, function in session.js --- engine/server.ts | 1 + engine/session.js | 67 +++++++- engine/session_live.js | 162 +++++++++--------- engine/stream_switcher.js | 346 ++++++++++++++++++++++++++++---------- engine/util.js | 9 + 5 files changed, 406 insertions(+), 179 deletions(-) diff --git a/engine/server.ts b/engine/server.ts index d165642a..6a72c95f 100644 --- a/engine/server.ts +++ b/engine/server.ts @@ -482,6 +482,7 @@ export class ChannelEngine { sessionSwitchers[channel.id] = new StreamSwitcher({ sessionId: channel.id, + useDemuxedAudio: options.useDemuxedAudio, streamSwitchManager: this.streamSwitchManager ? this.streamSwitchManager : null }); diff --git a/engine/session.js b/engine/session.js index 540a9093..963f5bea 100644 --- a/engine/session.js +++ b/engine/session.js @@ -429,6 +429,69 @@ class Session { } } + async setCurrentAudioSequenceSegments(segments, mSeqOffset, reloadBehind) { + if (!this._sessionState) { + throw new Error("Session not ready"); + } + this.isSwitchingBackToV2L = true; + + this.switchDataForSession.reloadBehind = reloadBehind; + this.switchDataForSession.transitionSegments = segments; + this.switchDataForSession.mediaSeqOffset = mSeqOffset; + let waitTimeMs = 2000; + let groupId = Object.keys(segments)[0]; + let lang = Object.keys(segments[groupId])[0] + for (let i = segments[groupId][lang].length - 1; 0 < i; i--) { + const segment = segments[groupId][lang][i]; + if (segment.duration) { + waitTimeMs = parseInt(1000 * (segment.duration / 3), 10); + break; + } + } + let isLeader = await this._sessionStateStore.isLeader(this._instanceId); + if (!isLeader) { + debug(`[${this._sessionId}]: FOLLOWER: Invalidate cache to ensure having the correct VOD!`); + await this._sessionState.clearCurrentVodCache(); + + let vodReloaded = await this._sessionState.get("vodReloaded"); + let attempts = 9; + while (!isLeader && !vodReloaded && attempts > 0) { + debug(`[${this._sessionId}]: FOLLOWER: I arrived before LEADER. Waiting (1000ms) for LEADER to reload currentVod in store! (tries left=${attempts})`); + await timer(1000); + await this._sessionStateStore.clearLeaderCache(); + isLeader = await this._sessionStateStore.isLeader(this._instanceId); + vodReloaded = await this._sessionState.get("vodReloaded"); + attempts--; + } + + if (attempts === 0) { + debug(`[${this._sessionId}]: FOLLOWER: WARNING! Attempts=0 - Risk of using wrong currentVod`); + } + if (!isLeader || vodReloaded) { + debug(`[${this._sessionId}]: FOLLOWER: leader is alive, and has presumably updated currentVod. Clearing the cache now`); + await this._sessionState.clearCurrentVodCache(); + return; + } + debug(`[${this._sessionId}]: NEW LEADER: Setting state=VOD_RELOAD_INIT`); + this.isSwitchingBackToV2L = true; + await this._sessionState.set("state", SessionState.VOD_RELOAD_INIT); + + } else { + let vodReloaded = await this._sessionState.get("vodReloaded"); + let attempts = 12; + while (!vodReloaded && attempts > 0) { + debug(`[${this._sessionId}]: LEADER: Waiting (${waitTimeMs}ms) to buy some time reloading vod and adding it to store! (tries left=${attempts})`); + await timer(waitTimeMs); + vodReloaded = await this._sessionState.get("vodReloaded"); + attempts--; + } + if (attempts === 0) { + debug(`[${this._sessionId}]: LEADER: WARNING! Vod was never Reloaded!`); + return; + } + } + } + async getCurrentMediaSequenceSegments(opts) { if (!this._sessionState) { throw new Error('Session not ready'); @@ -504,7 +567,7 @@ class Session { tries--; state = await this.getSessionState(); } - + const playheadState = { vodMediaSeqAudio: null } @@ -520,7 +583,6 @@ class Session { if (currentVod) { try { const audioSegments = currentVod.getLiveAudioSequenceSegments(playheadState.vodMediaSeqAudio); - let audioSequenceValue = 0; if (currentVod.sequenceAlwaysContainNewSegments) { audioSequenceValue = currentVod.mediaSequenceValuesAudio[playheadState.vodMediaSeqAudio]; @@ -528,7 +590,6 @@ class Session { } else { audioSequenceValue = playheadState.vodMediaSeqAudio; } - debug(`[${this._sessionId}]: Requesting all audio segments from Media Sequence: ${playheadState.vodMediaSeqAudio}(${audioSequenceValue})_${currentVod.getLiveMediaSequencesCount("audio")}`); return audioSegments; } catch (err) { diff --git a/engine/session_live.js b/engine/session_live.js index 8b931197..4731ad15 100644 --- a/engine/session_live.js +++ b/engine/session_live.js @@ -61,9 +61,12 @@ class SessionLive { this.audioLiveSegsForFollowers = {}; this.timerCompensation = null; this.firstTime = true; + this.firstTimeAudio = true; this.allowedToSet = false; this.pushAmount = 0; this.restAmount = 0; + this.pushAmountAudio = 0; + this.restAmountAudio = 0; this.waitForPlayhead = true; this.blockGenerateManifest = false; @@ -155,7 +158,9 @@ class SessionLive { this.audioLiveSegsForFollowers = {}; this.timerCompensation = null; this.firstTime = true; + this.firstTimeAudio = true; this.pushAmount = 0; + this.pushAmountAudio = 0; this.allowedToSet = false; this.waitForPlayhead = true; this.blockGenerateManifest = false; @@ -266,6 +271,7 @@ class SessionLive { } // To make sure certain operations only occur once. this.firstTime = true; + this.firstTimeAudio = true; } // Return whether job was successful or not. if (!this.masterManifestUri) { @@ -457,8 +463,8 @@ class SessionLive { debug(`[${this.sessionId}]: Setting discSeqCount to: [${this.discSeqCount}]`); } if (leadersAudioDiscSeqCount !== null) { - this.discAudioSeqCount = leadersAudioDiscSeqCount; - debug(`[${this.sessionId}]: Setting discSeqCount to: [${this.discAudioSeqCount}]`); + this.audioDiscSeqCount = leadersAudioDiscSeqCount; + debug(`[${this.sessionId}]: Setting discSeqCount to: [${this.audioDiscSeqCount}]`); } } return true; @@ -532,7 +538,7 @@ class SessionLive { if (!isLeader) { const leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); if (leadersAudioSeqRaw > this.lastRequestedAudioSeqRaw) { - this.lastRequestedMediaSeqRaw = leadersAudioSeqRaw; + this.lastRequestedAudioSeqRaw = leadersAudioSeqRaw; this.liveAudioSegsForFollowers = await this.sessionLiveState.get("liveAudioSegsForFollowers"); this._updateAudioLiveSegQueue(); } @@ -544,18 +550,21 @@ class SessionLive { const groupIds = Object.keys(this.audioManifestURIs); for (let i = 0; i < groupIds.length; i++) { let groupId = groupIds[i]; + if (!currentAudioSequenceSegments[groupId]) { + currentAudioSequenceSegments[groupId] = {}; + } let langs = Object.keys(this.audioManifestURIs[groupIds[i]]); for (let j = 0; j < langs.length; j++) { - const liveTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.audioManifestURIs); const vodTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.vodAudioSegments); - + if (!vodTargetGroupLang.audioGroupId || !vodTargetGroupLang.audioLanguage) { + return null; + } // Remove segments and disc-tag if they are on top if (this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].length > 0 && this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage][0].discontinuity) { this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].shift(); increment = 1; } - segmentCount = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].length; currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = []; // In case we switch back before we've depleted all transitional segments @@ -565,7 +574,7 @@ class SessionLive { } } - this.discSeqCount += increment; + this.audioDiscSeqCount += increment; return { currMseqSegs: currentAudioSequenceSegments, segCount: segmentCount, @@ -656,7 +665,6 @@ class SessionLive { if (!m3u8) { throw new Error(`[${this.instanceId}][${this.sessionId}]: Failed to generate audio manifest after 10000ms`); } - console.log("return meu8", m3u8) return m3u8; } @@ -791,7 +799,8 @@ class SessionLive { // Push the New Live Segments to All Variants for (let segIdx = 0; segIdx < size; segIdx++) { - for (let i = 0; i < liveGroupIds.length; i++) {x + for (let i = 0; i < liveGroupIds.length; i++) { + x const liveGroupId = liveGroupIds[i]; const liveLangs = Object.keys(this.liveAudioSegsForFollowers[liveGroupId]) for (let j = 0; j < liveLangs.length; j++) { @@ -816,7 +825,7 @@ class SessionLive { } } // Remove older segments and update counts - const newTotalDuration = this._incrementAndShift("FOLLOWER"); + const newTotalDuration = this._incrementAndShiftAudio("FOLLOWER"); if (newTotalDuration) { debug(`[${this.sessionId}]: FOLLOWER: New Adjusted Playlist Duration=${newTotalDuration}s`); } @@ -1194,7 +1203,7 @@ class SessionLive { // ends here, where I only read from store. // ------------------------------------- let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (!isLeader && this.lastRequestedMediaSeqRaw !== null) { + if (!isLeader && this.lastRequestedAudioSeqRaw !== null) { debug(`[${this.sessionId}]: FOLLOWER: Reading data from store!`); let leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); @@ -1240,7 +1249,7 @@ class SessionLive { debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); break; } - if (leadersAudioSeqRaw <= this.lastRequestedMediaSeqRaw) { + if (leadersAudioSeqRaw <= this.lastRequestedAudioSeqRaw) { isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); if (isLeader) { debug(`[${this.instanceId}][${this.sessionId}]: I'm the new leader`); @@ -1280,7 +1289,7 @@ class SessionLive { let groupLangToSkipOnRetry = []; while (FETCH_ATTEMPTS > 0) { if (isLeader) { - console.log(`[${this.sessionId}]: LEADER: Trying to fetch manifests for all groups and language\n Attempts left=[${FETCH_ATTEMPTS}]`); + debug(`[${this.sessionId}]: LEADER: Trying to fetch manifests for all groups and language\n Attempts left=[${FETCH_ATTEMPTS}]`); } else { debug(`[${this.sessionId}]: NEW FOLLOWER: Trying to fetch manifests for all groups and language\n Attempts left=[${FETCH_ATTEMPTS}]`); } @@ -1293,7 +1302,7 @@ class SessionLive { // Reset Values Each Attempt let livePromises = []; let manifestList = []; - this.pushAmount = 0; + this.pushAmountAudio = 0; try { if (groupLangToSkipOnRetry.length > 0) { debug(`[${this.sessionId}]: (X) Skipping loadAudio promises for bws ${JSON.stringify(groupLangToSkipOnRetry)}`); @@ -1309,7 +1318,7 @@ class SessionLive { continue; } livePromises.push(this._loadAudioManifest(groupId, lang)); - console.log(`[${this.sessionId}]: Pushed loadAudio promise for groupId,lang=[${groupId}, ${lang}]`); + debug(`[${this.sessionId}]: Pushed loadAudio promise for groupId,lang=[${groupId}, ${lang}]`); } } // Fetch From Live Source @@ -1430,13 +1439,13 @@ class SessionLive { // Respawners never do this, only starter followers. // Edge Case: FOLLOWER transitioned from session with different segments from LEADER - if (leadersFirstSeqCounts.discSeqCount !== this.discSeqCount) { - this.discSeqCount = leadersFirstSeqCounts.discSeqCount; + if (leadersFirstSeqCounts.discSeqCount !== this.audioDiscSeqCount) { + this.audioDiscSeqCount = leadersFirstSeqCounts.discSeqCount; } - if (leadersFirstSeqCounts.mediaAudioSeqCount !== this.mediaAudioSeqCount) { - this.mediaAudioSeqCount = leadersFirstSeqCounts.mediaAudioSeqCount; + if (leadersFirstSeqCounts.audioSeqCount !== this.audioSeqCount) { + this.audioSeqCount = leadersFirstSeqCounts.audioSeqCount; debug( - `[${this.sessionId}]: FOLLOWER transitioned with wrong V2L segments, updating counts to [${this.mediaAudioSeqCount}][${this.discAudioSeqCount}], and reading 'transitSegs' from store` + `[${this.sessionId}]: FOLLOWER transitioned with wrong V2L segments, updating counts to [${this.audioSeqCount}][${this.audioDiscSeqCount}], and reading 'transitSegs' from store` ); const transitSegs = await this.sessionLiveState.get("transitAudioSegs"); if (!this._isEmpty(transitSegs)) { @@ -1447,13 +1456,13 @@ class SessionLive { // Prepare to load segments... debug(`[${this.instanceId}][${this.sessionId}]: Newest mseq from LIVE=${currentMseqRaw} First mseq in store=${leadersFirstSeqCounts.liveAudioSourceMseqCount}`); if (currentMseqRaw === leadersFirstSeqCounts.liveAudioSourceMseqCount) { - this.pushAmount = 1; // Follower from start + this.pushAmountAudio = 1; // Follower from start } else { // TODO: To support and account for past discontinuity tags in the Live Source stream, // we will need to get the real 'current' discontinuity-sequence count from Leader somehow. // RESPAWNED NODES - this.pushAudioAmount = currentMseqRaw - leadersFirstSeqCounts.liveAudioSourceMseqCount + 1; + this.pushAmountAudio = currentMseqRaw - leadersFirstSeqCounts.liveAudioSourceMseqCount + 1; const transitSegs = await this.sessionLiveState.get("transitAudioSegs"); //debug(`[${this.sessionId}]: NEW FOLLOWER: I tried to get 'transitSegs'. This is what I found ${JSON.stringify(transitSegs)}`); @@ -1461,16 +1470,16 @@ class SessionLive { this.vodAudioSegments = transitSegs; } } - console.log(`[${this.sessionId}]: ...pushAmount=${this.pushAudioAmount}`); + debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmountAudio}`); } else { // LEADER calculates pushAmount differently... - if (this.firstTime) { - this.pushAudioAmount = 1; // Leader from start + if (this.firstTimeAudio) { + this.pushAmountAudio = 1; // Leader from start } else { - this.pushAudioAmount = currentMseqRaw - this.lastRequestedAudioSeqRaw; - debug(`[${this.sessionId}]: ...calculating pushAmount=${currentMseqRaw}-${this.lastRequestedAudioSeqRaw}=${this.pushAudioAmount}`); + this.pushAmountAudio = currentMseqRaw - this.lastRequestedAudioSeqRaw; + debug(`[${this.sessionId}]: ...calculating pushAmount=${currentMseqRaw}-${this.lastRequestedAudioSeqRaw}=${this.pushAmountAudio}`); } - debug(`[${this.sessionId}]: ...pushAmount=${this.pushAudioAmount}`); + debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmountAudio}`); break; } // Live Source Data is in sync, and LEADER & new FOLLOWER are in sync @@ -1496,8 +1505,8 @@ class SessionLive { this.liveAudioSegsForFollowers = await this.sessionLiveState.get("liveAudioSegsForFollowers"); debug(`[${this.sessionId}]: NEW FOLLOWER: Leader is ahead or behind me! Clearing Queue and Getting latest segments from store.`); this._updateLiveAudioSegQueue(); - this.firstTime = false; - debug(`[${this.sessionId}]: Got all needed segments from live-source (read from store).\nWe are now able to build Live Manifest: [${this.mediaSeqCount}]`); + this.firstTimeAudio = false; + debug(`[${this.sessionId}]: Got all needed segments from live-source (read from store).\nWe are now able to build Audio Live Manifest: [${this.audioSeqCount}]`); return; } else if (leadersCurrentMseqRaw < this.lastRequestedAudioSeqRaw) { // WE ARE A RESPAWN-NODE, and we are ahead of leader. @@ -1506,7 +1515,6 @@ class SessionLive { } } if (this.allowedToSet) { - console.log("hej", this.audioManifestURIs) // Collect and Push Segment-Extracting Promises let pushPromises = []; for (let i = 0; i < Object.keys(this.audioManifestURIs).length; i++) { @@ -1525,7 +1533,7 @@ class SessionLive { // UPDATE COUNTS, & Shift Segments in vodSegments and liveSegQueue if needed. const leaderORFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; - const newTotalDuration = this._incrementAndShift(leaderORFollower); // might need audio + const newTotalDuration = this._incrementAndShiftAudio(leaderORFollower); // might need audio if (newTotalDuration) { debug(`[${this.sessionId}]: New Adjusted Playlist Duration=${newTotalDuration}s`); } @@ -1548,7 +1556,7 @@ class SessionLive { } // [LASTLY]: LEADER does this for respawned-FOLLOWERS' sake. - if (this.firstTime && this.allowedToSet) { + if (this.firstTimeAudio && this.allowedToSet) { // Buy some time for followers (NOT Respawned) to fetch their own L.S m3u8. await timer(1000); // maybe remove let firstCounts = await this.sessionLiveState.get("firstCounts"); @@ -1564,8 +1572,8 @@ class SessionLive { debug(`[${this.sessionId}]: NEW FOLLOWER: I am using segs from Mseq=${this.lastRequestedAudioSeqRaw}`); } - this.firstTime = false; - debug(`[${this.sessionId}]: Got all needed segments from live-source (from all bandwidths).\nWe are now able to build Live Manifest: [${this.audioSeqCount}]`); + this.firstTimeAudio = false; + debug(`[${this.sessionId}]: Got all needed segments from live-source (from all bandwidths).\nWe are now able to build Audi Live Manifest: [${this.audioSeqCount}]`); return; } @@ -1777,9 +1785,9 @@ class SessionLive { instanceName = "UNKNOWN"; } const vodGroupId = Object.keys(this.vodAudioSegments)[0]; - const vodLanguage = Object.keys(vodGroupId)[0]; + const vodLanguage = Object.keys(this.vodAudioSegments[vodGroupId])[0]; const liveGroupId = Object.keys(this.liveAudioSegQueue)[0]; - const liveLanguage = Object.keys(liveGroupId)[0]; + const liveLanguage = Object.keys(this.liveAudioSegQueue[vodGroupId])[0]; let vodTotalDur = 0; let liveTotalDur = 0; let totalDur = 0; @@ -1803,7 +1811,6 @@ class SessionLive { /** --- SHIFT then INCREMENT --- **/ // Shift V2L Segments - console.log("shift vodx") const outputV2L = this._shiftSegments({ name: instanceName, totalDur: totalDur, @@ -1819,7 +1826,6 @@ class SessionLive { removedSegments = outputV2L.removedSegments; removedDiscontinuities = outputV2L.removedDiscontinuities; // Shift LIVE Segments - console.log("shift live") const outputLIVE = this._shiftSegments({ name: instanceName, totalDur: totalDur, @@ -1836,19 +1842,19 @@ class SessionLive { removedDiscontinuities = outputLIVE.removedDiscontinuities; // Update Session Live Discontinuity Sequence Count... - this.prevAudioDiscSeqCount = this.discAudioSeqCount; - this.discAudioSeqCount += removedDiscontinuities; + this.prevAudioDiscSeqCount = this.audioDiscSeqCount; + this.audioDiscSeqCount += removedDiscontinuities; // Update Session Live Audio Sequence Count... this.prevAudioSeqCount = this.audioSeqCount; this.audioSeqCount += removedSegments; - if (this.restAmount) { - this.audioSeqCount += this.restAmount; - debug(`[${this.sessionId}]: ${instanceName}: Added restAmount=[${this.restAmount}] to 'mediaSeqCount'`); - this.restAmount = 0; + if (this.restAmountAudio) { + this.audioSeqCount += this.restAmountAudio; + debug(`[${this.sessionId}]: ${instanceName}: Added restAmountAudio=[${this.restAmountAudio}] to 'audioSeqCount'`); + this.restAmountAudio = 0; } - if (this.discAudioSeqCount !== this.prevAudioDiscSeqCount) { - debug(`[${this.sessionId}]: ${instanceName}: Incrementing Dseq Count from {${this.prevAudioDiscSeqCount}} -> {${this.discAudioSeqCount}}`); + if (this.audioDiscSeqCount !== this.prevAudioDiscSeqCount) { + debug(`[${this.sessionId}]: ${instanceName}: Incrementing Dseq Count from {${this.prevAudioDiscSeqCount}} -> {${this.audioDiscSeqCount}}`); } debug(`[${this.sessionId}]: ${instanceName}: Incrementing Mseq Count from [${this.prevAudioSeqCount}] -> [${this.audioSeqCount}]`); debug(`[${this.sessionId}]: ${instanceName}: Finished updating all Counts and Segment Queues!`); @@ -2008,20 +2014,19 @@ class SessionLive { } //debug(`[${this.sessionId}]: Current RAW Mseq: [${m3u.get("mediaSequence")}]`); - //debug(`[${this.sessionId}]: Previous RAW Mseq: [${this.lastRequestedMediaSeqRaw}]`); + //debug(`[${this.sessionId}]: Previous RAW Mseq: [${this.lastRequestedAudioSeqRaw}]`); - if (this.pushAmount >= 0) { - this.lastRequestedMediaSeqRaw = m3u.get("mediaSequence"); + if (this.pushAmountAudio >= 0) { + this.lastRequestedAudioSeqRaw = m3u.get("mediaSequence"); } this.targetDuration = m3u.get("targetDuration"); - let startIdx = m3u.items.PlaylistItem.length - this.pushAmount; + let startIdx = m3u.items.PlaylistItem.length - this.pushAmountAudio; if (startIdx < 0) { - this.restAmount = startIdx * -1; + this.restAmountAudio = startIdx * -1; startIdx = 0; } if (audioPlaylistUri) { // push segments - console.log("pushed segments"); this._addLiveAudioSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetGroupId, liveTargetLanguage, isLeader); } resolve(); @@ -2124,10 +2129,8 @@ class SessionLive { } _addLiveAudioSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetGroupId, liveTargetLanguage, isLeader) { - console.log("adding kive to queue", playlistItems.length) const leaderOrFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; for (let i = startIdx; i < playlistItems.length; i++) { - console.log("hej") let seg = {}; let playlistItem = playlistItems[i]; let segmentUri; @@ -2182,9 +2185,8 @@ class SessionLive { } }); } - console.log(playlistItem) + if (playlistItem.properties.uri) { - console.log("playlistitem hase uri") if (playlistItem.properties.uri.match("^http")) { segmentUri = playlistItem.properties.uri; } else { @@ -2199,9 +2201,8 @@ class SessionLive { // Push new Live Segments! But do not push duplicates const liveSegURIs = this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage].filter((seg) => seg.uri).map((seg) => seg.uri); if (seg.uri && liveSegURIs.includes(seg.uri)) { - console.log(`[${this.sessionId}]: ${leaderOrFollower}: Found duplicate live segment. Skip push! (${liveTargetGroupId, liveTargetLanguage})`); + debug(`[${this.sessionId}]: ${leaderOrFollower}: Found duplicate live segment. Skip push! (${liveTargetGroupId, liveTargetLanguage})`); } else { - console.log("pushing lice seq queue", seg) this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage].push(seg); this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage].push(seg); debug(`[${this.sessionId}]: ${leaderOrFollower}: Pushed segment (${seg.uri ? seg.uri : "Disc-tag"}) to 'liveSegQueue' (${liveTargetGroupId, liveTargetLanguage})`); @@ -2308,22 +2309,7 @@ class SessionLive { return null; } - // Uncomment below to guarantee that node always return the most current m3u8, - // But it will cost an extra trip to store for every client request... - /* - // DO NOT GENERATE MANIFEST CASE: Node is NOT in sync with Leader. (Store has new segs, but node hasn't read them yet) - const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (!isLeader) { - let leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); - if (leadersMediaSeqRaw !== this.lastRequestedMediaSeqRaw) { - debug(`[${this.sessionId}]: FOLLOWER: Cannot Audio Generate Manifest! <${this.instanceId}> New segments need to be collected first!...`); - return null; - } - } - */ - // DO NOT GENERATE MANIFEST CASE: Node has not found anything in store OR Node has not even check yet. - console.log(this.liveAudioSegQueue, "live audio") if (Object.keys(this.liveAudioSegQueue).length === 0 || (this.liveAudioSegQueue[liveTargetTrackIds.audioGroupId] && this.liveAudioSegQueue[liveTargetTrackIds.audioGroupId][liveTargetTrackIds.audioLanguage] && @@ -2345,16 +2331,15 @@ class SessionLive { segAmounts.push(this.liveAudioSegQueue[groupId][lang].length); } } - } if (!segAmounts.every((val, i, arr) => val === arr[0])) { - debug(`[${this.sessionId}]: Cannot Generate Manifest! <${this.instanceId}> Not yet collected ALL segments from Live Source...`); + console(`[${this.sessionId}]: Cannot Generate audio Manifest! <${this.instanceId}> Not yet collected ALL segments from Live Source...`); return null; } - if (!this._isEmpty(this.liveAudioSegQueue) && this.liveAudioSegQueue[groupIds[0]][Object.keys(this.liveAudioSegQueue[groupIds[0]][0])].length !== 0) { - this.targetDuration = this._getMaxDuration(this.liveAudioSegQueue[groupIds[0]][Object.keys(this.liveAudioSegQueue[groupIds[0]][0])]); + if (!this._isEmpty(this.liveAudioSegQueue) && this.liveAudioSegQueue[groupIds[0]][Object.keys(this.liveAudioSegQueue[groupIds[0]])[0]].length !== 0) { + this.targetDuration = this._getMaxDuration(this.liveAudioSegQueue[groupIds[0]][Object.keys(this.liveAudioSegQueue[groupIds[0]])[0]]); } // Determine if VOD segments influence targetDuration @@ -2366,22 +2351,22 @@ class SessionLive { } } - debug(`[${this.sessionId}]: Started Generating the Manifest File:[${this.mediaSeqCount}]...`); + debug(`[${this.sessionId}]: Started Generating the Audio Manifest File:[${this.audioSeqCount}]...`); let m3u8 = "#EXTM3U\n"; m3u8 += "#EXT-X-VERSION:6\n"; m3u8 += m3u8Header(this.instanceId); m3u8 += "#EXT-X-INDEPENDENT-SEGMENTS\n"; m3u8 += "#EXT-X-TARGETDURATION:" + Math.round(this.targetDuration) + "\n"; - m3u8 += "#EXT-X-MEDIA-SEQUENCE:" + this.mediaSeqCount + "\n"; - m3u8 += "#EXT-X-DISCONTINUITY-SEQUENCE:" + this.discSeqCount + "\n"; + m3u8 += "#EXT-X-MEDIA-SEQUENCE:" + this.audioSeqCount + "\n"; + m3u8 += "#EXT-X-DISCONTINUITY-SEQUENCE:" + this.audioDiscSeqCount + "\n"; if (Object.keys(this.vodAudioSegments).length !== 0) { // Add transitional segments if there are any left. - debug(`[${this.sessionId}]: Adding a Total of (${this.vodAudioSegments[vodTargetTrackIds.audioGroupId][vodTargetTrackIds.audioLanguage].length}) VOD segments to manifest`); + debug(`[${this.sessionId}]: Adding a Total of (${this.vodAudioSegments[vodTargetTrackIds.audioGroupId][vodTargetTrackIds.audioLanguage].length}) VOD audio segments to manifest`); m3u8 = this._setAudioManifestTags(this.vodAudioSegments, m3u8, vodTargetTrackIds); // Add live-source segments m3u8 = this._setAudioManifestTags(this.liveAudioSegQueue, m3u8, liveTargetTrackIds); } - debug(`[${this.sessionId}]: Manifest Generation Complete!`); + debug(`[${this.sessionId}]: Audio manifest Generation Complete!`); return m3u8; } _setMediaManifestTags(segments, m3u8, bw) { @@ -2400,7 +2385,8 @@ class SessionLive { return m3u8 } - _setTagsOnSegment(segment, m3u8) { + _setTagsOnSegment(segment) { + let m3u8 = ""; if (segment.discontinuity) { m3u8 += "#EXT-X-DISCONTINUITY\n"; } @@ -2530,6 +2516,14 @@ class SessionLive { } if (!array[audioGroupId][audioLanguage]) { const fallbackLang = this._getFirstAudioLanguageWithSegments(audioGroupId, array); + if (!fallbackLang) { + if (Object.keys(array[audioGroupId]).length > 0) { + return { + "audioGroupId": audioGroupId, + "audioLanguage": Object.keys(array[audioGroupId])[0], + }; + } + } return { "audioGroupId": audioGroupId, "audioLanguage": fallbackLang diff --git a/engine/stream_switcher.js b/engine/stream_switcher.js index 5288026a..5cf96f4e 100644 --- a/engine/stream_switcher.js +++ b/engine/stream_switcher.js @@ -3,7 +3,7 @@ const crypto = require("crypto"); const fetch = require("node-fetch"); const { AbortController } = require("abort-controller"); const { SessionState } = require("./session_state"); -const { timer, findNearestValue, isValidUrl, fetchWithRetry } = require("./util"); +const { timer, findNearestValue, isValidUrl, fetchWithRetry, findAudioGroupOrLang } = require("./util"); const m3u8 = require("@eyevinn/m3u8"); const SwitcherState = Object.freeze({ @@ -117,8 +117,8 @@ class StreamSwitcher { try { const segments = await this._loadPreroll(prerollUri); const prerollItem = { - segments: segments, - audioSegments: segments, + segments: segments.mediaSegments, + audioSegments: segments.audioSegments, maxAge: tsNow + 30 * 60 * 1000, }; this.prerollsCache[this.sessionId] = prerollItem; @@ -241,17 +241,21 @@ class StreamSwitcher { this.eventId = scheduleObj.eventId; currVodCounts = await session.getCurrentMediaAndDiscSequenceCount(); currVodSegments = await session.getCurrentMediaSequenceSegments({ targetMseq: currVodCounts.vodMediaSeqVideo }); - currVodAudioSegments = await session.getCurrentAudioSequenceSegments({ targetMseq: currVodCounts.vodMediaSeqAudio }); + if (this.useDemuxedAudio) { + currVodAudioSegments = await session.getCurrentAudioSequenceSegments({ targetMseq: currVodCounts.vodMediaSeqAudio }); + } // Insert preroll if available for current channel if (this.prerollsCache[this.sessionId]) { - const prerollSegments = this.prerollsCache[this.sessionId].segments;//audio segments???? + const prerollSegments = this.prerollsCache[this.sessionId].segments; this._insertTimedMetadata(prerollSegments, scheduleObj.timedMetadata || {}); currVodSegments = this._mergeSegments(prerollSegments, currVodSegments, false); + if (this.useDemuxedAudio) { - // TODO add preroll audio support - // const prerollAudioSegments = this.prerollsCache[this.sessionId].audioSegments; - // currVodAudioSegments = this._mergeSegments(prerollAudioSegments, currVodAudioSegments, false); + const prerollAudioSegments = this.prerollsCache[this.sessionId].audioSegments; + this._insertTimedMetadataAudio(prerollAudioSegments, scheduleObj.timedMetadata || {}); + currVodAudioSegments = this._mergeAudioSegments(prerollAudioSegments, currVodAudioSegments, false); + } } // In risk that the SL-playhead might have updated some data after @@ -302,6 +306,10 @@ class StreamSwitcher { if (this.prerollsCache[this.sessionId]) { const prerollSegments = this.prerollsCache[this.sessionId].segments; eventSegments = this._mergeSegments(prerollSegments, eventSegments, true); + if (this.useDemuxedAudio) { + const prerollAudioSegments = this.prerollsCache[this.sessionId].audioSegments; + eventAudioSegments = this._mergeAudioSegments(prerollAudioSegments, eventAudioSegments, true); + } } await session.setCurrentMediaAndDiscSequenceCount(currVodCounts.mediaSeq, currVodCounts.discSeq, currVodCounts.audioSeq, currVodCounts.audioDiscSeq); @@ -323,9 +331,10 @@ class StreamSwitcher { debug(`[${this.sessionId}]: [ INIT Switching from LIVE->V2L ]`); this.eventId = null; liveSegments = await sessionLive.getCurrentMediaSequenceSegments(); - liveAudioSegments = await sessionLive.getCurrentAudioSequenceSegments() + if (this.useDemuxedAudio) { + liveAudioSegments = await sessionLive.getCurrentAudioSequenceSegments(); + } liveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount(); - if (scheduleObj && !scheduleObj.duration) { debug(`[${this.sessionId}]: Cannot switch VOD. No duration specified for schedule item: [${scheduleObj.assetId}]`); } @@ -336,25 +345,24 @@ class StreamSwitcher { debug(`[${this.sessionId}]: [ Switched from LIVE->V2L ]`); return false; } - // Insert preroll, if available, for current channel if (this.prerollsCache[this.sessionId]) { const prerollSegments = this.prerollsCache[this.sessionId].segments; liveSegments.currMseqSegs = this._mergeSegments(prerollSegments, liveSegments.currMseqSegs, false); liveSegments.segCount += prerollSegments.length; - // AUDIO NOT YET IMPLEMENTED - /*const prerollAudioSegments = this.prerollsCache[this.sessionId].audioSegments; - liveAudioSegments.currMseqSegs = this._mergeSegments(prerollAudioSegments, liveSegments.currMseqSegs, false); - liveAudioSegments.segCount += prerollAudioSegments.length;*/ + if (this.useDemuxedAudio) { + const prerollAudioSegments = this.prerollsCache[this.sessionId].audioSegments; + liveAudioSegments.currMseqSegs = this._mergeAudioSegments(prerollAudioSegments, liveAudioSegments.currMseqSegs, false); + liveAudioSegments.segCount += prerollAudioSegments.length; + } } - await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq, liveCounts.discSeq, liveCounts.audioSeq, liveCounts.audioDiscSeq); await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount); - await session.setCurrentAudioSequenceSegments(liveAudioSegments.currMseqSegs, liveAudioSegments.segCount) - + if (this.useDemuxedAudio) { + await session.setCurrentAudioSequenceSegments(liveAudioSegments.currMseqSegs, liveAudioSegments.segCount) + } await sessionLive.resetSession(); sessionLive.resetLiveStoreAsync(RESET_DELAY); // In parallel - this.working = false; this.streamTypeLive = false; debug(`[${this.sessionId}]: [ Switched from LIVE->V2L ]`); @@ -376,6 +384,8 @@ class StreamSwitcher { liveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount(); eventSegments = await session.getTruncatedVodSegments(scheduleObj.uri, scheduleObj.duration / 1000); + eventAudioSegments = await session.getTruncatedVodAudioSegments(scheduleObj.uri, scheduleObj.duration / 1000); + if (!eventSegments) { debug(`[${this.sessionId}]: [ ERROR Switching from LIVE->VOD ]`); @@ -393,6 +403,11 @@ class StreamSwitcher { if (this.prerollsCache[this.sessionId]) { const prerollSegments = this.prerollsCache[this.sessionId].segments; eventSegments = this._mergeSegments(prerollSegments, eventSegments, true); + + if (this.useDemuxedAudio) { + const prerollAudioSegments = this.prerollsCache[this.sessionId].audioSegments; + eventAudioSegments = this._mergeSegments(prerollAudioSegments, eventAudioSegments, true); + } } await session.setCurrentMediaSequenceSegments(eventSegments, 0, true); @@ -427,6 +442,12 @@ class StreamSwitcher { const prerollSegments = this.prerollsCache[this.sessionId].segments; this._insertTimedMetadata(prerollSegments, scheduleObj.timedMetadata || {}); eventSegments.currMseqSegs = this._mergeSegments(prerollSegments, eventSegments.currMseqSegs, false); + + if (this.useDemuxedAudio) { + const prerollSegmentsAudio = this.prerollsCache[this.sessionId].audioSegments; + this._insertTimedMetadataAudio(prerollSegmentsAudio, scheduleObj.timedMetadata || {}); + eventSegments.currMseqSegs = this._mergeAudioSegments(prerollSegmentsAudio, eventAudioSegments.currMseqSegs, false); + } } const faild = await sessionLive.setCurrentMediaAndDiscSequenceCount(currLiveCounts.mediaSeq, currLiveCounts.discSeq, currLiveCounts.audioSeq, currLiveCounts.audioDiscSeq); @@ -500,9 +521,11 @@ class StreamSwitcher { async _loadPreroll(uri) { const prerollSegments = {}; + const prerollSegmentsAudio = {}; const mediaM3UPlaylists = {}; const mediaURIs = {}; const audioURIs = {}; + const audioM3UPlaylists = {}; try { const m3u = await this._fetchParseM3u8(uri); debug(`[${this.sessionId}]: ...Fetched a New Preroll Slate Master Manifest from:\n${uri}`); @@ -512,7 +535,7 @@ class StreamSwitcher { for (let i = 0; i < m3u.items.StreamItem.length; i++) { const streamItem = m3u.items.StreamItem[i]; const bw = streamItem.get("bandwidth"); - + const mediaUri = streamItem.get("uri"); if (mediaUri.match("^http")) { @@ -520,8 +543,9 @@ class StreamSwitcher { } else { mediaURIs[bw] = new URL(mediaUri, uri).href; } - const audioGroupId = streamItem.get("audio") - if (audioGroupId) { + + if (streamItem.get("audio")) { + const audioGroupId = streamItem.get("audio") audioURIs[audioGroupId] = {}; let audioGroupItems = m3u.items.MediaItem.filter((item) => { return item.get("type") === "AUDIO" && item.get("group-id") === audioGroupId; @@ -534,9 +558,10 @@ class StreamSwitcher { itemLang = item.get("language"); } audioURIs[audioGroupId][itemLang] = []; + return itemLang; }); - for(let j = 0; j < audioLanguages.length; j++) { - const mediaUri = streamItem.get("uri"); + for (let j = 0; j < audioGroupItems.length; j++) { + const mediaUri = audioGroupItems[j].get("uri"); if (mediaUri.match("^http")) { audioURIs[audioGroupId][audioLanguages[j]] = mediaUri; } else { @@ -546,14 +571,33 @@ class StreamSwitcher { } } + if (this.useDemuxedAudio && !audioURIs) { + throw new Error("Preroll is not demuxed"); + } + // Fetch and parse Media URIs const bandwidths = Object.keys(mediaURIs); const loadMediaPromises = []; - const loadAudioPromises = [];// TODO rest of preroll + const loadAudioPromises = []; // Queue up... - bandwidths.forEach((bw) => loadMediaPromises.push(this._fetchParseM3u8(mediaURIs[bw]))); + bandwidths.forEach( + (bw) => loadMediaPromises.push( + this._fetchParseM3u8(mediaURIs[bw]) + )); + if (this.useDemuxedAudio) { + const groupIds = Object.keys(audioURIs); + for (let i = 0; i < groupIds.length; i++) { + const groupId = groupIds[i]; + const langs = Object.keys(audioURIs[groupId]); + for (let j = 0; j < langs.length; j++) { + const lang = langs[j]; + loadAudioPromises.push(this._fetchParseM3u8(audioURIs[groupId][lang])); + } + } + } // Execute... const results = await Promise.allSettled(loadMediaPromises); + const resultsAudio = await Promise.allSettled(loadAudioPromises); // Process... results.forEach((item, idx) => { if (item.status === "fulfilled" && item.value) { @@ -562,6 +606,18 @@ class StreamSwitcher { mediaM3UPlaylists[bw] = resultM3U.items.PlaylistItem; } }); + + if (resultsAudio) { + resultsAudio.forEach((item, idx) => { + const resultM3U = item.value; + const indexes = this._getGroupAndLangIdxFromIdx(idx, audioURIs) + if (!audioM3UPlaylists[indexes.groupId]) { + audioM3UPlaylists[indexes.groupId] = {}; + } + audioM3UPlaylists[indexes.groupId][indexes.lang] = resultM3U.items.PlaylistItem; + }); + } + } else if (m3u.items.PlaylistItem.length > 0) { // Process the Media M3U. const arbitraryBw = 1; @@ -578,83 +634,128 @@ class StreamSwitcher { if (!prerollSegments[bw]) { prerollSegments[bw] = []; } - for (let k = 0; k < mediaM3UPlaylists[bw].length; k++) { - let seg = {}; - let playlistItem = mediaM3UPlaylists[bw][k]; - let segmentUri; - let cueData = null; - let daterangeData = null; - let attributes = playlistItem["attributes"].attributes; - if (playlistItem.properties.discontinuity) { - prerollSegments[bw].push({ discontinuity: true }); - } - if ("cuein" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["in"] = true; - } - if ("cueout" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["out"] = true; - cueData["duration"] = attributes["cueout"]; - } - if ("cuecont" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["cont"] = true; - } - if ("scteData" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["scteData"] = attributes["scteData"]; - } - if ("assetData" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["assetData"] = attributes["assetData"]; - } - if ("daterange" in attributes) { - if (!daterangeData) { - daterangeData = {}; - } - let allDaterangeAttributes = Object.keys(attributes["daterange"]); - allDaterangeAttributes.forEach((attr) => { - if (attr.match(/DURATION$/)) { - daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]); - } else { - daterangeData[attr.toLowerCase()] = attributes["daterange"][attr]; - } - }); - } - if (playlistItem.properties.uri) { - if (playlistItem.properties.uri.match("^http")) { - segmentUri = playlistItem.properties.uri; - } else { - segmentUri = new URL(playlistItem.properties.uri, mediaURIs[bw]).href; + prerollSegments[bw] = this._createCustomSimpleSegmentList(mediaM3UPlaylists[bw], bw, null, "video", mediaURIs); + } + if (this.useDemuxedAudio) { + const groupIds = Object.keys(audioM3UPlaylists); + for (let i = 0; i < groupIds.length; i++) { + const groupId = groupIds[i]; + const langs = Object.keys(audioM3UPlaylists[groupId]); + for (let j = 0; j < langs.length; j++) { + const lang = langs[j]; + if (!prerollSegmentsAudio[groupId]) { + prerollSegmentsAudio[groupId] = {}; } - seg["duration"] = playlistItem.properties.duration; - seg["uri"] = segmentUri; - seg["cue"] = cueData; - if (daterangeData) { - seg["daterange"] = daterangeData; + if (!prerollSegmentsAudio[groupId][lang]) { + prerollSegmentsAudio[groupId][lang] = []; } + prerollSegmentsAudio[groupId][lang] = this._createCustomSimpleSegmentList(audioM3UPlaylists[groupId][lang], groupId, lang, "audio", audioURIs); } - prerollSegments[bw].push(seg); } } debug(`[${this.sessionId}]: Loaded all Variants of the Preroll Slate!`); - return prerollSegments; + return { mediaSegments: prerollSegments, audioSegments: prerollSegmentsAudio }; } catch (err) { throw new Error(err); } } + _createCustomSimpleSegmentList(segmentList, keyValue1, keyValue2, type, URIs) { + let segments = []; + for (let k = 0; k < segmentList.length; k++) { + let seg = {}; + let playlistItem = segmentList[k]; + let segmentUri; + let cueData = null; + let daterangeData = null; + let attributes = playlistItem["attributes"].attributes; + if (playlistItem.properties.discontinuity) { + segments.push({ discontinuity: true }); + } + if ("cuein" in attributes) { + if (!cueData) { + cueData = {}; + } + cueData["in"] = true; + } + if ("cueout" in attributes) { + if (!cueData) { + cueData = {}; + } + cueData["out"] = true; + cueData["duration"] = attributes["cueout"]; + } + if ("cuecont" in attributes) { + if (!cueData) { + cueData = {}; + } + cueData["cont"] = true; + } + if ("scteData" in attributes) { + if (!cueData) { + cueData = {}; + } + cueData["scteData"] = attributes["scteData"]; + } + if ("assetData" in attributes) { + if (!cueData) { + cueData = {}; + } + cueData["assetData"] = attributes["assetData"]; + } + if ("daterange" in attributes) { + if (!daterangeData) { + daterangeData = {}; + } + let allDaterangeAttributes = Object.keys(attributes["daterange"]); + allDaterangeAttributes.forEach((attr) => { + if (attr.match(/DURATION$/)) { + daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]); + } else { + daterangeData[attr.toLowerCase()] = attributes["daterange"][attr]; + } + }); + } + if (playlistItem.properties.uri) { + if (playlistItem.properties.uri.match("^http")) { + segmentUri = playlistItem.properties.uri; + } else { + if (type === "video") { + segmentUri = new URL(playlistItem.properties.uri, URIs[keyValue1]).href; + } else if (type === "audio") { + segmentUri = new URL(playlistItem.properties.uri, URIs[keyValue1][keyValue2]).href; + } + } + seg["duration"] = playlistItem.properties.duration; + seg["uri"] = segmentUri; + seg["cue"] = cueData; + if (daterangeData) { + seg["daterange"] = daterangeData; + } + } + segments.push(seg); + + } + return segments + } + + _getGroupAndLangIdxFromIdx(idx, audioObject) { + const startIdx = 0; + let answerFound = false; + let storedLength = 0; + while (!answerFound) { + let groupIds = Object.keys(audioObject); + let langs = Object.keys(audioObject[groupIds[startIdx]]); + if (langs.length + storedLength > idx) { + answerFound = true + } else { + storedLength = langs.length; + startIdx++; + } + } + return { groupId: groupIds[startIdx], lang: langs[idx - storedLength] } + } + // Input: hls vod uri. Output: an M3U object. async _fetchParseM3u8(uri) { const parser = m3u8.createStream(); @@ -701,6 +802,46 @@ class StreamSwitcher { return OUTPUT_SEGMENTS; } + _mergeAudioSegments(fromSegments, toSegments, prepend) { + const OUTPUT_SEGMENTS = {}; + const fromGroups = Object.keys(fromSegments); + const toGroups = Object.keys(toSegments); + for (let i = 0; i < toGroups.length; i++) { + const groupId = toGroups[i]; + if (!OUTPUT_SEGMENTS[groupId]) { + OUTPUT_SEGMENTS[groupId] = {} + } + const langs = Object.keys(toSegments[groupId]) + for (let j = 0; j < langs.length; j++) { + const lang = langs[j]; + if (!OUTPUT_SEGMENTS[groupId][lang]) { + OUTPUT_SEGMENTS[groupId][lang] = []; + } + + const targetGroupId = findAudioGroupOrLang(groupId, fromGroups); + const fromLangs = Object.keys(fromSegments[targetGroupId]); + const targetLang = findAudioGroupOrLang(lang, fromLangs); + if (prepend) { + OUTPUT_SEGMENTS[targetGroupId][targetLang] = fromSegments[targetGroupId][targetLang].concat(toSegments[targetGroupId][targetLang]); + OUTPUT_SEGMENTS[targetGroupId][targetLang].unshift({ discontinuity: true }); + } else { + const lastSeg = toSegments[targetGroupId][targetLang][toSegments[targetGroupId][targetLang].length - 1]; + if (lastSeg.uri && !lastSeg.discontinuity) { + toSegments[targetGroupId][targetLang].push({ discontinuity: true, cue: { in: true } }); + OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[targetGroupId]); + } else if (lastSeg.discontinuity && !lastSeg.cue) { + toSegments[targetGroupId][targetLang][toSegments[targetGroupId][targetLang].length - 1].cue = { in: true } + OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[targetGroupId][fromLangs]); + } else { + OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[targetGroupId][fromLangs]); + OUTPUT_SEGMENTS[targetGroupId][targetLang].push({ discontinuity: true }); + } + } + } + }; + return OUTPUT_SEGMENTS; + } + _insertTimedMetadata(segments, timedMetadata) { const bandwidths = Object.keys(segments); debug(`[${this.sessionId}]: Inserting timed metadata ${Object.keys(timedMetadata).join(',')}`); @@ -715,6 +856,27 @@ class StreamSwitcher { segments[bw][0]["daterange"] = daterangeData; }); } + + _insertTimedMetadataAudio(segments, timedMetadata) { + const groupIds = Object.keys(segments); + debug(`[${this.sessionId}]: Inserting timed metadata ${Object.keys(timedMetadata).join(',')}`); + for (let i = 0; i < groupIds.length; i++) { + const groupId = groupIds[i]; + const langs = Object.keys(segments[groupId]); + for (let j = 0; j < langs.length; j++) { + const lang = langs[j]; + let daterangeData = segments[groupId][lang][0]["daterange"]; + if (!daterangeData) { + daterangeData = {}; + Object.keys(timedMetadata).forEach((k) => { + daterangeData[k] = timedMetadata[k]; + }); + } + segments[groupId][lang][0]["daterange"] = daterangeData; + } + + } + } } module.exports = StreamSwitcher; diff --git a/engine/util.js b/engine/util.js index 8a4ac4fb..393bc922 100644 --- a/engine/util.js +++ b/engine/util.js @@ -158,6 +158,14 @@ const findNearestValue = (val, array) => { return Math.abs(b - val) < Math.abs(a - val) ? b : a; }); }; +const findAudioGroupOrLang = (val, array) => { + for(let i = 0; i < array.length; i++) { + if (array[i] === val) { + return val + } + } + return array[0]; +}; const isValidUrl = (url) => { try { @@ -256,4 +264,5 @@ module.exports = { fetchWithRetry, codecsFromString, timeLeft, + findAudioGroupOrLang, }; From b1bbe6c69bd04914e9cba677b6f7891f86d4a02d Mon Sep 17 00:00:00 2001 From: Johan Lautakoksi Date: Mon, 14 Aug 2023 09:04:00 +0200 Subject: [PATCH 03/17] use audio in hls-v2l reload --- engine/session.js | 117 ++++++++++++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 39 deletions(-) diff --git a/engine/session.js b/engine/session.js index 963f5bea..3dd4ed93 100644 --- a/engine/session.js +++ b/engine/session.js @@ -429,15 +429,14 @@ class Session { } } - async setCurrentAudioSequenceSegments(segments, mSeqOffset, reloadBehind) { + async setCurrentAudioSequenceSegments(segments, aSeqOffset, reloadBehind) { if (!this._sessionState) { throw new Error("Session not ready"); } this.isSwitchingBackToV2L = true; - this.switchDataForSession.reloadBehind = reloadBehind; - this.switchDataForSession.transitionSegments = segments; - this.switchDataForSession.mediaSeqOffset = mSeqOffset; + this.switchDataForSession.transitionAudioSegments = segments; + this.switchDataForSession.audioSeqOffset = aSeqOffset; let waitTimeMs = 2000; let groupId = Object.keys(segments)[0]; let lang = Object.keys(segments[groupId])[0] @@ -567,7 +566,7 @@ class Session { tries--; state = await this.getSessionState(); } - + const playheadState = { vodMediaSeqAudio: null } @@ -631,10 +630,10 @@ class Session { state = await this.getSessionState(); } - const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo", "mediaSeqAudio","vodMediaSeqAudio"]); + const playheadState = await this._playheadState.getValues(["mediaSeq", "vodMediaSeqVideo", "mediaSeqAudio", "vodMediaSeqAudio"]); const discSeqOffset = await this._sessionState.get("discSeq"); const discAudioSeqOffset = await this._sessionState.get("discSeqAudio"); - + // Clear Vod Cache here when Switching to Live just to be safe... if (playheadState.vodMediaSeqVideo === 0) { @@ -849,7 +848,7 @@ class Session { throw new Error("Engine not ready"); } } - + async getCurrentSubtitleManifestAsync(subtitleGroupId, subtitleLanguage) { if (!this._sessionState) { throw new Error('Session not ready'); @@ -1005,7 +1004,7 @@ class Session { } } while (!(-thresh < posDiff && posDiff < thresh)); audioIncrement = index; - debug(`[${this._sessionId}]: Current VOD Playhead Positions are to be: [${positionV.toFixed(3)}][${positionA.toFixed(3)}] (${(positionA-positionV).toFixed(3)})`); + debug(`[${this._sessionId}]: Current VOD Playhead Positions are to be: [${positionV.toFixed(3)}][${positionA.toFixed(3)}] (${(positionA - positionV).toFixed(3)})`); } debug(`[${this._sessionId}]: Will increment audio with ${audioIncrement}`); sessionState.vodMediaSeqAudio = await this._sessionState.increment("vodMediaSeqAudio", audioIncrement); @@ -1294,7 +1293,7 @@ class Session { const profileChannels = profile.channels ? profile.channels : "2"; audioGroupIdToUse = currentVod.getAudioGroupIdForCodecs(audioCodec, profileChannels); if (!audioGroupIds.includes(audioGroupIdToUse)) { - audioGroupIdToUse = defaultAudioGroupId; + audioGroupIdToUse = defaultAudioGroupId; } } @@ -1302,30 +1301,30 @@ class Session { // skip stream if no corresponding audio group can be found if (audioGroupIdToUse) { - m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + - ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] + - ',CODECS="' + profile.codecs + '"' + - `,AUDIO="${audioGroupIdToUse}"` + - (defaultSubtitleGroupId ? `,SUBTITLES="${defaultSubtitleGroupId}"` : '') + + m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + + ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] + + ',CODECS="' + profile.codecs + '"' + + `,AUDIO="${audioGroupIdToUse}"` + + (defaultSubtitleGroupId ? `,SUBTITLES="${defaultSubtitleGroupId}"` : '') + (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n'; m3u8 += "master" + profile.bw + ".m3u8%3Bsession=" + this._sessionId + "\n"; } } else { - m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + - ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] + - ',CODECS="' + profile.codecs + '"' + - (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') + - (defaultSubtitleGroupId ? `,SUBTITLES="${defaultSubtitleGroupId}"` : '') + + m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + + ',RESOLUTION=' + profile.resolution[0] + 'x' + profile.resolution[1] + + ',CODECS="' + profile.codecs + '"' + + (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') + + (defaultSubtitleGroupId ? `,SUBTITLES="${defaultSubtitleGroupId}"` : '') + (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n'; m3u8 += "master" + profile.bw + ".m3u8%3Bsession=" + this._sessionId + "\n"; } }); } else { currentVod.getUsageProfiles().forEach(profile => { - m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + - ',RESOLUTION=' + profile.resolution + - ',CODECS="' + profile.codecs + '"' + - (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') + + m3u8 += '#EXT-X-STREAM-INF:BANDWIDTH=' + profile.bw + + ',RESOLUTION=' + profile.resolution + + ',CODECS="' + profile.codecs + '"' + + (defaultAudioGroupId ? `,AUDIO="${defaultAudioGroupId}"` : '') + (defaultSubtitleGroupId ? `,SUBTITLES="${defaultSubtitleGroupId}"` : '') + (hasClosedCaptions ? ',CLOSED-CAPTIONS="cc"' : '') + '\n'; m3u8 += "master" + profile.bw + ".m3u8%3Bsession=" + this._sessionId + "\n"; @@ -1466,8 +1465,9 @@ class Session { let loadPromise; if (!vodResponse.type) { debug(`[${this._sessionId}]: got first VOD uri=${vodResponse.uri}:${vodResponse.offset || 0}`); - const hlsOpts = { sequenceAlwaysContainNewSegments: this.alwaysNewSegments, - forcedDemuxMode: this.use_demuxed_audio, + const hlsOpts = { + sequenceAlwaysContainNewSegments: this.alwaysNewSegments, + forcedDemuxMode: this.use_demuxed_audio, dummySubtitleEndpoint: this.dummySubtitleEndpoint, subtitleSliceEndpoint: this.subtitleSliceEndpoint, shouldContainSubtitles: this.use_vtt_subtitles, @@ -1481,7 +1481,7 @@ class Session { } currentVod = newVod; if (vodResponse.desiredDuration) { - const { mediaManifestLoader, audioManifestLoader} = await this._truncateVod(vodResponse); + const { mediaManifestLoader, audioManifestLoader } = await this._truncateVod(vodResponse); loadPromise = currentVod.load(null, mediaManifestLoader, audioManifestLoader); } else { loadPromise = currentVod.load(); @@ -1642,8 +1642,9 @@ class Session { let loadPromise; if (!vodResponse.type) { debug(`[${this._sessionId}]: got next VOD uri=${vodResponse.uri}:${vodResponse.offset}`); - const hlsOpts = { sequenceAlwaysContainNewSegments: this.alwaysNewSegments, - forcedDemuxMode: this.use_demuxed_audio, + const hlsOpts = { + sequenceAlwaysContainNewSegments: this.alwaysNewSegments, + forcedDemuxMode: this.use_demuxed_audio, dummySubtitleEndpoint: this.dummySubtitleEndpoint, subtitleSliceEndpoint: this.subtitleSliceEndpoint, shouldContainSubtitles: this.use_vtt_subtitles, @@ -1664,7 +1665,7 @@ class Session { } }); if (vodResponse.desiredDuration) { - const { mediaManifestLoader, audioManifestLoader} = await this._truncateVod(vodResponse); + const { mediaManifestLoader, audioManifestLoader } = await this._truncateVod(vodResponse); loadPromise = newVod.loadAfter(currentVod, null, mediaManifestLoader, audioManifestLoader); } else { loadPromise = newVod.loadAfter(currentVod); @@ -1769,7 +1770,7 @@ class Session { sessionState.state = await this._sessionState.set("state", SessionState.VOD_RELOAD_INITIATING); // 2) Set new 'offset' sequences, to carry on the continuity from session-live let mSeq = this.switchDataForSession.mediaSeq; - // TODO: support demux^ + let aSeq = this.switchDataForSession.audioSeq; let currentVod = await this._sessionState.getCurrentVod(); if (currentVod.sequenceAlwaysContainNewSegments) { // (!) will need to compensate if using this setting on HLSVod Object. @@ -1779,20 +1780,50 @@ class Session { shiftedSeg = this.switchDataForSession.transitionSegments[bw].shift(); } }); + if (this.use_demuxed_audio) { + const groupIds = Object.keys(this.switchDataForSession.transitionAudioSegments) + for (let i = 0; i < groupIds.length; i++) { + const groupId = groupIds[i]; + const langs = Object.keys(this.switchDataForSession.transitionAudioSegments[groupId]) + for (let j = 0; j < langs.length; j++) { + const lang = langs[j]; + let shiftedSeg = this.switchDataForSession.transitionAudioSegments[groupId][lang].shift(); + if (shiftedSeg && shiftedSeg.discontinuity) { + shiftedSeg = this.switchDataForSession.transitionAudioSegments[groupId][lang].shift(); + } + + } + } + } } const dSeq = this.switchDataForSession.discSeq; const mSeqOffset = this.switchDataForSession.mediaSeqOffset; const reloadBehind = this.switchDataForSession.reloadBehind; const segments = this.switchDataForSession.transitionSegments; + + const audioDSeq = this.switchDataForSession.discAudioSeq; + const aSeqOffset = this.switchDataForSession.audioSeqOffset; + const audioSegments = this.switchDataForSession.transitionAudioSegments; + + if ([mSeq, dSeq, mSeqOffset, reloadBehind, segments].includes(null)) { debug(`[${this._sessionId}]: LEADER: Cannot Reload VOD, missing switch-back data`); return; } + + if (this.use_demuxed_audio && [aSeq, audioDSeq, aSeqOffset, audioSegments].includes(null)) { + debug(`[${this._sessionId}]: LEADER: Cannot Reload VOD, missing switch-back data`); + return; + } await this._sessionState.set("mediaSeq", mSeq); await this._playheadState.set("mediaSeq", mSeq, isLeader); await this._sessionState.set("discSeq", dSeq); + await this._sessionState.set("mediaSeqAudio", aSeq); + await this._playheadState.set("mediaSeqAudio", aSeq, isLeader); + await this._sessionState.set("discSeqAudio", audioDSeq); // TODO: support demux^ debug(`[${this._sessionId}]: Setting current media and discontinuity count -> [${mSeq}]:[${dSeq}]`); + debug(`[${this._sessionId}]: Setting current audio media and discontinuity count -> [${aSeq}]:[${audioDSeq}]`); // 3) Set new media segments/currentVod, to carry on the continuity from session-live debug(`[${this._sessionId}]: LEADER: making changes to current VOD. I will also update currentVod in store.`); const playheadState = await this._playheadState.getValues(["vodMediaSeqVideo"]); @@ -1801,11 +1832,17 @@ class Session { nextMseq = currentVod.getLiveMediaSequencesCount() - 1; } + const playheadStateAudio = await this._playheadState.getValues(["vodMediaSeqAudio"]); + let nextAudioMseq = playheadStateAudio.vodMediaSeqAudio + 1; + if (nextAudioMseq > currentVod.getLiveMediaSequencesCount("audio") - 1) { + nextAudioMseq = currentVod.getLiveMediaSequencesCount("audio") - 1; + } + // ---------------------------------------------------. - // TODO: Support reloading with audioSegments and SubtitleSegments as well | + // TODO: Support reloading with SubtitleSegments as well | // ---------------------------------------------------' - await currentVod.reload(nextMseq, segments, null, reloadBehind); + await currentVod.reload(nextMseq, segments, audioSegments, reloadBehind); await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 }); await this._sessionState.set("vodReloaded", 1); await this._sessionState.set("vodMediaSeqVideo", 0); @@ -1820,6 +1857,7 @@ class Session { debug(`[${this._sessionId}]: next VOD Reloaded (${currentVod.getDeltaTimes()})`); debug(`[${this._sessionId}]: ${currentVod.getPlayheadPositions()}`); debug(`[${this._sessionId}]: msequences=${currentVod.getLiveMediaSequencesCount()}`); + debug(`[${this._sessionId}]: audio msequences=${currentVod.getLiveMediaSequencesCount("audio")}`); cloudWatchLog(!this.cloudWatchLogging, "engine-session", { event: "switchback", channel: this._sessionId, reqTimeMs: Date.now() - startTS }); return; } else { @@ -1889,8 +1927,9 @@ class Session { slateVod.load() .then(() => { - const hlsOpts = { sequenceAlwaysContainNewSegments: this.alwaysNewSegments, - forcedDemuxMode: this.use_demuxed_audio, + const hlsOpts = { + sequenceAlwaysContainNewSegments: this.alwaysNewSegments, + forcedDemuxMode: this.use_demuxed_audio, dummySubtitleEndpoint: this.dummySubtitleEndpoint, subtitleSliceEndpoint: this.subtitleSliceEndpoint, shouldContainSubtitles: this.use_vtt_subtitles, @@ -1987,10 +2026,10 @@ class Session { slateVod.load() .then(() => { - const hlsOpts = { - sequenceAlwaysContainNewSegments: this.alwaysNewSegments, - forcedDemuxMode: this.use_demuxed_audio, - dummySubtitleEndpoint: this.dummySubtitleEndpoint, + const hlsOpts = { + sequenceAlwaysContainNewSegments: this.alwaysNewSegments, + forcedDemuxMode: this.use_demuxed_audio, + dummySubtitleEndpoint: this.dummySubtitleEndpoint, subtitleSliceEndpoint: this.subtitleSliceEndpoint, shouldContainSubtitles: this.use_vtt_subtitles, expectedSubtitleTracks: this._subtitleTracks From 0753ee00841c2d7336d26274b9e0969d70735b47 Mon Sep 17 00:00:00 2001 From: Johan Lautakoksi Date: Tue, 15 Aug 2023 01:07:06 +0200 Subject: [PATCH 04/17] console loges for debugging will be removed when done --- engine/session_live.js | 15 +++++++++++++++ engine/stream_switcher.js | 2 ++ 2 files changed, 17 insertions(+) diff --git a/engine/session_live.js b/engine/session_live.js index 4731ad15..cf1cdb07 100644 --- a/engine/session_live.js +++ b/engine/session_live.js @@ -302,9 +302,12 @@ class SessionLive { for (let segIdx = 0; segIdx < segments[bw].length; segIdx++) { const v2lSegment = segments[bw][segIdx]; if (v2lSegment.cue) { + console.log("cue video") if (v2lSegment.cue["in"]) { + console.log("cue in exists video") cueInExists = true; } else { + console.log("cue in does not exists video") cueInExists = false; } } @@ -315,11 +318,13 @@ class SessionLive { if (!segments[bw][endIdx].discontinuity) { const finalSegItem = { discontinuity: true }; if (!cueInExists) { + console.log(" adding cue in video") finalSegItem["cue"] = { in: true }; } this.vodSegments[bw].push(finalSegItem); } else { if (!cueInExists) { + console.log(" adding cue in video") segments[bw][endIdx]["cue"] = { in: true }; } } @@ -365,9 +370,12 @@ class SessionLive { for (let segIdx = 0; segIdx < segments[groupId][lang].length; segIdx++) { const v2lSegment = segments[groupId][lang][segIdx]; if (v2lSegment.cue) { + console.log("cue audio") if (v2lSegment.cue["in"]) { + console.log("cue in exists audio") cueInExists = true; } else { + console.log("cue in does not exists audio") cueInExists = false; } } @@ -378,11 +386,13 @@ class SessionLive { if (!segments[groupId][lang][endIdx].discontinuity) { const finalSegItem = { discontinuity: true }; if (!cueInExists) { + console.log(" adding cue in audio") finalSegItem["cue"] = { in: true }; } this.vodAudioSegments[groupId][lang].push(finalSegItem); } else { if (!cueInExists) { + console.log(" adding cue in audio") segments[groupId][lang][endIdx]["cue"] = { in: true }; } } @@ -515,6 +525,7 @@ class SessionLive { currentMediaSequenceSegments[liveTargetBandwidth] = []; // In case we switch back before we've depleted all transitional segments currentMediaSequenceSegments[liveTargetBandwidth] = this.vodSegments[vodTargetBandwidth].concat(this.liveSegQueue[liveTargetBandwidth]); + console.log("adding extra video") currentMediaSequenceSegments[liveTargetBandwidth].push({ discontinuity: true, cue: { in: true } }); debug(`[${this.sessionId}]: Getting current media segments for bw=${bw}`); } @@ -569,6 +580,7 @@ class SessionLive { currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = []; // In case we switch back before we've depleted all transitional segments currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].concat(this.liveAudioSegQueue[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage]); + console.log("adding extra audio") currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage].push({ discontinuity: true, cue: { in: true } }); debug(`[${this.sessionId}]: Getting current audio segments for ${groupId, langs[j]}`); } @@ -2063,6 +2075,7 @@ class SessionLive { if (!cueData) { cueData = {}; } + console.log("adding live segment to queue") cueData["in"] = true; } if ("cueout" in attributes) { @@ -2142,6 +2155,7 @@ class SessionLive { this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage].push({ discontinuity: true }); } if ("cuein" in attributes) { + console.log("adding live segment to queue") if (!cueData) { cueData = {}; } @@ -2421,6 +2435,7 @@ class SessionLive { m3u8 += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n"; } // Mimick logic used in hls-vodtolive + //console.log(segment, segment.cue, 2000) if (segment.cue && segment.cue.in) { m3u8 += "#EXT-X-CUE-IN" + "\n"; } diff --git a/engine/stream_switcher.js b/engine/stream_switcher.js index 5cf96f4e..66da803a 100644 --- a/engine/stream_switcher.js +++ b/engine/stream_switcher.js @@ -257,6 +257,8 @@ class StreamSwitcher { currVodAudioSegments = this._mergeAudioSegments(prerollAudioSegments, currVodAudioSegments, false); } } + console.log(currVodAudioSegments["aac"], "audio") + console.log(currVodSegments, "video") // In risk that the SL-playhead might have updated some data after // we reset last time... we should Reset SessionLive before sending new data. From d56e7ddb6f8c901495560dbf27175a64411b92f2 Mon Sep 17 00:00:00 2001 From: Johan Lautakoksi Date: Wed, 16 Aug 2023 00:04:00 +0200 Subject: [PATCH 05/17] fixed bug when merging preroll --- engine/stream_switcher.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/engine/stream_switcher.js b/engine/stream_switcher.js index 66da803a..c94a7af6 100644 --- a/engine/stream_switcher.js +++ b/engine/stream_switcher.js @@ -408,7 +408,7 @@ class StreamSwitcher { if (this.useDemuxedAudio) { const prerollAudioSegments = this.prerollsCache[this.sessionId].audioSegments; - eventAudioSegments = this._mergeSegments(prerollAudioSegments, eventAudioSegments, true); + eventAudioSegments = this._mergeAudioSegments(prerollAudioSegments, eventAudioSegments, true); } } await session.setCurrentMediaSequenceSegments(eventSegments, 0, true); @@ -824,18 +824,19 @@ class StreamSwitcher { const fromLangs = Object.keys(fromSegments[targetGroupId]); const targetLang = findAudioGroupOrLang(lang, fromLangs); if (prepend) { - OUTPUT_SEGMENTS[targetGroupId][targetLang] = fromSegments[targetGroupId][targetLang].concat(toSegments[targetGroupId][targetLang]); + OUTPUT_SEGMENTS[targetGroupId][targetLang] = fromSegments[groupId][lang].concat(toSegments[targetGroupId][targetLang]); OUTPUT_SEGMENTS[targetGroupId][targetLang].unshift({ discontinuity: true }); } else { - const lastSeg = toSegments[targetGroupId][targetLang][toSegments[targetGroupId][targetLang].length - 1]; + const size = toSegments[targetGroupId][targetLang].length; + const lastSeg = toSegments[targetGroupId][targetLang][size -1]; if (lastSeg.uri && !lastSeg.discontinuity) { toSegments[targetGroupId][targetLang].push({ discontinuity: true, cue: { in: true } }); - OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[targetGroupId]); + OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[groupId][lang]); } else if (lastSeg.discontinuity && !lastSeg.cue) { toSegments[targetGroupId][targetLang][toSegments[targetGroupId][targetLang].length - 1].cue = { in: true } - OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[targetGroupId][fromLangs]); + OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[groupId][lang]); } else { - OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[targetGroupId][fromLangs]); + OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[groupId][lang]); OUTPUT_SEGMENTS[targetGroupId][targetLang].push({ discontinuity: true }); } } From d816f1ccf779e0537425308e65ff5a6e74ff3aab Mon Sep 17 00:00:00 2001 From: Johan Lautakoksi Date: Wed, 16 Aug 2023 23:45:50 +0200 Subject: [PATCH 06/17] working on fixing issue with extra audio langs getting added and --- engine/session_live.js | 59 +++++++++++++++++++++++++-------------- engine/stream_switcher.js | 18 +++++++++--- 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/engine/session_live.js b/engine/session_live.js index cf1cdb07..c70f6cb3 100644 --- a/engine/session_live.js +++ b/engine/session_live.js @@ -302,12 +302,9 @@ class SessionLive { for (let segIdx = 0; segIdx < segments[bw].length; segIdx++) { const v2lSegment = segments[bw][segIdx]; if (v2lSegment.cue) { - console.log("cue video") if (v2lSegment.cue["in"]) { - console.log("cue in exists video") cueInExists = true; } else { - console.log("cue in does not exists video") cueInExists = false; } } @@ -318,13 +315,11 @@ class SessionLive { if (!segments[bw][endIdx].discontinuity) { const finalSegItem = { discontinuity: true }; if (!cueInExists) { - console.log(" adding cue in video") finalSegItem["cue"] = { in: true }; } this.vodSegments[bw].push(finalSegItem); } else { if (!cueInExists) { - console.log(" adding cue in video") segments[bw][endIdx]["cue"] = { in: true }; } } @@ -332,7 +327,6 @@ class SessionLive { } else { debug(`[${this.sessionId}]: 'vodSegments' not empty = Using 'transitSegs'`); } - debug(`[${this.sessionId}]: Setting CurrentMediaSequenceSegments. First seg is: [${this.vodSegments[Object.keys(this.vodSegments)[0]][0].uri}]`); const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); @@ -370,12 +364,9 @@ class SessionLive { for (let segIdx = 0; segIdx < segments[groupId][lang].length; segIdx++) { const v2lSegment = segments[groupId][lang][segIdx]; if (v2lSegment.cue) { - console.log("cue audio") if (v2lSegment.cue["in"]) { - console.log("cue in exists audio") cueInExists = true; } else { - console.log("cue in does not exists audio") cueInExists = false; } } @@ -386,13 +377,11 @@ class SessionLive { if (!segments[groupId][lang][endIdx].discontinuity) { const finalSegItem = { discontinuity: true }; if (!cueInExists) { - console.log(" adding cue in audio") finalSegItem["cue"] = { in: true }; } this.vodAudioSegments[groupId][lang].push(finalSegItem); } else { if (!cueInExists) { - console.log(" adding cue in audio") segments[groupId][lang][endIdx]["cue"] = { in: true }; } } @@ -525,7 +514,6 @@ class SessionLive { currentMediaSequenceSegments[liveTargetBandwidth] = []; // In case we switch back before we've depleted all transitional segments currentMediaSequenceSegments[liveTargetBandwidth] = this.vodSegments[vodTargetBandwidth].concat(this.liveSegQueue[liveTargetBandwidth]); - console.log("adding extra video") currentMediaSequenceSegments[liveTargetBandwidth].push({ discontinuity: true, cue: { in: true } }); debug(`[${this.sessionId}]: Getting current media segments for bw=${bw}`); } @@ -580,7 +568,36 @@ class SessionLive { currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = []; // In case we switch back before we've depleted all transitional segments currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].concat(this.liveAudioSegQueue[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage]); - console.log("adding extra audio") + currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage].push({ discontinuity: true, cue: { in: true } }); + debug(`[${this.sessionId}]: Getting current audio segments for ${groupId, langs[j]}`); + } + } + + const groupIdsVod = Object.keys(this.vodAudioSegments); + for (let i = 0; i < groupIdsVod.length; i++) { + let groupId = groupIdsVod[i]; + if (!currentAudioSequenceSegments[groupId]) { + currentAudioSequenceSegments[groupId] = {}; + } + let langs = Object.keys(this.vodAudioSegments[groupIdsVod[i]]); + for (let j = 0; j < langs.length; j++) { + if (currentAudioSequenceSegments[groupId][langs[j]].length > 0) { + continue; + } + const liveTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.audioManifestURIs); + const vodTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.vodAudioSegments); + if (!vodTargetGroupLang.audioGroupId || !vodTargetGroupLang.audioLanguage) { + return null; + } + // Remove segments and disc-tag if they are on top + if (this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].length > 0 && this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage][0].discontinuity) { + this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].shift(); + increment = 1; + } + segmentCount = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].length; + currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = []; + // In case we switch back before we've depleted all transitional segments + currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].concat(this.liveAudioSegQueue[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage]); currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage].push({ discontinuity: true, cue: { in: true } }); debug(`[${this.sessionId}]: Getting current audio segments for ${groupId, langs[j]}`); } @@ -1585,7 +1602,7 @@ class SessionLive { } this.firstTimeAudio = false; - debug(`[${this.sessionId}]: Got all needed segments from live-source (from all bandwidths).\nWe are now able to build Audi Live Manifest: [${this.audioSeqCount}]`); + debug(`[${this.sessionId}]: Got all needed segments from live-source (from all groupIds and langs).\nWe are now able to build Audi Live Manifest: [${this.audioSeqCount}]`); return; } @@ -2075,7 +2092,6 @@ class SessionLive { if (!cueData) { cueData = {}; } - console.log("adding live segment to queue") cueData["in"] = true; } if ("cueout" in attributes) { @@ -2155,7 +2171,6 @@ class SessionLive { this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage].push({ discontinuity: true }); } if ("cuein" in attributes) { - console.log("adding live segment to queue") if (!cueData) { cueData = {}; } @@ -2435,7 +2450,6 @@ class SessionLive { m3u8 += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n"; } // Mimick logic used in hls-vodtolive - //console.log(segment, segment.cue, 2000) if (segment.cue && segment.cue.in) { m3u8 += "#EXT-X-CUE-IN" + "\n"; } @@ -2585,14 +2599,17 @@ class SessionLive { } _filterLiveAudioTracks() { - let audioTracks = this.sessionAudioTracks; const toKeep = new Set(); let newItemsAudio = {}; - audioTracks.forEach((audioTrack) => { - let groupAndLangToKeep = this._findAudioGroupsForLang(audioTrack.language, this.audioManifestURIs); + const groupIds = Object.keys(this.vodAudioSegments) + for(let i = 0; i < groupIds.length; i++) { + const langs = Object.keys(this.vodAudioSegments[groupIds[j]]) + for(let idx = 0; idx < langs.length; idx++) { + let groupAndLangToKeep = this._findAudioGroupsForLang(audioTrack.language, this.audioManifestURIs); toKeep.add(...groupAndLangToKeep); - }); + } + } toKeep.forEach((trackInfo) => { if (!newItemsAudio[trackInfo.audioGroupId]) { newItemsAudio[trackInfo.audioGroupId] = {} diff --git a/engine/stream_switcher.js b/engine/stream_switcher.js index c94a7af6..cbbf301f 100644 --- a/engine/stream_switcher.js +++ b/engine/stream_switcher.js @@ -257,8 +257,7 @@ class StreamSwitcher { currVodAudioSegments = this._mergeAudioSegments(prerollAudioSegments, currVodAudioSegments, false); } } - console.log(currVodAudioSegments["aac"], "audio") - console.log(currVodSegments, "video") + console.log(currVodAudioSegments, 200) // In risk that the SL-playhead might have updated some data after // we reset last time... we should Reset SessionLive before sending new data. @@ -332,21 +331,26 @@ class StreamSwitcher { try { debug(`[${this.sessionId}]: [ INIT Switching from LIVE->V2L ]`); this.eventId = null; + console.log("hej", 1) liveSegments = await sessionLive.getCurrentMediaSequenceSegments(); if (this.useDemuxedAudio) { liveAudioSegments = await sessionLive.getCurrentAudioSequenceSegments(); } + console.log("hej", 2) liveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount(); if (scheduleObj && !scheduleObj.duration) { debug(`[${this.sessionId}]: Cannot switch VOD. No duration specified for schedule item: [${scheduleObj.assetId}]`); } - + console.log("hej", 3) if (this._isEmpty(liveSegments.currMseqSegs) || (this.useDemuxedAudio && this._isEmpty(liveAudioSegments.currMseqSegs))) { this.working = false; this.streamTypeLive = false; debug(`[${this.sessionId}]: [ Switched from LIVE->V2L ]`); return false; } + console.log("hej", 4) + console.log(liveAudioSegments) + // Insert preroll, if available, for current channel if (this.prerollsCache[this.sessionId]) { const prerollSegments = this.prerollsCache[this.sessionId].segments; @@ -356,13 +360,20 @@ class StreamSwitcher { const prerollAudioSegments = this.prerollsCache[this.sessionId].audioSegments; liveAudioSegments.currMseqSegs = this._mergeAudioSegments(prerollAudioSegments, liveAudioSegments.currMseqSegs, false); liveAudioSegments.segCount += prerollAudioSegments.length; + console.log(prerollAudioSegments) } } + + console.log("hej", 5) await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq, liveCounts.discSeq, liveCounts.audioSeq, liveCounts.audioDiscSeq); + console.log("hej", 5.2) await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount); + console.log("hej", 5.5) if (this.useDemuxedAudio) { await session.setCurrentAudioSequenceSegments(liveAudioSegments.currMseqSegs, liveAudioSegments.segCount) + console.log("hej", 5.7) } + console.log("hej", 6) await sessionLive.resetSession(); sessionLive.resetLiveStoreAsync(RESET_DELAY); // In parallel this.working = false; @@ -384,7 +395,6 @@ class StreamSwitcher { liveSegments = await sessionLive.getCurrentMediaSequenceSegments(); liveAudioSegments = await sessionLive.getCurrentAudioSequenceSegments(); liveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount(); - eventSegments = await session.getTruncatedVodSegments(scheduleObj.uri, scheduleObj.duration / 1000); eventAudioSegments = await session.getTruncatedVodAudioSegments(scheduleObj.uri, scheduleObj.duration / 1000); From 4a0d7b986d72da31bdbea30d8f80ad1373a854a4 Mon Sep 17 00:00:00 2001 From: Johan Lautakoksi Date: Thu, 17 Aug 2023 22:07:02 +0200 Subject: [PATCH 07/17] fix video freeze and long reloads --- engine/session.js | 76 +---- engine/session_live.js | 55 +--- engine/stream_switcher.js | 51 ++-- spec/engine/stream_switcher_spec.js | 449 ++++++++++++++++------------ 4 files changed, 320 insertions(+), 311 deletions(-) diff --git a/engine/session.js b/engine/session.js index 3dd4ed93..6a8a7ac3 100644 --- a/engine/session.js +++ b/engine/session.js @@ -367,7 +367,7 @@ class Session { } } - async setCurrentMediaSequenceSegments(segments, mSeqOffset, reloadBehind) { + async setCurrentMediaSequenceSegments(segments, mSeqOffset, reloadBehind, audioSegments, aSeqOffset) { if (!this._sessionState) { throw new Error("Session not ready"); } @@ -376,6 +376,8 @@ class Session { this.switchDataForSession.reloadBehind = reloadBehind; this.switchDataForSession.transitionSegments = segments; this.switchDataForSession.mediaSeqOffset = mSeqOffset; + this.switchDataForSession.transitionAudioSegments = audioSegments; + this.switchDataForSession.audioSeqOffset = aSeqOffset; let waitTimeMs = 2000; for (let i = segments[Object.keys(segments)[0]].length - 1; 0 < i; i--) { @@ -385,68 +387,20 @@ class Session { } } - let isLeader = await this._sessionStateStore.isLeader(this._instanceId); - if (!isLeader) { - debug(`[${this._sessionId}]: FOLLOWER: Invalidate cache to ensure having the correct VOD!`); - await this._sessionState.clearCurrentVodCache(); - - let vodReloaded = await this._sessionState.get("vodReloaded"); - let attempts = 9; - while (!isLeader && !vodReloaded && attempts > 0) { - debug(`[${this._sessionId}]: FOLLOWER: I arrived before LEADER. Waiting (1000ms) for LEADER to reload currentVod in store! (tries left=${attempts})`); - await timer(1000); - await this._sessionStateStore.clearLeaderCache(); - isLeader = await this._sessionStateStore.isLeader(this._instanceId); - vodReloaded = await this._sessionState.get("vodReloaded"); - attempts--; - } - - if (attempts === 0) { - debug(`[${this._sessionId}]: FOLLOWER: WARNING! Attempts=0 - Risk of using wrong currentVod`); - } - if (!isLeader || vodReloaded) { - debug(`[${this._sessionId}]: FOLLOWER: leader is alive, and has presumably updated currentVod. Clearing the cache now`); - await this._sessionState.clearCurrentVodCache(); - return; - } - debug(`[${this._sessionId}]: NEW LEADER: Setting state=VOD_RELOAD_INIT`); - this.isSwitchingBackToV2L = true; - await this._sessionState.set("state", SessionState.VOD_RELOAD_INIT); - - } else { - let vodReloaded = await this._sessionState.get("vodReloaded"); - let attempts = 12; - while (!vodReloaded && attempts > 0) { - debug(`[${this._sessionId}]: LEADER: Waiting (${waitTimeMs}ms) to buy some time reloading vod and adding it to store! (tries left=${attempts})`); - await timer(waitTimeMs); - vodReloaded = await this._sessionState.get("vodReloaded"); - attempts--; - } - if (attempts === 0) { - debug(`[${this._sessionId}]: LEADER: WARNING! Vod was never Reloaded!`); - return; + if (this.use_demuxed_audio && audioSegments) { + let waitTimeMsAudio = 2000; + let groupId = Object.keys(audioSegments)[0]; + let lang = Object.keys(audioSegments[groupId])[0] + for (let i = audioSegments[groupId][lang].length - 1; 0 < i; i--) { + const segment = audioSegments[groupId][lang][i]; + if (segment.duration) { + waitTimeMsAudio = parseInt(1000 * (segment.duration / 3), 10); + break; + } } + waitTimeMs = waitTimeMs > waitTimeMsAudio ? waitTimeMs : waitTimeMsAudio; } - } - async setCurrentAudioSequenceSegments(segments, aSeqOffset, reloadBehind) { - if (!this._sessionState) { - throw new Error("Session not ready"); - } - this.isSwitchingBackToV2L = true; - - this.switchDataForSession.transitionAudioSegments = segments; - this.switchDataForSession.audioSeqOffset = aSeqOffset; - let waitTimeMs = 2000; - let groupId = Object.keys(segments)[0]; - let lang = Object.keys(segments[groupId])[0] - for (let i = segments[groupId][lang].length - 1; 0 < i; i--) { - const segment = segments[groupId][lang][i]; - if (segment.duration) { - waitTimeMs = parseInt(1000 * (segment.duration / 3), 10); - break; - } - } let isLeader = await this._sessionStateStore.isLeader(this._instanceId); if (!isLeader) { debug(`[${this._sessionId}]: FOLLOWER: Invalidate cache to ensure having the correct VOD!`); @@ -1842,7 +1796,9 @@ class Session { // TODO: Support reloading with SubtitleSegments as well | // ---------------------------------------------------' + console.log("br") await currentVod.reload(nextMseq, segments, audioSegments, reloadBehind); + console.log("ar") await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 }); await this._sessionState.set("vodReloaded", 1); await this._sessionState.set("vodMediaSeqVideo", 0); diff --git a/engine/session_live.js b/engine/session_live.js index c70f6cb3..57b73c9c 100644 --- a/engine/session_live.js +++ b/engine/session_live.js @@ -373,7 +373,7 @@ class SessionLive { this.vodAudioSegments[groupId][lang].push(v2lSegment); } - const endIdx = segments[groupId][langs].length - 1; + const endIdx = segments[groupId][lang].length - 1; if (!segments[groupId][lang][endIdx].discontinuity) { const finalSegItem = { discontinuity: true }; if (!cueInExists) { @@ -390,7 +390,7 @@ class SessionLive { } else { debug(`[${this.sessionId}]: 'vodAudioSegments' not empty = Using 'transitSegs'`); } - debug(`[${this.sessionId}]: Setting CurrentAudioSequenceSegments. First seg is: [${this.vodAudioSegments[Object.keys(this.vodAudioSegments)[0]][Object.keys(this.vodAudioSegments[Object.keys(this.vodAudioSegments)[0]])][0].uri}]`); + debug(`[${this.sessionId}]: Setting CurrentAudioSequenceSegments. First seg is: [${this.vodAudioSegments[Object.keys(this.vodAudioSegments)[0]][Object.keys(this.vodAudioSegments[Object.keys(this.vodAudioSegments)[0]])[0]][0].uri}]`); const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); if (isLeader) { @@ -546,13 +546,13 @@ class SessionLive { let currentAudioSequenceSegments = {}; let segmentCount = 0; let increment = 0; - const groupIds = Object.keys(this.audioManifestURIs); + const groupIds = Object.keys(this.vodAudioSegments); for (let i = 0; i < groupIds.length; i++) { let groupId = groupIds[i]; if (!currentAudioSequenceSegments[groupId]) { currentAudioSequenceSegments[groupId] = {}; } - let langs = Object.keys(this.audioManifestURIs[groupIds[i]]); + let langs = Object.keys(this.vodAudioSegments[groupIds[i]]); for (let j = 0; j < langs.length; j++) { const liveTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.audioManifestURIs); const vodTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.vodAudioSegments); @@ -573,36 +573,6 @@ class SessionLive { } } - const groupIdsVod = Object.keys(this.vodAudioSegments); - for (let i = 0; i < groupIdsVod.length; i++) { - let groupId = groupIdsVod[i]; - if (!currentAudioSequenceSegments[groupId]) { - currentAudioSequenceSegments[groupId] = {}; - } - let langs = Object.keys(this.vodAudioSegments[groupIdsVod[i]]); - for (let j = 0; j < langs.length; j++) { - if (currentAudioSequenceSegments[groupId][langs[j]].length > 0) { - continue; - } - const liveTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.audioManifestURIs); - const vodTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.vodAudioSegments); - if (!vodTargetGroupLang.audioGroupId || !vodTargetGroupLang.audioLanguage) { - return null; - } - // Remove segments and disc-tag if they are on top - if (this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].length > 0 && this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage][0].discontinuity) { - this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].shift(); - increment = 1; - } - segmentCount = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].length; - currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = []; - // In case we switch back before we've depleted all transitional segments - currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].concat(this.liveAudioSegQueue[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage]); - currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage].push({ discontinuity: true, cue: { in: true } }); - debug(`[${this.sessionId}]: Getting current audio segments for ${groupId, langs[j]}`); - } - } - this.audioDiscSeqCount += increment; return { currMseqSegs: currentAudioSequenceSegments, @@ -746,6 +716,7 @@ class SessionLive { this.mediaManifestURIs[streamItemBW] = ""; } this.mediaManifestURIs[streamItemBW] = mediaManifestUri; + if (streamItem.get("audio") && this.useDemuxedAudio) { let audioGroupId = streamItem.get("audio") let audioGroupItems = m3u.items.MediaItem.filter((item) => { @@ -766,9 +737,9 @@ class SessionLive { itemLang = item.get("language"); } if (!this.audioManifestURIs[audioGroupId][itemLang]) { - this.audioManifestURIs[audioGroupId][itemLang] = "ehj" + this.audioManifestURIs[audioGroupId][itemLang] = "" } - const audioManifestUri = url.resolve(baseUrl, streamItem.get("uri")) + const audioManifestUri = url.resolve(baseUrl, item.get("uri")) this.audioManifestURIs[audioGroupId][itemLang] = audioManifestUri; }); } @@ -2599,17 +2570,15 @@ class SessionLive { } _filterLiveAudioTracks() { + let audioTracks = this.sessionAudioTracks; const toKeep = new Set(); let newItemsAudio = {}; - const groupIds = Object.keys(this.vodAudioSegments) - for(let i = 0; i < groupIds.length; i++) { - const langs = Object.keys(this.vodAudioSegments[groupIds[j]]) - for(let idx = 0; idx < langs.length; idx++) { - let groupAndLangToKeep = this._findAudioGroupsForLang(audioTrack.language, this.audioManifestURIs); + audioTracks.forEach((audioTrack) => { + let groupAndLangToKeep = this._findAudioGroupsForLang(audioTrack.language, this.audioManifestURIs); toKeep.add(...groupAndLangToKeep); - } - } + }); + toKeep.forEach((trackInfo) => { if (!newItemsAudio[trackInfo.audioGroupId]) { newItemsAudio[trackInfo.audioGroupId] = {} diff --git a/engine/stream_switcher.js b/engine/stream_switcher.js index cbbf301f..22ca40a2 100644 --- a/engine/stream_switcher.js +++ b/engine/stream_switcher.js @@ -314,8 +314,7 @@ class StreamSwitcher { } await session.setCurrentMediaAndDiscSequenceCount(currVodCounts.mediaSeq, currVodCounts.discSeq, currVodCounts.audioSeq, currVodCounts.audioDiscSeq); - await session.setCurrentMediaSequenceSegments(eventSegments, 0, true); - await session.setCurrentAudioSequenceSegments(eventAudioSegments, 0, true); + await session.setCurrentMediaSequenceSegments(eventSegments, 0, true, eventAudioSegments, 0); this.working = false; debug(`[${this.sessionId}]: [ Switched from V2L->VOD ]`); @@ -350,7 +349,7 @@ class StreamSwitcher { } console.log("hej", 4) console.log(liveAudioSegments) - + // Insert preroll, if available, for current channel if (this.prerollsCache[this.sessionId]) { const prerollSegments = this.prerollsCache[this.sessionId].segments; @@ -360,19 +359,18 @@ class StreamSwitcher { const prerollAudioSegments = this.prerollsCache[this.sessionId].audioSegments; liveAudioSegments.currMseqSegs = this._mergeAudioSegments(prerollAudioSegments, liveAudioSegments.currMseqSegs, false); liveAudioSegments.segCount += prerollAudioSegments.length; - console.log(prerollAudioSegments) } } - + console.log("hej", 5) await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq, liveCounts.discSeq, liveCounts.audioSeq, liveCounts.audioDiscSeq); console.log("hej", 5.2) - await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount); - console.log("hej", 5.5) if (this.useDemuxedAudio) { - await session.setCurrentAudioSequenceSegments(liveAudioSegments.currMseqSegs, liveAudioSegments.segCount) - console.log("hej", 5.7) + await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount, false, liveAudioSegments.currMseqSegs, liveAudioSegments.segCount); + } else { + await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount, false); } + console.log("hej", 6) await sessionLive.resetSession(); sessionLive.resetLiveStoreAsync(RESET_DELAY); // In parallel @@ -408,8 +406,11 @@ class StreamSwitcher { } await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq - 1, liveCounts.discSeq - 1, liveCounts.audioSeq - 1, liveCounts.audioDiscSeq - 1); - await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount); - await session.setCurrentAudioSequenceSegments(liveAudioSegments.currMseqSegs, liveAudioSegments.segCount); + if (this.useDemuxedAudio) { + await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount, false, liveAudioSegments.currMseqSegs, liveAudioSegments.segCount); + } else { + await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount); + } // Insert preroll, if available, for current channel if (this.prerollsCache[this.sessionId]) { @@ -818,14 +819,16 @@ class StreamSwitcher { const OUTPUT_SEGMENTS = {}; const fromGroups = Object.keys(fromSegments); const toGroups = Object.keys(toSegments); + for (let i = 0; i < toGroups.length; i++) { const groupId = toGroups[i]; if (!OUTPUT_SEGMENTS[groupId]) { OUTPUT_SEGMENTS[groupId] = {} } - const langs = Object.keys(toSegments[groupId]) - for (let j = 0; j < langs.length; j++) { - const lang = langs[j]; + const toLangs = Object.keys(toSegments[groupId]) + + for (let j = 0; j < toLangs.length; j++) { + const lang = toLangs[j]; if (!OUTPUT_SEGMENTS[groupId][lang]) { OUTPUT_SEGMENTS[groupId][lang] = []; } @@ -834,20 +837,20 @@ class StreamSwitcher { const fromLangs = Object.keys(fromSegments[targetGroupId]); const targetLang = findAudioGroupOrLang(lang, fromLangs); if (prepend) { - OUTPUT_SEGMENTS[targetGroupId][targetLang] = fromSegments[groupId][lang].concat(toSegments[targetGroupId][targetLang]); - OUTPUT_SEGMENTS[targetGroupId][targetLang].unshift({ discontinuity: true }); + OUTPUT_SEGMENTS[groupId][lang] = fromSegments[targetGroupId][targetLang].concat(toSegments[groupId][lang]); + OUTPUT_SEGMENTS[groupId][lang].unshift({ discontinuity: true }); } else { - const size = toSegments[targetGroupId][targetLang].length; - const lastSeg = toSegments[targetGroupId][targetLang][size -1]; + const size = toSegments[groupId][lang].length; + const lastSeg = toSegments[groupId][lang][size - 1]; if (lastSeg.uri && !lastSeg.discontinuity) { - toSegments[targetGroupId][targetLang].push({ discontinuity: true, cue: { in: true } }); - OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[groupId][lang]); + toSegments[groupId][lang].push({ discontinuity: true, cue: { in: true } }); + OUTPUT_SEGMENTS[groupId][lang] = toSegments[groupId][lang].concat(fromSegments[targetGroupId][targetLang]); } else if (lastSeg.discontinuity && !lastSeg.cue) { - toSegments[targetGroupId][targetLang][toSegments[targetGroupId][targetLang].length - 1].cue = { in: true } - OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[groupId][lang]); + toSegments[targetGroupId][lang][toSegments[groupId][lang].length - 1].cue = { in: true } + OUTPUT_SEGMENTS[groupId][lang] = toSegments[groupId][lang].concat(fromSegments[targetGroupId][targetLang]); } else { - OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[groupId][lang]); - OUTPUT_SEGMENTS[targetGroupId][targetLang].push({ discontinuity: true }); + OUTPUT_SEGMENTS[groupId][lang] = toSegments[groupId][lang].concat(fromSegments[targetGroupId][targetLang]); + OUTPUT_SEGMENTS[groupId][lang].push({ discontinuity: true }); } } } diff --git a/spec/engine/stream_switcher_spec.js b/spec/engine/stream_switcher_spec.js index 4a7a3379..f052a991 100644 --- a/spec/engine/stream_switcher_spec.js +++ b/spec/engine/stream_switcher_spec.js @@ -12,37 +12,37 @@ const StreamType = Object.freeze({ const tsNow = Date.now(); class TestAssetManager { - constructor(opts, assets) { - this.assets = [ - { id: 1, title: "Tears of Steel", uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/master.m3u8" }, - { id: 2, title: "VINN", uri: "https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8" } - ]; - if (assets) { - this.assets = assets; - } - this.pos = 0; - this.doFail = false; - if (opts && opts.fail) { - this.doFail = true; - } - if (opts && opts.failOnIndex) { - this.failOnIndex = 1; - } + constructor(opts, assets) { + this.assets = [ + { id: 1, title: "Tears of Steel", uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/master.m3u8" }, + { id: 2, title: "VINN", uri: "https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8" } + ]; + if (assets) { + this.assets = assets; + } + this.pos = 0; + this.doFail = false; + if (opts && opts.fail) { + this.doFail = true; + } + if (opts && opts.failOnIndex) { + this.failOnIndex = 1; } + } - getNextVod(vodRequest) { - return new Promise((resolve, reject) => { - if (this.doFail || this.pos === this.failOnIndex) { - reject("should fail"); - } else { - const vod = this.assets[this.pos++]; - if (this.pos > this.assets.length - 1) { - this.pos = 0; - } - resolve(vod); + getNextVod(vodRequest) { + return new Promise((resolve, reject) => { + if (this.doFail || this.pos === this.failOnIndex) { + reject("should fail"); + } else { + const vod = this.assets[this.pos++]; + if (this.pos > this.assets.length - 1) { + this.pos = 0; } - }); - } + resolve(vod); + } + }); + } } const allListSchedules = [ @@ -64,7 +64,7 @@ const allListSchedules = [ start_time: tsNow + 20 * 1000, end_time: tsNow + 20 * 1000 + 1 * 60 * 1000, uri: "https://maitv-vod.lab.eyevinn.technology/MORBIUS_Trailer_2020.mp4/master.m3u8", - duration: 60*1000, + duration: 60 * 1000, } ], [ @@ -83,7 +83,7 @@ const allListSchedules = [ start_time: tsNow + 20 * 1000 + 1 * 60 * 1000, end_time: tsNow + 20 * 1000 + 1 * 60 * 1000 + 60 * 1000, uri: "https://maitv-vod.lab.eyevinn.technology/MORBIUS_Trailer_2020.mp4/master.m3u8", - duration: 60*1000, + duration: 60 * 1000, } ], [ @@ -112,7 +112,7 @@ const allListSchedules = [ start_time: tsNow + 20 * 1000, end_time: tsNow + 20 * 1000 + 1 * 60 * 1000, uri: "https://maitv-vod.lab.eyevinn.technology/MORBIUS_Trailer_2020.mp4/master.m3u8", - duration: 60*1000, + duration: 60 * 1000, }, { eventId: "live-4", @@ -131,7 +131,7 @@ const allListSchedules = [ start_time: tsNow + 20 * 1000, end_time: tsNow + 20 * 1000 + 1 * 60 * 1000, uri: "https://maitv-vod.lab.eyevinn.technology/MORBIUS_Trailer_2020.mp4/master.m3u8", - duration: 60*1000, + duration: 60 * 1000, }, { eventId: "vod-4", @@ -140,7 +140,7 @@ const allListSchedules = [ start_time: tsNow + 20 * 1000 + 1 * 60 * 1000, end_time: tsNow + 20 * 1000 + 1 * 60 * 1000 + 60 * 1000, uri: "https://maitv-vod.lab.eyevinn.technology/MORBIUS_Trailer_2020.mp4/master.m3u8", - duration: 60*1000, + duration: 60 * 1000, }, ], [ @@ -168,33 +168,33 @@ class TestSwitchManager { } const mockLiveSegments = { - "180000": [{duration: 7,uri: "http://mock.mock.com/180000/seg09.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg10.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg11.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg12.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg13.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg14.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg15.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg16.ts"}, - {discontinuity: true }], - "1258000": [{duration: 7,uri: "http://mock.mock.com/1258000/seg09.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg10.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg11.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg12.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg13.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg14.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg15.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg16.ts"}, - {discontinuity: true }], - "2488000": [{duration: 7,uri: "http://mock.mock.com/2488000/seg09.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg10.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg11.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg12.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg13.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg14.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg15.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg16.ts"}, - {discontinuity: true }] + "180000": [{ duration: 7, uri: "http://mock.mock.com/180000/seg09.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg10.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg11.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg12.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg13.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg14.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg15.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg16.ts" }, + { discontinuity: true }], + "1258000": [{ duration: 7, uri: "http://mock.mock.com/1258000/seg09.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg10.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg11.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg12.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg13.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg14.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg15.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg16.ts" }, + { discontinuity: true }], + "2488000": [{ duration: 7, uri: "http://mock.mock.com/2488000/seg09.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg10.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg11.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg12.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg13.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg14.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg15.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg16.ts" }, + { discontinuity: true }] }; describe("The Stream Switcher", () => { @@ -220,8 +220,8 @@ describe("The Stream Switcher", () => { it("should return false if no StreamSwitchManager was given.", async () => { const assetMgr = new TestAssetManager(); const testStreamSwitcher = new StreamSwitcher(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); await session.initAsync(); await session.incrementAsync(); @@ -233,12 +233,12 @@ describe("The Stream Switcher", () => { it("should validate uri and switch back to linear-vod (session) from event-livestream (sessionLive) if uri is unreachable", async () => { const switchMgr = new TestSwitchManager(0); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "resetLiveStoreAsync").and.callFake( () => true ); - spyOn(sessionLive, "resetSession").and.callFake( () => true ); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "resetLiveStoreAsync").and.callFake(() => true); + spyOn(sessionLive, "resetSession").and.callFake(() => true); spyOn(sessionLive, "getCurrentMediaSequenceSegments").and.returnValue({ currMseqSegs: mockLiveSegments, segCount: 8 }); spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); @@ -248,7 +248,7 @@ describe("The Stream Switcher", () => { await sessionLive.initAsync(); sessionLive.startPlayheadAsync(); - + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); expect(testStreamSwitcher.getEventId()).toBe(null); jasmine.clock().mockDate(tsNow); @@ -271,12 +271,12 @@ describe("The Stream Switcher", () => { it("should switch from linear-vod (session) to event-livestream (sessionLive) according to schedule", async () => { const switchMgr = new TestSwitchManager(0); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "resetLiveStoreAsync").and.callFake( () => true ); - spyOn(sessionLive, "resetSession").and.callFake( () => true ); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "resetLiveStoreAsync").and.callFake(() => true); + spyOn(sessionLive, "resetSession").and.callFake(() => true); spyOn(sessionLive, "getCurrentMediaSequenceSegments").and.returnValue({ currMseqSegs: mockLiveSegments, segCount: 8 }); await session.initAsync(); @@ -294,12 +294,12 @@ describe("The Stream Switcher", () => { it("should switch from event-livestream (sessionLive) to linear-vod (session) according to schedule", async () => { const switchMgr = new TestSwitchManager(0); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "resetLiveStoreAsync").and.callFake( () => true ); - spyOn(sessionLive, "resetSession").and.callFake( () => true ); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "resetLiveStoreAsync").and.callFake(() => true); + spyOn(sessionLive, "resetSession").and.callFake(() => true); spyOn(sessionLive, "getCurrentMediaSequenceSegments").and.returnValue({ currMseqSegs: mockLiveSegments, segCount: 8 }); spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); @@ -320,9 +320,9 @@ describe("The Stream Switcher", () => { it("should switch from linear-vod (session) to event-vod (session) according to schedule", async () => { const switchMgr = new TestSwitchManager(1); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); @@ -340,9 +340,9 @@ describe("The Stream Switcher", () => { it("should switch from event-vod (session) to linear-vod (session) according to schedule", async () => { const switchMgr = new TestSwitchManager(1); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); @@ -360,9 +360,9 @@ describe("The Stream Switcher", () => { it("should not switch from linear-vod (session) to event-vod (session) if duration is not set in schedule", async () => { const switchMgr = new TestSwitchManager(6); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); await session.initAsync(); await session.incrementAsync(); @@ -374,113 +374,194 @@ describe("The Stream Switcher", () => { }); xit("should switch from linear-vod (session) to event-livestream (sessionLive) according to schedule. " + - "\nThen directly to event-vod (session) and finally switch back to linear-vod (session)", async () => { - const switchMgr = new TestSwitchManager(2); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); - const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "_loadAllMediaManifests").and.returnValue(mockLiveSegments); - - await session.initAsync(); - await session.incrementAsync(); - sessionLive.initAsync(); - - jasmine.clock().mockDate(tsNow); - jasmine.clock().tick((25 * 1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); - expect(testStreamSwitcher.getEventId()).toBe("live-1"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); - await sessionLive.getCurrentMediaManifestAsync(180000); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe("vod-1"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60*1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe(null); - }); + "\nThen directly to event-vod (session) and finally switch back to linear-vod (session)", async () => { + const switchMgr = new TestSwitchManager(2); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); + const assetMgr = new TestAssetManager(); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "_loadAllMediaManifests").and.returnValue(mockLiveSegments); + + await session.initAsync(); + await session.incrementAsync(); + sessionLive.initAsync(); + + jasmine.clock().mockDate(tsNow); + jasmine.clock().tick((25 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); + expect(testStreamSwitcher.getEventId()).toBe("live-1"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); + await sessionLive.getCurrentMediaManifestAsync(180000); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe("vod-1"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe(null); + }); xit("should switch from linear-vod (session) to event-livestream (sessionLive) according to schedule. " + - "\nThen directly to 2nd event-livestream (sessionLive) and finally switch back to linear-vod (session)", async () => { - const switchMgr = new TestSwitchManager(3); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); - const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "_loadAllMediaManifests").and.returnValue(mockLiveSegments); - - await session.initAsync(); - await session.incrementAsync(); - await sessionLive.initAsync(); - - jasmine.clock().mockDate(tsNow); - jasmine.clock().tick((25 * 1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); - expect(testStreamSwitcher.getEventId()).toBe("live-2"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); - await sessionLive.getCurrentMediaManifestAsync(180000); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); - expect(testStreamSwitcher.getEventId()).toBe("live-3"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60*1000)); - await sessionLive.getCurrentMediaManifestAsync(180000); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe(null); - }); + "\nThen directly to 2nd event-livestream (sessionLive) and finally switch back to linear-vod (session)", async () => { + const switchMgr = new TestSwitchManager(3); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); + const assetMgr = new TestAssetManager(); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "_loadAllMediaManifests").and.returnValue(mockLiveSegments); + + await session.initAsync(); + await session.incrementAsync(); + await sessionLive.initAsync(); + + jasmine.clock().mockDate(tsNow); + jasmine.clock().tick((25 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); + expect(testStreamSwitcher.getEventId()).toBe("live-2"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); + await sessionLive.getCurrentMediaManifestAsync(180000); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); + expect(testStreamSwitcher.getEventId()).toBe("live-3"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60 * 1000)); + await sessionLive.getCurrentMediaManifestAsync(180000); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe(null); + }); it("should switch from linear-vod (session) to event-vod (session) according to schedule. " + - "\nThen directly to event-livestream (sessionLive) and finally switch back to linear-vod (session)", async () => { - const switchMgr = new TestSwitchManager(4); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); - const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "resetLiveStoreAsync").and.callFake( () => true ); - spyOn(sessionLive, "resetSession").and.callFake( () => true ); - spyOn(sessionLive, "getCurrentMediaSequenceSegments").and.returnValue({ currMseqSegs: mockLiveSegments, segCount: 8 }); - spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); - spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); - - await session.initAsync(); - await session.incrementAsync(); - await sessionLive.initAsync(); - - sessionLive.startPlayheadAsync(); - - jasmine.clock().mockDate(tsNow); - jasmine.clock().tick((25 * 1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe("vod-2"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); - expect(testStreamSwitcher.getEventId()).toBe("live-4"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60*1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe(null); - }); + "\nThen directly to event-livestream (sessionLive) and finally switch back to linear-vod (session)", async () => { + const switchMgr = new TestSwitchManager(4); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); + const assetMgr = new TestAssetManager(); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "resetLiveStoreAsync").and.callFake(() => true); + spyOn(sessionLive, "resetSession").and.callFake(() => true); + spyOn(sessionLive, "getCurrentMediaSequenceSegments").and.returnValue({ currMseqSegs: mockLiveSegments, segCount: 8 }); + spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); + spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); + + await session.initAsync(); + await session.incrementAsync(); + await sessionLive.initAsync(); + + sessionLive.startPlayheadAsync(); + + jasmine.clock().mockDate(tsNow); + jasmine.clock().tick((25 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe("vod-2"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); + expect(testStreamSwitcher.getEventId()).toBe("live-4"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe(null); + }); it("should switch from linear-vod (session) to event-vod (session) according to schedule. " + - "\nThen directly to 2nd event-vod (session) and finally switch back to linear-vod (session)", async () => { + "\nThen directly to 2nd event-vod (session) and finally switch back to linear-vod (session)", async () => { + const switchMgr = new TestSwitchManager(5); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); + const assetMgr = new TestAssetManager(); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "_loadAllMediaManifests").and.returnValue(mockLiveSegments); + spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); + spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); + + await session.initAsync(); + await session.incrementAsync(); + await sessionLive.initAsync(); + + jasmine.clock().mockDate(tsNow); + jasmine.clock().tick((25 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe("vod-3"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe("vod-4"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe(null); + }); + + fit("should merge audio segments correctly", async () => { const switchMgr = new TestSwitchManager(5); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); - const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "_loadAllMediaManifests").and.returnValue(mockLiveSegments); - spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); - spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); - - await session.initAsync(); - await session.incrementAsync(); - await sessionLive.initAsync(); + const sessionLive = new StreamSwitcher({ streamSwitchManager: switchMgr }); + const fromSegments = { + aac: { + en: [ + { + id: 1, + uri: "1.m3u8" + }, + { + id: 2, + uri: "2.m3u8" + }, + { + id: 3, + uri: "3.m3u8" + }, + ], + es: [ + { + id: 1, + uri: "1.m3u8" + }, + { + id: 2, + uri: "2.m3u8" + }, + { + id: 3, + uri: "3.m3u8" + }, + ] + } - jasmine.clock().mockDate(tsNow); - jasmine.clock().tick((25 * 1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe("vod-3"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe("vod-4"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60*1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe(null); + }; + const toSegments = { + aac: { + en: [ + { + id: 4, + uri: "4.m3u8" + }, + { + id: 5, + uri: "5.m3u8" + }, + { + id: 6, + uri: "6.m3u8" + }, + ] + } + }; + let newList = sessionLive._mergeAudioSegments(toSegments, fromSegments, true); + let result = { + aac: { + en: [ + { discontinuity: true }, + { id: 4, uri: '4.m3u8' }, + { id: 5, uri: '5.m3u8' }, + { id: 6, uri: '6.m3u8' }, + { id: 1, uri: '1.m3u8' }, + { id: 2, uri: '2.m3u8' }, + { id: 3, uri: '3.m3u8' } + ], + es: [ + { discontinuity: true }, + { id: 4, uri: '4.m3u8' }, + { id: 5, uri: '5.m3u8' }, + { id: 6, uri: '6.m3u8' }, + { id: 1, uri: '1.m3u8' }, + { id: 2, uri: '2.m3u8' }, + { id: 3, uri: '3.m3u8' } + ] + } + } + + expect(newList).toEqual(result); }); }); \ No newline at end of file From 0beaf3c3c8a818d4ced5ba5eaa540e7b50c04840 Mon Sep 17 00:00:00 2001 From: Johan Lautakoksi Date: Sun, 20 Aug 2023 23:59:44 +0200 Subject: [PATCH 08/17] worked on fixing miss match in audio langs --- engine/session_live.js | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/engine/session_live.js b/engine/session_live.js index 57b73c9c..6fda0ad0 100644 --- a/engine/session_live.js +++ b/engine/session_live.js @@ -1517,11 +1517,34 @@ class SessionLive { if (this.allowedToSet) { // Collect and Push Segment-Extracting Promises let pushPromises = []; + + let isDifferent = 0 + if (this.vodAudioSegments) { + if (Object.keys(this.vodAudioSegments).length > Object.keys(this.audioManifestURIs)) { + isDifferent = 1; + } else if (Object.keys(this.vodAudioSegments).length > Object.keys(this.audioManifestURIs)) { + isDifferent = -1; + } + } + + for (let i = 0; i < toGroups.length; i++) { + const groupId = toGroups[i]; + const toLangs = Object.keys(toSegments[groupId]) + + for (let j = 0; j < toLangs.length; j++) { + const lang2 = toLangs[j]; + const targetGroupId = findAudioGroupOrLang(groupId, fromGroups); + const fromLangs = Object.keys(fromSegments[targetGroupId]); + const targetLang = findAudioGroupOrLang(lang2, fromLangs); + } + } + for (let i = 0; i < Object.keys(this.audioManifestURIs).length; i++) { let groupId = Object.keys(this.audioManifestURIs)[i]; let langs = Object.keys(this.audioManifestURIs[groupId]); for (let j = 0; j < langs.length; j++) { let lang = langs[j]; + // will add new segments to live seg queue pushPromises.push(this._parseAudioManifest(this.liveAudioSourceM3Us[groupId][lang].M3U, this.audioManifestURIs[groupId][lang], groupId, lang, isLeader)); debug(`[${this.sessionId}]: Pushed pushPromise for groupId=${groupId} & lang${lang}`); @@ -2026,7 +2049,6 @@ class SessionLive { startIdx = 0; } if (audioPlaylistUri) { - // push segments this._addLiveAudioSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetGroupId, liveTargetLanguage, isLeader); } resolve(); @@ -2309,6 +2331,11 @@ class SessionLive { return null; } + const targetGroupId = findAudioGroupOrLang(groupId, fromGroups); + const fromLangs = Object.keys(fromSegments[targetGroupId]); + const targetLang = findAudioGroupOrLang(lang2, fromLangs); + + // DO NOT GENERATE MANIFEST CASE: Node has not found anything in store OR Node has not even check yet. if (Object.keys(this.liveAudioSegQueue).length === 0 || (this.liveAudioSegQueue[liveTargetTrackIds.audioGroupId] && @@ -2324,15 +2351,20 @@ class SessionLive { let segAmounts = []; for (let i = 0; i < groupIds.length; i++) { const groupId = groupIds[i]; + const targetGroupId = findAudioGroupOrLang(groupId, fromGroups); + const fromLangs = Object.keys(fromSegments[targetGroupId]); + const targetLang = findAudioGroupOrLang(lang2, fromLangs); const langs = Object.keys(this.liveAudioSegQueue[groupId]); for (let j = 0; j < langs.length; j++) { const lang = langs[j]; if (this.liveAudioSegQueue[groupId][lang].length !== 0) { + segAmounts.push(this.liveAudioSegQueue[groupId][lang].length); } } } + if (!segAmounts.every((val, i, arr) => val === arr[0])) { console(`[${this.sessionId}]: Cannot Generate audio Manifest! <${this.instanceId}> Not yet collected ALL segments from Live Source...`); return null; @@ -2578,7 +2610,7 @@ class SessionLive { let groupAndLangToKeep = this._findAudioGroupsForLang(audioTrack.language, this.audioManifestURIs); toKeep.add(...groupAndLangToKeep); }); - + toKeep.forEach((trackInfo) => { if (!newItemsAudio[trackInfo.audioGroupId]) { newItemsAudio[trackInfo.audioGroupId] = {} From 323c106e663c68fc4e164aeaf16134859476a0da Mon Sep 17 00:00:00 2001 From: Johan Lautakoksi Date: Tue, 22 Aug 2023 23:23:24 +0200 Subject: [PATCH 09/17] cleaned up code and clarified tests, fixed undefiend bug --- engine/session.js | 2 -- engine/session_live.js | 34 +++-------------------- engine/stream_switcher.js | 9 ------- spec/engine/stream_switcher_spec.js | 42 ++++++++++++++--------------- 4 files changed, 24 insertions(+), 63 deletions(-) diff --git a/engine/session.js b/engine/session.js index 6a8a7ac3..e3e69162 100644 --- a/engine/session.js +++ b/engine/session.js @@ -1796,9 +1796,7 @@ class Session { // TODO: Support reloading with SubtitleSegments as well | // ---------------------------------------------------' - console.log("br") await currentVod.reload(nextMseq, segments, audioSegments, reloadBehind); - console.log("ar") await this._sessionState.setCurrentVod(currentVod, { ttl: currentVod.getDuration() * 1000 }); await this._sessionState.set("vodReloaded", 1); await this._sessionState.set("vodMediaSeqVideo", 0); diff --git a/engine/session_live.js b/engine/session_live.js index 6fda0ad0..15db758c 100644 --- a/engine/session_live.js +++ b/engine/session_live.js @@ -565,10 +565,10 @@ class SessionLive { increment = 1; } segmentCount = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].length; - currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = []; + currentAudioSequenceSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage] = []; // In case we switch back before we've depleted all transitional segments - currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].concat(this.liveAudioSegQueue[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage]); - currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage].push({ discontinuity: true, cue: { in: true } }); + currentAudioSequenceSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage] = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].concat(this.liveAudioSegQueue[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage]); + currentAudioSequenceSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].push({ discontinuity: true, cue: { in: true } }); debug(`[${this.sessionId}]: Getting current audio segments for ${groupId, langs[j]}`); } } @@ -1518,27 +1518,6 @@ class SessionLive { // Collect and Push Segment-Extracting Promises let pushPromises = []; - let isDifferent = 0 - if (this.vodAudioSegments) { - if (Object.keys(this.vodAudioSegments).length > Object.keys(this.audioManifestURIs)) { - isDifferent = 1; - } else if (Object.keys(this.vodAudioSegments).length > Object.keys(this.audioManifestURIs)) { - isDifferent = -1; - } - } - - for (let i = 0; i < toGroups.length; i++) { - const groupId = toGroups[i]; - const toLangs = Object.keys(toSegments[groupId]) - - for (let j = 0; j < toLangs.length; j++) { - const lang2 = toLangs[j]; - const targetGroupId = findAudioGroupOrLang(groupId, fromGroups); - const fromLangs = Object.keys(fromSegments[targetGroupId]); - const targetLang = findAudioGroupOrLang(lang2, fromLangs); - } - } - for (let i = 0; i < Object.keys(this.audioManifestURIs).length; i++) { let groupId = Object.keys(this.audioManifestURIs)[i]; let langs = Object.keys(this.audioManifestURIs[groupId]); @@ -2330,10 +2309,6 @@ class SessionLive { debug(`[${this.sessionId}]: FOLLOWER: Cannot Generate Audio Manifest! Waiting to sync-up with Leader...`); return null; } - - const targetGroupId = findAudioGroupOrLang(groupId, fromGroups); - const fromLangs = Object.keys(fromSegments[targetGroupId]); - const targetLang = findAudioGroupOrLang(lang2, fromLangs); // DO NOT GENERATE MANIFEST CASE: Node has not found anything in store OR Node has not even check yet. @@ -2351,9 +2326,6 @@ class SessionLive { let segAmounts = []; for (let i = 0; i < groupIds.length; i++) { const groupId = groupIds[i]; - const targetGroupId = findAudioGroupOrLang(groupId, fromGroups); - const fromLangs = Object.keys(fromSegments[targetGroupId]); - const targetLang = findAudioGroupOrLang(lang2, fromLangs); const langs = Object.keys(this.liveAudioSegQueue[groupId]); for (let j = 0; j < langs.length; j++) { const lang = langs[j]; diff --git a/engine/stream_switcher.js b/engine/stream_switcher.js index 22ca40a2..64b0c73e 100644 --- a/engine/stream_switcher.js +++ b/engine/stream_switcher.js @@ -257,7 +257,6 @@ class StreamSwitcher { currVodAudioSegments = this._mergeAudioSegments(prerollAudioSegments, currVodAudioSegments, false); } } - console.log(currVodAudioSegments, 200) // In risk that the SL-playhead might have updated some data after // we reset last time... we should Reset SessionLive before sending new data. @@ -330,25 +329,20 @@ class StreamSwitcher { try { debug(`[${this.sessionId}]: [ INIT Switching from LIVE->V2L ]`); this.eventId = null; - console.log("hej", 1) liveSegments = await sessionLive.getCurrentMediaSequenceSegments(); if (this.useDemuxedAudio) { liveAudioSegments = await sessionLive.getCurrentAudioSequenceSegments(); } - console.log("hej", 2) liveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount(); if (scheduleObj && !scheduleObj.duration) { debug(`[${this.sessionId}]: Cannot switch VOD. No duration specified for schedule item: [${scheduleObj.assetId}]`); } - console.log("hej", 3) if (this._isEmpty(liveSegments.currMseqSegs) || (this.useDemuxedAudio && this._isEmpty(liveAudioSegments.currMseqSegs))) { this.working = false; this.streamTypeLive = false; debug(`[${this.sessionId}]: [ Switched from LIVE->V2L ]`); return false; } - console.log("hej", 4) - console.log(liveAudioSegments) // Insert preroll, if available, for current channel if (this.prerollsCache[this.sessionId]) { @@ -362,16 +356,13 @@ class StreamSwitcher { } } - console.log("hej", 5) await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq, liveCounts.discSeq, liveCounts.audioSeq, liveCounts.audioDiscSeq); - console.log("hej", 5.2) if (this.useDemuxedAudio) { await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount, false, liveAudioSegments.currMseqSegs, liveAudioSegments.segCount); } else { await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount, false); } - console.log("hej", 6) await sessionLive.resetSession(); sessionLive.resetLiveStoreAsync(RESET_DELAY); // In parallel this.working = false; diff --git a/spec/engine/stream_switcher_spec.js b/spec/engine/stream_switcher_spec.js index f052a991..17e9aad7 100644 --- a/spec/engine/stream_switcher_spec.js +++ b/spec/engine/stream_switcher_spec.js @@ -492,29 +492,29 @@ describe("The Stream Switcher", () => { en: [ { id: 1, - uri: "1.m3u8" + uri: "en1.m3u8" }, { id: 2, - uri: "2.m3u8" + uri: "en2.m3u8" }, { id: 3, - uri: "3.m3u8" + uri: "en3.m3u8" }, ], es: [ { id: 1, - uri: "1.m3u8" + uri: "es1.m3u8" }, { id: 2, - uri: "2.m3u8" + uri: "es2.m3u8" }, { id: 3, - uri: "3.m3u8" + uri: "es3.m3u8" }, ] } @@ -525,15 +525,15 @@ describe("The Stream Switcher", () => { en: [ { id: 4, - uri: "4.m3u8" + uri: "en4.m3u8" }, { id: 5, - uri: "5.m3u8" + uri: "en5.m3u8" }, { id: 6, - uri: "6.m3u8" + uri: "en6.m3u8" }, ] } @@ -543,21 +543,21 @@ describe("The Stream Switcher", () => { aac: { en: [ { discontinuity: true }, - { id: 4, uri: '4.m3u8' }, - { id: 5, uri: '5.m3u8' }, - { id: 6, uri: '6.m3u8' }, - { id: 1, uri: '1.m3u8' }, - { id: 2, uri: '2.m3u8' }, - { id: 3, uri: '3.m3u8' } + { id: 4, uri: 'en4.m3u8' }, + { id: 5, uri: 'en5.m3u8' }, + { id: 6, uri: 'en6.m3u8' }, + { id: 1, uri: 'en1.m3u8' }, + { id: 2, uri: 'en2.m3u8' }, + { id: 3, uri: 'en3.m3u8' } ], es: [ { discontinuity: true }, - { id: 4, uri: '4.m3u8' }, - { id: 5, uri: '5.m3u8' }, - { id: 6, uri: '6.m3u8' }, - { id: 1, uri: '1.m3u8' }, - { id: 2, uri: '2.m3u8' }, - { id: 3, uri: '3.m3u8' } + { id: 4, uri: 'en4.m3u8' }, + { id: 5, uri: 'en5.m3u8' }, + { id: 6, uri: 'en6.m3u8' }, + { id: 1, uri: 'es1.m3u8' }, + { id: 2, uri: 'es2.m3u8' }, + { id: 3, uri: 'es3.m3u8' } ] } } From b7e42803bae76baa1a64a132d604220788206736 Mon Sep 17 00:00:00 2001 From: Johan Lautakoksi Date: Fri, 15 Sep 2023 11:22:45 +0200 Subject: [PATCH 10/17] latest release of hls-v2l --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2d399e3c..277f37c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@eyevinn/hls-repeat": "^0.2.0", "@eyevinn/hls-truncate": "^0.2.0", - "@eyevinn/hls-vodtolive": "3.1.0", + "@eyevinn/hls-vodtolive": "^4.1.0", "@eyevinn/m3u8": "^0.5.3", "abort-controller": "^3.0.0", "debug": "^3.2.7", @@ -99,9 +99,9 @@ } }, "node_modules/@eyevinn/hls-vodtolive": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eyevinn/hls-vodtolive/-/hls-vodtolive-3.1.0.tgz", - "integrity": "sha512-/NOh+pq1ZAgtHSMDoUZEu2x/JPzHGuIHHPaasyfviWKwUdB/H3ReVB1V+23aXNbRX1JnlOLJLdUph6ZXe7QjMQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@eyevinn/hls-vodtolive/-/hls-vodtolive-4.1.0.tgz", + "integrity": "sha512-1f05IxohO3fUmaJUTAGDBs0zViYoMImlE9/bstHRqiJVAQ7V2U7QCe3nvVuRv/7aPyqy6efsymwnfODax0abvg==", "dependencies": { "@eyevinn/m3u8": "^0.5.6", "abort-controller": "^3.0.0", @@ -2624,9 +2624,9 @@ } }, "@eyevinn/hls-vodtolive": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eyevinn/hls-vodtolive/-/hls-vodtolive-3.1.0.tgz", - "integrity": "sha512-/NOh+pq1ZAgtHSMDoUZEu2x/JPzHGuIHHPaasyfviWKwUdB/H3ReVB1V+23aXNbRX1JnlOLJLdUph6ZXe7QjMQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@eyevinn/hls-vodtolive/-/hls-vodtolive-4.1.0.tgz", + "integrity": "sha512-1f05IxohO3fUmaJUTAGDBs0zViYoMImlE9/bstHRqiJVAQ7V2U7QCe3nvVuRv/7aPyqy6efsymwnfODax0abvg==", "requires": { "@eyevinn/m3u8": "^0.5.6", "abort-controller": "^3.0.0", diff --git a/package.json b/package.json index edd7f33f..077cff98 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "dependencies": { "@eyevinn/hls-repeat": "^0.2.0", "@eyevinn/hls-truncate": "^0.2.0", - "@eyevinn/hls-vodtolive": "3.1.0", + "@eyevinn/hls-vodtolive": "^4.1.0", "@eyevinn/m3u8": "^0.5.3", "abort-controller": "^3.0.0", "debug": "^3.2.7", From 883bab374392c45f63c995d2e9a6c97f386d4e44 Mon Sep 17 00:00:00 2001 From: Johan Lautakoksi Date: Fri, 15 Sep 2023 11:23:32 +0200 Subject: [PATCH 11/17] used for testing purposes should be reverted when done --- examples/demux.ts | 2 +- examples/livemix.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/demux.ts b/examples/demux.ts index c6078980..6d4a025a 100644 --- a/examples/demux.ts +++ b/examples/demux.ts @@ -24,7 +24,7 @@ class RefAssetManager implements IAssetManager { { id: 1, title: "Elephants dream", - uri: "https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/index.m3u8", + uri: "https://mtoczko.github.io/hls-test-streams/test-audio-pdt/playlist.m3u8", }, ], diff --git a/examples/livemix.ts b/examples/livemix.ts index 3eb69e55..9e2e7af6 100644 --- a/examples/livemix.ts +++ b/examples/livemix.ts @@ -16,7 +16,7 @@ class RefAssetManager implements IAssetManager { constructor(opts?) { this.assets = { '1': [ - { id: 1, title: "Tears of Steel", uri: "https://mtoczko.github.io/hls-test-streams/test-audio-pdt/playlist.m3u8" }, + { id: 1, title: "Tears of Steel", uri: "https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/index.m3u8" }, ] }; this.pos = { @@ -45,7 +45,8 @@ class RefAssetManager implements IAssetManager { { pos: 0, duration: 15 * 1000, - url: "https://mtoczko.github.io/hls-test-streams/test-audio-pdt/playlist.m3u8" + url: "https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/index.m3u8" + } ] }; @@ -97,7 +98,6 @@ class RefChannelManager implements IChannelManager { _getAudioTracks(): AudioTracks[] { return [ { language: "en", name: "English", default: true }, - { language: "es", name: "Spanish", default: false }, ]; } } @@ -128,7 +128,7 @@ class StreamSwitchManager implements IStreamSwitchManager { } getPrerollUri(channelId): Promise { - const defaultPrerollSlateUri = "https://maitv-vod.lab.eyevinn.technology/slate-consuo.mp4/master.m3u8" + const defaultPrerollSlateUri = "http://localhost:8002/playlist.m3u8" return new Promise((resolve, reject) => { resolve(defaultPrerollSlateUri); }); } From 88d1226fb3d9066de0862154b884036cd4b2b28b Mon Sep 17 00:00:00 2001 From: Nicholas Frederiksen Date: Tue, 3 Oct 2023 11:07:17 +0200 Subject: [PATCH 12/17] chore: refactor demux - add cmaf support - add sync fix --- engine/session_live.js | 2223 +++++++++++++++++-------------------- engine/stream_switcher.js | 174 +-- package-lock.json | 806 +++++++++----- package.json | 2 +- 4 files changed, 1616 insertions(+), 1589 deletions(-) diff --git a/engine/session_live.js b/engine/session_live.js index 15db758c..e9fc59b3 100644 --- a/engine/session_live.js +++ b/engine/session_live.js @@ -2,6 +2,7 @@ const debug = require("debug")("engine-session-live"); const allSettled = require("promise.allsettled"); const crypto = require("crypto"); const m3u8 = require("@eyevinn/m3u8"); +const { segToM3u8 } = require("@eyevinn/hls-vodtolive/utils.js"); const url = require("url"); const fetch = require("node-fetch"); const { m3u8Header } = require("./util.js"); @@ -15,6 +16,7 @@ const daterangeAttribute = (key, attr) => { return key.toUpperCase() + "=" + `"${attr}"`; } }; +const HIGHEST_MEDIA_SEQUENCE_COUNT = 0; const TARGET_PLAYLIST_DURATION_SEC = 60; const RESET_DELAY = 5000; const FAIL_TIMEOUT = 4000; @@ -25,6 +27,11 @@ const PlayheadState = Object.freeze({ CRASHED: 3, IDLE: 4, }); +const PlaylistTypes = Object.freeze({ + VIDEO: 1, + AUDIO: 2, + SUBTITLE: 3, +}); /** * When we implement subtitle support in live-mix we should place it in its own file/or share it with audio @@ -47,18 +54,18 @@ class SessionLive { this.targetDuration = 0; this.masterManifestUri = null; this.vodSegments = {}; - this.vodAudioSegments = {}; + this.vodSegmentsAudio = {}; this.mediaManifestURIs = {}; this.audioManifestURIs = {}; this.liveSegQueue = {}; this.lastRequestedMediaSeqRaw = null; this.liveSourceM3Us = {}; - this.liveAudioSegQueue = {}; + this.liveSegQueueAudio = {}; this.lastRequestedAudioSeqRaw = null; this.liveAudioSourceM3Us = {}; this.playheadState = PlayheadState.IDLE; this.liveSegsForFollowers = {}; - this.audioLiveSegsForFollowers = {}; + this.liveSegsForFollowersAudio = {}; this.timerCompensation = null; this.firstTime = true; this.firstTimeAudio = true; @@ -106,11 +113,15 @@ class SessionLive { if (resetDelay === null || resetDelay < 0) { resetDelay = RESET_DELAY; } - debug(`[${this.instanceId}][${this.sessionId}]: LEADER: Resetting SessionLive values in Store ${resetDelay === 0 ? "Immediately" : `after a delay=(${resetDelay}ms)`}`); + debug( + `[${this.instanceId}][${this.sessionId}]: LEADER: Resetting SessionLive values in Store ${ + resetDelay === 0 ? "Immediately" : `after a delay=(${resetDelay}ms)` + }` + ); await timer(resetDelay); await this.sessionLiveState.set("liveSegsForFollowers", null); await this.sessionLiveState.set("lastRequestedMediaSeqRaw", null); - await this.sessionLiveState.set("liveAudioSegsForFollowers", null); + await this.sessionLiveState.set("liveSegsForFollowersAudio", null); await this.sessionLiveState.set("lastRequestedAudioSeqRaw", null); await this.sessionLiveState.set("transitSegs", null); await this.sessionLiveState.set("transitSegsAudio", null); @@ -120,7 +131,7 @@ class SessionLive { mediaSeqCount: null, audioSeqCount: null, discSeqCount: null, - audioDiscSeqCount: null + audioDiscSeqCount: null, }); debug(`[${this.instanceId}][${this.sessionId}]: LEADER: SessionLive values in Store have now been reset!`); } @@ -145,17 +156,17 @@ class SessionLive { this.targetDuration = 0; this.masterManifestUri = null; this.vodSegments = {}; - this.vodAudioSegments = {}; + this.vodSegmentsAudio = {}; this.mediaManifestURIs = {}; this.audioManifestURIs = {}; this.liveSegQueue = {}; - this.liveAudioSegQueue = {}; + this.liveSegQueueAudio = {}; this.lastRequestedMediaSeqRaw = null; this.lastRequestedAudioSeqRaw = null; this.liveSourceM3Us = {}; this.liveAudioSourceM3Us = {}; this.liveSegsForFollowers = {}; - this.audioLiveSegsForFollowers = {}; + this.liveSegsForFollowersAudio = {}; this.timerCompensation = null; this.firstTime = true; this.firstTimeAudio = true; @@ -192,8 +203,7 @@ class SessionLive { this.waitForPlayhead = true; const tsIncrementBegin = Date.now(); - await this._loadAllMediaManifests(); - await this._loadAllAudioManifests(); + await this._loadAllPlaylistManifests(); const tsIncrementEnd = Date.now(); this.waitForPlayhead = false; @@ -260,10 +270,11 @@ class SessionLive { debug(`[${this.sessionId}]: Filtered Live profiles! (${Object.keys(this.mediaManifestURIs).length}) profiles left!`); } if (this.sessionAudioTracks) { - this._filterLiveAudioTracks(); + this._filterLiveProfilesAudio(); debug(`[${this.sessionId}]: Filtered Live audio tracks! (${Object.keys([Object.keys(this.audioManifestURIs)[0]]).length}) profiles left!`); } } catch (err) { + console.error(err); this.masterManifestUri = null; debug(`[${this.instanceId}][${this.sessionId}]: Failed to fetch Live Master Manifest! ${err}`); debug(`[${this.instanceId}][${this.sessionId}]: Will try again in 1000ms! (tries left=${attempts})`); @@ -343,18 +354,16 @@ class SessionLive { } // Make it possible to add & share new segments this.allowedToSet = true; - if (this._isEmpty(this.vodAudioSegments)) { + if (this._isEmpty(this.vodSegmentsAudio)) { const groupIds = Object.keys(segments); for (let i = 0; i < groupIds.length; i++) { const groupId = groupIds[i]; const langs = Object.keys(segments[groupId]); for (let j = 0; j < langs.length; j++) { const lang = langs[j]; - if (!this.vodAudioSegments[groupId]) { - this.vodAudioSegments[groupId] = {}; - } - if (!this.vodAudioSegments[groupId][lang]) { - this.vodAudioSegments[groupId][lang] = []; + const audiotrack = this._getTrackFromGroupAndLang(groupId, lang); + if (!this.vodSegmentsAudio[audiotrack]) { + this.vodSegmentsAudio[audiotrack] = []; } if (segments[groupId][lang][0].discontinuity) { @@ -370,7 +379,7 @@ class SessionLive { cueInExists = false; } } - this.vodAudioSegments[groupId][lang].push(v2lSegment); + this.vodSegmentsAudio[audiotrack].push(v2lSegment); } const endIdx = segments[groupId][lang].length - 1; @@ -379,7 +388,7 @@ class SessionLive { if (!cueInExists) { finalSegItem["cue"] = { in: true }; } - this.vodAudioSegments[groupId][lang].push(finalSegItem); + this.vodSegmentsAudio[audiotrack].push(finalSegItem); } else { if (!cueInExists) { segments[groupId][lang][endIdx]["cue"] = { in: true }; @@ -388,14 +397,18 @@ class SessionLive { } } } else { - debug(`[${this.sessionId}]: 'vodAudioSegments' not empty = Using 'transitSegs'`); + debug(`[${this.sessionId}]: 'vodSegmentsAudio' not empty = Using 'transitSegs'`); } - debug(`[${this.sessionId}]: Setting CurrentAudioSequenceSegments. First seg is: [${this.vodAudioSegments[Object.keys(this.vodAudioSegments)[0]][Object.keys(this.vodAudioSegments[Object.keys(this.vodAudioSegments)[0]])[0]][0].uri}]`); + debug( + `[${this.sessionId}]: Setting CurrentAudioSequenceSegments. First seg is: [${ + this.vodSegmentsAudio[Object.keys(this.vodSegmentsAudio)[0]][0].uri + }` + ); const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); if (isLeader) { //debug(`[${this.sessionId}]: LEADER: I am adding 'transitSegs'=${JSON.stringify(this.vodSegments)} to Store for future followers`); - await this.sessionLiveState.set("transitSegs", this.vodAudioSegments); + await this.sessionLiveState.set("transitSegs", this.vodSegmentsAudio); debug(`[${this.sessionId}]: LEADER: I am adding 'transitSegs' to Store for future followers`); } } @@ -409,7 +422,9 @@ class SessionLive { debug(`[${this.sessionId}]: No media or disc sequence for audio provided`); return false; } - debug(`[${this.sessionId}]: Setting mediaSeqCount, discSeqCount, audioSeqCount and audioDiscSeqCount to: [${mediaSeq}]:[${discSeq}], [${audioMediaSeq}]:[${audioDiscSeq}]`); + debug( + `[${this.sessionId}]: Setting mediaSeqCount, discSeqCount, audioSeqCount and audioDiscSeqCount to: [${mediaSeq}]:[${discSeq}], [${audioMediaSeq}]:[${audioDiscSeq}]` + ); this.mediaSeqCount = mediaSeq; this.discSeqCount = discSeq; this.audioSeqCount = audioMediaSeq; @@ -454,7 +469,7 @@ class SessionLive { const transitAudioSegs = await this.sessionLiveState.get("transitAudioSegs"); if (!this._isEmpty(transitAudioSegs)) { debug(`[${this.sessionId}]: Getting and loading 'transitSegs'`); - this.vodAudioSegments = transitAudioSegs; + this.vodSegmentsAudio = transitAudioSegs; } } if (leadersDiscSeqCount !== null) { @@ -474,7 +489,7 @@ class SessionLive { } async getTransitionalAudioSegments() { - return this.vodAudioSegments; + return this.vodSegmentsAudio; } async getCurrentMediaSequenceSegments() { @@ -538,41 +553,37 @@ class SessionLive { const leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); if (leadersAudioSeqRaw > this.lastRequestedAudioSeqRaw) { this.lastRequestedAudioSeqRaw = leadersAudioSeqRaw; - this.liveAudioSegsForFollowers = await this.sessionLiveState.get("liveAudioSegsForFollowers"); - this._updateAudioLiveSegQueue(); + this.liveSegsForFollowersAudio = await this.sessionLiveState.get("liveSegsForFollowersAudio"); + this._updateLiveSegQueueAudio(); } } let currentAudioSequenceSegments = {}; let segmentCount = 0; let increment = 0; - const groupIds = Object.keys(this.vodAudioSegments); - for (let i = 0; i < groupIds.length; i++) { - let groupId = groupIds[i]; - if (!currentAudioSequenceSegments[groupId]) { - currentAudioSequenceSegments[groupId] = {}; + const vodAudiotracks = Object.keys(this.vodSegmentsAudio); + for (let vat of vodAudiotracks) { + const liveTargetTrack = this._findNearestAudiotrack(vat, Object.keys(this.audioManifestURIs)); + const vodTargetTrack = vat; + let vti = this._getGroupAndLangFromTrack(vat); // get the Vod Track Item + if (!currentAudioSequenceSegments[vti.groupId]) { + currentAudioSequenceSegments[vti.groupId] = {}; } - let langs = Object.keys(this.vodAudioSegments[groupIds[i]]); - for (let j = 0; j < langs.length; j++) { - const liveTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.audioManifestURIs); - const vodTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.vodAudioSegments); - if (!vodTargetGroupLang.audioGroupId || !vodTargetGroupLang.audioLanguage) { - return null; - } - // Remove segments and disc-tag if they are on top - if (this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].length > 0 && this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage][0].discontinuity) { - this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].shift(); - increment = 1; - } - segmentCount = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].length; - currentAudioSequenceSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage] = []; - // In case we switch back before we've depleted all transitional segments - currentAudioSequenceSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage] = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].concat(this.liveAudioSegQueue[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage]); - currentAudioSequenceSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].push({ discontinuity: true, cue: { in: true } }); - debug(`[${this.sessionId}]: Getting current audio segments for ${groupId, langs[j]}`); + // Remove segments and disc-tag if they are on top + if (this.vodSegmentsAudio[vodTargetTrack].length > 0 && this.vodSegmentsAudio[vodTargetTrack][0].discontinuity) { + this.vodSegmentsAudio[vodTargetTrack].shift(); + increment = 1; } + segmentCount = this.vodSegmentsAudio[vodTargetTrack].length; + currentAudioSequenceSegments[vti.groupId][vti.language] = []; + // In case we switch back before we've depleted all transitional segments + currentAudioSequenceSegments[vti.groupId][vti.language] = this.vodSegmentsAudio[vodTargetTrack].concat(this.liveSegQueueAudio[liveTargetTrack]); + currentAudioSequenceSegments[vti.groupId][vti.language].push({ + discontinuity: true, + cue: { in: true }, + }); + debug(`[${this.sessionId}]: Getting current audio segments for ${vodTargetTrack}`); } - this.audioDiscSeqCount += increment; return { currMseqSegs: currentAudioSequenceSegments, @@ -584,8 +595,8 @@ class SessionLive { return { mediaSeq: this.mediaSeqCount, discSeq: this.discSeqCount, - audioSeq: this.audioSeqCount, - audioDiscSeq: this.audioDiscSeqCount, + audioSeq: this.mediaSeqCount, + audioDiscSeq: this.discSeqCount, }; } @@ -658,7 +669,9 @@ class SessionLive { await timer(1000); } } catch (exc) { - throw new Error(`[${this.instanceId}][${this.sessionId}]: Failed to generate audio manifest. Live Session might have ended already. \n${exc}`); + throw new Error( + `[${this.instanceId}][${this.sessionId}]: Failed to generate audio manifest. Live Session might have ended already. \n${exc}` + ); } } if (!m3u8) { @@ -718,17 +731,13 @@ class SessionLive { this.mediaManifestURIs[streamItemBW] = mediaManifestUri; if (streamItem.get("audio") && this.useDemuxedAudio) { - let audioGroupId = streamItem.get("audio") + let audioGroupId = streamItem.get("audio"); let audioGroupItems = m3u.items.MediaItem.filter((item) => { return item.get("type") === "AUDIO" && item.get("group-id") === audioGroupId; }); // # Find all langs amongst the mediaItems that have this group id. // # It extracts each mediaItems language attribute value. // # ALSO initialize in this.audioSegments a lang. property who's value is an array [{seg1}, {seg2}, ...]. - if (!this.audioManifestURIs[audioGroupId]) { - this.audioManifestURIs[audioGroupId] = {} - } - audioGroupItems.map((item) => { let itemLang; if (!item.get("language")) { @@ -736,16 +745,19 @@ class SessionLive { } else { itemLang = item.get("language"); } - if (!this.audioManifestURIs[audioGroupId][itemLang]) { - this.audioManifestURIs[audioGroupId][itemLang] = "" + const audiotrack = this._getTrackFromGroupAndLang(audioGroupId, itemLang); + if (!this.audioManifestURIs[audiotrack]) { + this.audioManifestURIs[audiotrack] = ""; } - const audioManifestUri = url.resolve(baseUrl, item.get("uri")) - this.audioManifestURIs[audioGroupId][itemLang] = audioManifestUri; + const audioManifestUri = url.resolve(baseUrl, item.get("uri")); + this.audioManifestURIs[audiotrack] = audioManifestUri; }); } } - debug(`[${this.sessionId}]: All Live Media Manifest URIs have been collected. (${Object.keys(this.mediaManifestURIs).length}) profiles found!`); - debug(`[${this.sessionId}]: All Live Audio Manifest URIs have been collected. (${Object.keys(this.audioManifestURIs[Object.keys(this.audioManifestURIs)[0]]).length}) tracks found!`); + debug( + `[${this.sessionId}]: All Live Media Manifest URIs have been collected. (${Object.keys(this.mediaManifestURIs).length}) profiles found!` + ); + debug(`[${this.sessionId}]: All Live Audio Manifest URIs have been collected. (${Object.keys(this.audioManifestURIs).length}) tracks found!`); resolve(); parser.on("error", (exc) => { debug(`Parser Error: ${JSON.stringify(exc)}`); @@ -757,827 +769,617 @@ class SessionLive { // FOLLOWER only function _updateLiveSegQueue() { - if (Object.keys(this.liveSegsForFollowers).length === 0) { - debug(`[${this.sessionId}]: FOLLOWER: Error No Segments found at all.`); - } - const liveBws = Object.keys(this.liveSegsForFollowers); - const size = this.liveSegsForFollowers[liveBws[0]].length; - - // Push the New Live Segments to All Variants - for (let segIdx = 0; segIdx < size; segIdx++) { - for (let i = 0; i < liveBws.length; i++) { - const liveBw = liveBws[i]; - const liveSegFromLeader = this.liveSegsForFollowers[liveBw][segIdx]; - if (!this.liveSegQueue[liveBw]) { - this.liveSegQueue[liveBw] = []; - } - // Do not push duplicates - const liveSegURIs = this.liveSegQueue[liveBw].filter((seg) => seg.uri).map((seg) => seg.uri); - if (liveSegFromLeader.uri && liveSegURIs.includes(liveSegFromLeader.uri)) { - debug(`[${this.sessionId}]: FOLLOWER: Found duplicate live segment. Skip push! (${liveBw})`); - } else { - this.liveSegQueue[liveBw].push(liveSegFromLeader); - debug(`[${this.sessionId}]: FOLLOWER: Pushed segment (${liveSegFromLeader.uri ? liveSegFromLeader.uri : "Disc-tag"}) to 'liveSegQueue' (${liveBw})`); + try { + if (Object.keys(this.liveSegsForFollowers).length === 0) { + debug(`[${this.sessionId}]: FOLLOWER: Error No Segments found at all.`); + } + const liveBws = Object.keys(this.liveSegsForFollowers); + const size = this.liveSegsForFollowers[liveBws[0]].length; + + // Push the New Live Segments to All Variants + for (let segIdx = 0; segIdx < size; segIdx++) { + for (let i = 0; i < liveBws.length; i++) { + const liveBw = liveBws[i]; + const liveSegFromLeader = this.liveSegsForFollowers[liveBw][segIdx]; + if (!this.liveSegQueue[liveBw]) { + this.liveSegQueue[liveBw] = []; + } + // Do not push duplicates + const liveSegURIs = this.liveSegQueue[liveBw].filter((seg) => seg.uri).map((seg) => seg.uri); + if (liveSegFromLeader.uri && liveSegURIs.includes(liveSegFromLeader.uri)) { + debug(`[${this.sessionId}]: FOLLOWER: Found duplicate live segment. Skip push! (${liveBw})`); + } else { + this.liveSegQueue[liveBw].push(liveSegFromLeader); + debug( + `[${this.sessionId}]: FOLLOWER: Pushed Video segment (${ + liveSegFromLeader.uri ? liveSegFromLeader.uri : "Disc-tag" + }) to 'liveSegQueue' (${liveBw})` + ); + } } } - } - // Remove older segments and update counts - const newTotalDuration = this._incrementAndShift("FOLLOWER"); - if (newTotalDuration) { - debug(`[${this.sessionId}]: FOLLOWER: New Adjusted Playlist Duration=${newTotalDuration}s`); + // Remove older segments and update counts + const newTotalDuration = this._incrementAndShift("FOLLOWER"); + if (newTotalDuration) { + debug(`[${this.sessionId}]: FOLLOWER: New Adjusted Playlist Duration=${newTotalDuration}s`); + } + } catch (e) { + console.error(e); + return Promise.reject(e); } } - _updateLiveAudioSegQueue() { - let followerGroupIds = Object.keys(this.liveAudioSegsForFollowers); - let followerLangs = Object.keys(Object.keys(this.liveAudioSegsForFollowers[followerGroupIds[0]])); - if (this.liveAudioSegsForFollowers[followerGroupIds[0]][followerLangs[0]].length === 0) { - debug(`[${this.sessionId}]: FOLLOWER: Error No Segments found at all.`); - } - const liveGroupIds = Object.keys(this.liveAudioSegsForFollowers); - const size = this.liveAudioSegsForFollowers[liveGroupIds[0]][Object.keys(this.liveAudioSegsForFollowers[liveGroupIds[0]])].length; - - // Push the New Live Segments to All Variants - for (let segIdx = 0; segIdx < size; segIdx++) { - for (let i = 0; i < liveGroupIds.length; i++) { - x - const liveGroupId = liveGroupIds[i]; - const liveLangs = Object.keys(this.liveAudioSegsForFollowers[liveGroupId]) - for (let j = 0; j < liveLangs.length; j++) { - const liveLang = liveLangs[j]; - - const liveSegFromLeader = this.liveAudioSegsForFollowers[liveGroupId][liveLang][segIdx]; - if (!this.liveAudioSegQueue[liveGroupId]) { - this.liveAudioSegQueue[liveGroupId] = {}; - } - if (!this.liveAudioSegQueue[liveGroupId][liveLang]) { - this.liveAudioSegQueue[liveGroupId][liveLang] = []; + _updateLiveSegQueueAudio() { + try { + let followerAudiotracks = Object.keys(this.liveSegsForFollowersAudio); + if (this.liveSegsForFollowersAudio[followerAudiotracks[0]].length === 0) { + debug(`[${this.sessionId}]: FOLLOWER: Error No Audio Segments found at all.`); + } + const size = this.liveSegsForFollowersAudio[followerAudiotracks[0]].length; + // Push the New Live Segments to All Variants + for (let segIdx = 0; segIdx < size; segIdx++) { + for (let i = 0; i < followerAudiotracks.length; i++) { + const fat = followerAudiotracks[i]; + const liveSegFromLeader = this.liveSegsForFollowersAudio[fat][segIdx]; + if (!this.liveSegQueueAudio[fat]) { + this.liveSegQueueAudio[fat] = []; } // Do not push duplicates - const liveSegURIs = this.liveAudioSegQueue[liveGroupId][liveLang].filter((seg) => seg.uri).map((seg) => seg.uri); + const liveSegURIs = this.liveSegQueueAudio[fat].filter((seg) => seg.uri).map((seg) => seg.uri); if (liveSegFromLeader.uri && liveSegURIs.includes(liveSegFromLeader.uri)) { debug(`[${this.sessionId}]: FOLLOWER: Found duplicate live segment. Skip push! (${liveGroupId})`); } else { - this.liveAudioSegQueue[liveGroupId][liveLang].push(liveSegFromLeader); - debug(`[${this.sessionId}]: FOLLOWER: Pushed segment (${liveSegFromLeader.uri ? liveSegFromLeader.uri : "Disc-tag"}) to 'liveAudioSegQueue' (${liveGroupId, liveLang})`); + this.liveSegQueueAudio[fat].push(liveSegFromLeader); + debug( + `[${this.sessionId}]: FOLLOWER: Pushed Audio segment (${ + liveSegFromLeader.uri ? liveSegFromLeader.uri : "Disc-tag" + }) to 'liveSegQueueAudio' (${fat})` + ); } } } - } - // Remove older segments and update counts - const newTotalDuration = this._incrementAndShiftAudio("FOLLOWER"); - if (newTotalDuration) { - debug(`[${this.sessionId}]: FOLLOWER: New Adjusted Playlist Duration=${newTotalDuration}s`); + // Remove older segments and update counts + const newTotalDuration = this._incrementAndShiftAudio("FOLLOWER"); + if (newTotalDuration) { + debug(`[${this.sessionId}]: FOLLOWER: New Adjusted Playlist Duration=${newTotalDuration}s`); + } + } catch (e) { + console.error(e); + return Promise.reject(e); } } - /** - * This function adds new live segments to the node from which it can - * generate new manifests from. Method for attaining new segments differ - * depending on node Rank. The Leader collects from live source and - * Followers collect from shared storage. - * - * @returns Nothing, but gives data to certain class-variables - */ - async _loadAllMediaManifests() { - debug(`[${this.sessionId}]: Attempting to load all media manifest URIs in=${Object.keys(this.mediaManifestURIs)}`); - let currentMseqRaw = null; - // ------------------------------------- - // If I am a Follower-node then my job - // ends here, where I only read from store. - // ------------------------------------- - let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (!isLeader && this.lastRequestedMediaSeqRaw !== null) { - debug(`[${this.sessionId}]: FOLLOWER: Reading data from store!`); - - let leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); - - if (!leadersMediaSeqRaw < this.lastRequestedMediaSeqRaw && this.blockGenerateManifest) { - this.blockGenerateManifest = false; - } + async _collectSegmentsFromStore() { + try { + // check if audio is enabled + let hasAudio = this.audioManifestURIs.length > 0 ? true : false; + // ------------------------------------- + // If I am a Follower-node then my job + // ends here, where I only read from store. + // ------------------------------------- + let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (!isLeader && this.lastRequestedMediaSeqRaw !== null) { + debug(`[${this.sessionId}]: FOLLOWER: Reading data from store!`); + + let leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); + + if (!leadersMediaSeqRaw < this.lastRequestedMediaSeqRaw && this.blockGenerateManifest) { + this.blockGenerateManifest = false; + } + + let attempts = 10; + // CHECK AGAIN CASE 1: Store Empty + while (!leadersMediaSeqRaw && attempts > 0) { + if (!leadersMediaSeqRaw) { + isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (isLeader) { + debug(`[${this.instanceId}]: I'm the new leader`); + return; + } + } - let attempts = 10; - // CHECK AGAIN CASE 1: Store Empty - while (!leadersMediaSeqRaw && attempts > 0) { - if (!leadersMediaSeqRaw) { - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.instanceId}]: I'm the new leader`); - return; + if (!this.allowedToSet) { + debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); + break; } + const segDur = this._getAnyFirstSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; + const waitTimeMs = parseInt(segDur / 3, 10); + debug( + `[${this.sessionId}]: FOLLOWER: Leader has not put anything in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])` + ); + await timer(waitTimeMs); + this.timerCompensation = false; + leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); + attempts--; } - if (!this.allowedToSet) { - debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); - break; + if (!leadersMediaSeqRaw) { + debug(`[${this.instanceId}]: The leader is still alive`); + return; } - const segDur = this._getAnyFirstSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; - const waitTimeMs = parseInt(segDur / 3, 10); - debug(`[${this.sessionId}]: FOLLOWER: Leader has not put anything in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`); - await timer(waitTimeMs); - this.timerCompensation = false; - leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); - attempts--; - } - - if (!leadersMediaSeqRaw) { - debug(`[${this.instanceId}]: The leader is still alive`); - return; - } - let liveSegsInStore = await this.sessionLiveState.get("liveSegsForFollowers"); - attempts = 10; - // CHECK AGAIN CASE 2: Store Old - while ((leadersMediaSeqRaw <= this.lastRequestedMediaSeqRaw && attempts > 0) || (this._containsSegment(this.liveSegsForFollowers, liveSegsInStore) && attempts > 0)) { - if (!this.allowedToSet) { - debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); - break; + let liveSegsInStore = await this.sessionLiveState.get("liveSegsForFollowers"); + let liveSegsInStoreAudio = hasAudio ? await this.sessionLiveState.get("liveSegsForFollowersAudio") : null; + attempts = 10; + // CHECK AGAIN CASE 2: Store Old + while ( + (leadersMediaSeqRaw <= this.lastRequestedMediaSeqRaw && attempts > 0) || + (this._containsSegment(this.liveSegsForFollowers, liveSegsInStore) && attempts > 0) + ) { + if (!this.allowedToSet) { + debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); + break; + } + if (leadersMediaSeqRaw <= this.lastRequestedMediaSeqRaw) { + isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (isLeader) { + debug(`[${this.instanceId}][${this.sessionId}]: I'm the new leader`); + return; + } + } + if (this._containsSegment(this.liveSegsForFollowers, liveSegsInStore)) { + debug(`[${this.sessionId}]: FOLLOWER: _containsSegment=true,${leadersMediaSeqRaw},${this.lastRequestedMediaSeqRaw}`); + } + const segDur = this._getAnyFirstSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; + const waitTimeMs = parseInt(segDur / 3, 10); + debug(`[${this.sessionId}]: FOLLOWER: Cannot find anything NEW in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`); + await timer(waitTimeMs); + this.timerCompensation = false; + leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); + liveSegsInStore = await this.sessionLiveState.get("liveSegsForFollowers"); + liveSegsInStoreAudio = hasAudio ? await this.sessionLiveState.get("liveSegsForFollowersAudio") : null; + attempts--; } + // FINALLY if (leadersMediaSeqRaw <= this.lastRequestedMediaSeqRaw) { - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.instanceId}][${this.sessionId}]: I'm the new leader`); - return; - } + debug(`[${this.instanceId}][${this.sessionId}]: The leader is still alive`); + return; + } + // Follower updates its manifest building blocks (segment holders & counts) + this.lastRequestedMediaSeqRaw = leadersMediaSeqRaw; + this.liveSegsForFollowers = liveSegsInStore; + this.liveSegsForFollowersAudio = liveSegsInStoreAudio; + debug( + `[${this.sessionId}]: These are the segments from store:\nV[${JSON.stringify(this.liveSegsForFollowers)}]${ + hasAudio ? `\nA[${JSON.stringify(this.liveSegsForFollowersAudio)}]` : "" + }` + ); + this._updateLiveSegQueue(); + if (hasAudio) { + this._updateLiveSegQueueAudio(); } - if (this._containsSegment(this.liveSegsForFollowers, liveSegsInStore)) { - debug(`[${this.sessionId}]: FOLLOWER: _containsSegment=true,${leadersMediaSeqRaw},${this.lastRequestedMediaSeqRaw}`); - } - const segDur = this._getAnyFirstSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; - const waitTimeMs = parseInt(segDur / 3, 10); - debug(`[${this.sessionId}]: FOLLOWER: Cannot find anything NEW in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`); - await timer(waitTimeMs); - this.timerCompensation = false; - leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); - liveSegsInStore = await this.sessionLiveState.get("liveSegsForFollowers"); - attempts--; - } - // FINALLY - if (leadersMediaSeqRaw <= this.lastRequestedMediaSeqRaw) { - debug(`[${this.instanceId}][${this.sessionId}]: The leader is still alive`); return; } - // Follower updates its manifest building blocks (segment holders & counts) - this.lastRequestedMediaSeqRaw = leadersMediaSeqRaw; - this.liveSegsForFollowers = liveSegsInStore; - debug(`[${this.sessionId}]: These are the segments from store: [${JSON.stringify(this.liveSegsForFollowers)}]`); - this._updateLiveSegQueue(); - return; + } catch (e) { + console.error(e); + return Promise.reject(e); } + } - // --------------------------------- - // FETCHING FROM LIVE-SOURCE - New Followers (once) & Leaders do this. - // --------------------------------- - let FETCH_ATTEMPTS = 10; - this.liveSegsForFollowers = {}; - let bandwidthsToSkipOnRetry = []; - while (FETCH_ATTEMPTS > 0) { - if (isLeader) { - debug(`[${this.sessionId}]: LEADER: Trying to fetch manifests for all bandwidths\n Attempts left=[${FETCH_ATTEMPTS}]`); - } else { - debug(`[${this.sessionId}]: NEW FOLLOWER: Trying to fetch manifests for all bandwidths\n Attempts left=[${FETCH_ATTEMPTS}]`); - } + async _fetchFromLiveSource() { + try { + let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + + let currentMseqRaw = null; + let FETCH_ATTEMPTS = 10; + this.liveSegsForFollowers = {}; + this.liveSegsForFollowersAudio = {}; + let bandwidthsToSkipOnRetry = []; + let audiotracksToSkipOnRetry = []; + const audioTracksExist = Object.keys(this.audioManifestURIs).length > 0 ? true : false; + debug(`[${this.sessionId}]: Attempting to load all MEDIA manifest URIs in=${Object.keys(this.mediaManifestURIs)}`); + if (audioTracksExist) { + debug(`[${this.sessionId}]: Attempting to load all AUDIO manifest URIs in=${Object.keys(this.audioManifestURIs)}`); + } + // --------------------------------- + // FETCHING FROM LIVE-SOURCE - New Followers (once) & Leaders do this. + // --------------------------------- + while (FETCH_ATTEMPTS > 0) { + const MSG_1 = (rank, id, count, hasAudio) => { + return `[${id}]: ${rank}: Trying to fetch manifests for all bandwidths${hasAudio ? " and audiotracks" : ""}\n Attempts left=[${count}]`; + }; - if (!this.allowedToSet) { - debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Live-Source`); - break; - } + if (isLeader) { + debug(MSG_1("LEADER", this.sessionId, FETCH_ATTEMPTS, audioTracksExist)); + } else { + debug(MSG_1("NEW FOLLOWER", this.sessionId, FETCH_ATTEMPTS, audioTracksExist)); + } - // Reset Values Each Attempt - let livePromises = []; - let manifestList = []; - this.pushAmount = 0; - try { - if (bandwidthsToSkipOnRetry.length > 0) { - debug(`[${this.sessionId}]: (X) Skipping loadMedia promises for bws ${JSON.stringify(bandwidthsToSkipOnRetry)}`); + if (!this.allowedToSet) { + debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Live-Source`); + break; } - // Collect Live Source Requesting Promises - for (let i = 0; i < Object.keys(this.mediaManifestURIs).length; i++) { - let bw = Object.keys(this.mediaManifestURIs)[i]; - if (bandwidthsToSkipOnRetry.includes(bw)) { - continue; + + // Reset Values Each Attempt + let livePromises = []; + let manifestList = []; + this.pushAmount = 0; + try { + if (bandwidthsToSkipOnRetry.length > 0) { + debug(`[${this.sessionId}]: (X) Skipping loadMedia promises for bws ${JSON.stringify(bandwidthsToSkipOnRetry)}`); + } + if (audiotracksToSkipOnRetry.length > 0) { + debug(`[${this.sessionId}]: (X) Skipping loadMedia promises for audiotracks ${JSON.stringify(audiotracksToSkipOnRetry)}`); + } + // Collect Live Source Requesting Promises + for (let i = 0; i < Object.keys(this.mediaManifestURIs).length; i++) { + let bw = Object.keys(this.mediaManifestURIs)[i]; + if (bandwidthsToSkipOnRetry.includes(bw)) { + continue; + } + livePromises.push(this._loadMediaManifest(bw)); + debug(`[${this.sessionId}]: Pushed loadMedia promise for bw=[${bw}]`); + } + // Collect Live Source Requesting Promises (audio) + for (let i = 0; i < Object.keys(this.audioManifestURIs).length; i++) { + let atStr = Object.keys(this.audioManifestURIs)[i]; + if (audiotracksToSkipOnRetry.includes(atStr)) { + continue; + } + livePromises.push(this._loadAudioManifest(atStr)); + debug(`[${this.sessionId}]: Pushed loadMedia promise for audiotrack=${atStr}`); } - livePromises.push(this._loadMediaManifest(bw)); - debug(`[${this.sessionId}]: Pushed loadMedia promise for bw=[${bw}]`); + // Fetch From Live Source + debug(`[${this.sessionId}]: Executing Promises I: Fetch From Live Source`); + manifestList = await allSettled(livePromises); + livePromises = []; + } catch (err) { + debug(`[${this.sessionId}]: Promises I: FAILURE!\n${err}`); + return; } - // Fetch From Live Source - debug(`[${this.sessionId}]: Executing Promises I: Fetch From Live Source`); - manifestList = await allSettled(livePromises); - livePromises = []; - } catch (err) { - debug(`[${this.sessionId}]: Promises I: FAILURE!\n${err}`); - return; - } - - // Handle if any promise got rejected - if (manifestList.some((result) => result.status === "rejected")) { - FETCH_ATTEMPTS--; - debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again in 1000ms...`); - await timer(1000); - continue; - } - // Store the results locally - manifestList.forEach((variantItem) => { - const bw = variantItem.value.bandwidth; - if (!this.liveSourceM3Us[bw]) { - this.liveSourceM3Us[bw] = {}; + // Handle if any promise got rejected + if (manifestList.some((result) => result.status === "rejected")) { + FETCH_ATTEMPTS--; + debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again in 1000ms...`); + console.log( + manifestList.map((r) => { + return { status: r.status }; + }) + ); + await timer(1000); + continue; } - this.liveSourceM3Us[bw] = variantItem.value; - }); - - const allStoredMediaSeqCounts = Object.keys(this.liveSourceM3Us).map((variant) => this.liveSourceM3Us[variant].mediaSeq); - // Handle if mediaSeqCounts are NOT synced up! - if (!allStoredMediaSeqCounts.every((val, i, arr) => val === arr[0])) { - debug(`[${this.sessionId}]: Live Mseq counts=[${allStoredMediaSeqCounts}]`); - // Figure out what bw's are behind. - const highestMediaSeqCount = Math.max(...allStoredMediaSeqCounts); - bandwidthsToSkipOnRetry = Object.keys(this.liveSourceM3Us).filter((bw) => { - if (this.liveSourceM3Us[bw].mediaSeq === highestMediaSeqCount) { - return true; + // Fill "liveSourceM3Us" and Store the results locally + manifestList.forEach((variantItem) => { + let variantKey = ""; + if (variantItem.value.bandwidth) { + variantKey = variantItem.value.bandwidth; + } else if (variantItem.value.audiotrack) { + variantKey = variantItem.value.audiotrack; + } else { + console.error("NO 'bandwidth' or 'audiotrack' in item:", JSON.stringify(variantItem)); + } + if (!this.liveSourceM3Us[variantKey]) { + this.liveSourceM3Us[variantKey] = {}; } - return false; + this.liveSourceM3Us[variantKey] = variantItem.value; }); - // Decrement fetch counter - FETCH_ATTEMPTS--; - // Calculate retry delay time. Default=1000 - let retryDelayMs = 1000; - if (Object.keys(this.liveSegQueue).length > 0) { - const firstBw = Object.keys(this.liveSegQueue)[0]; - const lastIdx = this.liveSegQueue[firstBw].length - 1; - if (this.liveSegQueue[firstBw][lastIdx].duration) { - retryDelayMs = this.liveSegQueue[firstBw][lastIdx].duration * 1000 * 0.25; + + const allStoredMediaSeqCounts = Object.keys(this.liveSourceM3Us).map((variant) => this.liveSourceM3Us[variant].mediaSeq); + + // Handle if mediaSeqCounts are NOT synced up! + if (!allStoredMediaSeqCounts.every((val, i, arr) => val === arr[0])) { + bandwidthsToSkipOnRetry = []; + audiotracksToSkipOnRetry = []; + debug(`[${this.sessionId}]: Live Mseq counts=[${allStoredMediaSeqCounts}]`); + // Figure out what variants's are behind. + HIGHEST_MEDIA_SEQUENCE_COUNT = Math.max(...allStoredMediaSeqCounts); + Object.keys(this.liveSourceM3Us).map((variantKey) => { + if (this.liveSourceM3Us[variantKey].mediaSeq === HIGHEST_MEDIA_SEQUENCE_COUNT) { + if (this._isBandwidth(variantKey)) { + bandwidthsToSkipOnRetry.push(variantKey); + } else { + audiotracksToSkipOnRetry.push(variantKey); + } + } + }); + // Decrement fetch counter + FETCH_ATTEMPTS--; + // Calculate retry delay time. Default=1000 + let retryDelayMs = 1000; + if (Object.keys(this.liveSegQueue).length > 0) { + const firstBw = Object.keys(this.liveSegQueue)[0]; + const lastIdx = this.liveSegQueue[firstBw].length - 1; + if (this.liveSegQueue[firstBw][lastIdx].duration) { + retryDelayMs = this.liveSegQueue[firstBw][lastIdx].duration * 1000 * 0.25; + } + } + // If 3 tries already and only video is unsynced, Make the BAD VARIANTS INHERIT M3U's from the good ones. + if (FETCH_ATTEMPTS >= 7 && audiotracksToSkipOnRetry.length === this.audioManifestURIs.length) { + // Find Highest MSEQ + let [ahead, behind] = Object.keys(this.liveSourceM3Us).map((v) => { + const c = this.liveSourceM3Us[v].mediaSeq; + const a = []; + const b = []; + if (c === HIGHEST_MEDIA_SEQUENCE_COUNT) { + a.push({ c, v }); + } else { + b.push({ c, v }); + } + }); + // Find lowest bitrate with that highest MSEQ + const variantToPaste = ahead.reduce((min, item) => (item.v < min.v ? item : min), list[0]); + // Reassign that bitrate onto the one's originally planned for retry + const m3uToPaste = this.liveSourceM3Us[variantToPaste]; + behind.forEach((item) => { + this.liveSourceM3Us[item.v] = m3uToPaste; + }); + debug(`[${this.sessionId}]: ALERT! Live Source Data NOT in sync! Will fake sync by copy-pasting segments from best mseq`); + } else { + // Wait a little before trying again + debug(`[${this.sessionId}]: ALERT! Live Source Data NOT in sync! Will try again after ${retryDelayMs}ms`); + await timer(retryDelayMs); + if (isLeader) { + this.timerCompensation = false; + } + continue; } } - // Wait a little before trying again - debug(`[${this.sessionId}]: ALERT! Live Source Data NOT in sync! Will try again after ${retryDelayMs}ms`); - await timer(retryDelayMs); - if (isLeader) { - this.timerCompensation = false; - } - continue; - } - currentMseqRaw = allStoredMediaSeqCounts[0]; + currentMseqRaw = allStoredMediaSeqCounts[0]; - if (!isLeader) { - let leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); - let tries = 20; + if (!isLeader) { + let leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); + let tries = 20; - while ((!isLeader && !leadersFirstSeqCounts.liveSourceMseqCount && tries > 0) || leadersFirstSeqCounts.liveSourceMseqCount === 0) { - debug(`[${this.sessionId}]: NEW FOLLOWER: Waiting for LEADER to add 'firstCounts' in store! Will look again after 1000ms (tries left=${tries})`); - await timer(1000); - leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); - tries--; - // Might take over as Leader if Leader is not setting data due to being down. - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); + while ((!isLeader && !leadersFirstSeqCounts.liveSourceMseqCount && tries > 0) || leadersFirstSeqCounts.liveSourceMseqCount === 0) { + debug( + `[${this.sessionId}]: NEW FOLLOWER: Waiting for LEADER to add 'firstCounts' in store! Will look again after 1000ms (tries left=${tries})` + ); + await timer(1000); + leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); + tries--; + // Might take over as Leader if Leader is not setting data due to being down. + isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (isLeader) { + debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); + } + } + + if (tries === 0) { + isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (isLeader) { + debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); + break; + } else { + debug(`[${this.sessionId}][${this.instanceId}]: The leader is still alive`); + leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); + if (!leadersFirstSeqCounts.liveSourceMseqCount) { + debug( + `[${this.sessionId}][${this.instanceId}]: Could not find 'firstCounts' in store. Abort Executing Promises II & Returning to Playhead.` + ); + return; + } + } } - } - if (tries === 0) { - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); if (isLeader) { - debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); - break; - } else { - debug(`[${this.sessionId}][${this.instanceId}]: The leader is still alive`); - leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); - if (!leadersFirstSeqCounts.liveSourceMseqCount) { - debug(`[${this.sessionId}][${this.instanceId}]: Could not find 'firstCounts' in store. Abort Executing Promises II & Returning to Playhead.`); - return; + debug(`[${this.sessionId}]: NEW LEADER: Original Leader went missing, I am retrying live source fetch...`); + await this.sessionLiveState.set("transitSegs", this.vodSegments); + if (audioTracksExist) { + await this.sessionLiveState.set("transitSegsAudio", this.vodSegmentsAudio); } + debug( + `[${this.sessionId}]: NEW LEADER: I am adding 'transitSegs'${ + audioTracksExist ? "and 'transitSegsAudio'" : "" + } to Store for future followers` + ); + continue; } - } - if (isLeader) { - debug(`[${this.sessionId}]: NEW LEADER: Original Leader went missing, I am retrying live source fetch...`); - await this.sessionLiveState.set("transitSegs", this.vodSegments); - debug(`[${this.sessionId}]: NEW LEADER: I am adding 'transitSegs' to Store for future followers`); - continue; - } + // Respawners never do this, only starter followers. + // Edge Case: FOLLOWER transitioned from session with different segments from LEADER + if (leadersFirstSeqCounts.discSeqCount !== this.discSeqCount) { + this.discSeqCount = leadersFirstSeqCounts.discSeqCount; + } + if (leadersFirstSeqCounts.mediaSeqCount !== this.mediaSeqCount) { + this.mediaSeqCount = leadersFirstSeqCounts.mediaSeqCount; + debug( + `[${this.sessionId}]: FOLLOWER transistioned with wrong V2L segments, updating counts to [${this.mediaSeqCount}][${this.discSeqCount}], and reading 'transitSegs' from store` + ); + const transitSegs = await this.sessionLiveState.get("transitSegs"); + if (!this._isEmpty(transitSegs)) { + this.vodSegments = transitSegs; + } + if (audioTracksExist) { + const transitSegsAudio = await this.sessionLiveState.get("transitSegsAudio"); + if (!this._isEmpty(transitSegsAudio)) { + this.vodSegmentsAudio = transitSegsAudio; + } + } + } - // Respawners never do this, only starter followers. - // Edge Case: FOLLOWER transitioned from session with different segments from LEADER - if (leadersFirstSeqCounts.discSeqCount !== this.discSeqCount) { - this.discSeqCount = leadersFirstSeqCounts.discSeqCount; - } - if (leadersFirstSeqCounts.mediaSeqCount !== this.mediaSeqCount) { - this.mediaSeqCount = leadersFirstSeqCounts.mediaSeqCount; + // Prepare to load segments... debug( - `[${this.sessionId}]: FOLLOWER transistioned with wrong V2L segments, updating counts to [${this.mediaSeqCount}][${this.discSeqCount}], and reading 'transitSegs' from store` + `[${this.instanceId}][${this.sessionId}]: Newest mseq from LIVE=${currentMseqRaw} First mseq in store=${leadersFirstSeqCounts.liveSourceMseqCount}` ); - const transitSegs = await this.sessionLiveState.get("transitSegs"); - if (!this._isEmpty(transitSegs)) { - this.vodSegments = transitSegs; - } - } - - // Prepare to load segments... - debug(`[${this.instanceId}][${this.sessionId}]: Newest mseq from LIVE=${currentMseqRaw} First mseq in store=${leadersFirstSeqCounts.liveSourceMseqCount}`); - if (currentMseqRaw === leadersFirstSeqCounts.liveSourceMseqCount) { - this.pushAmount = 1; // Follower from start - } else { - // TODO: To support and account for past discontinuity tags in the Live Source stream, - // we will need to get the real 'current' discontinuity-sequence count from Leader somehow. + if (currentMseqRaw === leadersFirstSeqCounts.liveSourceMseqCount) { + this.pushAmount = 1; // Follower from start + } else { + // TODO: To support and account for past discontinuity tags in the Live Source stream, + // we will need to get the real 'current' discontinuity-sequence count from Leader somehow. - // RESPAWNED NODES - this.pushAmount = currentMseqRaw - leadersFirstSeqCounts.liveSourceMseqCount + 1; + // RESPAWNED NODES + this.pushAmount = currentMseqRaw - leadersFirstSeqCounts.liveSourceMseqCount + 1; - const transitSegs = await this.sessionLiveState.get("transitSegs"); - //debug(`[${this.sessionId}]: NEW FOLLOWER: I tried to get 'transitSegs'. This is what I found ${JSON.stringify(transitSegs)}`); - if (!this._isEmpty(transitSegs)) { - this.vodSegments = transitSegs; + const transitSegs = await this.sessionLiveState.get("transitSegs"); + //debug(`[${this.sessionId}]: NEW FOLLOWER: I tried to get 'transitSegs'. This is what I found ${JSON.stringify(transitSegs)}`); + if (!this._isEmpty(transitSegs)) { + this.vodSegments = transitSegs; + } + if (audioTracksExist) { + const transitSegsAudio = await this.sessionLiveState.get("transitSegsAudio"); + if (!this._isEmpty(transitSegsAudio)) { + this.vodSegmentsAudio = transitSegsAudio; + } + } } - } - debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmount}`); - } else { - // LEADER calculates pushAmount differently... - if (this.firstTime) { - this.pushAmount = 1; // Leader from start + debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmount}`); } else { - this.pushAmount = currentMseqRaw - this.lastRequestedMediaSeqRaw; - debug(`[${this.sessionId}]: ...calculating pushAmount=${currentMseqRaw}-${this.lastRequestedMediaSeqRaw}=${this.pushAmount}`); + // LEADER calculates pushAmount differently... + if (this.firstTime) { + this.pushAmount = 1; // Leader from start + } else { + this.pushAmount = currentMseqRaw - this.lastRequestedMediaSeqRaw; + debug(`[${this.sessionId}]: ...calculating pushAmount=${currentMseqRaw}-${this.lastRequestedMediaSeqRaw}=${this.pushAmount}`); + } + debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmount}`); + break; } - debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmount}`); + // Live Source Data is in sync, and LEADER & new FOLLOWER are in sync break; } - // Live Source Data is in sync, and LEADER & new FOLLOWER are in sync - break; - } - - if (FETCH_ATTEMPTS === 0) { - debug(`[${this.sessionId}]: Fetching from Live-Source did not work! Returning to Playhead Loop...`); - return; - } - - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - // NEW FOLLOWER - Edge Case: One Instance is ahead of another. Read latest live segs from store - if (!isLeader) { - const leadersCurrentMseqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); - const counts = await this.sessionLiveState.get("firstCounts"); - const leadersFirstMseqRaw = counts.liveSourceMseqCount; - if (leadersCurrentMseqRaw !== null && leadersCurrentMseqRaw > currentMseqRaw) { - // if leader never had any segs from prev mseq - if (leadersFirstMseqRaw !== null && leadersFirstMseqRaw === leadersCurrentMseqRaw) { - // Follower updates it's manifest ingedients (segment holders & counts) - this.lastRequestedMediaSeqRaw = leadersCurrentMseqRaw; - this.liveSegsForFollowers = await this.sessionLiveState.get("liveSegsForFollowers"); - debug(`[${this.sessionId}]: NEW FOLLOWER: Leader is ahead or behind me! Clearing Queue and Getting latest segments from store.`); - this._updateLiveSegQueue(); - this.firstTime = false; - debug(`[${this.sessionId}]: Got all needed segments from live-source (read from store).\nWe are now able to build Live Manifest: [${this.mediaSeqCount}]`); - return; - } else if (leadersCurrentMseqRaw < this.lastRequestedMediaSeqRaw) { - // WE ARE A RESPAWN-NODE, and we are ahead of leader. - this.blockGenerateManifest = true; - } - } - } - if (this.allowedToSet) { - // Collect and Push Segment-Extracting Promises - let pushPromises = []; - for (let i = 0; i < Object.keys(this.mediaManifestURIs).length; i++) { - let bw = Object.keys(this.mediaManifestURIs)[i]; - // will add new segments to live seg queue - pushPromises.push(this._parseMediaManifest(this.liveSourceM3Us[bw].M3U, this.mediaManifestURIs[bw], bw, isLeader)); - debug(`[${this.sessionId}]: Pushed pushPromise for bw=${bw}`); - } - // Segment Pushing - debug(`[${this.sessionId}]: Executing Promises II: Segment Pushing`); - await allSettled(pushPromises); - - // UPDATE COUNTS, & Shift Segments in vodSegments and liveSegQueue if needed. - const leaderORFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; - const newTotalDuration = this._incrementAndShift(leaderORFollower); - if (newTotalDuration) { - debug(`[${this.sessionId}]: New Adjusted Playlist Duration=${newTotalDuration}s`); - } - } - - // ----------------------------------------------------- - // Leader writes to store so that Followers can read. - // ----------------------------------------------------- - if (isLeader) { - if (this.allowedToSet) { - const liveBws = Object.keys(this.liveSegsForFollowers); - const segListSize = this.liveSegsForFollowers[liveBws[0]].length; - // Do not replace old data with empty data - if (segListSize > 0) { - debug(`[${this.sessionId}]: LEADER: Adding data to store!`); - await this.sessionLiveState.set("lastRequestedMediaSeqRaw", this.lastRequestedMediaSeqRaw); - await this.sessionLiveState.set("liveSegsForFollowers", this.liveSegsForFollowers); - } - } - - // [LASTLY]: LEADER does this for respawned-FOLLOWERS' sake. - if (this.firstTime && this.allowedToSet) { - // Buy some time for followers (NOT Respawned) to fetch their own L.S m3u8. - await timer(1000); // maybe remove - let firstCounts = await this.sessionLiveState.get("firstCounts"); - firstCounts.liveSourceMseqCount = this.lastRequestedMediaSeqRaw; - firstCounts.mediaSeqCount = this.prevMediaSeqCount; - firstCounts.discSeqCount = this.prevDiscSeqCount; - - debug(`[${this.sessionId}]: LEADER: I am adding 'firstCounts'=${JSON.stringify(firstCounts)} to Store for future followers`); - await this.sessionLiveState.set("firstCounts", firstCounts); - } - debug(`[${this.sessionId}]: LEADER: I am using segs from Mseq=${this.lastRequestedMediaSeqRaw}`); - } else { - debug(`[${this.sessionId}]: NEW FOLLOWER: I am using segs from Mseq=${this.lastRequestedMediaSeqRaw}`); + return { + success: FETCH_ATTEMPTS ? true : false, + currentMseqRaw: currentMseqRaw, + }; + } catch (e) { + console.error(e); + return Promise.reject(e); } - - this.firstTime = false; - debug(`[${this.sessionId}]: Got all needed segments from live-source (from all bandwidths).\nWe are now able to build Live Manifest: [${this.mediaSeqCount}]`); - - return; } - async _loadAllAudioManifests() { - debug(`[${this.sessionId}]: Attempting to load all audio manifest URIs in=${Object.keys(this.audioManifestURIs[Object.keys(this.audioManifestURIs)[0]])}`); - let currentMseqRaw = null; - // ------------------------------------- - // If I am a Follower-node then my job - // ends here, where I only read from store. - // ------------------------------------- - let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (!isLeader && this.lastRequestedAudioSeqRaw !== null) { - debug(`[${this.sessionId}]: FOLLOWER: Reading data from store!`); - - let leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); + async _parseFromLiveSource(current_mediasequence_raw) { + try { + // --------------------------------- + // PARSE M3U's FROM LIVE-SOURCE + // --------------------------------- + let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + const audioTracksExist = Object.keys(this.audioManifestURIs).length > 0 ? true : false; + // NEW FOLLOWER - Edge Case: One Instance is ahead of another. Read latest live segs from store + if (!isLeader) { + const leadersCurrentMseqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); + const counts = await this.sessionLiveState.get("firstCounts"); + const leadersFirstMseqRaw = counts.liveSourceMseqCount; + if (leadersCurrentMseqRaw !== null && leadersCurrentMseqRaw > current_mediasequence_raw) { + // if leader never had any segs from prev mseq + if (leadersFirstMseqRaw !== null && leadersFirstMseqRaw === leadersCurrentMseqRaw) { + // Follower updates it's manifest ingedients (segment holders & counts) + this.lastRequestedMediaSeqRaw = leadersCurrentMseqRaw; + this.liveSegsForFollowers = await this.sessionLiveState.get("liveSegsForFollowers"); + if (audioTracksExist) { + this.liveSegsForFollowersAudio = await this.sessionLiveState.get("liveSegsForFollowersAudio"); + } - if (!leadersAudioSeqRaw < this.lastRequestedAudioSeqRaw && this.blockGenerateManifest) { - this.blockGenerateManifest = false; - } + debug(`[${this.sessionId}]: NEW FOLLOWER: Leader is ahead or behind me! Clearing Queue and Getting latest segments from store.`); + this._updateLiveSegQueue(); + if (audioTracksExist) { + this._updateLiveSegQueueAudio(); + } - let attempts = 10; - // CHECK AGAIN CASE 1: Store Empty - while (!leadersAudioSeqRaw && attempts > 0) { - if (!leadersAudioSeqRaw) { - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.instanceId}]: I'm the new leader`); + this.firstTime = false; + debug( + `[${this.sessionId}]: Got all needed segments from live-source (read from store).\nWe are now able to build Live Manifest: [${this.mediaSeqCount}]` + ); return; + } else if (leadersCurrentMseqRaw < this.lastRequestedMediaSeqRaw) { + // WE ARE A RESPAWN-NODE, and we are ahead of leader. + this.blockGenerateManifest = true; } } - - if (!this.allowedToSet) { - debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); - break; - } - const segDur = this._getAnyFirstAudioSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; - const waitTimeMs = parseInt(segDur / 3, 10); - debug(`[${this.sessionId}]: FOLLOWER: Leader has not put anything in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`); - await timer(waitTimeMs); - this.timerCompensation = false; - leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); - attempts--; - } - - if (!leadersAudioSeqRaw) { - debug(`[${this.instanceId}]: The leader is still alive`); - return; } + if (this.allowedToSet) { + // Collect and Push Segment-Extracting Promises + let pushPromises = []; + for (let i = 0; i < Object.keys(this.mediaManifestURIs).length; i++) { + let bw = Object.keys(this.mediaManifestURIs)[i]; + // will add new segments to live seg queue + pushPromises.push(this._parseMediaManifest(this.liveSourceM3Us[bw].M3U, this.mediaManifestURIs[bw], bw, isLeader)); + debug(`[${this.sessionId}]: Pushed pushPromise for bw=${bw}`); + } + // Collect and Push Segment-Extracting Promises (audio) + for (let i = 0; i < Object.keys(this.audioManifestURIs).length; i++) { + let at = Object.keys(this.audioManifestURIs)[i]; + // will add new segments to live seg queue + pushPromises.push(this._parseAudioManifest(this.liveSourceM3Us[at].M3U, this.audioManifestURIs[at], at, isLeader)); + debug(`[${this.sessionId}]: Pushed pushPromise for audiotrack=${at}`); + } + // Segment Pushing + debug(`[${this.sessionId}]: Executing Promises II: Segment Pushing`); + await allSettled(pushPromises); - let liveAudioSegsInStore = await this.sessionLiveState.get("liveAudioSegsForFollowers"); - attempts = 10; - // CHECK AGAIN CASE 2: Store Old - while ((leadersAudioSeqRaw <= this.lastRequestedAudioSeqRaw && attempts > 0) || (this._containsAudioSegment(this.liveAudioSegsForFollowers, liveAudioSegsInStore) && attempts > 0)) { - if (!this.allowedToSet) { - debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); - break; + // UPDATE COUNTS, & Shift Segments in vodSegments and liveSegQueue if needed. + const leaderORFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; + const newTotalDuration = this._incrementAndShift(leaderORFollower); + if (audioTracksExist) { + this._incrementAndShiftAudio(leaderORFollower); } - if (leadersAudioSeqRaw <= this.lastRequestedAudioSeqRaw) { - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.instanceId}][${this.sessionId}]: I'm the new leader`); - return; - } + if (newTotalDuration) { + debug(`[${this.sessionId}]: New Adjusted Playlist Duration=${newTotalDuration}s`); } - if (this._containsAudioSegment(this.liveAudioSegsForFollowers, liveAudioSegsInStore)) { - debug(`[${this.sessionId}]: FOLLOWER: _containsSegment=true,${leadersAudioSeqRaw},${this.lastRequestedAudioSeqRaw}`); - } - const segDur = this._getAnyFirstAudioSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; - const waitTimeMs = parseInt(segDur / 3, 10); - debug(`[${this.sessionId}]: FOLLOWER: Cannot find anything NEW in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`); - await timer(waitTimeMs); - this.timerCompensation = false; - leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); - liveAudioSegsInStore = await this.sessionLiveState.get("liveAudioSegsForFollowers"); - attempts--; - } - // FINALLY - if (leadersAudioSeqRaw <= this.lastRequestedAudioSeqRaw) { - debug(`[${this.instanceId}][${this.sessionId}]: The leader is still alive`); - return; } - // Follower updates its manifest building blocks (segment holders & counts) - this.lastRequestedAudioSeqRaw = leadersAudioSeqRaw; - this.liveAudioSegsForFollowers = liveAudioSegsInStore; - debug(`[${this.sessionId}]: These are the segments from store: [${JSON.stringify(this.liveAudioSegsForFollowers)}]`); - this._updateLiveAudioSegQueue(); - return; - } - // --------------------------------- - // FETCHING FROM LIVE-SOURCE - New Followers (once) & Leaders do this. - // --------------------------------- - let FETCH_ATTEMPTS = 10; - this.liveAudioSegsForFollowers = {}; - let groupLangToSkipOnRetry = []; - while (FETCH_ATTEMPTS > 0) { + // ----------------------------------------------------- + // Leader writes to store so that Followers can read. + // ----------------------------------------------------- if (isLeader) { - debug(`[${this.sessionId}]: LEADER: Trying to fetch manifests for all groups and language\n Attempts left=[${FETCH_ATTEMPTS}]`); - } else { - debug(`[${this.sessionId}]: NEW FOLLOWER: Trying to fetch manifests for all groups and language\n Attempts left=[${FETCH_ATTEMPTS}]`); - } - - if (!this.allowedToSet) { - debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Live-Source`); - break; - } - - // Reset Values Each Attempt - let livePromises = []; - let manifestList = []; - this.pushAmountAudio = 0; - try { - if (groupLangToSkipOnRetry.length > 0) { - debug(`[${this.sessionId}]: (X) Skipping loadAudio promises for bws ${JSON.stringify(groupLangToSkipOnRetry)}`); - } - // Collect Live Source Requesting Promises - const groupIds = Object.keys(this.audioManifestURIs) - for (let i = 0; i < groupIds.length; i++) { - let groupId = groupIds[i]; - let langs = Object.keys(this.audioManifestURIs[groupId]); - for (let j = 0; j < langs.length; j++) { - const lang = langs[j]; - if (groupLangToSkipOnRetry.includes(groupId + lang)) { - continue; + if (this.allowedToSet) { + const liveBws = Object.keys(this.liveSegsForFollowers); + const segListSize = this.liveSegsForFollowers[liveBws[0]].length; + // Do not replace old data with empty data + if (segListSize > 0) { + debug(`[${this.sessionId}]: LEADER: Adding data to store!`); + await this.sessionLiveState.set("lastRequestedMediaSeqRaw", this.lastRequestedMediaSeqRaw); + await this.sessionLiveState.set("liveSegsForFollowers", this.liveSegsForFollowers); + if (audioTracksExist) { + await this.sessionLiveState.set("liveSegsForFollowersAudio", this.liveSegsForFollowersAudio); } - livePromises.push(this._loadAudioManifest(groupId, lang)); - debug(`[${this.sessionId}]: Pushed loadAudio promise for groupId,lang=[${groupId}, ${lang}]`); } } - // Fetch From Live Source - debug(`[${this.sessionId}]: Executing Promises I: Fetch From Live Audio Source`); - manifestList = await allSettled(livePromises); - livePromises = []; - } catch (err) { - debug(`[${this.sessionId}]: Promises I: FAILURE!\n${err}`); - return; - } - - // Handle if any promise got rejected - if (manifestList.some((result) => result.status === "rejected")) { - FETCH_ATTEMPTS--; - debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again in 1000ms...`); - await timer(1000); - continue; - } - - // Store the results locally - manifestList.forEach((variantItem) => { - const groupId = variantItem.value.groupId; - const lang = variantItem.value.lang; - if (!this.liveAudioSourceM3Us[groupId]) { - this.liveAudioSourceM3Us[groupId] = {}; - } - if (!this.liveAudioSourceM3Us[groupId][lang]) { - this.liveAudioSourceM3Us[groupId][lang] = {}; - } - this.liveAudioSourceM3Us[groupId][lang] = variantItem.value; - }); - const allStoredAudioSeqCounts = [];//Object.keys(this.liveAudioSourceM3Us).map((variant) => this.liveSourceM3Us[variant].mediaSeq); + // [LASTLY]: LEADER does this for respawned-FOLLOWERS' sake. + if (this.firstTime && this.allowedToSet) { + // Buy some time for followers (NOT Respawned) to fetch their own L.S m3u8. + await timer(1000); // maybe remove + let firstCounts = await this.sessionLiveState.get("firstCounts"); + firstCounts.liveSourceMseqCount = this.lastRequestedMediaSeqRaw; + firstCounts.mediaSeqCount = this.prevMediaSeqCount; + firstCounts.discSeqCount = this.prevDiscSeqCount; - const groupIds = Object.keys(this.liveAudioSourceM3Us) - for (let i = 0; i < groupIds.length; i++) { - const langs = Object.keys(this.liveAudioSourceM3Us[groupIds[i]]); - for (let j = 0; j < langs.length; j++) { - allStoredAudioSeqCounts.push(this.liveAudioSourceM3Us[groupIds[i]][langs[j]].mediaSeq); + debug(`[${this.sessionId}]: LEADER: I am adding 'firstCounts'=${JSON.stringify(firstCounts)} to Store for future followers`); + await this.sessionLiveState.set("firstCounts", firstCounts); } + debug(`[${this.sessionId}]: LEADER: I am using segs from Mseq=${this.lastRequestedMediaSeqRaw}`); + } else { + debug(`[${this.sessionId}]: NEW FOLLOWER: I am using segs from Mseq=${this.lastRequestedMediaSeqRaw}`); } - // Handle if mediaSeqCounts are NOT synced up! - if (!allStoredAudioSeqCounts.every((val, i, arr) => val === arr[0])) { - debug(`[${this.sessionId}]: Live audio Mseq counts=[${allStoredAudioSeqCounts}]`); - // Figure out what group lang is behind. - const highestMediaSeqCount = Math.max(...allStoredAudioSeqCounts); - const gi = Object.keys(this.liveAudioSourceM3Us) - for (let i = 0; i < gi.length; i++) { - const langs = Object.keys(this.liveAudioSourceM3Us[groupIds[i]]); - for (let j = 0; j < langs.length; j++) { - if (this.liveSourceM3Us[gi[i]][langs[j]].mediaSeq === highestMediaSeqCount) { - groupLangToSkipOnRetry.push(gi[i] + langs[j]) - } - } - } + this.firstTime = false; + debug( + `[${this.sessionId}]: Got all needed segments from live-source (from all bandwidths).\nWe are now able to build Live Manifest: [${this.mediaSeqCount}]` + ); - // Decrement fetch counter - FETCH_ATTEMPTS--; - // Calculate retry delay time. Default=1000 - let retryDelayMs = 1000; - if (Object.keys(this.liveAudioSegQueue).length > 0) { - const firstGroupId = Object.keys(this.liveAudioSegQueue)[0]; - const firstLang = Object.keys(this.liveAudioSegQueue[firstGroupId])[0]; - const lastIdx = this.liveAudioSegQueue[firstGroupId][firstLang].length - 1; - if (this.liveAudioSegQueue[firstGroupId][lastIdx].duration) { - retryDelayMs = this.liveAudioSegQueue[firstGroupId][lastIdx].duration * 1000 * 0.25; - } - } - // Wait a little before trying again - debug(`[${this.sessionId}]: ALERT! Live Source Data NOT in sync! Will try again after ${retryDelayMs}ms`); - await timer(retryDelayMs); - if (isLeader) { - this.timerCompensation = false; - } - continue; - } - - currentMseqRaw = allStoredAudioSeqCounts[0]; - - if (!isLeader) { - let leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); - let tries = 20; - - while ((!isLeader && !leadersFirstSeqCounts.liveSourceAudioMseqCount && tries > 0) || leadersFirstSeqCounts.liveAudioSourceMseqCount === 0) { - debug(`[${this.sessionId}]: NEW FOLLOWER: Waiting for LEADER to add 'firstCounts' in store! Will look again after 1000ms (tries left=${tries})`); - await timer(1000); - leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); - tries--; - // Might take over as Leader if Leader is not setting data due to being down. - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); - } - } - - if (tries === 0) { - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); - break; - } else { - debug(`[${this.sessionId}][${this.instanceId}]: The leader is still alive`); - leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); - if (!leadersFirstSeqCounts.liveSourceMseqCount) { - debug(`[${this.sessionId}][${this.instanceId}]: Could not find 'firstCounts' in store. Abort Executing Promises II & Returning to Playhead.`); - return; - } - } - } - - if (isLeader) { - debug(`[${this.sessionId}]: NEW LEADER: Original Leader went missing, I am retrying live source fetch...`); - await this.sessionLiveState.set("transitAudioSegs", this.vodAudioSegments); - debug(`[${this.sessionId}]: NEW LEADER: I am adding 'transitSegs' to Store for future followers`); - continue; - } - - // Respawners never do this, only starter followers. - // Edge Case: FOLLOWER transitioned from session with different segments from LEADER - if (leadersFirstSeqCounts.discSeqCount !== this.audioDiscSeqCount) { - this.audioDiscSeqCount = leadersFirstSeqCounts.discSeqCount; - } - if (leadersFirstSeqCounts.audioSeqCount !== this.audioSeqCount) { - this.audioSeqCount = leadersFirstSeqCounts.audioSeqCount; - debug( - `[${this.sessionId}]: FOLLOWER transitioned with wrong V2L segments, updating counts to [${this.audioSeqCount}][${this.audioDiscSeqCount}], and reading 'transitSegs' from store` - ); - const transitSegs = await this.sessionLiveState.get("transitAudioSegs"); - if (!this._isEmpty(transitSegs)) { - this.vodAudioSegments = transitSegs; - } - } - - // Prepare to load segments... - debug(`[${this.instanceId}][${this.sessionId}]: Newest mseq from LIVE=${currentMseqRaw} First mseq in store=${leadersFirstSeqCounts.liveAudioSourceMseqCount}`); - if (currentMseqRaw === leadersFirstSeqCounts.liveAudioSourceMseqCount) { - this.pushAmountAudio = 1; // Follower from start - } else { - // TODO: To support and account for past discontinuity tags in the Live Source stream, - // we will need to get the real 'current' discontinuity-sequence count from Leader somehow. - - // RESPAWNED NODES - this.pushAmountAudio = currentMseqRaw - leadersFirstSeqCounts.liveAudioSourceMseqCount + 1; - - const transitSegs = await this.sessionLiveState.get("transitAudioSegs"); - //debug(`[${this.sessionId}]: NEW FOLLOWER: I tried to get 'transitSegs'. This is what I found ${JSON.stringify(transitSegs)}`); - if (!this._isEmpty(transitSegs)) { - this.vodAudioSegments = transitSegs; - } - } - debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmountAudio}`); - } else { - // LEADER calculates pushAmount differently... - if (this.firstTimeAudio) { - this.pushAmountAudio = 1; // Leader from start - } else { - this.pushAmountAudio = currentMseqRaw - this.lastRequestedAudioSeqRaw; - debug(`[${this.sessionId}]: ...calculating pushAmount=${currentMseqRaw}-${this.lastRequestedAudioSeqRaw}=${this.pushAmountAudio}`); - } - debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmountAudio}`); - break; - } - // Live Source Data is in sync, and LEADER & new FOLLOWER are in sync - break; - } - - if (FETCH_ATTEMPTS === 0) { - debug(`[${this.sessionId}]: Fetching from Live-Source did not work! Returning to Playhead Loop...`); return; + } catch (e) { + console.error(e); + return Promise.reject(e); } - - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - // NEW FOLLOWER - Edge Case: One Instance is ahead of another. Read latest live segs from store - if (!isLeader) { - const leadersCurrentMseqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); - const counts = await this.sessionLiveState.get("firstCounts"); - const leadersFirstMseqRaw = counts.liveSourceAudioMseqCount; - if (leadersCurrentMseqRaw !== null && leadersCurrentMseqRaw > currentMseqRaw) { - // if leader never had any segs from prev mseq - if (leadersFirstMseqRaw !== null && leadersFirstMseqRaw === leadersCurrentMseqRaw) { - // Follower updates it's manifest ingedients (segment holders & counts) - this.lastRequestedAudioSeqRaw = leadersCurrentMseqRaw; - this.liveAudioSegsForFollowers = await this.sessionLiveState.get("liveAudioSegsForFollowers"); - debug(`[${this.sessionId}]: NEW FOLLOWER: Leader is ahead or behind me! Clearing Queue and Getting latest segments from store.`); - this._updateLiveAudioSegQueue(); - this.firstTimeAudio = false; - debug(`[${this.sessionId}]: Got all needed segments from live-source (read from store).\nWe are now able to build Audio Live Manifest: [${this.audioSeqCount}]`); - return; - } else if (leadersCurrentMseqRaw < this.lastRequestedAudioSeqRaw) { - // WE ARE A RESPAWN-NODE, and we are ahead of leader. - this.blockGenerateManifest = true; - } - } - } - if (this.allowedToSet) { - // Collect and Push Segment-Extracting Promises - let pushPromises = []; - - for (let i = 0; i < Object.keys(this.audioManifestURIs).length; i++) { - let groupId = Object.keys(this.audioManifestURIs)[i]; - let langs = Object.keys(this.audioManifestURIs[groupId]); - for (let j = 0; j < langs.length; j++) { - let lang = langs[j]; - - // will add new segments to live seg queue - pushPromises.push(this._parseAudioManifest(this.liveAudioSourceM3Us[groupId][lang].M3U, this.audioManifestURIs[groupId][lang], groupId, lang, isLeader)); - debug(`[${this.sessionId}]: Pushed pushPromise for groupId=${groupId} & lang${lang}`); + } + /** + * This function adds new live segments to the node from which it can + * generate new manifests from. Method for attaining new segments differ + * depending on node Rank. The Leader collects from live source and + * Followers collect from shared storage. + */ + async _loadAllPlaylistManifests() { + try { + let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (!isLeader && this.lastRequestedMediaSeqRaw !== null) { + // FOLLWERS Do this + await this._collectSegmentsFromStore(); + } else { + // LEADERS and NEW-FOLLOWERS Do this + const result = await this._fetchFromLiveSource(); + if (result.success) { + await this._parseFromLiveSource(result.currentMseqRaw); } } - // Segment Pushing - debug(`[${this.sessionId}]: Executing Promises II: Segment Pushing`); - await allSettled(pushPromises); - - // UPDATE COUNTS, & Shift Segments in vodSegments and liveSegQueue if needed. - const leaderORFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; - const newTotalDuration = this._incrementAndShiftAudio(leaderORFollower); // might need audio - if (newTotalDuration) { - debug(`[${this.sessionId}]: New Adjusted Playlist Duration=${newTotalDuration}s`); - } - } - - // ----------------------------------------------------- - // Leader writes to store so that Followers can read. - // ----------------------------------------------------- - if (isLeader) { - if (this.allowedToSet) { - const liveGroupIds = Object.keys(this.liveAudioSegsForFollowers); - const liveLangs = Object.keys(this.liveAudioSegsForFollowers[liveGroupIds[0]]); - const segListSize = this.liveAudioSegsForFollowers[liveGroupIds[0]][liveLangs[0]].length; - // Do not replace old data with empty data - if (segListSize > 0) { - debug(`[${this.sessionId}]: LEADER: Adding data to store!`); - await this.sessionLiveState.set("lastRequestedAudioSeqRaw", this.lastRequestedAudioSeqRaw); - await this.sessionLiveState.set("liveAudioSegsForFollowers", this.liveAudioSegsForFollowers); - } - } - - // [LASTLY]: LEADER does this for respawned-FOLLOWERS' sake. - if (this.firstTimeAudio && this.allowedToSet) { - // Buy some time for followers (NOT Respawned) to fetch their own L.S m3u8. - await timer(1000); // maybe remove - let firstCounts = await this.sessionLiveState.get("firstCounts"); - firstCounts.liveSourceAudioMseqCount = this.lastRequestedAudioSeqRaw; - firstCounts.audioSeqCount = this.prevAudioSeqCount; - firstCounts.discAudioSeqCount = this.prevAudioDiscSeqCount; - - debug(`[${this.sessionId}]: LEADER: I am adding 'firstCounts'=${JSON.stringify(firstCounts)} to Store for future followers`); - await this.sessionLiveState.set("firstCounts", firstCounts); - } - debug(`[${this.sessionId}]: LEADER: I am using segs from Mseq=${this.lastRequestedAudioSeqRaw}`); - } else { - debug(`[${this.sessionId}]: NEW FOLLOWER: I am using segs from Mseq=${this.lastRequestedAudioSeqRaw}`); + return; + } catch (e) { + console.error("Failure in _loadAllPlaylistManifests:" + e); } - - this.firstTimeAudio = false; - debug(`[${this.sessionId}]: Got all needed segments from live-source (from all groupIds and langs).\nWe are now able to build Audi Live Manifest: [${this.audioSeqCount}]`); - - return; } _shiftSegments(opt) { @@ -1606,22 +1408,24 @@ class SessionLive { if (opt && opt.type) { _type = opt.type; } - const bws = Object.keys(_segments); - + const variantKeys = Object.keys(_segments); /* When Total Duration is past the Limit, start Shifting V2L|LIVE segments if found */ while (_totalDur > TARGET_PLAYLIST_DURATION_SEC) { let result = null; - if (_type === "VIDEO") { - result = this._shiftMediaSegments(bws, _name, _segments, _totalDur); - } else { - result = this._shiftAudioSegments(bws, _name, _segments, _totalDur); - } + result = this._shiftVariantSegments(variantKeys, _name, _segments); // Skip loop if there are no more segments to remove... if (!result) { - return { totalDuration: _totalDur, removedSegments: _removedSegments, removedDiscontinuities: _removedDiscontinuities, shiftedSegments: _segments }; - } - debug(`[${this.sessionId}]: ${_name}: (${_totalDur})s/(${TARGET_PLAYLIST_DURATION_SEC})s - Playlist Duration is Over the Target. Shift needed!`); + return { + totalDuration: _totalDur, + removedSegments: _removedSegments, + removedDiscontinuities: _removedDiscontinuities, + shiftedSegments: _segments, + }; + } + debug( + `[${this.sessionId}]: ${_name}: (${_totalDur})s/(${TARGET_PLAYLIST_DURATION_SEC})s - Playlist Duration is Over the Target. Shift needed!` + ); _segments = result.segments; if (result.timeToRemove) { _totalDur -= result.timeToRemove; @@ -1633,28 +1437,33 @@ class SessionLive { _removedDiscontinuities++; } } - return { totalDuration: _totalDur, removedSegments: _removedSegments, removedDiscontinuities: _removedDiscontinuities, shiftedSegments: _segments }; + return { + totalDuration: _totalDur, + removedSegments: _removedSegments, + removedDiscontinuities: _removedDiscontinuities, + shiftedSegments: _segments, + }; } - _shiftMediaSegments(bws, _name, _segments) { - if (_segments[bws[0]].length === 0) { + _shiftVariantSegments(variantKeys, _name, _segments) { + if (_segments[variantKeys[0]].length === 0) { return null; } let timeToRemove = 0; let incrementDiscSeqCount = false; // Shift Segments for each variant... - for (let i = 0; i < bws.length; i++) { - let seg = _segments[bws[i]].shift(); + for (let i = 0; i < variantKeys.length; i++) { + let seg = _segments[variantKeys[i]].shift(); if (i === 0) { - debug(`[${this.sessionId}]: ${_name}: (${bws[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); + debug(`[${this.sessionId}]: ${_name}: (${variantKeys[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); } if (seg && seg.discontinuity) { incrementDiscSeqCount = true; - if (_segments[bws[i]].length > 0) { - seg = _segments[bws[i]].shift(); + if (_segments[variantKeys[i]].length > 0) { + seg = _segments[variantKeys[i]].shift(); if (i === 0) { - debug(`[${this.sessionId}]: ${_name}: (${bws[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); + debug(`[${this.sessionId}]: ${_name}: (${variantKeys[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); } } } @@ -1662,40 +1471,7 @@ class SessionLive { timeToRemove = seg.duration; } } - return { timeToRemove: timeToRemove, incrementDiscSeqCount: incrementDiscSeqCount, segments: _segments } - } - - _shiftAudioSegments(groupIds, _name, _segments) { - const firstLang = Object.keys(_segments[groupIds[0]])[0]; - if (_segments[groupIds[0]][firstLang].length === 0) { - return null; - } - let timeToRemove = 0; - let incrementDiscSeqCount = false; - - // Shift Segments for each variant... - for (let i = 0; i < groupIds.length; i++) { - const langs = Object.keys(_segments[groupIds[i]]); - for (let j = 0; j < langs.length; j++) { - let seg = _segments[groupIds[i]][langs[j]].shift(); - if (i === 0) { - debug(`[${this.sessionId}]: ${_name}: (${groupIds[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); - } - if (seg && seg.discontinuity) { - incrementDiscSeqCount = true; - if (_segments[groupIds[i]][langs[j]].length > 0) { - seg = _segments[groupIds[i]][langs[j]].shift(); - if (i === 0) { - debug(`[${this.sessionId}]: ${_name}: (${groupIds[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); - } - } - } - if (seg && seg.duration) { - timeToRemove = seg.duration; - } - } - } - return { timeToRemove: timeToRemove, incrementDiscSeqCount: incrementDiscSeqCount, segments: _segments } + return { timeToRemove: timeToRemove, incrementDiscSeqCount: incrementDiscSeqCount, segments: _segments }; } /** @@ -1786,10 +1562,8 @@ class SessionLive { if (!instanceName) { instanceName = "UNKNOWN"; } - const vodGroupId = Object.keys(this.vodAudioSegments)[0]; - const vodLanguage = Object.keys(this.vodAudioSegments[vodGroupId])[0]; - const liveGroupId = Object.keys(this.liveAudioSegQueue)[0]; - const liveLanguage = Object.keys(this.liveAudioSegQueue[vodGroupId])[0]; + const vodAudiotrack = Object.keys(this.vodSegmentsAudio); + const liveAudiotrack = Object.keys(this.liveSegQueueAudio); let vodTotalDur = 0; let liveTotalDur = 0; let totalDur = 0; @@ -1797,12 +1571,12 @@ class SessionLive { let removedDiscontinuities = 0; // Calculate Playlist Total Duration - this.vodAudioSegments[vodGroupId][vodLanguage].forEach((seg) => { + this.vodSegmentsAudio[vodAudiotrack[0]].forEach((seg) => { if (seg.duration) { vodTotalDur += seg.duration; } }); - this.liveAudioSegQueue[liveGroupId][liveLanguage].forEach((seg) => { + this.liveSegQueueAudio[liveAudiotrack[0]].forEach((seg) => { if (seg.duration) { liveTotalDur += seg.duration; } @@ -1816,13 +1590,13 @@ class SessionLive { const outputV2L = this._shiftSegments({ name: instanceName, totalDur: totalDur, - segments: this.vodAudioSegments, + segments: this.vodSegmentsAudio, removedSegments: removedSegments, removedDiscontinuities: removedDiscontinuities, type: "AUDIO", }); // Update V2L Segments - this.vodAudioSegments = outputV2L.shiftedSegments; + this.vodSegmentsAudio = outputV2L.shiftedSegments; // Update values totalDur = outputV2L.totalDuration; removedSegments = outputV2L.removedSegments; @@ -1831,13 +1605,13 @@ class SessionLive { const outputLIVE = this._shiftSegments({ name: instanceName, totalDur: totalDur, - segments: this.liveAudioSegQueue, + segments: this.liveSegQueueAudio, removedSegments: removedSegments, removedDiscontinuities: removedDiscontinuities, type: "AUDIO", }); // Update LIVE Segments - this.liveAudioSegQueue = outputLIVE.shiftedSegments; + this.liveSegQueueAudio = outputLIVE.shiftedSegments; // Update values totalDur = outputLIVE.totalDuration; removedSegments = outputLIVE.removedSegments; @@ -1909,50 +1683,53 @@ class SessionLive { }); } - async _loadAudioManifest(groupId, lang) { - if (!this.sessionLiveState) { - throw new Error("SessionLive not ready"); - } - const liveTargetGroupLang = this._findAudioGroupAndLang(groupId, lang, this.audioManifestURIs); - debug(`[${this.sessionId}]: Requesting groupId=(${groupId}) & lang=(${lang}), Nearest match is: ${JSON.stringify(liveTargetGroupLang)}`); - // Get the target media manifest - const audioManifestUri = this.audioManifestURIs[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage]; - const parser = m3u8.createStream(); - const controller = new AbortController(); - const timeout = setTimeout(() => { - debug(`[${this.sessionId}]: Request Timeout! Aborting Request to ${audioManifestUri}`); - controller.abort(); - }, FAIL_TIMEOUT); - - const response = await fetch(audioManifestUri, { signal: controller.signal }); + async _loadAudioManifest(audiotrack) { try { - response.body.pipe(parser); + if (!this.sessionLiveState) { + throw new Error("SessionLive not ready"); + } + const liveTargetAudiotrack = this._findNearestAudiotrack(audiotrack, Object.keys(this.audioManifestURIs)); + debug(`[${this.sessionId}]: Requesting audiotrack (${audiotrack}), Nearest match is: ${JSON.stringify(liveTargetAudiotrack)}`); + // Get the target media manifest + const audioManifestUri = this.audioManifestURIs[liveTargetAudiotrack]; + const parser = m3u8.createStream(); + const controller = new AbortController(); + const timeout = setTimeout(() => { + debug(`[${this.sessionId}]: Request Timeout! Aborting Request to ${audioManifestUri}`); + controller.abort(); + }, FAIL_TIMEOUT); + const response = await fetch(audioManifestUri, { signal: controller.signal }); + try { + response.body.pipe(parser); + } catch (err) { + debug(`[${this.sessionId}]: Error when piping response to parser! ${JSON.stringify(err)}`); + return Promise.reject(err); + } finally { + clearTimeout(timeout); + } + return new Promise((resolve, reject) => { + parser.on("m3u", (m3u) => { + try { + const resolveObj = { + M3U: m3u, + mediaSeq: m3u.get("mediaSequence"), + audiotrack: liveTargetAudiotrack, + }; + resolve(resolveObj); + } catch (exc) { + debug(`[${this.sessionId}]: Error when parsing latest manifest`); + reject(exc); + } + }); + parser.on("error", (exc) => { + debug(`Parser Error: ${JSON.stringify(exc)}`); + reject(exc); + }); + }); } catch (err) { - debug(`[${this.sessionId}]: Error when piping response to parser! ${JSON.stringify(err)}`); + console.error(err); return Promise.reject(err); - } finally { - clearTimeout(timeout); } - return new Promise((resolve, reject) => { - parser.on("m3u", (m3u) => { - try { - const resolveObj = { - M3U: m3u, - mediaSeq: m3u.get("mediaSequence"), - groupId: liveTargetGroupLang.audioGroupId, - lang: liveTargetGroupLang.audioLanguage, - }; - resolve(resolveObj); - } catch (exc) { - debug(`[${this.sessionId}]: Error when parsing latest manifest`); - reject(exc); - } - }); - parser.on("error", (exc) => { - debug(`Parser Error: ${JSON.stringify(exc)}`); - reject(exc); - }); - }); } _parseMediaManifest(m3u, mediaManifestUri, liveTargetBandwidth, isLeader) { @@ -1984,7 +1761,7 @@ class SessionLive { } if (mediaManifestUri) { // push segments - this._addLiveSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetBandwidth, isLeader); + this._addLiveSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetBandwidth, isLeader, PlaylistTypes.VIDEO); } resolve(); } catch (exc) { @@ -1994,20 +1771,14 @@ class SessionLive { }); } - _parseAudioManifest(m3u, audioPlaylistUri, liveTargetGroupId, liveTargetLanguage, isLeader) { + _parseAudioManifest(m3u, audioPlaylistUri, liveTargetAudiotrack, isLeader) { return new Promise(async (resolve, reject) => { try { - if (!this.liveAudioSegQueue[liveTargetGroupId]) { - this.liveAudioSegQueue[liveTargetGroupId] = {}; + if (!this.liveSegQueueAudio[liveTargetAudiotrack]) { + this.liveSegQueueAudio[liveTargetAudiotrack] = []; } - if (!this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage]) { - this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage] = []; - } - if (!this.liveAudioSegsForFollowers[liveTargetGroupId]) { - this.liveAudioSegsForFollowers[liveTargetGroupId] = {}; - } - if (!this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage]) { - this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage] = []; + if (!this.liveSegsForFollowersAudio[liveTargetAudiotrack]) { + this.liveSegsForFollowersAudio[liveTargetAudiotrack] = []; } let baseUrl = ""; const m = audioPlaylistUri.match(/^(.*)\/.*?$/); @@ -2017,18 +1788,22 @@ class SessionLive { //debug(`[${this.sessionId}]: Current RAW Mseq: [${m3u.get("mediaSequence")}]`); //debug(`[${this.sessionId}]: Previous RAW Mseq: [${this.lastRequestedAudioSeqRaw}]`); - - if (this.pushAmountAudio >= 0) { - this.lastRequestedAudioSeqRaw = m3u.get("mediaSequence"); + + /* + WARN: We are assuming here that the MSEQ and Segment lengths are the same on Audio and Video + and therefor need to push an equal amount of segments + */ + if (this.pushAmount >= 0) { + this.lastRequestedMediaSeqRaw = m3u.get("mediaSequence"); } this.targetDuration = m3u.get("targetDuration"); - let startIdx = m3u.items.PlaylistItem.length - this.pushAmountAudio; + let startIdx = m3u.items.PlaylistItem.length - this.pushAmount; if (startIdx < 0) { - this.restAmountAudio = startIdx * -1; + this.restAmount = startIdx * -1; startIdx = 0; } if (audioPlaylistUri) { - this._addLiveAudioSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetGroupId, liveTargetLanguage, isLeader); + this._addLiveSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetAudiotrack, isLeader, PlaylistTypes.AUDIO); } resolve(); } catch (exc) { @@ -2046,169 +1821,174 @@ class SessionLive { * @param {string} baseUrl * @param {string} liveTargetBandwidth */ - _addLiveSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetBandwidth, isLeader) { - const leaderOrFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; - - for (let i = startIdx; i < playlistItems.length; i++) { - let seg = {}; - let playlistItem = playlistItems[i]; - let segmentUri; - let cueData = null; - let daterangeData = null; - let attributes = playlistItem["attributes"].attributes; - if (playlistItem.properties.discontinuity) { - this.liveSegQueue[liveTargetBandwidth].push({ discontinuity: true }); - this.liveSegsForFollowers[liveTargetBandwidth].push({ discontinuity: true }); - } - if ("cuein" in attributes) { - if (!cueData) { - cueData = {}; + _addLiveSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetVariant, isLeader, plType) { + try { + const leaderOrFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; + for (let i = startIdx; i < playlistItems.length; i++) { + let seg = {}; + const playlistItem = playlistItems[i]; + let segmentUri; + let byteRange = undefined; + let initSegment = undefined; + let initSegmentByteRange = undefined; + let keys = undefined; + let daterangeData = null; + if (i === startIdx) { + for (let j = startIdx; j >= 0; j--) { + const pli = playlistItems[j]; + if (pli.get("map-uri")) { + initSegmentByteRange = pli.get("map-byterange"); + if (pli.get("map-uri").match("^http")) { + initSegment = pli.get("map-uri"); + } else { + initSegment = urlResolve(baseUrl, pli.get("map-uri")); + } + break; + } + } } - cueData["in"] = true; - } - if ("cueout" in attributes) { - if (!cueData) { - cueData = {}; + let attributes = playlistItem["attributes"].attributes; + if (playlistItem.get("map-uri")) { + initSegmentByteRange = playlistItem.get("map-byterange"); + if (playlistItem.get("map-uri").match("^http")) { + initSegment = playlistItem.get("map-uri"); + } else { + initSegment = urlResolve(baseUrl, playlistItem.get("map-uri")); + } } - cueData["out"] = true; - cueData["duration"] = attributes["cueout"]; - } - if ("cuecont" in attributes) { - if (!cueData) { - cueData = {}; + // some items such as CUE-IN parse as a PlaylistItem + // but have no URI + if (playlistItem.get("uri")) { + if (playlistItem.get("uri").match("^http")) { + segmentUri = playlistItem.get("uri"); + } else { + segmentUri = urlResolve(baseUrl, playlistItem.get("uri")); + } } - cueData["cont"] = true; - } - if ("scteData" in attributes) { - if (!cueData) { - cueData = {}; + if (playlistItem.get("discontinuity")) { + if (plType === PlaylistTypes.VIDEO) { + this.liveSegQueue[liveTargetVariant].push({ discontinuity: true }); + this.liveSegsForFollowers[liveTargetVariant].push({ discontinuity: true }); + } else if (plType === PlaylistTypes.AUDIO) { + this.liveSegQueueAudio[liveTargetVariant].push({ discontinuity: true }); + this.liveSegsForFollowersAudio[liveTargetVariant].push({ discontinuity: true }); + } else { + console.warn(`[${this.sessionId}]: WARNING: plType=${plType} Not valid (disc-seg)`); + } } - cueData["scteData"] = attributes["scteData"]; - } - if ("assetData" in attributes) { - if (!cueData) { - cueData = {}; + if (playlistItem.get("byteRange")) { + let [_, r, o] = playlistItem.get("byteRange").match(/^(\d+)@*(\d*)$/); + if (!o) { + o = byteRangeOffset; + } + byteRangeOffset = parseInt(r) + parseInt(o); + byteRange = `${r}@${o}`; + } + if (playlistItem.get("keys")) { + keys = playlistItem.get("keys"); + } + let assetData = playlistItem.get("assetdata"); + let cueOut = playlistItem.get("cueout"); + let cueIn = playlistItem.get("cuein"); + let cueOutCont = playlistItem.get("cont-offset"); + let duration = 0; + let scteData = playlistItem.get("sctedata"); + if (typeof cueOut !== "undefined") { + duration = cueOut; + } else if (typeof cueOutCont !== "undefined") { + duration = playlistItem.get("cont-dur"); + } + let cue = + cueOut || cueIn || cueOutCont || assetData + ? { + out: typeof cueOut !== "undefined", + cont: typeof cueOutCont !== "undefined" ? cueOutCont : null, + scteData: typeof scteData !== "undefined" ? scteData : null, + in: cueIn ? true : false, + duration: duration, + assetData: typeof assetData !== "undefined" ? assetData : null, + } + : null; + seg = { + duration: playlistItem.get("duration"), + timelinePosition: this.timeOffset != null ? this.timeOffset + timelinePosition : null, + cue: cue, + byteRange: byteRange, + }; + if (initSegment) { + seg.initSegment = initSegment; + } + if (initSegmentByteRange) { + seg.initSegmentByteRange = initSegmentByteRange; + } + if (segmentUri) { + seg.uri = segmentUri; + } + if (keys) { + seg.keys = keys; + } + if (i === startIdx) { + // Add daterange metadata if this is the first segment + if (this.rangeMetadata && !this._isEmpty(this.rangeMetadata)) { + seg["daterange"] = this.rangeMetadata; + } } - cueData["assetData"] = attributes["assetData"]; - } - if ("daterange" in attributes) { - if (!daterangeData) { - daterangeData = {}; + if ("daterange" in attributes) { + if (!daterangeData) { + daterangeData = {}; + } + let allDaterangeAttributes = Object.keys(attributes["daterange"]); + allDaterangeAttributes.forEach((attr) => { + if (attr.match(/DURATION$/)) { + daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]); + } else { + daterangeData[attr.toLowerCase()] = attributes["daterange"][attr]; + } + }); } - let allDaterangeAttributes = Object.keys(attributes["daterange"]); - allDaterangeAttributes.forEach((attr) => { - if (attr.match(/DURATION$/)) { - daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]); + if (playlistItem.get("uri")) { + if (daterangeData && !this._isEmpty(daterangeData)) { + seg["daterange"] = daterangeData; + } + // Push new Live Segments! But do not push duplicates + if (plType === PlaylistTypes.VIDEO) { + this._pushToQueue(seg, liveTargetVariant, leaderOrFollower); + } else if (plType === PlaylistTypes.AUDIO) { + this._pushToQueueAudio(seg, liveTargetVariant, leaderOrFollower); } else { - daterangeData[attr.toLowerCase()] = attributes["daterange"][attr]; + console.warn(`[${this.sessionId}]: WARNING: plType=${plType} Not valid (seg)`); } - }); - } - if (playlistItem.properties.uri) { - if (playlistItem.properties.uri.match("^http")) { - segmentUri = playlistItem.properties.uri; - } else { - segmentUri = url.resolve(baseUrl, playlistItem.properties.uri); - } - seg["duration"] = playlistItem.properties.duration; - seg["uri"] = segmentUri; - seg["cue"] = cueData; - if (daterangeData) { - seg["daterange"] = daterangeData; - } - // Push new Live Segments! But do not push duplicates - const liveSegURIs = this.liveSegQueue[liveTargetBandwidth].filter((seg) => seg.uri).map((seg) => seg.uri); - if (seg.uri && liveSegURIs.includes(seg.uri)) { - debug(`[${this.sessionId}]: ${leaderOrFollower}: Found duplicate live segment. Skip push! (${liveTargetBandwidth})`); - } else { - this.liveSegQueue[liveTargetBandwidth].push(seg); - this.liveSegsForFollowers[liveTargetBandwidth].push(seg); - debug(`[${this.sessionId}]: ${leaderOrFollower}: Pushed segment (${seg.uri ? seg.uri : "Disc-tag"}) to 'liveSegQueue' (${liveTargetBandwidth})`); } } + } catch (e) { + console.error(e); + return Promise.reject(e); } } - _addLiveAudioSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetGroupId, liveTargetLanguage, isLeader) { - const leaderOrFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; - for (let i = startIdx; i < playlistItems.length; i++) { - let seg = {}; - let playlistItem = playlistItems[i]; - let segmentUri; - let cueData = null; - let daterangeData = null; - let attributes = playlistItem["attributes"].attributes; - if (playlistItem.properties.discontinuity) { - this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage].push({ discontinuity: true }); - this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage].push({ discontinuity: true }); - } - if ("cuein" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["in"] = true; - } - if ("cueout" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["out"] = true; - cueData["duration"] = attributes["cueout"]; - } - if ("cuecont" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["cont"] = true; - } - if ("scteData" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["scteData"] = attributes["scteData"]; - } - if ("assetData" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["assetData"] = attributes["assetData"]; - } - if ("daterange" in attributes) { - if (!daterangeData) { - daterangeData = {}; - } - let allDaterangeAttributes = Object.keys(attributes["daterange"]); - allDaterangeAttributes.forEach((attr) => { - if (attr.match(/DURATION$/)) { - daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]); - } else { - daterangeData[attr.toLowerCase()] = attributes["daterange"][attr]; - } - }); - } + _pushToQueue(seg, liveTargetBandwidth, logName) { + const liveSegURIs = this.liveSegQueue[liveTargetBandwidth].filter((seg) => seg.uri).map((seg) => seg.uri); + if (seg.uri && liveSegURIs.includes(seg.uri)) { + debug(`[${this.sessionId}]: ${logName}: Found duplicate live segment. Skip push! (${liveTargetBandwidth})`); + } else { + this.liveSegQueue[liveTargetBandwidth].push(seg); + this.liveSegsForFollowers[liveTargetBandwidth].push(seg); + debug(`[${this.sessionId}]: ${logName}: Pushed Video segment (${seg.uri ? seg.uri : "Disc-tag"}) to 'liveSegQueue' (${liveTargetBandwidth})`); + } + } - if (playlistItem.properties.uri) { - if (playlistItem.properties.uri.match("^http")) { - segmentUri = playlistItem.properties.uri; - } else { - segmentUri = url.resolve(baseUrl, playlistItem.properties.uri); - } - seg["duration"] = playlistItem.properties.duration; - seg["uri"] = segmentUri; - seg["cue"] = cueData; - if (daterangeData) { - seg["daterange"] = daterangeData; - } - // Push new Live Segments! But do not push duplicates - const liveSegURIs = this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage].filter((seg) => seg.uri).map((seg) => seg.uri); - if (seg.uri && liveSegURIs.includes(seg.uri)) { - debug(`[${this.sessionId}]: ${leaderOrFollower}: Found duplicate live segment. Skip push! (${liveTargetGroupId, liveTargetLanguage})`); - } else { - this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage].push(seg); - this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage].push(seg); - debug(`[${this.sessionId}]: ${leaderOrFollower}: Pushed segment (${seg.uri ? seg.uri : "Disc-tag"}) to 'liveSegQueue' (${liveTargetGroupId, liveTargetLanguage})`); - } - } + _pushToQueueAudio(seg, liveTargetAudiotrack, logName) { + const liveSegURIs = this.liveSegQueueAudio[liveTargetAudiotrack].filter((seg) => seg.uri).map((seg) => seg.uri); + if (seg.uri && liveSegURIs.includes(seg.uri)) { + debug(`[${this.sessionId}]: ${logName}: Found duplicate live segment. Skip push! track -> (${liveTargetAudiotrack})`); + } else { + this.liveSegQueueAudio[liveTargetAudiotrack].push(seg); + this.liveSegsForFollowersAudio[liveTargetAudiotrack].push(seg); + debug( + `[${this.sessionId}]: ${logName}: Pushed Audio segment (${ + seg.uri ? seg.uri : "Disc-tag" + }) to 'liveSegQueue' track -> (${liveTargetAudiotrack})` + ); } } @@ -2249,7 +2029,10 @@ class SessionLive { */ // DO NOT GENERATE MANIFEST CASE: Node has not found anything in store OR Node has not even check yet. - if (Object.keys(this.liveSegQueue).length === 0 || (this.liveSegQueue[liveTargetBandwidth] && this.liveSegQueue[liveTargetBandwidth].length === 0)) { + if ( + Object.keys(this.liveSegQueue).length === 0 || + (this.liveSegQueue[liveTargetBandwidth] && this.liveSegQueue[liveTargetBandwidth].length === 0) + ) { debug(`[${this.sessionId}]: Cannot Generate Manifest! <${this.instanceId}> Not yet collected ANY segments from Live Source...`); return null; } @@ -2286,9 +2069,9 @@ class SessionLive { if (Object.keys(this.vodSegments).length !== 0) { // Add transitional segments if there are any left. debug(`[${this.sessionId}]: Adding a Total of (${this.vodSegments[vodTargetBandwidth].length}) VOD segments to manifest`); - m3u8 = this._setMediaManifestTags(this.vodSegments, m3u8, vodTargetBandwidth); + m3u8 = this._setVariantManifestTags(this.vodSegments, m3u8, vodTargetBandwidth); // Add live-source segments - m3u8 = this._setMediaManifestTags(this.liveSegQueue, m3u8, liveTargetBandwidth); + m3u8 = this._setVariantManifestTags(this.liveSegQueue, m3u8, liveTargetBandwidth); } debug(`[${this.sessionId}]: Manifest Generation Complete!`); return m3u8; @@ -2301,54 +2084,53 @@ class SessionLive { if (audioLanguage === null) { throw new Error("No audioLanguage provided"); } - const liveTargetTrackIds = this._findAudioGroupAndLang(audioGroupId, audioLanguage, this.audioManifestURIs); - const vodTargetTrackIds = this._findAudioGroupAndLang(audioGroupId, audioLanguage, this.vodAudioSegments); - debug(`[${this.sessionId}]: Client requesting manifest for VodTrackInfo=(${JSON.stringify(vodTargetTrackIds)}). Nearest LiveTrackInfo=(${JSON.stringify(liveTargetTrackIds)})`); + const liveTargetTrack = this._findNearestAudiotrack( + this._getTrackFromGroupAndLang(audioGroupId, audioLanguage), + Object.keys(this.audioManifestURIs) + ); + const vodTargetTrack = this._findNearestAudiotrack( + this._getTrackFromGroupAndLang(audioGroupId, audioLanguage), + Object.keys(this.vodSegmentsAudio) + ); + debug( + `[${this.sessionId}]: Client requesting manifest for VodTrackInfo=(${JSON.stringify(vodTargetTrack)}). Nearest LiveTrackInfo=(${JSON.stringify( + liveTargetTrack + )})` + ); if (this.blockGenerateManifest) { debug(`[${this.sessionId}]: FOLLOWER: Cannot Generate Audio Manifest! Waiting to sync-up with Leader...`); return null; } - // DO NOT GENERATE MANIFEST CASE: Node has not found anything in store OR Node has not even check yet. - if (Object.keys(this.liveAudioSegQueue).length === 0 || - (this.liveAudioSegQueue[liveTargetTrackIds.audioGroupId] && - this.liveAudioSegQueue[liveTargetTrackIds.audioGroupId][liveTargetTrackIds.audioLanguage] && - this.liveAudioSegQueue[liveTargetTrackIds.audioGroupId][liveTargetTrackIds.audioLanguage].length === 0) - ) { + if (Object.keys(this.liveSegQueueAudio).length === 0 || this.liveSegQueueAudio[liveTargetTrack].length === 0) { debug(`[${this.sessionId}]: Cannot Generate Audio Manifest! <${this.instanceId}> Not yet collected ANY segments from Live Source...`); return null; } // DO NOT GENERATE MANIFEST CASE: Node is in the middle of gathering segs of all variants. - const groupIds = Object.keys(this.liveAudioSegQueue); + const tracks = Object.keys(this.liveSegQueueAudio); let segAmounts = []; - for (let i = 0; i < groupIds.length; i++) { - const groupId = groupIds[i]; - const langs = Object.keys(this.liveAudioSegQueue[groupId]); - for (let j = 0; j < langs.length; j++) { - const lang = langs[j]; - if (this.liveAudioSegQueue[groupId][lang].length !== 0) { - - segAmounts.push(this.liveAudioSegQueue[groupId][lang].length); - } + for (let i = 0; i < tracks.length; i++) { + const track = tracks[i]; + if (this.liveSegQueueAudio[track].length !== 0) { + segAmounts.push(this.liveSegQueueAudio[track].length); } } - if (!segAmounts.every((val, i, arr) => val === arr[0])) { console(`[${this.sessionId}]: Cannot Generate audio Manifest! <${this.instanceId}> Not yet collected ALL segments from Live Source...`); return null; } - if (!this._isEmpty(this.liveAudioSegQueue) && this.liveAudioSegQueue[groupIds[0]][Object.keys(this.liveAudioSegQueue[groupIds[0]])[0]].length !== 0) { - this.targetDuration = this._getMaxDuration(this.liveAudioSegQueue[groupIds[0]][Object.keys(this.liveAudioSegQueue[groupIds[0]])[0]]); + if (!this._isEmpty(this.liveSegQueueAudio) && this.liveSegQueueAudio[tracks[0]].length !== 0) { + this.targetDuration = this._getMaxDuration(this.liveSegQueueAudio[tracks[0]]); } // Determine if VOD segments influence targetDuration - for (let i = 0; i < this.vodAudioSegments[vodTargetTrackIds.audioGroupId][vodTargetTrackIds.audioLanguage].length; i++) { - let vodSeg = this.vodAudioSegments[vodTargetTrackIds.audioGroupId][vodTargetTrackIds.audioLanguage][i]; + for (let i = 0; i < this.vodSegmentsAudio[vodTargetTrack].length; i++) { + let vodSeg = this.vodSegmentsAudio[vodTargetTrack][i]; // Get max duration amongst segments if (vodSeg.duration > this.targetDuration) { this.targetDuration = vodSeg.duration; @@ -2363,78 +2145,38 @@ class SessionLive { m3u8 += "#EXT-X-TARGETDURATION:" + Math.round(this.targetDuration) + "\n"; m3u8 += "#EXT-X-MEDIA-SEQUENCE:" + this.audioSeqCount + "\n"; m3u8 += "#EXT-X-DISCONTINUITY-SEQUENCE:" + this.audioDiscSeqCount + "\n"; - if (Object.keys(this.vodAudioSegments).length !== 0) { + if (Object.keys(this.vodSegmentsAudio).length !== 0) { // Add transitional segments if there are any left. - debug(`[${this.sessionId}]: Adding a Total of (${this.vodAudioSegments[vodTargetTrackIds.audioGroupId][vodTargetTrackIds.audioLanguage].length}) VOD audio segments to manifest`); - m3u8 = this._setAudioManifestTags(this.vodAudioSegments, m3u8, vodTargetTrackIds); + debug(`[${this.sessionId}]: Adding a Total of (${this.vodSegmentsAudio[vodTargetTrack].length}) VOD audio segments to manifest`); + m3u8 = this._setVariantManifestTags(this.vodSegmentsAudio, m3u8, vodTargetTrack); // Add live-source segments - m3u8 = this._setAudioManifestTags(this.liveAudioSegQueue, m3u8, liveTargetTrackIds); + m3u8 = this._setVariantManifestTags(this.liveSegQueueAudio, m3u8, liveTargetTrack); } debug(`[${this.sessionId}]: Audio manifest Generation Complete!`); return m3u8; } - _setMediaManifestTags(segments, m3u8, bw) { - for (let i = 0; i < segments[bw].length; i++) { - const seg = segments[bw][i]; - m3u8 += this._setTagsOnSegment(seg, m3u8) - } - return m3u8 - } - - _setAudioManifestTags(segments, m3u8, trackIds) { - for (let i = 0; i < segments[trackIds.audioGroupId][trackIds.audioLanguage].length; i++) { - const seg = segments[trackIds.audioGroupId][trackIds.audioLanguage][i]; - m3u8 += this._setTagsOnSegment(seg, m3u8) - } - return m3u8 - } - - _setTagsOnSegment(segment) { - let m3u8 = ""; - if (segment.discontinuity) { - m3u8 += "#EXT-X-DISCONTINUITY\n"; - } - if (segment.cue) { - if (segment.cue.out) { - if (segment.cue.scteData) { - m3u8 += "#EXT-OATCLS-SCTE35:" + segment.cue.scteData + "\n"; - } - if (segment.cue.assetData) { - m3u8 += "#EXT-X-ASSET:" + segment.cue.assetData + "\n"; - } - m3u8 += "#EXT-X-CUE-OUT:DURATION=" + segment.cue.duration + "\n"; - } - if (segment.cue.cont) { - if (segment.cue.scteData) { - m3u8 += "#EXT-X-CUE-OUT-CONT:ElapsedTime=" + segment.cue.cont + ",Duration=" + segment.cue.duration + ",SCTE35=" + segment.cue.scteData + "\n"; - } else { - m3u8 += "#EXT-X-CUE-OUT-CONT:" + segment.cue.cont + "/" + segment.cue.duration + "\n"; + _setVariantManifestTags(segments, m3u8, variantKey) { + let previousSeg = null; + const size = segments[variantKey].length; + for (let i = 0; i < size; i++) { + const seg = segments[variantKey][i]; + const nextSeg = segments[variantKey][i + 1]; + if (seg.discontinuity && nextSeg && nextSeg.discontinuity) { + nextSeg.discontinuity = false; + } + if (seg.discontinuity && !seg.cue) { + // Avoid printing duplicate disc-tags + if (!m3u8.endsWith("#EXT-X-DISCONTINUITY\n")) { + if (!nextSeg || !nextSeg.discontinuity) { + m3u8 += "#EXT-X-DISCONTINUITY\n"; + } } } - } - if (segment.datetime) { - m3u8 += `#EXT-X-PROGRAM-DATE-TIME:${segment.datetime}\n`; - } - if (segment.daterange) { - const dateRangeAttributes = Object.keys(segment.daterange) - .map((key) => daterangeAttribute(key, segment.daterange[key])) - .join(","); - if (!segment.datetime && segment.daterange["start-date"]) { - m3u8 += "#EXT-X-PROGRAM-DATE-TIME:" + segment.daterange["start-date"] + "\n"; - } - m3u8 += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n"; - } - // Mimick logic used in hls-vodtolive - if (segment.cue && segment.cue.in) { - m3u8 += "#EXT-X-CUE-IN" + "\n"; - } - if (segment.uri) { - m3u8 += "#EXTINF:" + segment.duration.toFixed(3) + ",\n"; - m3u8 += segment.uri + "\n"; + m3u8 += segToM3u8(seg, i, size, nextSeg, previousSeg); + previousSeg = seg; } return m3u8; } - _findNearestBw(bw, array) { const sorted = array.sort((a, b) => b - a); return sorted.reduce((a, b) => { @@ -2467,34 +2209,8 @@ class SessionLive { debug(`[${this.sessionId}]: ERROR Could not find any bandwidth with segments`); return null; } - - _getFirstAudioGroupWithSegments(array) { - const audioGroupIds = Object.keys(array).filter((id) => { - let idLangs = Object.keys(array[id]).filter((lang) => { - return array[id][lang].length > 0; - }); - return idLangs.length > 0; - }); - if (audioGroupIds.length > 0) { - return audioGroupIds[0]; - } else { - return null; - } - } - - _getFirstAudioLanguageWithSegments(groupId, array) { - const langsWithSegments = Object.keys(array[groupId]).filter((lang) => { - return array[groupId][lang].length > 0; - }); - if (langsWithSegments.length > 0) { - return langsWithSegments[0]; - } else { - return null; - } - } - _findAudioGroupsForLang(audioLanguage, segments) { - let trackInfos = [] + let trackInfos = []; const groupIds = Object.keys(segments); for (let i = 0; i < groupIds.length; i++) { const groupId = groupIds[i]; @@ -2502,8 +2218,7 @@ class SessionLive { for (let j = 0; j < langs.length; j++) { const lang = langs[j]; if (lang === audioLanguage) { - - trackInfos.push({ audioGroupId: groupId, audioLanguage: lang }) + trackInfos.push({ audioGroupId: groupId, audioLanguage: lang }); break; } } @@ -2511,33 +2226,33 @@ class SessionLive { return trackInfos; } - _findAudioGroupAndLang(audioGroupId, audioLanguage, array) { - if (audioGroupId === null || !array[audioGroupId]) { - audioGroupId = this._getFirstAudioGroupWithSegments(array); - if (!audioGroupId) { - return []; + _findNearestAudiotrack(track, tracks) { + // perfect match + if (tracks.includes(track)) { + return track; + } + let tracksMatchingOnLanguage = tracks.filter((t) => { + if (this._getLangFromTrack(t) === track) { + return true; } + return false; + }); + // If any matches, then it implies that no group ID matches, so use a fallback (first) group + if (tracksMatchingOnLanguage.length > 0) { + return tracksMatchingOnLanguage[0]; } - if (!array[audioGroupId][audioLanguage]) { - const fallbackLang = this._getFirstAudioLanguageWithSegments(audioGroupId, array); - if (!fallbackLang) { - if (Object.keys(array[audioGroupId]).length > 0) { - return { - "audioGroupId": audioGroupId, - "audioLanguage": Object.keys(array[audioGroupId])[0], - }; - } + // If no matches then check if we have any matched on group id, then use fallback (first) language + let tracksMatchingOnGroupId = tracks.filter((t) => { + if (this._getLangFromTrack(t) === track) { + return true; } - return { - "audioGroupId": audioGroupId, - "audioLanguage": fallbackLang - }; + return false; + }); + if (tracksMatchingOnGroupId.length > 0) { + return tracksMatchingOnGroupId[0]; } - return { - "audioGroupId": audioGroupId, - "audioLanguage": audioLanguage - }; - + // No groupId or language matches the target, use fallback (first) track + return tracks[0]; } _getMaxDuration(segments) { @@ -2569,8 +2284,21 @@ class SessionLive { newItem[bw] = this.mediaManifestURIs[bw]; }); this.mediaManifestURIs = newItem; - - + } + _filterLiveProfilesAudio() { + const tracks = this.sessionAudioTracks.map((trackItem) => { + return this._getTrackFromGroupAndLang(trackItem.groupId, trackItem.language); + }); + const toKeep = new Set(); + let newItem = {}; + tracks.forEach((t) => { + let atToKeep = this._findNearestAudiotrack(t, Object.keys(this.audioManifestURIs)); + toKeep.add(atToKeep); + }); + toKeep.forEach((at) => { + newItem[at] = this.audioManifestURIs[at]; + }); + this.audioManifestURIs = newItem; } _filterLiveAudioTracks() { @@ -2584,14 +2312,16 @@ class SessionLive { }); toKeep.forEach((trackInfo) => { - if (!newItemsAudio[trackInfo.audioGroupId]) { - newItemsAudio[trackInfo.audioGroupId] = {} + if (trackInfo) { + if (!newItemsAudio[trackInfo.audioGroupId]) { + newItemsAudio[trackInfo.audioGroupId] = {}; + } + newItemsAudio[trackInfo.audioGroupId][trackInfo.audioLanguage] = this.audioManifestURIs[trackInfo.audioGroupId][trackInfo.audioLanguage]; } - newItemsAudio[trackInfo.audioGroupId][trackInfo.audioLanguage] = this.audioManifestURIs[trackInfo.audioGroupId][trackInfo.audioLanguage]; - }); - - this.audioManifestURIs = newItemsAudio; + if (!this._isEmpty(newItemsAudio)) { + this.audioManifestURIs = newItemsAudio; + } } _getAnyFirstSegmentDurationMs() { @@ -2646,6 +2376,67 @@ class SessionLive { } return false; } + + _getGroupAndLangFromTrack(track) { + const GLItem = { + groupId: null, + language: null, + }; + const match = track.match(/g:(.*?),l:(.*)/); + if (match) { + const g = match[1]; + const l = match[2]; + if (g && l) { + GLItem.groupId = g; + GLItem.language = l; + return GLItem; + } + } + console.error(`Failed to extract GroupID and Language g=${g};l=${l}`); + return GLItem; + } + + _getLangFromTrack(track) { + const match = track.match(/g:(.*?),l:(.*)/); + if (match) { + const g = match[1]; + const l = match[2]; + if (g && l) { + return l; + } + } + console.error(`Failed to extract Language g=${g};l=${l}`); + return null; + } + + _getGroupFromTrack(track) { + const match = track.match(/g:(.*?),l:(.*)/); + if (match) { + const g = match[1]; + const l = match[2]; + if (g && l) { + return g; + } + } + console.error(`Failed to extract Group ID g=${g};l=${l}`); + return null; + } + + _getTrackFromGroupAndLang(g, l) { + return `g:${g},l:${l}`; + } + + _isBandwidth(bw) { + if (typeof bw === "number") { + return true; + } else if (typeof bw === "string") { + const parsedNumber = parseFloat(bw); + if (!isNaN(parsedNumber)) { + return true; + } + } + return false; + } } module.exports = SessionLive; diff --git a/engine/stream_switcher.js b/engine/stream_switcher.js index 64b0c73e..4bd80757 100644 --- a/engine/stream_switcher.js +++ b/engine/stream_switcher.js @@ -1,6 +1,7 @@ const debug = require("debug")("engine-stream-switcher"); const crypto = require("crypto"); const fetch = require("node-fetch"); +const url = require("url"); const { AbortController } = require("abort-controller"); const { SessionState } = require("./session_state"); const { timer, findNearestValue, isValidUrl, fetchWithRetry, findAudioGroupOrLang } = require("./util"); @@ -638,7 +639,7 @@ class StreamSwitcher { if (!prerollSegments[bw]) { prerollSegments[bw] = []; } - prerollSegments[bw] = this._createCustomSimpleSegmentList(mediaM3UPlaylists[bw], bw, null, "video", mediaURIs); + prerollSegments[bw] = this._createCustomSimpleSegmentList(mediaM3UPlaylists[bw], mediaURIs[bw]); } if (this.useDemuxedAudio) { const groupIds = Object.keys(audioM3UPlaylists); @@ -653,7 +654,7 @@ class StreamSwitcher { if (!prerollSegmentsAudio[groupId][lang]) { prerollSegmentsAudio[groupId][lang] = []; } - prerollSegmentsAudio[groupId][lang] = this._createCustomSimpleSegmentList(audioM3UPlaylists[groupId][lang], groupId, lang, "audio", audioURIs); + prerollSegmentsAudio[groupId][lang] = this._createCustomSimpleSegmentList(audioM3UPlaylists[groupId][lang], audioURIs[groupId][lang]); } } } @@ -664,81 +665,118 @@ class StreamSwitcher { } } - _createCustomSimpleSegmentList(segmentList, keyValue1, keyValue2, type, URIs) { + _createCustomSimpleSegmentList(segmentList, manifestURI) { let segments = []; for (let k = 0; k < segmentList.length; k++) { - let seg = {}; - let playlistItem = segmentList[k]; - let segmentUri; - let cueData = null; - let daterangeData = null; - let attributes = playlistItem["attributes"].attributes; - if (playlistItem.properties.discontinuity) { - segments.push({ discontinuity: true }); - } - if ("cuein" in attributes) { - if (!cueData) { - cueData = {}; + try { + let seg = {}; + const playlistItem = segmentList[k]; + let segmentUri; + let byteRange = undefined; + let initSegment = undefined; + let initSegmentByteRange = undefined; + let keys = undefined; + let daterangeData = null; + let attributes = playlistItem["attributes"].attributes; + if (playlistItem.get("map-uri")) { + initSegmentByteRange = playlistItem.get("map-byterange"); + if (playlistItem.get("map-uri").match("^http")) { + initSegment = playlistItem.get("map-uri"); + } else { + initSegment = new URL(playlistItem.get("map-uri"), manifestURI).href; + } } - cueData["in"] = true; - } - if ("cueout" in attributes) { - if (!cueData) { - cueData = {}; + // some items such as CUE-IN parse as a PlaylistItem + // but have no URI + if (playlistItem.get("uri")) { + if (playlistItem.get("uri").match("^http")) { + segmentUri = playlistItem.get("uri"); + } else { + segmentUri = new URL(playlistItem.get("uri"), manifestURI).href; + } } - cueData["out"] = true; - cueData["duration"] = attributes["cueout"]; - } - if ("cuecont" in attributes) { - if (!cueData) { - cueData = {}; + if (playlistItem.get("discontinuity")) { + segments.push({ discontinuity: true }); } - cueData["cont"] = true; - } - if ("scteData" in attributes) { - if (!cueData) { - cueData = {}; + if (playlistItem.get("byteRange")) { + let [_, r, o] = playlistItem.get("byteRange").match(/^(\d+)@*(\d*)$/); + if (!o) { + o = byteRangeOffset; + } + byteRangeOffset = parseInt(r) + parseInt(o); + byteRange = `${r}@${o}`; } - cueData["scteData"] = attributes["scteData"]; - } - if ("assetData" in attributes) { - if (!cueData) { - cueData = {}; + if (playlistItem.get("keys")) { + keys = playlistItem.get("keys"); } - cueData["assetData"] = attributes["assetData"]; - } - if ("daterange" in attributes) { - if (!daterangeData) { - daterangeData = {}; + let assetData = playlistItem.get("assetdata"); + let cueOut = playlistItem.get("cueout"); + let cueIn = playlistItem.get("cuein"); + let cueOutCont = playlistItem.get("cont-offset"); + let duration = 0; + let scteData = playlistItem.get("sctedata"); + if (typeof cueOut !== "undefined") { + duration = cueOut; + } else if (typeof cueOutCont !== "undefined") { + duration = playlistItem.get("cont-dur"); } - let allDaterangeAttributes = Object.keys(attributes["daterange"]); - allDaterangeAttributes.forEach((attr) => { - if (attr.match(/DURATION$/)) { - daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]); - } else { - daterangeData[attr.toLowerCase()] = attributes["daterange"][attr]; + let cue = + cueOut || cueIn || cueOutCont || assetData + ? { + out: typeof cueOut !== "undefined", + cont: typeof cueOutCont !== "undefined" ? cueOutCont : null, + scteData: typeof scteData !== "undefined" ? scteData : null, + in: cueIn ? true : false, + duration: duration, + assetData: typeof assetData !== "undefined" ? assetData : null, + } + : null; + seg = { + duration: playlistItem.get("duration"), + timelinePosition: this.timeOffset != null ? this.timeOffset + timelinePosition : null, + cue: cue, + byteRange: byteRange, + }; + if (initSegment) { + seg.initSegment = initSegment; + } + if (initSegmentByteRange) { + seg.initSegmentByteRange = initSegmentByteRange; + } + if (segmentUri) { + seg.uri = segmentUri; + } + if (keys) { + seg.keys = keys; + } + if (segments.length === 0) { + // Add daterange metadata if this is the first segment + if (this.rangeMetadata && !this._isEmpty(this.rangeMetadata)) { + seg["daterange"] = this.rangeMetadata; } - }); - } - if (playlistItem.properties.uri) { - if (playlistItem.properties.uri.match("^http")) { - segmentUri = playlistItem.properties.uri; - } else { - if (type === "video") { - segmentUri = new URL(playlistItem.properties.uri, URIs[keyValue1]).href; - } else if (type === "audio") { - segmentUri = new URL(playlistItem.properties.uri, URIs[keyValue1][keyValue2]).href; + } + if ("daterange" in attributes) { + if (!daterangeData) { + daterangeData = {}; } + let allDaterangeAttributes = Object.keys(attributes["daterange"]); + allDaterangeAttributes.forEach((attr) => { + if (attr.match(/DURATION$/)) { + daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]); + } else { + daterangeData[attr.toLowerCase()] = attributes["daterange"][attr]; + } + }); } - seg["duration"] = playlistItem.properties.duration; - seg["uri"] = segmentUri; - seg["cue"] = cueData; - if (daterangeData) { - seg["daterange"] = daterangeData; + if (playlistItem.properties.uri) { + if (daterangeData && !this._isEmpty(this.daterangeData)) { + seg["daterange"] = daterangeData; + } } + segments.push(seg); + } catch (e) { + console.error(e); } - segments.push(seg); - } return segments } @@ -860,7 +898,9 @@ class StreamSwitcher { daterangeData[k] = timedMetadata[k]; }); } - segments[bw][0]["daterange"] = daterangeData; + if (Object.keys(daterangeData).length > 0) { + segments[bw][0]["daterange"] = daterangeData; + } }); } @@ -879,7 +919,9 @@ class StreamSwitcher { daterangeData[k] = timedMetadata[k]; }); } - segments[groupId][lang][0]["daterange"] = daterangeData; + if (Object.keys(daterangeData).length > 0) { + segments[groupId][lang][0]["daterange"] = daterangeData; + } } } diff --git a/package-lock.json b/package-lock.json index 277f37c2..f72678fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@eyevinn/hls-repeat": "^0.2.0", "@eyevinn/hls-truncate": "^0.2.0", "@eyevinn/hls-vodtolive": "^4.1.0", - "@eyevinn/m3u8": "^0.5.3", + "@eyevinn/m3u8": "^0.5.6", "abort-controller": "^3.0.0", "debug": "^3.2.7", "memcache-client": "^0.10.1", @@ -34,9 +34,6 @@ "node": ">=14 <20" } }, - "@eyevinn/master": { - "extraneous": true - }, "node_modules/@eyevinn/hls-repeat": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@eyevinn/hls-repeat/-/hls-repeat-0.2.0.tgz", @@ -176,9 +173,9 @@ ] }, "node_modules/@types/node": { - "version": "18.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.5.tgz", - "integrity": "sha512-seOA34WMo9KB+UA78qaJoCO20RJzZGVXQ5Sh6FWu0g/hfT44nKXnej3/tCQl7FL97idFpBhisLYCTB50S0EirA==", + "version": "18.18.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.0.tgz", + "integrity": "sha512-3xA4X31gHT1F1l38ATDIL9GpRLdwVhnEFC8Uikv5ZLlXATwrCYyPq7ZWHxzxc3J/30SUiwiYT+bQe0/XvKlWbw==", "dev": true }, "node_modules/abort-controller": { @@ -220,13 +217,13 @@ } }, "node_modules/array.prototype.map": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.5.tgz", - "integrity": "sha512-gfaKntvwqYIuC7mLLyv2wzZIJqrRhn5PZ9EfFejSx6a78sV7iDsGpG9P+3oUPtm1Rerqm6nrKS4FYuTIvWfo3g==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.6.tgz", + "integrity": "sha512-nK1psgF2cXqP3wSyCSq0Hc7zwNq3sfljQqaG27r/7a7ooNUnn5nGq6yYWyks9jMO5EoFQ0ax80hSg6oXSRNXaw==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-array-method-boxes-properly": "^1.0.0", "is-string": "^1.0.7" }, @@ -237,6 +234,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -404,33 +421,33 @@ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "node_modules/csv": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.8.tgz", - "integrity": "sha512-QGemEqFr5XgbHcufWwO+qnReFhClbd+6WywKDVqEXmRfGrJHl5fRrlphZUnsky2YAwcCxYJJilPUfaWbuBMfWA==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.3.3.tgz", + "integrity": "sha512-TuOM1iZgdDiB6IuwJA8oqeu7g61d9CU9EQJGzCJ1AE03amPSh/UK5BMjAVx+qZUBb/1XEo133WHzWSwifa6Yqw==", "dependencies": { - "csv-generate": "^4.2.2", - "csv-parse": "^5.3.6", - "csv-stringify": "^6.3.0", - "stream-transform": "^3.2.2" + "csv-generate": "^4.2.8", + "csv-parse": "^5.5.0", + "csv-stringify": "^6.4.2", + "stream-transform": "^3.2.8" }, "engines": { "node": ">= 0.1.90" } }, "node_modules/csv-generate": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.2.tgz", - "integrity": "sha512-Ah/NcMxHMqwQsuL173yp8EOzHrbLh8iyScqTy990b+TJZNjHhy7gs5FfSmyQ2arLC2QVrueO3DYJVQnibJB3WQ==" + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.8.tgz", + "integrity": "sha512-qQ5CUs4I58kfo90EDBKjdp0SpJ3xWnN1Xk1lZ1ITvfvMtNRf+jrEP8tNPeEPiI9xJJ6Bd/km/1hMjyYlTpY42g==" }, "node_modules/csv-parse": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.6.tgz", - "integrity": "sha512-WI330GjCuEioK/ii8HM2YE/eV+ynpeLvU+RXw4R8bRU8R0laK5zO3fDsc4gH8s472e3Ga38rbIjCAiQh+tEHkw==" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.0.tgz", + "integrity": "sha512-RxruSK3M4XgzcD7Trm2wEN+SJ26ChIb903+IWxNOcB5q4jT2Cs+hFr6QP39J05EohshRFEvyzEBoZ/466S2sbw==" }, "node_modules/csv-stringify": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.3.0.tgz", - "integrity": "sha512-kTnnBkkLmAR1G409aUdShppWUClNbBQZXhrKrXzKYBGw4yfROspiFvVmjbKonCrdGfwnqwMXKLQG7ej7K/jwjg==" + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.4.2.tgz", + "integrity": "sha512-DXIdnnCUQYjDKTu6TgCSzRDiAuLxDjhl4ErFP9FGMF3wzBGOVMg9bZTLaUcYtuvhXgNbeXPKeaRfpgyqE4xySw==" }, "node_modules/dashdash": { "version": "1.14.1", @@ -451,11 +468,25 @@ "ms": "^2.1.1" } }, + "node_modules/define-data-property": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", + "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -540,17 +571,18 @@ } }, "node_modules/es-abstract": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", + "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", "dependencies": { "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", @@ -565,19 +597,23 @@ "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -710,25 +746,25 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-querystring": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.1.tgz", - "integrity": "sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "node_modules/fast-redact": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.2.tgz", - "integrity": "sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", "engines": { "node": ">=6" } }, "node_modules/find-my-way": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.6.0.tgz", - "integrity": "sha512-H7berWdHJ+5CNVr4ilLWPai4ml7Y2qAsxjw3pfeBxPigZmaDTzF0wjJLj90xRCmGcWYcyt050yN+34OZDJm1eQ==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.6.2.tgz", + "integrity": "sha512-0OjHn1b1nCX3eVbm9ByeEHiscPYiHLfhei1wOUU9qffQkk98wE0Lo8VrVYfSGMgnSnDh86DxedduAnBf4nwUEw==", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", @@ -796,14 +832,14 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -821,12 +857,13 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" }, "funding": { @@ -1292,15 +1329,11 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -1497,9 +1530,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", "optional": true }, "node_modules/negotiator": { @@ -1511,9 +1544,9 @@ } }, "node_modules/nock": { - "version": "13.3.1", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.1.tgz", - "integrity": "sha512-vHnopocZuI93p2ccivFyGuUfzjq2fxNyNurp7816mlT5V5HF4SzXu8lvLrVzBbNqzs+ODooZ6OksuSUNM7Njkw==", + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.3.tgz", + "integrity": "sha512-z+KUlILy9SK/RjpeXDiDUEAq4T94ADPHE3qaRkf66mpEhzc/ytOMm3Bwdrbq6k1tMWkbdujiKim3G2tfQARuJw==", "dependencies": { "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", @@ -1546,9 +1579,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -1671,14 +1704,14 @@ } }, "node_modules/pino": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.11.0.tgz", - "integrity": "sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==", + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.15.1.tgz", + "integrity": "sha512-Cp4QzUQrvWCRJaQ8Lzv0mJzXVk4z2jlq8JNKMGaixC2Pz5L4l2p95TkuRvYbrEbe85NQsDKrAd4zalf7Ml6WiA==", "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "v1.0.0", + "pino-abstract-transport": "v1.1.0", "pino-std-serializers": "^6.0.0", "process-warning": "^2.0.0", "quick-format-unescaped": "^4.0.3", @@ -1692,32 +1725,18 @@ } }, "node_modules/pino-abstract-transport": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", - "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, - "node_modules/pino-abstract-transport/node_modules/readable-stream": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", - "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/pino-std-serializers": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.0.tgz", - "integrity": "sha512-IWgSzUL8X1w4BIWTwErRgtV8PyOGOOi60uqv0oKuS/fOA8Nco/OeI6lBuc4dyP8MMfdFwyHqTMcBIA7nDiqEqA==" + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" }, "node_modules/process": { "version": "0.11.10", @@ -1738,15 +1757,15 @@ "integrity": "sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==" }, "node_modules/promise.allsettled": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.6.tgz", - "integrity": "sha512-22wJUOD3zswWFqgwjNHa1965LvqTX87WPu/lreY2KSd7SVcERfuZ4GfUaOnJNnvtoIv2yXT/W00YIGMetXtFXg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.7.tgz", + "integrity": "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA==", "dependencies": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" }, "engines": { @@ -1799,16 +1818,18 @@ } }, "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">= 6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/real-require": { @@ -1862,13 +1883,13 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" + "set-function-name": "^2.0.0" }, "engines": { "node": ">= 0.4" @@ -2003,9 +2024,9 @@ } }, "node_modules/restify/node_modules/qs": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", - "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dependencies": { "side-channel": "^1.0.4" }, @@ -2017,9 +2038,13 @@ } }, "node_modules/restify/node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -2032,6 +2057,23 @@ "node": ">=4" } }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2097,9 +2139,9 @@ "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" }, "node_modules/semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -2168,6 +2210,19 @@ "node": ">=4" } }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -2187,9 +2242,9 @@ } }, "node_modules/sonic-boom": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.3.0.tgz", - "integrity": "sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.4.0.tgz", + "integrity": "sha512-zSe9QQW30nPzjkSJ0glFQO5T9lHsk39tz+2bAAwCj8CNgEG8ItZiX7Wb2ZgA8I04dwRGCcf1m3ABJa8AYm12Fw==", "dependencies": { "atomic-sleep": "^1.0.0" } @@ -2243,6 +2298,19 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/spdy/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2316,9 +2384,9 @@ } }, "node_modules/stream-transform": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.2.tgz", - "integrity": "sha512-DHZQPNxvjU2qdQlGcpitn8pkJHQVTqdshtgXaLz6Vc5VCAognbGuuwGS5ugeqGVnyw8j4h89QcV8cwm0D1+V0A==" + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.8.tgz", + "integrity": "sha512-NUx0mBuI63KbNEEh9Yj0OzKB7iMOSTpkuODM2G7By+TTVihEIJ0cYp5X+pq/TdJRlsznt6CYR8HqxexyC6/bTw==" }, "node_modules/string_decoder": { "version": "1.3.0", @@ -2329,13 +2397,13 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -2345,35 +2413,35 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/thread-stream": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.3.0.tgz", - "integrity": "sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.0.tgz", + "integrity": "sha512-xZYtOtmnA63zj04Q+F9bdEay5r47bvpo1CaNqsKi7TpoJHcotUez8Fkfo2RJWpW91lnnaApdpRbVwCWsy+ifcw==", "dependencies": { "real-require": "^0.2.0" } @@ -2419,6 +2487,54 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -2542,16 +2658,15 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -2683,9 +2798,9 @@ } }, "@types/node": { - "version": "18.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.5.tgz", - "integrity": "sha512-seOA34WMo9KB+UA78qaJoCO20RJzZGVXQ5Sh6FWu0g/hfT44nKXnej3/tCQl7FL97idFpBhisLYCTB50S0EirA==", + "version": "18.18.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.0.tgz", + "integrity": "sha512-3xA4X31gHT1F1l38ATDIL9GpRLdwVhnEFC8Uikv5ZLlXATwrCYyPq7ZWHxzxc3J/30SUiwiYT+bQe0/XvKlWbw==", "dev": true }, "abort-controller": { @@ -2717,17 +2832,31 @@ } }, "array.prototype.map": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.5.tgz", - "integrity": "sha512-gfaKntvwqYIuC7mLLyv2wzZIJqrRhn5PZ9EfFejSx6a78sV7iDsGpG9P+3oUPtm1Rerqm6nrKS4FYuTIvWfo3g==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.6.tgz", + "integrity": "sha512-nK1psgF2cXqP3wSyCSq0Hc7zwNq3sfljQqaG27r/7a7ooNUnn5nGq6yYWyks9jMO5EoFQ0ax80hSg6oXSRNXaw==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-array-method-boxes-properly": "^1.0.0", "is-string": "^1.0.7" } }, + "arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + } + }, "asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -2843,30 +2972,30 @@ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "csv": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.8.tgz", - "integrity": "sha512-QGemEqFr5XgbHcufWwO+qnReFhClbd+6WywKDVqEXmRfGrJHl5fRrlphZUnsky2YAwcCxYJJilPUfaWbuBMfWA==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.3.3.tgz", + "integrity": "sha512-TuOM1iZgdDiB6IuwJA8oqeu7g61d9CU9EQJGzCJ1AE03amPSh/UK5BMjAVx+qZUBb/1XEo133WHzWSwifa6Yqw==", "requires": { - "csv-generate": "^4.2.2", - "csv-parse": "^5.3.6", - "csv-stringify": "^6.3.0", - "stream-transform": "^3.2.2" + "csv-generate": "^4.2.8", + "csv-parse": "^5.5.0", + "csv-stringify": "^6.4.2", + "stream-transform": "^3.2.8" } }, "csv-generate": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.2.tgz", - "integrity": "sha512-Ah/NcMxHMqwQsuL173yp8EOzHrbLh8iyScqTy990b+TJZNjHhy7gs5FfSmyQ2arLC2QVrueO3DYJVQnibJB3WQ==" + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.8.tgz", + "integrity": "sha512-qQ5CUs4I58kfo90EDBKjdp0SpJ3xWnN1Xk1lZ1ITvfvMtNRf+jrEP8tNPeEPiI9xJJ6Bd/km/1hMjyYlTpY42g==" }, "csv-parse": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.6.tgz", - "integrity": "sha512-WI330GjCuEioK/ii8HM2YE/eV+ynpeLvU+RXw4R8bRU8R0laK5zO3fDsc4gH8s472e3Ga38rbIjCAiQh+tEHkw==" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.0.tgz", + "integrity": "sha512-RxruSK3M4XgzcD7Trm2wEN+SJ26ChIb903+IWxNOcB5q4jT2Cs+hFr6QP39J05EohshRFEvyzEBoZ/466S2sbw==" }, "csv-stringify": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.3.0.tgz", - "integrity": "sha512-kTnnBkkLmAR1G409aUdShppWUClNbBQZXhrKrXzKYBGw4yfROspiFvVmjbKonCrdGfwnqwMXKLQG7ej7K/jwjg==" + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.4.2.tgz", + "integrity": "sha512-DXIdnnCUQYjDKTu6TgCSzRDiAuLxDjhl4ErFP9FGMF3wzBGOVMg9bZTLaUcYtuvhXgNbeXPKeaRfpgyqE4xySw==" }, "dashdash": { "version": "1.14.1", @@ -2884,11 +3013,22 @@ "ms": "^2.1.1" } }, + "define-data-property": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", + "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, "define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "requires": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } @@ -2947,17 +3087,18 @@ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "es-abstract": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", + "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", "requires": { "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", @@ -2972,19 +3113,23 @@ "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.11" } }, "es-array-method-boxes-properly": { @@ -3087,22 +3232,22 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fast-querystring": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.1.tgz", - "integrity": "sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", "requires": { "fast-decode-uri-component": "^1.0.1" } }, "fast-redact": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.2.tgz", - "integrity": "sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==" }, "find-my-way": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.6.0.tgz", - "integrity": "sha512-H7berWdHJ+5CNVr4ilLWPai4ml7Y2qAsxjw3pfeBxPigZmaDTzF0wjJLj90xRCmGcWYcyt050yN+34OZDJm1eQ==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.6.2.tgz", + "integrity": "sha512-0OjHn1b1nCX3eVbm9ByeEHiscPYiHLfhei1wOUU9qffQkk98wE0Lo8VrVYfSGMgnSnDh86DxedduAnBf4nwUEw==", "requires": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", @@ -3154,14 +3299,14 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" } }, "functions-have-names": { @@ -3170,12 +3315,13 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" }, "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" } }, @@ -3492,15 +3638,11 @@ } }, "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" } }, "is-typedarray": { @@ -3655,9 +3797,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", "optional": true }, "negotiator": { @@ -3666,9 +3808,9 @@ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, "nock": { - "version": "13.3.1", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.1.tgz", - "integrity": "sha512-vHnopocZuI93p2ccivFyGuUfzjq2fxNyNurp7816mlT5V5HF4SzXu8lvLrVzBbNqzs+ODooZ6OksuSUNM7Njkw==", + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.3.tgz", + "integrity": "sha512-z+KUlILy9SK/RjpeXDiDUEAq4T94ADPHE3qaRkf66mpEhzc/ytOMm3Bwdrbq6k1tMWkbdujiKim3G2tfQARuJw==", "requires": { "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", @@ -3692,9 +3834,9 @@ } }, "node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "requires": { "whatwg-url": "^5.0.0" } @@ -3779,14 +3921,14 @@ } }, "pino": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.11.0.tgz", - "integrity": "sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==", + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.15.1.tgz", + "integrity": "sha512-Cp4QzUQrvWCRJaQ8Lzv0mJzXVk4z2jlq8JNKMGaixC2Pz5L4l2p95TkuRvYbrEbe85NQsDKrAd4zalf7Ml6WiA==", "requires": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "v1.0.0", + "pino-abstract-transport": "v1.1.0", "pino-std-serializers": "^6.0.0", "process-warning": "^2.0.0", "quick-format-unescaped": "^4.0.3", @@ -3797,31 +3939,18 @@ } }, "pino-abstract-transport": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", - "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", "requires": { "readable-stream": "^4.0.0", "split2": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", - "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", - "requires": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10" - } - } } }, "pino-std-serializers": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.0.tgz", - "integrity": "sha512-IWgSzUL8X1w4BIWTwErRgtV8PyOGOOi60uqv0oKuS/fOA8Nco/OeI6lBuc4dyP8MMfdFwyHqTMcBIA7nDiqEqA==" + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" }, "process": { "version": "0.11.10", @@ -3839,15 +3968,15 @@ "integrity": "sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==" }, "promise.allsettled": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.6.tgz", - "integrity": "sha512-22wJUOD3zswWFqgwjNHa1965LvqTX87WPu/lreY2KSd7SVcERfuZ4GfUaOnJNnvtoIv2yXT/W00YIGMetXtFXg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.7.tgz", + "integrity": "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA==", "requires": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" } }, @@ -3882,13 +4011,15 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" } }, "real-require": { @@ -3926,13 +4057,13 @@ } }, "regexp.prototype.flags": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "requires": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" + "set-function-name": "^2.0.0" } }, "request": { @@ -4025,17 +4156,17 @@ } }, "qs": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", - "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "requires": { "side-channel": "^1.0.4" } }, "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" } } }, @@ -4055,6 +4186,17 @@ "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==" }, + "safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4100,9 +4242,9 @@ "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" }, "semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" }, @@ -4159,6 +4301,16 @@ } } }, + "set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "requires": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + } + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -4175,9 +4327,9 @@ } }, "sonic-boom": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.3.0.tgz", - "integrity": "sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.4.0.tgz", + "integrity": "sha512-zSe9QQW30nPzjkSJ0glFQO5T9lHsk39tz+2bAAwCj8CNgEG8ItZiX7Wb2ZgA8I04dwRGCcf1m3ABJa8AYm12Fw==", "requires": { "atomic-sleep": "^1.0.0" } @@ -4234,6 +4386,16 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } } } }, @@ -4272,9 +4434,9 @@ } }, "stream-transform": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.2.tgz", - "integrity": "sha512-DHZQPNxvjU2qdQlGcpitn8pkJHQVTqdshtgXaLz6Vc5VCAognbGuuwGS5ugeqGVnyw8j4h89QcV8cwm0D1+V0A==" + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.8.tgz", + "integrity": "sha512-NUx0mBuI63KbNEEh9Yj0OzKB7iMOSTpkuODM2G7By+TTVihEIJ0cYp5X+pq/TdJRlsznt6CYR8HqxexyC6/bTw==" }, "string_decoder": { "version": "1.3.0", @@ -4285,39 +4447,39 @@ } }, "string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "thread-stream": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.3.0.tgz", - "integrity": "sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.0.tgz", + "integrity": "sha512-xZYtOtmnA63zj04Q+F9bdEay5r47bvpo1CaNqsKi7TpoJHcotUez8Fkfo2RJWpW91lnnaApdpRbVwCWsy+ifcw==", "requires": { "real-require": "^0.2.0" } @@ -4354,6 +4516,39 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, + "typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, "typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -4452,16 +4647,15 @@ } }, "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" } }, "wrappy": { diff --git a/package.json b/package.json index 077cff98..30e41608 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@eyevinn/hls-repeat": "^0.2.0", "@eyevinn/hls-truncate": "^0.2.0", "@eyevinn/hls-vodtolive": "^4.1.0", - "@eyevinn/m3u8": "^0.5.3", + "@eyevinn/m3u8": "^0.5.6", "abort-controller": "^3.0.0", "debug": "^3.2.7", "memcache-client": "^0.10.1", From e5e87205fe3900d4dc979d56d85fc7ba4cc9ff8c Mon Sep 17 00:00:00 2001 From: Nicholas Frederiksen Date: Tue, 3 Oct 2023 15:11:48 +0200 Subject: [PATCH 13/17] chore: refactor demux - add cmaf support - add sync fix --- engine/session.js | 74 +- engine/session_live.js | 2235 ++++++++++++--------------- engine/stream_switcher.js | 224 +-- package-lock.json | 820 ++++++---- package.json | 4 +- spec/engine/stream_switcher_spec.js | 449 +++--- 6 files changed, 1928 insertions(+), 1878 deletions(-) diff --git a/engine/session.js b/engine/session.js index 3dd4ed93..e3e69162 100644 --- a/engine/session.js +++ b/engine/session.js @@ -367,7 +367,7 @@ class Session { } } - async setCurrentMediaSequenceSegments(segments, mSeqOffset, reloadBehind) { + async setCurrentMediaSequenceSegments(segments, mSeqOffset, reloadBehind, audioSegments, aSeqOffset) { if (!this._sessionState) { throw new Error("Session not ready"); } @@ -376,6 +376,8 @@ class Session { this.switchDataForSession.reloadBehind = reloadBehind; this.switchDataForSession.transitionSegments = segments; this.switchDataForSession.mediaSeqOffset = mSeqOffset; + this.switchDataForSession.transitionAudioSegments = audioSegments; + this.switchDataForSession.audioSeqOffset = aSeqOffset; let waitTimeMs = 2000; for (let i = segments[Object.keys(segments)[0]].length - 1; 0 < i; i--) { @@ -385,68 +387,20 @@ class Session { } } - let isLeader = await this._sessionStateStore.isLeader(this._instanceId); - if (!isLeader) { - debug(`[${this._sessionId}]: FOLLOWER: Invalidate cache to ensure having the correct VOD!`); - await this._sessionState.clearCurrentVodCache(); - - let vodReloaded = await this._sessionState.get("vodReloaded"); - let attempts = 9; - while (!isLeader && !vodReloaded && attempts > 0) { - debug(`[${this._sessionId}]: FOLLOWER: I arrived before LEADER. Waiting (1000ms) for LEADER to reload currentVod in store! (tries left=${attempts})`); - await timer(1000); - await this._sessionStateStore.clearLeaderCache(); - isLeader = await this._sessionStateStore.isLeader(this._instanceId); - vodReloaded = await this._sessionState.get("vodReloaded"); - attempts--; - } - - if (attempts === 0) { - debug(`[${this._sessionId}]: FOLLOWER: WARNING! Attempts=0 - Risk of using wrong currentVod`); - } - if (!isLeader || vodReloaded) { - debug(`[${this._sessionId}]: FOLLOWER: leader is alive, and has presumably updated currentVod. Clearing the cache now`); - await this._sessionState.clearCurrentVodCache(); - return; - } - debug(`[${this._sessionId}]: NEW LEADER: Setting state=VOD_RELOAD_INIT`); - this.isSwitchingBackToV2L = true; - await this._sessionState.set("state", SessionState.VOD_RELOAD_INIT); - - } else { - let vodReloaded = await this._sessionState.get("vodReloaded"); - let attempts = 12; - while (!vodReloaded && attempts > 0) { - debug(`[${this._sessionId}]: LEADER: Waiting (${waitTimeMs}ms) to buy some time reloading vod and adding it to store! (tries left=${attempts})`); - await timer(waitTimeMs); - vodReloaded = await this._sessionState.get("vodReloaded"); - attempts--; - } - if (attempts === 0) { - debug(`[${this._sessionId}]: LEADER: WARNING! Vod was never Reloaded!`); - return; + if (this.use_demuxed_audio && audioSegments) { + let waitTimeMsAudio = 2000; + let groupId = Object.keys(audioSegments)[0]; + let lang = Object.keys(audioSegments[groupId])[0] + for (let i = audioSegments[groupId][lang].length - 1; 0 < i; i--) { + const segment = audioSegments[groupId][lang][i]; + if (segment.duration) { + waitTimeMsAudio = parseInt(1000 * (segment.duration / 3), 10); + break; + } } + waitTimeMs = waitTimeMs > waitTimeMsAudio ? waitTimeMs : waitTimeMsAudio; } - } - async setCurrentAudioSequenceSegments(segments, aSeqOffset, reloadBehind) { - if (!this._sessionState) { - throw new Error("Session not ready"); - } - this.isSwitchingBackToV2L = true; - - this.switchDataForSession.transitionAudioSegments = segments; - this.switchDataForSession.audioSeqOffset = aSeqOffset; - let waitTimeMs = 2000; - let groupId = Object.keys(segments)[0]; - let lang = Object.keys(segments[groupId])[0] - for (let i = segments[groupId][lang].length - 1; 0 < i; i--) { - const segment = segments[groupId][lang][i]; - if (segment.duration) { - waitTimeMs = parseInt(1000 * (segment.duration / 3), 10); - break; - } - } let isLeader = await this._sessionStateStore.isLeader(this._instanceId); if (!isLeader) { debug(`[${this._sessionId}]: FOLLOWER: Invalidate cache to ensure having the correct VOD!`); diff --git a/engine/session_live.js b/engine/session_live.js index cf1cdb07..39c8d2d9 100644 --- a/engine/session_live.js +++ b/engine/session_live.js @@ -2,6 +2,7 @@ const debug = require("debug")("engine-session-live"); const allSettled = require("promise.allsettled"); const crypto = require("crypto"); const m3u8 = require("@eyevinn/m3u8"); +const { segToM3u8 } = require("@eyevinn/hls-vodtolive/utils.js"); const url = require("url"); const fetch = require("node-fetch"); const { m3u8Header } = require("./util.js"); @@ -15,6 +16,7 @@ const daterangeAttribute = (key, attr) => { return key.toUpperCase() + "=" + `"${attr}"`; } }; +const HIGHEST_MEDIA_SEQUENCE_COUNT = 0; const TARGET_PLAYLIST_DURATION_SEC = 60; const RESET_DELAY = 5000; const FAIL_TIMEOUT = 4000; @@ -25,6 +27,11 @@ const PlayheadState = Object.freeze({ CRASHED: 3, IDLE: 4, }); +const PlaylistTypes = Object.freeze({ + VIDEO: 1, + AUDIO: 2, + SUBTITLE: 3, +}); /** * When we implement subtitle support in live-mix we should place it in its own file/or share it with audio @@ -47,18 +54,18 @@ class SessionLive { this.targetDuration = 0; this.masterManifestUri = null; this.vodSegments = {}; - this.vodAudioSegments = {}; + this.vodSegmentsAudio = {}; this.mediaManifestURIs = {}; this.audioManifestURIs = {}; this.liveSegQueue = {}; this.lastRequestedMediaSeqRaw = null; this.liveSourceM3Us = {}; - this.liveAudioSegQueue = {}; + this.liveSegQueueAudio = {}; this.lastRequestedAudioSeqRaw = null; this.liveAudioSourceM3Us = {}; this.playheadState = PlayheadState.IDLE; this.liveSegsForFollowers = {}; - this.audioLiveSegsForFollowers = {}; + this.liveSegsForFollowersAudio = {}; this.timerCompensation = null; this.firstTime = true; this.firstTimeAudio = true; @@ -106,11 +113,15 @@ class SessionLive { if (resetDelay === null || resetDelay < 0) { resetDelay = RESET_DELAY; } - debug(`[${this.instanceId}][${this.sessionId}]: LEADER: Resetting SessionLive values in Store ${resetDelay === 0 ? "Immediately" : `after a delay=(${resetDelay}ms)`}`); + debug( + `[${this.instanceId}][${this.sessionId}]: LEADER: Resetting SessionLive values in Store ${ + resetDelay === 0 ? "Immediately" : `after a delay=(${resetDelay}ms)` + }` + ); await timer(resetDelay); await this.sessionLiveState.set("liveSegsForFollowers", null); await this.sessionLiveState.set("lastRequestedMediaSeqRaw", null); - await this.sessionLiveState.set("liveAudioSegsForFollowers", null); + await this.sessionLiveState.set("liveSegsForFollowersAudio", null); await this.sessionLiveState.set("lastRequestedAudioSeqRaw", null); await this.sessionLiveState.set("transitSegs", null); await this.sessionLiveState.set("transitSegsAudio", null); @@ -120,7 +131,7 @@ class SessionLive { mediaSeqCount: null, audioSeqCount: null, discSeqCount: null, - audioDiscSeqCount: null + audioDiscSeqCount: null, }); debug(`[${this.instanceId}][${this.sessionId}]: LEADER: SessionLive values in Store have now been reset!`); } @@ -145,17 +156,17 @@ class SessionLive { this.targetDuration = 0; this.masterManifestUri = null; this.vodSegments = {}; - this.vodAudioSegments = {}; + this.vodSegmentsAudio = {}; this.mediaManifestURIs = {}; this.audioManifestURIs = {}; this.liveSegQueue = {}; - this.liveAudioSegQueue = {}; + this.liveSegQueueAudio = {}; this.lastRequestedMediaSeqRaw = null; this.lastRequestedAudioSeqRaw = null; this.liveSourceM3Us = {}; this.liveAudioSourceM3Us = {}; this.liveSegsForFollowers = {}; - this.audioLiveSegsForFollowers = {}; + this.liveSegsForFollowersAudio = {}; this.timerCompensation = null; this.firstTime = true; this.firstTimeAudio = true; @@ -192,8 +203,7 @@ class SessionLive { this.waitForPlayhead = true; const tsIncrementBegin = Date.now(); - await this._loadAllMediaManifests(); - await this._loadAllAudioManifests(); + await this._loadAllPlaylistManifests(); const tsIncrementEnd = Date.now(); this.waitForPlayhead = false; @@ -260,10 +270,11 @@ class SessionLive { debug(`[${this.sessionId}]: Filtered Live profiles! (${Object.keys(this.mediaManifestURIs).length}) profiles left!`); } if (this.sessionAudioTracks) { - this._filterLiveAudioTracks(); + this._filterLiveProfilesAudio(); debug(`[${this.sessionId}]: Filtered Live audio tracks! (${Object.keys([Object.keys(this.audioManifestURIs)[0]]).length}) profiles left!`); } } catch (err) { + console.error(err); this.masterManifestUri = null; debug(`[${this.instanceId}][${this.sessionId}]: Failed to fetch Live Master Manifest! ${err}`); debug(`[${this.instanceId}][${this.sessionId}]: Will try again in 1000ms! (tries left=${attempts})`); @@ -302,12 +313,9 @@ class SessionLive { for (let segIdx = 0; segIdx < segments[bw].length; segIdx++) { const v2lSegment = segments[bw][segIdx]; if (v2lSegment.cue) { - console.log("cue video") if (v2lSegment.cue["in"]) { - console.log("cue in exists video") cueInExists = true; } else { - console.log("cue in does not exists video") cueInExists = false; } } @@ -318,13 +326,11 @@ class SessionLive { if (!segments[bw][endIdx].discontinuity) { const finalSegItem = { discontinuity: true }; if (!cueInExists) { - console.log(" adding cue in video") finalSegItem["cue"] = { in: true }; } this.vodSegments[bw].push(finalSegItem); } else { if (!cueInExists) { - console.log(" adding cue in video") segments[bw][endIdx]["cue"] = { in: true }; } } @@ -332,7 +338,6 @@ class SessionLive { } else { debug(`[${this.sessionId}]: 'vodSegments' not empty = Using 'transitSegs'`); } - debug(`[${this.sessionId}]: Setting CurrentMediaSequenceSegments. First seg is: [${this.vodSegments[Object.keys(this.vodSegments)[0]][0].uri}]`); const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); @@ -349,18 +354,16 @@ class SessionLive { } // Make it possible to add & share new segments this.allowedToSet = true; - if (this._isEmpty(this.vodAudioSegments)) { + if (this._isEmpty(this.vodSegmentsAudio)) { const groupIds = Object.keys(segments); for (let i = 0; i < groupIds.length; i++) { const groupId = groupIds[i]; const langs = Object.keys(segments[groupId]); for (let j = 0; j < langs.length; j++) { const lang = langs[j]; - if (!this.vodAudioSegments[groupId]) { - this.vodAudioSegments[groupId] = {}; - } - if (!this.vodAudioSegments[groupId][lang]) { - this.vodAudioSegments[groupId][lang] = []; + const audiotrack = this._getTrackFromGroupAndLang(groupId, lang); + if (!this.vodSegmentsAudio[audiotrack]) { + this.vodSegmentsAudio[audiotrack] = []; } if (segments[groupId][lang][0].discontinuity) { @@ -370,43 +373,42 @@ class SessionLive { for (let segIdx = 0; segIdx < segments[groupId][lang].length; segIdx++) { const v2lSegment = segments[groupId][lang][segIdx]; if (v2lSegment.cue) { - console.log("cue audio") if (v2lSegment.cue["in"]) { - console.log("cue in exists audio") cueInExists = true; } else { - console.log("cue in does not exists audio") cueInExists = false; } } - this.vodAudioSegments[groupId][lang].push(v2lSegment); + this.vodSegmentsAudio[audiotrack].push(v2lSegment); } - const endIdx = segments[groupId][langs].length - 1; + const endIdx = segments[groupId][lang].length - 1; if (!segments[groupId][lang][endIdx].discontinuity) { const finalSegItem = { discontinuity: true }; if (!cueInExists) { - console.log(" adding cue in audio") finalSegItem["cue"] = { in: true }; } - this.vodAudioSegments[groupId][lang].push(finalSegItem); + this.vodSegmentsAudio[audiotrack].push(finalSegItem); } else { if (!cueInExists) { - console.log(" adding cue in audio") segments[groupId][lang][endIdx]["cue"] = { in: true }; } } } } } else { - debug(`[${this.sessionId}]: 'vodAudioSegments' not empty = Using 'transitSegs'`); + debug(`[${this.sessionId}]: 'vodSegmentsAudio' not empty = Using 'transitSegs'`); } - debug(`[${this.sessionId}]: Setting CurrentAudioSequenceSegments. First seg is: [${this.vodAudioSegments[Object.keys(this.vodAudioSegments)[0]][Object.keys(this.vodAudioSegments[Object.keys(this.vodAudioSegments)[0]])][0].uri}]`); + debug( + `[${this.sessionId}]: Setting CurrentAudioSequenceSegments. First seg is: [${ + this.vodSegmentsAudio[Object.keys(this.vodSegmentsAudio)[0]][0].uri + }` + ); const isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); if (isLeader) { //debug(`[${this.sessionId}]: LEADER: I am adding 'transitSegs'=${JSON.stringify(this.vodSegments)} to Store for future followers`); - await this.sessionLiveState.set("transitSegs", this.vodAudioSegments); + await this.sessionLiveState.set("transitSegs", this.vodSegmentsAudio); debug(`[${this.sessionId}]: LEADER: I am adding 'transitSegs' to Store for future followers`); } } @@ -420,7 +422,9 @@ class SessionLive { debug(`[${this.sessionId}]: No media or disc sequence for audio provided`); return false; } - debug(`[${this.sessionId}]: Setting mediaSeqCount, discSeqCount, audioSeqCount and audioDiscSeqCount to: [${mediaSeq}]:[${discSeq}], [${audioMediaSeq}]:[${audioDiscSeq}]`); + debug( + `[${this.sessionId}]: Setting mediaSeqCount, discSeqCount, audioSeqCount and audioDiscSeqCount to: [${mediaSeq}]:[${discSeq}], [${audioMediaSeq}]:[${audioDiscSeq}]` + ); this.mediaSeqCount = mediaSeq; this.discSeqCount = discSeq; this.audioSeqCount = audioMediaSeq; @@ -465,7 +469,7 @@ class SessionLive { const transitAudioSegs = await this.sessionLiveState.get("transitAudioSegs"); if (!this._isEmpty(transitAudioSegs)) { debug(`[${this.sessionId}]: Getting and loading 'transitSegs'`); - this.vodAudioSegments = transitAudioSegs; + this.vodSegmentsAudio = transitAudioSegs; } } if (leadersDiscSeqCount !== null) { @@ -485,7 +489,7 @@ class SessionLive { } async getTransitionalAudioSegments() { - return this.vodAudioSegments; + return this.vodSegmentsAudio; } async getCurrentMediaSequenceSegments() { @@ -525,7 +529,6 @@ class SessionLive { currentMediaSequenceSegments[liveTargetBandwidth] = []; // In case we switch back before we've depleted all transitional segments currentMediaSequenceSegments[liveTargetBandwidth] = this.vodSegments[vodTargetBandwidth].concat(this.liveSegQueue[liveTargetBandwidth]); - console.log("adding extra video") currentMediaSequenceSegments[liveTargetBandwidth].push({ discontinuity: true, cue: { in: true } }); debug(`[${this.sessionId}]: Getting current media segments for bw=${bw}`); } @@ -550,42 +553,37 @@ class SessionLive { const leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); if (leadersAudioSeqRaw > this.lastRequestedAudioSeqRaw) { this.lastRequestedAudioSeqRaw = leadersAudioSeqRaw; - this.liveAudioSegsForFollowers = await this.sessionLiveState.get("liveAudioSegsForFollowers"); - this._updateAudioLiveSegQueue(); + this.liveSegsForFollowersAudio = await this.sessionLiveState.get("liveSegsForFollowersAudio"); + this._updateLiveSegQueueAudio(); } } let currentAudioSequenceSegments = {}; let segmentCount = 0; let increment = 0; - const groupIds = Object.keys(this.audioManifestURIs); - for (let i = 0; i < groupIds.length; i++) { - let groupId = groupIds[i]; - if (!currentAudioSequenceSegments[groupId]) { - currentAudioSequenceSegments[groupId] = {}; + const vodAudiotracks = Object.keys(this.vodSegmentsAudio); + for (let vat of vodAudiotracks) { + const liveTargetTrack = this._findNearestAudiotrack(vat, Object.keys(this.audioManifestURIs)); + const vodTargetTrack = vat; + let vti = this._getGroupAndLangFromTrack(vat); // get the Vod Track Item + if (!currentAudioSequenceSegments[vti.groupId]) { + currentAudioSequenceSegments[vti.groupId] = {}; } - let langs = Object.keys(this.audioManifestURIs[groupIds[i]]); - for (let j = 0; j < langs.length; j++) { - const liveTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.audioManifestURIs); - const vodTargetGroupLang = this._findAudioGroupAndLang(groupId, langs[j], this.vodAudioSegments); - if (!vodTargetGroupLang.audioGroupId || !vodTargetGroupLang.audioLanguage) { - return null; - } - // Remove segments and disc-tag if they are on top - if (this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].length > 0 && this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage][0].discontinuity) { - this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].shift(); - increment = 1; - } - segmentCount = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].length; - currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = []; - // In case we switch back before we've depleted all transitional segments - currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage] = this.vodAudioSegments[vodTargetGroupLang.audioGroupId][vodTargetGroupLang.audioLanguage].concat(this.liveAudioSegQueue[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage]); - console.log("adding extra audio") - currentAudioSequenceSegments[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage].push({ discontinuity: true, cue: { in: true } }); - debug(`[${this.sessionId}]: Getting current audio segments for ${groupId, langs[j]}`); + // Remove segments and disc-tag if they are on top + if (this.vodSegmentsAudio[vodTargetTrack].length > 0 && this.vodSegmentsAudio[vodTargetTrack][0].discontinuity) { + this.vodSegmentsAudio[vodTargetTrack].shift(); + increment = 1; } + segmentCount = this.vodSegmentsAudio[vodTargetTrack].length; + currentAudioSequenceSegments[vti.groupId][vti.language] = []; + // In case we switch back before we've depleted all transitional segments + currentAudioSequenceSegments[vti.groupId][vti.language] = this.vodSegmentsAudio[vodTargetTrack].concat(this.liveSegQueueAudio[liveTargetTrack]); + currentAudioSequenceSegments[vti.groupId][vti.language].push({ + discontinuity: true, + cue: { in: true }, + }); + debug(`[${this.sessionId}]: Getting current audio segments for ${vodTargetTrack}`); } - this.audioDiscSeqCount += increment; return { currMseqSegs: currentAudioSequenceSegments, @@ -597,8 +595,8 @@ class SessionLive { return { mediaSeq: this.mediaSeqCount, discSeq: this.discSeqCount, - audioSeq: this.audioSeqCount, - audioDiscSeq: this.audioDiscSeqCount, + audioSeq: this.mediaSeqCount, + audioDiscSeq: this.discSeqCount, }; } @@ -671,7 +669,9 @@ class SessionLive { await timer(1000); } } catch (exc) { - throw new Error(`[${this.instanceId}][${this.sessionId}]: Failed to generate audio manifest. Live Session might have ended already. \n${exc}`); + throw new Error( + `[${this.instanceId}][${this.sessionId}]: Failed to generate audio manifest. Live Session might have ended already. \n${exc}` + ); } } if (!m3u8) { @@ -729,18 +729,15 @@ class SessionLive { this.mediaManifestURIs[streamItemBW] = ""; } this.mediaManifestURIs[streamItemBW] = mediaManifestUri; + if (streamItem.get("audio") && this.useDemuxedAudio) { - let audioGroupId = streamItem.get("audio") + let audioGroupId = streamItem.get("audio"); let audioGroupItems = m3u.items.MediaItem.filter((item) => { return item.get("type") === "AUDIO" && item.get("group-id") === audioGroupId; }); // # Find all langs amongst the mediaItems that have this group id. // # It extracts each mediaItems language attribute value. // # ALSO initialize in this.audioSegments a lang. property who's value is an array [{seg1}, {seg2}, ...]. - if (!this.audioManifestURIs[audioGroupId]) { - this.audioManifestURIs[audioGroupId] = {} - } - audioGroupItems.map((item) => { let itemLang; if (!item.get("language")) { @@ -748,16 +745,19 @@ class SessionLive { } else { itemLang = item.get("language"); } - if (!this.audioManifestURIs[audioGroupId][itemLang]) { - this.audioManifestURIs[audioGroupId][itemLang] = "ehj" + const audiotrack = this._getTrackFromGroupAndLang(audioGroupId, itemLang); + if (!this.audioManifestURIs[audiotrack]) { + this.audioManifestURIs[audiotrack] = ""; } - const audioManifestUri = url.resolve(baseUrl, streamItem.get("uri")) - this.audioManifestURIs[audioGroupId][itemLang] = audioManifestUri; + const audioManifestUri = url.resolve(baseUrl, item.get("uri")); + this.audioManifestURIs[audiotrack] = audioManifestUri; }); } } - debug(`[${this.sessionId}]: All Live Media Manifest URIs have been collected. (${Object.keys(this.mediaManifestURIs).length}) profiles found!`); - debug(`[${this.sessionId}]: All Live Audio Manifest URIs have been collected. (${Object.keys(this.audioManifestURIs[Object.keys(this.audioManifestURIs)[0]]).length}) tracks found!`); + debug( + `[${this.sessionId}]: All Live Media Manifest URIs have been collected. (${Object.keys(this.mediaManifestURIs).length}) profiles found!` + ); + debug(`[${this.sessionId}]: All Live Audio Manifest URIs have been collected. (${Object.keys(this.audioManifestURIs).length}) tracks found!`); resolve(); parser.on("error", (exc) => { debug(`Parser Error: ${JSON.stringify(exc)}`); @@ -769,825 +769,617 @@ class SessionLive { // FOLLOWER only function _updateLiveSegQueue() { - if (Object.keys(this.liveSegsForFollowers).length === 0) { - debug(`[${this.sessionId}]: FOLLOWER: Error No Segments found at all.`); - } - const liveBws = Object.keys(this.liveSegsForFollowers); - const size = this.liveSegsForFollowers[liveBws[0]].length; - - // Push the New Live Segments to All Variants - for (let segIdx = 0; segIdx < size; segIdx++) { - for (let i = 0; i < liveBws.length; i++) { - const liveBw = liveBws[i]; - const liveSegFromLeader = this.liveSegsForFollowers[liveBw][segIdx]; - if (!this.liveSegQueue[liveBw]) { - this.liveSegQueue[liveBw] = []; - } - // Do not push duplicates - const liveSegURIs = this.liveSegQueue[liveBw].filter((seg) => seg.uri).map((seg) => seg.uri); - if (liveSegFromLeader.uri && liveSegURIs.includes(liveSegFromLeader.uri)) { - debug(`[${this.sessionId}]: FOLLOWER: Found duplicate live segment. Skip push! (${liveBw})`); - } else { - this.liveSegQueue[liveBw].push(liveSegFromLeader); - debug(`[${this.sessionId}]: FOLLOWER: Pushed segment (${liveSegFromLeader.uri ? liveSegFromLeader.uri : "Disc-tag"}) to 'liveSegQueue' (${liveBw})`); + try { + if (Object.keys(this.liveSegsForFollowers).length === 0) { + debug(`[${this.sessionId}]: FOLLOWER: Error No Segments found at all.`); + } + const liveBws = Object.keys(this.liveSegsForFollowers); + const size = this.liveSegsForFollowers[liveBws[0]].length; + + // Push the New Live Segments to All Variants + for (let segIdx = 0; segIdx < size; segIdx++) { + for (let i = 0; i < liveBws.length; i++) { + const liveBw = liveBws[i]; + const liveSegFromLeader = this.liveSegsForFollowers[liveBw][segIdx]; + if (!this.liveSegQueue[liveBw]) { + this.liveSegQueue[liveBw] = []; + } + // Do not push duplicates + const liveSegURIs = this.liveSegQueue[liveBw].filter((seg) => seg.uri).map((seg) => seg.uri); + if (liveSegFromLeader.uri && liveSegURIs.includes(liveSegFromLeader.uri)) { + debug(`[${this.sessionId}]: FOLLOWER: Found duplicate live segment. Skip push! (${liveBw})`); + } else { + this.liveSegQueue[liveBw].push(liveSegFromLeader); + debug( + `[${this.sessionId}]: FOLLOWER: Pushed Video segment (${ + liveSegFromLeader.uri ? liveSegFromLeader.uri : "Disc-tag" + }) to 'liveSegQueue' (${liveBw})` + ); + } } } - } - // Remove older segments and update counts - const newTotalDuration = this._incrementAndShift("FOLLOWER"); - if (newTotalDuration) { - debug(`[${this.sessionId}]: FOLLOWER: New Adjusted Playlist Duration=${newTotalDuration}s`); + // Remove older segments and update counts + const newTotalDuration = this._incrementAndShift("FOLLOWER"); + if (newTotalDuration) { + debug(`[${this.sessionId}]: FOLLOWER: New Adjusted Playlist Duration=${newTotalDuration}s`); + } + } catch (e) { + console.error(e); + return Promise.reject(e); } } - _updateLiveAudioSegQueue() { - let followerGroupIds = Object.keys(this.liveAudioSegsForFollowers); - let followerLangs = Object.keys(Object.keys(this.liveAudioSegsForFollowers[followerGroupIds[0]])); - if (this.liveAudioSegsForFollowers[followerGroupIds[0]][followerLangs[0]].length === 0) { - debug(`[${this.sessionId}]: FOLLOWER: Error No Segments found at all.`); - } - const liveGroupIds = Object.keys(this.liveAudioSegsForFollowers); - const size = this.liveAudioSegsForFollowers[liveGroupIds[0]][Object.keys(this.liveAudioSegsForFollowers[liveGroupIds[0]])].length; - - // Push the New Live Segments to All Variants - for (let segIdx = 0; segIdx < size; segIdx++) { - for (let i = 0; i < liveGroupIds.length; i++) { - x - const liveGroupId = liveGroupIds[i]; - const liveLangs = Object.keys(this.liveAudioSegsForFollowers[liveGroupId]) - for (let j = 0; j < liveLangs.length; j++) { - const liveLang = liveLangs[j]; - - const liveSegFromLeader = this.liveAudioSegsForFollowers[liveGroupId][liveLang][segIdx]; - if (!this.liveAudioSegQueue[liveGroupId]) { - this.liveAudioSegQueue[liveGroupId] = {}; - } - if (!this.liveAudioSegQueue[liveGroupId][liveLang]) { - this.liveAudioSegQueue[liveGroupId][liveLang] = []; + _updateLiveSegQueueAudio() { + try { + let followerAudiotracks = Object.keys(this.liveSegsForFollowersAudio); + if (this.liveSegsForFollowersAudio[followerAudiotracks[0]].length === 0) { + debug(`[${this.sessionId}]: FOLLOWER: Error No Audio Segments found at all.`); + } + const size = this.liveSegsForFollowersAudio[followerAudiotracks[0]].length; + // Push the New Live Segments to All Variants + for (let segIdx = 0; segIdx < size; segIdx++) { + for (let i = 0; i < followerAudiotracks.length; i++) { + const fat = followerAudiotracks[i]; + const liveSegFromLeader = this.liveSegsForFollowersAudio[fat][segIdx]; + if (!this.liveSegQueueAudio[fat]) { + this.liveSegQueueAudio[fat] = []; } // Do not push duplicates - const liveSegURIs = this.liveAudioSegQueue[liveGroupId][liveLang].filter((seg) => seg.uri).map((seg) => seg.uri); + const liveSegURIs = this.liveSegQueueAudio[fat].filter((seg) => seg.uri).map((seg) => seg.uri); if (liveSegFromLeader.uri && liveSegURIs.includes(liveSegFromLeader.uri)) { debug(`[${this.sessionId}]: FOLLOWER: Found duplicate live segment. Skip push! (${liveGroupId})`); } else { - this.liveAudioSegQueue[liveGroupId][liveLang].push(liveSegFromLeader); - debug(`[${this.sessionId}]: FOLLOWER: Pushed segment (${liveSegFromLeader.uri ? liveSegFromLeader.uri : "Disc-tag"}) to 'liveAudioSegQueue' (${liveGroupId, liveLang})`); + this.liveSegQueueAudio[fat].push(liveSegFromLeader); + debug( + `[${this.sessionId}]: FOLLOWER: Pushed Audio segment (${ + liveSegFromLeader.uri ? liveSegFromLeader.uri : "Disc-tag" + }) to 'liveSegQueueAudio' (${fat})` + ); } } } - } - // Remove older segments and update counts - const newTotalDuration = this._incrementAndShiftAudio("FOLLOWER"); - if (newTotalDuration) { - debug(`[${this.sessionId}]: FOLLOWER: New Adjusted Playlist Duration=${newTotalDuration}s`); + // Remove older segments and update counts + const newTotalDuration = this._incrementAndShiftAudio("FOLLOWER"); + if (newTotalDuration) { + debug(`[${this.sessionId}]: FOLLOWER: New Adjusted Playlist Duration=${newTotalDuration}s`); + } + } catch (e) { + console.error(e); + return Promise.reject(e); } } - /** - * This function adds new live segments to the node from which it can - * generate new manifests from. Method for attaining new segments differ - * depending on node Rank. The Leader collects from live source and - * Followers collect from shared storage. - * - * @returns Nothing, but gives data to certain class-variables - */ - async _loadAllMediaManifests() { - debug(`[${this.sessionId}]: Attempting to load all media manifest URIs in=${Object.keys(this.mediaManifestURIs)}`); - let currentMseqRaw = null; - // ------------------------------------- - // If I am a Follower-node then my job - // ends here, where I only read from store. - // ------------------------------------- - let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (!isLeader && this.lastRequestedMediaSeqRaw !== null) { - debug(`[${this.sessionId}]: FOLLOWER: Reading data from store!`); - - let leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); - - if (!leadersMediaSeqRaw < this.lastRequestedMediaSeqRaw && this.blockGenerateManifest) { - this.blockGenerateManifest = false; - } + async _collectSegmentsFromStore() { + try { + // check if audio is enabled + let hasAudio = this.audioManifestURIs.length > 0 ? true : false; + // ------------------------------------- + // If I am a Follower-node then my job + // ends here, where I only read from store. + // ------------------------------------- + let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (!isLeader && this.lastRequestedMediaSeqRaw !== null) { + debug(`[${this.sessionId}]: FOLLOWER: Reading data from store!`); + + let leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); + + if (!leadersMediaSeqRaw < this.lastRequestedMediaSeqRaw && this.blockGenerateManifest) { + this.blockGenerateManifest = false; + } + + let attempts = 10; + // CHECK AGAIN CASE 1: Store Empty + while (!leadersMediaSeqRaw && attempts > 0) { + if (!leadersMediaSeqRaw) { + isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (isLeader) { + debug(`[${this.instanceId}]: I'm the new leader`); + return; + } + } - let attempts = 10; - // CHECK AGAIN CASE 1: Store Empty - while (!leadersMediaSeqRaw && attempts > 0) { - if (!leadersMediaSeqRaw) { - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.instanceId}]: I'm the new leader`); - return; + if (!this.allowedToSet) { + debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); + break; } + const segDur = this._getAnyFirstSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; + const waitTimeMs = parseInt(segDur / 3, 10); + debug( + `[${this.sessionId}]: FOLLOWER: Leader has not put anything in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])` + ); + await timer(waitTimeMs); + this.timerCompensation = false; + leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); + attempts--; } - if (!this.allowedToSet) { - debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); - break; + if (!leadersMediaSeqRaw) { + debug(`[${this.instanceId}]: The leader is still alive`); + return; } - const segDur = this._getAnyFirstSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; - const waitTimeMs = parseInt(segDur / 3, 10); - debug(`[${this.sessionId}]: FOLLOWER: Leader has not put anything in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`); - await timer(waitTimeMs); - this.timerCompensation = false; - leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); - attempts--; - } - - if (!leadersMediaSeqRaw) { - debug(`[${this.instanceId}]: The leader is still alive`); - return; - } - let liveSegsInStore = await this.sessionLiveState.get("liveSegsForFollowers"); - attempts = 10; - // CHECK AGAIN CASE 2: Store Old - while ((leadersMediaSeqRaw <= this.lastRequestedMediaSeqRaw && attempts > 0) || (this._containsSegment(this.liveSegsForFollowers, liveSegsInStore) && attempts > 0)) { - if (!this.allowedToSet) { - debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); - break; + let liveSegsInStore = await this.sessionLiveState.get("liveSegsForFollowers"); + let liveSegsInStoreAudio = hasAudio ? await this.sessionLiveState.get("liveSegsForFollowersAudio") : null; + attempts = 10; + // CHECK AGAIN CASE 2: Store Old + while ( + (leadersMediaSeqRaw <= this.lastRequestedMediaSeqRaw && attempts > 0) || + (this._containsSegment(this.liveSegsForFollowers, liveSegsInStore) && attempts > 0) + ) { + if (!this.allowedToSet) { + debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); + break; + } + if (leadersMediaSeqRaw <= this.lastRequestedMediaSeqRaw) { + isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (isLeader) { + debug(`[${this.instanceId}][${this.sessionId}]: I'm the new leader`); + return; + } + } + if (this._containsSegment(this.liveSegsForFollowers, liveSegsInStore)) { + debug(`[${this.sessionId}]: FOLLOWER: _containsSegment=true,${leadersMediaSeqRaw},${this.lastRequestedMediaSeqRaw}`); + } + const segDur = this._getAnyFirstSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; + const waitTimeMs = parseInt(segDur / 3, 10); + debug(`[${this.sessionId}]: FOLLOWER: Cannot find anything NEW in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`); + await timer(waitTimeMs); + this.timerCompensation = false; + leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); + liveSegsInStore = await this.sessionLiveState.get("liveSegsForFollowers"); + liveSegsInStoreAudio = hasAudio ? await this.sessionLiveState.get("liveSegsForFollowersAudio") : null; + attempts--; } + // FINALLY if (leadersMediaSeqRaw <= this.lastRequestedMediaSeqRaw) { - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.instanceId}][${this.sessionId}]: I'm the new leader`); - return; - } + debug(`[${this.instanceId}][${this.sessionId}]: The leader is still alive`); + return; + } + // Follower updates its manifest building blocks (segment holders & counts) + this.lastRequestedMediaSeqRaw = leadersMediaSeqRaw; + this.liveSegsForFollowers = liveSegsInStore; + this.liveSegsForFollowersAudio = liveSegsInStoreAudio; + debug( + `[${this.sessionId}]: These are the segments from store:\nV[${JSON.stringify(this.liveSegsForFollowers)}]${ + hasAudio ? `\nA[${JSON.stringify(this.liveSegsForFollowersAudio)}]` : "" + }` + ); + this._updateLiveSegQueue(); + if (hasAudio) { + this._updateLiveSegQueueAudio(); } - if (this._containsSegment(this.liveSegsForFollowers, liveSegsInStore)) { - debug(`[${this.sessionId}]: FOLLOWER: _containsSegment=true,${leadersMediaSeqRaw},${this.lastRequestedMediaSeqRaw}`); - } - const segDur = this._getAnyFirstSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; - const waitTimeMs = parseInt(segDur / 3, 10); - debug(`[${this.sessionId}]: FOLLOWER: Cannot find anything NEW in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`); - await timer(waitTimeMs); - this.timerCompensation = false; - leadersMediaSeqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); - liveSegsInStore = await this.sessionLiveState.get("liveSegsForFollowers"); - attempts--; - } - // FINALLY - if (leadersMediaSeqRaw <= this.lastRequestedMediaSeqRaw) { - debug(`[${this.instanceId}][${this.sessionId}]: The leader is still alive`); return; } - // Follower updates its manifest building blocks (segment holders & counts) - this.lastRequestedMediaSeqRaw = leadersMediaSeqRaw; - this.liveSegsForFollowers = liveSegsInStore; - debug(`[${this.sessionId}]: These are the segments from store: [${JSON.stringify(this.liveSegsForFollowers)}]`); - this._updateLiveSegQueue(); - return; + } catch (e) { + console.error(e); + return Promise.reject(e); } + } - // --------------------------------- - // FETCHING FROM LIVE-SOURCE - New Followers (once) & Leaders do this. - // --------------------------------- - let FETCH_ATTEMPTS = 10; - this.liveSegsForFollowers = {}; - let bandwidthsToSkipOnRetry = []; - while (FETCH_ATTEMPTS > 0) { - if (isLeader) { - debug(`[${this.sessionId}]: LEADER: Trying to fetch manifests for all bandwidths\n Attempts left=[${FETCH_ATTEMPTS}]`); - } else { - debug(`[${this.sessionId}]: NEW FOLLOWER: Trying to fetch manifests for all bandwidths\n Attempts left=[${FETCH_ATTEMPTS}]`); - } + async _fetchFromLiveSource() { + try { + let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + + let currentMseqRaw = null; + let FETCH_ATTEMPTS = 10; + this.liveSegsForFollowers = {}; + this.liveSegsForFollowersAudio = {}; + let bandwidthsToSkipOnRetry = []; + let audiotracksToSkipOnRetry = []; + const audioTracksExist = Object.keys(this.audioManifestURIs).length > 0 ? true : false; + debug(`[${this.sessionId}]: Attempting to load all MEDIA manifest URIs in=${Object.keys(this.mediaManifestURIs)}`); + if (audioTracksExist) { + debug(`[${this.sessionId}]: Attempting to load all AUDIO manifest URIs in=${Object.keys(this.audioManifestURIs)}`); + } + // --------------------------------- + // FETCHING FROM LIVE-SOURCE - New Followers (once) & Leaders do this. + // --------------------------------- + while (FETCH_ATTEMPTS > 0) { + const MSG_1 = (rank, id, count, hasAudio) => { + return `[${id}]: ${rank}: Trying to fetch manifests for all bandwidths${hasAudio ? " and audiotracks" : ""}\n Attempts left=[${count}]`; + }; - if (!this.allowedToSet) { - debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Live-Source`); - break; - } + if (isLeader) { + debug(MSG_1("LEADER", this.sessionId, FETCH_ATTEMPTS, audioTracksExist)); + } else { + debug(MSG_1("NEW FOLLOWER", this.sessionId, FETCH_ATTEMPTS, audioTracksExist)); + } - // Reset Values Each Attempt - let livePromises = []; - let manifestList = []; - this.pushAmount = 0; - try { - if (bandwidthsToSkipOnRetry.length > 0) { - debug(`[${this.sessionId}]: (X) Skipping loadMedia promises for bws ${JSON.stringify(bandwidthsToSkipOnRetry)}`); + if (!this.allowedToSet) { + debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Live-Source`); + break; } - // Collect Live Source Requesting Promises - for (let i = 0; i < Object.keys(this.mediaManifestURIs).length; i++) { - let bw = Object.keys(this.mediaManifestURIs)[i]; - if (bandwidthsToSkipOnRetry.includes(bw)) { - continue; + + // Reset Values Each Attempt + let livePromises = []; + let manifestList = []; + this.pushAmount = 0; + try { + if (bandwidthsToSkipOnRetry.length > 0) { + debug(`[${this.sessionId}]: (X) Skipping loadMedia promises for bws ${JSON.stringify(bandwidthsToSkipOnRetry)}`); } - livePromises.push(this._loadMediaManifest(bw)); - debug(`[${this.sessionId}]: Pushed loadMedia promise for bw=[${bw}]`); + if (audiotracksToSkipOnRetry.length > 0) { + debug(`[${this.sessionId}]: (X) Skipping loadMedia promises for audiotracks ${JSON.stringify(audiotracksToSkipOnRetry)}`); + } + // Collect Live Source Requesting Promises + for (let i = 0; i < Object.keys(this.mediaManifestURIs).length; i++) { + let bw = Object.keys(this.mediaManifestURIs)[i]; + if (bandwidthsToSkipOnRetry.includes(bw)) { + continue; + } + livePromises.push(this._loadMediaManifest(bw)); + debug(`[${this.sessionId}]: Pushed loadMedia promise for bw=[${bw}]`); + } + // Collect Live Source Requesting Promises (audio) + for (let i = 0; i < Object.keys(this.audioManifestURIs).length; i++) { + let atStr = Object.keys(this.audioManifestURIs)[i]; + if (audiotracksToSkipOnRetry.includes(atStr)) { + continue; + } + livePromises.push(this._loadAudioManifest(atStr)); + debug(`[${this.sessionId}]: Pushed loadMedia promise for audiotrack=${atStr}`); + } + // Fetch From Live Source + debug(`[${this.sessionId}]: Executing Promises I: Fetch From Live Source`); + manifestList = await allSettled(livePromises); + livePromises = []; + } catch (err) { + debug(`[${this.sessionId}]: Promises I: FAILURE!\n${err}`); + return; } - // Fetch From Live Source - debug(`[${this.sessionId}]: Executing Promises I: Fetch From Live Source`); - manifestList = await allSettled(livePromises); - livePromises = []; - } catch (err) { - debug(`[${this.sessionId}]: Promises I: FAILURE!\n${err}`); - return; - } - - // Handle if any promise got rejected - if (manifestList.some((result) => result.status === "rejected")) { - FETCH_ATTEMPTS--; - debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again in 1000ms...`); - await timer(1000); - continue; - } - // Store the results locally - manifestList.forEach((variantItem) => { - const bw = variantItem.value.bandwidth; - if (!this.liveSourceM3Us[bw]) { - this.liveSourceM3Us[bw] = {}; + // Handle if any promise got rejected + if (manifestList.some((result) => result.status === "rejected")) { + FETCH_ATTEMPTS--; + debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again in 1000ms...`); + console.log( + manifestList.map((r) => { + return { status: r.status }; + }) + ); + await timer(1000); + continue; } - this.liveSourceM3Us[bw] = variantItem.value; - }); - - const allStoredMediaSeqCounts = Object.keys(this.liveSourceM3Us).map((variant) => this.liveSourceM3Us[variant].mediaSeq); - // Handle if mediaSeqCounts are NOT synced up! - if (!allStoredMediaSeqCounts.every((val, i, arr) => val === arr[0])) { - debug(`[${this.sessionId}]: Live Mseq counts=[${allStoredMediaSeqCounts}]`); - // Figure out what bw's are behind. - const highestMediaSeqCount = Math.max(...allStoredMediaSeqCounts); - bandwidthsToSkipOnRetry = Object.keys(this.liveSourceM3Us).filter((bw) => { - if (this.liveSourceM3Us[bw].mediaSeq === highestMediaSeqCount) { - return true; + // Fill "liveSourceM3Us" and Store the results locally + manifestList.forEach((variantItem) => { + let variantKey = ""; + if (variantItem.value.bandwidth) { + variantKey = variantItem.value.bandwidth; + } else if (variantItem.value.audiotrack) { + variantKey = variantItem.value.audiotrack; + } else { + console.error("NO 'bandwidth' or 'audiotrack' in item:", JSON.stringify(variantItem)); + } + if (!this.liveSourceM3Us[variantKey]) { + this.liveSourceM3Us[variantKey] = {}; } - return false; + this.liveSourceM3Us[variantKey] = variantItem.value; }); - // Decrement fetch counter - FETCH_ATTEMPTS--; - // Calculate retry delay time. Default=1000 - let retryDelayMs = 1000; - if (Object.keys(this.liveSegQueue).length > 0) { - const firstBw = Object.keys(this.liveSegQueue)[0]; - const lastIdx = this.liveSegQueue[firstBw].length - 1; - if (this.liveSegQueue[firstBw][lastIdx].duration) { - retryDelayMs = this.liveSegQueue[firstBw][lastIdx].duration * 1000 * 0.25; + + const allStoredMediaSeqCounts = Object.keys(this.liveSourceM3Us).map((variant) => this.liveSourceM3Us[variant].mediaSeq); + + // Handle if mediaSeqCounts are NOT synced up! + if (!allStoredMediaSeqCounts.every((val, i, arr) => val === arr[0])) { + bandwidthsToSkipOnRetry = []; + audiotracksToSkipOnRetry = []; + debug(`[${this.sessionId}]: Live Mseq counts=[${allStoredMediaSeqCounts}]`); + // Figure out what variants's are behind. + HIGHEST_MEDIA_SEQUENCE_COUNT = Math.max(...allStoredMediaSeqCounts); + Object.keys(this.liveSourceM3Us).map((variantKey) => { + if (this.liveSourceM3Us[variantKey].mediaSeq === HIGHEST_MEDIA_SEQUENCE_COUNT) { + if (this._isBandwidth(variantKey)) { + bandwidthsToSkipOnRetry.push(variantKey); + } else { + audiotracksToSkipOnRetry.push(variantKey); + } + } + }); + // Decrement fetch counter + FETCH_ATTEMPTS--; + // Calculate retry delay time. Default=1000 + let retryDelayMs = 1000; + if (Object.keys(this.liveSegQueue).length > 0) { + const firstBw = Object.keys(this.liveSegQueue)[0]; + const lastIdx = this.liveSegQueue[firstBw].length - 1; + if (this.liveSegQueue[firstBw][lastIdx].duration) { + retryDelayMs = this.liveSegQueue[firstBw][lastIdx].duration * 1000 * 0.25; + } + } + // If 3 tries already and only video is unsynced, Make the BAD VARIANTS INHERIT M3U's from the good ones. + if (FETCH_ATTEMPTS >= 7 && audiotracksToSkipOnRetry.length === this.audioManifestURIs.length) { + // Find Highest MSEQ + let [ahead, behind] = Object.keys(this.liveSourceM3Us).map((v) => { + const c = this.liveSourceM3Us[v].mediaSeq; + const a = []; + const b = []; + if (c === HIGHEST_MEDIA_SEQUENCE_COUNT) { + a.push({ c, v }); + } else { + b.push({ c, v }); + } + }); + // Find lowest bitrate with that highest MSEQ + const variantToPaste = ahead.reduce((min, item) => (item.v < min.v ? item : min), list[0]); + // Reassign that bitrate onto the one's originally planned for retry + const m3uToPaste = this.liveSourceM3Us[variantToPaste]; + behind.forEach((item) => { + this.liveSourceM3Us[item.v] = m3uToPaste; + }); + debug(`[${this.sessionId}]: ALERT! Live Source Data NOT in sync! Will fake sync by copy-pasting segments from best mseq`); + } else { + // Wait a little before trying again + debug(`[${this.sessionId}]: ALERT! Live Source Data NOT in sync! Will try again after ${retryDelayMs}ms`); + await timer(retryDelayMs); + if (isLeader) { + this.timerCompensation = false; + } + continue; } } - // Wait a little before trying again - debug(`[${this.sessionId}]: ALERT! Live Source Data NOT in sync! Will try again after ${retryDelayMs}ms`); - await timer(retryDelayMs); - if (isLeader) { - this.timerCompensation = false; - } - continue; - } - currentMseqRaw = allStoredMediaSeqCounts[0]; + currentMseqRaw = allStoredMediaSeqCounts[0]; - if (!isLeader) { - let leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); - let tries = 20; + if (!isLeader) { + let leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); + let tries = 20; - while ((!isLeader && !leadersFirstSeqCounts.liveSourceMseqCount && tries > 0) || leadersFirstSeqCounts.liveSourceMseqCount === 0) { - debug(`[${this.sessionId}]: NEW FOLLOWER: Waiting for LEADER to add 'firstCounts' in store! Will look again after 1000ms (tries left=${tries})`); - await timer(1000); - leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); - tries--; - // Might take over as Leader if Leader is not setting data due to being down. - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); + while ((!isLeader && !leadersFirstSeqCounts.liveSourceMseqCount && tries > 0) || leadersFirstSeqCounts.liveSourceMseqCount === 0) { + debug( + `[${this.sessionId}]: NEW FOLLOWER: Waiting for LEADER to add 'firstCounts' in store! Will look again after 1000ms (tries left=${tries})` + ); + await timer(1000); + leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); + tries--; + // Might take over as Leader if Leader is not setting data due to being down. + isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (isLeader) { + debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); + } + } + + if (tries === 0) { + isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (isLeader) { + debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); + break; + } else { + debug(`[${this.sessionId}][${this.instanceId}]: The leader is still alive`); + leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); + if (!leadersFirstSeqCounts.liveSourceMseqCount) { + debug( + `[${this.sessionId}][${this.instanceId}]: Could not find 'firstCounts' in store. Abort Executing Promises II & Returning to Playhead.` + ); + return; + } + } } - } - if (tries === 0) { - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); if (isLeader) { - debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); - break; - } else { - debug(`[${this.sessionId}][${this.instanceId}]: The leader is still alive`); - leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); - if (!leadersFirstSeqCounts.liveSourceMseqCount) { - debug(`[${this.sessionId}][${this.instanceId}]: Could not find 'firstCounts' in store. Abort Executing Promises II & Returning to Playhead.`); - return; + debug(`[${this.sessionId}]: NEW LEADER: Original Leader went missing, I am retrying live source fetch...`); + await this.sessionLiveState.set("transitSegs", this.vodSegments); + if (audioTracksExist) { + await this.sessionLiveState.set("transitSegsAudio", this.vodSegmentsAudio); } + debug( + `[${this.sessionId}]: NEW LEADER: I am adding 'transitSegs'${ + audioTracksExist ? "and 'transitSegsAudio'" : "" + } to Store for future followers` + ); + continue; } - } - if (isLeader) { - debug(`[${this.sessionId}]: NEW LEADER: Original Leader went missing, I am retrying live source fetch...`); - await this.sessionLiveState.set("transitSegs", this.vodSegments); - debug(`[${this.sessionId}]: NEW LEADER: I am adding 'transitSegs' to Store for future followers`); - continue; - } + // Respawners never do this, only starter followers. + // Edge Case: FOLLOWER transitioned from session with different segments from LEADER + if (leadersFirstSeqCounts.discSeqCount !== this.discSeqCount) { + this.discSeqCount = leadersFirstSeqCounts.discSeqCount; + } + if (leadersFirstSeqCounts.mediaSeqCount !== this.mediaSeqCount) { + this.mediaSeqCount = leadersFirstSeqCounts.mediaSeqCount; + debug( + `[${this.sessionId}]: FOLLOWER transistioned with wrong V2L segments, updating counts to [${this.mediaSeqCount}][${this.discSeqCount}], and reading 'transitSegs' from store` + ); + const transitSegs = await this.sessionLiveState.get("transitSegs"); + if (!this._isEmpty(transitSegs)) { + this.vodSegments = transitSegs; + } + if (audioTracksExist) { + const transitSegsAudio = await this.sessionLiveState.get("transitSegsAudio"); + if (!this._isEmpty(transitSegsAudio)) { + this.vodSegmentsAudio = transitSegsAudio; + } + } + } - // Respawners never do this, only starter followers. - // Edge Case: FOLLOWER transitioned from session with different segments from LEADER - if (leadersFirstSeqCounts.discSeqCount !== this.discSeqCount) { - this.discSeqCount = leadersFirstSeqCounts.discSeqCount; - } - if (leadersFirstSeqCounts.mediaSeqCount !== this.mediaSeqCount) { - this.mediaSeqCount = leadersFirstSeqCounts.mediaSeqCount; + // Prepare to load segments... debug( - `[${this.sessionId}]: FOLLOWER transistioned with wrong V2L segments, updating counts to [${this.mediaSeqCount}][${this.discSeqCount}], and reading 'transitSegs' from store` + `[${this.instanceId}][${this.sessionId}]: Newest mseq from LIVE=${currentMseqRaw} First mseq in store=${leadersFirstSeqCounts.liveSourceMseqCount}` ); - const transitSegs = await this.sessionLiveState.get("transitSegs"); - if (!this._isEmpty(transitSegs)) { - this.vodSegments = transitSegs; - } - } - - // Prepare to load segments... - debug(`[${this.instanceId}][${this.sessionId}]: Newest mseq from LIVE=${currentMseqRaw} First mseq in store=${leadersFirstSeqCounts.liveSourceMseqCount}`); - if (currentMseqRaw === leadersFirstSeqCounts.liveSourceMseqCount) { - this.pushAmount = 1; // Follower from start - } else { - // TODO: To support and account for past discontinuity tags in the Live Source stream, - // we will need to get the real 'current' discontinuity-sequence count from Leader somehow. + if (currentMseqRaw === leadersFirstSeqCounts.liveSourceMseqCount) { + this.pushAmount = 1; // Follower from start + } else { + // TODO: To support and account for past discontinuity tags in the Live Source stream, + // we will need to get the real 'current' discontinuity-sequence count from Leader somehow. - // RESPAWNED NODES - this.pushAmount = currentMseqRaw - leadersFirstSeqCounts.liveSourceMseqCount + 1; + // RESPAWNED NODES + this.pushAmount = currentMseqRaw - leadersFirstSeqCounts.liveSourceMseqCount + 1; - const transitSegs = await this.sessionLiveState.get("transitSegs"); - //debug(`[${this.sessionId}]: NEW FOLLOWER: I tried to get 'transitSegs'. This is what I found ${JSON.stringify(transitSegs)}`); - if (!this._isEmpty(transitSegs)) { - this.vodSegments = transitSegs; + const transitSegs = await this.sessionLiveState.get("transitSegs"); + //debug(`[${this.sessionId}]: NEW FOLLOWER: I tried to get 'transitSegs'. This is what I found ${JSON.stringify(transitSegs)}`); + if (!this._isEmpty(transitSegs)) { + this.vodSegments = transitSegs; + } + if (audioTracksExist) { + const transitSegsAudio = await this.sessionLiveState.get("transitSegsAudio"); + if (!this._isEmpty(transitSegsAudio)) { + this.vodSegmentsAudio = transitSegsAudio; + } + } } - } - debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmount}`); - } else { - // LEADER calculates pushAmount differently... - if (this.firstTime) { - this.pushAmount = 1; // Leader from start + debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmount}`); } else { - this.pushAmount = currentMseqRaw - this.lastRequestedMediaSeqRaw; - debug(`[${this.sessionId}]: ...calculating pushAmount=${currentMseqRaw}-${this.lastRequestedMediaSeqRaw}=${this.pushAmount}`); + // LEADER calculates pushAmount differently... + if (this.firstTime) { + this.pushAmount = 1; // Leader from start + } else { + this.pushAmount = currentMseqRaw - this.lastRequestedMediaSeqRaw; + debug(`[${this.sessionId}]: ...calculating pushAmount=${currentMseqRaw}-${this.lastRequestedMediaSeqRaw}=${this.pushAmount}`); + } + debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmount}`); + break; } - debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmount}`); + // Live Source Data is in sync, and LEADER & new FOLLOWER are in sync break; } - // Live Source Data is in sync, and LEADER & new FOLLOWER are in sync - break; - } - - if (FETCH_ATTEMPTS === 0) { - debug(`[${this.sessionId}]: Fetching from Live-Source did not work! Returning to Playhead Loop...`); - return; - } - - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - // NEW FOLLOWER - Edge Case: One Instance is ahead of another. Read latest live segs from store - if (!isLeader) { - const leadersCurrentMseqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); - const counts = await this.sessionLiveState.get("firstCounts"); - const leadersFirstMseqRaw = counts.liveSourceMseqCount; - if (leadersCurrentMseqRaw !== null && leadersCurrentMseqRaw > currentMseqRaw) { - // if leader never had any segs from prev mseq - if (leadersFirstMseqRaw !== null && leadersFirstMseqRaw === leadersCurrentMseqRaw) { - // Follower updates it's manifest ingedients (segment holders & counts) - this.lastRequestedMediaSeqRaw = leadersCurrentMseqRaw; - this.liveSegsForFollowers = await this.sessionLiveState.get("liveSegsForFollowers"); - debug(`[${this.sessionId}]: NEW FOLLOWER: Leader is ahead or behind me! Clearing Queue and Getting latest segments from store.`); - this._updateLiveSegQueue(); - this.firstTime = false; - debug(`[${this.sessionId}]: Got all needed segments from live-source (read from store).\nWe are now able to build Live Manifest: [${this.mediaSeqCount}]`); - return; - } else if (leadersCurrentMseqRaw < this.lastRequestedMediaSeqRaw) { - // WE ARE A RESPAWN-NODE, and we are ahead of leader. - this.blockGenerateManifest = true; - } - } - } - if (this.allowedToSet) { - // Collect and Push Segment-Extracting Promises - let pushPromises = []; - for (let i = 0; i < Object.keys(this.mediaManifestURIs).length; i++) { - let bw = Object.keys(this.mediaManifestURIs)[i]; - // will add new segments to live seg queue - pushPromises.push(this._parseMediaManifest(this.liveSourceM3Us[bw].M3U, this.mediaManifestURIs[bw], bw, isLeader)); - debug(`[${this.sessionId}]: Pushed pushPromise for bw=${bw}`); - } - // Segment Pushing - debug(`[${this.sessionId}]: Executing Promises II: Segment Pushing`); - await allSettled(pushPromises); - - // UPDATE COUNTS, & Shift Segments in vodSegments and liveSegQueue if needed. - const leaderORFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; - const newTotalDuration = this._incrementAndShift(leaderORFollower); - if (newTotalDuration) { - debug(`[${this.sessionId}]: New Adjusted Playlist Duration=${newTotalDuration}s`); - } - } - - // ----------------------------------------------------- - // Leader writes to store so that Followers can read. - // ----------------------------------------------------- - if (isLeader) { - if (this.allowedToSet) { - const liveBws = Object.keys(this.liveSegsForFollowers); - const segListSize = this.liveSegsForFollowers[liveBws[0]].length; - // Do not replace old data with empty data - if (segListSize > 0) { - debug(`[${this.sessionId}]: LEADER: Adding data to store!`); - await this.sessionLiveState.set("lastRequestedMediaSeqRaw", this.lastRequestedMediaSeqRaw); - await this.sessionLiveState.set("liveSegsForFollowers", this.liveSegsForFollowers); - } - } - - // [LASTLY]: LEADER does this for respawned-FOLLOWERS' sake. - if (this.firstTime && this.allowedToSet) { - // Buy some time for followers (NOT Respawned) to fetch their own L.S m3u8. - await timer(1000); // maybe remove - let firstCounts = await this.sessionLiveState.get("firstCounts"); - firstCounts.liveSourceMseqCount = this.lastRequestedMediaSeqRaw; - firstCounts.mediaSeqCount = this.prevMediaSeqCount; - firstCounts.discSeqCount = this.prevDiscSeqCount; - - debug(`[${this.sessionId}]: LEADER: I am adding 'firstCounts'=${JSON.stringify(firstCounts)} to Store for future followers`); - await this.sessionLiveState.set("firstCounts", firstCounts); - } - debug(`[${this.sessionId}]: LEADER: I am using segs from Mseq=${this.lastRequestedMediaSeqRaw}`); - } else { - debug(`[${this.sessionId}]: NEW FOLLOWER: I am using segs from Mseq=${this.lastRequestedMediaSeqRaw}`); + return { + success: FETCH_ATTEMPTS ? true : false, + currentMseqRaw: currentMseqRaw, + }; + } catch (e) { + console.error(e); + return Promise.reject(e); } - - this.firstTime = false; - debug(`[${this.sessionId}]: Got all needed segments from live-source (from all bandwidths).\nWe are now able to build Live Manifest: [${this.mediaSeqCount}]`); - - return; } - async _loadAllAudioManifests() { - debug(`[${this.sessionId}]: Attempting to load all audio manifest URIs in=${Object.keys(this.audioManifestURIs[Object.keys(this.audioManifestURIs)[0]])}`); - let currentMseqRaw = null; - // ------------------------------------- - // If I am a Follower-node then my job - // ends here, where I only read from store. - // ------------------------------------- - let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (!isLeader && this.lastRequestedAudioSeqRaw !== null) { - debug(`[${this.sessionId}]: FOLLOWER: Reading data from store!`); - - let leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); + async _parseFromLiveSource(current_mediasequence_raw) { + try { + // --------------------------------- + // PARSE M3U's FROM LIVE-SOURCE + // --------------------------------- + let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + const audioTracksExist = Object.keys(this.audioManifestURIs).length > 0 ? true : false; + // NEW FOLLOWER - Edge Case: One Instance is ahead of another. Read latest live segs from store + if (!isLeader) { + const leadersCurrentMseqRaw = await this.sessionLiveState.get("lastRequestedMediaSeqRaw"); + const counts = await this.sessionLiveState.get("firstCounts"); + const leadersFirstMseqRaw = counts.liveSourceMseqCount; + if (leadersCurrentMseqRaw !== null && leadersCurrentMseqRaw > current_mediasequence_raw) { + // if leader never had any segs from prev mseq + if (leadersFirstMseqRaw !== null && leadersFirstMseqRaw === leadersCurrentMseqRaw) { + // Follower updates it's manifest ingedients (segment holders & counts) + this.lastRequestedMediaSeqRaw = leadersCurrentMseqRaw; + this.liveSegsForFollowers = await this.sessionLiveState.get("liveSegsForFollowers"); + if (audioTracksExist) { + this.liveSegsForFollowersAudio = await this.sessionLiveState.get("liveSegsForFollowersAudio"); + } - if (!leadersAudioSeqRaw < this.lastRequestedAudioSeqRaw && this.blockGenerateManifest) { - this.blockGenerateManifest = false; - } + debug(`[${this.sessionId}]: NEW FOLLOWER: Leader is ahead or behind me! Clearing Queue and Getting latest segments from store.`); + this._updateLiveSegQueue(); + if (audioTracksExist) { + this._updateLiveSegQueueAudio(); + } - let attempts = 10; - // CHECK AGAIN CASE 1: Store Empty - while (!leadersAudioSeqRaw && attempts > 0) { - if (!leadersAudioSeqRaw) { - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.instanceId}]: I'm the new leader`); + this.firstTime = false; + debug( + `[${this.sessionId}]: Got all needed segments from live-source (read from store).\nWe are now able to build Live Manifest: [${this.mediaSeqCount}]` + ); return; + } else if (leadersCurrentMseqRaw < this.lastRequestedMediaSeqRaw) { + // WE ARE A RESPAWN-NODE, and we are ahead of leader. + this.blockGenerateManifest = true; } } - - if (!this.allowedToSet) { - debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); - break; - } - const segDur = this._getAnyFirstAudioSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; - const waitTimeMs = parseInt(segDur / 3, 10); - debug(`[${this.sessionId}]: FOLLOWER: Leader has not put anything in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`); - await timer(waitTimeMs); - this.timerCompensation = false; - leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); - attempts--; - } - - if (!leadersAudioSeqRaw) { - debug(`[${this.instanceId}]: The leader is still alive`); - return; } + if (this.allowedToSet) { + // Collect and Push Segment-Extracting Promises + let pushPromises = []; + for (let i = 0; i < Object.keys(this.mediaManifestURIs).length; i++) { + let bw = Object.keys(this.mediaManifestURIs)[i]; + // will add new segments to live seg queue + pushPromises.push(this._parseMediaManifest(this.liveSourceM3Us[bw].M3U, this.mediaManifestURIs[bw], bw, isLeader)); + debug(`[${this.sessionId}]: Pushed pushPromise for bw=${bw}`); + } + // Collect and Push Segment-Extracting Promises (audio) + for (let i = 0; i < Object.keys(this.audioManifestURIs).length; i++) { + let at = Object.keys(this.audioManifestURIs)[i]; + // will add new segments to live seg queue + pushPromises.push(this._parseAudioManifest(this.liveSourceM3Us[at].M3U, this.audioManifestURIs[at], at, isLeader)); + debug(`[${this.sessionId}]: Pushed pushPromise for audiotrack=${at}`); + } + // Segment Pushing + debug(`[${this.sessionId}]: Executing Promises II: Segment Pushing`); + await allSettled(pushPromises); - let liveAudioSegsInStore = await this.sessionLiveState.get("liveAudioSegsForFollowers"); - attempts = 10; - // CHECK AGAIN CASE 2: Store Old - while ((leadersAudioSeqRaw <= this.lastRequestedAudioSeqRaw && attempts > 0) || (this._containsAudioSegment(this.liveAudioSegsForFollowers, liveAudioSegsInStore) && attempts > 0)) { - if (!this.allowedToSet) { - debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Store`); - break; + // UPDATE COUNTS, & Shift Segments in vodSegments and liveSegQueue if needed. + const leaderORFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; + const newTotalDuration = this._incrementAndShift(leaderORFollower); + if (audioTracksExist) { + this._incrementAndShiftAudio(leaderORFollower); } - if (leadersAudioSeqRaw <= this.lastRequestedAudioSeqRaw) { - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.instanceId}][${this.sessionId}]: I'm the new leader`); - return; - } + if (newTotalDuration) { + debug(`[${this.sessionId}]: New Adjusted Playlist Duration=${newTotalDuration}s`); } - if (this._containsAudioSegment(this.liveAudioSegsForFollowers, liveAudioSegsInStore)) { - debug(`[${this.sessionId}]: FOLLOWER: _containsSegment=true,${leadersAudioSeqRaw},${this.lastRequestedAudioSeqRaw}`); - } - const segDur = this._getAnyFirstAudioSegmentDurationMs() || DEFAULT_PLAYHEAD_INTERVAL_MS; - const waitTimeMs = parseInt(segDur / 3, 10); - debug(`[${this.sessionId}]: FOLLOWER: Cannot find anything NEW in store... Will check again in ${waitTimeMs}ms (Tries left=[${attempts}])`); - await timer(waitTimeMs); - this.timerCompensation = false; - leadersAudioSeqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); - liveAudioSegsInStore = await this.sessionLiveState.get("liveAudioSegsForFollowers"); - attempts--; - } - // FINALLY - if (leadersAudioSeqRaw <= this.lastRequestedAudioSeqRaw) { - debug(`[${this.instanceId}][${this.sessionId}]: The leader is still alive`); - return; } - // Follower updates its manifest building blocks (segment holders & counts) - this.lastRequestedAudioSeqRaw = leadersAudioSeqRaw; - this.liveAudioSegsForFollowers = liveAudioSegsInStore; - debug(`[${this.sessionId}]: These are the segments from store: [${JSON.stringify(this.liveAudioSegsForFollowers)}]`); - this._updateLiveAudioSegQueue(); - return; - } - // --------------------------------- - // FETCHING FROM LIVE-SOURCE - New Followers (once) & Leaders do this. - // --------------------------------- - let FETCH_ATTEMPTS = 10; - this.liveAudioSegsForFollowers = {}; - let groupLangToSkipOnRetry = []; - while (FETCH_ATTEMPTS > 0) { + // ----------------------------------------------------- + // Leader writes to store so that Followers can read. + // ----------------------------------------------------- if (isLeader) { - debug(`[${this.sessionId}]: LEADER: Trying to fetch manifests for all groups and language\n Attempts left=[${FETCH_ATTEMPTS}]`); - } else { - debug(`[${this.sessionId}]: NEW FOLLOWER: Trying to fetch manifests for all groups and language\n Attempts left=[${FETCH_ATTEMPTS}]`); - } - - if (!this.allowedToSet) { - debug(`[${this.sessionId}]: We are about to switch away from LIVE. Abort fetching from Live-Source`); - break; - } - - // Reset Values Each Attempt - let livePromises = []; - let manifestList = []; - this.pushAmountAudio = 0; - try { - if (groupLangToSkipOnRetry.length > 0) { - debug(`[${this.sessionId}]: (X) Skipping loadAudio promises for bws ${JSON.stringify(groupLangToSkipOnRetry)}`); - } - // Collect Live Source Requesting Promises - const groupIds = Object.keys(this.audioManifestURIs) - for (let i = 0; i < groupIds.length; i++) { - let groupId = groupIds[i]; - let langs = Object.keys(this.audioManifestURIs[groupId]); - for (let j = 0; j < langs.length; j++) { - const lang = langs[j]; - if (groupLangToSkipOnRetry.includes(groupId + lang)) { - continue; + if (this.allowedToSet) { + const liveBws = Object.keys(this.liveSegsForFollowers); + const segListSize = this.liveSegsForFollowers[liveBws[0]].length; + // Do not replace old data with empty data + if (segListSize > 0) { + debug(`[${this.sessionId}]: LEADER: Adding data to store!`); + await this.sessionLiveState.set("lastRequestedMediaSeqRaw", this.lastRequestedMediaSeqRaw); + await this.sessionLiveState.set("liveSegsForFollowers", this.liveSegsForFollowers); + if (audioTracksExist) { + await this.sessionLiveState.set("liveSegsForFollowersAudio", this.liveSegsForFollowersAudio); } - livePromises.push(this._loadAudioManifest(groupId, lang)); - debug(`[${this.sessionId}]: Pushed loadAudio promise for groupId,lang=[${groupId}, ${lang}]`); } } - // Fetch From Live Source - debug(`[${this.sessionId}]: Executing Promises I: Fetch From Live Audio Source`); - manifestList = await allSettled(livePromises); - livePromises = []; - } catch (err) { - debug(`[${this.sessionId}]: Promises I: FAILURE!\n${err}`); - return; - } - - // Handle if any promise got rejected - if (manifestList.some((result) => result.status === "rejected")) { - FETCH_ATTEMPTS--; - debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again in 1000ms...`); - await timer(1000); - continue; - } - - // Store the results locally - manifestList.forEach((variantItem) => { - const groupId = variantItem.value.groupId; - const lang = variantItem.value.lang; - if (!this.liveAudioSourceM3Us[groupId]) { - this.liveAudioSourceM3Us[groupId] = {}; - } - if (!this.liveAudioSourceM3Us[groupId][lang]) { - this.liveAudioSourceM3Us[groupId][lang] = {}; - } - this.liveAudioSourceM3Us[groupId][lang] = variantItem.value; - }); - const allStoredAudioSeqCounts = [];//Object.keys(this.liveAudioSourceM3Us).map((variant) => this.liveSourceM3Us[variant].mediaSeq); + // [LASTLY]: LEADER does this for respawned-FOLLOWERS' sake. + if (this.firstTime && this.allowedToSet) { + // Buy some time for followers (NOT Respawned) to fetch their own L.S m3u8. + await timer(1000); // maybe remove + let firstCounts = await this.sessionLiveState.get("firstCounts"); + firstCounts.liveSourceMseqCount = this.lastRequestedMediaSeqRaw; + firstCounts.mediaSeqCount = this.prevMediaSeqCount; + firstCounts.discSeqCount = this.prevDiscSeqCount; - const groupIds = Object.keys(this.liveAudioSourceM3Us) - for (let i = 0; i < groupIds.length; i++) { - const langs = Object.keys(this.liveAudioSourceM3Us[groupIds[i]]); - for (let j = 0; j < langs.length; j++) { - allStoredAudioSeqCounts.push(this.liveAudioSourceM3Us[groupIds[i]][langs[j]].mediaSeq); + debug(`[${this.sessionId}]: LEADER: I am adding 'firstCounts'=${JSON.stringify(firstCounts)} to Store for future followers`); + await this.sessionLiveState.set("firstCounts", firstCounts); } + debug(`[${this.sessionId}]: LEADER: I am using segs from Mseq=${this.lastRequestedMediaSeqRaw}`); + } else { + debug(`[${this.sessionId}]: NEW FOLLOWER: I am using segs from Mseq=${this.lastRequestedMediaSeqRaw}`); } - // Handle if mediaSeqCounts are NOT synced up! - if (!allStoredAudioSeqCounts.every((val, i, arr) => val === arr[0])) { - debug(`[${this.sessionId}]: Live audio Mseq counts=[${allStoredAudioSeqCounts}]`); - // Figure out what group lang is behind. - const highestMediaSeqCount = Math.max(...allStoredAudioSeqCounts); - const gi = Object.keys(this.liveAudioSourceM3Us) - for (let i = 0; i < gi.length; i++) { - const langs = Object.keys(this.liveAudioSourceM3Us[groupIds[i]]); - for (let j = 0; j < langs.length; j++) { - if (this.liveSourceM3Us[gi[i]][langs[j]].mediaSeq === highestMediaSeqCount) { - groupLangToSkipOnRetry.push(gi[i] + langs[j]) - } - } - } - - // Decrement fetch counter - FETCH_ATTEMPTS--; - // Calculate retry delay time. Default=1000 - let retryDelayMs = 1000; - if (Object.keys(this.liveAudioSegQueue).length > 0) { - const firstGroupId = Object.keys(this.liveAudioSegQueue)[0]; - const firstLang = Object.keys(this.liveAudioSegQueue[firstGroupId])[0]; - const lastIdx = this.liveAudioSegQueue[firstGroupId][firstLang].length - 1; - if (this.liveAudioSegQueue[firstGroupId][lastIdx].duration) { - retryDelayMs = this.liveAudioSegQueue[firstGroupId][lastIdx].duration * 1000 * 0.25; - } - } - // Wait a little before trying again - debug(`[${this.sessionId}]: ALERT! Live Source Data NOT in sync! Will try again after ${retryDelayMs}ms`); - await timer(retryDelayMs); - if (isLeader) { - this.timerCompensation = false; - } - continue; - } - - currentMseqRaw = allStoredAudioSeqCounts[0]; - - if (!isLeader) { - let leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); - let tries = 20; - - while ((!isLeader && !leadersFirstSeqCounts.liveSourceAudioMseqCount && tries > 0) || leadersFirstSeqCounts.liveAudioSourceMseqCount === 0) { - debug(`[${this.sessionId}]: NEW FOLLOWER: Waiting for LEADER to add 'firstCounts' in store! Will look again after 1000ms (tries left=${tries})`); - await timer(1000); - leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); - tries--; - // Might take over as Leader if Leader is not setting data due to being down. - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); - } - } - - if (tries === 0) { - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - if (isLeader) { - debug(`[${this.sessionId}][${this.instanceId}]: I'm the new leader, and now I am going to add 'firstCounts' in store`); - break; - } else { - debug(`[${this.sessionId}][${this.instanceId}]: The leader is still alive`); - leadersFirstSeqCounts = await this.sessionLiveState.get("firstCounts"); - if (!leadersFirstSeqCounts.liveSourceMseqCount) { - debug(`[${this.sessionId}][${this.instanceId}]: Could not find 'firstCounts' in store. Abort Executing Promises II & Returning to Playhead.`); - return; - } - } - } - - if (isLeader) { - debug(`[${this.sessionId}]: NEW LEADER: Original Leader went missing, I am retrying live source fetch...`); - await this.sessionLiveState.set("transitAudioSegs", this.vodAudioSegments); - debug(`[${this.sessionId}]: NEW LEADER: I am adding 'transitSegs' to Store for future followers`); - continue; - } - - // Respawners never do this, only starter followers. - // Edge Case: FOLLOWER transitioned from session with different segments from LEADER - if (leadersFirstSeqCounts.discSeqCount !== this.audioDiscSeqCount) { - this.audioDiscSeqCount = leadersFirstSeqCounts.discSeqCount; - } - if (leadersFirstSeqCounts.audioSeqCount !== this.audioSeqCount) { - this.audioSeqCount = leadersFirstSeqCounts.audioSeqCount; - debug( - `[${this.sessionId}]: FOLLOWER transitioned with wrong V2L segments, updating counts to [${this.audioSeqCount}][${this.audioDiscSeqCount}], and reading 'transitSegs' from store` - ); - const transitSegs = await this.sessionLiveState.get("transitAudioSegs"); - if (!this._isEmpty(transitSegs)) { - this.vodAudioSegments = transitSegs; - } - } + this.firstTime = false; + debug( + `[${this.sessionId}]: Got all needed segments from live-source (from all bandwidths).\nWe are now able to build Live Manifest: [${this.mediaSeqCount}]` + ); - // Prepare to load segments... - debug(`[${this.instanceId}][${this.sessionId}]: Newest mseq from LIVE=${currentMseqRaw} First mseq in store=${leadersFirstSeqCounts.liveAudioSourceMseqCount}`); - if (currentMseqRaw === leadersFirstSeqCounts.liveAudioSourceMseqCount) { - this.pushAmountAudio = 1; // Follower from start - } else { - // TODO: To support and account for past discontinuity tags in the Live Source stream, - // we will need to get the real 'current' discontinuity-sequence count from Leader somehow. - - // RESPAWNED NODES - this.pushAmountAudio = currentMseqRaw - leadersFirstSeqCounts.liveAudioSourceMseqCount + 1; - - const transitSegs = await this.sessionLiveState.get("transitAudioSegs"); - //debug(`[${this.sessionId}]: NEW FOLLOWER: I tried to get 'transitSegs'. This is what I found ${JSON.stringify(transitSegs)}`); - if (!this._isEmpty(transitSegs)) { - this.vodAudioSegments = transitSegs; - } - } - debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmountAudio}`); - } else { - // LEADER calculates pushAmount differently... - if (this.firstTimeAudio) { - this.pushAmountAudio = 1; // Leader from start - } else { - this.pushAmountAudio = currentMseqRaw - this.lastRequestedAudioSeqRaw; - debug(`[${this.sessionId}]: ...calculating pushAmount=${currentMseqRaw}-${this.lastRequestedAudioSeqRaw}=${this.pushAmountAudio}`); - } - debug(`[${this.sessionId}]: ...pushAmount=${this.pushAmountAudio}`); - break; - } - // Live Source Data is in sync, and LEADER & new FOLLOWER are in sync - break; - } - - if (FETCH_ATTEMPTS === 0) { - debug(`[${this.sessionId}]: Fetching from Live-Source did not work! Returning to Playhead Loop...`); return; + } catch (e) { + console.error(e); + return Promise.reject(e); } - - isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); - // NEW FOLLOWER - Edge Case: One Instance is ahead of another. Read latest live segs from store - if (!isLeader) { - const leadersCurrentMseqRaw = await this.sessionLiveState.get("lastRequestedAudioSeqRaw"); - const counts = await this.sessionLiveState.get("firstCounts"); - const leadersFirstMseqRaw = counts.liveSourceAudioMseqCount; - if (leadersCurrentMseqRaw !== null && leadersCurrentMseqRaw > currentMseqRaw) { - // if leader never had any segs from prev mseq - if (leadersFirstMseqRaw !== null && leadersFirstMseqRaw === leadersCurrentMseqRaw) { - // Follower updates it's manifest ingedients (segment holders & counts) - this.lastRequestedAudioSeqRaw = leadersCurrentMseqRaw; - this.liveAudioSegsForFollowers = await this.sessionLiveState.get("liveAudioSegsForFollowers"); - debug(`[${this.sessionId}]: NEW FOLLOWER: Leader is ahead or behind me! Clearing Queue and Getting latest segments from store.`); - this._updateLiveAudioSegQueue(); - this.firstTimeAudio = false; - debug(`[${this.sessionId}]: Got all needed segments from live-source (read from store).\nWe are now able to build Audio Live Manifest: [${this.audioSeqCount}]`); - return; - } else if (leadersCurrentMseqRaw < this.lastRequestedAudioSeqRaw) { - // WE ARE A RESPAWN-NODE, and we are ahead of leader. - this.blockGenerateManifest = true; - } - } - } - if (this.allowedToSet) { - // Collect and Push Segment-Extracting Promises - let pushPromises = []; - for (let i = 0; i < Object.keys(this.audioManifestURIs).length; i++) { - let groupId = Object.keys(this.audioManifestURIs)[i]; - let langs = Object.keys(this.audioManifestURIs[groupId]); - for (let j = 0; j < langs.length; j++) { - let lang = langs[j]; - // will add new segments to live seg queue - pushPromises.push(this._parseAudioManifest(this.liveAudioSourceM3Us[groupId][lang].M3U, this.audioManifestURIs[groupId][lang], groupId, lang, isLeader)); - debug(`[${this.sessionId}]: Pushed pushPromise for groupId=${groupId} & lang${lang}`); + } + /** + * This function adds new live segments to the node from which it can + * generate new manifests from. Method for attaining new segments differ + * depending on node Rank. The Leader collects from live source and + * Followers collect from shared storage. + */ + async _loadAllPlaylistManifests() { + try { + let isLeader = await this.sessionLiveStateStore.isLeader(this.instanceId); + if (!isLeader && this.lastRequestedMediaSeqRaw !== null) { + // FOLLWERS Do this + await this._collectSegmentsFromStore(); + } else { + // LEADERS and NEW-FOLLOWERS Do this + const result = await this._fetchFromLiveSource(); + if (result.success) { + await this._parseFromLiveSource(result.currentMseqRaw); } } - // Segment Pushing - debug(`[${this.sessionId}]: Executing Promises II: Segment Pushing`); - await allSettled(pushPromises); - - // UPDATE COUNTS, & Shift Segments in vodSegments and liveSegQueue if needed. - const leaderORFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; - const newTotalDuration = this._incrementAndShiftAudio(leaderORFollower); // might need audio - if (newTotalDuration) { - debug(`[${this.sessionId}]: New Adjusted Playlist Duration=${newTotalDuration}s`); - } - } - - // ----------------------------------------------------- - // Leader writes to store so that Followers can read. - // ----------------------------------------------------- - if (isLeader) { - if (this.allowedToSet) { - const liveGroupIds = Object.keys(this.liveAudioSegsForFollowers); - const liveLangs = Object.keys(this.liveAudioSegsForFollowers[liveGroupIds[0]]); - const segListSize = this.liveAudioSegsForFollowers[liveGroupIds[0]][liveLangs[0]].length; - // Do not replace old data with empty data - if (segListSize > 0) { - debug(`[${this.sessionId}]: LEADER: Adding data to store!`); - await this.sessionLiveState.set("lastRequestedAudioSeqRaw", this.lastRequestedAudioSeqRaw); - await this.sessionLiveState.set("liveAudioSegsForFollowers", this.liveAudioSegsForFollowers); - } - } - - // [LASTLY]: LEADER does this for respawned-FOLLOWERS' sake. - if (this.firstTimeAudio && this.allowedToSet) { - // Buy some time for followers (NOT Respawned) to fetch their own L.S m3u8. - await timer(1000); // maybe remove - let firstCounts = await this.sessionLiveState.get("firstCounts"); - firstCounts.liveSourceAudioMseqCount = this.lastRequestedAudioSeqRaw; - firstCounts.audioSeqCount = this.prevAudioSeqCount; - firstCounts.discAudioSeqCount = this.prevAudioDiscSeqCount; - - debug(`[${this.sessionId}]: LEADER: I am adding 'firstCounts'=${JSON.stringify(firstCounts)} to Store for future followers`); - await this.sessionLiveState.set("firstCounts", firstCounts); - } - debug(`[${this.sessionId}]: LEADER: I am using segs from Mseq=${this.lastRequestedAudioSeqRaw}`); - } else { - debug(`[${this.sessionId}]: NEW FOLLOWER: I am using segs from Mseq=${this.lastRequestedAudioSeqRaw}`); + return; + } catch (e) { + console.error("Failure in _loadAllPlaylistManifests:" + e); } - - this.firstTimeAudio = false; - debug(`[${this.sessionId}]: Got all needed segments from live-source (from all bandwidths).\nWe are now able to build Audi Live Manifest: [${this.audioSeqCount}]`); - - return; } _shiftSegments(opt) { @@ -1616,22 +1408,24 @@ class SessionLive { if (opt && opt.type) { _type = opt.type; } - const bws = Object.keys(_segments); - + const variantKeys = Object.keys(_segments); /* When Total Duration is past the Limit, start Shifting V2L|LIVE segments if found */ while (_totalDur > TARGET_PLAYLIST_DURATION_SEC) { let result = null; - if (_type === "VIDEO") { - result = this._shiftMediaSegments(bws, _name, _segments, _totalDur); - } else { - result = this._shiftAudioSegments(bws, _name, _segments, _totalDur); - } + result = this._shiftVariantSegments(variantKeys, _name, _segments); // Skip loop if there are no more segments to remove... if (!result) { - return { totalDuration: _totalDur, removedSegments: _removedSegments, removedDiscontinuities: _removedDiscontinuities, shiftedSegments: _segments }; - } - debug(`[${this.sessionId}]: ${_name}: (${_totalDur})s/(${TARGET_PLAYLIST_DURATION_SEC})s - Playlist Duration is Over the Target. Shift needed!`); + return { + totalDuration: _totalDur, + removedSegments: _removedSegments, + removedDiscontinuities: _removedDiscontinuities, + shiftedSegments: _segments, + }; + } + debug( + `[${this.sessionId}]: ${_name}: (${_totalDur})s/(${TARGET_PLAYLIST_DURATION_SEC})s - Playlist Duration is Over the Target. Shift needed!` + ); _segments = result.segments; if (result.timeToRemove) { _totalDur -= result.timeToRemove; @@ -1643,28 +1437,33 @@ class SessionLive { _removedDiscontinuities++; } } - return { totalDuration: _totalDur, removedSegments: _removedSegments, removedDiscontinuities: _removedDiscontinuities, shiftedSegments: _segments }; + return { + totalDuration: _totalDur, + removedSegments: _removedSegments, + removedDiscontinuities: _removedDiscontinuities, + shiftedSegments: _segments, + }; } - _shiftMediaSegments(bws, _name, _segments) { - if (_segments[bws[0]].length === 0) { + _shiftVariantSegments(variantKeys, _name, _segments) { + if (_segments[variantKeys[0]].length === 0) { return null; } let timeToRemove = 0; let incrementDiscSeqCount = false; // Shift Segments for each variant... - for (let i = 0; i < bws.length; i++) { - let seg = _segments[bws[i]].shift(); + for (let i = 0; i < variantKeys.length; i++) { + let seg = _segments[variantKeys[i]].shift(); if (i === 0) { - debug(`[${this.sessionId}]: ${_name}: (${bws[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); + debug(`[${this.sessionId}]: ${_name}: (${variantKeys[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); } if (seg && seg.discontinuity) { incrementDiscSeqCount = true; - if (_segments[bws[i]].length > 0) { - seg = _segments[bws[i]].shift(); + if (_segments[variantKeys[i]].length > 0) { + seg = _segments[variantKeys[i]].shift(); if (i === 0) { - debug(`[${this.sessionId}]: ${_name}: (${bws[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); + debug(`[${this.sessionId}]: ${_name}: (${variantKeys[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); } } } @@ -1672,40 +1471,7 @@ class SessionLive { timeToRemove = seg.duration; } } - return { timeToRemove: timeToRemove, incrementDiscSeqCount: incrementDiscSeqCount, segments: _segments } - } - - _shiftAudioSegments(groupIds, _name, _segments) { - const firstLang = Object.keys(_segments[groupIds[0]])[0]; - if (_segments[groupIds[0]][firstLang].length === 0) { - return null; - } - let timeToRemove = 0; - let incrementDiscSeqCount = false; - - // Shift Segments for each variant... - for (let i = 0; i < groupIds.length; i++) { - const langs = Object.keys(_segments[groupIds[i]]); - for (let j = 0; j < langs.length; j++) { - let seg = _segments[groupIds[i]][langs[j]].shift(); - if (i === 0) { - debug(`[${this.sessionId}]: ${_name}: (${groupIds[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); - } - if (seg && seg.discontinuity) { - incrementDiscSeqCount = true; - if (_segments[groupIds[i]][langs[j]].length > 0) { - seg = _segments[groupIds[i]][langs[j]].shift(); - if (i === 0) { - debug(`[${this.sessionId}]: ${_name}: (${groupIds[i]}) Ejected from playlist->: ${JSON.stringify(seg, null, 2)}`); - } - } - } - if (seg && seg.duration) { - timeToRemove = seg.duration; - } - } - } - return { timeToRemove: timeToRemove, incrementDiscSeqCount: incrementDiscSeqCount, segments: _segments } + return { timeToRemove: timeToRemove, incrementDiscSeqCount: incrementDiscSeqCount, segments: _segments }; } /** @@ -1796,10 +1562,8 @@ class SessionLive { if (!instanceName) { instanceName = "UNKNOWN"; } - const vodGroupId = Object.keys(this.vodAudioSegments)[0]; - const vodLanguage = Object.keys(this.vodAudioSegments[vodGroupId])[0]; - const liveGroupId = Object.keys(this.liveAudioSegQueue)[0]; - const liveLanguage = Object.keys(this.liveAudioSegQueue[vodGroupId])[0]; + const vodAudiotrack = Object.keys(this.vodSegmentsAudio); + const liveAudiotrack = Object.keys(this.liveSegQueueAudio); let vodTotalDur = 0; let liveTotalDur = 0; let totalDur = 0; @@ -1807,12 +1571,12 @@ class SessionLive { let removedDiscontinuities = 0; // Calculate Playlist Total Duration - this.vodAudioSegments[vodGroupId][vodLanguage].forEach((seg) => { + this.vodSegmentsAudio[vodAudiotrack[0]].forEach((seg) => { if (seg.duration) { vodTotalDur += seg.duration; } }); - this.liveAudioSegQueue[liveGroupId][liveLanguage].forEach((seg) => { + this.liveSegQueueAudio[liveAudiotrack[0]].forEach((seg) => { if (seg.duration) { liveTotalDur += seg.duration; } @@ -1826,13 +1590,13 @@ class SessionLive { const outputV2L = this._shiftSegments({ name: instanceName, totalDur: totalDur, - segments: this.vodAudioSegments, + segments: this.vodSegmentsAudio, removedSegments: removedSegments, removedDiscontinuities: removedDiscontinuities, type: "AUDIO", }); // Update V2L Segments - this.vodAudioSegments = outputV2L.shiftedSegments; + this.vodSegmentsAudio = outputV2L.shiftedSegments; // Update values totalDur = outputV2L.totalDuration; removedSegments = outputV2L.removedSegments; @@ -1841,13 +1605,13 @@ class SessionLive { const outputLIVE = this._shiftSegments({ name: instanceName, totalDur: totalDur, - segments: this.liveAudioSegQueue, + segments: this.liveSegQueueAudio, removedSegments: removedSegments, removedDiscontinuities: removedDiscontinuities, type: "AUDIO", }); // Update LIVE Segments - this.liveAudioSegQueue = outputLIVE.shiftedSegments; + this.liveSegQueueAudio = outputLIVE.shiftedSegments; // Update values totalDur = outputLIVE.totalDuration; removedSegments = outputLIVE.removedSegments; @@ -1919,50 +1683,53 @@ class SessionLive { }); } - async _loadAudioManifest(groupId, lang) { - if (!this.sessionLiveState) { - throw new Error("SessionLive not ready"); - } - const liveTargetGroupLang = this._findAudioGroupAndLang(groupId, lang, this.audioManifestURIs); - debug(`[${this.sessionId}]: Requesting groupId=(${groupId}) & lang=(${lang}), Nearest match is: ${JSON.stringify(liveTargetGroupLang)}`); - // Get the target media manifest - const audioManifestUri = this.audioManifestURIs[liveTargetGroupLang.audioGroupId][liveTargetGroupLang.audioLanguage]; - const parser = m3u8.createStream(); - const controller = new AbortController(); - const timeout = setTimeout(() => { - debug(`[${this.sessionId}]: Request Timeout! Aborting Request to ${audioManifestUri}`); - controller.abort(); - }, FAIL_TIMEOUT); - - const response = await fetch(audioManifestUri, { signal: controller.signal }); + async _loadAudioManifest(audiotrack) { try { - response.body.pipe(parser); + if (!this.sessionLiveState) { + throw new Error("SessionLive not ready"); + } + const liveTargetAudiotrack = this._findNearestAudiotrack(audiotrack, Object.keys(this.audioManifestURIs)); + debug(`[${this.sessionId}]: Requesting audiotrack (${audiotrack}), Nearest match is: ${JSON.stringify(liveTargetAudiotrack)}`); + // Get the target media manifest + const audioManifestUri = this.audioManifestURIs[liveTargetAudiotrack]; + const parser = m3u8.createStream(); + const controller = new AbortController(); + const timeout = setTimeout(() => { + debug(`[${this.sessionId}]: Request Timeout! Aborting Request to ${audioManifestUri}`); + controller.abort(); + }, FAIL_TIMEOUT); + const response = await fetch(audioManifestUri, { signal: controller.signal }); + try { + response.body.pipe(parser); + } catch (err) { + debug(`[${this.sessionId}]: Error when piping response to parser! ${JSON.stringify(err)}`); + return Promise.reject(err); + } finally { + clearTimeout(timeout); + } + return new Promise((resolve, reject) => { + parser.on("m3u", (m3u) => { + try { + const resolveObj = { + M3U: m3u, + mediaSeq: m3u.get("mediaSequence"), + audiotrack: liveTargetAudiotrack, + }; + resolve(resolveObj); + } catch (exc) { + debug(`[${this.sessionId}]: Error when parsing latest manifest`); + reject(exc); + } + }); + parser.on("error", (exc) => { + debug(`Parser Error: ${JSON.stringify(exc)}`); + reject(exc); + }); + }); } catch (err) { - debug(`[${this.sessionId}]: Error when piping response to parser! ${JSON.stringify(err)}`); + console.error(err); return Promise.reject(err); - } finally { - clearTimeout(timeout); } - return new Promise((resolve, reject) => { - parser.on("m3u", (m3u) => { - try { - const resolveObj = { - M3U: m3u, - mediaSeq: m3u.get("mediaSequence"), - groupId: liveTargetGroupLang.audioGroupId, - lang: liveTargetGroupLang.audioLanguage, - }; - resolve(resolveObj); - } catch (exc) { - debug(`[${this.sessionId}]: Error when parsing latest manifest`); - reject(exc); - } - }); - parser.on("error", (exc) => { - debug(`Parser Error: ${JSON.stringify(exc)}`); - reject(exc); - }); - }); } _parseMediaManifest(m3u, mediaManifestUri, liveTargetBandwidth, isLeader) { @@ -1994,7 +1761,7 @@ class SessionLive { } if (mediaManifestUri) { // push segments - this._addLiveSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetBandwidth, isLeader); + this._addLiveSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetBandwidth, isLeader, PlaylistTypes.VIDEO); } resolve(); } catch (exc) { @@ -2004,20 +1771,14 @@ class SessionLive { }); } - _parseAudioManifest(m3u, audioPlaylistUri, liveTargetGroupId, liveTargetLanguage, isLeader) { + _parseAudioManifest(m3u, audioPlaylistUri, liveTargetAudiotrack, isLeader) { return new Promise(async (resolve, reject) => { try { - if (!this.liveAudioSegQueue[liveTargetGroupId]) { - this.liveAudioSegQueue[liveTargetGroupId] = {}; + if (!this.liveSegQueueAudio[liveTargetAudiotrack]) { + this.liveSegQueueAudio[liveTargetAudiotrack] = []; } - if (!this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage]) { - this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage] = []; - } - if (!this.liveAudioSegsForFollowers[liveTargetGroupId]) { - this.liveAudioSegsForFollowers[liveTargetGroupId] = {}; - } - if (!this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage]) { - this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage] = []; + if (!this.liveSegsForFollowersAudio[liveTargetAudiotrack]) { + this.liveSegsForFollowersAudio[liveTargetAudiotrack] = []; } let baseUrl = ""; const m = audioPlaylistUri.match(/^(.*)\/.*?$/); @@ -2028,18 +1789,21 @@ class SessionLive { //debug(`[${this.sessionId}]: Current RAW Mseq: [${m3u.get("mediaSequence")}]`); //debug(`[${this.sessionId}]: Previous RAW Mseq: [${this.lastRequestedAudioSeqRaw}]`); - if (this.pushAmountAudio >= 0) { - this.lastRequestedAudioSeqRaw = m3u.get("mediaSequence"); + /* + WARN: We are assuming here that the MSEQ and Segment lengths are the same on Audio and Video + and therefor need to push an equal amount of segments + */ + if (this.pushAmount >= 0) { + this.lastRequestedMediaSeqRaw = m3u.get("mediaSequence"); } this.targetDuration = m3u.get("targetDuration"); - let startIdx = m3u.items.PlaylistItem.length - this.pushAmountAudio; + let startIdx = m3u.items.PlaylistItem.length - this.pushAmount; if (startIdx < 0) { - this.restAmountAudio = startIdx * -1; + this.restAmount = startIdx * -1; startIdx = 0; } if (audioPlaylistUri) { - // push segments - this._addLiveAudioSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetGroupId, liveTargetLanguage, isLeader); + this._addLiveSegmentsToQueue(startIdx, m3u.items.PlaylistItem, baseUrl, liveTargetAudiotrack, isLeader, PlaylistTypes.AUDIO); } resolve(); } catch (exc) { @@ -2057,171 +1821,174 @@ class SessionLive { * @param {string} baseUrl * @param {string} liveTargetBandwidth */ - _addLiveSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetBandwidth, isLeader) { - const leaderOrFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; - - for (let i = startIdx; i < playlistItems.length; i++) { - let seg = {}; - let playlistItem = playlistItems[i]; - let segmentUri; - let cueData = null; - let daterangeData = null; - let attributes = playlistItem["attributes"].attributes; - if (playlistItem.properties.discontinuity) { - this.liveSegQueue[liveTargetBandwidth].push({ discontinuity: true }); - this.liveSegsForFollowers[liveTargetBandwidth].push({ discontinuity: true }); - } - if ("cuein" in attributes) { - if (!cueData) { - cueData = {}; + _addLiveSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetVariant, isLeader, plType) { + try { + const leaderOrFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; + for (let i = startIdx; i < playlistItems.length; i++) { + let seg = {}; + const playlistItem = playlistItems[i]; + let segmentUri; + let byteRange = undefined; + let initSegment = undefined; + let initSegmentByteRange = undefined; + let keys = undefined; + let daterangeData = null; + if (i === startIdx) { + for (let j = startIdx; j >= 0; j--) { + const pli = playlistItems[j]; + if (pli.get("map-uri")) { + initSegmentByteRange = pli.get("map-byterange"); + if (pli.get("map-uri").match("^http")) { + initSegment = pli.get("map-uri"); + } else { + initSegment = urlResolve(baseUrl, pli.get("map-uri")); + } + break; + } + } } - console.log("adding live segment to queue") - cueData["in"] = true; - } - if ("cueout" in attributes) { - if (!cueData) { - cueData = {}; + let attributes = playlistItem["attributes"].attributes; + if (playlistItem.get("map-uri")) { + initSegmentByteRange = playlistItem.get("map-byterange"); + if (playlistItem.get("map-uri").match("^http")) { + initSegment = playlistItem.get("map-uri"); + } else { + initSegment = urlResolve(baseUrl, playlistItem.get("map-uri")); + } } - cueData["out"] = true; - cueData["duration"] = attributes["cueout"]; - } - if ("cuecont" in attributes) { - if (!cueData) { - cueData = {}; + // some items such as CUE-IN parse as a PlaylistItem + // but have no URI + if (playlistItem.get("uri")) { + if (playlistItem.get("uri").match("^http")) { + segmentUri = playlistItem.get("uri"); + } else { + segmentUri = urlResolve(baseUrl, playlistItem.get("uri")); + } } - cueData["cont"] = true; - } - if ("scteData" in attributes) { - if (!cueData) { - cueData = {}; + if (playlistItem.get("discontinuity")) { + if (plType === PlaylistTypes.VIDEO) { + this.liveSegQueue[liveTargetVariant].push({ discontinuity: true }); + this.liveSegsForFollowers[liveTargetVariant].push({ discontinuity: true }); + } else if (plType === PlaylistTypes.AUDIO) { + this.liveSegQueueAudio[liveTargetVariant].push({ discontinuity: true }); + this.liveSegsForFollowersAudio[liveTargetVariant].push({ discontinuity: true }); + } else { + console.warn(`[${this.sessionId}]: WARNING: plType=${plType} Not valid (disc-seg)`); + } } - cueData["scteData"] = attributes["scteData"]; - } - if ("assetData" in attributes) { - if (!cueData) { - cueData = {}; + if (playlistItem.get("byteRange")) { + let [_, r, o] = playlistItem.get("byteRange").match(/^(\d+)@*(\d*)$/); + if (!o) { + o = byteRangeOffset; + } + byteRangeOffset = parseInt(r) + parseInt(o); + byteRange = `${r}@${o}`; + } + if (playlistItem.get("keys")) { + keys = playlistItem.get("keys"); + } + let assetData = playlistItem.get("assetdata"); + let cueOut = playlistItem.get("cueout"); + let cueIn = playlistItem.get("cuein"); + let cueOutCont = playlistItem.get("cont-offset"); + let duration = 0; + let scteData = playlistItem.get("sctedata"); + if (typeof cueOut !== "undefined") { + duration = cueOut; + } else if (typeof cueOutCont !== "undefined") { + duration = playlistItem.get("cont-dur"); + } + let cue = + cueOut || cueIn || cueOutCont || assetData + ? { + out: typeof cueOut !== "undefined", + cont: typeof cueOutCont !== "undefined" ? cueOutCont : null, + scteData: typeof scteData !== "undefined" ? scteData : null, + in: cueIn ? true : false, + duration: duration, + assetData: typeof assetData !== "undefined" ? assetData : null, + } + : null; + seg = { + duration: playlistItem.get("duration"), + timelinePosition: this.timeOffset != null ? this.timeOffset + timelinePosition : null, + cue: cue, + byteRange: byteRange, + }; + if (initSegment) { + seg.initSegment = initSegment; + } + if (initSegmentByteRange) { + seg.initSegmentByteRange = initSegmentByteRange; + } + if (segmentUri) { + seg.uri = segmentUri; + } + if (keys) { + seg.keys = keys; + } + if (i === startIdx) { + // Add daterange metadata if this is the first segment + if (this.rangeMetadata && !this._isEmpty(this.rangeMetadata)) { + seg["daterange"] = this.rangeMetadata; + } } - cueData["assetData"] = attributes["assetData"]; - } - if ("daterange" in attributes) { - if (!daterangeData) { - daterangeData = {}; + if ("daterange" in attributes) { + if (!daterangeData) { + daterangeData = {}; + } + let allDaterangeAttributes = Object.keys(attributes["daterange"]); + allDaterangeAttributes.forEach((attr) => { + if (attr.match(/DURATION$/)) { + daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]); + } else { + daterangeData[attr.toLowerCase()] = attributes["daterange"][attr]; + } + }); } - let allDaterangeAttributes = Object.keys(attributes["daterange"]); - allDaterangeAttributes.forEach((attr) => { - if (attr.match(/DURATION$/)) { - daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]); + if (playlistItem.get("uri")) { + if (daterangeData && !this._isEmpty(daterangeData)) { + seg["daterange"] = daterangeData; + } + // Push new Live Segments! But do not push duplicates + if (plType === PlaylistTypes.VIDEO) { + this._pushToQueue(seg, liveTargetVariant, leaderOrFollower); + } else if (plType === PlaylistTypes.AUDIO) { + this._pushToQueueAudio(seg, liveTargetVariant, leaderOrFollower); } else { - daterangeData[attr.toLowerCase()] = attributes["daterange"][attr]; + console.warn(`[${this.sessionId}]: WARNING: plType=${plType} Not valid (seg)`); } - }); - } - if (playlistItem.properties.uri) { - if (playlistItem.properties.uri.match("^http")) { - segmentUri = playlistItem.properties.uri; - } else { - segmentUri = url.resolve(baseUrl, playlistItem.properties.uri); - } - seg["duration"] = playlistItem.properties.duration; - seg["uri"] = segmentUri; - seg["cue"] = cueData; - if (daterangeData) { - seg["daterange"] = daterangeData; - } - // Push new Live Segments! But do not push duplicates - const liveSegURIs = this.liveSegQueue[liveTargetBandwidth].filter((seg) => seg.uri).map((seg) => seg.uri); - if (seg.uri && liveSegURIs.includes(seg.uri)) { - debug(`[${this.sessionId}]: ${leaderOrFollower}: Found duplicate live segment. Skip push! (${liveTargetBandwidth})`); - } else { - this.liveSegQueue[liveTargetBandwidth].push(seg); - this.liveSegsForFollowers[liveTargetBandwidth].push(seg); - debug(`[${this.sessionId}]: ${leaderOrFollower}: Pushed segment (${seg.uri ? seg.uri : "Disc-tag"}) to 'liveSegQueue' (${liveTargetBandwidth})`); } } + } catch (e) { + console.error(e); + return Promise.reject(e); } } - _addLiveAudioSegmentsToQueue(startIdx, playlistItems, baseUrl, liveTargetGroupId, liveTargetLanguage, isLeader) { - const leaderOrFollower = isLeader ? "LEADER" : "NEW FOLLOWER"; - for (let i = startIdx; i < playlistItems.length; i++) { - let seg = {}; - let playlistItem = playlistItems[i]; - let segmentUri; - let cueData = null; - let daterangeData = null; - let attributes = playlistItem["attributes"].attributes; - if (playlistItem.properties.discontinuity) { - this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage].push({ discontinuity: true }); - this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage].push({ discontinuity: true }); - } - if ("cuein" in attributes) { - console.log("adding live segment to queue") - if (!cueData) { - cueData = {}; - } - cueData["in"] = true; - } - if ("cueout" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["out"] = true; - cueData["duration"] = attributes["cueout"]; - } - if ("cuecont" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["cont"] = true; - } - if ("scteData" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["scteData"] = attributes["scteData"]; - } - if ("assetData" in attributes) { - if (!cueData) { - cueData = {}; - } - cueData["assetData"] = attributes["assetData"]; - } - if ("daterange" in attributes) { - if (!daterangeData) { - daterangeData = {}; - } - let allDaterangeAttributes = Object.keys(attributes["daterange"]); - allDaterangeAttributes.forEach((attr) => { - if (attr.match(/DURATION$/)) { - daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]); - } else { - daterangeData[attr.toLowerCase()] = attributes["daterange"][attr]; - } - }); - } + _pushToQueue(seg, liveTargetBandwidth, logName) { + const liveSegURIs = this.liveSegQueue[liveTargetBandwidth].filter((seg) => seg.uri).map((seg) => seg.uri); + if (seg.uri && liveSegURIs.includes(seg.uri)) { + debug(`[${this.sessionId}]: ${logName}: Found duplicate live segment. Skip push! (${liveTargetBandwidth})`); + } else { + this.liveSegQueue[liveTargetBandwidth].push(seg); + this.liveSegsForFollowers[liveTargetBandwidth].push(seg); + debug(`[${this.sessionId}]: ${logName}: Pushed Video segment (${seg.uri ? seg.uri : "Disc-tag"}) to 'liveSegQueue' (${liveTargetBandwidth})`); + } + } - if (playlistItem.properties.uri) { - if (playlistItem.properties.uri.match("^http")) { - segmentUri = playlistItem.properties.uri; - } else { - segmentUri = url.resolve(baseUrl, playlistItem.properties.uri); - } - seg["duration"] = playlistItem.properties.duration; - seg["uri"] = segmentUri; - seg["cue"] = cueData; - if (daterangeData) { - seg["daterange"] = daterangeData; - } - // Push new Live Segments! But do not push duplicates - const liveSegURIs = this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage].filter((seg) => seg.uri).map((seg) => seg.uri); - if (seg.uri && liveSegURIs.includes(seg.uri)) { - debug(`[${this.sessionId}]: ${leaderOrFollower}: Found duplicate live segment. Skip push! (${liveTargetGroupId, liveTargetLanguage})`); - } else { - this.liveAudioSegQueue[liveTargetGroupId][liveTargetLanguage].push(seg); - this.liveAudioSegsForFollowers[liveTargetGroupId][liveTargetLanguage].push(seg); - debug(`[${this.sessionId}]: ${leaderOrFollower}: Pushed segment (${seg.uri ? seg.uri : "Disc-tag"}) to 'liveSegQueue' (${liveTargetGroupId, liveTargetLanguage})`); - } - } + _pushToQueueAudio(seg, liveTargetAudiotrack, logName) { + const liveSegURIs = this.liveSegQueueAudio[liveTargetAudiotrack].filter((seg) => seg.uri).map((seg) => seg.uri); + if (seg.uri && liveSegURIs.includes(seg.uri)) { + debug(`[${this.sessionId}]: ${logName}: Found duplicate live segment. Skip push! track -> (${liveTargetAudiotrack})`); + } else { + this.liveSegQueueAudio[liveTargetAudiotrack].push(seg); + this.liveSegsForFollowersAudio[liveTargetAudiotrack].push(seg); + debug( + `[${this.sessionId}]: ${logName}: Pushed Audio segment (${ + seg.uri ? seg.uri : "Disc-tag" + }) to 'liveSegQueue' track -> (${liveTargetAudiotrack})` + ); } } @@ -2262,7 +2029,10 @@ class SessionLive { */ // DO NOT GENERATE MANIFEST CASE: Node has not found anything in store OR Node has not even check yet. - if (Object.keys(this.liveSegQueue).length === 0 || (this.liveSegQueue[liveTargetBandwidth] && this.liveSegQueue[liveTargetBandwidth].length === 0)) { + if ( + Object.keys(this.liveSegQueue).length === 0 || + (this.liveSegQueue[liveTargetBandwidth] && this.liveSegQueue[liveTargetBandwidth].length === 0) + ) { debug(`[${this.sessionId}]: Cannot Generate Manifest! <${this.instanceId}> Not yet collected ANY segments from Live Source...`); return null; } @@ -2299,9 +2069,9 @@ class SessionLive { if (Object.keys(this.vodSegments).length !== 0) { // Add transitional segments if there are any left. debug(`[${this.sessionId}]: Adding a Total of (${this.vodSegments[vodTargetBandwidth].length}) VOD segments to manifest`); - m3u8 = this._setMediaManifestTags(this.vodSegments, m3u8, vodTargetBandwidth); + m3u8 = this._setVariantManifestSegmentTags(this.vodSegments, m3u8, vodTargetBandwidth); // Add live-source segments - m3u8 = this._setMediaManifestTags(this.liveSegQueue, m3u8, liveTargetBandwidth); + m3u8 = this._setVariantManifestSegmentTags(this.liveSegQueue, m3u8, liveTargetBandwidth); } debug(`[${this.sessionId}]: Manifest Generation Complete!`); return m3u8; @@ -2314,9 +2084,19 @@ class SessionLive { if (audioLanguage === null) { throw new Error("No audioLanguage provided"); } - const liveTargetTrackIds = this._findAudioGroupAndLang(audioGroupId, audioLanguage, this.audioManifestURIs); - const vodTargetTrackIds = this._findAudioGroupAndLang(audioGroupId, audioLanguage, this.vodAudioSegments); - debug(`[${this.sessionId}]: Client requesting manifest for VodTrackInfo=(${JSON.stringify(vodTargetTrackIds)}). Nearest LiveTrackInfo=(${JSON.stringify(liveTargetTrackIds)})`); + const liveTargetTrack = this._findNearestAudiotrack( + this._getTrackFromGroupAndLang(audioGroupId, audioLanguage), + Object.keys(this.audioManifestURIs) + ); + const vodTargetTrack = this._findNearestAudiotrack( + this._getTrackFromGroupAndLang(audioGroupId, audioLanguage), + Object.keys(this.vodSegmentsAudio) + ); + debug( + `[${this.sessionId}]: Client requesting manifest for VodTrackInfo=(${JSON.stringify(vodTargetTrack)}). Nearest LiveTrackInfo=(${JSON.stringify( + liveTargetTrack + )})` + ); if (this.blockGenerateManifest) { debug(`[${this.sessionId}]: FOLLOWER: Cannot Generate Audio Manifest! Waiting to sync-up with Leader...`); @@ -2324,26 +2104,18 @@ class SessionLive { } // DO NOT GENERATE MANIFEST CASE: Node has not found anything in store OR Node has not even check yet. - if (Object.keys(this.liveAudioSegQueue).length === 0 || - (this.liveAudioSegQueue[liveTargetTrackIds.audioGroupId] && - this.liveAudioSegQueue[liveTargetTrackIds.audioGroupId][liveTargetTrackIds.audioLanguage] && - this.liveAudioSegQueue[liveTargetTrackIds.audioGroupId][liveTargetTrackIds.audioLanguage].length === 0) - ) { + if (Object.keys(this.liveSegQueueAudio).length === 0 || this.liveSegQueueAudio[liveTargetTrack].length === 0) { debug(`[${this.sessionId}]: Cannot Generate Audio Manifest! <${this.instanceId}> Not yet collected ANY segments from Live Source...`); return null; } // DO NOT GENERATE MANIFEST CASE: Node is in the middle of gathering segs of all variants. - const groupIds = Object.keys(this.liveAudioSegQueue); + const tracks = Object.keys(this.liveSegQueueAudio); let segAmounts = []; - for (let i = 0; i < groupIds.length; i++) { - const groupId = groupIds[i]; - const langs = Object.keys(this.liveAudioSegQueue[groupId]); - for (let j = 0; j < langs.length; j++) { - const lang = langs[j]; - if (this.liveAudioSegQueue[groupId][lang].length !== 0) { - segAmounts.push(this.liveAudioSegQueue[groupId][lang].length); - } + for (let i = 0; i < tracks.length; i++) { + const track = tracks[i]; + if (this.liveSegQueueAudio[track].length !== 0) { + segAmounts.push(this.liveSegQueueAudio[track].length); } } @@ -2352,13 +2124,13 @@ class SessionLive { return null; } - if (!this._isEmpty(this.liveAudioSegQueue) && this.liveAudioSegQueue[groupIds[0]][Object.keys(this.liveAudioSegQueue[groupIds[0]])[0]].length !== 0) { - this.targetDuration = this._getMaxDuration(this.liveAudioSegQueue[groupIds[0]][Object.keys(this.liveAudioSegQueue[groupIds[0]])[0]]); + if (!this._isEmpty(this.liveSegQueueAudio) && this.liveSegQueueAudio[tracks[0]].length !== 0) { + this.targetDuration = this._getMaxDuration(this.liveSegQueueAudio[tracks[0]]); } // Determine if VOD segments influence targetDuration - for (let i = 0; i < this.vodAudioSegments[vodTargetTrackIds.audioGroupId][vodTargetTrackIds.audioLanguage].length; i++) { - let vodSeg = this.vodAudioSegments[vodTargetTrackIds.audioGroupId][vodTargetTrackIds.audioLanguage][i]; + for (let i = 0; i < this.vodSegmentsAudio[vodTargetTrack].length; i++) { + let vodSeg = this.vodSegmentsAudio[vodTargetTrack][i]; // Get max duration amongst segments if (vodSeg.duration > this.targetDuration) { this.targetDuration = vodSeg.duration; @@ -2373,79 +2145,32 @@ class SessionLive { m3u8 += "#EXT-X-TARGETDURATION:" + Math.round(this.targetDuration) + "\n"; m3u8 += "#EXT-X-MEDIA-SEQUENCE:" + this.audioSeqCount + "\n"; m3u8 += "#EXT-X-DISCONTINUITY-SEQUENCE:" + this.audioDiscSeqCount + "\n"; - if (Object.keys(this.vodAudioSegments).length !== 0) { + if (Object.keys(this.vodSegmentsAudio).length !== 0) { // Add transitional segments if there are any left. - debug(`[${this.sessionId}]: Adding a Total of (${this.vodAudioSegments[vodTargetTrackIds.audioGroupId][vodTargetTrackIds.audioLanguage].length}) VOD audio segments to manifest`); - m3u8 = this._setAudioManifestTags(this.vodAudioSegments, m3u8, vodTargetTrackIds); + debug(`[${this.sessionId}]: Adding a Total of (${this.vodSegmentsAudio[vodTargetTrack].length}) VOD audio segments to manifest`); + m3u8 = this._setVariantManifestSegmentTags(this.vodSegmentsAudio, m3u8, vodTargetTrack); // Add live-source segments - m3u8 = this._setAudioManifestTags(this.liveAudioSegQueue, m3u8, liveTargetTrackIds); + m3u8 = this._setVariantManifestSegmentTags(this.liveSegQueueAudio, m3u8, liveTargetTrack); } debug(`[${this.sessionId}]: Audio manifest Generation Complete!`); return m3u8; } - _setMediaManifestTags(segments, m3u8, bw) { - for (let i = 0; i < segments[bw].length; i++) { - const seg = segments[bw][i]; - m3u8 += this._setTagsOnSegment(seg, m3u8) - } - return m3u8 - } - - _setAudioManifestTags(segments, m3u8, trackIds) { - for (let i = 0; i < segments[trackIds.audioGroupId][trackIds.audioLanguage].length; i++) { - const seg = segments[trackIds.audioGroupId][trackIds.audioLanguage][i]; - m3u8 += this._setTagsOnSegment(seg, m3u8) - } - return m3u8 - } - - _setTagsOnSegment(segment) { - let m3u8 = ""; - if (segment.discontinuity) { - m3u8 += "#EXT-X-DISCONTINUITY\n"; - } - if (segment.cue) { - if (segment.cue.out) { - if (segment.cue.scteData) { - m3u8 += "#EXT-OATCLS-SCTE35:" + segment.cue.scteData + "\n"; - } - if (segment.cue.assetData) { - m3u8 += "#EXT-X-ASSET:" + segment.cue.assetData + "\n"; - } - m3u8 += "#EXT-X-CUE-OUT:DURATION=" + segment.cue.duration + "\n"; - } - if (segment.cue.cont) { - if (segment.cue.scteData) { - m3u8 += "#EXT-X-CUE-OUT-CONT:ElapsedTime=" + segment.cue.cont + ",Duration=" + segment.cue.duration + ",SCTE35=" + segment.cue.scteData + "\n"; - } else { - m3u8 += "#EXT-X-CUE-OUT-CONT:" + segment.cue.cont + "/" + segment.cue.duration + "\n"; - } - } - } - if (segment.datetime) { - m3u8 += `#EXT-X-PROGRAM-DATE-TIME:${segment.datetime}\n`; - } - if (segment.daterange) { - const dateRangeAttributes = Object.keys(segment.daterange) - .map((key) => daterangeAttribute(key, segment.daterange[key])) - .join(","); - if (!segment.datetime && segment.daterange["start-date"]) { - m3u8 += "#EXT-X-PROGRAM-DATE-TIME:" + segment.daterange["start-date"] + "\n"; - } - m3u8 += "#EXT-X-DATERANGE:" + dateRangeAttributes + "\n"; - } - // Mimick logic used in hls-vodtolive - //console.log(segment, segment.cue, 2000) - if (segment.cue && segment.cue.in) { - m3u8 += "#EXT-X-CUE-IN" + "\n"; - } - if (segment.uri) { - m3u8 += "#EXTINF:" + segment.duration.toFixed(3) + ",\n"; - m3u8 += segment.uri + "\n"; + _setVariantManifestSegmentTags(segments, m3u8, variantKey) { + let previousSeg = null; + const size = segments[variantKey].length; + for (let i = 0; i < size; i++) { + const seg = segments[variantKey][i]; + const nextSeg = segments[variantKey][i + 1] ? segments[variantKey][i + 1] : {}; + m3u8 += segToM3u8(seg, i, size, nextSeg, previousSeg); + if (m3u8.includes("#EXT-X-DISCONTINUITY\n#EXT-X-DISCONTINUITY\n")) { + debug(`[${this.sessionId}]: Removing Duplicate Disc-tag from output M3u8`); + // In case of duplicate disc-tags, remove one. + m3u8 = m3u8.replace("#EXT-X-DISCONTINUITY\n#EXT-X-DISCONTINUITY\n", "#EXT-X-DISCONTINUITY\n"); + } + previousSeg = seg; } return m3u8; } - _findNearestBw(bw, array) { const sorted = array.sort((a, b) => b - a); return sorted.reduce((a, b) => { @@ -2478,34 +2203,8 @@ class SessionLive { debug(`[${this.sessionId}]: ERROR Could not find any bandwidth with segments`); return null; } - - _getFirstAudioGroupWithSegments(array) { - const audioGroupIds = Object.keys(array).filter((id) => { - let idLangs = Object.keys(array[id]).filter((lang) => { - return array[id][lang].length > 0; - }); - return idLangs.length > 0; - }); - if (audioGroupIds.length > 0) { - return audioGroupIds[0]; - } else { - return null; - } - } - - _getFirstAudioLanguageWithSegments(groupId, array) { - const langsWithSegments = Object.keys(array[groupId]).filter((lang) => { - return array[groupId][lang].length > 0; - }); - if (langsWithSegments.length > 0) { - return langsWithSegments[0]; - } else { - return null; - } - } - _findAudioGroupsForLang(audioLanguage, segments) { - let trackInfos = [] + let trackInfos = []; const groupIds = Object.keys(segments); for (let i = 0; i < groupIds.length; i++) { const groupId = groupIds[i]; @@ -2513,8 +2212,7 @@ class SessionLive { for (let j = 0; j < langs.length; j++) { const lang = langs[j]; if (lang === audioLanguage) { - - trackInfos.push({ audioGroupId: groupId, audioLanguage: lang }) + trackInfos.push({ audioGroupId: groupId, audioLanguage: lang }); break; } } @@ -2522,33 +2220,33 @@ class SessionLive { return trackInfos; } - _findAudioGroupAndLang(audioGroupId, audioLanguage, array) { - if (audioGroupId === null || !array[audioGroupId]) { - audioGroupId = this._getFirstAudioGroupWithSegments(array); - if (!audioGroupId) { - return []; + _findNearestAudiotrack(track, tracks) { + // perfect match + if (tracks.includes(track)) { + return track; + } + let tracksMatchingOnLanguage = tracks.filter((t) => { + if (this._getLangFromTrack(t) === track) { + return true; } + return false; + }); + // If any matches, then it implies that no group ID matches, so use a fallback (first) group + if (tracksMatchingOnLanguage.length > 0) { + return tracksMatchingOnLanguage[0]; } - if (!array[audioGroupId][audioLanguage]) { - const fallbackLang = this._getFirstAudioLanguageWithSegments(audioGroupId, array); - if (!fallbackLang) { - if (Object.keys(array[audioGroupId]).length > 0) { - return { - "audioGroupId": audioGroupId, - "audioLanguage": Object.keys(array[audioGroupId])[0], - }; - } + // If no matches then check if we have any matched on group id, then use fallback (first) language + let tracksMatchingOnGroupId = tracks.filter((t) => { + if (this._getLangFromTrack(t) === track) { + return true; } - return { - "audioGroupId": audioGroupId, - "audioLanguage": fallbackLang - }; + return false; + }); + if (tracksMatchingOnGroupId.length > 0) { + return tracksMatchingOnGroupId[0]; } - return { - "audioGroupId": audioGroupId, - "audioLanguage": audioLanguage - }; - + // No groupId or language matches the target, use fallback (first) track + return tracks[0]; } _getMaxDuration(segments) { @@ -2580,8 +2278,21 @@ class SessionLive { newItem[bw] = this.mediaManifestURIs[bw]; }); this.mediaManifestURIs = newItem; - - + } + _filterLiveProfilesAudio() { + const tracks = this.sessionAudioTracks.map((trackItem) => { + return this._getTrackFromGroupAndLang(trackItem.groupId, trackItem.language); + }); + const toKeep = new Set(); + let newItem = {}; + tracks.forEach((t) => { + let atToKeep = this._findNearestAudiotrack(t, Object.keys(this.audioManifestURIs)); + toKeep.add(atToKeep); + }); + toKeep.forEach((at) => { + newItem[at] = this.audioManifestURIs[at]; + }); + this.audioManifestURIs = newItem; } _filterLiveAudioTracks() { @@ -2593,15 +2304,18 @@ class SessionLive { let groupAndLangToKeep = this._findAudioGroupsForLang(audioTrack.language, this.audioManifestURIs); toKeep.add(...groupAndLangToKeep); }); + toKeep.forEach((trackInfo) => { - if (!newItemsAudio[trackInfo.audioGroupId]) { - newItemsAudio[trackInfo.audioGroupId] = {} + if (trackInfo) { + if (!newItemsAudio[trackInfo.audioGroupId]) { + newItemsAudio[trackInfo.audioGroupId] = {}; + } + newItemsAudio[trackInfo.audioGroupId][trackInfo.audioLanguage] = this.audioManifestURIs[trackInfo.audioGroupId][trackInfo.audioLanguage]; } - newItemsAudio[trackInfo.audioGroupId][trackInfo.audioLanguage] = this.audioManifestURIs[trackInfo.audioGroupId][trackInfo.audioLanguage]; - }); - - this.audioManifestURIs = newItemsAudio; + if (!this._isEmpty(newItemsAudio)) { + this.audioManifestURIs = newItemsAudio; + } } _getAnyFirstSegmentDurationMs() { @@ -2656,6 +2370,67 @@ class SessionLive { } return false; } + + _getGroupAndLangFromTrack(track) { + const GLItem = { + groupId: null, + language: null, + }; + const match = track.match(/g:(.*?),l:(.*)/); + if (match) { + const g = match[1]; + const l = match[2]; + if (g && l) { + GLItem.groupId = g; + GLItem.language = l; + return GLItem; + } + } + console.error(`Failed to extract GroupID and Language g=${g};l=${l}`); + return GLItem; + } + + _getLangFromTrack(track) { + const match = track.match(/g:(.*?),l:(.*)/); + if (match) { + const g = match[1]; + const l = match[2]; + if (g && l) { + return l; + } + } + console.error(`Failed to extract Language g=${g};l=${l}`); + return null; + } + + _getGroupFromTrack(track) { + const match = track.match(/g:(.*?),l:(.*)/); + if (match) { + const g = match[1]; + const l = match[2]; + if (g && l) { + return g; + } + } + console.error(`Failed to extract Group ID g=${g};l=${l}`); + return null; + } + + _getTrackFromGroupAndLang(g, l) { + return `g:${g},l:${l}`; + } + + _isBandwidth(bw) { + if (typeof bw === "number") { + return true; + } else if (typeof bw === "string") { + const parsedNumber = parseFloat(bw); + if (!isNaN(parsedNumber)) { + return true; + } + } + return false; + } } module.exports = SessionLive; diff --git a/engine/stream_switcher.js b/engine/stream_switcher.js index c94a7af6..4bd80757 100644 --- a/engine/stream_switcher.js +++ b/engine/stream_switcher.js @@ -1,6 +1,7 @@ const debug = require("debug")("engine-stream-switcher"); const crypto = require("crypto"); const fetch = require("node-fetch"); +const url = require("url"); const { AbortController } = require("abort-controller"); const { SessionState } = require("./session_state"); const { timer, findNearestValue, isValidUrl, fetchWithRetry, findAudioGroupOrLang } = require("./util"); @@ -257,8 +258,6 @@ class StreamSwitcher { currVodAudioSegments = this._mergeAudioSegments(prerollAudioSegments, currVodAudioSegments, false); } } - console.log(currVodAudioSegments["aac"], "audio") - console.log(currVodSegments, "video") // In risk that the SL-playhead might have updated some data after // we reset last time... we should Reset SessionLive before sending new data. @@ -315,8 +314,7 @@ class StreamSwitcher { } await session.setCurrentMediaAndDiscSequenceCount(currVodCounts.mediaSeq, currVodCounts.discSeq, currVodCounts.audioSeq, currVodCounts.audioDiscSeq); - await session.setCurrentMediaSequenceSegments(eventSegments, 0, true); - await session.setCurrentAudioSequenceSegments(eventAudioSegments, 0, true); + await session.setCurrentMediaSequenceSegments(eventSegments, 0, true, eventAudioSegments, 0); this.working = false; debug(`[${this.sessionId}]: [ Switched from V2L->VOD ]`); @@ -340,13 +338,13 @@ class StreamSwitcher { if (scheduleObj && !scheduleObj.duration) { debug(`[${this.sessionId}]: Cannot switch VOD. No duration specified for schedule item: [${scheduleObj.assetId}]`); } - if (this._isEmpty(liveSegments.currMseqSegs) || (this.useDemuxedAudio && this._isEmpty(liveAudioSegments.currMseqSegs))) { this.working = false; this.streamTypeLive = false; debug(`[${this.sessionId}]: [ Switched from LIVE->V2L ]`); return false; } + // Insert preroll, if available, for current channel if (this.prerollsCache[this.sessionId]) { const prerollSegments = this.prerollsCache[this.sessionId].segments; @@ -358,11 +356,14 @@ class StreamSwitcher { liveAudioSegments.segCount += prerollAudioSegments.length; } } + await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq, liveCounts.discSeq, liveCounts.audioSeq, liveCounts.audioDiscSeq); - await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount); if (this.useDemuxedAudio) { - await session.setCurrentAudioSequenceSegments(liveAudioSegments.currMseqSegs, liveAudioSegments.segCount) + await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount, false, liveAudioSegments.currMseqSegs, liveAudioSegments.segCount); + } else { + await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount, false); } + await sessionLive.resetSession(); sessionLive.resetLiveStoreAsync(RESET_DELAY); // In parallel this.working = false; @@ -384,7 +385,6 @@ class StreamSwitcher { liveSegments = await sessionLive.getCurrentMediaSequenceSegments(); liveAudioSegments = await sessionLive.getCurrentAudioSequenceSegments(); liveCounts = await sessionLive.getCurrentMediaAndDiscSequenceCount(); - eventSegments = await session.getTruncatedVodSegments(scheduleObj.uri, scheduleObj.duration / 1000); eventAudioSegments = await session.getTruncatedVodAudioSegments(scheduleObj.uri, scheduleObj.duration / 1000); @@ -398,8 +398,11 @@ class StreamSwitcher { } await session.setCurrentMediaAndDiscSequenceCount(liveCounts.mediaSeq - 1, liveCounts.discSeq - 1, liveCounts.audioSeq - 1, liveCounts.audioDiscSeq - 1); - await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount); - await session.setCurrentAudioSequenceSegments(liveAudioSegments.currMseqSegs, liveAudioSegments.segCount); + if (this.useDemuxedAudio) { + await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount, false, liveAudioSegments.currMseqSegs, liveAudioSegments.segCount); + } else { + await session.setCurrentMediaSequenceSegments(liveSegments.currMseqSegs, liveSegments.segCount); + } // Insert preroll, if available, for current channel if (this.prerollsCache[this.sessionId]) { @@ -636,7 +639,7 @@ class StreamSwitcher { if (!prerollSegments[bw]) { prerollSegments[bw] = []; } - prerollSegments[bw] = this._createCustomSimpleSegmentList(mediaM3UPlaylists[bw], bw, null, "video", mediaURIs); + prerollSegments[bw] = this._createCustomSimpleSegmentList(mediaM3UPlaylists[bw], mediaURIs[bw]); } if (this.useDemuxedAudio) { const groupIds = Object.keys(audioM3UPlaylists); @@ -651,7 +654,7 @@ class StreamSwitcher { if (!prerollSegmentsAudio[groupId][lang]) { prerollSegmentsAudio[groupId][lang] = []; } - prerollSegmentsAudio[groupId][lang] = this._createCustomSimpleSegmentList(audioM3UPlaylists[groupId][lang], groupId, lang, "audio", audioURIs); + prerollSegmentsAudio[groupId][lang] = this._createCustomSimpleSegmentList(audioM3UPlaylists[groupId][lang], audioURIs[groupId][lang]); } } } @@ -662,81 +665,118 @@ class StreamSwitcher { } } - _createCustomSimpleSegmentList(segmentList, keyValue1, keyValue2, type, URIs) { + _createCustomSimpleSegmentList(segmentList, manifestURI) { let segments = []; for (let k = 0; k < segmentList.length; k++) { - let seg = {}; - let playlistItem = segmentList[k]; - let segmentUri; - let cueData = null; - let daterangeData = null; - let attributes = playlistItem["attributes"].attributes; - if (playlistItem.properties.discontinuity) { - segments.push({ discontinuity: true }); - } - if ("cuein" in attributes) { - if (!cueData) { - cueData = {}; + try { + let seg = {}; + const playlistItem = segmentList[k]; + let segmentUri; + let byteRange = undefined; + let initSegment = undefined; + let initSegmentByteRange = undefined; + let keys = undefined; + let daterangeData = null; + let attributes = playlistItem["attributes"].attributes; + if (playlistItem.get("map-uri")) { + initSegmentByteRange = playlistItem.get("map-byterange"); + if (playlistItem.get("map-uri").match("^http")) { + initSegment = playlistItem.get("map-uri"); + } else { + initSegment = new URL(playlistItem.get("map-uri"), manifestURI).href; + } } - cueData["in"] = true; - } - if ("cueout" in attributes) { - if (!cueData) { - cueData = {}; + // some items such as CUE-IN parse as a PlaylistItem + // but have no URI + if (playlistItem.get("uri")) { + if (playlistItem.get("uri").match("^http")) { + segmentUri = playlistItem.get("uri"); + } else { + segmentUri = new URL(playlistItem.get("uri"), manifestURI).href; + } } - cueData["out"] = true; - cueData["duration"] = attributes["cueout"]; - } - if ("cuecont" in attributes) { - if (!cueData) { - cueData = {}; + if (playlistItem.get("discontinuity")) { + segments.push({ discontinuity: true }); } - cueData["cont"] = true; - } - if ("scteData" in attributes) { - if (!cueData) { - cueData = {}; + if (playlistItem.get("byteRange")) { + let [_, r, o] = playlistItem.get("byteRange").match(/^(\d+)@*(\d*)$/); + if (!o) { + o = byteRangeOffset; + } + byteRangeOffset = parseInt(r) + parseInt(o); + byteRange = `${r}@${o}`; } - cueData["scteData"] = attributes["scteData"]; - } - if ("assetData" in attributes) { - if (!cueData) { - cueData = {}; + if (playlistItem.get("keys")) { + keys = playlistItem.get("keys"); } - cueData["assetData"] = attributes["assetData"]; - } - if ("daterange" in attributes) { - if (!daterangeData) { - daterangeData = {}; + let assetData = playlistItem.get("assetdata"); + let cueOut = playlistItem.get("cueout"); + let cueIn = playlistItem.get("cuein"); + let cueOutCont = playlistItem.get("cont-offset"); + let duration = 0; + let scteData = playlistItem.get("sctedata"); + if (typeof cueOut !== "undefined") { + duration = cueOut; + } else if (typeof cueOutCont !== "undefined") { + duration = playlistItem.get("cont-dur"); } - let allDaterangeAttributes = Object.keys(attributes["daterange"]); - allDaterangeAttributes.forEach((attr) => { - if (attr.match(/DURATION$/)) { - daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]); - } else { - daterangeData[attr.toLowerCase()] = attributes["daterange"][attr]; + let cue = + cueOut || cueIn || cueOutCont || assetData + ? { + out: typeof cueOut !== "undefined", + cont: typeof cueOutCont !== "undefined" ? cueOutCont : null, + scteData: typeof scteData !== "undefined" ? scteData : null, + in: cueIn ? true : false, + duration: duration, + assetData: typeof assetData !== "undefined" ? assetData : null, + } + : null; + seg = { + duration: playlistItem.get("duration"), + timelinePosition: this.timeOffset != null ? this.timeOffset + timelinePosition : null, + cue: cue, + byteRange: byteRange, + }; + if (initSegment) { + seg.initSegment = initSegment; + } + if (initSegmentByteRange) { + seg.initSegmentByteRange = initSegmentByteRange; + } + if (segmentUri) { + seg.uri = segmentUri; + } + if (keys) { + seg.keys = keys; + } + if (segments.length === 0) { + // Add daterange metadata if this is the first segment + if (this.rangeMetadata && !this._isEmpty(this.rangeMetadata)) { + seg["daterange"] = this.rangeMetadata; } - }); - } - if (playlistItem.properties.uri) { - if (playlistItem.properties.uri.match("^http")) { - segmentUri = playlistItem.properties.uri; - } else { - if (type === "video") { - segmentUri = new URL(playlistItem.properties.uri, URIs[keyValue1]).href; - } else if (type === "audio") { - segmentUri = new URL(playlistItem.properties.uri, URIs[keyValue1][keyValue2]).href; + } + if ("daterange" in attributes) { + if (!daterangeData) { + daterangeData = {}; } + let allDaterangeAttributes = Object.keys(attributes["daterange"]); + allDaterangeAttributes.forEach((attr) => { + if (attr.match(/DURATION$/)) { + daterangeData[attr.toLowerCase()] = parseFloat(attributes["daterange"][attr]); + } else { + daterangeData[attr.toLowerCase()] = attributes["daterange"][attr]; + } + }); } - seg["duration"] = playlistItem.properties.duration; - seg["uri"] = segmentUri; - seg["cue"] = cueData; - if (daterangeData) { - seg["daterange"] = daterangeData; + if (playlistItem.properties.uri) { + if (daterangeData && !this._isEmpty(this.daterangeData)) { + seg["daterange"] = daterangeData; + } } + segments.push(seg); + } catch (e) { + console.error(e); } - segments.push(seg); - } return segments } @@ -808,14 +848,16 @@ class StreamSwitcher { const OUTPUT_SEGMENTS = {}; const fromGroups = Object.keys(fromSegments); const toGroups = Object.keys(toSegments); + for (let i = 0; i < toGroups.length; i++) { const groupId = toGroups[i]; if (!OUTPUT_SEGMENTS[groupId]) { OUTPUT_SEGMENTS[groupId] = {} } - const langs = Object.keys(toSegments[groupId]) - for (let j = 0; j < langs.length; j++) { - const lang = langs[j]; + const toLangs = Object.keys(toSegments[groupId]) + + for (let j = 0; j < toLangs.length; j++) { + const lang = toLangs[j]; if (!OUTPUT_SEGMENTS[groupId][lang]) { OUTPUT_SEGMENTS[groupId][lang] = []; } @@ -824,20 +866,20 @@ class StreamSwitcher { const fromLangs = Object.keys(fromSegments[targetGroupId]); const targetLang = findAudioGroupOrLang(lang, fromLangs); if (prepend) { - OUTPUT_SEGMENTS[targetGroupId][targetLang] = fromSegments[groupId][lang].concat(toSegments[targetGroupId][targetLang]); - OUTPUT_SEGMENTS[targetGroupId][targetLang].unshift({ discontinuity: true }); + OUTPUT_SEGMENTS[groupId][lang] = fromSegments[targetGroupId][targetLang].concat(toSegments[groupId][lang]); + OUTPUT_SEGMENTS[groupId][lang].unshift({ discontinuity: true }); } else { - const size = toSegments[targetGroupId][targetLang].length; - const lastSeg = toSegments[targetGroupId][targetLang][size -1]; + const size = toSegments[groupId][lang].length; + const lastSeg = toSegments[groupId][lang][size - 1]; if (lastSeg.uri && !lastSeg.discontinuity) { - toSegments[targetGroupId][targetLang].push({ discontinuity: true, cue: { in: true } }); - OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[groupId][lang]); + toSegments[groupId][lang].push({ discontinuity: true, cue: { in: true } }); + OUTPUT_SEGMENTS[groupId][lang] = toSegments[groupId][lang].concat(fromSegments[targetGroupId][targetLang]); } else if (lastSeg.discontinuity && !lastSeg.cue) { - toSegments[targetGroupId][targetLang][toSegments[targetGroupId][targetLang].length - 1].cue = { in: true } - OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[groupId][lang]); + toSegments[targetGroupId][lang][toSegments[groupId][lang].length - 1].cue = { in: true } + OUTPUT_SEGMENTS[groupId][lang] = toSegments[groupId][lang].concat(fromSegments[targetGroupId][targetLang]); } else { - OUTPUT_SEGMENTS[targetGroupId][targetLang] = toSegments[targetGroupId][targetLang].concat(fromSegments[groupId][lang]); - OUTPUT_SEGMENTS[targetGroupId][targetLang].push({ discontinuity: true }); + OUTPUT_SEGMENTS[groupId][lang] = toSegments[groupId][lang].concat(fromSegments[targetGroupId][targetLang]); + OUTPUT_SEGMENTS[groupId][lang].push({ discontinuity: true }); } } } @@ -856,7 +898,9 @@ class StreamSwitcher { daterangeData[k] = timedMetadata[k]; }); } - segments[bw][0]["daterange"] = daterangeData; + if (Object.keys(daterangeData).length > 0) { + segments[bw][0]["daterange"] = daterangeData; + } }); } @@ -875,7 +919,9 @@ class StreamSwitcher { daterangeData[k] = timedMetadata[k]; }); } - segments[groupId][lang][0]["daterange"] = daterangeData; + if (Object.keys(daterangeData).length > 0) { + segments[groupId][lang][0]["daterange"] = daterangeData; + } } } diff --git a/package-lock.json b/package-lock.json index 2d399e3c..f72678fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,8 +11,8 @@ "dependencies": { "@eyevinn/hls-repeat": "^0.2.0", "@eyevinn/hls-truncate": "^0.2.0", - "@eyevinn/hls-vodtolive": "3.1.0", - "@eyevinn/m3u8": "^0.5.3", + "@eyevinn/hls-vodtolive": "^4.1.0", + "@eyevinn/m3u8": "^0.5.6", "abort-controller": "^3.0.0", "debug": "^3.2.7", "memcache-client": "^0.10.1", @@ -34,9 +34,6 @@ "node": ">=14 <20" } }, - "@eyevinn/master": { - "extraneous": true - }, "node_modules/@eyevinn/hls-repeat": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@eyevinn/hls-repeat/-/hls-repeat-0.2.0.tgz", @@ -99,9 +96,9 @@ } }, "node_modules/@eyevinn/hls-vodtolive": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eyevinn/hls-vodtolive/-/hls-vodtolive-3.1.0.tgz", - "integrity": "sha512-/NOh+pq1ZAgtHSMDoUZEu2x/JPzHGuIHHPaasyfviWKwUdB/H3ReVB1V+23aXNbRX1JnlOLJLdUph6ZXe7QjMQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@eyevinn/hls-vodtolive/-/hls-vodtolive-4.1.0.tgz", + "integrity": "sha512-1f05IxohO3fUmaJUTAGDBs0zViYoMImlE9/bstHRqiJVAQ7V2U7QCe3nvVuRv/7aPyqy6efsymwnfODax0abvg==", "dependencies": { "@eyevinn/m3u8": "^0.5.6", "abort-controller": "^3.0.0", @@ -176,9 +173,9 @@ ] }, "node_modules/@types/node": { - "version": "18.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.5.tgz", - "integrity": "sha512-seOA34WMo9KB+UA78qaJoCO20RJzZGVXQ5Sh6FWu0g/hfT44nKXnej3/tCQl7FL97idFpBhisLYCTB50S0EirA==", + "version": "18.18.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.0.tgz", + "integrity": "sha512-3xA4X31gHT1F1l38ATDIL9GpRLdwVhnEFC8Uikv5ZLlXATwrCYyPq7ZWHxzxc3J/30SUiwiYT+bQe0/XvKlWbw==", "dev": true }, "node_modules/abort-controller": { @@ -220,13 +217,13 @@ } }, "node_modules/array.prototype.map": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.5.tgz", - "integrity": "sha512-gfaKntvwqYIuC7mLLyv2wzZIJqrRhn5PZ9EfFejSx6a78sV7iDsGpG9P+3oUPtm1Rerqm6nrKS4FYuTIvWfo3g==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.6.tgz", + "integrity": "sha512-nK1psgF2cXqP3wSyCSq0Hc7zwNq3sfljQqaG27r/7a7ooNUnn5nGq6yYWyks9jMO5EoFQ0ax80hSg6oXSRNXaw==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-array-method-boxes-properly": "^1.0.0", "is-string": "^1.0.7" }, @@ -237,6 +234,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -404,33 +421,33 @@ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "node_modules/csv": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.8.tgz", - "integrity": "sha512-QGemEqFr5XgbHcufWwO+qnReFhClbd+6WywKDVqEXmRfGrJHl5fRrlphZUnsky2YAwcCxYJJilPUfaWbuBMfWA==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.3.3.tgz", + "integrity": "sha512-TuOM1iZgdDiB6IuwJA8oqeu7g61d9CU9EQJGzCJ1AE03amPSh/UK5BMjAVx+qZUBb/1XEo133WHzWSwifa6Yqw==", "dependencies": { - "csv-generate": "^4.2.2", - "csv-parse": "^5.3.6", - "csv-stringify": "^6.3.0", - "stream-transform": "^3.2.2" + "csv-generate": "^4.2.8", + "csv-parse": "^5.5.0", + "csv-stringify": "^6.4.2", + "stream-transform": "^3.2.8" }, "engines": { "node": ">= 0.1.90" } }, "node_modules/csv-generate": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.2.tgz", - "integrity": "sha512-Ah/NcMxHMqwQsuL173yp8EOzHrbLh8iyScqTy990b+TJZNjHhy7gs5FfSmyQ2arLC2QVrueO3DYJVQnibJB3WQ==" + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.8.tgz", + "integrity": "sha512-qQ5CUs4I58kfo90EDBKjdp0SpJ3xWnN1Xk1lZ1ITvfvMtNRf+jrEP8tNPeEPiI9xJJ6Bd/km/1hMjyYlTpY42g==" }, "node_modules/csv-parse": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.6.tgz", - "integrity": "sha512-WI330GjCuEioK/ii8HM2YE/eV+ynpeLvU+RXw4R8bRU8R0laK5zO3fDsc4gH8s472e3Ga38rbIjCAiQh+tEHkw==" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.0.tgz", + "integrity": "sha512-RxruSK3M4XgzcD7Trm2wEN+SJ26ChIb903+IWxNOcB5q4jT2Cs+hFr6QP39J05EohshRFEvyzEBoZ/466S2sbw==" }, "node_modules/csv-stringify": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.3.0.tgz", - "integrity": "sha512-kTnnBkkLmAR1G409aUdShppWUClNbBQZXhrKrXzKYBGw4yfROspiFvVmjbKonCrdGfwnqwMXKLQG7ej7K/jwjg==" + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.4.2.tgz", + "integrity": "sha512-DXIdnnCUQYjDKTu6TgCSzRDiAuLxDjhl4ErFP9FGMF3wzBGOVMg9bZTLaUcYtuvhXgNbeXPKeaRfpgyqE4xySw==" }, "node_modules/dashdash": { "version": "1.14.1", @@ -451,11 +468,25 @@ "ms": "^2.1.1" } }, + "node_modules/define-data-property": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", + "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -540,17 +571,18 @@ } }, "node_modules/es-abstract": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", + "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", "dependencies": { "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", @@ -565,19 +597,23 @@ "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -710,25 +746,25 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-querystring": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.1.tgz", - "integrity": "sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "node_modules/fast-redact": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.2.tgz", - "integrity": "sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", "engines": { "node": ">=6" } }, "node_modules/find-my-way": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.6.0.tgz", - "integrity": "sha512-H7berWdHJ+5CNVr4ilLWPai4ml7Y2qAsxjw3pfeBxPigZmaDTzF0wjJLj90xRCmGcWYcyt050yN+34OZDJm1eQ==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.6.2.tgz", + "integrity": "sha512-0OjHn1b1nCX3eVbm9ByeEHiscPYiHLfhei1wOUU9qffQkk98wE0Lo8VrVYfSGMgnSnDh86DxedduAnBf4nwUEw==", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", @@ -796,14 +832,14 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -821,12 +857,13 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" }, "funding": { @@ -1292,15 +1329,11 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" }, "engines": { "node": ">= 0.4" @@ -1497,9 +1530,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", "optional": true }, "node_modules/negotiator": { @@ -1511,9 +1544,9 @@ } }, "node_modules/nock": { - "version": "13.3.1", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.1.tgz", - "integrity": "sha512-vHnopocZuI93p2ccivFyGuUfzjq2fxNyNurp7816mlT5V5HF4SzXu8lvLrVzBbNqzs+ODooZ6OksuSUNM7Njkw==", + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.3.tgz", + "integrity": "sha512-z+KUlILy9SK/RjpeXDiDUEAq4T94ADPHE3qaRkf66mpEhzc/ytOMm3Bwdrbq6k1tMWkbdujiKim3G2tfQARuJw==", "dependencies": { "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", @@ -1546,9 +1579,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -1671,14 +1704,14 @@ } }, "node_modules/pino": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.11.0.tgz", - "integrity": "sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==", + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.15.1.tgz", + "integrity": "sha512-Cp4QzUQrvWCRJaQ8Lzv0mJzXVk4z2jlq8JNKMGaixC2Pz5L4l2p95TkuRvYbrEbe85NQsDKrAd4zalf7Ml6WiA==", "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "v1.0.0", + "pino-abstract-transport": "v1.1.0", "pino-std-serializers": "^6.0.0", "process-warning": "^2.0.0", "quick-format-unescaped": "^4.0.3", @@ -1692,32 +1725,18 @@ } }, "node_modules/pino-abstract-transport": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", - "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, - "node_modules/pino-abstract-transport/node_modules/readable-stream": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", - "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, "node_modules/pino-std-serializers": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.0.tgz", - "integrity": "sha512-IWgSzUL8X1w4BIWTwErRgtV8PyOGOOi60uqv0oKuS/fOA8Nco/OeI6lBuc4dyP8MMfdFwyHqTMcBIA7nDiqEqA==" + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" }, "node_modules/process": { "version": "0.11.10", @@ -1738,15 +1757,15 @@ "integrity": "sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==" }, "node_modules/promise.allsettled": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.6.tgz", - "integrity": "sha512-22wJUOD3zswWFqgwjNHa1965LvqTX87WPu/lreY2KSd7SVcERfuZ4GfUaOnJNnvtoIv2yXT/W00YIGMetXtFXg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.7.tgz", + "integrity": "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA==", "dependencies": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" }, "engines": { @@ -1799,16 +1818,18 @@ } }, "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">= 6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/real-require": { @@ -1862,13 +1883,13 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" + "set-function-name": "^2.0.0" }, "engines": { "node": ">= 0.4" @@ -2003,9 +2024,9 @@ } }, "node_modules/restify/node_modules/qs": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", - "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dependencies": { "side-channel": "^1.0.4" }, @@ -2017,9 +2038,13 @@ } }, "node_modules/restify/node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -2032,6 +2057,23 @@ "node": ">=4" } }, + "node_modules/safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2097,9 +2139,9 @@ "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" }, "node_modules/semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -2168,6 +2210,19 @@ "node": ">=4" } }, + "node_modules/set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "dependencies": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -2187,9 +2242,9 @@ } }, "node_modules/sonic-boom": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.3.0.tgz", - "integrity": "sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.4.0.tgz", + "integrity": "sha512-zSe9QQW30nPzjkSJ0glFQO5T9lHsk39tz+2bAAwCj8CNgEG8ItZiX7Wb2ZgA8I04dwRGCcf1m3ABJa8AYm12Fw==", "dependencies": { "atomic-sleep": "^1.0.0" } @@ -2243,6 +2298,19 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/spdy/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2316,9 +2384,9 @@ } }, "node_modules/stream-transform": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.2.tgz", - "integrity": "sha512-DHZQPNxvjU2qdQlGcpitn8pkJHQVTqdshtgXaLz6Vc5VCAognbGuuwGS5ugeqGVnyw8j4h89QcV8cwm0D1+V0A==" + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.8.tgz", + "integrity": "sha512-NUx0mBuI63KbNEEh9Yj0OzKB7iMOSTpkuODM2G7By+TTVihEIJ0cYp5X+pq/TdJRlsznt6CYR8HqxexyC6/bTw==" }, "node_modules/string_decoder": { "version": "1.3.0", @@ -2329,13 +2397,13 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "engines": { "node": ">= 0.4" @@ -2345,35 +2413,35 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/thread-stream": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.3.0.tgz", - "integrity": "sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.0.tgz", + "integrity": "sha512-xZYtOtmnA63zj04Q+F9bdEay5r47bvpo1CaNqsKi7TpoJHcotUez8Fkfo2RJWpW91lnnaApdpRbVwCWsy+ifcw==", "dependencies": { "real-require": "^0.2.0" } @@ -2419,6 +2487,54 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, + "node_modules/typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -2542,16 +2658,15 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -2624,9 +2739,9 @@ } }, "@eyevinn/hls-vodtolive": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eyevinn/hls-vodtolive/-/hls-vodtolive-3.1.0.tgz", - "integrity": "sha512-/NOh+pq1ZAgtHSMDoUZEu2x/JPzHGuIHHPaasyfviWKwUdB/H3ReVB1V+23aXNbRX1JnlOLJLdUph6ZXe7QjMQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@eyevinn/hls-vodtolive/-/hls-vodtolive-4.1.0.tgz", + "integrity": "sha512-1f05IxohO3fUmaJUTAGDBs0zViYoMImlE9/bstHRqiJVAQ7V2U7QCe3nvVuRv/7aPyqy6efsymwnfODax0abvg==", "requires": { "@eyevinn/m3u8": "^0.5.6", "abort-controller": "^3.0.0", @@ -2683,9 +2798,9 @@ } }, "@types/node": { - "version": "18.16.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.5.tgz", - "integrity": "sha512-seOA34WMo9KB+UA78qaJoCO20RJzZGVXQ5Sh6FWu0g/hfT44nKXnej3/tCQl7FL97idFpBhisLYCTB50S0EirA==", + "version": "18.18.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.0.tgz", + "integrity": "sha512-3xA4X31gHT1F1l38ATDIL9GpRLdwVhnEFC8Uikv5ZLlXATwrCYyPq7ZWHxzxc3J/30SUiwiYT+bQe0/XvKlWbw==", "dev": true }, "abort-controller": { @@ -2717,17 +2832,31 @@ } }, "array.prototype.map": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.5.tgz", - "integrity": "sha512-gfaKntvwqYIuC7mLLyv2wzZIJqrRhn5PZ9EfFejSx6a78sV7iDsGpG9P+3oUPtm1Rerqm6nrKS4FYuTIvWfo3g==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.6.tgz", + "integrity": "sha512-nK1psgF2cXqP3wSyCSq0Hc7zwNq3sfljQqaG27r/7a7ooNUnn5nGq6yYWyks9jMO5EoFQ0ax80hSg6oXSRNXaw==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", "es-array-method-boxes-properly": "^1.0.0", "is-string": "^1.0.7" } }, + "arraybuffer.prototype.slice": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", + "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "requires": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", + "is-array-buffer": "^3.0.2", + "is-shared-array-buffer": "^1.0.2" + } + }, "asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -2843,30 +2972,30 @@ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "csv": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/csv/-/csv-6.2.8.tgz", - "integrity": "sha512-QGemEqFr5XgbHcufWwO+qnReFhClbd+6WywKDVqEXmRfGrJHl5fRrlphZUnsky2YAwcCxYJJilPUfaWbuBMfWA==", + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.3.3.tgz", + "integrity": "sha512-TuOM1iZgdDiB6IuwJA8oqeu7g61d9CU9EQJGzCJ1AE03amPSh/UK5BMjAVx+qZUBb/1XEo133WHzWSwifa6Yqw==", "requires": { - "csv-generate": "^4.2.2", - "csv-parse": "^5.3.6", - "csv-stringify": "^6.3.0", - "stream-transform": "^3.2.2" + "csv-generate": "^4.2.8", + "csv-parse": "^5.5.0", + "csv-stringify": "^6.4.2", + "stream-transform": "^3.2.8" } }, "csv-generate": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.2.tgz", - "integrity": "sha512-Ah/NcMxHMqwQsuL173yp8EOzHrbLh8iyScqTy990b+TJZNjHhy7gs5FfSmyQ2arLC2QVrueO3DYJVQnibJB3WQ==" + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.2.8.tgz", + "integrity": "sha512-qQ5CUs4I58kfo90EDBKjdp0SpJ3xWnN1Xk1lZ1ITvfvMtNRf+jrEP8tNPeEPiI9xJJ6Bd/km/1hMjyYlTpY42g==" }, "csv-parse": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.3.6.tgz", - "integrity": "sha512-WI330GjCuEioK/ii8HM2YE/eV+ynpeLvU+RXw4R8bRU8R0laK5zO3fDsc4gH8s472e3Ga38rbIjCAiQh+tEHkw==" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.0.tgz", + "integrity": "sha512-RxruSK3M4XgzcD7Trm2wEN+SJ26ChIb903+IWxNOcB5q4jT2Cs+hFr6QP39J05EohshRFEvyzEBoZ/466S2sbw==" }, "csv-stringify": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.3.0.tgz", - "integrity": "sha512-kTnnBkkLmAR1G409aUdShppWUClNbBQZXhrKrXzKYBGw4yfROspiFvVmjbKonCrdGfwnqwMXKLQG7ej7K/jwjg==" + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.4.2.tgz", + "integrity": "sha512-DXIdnnCUQYjDKTu6TgCSzRDiAuLxDjhl4ErFP9FGMF3wzBGOVMg9bZTLaUcYtuvhXgNbeXPKeaRfpgyqE4xySw==" }, "dashdash": { "version": "1.14.1", @@ -2884,11 +3013,22 @@ "ms": "^2.1.1" } }, + "define-data-property": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", + "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "requires": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + } + }, "define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "requires": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } @@ -2947,17 +3087,18 @@ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" }, "es-abstract": { - "version": "1.21.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", - "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", + "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", "requires": { "array-buffer-byte-length": "^1.0.0", + "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.1", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", @@ -2972,19 +3113,23 @@ "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", + "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", + "regexp.prototype.flags": "^1.5.1", + "safe-array-concat": "^1.0.1", "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", + "string.prototype.trim": "^1.2.8", + "string.prototype.trimend": "^1.0.7", + "string.prototype.trimstart": "^1.0.7", + "typed-array-buffer": "^1.0.0", + "typed-array-byte-length": "^1.0.0", + "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" + "which-typed-array": "^1.1.11" } }, "es-array-method-boxes-properly": { @@ -3087,22 +3232,22 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "fast-querystring": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.1.tgz", - "integrity": "sha512-qR2r+e3HvhEFmpdHMv//U8FnFlnYjaC6QKDuaXALDkw2kvHO8WDjxH+f/rHGR4Me4pnk8p9JAkRNTjYHAKRn2Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", "requires": { "fast-decode-uri-component": "^1.0.1" } }, "fast-redact": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.2.tgz", - "integrity": "sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw==" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==" }, "find-my-way": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.6.0.tgz", - "integrity": "sha512-H7berWdHJ+5CNVr4ilLWPai4ml7Y2qAsxjw3pfeBxPigZmaDTzF0wjJLj90xRCmGcWYcyt050yN+34OZDJm1eQ==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.6.2.tgz", + "integrity": "sha512-0OjHn1b1nCX3eVbm9ByeEHiscPYiHLfhei1wOUU9qffQkk98wE0Lo8VrVYfSGMgnSnDh86DxedduAnBf4nwUEw==", "requires": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", @@ -3154,14 +3299,14 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" } }, "functions-have-names": { @@ -3170,12 +3315,13 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==" }, "get-intrinsic": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", - "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" } }, @@ -3492,15 +3638,11 @@ } }, "is-typed-array": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", - "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "which-typed-array": "^1.1.11" } }, "is-typedarray": { @@ -3655,9 +3797,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", "optional": true }, "negotiator": { @@ -3666,9 +3808,9 @@ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" }, "nock": { - "version": "13.3.1", - "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.1.tgz", - "integrity": "sha512-vHnopocZuI93p2ccivFyGuUfzjq2fxNyNurp7816mlT5V5HF4SzXu8lvLrVzBbNqzs+ODooZ6OksuSUNM7Njkw==", + "version": "13.3.3", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.3.tgz", + "integrity": "sha512-z+KUlILy9SK/RjpeXDiDUEAq4T94ADPHE3qaRkf66mpEhzc/ytOMm3Bwdrbq6k1tMWkbdujiKim3G2tfQARuJw==", "requires": { "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", @@ -3692,9 +3834,9 @@ } }, "node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "requires": { "whatwg-url": "^5.0.0" } @@ -3779,14 +3921,14 @@ } }, "pino": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.11.0.tgz", - "integrity": "sha512-Z2eKSvlrl2rH8p5eveNUnTdd4AjJk8tAsLkHYZQKGHP4WTh2Gi1cOSOs3eWPqaj+niS3gj4UkoreoaWgF3ZWYg==", + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.15.1.tgz", + "integrity": "sha512-Cp4QzUQrvWCRJaQ8Lzv0mJzXVk4z2jlq8JNKMGaixC2Pz5L4l2p95TkuRvYbrEbe85NQsDKrAd4zalf7Ml6WiA==", "requires": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "v1.0.0", + "pino-abstract-transport": "v1.1.0", "pino-std-serializers": "^6.0.0", "process-warning": "^2.0.0", "quick-format-unescaped": "^4.0.3", @@ -3797,31 +3939,18 @@ } }, "pino-abstract-transport": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", - "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", "requires": { "readable-stream": "^4.0.0", "split2": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz", - "integrity": "sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ==", - "requires": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10" - } - } } }, "pino-std-serializers": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.0.tgz", - "integrity": "sha512-IWgSzUL8X1w4BIWTwErRgtV8PyOGOOi60uqv0oKuS/fOA8Nco/OeI6lBuc4dyP8MMfdFwyHqTMcBIA7nDiqEqA==" + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" }, "process": { "version": "0.11.10", @@ -3839,15 +3968,15 @@ "integrity": "sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==" }, "promise.allsettled": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.6.tgz", - "integrity": "sha512-22wJUOD3zswWFqgwjNHa1965LvqTX87WPu/lreY2KSd7SVcERfuZ4GfUaOnJNnvtoIv2yXT/W00YIGMetXtFXg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.7.tgz", + "integrity": "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA==", "requires": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" } }, @@ -3882,13 +4011,15 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" } }, "real-require": { @@ -3926,13 +4057,13 @@ } }, "regexp.prototype.flags": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", - "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", + "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "requires": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", - "functions-have-names": "^1.2.3" + "set-function-name": "^2.0.0" } }, "request": { @@ -4025,17 +4156,17 @@ } }, "qs": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz", - "integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "requires": { "side-channel": "^1.0.4" } }, "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" } } }, @@ -4055,6 +4186,17 @@ "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==" }, + "safe-array-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", + "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4100,9 +4242,9 @@ "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==" }, "semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "requires": { "lru-cache": "^6.0.0" }, @@ -4159,6 +4301,16 @@ } } }, + "set-function-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", + "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "requires": { + "define-data-property": "^1.0.1", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.0" + } + }, "setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -4175,9 +4327,9 @@ } }, "sonic-boom": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.3.0.tgz", - "integrity": "sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.4.0.tgz", + "integrity": "sha512-zSe9QQW30nPzjkSJ0glFQO5T9lHsk39tz+2bAAwCj8CNgEG8ItZiX7Wb2ZgA8I04dwRGCcf1m3ABJa8AYm12Fw==", "requires": { "atomic-sleep": "^1.0.0" } @@ -4234,6 +4386,16 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } } } }, @@ -4272,9 +4434,9 @@ } }, "stream-transform": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.2.tgz", - "integrity": "sha512-DHZQPNxvjU2qdQlGcpitn8pkJHQVTqdshtgXaLz6Vc5VCAognbGuuwGS5ugeqGVnyw8j4h89QcV8cwm0D1+V0A==" + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.2.8.tgz", + "integrity": "sha512-NUx0mBuI63KbNEEh9Yj0OzKB7iMOSTpkuODM2G7By+TTVihEIJ0cYp5X+pq/TdJRlsznt6CYR8HqxexyC6/bTw==" }, "string_decoder": { "version": "1.3.0", @@ -4285,39 +4447,39 @@ } }, "string.prototype.trim": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", - "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", + "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", + "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", + "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1" } }, "thread-stream": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.3.0.tgz", - "integrity": "sha512-kaDqm1DET9pp3NXwR8382WHbnpXnRkN9xGN9dQt3B2+dmXiW8X1SOwmFOxAErEQ47ObhZ96J6yhZNXuyCOL7KA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.0.tgz", + "integrity": "sha512-xZYtOtmnA63zj04Q+F9bdEay5r47bvpo1CaNqsKi7TpoJHcotUez8Fkfo2RJWpW91lnnaApdpRbVwCWsy+ifcw==", "requires": { "real-require": "^0.2.0" } @@ -4354,6 +4516,39 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, + "typed-array-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", + "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", + "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, + "typed-array-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", + "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "has-proto": "^1.0.1", + "is-typed-array": "^1.1.10" + } + }, "typed-array-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", @@ -4452,16 +4647,15 @@ } }, "which-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", - "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", + "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" + "has-tostringtag": "^1.0.0" } }, "wrappy": { diff --git a/package.json b/package.json index edd7f33f..30e41608 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,8 @@ "dependencies": { "@eyevinn/hls-repeat": "^0.2.0", "@eyevinn/hls-truncate": "^0.2.0", - "@eyevinn/hls-vodtolive": "3.1.0", - "@eyevinn/m3u8": "^0.5.3", + "@eyevinn/hls-vodtolive": "^4.1.0", + "@eyevinn/m3u8": "^0.5.6", "abort-controller": "^3.0.0", "debug": "^3.2.7", "memcache-client": "^0.10.1", diff --git a/spec/engine/stream_switcher_spec.js b/spec/engine/stream_switcher_spec.js index 4a7a3379..17e9aad7 100644 --- a/spec/engine/stream_switcher_spec.js +++ b/spec/engine/stream_switcher_spec.js @@ -12,37 +12,37 @@ const StreamType = Object.freeze({ const tsNow = Date.now(); class TestAssetManager { - constructor(opts, assets) { - this.assets = [ - { id: 1, title: "Tears of Steel", uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/master.m3u8" }, - { id: 2, title: "VINN", uri: "https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8" } - ]; - if (assets) { - this.assets = assets; - } - this.pos = 0; - this.doFail = false; - if (opts && opts.fail) { - this.doFail = true; - } - if (opts && opts.failOnIndex) { - this.failOnIndex = 1; - } + constructor(opts, assets) { + this.assets = [ + { id: 1, title: "Tears of Steel", uri: "https://maitv-vod.lab.eyevinn.technology/tearsofsteel_4k.mov/master.m3u8" }, + { id: 2, title: "VINN", uri: "https://maitv-vod.lab.eyevinn.technology/VINN.mp4/master.m3u8" } + ]; + if (assets) { + this.assets = assets; + } + this.pos = 0; + this.doFail = false; + if (opts && opts.fail) { + this.doFail = true; + } + if (opts && opts.failOnIndex) { + this.failOnIndex = 1; } + } - getNextVod(vodRequest) { - return new Promise((resolve, reject) => { - if (this.doFail || this.pos === this.failOnIndex) { - reject("should fail"); - } else { - const vod = this.assets[this.pos++]; - if (this.pos > this.assets.length - 1) { - this.pos = 0; - } - resolve(vod); + getNextVod(vodRequest) { + return new Promise((resolve, reject) => { + if (this.doFail || this.pos === this.failOnIndex) { + reject("should fail"); + } else { + const vod = this.assets[this.pos++]; + if (this.pos > this.assets.length - 1) { + this.pos = 0; } - }); - } + resolve(vod); + } + }); + } } const allListSchedules = [ @@ -64,7 +64,7 @@ const allListSchedules = [ start_time: tsNow + 20 * 1000, end_time: tsNow + 20 * 1000 + 1 * 60 * 1000, uri: "https://maitv-vod.lab.eyevinn.technology/MORBIUS_Trailer_2020.mp4/master.m3u8", - duration: 60*1000, + duration: 60 * 1000, } ], [ @@ -83,7 +83,7 @@ const allListSchedules = [ start_time: tsNow + 20 * 1000 + 1 * 60 * 1000, end_time: tsNow + 20 * 1000 + 1 * 60 * 1000 + 60 * 1000, uri: "https://maitv-vod.lab.eyevinn.technology/MORBIUS_Trailer_2020.mp4/master.m3u8", - duration: 60*1000, + duration: 60 * 1000, } ], [ @@ -112,7 +112,7 @@ const allListSchedules = [ start_time: tsNow + 20 * 1000, end_time: tsNow + 20 * 1000 + 1 * 60 * 1000, uri: "https://maitv-vod.lab.eyevinn.technology/MORBIUS_Trailer_2020.mp4/master.m3u8", - duration: 60*1000, + duration: 60 * 1000, }, { eventId: "live-4", @@ -131,7 +131,7 @@ const allListSchedules = [ start_time: tsNow + 20 * 1000, end_time: tsNow + 20 * 1000 + 1 * 60 * 1000, uri: "https://maitv-vod.lab.eyevinn.technology/MORBIUS_Trailer_2020.mp4/master.m3u8", - duration: 60*1000, + duration: 60 * 1000, }, { eventId: "vod-4", @@ -140,7 +140,7 @@ const allListSchedules = [ start_time: tsNow + 20 * 1000 + 1 * 60 * 1000, end_time: tsNow + 20 * 1000 + 1 * 60 * 1000 + 60 * 1000, uri: "https://maitv-vod.lab.eyevinn.technology/MORBIUS_Trailer_2020.mp4/master.m3u8", - duration: 60*1000, + duration: 60 * 1000, }, ], [ @@ -168,33 +168,33 @@ class TestSwitchManager { } const mockLiveSegments = { - "180000": [{duration: 7,uri: "http://mock.mock.com/180000/seg09.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg10.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg11.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg12.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg13.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg14.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg15.ts"}, - {duration: 7,uri: "http://mock.mock.com/180000/seg16.ts"}, - {discontinuity: true }], - "1258000": [{duration: 7,uri: "http://mock.mock.com/1258000/seg09.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg10.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg11.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg12.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg13.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg14.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg15.ts"}, - {duration: 7,uri: "http://mock.mock.com/1258000/seg16.ts"}, - {discontinuity: true }], - "2488000": [{duration: 7,uri: "http://mock.mock.com/2488000/seg09.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg10.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg11.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg12.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg13.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg14.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg15.ts"}, - {duration: 7,uri: "http://mock.mock.com/2488000/seg16.ts"}, - {discontinuity: true }] + "180000": [{ duration: 7, uri: "http://mock.mock.com/180000/seg09.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg10.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg11.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg12.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg13.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg14.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg15.ts" }, + { duration: 7, uri: "http://mock.mock.com/180000/seg16.ts" }, + { discontinuity: true }], + "1258000": [{ duration: 7, uri: "http://mock.mock.com/1258000/seg09.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg10.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg11.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg12.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg13.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg14.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg15.ts" }, + { duration: 7, uri: "http://mock.mock.com/1258000/seg16.ts" }, + { discontinuity: true }], + "2488000": [{ duration: 7, uri: "http://mock.mock.com/2488000/seg09.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg10.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg11.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg12.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg13.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg14.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg15.ts" }, + { duration: 7, uri: "http://mock.mock.com/2488000/seg16.ts" }, + { discontinuity: true }] }; describe("The Stream Switcher", () => { @@ -220,8 +220,8 @@ describe("The Stream Switcher", () => { it("should return false if no StreamSwitchManager was given.", async () => { const assetMgr = new TestAssetManager(); const testStreamSwitcher = new StreamSwitcher(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); await session.initAsync(); await session.incrementAsync(); @@ -233,12 +233,12 @@ describe("The Stream Switcher", () => { it("should validate uri and switch back to linear-vod (session) from event-livestream (sessionLive) if uri is unreachable", async () => { const switchMgr = new TestSwitchManager(0); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "resetLiveStoreAsync").and.callFake( () => true ); - spyOn(sessionLive, "resetSession").and.callFake( () => true ); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "resetLiveStoreAsync").and.callFake(() => true); + spyOn(sessionLive, "resetSession").and.callFake(() => true); spyOn(sessionLive, "getCurrentMediaSequenceSegments").and.returnValue({ currMseqSegs: mockLiveSegments, segCount: 8 }); spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); @@ -248,7 +248,7 @@ describe("The Stream Switcher", () => { await sessionLive.initAsync(); sessionLive.startPlayheadAsync(); - + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); expect(testStreamSwitcher.getEventId()).toBe(null); jasmine.clock().mockDate(tsNow); @@ -271,12 +271,12 @@ describe("The Stream Switcher", () => { it("should switch from linear-vod (session) to event-livestream (sessionLive) according to schedule", async () => { const switchMgr = new TestSwitchManager(0); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "resetLiveStoreAsync").and.callFake( () => true ); - spyOn(sessionLive, "resetSession").and.callFake( () => true ); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "resetLiveStoreAsync").and.callFake(() => true); + spyOn(sessionLive, "resetSession").and.callFake(() => true); spyOn(sessionLive, "getCurrentMediaSequenceSegments").and.returnValue({ currMseqSegs: mockLiveSegments, segCount: 8 }); await session.initAsync(); @@ -294,12 +294,12 @@ describe("The Stream Switcher", () => { it("should switch from event-livestream (sessionLive) to linear-vod (session) according to schedule", async () => { const switchMgr = new TestSwitchManager(0); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "resetLiveStoreAsync").and.callFake( () => true ); - spyOn(sessionLive, "resetSession").and.callFake( () => true ); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "resetLiveStoreAsync").and.callFake(() => true); + spyOn(sessionLive, "resetSession").and.callFake(() => true); spyOn(sessionLive, "getCurrentMediaSequenceSegments").and.returnValue({ currMseqSegs: mockLiveSegments, segCount: 8 }); spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); @@ -320,9 +320,9 @@ describe("The Stream Switcher", () => { it("should switch from linear-vod (session) to event-vod (session) according to schedule", async () => { const switchMgr = new TestSwitchManager(1); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); @@ -340,9 +340,9 @@ describe("The Stream Switcher", () => { it("should switch from event-vod (session) to linear-vod (session) according to schedule", async () => { const switchMgr = new TestSwitchManager(1); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); @@ -360,9 +360,9 @@ describe("The Stream Switcher", () => { it("should not switch from linear-vod (session) to event-vod (session) if duration is not set in schedule", async () => { const switchMgr = new TestSwitchManager(6); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); await session.initAsync(); await session.incrementAsync(); @@ -374,113 +374,194 @@ describe("The Stream Switcher", () => { }); xit("should switch from linear-vod (session) to event-livestream (sessionLive) according to schedule. " + - "\nThen directly to event-vod (session) and finally switch back to linear-vod (session)", async () => { - const switchMgr = new TestSwitchManager(2); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); - const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "_loadAllMediaManifests").and.returnValue(mockLiveSegments); - - await session.initAsync(); - await session.incrementAsync(); - sessionLive.initAsync(); - - jasmine.clock().mockDate(tsNow); - jasmine.clock().tick((25 * 1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); - expect(testStreamSwitcher.getEventId()).toBe("live-1"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); - await sessionLive.getCurrentMediaManifestAsync(180000); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe("vod-1"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60*1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe(null); - }); + "\nThen directly to event-vod (session) and finally switch back to linear-vod (session)", async () => { + const switchMgr = new TestSwitchManager(2); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); + const assetMgr = new TestAssetManager(); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "_loadAllMediaManifests").and.returnValue(mockLiveSegments); + + await session.initAsync(); + await session.incrementAsync(); + sessionLive.initAsync(); + + jasmine.clock().mockDate(tsNow); + jasmine.clock().tick((25 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); + expect(testStreamSwitcher.getEventId()).toBe("live-1"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); + await sessionLive.getCurrentMediaManifestAsync(180000); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe("vod-1"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe(null); + }); xit("should switch from linear-vod (session) to event-livestream (sessionLive) according to schedule. " + - "\nThen directly to 2nd event-livestream (sessionLive) and finally switch back to linear-vod (session)", async () => { - const switchMgr = new TestSwitchManager(3); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); - const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "_loadAllMediaManifests").and.returnValue(mockLiveSegments); - - await session.initAsync(); - await session.incrementAsync(); - await sessionLive.initAsync(); - - jasmine.clock().mockDate(tsNow); - jasmine.clock().tick((25 * 1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); - expect(testStreamSwitcher.getEventId()).toBe("live-2"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); - await sessionLive.getCurrentMediaManifestAsync(180000); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); - expect(testStreamSwitcher.getEventId()).toBe("live-3"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60*1000)); - await sessionLive.getCurrentMediaManifestAsync(180000); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe(null); - }); + "\nThen directly to 2nd event-livestream (sessionLive) and finally switch back to linear-vod (session)", async () => { + const switchMgr = new TestSwitchManager(3); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); + const assetMgr = new TestAssetManager(); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "_loadAllMediaManifests").and.returnValue(mockLiveSegments); + + await session.initAsync(); + await session.incrementAsync(); + await sessionLive.initAsync(); + + jasmine.clock().mockDate(tsNow); + jasmine.clock().tick((25 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); + expect(testStreamSwitcher.getEventId()).toBe("live-2"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); + await sessionLive.getCurrentMediaManifestAsync(180000); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); + expect(testStreamSwitcher.getEventId()).toBe("live-3"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60 * 1000)); + await sessionLive.getCurrentMediaManifestAsync(180000); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe(null); + }); it("should switch from linear-vod (session) to event-vod (session) according to schedule. " + - "\nThen directly to event-livestream (sessionLive) and finally switch back to linear-vod (session)", async () => { - const switchMgr = new TestSwitchManager(4); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); - const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "resetLiveStoreAsync").and.callFake( () => true ); - spyOn(sessionLive, "resetSession").and.callFake( () => true ); - spyOn(sessionLive, "getCurrentMediaSequenceSegments").and.returnValue({ currMseqSegs: mockLiveSegments, segCount: 8 }); - spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); - spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); - - await session.initAsync(); - await session.incrementAsync(); - await sessionLive.initAsync(); - - sessionLive.startPlayheadAsync(); - - jasmine.clock().mockDate(tsNow); - jasmine.clock().tick((25 * 1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe("vod-2"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); - expect(testStreamSwitcher.getEventId()).toBe("live-4"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60*1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe(null); - }); + "\nThen directly to event-livestream (sessionLive) and finally switch back to linear-vod (session)", async () => { + const switchMgr = new TestSwitchManager(4); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); + const assetMgr = new TestAssetManager(); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "resetLiveStoreAsync").and.callFake(() => true); + spyOn(sessionLive, "resetSession").and.callFake(() => true); + spyOn(sessionLive, "getCurrentMediaSequenceSegments").and.returnValue({ currMseqSegs: mockLiveSegments, segCount: 8 }); + spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); + spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); + + await session.initAsync(); + await session.incrementAsync(); + await sessionLive.initAsync(); + + sessionLive.startPlayheadAsync(); + + jasmine.clock().mockDate(tsNow); + jasmine.clock().tick((25 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe("vod-2"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(true); + expect(testStreamSwitcher.getEventId()).toBe("live-4"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe(null); + }); it("should switch from linear-vod (session) to event-vod (session) according to schedule. " + - "\nThen directly to 2nd event-vod (session) and finally switch back to linear-vod (session)", async () => { + "\nThen directly to 2nd event-vod (session) and finally switch back to linear-vod (session)", async () => { + const switchMgr = new TestSwitchManager(5); + const testStreamSwitcher = new StreamSwitcher({ streamSwitchManager: switchMgr }); + const assetMgr = new TestAssetManager(); + const session = new Session(assetMgr, { sessionId: "1" }, sessionStore); + const sessionLive = new SessionLive({ sessionId: "1" }, sessionLiveStore); + spyOn(sessionLive, "_loadAllMediaManifests").and.returnValue(mockLiveSegments); + spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); + spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); + + await session.initAsync(); + await session.incrementAsync(); + await sessionLive.initAsync(); + + jasmine.clock().mockDate(tsNow); + jasmine.clock().tick((25 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe("vod-3"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe("vod-4"); + jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60 * 1000)); + expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); + expect(testStreamSwitcher.getEventId()).toBe(null); + }); + + fit("should merge audio segments correctly", async () => { const switchMgr = new TestSwitchManager(5); - const testStreamSwitcher = new StreamSwitcher({streamSwitchManager: switchMgr}); - const assetMgr = new TestAssetManager(); - const session = new Session(assetMgr, {sessionId: "1"}, sessionStore); - const sessionLive = new SessionLive({sessionId: "1"}, sessionLiveStore); - spyOn(sessionLive, "_loadAllMediaManifests").and.returnValue(mockLiveSegments); - spyOn(session, "setCurrentMediaSequenceSegments").and.returnValue(true); - spyOn(session, "setCurrentMediaAndDiscSequenceCount").and.returnValue(true); - - await session.initAsync(); - await session.incrementAsync(); - await sessionLive.initAsync(); + const sessionLive = new StreamSwitcher({ streamSwitchManager: switchMgr }); + const fromSegments = { + aac: { + en: [ + { + id: 1, + uri: "en1.m3u8" + }, + { + id: 2, + uri: "en2.m3u8" + }, + { + id: 3, + uri: "en3.m3u8" + }, + ], + es: [ + { + id: 1, + uri: "es1.m3u8" + }, + { + id: 2, + uri: "es2.m3u8" + }, + { + id: 3, + uri: "es3.m3u8" + }, + ] + } - jasmine.clock().mockDate(tsNow); - jasmine.clock().tick((25 * 1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe("vod-3"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000))); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe("vod-4"); - jasmine.clock().tick((1 + (60 * 1000 + 20 * 1000)) + (60*1000)); - expect(await testStreamSwitcher.streamSwitcher(session, sessionLive)).toBe(false); - expect(testStreamSwitcher.getEventId()).toBe(null); + }; + const toSegments = { + aac: { + en: [ + { + id: 4, + uri: "en4.m3u8" + }, + { + id: 5, + uri: "en5.m3u8" + }, + { + id: 6, + uri: "en6.m3u8" + }, + ] + } + }; + let newList = sessionLive._mergeAudioSegments(toSegments, fromSegments, true); + let result = { + aac: { + en: [ + { discontinuity: true }, + { id: 4, uri: 'en4.m3u8' }, + { id: 5, uri: 'en5.m3u8' }, + { id: 6, uri: 'en6.m3u8' }, + { id: 1, uri: 'en1.m3u8' }, + { id: 2, uri: 'en2.m3u8' }, + { id: 3, uri: 'en3.m3u8' } + ], + es: [ + { discontinuity: true }, + { id: 4, uri: 'en4.m3u8' }, + { id: 5, uri: 'en5.m3u8' }, + { id: 6, uri: 'en6.m3u8' }, + { id: 1, uri: 'es1.m3u8' }, + { id: 2, uri: 'es2.m3u8' }, + { id: 3, uri: 'es3.m3u8' } + ] + } + } + + expect(newList).toEqual(result); }); }); \ No newline at end of file From 4aab8bbb7565763fcc08a3091c13926df9dacffb Mon Sep 17 00:00:00 2001 From: Nicholas Frederiksen Date: Wed, 4 Oct 2023 08:54:35 +0200 Subject: [PATCH 14/17] fix: put back the disc-tag text adder --- engine/session_live.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/engine/session_live.js b/engine/session_live.js index 034942b8..03842811 100644 --- a/engine/session_live.js +++ b/engine/session_live.js @@ -2160,6 +2160,9 @@ class SessionLive { for (let i = 0; i < size; i++) { const seg = segments[variantKey][i]; const nextSeg = segments[variantKey][i + 1] ? segments[variantKey][i + 1] : {}; + if (seg.discontinuity && !seg.cue) { + m3u8 += "#EXT-X-DISCONTINUITY\n"; + } m3u8 += segToM3u8(seg, i, size, nextSeg, previousSeg); if (m3u8.includes("#EXT-X-DISCONTINUITY\n#EXT-X-DISCONTINUITY\n")) { debug(`[${this.sessionId}]: Removing Duplicate Disc-tag from output M3u8`); From e457946caa54ed0b1ba9c5418508783354453d95 Mon Sep 17 00:00:00 2001 From: Nicholas Frederiksen Date: Thu, 22 Aug 2024 13:31:59 +0200 Subject: [PATCH 15/17] chore: clean up, test assets, log req time --- engine/server.ts | 2 ++ engine/session_live.js | 17 ++++++----------- examples/demux.ts | 40 ++++++++++++++++++++++++++++++++-------- examples/livemix.ts | 34 +++++++++++++++++++++++++++------- 4 files changed, 67 insertions(+), 26 deletions(-) diff --git a/engine/server.ts b/engine/server.ts index 6a72c95f..a93721de 100644 --- a/engine/server.ts +++ b/engine/server.ts @@ -870,6 +870,7 @@ export class ChannelEngine { await timer(500); } debug(`switcherStatus[${req.params[1]}]=[${switcherStatus[req.params[1]]}]`); + let ts1 = Date.now(); if (switcherStatus[req.params[1]]) { debug(`[${req.params[1]}]: Responding with Live-stream manifest`); body = await sessionLive.getCurrentMediaManifestAsync(req.params[0]); @@ -877,6 +878,7 @@ export class ChannelEngine { debug(`[${req.params[1]}]: Responding with VOD2Live manifest`); body = await session.getCurrentMediaManifestAsync(req.params[0], req.headers["x-playback-session-id"]); } + debug(`[${req.params[1]}]: Manifest Request Took (${Date.now() - ts1})ms`); } //verbose(`[${session.sessionId}] body=`); diff --git a/engine/session_live.js b/engine/session_live.js index 03842811..4ef93486 100644 --- a/engine/session_live.js +++ b/engine/session_live.js @@ -1032,11 +1032,6 @@ class SessionLive { if (manifestList.some((result) => result.status === "rejected")) { FETCH_ATTEMPTS--; debug(`[${this.sessionId}]: ALERT! Promises I: Failed, Rejection Found! Trying again in 1000ms...`); - console.log( - manifestList.map((r) => { - return { status: r.status }; - }) - ); await timer(1000); continue; } @@ -2228,7 +2223,7 @@ class SessionLive { return track; } let tracksMatchingOnLanguage = tracks.filter((t) => { - if (this._getLangFromTrack(t) === track) { + if (this._getLangFromTrack(t) === this._getLangFromTrack(track)) { return true; } return false; @@ -2239,7 +2234,7 @@ class SessionLive { } // If no matches then check if we have any matched on group id, then use fallback (first) language let tracksMatchingOnGroupId = tracks.filter((t) => { - if (this._getLangFromTrack(t) === track) { + if (this._getGroupFromTrack(t) === this._getGroupFromTrack(track)) { return true; } return false; @@ -2378,7 +2373,7 @@ class SessionLive { groupId: null, language: null, }; - const match = track.match(/g:(.*?),l:(.*)/); + const match = track.match(/g:(.*?);l:(.*)/); if (match) { const g = match[1]; const l = match[2]; @@ -2393,7 +2388,7 @@ class SessionLive { } _getLangFromTrack(track) { - const match = track.match(/g:(.*?),l:(.*)/); + const match = track.match(/g:(.*?);l:(.*)/); if (match) { const g = match[1]; const l = match[2]; @@ -2406,7 +2401,7 @@ class SessionLive { } _getGroupFromTrack(track) { - const match = track.match(/g:(.*?),l:(.*)/); + const match = track.match(/g:(.*?);l:(.*)/); if (match) { const g = match[1]; const l = match[2]; @@ -2419,7 +2414,7 @@ class SessionLive { } _getTrackFromGroupAndLang(g, l) { - return `g:${g},l:${l}`; + return `g:${g};l:${l}`; } _isBandwidth(bw) { diff --git a/examples/demux.ts b/examples/demux.ts index c6078980..e6f456bc 100644 --- a/examples/demux.ts +++ b/examples/demux.ts @@ -23,14 +23,30 @@ class RefAssetManager implements IAssetManager { 1: [ { id: 1, - title: "Elephants dream", - uri: "https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/index.m3u8", + title: "DEV DEMUX ASSET", + uri: "https://trailer-admin-cdn.a2d.tv/virtualchannels/dev_asset_001/demux/demux.m3u8", }, ], + 2: [ + { + id: 2, + title: "DEV DEMUX ASSET ts but perfect match langs", + uri: "https://trailer-admin-cdn.a2d.tv/virtualchannels/dev_asset_001/demux/demux2.2.m3u8", + }, + ], + 3: [ + { + id: 3, + title: "DEV DEMUX ASSET ts but has 3 langs not 2", + uri: "https://trailer-admin-cdn.a2d-dev.tv/demux/asset_001/master_720360enspde.m3u8", + }, + ], }; this.pos = { 1: 0, + 2: 0, + 3: 0, }; } @@ -61,7 +77,14 @@ class RefAssetManager implements IAssetManager { class RefChannelManager implements IChannelManager { getChannels(): Channel[] { - return [{ id: "1", profile: this._getProfile(), audioTracks: this._getAudioTracks(), subtitleTracks: this._getSubtitleTracks() }]; + return [ + //{ id: "1", profile: this._getProfile(), audioTracks: this._getAudioTracks(), subtitleTracks: this._getSubtitleTracks() }, + //{ id: "2", profile: this._getProfile(), audioTracks: this._getAudioTracks(), subtitleTracks: this._getSubtitleTracks() }, + { id: "3", profile: this._getProfile(), audioTracks: [ + { language: "en", name: "English", default: true }, + { language: "sp", name: "Spanish", default: false }, + { language: "de", name: "German", default: false }, + ], subtitleTracks: this._getSubtitleTracks() }]; } _getProfile(): ChannelProfile[] { @@ -85,13 +108,14 @@ class RefChannelManager implements IChannelManager { _getAudioTracks(): AudioTracks[] { return [ { language: "en", name: "English", default: true }, - { language: "es", name: "Spanish", default: false }, + { language: "sp", name: "Spanish", default: false }, + { language: "de", name: "German", default: false }, ]; } _getSubtitleTracks(): SubtitleTracks[] { return [ - { language: "zh", name: "chinese", default: true }, - { language: "fr", name: "french", default: false } + // { language: "zh", name: "chinese", default: true }, + // { language: "fr", name: "french", default: false } ]; } } @@ -107,10 +131,10 @@ const engineOptions: ChannelEngineOpts = { slateRepetitions: 10, redisUrl: process.env.REDIS_URL, useDemuxedAudio: true, - alwaysNewSegments: false, + alwaysNewSegments: true, useVTTSubtitles: true }; const engine = new ChannelEngine(refAssetManager, engineOptions); engine.start(); -engine.listen(process.env.PORT || 8001); +engine.listen(process.env.PORT || 5000); diff --git a/examples/livemix.ts b/examples/livemix.ts index 3eb69e55..1c83d357 100644 --- a/examples/livemix.ts +++ b/examples/livemix.ts @@ -9,6 +9,25 @@ import { ChannelEngine, ChannelEngineOpts, } from "../index"; const { v4: uuidv4 } = require('uuid'); +const DEMUX_CONTENT = { + ts: { + slate: "https://trailer-admin-cdn.a2d.tv/virtualchannels/bumper/demux/demux.m3u8", + vod: "https://playertest.longtailvideo.com/adaptive/elephants_dream_v4/index.m3u8", + trailer: "https://trailer-admin-cdn.a2d.tv/virtualchannels/trailers/demux002/demux.m3u8", + bumper: "https://trailer-admin-cdn.a2d.tv/virtualchannels/bumper/demux/demux.m3u8", + live: "http://localhost:5000/channels/3/master.m3u8" + }, + cmaf: { + slate: "https://vod.streaming.a2d.tv/trailers/fillers/6409d46c07b49f0029c1b170/output_v2.ism/.m3u8", + vod: "https://vod.streaming.a2d.tv/13ec7661-66d7-44d6-b818-7743fe916a87/b747af60-ef3f-11ed-bd7e-9125837ccca3_20343615.ism/.m3u8", // 66 min + trailer: "https://vod.streaming.a2d.tv/trailers/650c397b298d58002a812ca0/output_v2.ism/.m3u8", + bumper: "https://vod.streaming.a2d.tv/trailers/bumpers/tv4_summer/start/output_v2.ism/.m3u8", + live: "https://vc-engine-alb.a2d.tv/channels/a7b2c62f-99b7-4fd9-bde5-56201c59b0a2/master.m3u8"//"https://vc-engine-alb.a2d-dev.tv/channels/1d1847f1-2de7-4f87-b06c-c971107d0ca3/master.m3u8" + } +} + +const HLS_CONTENT = DEMUX_CONTENT["ts"]; + const STITCH_ENDPOINT = process.env.STITCH_ENDPOINT || "http://lambda.eyevinn.technology/stitch/master.m3u8"; class RefAssetManager implements IAssetManager { private assets; @@ -16,7 +35,7 @@ class RefAssetManager implements IAssetManager { constructor(opts?) { this.assets = { '1': [ - { id: 1, title: "Tears of Steel", uri: "https://mtoczko.github.io/hls-test-streams/test-audio-pdt/playlist.m3u8" }, + { id: 1, title: "Tears of Steel", uri: HLS_CONTENT.vod }, ] }; this.pos = { @@ -44,8 +63,9 @@ class RefAssetManager implements IAssetManager { breaks: [ { pos: 0, - duration: 15 * 1000, - url: "https://mtoczko.github.io/hls-test-streams/test-audio-pdt/playlist.m3u8" + duration: 30 * 1000, + url: HLS_CONTENT.trailer + } ] }; @@ -97,7 +117,7 @@ class RefChannelManager implements IChannelManager { _getAudioTracks(): AudioTracks[] { return [ { language: "en", name: "English", default: true }, - { language: "es", name: "Spanish", default: false }, + { language: "sp", name: "Spanish", default: false }, ]; } } @@ -128,7 +148,7 @@ class StreamSwitchManager implements IStreamSwitchManager { } getPrerollUri(channelId): Promise { - const defaultPrerollSlateUri = "https://maitv-vod.lab.eyevinn.technology/slate-consuo.mp4/master.m3u8" + const defaultPrerollSlateUri = HLS_CONTENT.bumper; return new Promise((resolve, reject) => { resolve(defaultPrerollSlateUri); }); } @@ -151,7 +171,7 @@ class StreamSwitchManager implements IStreamSwitchManager { type: StreamType.LIVE, start_time: startOffset, end_time: endTime, - uri: "http://localhost:8001/channels/1/master.m3u8", + uri: HLS_CONTENT.live, }/*, { eventId: this.generateID(), @@ -181,7 +201,7 @@ const engineOptions: ChannelEngineOpts = { averageSegmentDuration: 2000, channelManager: refChannelManager, streamSwitchManager: refStreamSwitchManager, - defaultSlateUri: "https://maitv-vod.lab.eyevinn.technology/slate-consuo.mp4/master.m3u8", + defaultSlateUri: HLS_CONTENT.slate, slateRepetitions: 10, redisUrl: process.env.REDIS_URL, useDemuxedAudio: true, From 2c617c85fe00aa611982c0ef2e8774bd2d251ff8 Mon Sep 17 00:00:00 2001 From: Nfrederiksen Date: Thu, 22 Aug 2024 13:44:37 +0200 Subject: [PATCH 16/17] fix log line --- engine/session.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/session.js b/engine/session.js index e3e69162..d21f14a4 100644 --- a/engine/session.js +++ b/engine/session.js @@ -1548,6 +1548,7 @@ class Session { debug(`[${this._sessionId}]: state=VOD_PLAYING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`); return; case SessionState.VOD_NEXT_INITIATING: + debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`); debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`); if (!isLeader) { debug(`[${this._sessionId}]: not the leader so just waiting for the VOD to be initiated`); @@ -1668,7 +1669,8 @@ class Session { this.leaderIsSettingNextVod = false; await this._playheadState.set("playheadRef", Date.now(), isLeader); await this._playheadState.set("diffCompensation", this.diffCompensation, isLeader); - debug(`[${this._sessionId}]: sharing durrent vods diffCompensation=${this.diffCompensation}`); + debug(`[${this._sessionId}]: sharing currentVod's diffCompensation=${this.diffCompensation}`); + debug(`[${this._sessionId}]: sharing currentVod's diffCompensation=${this.diffCompensation}`); this.produceEvent({ type: 'NOW_PLAYING', data: { From dd988b643181e911c80109bf18d0481b70537479 Mon Sep 17 00:00:00 2001 From: Nfrederiksen Date: Thu, 22 Aug 2024 13:46:12 +0200 Subject: [PATCH 17/17] fix: clean up duplicate log lines --- engine/session.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/engine/session.js b/engine/session.js index d21f14a4..c2e3da5f 100644 --- a/engine/session.js +++ b/engine/session.js @@ -1548,7 +1548,6 @@ class Session { debug(`[${this._sessionId}]: state=VOD_PLAYING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`); return; case SessionState.VOD_NEXT_INITIATING: - debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`); debug(`[${this._sessionId}]: state=VOD_NEXT_INITIATING (${sessionState.vodMediaSeqVideo}_${sessionState.vodMediaSeqAudio}_${sessionState.vodMediaSeqSubtitle}, ${currentVod.getLiveMediaSequencesCount()}_${currentVod.getLiveMediaSequencesCount("audio")}_${currentVod.getLiveMediaSequencesCount("subtitle")})`); if (!isLeader) { debug(`[${this._sessionId}]: not the leader so just waiting for the VOD to be initiated`); @@ -1670,7 +1669,6 @@ class Session { await this._playheadState.set("playheadRef", Date.now(), isLeader); await this._playheadState.set("diffCompensation", this.diffCompensation, isLeader); debug(`[${this._sessionId}]: sharing currentVod's diffCompensation=${this.diffCompensation}`); - debug(`[${this._sessionId}]: sharing currentVod's diffCompensation=${this.diffCompensation}`); this.produceEvent({ type: 'NOW_PLAYING', data: {