refactor for npm instead of submodule
/ release (push) Successful in 37s

This commit is contained in:
2025-12-20 07:39:13 -08:00
parent 7cc765ddc4
commit 2134290895
9 changed files with 544 additions and 475 deletions
+339
View File
@@ -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)
+69
View File
@@ -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) })