From 64243c37e684bee6cbbe3217e7ee90d65746b8c1 Mon Sep 17 00:00:00 2001 From: lastpeony Date: Fri, 15 Nov 2024 15:08:43 +0300 Subject: [PATCH 1/2] switch backup stream --- src/web_player.js | 265 +++++++++++++++++------ test/embedded-player.test.js | 403 ++++++++++++++++++++++++++++++++--- 2 files changed, 573 insertions(+), 95 deletions(-) diff --git a/src/web_player.js b/src/web_player.js index 9a008b7..744e8aa 100644 --- a/src/web_player.js +++ b/src/web_player.js @@ -20,6 +20,10 @@ export class WebPlayer { static DEFAULT_PLAY_TYPE = ["mp4", "webm"]; + static DEFAULT_BACKUP_STREAM_MAX_TRY_ATTEMPT = 5 + + static DEFAULT_BACKUP_STREAM_TRY_ATTEMPT_INTERVAL_MS = 5000 + static HLS_EXTENSION = "m3u8"; static WEBRTC_EXTENSION = "webrtc"; @@ -35,7 +39,6 @@ export class WebPlayer { * lowLatencyHlsFolder: ll-hls folder. Optional. Default value is "ll-hls" */ static LL_HLS_FOLDER = "ll-hls"; - /** * Video HTML content. It's by default STATIC_VIDEO_HTML @@ -97,7 +100,6 @@ export class WebPlayer { */ mute = false; - /** * controls: Toggles the visibility of player controls. */ @@ -112,8 +114,7 @@ export class WebPlayer { * targetLatency: target latency in seconds. Optional. Default value is 3. * It will be taken from url parameter "targetLatency". * It's used for dash(cmaf) playback. - */ - + */ targetLatency = 3; /** @@ -220,6 +221,31 @@ export class WebPlayer { * Is IP Camera */ isIPCamera; + + /** + * Stream id of backup stream. + */ + backupStreamId; + + /** + * Periodic timer interval for trying to play backup stream. + */ + backupStreamPlayerInterval; + + /** + * Current number of attempts to play the backup stream. + */ + backupStreamTryCount; + + /** + * Maximum allowed attempts to play the backup stream. + */ + maxBackupStreamTryCount; + + /** + * Time to wait between backup stream play attempts in miliseconds. + */ + backupStreamPlayerIntervalMs; constructor(configOrWindow, containerElement, placeHolderElement) { @@ -245,6 +271,9 @@ export class WebPlayer { 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(); @@ -410,6 +439,10 @@ export class WebPlayer { this.restAPIPromise = null; 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; } initializeFromUrlParams() { @@ -560,17 +593,57 @@ export class WebPlayer { } } + isBackupStreamEnabled(){ + return this.backupStreamId && this.playOrder.length === 1 && this.backupStreamTryCount !== this.maxBackupStreamTryCount + } + + handleWebRTCErrorMessages(errors){ + if(errors["error"] === "no_stream_exist" && this.isBackupStreamEnabled()){ + this.startBackupStreamPlayerInterval(); + } + else if (errors["error"] == "no_stream_exist" || errors["error"] == "WebSocketNotConnected" + || errors["error"] == "not_initialized_yet" || errors["error"] == "data_store_not_available" + || errors["error"] == "highResourceUsage" || errors["error"] == "unauthorized_access" + || errors["error"] == "user_blocked") { + + //handle high resource usage and not authroized errors && websocket disconnected + //Even if webrtc adaptor has auto reconnect scenario, we dispose the videojs immediately in tryNextTech + // so that reconnect scenario is managed here + this.tryNextTech(); + + + } + else if (errors["error"] == "notSetRemoteDescription") { + /* + * If getting codec incompatible or remote description error, it will redirect HLS player. + */ + Logger.warn("notSetRemoteDescription error. Redirecting to HLS player."); + this.playIfExists("hls"); + } + + if (this.playerListener != null) { + this.playerListener("webrtc-error", errors); + } + + } handleWebRTCInfoMessages(infos) { - if (infos["info"] == "ice_connection_state_changed") { + 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 (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. tryNextTech to replay"); - this.tryNextTech(); + 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() + }else{ + Logger.warn("tryNextTech to replay"); + this.tryNextTech(); + } } } @@ -589,13 +662,26 @@ 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); + } + + } + return streamUrl; + } + /** * Play the stream via videojs * @param {*} streamUrl * @param {*} extension * @returns */ - playWithVideoJS(streamUrl, extension) { + playWithVideoJS(streamUrl, extension, playBackupStream) { var type; if (extension == "mp4") { type = "video/mp4"; @@ -628,8 +714,14 @@ export class WebPlayer { return; } - var preview = this.streamId; - if (this.streamId.endsWith("_adaptive")) { + var streamId = this.streamId; + if(playBackupStream){ + streamId = this.backupStreamId; + streamUrl = this.replaceStreamIdWithBackupStreamId(streamUrl, extension) + } + + var preview = streamId; + if (streamId.endsWith("_adaptive")) { preview = streamId.substring(0, streamId.indexOf("_adaptive")); } @@ -649,7 +741,7 @@ export class WebPlayer { class: 'video-js vjs-default-skin vjs-big-play-centered', muted: this.mute, preload: "auto", - autoplay: this.autoPlay + autoplay: this.autoPlay, }); @@ -658,7 +750,7 @@ export class WebPlayer { this.videojsPlayer.on('webrtc-info', (event, infos) => { - //Logger.warn("info callback: " + JSON.stringify(infos)); + console.log("info callback: " + JSON.stringify(infos)); this.handleWebRTCInfoMessages(infos); }); @@ -666,28 +758,8 @@ export class WebPlayer { this.videojsPlayer.on('webrtc-error', (event, errors) => { //some of the possible errors, NotFoundError, SecurityError,PermissionDeniedError Logger.warn("error callback: " + JSON.stringify(errors)); - - if (errors["error"] == "no_stream_exist" || errors["error"] == "WebSocketNotConnected" - || errors["error"] == "not_initialized_yet" || errors["error"] == "data_store_not_available" - || errors["error"] == "highResourceUsage" || errors["error"] == "unauthorized_access" - || errors["error"] == "user_blocked") { - - //handle high resource usage and not authroized errors && websocket disconnected - //Even if webrtc adaptor has auto reconnect scenario, we dispose the videojs immediately in tryNextTech - // so that reconnect scenario is managed here - - this.tryNextTech(); - } - else if (errors["error"] == "notSetRemoteDescription") { - /* - * If getting codec incompatible or remote description error, it will redirect HLS player. - */ - Logger.warn("notSetRemoteDescription error. Redirecting to HLS player."); - this.playIfExists("hls"); - } - if (this.playerListener != null) { - this.playerListener("webrtc-error", errors); - } + this.handleWebRTCErrorMessages(errors) + }); this.videojsPlayer.on("webrtc-data-received", (event, obj) => { @@ -986,6 +1058,42 @@ 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); + } + /** * try next tech if current tech is not working */ @@ -1018,7 +1126,7 @@ export class WebPlayer { * play stream throgugh dash player * @param {string"} streamUrl */ - playViaDash(streamUrl) { + playViaDash(streamUrl, playBackupStream) { this.destroyDashPlayer(); this.dashPlayer = dashjs.MediaPlayer().create(); this.dashPlayer.extend("RequestModifier", () => { @@ -1089,11 +1197,11 @@ export class WebPlayer { { //do not play again if it's dash because it play last seconds again, let the server clear it setTimeout(() => { - this.playIfExists(this.playOrder[0]); + this.playIfExists(this.playOrder[0], playBackupStream); }, 10000); } else { - this.playIfExists(this.playOrder[0]); + this.playIfExists(this.playOrder[0], playBackupStream); } if (this.playerListener != null) { this.playerListener("ended"); @@ -1101,11 +1209,19 @@ export class WebPlayer { }); this.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ERROR, (event) => { Logger.warn("dash playback error: " + event); - this.tryNextTech(); + if(playBackupStream){ + this.startBackupStreamPlayerInterval() + }else{ + this.tryNextTech(); + } }); this.dashPlayer.on(dashjs.MediaPlayer.events.ERROR, (event) => { Logger.warn("error: " + event); - this.tryNextTech(); + if(playBackupStream){ + this.startBackupStreamPlayerInterval() + }else{ + this.tryNextTech(); + } }); this.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_NOT_ALLOWED, (event) => { @@ -1189,7 +1305,8 @@ export class WebPlayer { * play the stream with the given tech * @param {string} tech */ - async playIfExists(tech) { + async playIfExists(tech, playBackupStream) { + var streamId = playBackupStream ? this.backupStreamId : this.streamId this.currentPlayType = tech; this.destroyVideoJSPlayer(); this.destroyDashPlayer(); @@ -1198,75 +1315,93 @@ export class WebPlayer { this.containerElement.innerHTML = this.videoHTMLContent; - Logger.warn("Try to play the stream " + this.streamId + " with " + this.currentPlayType); + Logger.warn("Try to play the stream " + streamId + " with " + this.currentPlayType); + // eslint-disable-next-line default-case switch (this.currentPlayType) { case "hls": //TODO: Test case for hls //1. Play stream with adaptive m3u8 for live and VoD //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, this.streamId, WebPlayer.HLS_EXTENSION).then((streamPath) => { - - this.playWithVideoJS(streamPath, WebPlayer.HLS_EXTENSION); + 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); }).catch((error) => { - Logger.warn("HLS stream resource not available for stream:" + this.streamId + " error is " + error + ". Try next play tech"); - this.tryNextTech(); + Logger.warn("HLS stream resource not available for stream:" + streamId + " error is " + error + ". Try next play tech"); + if(this.isBackupStreamEnabled()){ + this.startBackupStreamPlayerInterval() + }else{ + this.tryNextTech(); + } }); case "ll-hls": - return this.checkStreamExistsViaHttp(WebPlayer.STREAMS_FOLDER + "/" + WebPlayer.LL_HLS_FOLDER, this.streamId, WebPlayer.HLS_EXTENSION).then((streamPath) => { - - this.playWithVideoJS(streamPath, WebPlayer.HLS_EXTENSION); + 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); }).catch((error) => { - Logger.warn("LL-HLS stream resource not available for stream:" + this.streamId + " error is " + error + ". Try next play tech"); - this.tryNextTech(); + Logger.warn("LL-HLS stream resource not available for stream:" + streamId + " error is " + error + ". Try next play tech"); + if(this.isBackupStreamEnabled()){ + this.startBackupStreamPlayerInterval() + }else{ + this.tryNextTech(); + } }); case "dash": - return this.checkStreamExistsViaHttp(WebPlayer.STREAMS_FOLDER, this.streamId + "/" + this.streamId, WebPlayer.DASH_EXTENSION).then((streamPath) => { + 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:" + this.streamId + " error is " + error + ". Try next play tech"); - this.tryNextTech(); + Logger.warn("DASH stream resource not available for stream:" + streamId + " error is " + error + ". Try next play tech"); + if(this.isBackupStreamEnabled()){ + this.startBackupStreamPlayerInterval() + }else{ + this.tryNextTech(); + } }); case "webrtc": - - - return this.playWithVideoJS(this.addSecurityParams(this.websocketURL), WebPlayer.WEBRTC_EXTENSION); + return this.playWithVideoJS(this.addSecurityParams(this.websocketURL), WebPlayer.WEBRTC_EXTENSION, playBackupStream); case "vod": //TODO: Test case for vod //1. Play stream with mp4 for VoD //2. Play stream with webm for VoD //3. Play stream with playOrder type - var lastIndexOfDot = this.streamId.lastIndexOf("."); + var lastIndexOfDot = streamId.lastIndexOf("."); var extension; if (lastIndexOfDot != -1) { //if there is a dot in the streamId, it means that this is extension, use it. make the extension empty this.playType[0] = ""; - extension = this.streamId.substring(lastIndexOfDot + 1); + extension = streamId.substring(lastIndexOfDot + 1); } else { //we need to give extension to playWithVideoJS extension = this.playType[0]; } - return this.checkStreamExistsViaHttp(WebPlayer.STREAMS_FOLDER, this.streamId, this.playType[0]).then((streamPath) => { + return this.checkStreamExistsViaHttp(WebPlayer.STREAMS_FOLDER, streamId, this.playType[0]).then((streamPath) => { //we need to give extension to playWithVideoJS this.playWithVideoJS(streamPath, extension); }).catch((error) => { - Logger.warn("VOD stream resource not available for stream:" + this.streamId + " and play type " + this.playType[0] + ". Error is " + error); + Logger.warn("VOD stream resource not available for stream:" + streamId + " and play type " + this.playType[0] + ". Error is " + error); if (this.playType.length > 1) { Logger.warn("Try next play type which is " + this.playType[1] + ".") - this.checkStreamExistsViaHttp(WebPlayer.STREAMS_FOLDER, this.streamId, this.playType[1]).then((streamPath) => { + this.checkStreamExistsViaHttp(WebPlayer.STREAMS_FOLDER, streamId, this.playType[1]).then((streamPath) => { this.playWithVideoJS(streamPath, this.playType[1]); }).catch((error) => { Logger.warn("VOD stream resource not available for stream:" + this.streamId + " and play type error is " + error); diff --git a/test/embedded-player.test.js b/test/embedded-player.test.js index ee7f44e..90fa1ad 100644 --- a/test/embedded-player.test.js +++ b/test/embedded-player.test.js @@ -1,6 +1,5 @@ import { STATIC_VIDEO_HTML, WebPlayer } from '../dist/es/web_player.js'; - //import { isMobile } from "../../../main/js/fetch.stream.js"; describe("WebPlayer", function() { @@ -320,7 +319,7 @@ describe("WebPlayer", function() { var testFolder = "testFolder"; var streamId = "stream123"; var extension = "m3u8"; - await player.checkStreamExistsViaHttp(testFolder, streamId, extension).then((streamPath) => { + await player.checkStreamExistsViaHttp(testFolder, streamId, extension).then((streamPath) => { expect(streamPath).to.be.equal("http://example.antmedia.io:5080" + "/" + testFolder + "/" + streamId + "_adaptive" + "." + extension); }).catch((err) => { expect.fail("it should not throw exception. error:" + err); @@ -442,7 +441,7 @@ describe("WebPlayer", function() { expect(destroyDashPlayer.calledOnce).to.be.true; expect(destroyVideoJSPlayer.calledOnce).to.be.true; expect(setPlayerVisible.calledOnce).to.be.true; - expect(setPlayerVisible.calledWithMatch(false)).to.be.true; + expect(setPlayerVisible.calledWithMatch(false)).to.be.true; clock.tick(2500); @@ -610,7 +609,7 @@ describe("WebPlayer", function() { await player.playIfExists("webrtc"); expect(playWithVideoJS.callCount).to.be.equal(1); - expect(playWithVideoJS.calledWithExactly("ws://example.com:5080/stream123.webrtc", "webrtc")).to.be.true; + expect(playWithVideoJS.calledWithExactly("ws://example.com:5080/stream123.webrtc", "webrtc", undefined)).to.be.true; }); @@ -726,71 +725,111 @@ describe("WebPlayer", function() { it("handleWebRTCInfoMessages", 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 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); 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 infos = { info: "ice_connection_state_changed", obj: { state: "completed" } - } + }; + expect(player.iceConnected).to.be.false; player.handleWebRTCInfoMessages(infos); expect(player.iceConnected).to.be.true; - + infos = { info: "ice_connection_state_changed", obj: { state: "failed" } - } + }; player.handleWebRTCInfoMessages(infos); - + expect(tryNextTech.calledOnce).to.be.true; - + infos = { info: "closed", - - } + }; player.handleWebRTCInfoMessages(infos); - + expect(tryNextTech.calledTwice).to.be.true; - + await player.playIfExists("webrtc"); - + expect(player.videojsPlayer).to.not.be.null; - + var pauseMethod = sinon.replace(player.videojsPlayer, "pause", sinon.fake()); var playMethod = sinon.replace(player.videojsPlayer, "play", sinon.fake()); - + infos = { info: "resolutionChangeInfo", - } - + }; + player.handleWebRTCInfoMessages(infos); - + expect(pauseMethod.calledOnce).to.be.true; expect(playMethod.calledOnce).to.be.false; - - + clock.tick(2500); - + expect(pauseMethod.calledOnce).to.be.true; expect(playMethod.calledOnce).to.be.true; + + infos = { + info: "ice_connection_state_changed", + obj: { + state: "completed" + } + }; + player.backupStreamPlayerInterval = 1; + + player.handleWebRTCInfoMessages(infos); + + infos = { + info: "ice_connection_state_changed", + obj: { + state: "connected" + } + }; + + 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; + + infos = { + info: "ice_connection_state_changed", + obj: { + state: "failed" + } + }; + + player.handleWebRTCInfoMessages(infos); + + expect(isBackupStreamEnabledSpy.calledTwice).to.be.true; + expect(isBackupStreamEnabledSpy.returned(true)).to.be.true; - + expect(startBackupStreamPlayerInterval.calledOnce).to.be.true; }); it("testAutoPlay",async function(){ @@ -1103,8 +1142,312 @@ describe("WebPlayer", function() { expect(player.playerListener.calledWith('ratechange', sinon.match.any, { playbackRate: 1.0 })).to.be.true; }) - - + + 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"); + var placeHolder = document.createElement("div"); + + var streamId = "stream123"; + var backupStreamId = "backupStreamId"; + + var wsUrl = "ws://example.com/" + streamId + ".webrtc"; + var backupWsUrl = "ws://example.com/" + backupStreamId + ".webrtc"; + + 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 vjsMock = { + play: sinon.fake.resolves(), // Mock play to return a resolved promise + muted: sinon.fake(), // Mock muted method + controls: true, + class: 'video-js vjs-default-skin vjs-big-play-centered', + preload: "auto", + autoplay: true, + poster: "test", + liveui: true, + liveTracker: { + trackingThreshold: 0 + }, + html5: { + vhs: { + limitRenditionByPlayerDimensions: false + } + }, + on: sinon.fake(), + src: sinon.fake() + }; + + const mockVideoJS = sinon.stub(window, 'videojs').returns(vjsMock); + + sinon.replace(vjsMock, "muted", sinon.fake()); + + player.websocketURL = wsUrl; + player.streamId = streamId; + player.backupStreamId = backupStreamId; + + var replaceStreamIdWithBackupStreamIdSpy = sinon.spy(player, "replaceStreamIdWithBackupStreamId"); + var playWithVideoJSSpy = sinon.spy(player, "playWithVideoJS"); + + await player.playIfExists("webrtc", true); + + expect(playWithVideoJSSpy.calledOnce).to.be.true; + expect(replaceStreamIdWithBackupStreamIdSpy.calledOnce).to.be.true; + + expect(replaceStreamIdWithBackupStreamIdSpy.returnValues[0]).to.equal("ws://example.com/" + backupStreamId + ".webrtc"); + + expect(mockVideoJS.calledWith(WebPlayer.VIDEO_PLAYER_ID)).to.be.true; + + expect(vjsMock.src.calledOnce).to.be.true; + expect(vjsMock.src.calledWith({ + src: backupWsUrl, // Check if 'src' was called with the expected WebSocket URL + type: 'video/webrtc', + withCredentials: true, + iceServers: player.iceServers, + reconnect: false + })).to.be.true; + + }); + + + it("tryBackupStream", 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); + 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() { + 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); + 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()); + + + 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 + + await player.playIfExists("hls", playBackupStream) + await player.playIfExists("dash", playBackupStream) + + expect(isBackupStreamEnabledSpy.calledTwice).to.be.true; + expect(startBackupStreamPlayerInterval.calledTwice).to.be.true; + }) + + it("handleWebRTCErrorMessages", 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); + 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 errors = { + error: "no_stream_exist", + }; + + player.backupStreamId = "backupStreamId"; + player.playOrder = ["webrtc"]; + player.backupStreamTryCount = 1; + player.maxBackupStreamTryCount = 5; + + + player.handleWebRTCErrorMessages(errors); + + expect(isBackupStreamEnabledSpy.calledOnce).to.be.true; + expect(isBackupStreamEnabledSpy.returned(true)).to.be.true; + expect(startBackupStreamPlayerInterval.calledOnce).to.be.true; + + var errors = { + error: "no_stream_exist", + }; + + player.backupStreamTryCount = 5; + player.maxBackupStreamTryCount = 5; + + player.handleWebRTCErrorMessages(errors); + + expect(tryNextTech.calledOnce).to.be.true; + + var errors = { + error: "notSetRemoteDescription", + }; + + player.handleWebRTCErrorMessages(errors); + + expect(playIfExists.calledWith("hls")).to.be.true; + + }); + }); From 3c4cfcd007f7c36ef22ed60f7159aca2e1c43e37 Mon Sep 17 00:00:00 2001 From: lastpeony Date: Mon, 18 Nov 2024 19:19:21 +0300 Subject: [PATCH 2/2] 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 +});