DevelopersDocumentation

FUG Scripts & Playback Integration Guide

This guide describes the integration of FUG script storage and autonomous playback into a browser page. The integration uses standard browser APIs and requires no framework-specific library.

Open the live browser example in a new tab ยท Download the standalone HTML file

The live example provides a complete working interface for local file upload, import from URL, script management, playback controls, and live playback status.

Related documentation:


1. Integration Result

The completed browser integration follows this lifecycle:

  1. A FUG environment and Device Connection Key (DCK) are configured.
  2. An authenticated Server-Sent Events (SSE) stream is opened for the device.
  3. A Funscript or Autoblow CSV file is uploaded inline, or imported from an HTTPS URL.
  4. The returned script_id is selected for playback.
  5. Playback is started and controlled with pause, seek, sync, speed, offset, and stop actions.
  6. Playback and device status frames update the page in real time.
  7. Temporary scripts and open network streams are cleaned up when no longer needed.

FUG executes playback asynchronously. Every playback control returns 202 Accepted before the player-worker applies the action. The SSE stream carries the observable result.


2. Prerequisites

Requirement Description
FUG base URL The production API URL.
DCK A valid Device Connection Key for the target device.
Device connection The target device must be connected to the room identified by the DCK for physical movement and forwarded device status.
Browser A modern browser with fetch, ReadableStream, TextDecoderStream, and AbortController support.
CORS The page origin must be allowed by the FUG deployment. The Authorization request header triggers a CORS preflight for cross-origin requests.
Script Funscript JSON or Autoblow time_ms,position CSV content.

Production API

Base URL Intended use
https://fug-prd.feelme.com Production traffic.

Every public scripts and playback request uses the following header:

Authorization: DCK <device_connection_key>

The DCK acts as both the authenticated tenant identity and the target device identity. Browser code must not hardcode a real DCK, include a DCK in a URL, write a DCK to logs, or persist a DCK in localStorage. The live example keeps the value only in a password input and request memory. A production application should supply the value through an existing authenticated application flow.

The scripts and playback feature depends on the FUG store-backed runtime. A deployment with the feature disabled returns 404 Not Found for these routes.


3. Request Client

A shared request helper should normalize the base URL, attach DCK authentication, serialize JSON bodies, and surface the API error detail. Centralized request handling prevents inconsistent headers across controls.

async function fugRequest(baseUrl, dck, path, { method = "GET", body } = {}) {
  const response = await fetch(`${baseUrl.replace(/\/$/, "")}${path}`, {
    method,
    headers: {
      Authorization: `DCK ${dck}`,
      ...(body === undefined ? {} : { "Content-Type": "application/json" }),
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({ detail: response.statusText }));
    throw new Error(`${response.status}: ${JSON.stringify(error.detail)}`);
  }

  return response.status === 204 ? null : response.json();
}

The live example adds content-type detection, typed status errors, safe rendering with textContent, and a shared event log.


4. Status Stream

The status stream should be connected before playback starts. This order captures the earliest playback result and supports correlation between a control acknowledgement and a later status frame.

Native EventSource cannot attach the required Authorization header. An authenticated streaming fetch request is therefore required for GET /api/playback/status.

const response = await fetch(`${baseUrl}/api/playback/status`, {
  headers: { Authorization: `DCK ${dck}` },
  signal: abortController.signal,
});

if (!response.ok) {
  throw new Error(`Status stream returned ${response.status}`);
}

const reader = response.body
  .pipeThrough(new TextDecoderStream())
  .getReader();

The stream sends blocks separated by a blank line. Each JSON frame appears on one or more data: lines. Keepalive comment frames contain no data: field and should be ignored.

Two application frame types are available:

A reconnect opens a fresh stream. When playback state exists, the new stream receives an initial frame built from the latest checkpoint and desired state. The live example applies capped exponential backoff and stops automatic retries after an authentication failure.


5. Script Upload and Selection

Inline browser upload

The browser File API exposes selected content without a separate upload server. POST /api/scripts accepts the file text inside a JSON body.

const file = fileInput.files[0];
const metadata = await fugRequest(baseUrl, dck, "/api/scripts", {
  method: "POST",
  body: {
    content: await file.text(),
    content_type: file.type || null,
    name: file.name,
    tags: ["browser-upload"],
  },
});

const scriptId = metadata.script_id;

Server-side URL import

POST /api/scripts/from-url accepts a public script URL. HTTPS is required by default. FUG performs DNS and address checks, validates every redirect, and enforces a streamed byte limit.

const metadata = await fugRequest(baseUrl, dck, "/api/scripts/from-url", {
  method: "POST",
  body: { url: "https://cdn.example.com/example.funscript" },
});

Script selection

GET /api/scripts returns owned scripts in newest-first order. The returned metadata can populate a safe <select> element through DOM properties:

const scripts = await fugRequest(baseUrl, dck, "/api/scripts");

for (const script of scripts) {
  const option = document.createElement("option");
  option.value = script.script_id;
  option.textContent = `${script.name || "unnamed"} | ${script.duration_ms} ms`;
  scriptSelect.append(option);
}

6. Playback Controls

Start, resume, or switch

const accepted = await fugRequest(baseUrl, dck, "/api/playback/play", {
  method: "POST",
  body: {
    script_id: scriptId,
    start_ms: 0,
    speed_scale: 1.0,
    profile: "keon_wifi",
  },
});

Possible acceptance statuses are started, resumed, switching, and rejected. A rejection for the same active script still uses HTTP 202 and includes explanatory detail.

Remaining controls

Action Request
Pause POST /api/playback/pause with no body
Seek POST /api/playback/seek with { "position_ms": 12000 }
Sync POST /api/playback/sync with { "position_ms": 12000 }
Speed POST /api/playback/speed with { "speed": 1.5 }
Offset POST /api/playback/offset with { "offset_ms": -250 }
Stop POST /api/playback/stop with no body

seek represents explicit interface intent and updates the durable desired position. sync represents drift correction from an external media clock. Sync correction applies only during active playback, only when drift exceeds the configured threshold, and no more frequently than the configured sync interval.


7. Acknowledgement and Status Correlation

The following response confirms asynchronous acceptance:

{
  "status": "accepted",
  "request_id": "f9e45c14631c4f90939c16aebd0d65bd",
  "detail": null
}

The response does not confirm physical device execution. A matching playback frame provides the observable state:

{
  "type": "playback",
  "device_key": "<device_connection_key>",
  "script_id": "4df8eead6bad4eecb50b236bf49d1c09",
  "state": "paused",
  "position_ms": 18420,
  "index": 126,
  "point_count": 820,
  "speed_scale": 1.0,
  "offset_ms": 0,
  "origin": "f9e45c14631c4f90939c16aebd0d65bd",
  "detail": null
}

The request_id from the acknowledgement normally appears as origin when the worker applies the direct control event. An origin value of system identifies a system-generated or reconciled event, so correlation should also compare the requested state and values.


8. Ownership and Cleanup

The DCK that creates a script owns the script. Listing and deletion are owner-scoped. Metadata lookup and playback are available by script_id to another valid DCK, while source_url remains visible only to the owner. A script ID is therefore shareable application data rather than an authentication credential.

Scripts expire automatically after the configured TTL, currently 48 hours by default. Playback refreshes the stored script lifetime. Explicit deletion remains useful for temporary uploads and quota management.

await fugRequest(baseUrl, dck, `/api/scripts/${encodeURIComponent(scriptId)}`, {
  method: "DELETE",
});

A script has no update operation. Replacement requires deletion followed by another upload. POST /api/playback/stop should precede deletion when an active session uses the selected script.

The status request should be cancelled during page cleanup:

abortController.abort();

9. Error Handling

Status Typical cause Page behavior
400 Invalid script content, unsupported format, or blocked URL. Display the API detail; correct the source before another upload.
401 Missing, malformed, or invalid DCK. Stop automatic authentication retries and request a valid application credential.
403 A delete request targets a script owned by another DCK. Keep the script read-only.
404 Script missing or expired; scripts/playback feature disabled. Refresh the list and verify the selected environment.
409 Per-owner script quota reached. Delete an unused owned script before another upload.
413 Inline content or fetched content exceeds the configured size cap. Reject the selected source and show the size limit.
422 Request validation or script limits failed. Display validation details beside the relevant input.
429 Per-instance SSE stream capacity reached. Reconnect after a delay.
502 Remote script host returned an error or failed during fetch. Verify the public source URL and host availability.
504 Remote script fetch timed out. Retry later or use inline upload.

A network timeout after play creates an ambiguous result because the request may already have reached FUG. A blind immediate retry can cause an unwanted resume, switch, or rejection event. The status stream should be checked before another play request.


10. Production Checklist