MCPcopy Index your code
hub / github.com/WofWca/jumpcutter

github.com/WofWca/jumpcutter @v1.32.2

Chat with this repo
repository ↗ · DeepWiki ↗ · release v1.32.2 ↗ · + Follow
321 symbols 726 edges 120 files 39 documented · 12%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Logo Jump Cutter

[Chrome Web Store][chrome-web-store] [Firefox Browser Add-ons][addons-mozilla-org] Matrix Discord [Translation status][weblate]

Download:

[Chrome Web Store][chrome-web-store] [Firefox Browser Add-ons][addons-mozilla-org] [Microsoft Edge Add-ons][microsoft-edge-addons] or from GitHub: Chromium / Gecko (Firefox)

Skips silent parts in videos, in real time.

Can be useful for watching lectures, stream recordings (VODs), webinars, podcasts, and other unedited videos.

Demo:

https://user-images.githubusercontent.com/39462442/131825020-5308b879-0509-41a3-95c9-bb4ad8938dc0.mp4

Inspired by this video by carykh.

How it works

Simple (mostly).

Currently there are 2 separate algorithms in place.

The first one we call "the stretching algorithm", and it's in this file. It simply looks at the output audio of a media element, determines its current loudness and, when it's not loud, increases its playbackRate. (We're using Web Audio API's createMediaElementSource and AudioWorkletProcessor for this).

Details, why it's called "stretching"

The algorithm we just described cannot "look ahead" in the audio timeline. It only looks at the current loudness, at the sample that we've already sent to the audio output device.

But looking ahead (a.k.a. "Margin before") is important, because, for example, there are certain sounds in speech that you can start a word with that are not very loud. But it's not good to skip such sounds just because of that. The speech would become harder to understand. For example, "throb" would become "rob".

Here is where the "stretching" part comes in. It's about how we're able to "look ahead" and slow down shortly before a loud part. Basically it involves slightly (~200ms) delaying the audio before outputting it (and that is for a purpose!).

Imagine that we're currently playing a silent part, so the playback rate is higher. Now, when we encounter a loud part, we go "aha! That might be a word, and it might start with 'th'".

As said above, we always delay (buffer) the audio for ~200ms before outputting it. So we know that these 200ms of buffered audio must contain that "th" sound, and we want the user to hear that "th" sound. But remember: at the time we recorded the said sound, the video was playing at a high speed, but we want to play back that 'th' at normal speed. So we can't just output it as is. What do we do?

What we do is we take that buffered (delayed) audio, and we slow it down (stretch and pitch-shift it) so that it appears to have been played at normal speed! Only then do we pass it to the system (which then passes it to your speakers).

And that, kids, is why we call it "the stretching algorithm".

For more details, you can check out the comments in its source code.

The second algorithm is "the cloning algorithm", and it's here. It creates a hidden clone of the target media element and plays it ahead of the original element, looking for silent parts and writing down where they are. When the target element reaches a silent part, we increase its playbackRate, or skip (seek) the silent part entirely. Currently you can enable this algorithm by checking the "Use the experimental algorithm" checkbox.

We look for video elements by injecting a script in all pages and simply document.getElementsByTagName('video'). But new video elements could get inserted after the page has already loaded, so we watch for new elements with a MutationObserver.

High-level architecture chart

If below you see a block of text instead of a chart, go here.

graph
%%graph TD

    %% TODO add links https://mermaid.js.org/syntax/flowchart.html#interaction

    watchAllElements["watchAllElements
        looks for media elements
        on the page"]
    click watchAllElements "https://github.com/WofWca/jumpcutter/blob/890a2b25948f39f1553cb9afb06c4cc10c9d2a19/src/entry-points/content/watchAllElements.ts"

    AllMediaElementsController["AllMediaElementsController
        the orchestrator,"]
    click AllMediaElementsController "https://github.com/WofWca/jumpcutter/blob/44fadb1982fbe7dd20c64741ae9e754ba9261042/src/entry-points/content/AllMediaElementsController.ts"

    watchAllElements -->|"onNewMediaElements(...elements)"| AllMediaElementsController
    AllMediaElementsController -->|original HTMLMediaElement| chooseController{choose
        appropriate
        controller}
    chooseController -->|original HTMLMediaElement| ElementPlaybackControllerCloning & ElementPlaybackControllerStretching


    %% ElementPlaybackControllerCloning

    %% subgraph "ElementPlaybackControllerCloning"

    ElementPlaybackControllerCloning["ElementPlaybackControllerCloning
        controls playbackRate
        of the original
        HTMLMediaElement"]
    click ElementPlaybackControllerCloning "https://github.com/WofWca/jumpcutter/blob/3ff011318dc9af407eaf9f4cc8d33dfafaf0b53e/src/entry-points/content/ElementPlaybackControllerCloning/ElementPlaybackControllerCloning.ts"

    Lookahead["Lookahead
        plays back the clone element
        to look for silence ranges"]
    click Lookahead "https://github.com/WofWca/jumpcutter/blob/988ec301bf2e6c07e6cc328f73a4177f7504f1e1/src/entry-points/content/ElementPlaybackControllerCloning/Lookahead.ts"

    ElementPlaybackControllerCloning --> | original HTMLMediaElement reference| Lookahead
    Lookahead --> |silence ranges| ElementPlaybackControllerCloning

    createCloneElementWithSameSrc --> |HTMLMediaElement clone| Lookahead
    Lookahead --> |original HTMLMediaElement reference| createCloneElementWithSameSrc
    click createCloneElementWithSameSrc "https://github.com/WofWca/jumpcutter/blob/e9daff122f12263a50fb1c4a10e4b13c7fd190cf/src/entry-points/content/ElementPlaybackControllerCloning/createCloneElementWithSameSrc.ts"

    cloneMediaSources["`cloneMediaSources
        intercepts all MediaSources
        and holds a clone
        HTMLMediaElement`"]
    click cloneMediaSources "https://github.com/WofWca/jumpcutter/blob/5bcfdaf53066c5ac4f664e089b916118afe37ae2/src/entry-points/content/cloneMediaSources/lib.ts"

    cloneMediaSources -->|HTMLMediaElement clone| Lookahead
    Lookahead -->|getMediaSourceCloneElement| cloneMediaSources

    SilenceDetector1["SilenceDetector
        utilizes Web Audio API
        to detect silence"]
    click SilenceDetector1 "https://github.com/WofWca/jumpcutter/blob/e3283500aeefe994a8be5bb7fdd8f7308e895f4f/src/entry-points/content/SilenceDetector/SilenceDetectorProcessor.ts"
    Lookahead --> |clone HTMLMediaElement audio| SilenceDetector1
    SilenceDetector1 --> |
        SILENCE_END &
        SILENCE_START events
        with timestamps
    | Lookahead

    %% end

    %% ElementPlaybackControllerStretching

    %% subgraph "ElementPlaybackControllerStretching"

    ElementPlaybackControllerStretching["ElementPlaybackControllerStretching
        controls playbackRate
        of the original
        HTMLMediaElement"]
    click ElementPlaybackControllerStretching "https://github.com/WofWca/jumpcutter/blob/3ff011318dc9af407eaf9f4cc8d33dfafaf0b53e/src/entry-points/content/ElementPlaybackControllerStretching/ElementPlaybackControllerStretching.ts"

    SilenceDetector2["SilenceDetector
        utilizes Web Audio API
        to detect silence"]
    click SilenceDetector2 "https://github.com/WofWca/jumpcutter/blob/e3283500aeefe994a8be5bb7fdd8f7308e895f4f/src/entry-points/content/SilenceDetector/SilenceDetectorProcessor.ts"
    ElementPlaybackControllerStretching --> |original HTMLMediaElement audio| SilenceDetector2
    SilenceDetector2 --> |
        SILENCE_END &
        SILENCE_START events
        with timestamps
    | ElementPlaybackControllerStretching

    %% end


    %% Telemetry

    %% ElementPlaybackControllerCloning & ElementPlaybackControllerStretching --> |telemetry| AllMediaElementsController

    Popup["Popup
        (UI, chart)"]
    click Popup "https://github.com/WofWca/jumpcutter/blob/44fadb1982fbe7dd20c64741ae9e754ba9261042/src/entry-points/popup/App.svelte"
    AllMediaElementsController --> |telemetry| Popup

Contribute

  • [🌐 Translate (on Weblate)][weblate]
  • 👨‍💻 Code. See CONTRIBUTING.md on how to get started. And feel free to contact me.
  • 💸 Donate
  • General feedback and questioning my decisions is appreciated

Build

  1. Install base tools:
  2. Run

    bash yarn install

3. Fill the src/_locales directory with localization files. Skip this step if they're already there.

Extension points exported contracts — how you extend this code

AudioWorkletProcessor (Interface)
(no doc) [2 implementers]
src/entry-points/content/AudioWorkletGlobalScope.d.ts
Settings (Interface)
(no doc)
src/settings/index.ts
KeyCombination (Interface)
(no doc)
src/hotkeys/index.ts
TelemetryRecord (Interface)
(no doc)
src/entry-points/content/ElementPlaybackControllerAlwaysSounded/index.ts
TelemetryRecord (Interface)
(no doc)
src/entry-points/content/ElementPlaybackControllerStretching/ElementPlaybackControllerStretching.ts
TelemetryRecord (Interface)
(no doc)
src/entry-points/content/ElementPlaybackControllerCloning/ElementPlaybackControllerCloning.ts

Core symbols most depended-on inside this repo

assertDev
called by 30
src/helpers/assertDev.ts
push
called by 22
src/entry-points/content/VolumeFilter/VolumeFilterProcessor.ts
addListener
called by 11
src/entry-points/content/ElementPlaybackControllerCloning/Lookahead.ts
updateClamped
called by 10
src/hotkeys/keydownEventToActions.ts
requestIdlePromise
called by 9
src/entry-points/content/helpers/requestIdlePromise.ts
getSettings
called by 8
src/settings/getSettings.ts
setPlaybackRateAndRememberIt
called by 7
src/entry-points/content/playbackRateChangeTracking.ts
getDelayFromInputToStretcherOutput
called by 7
src/entry-points/content/helpers/getDelayFromInputToStretcherOutput.ts

Shape

Function 144
Method 127
Class 36
Enum 8
Interface 6

Languages

TypeScript99%
Python1%

Modules by API surface

src/entry-points/content/ElementPlaybackControllerCloning/ElementPlaybackControllerCloning.ts32 symbols
src/entry-points/content/TimeSavedTracker.ts25 symbols
src/entry-points/content/AllMediaElementsController.ts21 symbols
src/entry-points/content/ElementPlaybackControllerStretching/ElementPlaybackControllerStretching.ts20 symbols
src/entry-points/content/cloneMediaSources/lib.ts18 symbols
src/entry-points/content/ElementPlaybackControllerCloning/Lookahead.ts18 symbols
src/entry-points/content/ElementPlaybackControllerStretching/StretcherAndPitchCorrectorNode.ts17 symbols
src/entry-points/content/VolumeFilter/VolumeFilterProcessor.ts11 symbols
src/entry-points/content/ElementPlaybackControllerAlwaysSounded/index.ts9 symbols
src/entry-points/background/iconAndBadgeUpdater.ts9 symbols
src/entry-points/content/lifetimeTimeSaved.ts8 symbols
src/entry-points/content/SilenceDetector/SilenceDetectorProcessor.ts6 symbols

For agents

$ claude mcp add jumpcutter \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact