QEMU-VNC erscheint live in der App-Desktop-Kachel, komplett ueber RVS: - Bridge: TCP↔RVS-VNC-Bruecke. vnc_open oeffnet asyncio-Verbindung zu host.docker.internal:5901, Reader-Loop streamt RFB-Bytes als vnc_data; vnc_input schreibt zurueck; vnc_close raeumt auf. check_desktop probt den Port und meldet desktop_status. Base64-in-JSON, kein websockify/noVNC auf dem Host noetig. - App: novncHtml.ts laedt noVNC (CDN) und ersetzt window.WebSocket durch einen Shim, der RFB-Bytes per postMessage ueber RVS brueckt (server-speaks-first → timing-robust). VncTile mountet die WebView nur im Fokus, oeffnet bei 'ready' den Tunnel (desktop.openVnc), speist Server-Bytes ein und schickt Eingaben als vnc_input; beim Verlassen wird der Tunnel geschlossen (VM laeuft weiter). tsc/py clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
98 lines
4.3 KiB
TypeScript
98 lines
4.3 KiB
TypeScript
/**
|
|
* novncHtml — noVNC-Client fuer die WebView, dessen WebSocket durch den
|
|
* RVS-Tunnel gebrueckt wird.
|
|
*
|
|
* Trick: window.WebSocket wird VOR dem Laden von noVNC durch einen Shim
|
|
* ersetzt. noVNC (RFB) glaubt, ein echtes WebSocket zu benutzen; tatsaechlich
|
|
* gehen die RFB-Bytes als Base64 per postMessage an RN → RVS → Bridge → QEMU
|
|
* (und zurueck). Da RFB "server-speaks-first" ist, ist die Reihenfolge robust.
|
|
*
|
|
* noVNC wird vom CDN geladen (das Telefon hat Internet, da es ohnehin am RVS
|
|
* haengt). Voll-offline-Bundling waere ein spaeterer Schritt.
|
|
*
|
|
* Protokoll:
|
|
* RN -> WebView window.ariaVnc.onData(b64) RFB-Bytes vom Server
|
|
* WebView -> RN {event:'ready'} RFB initialisiert → Tunnel oeffnen
|
|
* {event:'vnc_send', b64} RFB-Bytes an den Server
|
|
* {event:'vnc_close'} RFB hat geschlossen
|
|
* {event:'vnc_state', state} connected|disconnected
|
|
*/
|
|
|
|
export const NOVNC_HTML = `<!doctype html><html><head><meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
|
<style>
|
|
* { margin:0; padding:0; }
|
|
html, body { height:100%; background:#000; overflow:hidden; }
|
|
#screen { width:100%; height:100%; }
|
|
#msg { position:absolute; top:8px; left:0; right:0; text-align:center;
|
|
color:#9090B0; font-family:sans-serif; font-size:12px; pointer-events:none; }
|
|
</style></head><body>
|
|
<div id="screen"></div>
|
|
<div id="msg">Verbinde mit Desktop …</div>
|
|
<script>
|
|
(function(){
|
|
function post(o){ if(window.ReactNativeWebView) window.ReactNativeWebView.postMessage(JSON.stringify(o)); }
|
|
function b64FromBytes(bytes){
|
|
var CHUNK=0x8000, parts=[];
|
|
for(var i=0;i<bytes.length;i+=CHUNK){ parts.push(String.fromCharCode.apply(null, bytes.subarray(i,i+CHUNK))); }
|
|
return btoa(parts.join(''));
|
|
}
|
|
function bytesFromB64(b64){
|
|
var s=atob(b64), a=new Uint8Array(s.length);
|
|
for(var i=0;i<s.length;i++) a[i]=s.charCodeAt(i);
|
|
return a;
|
|
}
|
|
|
|
// --- WebSocket-Shim ---
|
|
function BridgeSocket(url, protocols){
|
|
this.url=url; this.protocol=''; this.readyState=0; this.binaryType='arraybuffer';
|
|
this.onopen=null; this.onclose=null; this.onerror=null; this.onmessage=null;
|
|
var self=this; window.__vncSocket=self;
|
|
setTimeout(function(){ self.readyState=1; if(self.onopen) self.onopen({type:'open'}); }, 0);
|
|
}
|
|
BridgeSocket.CONNECTING=0; BridgeSocket.OPEN=1; BridgeSocket.CLOSING=2; BridgeSocket.CLOSED=3;
|
|
BridgeSocket.prototype.send=function(data){
|
|
var bytes;
|
|
if(data instanceof ArrayBuffer) bytes=new Uint8Array(data);
|
|
else if(ArrayBuffer.isView(data)) bytes=new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
else bytes=new Uint8Array(0);
|
|
post({event:'vnc_send', b64:b64FromBytes(bytes)});
|
|
};
|
|
BridgeSocket.prototype.close=function(){
|
|
if(this.readyState===3) return;
|
|
this.readyState=3; if(this.onclose) this.onclose({type:'close'}); post({event:'vnc_close'});
|
|
};
|
|
BridgeSocket.prototype.addEventListener=function(t,fn){ this['on'+t]=fn; };
|
|
BridgeSocket.prototype.removeEventListener=function(t){ this['on'+t]=null; };
|
|
window.WebSocket = BridgeSocket;
|
|
|
|
// Eingehende Server-Bytes → in den Shim einspeisen.
|
|
window.ariaVnc = {
|
|
onData:function(b64){
|
|
var sock=window.__vncSocket;
|
|
if(!sock || !sock.onmessage) return;
|
|
sock.onmessage({ type:'message', data: bytesFromB64(b64).buffer });
|
|
}
|
|
};
|
|
|
|
var msg=document.getElementById('msg');
|
|
// noVNC per ESM vom CDN laden.
|
|
import('https://cdn.jsdelivr.net/npm/@novnc/novnc@1.4.0/core/rfb.js').then(function(mod){
|
|
var RFB = mod.default;
|
|
var rfb = new RFB(document.getElementById('screen'), 'ws://aria-vnc/', {});
|
|
rfb.scaleViewport = true;
|
|
rfb.clipViewport = false;
|
|
rfb.addEventListener('connect', function(){ msg.style.display='none'; post({event:'vnc_state', state:'connected'}); });
|
|
rfb.addEventListener('disconnect', function(e){
|
|
msg.style.display='block'; msg.textContent='Desktop getrennt';
|
|
post({event:'vnc_state', state:'disconnected'});
|
|
});
|
|
window.__rfb = rfb;
|
|
post({event:'ready'});
|
|
}).catch(function(err){
|
|
msg.textContent='noVNC konnte nicht geladen werden (Internet?)';
|
|
post({event:'vnc_state', state:'error', error:String(err)});
|
|
});
|
|
})();
|
|
</script></body></html>`;
|