#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.

#Contents

#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 pathBrowser URL
/JAMC/extensions/studio-dashboard/index.htmlhttp://jamcorder.local/extensions/studio-dashboard/
/JAMC/extensions/studio-dashboard/app.jshttp://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"
}
FieldMeaning
nameUser-facing extension name. A packaged extension uses a hyphenated form of this name as its installed folder name.
authorPerson or organization that created the extension.
versionDisplay 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.css

The 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

MethodEndpointPurpose
GET/api/extensions/capabilitiesCheck extension API support.
GET/api/extensions/listList extension folders and optional manifest/logo metadata.
POST/api/extensions/upload-install-htmlInstall an HTML file using a JAMCJSBN upload.
POST/api/extensions/upload-install-tarInstall a .jext.tar package using a JAMCJSBN upload.
POST/api/extensions/uninstallDelete 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

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.