mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-26 07:45:34 +00:00
add native frame capture utilities
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
const RGBA_BYTES_PER_PIXEL = 4
|
||||
|
||||
let nativeFrameCaptureMode: 'video-frame-rgba' | 'canvas-readback' | null = null
|
||||
let nativeFrameCaptureFallbackWarned = false
|
||||
let fallbackCanvas:
|
||||
| HTMLCanvasElement
|
||||
| OffscreenCanvas
|
||||
| null = null
|
||||
let fallbackContext:
|
||||
| CanvasRenderingContext2D
|
||||
| OffscreenCanvasRenderingContext2D
|
||||
| null = null
|
||||
|
||||
function getRgbaLayout(width: number): PlaneLayout[] {
|
||||
return [{ offset: 0, stride: width * RGBA_BYTES_PER_PIXEL }]
|
||||
}
|
||||
|
||||
function getRgbaByteSize(width: number, height: number): number {
|
||||
return width * height * RGBA_BYTES_PER_PIXEL
|
||||
}
|
||||
|
||||
function getFallbackReadbackContext(
|
||||
width: number,
|
||||
height: number,
|
||||
): CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D {
|
||||
if (!fallbackCanvas || fallbackCanvas.width !== width || fallbackCanvas.height !== height) {
|
||||
if (typeof OffscreenCanvas !== 'undefined') {
|
||||
fallbackCanvas = new OffscreenCanvas(width, height)
|
||||
fallbackContext = fallbackCanvas.getContext('2d', {
|
||||
willReadFrequently: true,
|
||||
})
|
||||
} else if (typeof document !== 'undefined') {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
fallbackCanvas = canvas
|
||||
fallbackContext = canvas.getContext('2d', {
|
||||
willReadFrequently: true,
|
||||
})
|
||||
} else {
|
||||
throw new Error('No canvas readback path is available for native export')
|
||||
}
|
||||
}
|
||||
|
||||
if (!fallbackContext) {
|
||||
throw new Error('Failed to initialize native export frame readback context')
|
||||
}
|
||||
|
||||
return fallbackContext
|
||||
}
|
||||
|
||||
function captureCanvasFrameWithReadback(canvas: HTMLCanvasElement, targetWidth?: number, targetHeight?: number): Uint8Array {
|
||||
const outWidth = targetWidth ?? canvas.width
|
||||
const outHeight = targetHeight ?? canvas.height
|
||||
const context = getFallbackReadbackContext(outWidth, outHeight)
|
||||
context.clearRect(0, 0, outWidth, outHeight)
|
||||
context.drawImage(canvas, 0, 0, outWidth, outHeight)
|
||||
const imageData = context.getImageData(0, 0, outWidth, outHeight)
|
||||
return new Uint8Array(imageData.data)
|
||||
}
|
||||
|
||||
function flipRgbaRowsInPlace(buffer: Uint8Array, width: number, height: number): void {
|
||||
const rowByteLength = width * RGBA_BYTES_PER_PIXEL
|
||||
const scratchRow = new Uint8Array(rowByteLength)
|
||||
const halfRows = Math.floor(height / 2)
|
||||
|
||||
for (let rowIndex = 0; rowIndex < halfRows; rowIndex += 1) {
|
||||
const topOffset = rowIndex * rowByteLength
|
||||
const bottomOffset = (height - rowIndex - 1) * rowByteLength
|
||||
|
||||
scratchRow.set(buffer.subarray(topOffset, topOffset + rowByteLength))
|
||||
buffer.copyWithin(topOffset, bottomOffset, bottomOffset + rowByteLength)
|
||||
buffer.set(scratchRow, bottomOffset)
|
||||
}
|
||||
}
|
||||
|
||||
export async function captureCanvasFrameForNativeExport(
|
||||
canvas: HTMLCanvasElement,
|
||||
timestamp: number,
|
||||
flipVertical = false,
|
||||
targetWidth?: number,
|
||||
targetHeight?: number,
|
||||
): Promise<Uint8Array> {
|
||||
const outWidth = targetWidth ?? canvas.width
|
||||
const outHeight = targetHeight ?? canvas.height
|
||||
|
||||
if (nativeFrameCaptureMode !== 'canvas-readback') {
|
||||
const buffer = new Uint8Array(getRgbaByteSize(outWidth, outHeight))
|
||||
const frame = new VideoFrame(canvas, { timestamp })
|
||||
|
||||
try {
|
||||
await frame.copyTo(buffer, {
|
||||
format: 'RGBA',
|
||||
layout: getRgbaLayout(outWidth),
|
||||
})
|
||||
if (flipVertical) {
|
||||
flipRgbaRowsInPlace(buffer, outWidth, outHeight)
|
||||
}
|
||||
nativeFrameCaptureMode = 'video-frame-rgba'
|
||||
return buffer
|
||||
} catch (error) {
|
||||
nativeFrameCaptureMode = 'canvas-readback'
|
||||
if (!nativeFrameCaptureFallbackWarned) {
|
||||
nativeFrameCaptureFallbackWarned = true
|
||||
console.warn(
|
||||
'[native-export] VideoFrame RGBA copyTo failed, falling back to canvas readback',
|
||||
error,
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
frame.close()
|
||||
}
|
||||
}
|
||||
|
||||
const buffer = captureCanvasFrameWithReadback(canvas, outWidth, outHeight)
|
||||
if (flipVertical) {
|
||||
flipRgbaRowsInPlace(buffer, outWidth, outHeight)
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface ShadowLayerProfile {
|
||||
offsetScale: number;
|
||||
alphaScale: number;
|
||||
blurScale: number;
|
||||
}
|
||||
|
||||
export const VIDEO_SHADOW_LAYER_PROFILES: ReadonlyArray<ShadowLayerProfile> = Object.freeze([
|
||||
{ offsetScale: 12, alphaScale: 0.7, blurScale: 48 },
|
||||
{ offsetScale: 4, alphaScale: 0.5, blurScale: 16 },
|
||||
{ offsetScale: 2, alphaScale: 0.3, blurScale: 8 },
|
||||
]);
|
||||
|
||||
export const WEBCAM_SHADOW_LAYER_PROFILES: ReadonlyArray<ShadowLayerProfile> = Object.freeze([
|
||||
{ offsetScale: 0.06, alphaScale: 1, blurScale: 0.22 },
|
||||
]);
|
||||
|
||||
export function getShadowFilterPadding(blur: number, offsetY: number): number {
|
||||
return Math.ceil(Math.max(0, blur * 2 + Math.abs(offsetY)));
|
||||
}
|
||||
Reference in New Issue
Block a user