v8.77.0

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

Please note that client-side video encoding is useful for encoding short videos, it's advised to use server side encoding for content longer than a couple minutes.

The video extension currently doesn't support applying frames styles and redacting content.

Live demo of Pintura with video editor extension

When using createFFmpegEncoder, createMediaStreamEncoder, or the createMuxerEncoder encoder you can encode video on the client. But as described in the documentation every approach has 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 Pintura Video Extension package and only serves as a suggestion on how you could configure FFmpeg on your server. It also currently doesn't include rendering annotations.

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

To upload only the instructions we can use a custom imageWriter.

// custom upload writer
const createUploadWriter = () => [
    [
        // task
        async (state, options, onprogress) => {
            console.log('my-upload-task', state, options, onprogress);

            // upload instructions to your server here
            // - state.src => src file
            // - state.imageState => current state

            return {
                ...state,
            };
        },

        // name of task
        'upload',
    ],
];

// set as imageWriter
const editor = openDefaultEditor({
    src: './my-video.mp4',
    imageWriter: createUploadWriter(),
});

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));
    const [ro, go, bo] = [cl[4] + cl[3], cl[9] + cl[8], cl[14] + cl[13]];
    filters.push(
        `colorchannelmixer=${ccm.join(':')}`,
        `lutrgb=r=val+(${ro * 255}):g=val+(${go * 255}):b=val+(${bo * 255})`
    );
}

// 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;
}