npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wral/sdk-live-vod-handoff

v0.0.5

Published

A Software Development Kit for working with the Live VOD Handoff API

Readme

sdk-live-vod-handoff

The sdk-live-vod-handoff is a JavaScript SDK (Software Development Kit) for the Live VOD Handoff API (service-live-vod-handoff), which owns one stable reference URL per live event and reroutes what that URL resolves to across the event's life — live source, then a slate, then the finished VOD.

Installation

npm install @wral/sdk-live-vod-handoff

Usage

import { createClient } from '@wral/sdk-live-vod-handoff/v1';
// Or import everything:
// import * as v1 from '@wral/sdk-live-vod-handoff/v1';

Alternatively, import the whole SDK and use multiple versions:

import handoff from '@wral/sdk-live-vod-handoff';
// Then use it with handoff.v1.createClient(), etc.

Create a client with your configuration:

const config = {
  baseUrl: 'https://api.wral.com/dev/live-vod-handoff/v1',
  apiKey: 'YOUR_JWT',
};

const client = createClient(config);

baseUrl may be given with or without the /v1 suffix.

| | | | --- | --- | | dev | https://api.wral.com/dev/live-vod-handoff/v1 | | prod | https://api.wral.com/live-vod-handoff/v1not deployed yet |

Then use the client's methods:

// Start broadcasting now — this claims a shared encoder port immediately
const handoff = await client.createHandoff({
  title:  'Wake County commissioners',
  slug:   'wake-county-commissioners',
  source: 'magnum://172.16.90.12:4013/SDVN/src/2',
});

// Or schedule it; scheduledEnd is required whenever scheduledStart is set
await client.createHandoff({
  title:          'Wake County commissioners',
  slug:           'wake-county-commissioners',
  source:         'magnum://172.16.90.12:4013/SDVN/src/2',
  scheduledStart: '2026-08-07T14:00:00Z',
  scheduledEnd:   '2026-08-07T15:00:00Z',
});

// Read one record
const record = await client.getHandoff({ id: handoff.id });

// Page through a state
let page = await client.listHandoffs({ state: 'Live' });
while (page.next) {
  page = await page.fetchNext();
}

// Stop the stream and tear the infrastructure down
await client.endHandoff({ id: handoff.id });

// Send a rendered clip off to be transcoded
await client.submitSource({ id: handoff.id, mp4Url: 'https://…/clip.mp4' });

// Or point straight at a VOD asset that already exists
await client.setTarget({ id: handoff.id, uri: 'urn:uuid:8a85c11f-…' });

API

createClient(config)

Creates a new client instance with the provided configuration.

  • config: An object containing API configuration parameters:
    • baseUrl: The base URL of the API, with or without /v1.
    • apiKey: The bearer JWT.

Returns a client instance with methods for interacting with the API.

Methods

  • api(path, options): fetch wrapper for the API. Takes either a path (/handoffs/{id}) or a whole URL the API handed back, such as a page's nextUrl. A URL on any other origin is refused rather than called.
  • listHandoffs({ state, limit, order, next }): a page of handoffs in one state. state is required — the index partitions by it — and the SDK rejects without it. limit is 1–100 (default 25), order is asc|desc (default desc, by updatedAt). Resolves to { items, count, next, nextUrl, fetchNext }.
  • getHandoff({ id }): the full handoff record.
  • createHandoff({ title, slug, source, vodAssetId, scheduledStart, scheduledEnd }): creates a handoff. title, slug and source are required.
  • endHandoff({ id }): ends the stream and tears down the encoder port, video-live registration and recording.
  • setTarget({ id, uri }): points the handoff at an existing service-vod asset. Idempotent.
  • submitSource({ id, mp4Url }): submits a rendered mp4 for transcoding.

Scopes

Scopes are per endpoint, and they are the downstream scopes the call will exercise — the service deliberately has no handoff:* namespace of its own, so it can never exceed your token's authority. Hide or disable whatever the operator's token cannot perform.

| Method | Scopes required (all of) | | --- | --- | | listHandoffs, getHandoff | video:read | | createHandoff (immediate) | publisher:write, sr:write, video:write | | createHandoff (scheduled) | publisher:write, vid-sch:write | | endHandoff | publisher:write, sr:write, video:write | | setTarget | publisher:write, vod:read | | submitSource | publisher:write, vod:write |

Which createHandoff row applies is decided by scheduledStart. A 403 means the token is missing one or more of the scopes listed above.

Pagination

count is the size of the page, not a total — the API has no total. Follow next until it is null, or call fetchNext() on the resolved page. Calling fetchNext() past the last page resolves to an object with only a fetchNext, so a while (page.items?.length) loop terminates cleanly.

nextUrl is the same cursor as a ready-made URL. Pass it straight to api() if you would rather drive the paging yourself.

Errors

A non-2xx response throws an Error enriched with:

  • status, statusText, url
  • body: the parsed response payload
  • code: the API's stable error code, e.g. ILLEGAL_TRANSITION

The error's message is the API's human-readable message. Branch on code, display message.

body and code are populated only when the response was JSON. A gateway failure — a 502 in HTML, say — leaves both undefined. code is also absent on 401 and 403, which carry only a generic message. Check status before keying off code.

The SDK also throws before it makes a request, with a code but no status:

  • MISSING_STATElistHandoffs got neither state nor next.
  • CROSS_ORIGIN_URLapi() got a URL on an origin other than the configured baseUrl, which would have sent your JWT off-site.
try {
  await client.endHandoff({ id });
} catch (error) {
  if (error.code === 'TEARDOWN_INCOMPLETE') {
    // Infrastructure may still be allocated — retrying /end is the repair path
  }
}

Notes

  • Creating is not free. createHandoff without scheduledStart claims one of eight shared encoder ports and starts broadcasting. An abandoned handoff holds that hardware until someone calls endHandoff.
  • Only id, state, title, createdAt and updatedAt are guaranteed. Every other field appears once the lifecycle has produced it, so treat any field as possibly absent rather than keying off state.
  • lastError present does not mean the handoff is dead. It records the most recent failed attempt and is cleared when a later one succeeds.
  • State changes arrive by webhook, not from your calls, and every response is no-store. Poll or offer a refresh.

Related