@streamfox/plugin-sdk
v0.7.2
Published
Unified Node SDK for StreamFox media plugins
Readme
StreamFox Plugin SDK
@streamfox/plugin-sdk is the Node.js SDK for StreamFox remote plugins.
It includes:
- declarative plugin contract with
definePlugin(...) - runtime server with
createServer(...)/serve(...) - strict schema-major validation (
schemaVersion.major === 1) - built-in installer UI and typed settings parsing
- canonical contract parity with
swift-media-plugin-kit
Install
npm i @streamfox/plugin-sdkQuick Start
import { definePlugin, ids, serve } from "@streamfox/plugin-sdk";
const plugin = definePlugin({
plugin: {
id: ids.plugin("com.example.demo"),
name: "Demo",
version: "0.1.0",
},
resources: {
stream: {
mediaType: "movie",
handler: async () => ({
streams: [
{
transport: {
kind: "http",
url: "https://cdn.example.com/movie.mp4",
mode: "stream",
},
},
],
}),
},
},
});
const server = await serve(plugin, {
port: 7000,
integration: {
installScheme: "streamfox",
launchBaseURL: "https://streamfox.app/#",
autoOpen: "none",
},
});
console.log(server.url); // manifest URL
console.log(server.installURL); // install deeplink
console.log(server.launchURL); // launch URLProgressive Shorthand
definePlugin(...) accepts shorthand aliases and normalizes them to canonical manifest fields:
type->mediaTypes: [type]mediaType->mediaTypes: [mediaType]
Conflict guard:
- equivalent canonical+alias values are accepted
- mismatched canonical+alias values throw
MANIFEST_INVALID
Defaults (when omitted):
meta.mediaTypes:["movie", "series"]stream.mediaTypes:["movie", "series"]subtitles.mediaTypes:["movie", "episode"]stream.supportedTransports:["http"]subtitles.defaultLanguages:["en"]catalog.endpoint.mediaTypes:["movie"]
Validation Lifecycle
Validation runs automatically in three phases:
createPlugin(...)/definePlugin(...): manifest + capability shape validation.- Incoming request handling (
createServerroutes): request validation against manifest capabilities. - Outgoing handler responses: response validation before JSON emission.
Redirect responses are validated too (redirect.url, redirect.status) before the redirect is returned.
Canonical Routes
GET /manifestGET /studio-configGET /catalog/:mediaType/:catalogIDGET /meta/:mediaType/:itemIDGET /stream/:mediaType/:itemIDGET /subtitles/:mediaType/:itemIDGET /plugin_catalog/:catalogID/:pluginKind
GET Query Style
Resource routes use one HTTP style:
- path params for identity
- plain query aliases for request shaping
Examples:
/catalog/movie/browse?genre=action&year=2024&locale=el-GR&page=0&pageSize=20&orderBy=popular/catalog/movie/browse?year=2000..2024&rating=7..10/catalog/movie/browse?orderBy=rating&order=asc/meta/movie/tt0133093?locale=el-GR®ionCode=GR/stream/movie/tt0133093?videoID=trailer&startPositionSeconds=123&networkProfile=wifi/stream/movie/tt0133093?quality=1080p&videoID=trailer&startPositionSeconds=123&networkProfile=wifi/subtitles/movie/tt0133093?source=opensubtitles&hearingImpaired=true&videoHash=abc123&videoSize=1234567&filename=matrix.mkv&languagePreferences=en,el/plugin_catalog/featured/catalog?page=0&experimental=streamfox:beta
Legacy structured query params such as request, schemaVersion, context, experimental, filters, sort, playback, and videoFingerprint are rejected on GET resource routes.
Strong ID Helpers
Use ids.* helpers when authoring manifests and responses:
ids.plugin("com.example.demo")ids.catalog("browse")ids.item("tt0133093")ids.video("main")ids.imdb("tt0133093")for strict IMDb format (tt+ digits)
IMDb-Only IDs
This SDK enforces IMDb-only media IDs:
- Item/media IDs must match
tt<digits> - Episodic video IDs must match
tt<digits>:<season>:<episode>when colon format is used - ID prefix configuration is not part of the SDK API
Examples:
- valid item ID:
tt0133093 - invalid item ID:
tmdb:603 - valid episodic video ID:
tt0944947:1:1 - invalid episodic video ID:
episode-1
Migration Note
If older code used custom ID prefixes, migrate to IMDb IDs (tt...) and episodic video IDs in tt...:season:episode format.
Unified Filter Ergonomics
Catalog filters support:
- reusable
filterSetsshared across endpoints - reusable
sortSetsshared across endpoints - richer filter metadata for UI and docs
- richer sort metadata for UI and docs
filters.*andsorts.*helpers for authoring plain manifest-compatible specs- query alias normalization through
options[].aliases - ordering aliases through
orderBy isRequiredfor required (always-visible) controlsindex(>= 0) for deterministic UI orderingoptionsLimitandmaxSelectedfor bounded multi-selectsdynamicOptionsfor provider-driven filter options with caching/fallback behaviorvisibleWhen/enabledWhenfor conditional filter UX- the same
FilterSpecmodel oncatalog,stream, andsubtitles
Example:
import { definePlugin, filters, ids, sorts } from "@streamfox/plugin-sdk";
const plugin = definePlugin({
plugin: {
id: ids.plugin("com.example.catalog"),
name: "Catalog Demo",
version: "0.1.0",
},
resources: {
catalog: {
filterSets: {
commonCatalogFilters: [
filters.select("language", {
label: "Language",
group: "regional",
options: [
{ label: "Japanese", value: "ja", aliases: ["Japanese (ja)"] },
{ label: "English", value: "en", aliases: ["English (en)"] },
],
}),
filters.select("genre", {
options: [
{ label: "Action", value: "action", aliases: ["Action"] },
{ label: "Drama", value: "drama" },
],
}),
],
},
sortSets: {
browseSorts: [
sorts.desc("popularity", {
label: "Popular",
aliases: ["popular"],
}),
sorts.choice("rating", {
label: "Top Rated",
aliases: ["top-rated"],
directions: ["descending", "ascending"],
defaultDirection: "descending",
}),
],
},
endpoints: [
{
id: ids.catalog("browse"),
name: "Browse",
mediaTypes: ["movie"],
filterSetRefs: ["commonCatalogFilters"],
sortSetRefs: ["browseSorts"],
filters: [filters.intOrRange("year"), filters.range("rating")],
},
{
id: ids.catalog("episodes"),
name: "Episodes",
mediaTypes: ["series"],
filters: [
filters.number("season", {
label: "Season",
group: "episodes",
}),
],
},
],
handler: async () => ({ items: [] }),
},
},
});Prefer semantic endpoint IDs such as browse, discover, and search. Keep variable controls in the query string:
/catalog/movie/browse?language=ja/catalog/movie/browse?year=2024/catalog/movie/browse?year=2000..2024/catalog/movie/browse?query=matrix/catalog/movie/browse?orderBy=popular/catalog/series/episodes?season=1/catalog/series/episodesto return all episodes when no season is provided
stream and subtitles can declare their own filters too, while still using dedicated aliases like videoID, videoHash, and languagePreferences.
Reserved filter keys are rejected across all resource kinds:
query,page,pageSize,orderBy,ordervideoID,videoHash,videoSize,filename,languagePreferenceslocale,regionCode,traceID,experimental
Manifest Policy + Quality Metadata
Manifest now supports:
safety(adult,p2p) for install-time trust hintsconfiguration(required,fields) as the first-class plugin config schemacapabilityConstraints(accountRequired,bandwidth, geo allow/block regions)qualitySignals(providerSuccessRate,timeoutRatio,freshnessTimestamp)
Capability-level guardrails:
- IMDb-only ID enforcement for
meta,stream, andsubtitlescapabilities meta.embeddedVideoStreamStrategy(exclusive|merge|prefer_external)- catalog
discoverymetadata for UI defaults (mode,defaultSort,defaultFilters)
Rich Media Details
MediaSummary stays lightweight for browse/cards, but supports a few presentation fields:
backgroundruntimeyearLabellogoURLreleasedAtslugimdbRatingsourceRatingspopularity
MediaDetail is the richer app-detail model and supports:
releasedAtdvdReleaseAtlogoURLlanguagecountryawardsslugimdbRatingsourceRatingspopularitypopularityBySourcecast,directors,writersbehaviorHintsvideostrailerssimilarItems
behaviorHints follows the Cinemeta-style shape:
behaviorHints?: {
defaultVideoId?: VideoID | null;
hasScheduledVideos?: boolean;
}Video entries support both:
releasedAt: generic availability datefirstAiredAt: schedule/broadcast date for upcoming-episode UXrating: optional video/episode rating
There is no separate trailerStreams field; trailers are represented only through trailers.
ID Model
StreamFox uses typed IDs in the SDK contract:
MediaSummary.idMediaDetail.summary.idsimilarItems[].idVideoUnit.id
ID semantics depend on the entity:
- media/title IDs identify the title itself, for example
tt0133093 - video IDs identify the video resource itself, for example
mainortt8599532:1:4
Recommended episodic video ID format:
{parentMediaID}:{season}:{episode}
defaultVideoID always points to one videos[].id.
Use ids.item(...) for media IDs, ids.video(...) for video IDs, and ids.imdb(...) when strict IMDb format is required.
Custom Frontends
There are two supported ways to own the installer/frontend experience:
- Headless mode:
await serve(plugin, {
frontend: false,
});Use your own app against GET /manifest, GET /studio-config, and the resource routes.
- Custom static bundle served by the SDK:
await serve(plugin, {
frontend: {
mountPath: "/installer",
distPath: "/absolute/path/to/frontend-dist",
assetsMountPath: "/installer/assets",
},
});/studio-config is the frontend contract for installer metadata, field definitions, deeplink scheme, and configurationRequired (derived from manifest.configuration.required).
Docs
Advanced API
import {
createPlugin,
parseJsonWithLimits,
maximumJsonNestingDepth,
validateManifest,
validateRequest,
validateResponse,
ProtocolError,
} from "@streamfox/plugin-sdk";JSON payload size/depth limit controls live in the schema utilities above, not in createServer(...) or serve(...).
Subpath export is also available:
import {
validateManifest,
validateRequest,
validateResponse,
} from "@streamfox/plugin-sdk/advanced";Development
npm install
npm run format
npm run build
npm test