UNPKG

ffmpeg-toolkit

Version:

A modern FFmpeg toolkit for Node.js

301 lines 13.8 kB
export class VideoModule { constructor(base) { this.base = base; } calculateCropFilters(inputWidth, inputHeight, targetWidth, targetHeight) { try { if (inputWidth === undefined || inputHeight === undefined || targetWidth === undefined || targetHeight === undefined || isNaN(inputWidth) || isNaN(inputHeight) || isNaN(targetWidth) || isNaN(targetHeight)) { throw new Error('Invalid input dimensions'); } if (inputWidth <= 0 || inputHeight <= 0 || targetWidth <= 0 || targetHeight <= 0) { throw new Error('Dimensions must be positive numbers'); } const aspectRatio = targetWidth / targetHeight; const inputAspectRatio = inputWidth / inputHeight; if (Math.abs(inputAspectRatio - aspectRatio) < 0.001) { return { scaleFilter: `scale=${targetWidth}:${targetHeight},setsar=1:1`, cropFilter: '', }; } const isLandscapeToPortrait = inputAspectRatio > aspectRatio; const scaledHeight = isLandscapeToPortrait ? targetHeight : Math.round(targetWidth / inputAspectRatio); const scaledWidth = isLandscapeToPortrait ? Math.round(scaledHeight * inputAspectRatio) : targetWidth; const scaleFilter = `scale=${scaledWidth}:${scaledHeight},setsar=1:1`; const cropFilter = isLandscapeToPortrait ? `crop=${targetWidth}:${targetHeight}:${Math.floor((scaledWidth - targetWidth) / 2)}:0` : `crop=${targetWidth}:${targetHeight}:0:${Math.floor((scaledHeight - targetHeight) / 2)}`; return { scaleFilter, cropFilter }; } catch (error) { this.base.handleModuleError(error, 'Failed to calculate crop filters'); } } async cropVideo(options) { try { const { width, height, duration: durationOptions, inputPath } = options; const videoInfo = await this.base.getVideoInfoPublic(inputPath); const durationVideo = durationOptions && durationOptions < (Number(videoInfo.duration) ?? 0) ? durationOptions : Number(videoInfo.duration) ?? 0; const filters = []; if (width !== videoInfo.width || height !== videoInfo.height) { const { scaleFilter, cropFilter } = this.calculateCropFilters(videoInfo.width ?? 0, videoInfo.height ?? 0, width, height); if (scaleFilter) { filters.push(scaleFilter); } if (cropFilter) { filters.push(cropFilter); } } return await this.base.process({ callback: () => { const command = this.base.ffmpeg(inputPath).noAudio().setDuration(durationVideo); if (filters.length > 0) { command.videoFilters(filters); } return command; }, data: { pathOutput: options.pathOutput, }, inputPath: options.inputPath, pathOutput: options.pathOutput, isDefaultOptions: false, }); } catch (error) { this.base.handleModuleError(error, 'Failed to crop video'); } } async convertVideoToPlatform(options) { try { return await this.base.process({ callback: () => { const command = this.base.ffmpeg(); command.input(options.inputPath); return command.output(options.pathOutput); }, data: { pathOutput: options.pathOutput, }, inputPath: options.inputPath, platform: options.platform, }); } catch (error) { this.base.handleModuleError(error, 'Failed to convert video to platform'); } } async concatVideos(options) { try { const { inputPaths, isNoAudio = false, pathOutput } = options; return await this.base.process({ callback: () => { const command = this.base.ffmpeg(); inputPaths.forEach((path) => { command.input(path); }); const n = inputPaths.length; // Build filter complex string const filters = []; // Scale and pad video streams const scaleFilters = Array.from({ length: n }, (_, i) => `[${i}:v]scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2[v${i}]`); filters.push(scaleFilters.join(';')); if (!isNoAudio) { const audioInputs = Array.from({ length: n }, (_, i) => `[${i}:a]`).join(''); filters.push(`${audioInputs}concat=n=${n}:v=0:a=1[aout]`); } const concatInputs = Array.from({ length: n }, (_, i) => `[v${i}]`).join(''); filters.push(`${concatInputs}concat=n=${n}:v=1[outv]`); command.complexFilter(filters.join(';')); command.map('[outv]'); if (!isNoAudio) { command.map('[aout]'); } return command.output(pathOutput); }, data: { pathOutput, }, isDefaultOptions: false, }); } catch (error) { this.base.handleModuleError(error, 'Failed to concat videos'); } } async getVideoDuration(inputPath) { try { return new Promise((resolve, reject) => { this.base .ffmpeg() .input(inputPath) .ffprobe((err, data) => { if (err) reject(err); resolve(data.format.duration || 0); }); }); } catch (error) { this.base.handleModuleError(error, 'Failed to get video duration'); } } async concatVideosWithTransition(options) { try { const transitionDuration = options.transitionDuration || 1; const transitionType = options.transitionType || 'fade'; let filterComplex = ''; switch (transitionType) { case 'fade': { const trans = transitionDuration; const durations = await Promise.all(options.inputPaths.map((path) => this.getVideoDuration(path))); const steps = []; const concatInputs = []; let labelIndex = 0; for (let i = 0; i < options.inputPaths.length; i++) { const d = durations[i]; const dStr = d.toFixed(3); const startFade = Math.max(0, d - trans).toFixed(3); steps.push(`[${i}:v]trim=0:${startFade},setpts=PTS-STARTPTS,format=yuv420p[v${labelIndex}]`); concatInputs.push(`[v${labelIndex}]`); labelIndex++; if (i < options.inputPaths.length - 1) { steps.push(`[${i}:v]trim=${startFade}:${dStr},setpts=PTS-STARTPTS,fade=t=out:st=0:d=${trans}[v${labelIndex}]`); concatInputs.push(`[v${labelIndex}]`); labelIndex++; steps.push(`[${i + 1}:v]trim=0:${trans},setpts=PTS-STARTPTS,fade=t=in:st=0:d=${trans}[v${labelIndex}]`); concatInputs.push(`[v${labelIndex}]`); labelIndex++; } else { steps.push(`[${i}:v]trim=${startFade}:${dStr},setpts=PTS-STARTPTS[v${labelIndex}]`); concatInputs.push(`[v${labelIndex}]`); labelIndex++; } } filterComplex = ` ${steps.join(';')} ; ${concatInputs.join('')} concat=n=${concatInputs.length}:v=1[outv] `; break; } case 'zoom': filterComplex = options.inputPaths .map((_, i) => { const input = `[${i}:v]`; if (i === 0) return `${input}[v${i}]`; return `${input}scale=iw*1.5:ih*1.5,zoompan=z='min(zoom+0.0015,1.5)':d=125:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=1080x1918[v${i}]`; }) .join(';') + ';' + options.inputPaths.map((_, i) => `[v${i}]`).join('') + `concat=n=${options.inputPaths.length}:v=1[outv]`; break; case 'rotate': filterComplex = options.inputPaths .map((_, i) => { const input = `[${i}:v]`; if (i === 0) return `${input}[v${i}]`; return `${input}rotate=angle='if(lt(t,${transitionDuration}),t*360/${transitionDuration},360)':fillcolor=black[v${i}]`; }) .join(';') + ';' + options.inputPaths.map((_, i) => `[v${i}]`).join('') + `concat=n=${options.inputPaths.length}:v=1[outv]`; break; case 'pixelate': filterComplex = options.inputPaths .map((_, i) => { const input = `[${i}:v]`; if (i === 0) return `${input}[v${i}]`; return `${input}scale=iw/10:ih/10,scale=iw*10:ih*10:flags=neighbor[v${i}]`; }) .join(';') + ';' + options.inputPaths.map((_, i) => `[v${i}]`).join('') + `concat=n=${options.inputPaths.length}:v=1[outv]`; break; case 'wipe': filterComplex = options.inputPaths .map((_, i) => { const input = `[${i}:v]`; if (i === 0) return `${input}[v${i}]`; return `${input}transpose=1,transpose=2[v${i}]`; }) .join(';') + ';' + options.inputPaths.map((_, i) => `[v${i}]`).join('') + `concat=n=${options.inputPaths.length}:v=1[outv]`; break; case 'dissolve': filterComplex = options.inputPaths .map((_, i) => { const input = `[${i}:v]`; if (i === 0) return `${input}[v${i}]`; return `${input}format=rgba,colorchannelmixer=aa=0.5[v${i}]`; }) .join(';') + ';' + options.inputPaths.map((_, i) => `[v${i}]`).join('') + `concat=n=${options.inputPaths.length}:v=1[outv]`; break; case 'slide': filterComplex = options.inputPaths .map((_, i) => { const input = `[${i}:v]`; if (i === 0) return `${input}[v${i}]`; return `${input}crop=iw:ih:0:0,pad=iw:ih*2:0:0[v${i}]`; }) .join(';') + ';' + options.inputPaths.map((_, i) => `[v${i}]`).join('') + `concat=n=${options.inputPaths.length}:v=1[outv]`; break; } filterComplex = filterComplex.replace(/\s+/g, '').trim(); return await this.base.process({ callback: () => { const command = this.base.ffmpeg(); options.inputPaths.forEach((path) => { command.input(path); }); return command.complexFilter(filterComplex).map('[outv]').output(options.pathOutput); }, data: { pathOutput: options.pathOutput, }, }); } catch (error) { this.base.handleModuleError(error, 'Failed to concat videos with transition'); } } } //# sourceMappingURL=video.js.map