Skip to content

Commit

Permalink
Remove thumbnails (#665)
Browse files Browse the repository at this point in the history
* Simplifying Output

* Moved Values

* Moved Visibility

* Move outputwindows to outputhelper

* Moved more code to bounds

* Added LifeCycle

* Fully replaced output with OutputHelper

* Moved OutputHelper

* Updated all code to reference Output object instead of outputWindows[]

* Got previewWindow displaying

* Locked preview to window

* Got screens aligned and sticking in place

* Fixed minimize/restore and add/remove outputs

* Removed unused preview capture code

* Re-enabled ndi and server outputs without beginFrameSubscription

* Added CaptureHelper

* Reorganized

* Moved captures list to Output.captureOptions

* Moved CaptureTransmitter

* Code cleanup

* Removed output windows

* Got good performance out of preview renders

* Previews look good except full screen

* Fixed fullscreen

* Mirrored preview outputs

* Got preview videos playing smoothly

* Fixed website scaling
  • Loading branch information
jzongker authored Jul 16, 2024
1 parent c74057b commit 454e400
Show file tree
Hide file tree
Showing 24 changed files with 990 additions and 791 deletions.
72 changes: 72 additions & 0 deletions src/electron/capture/CaptureHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { BrowserWindow, Display, NativeImage, Size } from "electron"
import electron from "electron"
import { NdiSender } from "../ndi/NdiSender"
import { CaptureTransmitter } from "./helpers/CaptureTransmitter"
import { CaptureOptions } from "./CaptureOptions"
import { CaptureLifecycle } from "./helpers/CaptureLifecycle"
import { OutputHelper } from "../output/OutputHelper"

export class CaptureHelper {
static Lifecycle = CaptureLifecycle
static Transmitter = CaptureTransmitter

private static framerates: any = {
server: 30,
unconnected: 1,
connected: 30,
}
static customFramerates: any = {}

static getDefaultCapture(window: BrowserWindow, id: string): CaptureOptions {
let screen: Display = this.getWindowScreen(window)

let defaultFramerates = {
server: this.framerates.server,
ndi: this.framerates.connected,
}

return {
window,
subscribed: false,
displayFrequency: screen.displayFrequency || 60,
options: { server: false, ndi: false },
framerates: defaultFramerates,
id,
}
}

// START

static storedFrames: any = {}

static updateFramerate(id: string) {
const captureOptions = OutputHelper.getOutput(id)?.captureOptions
if (!captureOptions) return

if (NdiSender.NDI[id]) {
let ndiFramerate = this.framerates.unconnected
if (NdiSender.NDI[id].status === "connected") ndiFramerate = this.customFramerates[id]?.ndi || this.framerates.connected

if (captureOptions.framerates.ndi !== parseInt(ndiFramerate)) {
captureOptions.framerates.ndi = parseInt(ndiFramerate)
CaptureTransmitter.startChannel(id, "ndi")
}
}
}

static getWindowScreen(window: BrowserWindow) {
return electron.screen.getDisplayMatching({
x: window.getBounds().x,
y: window.getBounds().y,
width: window.getBounds().width,
height: window.getBounds().height,
})
}

static 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 })

return image
}
}
10 changes: 10 additions & 0 deletions src/electron/capture/CaptureOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { BrowserWindow } from "electron"

export type CaptureOptions = {
id: string
window: BrowserWindow
subscribed: boolean
displayFrequency: number
options: any
framerates: any
}
86 changes: 86 additions & 0 deletions src/electron/capture/helpers/CaptureLifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { NativeImage } from "electron"
import { CaptureHelper } from "../CaptureHelper"
import { OutputHelper } from "../../output/OutputHelper"

export class CaptureLifecycle {
static startCapture(id: string, toggle: any = {}) {
const output = OutputHelper.getOutput(id)
let window = output?.window
let windowIsRemoved = !window || window.isDestroyed()
if (windowIsRemoved) {
delete output.captureOptions
return
}

if (!output.captureOptions) output.captureOptions = CaptureHelper.getDefaultCapture(window, id)

if (output.captureOptions) {
const capture = output.captureOptions
Object.keys(toggle).map((key) => {
capture.options[key] = toggle[key]
})
}

CaptureHelper.updateFramerate(id)

if (output.captureOptions.subscribed) return
CaptureHelper.Transmitter.startTransmitting(id)
output.captureOptions.subscribed = true

cpuCapture()
async function cpuCapture() {
if (!output.captureOptions || output.captureOptions.window.isDestroyed()) return
let image = await output.captureOptions.window.webContents.capturePage()
processFrame(image)
let frameRate = output.captureOptions.framerates.ndi
if (output.captureOptions.framerates.server > frameRate) frameRate = output.captureOptions.framerates.server
const ms = Math.round(1000 / frameRate)
setTimeout(cpuCapture, ms)
}

function processFrame(image: NativeImage) {
CaptureHelper.storedFrames[id] = image
}
}

// STOP
static stopAllCaptures() {
OutputHelper.getAllOutputs().forEach((output) => {
if (output[1].captureOptions) this.stopCapture(output[0])
})
}

static stopCapture(id: string) {
const output = OutputHelper.getOutput(id)
const capture = output.captureOptions
return new Promise((resolve) => {
if (!capture) return resolve(true)
CaptureHelper.Transmitter.removeAllChannels(id)
let windowIsRemoved = !capture.window || capture.window.isDestroyed()
if (windowIsRemoved) return deleteAndResolve()

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

endSubscription()
removeListeners()
deleteAndResolve()

function deleteAndResolve() {
delete output.captureOptions
resolve(true)
}
})

function endSubscription() {
if (!capture?.subscribed) return

capture.window.webContents.endFrameSubscription()
capture.subscribed = false
}

function removeListeners() {
capture?.window.removeAllListeners()
capture?.window.webContents.removeAllListeners()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import type { NativeImage, Size } from "electron"
import os from "os"
import { toApp } from ".."
import { OUTPUT, OUTPUT_STREAM } from "../../types/Channels"
import { toServer } from "../servers"
import { sendToWindow } from "./output"
import util from "../ndi/vingester-util"
import { NdiSender } from "../ndi/NdiSender"
import { CaptureOptions, captures, previewSize, storedFrames } from "./capture"

import { OUTPUT_STREAM } from "../../../types/Channels"
import { toServer } from "../../servers"
import util from "../../ndi/vingester-util"
import { NdiSender } from "../../ndi/NdiSender"
import { CaptureHelper } from "../CaptureHelper"
import { CaptureOptions } from "../CaptureOptions"
import { OutputHelper } from "../../output/OutputHelper"

export type Channel = {
key: string
Expand All @@ -17,17 +16,15 @@ export type Channel = {
}

export class CaptureTransmitter {

static stageWindows: string[] = []
static requestList: any[] = []
//static ndiFrameCount = 0
static channels: { [key: string]: Channel } = {}


static startTransmitting(captureId: string) {
const capture = captures[captureId]
const capture = OutputHelper.getOutput(captureId)?.captureOptions
if (!capture) return
this.startChannel(captureId, "preview")
//this.startChannel(captureId, "preview")
if (capture.options.ndi) this.startChannel(captureId, "ndi")
if (capture.options.server) this.startChannel(captureId, "server")

Expand All @@ -36,87 +33,59 @@ export class CaptureTransmitter {
/*
console.log("SETTING INTERVAL");
setInterval(() => {
console.log("NDI FRAMES:", CaptureTransmitter.ndiFrameCount, " - ", captureId);
CaptureTransmitter.ndiFrameCount = 0
console.log("NDI FRAMES:", this.ndiFrameCount, " - ", captureId);
this.ndiFrameCount = 0
},1000);
*/
}
}

static startChannel(captureId: string, key: string) {
const combinedKey = `${captureId}-${key}`
const interval = 1000 / captures[captureId].framerates[key]
const interval = 1000 / OutputHelper.getOutput(captureId)?.captureOptions?.framerates[key] || 10

if (this.channels[combinedKey]?.timer) {
clearInterval(this.channels[combinedKey].timer)
this.channels[combinedKey].timer = setInterval(() => this.handleChannelInterval(captureId, key), interval)
} else {
this.channels[combinedKey] = {
key,
captureId,
timer: setInterval(() => this.handleChannelInterval(captureId, key), interval),
lastImage: storedFrames[captureId]
this.channels[combinedKey] = {
key,
captureId,
timer: setInterval(() => this.handleChannelInterval(captureId, key), interval),
lastImage: CaptureHelper.storedFrames[captureId],
}
}
}

static handleChannelInterval(captureId:string, key:string) {
static handleChannelInterval(captureId: string, key: string) {
const combinedKey = `${captureId}-${key}`
const channel = this.channels[combinedKey]
if (!channel) return
const image = storedFrames[captureId]
if (!image || channel.lastImage===image) return
const image = CaptureHelper.storedFrames[captureId]
if (!image || channel.lastImage === image) return
const size = image.getSize()
channel.lastImage = image
switch (key) {
case "preview":
this.sendBufferToPreview(channel.captureId, image, { size })
break
//case "preview":
//this.sendBufferToPreview(channel.captureId, image, { size })
//break
case "ndi":
this.sendBufferToNdi(channel.captureId, image, { size })
break
case "server":
this.sendBufferToServer(captures[captureId], image)
const options = OutputHelper.getOutput(captureId)?.captureOptions
if (options) this.sendBufferToServer(options, image)
break
}

}

/*
static async sendFrames(capture: CaptureOptions, image: NativeImage, rates: any) {
if (!capture || !image) return
const size = image.getSize()
if (rates.previewFrame) this.sendBufferToPreview(capture.id, image, { size })
if (rates.serverFrame && capture.options.server) this.sendBufferToServer(capture, image)
if (rates.ndiFrame && capture.options.ndi) this.sendBufferToNdi(capture.id, image, { size })
}
*/

// NDI
static sendBufferToNdi(captureId:string, image: NativeImage, { size }: any) {
static sendBufferToNdi(captureId: string, image: NativeImage, { size }: any) {
const buffer = image.getBitmap()
const ratio = image.getAspectRatio()
//this.ndiFrameCount++
// WIP refresh on enable?
NdiSender.sendVideoBufferNDI(captureId, buffer, { size, ratio, framerate: captures[captureId].framerates.ndi })
}

// PREVIEW
static sendBufferToPreview(captureId:string, image: NativeImage, options: any) {
if (!image) return
image = this.resizeImage(image, options.size, previewSize)

const buffer = image.getBitmap()
const size = image.getSize()

/* 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)

let msg = { channel: "PREVIEW", data: { id:captureId, buffer, size, originalSize: options.size } }
toApp(OUTPUT, msg)
this.sendToStageOutputs(msg)
this.sendToRequested(msg)
NdiSender.sendVideoBufferNDI(captureId, buffer, { size, ratio, framerate: OutputHelper.getOutput(captureId)?.captureOptions?.framerates.ndi || 10 })
}

static resizeImage(image: NativeImage, initialSize: Size, newSize: Size) {
Expand All @@ -127,7 +96,7 @@ export class CaptureTransmitter {
}

static sendToStageOutputs(msg: any) {
;[...new Set(this.stageWindows)].forEach((id) => sendToWindow(id, msg))
;[...new Set(this.stageWindows)].forEach((id) => OutputHelper.Send.sendToWindow(id, msg))
}

static sendToRequested(msg: any) {
Expand All @@ -141,7 +110,7 @@ export class CaptureTransmitter {
return
}

sendToWindow(data.id, msg)
OutputHelper.Send.sendToWindow(data.id, msg)
})

this.requestList = newList
Expand All @@ -163,13 +132,13 @@ export class CaptureTransmitter {
if (os.endianness() === "BE") util.ImageBufferAdjustment.ARGBtoRGBA(buffer)
else util.ImageBufferAdjustment.BGRAtoRGBA(buffer)

toServer(OUTPUT_STREAM, { channel: "STREAM", data: { id:capture.id, buffer, size } })
toServer(OUTPUT_STREAM, { channel: "STREAM", data: { id: capture.id, buffer, size } })
}

static requestPreview(data: any) {
this.requestList.push(JSON.stringify(data))
}

static removeAllChannels(captureId: string) {
Object.keys(this.channels).forEach((key) => {
if (key.includes(captureId)) this.removeChannel(captureId, key)
Expand All @@ -182,5 +151,4 @@ export class CaptureTransmitter {
if (this.channels[combinedKey].timer) clearInterval(this.channels[combinedKey].timer)
delete this.channels[combinedKey]
}

}
}
Loading

0 comments on commit 454e400

Please sign in to comment.