简体   繁体   中英

fluent-ffmpeg aspect ratio with crop

I am having trouble changing the aspect ratio between 16:9, 1:1 and 9:16 using fluent-ffmpeg When I try to change from 16:9 to 9:16, I kinda receive a squeezed video but actually I want the extra part should be removed.

I tried this with many combinations:

FFmpeg().input(video).size("608x?").aspect("9:16").output(tempFile).run();

My input 16:9 video 1920x1080 我输入 16:9 视频 1920x1080

.

My Expected result 9:16 video 608x1080

我的预期结果 9:16 视频 608x1080

The most optimal solution I came with:
As the fluent-ffmpeg do not provide any builtin method to crop and scale a video that's why we need to implement it by ourselves.

Step 1:
We need to calculate an intermediate crop resolution to achieve the target aspect ratio either by cropping the extra (landscape to portrait) or by adding black bars (portrait to landscape) to avoid squeezing or stretching of the video).

Note:
We can also add a nice blur effect in case of portrait to landscape but this will add an extra step (ffmpeg blur filter required).

Step 2:
We will simply scale up or down to our target resolution.

The code seems to be too lengthy but believe me it's way to easy and divided into methods. Start from the bottom to understand it.
Ignore the types if you want a JavaScript version.

Just run this code and it will crop/scale a single video for you.

 import * as FFmpeg from "fluent-ffmpeg"; function resizingFFmpeg( video: string, width: number, height: number, tempFile: string, autoPad?: boolean, padColor?: string ): Promise<string> { return new Promise((res, rej) => { let ff = FFmpeg().input(video).size(`${width}x${height}`); autoPad ? (ff = ff.autoPad(autoPad, padColor)) : null; ff.output(tempFile) .on("start", function (commandLine) { console.log("Spawned FFmpeg with command: " + commandLine); console.log("Start resizingFFmpeg:", video); }) // .on("progress", function(progress) { // console.log(progress); // }) .on("error", function (err) { console.log("Problem performing ffmpeg function"); rej(err); }) .on("end", function () { console.log("End resizingFFmpeg:", tempFile); res(tempFile); }) .run(); }); } function videoCropCenterFFmpeg( video: string, w: number, h: number, tempFile: string ): Promise<string> { return new Promise((res, rej) => { FFmpeg() .input(video) .videoFilters([ { filter: "crop", options: { w, h, }, }, ]) .output(tempFile) .on("start", function (commandLine) { console.log("Spawned FFmpeg with command: " + commandLine); console.log("Start videoCropCenterFFmpeg:", video); }) // .on("progress", function(progress) { // console.log(progress); // }) .on("error", function (err) { console.log("Problem performing ffmpeg function"); rej(err); }) .on("end", function () { console.log("End videoCropCenterFFmpeg:", tempFile); res(tempFile); }) .run(); }); } function getDimentions(media: string) { console.log("Getting Dimentions from:", media); return new Promise<{ width: number; height: number }>((res, rej) => { FFmpeg.ffprobe(media, async function (err, metadata) { if (err) { console.log("Error occured while getting dimensions of:", media); rej(err); } res({ width: metadata.streams[0].width, height: metadata.streams[0].height, }); }); }); } async function videoScale(video: string, newWidth: number, newHeight: number) { const output = "scaledOutput.mp4"; const { width, height } = await getDimentions(video); if ((width / height).toFixed(2) > (newWidth / newHeight).toFixed(2)) { // y=0 case // landscape to potrait case const x = width - (newWidth / newHeight) * height; console.log(`New Intrim Res: ${width - x}x${height}`); const cropping = "tempCropped-" + output; let cropped = await videoCropCenterFFmpeg( video, width - x, height, cropping ); let resized = await resizingFFmpeg(cropped, newWidth, newHeight, output); // unlink temp cropping file // fs.unlink(cropping, (err) => { // if (err) console.log(err); // console.log(`Temp file ${cropping} deleted Successfuly...`); // }); return resized; } else if ((width / height).toFixed(2) < (newWidth / newHeight).toFixed(2)) { // x=0 case // potrait to landscape case // calculate crop or resize with padding or blur sides // or just return with black bars on the side return await resizingFFmpeg(video, newWidth, newHeight, output, true); } else { console.log("Same Aspect Ratio forward for resizing"); return await resizingFFmpeg(video, newWidth, newHeight, output); } } videoScale("./path-to-some-video.mp4", 270, 480);

Adding to the above solution, which I wasn't a huge fan of.

The routine below tries to simulate the object-fit: contain CSS cropping and sizing. This first figures out what size the intermediate resize step needs to be to both maintain the aspect ratio and provide the necessary width and height to crop to the desired output, and then runs a video filter crop on the result to just pull the desired output dimensions.

I also use the temp npm package to generate empty files in the system temp folder that will be used as output file destinations for ffmpeg .

import FFMpeg from 'fluent-ffmpeg';
import temp from 'temp-write';

function getDimensions(media) {
    return new Promise((resolve, reject) => {
        FFMpeg.ffprobe(media, async (err, metadata) => {
            if (err) {
                reject(err);
                return;
            }
            resolve({
                mediaWidth: metadata.streams[0].width,
                mediaHeight: metadata.streams[0].height,
            });
        });
    });
}

function FFMpegPromisify(routine, output) {
    return new Promise((resolve, reject) => {
        routine
            .on('error', (err) => {
                reject(err);
            })
            .on('end', () => {
                resolve();
            })
            .save(output);
    });
}

module.exports = {
    resize: async ({ data, width, height }) => {
        let path = temp.sync(data);
        const { mediaWidth, mediaHeight } = await getDimensions(path);
        let mediaAspectRatio = mediaWidth / mediaHeight;
        let widthResizeRatio = width / mediaWidth;
        let heightResizeRatio = height / mediaHeight;
        let maxAdjustedWidth = Math.round(Math.max(mediaWidth * widthResizeRatio, height * mediaAspectRatio));
        let maxAdjustedHeight = Math.round(Math.max(mediaHeight * heightResizeRatio, width / mediaAspectRatio));

        let tempResizePath = temp.sync('', 'file.mp4');
        await FFMpegPromisify(FFMpeg(path).format('mp4').size(`${maxAdjustedWidth}x${maxAdjustedHeight}`), tempResizePath);

        let tempCropPath = temp.sync('', 'file.mp4');
        let cropX = (maxAdjustedWidth - width) / 2;
        let cropY = (maxAdjustedHeight - height) / 2;
        await FFMpegPromisify(FFMpeg(tempResizePath).format('mp4').videoFilter([
            {
                filter: "crop",
                options: {
                    w: width,
                    h: height,
                    x: cropX,
                    y: cropY
                },
            }
        ]), tempCropPath);

        return tempCropPath; // contains the final, cropped result
    }
}

let file = require('fs').readFileSync('C:\\FFMpeg\\sample.mp4');

module.exports.resize({ data: file, width: 320, height: 1080 });

I feel like the other answers were making it more complex than it needs to be.

As the others said, there is no way to just enter aspect ratio directly, so you should find the desired size before cropping.

Here's a code that gets video dimensions using ffprobe and finds maximum possible size for the selected video with the desired aspect ratio.

In this example it would get maximum file dimensions possible with a 6:9 aspect ratio.

const ffmpeg = require('fluent-ffmpeg');
ffmpeg.ffprobe(`./Videos/video.mp4`, function (err, metadata) {
if (err) {
    console.error(err);
} else {

    let mediaWidth = metadata.streams[0].width;
    let mediaHeight = metadata.streams[0].height;
    let widthAspectRatio = 6;
    let heightAspectRation = 9;
    let newWidth = 1;
    let newHeight = 1;

    while (newWidth < mediaWidth && newHeight < mediaHeight) {
        newWidth += widthAspectRatio;
        newHeight += heightAspectRation;
    }
    newHeight -= widthAspectRatio;
    newHeight -= heightAspectRation;

    console.log("Original dimensions:", mediaWidth, mediaHeight);
    console.log("New dimensions:", newWidth, newHeight);

}
});

Output:

Original dimensions: 2560 1440
New dimensions: 961 1426

After that, simply crop the video to the dimensions you get.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM