Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
Size: Mime:
øôï{"files":{"index.html":{"size":9516,"integrity":{"algorithm":"SHA256","hash":"0664f0c42d7004bb0173cef3d5cf4041e5795db95dc902705a95108a4bb03982","blockSize":4194304,"blocks":["0664f0c42d7004bb0173cef3d5cf4041e5795db95dc902705a95108a4bb03982"]},"offset":"0"},"main.js":{"size":2730,"integrity":{"algorithm":"SHA256","hash":"84233c6f2c5595e3774ccfa3b1b093395015dfacf306f4c224217d9f2d3e53a0","blockSize":4194304,"blocks":["84233c6f2c5595e3774ccfa3b1b093395015dfacf306f4c224217d9f2d3e53a0"]},"offset":"9516"},"package.json":{"size":326,"integrity":{"algorithm":"SHA256","hash":"c7f9d65e68dcf8f0989d8f96ad5becc4394d076b05b4b2ca99b3c2f9cb4f1550","blockSize":4194304,"blocks":["c7f9d65e68dcf8f0989d8f96ad5becc4394d076b05b4b2ca99b3c2f9cb4f1550"]},"offset":"12246"},"preload.js":{"size":732,"integrity":{"algorithm":"SHA256","hash":"914c086e7640f9df8e3a565a97c4b4552a1f1bca0f09b4d764df492062d3c5f4","blockSize":4194304,"blocks":["914c086e7640f9df8e3a565a97c4b4552a1f1bca0f09b4d764df492062d3c5f4"]},"offset":"12572"}}}<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>SecondScreen</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style>
  :root {
    --accent: #3689e6;
    --bg: #000;
    --fg: #fff;
  }
  html,body {
    height: 100%;
    margin: 0;
    background: var(--bg);
    color: var(--fg);
    font-family: system-ui, Arial, sans-serif;
    overflow: hidden;
  }

  #controls {
    position: absolute;
    top: 12px;
    left: 12px;
    z-index: 30;
    display: flex;
    gap: 8px;
    align-items: center;
    background: rgba(0,0,0,0.45);
    padding: 8px;
    border-radius: 8px;
  }
  select, button {
    background: rgba(0,0,0,0.8);
    color: var(--fg);
    border: 1px solid rgba(255,255,255,0.08);
    padding: 6px 10px;
    border-radius: 6px;
    outline: none;
  }
  select option {
    background: #000;
    color: var(--fg);
  }
  button {
    cursor: pointer;
  }
  button:hover {
    background: rgba(255,255,255,0.12);
  }

  #vaapi-status {
    position: absolute;
    top: 12px;
    right: 12px;
    z-index: 30;
    background: rgba(0,0,0,0.45);
    padding: 6px 10px;
    border-radius: 8px;
    border: 1px solid rgba(255,255,255,0.06);
    color: var(--fg);
    font-size: 13px;
  }

  #hdmi {
    width: 100vw;
    height: 100vh;
    background: black;
    object-fit: contain;
  }

  #fullscreenBtn {
    position: absolute;
    right: 18px;
    bottom: 18px;
    z-index: 40;
    background: rgba(0,0,0,0.45);
    color: var(--accent);
    border: 1px solid var(--accent);
    padding: 10px 12px;
    border-radius: 10px;
    font-size: 18px;
    cursor: pointer;
  }

  #hint {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    bottom: 18px;
    z-index: 40;
    background: rgba(0,0,0,0.45);
    padding: 6px 10px;
    border-radius: 8px;
    font-size: 13px;
    color: #ddd;
  }

  /* Hide controls and hint in fullscreen */
  :fullscreen #controls,
  :fullscreen #vaapi-status,
  :fullscreen #hint {
    display: none;
  }

  #error {
    position: absolute;
    inset: 0;
    display: none;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 20px;
    z-index: 50;
    background: rgba(0,0,0,0.8);
  }
  #error p { color: #f88; font-size: 18px; max-width: 600px; }
</style>
</head>
<body>
  <div id="controls">
    <label style="font-size:13px;">Device:</label>
    <select id="deviceSelect"></select>

    <label style="font-size:13px;">Resolution:</label>
    <select id="resSelect">
      <option value="1280x720">1280x720</option>
      <option value="1920x1080" selected>1920x1080</option>
      <option value="2560x1440">2560x1440</option>
    </select>

    <label style="font-size:13px;">FPS:</label>
    <select id="fpsSelect">
      <option value="60" selected>60</option>
      <option value="30">30</option>
    </select>

    <button id="startBtn">Start</button>
    <button id="stopBtn" style="display:none;">Stop</button>
    <button id="rememberBtn">Remember</button>
  </div>

  <div id="vaapi-status">VAAPI: <span id="vaapiVal">checking…</span></div>

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

  <button id="fullscreenBtn">⛶</button>
  <div id="hint">Double-click video or press ⛶ to toggle fullscreen</div>

  <div id="error"><p id="errMsg"></p></div>

<script>
(async () => {
  const deviceSelect = document.getElementById('deviceSelect');
  const resSelect = document.getElementById('resSelect');
  const fpsSelect = document.getElementById('fpsSelect');
  const startBtn = document.getElementById('startBtn');
  const stopBtn = document.getElementById('stopBtn');
  const rememberBtn = document.getElementById('rememberBtn');
  const videoEl = document.getElementById('hdmi');
  const vaapiVal = document.getElementById('vaapiVal');
  const fullscreenBtn = document.getElementById('fullscreenBtn');
  const err = document.getElementById('error');
  const errMsg = document.getElementById('errMsg');

  // Auto-adjust FPS based on resolution
  resSelect.addEventListener('change', () => {
    const res = resSelect.value;
    if (res === '2560x1440') {
      // 1440p is capped at 30fps
      fpsSelect.value = '30';
      fpsSelect.disabled = true;
    } else {
      // 720p and 1080p can go up to 60fps
      fpsSelect.disabled = false;
    }
  });

  // Populate devices
  async function populateDevices() {
    const devs = await window.electronAPI.getVideoDevices();
    deviceSelect.innerHTML = '';
    if (!devs || devs.length === 0) {
      deviceSelect.appendChild(new Option('No camera found', ''));
      return;
    }
    devs.forEach(d => {
      const label = d.name || 'Camera';
      const opt = new Option(label, d.path);
      deviceSelect.appendChild(opt);
    });
    
    // Try to preselect remembered device
    const settings = await window.electronAPI.getSettings();
    if (settings.lastDevice) {
      for (let i = 0; i < deviceSelect.options.length; i++) {
        if (deviceSelect.options[i].value === settings.lastDevice) {
          deviceSelect.selectedIndex = i;
          break;
        }
      }
    }
    if (deviceSelect.value === '') {
      deviceSelect.selectedIndex = 0;
    }
  }

  await populateDevices();

  // VAAPI check - restored original logic
  const va = await window.electronAPI.checkVAAPI();
  const vaOk = (va.vainfoAvailable && va.vainfoOut && va.vainfoOut.length > 0) || (va.gpuStatus && va.gpuStatus['gles2']);
  
  if (va.vainfoAvailable) {
    // Quick heuristic: check for H264 or H264Main lines in vainfo output
    const out = va.vainfoOut.toLowerCase();
    const hasH264 = out.includes('h264') || out.includes('hevc') || out.includes('vp9');
    vaapiVal.textContent = hasH264 ? 'ON ✅' : 'available (no decode profiles?)';
    vaapiVal.title = va.vainfoOut.slice(0, 200) + (va.vainfoOut.length > 200 ? '…' : '');
  } else {
    // fallback to GPU info from chromium
    try {
      const gs = va.gpuStatus || {};
      const hwVideo = gs['video_decode'] || gs['video_decode_accelerator'] || gs['video_decode_status'] || '';
      vaapiVal.textContent = (gs && Object.keys(gs).length) ? 'maybe (see GPU status)' : 'NO';
      vaapiVal.title = JSON.stringify(gs, null, 2);
    } catch (e) {
      vaapiVal.textContent = 'unknown';
    }
  }

  // Start stream
  let currentStream = null;
  async function startStream(deviceId, resolution, fps) {
    try {
      // Stop existing stream
      if (currentStream) {
        currentStream.getTracks().forEach(t => t.stop());
        currentStream = null;
        videoEl.srcObject = null;
      }

      // Parse resolution
      const [width, height] = resolution.split('x').map(Number);

      // Use flexible constraints - let the device choose supported resolution
      const constraints = {
        video: {
          deviceId: { exact: deviceId },
          width: { ideal: width },
          height: { ideal: height },
          frameRate: { ideal: Number(fps) }
        },
        audio: false
      };

      const stream = await navigator.mediaDevices.getUserMedia(constraints);
      currentStream = stream;
      videoEl.srcObject = stream;
      await videoEl.play();
      err.style.display = 'none';
      
      // Show stop button, hide start button
      startBtn.style.display = 'none';
      stopBtn.style.display = 'inline-block';
      
      // Log actual resolution
      const track = stream.getVideoTracks()[0];
      const settings = track.getSettings();
      console.log(`Stream started: ${settings.width}x${settings.height} @ ${settings.frameRate}fps`);
    } catch (e) {
      console.error('startStream failed', e);
      err.style.display = 'flex';
      errMsg.textContent = 'Failed to open device: ' + (e.message || String(e)) + 
        '\n\nTry: 1) Granting camera permissions, 2) Selecting a different device';
    }
  }

  function stopStream() {
    if (currentStream) {
      currentStream.getTracks().forEach(t => t.stop());
      currentStream = null;
      videoEl.srcObject = null;
    }
    err.style.display = 'none';
    // Show start button, hide stop button
    startBtn.style.display = 'inline-block';
    stopBtn.style.display = 'none';
  }

  startBtn.addEventListener('click', () => {
    const dev = deviceSelect.value;
    const res = resSelect.value;
    let fps = fpsSelect.value;
    
    // Force 30fps for 1440p regardless of selection
    if (res === '2560x1440') {
      fps = '30';
    }
    
    if (!dev) return alert('Select a device first.');
    startStream(dev, res, fps);
  });

  stopBtn.addEventListener('click', () => {
    stopStream();
  });

  rememberBtn.addEventListener('click', async () => {
    const dev = deviceSelect.value;
    if (!dev) return alert('Select a device first.');
    const settings = await window.electronAPI.getSettings();
    settings.lastDevice = dev;
    await window.electronAPI.saveSettings(settings);
    alert('Saved device: ' + deviceSelect.options[deviceSelect.selectedIndex].text);
  });

  // Fullscreen handlers
  videoEl.addEventListener('dblclick', () => {
    if (!document.fullscreenElement) {
      document.documentElement.requestFullscreen();
    } else {
      document.exitFullscreen();
    }
  });

  fullscreenBtn.addEventListener('click', () => {
    if (!document.fullscreenElement) {
      document.documentElement.requestFullscreen();
    } else {
      document.exitFullscreen();
    }
  });

  // Don't auto-start - wait for user to click Start button

})();
</script>
</body>
</html>
const { app, BrowserWindow, ipcMain } = require('electron');
const path = require('path');
const fs = require('fs');
const { execSync } = require('child_process');

// Enable hardware video decode flags BEFORE app is ready
app.commandLine.appendSwitch('enable-features', 'VaapiVideoDecoder,VaapiVideoEncoder');
app.commandLine.appendSwitch('use-gl', 'desktop');
app.commandLine.appendSwitch('enable-gpu-rasterization');
app.commandLine.appendSwitch('enable-zero-copy');

let settings = {};
const settingsPath = path.join(app.getPath('userData'), 'settings.json');

// Load settings from file
function loadSettings() {
  try {
    if (fs.existsSync(settingsPath)) {
      settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
    }
  } catch (e) {
    console.error('Failed to load settings:', e);
    settings = {};
  }
}

// Save settings to file
function saveSettings() {
  try {
    fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
  } catch (e) {
    console.error('Failed to save settings:', e);
  }
}

// Check VAAPI support
async function checkVAAPI() {
  const result = {
    vainfoAvailable: false,
    vainfoOut: '',
    gpuStatus: {}
  };

  try {
    // Try to run vainfo
    const output = execSync('vainfo', { 
      encoding: 'utf8',
      timeout: 5000,
      stdio: ['pipe', 'pipe', 'pipe']
    });
    result.vainfoAvailable = true;
    result.vainfoOut = output;
  } catch (e) {
    // vainfo not available or failed - try to capture stderr too
    if (e.stdout) result.vainfoOut = e.stdout.toString();
    if (e.stderr && e.stderr.toString().includes('libva')) {
      result.vainfoAvailable = true;
      result.vainfoOut = e.stderr.toString();
    }
    console.log('vainfo check:', e.message);
  }

  // Try to get GPU info from Chromium (async)
  try {
    const gpuInfo = await app.getGPUInfo('basic');
    result.gpuStatus = gpuInfo || {};
  } catch (e) {
    console.log('Could not get GPU info:', e.message);
  }

  return result;
}

function createWindow() {
  loadSettings();
  
  const win = new BrowserWindow({
    width: 1280,
    height: 800,
    fullscreen: false,
    autoHideMenuBar: true,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true
    }
  });
  
  win.loadFile('index.html');
}

// IPC handlers for settings
ipcMain.handle('get-settings', () => settings);
ipcMain.handle('save-settings', (event, newSettings) => {
  settings = newSettings;
  saveSettings();
});

// IPC handler for VAAPI check (async)
ipcMain.handle('check-vaapi', async () => {
  return await checkVAAPI();
});

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});
{
  "name": "secondscreen",
  "version": "1.0.1",
  "description": "Turn your Linux tablet into a second monitor with HDMI capture support",
  "main": "main.js",
  "homepage": "https://github.com/yourusername/secondscreen",
  "author": {
    "name": "Your Name",
    "email": "your.email@example.com"
  },
  "license": "MIT"
}const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  // List all camera devices using Chromium's API
  getVideoDevices: async () => {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices
      .filter(d => d.kind === 'videoinput')
      .map(d => ({ 
        name: d.label || 'Camera', 
        path: d.deviceId
      }));
  },

  // Get settings from main process
  getSettings: () => ipcRenderer.invoke('get-settings'),
  
  // Save settings to main process
  saveSettings: (settings) => ipcRenderer.invoke('save-settings', settings),

  // VAAPI check - now calls main process
  checkVAAPI: () => ipcRenderer.invoke('check-vaapi')
});