diff --git a/src/web_player.js b/src/web_player.js index 9a008b7..acf84b9 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; /** @@ -186,10 +187,10 @@ export class WebPlayer { */ tryNextTechTimer; - /** + /** * Listener for ID3 text data */ - id3Listener; + id3Listener; /** * REST API Filter JWT @@ -220,6 +221,19 @@ export class WebPlayer { * Is IP Camera */ isIPCamera; + + /** + * Stream id of backup stream. + */ + backupStreamId; + + /** + * 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. + */ + triedBackupStream; constructor(configOrWindow, containerElement, placeHolderElement) { @@ -244,7 +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']; - // Initialize default values this.setDefaults(); @@ -410,6 +423,8 @@ export class WebPlayer { this.restAPIPromise = null; this.isIPCamera = false; this.playerEvents = WebPlayer.PLAYER_EVENTS + this.backupStreamId = null + this.triedBackupStream = false; } initializeFromUrlParams() { @@ -560,17 +575,54 @@ export class WebPlayer { } } + isBackupStreamEnabled(){ + return this.backupStreamId && this.playOrder.length === 1 && !this.triedBackupStream + } + + handleWebRTCErrorMessages(errors){ + if(errors["error"] === "no_stream_exist" && this.isBackupStreamEnabled()){ + this.tryBackupStream(); + } + 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") { 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.tryBackupStream(); + }else{ + Logger.warn("tryNextTech to replay"); + this.tryNextTech(); + } } } @@ -589,13 +641,24 @@ export class WebPlayer { } } + 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; + } + /** * 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 +691,16 @@ export class WebPlayer { return; } - var preview = this.streamId; - if (this.streamId.endsWith("_adaptive")) { + var streamId = this.streamId; + if(playBackupStream){ + streamId = this.backupStreamId; + if(extension === "webrtc"){ + streamUrl = this.replaceStreamIdWithBackupStreamId(streamUrl) + } + } + + var preview = streamId; + if (streamId.endsWith("_adaptive")) { preview = streamId.substring(0, streamId.indexOf("_adaptive")); } @@ -649,7 +720,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 +729,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 +737,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 +1037,12 @@ export class WebPlayer { return streamPath; } + tryBackupStream(){ + console.log("Trying to play backup stream.") + var playBackupStream = true; + this.playIfExists(this.currentPlayType, playBackupStream); + } + /** * try next tech if current tech is not working */ @@ -1018,7 +1075,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 +1146,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 +1158,19 @@ export class WebPlayer { }); this.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ERROR, (event) => { Logger.warn("dash playback error: " + event); - this.tryNextTech(); + if(playBackupStream){ + this.tryBackupStream() + }else{ + this.tryNextTech(); + } }); this.dashPlayer.on(dashjs.MediaPlayer.events.ERROR, (event) => { Logger.warn("error: " + event); - this.tryNextTech(); + if(playBackupStream){ + this.tryBackupStream() + }else{ + this.tryNextTech(); + } }); this.dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_NOT_ALLOWED, (event) => { @@ -1189,7 +1254,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(); @@ -1197,76 +1263,90 @@ export class WebPlayer { this.containerElement.innerHTML = this.videoHTMLContent; + if(playBackupStream){ + this.triedBackupStream = true + }else{ + this.triedBackupStream = false + } - Logger.warn("Try to play the stream " + this.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": //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) => { + 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.tryBackupStream() + }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) => { + 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.tryBackupStream() + }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) => { 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.tryBackupStream() + }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); @@ -1275,6 +1355,7 @@ export class WebPlayer { }); } + } /** @@ -1605,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 ee7f44e..60ac9cf 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,110 @@ 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); + + player.triedBackupStream = false; + var tryNextTech = sinon.replace(player, "tryNextTech", sinon.fake.returns(Promise.resolve(""))); + var isBackupStreamEnabledSpy = sinon.spy(player, "isBackupStreamEnabled"); // Use spy here + var tryBackupStream = sinon.replace(player, "tryBackupStream", 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.handleWebRTCInfoMessages(infos); + + infos = { + info: "ice_connection_state_changed", + obj: { + state: "connected" + } + }; + + player.handleWebRTCInfoMessages(infos); + + + // Set up player properties to ensure isBackupStreamEnabled returns true + player.backupStreamId = "backupStreamId"; + player.playOrder = ["webrtc"]; // Ensure playOrder length is 1 + player.triedBackupStream = false + + 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(tryBackupStream.calledOnce).to.be.true; }); it("testAutoPlay",async function(){ @@ -1103,12 +1141,293 @@ describe("WebPlayer", function() { expect(player.playerListener.calledWith('ratechange', sinon.match.any, { playbackRate: 1.0 })).to.be.true; }) - - -}); + 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 playIfExists = sinon.replace(player, "playIfExists", sinon.fake()); + + player.tryBackupStream(); + + sinon.assert.calledWith(playIfExists, "webrtc", true) + + }); + + it("playIfExists backup stream hls, ll-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 isBackupStreamEnabledSpy = sinon.spy(player, "isBackupStreamEnabled"); + var tryNextTech = sinon.replace(player, "tryNextTech", sinon.fake()); + + sinon.replace(player, "checkStreamExistsViaHttp", sinon.fake.rejects(new Error("Stream not found"))); + + var playBackupStream = true; + + player.backupStreamId = "backupStreamId" + + //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.calledThrice).to.be.true; + expect(isBackupStreamEnabledSpy.alwaysReturned(false)) + expect(tryNextTech.calledThrice).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 tryBackupStream = sinon.replace(player, "tryBackupStream", sinon.fake()); + + var errors = { + error: "no_stream_exist", + }; + + player.backupStreamId = "backupStreamId"; + player.playOrder = ["webrtc"]; + player.triedBackupStream = false; + + + player.handleWebRTCErrorMessages(errors); + expect(isBackupStreamEnabledSpy.calledOnce).to.be.true; + expect(isBackupStreamEnabledSpy.returned(true)).to.be.true; + expect(tryBackupStream.calledOnce).to.be.true; - - \ No newline at end of file + player.triedBackupStream = true; + + var errors = { + error: "no_stream_exist", + }; + + + player.handleWebRTCErrorMessages(errors); + + expect(tryNextTech.calledOnce).to.be.true; + + var errors = { + error: "notSetRemoteDescription", + }; + + player.handleWebRTCErrorMessages(errors); + + expect(playIfExists.calledWith("hls")).to.be.true; + + }); + + 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; + + }); + +});