feat: initial release — SRT Streamer v1.0.0
Cross-platform Electron + React + FFmpeg desktop app for sending multiple SRT streams simultaneously. Features: - Multiple simultaneous SRT output streams - Video sources: desktop, window capture, cameras, capture cards - Audio sources: microphones, system loopback, sound cards - H.264 encoding with HW acceleration (NVENC/QSV/AMF/VideoToolbox) - SRT modes: caller / listener / rendezvous - Frame profile presets (4K, 1080p, 720p, 480p, 360p) - Tolbek SRT receiver with configurable mode - System tray: minimize-to-tray, exit confirmation dialog - Portable build via electron-builder Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Creates minimal placeholder PNG icons without any dependencies.
|
||||
* Run: node assets/create-placeholder-icons.js
|
||||
*/
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// Minimal valid 16x16 PNG (dark blue background)
|
||||
function createSimplePng(size) {
|
||||
// Use a canvas-like approach with raw PNG data
|
||||
// This creates a simple colored square PNG
|
||||
const { createCanvas } = (() => {
|
||||
try { return require('canvas') } catch { return null }
|
||||
})() || {}
|
||||
|
||||
if (createCanvas) {
|
||||
const canvas = createCanvas(size, size)
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// Background
|
||||
ctx.fillStyle = '#1a1a2e'
|
||||
const r = size * 0.19
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(r, 0)
|
||||
ctx.lineTo(size - r, 0)
|
||||
ctx.quadraticCurveTo(size, 0, size, r)
|
||||
ctx.lineTo(size, size - r)
|
||||
ctx.quadraticCurveTo(size, size, size - r, size)
|
||||
ctx.lineTo(r, size)
|
||||
ctx.quadraticCurveTo(0, size, 0, size - r)
|
||||
ctx.lineTo(0, r)
|
||||
ctx.quadraticCurveTo(0, 0, r, 0)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
// Play triangle
|
||||
const cx = size / 2, cy = size / 2
|
||||
const s = size * 0.3
|
||||
ctx.fillStyle = '#4f8ef7'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(cx - s * 0.5, cy - s * 0.6)
|
||||
ctx.lineTo(cx + s * 0.7, cy)
|
||||
ctx.lineTo(cx - s * 0.5, cy + s * 0.6)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
// Red dot
|
||||
ctx.fillStyle = '#ef4444'
|
||||
ctx.beginPath()
|
||||
ctx.arc(size * 0.75, size * 0.25, size * 0.1, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
|
||||
return canvas.toBuffer('image/png')
|
||||
}
|
||||
|
||||
// Fallback: minimal 1x1 PNG
|
||||
return Buffer.from(
|
||||
'89504e470d0a1a0a0000000d4948445200000001000000010802000000' +
|
||||
'9001' + '2e00000000c4944415408d7626060600000000200014fd7181000000049454e44ae426082', 'hex'
|
||||
)
|
||||
}
|
||||
|
||||
const sizes = [
|
||||
{ name: 'icon.png', size: 512 },
|
||||
{ name: 'tray-icon.png', size: 32 }
|
||||
]
|
||||
|
||||
for (const { name, size } of sizes) {
|
||||
const buf = createSimplePng(size)
|
||||
fs.writeFileSync(path.join(__dirname, name), buf)
|
||||
console.log(`Created ${name}`)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Run with: node assets/generate-icons.js
|
||||
* Requires: npm install sharp (dev only)
|
||||
* Generates PNG icons from SVG for all platforms.
|
||||
* If sharp is not available, falls back to copying a placeholder.
|
||||
*/
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
async function generate() {
|
||||
const svgPath = path.join(__dirname, 'icon.svg')
|
||||
const svgData = fs.readFileSync(svgPath)
|
||||
|
||||
try {
|
||||
const sharp = require('sharp')
|
||||
// PNG 512x512
|
||||
await sharp(svgData).resize(512, 512).png().toFile(path.join(__dirname, 'icon.png'))
|
||||
console.log('icon.png generated')
|
||||
|
||||
// Tray icon 32x32
|
||||
await sharp(svgData).resize(32, 32).png().toFile(path.join(__dirname, 'tray-icon.png'))
|
||||
console.log('tray-icon.png generated')
|
||||
|
||||
console.log('Done. For ICO/ICNS, use electron-icon-builder or convert manually.')
|
||||
} catch (e) {
|
||||
console.log('sharp not available, creating placeholder PNG...')
|
||||
// Create a minimal 1x1 placeholder PNG (will work as fallback)
|
||||
const minPng = Buffer.from(
|
||||
'89504e470d0a1a0a0000000d49484452000000010000000108020000009001' +
|
||||
'2e000000000c4944415408d76360f8cfc00000000200017e21bc330000000049454e44ae426082',
|
||||
'hex'
|
||||
)
|
||||
fs.writeFileSync(path.join(__dirname, 'icon.png'), minPng)
|
||||
fs.writeFileSync(path.join(__dirname, 'tray-icon.png'), minPng)
|
||||
console.log('Placeholder icons created.')
|
||||
}
|
||||
}
|
||||
|
||||
generate().catch(console.error)
|
||||
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#1a1a2e"/>
|
||||
<stop offset="100%" stop-color="#16213e"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accent" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#4f8ef7"/>
|
||||
<stop offset="100%" stop-color="#22c55e"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="256" height="256" rx="48" fill="url(#bg)"/>
|
||||
<!-- Signal waves -->
|
||||
<path d="M60 128 Q60 68 128 68 Q196 68 196 128 Q196 188 128 188 Q60 188 60 128" fill="none" stroke="#4f8ef7" stroke-width="8" stroke-opacity="0.3" stroke-dasharray="8 6"/>
|
||||
<!-- Play triangle -->
|
||||
<polygon points="100,90 172,128 100,166" fill="url(#accent)" opacity="0.9"/>
|
||||
<!-- Live dot -->
|
||||
<circle cx="188" cy="68" r="18" fill="#ef4444"/>
|
||||
<circle cx="188" cy="68" r="10" fill="#ffffff"/>
|
||||
<!-- SRT text -->
|
||||
<text x="128" y="220" font-family="Arial,sans-serif" font-size="22" font-weight="700" fill="#94a3b8" text-anchor="middle" letter-spacing="4">SRT</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
Reference in New Issue
Block a user