Bash script for a vnc wine64 in debian. needs smoothing.

Jayson T Tolleson

New Member
Joined
Jun 16, 2025
Messages
5
Reaction score
0
Credits
41
Hello all! I am excited to have joined this forum.
I am Jayson.
I have two scripts here (huge) and full.
one tests for running a url on the web of a wine program.
one is the 'BROADCAST W/ ABLETON', a DAW being broadcast with video stream for web-DJ.
Anyhow,
I cannot get a url to populate with noVNC yet...i am so close. When i achieve the 1st script i will use it to um 'power' the end goal.....the broadcaster.

GENERAL IDEA:
Code:
Xvfb :0 -screen 0 1280x800x24 &
export DISPLAY=:0
xdpyinfo -display :0
openbox-session &
lxpanel &
wine64 notepad.exe
xterm &
x11vnc -display :0 -noxdamage -forever -shared -bg -nopw -rfbport 5900
lsof -i :5900
./utils/novnc_proxy --vnc localhost:5900 --listen 8082 --cert /path/to/cert.pem --key /path/to/key.pem &

Any Help diagnosing will be apreciated, or a safe run of some code....to get on track.


here we go.
script 1.

Bash:
#!/bin/bash
set -e
# === Config Variables ===
LOGFILE="/var/log/novnc_setup.log"
USER="jayson_tolleson"
USER_HOME="/home/$USER"
SSH_DIR="$USER_HOME/.ssh"
AUTH_KEYS="$SSH_DIR/authorized_keys"
KEY_FILE="$SSH_DIR/id_ed25519"
DISPLAY_VAR=":0"
export DISPLAY=$DISPLAY_VAR
XDG_RUNTIME_DIR="/tmp/xdg-runtime-$USER"
CERT_PATH="$USER_HOME/security/fullchain.pem"
KEY_PATH="$USER_HOME/security/key.pem"
# === Logging ===
echo "==== Script started at $(date) ====" | tee -a "$LOGFILE"
echo " Using virtual display $DISPLAY_VAR" | tee -a "$LOGFILE"
# === Install Dependencies ===
echo " Installing required packages..." | tee -a "$LOGFILE"
sudo wget -NP /etc/apt/sources.list.d/ https://dl.winehq.org/wine-builds/debian/dists/bookworm/winehq-bookworm.sources
sudo apt update
sudo apt install -y --install-recommends winehq-stable
sudo apt install -y wine64 fonts-wine curl unzip pulseaudio openbox xvfb x11vnc \
                    pavucontrol lsof alsa-utils lxde-core openssh-server dbus-x11 lxterminal python3-xdg cabextract
sudo apt remove light-locker
sudo sed -i '/light-locker/d' /etc/xdg/lxsession/LXDE/autostart
export XDG_RUNTIME_DIR="/tmp/xdg-runtime-$USER"
mkdir -p "$XDG_RUNTIME_DIR"
chmod 700 "$XDG_RUNTIME_DIR"
chown "$USER:$USER" "$XDG_RUNTIME_DIR"
# === Install Winetricks ===
if ! command -v winetricks &> /dev/null; then
    echo " Installing winetricks..." | tee -a "$LOGFILE"
    sudo curl -o /usr/local/bin/winetricks https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks
    sudo chmod +x /usr/local/bin/winetricks
    sudo -u "$USER" winetricks ole32
fi
# === SSH Key Generation ===
echo " Setting up SSH keys..." | tee -a "$LOGFILE"
sudo mkdir -p "$SSH_DIR"
sudo chown -R "$USER:$USER" "$SSH_DIR"
sudo chmod 700 "$SSH_DIR"
if [ ! -f "$KEY_FILE" ]; then
    echo " Generating new SSH key for $USER..." | tee -a "$LOGFILE"
    sudo -u "$USER" ssh-keygen -t ed25519 -C "[email protected]" -f "$KEY_FILE" -N ""
fi
PUBKEY=$(sudo cat "$KEY_FILE.pub")
echo "$PUBKEY" | sudo tee "$AUTH_KEYS" > /dev/null
sudo chown "$USER:$USER" "$AUTH_KEYS"
sudo chmod 600 "$AUTH_KEYS"
echo "✅ SSH key installed to $AUTH_KEYS" | tee -a "$LOGFILE"
# === Start SSH Server ===
echo " Starting SSH server..." | tee -a "$LOGFILE"
sudo systemctl enable ssh
sudo systemctl restart ssh
echo "✅ SSH server active." | tee -a "$LOGFILE"
# === Start Xvfb ===
if xdpyinfo -display $DISPLAY_VAR >/dev/null 2>&1; then
    echo "️ Xvfb already running on $DISPLAY_VAR" | tee -a "$LOGFILE"
else
    echo " Starting Xvfb on $DISPLAY_VAR..." | tee -a "$LOGFILE"
    Xvfb $DISPLAY_VAR -screen 0 1280x800x24 &
    sleep 2
fi
# Create Openbox config directory if it doesn't exist
sudo mkdir -p /var/lib/openbox

# Create the debian-menu.xml file with a minimal Openbox menu root
echo '<openbox_menu xmlns="http://openbox.org/3.4/menu"/>' | sudo tee /var/lib/openbox/debian-menu.xml > /dev/null

echo "✅ /var/lib/openbox/debian-menu.xml created."
mkdir -p ~/.config/openbox
sudo rm -f ~/.config/openbox/menu.xml
# Write a minimal Openbox menu.xml file
cat > ~/.config/openbox/menu.xml << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<openbox_menu xmlns="http://openbox.org/3.4/menu">
  <menu id="root-menu" label="Openbox">
    <item label="Terminal">
      <action name="Execute">
        <command>lxterminal</command>
      </action>
    </item>
    <item label="Web Browser">
      <action name="Execute">
        <command>firefox</command>
      </action>
    </item>
    <separator />
    <item label="Exit">
      <action name="Exit"/>
    </item>
  </menu>
</openbox_menu>
EOF

# Reload Openbox configuration if Openbox is running
if pgrep openbox >/dev/null; then
  openbox --reconfigure
fi
echo "✅ Openbox menu.xml created and reloaded (if Openbox is running)."
# === Start minimal desktop (Openbox + panel + desktop icons) ===
if ! pgrep -x "openbox-session" > /dev/null; then
  echo " Starting Openbox session + panel + desktop…" | tee -a "$LOGFILE"
  sudo -u "$USER" env HOME="$USER_HOME" XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" DISPLAY=$DISPLAY_VAR dbus-launch --exit-with-session openbox-session &
  sleep 2
  sudo -u "$USER" lxpanel &
  sleep 1
  sudo -u "$USER" pcmanfm --desktop --profile LXDE &
  sleep 2
fi
# === Start PulseAudio ===
if ! pgrep -x "pulseaudio" > /dev/null; then
    echo " Starting PulseAudio..." | tee -a "$LOGFILE"
    export PULSE_RUNTIME_PATH="/run/user/$(id -u $USER)/pulse"
    sudo groupadd -f pulse-access
    sudo usermod -aG pulse-access "$USER"
    sudo -u "$USER" env HOME="$USER_HOME" XDG_RUNTIME_DIR="$XDG_RUNTIME_DIR" DISPLAY=$DISPLAY_VAR pulseaudio --start --disallow-exit --disable-shm &
    sleep 2
fi
# === Wine Initialization ===
echo " Initializing Wine..." | tee -a "$LOGFILE"
sudo -u "$USER" wineboot --init
sudo -u "$USER" winetricks sound=alsa
# === Start Wine App ===
if pgrep -x "wine" > /dev/null; then
   echo " Wine already running." | tee -a "$LOGFILE"
else
    echo " Launching Wine Notepad..." | tee -a "$LOGFILE"
    sudo -u "$USER" DISPLAY=$DISPLAY_VAR wine notepad.exe &
    sleep 2
fi
# === Start x11vnc ===
if lsof -i:5900 >/dev/null; then
    echo " x11vnc already on port5900" | tee -a "$LOGFILE"
else
    echo " Starting x11vnc on $DISPLAY_VAR..." | tee -a "$LOGFILE"
    x11vnc -display $DISPLAY_VAR -noxdamage -nopw -forever -shared -rfbport 5900 -listen 0.0.0.0 &
    sleep 2
fi
# === Start noVNC with SSL ===
cd /usr/share/novnc
if lsof -i:8082 >/dev/null; then
    echo " noVNC already running on port 8082" | tee -a "$LOGFILE"
else
    echo " Launching noVNC with SSL..." | tee -a "$LOGFILE"
    ./utils/novnc_proxy \
        --vnc localhost:5900 \
        --listen 8082 \
        --cert "$CERT_PATH" \
        --key "$KEY_PATH" &
    sleep 2
fi
# === Done ===
echo "✅ noVNC running!" | tee -a "$LOGFILE"
echo " Access VNC via browser or client:"
echo "   ➤ http://localhost:8082/vnc.html -(without cert n key)" | tee -a "$LOGFILE"
echo "   ➤ https://$(hostname -f):8082/vnc.html"  | tee -a "$LOGFILE"
echo "   ➤ VNC Viewer: $(hostname -I | awk '{print $1}'):5900" | tee -a "$LOGFILE"
echo "==== Script completed at $(date) ====" | tee -a "$LOGFILE"

script 2....the real shebang:
Python:
from gevent import monkey
monkey.patch_all()
import ssl
import os
import subprocess
import time
from flask import Flask, Response, request, render_template, jsonify
from flask_socketio import SocketIO, emit
from flask_cors import CORS

app = Flask(__name__, static_folder='scripts')
CORS(app)
socketio = SocketIO(app, async_mode='gevent', cors_allowed_origins='*')

# Process holders
wine_proc = None
xvfb_proc = None
vnc_proc = None
websockify_proc = None

# WebRTC state
broadcaster_sid = None
viewers = {}

@app.route('/start', methods=['GET'])
def start_env():
    global wine_proc, xvfb_proc, vnc_proc, websockify_proc

    # Terminate previous if any
    for proc in [wine_proc, xvfb_proc, vnc_proc, websockify_proc]:
        if proc:
            proc.terminate()
            proc.wait()

    env = os.environ.copy()
    env["DISPLAY"] = ":99"
    env["XDG_RUNTIME_DIR"] = f"/tmp/xdg-runtime-{os.getenv('USER', 'jayson_tolleson')}"
    os.makedirs(env["XDG_RUNTIME_DIR"], exist_ok=True)

    # Start Xvfb
    xvfb_proc = subprocess.Popen(["Xvfb", ":99", "-screen", "0", "1280x800x24"], env=env)
    time.sleep(2)

    # Auth (optional)
    subprocess.call("xauth add :99 . $(mcookie)", shell=True, env=env)

    # Start Wine
    wine_proc = subprocess.Popen(["wine64", "explorer.exe"], env=env)
    time.sleep(2)

    # Start x11vnc
    vnc_proc = subprocess.Popen([
        "x11vnc", "-display", ":99", "-forever", "-nopw", "-shared", "-auth", "guess"
    ], env=env)
    time.sleep(2)

    # Start websockify for noVNC
    websockify_proc = subprocess.Popen([
        "websockify",
        "--web=/usr/share/novnc/",
        "--cert=security/fullchain.pem",
        "--key=security/key.pem",
        "6080", "localhost:5900"
    ], env=env)

    return jsonify(status="Wine, Xvfb, VNC, noVNC launched")

@app.route('/stop', methods=['GET'])
def stop_env():
    global wine_proc, xvfb_proc, vnc_proc, websockify_proc
    for proc in [wine_proc, xvfb_proc, vnc_proc, websockify_proc]:
        if proc:
            proc.terminate()
            proc.wait()
    return jsonify(status="All services stopped")

# SocketIO logic for WebRTC
@socketio.on('connect')
def handle_connect():
    print(f"Client connected: {request.sid}")

@socketio.on('disconnect')
def handle_disconnect():
    global broadcaster_sid
    print(f"Client disconnected: {request.sid}")
    if request.sid == broadcaster_sid:
        for viewer_id in viewers:
            emit('disconnectPeer', request.sid, room=viewer_id)
        broadcaster_sid = None
    viewers.pop(request.sid, None)
    broadcast_viewer_count()

@socketio.on('broadcaster')
def handle_broadcaster():
    global broadcaster_sid
    broadcaster_sid = request.sid
    broadcast_viewer_count()

@socketio.on('watcher')
def handle_watcher():
    viewers[request.sid] = True
    if broadcaster_sid:
        emit('watcher', request.sid, room=broadcaster_sid)
        broadcast_viewer_count()

@socketio.on('offer')
def handle_offer(data):
    emit('offer', {'id': request.sid, 'offer': data['offer']}, room=data['target'])

@socketio.on('answer')
def handle_answer(data):
    emit('answer', {'id': request.sid, 'answer': data['answer']}, room=data['target'])

@socketio.on('ice-candidate')
def handle_ice_candidate(data):
    emit('ice-candidate', {'id': request.sid, 'candidate': data['candidate']}, room=data['target'])

@socketio.on('videoSettingsChanged')
def handle_video_settings_changed(data):
    emit('settingsBroadcast', data, broadcast=True)

def broadcast_viewer_count():
    if broadcaster_sid:
        socketio.emit('viewerCount', {'count': len(viewers)}, room=broadcaster_sid)

@app.route('/')
def index():
    indexhtml="""
<html> <style>
div#container { background: black;
  margin: 24px auto;
        color: white;
        border-radius: 1 em;
        width:3880px;
    height: 2200px;
    overflow:hidden; /* if you don't want a scrollbar, set to hidden */
    overflow-x:hidden; /* hides horizontal scrollbar on newer browsers */
    /* resize and min-height are optional, allows user to resize viewable area */
    -webkit-resize:vertical;
    -moz-resize:vertical;
}
iframe#embed {
    width:3840px; /* set this to approximate width of entire page you're embedding */
    height:2160px; /* determines where the bottom of the page cuts off */
    margin-left:0px; /* clipping left side of page */
    margin-top:0px; /* clipping top of page */
    overflow:hidden;
    /* resize seems to inherit in at least Firefox */
    -webkit-resize:none;
    -moz-resize:none;
    resize:none;
}
div#container1 { background: black;
  margin: 24px auto;
        color: white;
        border-radius: 1 em;
        width:4050px;
    height:3060px;
    overflow:hidden; /* if you don't want a scrollbar, set to hidden */
    overflow-x:hidden; /* hides horizontal scrollbar on newer browsers */
    /* resize and min-height are optional, allows user to resize viewable area */
    -webkit-resize:vertical;
    -moz-resize:vertical;
}
iframe#embed1 {
    width:4032px; /* set this to approximate width of entire page you're embedding */
    height:3040px; /* determines where the bottom of the page cuts off */
    margin-left:0px; /* clipping left side of page */
    margin-top:0px; /* clipping top of page */
    overflow:hidden;
    /* resize seems to inherit in at least Firefox */
    -webkit-resize:none;
    -moz-resize:none;
    resize:none;
}
body {
  margin: 0;
  width: 100%;
  /*border: 1px solid orange;*/ font: normal 2.2em 'trebuchet ms', arial, sans-serif;
  background: #1339de;
  color: #777;
}
</style>
<h1> :::LFTR.biz </h1>
<br><hr>
<body>
    <div id="container1"><iframe id="embed1" scrolling="no" src="https://lftr.biz/watch"></iframe></div>
<br><hr>
    <div id="container1"><iframe id="embed1" scrolling="no" src="https://lftr.biz:8080/fishmap"></iframe></div>
<br><hr>
    <div id="container"><iframe id="embed" scrolling="no" src="https://www.youtube.com/embed/videoseries?list=PLVIftPRSOIthwubkq9WzCSk7B-mqaJ89B" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>
</body> </html>
"""
    return Response(indexhtml)
@app.route('/broadcast')
def broadcast():
    broadcasthtml="""<!DOCTYPE html>   
<html lang="en">
<head>
<meta charset="utf-8" />
<title>JAY VISION Broadcaster</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
  body {
    background: #111;
    color: #eee;
    font-family: sans-serif;
    margin: 0;
    padding: 0;
    text-align: center;
  }
  video {
    width: 96%;
    max-height: 40vh;
    border-radius: 10px;
    background: black;
  }
  select, button {
    margin: 5px;
    font-size: 16px;
    padding: 10px;
    width: 90%;
    max-width: 400px;
  }
  #viewerCount {
    font-size: 18px;
    margin-top: 10px;
  }
</style>

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
</head>
<body>

<h2> JAY VISION Broadcast</h2>
<div id="viewerCount">️ Viewers: 0</div>

<video id="videoInput" autoplay playsinline muted></video>

<select id="cameraSelect" title="Select Camera"></select>

<select id="resolutionSelect" title="Select Resolution">
  <option value="1080p">1080p</option>
  <option value="4k">4K</option>
  <option value="6k">6K</option>
</select>

<select id="streamModeSelect" title="Select Stream Mode">
  <option value="camera"> Camera</option>
  <option value="screen+mic">️ Screen Share + Mic</option>
  <option value="screen">️ Screen Only</option>
</select>

<label for="micSelect"> Mic:</label>
<select id="micSelect" title="Select Microphone"></select>

<label for="speakerSelect"> Speaker:</label>
<select id="speakerSelect" title="Select Speaker"></select>

<br/>

<button id="switchCameraBtn"> Switch Camera</button>
<button id="fullscreenBtn">⛶ Fullscreen</button>
<button id="recordBtn">⏺ Start Recording</button>
<button id="pipBtn"> PiP Mode</button>

<hr />
  <h2>Ableton Live via WINE</h2>
  <button onclick="start()">Start Ableton</button>
  <button onclick="stop()">Stop Ableton</button>
  <br/><br/>
<iframe src="https://lftr.biz:6080/vnc.html" style="width:1280px; height:800px; border:none;"></iframe>

  <script>
    async function start() {
      await fetch('https://lftr.biz/start');
      alert('Started WINE + VNC server');
    }
    async function stop() {
      await fetch('https://lftr.biz/stop');
      alert('Stopped WINE + VNC server');
    }

const videoInput = document.getElementById('videoInput');
const cameraSelect = document.getElementById('cameraSelect');
const resolutionSelect = document.getElementById('resolutionSelect');
const streamModeSelect = document.getElementById('streamModeSelect');
const micSelect = document.getElementById('micSelect');
const speakerSelect = document.getElementById('speakerSelect');

const viewerCountDisplay = document.getElementById('viewerCount');
const switchCameraBtn = document.getElementById('switchCameraBtn');
const fullscreenBtn = document.getElementById('fullscreenBtn');
const recordBtn = document.getElementById('recordBtn');
const pipBtn = document.getElementById('pipBtn');

let stream = null;
let usingFront = true;
let currentDeviceId = null;
let audioDeviceId = null;

const socket = io();
const peerConnections = {};
const config = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }], sdpSemantics: 'unified-plan' };

let isRecording = false;
let mediaRecorder;
let recordedChunks = [];

function getResConstraints() {
  const val = resolutionSelect.value;
  return val === '4k' ? { width: { ideal: 3840 }, height: { ideal: 2160 } } :
         val === '6k' ? { width: { ideal: 6144 }, height: { ideal: 3160 } } :
                        { width: { ideal: 1920 }, height: { ideal: 1080 } };
}

function stopStream() {
  if (stream) {
    stream.getTracks().forEach(t => t.stop());
    stream = null;
  }
}

async function setupStream() {
  videoInput.srcObject = stream;

  for (const id in peerConnections) {
    const pc = peerConnections[id];
    const senders = pc.getSenders();
    stream.getTracks().forEach(track => {
      const sender = senders.find(s => s.track?.kind === track.kind);
      if (sender) sender.replaceTrack(track);
    });
  }
}

async function updateStreamSettingsAuto() {
  stopStream();

  const mode = streamModeSelect.value;
  audioDeviceId = micSelect.value || null;

  try {
    if (mode === 'camera') {
      const constraints = {
        audio: audioDeviceId ? { deviceId: { exact: audioDeviceId } } : true,
        video: {
          facingMode: usingFront ? 'user' : 'environment',
          deviceId: currentDeviceId ? { exact: currentDeviceId } : undefined,
          ...getResConstraints()
        }
      };
      stream = await navigator.mediaDevices.getUserMedia(constraints);
    } else if (mode === 'screen+mic') {
      const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
      const micStream = await navigator.mediaDevices.getUserMedia({ audio: audioDeviceId ? { deviceId: { exact: audioDeviceId } } : true });
      
      // Combine audio tracks from screen + mic
      const audioContext = new AudioContext();
      const destination = audioContext.createMediaStreamDestination();

      if (screenStream.getAudioTracks().length) {
        const screenSource = audioContext.createMediaStreamSource(screenStream);
        screenSource.connect(destination);
      }
      const micSource = audioContext.createMediaStreamSource(micStream);
      micSource.connect(destination);

      const combinedStream = new MediaStream();
      screenStream.getVideoTracks().forEach(track => combinedStream.addTrack(track));
      destination.stream.getAudioTracks().forEach(track => combinedStream.addTrack(track));

      stream = combinedStream;
    } else if (mode === 'screen') {
      stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
    }

    await setupStream();
  } catch (err) {
    console.error('Error accessing media devices:', err);
    alert(`Error accessing media devices:\n${err.name}: ${err.message}`);
    videoInput.style.display = 'none';
    viewerCountDisplay.insertAdjacentHTML('afterend',
      `<p style="color:tomato;">Camera/mic access is required for broadcasting.</p>`);
  }
}

function setVideoSink() {
  if ('setSinkId' in videoInput) {
    videoInput.setSinkId(speakerSelect.value).catch(e => console.warn('setSinkId error:', e));
  }
}

// Event Handlers
switchCameraBtn.onclick = () => {
  usingFront = !usingFront;
  updateStreamSettingsAuto();
};

fullscreenBtn.onclick = () => {
  videoInput.requestFullscreen?.();
};

pipBtn.onclick = async () => {
  try {
    if (document.pictureInPictureElement) {
      await document.exitPictureInPicture();
    } else {
      await videoInput.requestPictureInPicture();
    }
  } catch (e) {
    console.warn('PiP error:', e);
  }
};

recordBtn.onclick = () => {
  if (!stream) return alert("Start streaming first");
  if (!MediaRecorder.isTypeSupported('video/webm')) return alert("Recording not supported in your browser");

  if (!isRecording) {
    recordedChunks = [];
    mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm; codecs=vp8,opus' });
    mediaRecorder.ondataavailable = e => { if (e.data.size > 0) recordedChunks.push(e.data); };
    mediaRecorder.onstop = () => {
      const blob = new Blob(recordedChunks, { type: 'video/webm' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = 'recording.webm';
      document.body.appendChild(a);
      a.click();
      setTimeout(() => {
        URL.revokeObjectURL(url);
        document.body.removeChild(a);
      }, 100);
    };
    mediaRecorder.start();
    recordBtn.textContent = '⏹ Stop Recording';
    isRecording = true;
  } else {
    mediaRecorder.stop();
    recordBtn.textContent = '⏺ Start Recording';
    isRecording = false;
  }
};

// Populate Camera Select
async function getCameras() {
  const devices = await navigator.mediaDevices.enumerateDevices();
  cameraSelect.innerHTML = '';
  devices.filter(d => d.kind === 'videoinput').forEach((device, i) => {
    const option = document.createElement('option');
    option.value = device.deviceId;
    option.text = device.label || `Camera ${i + 1}`;
    cameraSelect.appendChild(option);
  });
  if (cameraSelect.options.length > 0) {
    currentDeviceId = cameraSelect.value = cameraSelect.options[0].value;
  }
}

// Populate Mic and Speaker Selects
async function getAudioDevices() {
  const devices = await navigator.mediaDevices.enumerateDevices();
  micSelect.innerHTML = '';
  speakerSelect.innerHTML = '';
  devices.forEach(device => {
    const option = document.createElement('option');
    option.value = device.deviceId;
    option.text = device.label || `${device.kind} ${device.deviceId.slice(-4)}`;
    if (device.kind === 'audioinput') micSelect.appendChild(option);
    if (device.kind === 'audiooutput') speakerSelect.appendChild(option);
  });
}

micSelect.onchange = () => {
  audioDeviceId = micSelect.value;
  updateStreamSettingsAuto();
};

speakerSelect.onchange = () => {
  setVideoSink();
};

cameraSelect.onchange = () => {
  currentDeviceId = cameraSelect.value;
  updateStreamSettingsAuto();
};

resolutionSelect.onchange = updateStreamSettingsAuto;
streamModeSelect.onchange = updateStreamSettingsAuto;

navigator.mediaDevices.ondevicechange = () => {
  getCameras();
  getAudioDevices();
};

// WebRTC Signaling Handlers
socket.emit('broadcaster');

socket.on('watcher', id => {
  const pc = new RTCPeerConnection(config);
  peerConnections[id] = pc;

  if (stream) {
    stream.getTracks().forEach(track => pc.addTrack(track, stream));
  }

  pc.onicecandidate = event => {
    if (event.candidate) {
      socket.emit('ice-candidate', { target: id, candidate: event.candidate });
    }
  };

  pc.createOffer()
    .then(offer => pc.setLocalDescription(offer))
    .then(() => {
      socket.emit('offer', { target: id, offer: pc.localDescription });
    });
});

socket.on('answer', ({ id, answer }) => {
  const pc = peerConnections[id];
  if (pc) pc.setRemoteDescription(answer);
});

socket.on('ice-candidate', ({ id, candidate }) => {
  const pc = peerConnections[id];
  if (pc) pc.addIceCandidate(candidate);
});

socket.on('disconnectPeer', id => {
  const pc = peerConnections[id];
  if (pc) {
    pc.close();
    delete peerConnections[id];
  }
});

socket.on('viewerCount', data => {
  viewerCountDisplay.textContent = `️ Viewers: ${data.count}`;
});

// Initialize
async function init() {
  await getAudioDevices();
  await getCameras();
  await updateStreamSettingsAuto();
}

document.addEventListener('DOMContentLoaded', init);

</script>
</body>
</html>

"""
    return Response(broadcasthtml)

@app.route('/watch')
def watch():
    watchhtml="""<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>JAY VISION Viewer</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    html, body {
      margin: 0;
      padding: 0;
      background-color: #000;
      height: 100%;
      overflow: hidden;
      font-family: sans-serif;
    }
    video {
      width: 100%;
      height: auto;
      max-height: 100vh;
      background-color: black;
    }
    #overlay {
      position: fixed;
      top: 0; left: 0; right: 0; bottom: 0;
      display: none;
      align-items: center;
      justify-content: center;
      flex-direction: column;
      background-color: rgba(0, 0, 0, 0.7);
      color: white;
      font-size: 1.5em;
      z-index: 10;
      text-align: center;
    }
    .spinner-pie {
      width: 60px;
      height: 60px;
      border-radius: 50%;
      background: conic-gradient(
        #ff3e3e 0% 25%,
        #ffdd00 25% 50%,
        #4ade80 50% 75%,
        #3b82f6 75% 100%
      );
      animation: spin 1.5s linear infinite;
      margin-bottom: 1em;
    }
    @keyframes spin {
      from { transform: rotate(0deg); }
      to   { transform: rotate(360deg); }
    }
  </style>
</head>
<body>
  <div id="overlay">
    <div class="spinner-pie"></div>
    <div> Reconnecting to stream…</div>
  </div>
  <video id="watcherVideo" autoplay playsinline muted></video>

  <script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
  <script>
const video = document.getElementById('watcherVideo');
const overlay = document.getElementById('overlay');

let pc = null;
let streamSet = false;
let socket = null;
let connected = false;
let failureTimeout = null;

function showOverlay(message = ' Reconnecting to stream…') {
  overlay.style.display = 'flex';
  overlay.querySelector('div:last-child').textContent = message;
}

function hideOverlay() {
  overlay.style.display = 'none';
}

function connectSocket() {
  if (socket) socket.disconnect();

  socket = io({
    reconnection: true,
    reconnectionAttempts: Infinity,
    reconnectionDelay: 2000,
    reconnectionDelayMax: 5000,
  });

  socket.on('connect', () => {
    connected = true;
    streamSet = false;
    hideOverlay();
    clearTimeout(failureTimeout);
    socket.emit('watcher');
  });

  socket.on('disconnect', () => {
    connected = false;
    showOverlay();
    failureTimeout = setTimeout(() => location.reload(), 120000); // 2 min fallback
  });

  socket.on('offer', ({ id, offer }) => {
    setupPeerConnection(id, offer);

    setTimeout(() => {
      if (!streamSet && connected) {
        console.warn("No stream received, re-requesting...");
        socket.emit('watcher');
      }
    }, 5000);
  });

  socket.on('ice-candidate', ({ candidate }) => {
    if (pc) pc.addIceCandidate(new RTCIceCandidate(candidate)).catch(console.error);
  });

  socket.on('viewerCount', ({ count }) => {
    console.log(` Viewer count: ${count}`);
  });
}

function setupPeerConnection(id, offer) {
  if (pc) {
    pc.close();
    pc = null;
    streamSet = false;
  }

  pc = new RTCPeerConnection({
    iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
  });

  pc.ontrack = event => {
    if (!streamSet) {
      video.srcObject = event.streams[0];
      streamSet = true;
      hideOverlay();
    }
  };

  pc.onicecandidate = event => {
    if (event.candidate) {
      socket.emit('ice-candidate', { target: id, candidate: event.candidate });
    }
  };

  pc.onconnectionstatechange = () => {
    if (['failed', 'disconnected', 'closed'].includes(pc.connectionState)) {
      console.warn("PeerConnection state:", pc.connectionState);
      showOverlay();
      if (pc) pc.close();
      pc = null;
      streamSet = false;
      if (connected) {
        socket.emit('watcher');
      }
    }
  };

  pc.setRemoteDescription(new RTCSessionDescription(offer))
    .then(() => pc.createAnswer())
    .then(answer => pc.setLocalDescription(answer))
    .then(() => {
      socket.emit('answer', { target: id, answer: pc.localDescription });
    })
    .catch(console.error);
}

// Request fullscreen on first click
document.addEventListener('click', () => {
  if (video.requestFullscreen) {
    video.requestFullscreen();
  } else if (video.webkitRequestFullscreen) {
    video.webkitRequestFullscreen();
  } else if (video.msRequestFullscreen) {
    video.msRequestFullscreen();
  }
}, { once: true });

// Cleanup on unload
window.onbeforeunload = () => {
  if (socket) socket.close();
  if (pc) pc.close();
};

connectSocket();
  </script>
</body>
</html>


"""
    return Response(watchhtml)
if __name__ == "__main__":
    context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    context.load_cert_chain('security/fullchain.pem', 'security/privkey.pem')
    socketio.run(app, debug=True, host='0.0.0.0', ssl_context=context, port=443)


end game:

View attachment 26578
 


Thank for looking in and supplying some intel... both are running and now it seems like an openbox issue... or wine....
Screenshot from 2025-06-17 09-52-33.png
 
Last edited:
Im curious why you are using 2 separate scripting languages? I used to do that for my text manipulation work stuff, but as of recently i got chatgpt to combine all of it into a single perl script (wasn't easy...but eventually i had success.)
 
So, here is my updated script, 'the whole shebang'...looks like openbox and wine issue.
the broadcaster runs the novnc script (script1), with inner html printout for the terminal msgs.
Screenshot from 2025-06-17 10-02-14.png


~/scripts/script1.sh

sudo python3 ~/broadcast.py

Code:
from gevent import monkey
monkey.patch_all()
import threading
import ssl
import os
import subprocess
import time
from flask import Flask, Response, request, jsonify
from flask_socketio import SocketIO, emit
from flask_cors import CORS
import stat
# Configuration
USERNAME = "jayson_tolleson"
BASE_DIR = os.path.expanduser(f"/home/{USERNAME}")
SCRIPT_PATH = os.path.join(BASE_DIR, 'scripts', 'novnc_env.sh')
SUDOERS_FILE = '/etc/sudoers.d/novnc_env'

app = Flask(__name__, static_folder='scripts')
CORS(app)
socketio = SocketIO(app, async_mode='gevent', cors_allowed_origins='*')

# ---- Utility Functions ----

def create_sudoers_rule():
    """Ensure passwordless sudo for our setup script."""
    if not os.path.exists(SUDOERS_FILE):
        rule = f"{USERNAME} ALL=(ALL) NOPASSWD: /bin/bash {SCRIPT_PATH}\n"
        with open(SUDOERS_FILE, 'w') as f:
            f.write(rule)
        os.chmod(SUDOERS_FILE, stat.S_IRUSR | stat.S_IRGRP)
        app.logger.info(f"Sudoers entry created at {SUDOERS_FILE}")
    else:
        app.logger.debug(f"Sudoers entry already exists: {SUDOERS_FILE}")

# ---- Routes ----
@app.route('/start')
def start_script():
    """Start novnc_env.sh with full logging and error capture."""
    def _run_script():
        try:
            # 1) Ensure sudoers entry
            create_sudoers_rule()

            # 2) Double‑check the script exists & is executable
            if not os.path.isfile(SCRIPT_PATH):
                msg = f"Script not found: {SCRIPT_PATH}"
                app.logger.error(msg)
                socketio.emit('script_output', {'line': msg})
                socketio.emit('script_done', {'status': 'Failed'})
                return

            # 3) Launch it via sudo
            cmd = ['sudo', 'bash', SCRIPT_PATH]
            app.logger.info("Running command: " + " ".join(cmd))
            proc = subprocess.Popen(
                cmd,
                stdout=subprocess.PIPE,
                stderr=subprocess.STDOUT,
                text=True,
                bufsize=1
            )

            # 4) Stream its output
            for line in proc.stdout:
                app.logger.debug("SCRIPT: " + line.rstrip())
                socketio.emit('script_output', {'line': line.rstrip()})
                socketio.emit('script_output', {'data': line})  # No namespace
                socketio.sleep(0)

            # 5) Wait & report exit code
            ret = proc.wait()
            if ret == 0:
                socketio.emit('script_done', {'status': '✅ Script finished'})
            else:
                socketio.emit('script_done', {
                    'status': f'❌ Script exited with code {ret}'
                })

        except Exception as e:
            err = f"Exception running script: {e}"
            app.logger.exception(err)
            socketio.emit('script_output', {'line': err})
            socketio.emit('script_done', {'status': '❌ Failed'})
    socketio.start_background_task(target=_run_script)
    return jsonify(status=" Launching script… (check log for details)"), 202



@app.route('/stop', methods=['GET'])
def stop_env():
    """Stop all related processes."""
    procs = ['x11vnc', 'Xvfb', 'wine', 'openbox', 'novnc_proxy']
    for name in procs:
        subprocess.call(['pkill', '-f', name])
    return jsonify(status=" All processes terminated.")

# ---- WebRTC Socket.IO ----

broadcaster_sid = None
viewers = {}

def broadcast_viewer_count():
    if broadcaster_sid:
        socketio.emit('viewerCount', {'count': len(viewers)}, room=broadcaster_sid)

@socketio.on('connect')
def on_connect():
    app.logger.info(f"Client connected: {request.sid}")

@socketio.on('disconnect')
def on_disconnect():
    global broadcaster_sid
    sid = request.sid
    app.logger.info(f"Client disconnected: {sid}")
    if sid == broadcaster_sid:
        for v in viewers:
            socketio.emit('disconnectPeer', sid, room=v)
        broadcaster_sid = None
    viewers.pop(sid, None)
    broadcast_viewer_count()

@socketio.on('broadcaster')
def on_broadcaster():
    global broadcaster_sid
    broadcaster_sid = request.sid
    broadcast_viewer_count()

@socketio.on('watcher')
def on_watcher():
    viewers[request.sid] = True
    if broadcaster_sid:
        socketio.emit('watcher', request.sid, room=broadcaster_sid)
        broadcast_viewer_count()

@socketio.on('offer')
def on_offer(data):
    socketio.emit('offer', {'id': request.sid, 'offer': data['offer']}, room=data['target'])

@socketio.on('answer')
def on_answer(data):
    socketio.emit('answer', {'id': request.sid, 'answer': data['answer']}, room=data['target'])

@socketio.on('ice-candidate')
def on_ice(data):
    socketio.emit('ice-candidate', {'id': request.sid, 'candidate': data['candidate']}, room=data['target'])

@socketio.on('videoSettingsChanged')
def on_video_settings(data):
    socketio.emit('settingsBroadcast', data, broadcast=True)
    
@app.route('/')
def index():
    indexhtml="""
<html> <style> 
div#container { background: black;
  margin: 24px auto;
        color: white;
        border-radius: 1 em;
        width:3880px;
    height: 2200px;
    overflow:hidden; /* if you don't want a scrollbar, set to hidden */
    overflow-x:hidden; /* hides horizontal scrollbar on newer browsers */
    /* resize and min-height are optional, allows user to resize viewable area */
    -webkit-resize:vertical;
    -moz-resize:vertical;
}
iframe#embed {
    width:3840px; /* set this to approximate width of entire page you're embedding */
    height:2160px; /* determines where the bottom of the page cuts off */
    margin-left:0px; /* clipping left side of page */
    margin-top:0px; /* clipping top of page */
    overflow:hidden;
    /* resize seems to inherit in at least Firefox */
    -webkit-resize:none;
    -moz-resize:none;
    resize:none;
}
div#container1 { background: black;
  margin: 24px auto;
        color: white;
        border-radius: 1 em;
        width:4050px;
    height:3060px;
    overflow:hidden; /* if you don't want a scrollbar, set to hidden */
    overflow-x:hidden; /* hides horizontal scrollbar on newer browsers */
    /* resize and min-height are optional, allows user to resize viewable area */
    -webkit-resize:vertical;
    -moz-resize:vertical;
}
iframe#embed1 {
    width:4032px; /* set this to approximate width of entire page you're embedding */
    height:3040px; /* determines where the bottom of the page cuts off */
    margin-left:0px; /* clipping left side of page */
    margin-top:0px; /* clipping top of page */
    overflow:hidden;
    /* resize seems to inherit in at least Firefox */
    -webkit-resize:none;
    -moz-resize:none;
    resize:none;
}
body {
  margin: 0;
  width: 100%;
  /*border: 1px solid orange;*/ font: normal 2.2em 'trebuchet ms', arial, sans-serif;
  background: #1339de;
  color: #777;
}
</style>
<h1> :::LFTR.biz </h1>
<br><hr>
<body>
    <div id="container1"><iframe id="embed1" scrolling="no" src="https://lftr.biz/watch"></iframe></div>
<br><hr>
    <div id="container1"><iframe id="embed1" scrolling="no" src="https://lftr.biz:8080/fishmap"></iframe></div>
<br><hr>
    <div id="container"><iframe id="embed" scrolling="no" src="https://www.youtube.com/embed/videoseries?list=PLVIftPRSOIthwubkq9WzCSk7B-mqaJ89B" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>
</body> </html>
"""
    return Response(indexhtml)
@app.route('/broadcast')
def broadcast():
    broadcasthtml="""<!DOCTYPE html>    
<html lang="en">
<head>
<meta charset="utf-8" />
<title>JAY VISION Broadcaster</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
  body {
    background: #111;
    color: #eee;
    font-family: sans-serif;
    margin: 0;
    padding: 0;
    text-align: center;
  }
  video {
    width: 96%;
    max-height: 40vh;
    border-radius: 10px;
    background: black;
  }
  select, button {
    margin: 5px;
    font-size: 16px;
    padding: 10px;
    width: 90%;
    max-width: 400px;
  }
  #viewerCount {
    font-size: 18px;
    margin-top: 10px;
  }
</style>

<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
<script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
</head>
<body>

<h2> JAY VISION Broadcast</h2>
<div id="viewerCount">️ Viewers: 0</div>

<video id="videoInput" autoplay playsinline muted></video>

<select id="cameraSelect" title="Select Camera"></select>

<select id="resolutionSelect" title="Select Resolution">
  <option value="1080p">1080p</option>
  <option value="4k">4K</option>
  <option value="6k">6K</option>
</select>

<select id="streamModeSelect" title="Select Stream Mode">
  <option value="camera"> Camera</option>
  <option value="screen+mic">️ Screen Share + Mic</option>
  <option value="screen">️ Screen Only</option>
</select>

<label for="micSelect"> Mic:</label>
<select id="micSelect" title="Select Microphone"></select>

<label for="speakerSelect"> Speaker:</label>
<select id="speakerSelect" title="Select Speaker"></select>

<br/>

<button id="switchCameraBtn"> Switch Camera</button>
<button id="fullscreenBtn">⛶ Fullscreen</button>
<button id="recordBtn">⏺ Start Recording</button>
<button id="pipBtn"> PiP Mode</button>

<hr />
  <h2>Ableton Live via WINE</h2>
  <button onclick="start()">Start Ableton</button>
  <button onclick="stop()">Stop Ableton</button>
  <br/>
<div id="scriptOutput" style="max-height: 300px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; margin-top: 20px;">
    <!-- Script output will appear here -->
</div>

  <br/>
<iframe src="https://lftr.biz:8082/vnc.html" style="width:1280px; height:800px; border:none;"></iframe>

  <script>
    async function start() {
      await fetch('https://lftr.biz/start');
      alert('Started WINE + VNC server');
    }
    async function stop() {
      await fetch('https://lftr.biz/stop');
      alert('Stopped WINE + VNC server');
    }

const videoInput = document.getElementById('videoInput');
const cameraSelect = document.getElementById('cameraSelect');
const resolutionSelect = document.getElementById('resolutionSelect');
const streamModeSelect = document.getElementById('streamModeSelect');
const micSelect = document.getElementById('micSelect');
const speakerSelect = document.getElementById('speakerSelect');

const viewerCountDisplay = document.getElementById('viewerCount');
const switchCameraBtn = document.getElementById('switchCameraBtn');
const fullscreenBtn = document.getElementById('fullscreenBtn');
const recordBtn = document.getElementById('recordBtn');
const pipBtn = document.getElementById('pipBtn');

let stream = null;
let usingFront = true;
let currentDeviceId = null;
let audioDeviceId = null;

const socket = io();
const peerConnections = {};
const config = { iceServers: [{ urls: "stun:stun.l.google.com:19302" }], sdpSemantics: 'unified-plan' };

let isRecording = false;
let mediaRecorder;
let recordedChunks = [];

function getResConstraints() {
  const val = resolutionSelect.value;
  return val === '4k' ? { width: { ideal: 3840 }, height: { ideal: 2160 } } :
         val === '6k' ? { width: { ideal: 6144 }, height: { ideal: 3160 } } :
                        { width: { ideal: 1920 }, height: { ideal: 1080 } };
}

function stopStream() {
  if (stream) {
    stream.getTracks().forEach(t => t.stop());
    stream = null;
  }
}

async function setupStream() {
  videoInput.srcObject = stream;

  for (const id in peerConnections) {
    const pc = peerConnections[id];
    const senders = pc.getSenders();
    stream.getTracks().forEach(track => {
      const sender = senders.find(s => s.track?.kind === track.kind);
      if (sender) sender.replaceTrack(track);
    });
  }
}

async function updateStreamSettingsAuto() {
  stopStream();

  const mode = streamModeSelect.value;
  audioDeviceId = micSelect.value || null;

  try {
    if (mode === 'camera') {
      const constraints = {
        audio: audioDeviceId ? { deviceId: { exact: audioDeviceId } } : true,
        video: {
          facingMode: usingFront ? 'user' : 'environment',
          deviceId: currentDeviceId ? { exact: currentDeviceId } : undefined,
          ...getResConstraints()
        }
      };
      stream = await navigator.mediaDevices.getUserMedia(constraints);
    } else if (mode === 'screen+mic') {
      const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
      const micStream = await navigator.mediaDevices.getUserMedia({ audio: audioDeviceId ? { deviceId: { exact: audioDeviceId } } : true });
      
      // Combine audio tracks from screen + mic
      const audioContext = new AudioContext();
      const destination = audioContext.createMediaStreamDestination();

      if (screenStream.getAudioTracks().length) {
        const screenSource = audioContext.createMediaStreamSource(screenStream);
        screenSource.connect(destination);
      }
      const micSource = audioContext.createMediaStreamSource(micStream);
      micSource.connect(destination);

      const combinedStream = new MediaStream();
      screenStream.getVideoTracks().forEach(track => combinedStream.addTrack(track));
      destination.stream.getAudioTracks().forEach(track => combinedStream.addTrack(track));

      stream = combinedStream;
    } else if (mode === 'screen') {
      stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
    }

    await setupStream();
  } catch (err) {
    console.error('Error accessing media devices:', err);
    alert(`Error accessing media devices:\n${err.name}: ${err.message}`);
    videoInput.style.display = 'none';
    viewerCountDisplay.insertAdjacentHTML('afterend',
      `<p style="color:tomato;">Camera/mic access is required for broadcasting.</p>`);
  }
}

function setVideoSink() {
  if ('setSinkId' in videoInput) {
    videoInput.setSinkId(speakerSelect.value).catch(e => console.warn('setSinkId error:', e));
  }
}

// Event Handlers
switchCameraBtn.onclick = () => {
  usingFront = !usingFront;
  updateStreamSettingsAuto();
};

fullscreenBtn.onclick = () => {
  videoInput.requestFullscreen?.();
};

pipBtn.onclick = async () => {
  try {
    if (document.pictureInPictureElement) {
      await document.exitPictureInPicture();
    } else {
      await videoInput.requestPictureInPicture();
    }
  } catch (e) {
    console.warn('PiP error:', e);
  }
};

recordBtn.onclick = () => {
  if (!stream) return alert("Start streaming first");
  if (!MediaRecorder.isTypeSupported('video/webm')) return alert("Recording not supported in your browser");

  if (!isRecording) {
    recordedChunks = [];
    mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm; codecs=vp8,opus' });
    mediaRecorder.ondataavailable = e => { if (e.data.size > 0) recordedChunks.push(e.data); };
    mediaRecorder.onstop = () => {
      const blob = new Blob(recordedChunks, { type: 'video/webm' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = 'recording.webm';
      document.body.appendChild(a);
      a.click();
      setTimeout(() => {
        URL.revokeObjectURL(url);
        document.body.removeChild(a);
      }, 100);
    };
    mediaRecorder.start();
    recordBtn.textContent = '⏹ Stop Recording';
    isRecording = true;
  } else {
    mediaRecorder.stop();
    recordBtn.textContent = '⏺ Start Recording';
    isRecording = false;
  }
};

// Populate Camera Select
async function getCameras() {
  const devices = await navigator.mediaDevices.enumerateDevices();
  cameraSelect.innerHTML = '';
  devices.filter(d => d.kind === 'videoinput').forEach((device, i) => {
    const option = document.createElement('option');
    option.value = device.deviceId;
    option.text = device.label || `Camera ${i + 1}`;
    cameraSelect.appendChild(option);
  });
  if (cameraSelect.options.length > 0) {
    currentDeviceId = cameraSelect.value = cameraSelect.options[0].value;
  }
}

// Populate Mic and Speaker Selects
async function getAudioDevices() {
  const devices = await navigator.mediaDevices.enumerateDevices();
  micSelect.innerHTML = '';
  speakerSelect.innerHTML = '';
  devices.forEach(device => {
    const option = document.createElement('option');
    option.value = device.deviceId;
    option.text = device.label || `${device.kind} ${device.deviceId.slice(-4)}`;
    if (device.kind === 'audioinput') micSelect.appendChild(option);
    if (device.kind === 'audiooutput') speakerSelect.appendChild(option);
  });
}

micSelect.onchange = () => {
  audioDeviceId = micSelect.value;
  updateStreamSettingsAuto();
};

speakerSelect.onchange = () => {
  setVideoSink();
};

cameraSelect.onchange = () => {
  currentDeviceId = cameraSelect.value;
  updateStreamSettingsAuto();
};

resolutionSelect.onchange = updateStreamSettingsAuto;
streamModeSelect.onchange = updateStreamSettingsAuto;

navigator.mediaDevices.ondevicechange = () => {
  getCameras();
  getAudioDevices();
};

// WebRTC Signaling Handlers
socket.emit('broadcaster');

socket.on('watcher', id => {
  const pc = new RTCPeerConnection(config);
  peerConnections[id] = pc;

  if (stream) {
    stream.getTracks().forEach(track => pc.addTrack(track, stream));
  }

  pc.onicecandidate = event => {
    if (event.candidate) {
      socket.emit('ice-candidate', { target: id, candidate: event.candidate });
    }
  };

  pc.createOffer()
    .then(offer => pc.setLocalDescription(offer))
    .then(() => {
      socket.emit('offer', { target: id, offer: pc.localDescription });
    });
});

socket.on('answer', ({ id, answer }) => {
  const pc = peerConnections[id];
  if (pc) pc.setRemoteDescription(answer);
});

socket.on('ice-candidate', ({ id, candidate }) => {
  const pc = peerConnections[id];
  if (pc) pc.addIceCandidate(candidate);
});

socket.on('disconnectPeer', id => {
  const pc = peerConnections[id];
  if (pc) {
    pc.close();
    delete peerConnections[id];
  }
});

socket.on('viewerCount', data => {
  viewerCountDisplay.textContent = `️ Viewers: ${data.count}`;
});


// Initialize
async function init() {
  await getAudioDevices();
  await getCameras();
  await updateStreamSettingsAuto();

}

document.addEventListener('DOMContentLoaded', init);

socket.on('script_output', function(data) {
        const outputDiv = document.getElementById('scriptOutput');
        const newLine = document.createElement('p');
        newLine.textContent = data.line;
        outputDiv.appendChild(newLine);
        outputDiv.scrollTop = outputDiv.scrollHeight; // Auto-scroll to the bottom
    });

</script>
</body>
</html>

"""
    return Response(broadcasthtml)

@app.route('/watch')
def watch():
    watchhtml="""<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>JAY VISION Viewer</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    html, body {
      margin: 0;
      padding: 0;
      background-color: #000;
      height: 100%;
      overflow: hidden;
      font-family: sans-serif;
    }
    video {
      width: 100%;
      height: auto;
      max-height: 100vh;
      background-color: black;
    }
    #overlay {
      position: fixed;
      top: 0; left: 0; right: 0; bottom: 0;
      display: none;
      align-items: center;
      justify-content: center;
      flex-direction: column;
      background-color: rgba(0, 0, 0, 0.7);
      color: white;
      font-size: 1.5em;
      z-index: 10;
      text-align: center;
    }
    .spinner-pie {
      width: 60px;
      height: 60px;
      border-radius: 50%;
      background: conic-gradient(
        #ff3e3e 0% 25%,
        #ffdd00 25% 50%,
        #4ade80 50% 75%,
        #3b82f6 75% 100%
      );
      animation: spin 1.5s linear infinite;
      margin-bottom: 1em;
    }
    @keyframes spin {
      from { transform: rotate(0deg); }
      to   { transform: rotate(360deg); }
    }
  </style>
</head>
<body>
  <div id="overlay">
    <div class="spinner-pie"></div>
    <div> Reconnecting to stream…</div>
  </div>
  <video id="watcherVideo" autoplay playsinline muted></video>

  <script src="https://cdn.socket.io/4.8.1/socket.io.min.js"></script>
  <script>
const video = document.getElementById('watcherVideo');
const overlay = document.getElementById('overlay');

let pc = null;
let streamSet = false;
let socket = null;
let connected = false;
let failureTimeout = null;

function showOverlay(message = ' Reconnecting to stream…') {
  overlay.style.display = 'flex';
  overlay.querySelector('div:last-child').textContent = message;
}

function hideOverlay() {
  overlay.style.display = 'none';
}

function connectSocket() {
  if (socket) socket.disconnect();

  socket = io({
    reconnection: true,
    reconnectionAttempts: Infinity,
    reconnectionDelay: 2000,
    reconnectionDelayMax: 5000,
  });

  socket.on('connect', () => {
    connected = true;
    streamSet = false;
    hideOverlay();
    clearTimeout(failureTimeout);
    socket.emit('watcher');
  });

  socket.on('disconnect', () => {
    connected = false;
    showOverlay();
    failureTimeout = setTimeout(() => location.reload(), 120000); // 2 min fallback
  });

  socket.on('offer', ({ id, offer }) => {
    setupPeerConnection(id, offer);

    setTimeout(() => {
      if (!streamSet && connected) {
        console.warn("No stream received, re-requesting...");
        socket.emit('watcher');
      }
    }, 5000);
  });

  socket.on('ice-candidate', ({ candidate }) => {
    if (pc) pc.addIceCandidate(new RTCIceCandidate(candidate)).catch(console.error);
  });

  socket.on('viewerCount', ({ count }) => {
    console.log(` Viewer count: ${count}`);
  });
}

function setupPeerConnection(id, offer) {
  if (pc) {
    pc.close();
    pc = null;
    streamSet = false;
  }

  pc = new RTCPeerConnection({
    iceServers: [{ urls: "stun:stun.l.google.com:19302" }]
  });

  pc.ontrack = event => {
    if (!streamSet) {
      video.srcObject = event.streams[0];
      streamSet = true;
      hideOverlay();
    }
  };

  pc.onicecandidate = event => {
    if (event.candidate) {
      socket.emit('ice-candidate', { target: id, candidate: event.candidate });
    }
  };

  pc.onconnectionstatechange = () => {
    if (['failed', 'disconnected', 'closed'].includes(pc.connectionState)) {
      console.warn("PeerConnection state:", pc.connectionState);
      showOverlay();
      if (pc) pc.close();
      pc = null;
      streamSet = false;
      if (connected) {
        socket.emit('watcher');
      }
    }
  };

  pc.setRemoteDescription(new RTCSessionDescription(offer))
    .then(() => pc.createAnswer())
    .then(answer => pc.setLocalDescription(answer))
    .then(() => {
      socket.emit('answer', { target: id, answer: pc.localDescription });
    })
    .catch(console.error);
}

// Request fullscreen on first click
document.addEventListener('click', () => {
  if (video.requestFullscreen) {
    video.requestFullscreen();
  } else if (video.webkitRequestFullscreen) {
    video.webkitRequestFullscreen();
  } else if (video.msRequestFullscreen) {
    video.msRequestFullscreen();
  }
}, { once: true });

// Cleanup on unload
window.onbeforeunload = () => {
  if (socket) socket.close();
  if (pc) pc.close();
};

connectSocket();
  </script>
</body>
</html>


"""
    return Response(watchhtml)
if __name__ == "__main__":
    context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    context.load_cert_chain('security/fullchain.pem', 'security/privkey.pem')
    socketio.run(app, debug=True, host='0.0.0.0', ssl_context=context, port=443)

I will have to focus on the "script1.sh" now; as it has an error with wine, pulse, and openbox.
 
well, i have found something that is called 'xpra'. Xpra is another command to sling a gui to the web....I am trying it out because this script has strong logging and is readable.

Code:
from pyvirtualdisplay import Display
import subprocess
import time
import os

# Start a virtual X display with a high number (like :99)
display = Display(visible=0, size=(1280, 800), use_xauth=True)
display.start()

#display_id = f":{display.display}"  # Automatically assigned, e.g. ":99"
display_id = ":99"  # Automatically assigned, e.g. ":99"

log_dir = "/home/jayson_tolleson/xpra-logs"
os.makedirs(log_dir, exist_ok=True)

xpra_command = [
    "xpra", "start", display_id,
    "--start-child=wine64 notepad.exe",
    "--bind-tcp=0.0.0.0:8080",
    "--html=on",
    "--ssl-cert=/home/jayson_tolleson/security/fullchain.pem",
    "--ssl-key=/home/jayson_tolleson/security/privkey.pem",
    f"--log-dir={log_dir}",
    "--exit-with-children",
    "--debug=all"
]

print(f" Launching xpra on display {display_id}")
process = subprocess.Popen(
    xpra_command,
    stdout=subprocess.PIPE,
    stderr=subprocess.STDOUT,
    text=True,
    bufsize=1
)

# Stream xpra logs to terminal
for line in process.stdout:
    print(f"[XPRA] {line.strip()}")

# Keep alive
try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    print(" Stopping virtual display")
    display.stop()
 
Command line, coding and scripting inquiries belong in Command Line.

Moving this there,

Wizard
 
wowzer...thanks for the update....Well...my new server meant...i had not opened valuable ports.wow.it may be working....
 


Follow Linux.org

Members online


Top