Skip to content

Create an Adapter ‐ guidelines and rules (v2)

Alexander Cerutti edited this page Jun 16, 2026 · 11 revisions

Given the current web standard implementation situation, for what concerns media handling, could be described as a whole mess (according to author and other people), the API for creating adapters have been studied to prevent contributing to the entropy of the web itself.

Adapters are made to be safe and emit clear-to-understand errors.

First of all, what is an Adapter?

An adapter is a class that extends a common BaseAdapter class (available from @sub37/adapter-utils), that overrides some of its methods and performs the conversion between the format you decide to support and the format supported by sub37.

Adapters get instanced when provided to @sub37/server, one per playback session.

What should be overridden?

supportedType static property

This is a static property that will return the mime-type of the supported format, which will be matched by the server with the track. So it is important to provide the correct mime-type on both sides, otherwise matching won't happen and your users will be sad about your poor user experience.

import { BaseAdapter } from "@sub37/adapter-utils/BaseAdapter";

export class MyAdapter extends BaseAdapter {
	public static get supportedType(): string {
		return "application/vnd.acme.aster-subtitles";
	}
}

toString method

Note

The override of this method is optional but suggested.

This is useful to improve the error messages that you will receive, as they will likely contain your adapter name. BaseAdapter implements toString() to use the name of the class (or a default string if the adapter is an anonymous class). However, the applications this project will end up on, will likely be transpiled and minified. This means that the name of the class might be reduced to a couple of letters only, not letting you understanding where this error came from, especially if you use multiple adapters.

Both static and instance toString methods are suggested to get overridden.

import { BaseAdapter } from "@sub37/adapter-utils/BaseAdapter";

export class MyAdapter extends BaseAdapter {
	...

	public static toString(): string {
		return "MyAdapter";
	}

	public toString(): string {
		return "MyAdapter";
	}

	...
}

parse instance method

This is the entry point of your adapter. When chunks will be added to your track, this will be the method that will get called to parse the chunks.

This method is a generator, that must yield either when there's an error or when CueNodes are emitted.

Note

Each error is expressed through ParseError interface below:

interface ParseError {
	error: Error;
	isCritical: boolean;
  failedChunk: unknown;
}

Yielding allows the server to invoke the parser lazily and optimize the time spent on the main thread while the video streaming is loading. ParseGenerator interface expects adapter to always yield and array of CueNode[] | ParseError[].

import { CueNode } from "@sub37/adapter-utils/CueNode.js";
import { BaseAdapter } from "@sub37/adapter-utils/BaseAdapter";
import type { ParseGenerator } from "@sub37/adapter-utils/BaseAdapter";
import { MissingContentError } from "@sub37/adapter-utils/MissingContentError";

export default class MyAdapter extends BaseAdapter {
	...

	public *parse(content: unknown): ParseGenerator {
		if (!content) {
			return [
				{
					error: new MissingContentError(),
					isCritical: true,
					failedChunk: "...",
				}
			];
		}

		let nextCues: CueNode[] = [];

		while (true) {
			nextCues = parseCue(content);

			if (nextCues.length) {
				yield nextCues;
			}
		}

		return nextCues; // Return the last not emitted, if any
	}
}

Yielded errors can either be critical or non-critical:

  • Critical will halt immediately the parsing.
  • Non-critical will be emitted as CUE_ERROR event in the server.

All the errors should extend native class Error

Note

This is a guideline, not a rule. However, it is highly suggested to follow it.

The core idea is very easy: extending native functionalities, makes everything working better. In fact, Error natively has a name property and a message property.

Both can be overridden when you define your error. Overriding the name allows it to be printed when toString() is invoked on it. Also, this avoid loss of information after minification.

This is how your error class implementation should look like:

export class MissingContentError extends Error {
	constructor() {
		super();
		this.name = "MissingContentError";
		this.message = "Cannot parse content. Empty content received.";
	}
}

Note

Another guideline is to add the redundancy of "Error" at the end of the name.

Representing the region of a cue

If your captions format supports regions, you should define them through the Region interface.

import type { Region } from "@sub37/adapter-utils/Region";

export class MyRegionImplementation implements Region {
    ...
}

Sub37 works through the concept of Bring Your Own Region. Any adapter can define its own, as long as they respect the protocol. Each Region should have an ID and has a method getOrigin, that will allow you to compute the position of the region on the canvas based on the width and the height of the rendering area itself.

It is important that the id is consistent across same-region elements, as rendering might be using it to reuse the region element instead of creating a new one.

You can read more about them here.

Note

Although the adapters were born to run in the browser and not in the backend, future revisions might unlock further usages in the backend. Therefore, it is advised to not use any environment-specific API. Those should be relegated to be used in the renderer, which knows the platform.

As a general suggestion, you are invited to study how other adapters are made to understand better how they were designed and how yours can be designed.