13 Commits

Author SHA1 Message Date
Chris Ham
6e36e06ce9 case-insensitive commands, ttas vods, start/stop delay support 2018-11-13 17:17:23 -08:00
Chris Ham
fd75d11eec director events 2018-11-07 00:41:28 -08:00
Chris Ham
d01439cf15 vrmode timer, user skip voting 2018-10-30 19:12:57 -07:00
Chris Ham
b86e2f102f clear video timer on stream ending 2018-10-28 10:53:16 -07:00
Chris Ham
f3a4159823 auto fade out 2018-10-27 09:50:05 -07:00
Chris Ham
886a5f8b27 todo update 2018-10-23 17:23:59 -07:00
Chris Ham
8e92d37338 update msnoeg vid 2018-10-23 17:23:33 -07:00
Chris Ham
159d5a328c fix timer resolution 2018-10-23 11:52:26 -07:00
Chris Ham
2476e6a721 Merge branch 'master' into modularize
* master:
  rename auw
  vid updates
  notes
2018-10-22 11:08:30 -07:00
Chris Ham
c3da69bf48 rename auw 2018-10-22 10:58:19 -07:00
Chris Ham
0f030992bd vid updates 2018-10-20 10:24:28 -07:00
Chris Ham
eba9e7ef2f notes 2018-10-13 09:12:07 -07:00
Chris Ham
617f176f5f erp 2018-10-05 09:52:10 -07:00
11 changed files with 523 additions and 90 deletions

View File

@@ -12223,5 +12223,65 @@
"fps": 60,
"length": 48.933984
}
},
{
"id": 817,
"shortPath": "/comparisons/desert-exit.mp4",
"winPath": "\\comparisons\\desert-exit.mp4",
"dungeonId": "04",
"dungeonName": "hera",
"roomId": "01",
"roomName": "desert-exit-comparisons",
"videoData": {
"width": 960,
"height": 720,
"fps": 60,
"length": 14.183
}
},
{
"id": 818,
"shortPath": "/comparisons/bnc.mp4",
"winPath": "\\comparisons\\bnc.mp4",
"dungeonId": "01",
"dungeonName": "escape",
"roomId": "20",
"roomName": "bnc-comparisons",
"videoData": {
"width": 960,
"height": 720,
"fps": 60,
"length": 12.27
}
},
{
"id": 819,
"shortPath": "/comparisons/escape-1st-keyguard.mp4",
"winPath": "\\comparisons\\escape-1st-keyguard.mp4",
"dungeonId": "01",
"dungeonName": "escape",
"roomId": "11",
"roomName": "1st-keyguard-comparisons",
"videoData": {
"width": 960,
"height": 720,
"fps": 60,
"length": 5.78
}
},
{
"id": 820,
"shortPath": "/comparisons/pod-arrow-statue-dash-vs-walk.mp4",
"winPath": "\\comparisons\\pod-arrow-statue-dash-vs-walk.mp4",
"dungeonId": "06",
"dungeonName": "pod",
"roomId": "30",
"roomName": "pod-arrow-statue-comparisons",
"videoData": {
"width": 960,
"height": 720,
"fps": 60,
"length": 24.52
}
}
]

7
conf/timers.json Executable file
View File

@@ -0,0 +1,7 @@
[
{
"name": "vr",
"interval": 1800,
"value": "Video and room requests are on! Use $vr <video-id> to request a video from this list [https://pastebin.com/qv0wDkvB] or $room <room-id> to request a specific room (looped for a few minutes) from this list [https://goo.gl/qoNmuH]"
}
]

View File

@@ -1,5 +1,16 @@
{
"alttp": [
{
"id": "ttas",
"category": "Optimal TTAS",
"label": "Any% NMG Optimal Theory TAS [1:20:38.72]",
"name": "OTTAS Full",
"chatName": "OTTAS Full",
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\Risky\\full-risky-raw.mp4",
"sceneItem": "16x9ph",
"length": 4843,
"includeInShuffle": true
},
{
"id": "ot-seg-escape",
"category": "Optimal TTAS Segment",
@@ -154,6 +165,17 @@
"length": 108,
"includeInShuffle": true
},
{
"id": "rta-ttas",
"category": "Safe TTAS",
"label": "Any% NMG Safe/RTA Theory TAS [1:21:26.52]",
"name": "STTAS Full",
"chatName": "STTAS Full",
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\ttas\\RTA\\full-rta-raw.mp4",
"sceneItem": "16x9ph",
"length": 4892,
"includeInShuffle": true
},
{
"id": "st-seg-escape",
"category": "Safe TTAS Segment",
@@ -410,12 +432,12 @@
{
"id": "nmg-gold-mire",
"category": "Any% NMG Gold Segment",
"label": "Any% NMG Gold Segment: Misery Mire (7:07.17) [2018-06-10]",
"label": "Any% NMG Gold Segment: Misery Mire (7:06.58) [2018-10-21]",
"name": "Mire (NMG Gold)",
"chatName": "Mire (NMG Gold)",
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\timed\\11-[707.17]-2018-06-10-mire.mp4",
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\gold-segments\\any%-nmg-nsq\\720p\\11-[706.58]-2018-10-21-mire.mp4",
"sceneItem": "4x3ph",
"length": 432,
"length": 430,
"includeInShuffle": true
},
{
@@ -460,7 +482,7 @@
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\100%-mg-ahp\\2017-12-22-100mg-11912.mp4",
"sceneItem": "16x9ph",
"length": 4786,
"includeInShuffle": false
"includeInShuffle": true
},
{
"id": "pb-ab",
@@ -471,18 +493,18 @@
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\all-bosses\\2017-11-20-ab-10923.mp4",
"sceneItem": "16x9ph",
"length": 4200,
"includeInShuffle": false
"includeInShuffle": true
},
{
"id": "pb-ad",
"category": "Personal Best",
"label": "Personal Best: All Dungeons No EG/DW/WW (1:14:59) [2017-11-19]",
"label": "Personal Best: All Dungeons No EG/DG/WW (1:14:59) [2017-11-19]",
"name": "All Dungeons (PB)",
"chatName": "All Dungeons (PB)",
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\all-dungeons\\2017-11-19-ad-11459.mp4",
"sceneItem": "16x9ph",
"length": 4555,
"includeInShuffle": false
"includeInShuffle": true
},
{
"id": "pb-any-nmg",
@@ -493,17 +515,17 @@
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\any%-nmg-nsq\\2018-05-27-nmg-12624.mp4",
"sceneItem": "16x9ph",
"length": 5190,
"includeInShuffle": false
"includeInShuffle": true
},
{
"id": "pb-any-no-eg",
"category": "Personal Best",
"label": "Personal Best: Any% No EG (30:28) [2018-09-09]",
"label": "Personal Best: Any% No EG (29:05) [2018-10-14]",
"name": "Any% No EG (PB)",
"chatName": "Any% No EG (PB)",
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\any%-no-eg\\2018-09-09-no-eg-3028.mp4",
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\any%-no-eg\\2018-10-14-no-eg-2905.mp4",
"sceneItem": "16x9ph",
"length": 1833,
"length": 1777,
"includeInShuffle": true
},
{
@@ -526,7 +548,7 @@
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\mirror-shield\\2017-06-20-mirror-shield-5032.mp4",
"sceneItem": "16x9ph",
"length": 3068,
"includeInShuffle": false
"includeInShuffle": true
},
{
"id": "pb-ms-no-eg",
@@ -534,8 +556,8 @@
"label": "Personal Best: Mirror Shield No EG (11:38) [2017-07-02]",
"name": "Mirror Shield No EG (PB)",
"chatName": "Mirror Shield No EG (PB)",
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\mirror-shield-no-eg\\2017-07-02-mirror-shield-no-eg-1138.mp4",
"sceneItem": "16x9ph",
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\mirror-shield-no-eg\\2017-07-02-mirror-shield-no-eg-1138-resized.mp4",
"sceneItem": "4x3ph",
"length": 738,
"includeInShuffle": true
},
@@ -548,7 +570,7 @@
"filePath": "Y:\\media\\videos\\ALttP\\my-vids\\personal-bests\\rbo\\2017-12-01-rbo-11813.mp4",
"sceneItem": "16x9ph",
"length": 4725,
"includeInShuffle": false
"includeInShuffle": true
},
{
"id": "pb-100-nmg",

View File

@@ -1,35 +1,75 @@
TODO:
✔ Add cooldowns @done (18-10-02 10:16)
Move anything that calls director.state from app into fgfm lib
☐ Handle socket disconnect
Start/stop stream automation
☐ Support scheduled start/stop
- instead of countdown for X seconds, use datetime argument
☐ Start
- Countdown for X minutes is triggered and shown
☐ Decouple twitch chat from GHOBS
☐ Move anything that calls director.state from app into fgfm lib
☐ Restrict # of requests a user can have in the queue at once
☐ Move vrmode timer to this bot, delete from SLCB
☐ Support a CLI flag to delay showing queue until command is issued
☐ Room vid requests / import
☐ Improved interface for viewer requests
☐ Web interface? Twitch extension?
☐ Improvements
☐ When playing a room back, loop it at slower speeds for a few iterations
☐ Support viewer skip voting
☐ Remove currently playing video from vote choices
☐ Command to add sets of videos to the queue at once (like the entire ttas or all gold segments)
☐ Command to stop video rotation / timers (shutdown)
☐ Start/stop stream automation
☐ Start
- Starting Soon is shown until countdown is triggered
- Countdown for X minutes is triggered and shown
- Once countdown finishes, switch to intro scene and play
- Switch to fgfm once intro finishes
☐ Support for $pause -- not sure how to pull this off
☐ Support adding sets of videos to the queue at once (like the entire ttas or all gold segments)
☐ Tool to output list of video ID's / descriptions
☐ Stream alerts for chat
☐ Rotating background images (leftside)
☐ Support gif's via command through gifph
- !slowdance
- !bender
- !carlton
- !weeb
☐ Fix commercial playing issue (switches back to scene early)
☐ Change $auw and $meme to queue up the videos just like the others (don't switch scenes)
Ideas:
☐ Web interface for viewers to issue commands -- twitch extension?!
☐ Support songrequests -- play through discord?
StreamWebRemote:
- OBS Websocket Connection Mgmt
- Stream Start/Stop/Restart
- Video Queue Mgmt
- Video DB Mgmt
Users:
- use twitch login
- video requests
- room requests
___________________
Archive:
✔ Stop @done (18-11-06 21:54) @project(TODO)
✔ video starting/ending/skipped @done (18-11-06 21:53) @project(TODO)
✔ show status changing @done (18-11-06 21:53) @project(TODO)
✔ Have director emit events for bots to listen to @done (18-11-06 21:53) @project(TODO)
✔ Auto-enable vrmode timer when show starts (listen for event) @done (18-11-06 21:53) @project(TODO)
✔ hide sources before switching to credits @done (18-11-06 21:41) @project(TODO)
✔ Support a delay for Start Streaming @done (18-11-06 21:41) @project(TODO)
✔ Add a $rooms alias for $room @done (18-11-06 15:05) @project(TODO)
✔ Remove currently playing video from vote choices @done (18-10-30 11:51) @project(TODO)
✔ track votes for the current playing video @done (18-10-30 11:48) @project(TODO)
✔ if threshold is met, skip @done (18-10-30 11:48) @project(TODO)
✔ config item for how many votes are required to skip @done (18-10-30 11:48) @project(TODO)
✔ clear votes after video finishes @done (18-10-30 11:48) @project(TODO)
✔ Support viewer $skip voting @done (18-10-30 11:48) @project(TODO)
✔ update director showStatus to 'PAUSED' @done (18-10-30 11:33) @project(TODO)
✔ have nextVideo check for PAUSED @done (18-10-30 11:33) @project(TODO)
✔ Support for $pause (pauses queue after current video finishes) @done (18-10-30 11:33) @project(TODO)
✔ Move vrmode timer to this bot, delete from SLCB @done (18-10-30 11:20) @project(TODO)
✔ Don't auto-init GHOBS or FGFM, make them on-demand @done (18-10-30 08:26) @project(TODO)
✔ Switch to credits with 1 minute remaining @done (18-10-30 08:15) @project(TODO)
✔ Stop Stream @done (18-10-30 08:15) @project(TODO)
✔ Stop @done (18-10-30 08:15) @project(TODO)
✔ Parameter for how long until the stream should end @done (18-10-30 08:15) @project(TODO)
✔ Fade out audio sources with 5 seconds left @done (18-10-30 08:15) @project(TODO)
✔ Add parameter for countdown @done (18-10-30 08:15) @project(TODO)
✔ Set up the queue upon init so it can be managed during startup @done (18-10-30 08:14) @project(TODO)
✔ Starting Soon is shown until countdown is triggered @done (18-10-26 09:44) @project(TODO)
✔ Start stream @done (18-10-26 09:44) @project(TODO)
✔ Add cooldowns @done (18-10-02 10:16) @project(TODO)
✔ video length @done (18-09-28 09:33) @project(TODO)
✔ root folder name @done (18-09-28 09:33) @project(TODO)
✔ room ID @done (18-09-28 09:33) @project(TODO)

150
fgfm.js
View File

@@ -6,6 +6,7 @@
const irc = require('irc');
const schedule = require('node-schedule');
const md5 = require('md5');
const moment = require('moment');
// Import local packages
const GHOBS = require('./lib/ghobs');
@@ -18,6 +19,10 @@ let config = require('./config.json');
config.vods = require(config.vodConfigFile);
config.rooms = require(config.roomConfigFile);
let snesGames = require('./conf/snesgames.json');
let timersList = require('./conf/timers.json');
let activeTimers = [];
let skipVote = {target: null, count: 0};
// Main screen turn on
const obs = new GHOBS(config);
@@ -73,12 +78,49 @@ const streamInit = (config, twitch) => {
// All your comfy are belong to us
const director = new FGFM({config: config, obs: obs});
director.init();
// Handle show events from the director
director.on('SHOW_STARTED', () => {
// Enable vrmode timer
manageTimer('vr', 'on');
});
director.on('SHOW_ENDING', (secondsUntilCredits) => {
// Disable vrmode timer
manageTimer('vr', 'off');
// Let the chat know the stream is ending soon
twitch.botChat.say(config.twitch.channel, `The stream will be ending in ${parseFloat(secondsUntilCredits/60).toFixed(0)} minutes!`);
});
director.on('CREDITS_SHOWN', (secondsUntilEnd) => {
twitch.editorChat.say(config.twitch.channel, `Thanks to everyone for watching and lurking! Have a wonderful night and stay comfy. greenhComfy`);
});
// Chat commands
const commands = {
admin: {
init: (cmd) => {
let streamStartDelaySeconds = cmd.args[1] || 1;
let showStartDelaySeconds = cmd.args[2] || 300;
director.startingSoon(streamStartDelaySeconds, showStartDelaySeconds);
},
start: (cmd) => {
director.startTheShow();
},
end: (cmd) => {
let creditsDelay = cmd.args[1] || 1;
let endDelay = cmd.args[2] || 60;
director.endTheShow(creditsDelay, endDelay);
},
changevis: (cmd, newVisibility) => {
let sceneItem = command.args[1] || false;
if (!sceneItem) {
@@ -112,6 +154,23 @@ const streamInit = (config, twitch) => {
},
timer: (cmd) => {
let timerName = cmd.args[1] || false;
if (!timerName) {
twitch.botChat.say(cmd.to, `A timer name is required!`);
return;
}
let timerStatus = cmd.args[2] || false;
try {
manageTimer(timerName, timerStatus);
} catch (e) {
twitch.botChat.say(cmd.to, e);
}
},
auw: (cmd) => {
director.showMeme('auw');
},
@@ -156,6 +215,16 @@ const streamInit = (config, twitch) => {
},
showact: (cmd) => {
obs.showActivity().catch(console.error);
},
hideact: (cmd) => {
obs.hideActivity().catch(console.error);
},
add: (cmd) => {
// @TODO: DRY this out with the checks in vr
let requestedVideoId = cmd.args[1] || false;
@@ -193,6 +262,16 @@ const streamInit = (config, twitch) => {
},
pause: (cmd) => {
director.pause();
},
resume: (cmd) => {
director.resume();
},
clear: (cmd) => {
director.clearQueue();
},
@@ -347,10 +426,33 @@ const streamInit = (config, twitch) => {
rngames: (cmd) => {
twitch.botChat.say(cmd.to, snesGames.sort(util.randSort).slice(0, 10).join(' | '));
}
},
// voting to skip current video
skip: (cmd) => {
// check if there is an existing vote to skip for the director.state.currentVideo
if (skipVote.target === director.state.currentVideo.id) {
// if yes, add the vote, check if threshold is met, skip if necessary
skipVote.count++;
} else {
skipVote.target = director.state.currentVideo.id;
skipVote.count = 1;
}
if (skipVote.count >= config.skipVoteThreshold) {
director.skip();
skipVote.target = null;
}
},
}
};
// Aliases for chat commands
const aliases = {
"rooms": "room"
};
// Listen for the above commands
twitch.botChat.addListener('message', (from, to, message) => {
// Ignore everything from blacklisted users
@@ -372,6 +474,12 @@ const streamInit = (config, twitch) => {
// Ignore messages without a command
if (!key || key.length === 0) return;
// Case-insensitive
key.toLowerCase();
// Check for aliased commands
if (aliases.hasOwnProperty(key)) key = aliases[key];
// Ignore unrecognized commands
if (!commands.admin.hasOwnProperty(key) && !commands.user.hasOwnProperty(key)) return;
@@ -398,6 +506,42 @@ const streamInit = (config, twitch) => {
.catch(console.error);
});
const manageTimer = (timerName, timerStatus) => {
// search timers for matching name
let theTimerIndex = timersList.findIndex(e => e.name === timerName);
if (theTimerIndex === -1) {
throw("Invalid timer name!");
}
let theTimer = timersList[theTimerIndex];
// look in activeTimers for current status
let currentTimerIndex = activeTimers.findIndex(e => e.name === timerName);
if (!timerStatus || timerStatus !== 'on' || timerStatus !== 'off') {
// toggle by default
if (currentTimerIndex === -1) {
timerStatus = 'on';
} else {
timerStatus = 'off';
}
}
if (currentTimerIndex === -1 && timerStatus === 'on') {
let timerFunc = () => {
twitch.botChat.say(config.twitch.channel, theTimer.value);
};
let timerInterval = setInterval(timerFunc, theTimer.interval*1000);
activeTimers.push({name: theTimer.name, timer: timerInterval});
timerFunc();
} else if (timerStatus === 'off') {
clearInterval(activeTimers[currentTimerIndex].timer);
activeTimers.splice(currentTimerIndex, 1);
}
return;
}
// @TODO: Modularize timed events
//console.log(`Initializing stream timers...`);
let userVotes = currentChoices = [];
@@ -450,7 +594,7 @@ const streamInit = (config, twitch) => {
// choose more random videos from config.vods.alttp (that aren't already in the queue)
// @TODO: Move into FGFM
let vodsNotInQueue = config.vods.alttp.filter(e => {
let inQueue = director.state.videoQueue.findIndex(q => q.id === e.id) !== -1;
let inQueue = (director.state.videoQueue.findIndex(q => q.id === e.id) !== -1) && (director.state.currentVideo.id !== e.id);
return !inQueue;
});
currentChoices = vodsNotInQueue.sort(util.randSort).slice(0, config.videoPollSize);

View File

@@ -1,10 +1,13 @@
const util = require('./util');
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,
@@ -13,14 +16,118 @@ function FGFM(config) {
commercialPlaying: false
};
emitter.call(this);
this.startingSoon = (streamStartDelaySeconds, showStartDelaySeconds) => {
// @TODO: Move these defaults to config
if (typeof streamStartDelaySeconds === 'undefined') {
streamStartDelaySeconds = 1;
} else {
streamStartDelaySeconds = parseInt(streamStartDelaySeconds);
}
if (typeof showStartDelaySeconds === 'undefined') {
showStartDelaySeconds = 300;
} else {
showStartDelaySeconds = parseInt(showStartDelaySeconds);
showStartDelaySeconds += streamStartDelaySeconds;
}
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 after delay
console.log(`The stream will start in ${streamStartDelaySeconds} seconds!`);
setTimeout(() => {this.obs.startStream().then(() => {this.emit('STREAM_STARTED')})}, streamStartDelaySeconds*1000);
// Start the "show" after stream+show delay
// @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.init = () => {
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
@@ -71,6 +178,12 @@ function FGFM(config) {
// 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) {
@@ -142,6 +255,7 @@ function FGFM(config) {
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();
}
@@ -191,7 +305,20 @@ function FGFM(config) {
// 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;

View File

@@ -10,76 +10,100 @@ function GHOBS(config) {
this.websocket.connect({ address: this.config.obs.websocket.address, password: this.config.obs.websocket.password })
.then(() => {
console.log(`Success! We're connected to OBS!`);
this.websocket.getCurrentScene().then(res => this.currentScene = res.name);
this.websocket.onSwitchScenes(data => {
//console.log(`New Active Scene: ${data.sceneName}`);
this.currentScene = data.sceneName;
});
this.websocket.getCurrentScene().then(currentScene => this.currentScene = currentScene.name);
this.websocket.onSwitchScenes(newScene => this.currentScene = newScene.sceneName);
resolve();
})
.catch(reject);
// Listen for errors from OBS
// @TODO: Handle socket disconnect gracefully
/** { status: 'error',
description: 'There is no Socket connection available.',
code: 'NOT_CONNECTED',
error: 'There is no Socket connection available.' }*/
this.websocket.on('error', err => {
console.error(`OBS websocket error: ${JSON.stringify(err)}`);
});
});
};
// @TODO: pass any unrecognized commands to the websocket
this.startStream = () => {
return this.websocket.startStreaming();
};
this.stopStream = () => {
return this.websocket.stopStreaming();
};
this.setVolume = (source, volume) => {
return this.websocket.setVolume({source: source, volume: volume});
}
this.getVolume = (source) => {
return new Promise((resolve, reject) => {
this.websocket.getVolume({source: source})
.then(res => {
resolve(res.volume);
})
.catch(reject);
});
}
// Plays a video in the current scene and hides when finished
this.playVideo = (video, callback) => {
return new Promise((resolve, reject) => {
// @TODO Validation of video
// set the file path on the source
let sourceSettings = {
local_file: video.filePath,
looping: (typeof video.loops !== 'undefined' && video.loops > 1)
};
sourceSettings.loop = sourceSettings.looping;
this.websocket.setSourceSettings({"sourceName": video.sceneItem, "sourceSettings": sourceSettings})
// show the video scene item
.then(() => this.websocket.setSceneItemProperties({"item": video.sceneItem, "visible": true}))
// when the video is over, hide it and trigger the user callback, but resolve promise immediately with the timer
.then(() => {
// if this video is being looped, adjust timeout length to allow the requested number of loops to complete
if (sourceSettings.loop === true) {
video.length *= video.loops;
}
// resolve Promise with a timer of when the video will finish playback
// trigger user callback when the video finishes
let timer = setTimeout(() => {
this.websocket.setSceneItemProperties({"item": video.sceneItem, "visible": false});
if (typeof callback !== 'undefined') {
callback();
}
}, parseInt(video.length*1000));
resolve(timer);
})
.catch(reject);
});
}
// Shows a video in the given scene/item and then hides it and switches back to the original scene when finished
this.playVideoInScene = (video, scene, callback) => {
return new Promise((resolve, reject) => {
video.scene = scene;
let originalScene = this.currentScene || false;
//console.log(`Changing scene from ${originalScene} to ${scene}`);
let handleVideoEnd = () => {
if (originalScene !== false) {
this.websocket.setCurrentScene({"scene-name": originalScene});
}
if (typeof callback !== 'undefined') {
callback();
}
};
this.websocket.setCurrentScene({"scene-name": scene})
.then(res => {
// set the file path on the source
//console.log(`Setting file path to: ${video.filePath}`);
let sourceSettings = {
"local_file": video.filePath,
"looping": (typeof video.loops !== 'undefined' && video.loops > 1)
};
sourceSettings.loop = sourceSettings.looping;
// @TODO loop room vids at a slower speed for a few iterations
// @TODO support any sourceSetting?
//
/*{ close_when_inactive: true,
local_file: 'Y:\\media\\videos\\ALttP\\my-vids\\room-vids\\11-mire\\38-wizzpot-rta-hook-610.mp4',
loop: true,
looping: false,
restart_on_activate: false,
speed_percent: 100 }*/
//this.websocket.getSourceSettings({"sourceName": video.sceneItem}).then(console.log);
this.websocket.setSourceSettings({"sourceName": video.sceneItem, "sourceSettings": sourceSettings})
// show the video scene item
.then(() => this.websocket.setSceneItemProperties({"item": video.sceneItem, "scene-name": scene, "visible": true}))
// when the video is over, hide it and trigger the user callback, but resolve promise immediately with the timer
.then(data => {
// adjust timeout length to allow the requested number of loops to complete
if (sourceSettings.loop === true) {
video.length *= video.loops;
console.log(`Video is set to loop, adjusted length to ${video.length}`);
}
resolve(setTimeout(() => {
//console.log(`Hiding ${video.sceneItem}`);
this.websocket.setSceneItemProperties({"item": video.sceneItem, "scene-name": scene, "visible": false});
if (originalScene) {
//console.log(`Switching scene back to ${originalScene}`);
this.websocket.setCurrentScene({"scene-name": originalScene});
}
if (typeof callback !== 'undefined') {
//console.log('Triggering user callback');
callback(data);
}
}, parseInt(video.length*1000)))
});
})
.then(() => this.playVideo(video, handleVideoEnd))
.then(timer => { resolve(timer) })
.catch(reject);
});
};

View File

@@ -48,4 +48,13 @@ exports.average = function(e) {
return avg;
};
exports.randSort = () => { return 0.5 - Math.random() };
exports.randSort = () => { return 0.5 - Math.random() };
exports.sleep = (milliseconds) => {
var start = new Date().getTime();
for (var i = 0; i < 1e7; i++) {
if ((new Date().getTime() - start) > milliseconds){
break;
}
}
}

BIN
sfx/mouthfeel.mp3 Executable file

Binary file not shown.

BIN
sfx/wobbuffet.mp3 Executable file

Binary file not shown.