UNPKG

mfx

Version:

In-browser video editing toolkit, with effects accelerated by WebGL

261 lines (250 loc) 7.16 kB
import { codecs, decode, effect, encode, ExtendedVideoFrame, keyframes, visual } from "mfx"; import { easing } from "ts-easing"; import type { TestDefinition } from "../types"; import { openURL } from "../utils"; const step = (v: number[], size = 2500) => v.map((s, i) => ({ time: i * size, value: s })); async function createVideoFrameFromURL(url: string): Promise<VideoFrame> { const img = new Image(); img.crossOrigin = "anonymous"; img.src = url; await img.decode(); const imageBitmap = await createImageBitmap(img); return new VideoFrame(imageBitmap, { timestamp: 0 }); }; const createMaskFrame = (width: number, height: number, roundness = 50, color = "black") => { const scale = 1; const canvas = new OffscreenCanvas(width * scale, height * scale); const ctx = canvas.getContext("2d") as unknown as CanvasRenderingContext2D; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = color; ctx.beginPath(); ctx.roundRect(0, 0, canvas.width, canvas.height, roundness * scale); ctx.fill(); return new VideoFrame(canvas, { alpha: "keep", timestamp: 0, displayWidth: canvas.width, displayHeight: canvas.height, visibleRect: { width: canvas.width, height: canvas.height, x: 0, y: 0 }, }); }; const cornerXKeyframes = keyframes(step([ 0, 0, 1, 1, 0, 0, 1, 1 ]), easing.inOutSine); const cornerYKeyframes = keyframes(step([ 0, 0, 0, 0, 1, 1, 1, 1 ]), easing.inOutSine); export const definitions: TestDefinition[] = [{ id: "effect_zoom", title: "Zoom", description: "Zoom at a scale into a video coordinate", input: "AI.mp4", process: (stream) => ( effect(stream, [ visual.zoom({ factor: 2, x: 0.5, y: 0.5 }) ]) ) }, { id: "effect_zoom_sampling_check", title: "Zoom Sampling", description: "Verify zoom bilinear sampling", input: "AI.mp4", process: (stream) => ( effect(stream, [ visual.zoom({ factor: 10, x: 0.5, y: 0.5 }) ]) ) }, { id: "effect_zoom_corners", title: "Zoom Corners", description: "Zoom into x/y corners", input: "beach.mp4", process: (stream) => ( effect(stream, [ visual.zoom({ factor: 3, x: cornerXKeyframes, y: cornerYKeyframes }) ]) ), }, { id: "effect_zoom_stacked", title: "Stacked Zoom Effects", description: "Zoom at a scale into a video coordinate", input: "AI.mp4", process: (stream) => effect(stream, [ visual.zoom({ factor: 1.5, x: 1, y: 1 }), visual.zoom({ factor: 1.25, x: 1, y: 1 }), ]), }, { id: "effect_zoom_out", title: "Zoom Out", description: "Zoom out adjusting the alpha channel on area out of zoom", input: "AI.mp4", process: (stream) => effect(stream, [ visual.zoom({ factor: 0.5, x: 0.5, y: 0.25 }) ]) }, { id: "effect_blur", title: "Blur", description: "Blur video using fast gaussian and convolution", input: "boats.mp4", process: (stream) => effect(stream, [ visual.blur({ passes: 10, quality: 0.05 }) ]) }, { id: "effect_scale_alpha", title: "Scale with Alpha", description: "Scale video down with an underlying background", input: "boats.mp4", process: async (stream) => { const background = await createVideoFrameFromURL("https://images.unsplash.com/photo-1730982045412-326c81810ace?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"); const backgroundFrame = ExtendedVideoFrame.revise(background, background, {}, { keepOpen: true }); return effect(stream, [ visual.scale({ values: [0.7, 0.7, 1] }), visual.add(new ReadableStream<VideoFrame>({ pull: async (controller) => { controller.enqueue(backgroundFrame); }, }), { alpha: 1 }) ]); } }, { id: "effect_rotate", title: "Rotate", description: "Rotate transformation", input: "boats.mp4", process: (stream) => effect(stream, [ visual.scale({ values: [1 / 1.5, 1 / 1.5, 1 / 1.5] }), visual.rotate({ angle: keyframes([{ time: 0, value: 0 }, { time: 12000, value: 720 }], easing.linear), values: [1, 1, 1] }), visual.scale({ values: [1.5, 1.5, 1.5] }), ]), output: async (v, a, vt) => { return encode({ mimeType: `video/mp4; codecs="${codecs.avc.generateCodecString("baseline", "5.0")},opus"`, streaming: true, video: { stream: v, width: 640 * 4, height: 360 * 4, bitrate: 1e6 * 30, }, }); } }, { id: "composition_add", title: "Composition: Add", description: "Adding a new layer via composition", input: "boats.mp4", process: async (stream) => { const { video } = await decode(await openURL("city.mp4"), "video/mp4"); const videoConfig = video.track.config as VideoDecoderConfig; return effect(stream, [ visual.add(effect(video, [ visual.mask(new ReadableStream({ pull: (controller) => { controller.enqueue(createMaskFrame(videoConfig.codedWidth, videoConfig.codedHeight)); }, })), visual.scale({ values: [0.25, 0.25, 1].map((v) => keyframes([{ time: 0, value: 0, }, { time: 300, value: v }, { time: 3000, value: v }, { time: 3500, value: 0 }], easing.inOutCubic)) as any, origin: [1, 1, 1]}), visual.crop({ values: [0.25, 0.25, 1], origin: [1, 1, 1] }), ]), { offset: [0.95, 0.9], normal: 1 }), ], { trim: { start: 5000 } }) }, output: async (v, a, vt) => { return encode({ mimeType: `video/mp4; codecs="${codecs.avc.generateCodecString("baseline", "5.0")},opus"`, streaming: true, video: { stream: v, width: 640 * 4, height: 360 * 4, bitrate: 1e6 * 30, }, }); } }, { id: "effect_mask", title: "Masking", description: "Masking a layer", input: "AI.mp4", process: async (stream, track) => { return effect(stream, [ visual.mask(new ReadableStream({ pull: (controller) => { controller.enqueue(createMaskFrame(track.config.codedWidth, track.config.codedHeight)); }, })), visual.scale({ values: [0.9, 0.9, 1].map((v) => keyframes([{ time: 0, value: 0, }, { time: 300, value: v }, { time: 4000, value: v }, { time: 4500, value: 0 }], easing.inOutCubic)) as any, origin: [0.5, 0.5, 1]}), visual.add(new ReadableStream({ pull: (controller) => { controller.enqueue(createMaskFrame(track.config.codedWidth, track.config.codedHeight, 0, "red")); }, }), { alpha: 1 }) ], { trim: { end: 4500 } }) }, output: async (v, a, vt) => { return encode({ mimeType: `video/mp4; codecs="${codecs.avc.generateCodecString("baseline", "5.0")},opus"`, streaming: true, video: { stream: v, width: 640 * 4, height: 360 * 4, bitrate: 1e6 * 30, }, }); } }];