From 0254a30685bd45434763dddb3c6bec3e4bb4158c Mon Sep 17 00:00:00 2001 From: bryce Date: Tue, 22 Jul 2025 10:50:27 +1200 Subject: [PATCH] init upload Mostly working ** OBS WS ** - UI isn't connecting - bot is connecting --- index.html | 85 ++++++ obs-websocket.js | 1 + script.js | 705 +++++++++++++++++++++++++++++++++++++++++++++++ styles.css | 319 +++++++++++++++++++++ 4 files changed, 1110 insertions(+) create mode 100644 index.html create mode 100644 obs-websocket.js create mode 100644 script.js create mode 100644 styles.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..631e0ac --- /dev/null +++ b/index.html @@ -0,0 +1,85 @@ + + + + + + OBS Remote Control + + + + + +
+

OBS Remote Control

+ + +
+ + Disconnected + +
+ +
+

Connection Settings

+ +
+ + +
+ + +
+ + + +
+ + +
+

Scene Transitions

+
+ + + + + + +
+
+ + +
+

Scene Chooser (Preview)

+
+ +
+
+ + +
+

Audio Control

+
+ +
+
+ + +
+

Mute Groups

+
+ + +
+
+
+ + + + + diff --git a/obs-websocket.js b/obs-websocket.js new file mode 100644 index 0000000..a1e3ed5 --- /dev/null +++ b/obs-websocket.js @@ -0,0 +1 @@ +"use strict";var OBSWebSocket=(()=>{var He=Object.create;var re=Object.defineProperty;var We=Object.getOwnPropertyDescriptor;var Ke=Object.getOwnPropertyNames,W=Object.getOwnPropertySymbols,ze=Object.getPrototypeOf,ie=Object.prototype.hasOwnProperty,Se=Object.prototype.propertyIsEnumerable;var ne=(t,n,e)=>n in t?re(t,n,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[n]=e,D=(t,n)=>{for(var e in n||(n={}))ie.call(n,e)&&ne(t,e,n[e]);if(W)for(var e of W(n))Se.call(n,e)&&ne(t,e,n[e]);return t};var he=(t=>typeof require!="undefined"?require:typeof Proxy!="undefined"?new Proxy(t,{get:(n,e)=>(typeof require!="undefined"?require:n)[e]}):t)(function(t){if(typeof require!="undefined")return require.apply(this,arguments);throw Error('Dynamic require of "'+t+'" is not supported')});var ye=(t,n)=>{var e={};for(var i in t)ie.call(t,i)&&n.indexOf(i)<0&&(e[i]=t[i]);if(t!=null&&W)for(var i of W(t))n.indexOf(i)<0&&Se.call(t,i)&&(e[i]=t[i]);return e};var P=(t,n)=>()=>(t&&(n=t(t=0)),n);var G=(t,n)=>()=>(n||t((n={exports:{}}).exports,n),n.exports);var $e=(t,n,e,i)=>{if(n&&typeof n=="object"||typeof n=="function")for(let u of Ke(n))!ie.call(t,u)&&u!==e&&re(t,u,{get:()=>n[u],enumerable:!(i=We(n,u))||i.enumerable});return t};var K=(t,n,e)=>(e=t!=null?He(ze(t)):{},$e(n||!t||!t.__esModule?re(e,"default",{value:t,enumerable:!0}):e,t));var B=(t,n,e)=>ne(t,typeof n!="symbol"?n+"":n,e);var O=(t,n,e)=>new Promise((i,u)=>{var m=o=>{try{f(e.next(o))}catch(c){u(c)}},l=o=>{try{f(e.throw(o))}catch(c){u(c)}},f=o=>o.done?i(o.value):Promise.resolve(o.value).then(m,l);f((e=e.apply(t,n)).next())});var be=G((pt,ve)=>{"use strict";var U=1e3,A=U*60,L=A*60,x=L*24,Qe=x*7,Ze=x*365.25;ve.exports=function(t,n){n=n||{};var e=typeof t;if(e==="string"&&t.length>0)return Xe(t);if(e==="number"&&isFinite(t))return n.long?et(t):Ye(t);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(t))};function Xe(t){if(t=String(t),!(t.length>100)){var n=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(t);if(n){var e=parseFloat(n[1]),i=(n[2]||"ms").toLowerCase();switch(i){case"years":case"year":case"yrs":case"yr":case"y":return e*Ze;case"weeks":case"week":case"w":return e*Qe;case"days":case"day":case"d":return e*x;case"hours":case"hour":case"hrs":case"hr":case"h":return e*L;case"minutes":case"minute":case"mins":case"min":case"m":return e*A;case"seconds":case"second":case"secs":case"sec":case"s":return e*U;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return e;default:return}}}}function Ye(t){var n=Math.abs(t);return n>=x?Math.round(t/x)+"d":n>=L?Math.round(t/L)+"h":n>=A?Math.round(t/A)+"m":n>=U?Math.round(t/U)+"s":t+"ms"}function et(t){var n=Math.abs(t);return n>=x?z(t,n,x,"day"):n>=L?z(t,n,L,"hour"):n>=A?z(t,n,A,"minute"):n>=U?z(t,n,U,"second"):t+" ms"}function z(t,n,e,i){var u=n>=e*1.5;return Math.round(t/e)+" "+i+(u?"s":"")}});var Ie=G((lt,Ce)=>{"use strict";function tt(t){e.debug=e,e.default=e,e.coerce=o,e.disable=m,e.enable=u,e.enabled=l,e.humanize=be(),e.destroy=c,Object.keys(t).forEach(d=>{e[d]=t[d]}),e.names=[],e.skips=[],e.formatters={};function n(d){let s=0;for(let g=0;g{if(v==="%%")return"%";S++;let N=e.formatters[F];if(typeof N=="function"){let R=C[S];v=N.call(r,R),C.splice(S,1),S--}return v}),e.formatArgs.call(r,C),(r.log||e.log).apply(r,C)}return b.namespace=d,b.useColors=e.useColors(),b.color=e.selectColor(d),b.extend=i,b.destroy=e.destroy,Object.defineProperty(b,"enabled",{enumerable:!0,configurable:!1,get:()=>g!==null?g:(p!==e.namespaces&&(p=e.namespaces,y=e.enabled(d)),y),set:C=>{g=C}}),typeof e.init=="function"&&e.init(b),b}function i(d,s){let g=e(this.namespace+(typeof s=="undefined"?":":s)+d);return g.log=this.log,g}function u(d){e.save(d),e.namespaces=d,e.names=[],e.skips=[];let s,g=(typeof d=="string"?d:"").split(/[\s,]+/),p=g.length;for(s=0;s"-"+s)].join(",");return e.enable(""),d}function l(d){if(d[d.length-1]==="*")return!0;let s,g;for(s=0,g=e.skips.length;s{"use strict";M.formatArgs=rt;M.save=it;M.load=st;M.useColors=nt;M.storage=ot();M.destroy=(()=>{let t=!1;return()=>{t||(t=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})();M.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"];function nt(){if(typeof window!="undefined"&&window.process&&(window.process.type==="renderer"||window.process.__nwjs))return!0;if(typeof navigator!="undefined"&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))return!1;let t;return typeof document!="undefined"&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||typeof window!="undefined"&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||typeof navigator!="undefined"&&navigator.userAgent&&(t=navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/))&&parseInt(t[1],10)>=31||typeof navigator!="undefined"&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)}function rt(t){if(t[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+t[0]+(this.useColors?"%c ":" ")+"+"+$.exports.humanize(this.diff),!this.useColors)return;let n="color: "+this.color;t.splice(1,0,n,"color: inherit");let e=0,i=0;t[0].replace(/%[a-zA-Z%]/g,u=>{u!=="%%"&&(e++,u==="%c"&&(i=e))}),t.splice(i,0,n)}M.log=console.debug||console.log||(()=>{});function it(t){try{t?M.storage.setItem("debug",t):M.storage.removeItem("debug")}catch(n){}}function st(){let t;try{t=M.storage.getItem("debug")}catch(n){}return!t&&typeof process!="undefined"&&"env"in process&&(t=process.env.DEBUG),t}function ot(){try{return localStorage}catch(t){}}$.exports=Ie()(M);var{formatters:at}=$.exports;at.j=function(t){try{return JSON.stringify(t)}catch(n){return"[UnexpectedJSONParseError]: "+n.message}}});var Oe=G((mt,se)=>{"use strict";var ut=Object.prototype.hasOwnProperty,w="~";function J(){}Object.create&&(J.prototype=Object.create(null),new J().__proto__||(w=!1));function ct(t,n,e){this.fn=t,this.context=n,this.once=e||!1}function Ne(t,n,e,i,u){if(typeof e!="function")throw new TypeError("The listener must be a function");var m=new ct(e,i||t,u),l=w?w+n:n;return t._events[l]?t._events[l].fn?t._events[l]=[t._events[l],m]:t._events[l].push(m):(t._events[l]=m,t._eventsCount++),t}function Q(t,n){--t._eventsCount===0?t._events=new J:delete t._events[n]}function T(){this._events=new J,this._eventsCount=0}T.prototype.eventNames=function(){var n=[],e,i;if(this._eventsCount===0)return n;for(i in e=this._events)ut.call(e,i)&&n.push(w?i.slice(1):i);return Object.getOwnPropertySymbols?n.concat(Object.getOwnPropertySymbols(e)):n};T.prototype.listeners=function(n){var e=w?w+n:n,i=this._events[e];if(!i)return[];if(i.fn)return[i.fn];for(var u=0,m=i.length,l=new Array(m);u{"use strict";Z=K(Oe(),1)});var _,oe,Me=P(()=>{"use strict";_=null;typeof WebSocket!="undefined"?_=WebSocket:typeof MozWebSocket!="undefined"?_=MozWebSocket:typeof global!="undefined"?_=global.WebSocket||global.MozWebSocket:typeof window!="undefined"?_=window.WebSocket||window.MozWebSocket:typeof self!="undefined"&&(_=self.WebSocket||self.MozWebSocket);oe=_});var ae,ue,ce,Re=P(()=>{"use strict";ae=(c=>(c[c.Hello=0]="Hello",c[c.Identify=1]="Identify",c[c.Identified=2]="Identified",c[c.Reidentify=3]="Reidentify",c[c.Event=5]="Event",c[c.Request=6]="Request",c[c.RequestResponse=7]="RequestResponse",c[c.RequestBatch=8]="RequestBatch",c[c.RequestBatchResponse=9]="RequestBatchResponse",c))(ae||{}),ue=(r=>(r[r.None=0]="None",r[r.General=1]="General",r[r.Config=2]="Config",r[r.Scenes=4]="Scenes",r[r.Inputs=8]="Inputs",r[r.Transitions=16]="Transitions",r[r.Filters=32]="Filters",r[r.Outputs=64]="Outputs",r[r.SceneItems=128]="SceneItems",r[r.MediaInputs=256]="MediaInputs",r[r.Vendors=512]="Vendors",r[r.Ui=1024]="Ui",r[r.All=2047]="All",r[r.InputVolumeMeters=65536]="InputVolumeMeters",r[r.InputActiveStateChanged=131072]="InputActiveStateChanged",r[r.InputShowStateChanged=262144]="InputShowStateChanged",r[r.SceneItemTransformChanged=524288]="SceneItemTransformChanged",r))(ue||{}),ce=(u=>(u[u.None=-1]="None",u[u.SerialRealtime=0]="SerialRealtime",u[u.SerialFrame=1]="SerialFrame",u[u.Parallel=2]="Parallel",u))(ce||{})});var Be=G(()=>{"use strict"});var de=G((X,Fe)=>{"use strict";(function(t,n){typeof X=="object"?Fe.exports=X=n():typeof define=="function"&&define.amd?define([],n):t.CryptoJS=n()})(X,function(){var t=t||function(n,e){var i;if(typeof window!="undefined"&&window.crypto&&(i=window.crypto),typeof self!="undefined"&&self.crypto&&(i=self.crypto),typeof globalThis!="undefined"&&globalThis.crypto&&(i=globalThis.crypto),!i&&typeof window!="undefined"&&window.msCrypto&&(i=window.msCrypto),!i&&typeof global!="undefined"&&global.crypto&&(i=global.crypto),!i&&typeof he=="function")try{i=Be()}catch(r){}var u=function(){if(i){if(typeof i.getRandomValues=="function")try{return i.getRandomValues(new Uint32Array(1))[0]}catch(r){}if(typeof i.randomBytes=="function")try{return i.randomBytes(4).readInt32LE()}catch(r){}}throw new Error("Native crypto module could not be used to get secure random number.")},m=Object.create||function(){function r(){}return function(a){var h;return r.prototype=a,h=new r,r.prototype=null,h}}(),l={},f=l.lib={},o=f.Base=function(){return{extend:function(r){var a=m(this);return r&&a.mixIn(r),(!a.hasOwnProperty("init")||this.init===a.init)&&(a.init=function(){a.$super.init.apply(this,arguments)}),a.init.prototype=a,a.$super=this,a},create:function(){var r=this.extend();return r.init.apply(r,arguments),r},init:function(){},mixIn:function(r){for(var a in r)r.hasOwnProperty(a)&&(this[a]=r[a]);r.hasOwnProperty("toString")&&(this.toString=r.toString)},clone:function(){return this.init.prototype.extend(this)}}}(),c=f.WordArray=o.extend({init:function(r,a){r=this.words=r||[],a!=e?this.sigBytes=a:this.sigBytes=r.length*4},toString:function(r){return(r||s).stringify(this)},concat:function(r){var a=this.words,h=r.words,S=this.sigBytes,I=r.sigBytes;if(this.clamp(),S%4)for(var v=0;v>>2]>>>24-v%4*8&255;a[S+v>>>2]|=F<<24-(S+v)%4*8}else for(var N=0;N>>2]=h[N>>>2];return this.sigBytes+=I,this},clamp:function(){var r=this.words,a=this.sigBytes;r[a>>>2]&=4294967295<<32-a%4*8,r.length=n.ceil(a/4)},clone:function(){var r=o.clone.call(this);return r.words=this.words.slice(0),r},random:function(r){for(var a=[],h=0;h>>2]>>>24-I%4*8&255;S.push((v>>>4).toString(16)),S.push((v&15).toString(16))}return S.join("")},parse:function(r){for(var a=r.length,h=[],S=0;S>>3]|=parseInt(r.substr(S,2),16)<<24-S%8*4;return new c.init(h,a/2)}},g=d.Latin1={stringify:function(r){for(var a=r.words,h=r.sigBytes,S=[],I=0;I>>2]>>>24-I%4*8&255;S.push(String.fromCharCode(v))}return S.join("")},parse:function(r){for(var a=r.length,h=[],S=0;S>>2]|=(r.charCodeAt(S)&255)<<24-S%4*8;return new c.init(h,a)}},p=d.Utf8={stringify:function(r){try{return decodeURIComponent(escape(g.stringify(r)))}catch(a){throw new Error("Malformed UTF-8 data")}},parse:function(r){return g.parse(unescape(encodeURIComponent(r)))}},y=f.BufferedBlockAlgorithm=o.extend({reset:function(){this._data=new c.init,this._nDataBytes=0},_append:function(r){typeof r=="string"&&(r=p.parse(r)),this._data.concat(r),this._nDataBytes+=r.sigBytes},_process:function(r){var a,h=this._data,S=h.words,I=h.sigBytes,v=this.blockSize,F=v*4,N=I/F;r?N=n.ceil(N):N=n.max((N|0)-this._minBufferSize,0);var R=N*v,H=n.min(R*4,I);if(R){for(var j=0;j{"use strict";(function(t,n){typeof Y=="object"?ke.exports=Y=n(de()):typeof define=="function"&&define.amd?define(["./core"],n):n(t.CryptoJS)})(Y,function(t){return function(n){var e=t,i=e.lib,u=i.WordArray,m=i.Hasher,l=e.algo,f=[],o=[];(function(){function s(b){for(var C=n.sqrt(b),r=2;r<=C;r++)if(!(b%r))return!1;return!0}function g(b){return(b-(b|0))*4294967296|0}for(var p=2,y=0;y<64;)s(p)&&(y<8&&(f[y]=g(n.pow(p,1/2))),o[y]=g(n.pow(p,1/3)),y++),p++})();var c=[],d=l.SHA256=m.extend({_doReset:function(){this._hash=new u.init(f.slice(0))},_doProcessBlock:function(s,g){for(var p=this._hash.words,y=p[0],b=p[1],C=p[2],r=p[3],a=p[4],h=p[5],S=p[6],I=p[7],v=0;v<64;v++){if(v<16)c[v]=s[g+v]|0;else{var F=c[v-15],N=(F<<25|F>>>7)^(F<<14|F>>>18)^F>>>3,R=c[v-2],H=(R<<15|R>>>17)^(R<<13|R>>>19)^R>>>10;c[v]=N+c[v-7]+H+c[v-16]}var j=a&h^~a&S,je=y&b^y&C^b&C,De=(y<<30|y>>>2)^(y<<19|y>>>13)^(y<<10|y>>>22),Je=(a<<26|a>>>6)^(a<<21|a>>>11)^(a<<7|a>>>25),fe=I+Je+j+o[v]+c[v],Ve=De+je;I=S,S=h,h=a,a=r+fe|0,r=C,C=b,b=y,y=fe+Ve|0}p[0]=p[0]+y|0,p[1]=p[1]+b|0,p[2]=p[2]+C|0,p[3]=p[3]+r|0,p[4]=p[4]+a|0,p[5]=p[5]+h|0,p[6]=p[6]+S|0,p[7]=p[7]+I|0},_doFinalize:function(){var s=this._data,g=s.words,p=this._nDataBytes*8,y=s.sigBytes*8;return g[y>>>5]|=128<<24-y%32,g[(y+64>>>9<<4)+14]=n.floor(p/4294967296),g[(y+64>>>9<<4)+15]=p,s.sigBytes=g.length*4,this._process(),this._hash},clone:function(){var s=m.clone.call(this);return s._hash=this._hash.clone(),s}});e.SHA256=m._createHelper(d),e.HmacSHA256=m._createHmacHelper(d)}(Math),t.SHA256})});var Pe=G((ee,xe)=>{"use strict";(function(t,n){typeof ee=="object"?xe.exports=ee=n(de()):typeof define=="function"&&define.amd?define(["./core"],n):n(t.CryptoJS)})(ee,function(t){return function(){var n=t,e=n.lib,i=e.WordArray,u=n.enc,m=u.Base64={stringify:function(f){var o=f.words,c=f.sigBytes,d=this._map;f.clamp();for(var s=[],g=0;g>>2]>>>24-g%4*8&255,y=o[g+1>>>2]>>>24-(g+1)%4*8&255,b=o[g+2>>>2]>>>24-(g+2)%4*8&255,C=p<<16|y<<8|b,r=0;r<4&&g+r*.75>>6*(3-r)&63));var a=d.charAt(64);if(a)for(;s.length%4;)s.push(a);return s.join("")},parse:function(f){var o=f.length,c=this._map,d=this._reverseMap;if(!d){d=this._reverseMap=[];for(var s=0;s>>6-g%4*2,b=p|y;d[s>>>2]|=b<<24-s%4*8,s++}return i.create(d,s)}}(),t.enc.Base64})});function Ue(t,n,e){let i=pe.default.stringify((0,ge.default)(e+t));return pe.default.stringify((0,ge.default)(i+n))}var ge,pe,Ae=P(()=>{"use strict";ge=K(Ge(),1),pe=K(Pe(),1)});var Le,V,k,q,te,le=P(()=>{"use strict";Le=K(Te(),1);we();Me();Ae();V=(0,Le.default)("obs-websocket-js"),k=class extends Error{constructor(e,i){super(i);this.code=e}},q=class q extends Z.default{constructor(){super(...arguments);B(this,"_identified",!1);B(this,"internalListeners",new Z.default);B(this,"socket")}static generateMessageId(){return String(q.requestCounter++)}get identified(){return this._identified}connect(){return O(this,arguments,function*(e="ws://127.0.0.1:4455",i,u={}){this.socket&&(yield this.disconnect());try{let m=this.internalEventPromise("ConnectionClosed"),l=this.internalEventPromise("ConnectionError");return yield Promise.race([O(this,null,function*(){let f=yield this.createConnection(e);return this.emit("Hello",f),this.identify(f,i,u)}),new Promise((f,o)=>{l.then(c=>{c.message&&o(c)}),m.then(c=>{o(c)})})])}catch(m){throw yield this.disconnect(),m}})}disconnect(){return O(this,null,function*(){if(!this.socket||this.socket.readyState===oe.CLOSED)return;let e=this.internalEventPromise("ConnectionClosed");this.socket.close(),yield e})}reidentify(e){return O(this,null,function*(){let i=this.internalEventPromise("op:2");return yield this.message(3,e),i})}call(e,i){return O(this,null,function*(){let u=q.generateMessageId(),m=this.internalEventPromise(`res:${u}`);yield this.message(6,{requestId:u,requestType:e,requestData:i});let{requestStatus:l,responseData:f}=yield m;if(!l.result)throw new k(l.code,l.comment);return f})}callBatch(u){return O(this,arguments,function*(e,i={}){let m=q.generateMessageId(),l=this.internalEventPromise(`res:${m}`);yield this.message(8,D({requestId:m,requests:e},i));let{results:f}=yield l;return f})}cleanup(){this.socket&&(this.socket.onopen=null,this.socket.onmessage=null,this.socket.onerror=null,this.socket.onclose=null,this.socket=void 0,this._identified=!1,this.internalListeners.removeAllListeners())}createConnection(e){return O(this,null,function*(){var l;let i=this.internalEventPromise("ConnectionOpened"),u=this.internalEventPromise("op:0");this.socket=new oe(e,this.protocol),this.socket.onopen=this.onOpen.bind(this),this.socket.onmessage=this.onMessage.bind(this),this.socket.onerror=this.onError.bind(this),this.socket.onclose=this.onClose.bind(this),yield i;let m=(l=this.socket)==null?void 0:l.protocol;if(!m)throw new k(-1,"Server sent no subprotocol");if(m!==this.protocol)throw new k(-1,"Server sent an invalid subprotocol");return u})}identify(c,d){return O(this,arguments,function*(f,m,l={}){var o=f,{authentication:e,rpcVersion:i}=o,u=ye(o,["authentication","rpcVersion"]);let s=D({rpcVersion:i},l);e&&m&&(s.authentication=Ue(e.salt,e.challenge,m));let g=this.internalEventPromise("op:2");yield this.message(1,s);let p=yield g;return this._identified=!0,this.emit("Identified",p),D(D({rpcVersion:i},u),p)})}message(e,i){return O(this,null,function*(){if(!this.socket)throw new Error("Not connected");if(!this.identified&&e!==1)throw new Error("Socket not identified");let u=yield this.encodeMessage({op:e,d:i});this.socket.send(u)})}internalEventPromise(e){return O(this,null,function*(){return new Promise(i=>{this.internalListeners.once(e,i)})})}onOpen(e){V("socket.open"),this.emit("ConnectionOpened"),this.internalListeners.emit("ConnectionOpened",e)}onMessage(e){return O(this,null,function*(){try{let{op:i,d:u}=yield this.decodeMessage(e.data);if(V("socket.message: %d %j",i,u),i===void 0||u===void 0)return;switch(i){case 5:{let{eventType:m,eventData:l}=u;this.emit(m,l);return}case 7:case 9:{let{requestId:m}=u;this.internalListeners.emit(`res:${m}`,u);return}default:this.internalListeners.emit(`op:${i}`,u)}}catch(i){V("error handling message: %o",i)}})}onError(e){V("socket.error: %o",e);let i=new k(-1,e.message);this.emit("ConnectionError",i),this.internalListeners.emit("ConnectionError",i)}onClose(e){V("socket.close: %s (%d)",e.reason,e.code);let i=new k(e.code,e.reason);this.emit("ConnectionClosed",i),this.internalListeners.emit("ConnectionClosed",i),this.cleanup()}};B(q,"requestCounter",1);te=q;typeof exports!="undefined"&&Object.defineProperty(exports,"__esModule",{value:!0})});var me,_e,qe=P(()=>{"use strict";le();le();Re();me=class extends te{constructor(){super(...arguments);B(this,"protocol","obswebsocket.json")}encodeMessage(e){return O(this,null,function*(){return JSON.stringify(e)})}decodeMessage(e){return O(this,null,function*(){return JSON.parse(e)})}},_e=me});var dt=G((Pt,Ee)=>{qe();var E;Ee.exports=(E=class extends _e{},B(E,"OBSWebSocketError",k),B(E,"WebSocketOpCode",ae),B(E,"EventSubscription",ue),B(E,"RequestBatchExecutionType",ce),E)});return dt();})(); diff --git a/script.js b/script.js new file mode 100644 index 0000000..884c788 --- /dev/null +++ b/script.js @@ -0,0 +1,705 @@ +// --- OBS WebSocket Instance --- +const obs = new OBSWebSocket(); + +// --- DOM Elements --- +const statusLight = document.getElementById('status-light'); +const statusText = document.getElementById('status-text'); +const connectDialogButton = document.getElementById('connect-dialog-button'); +const connectionDialog = document.getElementById('connection-dialog'); +const obsIpInput = document.getElementById('obs-ip'); +const obsPortInput = document.getElementById('obs-port'); +const obsPasswordInput = document.getElementById('obs-password'); +const connectButton = document.getElementById('connect-button'); +const closeDialogButton = document.getElementById('close-dialog-button'); +const transitionButtons = document.querySelectorAll('.transition-button'); +const audioControlsGrid = document.getElementById('audio-controls-grid'); +const sceneThumbnailsContainer = document.getElementById('scene-thumbnails'); + +// New group button references +const toggleAllMicsButton = document.getElementById('toggle-all-mics'); +const toggleNonMicsButton = document.getElementById('toggle-non-mics'); + + +// --- Configuration (Editable based on your OBS setup) --- +const MIC_SOURCES = ['Room Mic', 'Headset 1', 'Wireless']; +const MUSIC_SOURCES = ['Music', 'SoundDesk']; +const ALERT_SOURCES = ['Alerts']; +const ALL_AUDIO_SOURCES = [ + 'Desktop Audio', 'Headset', 'Wireless', 'SoundDesk', 'Kofi Alert', 'Kofi GOAL' +]; // Explicitly list all desired sources + +const STINGER_INFO = { + 'Stinger 1': { name: 'Stinger 1', duration: 3000 }, + 'Stinger 2': { name: 'Stinger 2', duration: 3000 } +}; + +let currentProgramScene = ''; +let autoReconnectInterval = null; +const AUTO_RECONNECT_DELAY = 5000; + +// --- SVG Icons (same as before) --- +const micIcon = ` + + + +`; + +const micMutedIcon = ` + + + +`; + +const musicIcon = ` + + + +`; + +const musicMutedIcon = ` + + + +`; + +const alertIcon = ` + + + +`; + +const alertMutedIcon = ` + + + +`; + +// Group Icons (using generic ones for now, customize if needed) +const groupMicsIcon = micIcon; // Use the regular mic icon for the group +const groupMicsMutedIcon = micMutedIcon; // Use the muted mic icon for the group + +// For "Other Audio" group, perhaps a speaker or headphones icon? +// Using the music icon for now, you can create a speaker/headphones SVG if you like. +const groupNonMicsIcon = musicIcon; +const groupNonMicsMutedIcon = musicMutedIcon; + +// --- Connection Logic (same as before, with .classList.add/remove fix) --- +function updateConnectionStatus(isConnected, reconnecting = false) { + if (isConnected) { + statusLight.className = 'status-light green'; + statusText.textContent = 'Connected'; + clearInterval(autoReconnectInterval); + autoReconnectInterval = null; + } else { + statusLight.className = reconnecting ? 'status-light amber' : 'status-light red'; + statusText.textContent = reconnecting ? 'Attempting to Reconnect...' : 'Disconnected'; + if (!reconnecting && !autoReconnectInterval) { + autoReconnectInterval = setInterval(attemptConnect, AUTO_RECONNECT_DELAY); + } + } +} + +async function attemptConnect() { + const ip = obsIpInput.value || '127.0.0.1'; + const port = obsPortInput.value || '4455'; + const password = obsPasswordInput.value || 'dAJyC3vMggSchQPN'; + + localStorage.setItem('obsIp', ip); + localStorage.setItem('obsPort', port); + localStorage.setItem('obsPassword', password); + + updateConnectionStatus(false, true); + + try { + console.log('Attempting to connect to OBS WebSocket...'); + await obs.connect(`ws://${ip}:${port}`, password); + console.log('Connect method returned. OBS Connection Opened event will fire if successful.'); + updateConnectionStatus(true); + connectionDialog.classList.remove('show'); // Use class to hide dialog + } catch (error) { + console.error('Failed to connect to OBS:', error); + updateConnectionStatus(false); + } +} + +async function initializeOBSState() { + try { + const { studioModeEnabled } = await obs.call('GetStudioModeEnabled'); + if (!studioModeEnabled) { + await obs.call('SetStudioModeEnabled', { studioModeEnabled: true }); + console.log('Studio Mode enabled.'); + } else { + console.log('Studio Mode already enabled.'); + } + } catch (error) { + console.error('Error setting Studio Mode:', error); + } + + await updateSceneThumbnails(); + try { + const { currentProgramSceneName } = await obs.call('GetCurrentProgramScene'); + currentProgramScene = currentProgramSceneName; + highlightActiveThumbnail(currentProgramScene); + } catch (error) { + console.error('Error getting current program scene:', error); + } + + await updateAudioControls(); + await updateGroupMuteButtons(); // Update the state of group mute buttons +} + +// --- Event Listeners for Connection Dialog --- +connectDialogButton.addEventListener('click', () => { + connectionDialog.classList.add('show'); // Use class to show dialog +}); + +closeDialogButton.addEventListener('click', () => { + connectionDialog.classList.remove('show'); // Use class to hide dialog +}); + +connectButton.addEventListener('click', attemptConnect); + +// Load saved settings on page load +window.addEventListener('load', () => { + obsIpInput.value = localStorage.getItem('obsIp') || '192.168.0.124'; + obsPortInput.value = localStorage.getItem('obsPort') || '4455'; + obsPasswordInput.value = localStorage.getItem('obsPassword') || 'dAJyC3vMggSchQPN'; + attemptConnect(); +}); + +// --- OBS Event Handlers --- +obs.on('ConnectionOpened', async () => { + console.log('OBS Connection Opened Event Fired!'); + updateConnectionStatus(true); + + // ADD A SMALL DELAY HERE + setTimeout(async () => { + try { + await initializeOBSState(); // Keep this one here + console.log('OBS State fully initialized via ConnectionOpened event after delay.'); + } catch (error) { + console.error('Error during delayed OBS state initialization:', error); + } + }, 150); // Try 150ms delay. Adjust if needed (50-500ms) +}); + +obs.on('ConnectionClosed', () => { + console.log('OBS Connection Closed.'); + updateConnectionStatus(false); +}); + +obs.on('ConnectionError', (error) => { + console.error('OBS Connection Error:', error); + updateConnectionStatus(false); +}); + +obs.on('CurrentProgramSceneChanged', async (data) => { + console.log('Program Scene Changed to:', data.sceneName); + currentProgramScene = data.sceneName; + highlightActiveThumbnail(currentProgramScene); + // When scene changes, mute states might change. Re-evaluate and update group buttons. + await updateGroupMuteButtons(); +}); + +obs.on('CurrentPreviewSceneChanged', (data) => { + console.log('Preview Scene Changed to:', data.sceneName); + highlightActiveThumbnail(data.sceneName, true); +}); + +obs.on('SceneListChanged', async () => { + console.log('Scene List Changed. Updating thumbnails...'); + await updateSceneThumbnails(); +}); + +obs.on('InputVolumeMeters', (data) => { + data.inputs.forEach(input => { + const slider = document.querySelector(`.volume-slider[data-source="${input.inputName}"]`); + if (slider && !slider.classList.contains('dragging')) { + const volumePercent = Math.round(Math.pow(10, input.inputVolumeDb / 20) * 100); + slider.value = Math.max(0, Math.min(100, volumePercent)); + } + }); +}); + +obs.on('InputMuteStateChanged', async (data) => { + updateMuteButtonIcon(data.inputName, data.inputMuted); + // After individual mute state changes, re-evaluate group mute button states + await updateGroupMuteButtons(); +}); + + +// --- Scene Swapping Logic (same as before) --- +transitionButtons.forEach(button => { + button.addEventListener('click', async () => { + const transitionType = button.dataset.transition; + const duration = parseInt(button.dataset.duration); + const stingerName = button.dataset.stingerName; + + if (!obs.connected) { + alert('Not connected to OBS!'); + return; + } + + const sceneBeforeTransition = currentProgramScene; + + try { + await applyActionsForScene(sceneBeforeTransition, 'leaving'); + + if (transitionType === 'stinger' && stingerName) { + await obs.call('SetCurrentSceneTransition', { sceneTransitionName: stingerName }); + await obs.call('TransitionToProgram'); + } else { + await obs.call('SetCurrentSceneTransition', { + sceneTransitionName: transitionType + }); + if (duration !== undefined) { + await obs.call('SetCurrentSceneTransitionDuration', { + sceneTransitionDuration: duration + }); + } + await obs.call('TransitionToProgram'); + } + + console.log(`Transitioned with ${transitionType}`); + + } catch (error) { + console.error('Error during scene transition:', error); + alert(`Error performing transition: ${error.message}`); + } + }); +}); + +async function applyActionsForScene(sceneName, actionType) { + console.log(`Applying ${actionType} actions for scene: ${sceneName}`); + + switch (sceneName) { + case 'Roving': + if (actionType === 'leaving') { + await setInputMuteState('Room Mic', true); + await setInputMuteState('Music', true); + await setInputMuteState('Wireless', false); + await toggleRecording(true); + showAlert('Action: Set stream marker for Start of B-Roll / Interview'); + } + break; + case 'Room 1': + case 'Room 2': + if (actionType === 'leaving') { + await setInputMuteState('Wireless', true); + await setInputMuteState('Music', false); + await setInputMuteState('SoundDesk', false); + await toggleRecording(false); + showAlert('Action: Set marker - End of B-Roll / Interview'); + } + break; + case 'BRB / Break': + if (actionType === 'leaving') { + await muteAllMicsGroup(true); // Call the group mute function + } + break; + case 'Starting': + if (actionType === 'leaving') { + await muteAllMicsGroup(true); // Call the group mute function + await setInputMuteState('Alerts', false); + await setInputMuteState('Music', false); + } + break; + case 'END / Raiding': + if (actionType === 'leaving') { + await setInputMuteState('Music', true); + await setInputMuteState('SoundDesk', true); + await setInputMuteState('Wireless', true); + await setInputMuteState('Headset 1', false); + } + break; + default: + console.log(`No specific ${actionType} actions for scene: ${sceneName}`); + break; + } +} + + +// --- Audio Control Logic with new Mute/Unmute Buttons (same as before) --- +async function updateAudioControls() { + if (!obs.connected) return; + + audioControlsGrid.innerHTML = ''; + + for (const sourceName of ALL_AUDIO_SOURCES) { + const audioSourceDiv = document.createElement('div'); + audioSourceDiv.classList.add('audio-source'); + + const label = document.createElement('label'); + label.textContent = sourceName; + label.htmlFor = `volume-${sourceName.replace(/\s/g, '-')}`; + + const slider = document.createElement('input'); + slider.type = 'range'; + slider.id = `volume-${sourceName.replace(/\s/g, '-')}`; + slider.classList.add('volume-slider'); + slider.setAttribute('orient', 'vertical'); + slider.min = '0'; + slider.max = '100'; + slider.step = '1'; + slider.dataset.source = sourceName; + + const muteButton = document.createElement('button'); + muteButton.classList.add('mute-toggle-button'); + muteButton.dataset.source = sourceName; + + let iconHtml; + if (MIC_SOURCES.includes(sourceName)) { + iconHtml = micIcon; + } else if (MUSIC_SOURCES.includes(sourceName)) { + iconHtml = musicIcon; + } else if (ALERT_SOURCES.includes(sourceName)) { + iconHtml = alertIcon; + } else { + iconHtml = ''; + } + muteButton.innerHTML = iconHtml; + + audioSourceDiv.appendChild(label); + audioSourceDiv.appendChild(slider); + audioSourceDiv.appendChild(muteButton); + audioControlsGrid.appendChild(audioSourceDiv); + + try { + const { inputVolumeDb } = await obs.call('GetInputVolume', { inputName: sourceName }); + const volumePercent = Math.round(Math.pow(10, inputVolumeDb / 20) * 100); + slider.value = Math.max(0, Math.min(100, volumePercent)); + } catch (err) { + console.error(`Error getting volume for ${sourceName}:`, err); + } + + try { + const { inputMuted } = await obs.call('GetInputMute', { inputName: sourceName }); + updateMuteButtonIcon(sourceName, inputMuted); + } catch (err) { + console.error(`Error getting mute state for ${sourceName}:`, err); + } + } + + document.querySelectorAll('.volume-slider').forEach(slider => { + slider.addEventListener('mousedown', () => slider.classList.add('dragging')); + slider.addEventListener('mouseup', () => slider.classList.remove('dragging')); + slider.addEventListener('input', async (e) => { + const source = e.target.dataset.source; + const volume = parseFloat(e.target.value); + const volumeDb = 20 * Math.log10(Math.max(0.0001, volume / 100)); + try { + await obs.call('SetInputVolume', { inputName: source, inputVolumeDb: volumeDb }); + } catch (error) { + console.error(`Error setting volume for ${source}:`, error); + } + }); + }); + + document.querySelectorAll('.mute-toggle-button').forEach(button => { + button.addEventListener('click', async (e) => { + const source = e.currentTarget.dataset.source; + const isMuted = e.currentTarget.classList.contains('muted'); + await setInputMuteState(source, !isMuted); + }); + }); +} + +function updateMuteButtonIcon(sourceName, isMuted) { + const button = document.querySelector(`.mute-toggle-button[data-source="${sourceName}"]`); + if (!button) return; + + let iconToUse; + if (isMuted) { + if (MIC_SOURCES.includes(sourceName)) { + iconToUse = micMutedIcon; + } else if (MUSIC_SOURCES.includes(sourceName) || ALERT_SOURCES.includes(sourceName)) { + // Use muted music icon for both music and alerts, or make a separate alert muted icon + iconToUse = musicMutedIcon; + } else { + iconToUse = ''; + } + button.classList.add('muted'); + } else { + if (MIC_SOURCES.includes(sourceName)) { + iconToUse = micIcon; + } else if (MUSIC_SOURCES.includes(sourceName)) { + iconToUse = musicIcon; + } else if (ALERT_SOURCES.includes(sourceName)) { + iconToUse = alertIcon; + } else { + iconToUse = ''; + } + button.classList.remove('muted'); + } + button.innerHTML = iconToUse; +} + +async function setInputMuteState(sourceName, mute) { + if (!obs.connected) { + console.warn(`Not connected to OBS, cannot mute/unmute ${sourceName}.`); + return; + } + try { + await obs.call('SetInputMute', { inputName: sourceName, inputMuted: mute }); + console.log(`${sourceName} ${mute ? 'muted' : 'unmuted'}`); + // OBS will fire InputMuteStateChanged, which will then call updateMuteButtonIcon and updateGroupMuteButtons + } catch (error) { + console.error(`Error setting mute state for ${sourceName}:`, error); + } +} + +// --- Mute/Unmute Groups - NEW TOGGLE LOGIC --- +// Initial icon setup for group buttons +toggleAllMicsButton.innerHTML = groupMicsIcon + 'Mics'; +toggleNonMicsButton.innerHTML = groupNonMicsIcon + 'Other Audio'; + +toggleAllMicsButton.addEventListener('click', async (e) => { + const isMuted = e.currentTarget.classList.contains('muted'); // Check if currently showing muted state + await muteAllMicsGroup(!isMuted); // Toggle the state +}); + +toggleNonMicsButton.addEventListener('click', async (e) => { + const isMuted = e.currentTarget.classList.contains('muted'); + await muteNonMicsGroup(!isMuted); +}); + +async function muteAllMicsGroup(mute) { + // Determine the target state for all mics (either all mute or all unmute) + // Only execute if OBS is connected + if (!obs.connected) { + console.warn('Not connected to OBS, cannot mute/unmute group.'); + return; + } + + try { + for (const mic of MIC_SOURCES) { + await setInputMuteState(mic, mute); + } + console.log(`All mics group ${mute ? 'muted' : 'unmuted'}.`); + } catch (error) { + console.error('Error muting/unmuting mic group:', error); + } +} + +async function muteNonMicsGroup(mute) { + const nonMics = ALL_AUDIO_SOURCES.filter(source => !MIC_SOURCES.includes(source)); + if (!obs.connected) { + console.warn('Not connected to OBS, cannot mute/unmute group.'); + return; + } + + try { + for (const source of nonMics) { + await setInputMuteState(source, mute); + } + console.log(`All non-mic group ${mute ? 'muted' : 'unmuted'}.`); + } catch (error) { + console.error('Error muting/unmuting non-mic group:', error); + } +} + +// Function to update the visual state of the group mute buttons +async function updateGroupMuteButtons() { + if (!obs.connected) { + toggleAllMicsButton.classList.remove('muted'); + toggleAllMicsButton.innerHTML = groupMicsIcon + 'Mics'; + toggleNonMicsButton.classList.remove('muted'); + toggleNonMicsButton.innerHTML = groupNonMicsIcon + 'Other Audio'; + return; + } + + // Check state of all mic sources + let allMicsMuted = true; + for (const mic of MIC_SOURCES) { + try { + const { inputMuted } = await obs.call('GetInputMute', { inputName: mic }); + if (!inputMuted) { + allMicsMuted = false; + break; // If any mic is unmuted, the group is not fully muted + } + } catch (error) { + console.warn(`Could not get mute state for ${mic} (might not exist):`, error.message); + allMicsMuted = false; // Treat non-existent as unmuted for group status + break; + } + } + if (allMicsMuted) { + toggleAllMicsButton.classList.add('muted'); + toggleAllMicsButton.innerHTML = groupMicsMutedIcon + 'Mics'; + } else { + toggleAllMicsButton.classList.remove('muted'); + toggleAllMicsButton.innerHTML = groupMicsIcon + 'Mics'; + } + + // Check state of all non-mic sources + let allNonMicsMuted = true; + const nonMics = ALL_AUDIO_SOURCES.filter(source => !MIC_SOURCES.includes(source)); + for (const source of nonMics) { + try { + const { inputMuted } = await obs.call('GetInputMute', { inputName: source }); + if (!inputMuted) { + allNonMicsMuted = false; + break; + } + } catch (error) { + console.warn(`Could not get mute state for ${source} (might not exist):`, error.message); + allNonMicsMuted = false; + break; + } + } + if (allNonMicsMuted) { + toggleNonMicsButton.classList.add('muted'); + toggleNonMicsButton.innerHTML = groupNonMicsMutedIcon + 'Other Audio'; + } else { + toggleNonMicsButton.classList.remove('muted'); + toggleNonMicsButton.innerHTML = groupNonMicsIcon + 'Other Audio'; + } +} + + +// --- Recording Control (same as before) --- +async function toggleRecording(start) { + if (!obs.connected) { + console.warn('Not connected to OBS, cannot toggle recording.'); + return; + } + try { + const { outputActive } = await obs.call('GetRecordStatus'); + if (start && !outputActive) { + await obs.call('StartRecord'); + console.log('Recording started.'); + } else if (!start && outputActive) { + await obs.call('StopRecord'); + console.log('Recording stopped.'); + } else { + console.log(`Recording is already ${outputActive ? 'active' : 'inactive'}. No change.`); + } + } catch (error) { + console.error('Error toggling recording:', error); + } +} + +// --- Alerts (for Stream Markers) --- +function showAlert(message) { + alert(message); + console.log('Stream Marker Alert:', message); +} + + +// --- Scene Chooser (Studio Mode) (same as before) --- +async function updateSceneThumbnails() { + if (!obs.connected) return; + + try { + const { scenes } = await obs.call('GetSceneList'); + sceneThumbnailsContainer.innerHTML = ''; + + const { currentProgramSceneName } = await obs.call('GetCurrentProgramScene'); + const { currentPreviewSceneName } = await obs.call('GetCurrentPreviewScene'); + + for (const scene of scenes) { + const sceneName = scene.sceneName; + + const thumbnailDiv = document.createElement('div'); + thumbnailDiv.classList.add('scene-thumbnail'); + thumbnailDiv.dataset.sceneName = sceneName; + + const img = document.createElement('img'); + img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; + img.alt = sceneName; + img.style.transition = 'opacity 0.5s ease-in-out'; + + const span = document.createElement('span'); + span.textContent = sceneName; + + thumbnailDiv.appendChild(img); + thumbnailDiv.appendChild(span); + sceneThumbnailsContainer.appendChild(thumbnailDiv); + + if (sceneName === currentPreviewSceneName) { + thumbnailDiv.classList.add('active'); + } else if (sceneName === currentProgramSceneName) { + // Keep this empty, highlightActiveThumbnail handles un-previewing + } + + thumbnailDiv.addEventListener('click', async () => { + if (!obs.connected) return; + try { + await obs.call('SetCurrentPreviewScene', { sceneName: sceneName }); + console.log(`Set preview to: ${sceneName}`); + highlightActiveThumbnail(sceneName, true); + } catch (error) { + console.error(`Error setting preview scene to ${sceneName}:`, error); + } + }); + } + startThumbnailRefresh(); + + } catch (error) { + console.error('Error fetching scenes for thumbnails:', error); + } +} + +function highlightActiveThumbnail(sceneName, isPreview = false) { + document.querySelectorAll('.scene-thumbnail').forEach(thumb => { + if (isPreview) { + // If setting a preview, remove active from all others, then add to the target + if (thumb.dataset.sceneName === sceneName) { + thumb.classList.add('active'); + } else { + thumb.classList.remove('active'); + } + } + // No explicit handling for program scene in UI unless desired for a different visual cue + }); +} + + +let thumbnailRefreshInterval = null; +const THUMBNAIL_REFRESH_RATE = 1000; + +async function startThumbnailRefresh() { + if (thumbnailRefreshInterval) { + clearInterval(thumbnailRefreshInterval); + } + thumbnailRefreshInterval = setInterval(async () => { + if (!obs.connected) { + clearInterval(thumbnailRefreshInterval); + thumbnailRefreshInterval = null; + return; + } + try { + const { scenes } = await obs.call('GetSceneList'); + console.log('OBS Scenes List:', scenes); + for (const scene of scenes) { + const sceneName = scene.sceneName; + const thumbnailDiv = document.querySelector(`.scene-thumbnail[data-scene-name="${sceneName}"]`); + if (thumbnailDiv) { + const img = thumbnailDiv.querySelector('img'); + if (img) { + const { imageData } = await obs.call('GetSceneScreenshot', { + sceneName: sceneName, + imageFormat: 'png', + sourceWidth: 200, + sourceHeight: 112 + }); + img.style.opacity = '0'; + setTimeout(() => { + img.src = imageData; + img.style.opacity = '1'; + }, 100); + } + } + } + } catch (error) { + console.error('Error refreshing thumbnails:', error); + } + }, THUMBNAIL_REFRESH_RATE); +} + + +obs.on('ConnectionOpened', async () => { + console.log('OBS Connection Opened!'); + updateConnectionStatus(true); + await initializeOBSState(); +}); diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..830657a --- /dev/null +++ b/styles.css @@ -0,0 +1,319 @@ +body { + font-family: Arial, sans-serif; + background-color: #2c2c2c; + color: #f0f0f0; + margin: 0; + padding: 20px; + display: flex; + justify-content: center; + align-items: flex-start; + min-height: 100vh; +} + +.container { + background-color: #3a3a3a; + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); + width: 90%; + max-width: 800px; +} + +h1, h2 { + color: #e0e0e0; + text-align: center; + margin-bottom: 20px; +} + +.section { + margin-bottom: 30px; + padding: 15px; + background-color: #4a4a4a; + border-radius: 6px; +} + +/* Connection Status */ +.connection-status { + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + margin-bottom: 20px; +} + +.status-light { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #555; +} + +.status-light.green { background-color: #28a745; } /* Connected */ +.status-light.red { background-color: #dc3545; } /* Disconnected */ +.status-light.amber { background-color: #ffc107; } /* Auto Reconnect */ + +/* Dialog */ +.dialog { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: #3a3a3a; + padding: 30px; + border-radius: 10px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.5); + z-index: 1000; + text-align: center; + border: 1px solid #666; + + /* These are the crucial lines for initial hiding and transition */ + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.3s ease, visibility 0.3s ease; +} + +/* This is the class that makes it visible */ +.dialog.show { + opacity: 1; + visibility: visible; + pointer-events: all; +} + +.dialog label { + display: block; + margin-bottom: 8px; + font-weight: bold; +} + +.dialog input { + width: calc(100% - 20px); + padding: 10px; + margin-bottom: 15px; + border: 1px solid #555; + background-color: #2c2c2c; + color: #f0f0f0; + border-radius: 4px; +} + +/* Buttons */ +button { + background-color: #007bff; + color: white; + padding: 10px 15px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 16px; + margin: 5px; + transition: background-color 0.2s ease; + display: flex; /* For icons to be centered easily */ + align-items: center; + justify-content: center; + gap: 5px; /* Space between icon and text if any */ +} + +button:hover { + background-color: #0056b3; +} + +.dialog button { + width: 120px; +} + +.button-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 10px; + justify-content: center; +} + +.transition-button { + background-color: #28a745; /* Green for transitions */ +} +.transition-button:hover { + background-color: #218838; +} + +/* Audio Controls */ +.audio-controls-grid { + display: flex; /* Use flexbox for horizontal layout */ + gap: 20px; + justify-content: center; + flex-wrap: wrap; /* Allow wrapping for smaller screens */ +} + +.audio-source { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + padding: 10px; + border: 1px solid #555; + border-radius: 5px; + background-color: #3a3a3a; +} + +.volume-slider { + /* This style will attempt to make it vertical, but cross-browser support varies */ + -webkit-appearance: slider-vertical; /* For Chrome, Safari, Edge */ + writing-mode: bt-lr; /* For Firefox */ + height: 150px; /* Adjust height as needed */ + width: 15px; /* Adjust width as needed */ + background: #555; + border-radius: 5px; + outline: none; + transition: opacity .2s; + margin-bottom: 10px; +} + +.volume-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 25px; + height: 25px; + border-radius: 50%; + background: #007bff; + cursor: pointer; +} + +.volume-slider::-moz-range-thumb { + width: 25px; + height: 25px; + border-radius: 50%; + background: #007bff; + cursor: pointer; +} + +/* Mute Toggle Button - Now with SVG */ +.mute-toggle-button { + width: 60px; /* Make it square or adjust as needed for icon */ + height: 60px; + padding: 0; /* Remove padding as content is an SVG */ + background-color: #555; /* Default background */ + border: 2px solid #666; +} + +.mute-toggle-button:hover { + background-color: #777; +} + +.mute-toggle-button svg { + width: 30px; /* Size of the SVG icon */ + height: 30px; + transition: fill 0.2s ease, transform 0.1s ease; +} + +/* Muted State Styles */ +.mute-toggle-button.muted { + background-color: #dc3545; /* Red background when muted */ + border-color: #bb2d3b; +} + +.mute-toggle-button.muted:hover { + background-color: #bb2d3b; +} + +/* SVG Fill Colors (active/unmuted vs muted) */ +.mute-toggle-button svg path { + fill: #28a745; /* Green for unmuted (default) */ + transition: fill 0.2s ease; +} + +.mute-toggle-button.muted svg path { + fill: white; /* White for muted icons, contrasts with red background */ +} + +/* Scene Thumbnails */ +.scene-thumbnails-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + justify-content: center; +} + +.scene-thumbnail { + background-color: #555; + border: 2px solid #666; + border-radius: 8px; + overflow: hidden; + cursor: pointer; + transition: all 0.3s ease; + text-align: center; + padding-bottom: 10px; /* Space for text */ +} + +.scene-thumbnail:hover { + border-color: #007bff; + box-shadow: 0 0 15px rgba(0, 123, 255, 0.5); + transform: translateY(-3px); +} + +.scene-thumbnail.active { + border-color: #28a745; /* Green border for the active (preview) scene */ + box-shadow: 0 0 15px rgba(40, 167, 69, 0.7); +} + +.scene-thumbnail img { + width: 100%; + height: auto; + display: block; + border-bottom: 1px solid #666; +} + +.scene-thumbnail span { + display: block; + padding-top: 8px; + font-size: 1.1em; + font-weight: bold; + color: #e0e0e0; +} + +.mute-groups-grid { + display: flex; /* Use flexbox for layout */ + justify-content: center; + gap: 20px; + flex-wrap: wrap; + margin-top: 20px; +} + +.group-mute-toggle { + width: 120px; /* Adjust size as needed */ + height: 100px; /* Adjust size as needed */ + padding: 0; + flex-direction: column; /* Stack icon and text */ + background-color: #6c757d; /* Default gray for group buttons */ + border: 2px solid #555; + font-size: 1.1em; + font-weight: bold; +} + +.group-mute-toggle:hover { + background-color: #5a6268; +} + +.group-mute-toggle svg { + width: 40px; /* Larger icon for group buttons */ + height: 40px; + margin-bottom: 5px; /* Space between icon and text */ + transition: fill 0.2s ease; +} + +/* Muted state for group buttons */ +.group-mute-toggle.muted { + background-color: #dc3545; /* Red when group is muted */ + border-color: #bb2d3b; +} + +.group-mute-toggle.muted:hover { + background-color: #bb2d3b; +} + +.group-mute-toggle svg path { + fill: white; /* White icon fill (contrast with button color) */ + transition: fill 0.2s ease; +} + +.group-mute-toggle.unmuted { + background-color: #28a745; +}