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:

10,601 total downloads
Daily downloads all time
+2,963 / 30d
Daily Jetplay downloads on the JetBrains Marketplace
DateDownloads
2026-03-311
2026-04-016
2026-04-0225
2026-04-0326
2026-04-046
2026-04-054
2026-04-065
2026-04-075
2026-04-084
2026-04-097
2026-04-101
2026-04-114
2026-04-124
2026-04-136
2026-04-146
2026-04-152
2026-04-169
2026-04-171
2026-04-182
2026-04-193
2026-04-203
2026-04-213
2026-04-221
2026-04-233
2026-04-248
2026-04-256
2026-04-264
2026-04-272
2026-04-281
2026-04-296
2026-04-303
2026-05-011
2026-05-025
2026-05-032
2026-05-045
2026-05-056
2026-05-061
2026-05-072
2026-05-083
2026-05-090
2026-05-106
2026-05-115
2026-05-122
2026-05-138
2026-05-147
2026-05-158
2026-05-161
2026-05-171
2026-05-183
2026-05-191
2026-05-204
2026-05-216
2026-05-223
2026-05-233
2026-05-244
2026-05-252
2026-05-265
2026-05-277
2026-05-281
2026-05-295
2026-05-302
2026-05-311
2026-06-014
2026-06-023
2026-06-033
2026-06-044
2026-06-0514
2026-06-0613
2026-06-0724
2026-06-0831
2026-06-0941
2026-06-1037
2026-06-1198
2026-06-12122
2026-06-13108
2026-06-1463
2026-06-15158
2026-06-16207
2026-06-17161
2026-06-18140
2026-06-19106
2026-06-2044
2026-06-2168
2026-06-22126
2026-06-23176
2026-06-24125
2026-06-25109
2026-06-26133
2026-06-2790
2026-06-2869
2026-06-29116
2026-06-30118
2026-07-01136
2026-07-02190
2026-07-03136
2026-07-0473
2026-07-0563
2026-07-06117
2026-07-07141
2026-07-08127
2026-07-09124
2026-07-10110
2026-07-1168
2026-07-1256
2026-07-13136
2026-07-14137
2026-07-15121
2026-07-16112
2026-07-17122
2026-07-1878
2026-07-1979
2026-07-20127
2026-07-21142
2026-07-22136
2026-07-23122
2026-07-24101
2026-07-2569
2026-07-2669
2026-07-27112
2026-07-28110
2026-07-29131
2026-07-30127
2026-07-31101
2026-08-0165
2026-08-0264
2026-08-03119
2026-08-04107
2026-08-05100
2026-08-06118
2026-08-07103
2026-08-0854
2026-08-0944
2026-08-10113
2026-08-11101
2026-08-12101
2026-08-1390
2026-08-1499
2026-08-1546
2026-08-1653
2026-08-17118
2026-08-18103
2026-08-19137
2026-08-20140
2026-08-21116
2026-08-2249
2026-08-2351
2026-08-24125
2026-08-25127
2026-08-26100
2026-08-27117
2026-08-28102
2026-08-2979
2026-08-3050
2026-08-31127
2026-09-01102
2026-09-02128
2026-09-0393
2026-09-0474
2026-09-0554
2026-09-0645
2026-09-07129
2026-09-08126
2026-09-09118
2026-09-10110
2026-09-1189
2026-09-1290
2026-09-1361
2026-09-1494
2026-09-15109

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.