Skip to content

Commit

Permalink
Added bot audio capture
Browse files Browse the repository at this point in the history
  • Loading branch information
nicnacnic committed Jan 27, 2022
1 parent 7532f2e commit 016df82
Show file tree
Hide file tree
Showing 10 changed files with 1,681 additions and 235 deletions.
27 changes: 20 additions & 7 deletions configschema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,23 @@
"default": {
"address": "localhost:4444",
"password": "password",
"baseRtmlUrl": "https://rtmp.example.com/hls/"
"baseRtmpUrl": "https://rtmp.example.live/hls/",
"botToken": ""
},
"description": "The root schema comprises the entire JSON document.",
"examples": [
{
"address": "localhost:4444",
"password": "password",
"baseRtmlUrl": "https://rtmp.example.com/hls/"
"baseRtmpUrl": "https://rtmp.example.live/hls/",
"botToken": ""
}
],
"required": [
"address",
"password",
"baseRtmlUrl"
"baseRtmpUrl",
"botToken"
],
"title": "The root schema",
"type": "object",
Expand All @@ -42,14 +45,24 @@
"password"
]
},
"baseRtmlUrl": {
"$id": "#/properties/baseRtmlUrl",
"baseRtmpUrl": {
"$id": "#/properties/baseRtmpUrl",
"type": "string",
"title": "The baseRtmlUrl schema",
"title": "The baseRtmpUrl schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
"https://rtmp.example.com/hls/"
"https://rtmp.example.live/hls/"
]
},
"botToken": {
"$id": "#/properties/botToken",
"type": "string",
"title": "The botToken schema",
"description": "An explanation about the purpose of this instance.",
"default": "",
"examples": [
""
]
}
},
Expand Down
52 changes: 12 additions & 40 deletions dashboard/botSettings/botSettings.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
.select {
margin-bottom: 15px;
}

.input {
margin-bottom: 15px;
}
</style>
<button id="botActive" onClick="botSettings.value.active = !botSettings.value.active"></button>
<div class="select">
Expand All @@ -28,24 +32,17 @@
<label>Channel</label>
<div class="selectBorder"></div>
</div>
<div class="select">
<select id="device" onchange="botSettings.value.outputDevice = this.value;">
</select>
<label>Device</label>
<div class="selectBorder"></div>
</div>
<div class="select">
<select id="source" onchange="botSettings.value.source = this.value;">
</select>
<label>OBS Source</label>
<div class="selectBorder"></div>
<div class="input">
<input id="audioOffset" type="number" onChange="botSettings.value.audioOffset = this.value;">
</input>
<label>Audio Offset</label>
<div class="inputBorder"></div>
</div>
<script>
const botSettings = nodecg.Replicant('botSettings')
const audioSources = nodecg.Replicant('audioSources')

window.onload = () => {
NodeCG.waitForReplicants(botSettings, audioSources).then(() => {
NodeCG.waitForReplicants(botSettings).then(() => {
botSettings.on('change', (newVal, oldVal) => {
if (oldVal === undefined) {
switch (newVal.active) {
Expand All @@ -61,11 +58,8 @@
}
if (oldVal === undefined || Object.keys(newVal.channels).length !== Object.keys(oldVal.channels).length) updateChannels(newVal)
else if (newVal.channel !== oldVal.channel) document.getElementById('channel').value = newVal.channel;
if (oldVal === undefined || Object.keys(newVal.devices).length !== Object.keys(oldVal.devices).length) updateDevices(newVal)
else if (newVal.outputDevice !== oldVal.outputDevice) document.getElementById('device').value = newVal.outputDevice;
document.getElementById('audioOffset').value = newVal.audioOffset;
})

audioSources.on('change', (newVal) => updateSources(newVal))
})
}

Expand All @@ -79,31 +73,9 @@
channelList.value = newVal.channel;
}

function updateDevices(newVal) {
let deviceList = document.getElementById('device');
let sortedDevices = [];
for (let device in newVal.devices) { sortedDevices.push([device, newVal.devices[device]]) }
sortedDevices.sort((a, b) => { return a[1] - b[1]; });
deviceList.innerHTML = `<option value=-1>Default</option>`;
sortedDevices.forEach(device => deviceList.innerHTML = deviceList.innerHTML + `<option value=${device[0]}>${device[1]}</option>`);
deviceList.value = newVal.outputDevice;
}

function updateSources(newVal) {
const sourceTypes = ['wasapi_input_capture', 'wasapi_output_capture', 'pulse_input_capture', 'pulse_output_capture']
let sourceList = document.getElementById('source');
let sortedDevices = [];
newVal.forEach(source => {
if (sourceTypes.includes(source.type))
sourceList.innerHTML = sourceList.innerHTML + `<option>${source.name}</option>`;
})
sourceList.value = botSettings.value.outputSource;
}

function toggleDisabled(value) {
document.getElementById('channel').disabled = value;
document.getElementById('device').disabled = value;
document.getElementById('source').disabled = value;
document.getElementById('audioOffset').disabled = value;
}
</script>
</body>
Expand Down
2 changes: 1 addition & 1 deletion dashboard/welcome/welcome.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ <h2>Sync Your Races With StreamSync™</h2>
<h2>Automate (Almost) Everything</h2>
<p style="margin-top: -5px; font-size: 18px;">Make your restreamer's lives easier by automating (almost) everything! With the bundle, you can automatically change the layout in OBS and the stream key based on the current run. This requires additional setup in nodecg-speedcontrol, please view the <a style="color: #2196F3;" href="https://github.com/nicnacnic/nodecg-marathon-control/wiki" target="_blank">wiki</a> for more information.</p>
<h2>Need Help?</h2>
<p style="margin-top: -5px; font-size: 18px;">No problem! A full setup and user guide, as well as troubleshooting steps, can be found on the <a style="color: #2196F3;" href="https://github.com/nicnacnic/nodecg-marathon-control/wiki" target="_blank">wiki</a>. Additional support is also available on our <a style="color: #2196F3;" href="https://discord.com/invite/A34Qpfe" target="_blank">Discord</a> server.</p>
<p style="margin-top: -5px; font-size: 18px;">No problem! A full setup and user guide, as well as troubleshooting steps, can be found on <a style="color: #2196F3;" href="https://github.com/nicnacnic/nodecg-marathon-control/tree/main/docs" target="_blank">GitHub</a>. Additional support is also available on our <a style="color: #2196F3;" href="https://discord.com/invite/A34Qpfe" target="_blank">Discord</a> server.</p>
<p style="margin-top: 20px;"><i>This dialog is always shown on first load. To view this dialog again, go to Settings > Show Welcome Screen.</i></p>
</html>
56 changes: 28 additions & 28 deletions extension/bot.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
const fs = require('fs')
const fs = require('fs');
const path = require("path");
const portAudio = require('naudiodon')
const prism = require('prism-media')
const WebSocket = require('ws');
const express = require('express')
const prism = require('prism-media');
const { Mixer } = require('audio-mixer');
const { Client, Intents } = require('discord.js');
const { joinVoiceChannel, EndBehaviorType, createAudioPlayer, createAudioResource, StreamType } = require('@discordjs/voice');

module.exports.start = (nodecg) => {

let currentMembers = {};
let silenceInterval, connection, channel, ao;
let silenceInterval, connection, channel;
const botData = nodecg.Replicant('botData');
const botSpeaking = nodecg.Replicant('botSpeaking');
const botSettings = nodecg.Replicant('botSettings');
const settings = nodecg.Replicant('settings')

const client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_VOICE_STATES] });
const mixer = new Mixer({ channels: 2, bitDepth: 16, ampleRate: 48000 })
Expand All @@ -25,13 +28,10 @@ module.exports.start = (nodecg) => {
botSettings.value.channels[channel.id] = channel.name
})

// Get all audio devices.
botSettings.value.devices = {};
const audioDevices = portAudio.getDevices();
audioDevices.forEach(device => {
if (device.maxOutputChannels > 0)
botSettings.value.devices[device.id] = device.name;
});
// Stream audio to browser.
const app = nodecg.Router();
app.get('/bundles/nodecg-marathon-control/bot-audio', (req, res) => mixer.pipe(res))
nodecg.mount(app)

botData.value.users = {};
nodecg.log.info('Bot has been started!')
Expand All @@ -51,16 +51,25 @@ module.exports.start = (nodecg) => {
})

botSettings.on('change', async (newVal, oldVal) => {
if (oldVal === undefined || newVal.outputDevice !== oldVal.outputDevice) outputAudio(newVal.outputDevice)
if (oldVal !== undefined && newVal.channel !== oldVal.channel) {
if (oldVal !== undefined && newVal.channel !== oldVal.channel) {
botData.value.connected = false;
setTimeout(() => botData.value.connected = true, 250)
}
})

settings.on('change', (newVal, oldVal) => {
if (oldVal === undefined || newVal.inIntermission !== oldVal.inIntermission) {
let guildMember = client.channels.cache.get(botSettings.value.channel).guild.members.cache.get(client.user.id);
switch (newVal.inIntermission) {
case true: guildMember.setNickname("Offline"); break;
case false: guildMember.setNickname("🔴 LIVE"); break;
}
}
})

// Join the specified voice channel.
function joinChannel(value) {
if (value === '') return;
if (value === '' || value === null) return;
channel = client.channels.cache.get(value);
connection = joinVoiceChannel({
channelId: channel.id,
Expand Down Expand Up @@ -117,20 +126,11 @@ module.exports.start = (nodecg) => {
}
})

// Create audio output.
function outputAudio(device) {
ao = new portAudio.AudioIO({
outOptions: {
channelCount: 2,
sampleFormat: portAudio.SampleFormat16Bit,
sampleRate: 48000,
closeOnError: false,
deviceId: device,
}
});
mixer.pipe(ao)
ao.start();
}
// Detect when user is speaking or not.
// connection.receiver.speaking.on("start", (user) => console.log(user));
// connection.receiver.speaking.on("end", (userId) => {
// console.log( `${userId} end` );
// });
});

client.login(nodecg.bundleConfig.botToken);
Expand Down
9 changes: 4 additions & 5 deletions extension/defaultValues.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ module.exports.streamSync = {
autoSync: false,
maxOffset: 500,
error: false,
delay: [null, null, null, null]
delay: [null, null, null, null, null]
}

module.exports.autoRecord = {
Expand All @@ -52,11 +52,10 @@ module.exports.botData = {

module.exports.botSettings = {
active: false,
websocketURL: null,
channel: null,
outputDevice: -1,
outputSource: null,
channels: {},
devices: {}
audioOffset: 675,
channels: {}
}

module.exports.settings = {
Expand Down
33 changes: 13 additions & 20 deletions extension/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ module.exports = (nodecg) => {
const settings = nodecg.Replicant('settings', { defaultValue: defaultValue.settings }) // All dashboard settings.
const streamSync = nodecg.Replicant('streamSync', { defaultValue: defaultValue.streamSync }) // Stream Sync data.
const autoRecord = nodecg.Replicant('autoRecord', { defaultValue: defaultValue.autoRecord }) // Auto Record settings
const botSettings = nodecg.Replicant('botSettings', { defaultValue: defaultValue.botSettings }) // Bot settings.
const botData = nodecg.Replicant('botData', { defaultValue: defaultValue.botData }) // Bot data.
const botSpeaking = nodecg.Replicant('botSpeaking', { persistent: false, defaultValue: [] }) // Bot speaking map.
const botSettings = nodecg.Replicant('botSettings', { defaultValue: defaultValue.botSettings }) // Bot settings.
const adPlayer = nodecg.Replicant('adPlayer', { defaultValue: defaultValue.adPlayer }) // Ad player.
const checklist = nodecg.Replicant('checklist', { defaultValue: defaultValue.checklist }) // Checklist tasks.
const runDataActiveRun = nodecg.Replicant('runDataActiveRun', 'nodecg-speedcontrol') // Active run data from nodecg-speedcontrol.
Expand Down Expand Up @@ -105,7 +106,7 @@ module.exports = (nodecg) => {
// Auto set streamkey.
runDataActiveRun.on('change', (newVal, oldVal) => {
if (checklist.value.started) checklist.value.playRun = true;
if (oldVal !== undefined && newVal !== undefined) {
if (oldVal !== undefined && newVal !== undefined && oldVal.id !== newVal.id) {
if (settings.value.autoSetRunners) {
try {
let i = 0;
Expand Down Expand Up @@ -187,7 +188,7 @@ module.exports = (nodecg) => {
let filteredArray = newVal.delay.filter(e => e)
let biggestDelay = Math.max(...filteredArray)
let smallestDelay = Math.min(...filteredArray)
if (newVal.startSync || (newVal.autoSync && (biggestDelay - smallestDelay) > newVal.maxOffset) && filteredArray.length > 1) {
if (newVal.startSync || (newVal.autoSync && (biggestDelay - smallestDelay) > newVal.maxOffset) && filteredArray.length > 0) {
if (auto) nodecg.log.info('Auto stream sync activated on ' + Date() + '.')
let syncArray = [];
newVal.delay.forEach(delay => {
Expand All @@ -196,14 +197,16 @@ module.exports = (nodecg) => {
default: syncArray.push(biggestDelay - delay); break;
}
})
syncArray[4] = biggestDelay;
nodecg.sendMessage('syncStreams', syncArray)
setTimeout(() => {
streamSync.value.syncing = false;
streamSync.value.startSync = false;
checklist.value.syncStreams = true;
let array = newVal.delay;
for (let i = 0; i < 4; i++) {
if (array[i] !== null) array[i] = array[i] + syncArray[i];
for (let i = 0; i < 5; i++) {
if (i === 4) array[i] = biggestDelay;
else if (array[i] !== null) array[i] = array[i] + syncArray[i];
}
streamSync.value.delay = array;
}, Math.max(...syncArray));
Expand Down Expand Up @@ -376,26 +379,17 @@ module.exports = (nodecg) => {

function transitionBegin(scene) {
settings.value.inTransition = true;
if (!adPlayer.value.adPlaying && scene !== settings.value.intermissionScene) {
settings.value.inIntermission = false;
let runnerSources = activeRunners.value;
if (!adPlayer.value.adPlaying) {
runnerSources.forEach(source => {
obs.send('SetAudioMonitorType', { sourceName: source.source, monitorType: 'monitorAndOutput' }).catch((error) => websocketError(error, getCurrentLine()));
})
}
}
}

function updateCurrentScene(scene) {
currentScene.value.program = scene;
settings.value.inTransition = false;
if (scene === settings.value.intermissionScene) {
if (scene !== settings.value.intermissionScene && !adPlayer.value.adPlaying) {
settings.value.inIntermission = false;
settings.value.emergencyTransition = false;
}
else {
settings.value.inIntermission = true;
let runnerSources = activeRunners.value;
runnerSources.forEach(source => {
obs.send('SetAudioMonitorType', { sourceName: source.source, monitorType: 'monitorOnly' }).catch((error) => websocketError(error, getCurrentLine()));
})
if (timer.value.state === 'finished') {
checklist.value = {
started: true,
Expand All @@ -412,7 +406,6 @@ module.exports = (nodecg) => {
if (!adPlayer.value.videoAds && !adPlayer.value.twitchAds) checklist.value.playAd = true;
}
}
else settings.value.emergencyTransition = false;
}

// Emergency Transition logic.
Expand Down
Loading

0 comments on commit 016df82

Please sign in to comment.