mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 23:05:49 +00:00
add native export ffmpeg helpers
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
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'
|
||||
}
|
||||
Reference in New Issue
Block a user