mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-28 16:55:36 +00:00
172 lines
4.0 KiB
TypeScript
172 lines
4.0 KiB
TypeScript
const NATIVE_EXPORT_INPUT_BYTES_PER_PIXEL = 4
|
|
|
|
export type NativeExportEncodingMode = 'fast' | 'balanced' | 'quality'
|
|
|
|
export type NativeVideoExportAudioMode = 'none' | 'copy-source' | 'trim-source' | 'edited-track'
|
|
|
|
export interface NativeVideoExportStartOptions {
|
|
width: number
|
|
height: number
|
|
frameRate: number
|
|
bitrate: number
|
|
encodingMode: NativeExportEncodingMode
|
|
}
|
|
|
|
export interface NativeVideoExportAudioSegment {
|
|
startMs: number
|
|
endMs: number
|
|
}
|
|
|
|
export interface NativeVideoExportFinishOptions {
|
|
audioMode?: NativeVideoExportAudioMode
|
|
audioSourcePath?: string | null
|
|
trimSegments?: NativeVideoExportAudioSegment[]
|
|
editedAudioData?: ArrayBuffer
|
|
editedAudioMimeType?: string | null
|
|
}
|
|
|
|
export function getNativeVideoInputByteSize(width: number, height: number): number {
|
|
return width * height * NATIVE_EXPORT_INPUT_BYTES_PER_PIXEL
|
|
}
|
|
|
|
export function parseAvailableFfmpegEncoders(stdout: string): Set<string> {
|
|
const encoders = new Set<string>()
|
|
|
|
for (const line of stdout.split(/\r?\n/)) {
|
|
const match = line.match(/^\s*[A-Z.]{6}\s+([a-z0-9_]+)/i)
|
|
if (match?.[1]) {
|
|
encoders.add(match[1])
|
|
}
|
|
}
|
|
|
|
return encoders
|
|
}
|
|
|
|
export function getPreferredNativeVideoEncoders(platform: NodeJS.Platform): string[] {
|
|
switch (platform) {
|
|
case 'darwin':
|
|
return ['h264_videotoolbox', 'libx264']
|
|
case 'win32':
|
|
return ['h264_nvenc', 'h264_qsv', 'h264_amf', 'h264_mf', 'libx264']
|
|
case 'linux':
|
|
return ['h264_nvenc', 'h264_qsv', 'libx264']
|
|
default:
|
|
return ['libx264']
|
|
}
|
|
}
|
|
|
|
function getLibx264ModeArgs(encodingMode: NativeExportEncodingMode): string[] {
|
|
switch (encodingMode) {
|
|
case 'fast':
|
|
return ['-preset', 'ultrafast', '-tune', 'zerolatency']
|
|
case 'quality':
|
|
return ['-preset', 'slow']
|
|
case 'balanced':
|
|
default:
|
|
return ['-preset', 'medium']
|
|
}
|
|
}
|
|
|
|
function getBitrateArgs(bitrate: number): string[] {
|
|
const effectiveBitrate = Math.max(1_500_000, Math.round(bitrate))
|
|
const maxRate = Math.max(effectiveBitrate, Math.round(effectiveBitrate * 1.2))
|
|
const bufferSize = Math.max(maxRate * 2, effectiveBitrate * 2)
|
|
|
|
return [
|
|
'-b:v',
|
|
String(effectiveBitrate),
|
|
'-maxrate',
|
|
String(maxRate),
|
|
'-bufsize',
|
|
String(bufferSize),
|
|
]
|
|
}
|
|
|
|
export function buildNativeVideoExportArgs(
|
|
encoder: string,
|
|
options: NativeVideoExportStartOptions,
|
|
outputPath: string,
|
|
): string[] {
|
|
const args = [
|
|
'-y',
|
|
'-hide_banner',
|
|
'-loglevel',
|
|
'error',
|
|
'-f',
|
|
'rawvideo',
|
|
'-pix_fmt',
|
|
'rgba',
|
|
'-s:v',
|
|
`${options.width}x${options.height}`,
|
|
'-framerate',
|
|
String(options.frameRate),
|
|
'-i',
|
|
'pipe:0',
|
|
'-vf',
|
|
'vflip',
|
|
'-an',
|
|
'-c:v',
|
|
encoder,
|
|
'-g',
|
|
String(Math.max(1, Math.round(options.frameRate * 5))),
|
|
...getBitrateArgs(options.bitrate),
|
|
]
|
|
|
|
if (encoder === 'libx264') {
|
|
args.push(...getLibx264ModeArgs(options.encodingMode))
|
|
}
|
|
|
|
args.push('-pix_fmt', 'yuv420p', '-movflags', '+faststart', outputPath)
|
|
return args
|
|
}
|
|
|
|
function formatFfmpegSeconds(milliseconds: number): string {
|
|
return (milliseconds / 1000).toFixed(3)
|
|
}
|
|
|
|
export function buildTrimmedSourceAudioFilter(
|
|
segments: NativeVideoExportAudioSegment[],
|
|
): string | null {
|
|
if (segments.length === 0) {
|
|
return null
|
|
}
|
|
|
|
const filterParts: string[] = []
|
|
const segmentLabels: string[] = []
|
|
|
|
segments.forEach((segment, index) => {
|
|
const label = `trimmed_audio_${index}`
|
|
filterParts.push(
|
|
`[1:a]atrim=start=${formatFfmpegSeconds(segment.startMs)}:end=${formatFfmpegSeconds(segment.endMs)},asetpts=PTS-STARTPTS[${label}]`,
|
|
)
|
|
segmentLabels.push(`[${label}]`)
|
|
})
|
|
|
|
if (segmentLabels.length === 1) {
|
|
filterParts.push(`${segmentLabels[0]}anull[aout]`)
|
|
} else {
|
|
filterParts.push(`${segmentLabels.join('')}concat=n=${segmentLabels.length}:v=0:a=1[aout]`)
|
|
}
|
|
|
|
return filterParts.join(';')
|
|
}
|
|
|
|
export function getEditedAudioExtension(mimeType?: string | null): string {
|
|
if (!mimeType) {
|
|
return '.webm'
|
|
}
|
|
|
|
if (mimeType.includes('wav')) {
|
|
return '.wav'
|
|
}
|
|
|
|
if (mimeType.includes('mp4') || mimeType.includes('m4a')) {
|
|
return '.m4a'
|
|
}
|
|
|
|
if (mimeType.includes('ogg')) {
|
|
return '.ogg'
|
|
}
|
|
|
|
return '.webm'
|
|
} |