Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ jobs:
run:
working-directory: input_viewer_electron
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: 'npm'
Expand All @@ -54,7 +54,7 @@ jobs:
new_version: ${{ steps.new_version.outputs.version }}
should_release: ${{ steps.decide.outputs.should_release }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0

Expand Down Expand Up @@ -136,10 +136,10 @@ jobs:
run:
working-directory: input_viewer_electron
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: 'npm'
Expand All @@ -166,7 +166,7 @@ jobs:

- name: Upload artifacts
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: macos-build
path: |
Expand All @@ -186,10 +186,10 @@ jobs:
run:
working-directory: input_viewer_electron
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: 'npm'
Expand All @@ -215,7 +215,7 @@ jobs:

- name: Upload artifacts
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: windows-build
path: |
Expand All @@ -233,10 +233,10 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'

Expand Down Expand Up @@ -270,13 +270,13 @@ jobs:
contents: write
steps:
- name: Download macOS artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: macos-build
path: ./artifacts/mac

- name: Download Windows artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: windows-build
path: ./artifacts/win
Expand All @@ -294,7 +294,7 @@ jobs:
echo "tag=v${{ needs.analyze.outputs.new_version }}" >> $GITHUB_OUTPUT

- name: Create Release in source repo
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
tag_name: ${{ steps.tag.outputs.tag }}
files: |
Expand All @@ -306,7 +306,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Create Release in public releases repo
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
repository: LAB271/input-viewer-releases
tag_name: ${{ steps.tag.outputs.tag }}
Expand Down
5 changes: 1 addition & 4 deletions input_viewer_electron/src/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ const path = require('path')
const fs = require('fs')
const { exec } = require('child_process')

// Enable hardware acceleration for video capture
app.commandLine.appendSwitch('enable-accelerated-mjpeg-decode')
app.commandLine.appendSwitch('enable-accelerated-video-decode')
// Hardware acceleration for video decode/rendering
app.commandLine.appendSwitch('ignore-gpu-blocklist')
app.commandLine.appendSwitch('enable-native-gpu-memory-buffers')
app.commandLine.appendSwitch('enable-gpu-rasterization')

// Keep a global reference of the window object
Expand Down
57 changes: 35 additions & 22 deletions input_viewer_electron/src/renderer/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,10 @@ function captureFrame() {

async function getVideoDevices() {
try {
// Request permission first
await navigator.mediaDevices.getUserMedia({ video: true })

// Request permission first, then immediately release the stream
const permissionStream = await navigator.mediaDevices.getUserMedia({ video: true })
permissionStream.getTracks().forEach(track => track.stop())

const devices = await navigator.mediaDevices.enumerateDevices()
state.devices = devices.filter(device => device.kind === 'videoinput')

Expand Down Expand Up @@ -405,7 +406,8 @@ async function startVideoStream(deviceId, videoElement, side) {
video: {
deviceId: { exact: deviceId },
width: { ideal: 4096 },
height: { ideal: 2160 }
height: { ideal: 2160 },
frameRate: { ideal: 60 }
},
audio: {
deviceId: { exact: deviceId }
Expand All @@ -422,46 +424,57 @@ async function startVideoStream(deviceId, videoElement, side) {
video: {
deviceId: { exact: deviceId },
width: { ideal: 4096 },
height: { ideal: 2160 }
height: { ideal: 2160 },
frameRate: { ideal: 60 }
}
}
stream = await navigator.mediaDevices.getUserMedia(videoOnlyConstraints)
}
videoElement.srcObject = stream

// Some capture devices start at low resolution and need a restart to get high-res
// Check resolution after stream starts and retry if too low
// Log stream info for diagnostics
const track = stream.getVideoTracks()[0]
const settings = track.getSettings()
const caps = track.getCapabilities()
console.log(`[Video] ${side} stream: ${settings.width}x${settings.height} @ ${settings.frameRate}fps`)
console.log(`[Video] ${side} capabilities: ${caps.width?.max}x${caps.height?.max} @ ${caps.frameRate?.max}fps`)

if (caps.width && settings.width < caps.width.max) {
console.log(`[Video] Got ${settings.width}x${settings.height}, device supports up to ${caps.width.max}x${caps.height.max}. Restarting for higher res...`)

// Stop current stream
stream.getTracks().forEach(t => t.stop())
// Some capture cards start at low default resolution and need a retry.
// Cap target at 1920x1080 to prefer uncompressed formats over MJPEG.
if (settings.width <= 640 && caps.width?.max > 640) {
const targetWidth = Math.min(caps.width.max, 1920)
const targetHeight = Math.min(caps.height.max, 1080)
console.log(`[Video] ${side} resolution too low, retrying for ${targetWidth}x${targetHeight}...`)

// Small delay then request max resolution
await new Promise(resolve => setTimeout(resolve, 100))

// Check if original stream had audio
const hasAudio = stream.getAudioTracks().length > 0
stream.getTracks().forEach(t => t.stop())
await new Promise(resolve => setTimeout(resolve, 300))

const retryConstraints = {
video: {
deviceId: { exact: deviceId },
width: { ideal: caps.width.max },
height: { ideal: caps.height.max }
width: { ideal: targetWidth },
height: { ideal: targetHeight },
frameRate: { ideal: 60 }
}
}
if (hasAudio) {
retryConstraints.audio = { deviceId: { exact: deviceId } }
}
stream = await navigator.mediaDevices.getUserMedia(retryConstraints)
videoElement.srcObject = stream

const newSettings = stream.getVideoTracks()[0].getSettings()
console.log(`[Video] Retry got ${newSettings.width}x${newSettings.height}`)
try {
stream = await navigator.mediaDevices.getUserMedia(retryConstraints)
videoElement.srcObject = stream
const retrySettings = stream.getVideoTracks()[0].getSettings()
console.log(`[Video] ${side} retry: ${retrySettings.width}x${retrySettings.height} @ ${retrySettings.frameRate}fps`)
} catch (e) {
console.warn(`[Video] ${side} retry failed: ${e.message}`)
// Re-acquire at default resolution
stream = await navigator.mediaDevices.getUserMedia({
video: { deviceId: { exact: deviceId } }
})
videoElement.srcObject = stream
}
}

// Store stream reference
Expand Down
7 changes: 5 additions & 2 deletions input_viewer_electron/src/renderer/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,13 @@ body.single-view #video-wrapper {
height: 100%;
object-fit: contain;
background-color: #000;
/* GPU acceleration and crisp rendering */
/* GPU-composited rendering with high-quality upscaling.
filter: contrast(1) is a no-op that forces Chromium to render through
its GPU filter pipeline, which uses better interpolation for scaling. */
transform: translateZ(0);
will-change: contents;
image-rendering: -webkit-optimize-contrast;
image-rendering: high-quality;
filter: contrast(1);
}

/* Single view: video shows full content without cropping */
Expand Down
Loading