Skip to content

Commit

Permalink
v1.1.3 (#392)
Browse files Browse the repository at this point in the history
* 📽 NDI input streams in progress
- Minor improvements

* fix: ProPresenter file import issue with negative unicode characters (#384)

* ✨ Properly decode all EasyWorship ASCII decimal numbers
- Audio will not clear until muted
- Disable closing cloud method popup
- Updated Hungarian

* ✔ Show loading fixes
- Fixed some shows with different id not loading
- Fixed some duplicated shows with same id not loading
- Fixed broken shows freezing

* 📺 Stage improvements
- New show search color
- Optimized preview update rate
- Output screen automatically displays when just two displays
- Drop scripture verses into project
- Bible name included when creating show with multiple versions
- Fixed stage and output auto size not showing text
- Fixed auto size issue
- Importing PowerPoint should work more
- Fixed stage display next slide not working in web
- Fixed stage display slide notes not working in display
- Fixed image preview not showing in stage output slide
- CTRL/CMD + A to select all bible verses
- Improved scripture search

* ✔ Fixed custom user data location not loading
- No more media thumbnails in list mode for fast loading

* ✨ Next and previous buttons will change show in project if any
- Tweaked import formatting
- Video clearing when done playing
- Fixed preview not capturing at first sometimes

* 📺 Custom resolution working in remote

* ✔ Fixed videos clearing before going to next slide
- Version update

---------

Co-authored-by: 0x7A79 <[email protected]>
  • Loading branch information
vassbo and 0x7A79 authored Feb 8, 2024
1 parent c45907a commit 5bd2545
Show file tree
Hide file tree
Showing 58 changed files with 740 additions and 373 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "freeshow",
"version": "1.1.2",
"version": "1.1.3",
"private": true,
"main": "build/electron/index.js",
"description": "Show song lyrics and more for free!",
Expand Down
1 change: 1 addition & 0 deletions public/lang/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,7 @@
"recent": "Nemrég szerkesztett",
"enable_stage": "Színpad engedélyezése",
"select_stage": "Színpad kijelölése",
"next_slide": "Következő dia megjelenítése",
"use_slide_index": "Aktív index használata",
"slide_index": "Dia index",
"padding": "Belső margó",
Expand Down
4 changes: 4 additions & 0 deletions src/electron/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { catchErrors, loadScripture, loadShow, receiveMain, renameShows, saveRec
import { config, stores, updateDataPath, userDataPath } from "./utils/store"
import checkForUpdates from "./utils/updater"
import { loadingOptions, mainOptions } from "./utils/windowOptions"
import { stopReceiversNDI } from "./ndi/ndi"

// ----- STARTUP -----

Expand Down Expand Up @@ -47,6 +48,8 @@ function startApp() {
}

function initialize() {
updateDataPath({ load: true })

// midi
// createVirtualMidi()

Expand Down Expand Up @@ -204,6 +207,7 @@ export async function exitApp() {
dialogClose = false

await closeAllOutputs()
stopReceiversNDI()
closeServers()

// midi
Expand Down
27 changes: 11 additions & 16 deletions src/electron/ndi/capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ function getDefaultCapture(window: BrowserWindow): CaptureOptions {
// START

let storedFrames: any = {}
let cpuInterval: any = null
export function startCapture(id: string, toggle: any = {}, rate: any = {}) {
let window = outputWindows[id]
let windowIsRemoved = !window || window.isDestroyed()
Expand All @@ -69,38 +68,34 @@ export function startCapture(id: string, toggle: any = {}, rate: any = {}) {

console.log("Capture - starting: " + id)

captures[id].window.webContents.beginFrameSubscription(false, processFrame)
if (rate !== "optimized") captures[id].window.webContents.beginFrameSubscription(false, processFrame)
captures[id].subscribed = true

// optimize cpu on low end devices
if (cpuInterval) clearInterval(cpuInterval)
const timeUntilAutoUpdate = 60 // seconds
const frameUpdateRate = rate === "optimized" ? 5 : 2 // seconds
const captureRate = timeUntilAutoUpdate / frameUpdateRate
const autoOptimizePercentageCPU = 95 / 10 // % / 10
let captureCount = captureRate
const captureAmount = 50
let captureCount = captureAmount

if (rate !== "full" && !captures[id].options.ndi) {
cpuInterval = setInterval(cpuCapture, frameUpdateRate * 1000)
}
if (rate !== "full" && (rate === "optimized" || !captures[id].options.ndi)) cpuCapture()

async function cpuCapture() {
if (!captures[id] || captures[id].window.isDestroyed() || captures[id].window.webContents.isBeingCaptured()) return
if (!captures[id] || captures[id].window.isDestroyed()) return

let usage = process.getCPUUsage()

let isOptimizedOrLagging = rate === "optimized" || usage.percentCPUUsage > autoOptimizePercentageCPU || captureCount < captureRate
let isOptimizedOrLagging = rate === "optimized" || usage.percentCPUUsage > autoOptimizePercentageCPU || captureCount < captureAmount
if (isOptimizedOrLagging) {
if (captureCount === captureRate) captureCount = 0
if (captureCount > captureAmount) captureCount = 0
// limit frames
captures[id].window.webContents.endFrameSubscription()
if (captures[id].window.webContents.isBeingCaptured()) captures[id].window.webContents.endFrameSubscription()
let image = await captures[id].window.webContents.capturePage()
sendFrames(id, image, { previewFrame: true, serverFrame: true, ndiFrame: true })

// capture for 60 seconds then get cpu again
captureCount++
setTimeout(cpuCapture, rate === "optimized" ? 300 : 100)
} else {
captureCount = captureRate
captureCount = captureAmount
captures[id].window.webContents.beginFrameSubscription(false, processFrame)
}
}
Expand Down Expand Up @@ -224,7 +219,7 @@ export function updatePreviewResolution(data: any) {
if (data.id) sendFrames(data.id, storedFrames[data.id], { previewFrame: true })
}

function resizeImage(image: NativeImage, initialSize: Size, newSize: Size) {
export function resizeImage(image: NativeImage, initialSize: Size, newSize: Size) {
if (initialSize.width / initialSize.height >= newSize.width / newSize.height) image = image.resize({ width: newSize.width })
else image = image.resize({ height: newSize.height })

Expand Down
86 changes: 61 additions & 25 deletions src/electron/ndi/ndi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const ndiDisabled = isLinux && os.arch() !== "x64" && os.arch() !== "ia32"

let timeStart = BigInt(Date.now()) * BigInt(1e6) - process.hrtime.bigint()

// RECEIVER

export async function findStreamsNDI(): Promise<any> {
if (ndiDisabled) return
const grandiose = require("grandiose")
Expand All @@ -27,53 +29,87 @@ export async function findStreamsNDI(): Promise<any> {
.find({ showLocalSources: true })
.then((a: any) => {
// embedded, destroy(), sources(), wait()
let sources = a.sources()
resolve(sources)
// sources = [{ name: "<source_name>", urlAddress: "<IP-address>:<port>" }]
// this is returned as JSON string
resolve(a.sources())
})
.catch((err: any) => reject(err))
})
}

// let source = { name: "<source_name>", urlAddress: "<IP-address>:<port>" };
export async function receiveStreamNDI({ source }: any) {
if (receivers[source.urlAddress]) return

const receiverTimeout = 5000
export async function receiveStreamFrameNDI({ source }: any) {
if (ndiDisabled) return
const grandiose = require("grandiose")

// WIP this just crashes
let receiver = await grandiose.receive({ source })

let timeout = 5000 // Optional timeout, default is 10000ms

try {
let videoFrame = await receiver.video(timeout)
toApp("NDI", { channel: "RECEIVE_STREAM", data: videoFrame })
let videoFrame = await receiver.video(receiverTimeout)
sendBuffer(source.id, videoFrame)
} catch (err) {
console.error(err)
}
}

// receivers[source.urlAddress] = setInterval(async () => {
// let videoFrame = await receiver.video(timeout)
// let previewSize: Size = { width: 320, height: 180 }
export function sendBuffer(id: string, frame: any) {
if (!frame) return

// toApp("NDI", { channel: "RECEIVE_STREAM", data: videoFrame })
// }, 100)
frame.data = Buffer.from(frame.data)
toApp("NDI", { channel: "RECEIVE_STREAM", data: { id, frame } })

// for ( let x = 0 ; x < 10 ; x++) {
// let videoFrame = await receiver.video(timeout);
// return videoFrame
// }
} catch (e) {
console.error(e)
// const image: NativeImage = nativeImage.createFromBuffer(frame.data)
// // image = resizeImage(image, {width: frame.xres, height: frame.yres}, previewSize)
// const buffer = image.getBitmap()

// /* convert from ARGB/BGRA (Electron/Chromium capture output) to RGBA (Web canvas) */
// if (os.endianness() === "BE") util.ImageBufferAdjustment.ARGBtoRGBA(buffer)
// else util.ImageBufferAdjustment.BGRAtoRGBA(buffer)

// frame.data = buffer
// toApp("NDI", { channel: "RECEIVE_STREAM", data: { id, frame } })
}

export async function captureStreamNDI({ source, frameRate }: any) {
if (NDI_RECEIVERS[source.id]) return

if (ndiDisabled) return
const grandiose = require("grandiose")

NDI_RECEIVERS[source.id] = {
frameRate: frameRate || 0.1,
}
NDI_RECEIVERS[source.id].receiver = await grandiose.receive({ source })

try {
NDI_RECEIVERS[source.id].interval = setInterval(async () => {
let videoFrame = await NDI_RECEIVERS[source.id].receiver.video(receiverTimeout)

toApp("NDI", { channel: "RECEIVE_STREAM", data: { id: source.id, frame: videoFrame } })
}, NDI_RECEIVERS[source.id].frameRate * 1000)
} catch (err) {
console.error(err)
}
}

let receivers: any = {}
export function stopReceiversNDI() {
Object.values(receivers).forEach((interval: any) => {
let NDI_RECEIVERS: any = {}
export function stopReceiversNDI(data: any = null) {
if (data?.id) {
clearInterval(NDI_RECEIVERS[data.id].interval)
delete NDI_RECEIVERS[data.id]
return
}

Object.values(NDI_RECEIVERS).forEach((interval: any) => {
clearInterval(interval)
})

receivers = {}
NDI_RECEIVERS = {}
}

// SENDER

export function stopSenderNDI(id: string) {
if (!NDI[id]?.timer) return

Expand Down
7 changes: 4 additions & 3 deletions src/electron/ndi/talk.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { NDI } from "../../types/Channels"
import { Message } from "../../types/Socket"
import { customFramerates, updateFramerate } from "./capture"
import { findStreamsNDI, receiveStreamNDI, stopReceiversNDI } from "./ndi"
import { captureStreamNDI, findStreamsNDI, receiveStreamFrameNDI, stopReceiversNDI } from "./ndi"

export async function receiveNDI(e: any, msg: Message) {
let data: any = {}
Expand All @@ -12,8 +12,9 @@ export async function receiveNDI(e: any, msg: Message) {

export const ndiResponses: any = {
RECEIVE_LIST: async () => await findStreamsNDI(),
RECEIVE_STREAM: (data: any) => receiveStreamNDI(data),
RECEIVE_DESTROY: () => stopReceiversNDI(),
RECEIVE_STREAM: (data: any) => receiveStreamFrameNDI(data),
CAPTURE_STREAM: (data: any) => captureStreamNDI(data),
CAPTURE_DESTROY: (data: any) => stopReceiversNDI(data),

NDI_DATA: (data: any) => setDataNDI(data),

Expand Down
24 changes: 17 additions & 7 deletions src/electron/utils/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import path from "path"
import { FILE_INFO, MAIN, OPEN_FOLDER, READ_FOLDER, SHOW, STORE } from "../../types/Channels"
import { OPEN_FILE, READ_EXIF } from "./../../types/Channels"
import { mainWindow, toApp } from "./../index"
import { trimShow } from "./responses"
import { getAllShows, trimShow } from "./responses"
import { stores } from "./store"
import { uid } from "uid"

function actionComplete(err: Error | null, actionFailedMessage: string) {
if (err) console.error(actionFailedMessage + ":", err)
Expand Down Expand Up @@ -164,7 +165,7 @@ export function loadFile(p: string, contentId: string = ""): any {
let show = parseShow(content)
if (!show) return { error: "not_found", id: contentId }

if (contentId && show[0] !== contentId) return { error: "not_found", id: contentId, file_id: show[0] }
if (contentId && show[0] !== contentId) show[0] = contentId

return { id: contentId, content: show }
}
Expand Down Expand Up @@ -240,7 +241,7 @@ export function selectFolder(e: any, msg: { channel: string; title: string | und

if (msg.channel === "SHOWS") {
loadShows({ showsPath: folder })
toApp(MAIN, { channel: "FULL_SHOWS_LIST", data: readFolder(folder) })
toApp(MAIN, { channel: "FULL_SHOWS_LIST", data: getAllShows({ path: folder }) })
}

e.reply(OPEN_FOLDER, { channel: msg.channel, data: { path: folder } })
Expand Down Expand Up @@ -339,10 +340,15 @@ export function loadShows({ showsPath }: any) {

for (const name of filesInFolder) checkShow(name)
function checkShow(name: string) {
if (!name.includes(".show")) return
if (!name.toLowerCase().includes(".show")) return

let matchingShowId = Object.entries(cachedShows).find(([_id, a]: any) => a.name === name.slice(0, -5))?.[0]
if (matchingShowId) {
let trimmedName = name.slice(0, -5) // remove .show

// no name results in the id trying to be read leading to show not found
if (!trimmedName) return

let matchingShowId = Object.entries(cachedShows).find(([_id, a]: any) => a.name === trimmedName)?.[0]
if (matchingShowId && !newCachedShows[matchingShowId]) {
newCachedShows[matchingShowId] = cachedShows[matchingShowId]
return
}
Expand All @@ -353,7 +359,11 @@ export function loadShows({ showsPath }: any) {

if (!show || !show[1]) return

newCachedShows[show[0]] = trimShow({ ...show[1], name: name.replace(".show", "") })
let id = show[0]
// some old duplicated shows might have the same id
if (newCachedShows[id]) id = uid()

newCachedShows[id] = trimShow({ ...show[1], name: trimmedName })
}

toApp(STORE, { channel: "SHOWS", data: newCachedShows })
Expand Down
44 changes: 37 additions & 7 deletions src/electron/utils/output.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BrowserWindow } from "electron"
import { BrowserWindow, Rectangle, screen } from "electron"
import { isMac, loadWindowContent, mainWindow, toApp } from ".."
import { MAIN, OUTPUT } from "../../types/Channels"
import { Message } from "../../types/Socket"
Expand Down Expand Up @@ -120,12 +120,9 @@ function displayOutput(data: any) {

/////

let bounds = data.output.bounds
let xDif = bounds?.x - mainWindow!.getBounds().x
let yDif = bounds?.y - mainWindow!.getBounds().y

const margin = 50
let windowNotCoveringMain: boolean = xDif > margin || yDif < -margin || (xDif < -margin && yDif > margin)
if (data.autoPosition && !data.force) data.output.bounds = getSecondDisplay(data.output.bounds)
let bounds: Rectangle = data.output.bounds
let windowNotCoveringMain: boolean = isNotCovered(mainWindow!.getBounds(), bounds)

if (data.enabled && bounds && (data.force || window.isAlwaysOnTop() === false || windowNotCoveringMain)) {
showWindow(window)
Expand All @@ -141,6 +138,39 @@ function displayOutput(data: any) {
if (data.one !== true) toApp(OUTPUT, { channel: "DISPLAY", data })
}

function getSecondDisplay(bounds: Rectangle) {
let displays = screen.getAllDisplays()
if (displays.length !== 2) return bounds

let mainWindowBounds = mainWindow!.getBounds()
let amountCoveredByWindow = amountCovered(displays[1].bounds, mainWindowBounds)

let secondDisplay = displays[1]
if (amountCoveredByWindow > 0.5) secondDisplay = displays[0]

return secondDisplay.bounds
}

function isNotCovered(mainBounds: Rectangle, secondBounds: Rectangle) {
let xDif = secondBounds?.x - mainBounds.x
let yDif = secondBounds?.y - mainBounds.y

const margin = 50
let secondNotCoveringMain: boolean = xDif > margin || yDif < -margin || (xDif < -margin && yDif > margin)
return secondNotCoveringMain
}

function amountCovered(displayBounds: Rectangle, windowBounds: Rectangle) {
const overlapX = Math.max(0, Math.min(displayBounds.x + displayBounds.width, windowBounds.x + windowBounds.width) - Math.max(displayBounds.x, windowBounds.x))
const overlapY = Math.max(0, Math.min(displayBounds.y + displayBounds.height, windowBounds.y + windowBounds.height) - Math.max(displayBounds.y, windowBounds.y))
const overlapArea = overlapX * overlapY

const totalArea = displayBounds.width * displayBounds.height
const overlapAmount = overlapArea / totalArea

return overlapAmount
}

// MacOS Menu Bar
// https://stackoverflow.com/questions/39091964/remove-menubar-from-electron-app
// https://stackoverflow.com/questions/69629262/how-can-i-hide-the-menubar-from-an-electron-app
Expand Down
4 changes: 2 additions & 2 deletions src/electron/utils/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ function deleteShowsNotIndexed(data: any) {
toApp("MAIN", { channel: "DELETE_SHOWS", data: { deleted } })
}

function getAllShows(data: any) {
let filesInFolder: string[] = readFolder(data.path).filter((a) => a.includes(".show"))
export function getAllShows(data: any) {
let filesInFolder: string[] = readFolder(data.path).filter((a) => a.includes(".show") && a.length > 5)
return filesInFolder
}

Expand Down
Loading

0 comments on commit 5bd2545

Please sign in to comment.