From 3c4cfcd007f7c36ef22ed60f7159aca2e1c43e37 Mon Sep 17 00:00:00 2001 From: lastpeony Date: Mon, 18 Nov 2024 19:19:21 +0300 Subject: [PATCH] remove interval approach, move to alternating between main stream and backup stream --- src/web_player.js | 128 ++++++------------- test/embedded-player.test.js | 236 ++++++++++++++++------------------- 2 files changed, 143 insertions(+), 221 deletions(-) diff --git a/src/web_player.js b/src/web_player.js index 744e8aa..acf84b9 100644 --- a/src/web_player.js +++ b/src/web_player.js @@ -187,10 +187,10 @@ export class WebPlayer { */ tryNextTechTimer; - /** + /** * Listener for ID3 text data */ - id3Listener; + id3Listener; /** * REST API Filter JWT @@ -228,24 +228,12 @@ export class WebPlayer { backupStreamId; /** - * Periodic timer interval for trying to play backup stream. - */ - backupStreamPlayerInterval; - - /** - * Current number of attempts to play the backup stream. + * Indicates whether the backup stream has been attempted for playback. + * The backup stream mechanism activates when the main stream is unplayable, backupStreamId is defined and playOrder array size is 1. + * If playback of the backup stream fails, the system will switch back to the main stream + * and continue alternating between the two in an attempt to play the stream. */ - backupStreamTryCount; - - /** - * Maximum allowed attempts to play the backup stream. - */ - maxBackupStreamTryCount; - - /** - * Time to wait between backup stream play attempts in miliseconds. - */ - backupStreamPlayerIntervalMs; + triedBackupStream; constructor(configOrWindow, containerElement, placeHolderElement) { @@ -270,10 +258,6 @@ export class WebPlayer { WebPlayer.VIDEO_PLAYER_ID = "video-player"; WebPlayer.PLAYER_EVENTS = ['abort','canplay','canplaythrough','durationchange','emptied','ended','error','loadeddata','loadedmetadata','loadstart','pause','play','playing','progress','ratechange','seeked','seeking','stalled','suspend','timeupdate','volumechange','waiting','enterpictureinpicture','leavepictureinpicture','fullscreenchange','resize','audioonlymodechange','audiopostermodechange','controlsdisabled','controlsenabled','debugon','debugoff','disablepictureinpicturechanged','dispose','enterFullWindow','error','exitFullWindow','firstplay','fullscreenerror','languagechange','loadedmetadata','loadstart','playerreset','playerresize','posterchange','ready','textdata','useractive','userinactive','usingcustomcontrols','usingnativecontrols']; - - WebPlayer.DEFAULT_BACKUP_STREAM_MAX_TRY_ATTEMPT = 5 - - WebPlayer.DEFAULT_BACKUP_STREAM_TRY_ATTEMPT_INTERVAL_MS = 5000 // Initialize default values this.setDefaults(); @@ -440,9 +424,7 @@ export class WebPlayer { this.isIPCamera = false; this.playerEvents = WebPlayer.PLAYER_EVENTS this.backupStreamId = null - this.maxBackupStreamTryCount = WebPlayer.DEFAULT_BACKUP_STREAM_MAX_TRY_ATTEMPT; - this.backupStreamTryCount = 0; - this.backupStreamPlayerIntervalMs = WebPlayer.DEFAULT_BACKUP_STREAM_TRY_ATTEMPT_INTERVAL_MS; + this.triedBackupStream = false; } initializeFromUrlParams() { @@ -594,12 +576,12 @@ export class WebPlayer { } isBackupStreamEnabled(){ - return this.backupStreamId && this.playOrder.length === 1 && this.backupStreamTryCount !== this.maxBackupStreamTryCount + return this.backupStreamId && this.playOrder.length === 1 && !this.triedBackupStream } handleWebRTCErrorMessages(errors){ if(errors["error"] === "no_stream_exist" && this.isBackupStreamEnabled()){ - this.startBackupStreamPlayerInterval(); + this.tryBackupStream(); } else if (errors["error"] == "no_stream_exist" || errors["error"] == "WebSocketNotConnected" || errors["error"] == "not_initialized_yet" || errors["error"] == "data_store_not_available" @@ -631,15 +613,12 @@ export class WebPlayer { if (infos["info"] === "ice_connection_state_changed") { Logger.debug("ice connection state changed to " + infos["obj"].state); if (infos["obj"].state === "completed" || infos["obj"].state === "connected") { - if(this.backupStreamPlayerInterval){ - this.cancelBackupStreamPlayerInterval() - } this.iceConnected = true; } else if (infos["obj"].state === "failed" || infos["obj"].state === "disconnected" || infos["obj"].state === "closed") { Logger.warn("Ice connection is not connected.") if(this.isBackupStreamEnabled()){ - this.startBackupStreamPlayerInterval() + this.tryBackupStream(); }else{ Logger.warn("tryNextTech to replay"); this.tryNextTech(); @@ -662,16 +641,14 @@ export class WebPlayer { } } - replaceStreamIdWithBackupStreamId(streamUrl, extension){ - if(extension === "webrtc"){ - const lastSlashIndex = streamUrl.lastIndexOf('/'); - const lastDotIndex = streamUrl.lastIndexOf('.'); - - if (lastSlashIndex !== -1 && lastDotIndex !== -1 && lastSlashIndex < lastDotIndex) { - streamUrl = streamUrl.slice(0, lastSlashIndex + 1) + this.backupStreamId + streamUrl.slice(lastDotIndex); - } - + replaceStreamIdWithBackupStreamId(streamUrl){ + const lastSlashIndex = streamUrl.lastIndexOf('/'); + const lastDotIndex = streamUrl.lastIndexOf('.'); + + if (lastSlashIndex !== -1 && lastDotIndex !== -1 && lastSlashIndex < lastDotIndex) { + streamUrl = streamUrl.slice(0, lastSlashIndex + 1) + this.backupStreamId + streamUrl.slice(lastDotIndex); } + return streamUrl; } @@ -717,7 +694,9 @@ export class WebPlayer { var streamId = this.streamId; if(playBackupStream){ streamId = this.backupStreamId; - streamUrl = this.replaceStreamIdWithBackupStreamId(streamUrl, extension) + if(extension === "webrtc"){ + streamUrl = this.replaceStreamIdWithBackupStreamId(streamUrl) + } } var preview = streamId; @@ -1058,40 +1037,10 @@ export class WebPlayer { return streamPath; } - cancelBackupStreamPlayerInterval(){ - if(this.backupStreamPlayerInterval){ - clearInterval(this.backupStreamPlayerInterval) - this.backupStreamPlayerInterval = null - this.backupStreamTryCount = 0; - } - } - - startBackupStreamPlayerInterval(){ - if(!this.backupStreamPlayerInterval){ - Logger.warn("Setting backup stream player timer.") - this.tryBackupStream() - this.backupStreamPlayerInterval = setInterval(()=>{ - if(this.backupStreamTryCount === this.maxBackupStreamTryCount){ - Logger.warn("Playing backup stream failed after " + this.maxBackupStreamTryCount+ " attempts. Giving up. Switch to try playing main stream.") - this.cancelBackupStreamPlayerInterval() - this.tryNextTech() - }else{ - this.tryBackupStream() - } - }, this.backupStreamPlayerIntervalMs) - } - } - tryBackupStream(){ - this.backupStreamTryCount++; - Logger.warn("Trying to play backup stream. Attempt: "+ this.backupStreamTryCount) - this.destroyDashPlayer(); - this.destroyVideoJSPlayer(); - this.setPlayerVisible(false); - setTimeout(() => { - var playBackupStream = true; - this.playIfExists(this.currentPlayType, playBackupStream); - }, 500); + console.log("Trying to play backup stream.") + var playBackupStream = true; + this.playIfExists(this.currentPlayType, playBackupStream); } /** @@ -1210,7 +1159,7 @@ export class WebPlayer { this.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ERROR, (event) => { Logger.warn("dash playback error: " + event); if(playBackupStream){ - this.startBackupStreamPlayerInterval() + this.tryBackupStream() }else{ this.tryNextTech(); } @@ -1218,7 +1167,7 @@ export class WebPlayer { this.dashPlayer.on(dashjs.MediaPlayer.events.ERROR, (event) => { Logger.warn("error: " + event); if(playBackupStream){ - this.startBackupStreamPlayerInterval() + this.tryBackupStream() }else{ this.tryNextTech(); } @@ -1314,8 +1263,13 @@ export class WebPlayer { this.containerElement.innerHTML = this.videoHTMLContent; + if(playBackupStream){ + this.triedBackupStream = true + }else{ + this.triedBackupStream = false + } - Logger.warn("Try to play the stream " + streamId + " with " + this.currentPlayType); + console.log("Try to play the stream " + streamId + " with " + this.currentPlayType); // eslint-disable-next-line default-case switch (this.currentPlayType) { case "hls": @@ -1324,9 +1278,6 @@ export class WebPlayer { //2. Play stream with m3u8 for live and VoD //3. if files are not available check nextTech is being called return this.checkStreamExistsViaHttp(WebPlayer.STREAMS_FOLDER, streamId, WebPlayer.HLS_EXTENSION).then((streamPath) => { - if(this.backupStreamPlayerInterval){ - this.cancelBackupStreamPlayerInterval() - } this.playWithVideoJS(streamPath, WebPlayer.HLS_EXTENSION, playBackupStream); Logger.warn("incoming stream path: " + streamPath); @@ -1334,16 +1285,13 @@ export class WebPlayer { Logger.warn("HLS stream resource not available for stream:" + streamId + " error is " + error + ". Try next play tech"); if(this.isBackupStreamEnabled()){ - this.startBackupStreamPlayerInterval() + this.tryBackupStream() }else{ this.tryNextTech(); } }); case "ll-hls": return this.checkStreamExistsViaHttp(WebPlayer.STREAMS_FOLDER + "/" + WebPlayer.LL_HLS_FOLDER, streamId, WebPlayer.HLS_EXTENSION).then((streamPath) => { - if(this.backupStreamPlayerInterval){ - this.cancelBackupStreamPlayerInterval() - } this.playWithVideoJS(streamPath, WebPlayer.HLS_EXTENSION, playBackupStream); Logger.warn("incoming stream path: " + streamPath); @@ -1351,21 +1299,18 @@ export class WebPlayer { Logger.warn("LL-HLS stream resource not available for stream:" + streamId + " error is " + error + ". Try next play tech"); if(this.isBackupStreamEnabled()){ - this.startBackupStreamPlayerInterval() + this.tryBackupStream() }else{ this.tryNextTech(); } }); case "dash": return this.checkStreamExistsViaHttp(WebPlayer.STREAMS_FOLDER, streamId + "/" + streamId, WebPlayer.DASH_EXTENSION).then((streamPath) => { - if(this.backupStreamPlayerInterval){ - this.cancelBackupStreamPlayerInterval() - } this.playViaDash(streamPath); }).catch((error) => { Logger.warn("DASH stream resource not available for stream:" + streamId + " error is " + error + ". Try next play tech"); if(this.isBackupStreamEnabled()){ - this.startBackupStreamPlayerInterval() + this.tryBackupStream() }else{ this.tryNextTech(); } @@ -1410,6 +1355,7 @@ export class WebPlayer { }); } + } /** @@ -1740,4 +1686,4 @@ export class WebPlayer { } } -} +} \ No newline at end of file diff --git a/test/embedded-player.test.js b/test/embedded-player.test.js index 90fa1ad..60ac9cf 100644 --- a/test/embedded-player.test.js +++ b/test/embedded-player.test.js @@ -736,10 +736,12 @@ describe("WebPlayer", function() { }; var player = new WebPlayer(windowComponent, videoContainer, placeHolder); + + player.triedBackupStream = false; + var tryNextTech = sinon.replace(player, "tryNextTech", sinon.fake.returns(Promise.resolve(""))); - var cancelBackupStreamPlayerInterval = sinon.replace(player, "cancelBackupStreamPlayerInterval", sinon.fake()); var isBackupStreamEnabledSpy = sinon.spy(player, "isBackupStreamEnabled"); // Use spy here - var startBackupStreamPlayerInterval = sinon.replace(player, "startBackupStreamPlayerInterval", sinon.fake()); + var tryBackupStream = sinon.replace(player, "tryBackupStream", sinon.fake()); var infos = { info: "ice_connection_state_changed", @@ -796,7 +798,6 @@ describe("WebPlayer", function() { state: "completed" } }; - player.backupStreamPlayerInterval = 1; player.handleWebRTCInfoMessages(infos); @@ -809,13 +810,11 @@ describe("WebPlayer", function() { player.handleWebRTCInfoMessages(infos); - expect(cancelBackupStreamPlayerInterval.calledTwice).to.be.true; // Set up player properties to ensure isBackupStreamEnabled returns true player.backupStreamId = "backupStreamId"; player.playOrder = ["webrtc"]; // Ensure playOrder length is 1 - player.backupStreamTryCount = 1; - player.maxBackupStreamTryCount = 5; + player.triedBackupStream = false infos = { info: "ice_connection_state_changed", @@ -829,7 +828,7 @@ describe("WebPlayer", function() { expect(isBackupStreamEnabledSpy.calledTwice).to.be.true; expect(isBackupStreamEnabledSpy.returned(true)).to.be.true; - expect(startBackupStreamPlayerInterval.calledOnce).to.be.true; + expect(tryBackupStream.calledOnce).to.be.true; }); it("testAutoPlay",async function(){ @@ -1143,73 +1142,6 @@ describe("WebPlayer", function() { }) - it("Start backup stream interval", async function() { - this.timeout(10000); - - var videoContainer = document.createElement("video_container"); - var placeHolder = document.createElement("place_holder"); - - var locationComponent = { - href: 'http://example.com?id=stream123', - search: "?id=stream123", - pathname: "/", - protocol: "http:" - }; - var windowComponent = { - location: locationComponent, - document: document - }; - - var player = new WebPlayer(windowComponent, videoContainer, placeHolder); - - var tryBackupStream = sinon.replace(player, "tryBackupStream", sinon.fake()); - var tryNextTech = sinon.replace(player, "tryNextTech", sinon.fake()); - var cancelBackupStreamPlayerInterval = sinon.replace(player, "cancelBackupStreamPlayerInterval", sinon.fake()); - - player.playOrder = ["webrtc"]; - player.backupStreamId = "backupStreamId"; - player.currentPlayType = "webrtc"; - - player.startBackupStreamPlayerInterval(); - - expect(tryBackupStream.calledOnce).to.be.true; - expect(player.backupStreamPlayerInterval).to.not.be.undefined; - expect(player.backupStreamTryCount).to.equal(0); - - // Simulate backup stream failure after max attempts - player.backupStreamTryCount = player.maxBackupStreamTryCount; - player.startBackupStreamPlayerInterval(); - clock.tick(player.backupStreamPlayerIntervalMs); - - expect(cancelBackupStreamPlayerInterval.calledOnce).to.be.true; - expect(tryNextTech.calledOnce).to.be.true; - }); - - it("Cancel backup stream interval on success", async function(){ - - var videoContainer = document.createElement("video_container"); - - var placeHolder = document.createElement("place_holder"); - - var locationComponent = { href : 'http://example.com?id=stream123', search: "?id=stream123", pathname: "/", protocol:"http:" }; - var windowComponent = { location : locationComponent, - document: document, - addEventListener: window.addEventListener}; - - var player = new WebPlayer(windowComponent, videoContainer, placeHolder); - var cancelBackupStreamPlayerInterval = sinon.replace(player, "cancelBackupStreamPlayerInterval", sinon.fake()); - var checkStreamExistsViaHttp = sinon.replace(player, "checkStreamExistsViaHttp", sinon.fake.returns(Promise.resolve("streams/stream123.m3u8"))); - - player.backupStreamPlayerInterval = 1 - - await player.playIfExists("hls"); - await player.playIfExists("ll-hls"); - await player.playIfExists("dash") - - sinon.assert.calledThrice(checkStreamExistsViaHttp); - sinon.assert.calledThrice(cancelBackupStreamPlayerInterval); - - }) it("Webrtc play backup stream", async function() { var videoContainer = document.createElement("div"); @@ -1309,23 +1241,15 @@ describe("WebPlayer", function() { var player = new WebPlayer(windowComponent, videoContainer, placeHolder); player.currentPlayType = "webrtc"; - var destroyDashPlayer = sinon.replace(player, "destroyDashPlayer", sinon.fake()); - var destroyVideoJSPlayer = sinon.replace(player, "destroyVideoJSPlayer", sinon.fake()); - var setPlayerVisible = sinon.replace(player, "setPlayerVisible", sinon.fake()); var playIfExists = sinon.replace(player, "playIfExists", sinon.fake()); player.tryBackupStream(); - expect(player.backupStreamTryCount).to.equal(1); - sinon.assert.called(destroyDashPlayer); - sinon.assert.called(destroyVideoJSPlayer); - sinon.assert.calledWith(setPlayerVisible, false); - clock.tick(600); sinon.assert.calledWith(playIfExists, "webrtc", true) }); - it("cancelBackupStreamPlayerInterval", async function() { + it("playIfExists backup stream hls, ll-hls, dash fails", async function() { this.timeout(2000); var videoContainer = document.createElement("video_container"); @@ -1343,58 +1267,29 @@ describe("WebPlayer", function() { }; var player = new WebPlayer(windowComponent, videoContainer, placeHolder); - player.backupStreamPlayerInterval = 1 - player.backupStreamTryCount = 1 - - player.cancelBackupStreamPlayerInterval() - expect(player.backupStreamTryCount).to.equal(0); - expect(player.backupStreamPlayerInterval).to.be.null; - }) - it("playIfExists backup stream hls, dash fails", async function() { - this.timeout(2000); - - var videoContainer = document.createElement("video_container"); - var placeHolder = document.createElement("place_holder"); - - var locationComponent = { - href: 'http://example.com?id=stream123', - search: "?id=stream123", - pathname: "/", - protocol: "http:" - }; - var windowComponent = { - location: locationComponent, - document: document - }; - - var player = new WebPlayer(windowComponent, videoContainer, placeHolder); - - var destroyDashPlayer = sinon.replace(player, "destroyDashPlayer", sinon.fake()); - var destroyVideoJSPlayer = sinon.replace(player, "destroyVideoJSPlayer", sinon.fake()); - var setPlayerVisible = sinon.replace(player, "setPlayerVisible", sinon.fake()); var isBackupStreamEnabledSpy = sinon.spy(player, "isBackupStreamEnabled"); - var startBackupStreamPlayerInterval = sinon.replace(player, "startBackupStreamPlayerInterval", sinon.fake()); - + var tryNextTech = sinon.replace(player, "tryNextTech", sinon.fake()); sinon.replace(player, "checkStreamExistsViaHttp", sinon.fake.rejects(new Error("Stream not found"))); - var playBackupStream = true; - player.playOrder = ["hls"] player.backupStreamId = "backupStreamId" - player.backupStreamTryCount = 0 - player.maxBackupStreamTryCount = 2 + //try to play backup stream, fail, then try main stream(try next tech.) await player.playIfExists("hls", playBackupStream) + await player.playIfExists("ll-hls", playBackupStream) await player.playIfExists("dash", playBackupStream) - expect(isBackupStreamEnabledSpy.calledTwice).to.be.true; - expect(startBackupStreamPlayerInterval.calledTwice).to.be.true; + + expect(isBackupStreamEnabledSpy.calledThrice).to.be.true; + expect(isBackupStreamEnabledSpy.alwaysReturned(false)) + expect(tryNextTech.calledThrice).to.be.true; + }) - it("handleWebRTCErrorMessages", async function() { + it("handleWebRTCErrorMessages", async function() { var videoContainer = document.createElement("video_container"); var placeHolder = document.createElement("place_holder"); @@ -1409,7 +1304,7 @@ describe("WebPlayer", function() { var tryNextTech = sinon.replace(player, "tryNextTech", sinon.fake.returns(Promise.resolve(""))); var playIfExists = sinon.replace(player, "playIfExists", sinon.fake()); var isBackupStreamEnabledSpy = sinon.spy(player, "isBackupStreamEnabled"); - var startBackupStreamPlayerInterval = sinon.replace(player, "startBackupStreamPlayerInterval", sinon.fake()); + var tryBackupStream = sinon.replace(player, "tryBackupStream", sinon.fake()); var errors = { error: "no_stream_exist", @@ -1417,22 +1312,21 @@ describe("WebPlayer", function() { player.backupStreamId = "backupStreamId"; player.playOrder = ["webrtc"]; - player.backupStreamTryCount = 1; - player.maxBackupStreamTryCount = 5; + player.triedBackupStream = false; player.handleWebRTCErrorMessages(errors); expect(isBackupStreamEnabledSpy.calledOnce).to.be.true; expect(isBackupStreamEnabledSpy.returned(true)).to.be.true; - expect(startBackupStreamPlayerInterval.calledOnce).to.be.true; + expect(tryBackupStream.calledOnce).to.be.true; + + player.triedBackupStream = true; var errors = { error: "no_stream_exist", }; - player.backupStreamTryCount = 5; - player.maxBackupStreamTryCount = 5; player.handleWebRTCErrorMessages(errors); @@ -1448,10 +1342,92 @@ describe("WebPlayer", function() { }); -}); + it("Alternate between main stream and backup stream.", async function() { + var videoContainer = document.createElement("video_container"); + var placeHolder = document.createElement("place_holder"); + + var locationComponent = { href: 'http://example.com?id=stream123.mp4', search: "?id=stream123.mp4", pathname: "/", protocol: "http:" }; + var windowComponent = { + location: locationComponent, + document: document, + addEventListener: window.addEventListener + }; + + var player = new WebPlayer(windowComponent, videoContainer, placeHolder); + + player.backupStreamId = "backupStreamId"; + player.streamId = "mainStreamId"; + player.playOrder = ["webrtc"]; + player.currentPlayType = "webrtc"; + player.triedBackupStream = false; + + var tryNextTech = sinon.replace(player, "tryNextTech", sinon.fake.returns(Promise.resolve(""))); + var playIfExists = sinon.spy(player, "playIfExists"); + var playWithVideoJS = sinon.replace(player, "playWithVideoJS", sinon.fake()); + var isBackupStreamEnabledSpy = sinon.spy(player, "isBackupStreamEnabled"); + var tryBackupStream = sinon.spy(player, "tryBackupStream"); + + // Initial playback attempt, will try main stream id. + await player.playIfExists("webrtc"); + + expect(player.triedBackupStream).to.be.false; + // First failure - handleWebRTCInfoMessages with failed should try backup stream + player.handleWebRTCInfoMessages({ + info: "ice_connection_state_changed", + obj: { state: "failed" } + }); + + //Error info message triggers trying backup stream. + sinon.assert.calledOnce(isBackupStreamEnabledSpy); + expect(isBackupStreamEnabledSpy.returned(true)) + sinon.assert.calledOnce(tryBackupStream); + + // Verify backup stream is now tried + sinon.assert.calledWith(playIfExists, "webrtc", true); + + // Verify triedBackupStream is now true + expect(player.triedBackupStream).to.be.true; + + // Reset spies + isBackupStreamEnabledSpy.resetHistory(); + tryBackupStream.resetHistory(); + playIfExists.resetHistory(); + + // backup stream failure occurs. Should go back to main stream. + player.handleWebRTCInfoMessages({ + info: "ice_connection_state_changed", + obj: { state: "failed" } + }); + + // Verify attempt to go back to main stream. + sinon.assert.calledOnce(tryNextTech); + + // Simulate another attempt. try next tech calls like this. + await player.playIfExists("webrtc"); + + // Reset spies again + isBackupStreamEnabledSpy.resetHistory(); + tryBackupStream.resetHistory(); + playIfExists.resetHistory(); + tryNextTech.resetHistory(); + + // Try next techs call fails. Third failure - should try backup stream again + player.handleWebRTCInfoMessages({ + info: "ice_connection_state_changed", + obj: { state: "failed" } + }); + + // Verify backup stream is tried again + sinon.assert.calledOnce(isBackupStreamEnabledSpy); + sinon.assert.calledOnce(tryBackupStream); + sinon.assert.calledWith(playIfExists, "webrtc", true); + + // Verify triedBackupStream goes back to false after failed backup stream + expect(player.triedBackupStream).to.be.true; + + }); - - \ No newline at end of file +});