Usage Guide
Installation
npm install @amazon/vinyl
Creating a Player
To create a new player, call createVinylPlayer with desired configuration.
Refer to the API Documentation for configuration options.
import { createVinylPlayer } from '@amazon/vinyl'
createVinylPlayer({ media: new Audio() })
Loading Tracks
Tracks are constructed as-needed from load configuration objects.
For DRM configuration and encrypted content, see DRM Configuration.
Tracks are constructed when they are prefetched, then disposed when they are no longer in the queue and the cache evicts tracks least recently used.
Basic example:
import { createVinylPlayer } from '@amazon/vinyl'
const media = new Audio()
media.controls = true
document.body.appendChild(media)
const player = createVinylPlayer({ media })
player.load(
{
type: 'src',
uri: 'https://example.com/myTrack.mp3',
},
{
type: 'dash',
uri: 'https://example.com/myNextTrack.mpd',
},
{
type: 'hls',
uri: 'https://example.com/myTrack.m3u8',
}
)
HLS Track Configuration
HLS tracks use the 'hls' type and play fMP4 HLS streams via Media Source
Extensions. This works on all browsers that support MSE (Chrome, Firefox, Edge,
Safari 17+). The uri should point to an HLS main playlist (.m3u8).
player.load({
type: 'hls',
uri: 'https://example.com/main.m3u8',
})
Media playlists are fetched lazily — only the variant selected by ABR is fetched, and results are cached. For video content with separate audio renditions, the audio rendition playlist is resolved automatically.
Because HLS tracks use the same MSE pipeline as Dash, they benefit from all of Amazon Vinyl's streaming features: adaptive bitrate switching, track preloading and prefetching, quality filtering, buffering control, and network prioritization.
A custom manifest provider can be supplied to override the default fetch behavior:
player.load({
type: 'hls',
uri: 'unique_identifier',
manifestProvider: async (abort) => {
const baseUrl = 'https://example.com/main.m3u8'
const response = await requestWithRetry(baseUrl, undefined, { abort })
const text = await response.text()
const mainPlaylist = parseMainPlaylist(text)
return {
mainPlaylist,
baseUrl,
getMediaPlaylist: memoize(
async (uri) => {
const resp = await requestWithRetry(new URL(uri, baseUrl))
return parseMediaPlaylist(await resp.text())
},
(uri) => uri
),
}
},
})
Note: HLS via MSE supports both fMP4 streams (those with
#EXT-X-MAP) and MPEG-TS streams, which are transmuxed to fMP4 on the fly. MPEG-TS via MSE does not support encrypted content — for encrypted MPEG-TS, use native HLS playback on Safari and iOS by loading the stream with the'src'track type.
Dash Track Configuration
Dash track default behavior is to treat the uri as the Dash MPD location. This
can be overridden with a custom manifest provider, which is returns a parsed
dash manifest.
DASH streams may use ISO-BMFF (MP4/fMP4) or WebM containers, with
SegmentTemplate, SegmentList, or SegmentBase addressing. SegmentBase
streams are indexed from their ISO-BMFF sidx box or WebM Cues element as
appropriate. Codec support (e.g. AVC/HEVC/VP9 video, AAC/Opus/FLAC audio) is
gated by the browser's Media Source Extensions support.
player.load({
type: 'dash',
uri: 'unique_identifier',
manifestProvider: async () => {
const response = await requestWithRetry('https://example.com/service')
const manifestStr = await response.json().MPD
return {
manifest: parseDashManifest(manifestStr),
baseUri: new URL('https://example.com'),
}
},
})
Load sets the queue. To begin playback, invoke player.play(). Depending on
browser autoplay policies, the first call to play() must be in response to a
user interaction such as a click, touch, key, or voice event.
<script>
function play() {
// play() rejections should be handled, but are typically recoverable. Reasons for rejection include
// an AbortError from a track change, or NotAllowed due to not being first called from a user interaction such
// as a click or keypress.
// Not catching rejections will result in unhandled promise rejections logging to the console.
player.play().catch((error) => console.warn(error))
}
</script>
<body>
<button onclick="play()">PLAY</button>
</body>
To append tracks to the current queue without stopping the currently playing
track, use enqueue.
Track Preload
To preload tracks without adding them to the queue, for example on a mouse
hover, use preload. preload creates and caches the track.
Usage example:
<script>
const track = {
type: 'src',
uri: 'https://example.com/myTrack.mp3',
}
function preload() {
player.preload(track)
}
function load() {
player.load(track)
}
</script>
<body>
<img
alt="play"
src="https://example.com/image.jpg"
onmouseenter="preload()"
onclick="load()"
/>
</body>
Preload Cache
If a list of tracks are provided to preload, the cache capacity will automatically grow to accommodate all provided tracks.
Cached preloaded tracks are disposed automatically. When the cache capacity has been reached, the least recently used track will be disposed.
Track Enqueue
To append tracks to the current queue without affecting playback, use enqueue.
Usage example:
player.on('trackActivated', () => {
if (player.queue.length < 2) {
// Queue is nearing exhaustion, append more tracks
player.enqueue(
{
type: 'src',
uri: 'https://example.com/myTrack1.mp3',
},
{
type: 'dash',
uri: 'https://example.com/myTrack2.mpd',
},
{
type: 'dash',
uri: 'https://example.com/myTrack3.mpd',
}
)
}
})
Track Events
See TrackControllerEventMap in API Docs for full documentation.
trackActivated- Emitted when a track becomes active (carries the track).trackDeactivated- Emitted when the active track is torn down, because it was superseded by another track or unloaded.trackEnded- Emitted once a track has fully finished, after its postroll ad if any. Prefer this overendedto detect true track completion.queueEnded- Emitted when the last track of the playback queue has ended.
When the last track in the queue has ended, it will not automatically be unloaded. To automatically unload when the queue ends, one could write:
player.on('queueEnded', () => player.unload())
When a track ends, a low-level ended event is emitted before the queue moves
to the next track; if an ended handler changes the queue, the automatic track
transition is canceled. Note that ended also fires for each ad (ads share the
media element) and, for a track with a postroll, fires before the postroll
plays — so use trackEnded (above) to detect that a track is actually done.
For example, to interrupt an automatic queue transition, one could write:
player.on('ended', () => {
if (shouldInterrupt) player.clearQueue()
})
Track Preloading/Prefetching Configuration
To change initial cache capacity or number of tracks prefetched, when
constructing the player, provide configuration to trackController.
Example:
import { createVinylPlayer } from '@amazon/vinyl'
createVinylPlayer(
{ media: new Audio() },
{
trackController: {
trackPrefetchCount: 3,
preloadCapacity: 5,
},
}
)
Increasing prefetch count can reduce playback delay when rapidly skipping tracks at the cost of increased memory and network usage.
Playback Control
Controlling playback should be done through the Amazon Vinyl player reference, not the media element. This is to ensure that controls are consistent across browsers and devices.
The basic playback operations are play, pause, and seekTo.
play() invokes play() on the media element with additional safety around
awaiting track loading. Unlike when using the media element, play and pause
may be invoked before the track has finished loading.
play may reject if interrupted from another track load, or if the media
element is not 'unlocked' by calling play in response to a user interaction.
User interactions include click, touch, tap, key, or voice events. Autoplay
policies are browser-dependent, and may not apply to all devices such as
televisions.
It is recommended to connect UI elements such as a play button to a synchronous
call to play(), and adding a rejection handler which may ignore play
rejections.
seekTo seeks the media to the given time. seekTo has additional safety over
setting currentTime on the media element directly. seekTo awaits track
seekable ranges, ensures seeking is to a seekable time range, and ensures rapid
seek operations resolves to the final time.
For full documentation on playback commands, see the API Docs for
PlaybackController.
Playback Events
When observing Amazon Vinyl events, use on for event registration.
Example:
const timeUpdateSub = player.on('timeUpdate', (event) => {
console.log(`currentTime is now ${player.currentTime}.`)
})
Invoke the returned Unsubscribe callback to remove the handler. If the player is disposed, the handlers will be cleared.
Read the API Docs on PlaybackControllerEventMap for the full list of playback
events. Most events are directly from the media element, but there are
additional second-order events such as 'played', 'waited', or 'mutedChange'.
Stall Detection
Vinyl detects when the play head freezes during playback (no timeUpdate for
longer than a threshold) and reports it as a stallEntered/stallEnded pair.
Unlike the media's waiting/waited events, a stall is only reported once
playback has actually been observed — never during initial loading or while
awaiting a seek — and stallEnded carries the full frozen duration.
player.on('stallEntered', () => {
// The play head has frozen mid-playback; e.g. show a buffering spinner.
})
player.on('stallEnded', (event) => {
// event.reason is one of 'playing' (resumed), 'pause', 'seeking', 'emptied'.
// event.duration is the number of seconds the play head was frozen,
// measured from the last timeUpdate before the freeze.
console.log(`stalled for ${event.duration}s, ended by ${event.reason}`)
})
The freeze threshold defaults to 1 second and is configurable via the
stallThreshold playback option.
Buffer Status
There are two concepts to understand when talking about how much data is
buffered: buffered data and fetched data. fetched data refers to data
streamed from the network. buffered data refers to data that has finished
decoding and decrypting. When indicating to the user how much data is loaded,
the fetched time ranges is the more relevant of the two.
To show an indicator for prefetch, use the fetchedRangesChange event and
fetchedTimePercent property.
function setPrefetched(percent: number) {
// Update UI
}
player.on('fetchedRangesChange', () => {
setPrefetched(player.fetchedTimePercent)
})
setPrefetched(0)
Quality Information
Amazon Vinyl provides access to media quality information for different content types (audio, video, text) through quality accessor methods. Quality metadata progresses through three stages:
- Streaming Quality: The quality being requested for streaming
- Buffering Quality: The quality currently being buffered/decoded
- Playback Quality: The quality currently being played back
Accessing Quality Information
// Get current content types (e.g., Set(['audio', 'video']))
const contentTypes = player.contentTypes
// Get quality for specific content type
const audioStreamingQuality = player.getStreamingQuality('audio')
const videoBufferingQuality = player.getBufferingQuality('video')
const audioPlaybackQuality = player.getPlaybackQuality('audio')
// Returns null if no quality available for the content type
const textQuality = player.getStreamingQuality('text') // null if no text track
Quality Change Events
Listen for quality changes across all content types:
player.on('contentTypesChange', (event) => {
console.log('Content types changed:', event.previous, '→', event.current)
})
player.on('streamingQualityChange', (event) => {
console.log(
'Streaming quality changed:',
event.previous,
'→',
event.current
)
})
player.on('bufferingQualityChange', (event) => {
console.log(
'Buffering quality changed:',
event.previous,
'→',
event.current
)
})
player.on('playbackQualityChange', (event) => {
console.log('Playback quality changed:', event.previous, '→', event.current)
})
Adaptive Bitrate Configuration
Adaptive bitrate behavior is configured through player.configure({ abr }) and
can be changed at any time; the timeline is re-evaluated on the next quality
selection.
Capping Bandwidth
Use abr.maxBandwidth to cap the maximum per-second bandwidth (in bits per
second) of any selectable quality. This is a soft cap: if no qualities fit
within the limit, the lowest-bandwidth quality is selected so playback remains
possible. Setting maxBandwidth to 0 therefore pins playback to the lowest
available quality.
maxBandwidth has no effect when strategy is AbrStrategy.LOWEST or
AbrStrategy.HIGHEST, since those strategies pin selection regardless of
bandwidth.
// Limit selectable qualities to 1.5 Mbps or less.
player.configure({
abr: {
maxBandwidth: 1_500_000,
},
})
// Pin to the lowest available quality.
player.configure({ abr: { maxBandwidth: 0 } })
// Remove the cap.
player.configure({ abr: { maxBandwidth: null } })
Sidecar Text Tracks
Vinyl discovers WebVTT subtitle tracks delivered alongside HLS or DASH content
("sidecar" subtitles) and exposes them through VinylPlayer.
Discovery
For HLS, the player reads EXT-X-MEDIA:TYPE=SUBTITLES renditions from the
multivariant playlist. The rendition's URI may point at a .vtt file directly
or at a media playlist (.m3u8) whose segments are individual .vtt files;
both forms are supported.
For DASH, the player reads <AdaptationSet contentType="text"> (or any
<AdaptationSet> with a text/* mimeType). Each <Representation>'s
BaseURL chain is resolved against the manifest URL to produce the absolute
text track URL. Segmented text codecs (stpp, wvtt) are not surfaced in v1.
Discovery happens automatically when a track loads — applications need only
listen for the textTracksChange event or read player.textTracks.
Selecting captions
Caption selection is declarative: it is driven entirely by the text option
(VinylOptions.text), so a choice persists across track changes (e.g. across an
ad break) and re-resolves against whatever the next source exposes. There is no
imperative setActiveTextTrack call.
type CaptionMode = 'on' | 'off' | 'forced'
interface TextTrackSelection {
id?: string | null // exact track id (wins over the other criteria)
kind?: TextTrackKind | null // restrict to a kind, e.g. 'captions'
language?: string | readonly string[] | null // preferred language(s)
forced?: boolean | null // explicit forced-ness filter (overrides the mode)
}
interface TextTrackControllerOptions {
enabled?: CaptionMode | null // default 'forced'
selection?: TextTrackSelection | null
}
enabledgates rendering:'off'shows nothing,'forced'shows only forced (narrative) tracks,'on'shows the full subtitle track. The default is'forced'.selection.languageunset (or empty) falls back to the platform'snavigator.languages.selection.forced, when set, is an explicit filter that overrides the forced/full split implied byenabled.
// Default (no config): forced captions in the platform's preferred languages.
// Full subtitles in a specific language:
player.configure({ text: { enabled: 'on', selection: { language: 'en' } } })
// Forced narrative captions only, for a language:
player.configure({ text: { enabled: 'forced', selection: { language: 'es' } } })
// A specific discovered track by id:
player.configure({ text: { enabled: 'on', selection: { id: someTrack.id } } })
// Turn captions off:
player.configure({ text: { enabled: 'off' } })
The player still exposes the discovered list, the active selection, and change events (read-only):
console.log(player.textTracks)
// → [{ id, kind: 'subtitles', language: 'en', label: 'English', default: true,
// forced: false, characteristics: [], uri: 'https://.../subs/en.vtt',
// mimeType: 'text/vtt' }, ...]
console.log(player.activeTextTrack)
player.on('textTracksChange', (event) => {
console.log('Text tracks updated:', event.current)
})
player.on('activeTextTrackChange', (event) => {
console.log('Active text track:', event.current)
})
player.on('textTrackError', (event) => {
console.warn('Failed to load track', event.track.label, event.error)
})
Forced subtitles
A track flagged forced carries essential text (e.g. translations of
foreign-language dialogue) that is meant to display even when the user has not
enabled full subtitles. A forced track shares its language with the full
track, so selection distinguishes them by the forced flag rather than language
alone. Media characteristics / accessibility roles are surfaced in
characteristics (HLS CHARACTERISTICS, DASH Role/Accessibility).
Because enabled defaults to 'forced', out of the box the player shows a
forced track for the platform's preferred languages when one is discovered, and
nothing otherwise.
Rendering
When a track is selected, Vinyl fetches the WebVTT, parses it, and adds the cues
to a TextTrack on the underlying media element via
HTMLMediaElement.addTextTrack. By default that track is 'showing' and the
browser renders the cues (styleable only through the limited ::cue
pseudo-element).
For full control over caption styling and placement, inject a
TextTrackRenderer — Vinyl ships HtmlTextTrackRenderer, which paints cues as
an HTML overlay you position yourself:
import { createVinylPlayer, HtmlTextTrackRenderer } from '@amazon/vinyl'
const captions = new HtmlTextTrackRenderer()
const player = createVinylPlayer({ media, textTrackRenderer: captions })
videoContainer.appendChild(captions.element) // position it over the video
With a renderer injected, the DOM TextTrack is kept 'hidden' (used only for
cue timing) and the renderer paints the active cues, honoring each cue's WebVTT
settings (position / line / size / align / vertical), cue payload tags
(<c.class>, <i>, <b>, <v>, <lang>, …), and STYLE-block ::cue
rules. Implement the TextTrackRenderer interface for a fully custom renderer.
Ad Breaks (Server-Guided Ad Insertion)
Vinyl surfaces server-guided ad breaks through a single, provider-agnostic API
on VinylPlayer. Today this is driven by HLS Interstitials (SGAI); the same API
is intended to also carry DASH SCTE-35 splices, so applications observe one
interface regardless of the streaming protocol.
Discovery
For HLS, the player reads EXT-X-DATERANGE tags with
CLASS="com.apple.hls.interstitial" from the media playlist. Each interstitial
is mapped to an abstract AdBreakInfo:
- Its wall-clock
START-DATEis converted to a media-timeline start time by correlating it with the playlist'sEXT-X-PROGRAM-DATE-TIMEanchor. - Its duration is resolved from
DURATION, else theSTART-DATE/END-DATEspan, elsePLANNED-DURATION. - An
X-ASSET-URIyields a single resolved ad; anX-ASSET-LISTyields a break whose assets are resolved asynchronously (emptyadsup front). - The break is classified as
preroll,midroll, orpostroll.
Discovery happens automatically when a track loads — applications need only
listen for the currentTrackAdsChange event or read player.currentTrackAds.
API
// Inspect the ad breaks known for the current media (ordered by start time).
console.log(player.currentTrackAds)
// → { trackUri, adBreaks: [{ id, startTime: 12, duration: 15,
// placement: 'midroll', restrict, once, resumeOffset,
// ads /* resolver → AdInfo[] */ }, ...] }
// Inspect the break currently containing the playhead, if any.
console.log(player.currentAdBreak)
// React to changes and boundary crossings.
player.on('currentTrackAdsChange', (event) => {
console.log('Ad breaks updated:', event.current)
})
player.on('adBreakEntered', ({ adBreak }) => {
console.log('Entered ad break', adBreak.id)
})
player.on('adBreakCompleted', ({ adBreak, resumePosition }) => {
console.log('Completed ad break', adBreak.id, '→ resume at', resumePosition)
})
adBreakEntered fires when the playhead enters a break; adBreakCompleted
fires when it finishes (all ads played, were skipped, or it had none), carrying
the resumePosition at which content resumes. Breaks with an unknown duration
surface in currentTrackAds but never mark the playhead as inside a break until
their span resolves. See ADS.md for the full ad event set.