Developer Guide Javascript API Reference Developer Tools Release Notes

Live Channels

This demo demonstrates how to use the new Live Channels APIs in order to replace a VOD asset with a live event stream whenever a Live Channel becomes available.

github
Waiting for a Live Channel to be specified.

Utilize JW Player's Live Channels as your live event streaming solution.

This demo demonstrates how to place a player on your page which will play a VOD asset in a loop until a live event stream associated with a particular Live Channel becomes available.

Usage

  1. Enter the ID of your Live Channel and click "Select Channel".
  2. Start broadcasting to your channel.

After hitting "Select Channel" the code on this page will automatically check for availability of a live event stream in the Live Channel in the background and replace the VOD asset in the player with the live event stream associated with the channel.

Visit our support site for a tutorial on live streaming a live event with JW Player, or contact us for more information on Live Channels.

const selectButton = document.getElementById('select-channel');
const channelInput = document.getElementById('channel-input');
const statusMessage = document.getElementById('status-message');

/** The player config to use in order to initialize the player */
const VOD_CONFIG = {
  playlist: 'https://cdn.jwplayer.com/v2/media/8L4m9FJB',
  // Repeat the VOD indefinitely while we wait for the live event stream to become available.
  repeat: true
};

/**
 * How often we should be checking for an update to the live status.
 * 10 seconds is generally a good frequency, it is not useful to check more often.
 */
const UPDATE_FREQUENCY = 10 * 1e3;

/**
 * The code of the error which the player may raise if the livestream goes offline or finishes.
 */
const LIVESTREAM_COMPLETE_ERROR = 230001;

/**
 * The code of the error emitted by JW Player's HLS implementation if buffering stalls.
 * This may happen when a livestream (suddenly) finishes and no more segments are available to buffer.
 * In this case we'll switch back to VOD playback.
 */
const HLS_BUFFER_STALL_WARNING = 334001;

/**
 * The maximum number of times we'll try before giving up configuring a player.
 * @type {number}
 */
const MAX_RETRIES = 5;

/** The player on the page which we'll use for playback */
const playerInstance = jwplayer('player').setup(VOD_CONFIG);

/** The identifier of the channel for which we should be fetching updates */
let channelId;

/** The identifier of the current/last played event. */
let currentEventId;

/** An id used by the setInterval()/clearInterval() functions in order to manage the update loop. */
let intervalId;

// Register an event listener for the selectButton.
selectButton.addEventListener('click', (event) => {
  event.preventDefault();
  if (intervalId) {
    // If an update loop was already running, stop it.
    intervalId = clearInterval(intervalId);
  }

  // Validate the provided channel id.
  channelId = channelInput.value;
  if (!channelId.match(/[a-zA-Z0-9]{8}/)) {
    alert("The provided Channel ID is not a valid Live Channels channel ID.");
    return;
  }

  // Start the update loop.
  checkChannelStatus();
});

// Register an event listener that triggers when the JW Player has finished playing all
// elements in its playlist. In this demo, this event is triggered when livestream playback
// has finished.
playerInstance.on('playlistComplete', handleLivestreamFinished);

// Register an event listener that triggers when the player emits an error.
playerInstance.on('error', (error) => {
  // Check if the error may have been because the livestream stopped updating, in this case
  // we'll switch back to playing the VOD.
  if (playerInstance.getPlaylistItem().mediaid !== currentEventId) {
    // Ignore errors during VOD playback.
    return;
  }
  if (error.code === LIVESTREAM_COMPLETE_ERROR) {
    handleLivestreamFinished();
  }
});

// Register an event listener which listens for buffer warnings from the player.
// We can use the warnings generated by the player to realize a very fast switchover
// between the livestream and the VOD asset.
playerInstance.on('warning', (warn) => {
  if (playerInstance.getPlaylistItem().mediaid !== currentEventId) {
    // Ignore warnings during VOD playback.
    return;
  }
  if (warn.code === HLS_BUFFER_STALL_WARNING) {
    // The player failed to buffer more media.
    // This *may* be an indicator that the livestream has finished - in this demo we'll switch back to attempting to play
    // the VOD asset if this is the case.
    handleLivestreamFinished();
  }
});

function handleLivestreamFinished() {
  if (intervalId) {
    // We are already checking for a livestream.
    // In this state there should not be a reason to re-initialize the player -- it should already be in the correct
    // state.
    return;
  }
  console.log('Detected livestream completion. Switching to VOD playback.');

  // Enable looping of media.
  playerInstance.setConfig({repeat: true});
  // Reload the VOD playlist.
  playerInstance.load(VOD_CONFIG.playlist);
  if (channelId) {
    // Start checking for a new event.
    checkChannelStatus();
  }
  playerInstance.play();
}

/**
 * Periodically checks whether the specified livestream channel is available, and if it is, configures the player
 * to start playing it.
 */
function checkChannelStatus() {
  if (!intervalId) {
    statusMessage.textContent = `Waiting for Live Channel ${channelId} to become active.`;
    // Make sure to execute this method every UPDATE_FREQUENCY milliseconds.
    intervalId = setInterval(checkChannelStatus, UPDATE_FREQUENCY);
  }
  getChannelStatus(channelId).then((channelStatus) => {
    console.log(`Received channel status: %O`, channelStatus);
    if (channelStatus['status'] === 'active') {
      // Determine the id of the active event based on the returned status.
      const eventId = channelStatus['current_event'];

      // Check if we have seen this eventId before.
      if (currentEventId === eventId) {
        // The eventId returned by the API was not a *new* event id.
        // Ignore it and continue polling until we see a new id.
        return;
      }
      currentEventId = eventId;

      // Stop polling the channel status.
      intervalId = clearInterval(intervalId);

      // Attempt to configure the player in order to start livestream playback.
      configurePlayer(eventId).catch((error) => {
        statusMessage.textContent = `Failed to start live event stream playback: ${error}`;
      });
    }
  }, (error) => {
    statusMessage.textContent = `Unable to fetch the channel status for ${channelId}: ${error}`;
    // If we fail to retrieve the channel status, then give up.
    intervalId = clearInterval(intervalId);
  });
}

/**
 * (Re-)configures the active playerInstance to play the livestream identified by eventId.
 */
async function configurePlayer(eventId) {
  // There may be a slight delay between the livestream becoming available, and its playlist to become available.
  // Therefore, we first attempt to fetch the playlist for the new live event, as soon as we have successfully fetched
  // a playlist, we will load it on the player and start playback of the livestream.
  let playlist;
  let attempts = 0;
  statusMessage.textContent = `Fetching playlist for ${eventId}.`;
  while (!playlist) {
    try {
      playlist = await getPlaylist(eventId);
    } catch (e) {
      ++attempts;
      console.error(e);
      if (attempts >= MAX_RETRIES) {
        throw e;
      }
      // Retry with exponential backoff, i.e. first retry after 5, 10, 20, 40, 80 seconds
      // after which we ultimately give up.
      await sleep(2 ** (attempts - 1) * 5 * 1000);
    }
  }

  // Once a playlist is available, use it to configure the player.
  playerInstance.setConfig({
    repeat: false,
  });
  playerInstance.load(playlist.playlist);
  // Start playback
  playerInstance.play();
  statusMessage.textContent = `Playing live event stream with id '${eventId}'.`;
}

/**
 * Utility function to fetch a JSON document.
 * @param url
 */
async function fetchJSON(url) {
  return await fetch(url)
    .then((response) => {
      if (!response.ok) {
        throw new Error(`Unable to fetch ${url}: ${response.statusText}`);
      }
      return response.json();
    });
}

/**
 * Fetches the current status of a Live Channel.
 * Returns a promise that will yield the status for a particular channel.
 *
 * @param channelId The channel to fetch the status for.
 */
function getChannelStatus(channelId) {
  return fetchJSON(`https://cdn.jwplayer.com/live/channels/${channelId}.json`);
}

/**
 * Fetches a JW Platform feed for a particular media item.
 *
 * @param mediaId The media id to fetch a single item playlist for.
 */
function getPlaylist(mediaId) {
  return fetchJSON(`https://cdn.jwplayer.com/v2/media/${mediaId}`);
}

/**
 * A simple utility method which can be used to wait for some time between retries.
 *
 * @param ms The amount of milliseconds to wait between retries.
 */
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}