Jetplay: Audio and Video Playback for JetBrains IDEs

James Ding · Posted on · Last updated

So apparently JetBrains IDEs have never supported audio or video playback.

So when you open a video in IntelliJ, PyCharm, WebStorm, or any other JetBrains IDE, all you get is to set up a file association so some other program can open it. Which, in my opinion, is kind of cooked (VSCode supports this, btw.)

So why not build a plugin to handle this all within the IDE itself? So I built Jetplay.

This solves the dumb problem of audio and video playback support within your JetBrains IDE. Feel free to install it by clicking below:

Get it from JetBrains Marketplace 6,326 downloads

The rest of this writeup will mostly be about the hoops to build this and my engineering decisions.

Just Use a Browser

So apparently JetBrains already ships a browser. I think it’s already used in JetBrains official Markdown plugin for rendering, but there are other plugins harnessing this, like a PDF Viewer Plugin.

What actually lies under the hood is JCEF1 , which is basically Chromium (and most importantly, allows playback of audio and video). The first version did the obvious thing: load a small player page into JCEF and point an <audio> or <video> tag at the file.

And locally, that’s the whole trick: double-click and it plays. JCEF is happy to play a file:// URL, audio or video. The early versions of Jetplay were exactly that, a player with a progress bar and nothing else, and they worked fine.

The loopback server came later, when I added a waveform you could scrub. A null-origin page can play a file:// URL but can’t fetch() and decode its bytes, and drawing a waveform needs exactly that. file:// also has no real Range support2 , so seeking was shaky on big files too. So local media moved onto a tiny HTTP server on 127.0.0.1 that hands the page a real http:// URL, with the CORS header it wants, Accept-Ranges: bytes, and a 206 Partial Content for whatever range it asks for.3 Same playback, just a server handling a fetchable URL now.

Remote Development

All of that worked well, but only on local setups, which, for me, recently has been pretty rare. The first time I opened something over RD4 , it came apart. Video wouldn’t play at all. Audio would start, run for a couple of seconds, and then randomly cut out.

Architecturally, RD splits it across two machines: the file on the host, the browser on the client (since JCEF only runs there). And a loopback server only answers on its own machine. Put it on the host and the browser can’t reach it; put it on the client and there’s no file to read. Either way the player runs out of bytes, which is the cut-out, and why video never even started.

media file
audio or video on disk
loopback server
127.0.0.1:PORT/$token · CORS · Accept-Ranges · 206
JCEF <video>
in-process Chromium · seeks like the web
How a byte range gets from the file to the <video> tag. Local: a loopback server hands seekable bytes to the in-process browser. Remote Dev: the browser's on the client and the file's on the host, so the client's loopback server pulls each range from the host over @Rpc.

So I split the plugin in two and let the halves talk over the wire. There are four Plugin Model V25 modules: shared holds the contract, frontend is the player and the loopback server, client renders it in split mode, and backend runs FFmpeg on the host. The client runs its own loopback server, and every byte it needs it pulls from the host over an @Rpc6 call. When the browser seeks, the server turns that into readRange(offset, length), the host reads the slice off disk and sends the bytes back. Transcoding runs on the host too, where FFmpeg and the file already are.

You don’t see any of this. You double-click and it plays, whether the file is on your own disk or on a host three time zones away.

What it plays

Pretty much anything. If the embedded Chromium can decode it, it plays the moment you open it.

It’s not over just yet though, Chromium doesn’t support proprietary codecs.7 So we need to re-encode all them into codecs Chromium can play with FFmpeg. We bundle FFmpeg8 to transcode it to WebM the first time, with a progress bar. Either way you just double-click any registered media, and Jetplay either plays it natively or via a transcoded format

The current list of formats lives on GitHub, and it keeps growing. To install, search for “jetplay” under SettingsPluginsMarketplace.

Footnotes

  1. Java Chromium Embedded Framework, a full Chromium embedded in the IDE.
  2. The Range: bytes=… header. It asks for a slice of a file instead of the whole thing. Without it, seeking gets unreliable, because the player can’t ask for the bytes from 2:00 onward.
  3. Loopback only, a random token per file, and a Host-header check against DNS rebinding. It serves the files you’ve opened and nothing else.
  4. Remote Development: the IDE backend runs on a remote host, a thin client paints the UI on your laptop. Your files live on the host. You never see them locally.
  5. IntelliJ’s content-module system. A plugin splits into modules that load only where their target exists: host, client, or both.
  6. IntelliJ’s typed RPC over the Remote Dev protocol: an annotated Kotlin interface whose calls marshal across the host/client boundary, suspend functions and streams included.
  7. Chromium ships only royalty-free codecs; Chrome layers the licensed ones on top. So the embedded build won’t decode H.264, HEVC, or AAC, which is most of what’s sitting in an .mp4.
  8. JavaCV’s bundled build, so there’s no system install and nothing on your PATH.