const util = require('./util'), emitter = require('events').EventEmitter, sysutil = require('util'); function FGFM(config) { // Set up initial state this.config = config.config; this.obs = config.obs; this.state = { showStatus: 'IDLE', videoQueue: [], recentlyPlayed: [], currentVideo: null, videoTimer: null, lastCommercialShownAt: Date.now(), commercialPlaying: false }; emitter.call(this); this.startingSoon = (streamStartDelaySeconds, showStartDelaySeconds) => { // @TODO: Move these defaults to config if (typeof streamStartDelaySeconds === 'undefined') { streamStartDelaySeconds = 1; } if (typeof showStartDelaySeconds === 'undefined') { showStartDelaySeconds = 300; } this.state.showStatus = 'STARTING_SOON'; // Set up the initial queue by randomly choosing the configured amount of vods included in shuffling this.state.videoQueue = this.config.vods.alttp.filter(e => e.includeInShuffle === true).sort(util.randSort).slice(0, this.config.initialQueueSize); // Show the starting-soon scene this.obs.switchToScene('starting-soon'); // Restore volume this.obs.setVolume('headphones', 1.0); // Start the stream console.log(`The stream will start in ${streamStartDelaySeconds} seconds!`); setTimeout(() => {this.obs.startStream().then(() => {this.emit('STREAM_STARTED')})}, streamStartDelaySeconds*1000); // @TODO: Actually show the countdown in the scene console.log(`The show will start in ${showStartDelaySeconds} seconds!`); setTimeout(this.startTheShow, showStartDelaySeconds*1000); }; // Set up initial queue + start playback this.startTheShow = () => { // Set up the initial queue by randomly choosing the configured amount of vods included in shuffling this.state.videoQueue = this.config.vods.alttp.filter(e => e.includeInShuffle === true).sort(util.randSort).slice(0, this.config.initialQueueSize); // Start queue playback this.state.currentVideo = this.state.videoQueue.shift(); this.showVideo(this.state.currentVideo); // restore volume this.obs.setVolume('headphones', 1.0); this.state.showStatus = 'RUNNING'; this.emit('SHOW_STARTED'); }; this.endTheShow = (creditsDelaySeconds, endDelaySeconds) => { if (typeof creditsDelaySeconds === 'undefined' || creditsDelaySeconds === false) { creditsDelaySeconds = 0; } if (typeof endDelaySeconds === 'undefined' || endDelaySeconds === false) { endDelaySeconds = 60; } console.log(`Credits will be shown in ${creditsDelaySeconds} seconds!`); this.emit('SHOW_ENDING', creditsDelaySeconds); let end = () => { this.state.showStatus = 'ENDING'; // Hide current video, don't play next video this.obs.hide(this.state.currentVideo.sceneItem, this.config.defaultSceneName) clearTimeout(this.state.videoTimer); this.obs.switchToScene('credits') .then(() => { this.emit('CREDITS_SHOWN', endDelaySeconds); if (endDelaySeconds < 5) endDelaySeconds = 5; console.log(`Stream will stop in ${endDelaySeconds} seconds`); let fadeOutDelay = endDelaySeconds - 5; // Fade out volume with 5 seconds left setTimeout(() => { this.obs.getVolume('headphones') .then(currentVolume => { console.log(`current volume of headphones: ${currentVolume}`); let step = 0.1; while (currentVolume > 0.1) { currentVolume -= step; console.log(`setting volume to: ${currentVolume}`); this.obs.setVolume('headphones', currentVolume); util.sleep(250); } }) .catch(console.error); }, fadeOutDelay*1000); setTimeout(() => { this.obs.stopStream(); this.state.showStatus = 'ENDED'; this.emit('SHOW_ENDED'); }, endDelaySeconds*1000); }) .catch(console.error); }; if (creditsDelaySeconds > 0) { setTimeout(end, creditsDelaySeconds*1000); } else { end(); } }; // Shows.. a... video this.showVideo = video => { console.log(`Showing video: ${video.chatName}`); this.obs.playVideoInScene(video, this.config.defaultSceneName, this.nextVideo) .then(timer => { // track timer so we can cancel callback later on if necessary this.state.videoTimer = timer; // update activity label and show/hide appropriately if (video.hasOwnProperty('label') && video.label !== false) { this.obs.showActivity(video.label); } else { this.obs.hideActivity(); } }) .catch(console.error); }; // Adds a gameplay vod to the queue this.addVideo = video => { return this.state.videoQueue.push(video); }; // Adds a room to the queue and handles looping setup this.addRoomVideo = (room, loop) => { let loops = 1; if (typeof loop === 'undefined' || loop === true) { loops = Math.floor(this.config.roomVidPlaytime / room.videoData.length); } console.log(`Adding room video for ${room.dungeonName} - ${room.roomName} to the queue (${loops} loops)`); let video = { filePath: `${this.config.roomVidsBasePath}${room.winPath}`, sceneItem: (room.videoData.width === 960) ? "4x3ph" : "16x9ph", length: room.videoData.length, label: room.roomName, chatName: room.roomName, loops: loops, requestedBy: room.requestedBy }; this.state.videoQueue.push(video); }; // Picks the next video in the queue (shuffles if empty) // Also handles "commercial breaks" if enabled this.nextVideo = () => { // @TODO: Validate this.state.showStatus -- make sure the "show" hasn't been paused or stopped let ignoreStates = ['ENDING', 'ENDED', 'PAUSED']; if (ignoreStates.includes(this.state.showStatus)) { return; } // Show a "commercial break" if it's been long enough since the last one let secondsSinceLastCommercial = (Date.now() - this.state.lastCommercialShownAt) / 1000; if (this.config.commercialsEnabled === true && secondsSinceLastCommercial >= this.config.commercialInterval) { console.log(`It has been ${secondsSinceLastCommercial} seconds since the last commercial break!`); // Random chance for it to be "everybody wow" let memeId = false; if ((Math.floor(Math.random() * 100) + 1) <= this.config.auwChance) { console.log(`Showing AUW!`); memeId = 'auw'; } this.showMeme(memeId) .then(() => { this.state.lastCommercialShownAt = Date.now(); this.nextVideo(); }) .catch(console.error); return; } // Keep track of recently played videos if (this.state.recentlyPlayed.length === this.config.recentlyPlayedMemory) { this.state.recentlyPlayed.shift(); } this.state.recentlyPlayed.push(this.state.currentVideo.id); // If a commercial is playing, wait until it's done to switch while (this.state.commercialPlaying === true) {} // play the next video in the queue, or pick one at random if the queue is empty if (this.state.videoQueue.length > 0) { this.state.currentVideo = this.state.videoQueue.shift(); } else { // Random chance for room grind to be played for an amount of time instead of another video be shuffled to if ((Math.floor(Math.random() * 100) + 1) <= this.config.roomGrindChance) { console.log(`Room grind selected!`); // show room-grind source this.obs.showRoomGrind(this.config.roomGrindPlaytime, () => {this.nextVideo()}) .then(timer => { this.state.videoTimer = timer; }) .catch(console.error); return; } // Random chance for room videos to be added if ((Math.floor(Math.random() * 100) + 1) <= this.config.roomShuffleChance) { console.log(`Room vids selected!`); this.addRoomVideo(this.config.rooms.sort(util.randSort).slice(0, 1).shift()); // play the first one this.state.currentVideo = this.state.videoQueue.shift(); } else { // filter recently played from shuffle let freshVods = this.config.vods.alttp.filter(e => { return e.includeInShuffle === true && !this.state.recentlyPlayed.includes(e.id); }); this.state.currentVideo = freshVods.sort(util.randSort).slice(0, 1).shift(); } } this.showVideo(this.state.currentVideo); }; // "Commercials" this.showCommercial = (video, callback) => { return new Promise((resolve, reject) => { let handleFinish = () => { console.log('commercial is finished playing...'); this.state.commercialPlaying = false; if (typeof callback !== 'undefined') callback(); } this.obs.playVideoInScene(video, this.config.commercialSceneName, handleFinish) .then(timer => { this.state.commercialPlaying = true; resolve(timer); }) .catch(reject); }); }; // Memes-By-Id this.showMeme = id => { return new Promise((resolve, reject) => { // find the vod in memes let video = this.config.vods.memes.find(e => e.id === id); if (!video) { reject(`No meme found matching ID ${id}`); } let handleFinish = () => { if (id === 'auw') { this.obs.hide("owen", this.config.commercialSceneName); } resolve(); }; this.showCommercial(video, handleFinish) .then(videoHasStarted => { // in the case of 'auw', show owen if (id === 'auw') { this.obs.show("owen", this.config.commercialSceneName); } }) .catch(console.error); }); }; // Skip the current video and play the next this.skip = () => { clearTimeout(this.state.videoTimer); this.obs.hide(this.state.currentVideo.sceneItem, this.config.defaultSceneName).then(this.nextVideo).catch(console.error); }; // Clears.. the... queue this.clearQueue = () => { this.state.videoQueue = []; }; this.pause = () => { this.state.showStatus = 'PAUSED'; this.emit('SHOW_PAUSED'); }; this.resume = () => { this.state.showStatus = 'RUNNING'; this.nextVideo(); this.emit('SHOW_RESUMED'); }; } sysutil.inherits(FGFM, emitter); module.exports = FGFM;