Skip to content

Spins

The Spins package in Wowza Flowplayer is a lightweight, TypeScript-based package designed to create short-form video experiences using Wowza Video playlists or inline arrays of video items. It provides a vertical video player component, optimized for formats like TikTok, Instagram Reels, and YouTube Shorts. This package automatically handles playlist loading and provides the structure needed for smooth, mobile-optimized video playback.

info

Spins is currently in beta. Features, APIs, and behaviors may change as development continues. We encourage testing in staging environments and welcome your feedback.

Before you start

Before you start working with the Spins package, we make the following assumptions and recommendations:

  • You're familiar with the ECMAScript Module (ESM) approach of handling JavaScript modules.
  • You have Node.js and Node Package Manager (npm) installed on your machine.
  • Review the Token configuration section. To authorize the player, you need to configure it with your token.
  • Ensure you have a Wowza Video subscription if you want to configure playlists in Wowza Video and use the playlist ID to configure the Spins package. For more, see get started with a Wowza Video trial.

Install Spins

The @flowplayer/spins package is available for download via npm (recommended), yarn, or as an ES (ECMAScript) module. It automatically includes all minimum player components and CSS to work with the Wowza Flowplayer.

// Install the spins package from npm
npm install @flowplayer/spins

// Import the package for use
import { createSpins, spinEvents } from "@flowplayer/spins";
yarn add @flowplayer/spins
bun add @flowplayer/spins
<script type="module">
    import {createSpins, spinEvents} from "https://player.wowza.com/spins/stable/index.js"
</script>

Configure Spins

To use the Spins package, configure it with the createSpins() method. There are two ways to provide video content to Spins:

  • Using a Wowza Video playlist ID – Use the playlist ID of an existing playlist from your Wowza Video account, and Spins will automatically load and display the content. For more information on how to create a playlist and get a playlist ID, see Overview of playlists and Playlist details page in Wowza Video.

  • Using an array of SpinItem objects – Use this method to manually define each video clip, including source URL, title, poster image, description, and plugins.

PropertyDescription
playlist
string | SpinItem[]
Defines a Wowza Video playlist ID (composite media ID) or an array of SpinItem objects.
lang
string
Optional
Sets the language of the player UI and messages. For example, en for English.
token
string
Optional
Adds a Wowza Flowplayer token required to license and use the player.
ui
Optional
Sets UI configuration for customizing player appearance and behavior.
ima
Optional
Configures the Ads plugin for use with the spins container. For configuration details, see the Ads plugin.
share
Optional
Configures the Share plugin for use with the spins container. For configuration details, see the Share plugin.
consent
Optional
Configures the Consent plugin for use with the spins container. For configuration details, see the Consent plugin.
plugins
Optional
Sets plugins to include with the player. Accepted values: "subtitles", "vtsel", "ads", "share", "thumbnails", "asel", "qsel". Default (if omitted): all of the above except "qsel".
adsFrequency
Optional
Displays ads at the specified spin interval. For example, a value of 3 displays ads on the 3rd, 6th, 9th, and subsequent spins. The default frequency is 10, meaning ads display every 10th spin. This property requires you to set the ima property. If ima is not set, ads are not displayed. If ima is set but adsFrequency is not, ads default to every 10th spin. Ignored if adPositions is set. When firstAdIndex is also set, counting starts from that spin instead of from adsFrequency itself.
firstAdIndex
number
Optional
The index of the first spin an ad should be shown on. Used alone, only a single ad is shown at this index. Combined with adsFrequency, it sets the starting point the frequency counts from — for example, firstAdIndex: 5 with adsFrequency: 3 shows ads on the 5th, 8th, 11th, etc. spin. Ignored if adPositions is set.
adPositions
number[]
Optional
An explicit list of spin indexes to show an ad on (e.g. [2, 7, 11]), for fully custom, non-uniform placement. When set, it overrides both adsFrequency and firstAdIndex.
closeButton
boolean
Optional
When true, renders a close button overlaid on the container. Clicking it dispatches a spins:close event (spinEvents.CLOSE) on the container. The library does not remove the container — the implementor must listen for the event and handle teardown. Default: false.
articleLabel
string
Optional
The label rendered on the link pill for any spin that has an article_url (see SpinItem properties below). Applies to the whole feed. Default: "Read more".

SpinItem properties

Each SpinItem represents a single short-form video and includes properties such as the video URL, title, poster image, and subtitles.

PropertyDescription
url
string
Defines the video source URL or media ID from Wowza Video for the video.
title
string
Optional
Defines a title for the video.
poster
string
Optional
Sets a poster image shown before the video plays.
description
string
Optional
Defines a short description of the video content.
article_url
string
Optional
An absolute http/https URL linking to an article or external resource related to this spin. Rendered as a link pill above the player controls, using the feed-level articleLabel as its text, and opens in a new tab. Only absolute http/https URLs are accepted.
subtitles
Optional
Adds subtitles to the video.

Example

The following examples use the properties from the previous table to import the Spins package from npm and to configure it with a Wowza Video playlist ID or with an array of customized spin items. You can also see how event listeners are used to manage the spins container.

import { createSpins, spinEvents } from "@flowplayer/spins";

// Use a playlist ID from Wowza Video
const playlistID = "c30850bc-eeeb-421d-9ed3-158339d0851a";

// Create Spins using playlist ID
const container = createSpins({
    token: "[your-player-token]",
    playlist: playlistID,
    plugins: ["subtitles"]
});

// Listen for the SPIN_IN_VIEWPORT event
container.on(spinEvents.SPIN_IN_VIEWPORT, (ev) => {
    const { config, spin, index } = ev.detail;
    console.log("spin_in_viewport_data: ", config, spin, index);
    // Add your code here
});

// Append the container to the DOM
document.body.append(container);
import { createSpins, spinEvents } from "@flowplayer/spins";

// Manually define an array of spin items
const spinItemArray = [
    {
       // Add media ID for a Wowza Video asset
        url: "38a31588-07a0-49bf-ba55-fa0310e13f15"
    },
    {
        url: "https://cf23f0bd0.lwcdn.com/hls/9eda7730-23e9-4462-9ad4-c53e8004c3e9/playlist.m3u8",
        title: "A title",
        description: "A description",
        poster: "//cf23f0bd0.lwcdn.com/i/v-i-9eda7730-23e9-4462-9ad4-c53e8004c3e9-1.jpg"
    },
    {
        url: "https://stdlwcdn.lwcdn.com/hls/2cd21de6-1586-428b-bf7f-acc3fdcfd697/playlist.m3u8",
        poster: "//stdlwcdn.lwcdn.com/i/v-i-67059b8d-ce1a-457e-9af1-d927ff945826-1.jpg",
        title: "Test - missing mp4 01/09/2025 14:57"
    },
    {
        url: "https://cdn.flowplayer.com/d9cd469f-14fc-4b7b-a7f6-ccbfa755dcb8/hls/383f752a-cbd1-4691-a73f-a4e583391b3d/playlist.m3u8?t=1",
        subtitles: {
            tracks: [
                {
                    src: "https://builds.flowplayer.com/samples/en.vtt",
                    lang: "en",
                    label: "English"
                },
                {
                    src: "https://builds.flowplayer.com/samples/pt.vtt",
                    lang: "pt",
                    label: "Portuguese"
                }
            ]
        }
    }
];

// Create Spins using spin item array
const container = createSpins({
    playlist: spinItemArray,
    plugins: ["subtitles"]
});

// Listen for the SPIN_IN_VIEWPORT event
container.on(spinEvents.SPIN_IN_VIEWPORT, (ev) => {
    const { config, spin, index } = ev.detail;
    console.log("spin_in_viewport_data: ", config, spin, index);
    // Add your code here
});

// Append the container to the DOM
document.body.append(container);

Listen to events

This section describes the events you can capture to control and manage your spins container.

Spin events

EventDescription
SPIN_CREATEDEmitted when a new spin is created
SPIN_IN_VIEWPORTEmitted when a spin becomes visible in the view
CLOSEEmitted when the user clicks the close button (requires closeButton: true in the config). Carries no event detail — the library does not remove the container; the implementor is responsible for teardown.

Media events

EventDescription
CAN_PLAYEmitted when the browser can start playing the media without stopping for buffering.
TIME_UPDATEEmitted when the playback position changes (e.g., during playback or scrubbing).
PLAYINGEmitted when playback is actively progressing after being paused or buffering.
WAITINGEmitted when playback is delayed while the next frame is being loaded (buffering).
PAUSEEmitted when playback has been paused by the user or programmatically.
PLAYEmitted when playback has been started or resumed.
SEEKINGEmitted when the user begins seeking to a new playback position.
SEEKEDEmitted when a seek operation completed.
ENDEDEmitted when playback has reached the end of the media resource.

Ad events

EventDescription
AD_TEARDOWNEmitted when the ad teardown/cleanup process occurs.
AD_COMPLETEDEmitted when a single ad has finished playing.
AD_REQUEST_ERROREmitted when an ad request fails due to a network or server error.
AD_STARTEDEmitted when an ad starts playing.
AD_ERRORInternal or general ad error event.
AD_PLAYBACK_ERROREmitted when an ad request succeeds but the ad fails to play.
AD_BREAK_COMPLETEDEmitted when an ad break (group of ads) has completed.
AD_PROGRESSEmitted periodically as the ad progresses during playback.
AD_PAUSEDEmitted when an ad is paused.
AD_RESUMEDEmitted when an ad resumes after being paused.
AD_PREROLL_FINISHEDEmitted when pre-roll or post-roll playback is finished.
AD_SKIPPEDEmitted when the skip button is clicked and the ad is skipped.
AD_MUTEDEmitted when the ad has been muted.
AD_VOLUME_CHANGEDEmitted when the ad volume changes during playback.

SpinsContainer methods

MethodDescription
on(event: string, handler: (ev: CustomEvent) => void)Subscribes to an event (e.g., SPIN_CREATED, SPIN_IN_VIEWPORT).
off(event: string, handler: (ev: CustomEvent) => void)Removes a previously added event listener.
addSpins(spinItems: SpinItem[])Adds new spins to the container dynamically. Accepts an array of SpinItem objects.
onPlayerEvent(event: string, handler: (ev: CustomEvent) => void)Adds a listener for any player event, including mediaEvents and adEvents.
offPlayerEvent(event: string, handler: (ev: CustomEvent) => void)Removes a listener for any player event added with onPlayerEvent().

Example

This code initializes a Spins player instance using the createSpins function with a specified playlist ID, access token, and language. It also sets up a few event listeners:

import {
  createSpins,
  spinEvents,
  mediaEvents,
  adEvents
} from "@flowplayer/spins";

const container = createSpins({
  playlist: "[your-playlist-id]",
  token: "[your-player-token]",
  lang: "en",
  adsFrequency: 5,
  ima: {
    ads: [
      { adTag: "https://pubads.g.doubleclick.net/gampad/ads?...", time: 0 }
    ]
  }
});

// Spin lifecycle events
container.on(spinEvents.SPIN_CREATED, (ev) => {
  const { config, spin, index } = ev.detail;
  console.log("New spin created:", spin);
});

container.on(spinEvents.SPIN_IN_VIEWPORT, (ev) => {
  const { config, spin, index } = ev.detail;
  console.log("Spin is now in viewport:", spin);
});

// Media events
container.onPlayerEvent(mediaEvents.TIME_UPDATE, (ev) => {
  const { spinIndex, duration, currentTime } = ev.detail;
  console.log(`Progress: ${currentTime}/${duration}`);
});

// Ad events
container.onPlayerEvent(adEvents.AD_STARTED, (ev) => {
  console.log(`Ad started at spin ${ev.detail.spinIndex}`);
});

document.body.appendChild(container);

Advanced usage

Configure with React

Spins also works seamlessly with React. Here's how you can embed it inside a React component:

import { createSpins } from "@flowplayer/spins";
import { useEffect, useRef } from "react";

function SpinsPlayer({ playlistId }: { playlistId: string }) {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const spinsContainer = createSpins({
      playlist: playlistId,
      token: "[your-player-token]",
      lang: "en"
    });
    containerRef.current.appendChild(spinsContainer);

    return () => {
      if (containerRef.current.contains(spinsContainer)) {
        containerRef.current.removeChild(spinsContainer);
      }
    };
  }, [playlistId]);

  return <div ref={containerRef} className="spins-container" />;
}

Article links

Each spin can display a link pill (e.g. "Read more") that opens an associated article. Set article_url per item and optionally override the label feed-wide with articleLabel.

const container = createSpins({
  playlist: [
    {
      url: "https://example.com/video1.mp4",
      title: "Breaking news",
      article_url: "https://example.com/article-1",
    },
    {
      url: "https://example.com/video2.mp4",
      title: "Follow-up story",
      article_url: "https://example.com/article-2",
    },
  ],
  token: "[your-player-token]",
  articleLabel: "Go to article", // default: "Read more"
});

Only absolute http/https URLs are accepted. The link renders as a styled pill button above the player controls and opens in a new tab. You can restyle it via the .fp-spin-link CSS class.

Overlay with close button

The closeButton option is designed for overlay patterns where the container covers part or all of the screen. When clicked, it dispatches a spins:close event on the container — the library never removes itself, leaving teardown to the implementor.

import { createSpins, spinEvents } from "@flowplayer/spins";

const container = createSpins({
  playlist: "[your-playlist-id]",
  token: "[your-player-token]",
  closeButton: true,
});

container.on(spinEvents.CLOSE, () => {
  container.remove();
});

document.body.appendChild(container);

Custom ad placement

Use firstAdIndex to change where the first ad appears, optionally combined with adsFrequency to set the cadence from that point on:

const container = createSpins({
  playlist: "[your-playlist-id]",
  token: "[your-player-token]",
  firstAdIndex: 5,   // first ad on the 5th spin
  adsFrequency: 3,   // then every 3rd spin after: 5, 8, 11, ...
  ima: {
    ads: [{
      adTag: "https://pubads.g.doubleclick.net/gampad/ads?...",
      time: 0
    }]
  }
});

For fully custom, non-uniform placement, use adPositions instead — it overrides adsFrequency/firstAdIndex:

const container = createSpins({
  playlist: "[your-playlist-id]",
  token: "[your-player-token]",
  adPositions: [2, 7, 11],  // ads only on these spins
  ima: {
    ads: [{
      adTag: "https://pubads.g.doubleclick.net/gampad/ads?...",
      time: 0
    }]
  }
});