A high-performance Jellyfin plugin for retro game emulation. Runs standard RetroArch WebAssembly cores rendering directly onto a WebGL canvas with full USB/Bluetooth gamepad support. Now with Java ME game support via CheerpJ + FreeJ2ME.
mojo-snap-plugin/
โโโ build.ps1 โ Full build script (cores + J2ME + C# + packaging)
โโโ installer.iss โ Inno Setup Windows installer script
โโโ package.json โ ESLint / JS tooling
โ
โโโ src/ โ Jellyfin .NET Plugin (C#)
โ โโโ Api/
โ โ โโโ GameApiController.cs โ Retro ROM + save state API
โ โ โโโ J2meApiController.cs โ Java ME metadata, JAR streaming & save API
โ โโโ Web/
โ โ โโโ play.html โ Retro console player (WebGL canvas)
โ โ โโโ play.js โ RetroArch/WASM boot loader
โ โ โโโ j2me.html โ Java ME player page
โ โ โโโ j2me.js โ CheerpJ + FreeJ2ME boot loader
โ โโโ Plugin.cs โ Plugin registration
โ โโโ RomResolver.cs โ Jellyfin library item resolver
โ โโโ MojoSnapPlugin.csproj
โ
โโโ shared/ โ Common runtime assets served to the browser
โ โโโ cores/ โ RetroArch WASM cores (populated by build.ps1)
โ โโโ cheerpj/ โ CheerpJ + FreeJ2ME runtime slot
โ โ โโโ loader.js โ Optional self-hosted CheerpJ bridge
โ โ โโโ freej2me.jar โ FreeJ2ME runtime (downloaded by build.ps1)
โ โ โโโ README.md โ Self-hosting instructions
โ โโโ games/ โ Demo / shareware ROMs
โ โโโ gameplay.js โ Core emulator & gamepad logic
โ โโโ logo96.png
โ
โโโ docs/ โ GitHub Pages landing page
โ โโโ index.html
โโโ tests/
โโโ MojoSnapPlugin.Tests.csproj
| Tool | Minimum version | Notes |
|---|---|---|
| .NET SDK | 6.0 | dotnet --version |
| Node.js | 18+ | ESLint / JS tooling |
| 7-Zip | any | Core extraction (build.ps1 auto-detects) |
| PowerShell | 5.1+ | Build runner |
Open PowerShell and run:
powershell -ExecutionPolicy Bypass -File build.ps1The script runs 5 stages:
| Stage | What happens |
|---|---|
| 1 โ Clean | Wipes dist/ to ensure a fresh build |
| 2 โ RetroArch cores | Downloads the nightly Emscripten build from the libretro buildbot and extracts fceumm, snes9x2010, genesis_plus_gx, gambatte, mgba, and ecwolf WASM cores into shared/cores/ |
| 3 โ FreeJ2ME JAR | Downloads the latest freej2me.jar from GitHub Releases into shared/cheerpj/ (skipped if already present) |
| 4 โ C# plugin | dotnet restore + dotnet build -c Release โ dist/MojoSnapPlugin.dll |
| 5 โ Package | Copies src/Web/* and shared/ into dist/mojosnap/ and zips everything to MojoSnapPlugin-Release.zip |
- Copy
dist/MojoSnapPlugin.dllinto your Jellyfinplugins/folder. - Copy
dist/mojosnap/into your Jellyfin web folder, typically:- Windows:
C:\Program Files\Jellyfin\Server\jellyfin-web\mojosnap\ - Linux:
/usr/share/jellyfin/web/mojosnap/
- Windows:
- Restart your Jellyfin server.
Note: If you use the Windows installer (
MojoSnap_Setup.exe) these steps are done automatically.
| System | Core | File Extensions |
|---|---|---|
| NES | fceumm | .nes |
| SNES | snes9x2010 | .sfc, .smc |
| Sega Genesis / Master System / Game Gear | genesis_plus_gx | .md, .sms, .gg, .bin |
| Game Boy / Game Boy Color | gambatte | .gb, .gbc |
| Game Boy Advance | mgba | .gba |
| Wolfenstein 3D | ecwolf | .pk3, .zip |
| Format | Description |
|---|---|
.jar |
Java ME application archive |
.jad |
Java application descriptor (companion .jar auto-resolved) |
.zip |
ZIP-wrapped Java ME archive |
Java ME games run entirely in-browser using CheerpJ (browser JVM) and FreeJ2ME (MIDP runtime). No local Java installation required.
Because of Jellyfin 10.9+ security architecture, the C# plugin cannot automatically inject UI elements into the web client. To add a "Play" button to your ROM items, install the community JavaScript Injector plugin:
- In your Jellyfin Dashboard, go to Plugins โ Catalog and install Jellyfin JavaScript Injector.
- Restart Jellyfin.
- Open JavaScript Injector settings and paste the snippet below.
- Hard-refresh your browser (
Ctrl+F5).
let injectedForId = null;
// Java ME file extensions
const J2ME_EXTS = /\.(jar|jad)$/i;
// Retro console extensions
const RETRO_EXTS = /\.(nes|sfc|smc|md|gba|gb|gbc|sms|gg|bin|zip|pk3|img|cue|iso)$/i;
function checkAndInject() {
const url = window.location.href;
if (!url.includes('details?id=')) { injectedForId = null; return; }
const idMatch = url.match(/id=([a-zA-Z0-9]+)/);
if (!idMatch) return;
const id = idMatch[1];
if (injectedForId === id) return;
const playBtn = document.querySelector(
'button[title="Play"], button[aria-label="Play"], .btnPlay, button[data-action="play"]'
);
if (!playBtn) return;
const container = playBtn.parentElement;
if (container.querySelector('.btnMojoPlay')) return;
injectedForId = id;
const apiClient = window.ApiClient;
if (!apiClient) return;
apiClient.getItem(apiClient.getCurrentUserId(), id).then(item => {
if (!item) return;
const path = item.Path || '';
let href = null;
let label = '';
let color = '';
if (J2ME_EXTS.test(path)) {
// Java ME game
const title = encodeURIComponent(item.Name || '');
href = `/web/mojosnap/j2me.html?id=${id}&title=${title}`;
label = 'โ Play Java Game';
color = '#c86dd7';
} else if (RETRO_EXTS.test(path)) {
// Retro console game
href = `/web/mojosnap/play.html?id=${id}`;
label = '๐ฎ Play Retro Game';
color = '#52B54B';
}
if (!href) return;
const btn = document.createElement('button');
btn.className = playBtn.className + ' btnMojoPlay';
Object.assign(btn.style, {
backgroundColor: color,
color: '#fff',
marginLeft: '10px',
border: 'none',
borderRadius: '5px',
padding: '8px 16px',
cursor: 'pointer',
fontWeight: 'bold'
});
btn.innerHTML = label;
btn.onclick = e => { e.preventDefault(); e.stopPropagation(); window.location.href = href; };
container.appendChild(btn);
}).catch(err => console.warn('[MojoSnap] Error fetching item:', err));
}
setTimeout(checkAndInject, 500);
setTimeout(checkAndInject, 1500);
new MutationObserver(checkAndInject).observe(document.body, { childList: true, subtree: true });When you open a ROM or Java ME game in your Jellyfin library a coloured Play button will appear:
- ๐ข Play Retro Game โ for console ROMs
- ๐ฃ Play Java Game โ for
.jar/.jadfiles
The workflow (.github/workflows/build.yml) runs on every push and pull request to main:
push / pull_request
โ
โโโ lint (ubuntu-latest) โ ESLint on shared/gameplay.js, src/Web/*.js
โ
โโโ build (windows-latest) โ depends on lint
โโโ dotnet restore
โโโ dotnet test
โโโ build.ps1 (cores + FreeJ2ME + C# + package)
โโโ Inno Setup โ MojoSnap_Setup.exe
โโโ Upload artifacts:
MojoSnapPlugin-dll
MojoSnapPlugin-release-zip
MojoSnapPlugin-installer
Artifacts are available for download from the Actions tab of the GitHub repository after each successful build.
| # | Feature | Status |
|---|---|---|
| 1 | Network Service Discovery (mDNS) โ Virtual gamepad via phone using mDNS + binary WebSocket protocol | Planned |
| 2 | Settings & Controls Overlay โ EmulatorJS-style toolbar: play/pause, volume, save states, video/audio/hardware settings, controller remapping, cheats, context menu | Planned |
| 3 | DOS Emulation โ dosbox_pure core for DOS games via .zip archives |
Planned |
| 4 | Java ME Emulation โ CheerpJ + FreeJ2ME for .jar/.jad games |
โ Implemented |
Distributed under the GNU Lesser General Public License v2.1. See LICENSE for more information.