#Jamcorder local web extensions
A local web extension is a static webpage stored on the SD card and hosted directly by your Jamcorder device. Anyone on the same network can open it in a standard web browser—no app, cloud service, or separate web server is required.
Purpose: let people extend Jamcorder with new features, such as custom dashboards, recording browsers, live MIDI visualizers, setup tools, and specialized device controls.
Because the page is served by Jamcorder, its JavaScript can use the complete Jamcorder device API. An extension can read recordings and device state, display live MIDI, change settings, transfer files, or build an entirely custom interface.
Trust extensions like installed software. Jamcorder does not isolate an extension from the device API. Extension code can read private device data and call destructive endpoints. Install only extensions you trust, and expose Jamcorder only on a trusted network.
#Related documentation
#Contents
- How it works
- Quick start
- Extension structure
- Using the Jamcorder API
- Live MIDI
- Local development
- Installation and management
- Security and limitations
#How it works
Jamcorder serves any files found in this SD-card directory:
/JAMC/extensions/Each child folder is one extension. Opening the corresponding URL serves that folder’s index.html:
| SD-card path | Browser URL |
|---|---|
/JAMC/extensions/studio-dashboard/index.html | http://jamcorder.local/extensions/studio-dashboard/ |
/JAMC/extensions/studio-dashboard/app.js | http://jamcorder.local/extensions/studio-dashboard/app.js |
The device and browser must be reachable on the same local network. An IP address can be used instead of jamcorder.local, which is useful when multiple Jamcorders share a network or mDNS is unavailable.
Extensions are static client-side applications. Jamcorder serves their HTML, CSS, JavaScript, images, fonts, media, and other files; application logic runs in the browser.
#Quick start
Create a folder named hello-jamcorder inside /JAMC/extensions/, then save this file as index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello Jamcorder</title>
</head>
<body>
<h1>Hello Jamcorder</h1>
<pre id="identity">Loading…</pre>
<script>
async function loadIdentity() {
const response = await fetch("/api/identities/get");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const identity = await response.json();
document.querySelector("#identity").textContent =
JSON.stringify(identity, null, 2);
}
loadIdentity().catch((error) => {
document.querySelector("#identity").textContent = error.message;
});
</script>
</body>
</html>Insert or remount the SD card, make sure Jamcorder is on the network, and open:
http://jamcorder.local/extensions/hello-jamcorder/Root-relative URLs such as /api/identities/get automatically target the Jamcorder that served the page.
#Extension structure
An extension can contain only index.html. Metadata and a logo are optional:
/JAMC/extensions/studio-dashboard/
├── index.html Required entry page
├── manifest.json Optional extension metadata
└── logo.png Optional icon shown by Jamcorder#index.html
Jamcorder serves index.html when a browser requests the extension folder with or without a trailing slash. Any additional files are optional. Use root-relative paths for Jamcorder APIs:
fetch("/api/device-state/get");#manifest.json
The optional manifest describes the extension in Jamcorder’s installed-extension list. All fields are optional.
{
"name": "Studio Dashboard",
"author": "John Smith",
"version": "1.0.0"
}| Field | Meaning |
|---|---|
name | User-facing extension name. A packaged extension uses a hyphenated form of this name as its installed folder name. |
author | Person or organization that created the extension. |
version | Display version. Jamcorder does not impose a version-number format. |
#logo.png
If present at the extension root, logo.png is reported by the Extensions API and displayed by Jamcorder’s web interface. If it is absent, the built-in extension icon is used.
#Supported files
Jamcorder recognizes common web and media filename extensions, including HTML, JavaScript, CSS, text, JSON, PNG, JPEG, WebP, GIF, AAC, MP3, MIDI, WAV, MP4, MPEG, WebM, PDF, TAR, TTF, WOFF, and WOFF2. Use the conventional filename extension so the browser receives the correct media type.
#Using the Jamcorder API
An installed extension has same-origin access to every endpoint exposed by the running Jamcorder firmware. This includes device identity and state, recordings, files, configuration, diagnostics, firmware updates, and destructive maintenance operations. See the complete Jamcorder device API reference.
#Read device state
async function getDeviceState() {
const response = await fetch("/api/device-state/get");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}#List recent recordings
async function listRecentRecordings() {
const response = await fetch("/api/library/list/assets", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
getJmxEof: true,
getFilesize: true,
getAllDevices: false,
preferredCount: 20
})
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}The binary structure of recording assets is documented in JMX MIDI files.
#Write settings
async function renameJamcorder(name) {
const response = await fetch("/api/identities/rename/jamcorder", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ jamcorderName: name })
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}Check response.ok before parsing a successful response. API failures return structured JSON with fields such as error1, errorCodeNum, and errorCodeStr.
#Live MIDI
Extensions can receive and send live MIDI through /api/midi-io/websocket. The WebSocket uses binary Jamcorder framing rather than browser Web MIDI messages.
const midiSocket = new WebSocket(`ws://${location.host}/api/midi-io/websocket`);
midiSocket.binaryType = "arraybuffer";
midiSocket.addEventListener("message", ({ data }) => {
const bytes = new Uint8Array(data);
// 0x01 is the server keepalive ping.
if (bytes.length === 1 && bytes[0] === 0x01) {
midiSocket.send(Uint8Array.of(0x02));
return;
}
// Application frames begin with 0x00. Decode their 12-byte
// MIDI records according to the device API documentation.
if (bytes[0] === 0x00) {
console.log("MIDI frame", bytes);
}
});See WebSockets for framing, timestamps, record layout, keepalive behavior, and client-to-device MIDI output.
#Local development
You can develop an extension from a local web server on your computer without copying it to the SD card after every edit. Jamcorder API responses permit cross-origin requests, so a page served from http://localhost can call Jamcorder by hostname or IP address:
const JAMCORDER = "http://jamcorder.local";
async function getIdentity() {
const response = await fetch(`${JAMCORDER}/api/identities/get`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}Before installing the extension, switch Jamcorder requests to root-relative URLs such as /api/identities/get. This lets the same extension work with any Jamcorder address.
Browser note: a page loaded over HTTPS will normally be blocked from calling Jamcorder’s plain-HTTP API as mixed content. Use an HTTP local development server.
#Installation and management
#Copy to the SD card
The simplest installation method is to copy the complete extension folder into /JAMC/extensions/. The extension becomes available when the card is inserted and mounted by Jamcorder.
#Install through the web interface
Jamcorder’s built-in web interface accepts an HTML file or a packaged extension ending in .jext.tar. Open http://jamcorder.local/webapp, find Extensions, and drop the file into the installer.
#Package as .jext.tar
A packaged extension is an uncompressed TAR archive whose filename ends in .jext.tar. Put index.html, manifest.json, logo.png, and other resources at the archive root:
tar -cf studio-dashboard.jext.tar \
index.html manifest.json logo.png app.js styles.cssThe initial installation folder comes from the package filename. If the manifest contains name, Jamcorder renames the installed folder to the hyphenated form of that name.
#Manage with the API
| Method | Endpoint | Purpose |
|---|---|---|
GET | /api/extensions/capabilities | Check extension API support. |
GET | /api/extensions/list | List extension folders and optional manifest/logo metadata. |
POST | /api/extensions/upload-install-html | Install an HTML file using a JAMCJSBN upload. |
POST | /api/extensions/upload-install-tar | Install a .jext.tar package using a JAMCJSBN upload. |
POST | /api/extensions/uninstall | Delete an installed extension folder. |
The upload endpoints require JAMCJSBN metadata containing {"filename": "..."}. See JSON-plus-binary uploads for the wire format.
Example list response:
{
"extensions": [
{
"extFolder": "studio-dashboard",
"name": "Studio Dashboard",
"author": "John Smith",
"version": "1.0.0",
"hasLogo": true
}
]
}To uninstall that extension:
await fetch("/api/extensions/uninstall", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ extFolder: "studio-dashboard" })
});#Security and limitations
- No server-side code: Jamcorder hosts static files; JavaScript runs in the visitor’s browser.
- SD card required: extension files must be present on a detected and mounted SD card.
- Network-local: visitors must be able to reach Jamcorder on the same network or its device-hosted hotspot.
For extension compatibility, discover the endpoint set at runtime with /api/meta/capabilities and /api/meta/endpoints. Do not assume that every firmware version exposes every endpoint.