This commit is contained in:
+339
@@ -0,0 +1,339 @@
|
||||
import { FileReaderAsync, getFormDataDeep } from "@sugoidogo/js-util"
|
||||
import localforage from "localforage"
|
||||
import OBSWebSocket from "obs-websocket-js/json"
|
||||
|
||||
/** @type {Storage} */
|
||||
const localStorage = localforage.createInstance({ name: 'pngtube.v7' })
|
||||
const obs = new OBSWebSocket()
|
||||
/** @type {HTMLDivElement} */
|
||||
const status_div = document.querySelector('div#status')
|
||||
/** @type {HTMLSelectElement} */
|
||||
const instance_select = document.querySelector('select#instance')
|
||||
/** @type {HTMLFormElement} */
|
||||
const new_instance_form = document.querySelector('form[name=new_instance]')
|
||||
/** @type {HTMLFormElement} */
|
||||
const instance_config_form = document.querySelector('form[name=instance_config')
|
||||
/** @type {HTMLInputElement} */
|
||||
const tts_checkbox = document.querySelector('input[name=tts]')
|
||||
/** @type {HTMLInputElement} */
|
||||
const source_input = document.querySelector('input#sources')
|
||||
/** @type {HTMLUListElement} */
|
||||
const source_list = document.querySelector('ul#source_list')
|
||||
/** @type {HTMLUListElement} */
|
||||
const asset_list = document.querySelector('ul#asset_list')
|
||||
/** @type {HTMLInputElement} */
|
||||
const asset_file_input = document.querySelector('input#asset_file_input')
|
||||
/** @type {HTMLSpanElement} */
|
||||
const obs_status_span = document.querySelector('span#obs_status')
|
||||
/** @type {HTMLInputElement} */
|
||||
const obs_token_input = document.querySelector('input[name=obs_token]')
|
||||
/** @type {HTMLDataListElement} */
|
||||
const obs_sources_datalist = document.querySelector('datalist#obs_sources')
|
||||
/** @type {HTMLSpanElement} */
|
||||
const templates = document.querySelector('span#templates')
|
||||
function set_loading(loading) {
|
||||
if (loading) {
|
||||
status_div.innerHTML = 'Loading...'
|
||||
status_div.hidden = false
|
||||
instance_select.hidden = true
|
||||
new_instance_form.hidden = true
|
||||
instance_config_form.hidden = true
|
||||
} else {
|
||||
instance_select.hidden = false
|
||||
status_div.hidden = true
|
||||
}
|
||||
}
|
||||
async function updateReferences() {
|
||||
let refs = ['idle', 'active']
|
||||
for (const asset_list_item of asset_list.children) {
|
||||
const asset_url_input = asset_list_item.querySelector('input[name=url]')
|
||||
const asset_dataurl = asset_url_input.value
|
||||
if (asset_dataurl.startsWith('data:text')) {
|
||||
const asset_text = await fetch(asset_dataurl, { cache: 'no-cache' }).then(response => response.text())
|
||||
const asset_references = asset_text.matchAll(/#[\w-]+/g)
|
||||
for (const asset_reference of asset_references) {
|
||||
refs.push(asset_reference[0].substring(1))
|
||||
}
|
||||
}
|
||||
}
|
||||
refs = [...new Set(refs)]
|
||||
/** @type {HTMLDataListElement} */
|
||||
const element_references_datalist = document.querySelector('datalist#element_references')
|
||||
while (element_references_datalist.childElementCount > 0) {
|
||||
element_references_datalist.firstElementChild.remove()
|
||||
}
|
||||
for (const ref of refs) {
|
||||
const option = document.createElement('option')
|
||||
option.value = ref
|
||||
element_references_datalist.appendChild(option)
|
||||
}
|
||||
for (const asset_list_item of asset_list.children) {
|
||||
const asset_id_input = asset_list_item.querySelector('input[name=id]')
|
||||
const asset_id = asset_id_input.value
|
||||
if (refs.includes(asset_id)) {
|
||||
refs.splice(refs.indexOf(asset_id), 1)
|
||||
}
|
||||
}
|
||||
const asset_warning_div = document.querySelector('div#asset_warning')
|
||||
if (refs.length != 0) {
|
||||
const missing_assets_list = asset_warning_div.querySelector('ul#missing_assets')
|
||||
while (missing_assets_list.childElementCount > 0) {
|
||||
missing_assets_list.firstElementChild.remove()
|
||||
}
|
||||
for (const ref of refs) {
|
||||
const listItem = document.createElement('li')
|
||||
listItem.innerHTML = ref
|
||||
missing_assets_list.appendChild(listItem)
|
||||
}
|
||||
asset_warning_div.hidden = false
|
||||
} else {
|
||||
asset_warning_div.hidden = true
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {File|String} source
|
||||
* @param {String} id
|
||||
*/
|
||||
async function add_asset(source, id = undefined) {
|
||||
const asset_list_item = templates.querySelector('li.asset').cloneNode(true)
|
||||
const asset_id = asset_list_item.querySelector('input[name=id]')
|
||||
const asset_file_name = asset_list_item.querySelector('span#filename')
|
||||
const asset_url = asset_list_item.querySelector('input[name=url]')
|
||||
const asset_remove_button = asset_list_item.querySelector('button')
|
||||
asset_remove_button.addEventListener('click', updateReferences)
|
||||
if (!(source instanceof File)) {
|
||||
let filename = null
|
||||
if (!source.startsWith('data:')) {
|
||||
filename = source.split('/').at(-1)
|
||||
}
|
||||
source = await fetch(source, { cache: 'no-cache' }).then(response => response.blob())
|
||||
if (!filename) {
|
||||
filename = id + '.' + source.type.split('/')[1]
|
||||
}
|
||||
source = new File([source], filename, { type: source.type })
|
||||
}
|
||||
asset_file_name.innerHTML = source.name
|
||||
asset_url.value = await FileReaderAsync.readAs('DataURL', source)
|
||||
if (id) {
|
||||
asset_id.value = id
|
||||
} else {
|
||||
asset_id.value = source.name.split('.')[0]
|
||||
}
|
||||
asset_id.oninput = updateReferences
|
||||
asset_list.appendChild(asset_list_item)
|
||||
updateReferences()
|
||||
}
|
||||
/** @param {HTMLInputElement} source_input */
|
||||
async function adjust_sensitivity(source_input) {
|
||||
obs.call('GetSourceFilterList', { sourceName: source_input.value })
|
||||
.then((response) => {
|
||||
for (const filter of response.filters) {
|
||||
if (filter.filterKind = 'noise_gate_filter') {
|
||||
obs.call('OpenInputFiltersDialog', { inputName: source_input.value })
|
||||
return
|
||||
}
|
||||
}
|
||||
obs.call('CreateSourceFilter', {
|
||||
sourceName: source_input.value,
|
||||
filterName: 'Noise Gate for PNGTube',
|
||||
filterKind: 'noise_gate_filter',
|
||||
filterSettings: {
|
||||
close_threshold: -60,
|
||||
open_threshold: -30
|
||||
}
|
||||
}).then((response) => {
|
||||
obs.call('OpenInputFiltersDialog', { inputName: source_input.value })
|
||||
})
|
||||
}).catch(error => {
|
||||
if (error.message = 'Not connected') {
|
||||
alert('Not connected to OBS')
|
||||
}
|
||||
})
|
||||
}
|
||||
async function add_source(name = 'Mic/Aux') {
|
||||
const source_list_item = templates.querySelector('li.source').cloneNode(true)
|
||||
const source_input = source_list_item.querySelector('input[name=sources]')
|
||||
const source_sensitivity_button = source_list_item.querySelector('button.sensitivity')
|
||||
source_input.value = name
|
||||
source_sensitivity_button.onclick = () => adjust_sensitivity(source_input)
|
||||
source_list.appendChild(source_list_item)
|
||||
}
|
||||
document.querySelector('button#add_source').onclick = () => add_source()
|
||||
async function load_config(instance_name = instance_select.value) {
|
||||
set_loading(true)
|
||||
obs.disconnect()
|
||||
|
||||
for (const list of [asset_list, source_list]) {
|
||||
while (list.childElementCount > 0) {
|
||||
list.firstElementChild.remove()
|
||||
}
|
||||
}
|
||||
|
||||
if (instance_name == '') {
|
||||
set_loading(false)
|
||||
new_instance_form.hidden = false
|
||||
return false
|
||||
}
|
||||
|
||||
const loading_promises = []
|
||||
const config = await localStorage.getItem(instance_name) || await fetch('default.json', { cache: 'no-cache' }).then(response => response.json())
|
||||
|
||||
console.debug(config)
|
||||
|
||||
if (config.obs_token) {
|
||||
obs_token_input.value = config.obs_token
|
||||
obs_init()
|
||||
}
|
||||
if (config.assets) {
|
||||
if (!config.assets instanceof Array) {
|
||||
config.assets = [config.assets]
|
||||
}
|
||||
for (const asset of config.assets) {
|
||||
loading_promises.push(add_asset(asset.url, asset.id))
|
||||
}
|
||||
}
|
||||
if (config.sources) {
|
||||
if (typeof config.sources == 'string') {
|
||||
config.sources = [config.sources]
|
||||
}
|
||||
for (const source of config.sources) {
|
||||
loading_promises.push(add_source(source))
|
||||
}
|
||||
}
|
||||
return Promise.allSettled(loading_promises).then(() => {
|
||||
set_loading(false)
|
||||
instance_config_form.hidden = false
|
||||
return config
|
||||
})
|
||||
}
|
||||
instance_select.onchange = () => load_config()
|
||||
async function instance_select_init() {
|
||||
while (instance_select.childElementCount > 0) {
|
||||
instance_select.firstElementChild.remove()
|
||||
}
|
||||
for (const instance_name of await localStorage.keys()) {
|
||||
const option = document.createElement('option')
|
||||
option.value = instance_name
|
||||
option.innerHTML = instance_name
|
||||
instance_select.appendChild(option)
|
||||
}
|
||||
const option = document.createElement('option')
|
||||
option.innerHTML = 'Create New Instance'
|
||||
option.value = ''
|
||||
instance_select.appendChild(option)
|
||||
instance_select.hidden = false
|
||||
}
|
||||
async function add_instance(name) {
|
||||
const option = document.createElement('option')
|
||||
option.value = name
|
||||
option.innerHTML = name
|
||||
instance_select.prepend(option)
|
||||
instance_select.value = name
|
||||
load_config()
|
||||
}
|
||||
new_instance_form.onsubmit = (event) => {
|
||||
event.preventDefault()
|
||||
add_instance(new_instance_form.instance_name.value)
|
||||
new_instance_form.instance_name.value = ''
|
||||
}
|
||||
asset_file_input.onchange = (event) => {
|
||||
for (const file of asset_file_input.files) {
|
||||
add_asset(file)
|
||||
}
|
||||
asset_file_input.value = ''
|
||||
}
|
||||
obs.on('ConnectionClosed', (error) => {
|
||||
obs_status_span.innerHTML = 'Not connected to OBS'
|
||||
console.warn(error)
|
||||
})
|
||||
obs.on('Identified', () => {
|
||||
obs_status_span.innerHTML = 'Connected to OBS'
|
||||
obs.call('GetInputList')
|
||||
.then((response) => {
|
||||
for (const input of response.inputs) {
|
||||
const option = document.createElement('option')
|
||||
option.value = input.inputName
|
||||
obs_sources_datalist.append(option)
|
||||
}
|
||||
})
|
||||
})
|
||||
obs.on('InputCreated', (event) => {
|
||||
console.debug('source created: ' + event.inputName)
|
||||
const option = document.createElement('option')
|
||||
option.value = event.inputName
|
||||
obs_sources_datalist.append(option)
|
||||
})
|
||||
obs.on('InputRemoved', (event) => {
|
||||
console.debug('source removed: ' + event.inputName)
|
||||
document.querySelector("option[value='" + event.inputName + "']").remove()
|
||||
})
|
||||
obs.on('InputNameChanged', (event) => {
|
||||
console.debug('source renamed :' + event.oldInputName + " > " + event.inputName)
|
||||
document.querySelector("option[value='" + event.oldInputName + "']").value = event.inputName
|
||||
})
|
||||
async function obs_init() {
|
||||
let obsurl
|
||||
let obspassword
|
||||
const search_params = new URLSearchParams(location.search)
|
||||
if (search_params.has('obsurl')) {
|
||||
obsurl = search_params.get('obsurl')
|
||||
obspassword = search_params.get('obspassword')
|
||||
const url = new URL(obsurl)
|
||||
url.password = obspassword
|
||||
obs_token_input.value = url.href
|
||||
} else {
|
||||
if (obs_token_input.value) {
|
||||
const url = new URL(obs_token_input.value)
|
||||
obspassword = url.password
|
||||
url.password = ''
|
||||
obsurl = url.href
|
||||
} else {
|
||||
obs_status_span.innerHTML = 'No OBS Authorization found'
|
||||
}
|
||||
}
|
||||
return obs.connect(obsurl, obspassword)
|
||||
}
|
||||
async function obs_auth() {
|
||||
const url = new URL('https://sugoidogo.github.io/obsconnect')
|
||||
url.searchParams.append('redirect_uri', location.href)
|
||||
location.assign(url)
|
||||
}
|
||||
document.querySelector('button#obs_auth').onclick = obs_auth
|
||||
instance_config_form.onsubmit = (event) => {
|
||||
event.preventDefault()
|
||||
const config = getFormDataDeep(instance_config_form)
|
||||
console.debug(config)
|
||||
localStorage.setItem(instance_select.value, config)
|
||||
const url = new URL(location.origin + location.pathname)
|
||||
if (url.pathname.endsWith('/')) {
|
||||
url.pathname += 'overlay.html'
|
||||
} else {
|
||||
const path = url.pathname.split('/')
|
||||
path.pop()
|
||||
path.push('overlay.html')
|
||||
url.pathname = path.join('/')
|
||||
}
|
||||
fetch(url).then(response => response.text())
|
||||
.then(async html => {
|
||||
const script = document.createElement('script')
|
||||
script.type = 'module'
|
||||
script.innerHTML = 'const config=' + JSON.stringify(config)+'\n'
|
||||
script.innerHTML+=await fetch(new URL('overlay.js',url)).then(response=>response.text())
|
||||
html += script.outerHTML
|
||||
const blob = new Blob([html])
|
||||
const a = document.createElement('a')
|
||||
a.href = URL.createObjectURL(blob)
|
||||
a.download = 'pngtube-' + instance_select.value + '.html'
|
||||
a.click()
|
||||
})
|
||||
}
|
||||
async function remove_instance(name = instance_select.value) {
|
||||
localStorage.removeItem(name)
|
||||
.then(instance_select_init)
|
||||
.then(load_config)
|
||||
}
|
||||
document.querySelector('button#remove_instance').onclick = () => remove_instance()
|
||||
|
||||
|
||||
instance_select_init()
|
||||
.then(load_config)
|
||||
@@ -0,0 +1,69 @@
|
||||
import { loadAsset } from '@sugoidogo/js-util'
|
||||
import OBSWebSocket from 'obs-websocket-js/json'
|
||||
|
||||
console.debug(config)
|
||||
if (!(config.assets instanceof Array)) {
|
||||
config.assets = [config.assets]
|
||||
}
|
||||
if (typeof config.sources == 'string') {
|
||||
config.sources = [config.sources]
|
||||
}
|
||||
for (const source of config.assets) {
|
||||
loadAsset(source.url, source.id, true)
|
||||
}
|
||||
// backoff mechanism
|
||||
let backoff = false
|
||||
function backoffStart(event) {
|
||||
if (event instanceof AnimationEvent && getComputedStyle(event.target, event.pseudoElement).animationIterationCount == 'infinite') {
|
||||
return
|
||||
}
|
||||
backoff = true
|
||||
}
|
||||
function backoffEnd() {
|
||||
backoff = false
|
||||
}
|
||||
window.ontransitionstart = backoffStart
|
||||
window.ontransitionend = backoffEnd
|
||||
window.ontransitioncancel = backoffEnd
|
||||
window.onanimationstart = backoffStart
|
||||
window.onanimationend = backoffEnd
|
||||
window.ontransitioncancel = backoffEnd
|
||||
// animation function
|
||||
let active = false
|
||||
requestAnimationFrame(function animate() {
|
||||
if (!backoff) {
|
||||
for (const img of document.querySelectorAll('img')) {
|
||||
if (active) {
|
||||
img.classList.add('active')
|
||||
} else {
|
||||
img.classList.remove('active')
|
||||
}
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(animate)
|
||||
})
|
||||
// obs connection
|
||||
const obs = new OBSWebSocket()
|
||||
obs.on('ConnectionClosed', (error) => {
|
||||
window.alert('PNGTube error ' + error.code + ': ' + error.message)
|
||||
})
|
||||
obs.on('Identified', (event) => {
|
||||
console.debug(event)
|
||||
})
|
||||
obs.on('InputVolumeMeters', (event) => {
|
||||
const inputs = event.inputs.filter(input => config.sources.includes(input.inputName))
|
||||
for (const input of inputs) {
|
||||
for (const channel of input.inputLevelsMul) {
|
||||
if (channel[0] > 0) {
|
||||
active = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
active = false
|
||||
})
|
||||
const token = new URL(config.obs_token)
|
||||
const password = token.password
|
||||
token.password = ''
|
||||
const url = token.href
|
||||
obs.connect(url, password, { eventSubscriptions: (1 << 16) })
|
||||
Reference in New Issue
Block a user