Repository URL to install this package:
|
Version:
1.0.1 ▾
|
ø ô ï {"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')
});