Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5650f82d77 | ||
|
|
bd4628efea | ||
|
|
e0e6b83162 | ||
|
|
b088c5bf85 | ||
|
|
78e7d06fd5 | ||
|
|
3e335e72e3 | ||
|
|
a710dcf130 | ||
|
|
2134290895 |
+2
-1
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
.wrangler
|
||||
.wrangler
|
||||
assets/*.js*
|
||||
@@ -0,0 +1 @@
|
||||
@sugoidogo:registry=https://gitea.sugoidogo.com/api/packages/sugoidogo/npm/
|
||||
@@ -1,5 +1,21 @@
|
||||
{
|
||||
"assets":[
|
||||
{
|
||||
"id":"active",
|
||||
"url":"tubie/active.png"
|
||||
},
|
||||
{
|
||||
"id": "active-blink",
|
||||
"url": "tubie/active-blink.png"
|
||||
},
|
||||
{
|
||||
"id": "idle",
|
||||
"url": "tubie/idle.png"
|
||||
},
|
||||
{
|
||||
"id": "idle-blink",
|
||||
"url": "tubie/idle-blink.png"
|
||||
},
|
||||
{
|
||||
"id":"bounce_animation",
|
||||
"url":"bounce.css"
|
||||
|
||||
+16
-378
@@ -1,3 +1,10 @@
|
||||
<head>
|
||||
<link rel="icon" href="data:image/png;base64,iVBORw0KGgo=">
|
||||
<link rel="stylesheet" href="https://unpkg.com/sakura.css/css/sakura.css" media="screen" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/sakura.css/css/sakura-dark.css"
|
||||
media="screen and (prefers-color-scheme: dark)" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
</head>
|
||||
<h1>PNGTube</h1>
|
||||
<select id="instance" hidden></select>
|
||||
<form name="new_instance" hidden>
|
||||
@@ -93,8 +100,8 @@
|
||||
</span>
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: Inter, Roobert, Helvetica Neue, Helvetica, Arial, sans-serif;
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
form[name=new_instance]:not([hidden]),
|
||||
form[name=instance_config]:not([hidden]){
|
||||
@@ -110,384 +117,15 @@
|
||||
white-space: normal;
|
||||
width: 20em;
|
||||
}
|
||||
input:invalid:required {
|
||||
background-color: lightpink;
|
||||
}
|
||||
button#add_source,
|
||||
button.sensitivity {
|
||||
width: 100%;
|
||||
}
|
||||
.source {
|
||||
margin: 10px;
|
||||
}
|
||||
form, input[type=search] {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.js"
|
||||
integrity="sha256-6fEA3uF0XCtisIcFZy5HTT58XocXY7LX7JUfEiK6W5Y="
|
||||
crossorigin="anonymous">
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/obs-websocket-js@5.0.2/dist/obs-ws.js"
|
||||
integrity="sha256-WK517pkeDj2ugtVH+GZa/hQqIoNWNMcBk241/vo1mBw="
|
||||
crossorigin="anonymous">
|
||||
</script>
|
||||
<script type="module">
|
||||
try{
|
||||
const sentry=await import('https://cdn.jsdelivr.net/npm/@sentry/browser@8/+esm')
|
||||
sentry.init({
|
||||
dsn:'https://d0b7617221824fafa3cd9a4d7039a612@app.glitchtip.com/8355',
|
||||
environment:location.hostname,
|
||||
release:"8.1.0",
|
||||
integrations: [
|
||||
sentry.feedbackIntegration({colorScheme:"light"})
|
||||
]
|
||||
})
|
||||
}catch(e){
|
||||
console.warn('automatic error reporting failed to load',e)
|
||||
}
|
||||
|
||||
try{
|
||||
if(window.location.search.includes('remotejs')){
|
||||
const channel=new URLSearchParams(window.location.search).get('remotejs')
|
||||
const script=document.createElement('script')
|
||||
script.src='https://remotejs.com/agent/agent.js'
|
||||
script.setAttribute("data-consolejs-channel",channel)
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
}catch(e){
|
||||
console.warn('remotejs load failed',e)
|
||||
}
|
||||
|
||||
import FileReaderAsync from '../js-util/FileReaderAsync.mjs'
|
||||
import {getFormDataDeep} from '../js-util/FormDataDeep.mjs'
|
||||
/** @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(html=>{
|
||||
const script=document.createElement('script')
|
||||
script.innerHTML='const config='+JSON.stringify(config)
|
||||
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)
|
||||
|
||||
</script>
|
||||
<script type="module" src="index.js"></script>
|
||||
|
||||
@@ -22,102 +22,3 @@
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js"
|
||||
integrity="sha256-zBaNlfuSfUaxBDcmz+E5mOCJAv9j8kMw4rsikBCe0UU="
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/obs-websocket-js@5.0.2/dist/obs-ws.min.js"
|
||||
integrity="sha256-h/nnsIkJJZMNu08xQJBMuUvcrgcUj1YlTYmoA0q4Ep8="
|
||||
crossorigin="anonymous"></script>
|
||||
<script type="module">
|
||||
try{
|
||||
const sentry=await import('https://cdn.jsdelivr.net/npm/@sentry/browser@8/+esm')
|
||||
sentry.init({
|
||||
dsn:'https://d0b7617221824fafa3cd9a4d7039a612@app.glitchtip.com/8355',
|
||||
// this file is loaded locally, this information is not helpful
|
||||
//environment:location.hostname,
|
||||
release:"8.1.0"
|
||||
})
|
||||
}catch(e){
|
||||
console.warn('automatic error reporting failed to load',e)
|
||||
}
|
||||
|
||||
try{
|
||||
if(window.location.search.includes('remotejs')){
|
||||
const channel=new URLSearchParams(window.location.search).get('remotejs')
|
||||
const script=document.createElement('script')
|
||||
script.src='https://remotejs.com/agent/agent.js'
|
||||
script.setAttribute("data-consolejs-channel",channel)
|
||||
document.head.appendChild(script)
|
||||
}
|
||||
}catch(e){
|
||||
console.warn('remotejs load failed',e)
|
||||
}
|
||||
|
||||
console.debug(config)
|
||||
if (!(config.assets instanceof Array)) {
|
||||
config.assets = [config.assets]
|
||||
}
|
||||
if(typeof config.sources == 'string'){
|
||||
config.sources=[config.sources]
|
||||
}
|
||||
import loadAsset from 'https://sugoidogo.github.io/js-util/AssetLoader.mjs'
|
||||
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)})
|
||||
</script>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB |
Generated
+119
-1
@@ -4,6 +4,11 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"@sugoidogo/js-util": "^0.2.0",
|
||||
"localforage": "^1.10.0",
|
||||
"obs-websocket-js": "^5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^4.56.0"
|
||||
}
|
||||
@@ -996,6 +1001,15 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpack/msgpack": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz",
|
||||
"integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@poppinss/colors": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
|
||||
@@ -1045,6 +1059,11 @@
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/@sugoidogo/js-util": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://gitea.sugoidogo.com/api/packages/sugoidogo/npm/%40sugoidogo%2Fjs-util/-/0.2.0/js-util-0.2.0.tgz",
|
||||
"integrity": "sha512-2Jm+MVTzALFOeDNq1I9ZsW657IM67GA+rxeADcVX412LanqEAttmbGwEu3W7uVc1Q8Lk2xDQpmcxB9rgThp+CQ=="
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
@@ -1134,6 +1153,29 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/crypto-js": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
|
||||
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -1196,6 +1238,12 @@
|
||||
"@esbuild/win32-x64": "0.27.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/exit-hook": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz",
|
||||
@@ -1231,6 +1279,12 @@
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
||||
@@ -1238,6 +1292,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isomorphic-ws": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz",
|
||||
"integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"ws": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/kleur": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
|
||||
@@ -1248,6 +1311,24 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
|
||||
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/localforage": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
|
||||
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lie": "3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
|
||||
@@ -1288,6 +1369,30 @@
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/obs-websocket-js": {
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/obs-websocket-js/-/obs-websocket-js-5.0.7.tgz",
|
||||
"integrity": "sha512-SdSNSyrLVR6F0ogInKr7qcadV1tYaTUse/vbabxjkUL8hU3P3dyifxkZ7pEkDDrtCp3TkQ53Enx23kgZO0Cjcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^2.7.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"debug": "^4.3.2",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"isomorphic-ws": "^5.0.0",
|
||||
"type-fest": "^3.11.0",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
|
||||
@@ -1397,6 +1502,18 @@
|
||||
"license": "0BSD",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
|
||||
"integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=14.16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.14.0.tgz",
|
||||
@@ -1425,6 +1542,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"workerd": "bin/workerd"
|
||||
},
|
||||
@@ -1478,8 +1596,8 @@
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"scripts": {
|
||||
"test": "wrangler dev --ip=localhost"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^4.56.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sugoidogo/js-util": "^0.2.0",
|
||||
"localforage": "^1.10.0",
|
||||
"obs-websocket-js": "^5.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
+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://obsconnect.sugoidogo.com')
|
||||
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) })
|
||||
+5
-1
@@ -4,5 +4,9 @@
|
||||
"assets": {"directory": "assets"},
|
||||
"workers_dev": false,
|
||||
"keep_vars": true,
|
||||
"compatibility_date": "2025-12-20"
|
||||
"compatibility_date": "2025-12-20",
|
||||
"build": {
|
||||
"watch_dir": "./src",
|
||||
"command": "esbuild --outdir=assets --format=esm --bundle --minify --sourcemap --sources-content src/*"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user