Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .agents/skills/cliparr-www-assets/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ pnpm assets:capture --media-dir /absolute/path/to/media \
--hero 'hero.mkv' --mobile 'mobile.mkv' --write
```

When `--write` is enabled, the hero poster is also copied to `./.github/img/screenshot.webp`.
The README screenshot points at the homepage hero poster, so it updates automatically when captures complete.

Optional `--hero-seconds` and `--mobile-seconds` control recording duration only.
They must be positive and fit within the selected range. `--hero-subtitle` and
`--mobile-subtitle` accept Cliparr subtitle track keys to override its preferred
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

**Cliparr** is a streamlined media clipper that allows you to quickly create and download clips from the media currently playing on your Plex or Jellyfin server, or from a local video file opened directly in your browser.

<img src="./.github/img/screenshot.webp" alt="Cliparr editor showing a video preview, timeline clip selection, and subtitle controls." width="100%" />
<img src="./apps/www/src/assets/screenshot.webp" alt="Cliparr editor showing a video preview, timeline clip selection, and subtitle controls." width="100%" />

## Features

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
136 changes: 136 additions & 0 deletions apps/www/src/content/blog/cliparr-2-0-release.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
---
title: "Cliparr 2.0 - a new editor"
seoTitle: "Cliparr 2.0 is here: canvas-based editor and subtitle editing support"
description: "Cliparr 2.0 brings editor performance upgrades, a new canvas-based timeline engine, and subtitle editing support."
publishedAt: "2026-09-10"
updatedAt: "2026-09-10"
heroImage: cliparr-2-0-release-hero
heroImageAlt: "Cliparr 2.0 interface with the updated timeline editor and export actions."
author:
name: KyleTryon
url: https://github.com/KyleTryon
tags:
- engineering
---

import { Image } from "astro:assets";
import canvastimelineWebsite from "../../assets/blog/cliparr-2-0-release/canvastimeline-website.webp";
import stressTestBenchmark from "../../assets/blog/cliparr-2-0-release/stress-test-benchmark.webp";
import subtitleEditing from "../../assets/blog/cliparr-2-0-release/subtitle-editing.webp";
import sizeEstimation from "../../assets/blog/cliparr-2-0-release/size-estimation.webp";

Cliparr 2.0 is here after three months.

Don't worry, this is _not_ a full rewrite of Cliparr.
This is just _technically_ version 2.0 because I implement [semantic versioning](https://semver.org/).

Version 2.0, does however come with significant improvements, new additions, and optimizations.

## The New Canvas Timeline

When I first started adding support for loading and editing subtitles, it became clear the previous timeline implementation didn't scale for larger sessions.
When loading subtitles for a full TV episode or movie, the old UI could generate too many DOM nodes and cause stutter on my fairly beefy Mac Studio.

If the problem was DOM performance, the solution was to move timeline rendering to canvas.

So, I created [Canvas Timeline](https://canvastimeline.com/): a headless video editor timeline engine and a canvas renderer for high-performance timeline interactions.
It is open source and now available to everyone.

<a
href="https://canvastimeline.com/"
target="_blank"
rel="noopener noreferrer"
class="block mt-6"
>
<Image
src={canvastimelineWebsite}
alt="Canvas Timeline website preview showing the updated Cliparr timeline interface."
inferSize
loading="lazy"
class="w-full rounded-lg border border-border"
/>
</a>

At this point, Canvas Timeline is a significantly larger project than Cliparr and has yet to reach the same level of adoption within the developer community.
Still, the relationship between these two projects will drive improvements in both and deliver a better experience for developers and users alike.

### Stress test benchmark

To continuously evaluate performance and validate this approach, one of the first tests I ran was a simulated stress test that mirrored the exact problems I saw with the old solution.
You can run it yourself on the demo page [Timeline Stress Test & Benchmark](https://canvastimeline.com/demos/timeline-stress-test/).

<Image
src={stressTestBenchmark}
alt="Canvas Timeline stress-test benchmark results and responsiveness chart."
inferSize
loading="lazy"
class="w-full rounded-lg border border-border mt-6"
/>

Because the timeline is headless, we can compare both a canvas and a DOM-based implementation.

At first there were clear and massive gains in performance when selecting the canvas-based renderer.
After many rounds of optimizing and iterating with feedback from this stress test, both rendering systems are now within a comparable range.

The canvas-based renderer still needs some DOM elements, or you just end up essentially recreating the DOM in canvas for things like the interactive bits, tab focusing, accessability, and anything else you get "for free" from the DOM.
So, the canvas-based approach was optimized further, including only injecting the currently visible DOM elements into the canvas as needed.

Eventually, because all of the optimization work was happening on the DOM part of the canvas-based renderer, many of the same techniques could be applied to the DOM-based renderer as well, and now as it turns out, both rendering systems perform fairly similarly.

So, regardless of how we render, this timeline editor is more efficient.
Canvas still gives us a slight edge, but no matter how we implement it, the result is significantly better.

## Subtitles Editor

With the canvas-based editor, subtitle clips are no longer an issue to render in the timeline.

Now, it is fully possible to edit and re-time individual subtitle clips, just as you would the video clip.
If you are familiar with any major video editing software, this will feel right at home.

If you've used Cliparr before, we already had support for loading subtitles and editing their visuals, including size, position, color, and font.
But what we couldn't do was edit subtitle content or re-time them.

<a href="/blog/cliparr-2-0-release/#subtitles-editor" class="block mt-6">
<Image
src={subtitleEditing}
alt="Cliparr subtitle editing interface showing subtitle timing and content controls."
inferSize
loading="lazy"
class="w-full rounded-lg border border-border"
/>
</a>

In the next few updates, we'll add more customization options, like adding new subtitle tracks and clips.

## An even better mobile experience

With the release of V2, I spent extra time cleaning up the UI.

The changes are most noticeable on mobile, but the entire app also got a small visual refresh.

We found a way to fit more information on smaller screens without visual issues or sacrificing functionality.
There are now a few alternative layouts and components, plus a few horizontal-scrollable sections where necessary.
It should feel significantly more like a native app on small screens.

## Export size estimation

Cliparr offers the ability to select the resolution and quality you want on export.

But if you want to upload that video to Discord or Slack, it helps to know what the final file size will be _before_ exporting.

You may have already seen [Cliparr Convert](https://cliparr.dev/convert), a free web-based video converter you can use right now without uploading anything.
Most of the reason for building it—mentioned in our last blog post—is to add size estimation to Cliparr.

What I did was break out the export engine into a separate package that we can use in both Cliparr and Cliparr Convert.
The public-facing Cliparr Convert has minimal anonymous metrics reporting that helps improve the accuracy of size estimations in the future and track how well it is working now.
The self-hosted app benefits from the same improvements but includes no anonymous reporting.

<Image
src={sizeEstimation}
alt="Cliparr export size estimation interface and output size preview."
inferSize
loading="lazy"
class="w-full rounded-lg border border-border mt-6"
/>

For full release details, see the [Changelog](/changelog/).
1 change: 1 addition & 0 deletions apps/www/src/data/blog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const blogTagLabels = {
export const blogHeroImageIds = [
"what-is-cliparr-hero",
"convert-video-in-your-browser-hero",
"cliparr-2-0-release-hero",
] as const;

export type BlogHeroImageId = (typeof blogHeroImageIds)[number];
Expand Down
1 change: 1 addition & 0 deletions apps/www/src/data/blogImages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const blogHeroImagePaths = {
"what-is-cliparr-hero": "../assets/blog/what-is-cliparr/hero.webp",
"convert-video-in-your-browser-hero":
"../assets/blog/convert-video-in-your-browser/hero.webp",
"cliparr-2-0-release-hero": "../assets/blog/cliparr-2-0-release/hero.png",
} satisfies Record<BlogHeroImageId, string>;

const blogHeroImageModules = import.meta.glob<{ default: ImageMetadata }>(
Expand Down
8 changes: 8 additions & 0 deletions tools/www-assets/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ async function main() {
// The recorder writes this last, only after both complete and validate.
await readFile(path.join(output, "report.json"), "utf8");
if (values.write) {
const readmeAssetsDir = path.join(root, ".github", "img");
await mkdir(readmeAssetsDir, { recursive: true });
for (const scene of scenes) {
for (const extension of ["mp4", "webm"]) {
const name = `${scene.videoName}.${extension}`;
Expand All @@ -138,6 +140,12 @@ async function main() {
path.join(output, name),
path.join(root, "apps/www/src/assets", name),
);
if (scene.name === "hero") {
await copyFile(
path.join(output, name),
path.join(readmeAssetsDir, "screenshot.webp"),
);
}
}
}
process.stdout.write(
Expand Down