v8.51.4

Pintura Video Editor Server Configuration

The video editor extension isn't included in the Pintura image editor product package, it's available as an upgrade on the pricing page

The extension currently doesn't support annotations, decorations, redactions, and frame styles.

The clientside video encoding is aimed at encoding short videos, it's strongly advised to use server side encoding for video's longer than a couple minutes.

Live demo of Pintura with video editor extension

When using createFFmpegEncoder or createMediaStream encoder you can encode the video on the client. But as described in the documentation both approaches have their downsides.

Instead we can also run the commands used by createFFmpegEncoder on the server, we can see how to use the imageState object and generate the needed commands below.

Please note that this script isn't included in the product package and only serves as a suggestion on how you could configure FFmpeg on your server.

Support doesn't cover setting up FFmpeg on your server.

Let's take a look at the script.

  • We push all the base arguments to the args array.
  • We push all the filters we'll use to the filters array.
  • The size variable contains the video size { width, height }
  • The duration variable contains the duration of the video in seconds.
  • The targetSize variable contains the output size { width, height, fit, upscale }
// The base argument chain
const args = [];

// The FFmpeg filters we'll apply
const filters = [];

// Set input file to source file
args.push('-i', 'src.mp4');

// Get references to imageState props
const {
    flipX,
    flipY,
    rotation,
    crop,
    trim,
    gamma,
    colorMatrix,
    convolutionMatrix,
} = imageState;

// 1. Flip
if (flipX || flipY) {
    flipX && filters.push(`hflip`);
    flipY && filters.push(`vflip`);
}

// 2. Rotate
if (rotation) {
    const width =
        Math.abs(size.width * Math.sin(rotation)) +
        Math.abs(size.height * Math.cos(rotation));

    const height =
        Math.abs(size.width * Math.cos(rotation)) +
        Math.abs(size.height * Math.sin(rotation));

    filters.push(
        `rotate='${rotation}:ow=${Math.floor(width)}:oh=${Math.floor(height)}'`
    );
}

// 3. Crop
if (
    !(
        crop.x === 0 &&
        crop.y === 0 &&
        crop.width === size.width &&
        crop.height === size.height
    )
) {
    filters.push(`crop=${crop.width}:${crop.height}:${crop.x}:${crop.y}`);
}

// 4. Resize
if (targetSize) {
    const { fit = 'contain', upscale = false } = targetSize;
    let { width, height } = targetSize;

    width = width % 2 === 0 ? width : width + 1;
    height = height % 2 === 0 ? height : height + 1;

    // limit size if is not allowed to upscale
    if (!upscale) {
        const scalar = Math.min(
            (width || Number.MAX_SAFE_INTEGER) / crop.width,
            (height || Number.MAX_SAFE_INTEGER) / crop.height
        );
        if (scalar > 1) {
            if (width) width /= scalar;
            if (height) height /= scalar;
        }
    }

    if (width && !height) filters.push(`scale=${width}:trunc(ow/a/2)*2`);
    else if (height && !width) filters.push(`scale=trunc(oh/a/2)*2:${height}`);
    else if (width && height) {
        // if is contain, scale down
        if (fit === 'contain') {
            filters.push(
                `scale=${width}:${height}:force_original_aspect_ratio=decrease`
            );
        }

        // if is cover, scale up
        else if (fit === 'cover') {
            filters.push(
                `scale=${width}:${height}:force_original_aspect_ratio=increase,setsar=1`
            );
        }

        // ignore aspect ratio
        else if (fit === 'force') {
            filters.push(`scale=${width}:${height},setsar=1`);
        }
    }
}

// 5. Convolution Matrix
if (convolutionMatrix && convolutionMatrix.clarity) {
    filters.push(`convolution='${convolutionMatrix.clarity.join(' ')}'`);
}

// 6. Gamma
if (gamma > 0) {
    filters.push(`eq=gamma=${gamma}:gamma_weight=0.85`);
}

// 7. Color Matrix
const colorMatrices = Object.values(colorMatrix || {}).filter(Boolean);
if (colorMatrices.length) {
    // See helper section below for the getColorMatrixFromColorMatrices function definition
    const colorMatrix = getColorMatrixFromColorMatrices(colorMatrices);
    const skip = [4, 9, 14, 19];
    const cl = colorMatrix;
    const ccm = colorMatrix.filter((v, i) => !skip.includes(i));
    filters.push(
        `colorchannelmixer=${ccm.join(':')}`,
        `colorlevels=romin=${cl[4]}:gomin=${cl[9]}:bomin=${cl[14]}`
    );
}

// 8. Trim
if (trim) {
    const inputRanges = (
        Array.isArray(trim) && isNumber(trim[0]) ? [trim] : trim
    )
        .map((range, index) => {
            const from = range[0] * duration;
            const to = range[1] * duration;
            const v = `[0:v]trim=start=${from}:end=${to},setpts=PTS-STARTPTS${filters
                .map((filter) => ',' + filter)
                .join('')}[${index}v];`;
            const a = `[0:a]atrim=start=${from}:end=${to},asetpts=PTS-STARTPTS[${index}a];`;
            return v + a;
        })
        .join('');

    // Filters have now been moved to trim instructions
    filters.length = 0;

    const inputRangesKeys = trim
        .map((_, index) => `[${index}v][${index}a]`)
        .join('');

    const concatOutput = `${inputRangesKeys}concat=n=${trim.length}:v=1:a=1[outv][outa]`;

    args.push('-filter_complex', `${inputRanges}${concatOutput}`);
    args.push('-map', '[outv]', '-map', '[outa]');
} else {
    // add ffmpeg filters command
    filters.length && args.push('-filter_complex', `${filters.join(',')}`);
}

// Add output file
args.push('dest.mp4');

// The `args` array now contains the list of commands to pass to FFmpeg CLI
console.log(args);

Color Matrix multiplication helper functions.

function getColorMatrixFromColorMatrices(colorMatrices) {
    return colorMatrices.length
        ? colorMatrices.reduce(
              (previous, current) => dotColorMatrix([...previous], current),
              colorMatrices.shift()
          )
        : [];
}

function dotColorMatrix(a, b) {
    const res = new Array(20);

    // R
    res[0] = a[0] * b[0] + a[1] * b[5] + a[2] * b[10] + a[3] * b[15];
    res[1] = a[0] * b[1] + a[1] * b[6] + a[2] * b[11] + a[3] * b[16];
    res[2] = a[0] * b[2] + a[1] * b[7] + a[2] * b[12] + a[3] * b[17];
    res[3] = a[0] * b[3] + a[1] * b[8] + a[2] * b[13] + a[3] * b[18];
    res[4] = a[0] * b[4] + a[1] * b[9] + a[2] * b[14] + a[3] * b[19] + a[4];

    // G
    res[5] = a[5] * b[0] + a[6] * b[5] + a[7] * b[10] + a[8] * b[15];
    res[6] = a[5] * b[1] + a[6] * b[6] + a[7] * b[11] + a[8] * b[16];
    res[7] = a[5] * b[2] + a[6] * b[7] + a[7] * b[12] + a[8] * b[17];
    res[8] = a[5] * b[3] + a[6] * b[8] + a[7] * b[13] + a[8] * b[18];
    res[9] = a[5] * b[4] + a[6] * b[9] + a[7] * b[14] + a[8] * b[19] + a[9];

    // B
    res[10] = a[10] * b[0] + a[11] * b[5] + a[12] * b[10] + a[13] * b[15];
    res[11] = a[10] * b[1] + a[11] * b[6] + a[12] * b[11] + a[13] * b[16];
    res[12] = a[10] * b[2] + a[11] * b[7] + a[12] * b[12] + a[13] * b[17];
    res[13] = a[10] * b[3] + a[11] * b[8] + a[12] * b[13] + a[13] * b[18];
    res[14] =
        a[10] * b[4] + a[11] * b[9] + a[12] * b[14] + a[13] * b[19] + a[14];

    // A
    res[15] = a[15] * b[0] + a[16] * b[5] + a[17] * b[10] + a[18] * b[15];
    res[16] = a[15] * b[1] + a[16] * b[6] + a[17] * b[11] + a[18] * b[16];
    res[17] = a[15] * b[2] + a[16] * b[7] + a[17] * b[12] + a[18] * b[17];
    res[18] = a[15] * b[3] + a[16] * b[8] + a[17] * b[13] + a[18] * b[18];
    res[19] =
        a[15] * b[4] + a[16] * b[9] + a[17] * b[14] + a[18] * b[19] + a[19];

    return res;
}