diff --git a/server/2015Remote/2015Remote.rc b/server/2015Remote/2015Remote.rc
index 5ae5c9d..5ef24ff 100644
Binary files a/server/2015Remote/2015Remote.rc and b/server/2015Remote/2015Remote.rc differ
diff --git a/server/2015Remote/2015Remote_vs2015.vcxproj b/server/2015Remote/2015Remote_vs2015.vcxproj
index 36065d0..09b9d69 100644
--- a/server/2015Remote/2015Remote_vs2015.vcxproj
+++ b/server/2015Remote/2015Remote_vs2015.vcxproj
@@ -266,6 +266,7 @@
+
diff --git a/server/2015Remote/2015Remote_vs2015.vcxproj.filters b/server/2015Remote/2015Remote_vs2015.vcxproj.filters
index d6ffe7d..f4fc3a9 100644
--- a/server/2015Remote/2015Remote_vs2015.vcxproj.filters
+++ b/server/2015Remote/2015Remote_vs2015.vcxproj.filters
@@ -232,6 +232,7 @@
+
diff --git a/server/2015Remote/WebService.cpp b/server/2015Remote/WebService.cpp
index b8f3599..e31bd5e 100644
--- a/server/2015Remote/WebService.cpp
+++ b/server/2015Remote/WebService.cpp
@@ -255,6 +255,7 @@ void CWebService::ServerThread(int port) {
static std::string cachedXtermJs = LoadBinaryResourceAsString(IDR_WEB_XTERM_JS);
static std::string cachedXtermCss = LoadBinaryResourceAsString(IDR_WEB_XTERM_CSS);
static std::string cachedXtermFitJs = LoadBinaryResourceAsString(IDR_WEB_XTERM_FIT_JS);
+ static std::string cachedOpusDecoderJs = LoadBinaryResourceAsString(IDR_WEB_OPUS_DECODER_JS);
auto buildStatic = [](const std::string& body, const std::string& mime) {
ws::HttpResponse r = ws::HttpResponse::OK(body, mime);
@@ -274,6 +275,9 @@ void CWebService::ServerThread(int port) {
} else if (path == "/static/xterm-fit.js") {
if (cachedXtermFitJs.empty()) return ws::HttpResponse::NotFound();
return buildStatic(cachedXtermFitJs, "application/javascript; charset=utf-8");
+ } else if (path == "/static/opus-decoder.min.js") {
+ if (cachedOpusDecoderJs.empty()) return ws::HttpResponse::NotFound();
+ return buildStatic(cachedOpusDecoderJs, "application/javascript; charset=utf-8");
} else if (path == "/health") {
return ws::HttpResponse::OK("{\"status\":\"ok\"}", "application/json");
} else if (path == "/manifest.json") {
diff --git a/server/2015Remote/res/web/opus-decoder.min.js b/server/2015Remote/res/web/opus-decoder.min.js
new file mode 100644
index 0000000..e2a7117
--- /dev/null
+++ b/server/2015Remote/res/web/opus-decoder.min.js
@@ -0,0 +1,232 @@
+/* Copyright 2021-2026 Ethan Halsall. This file is part of wasm-audio-decoders. https://github.com/eshaz/wasm-audio-decoders */
+var i,t;i=this,t=function(i,t){const e=(i,t=4294967295,e=79764919)=>{const H=new Int32Array(256);let s,h,r,n=t;for(s=0;s<256;s++){for(r=s<<24,h=8;h>0;--h)r=2147483648&r?r<<1^e:r<<1;H[s]=r}for(s=0;s>24^i[s])];return n},H=(i,t=e)=>{const H=i=>new Uint8Array(i.length/2).map((t,e)=>parseInt(i.substring(2*e,2*(e+1)),16)),s=i=>H(i)[0],h=new Map;[,8364,,8218,402,8222,8230,8224,8225,710,8240,352,8249,338,,381,,,8216,8217,8220,8221,8226,8211,8212,732,8482,353,8250,339,,382,376].forEach((i,t)=>h.set(i,t));const r=new Uint8Array(i.length);let n,M,o,a=!1,c=0,I=42,u=i.length>13&&"dynEncode"===i.substring(0,9),y=0;u&&(y=11,M=s(i.substring(9,y)),M<=1&&(y+=2,I=s(i.substring(11,y))),1===M&&(y+=8,o=(i=>new DataView(H(i).buffer).getInt32(0,!0))(i.substring(13,y))));const l=256-I;for(let e=y;e255){const i=h.get(n);i&&(n=i+127)}a&&(a=!1,n-=64),r[c++]=n0?n+l:n-I}else a=!0;const d=r.subarray(0,c);if(u&&1===M){const i=t(d);if(i!==o){const t="Decode failed crc32 validation";throw console.error("`simple-yenc`\n",t+"\n","Expected: "+o+"; Got: "+i+"\n","Visit https://github.com/eshaz/simple-yenc for more information"),Error(t)}}return d};function s(){const i=Uint8Array,t=Float32Array;s.t||Object.defineProperties(s,{t:{value:new WeakMap},H:{value(i,t){s.t.set(i,Promise.resolve(t))}},M:{value(i,t){let e=s.t.get(i);return e||(t?e=WebAssembly.compile(H(t)):(t=i.I,e=s.u(t).then(i=>WebAssembly.compile(i))),s.t.set(i,e)),e}},p:{value(i,e){let H=new t(e),s=0,h=0;for(;s({errors:i,channelData:t,samplesDecoded:e,sampleRate:H,bitDepth:s})},W:{value(i,t,e,H,h,r){let n,M,o=[];for(n=0;n{const H=String.raw`dynEncode012804c7886d()((()>+*§§)§,§§§§)§+§§§)§+.-()(*)-+)(.7*§)i¸¸,3§(i¸¸,3/G+.¡*(,(,3+)2å:-),§H(P*DI*H(P*@I++hH)H*r,hH(H(P*WebAssembly.instantiate(i,{})).then(({exports:H})=>{const s=new Map(Object.entries(H)),h=s.get("puff"),r=s.get("memory").buffer,n=new i(r),M=new DataView(r);let o=s.get("__heap_base");const a=t.length,c=o;o+=4,M.setInt32(c,a,!0);const I=o;o+=a,n.set(t,I);const u=o;o+=4,M.setInt32(u,n.byteLength-o,!0),h(o,u,I,c),e(n.slice(o,o+M.getInt32(u,!0)))})})}}}),Object.defineProperty(this,"I",{enumerable:!0,get:()=>this.T}),this.P=(i,t,e)=>{let H=[],s=0;for(;s{const H=this.T.v(t.BYTES_PER_ELEMENT*i);return e&&this.A.add(H),{Y:H,N:i,F:new t(this.T.O,H,i)}},this.free=()=>{this.A.forEach(i=>{this.T.free(i)}),this.A.clear()},this.S=i=>{const t=[],e=new Uint8Array(this.T.O);for(let H=e[i];0!==H;H=e[++i])t.push(H);return String.fromCharCode.apply(null,t)},this.D=(i,t,e,H,s,h)=>{i.push({message:t,frameLength:e,frameNumber:H,inputBytes:s,outputSamples:h})},this.instantiate=(i,t)=>(t&&s.H(i,t),this.T=new i(s).instantiate(),this.A=new Set,this.T.ready.then(()=>this))}class h extends((()=>globalThis.Worker||t)()){constructor(i,t,e,H){s.t||new s;let h=s.t.get(e);if(!h){let i,t="text/javascript",r=`'use strict';(${""+((i,t,e)=>{let H,s,h=new Promise(i=>{s=i});self.onmessage=({data:{id:r,command:n,data:M}})=>{let o,a=h,c={id:r};"init"===n?(Object.defineProperties(i,{V:{value:t},$:{value:e},module:{value:M.module},G:{value:!0}}),H=new i(M.options),s()):"free"===n?H.free():"ready"===n?a=a.then(()=>H.ready):"reset"===n?a=a.then(()=>H.reset()):(Object.assign(c,H[n](Array.isArray(M)?M.map(i=>new Uint8Array(i)):new Uint8Array(M))),o=c.channelData?c.channelData.map(i=>i.buffer):[]),a.then(()=>self.postMessage(c,o))}})})(${e}, ${s}, ${H})`;try{i=void 0!==process.versions.node}catch{}h=i?`data:${t};base64,${Buffer.from(r).toString("base64")}`:URL.createObjectURL(new Blob([r],{type:t})),s.t.set(e,h)}super(h,{name:t}),this.L=Number.MIN_SAFE_INTEGER,this.B=new Map,this.onmessage=({data:i})=>{const{id:t,...e}=i;this.B.get(t)(e),this.B.delete(t)},new H(s).M().then(t=>{this.X("init",{module:t,options:i})})}async X(i,t){return new Promise(e=>{this.postMessage({command:i,id:this.L,data:t}),this.B.set(this.L++,e)})}get ready(){return this.X("ready")}async free(){await this.X("free").finally(()=>{this.terminate()})}async reset(){await this.X("reset")}}const r=(i,t)=>{Object.defineProperty(i,"name",{value:t})};function n(i){function t(){}for(var e,H,s={},h=Math.cos,r=new Uint8Array(123),M=25;M>=0;--M)r[48+M]=52+M,r[65+M]=M,r[97+M]=26+M;r[43]=62,r[47]=63;var o,a,c,I,u,y,l={d:()=>(()=>{throw""})(),c(){},e(i,t){if(s[i]&&(clearTimeout(s[i].id),delete s[i]),!t)return 0;var e=setTimeout(()=>{delete s[i],y(i,performance.now())},t);return s[i]={id:e,Z:t},0},a:h,f:i=>(e.length,!1),b(i){throw`exit(${i})`}};n.I||Object.defineProperty(n,"I",{get:()=>String.raw`dynEncode013bc58350c1Éð|@|O*XØ3R}@³ùÆöÁ¤½CuÙFõ¶=}VònоÑX³W~aGÕ5?Q= Ú
Î
N= WeǸß3í=MÛßǹHÜjLvïêÀ:m°¹30>ÉX!¿býOýn
+®Fj{øú¢ç=}(ÝñÜn moǰ¥àeò?È×µh
+ØW¨;Ñ aØXæU{¤øàÖÕÀì·ÝÌíø±tâÔøeVO÷PÉ´È~ÃòsX1´ÊPyK·ÿKZçµ WÑ¡¡_ dÍí+]Õ~%Yïþîñ ¾Ú·p[òß ¶ó÷ylhnn«²ù*¶Ìéñ®ï(éÇÃRÊÆRÌ-¬ÐoÒÊ2z+ä$:ÄsxÏ#Ït!MlBq§%Jè ®Ä 3ó°á¸eÅ_Ö=}iTÐÍmoðbMpl¬EP°e,¼Æju®ix>#}NgdÿÇÄbç9WZÇ·n\Öá1ÚÈt.ú6bì:ûào¯ú4Ïø üzå¯#ÆkM#úÿ9ë:üÒÝëÚà¡d%0ÒekÓ×mðÆ¸½ùÿâ¿Ò9 ÄgªÕÂ%ü¸» HÁM#Âq²~èn¯Ûa_9j= ùÿRèæÊó%M
+¨,Àæq%98a9% T2)´z«9gsÒÖ!ZhRõRÛyÕ²Ý=M<Öcº »~VFÕÖx:þrÞðn£x¦8.è$K9þ¹úSnÍ3¦v
üuºsÕñ=}1=M5ZʦCÿSÉ2×qÒ««Sa»ù±(¢º]ÀmÍh÷ /¥3@sns2 %7ù-Q|nÏoöjYÛ]}à2{EBêÎ6>ÁbÄü
+á, [DAÐuùjeNDEëÔàZºlq» J/˲ájºßCÃÝ1b)øtN7Ê=}gÕ®òÍá§8² kDïû¤åÒÅóºc=M²= aË.ooèf.QZj@bñ>qV©?æxñRô|_xwê¸÷È¡ö"C¶2[¼ï¯¸©KmäºúZ
¡%^°bóþ»-¼Ú¥ZI(pþ
µS%øµã*Ò±8f:t¡éþõz¶<Äfñ*öeP8AÓ¾/g×\С1Ï"= ¤ûGsW_¹ÔÈX TãàuTíEKPüݸ#åB¢ñ?:µßĬîFI8QY;X
¶í¦k{|¦éíà/fªi>´EÈoK= 6¼lÖr²ÿ*YÇvÄdàG{Ö,Gz½.gzSdïV%
z¥8¶²bO¡2\nOßV¡S2KçK´UûuÛã~f¾K)ÊÖ#´?#»ÅYXOøò TCB}LUk,kÍ)]íiE^ÿyW)Ê»³= ô1_½úÌÞÊçýS¡
+»£jÆkhÐ1G<è^àÙç?
æF¦Ç/ã«Eý6ZI¹tl.aëül|!&:MÓå?N@øËÅf 6cì$w,3~17F=M𸳰aõu\9æk I&>V®Æ/Øa´ %¶¢],ÿ:±wäé¬áS¥FK±U¢±ÓPóß&®e= ÍÍܯÏ×À@= Ø#Ðk[NCó÷ÀÄÞGQRcnI}HGñ\ç= öè^áygyDÔa©D¦
'ߣMߣPI¯à¨=}=MUÀ¸çy 9W½LíU·ôzwzÃsÝZòÓ3F¸Áè£âÑnÕ%Í¥k
á®=M×GTÀÊøfëM-UF;uåÙ#
X#
+n©ïÖDÅñ¸q ,Թи;h¿åñ#µNÓ÷¸v} *Ϊ_= ãýØb6N=MH§'®Iu¯78ªÊÝ#42p´[/¿E= =MxåÄV§Ým©3?;bô9fb÷Ð'¨ÈIS7ñ>¬IÍÜK tçÖJ 7û6ߥA/|BÏ=Mõªw¿T¼91ó íÒèÔ_6»Âõªvg2ý¡õ÷Ò=}Å3ýXm²¬bÛ}U¬0¸Ük~e
+aY¬ÈÛµ¨¢0Nñ¬t³VÉhÖYvx§"Ç5\N'ªÂ£æKlÂ?þ{m¼ôê±ñÔ|Lm¬t_2%¤E}Ù>ÎFûalSXø²tRªÀ8¥£Å|ÖW(Ù)\3Úwåì:oíý±Y4ì©géµC³¨Q\ÍõMÕ
+!J½6ë:Ëg ípoÒ=}I=}ëS¯¸
F=}ÙT__U¾&\íÛ ¼Âkg#úÚ¡ÀsÏÔTµ³ÑËßxNÉæGç^4Ñ¡%.¦U±O[ø¼4 pͺ7ÜâªcÔ¦¢:ou¹8ÌzîXf¬56DC´Þ]Ó!=MOÀÕâGÖÓ«0¤¿Å2ú
+éηNóÚÄr_Z)6y&2^F|#²ì36eòa¦rcLõ5Hö