Shaka Upgrade Guide, v1.x => v2

This is a detailed guide for upgrading from Shaka Player v1 to v2. It is a bit long to read from beginning to end, so feel free to skim or to search for the class and method names you are using in your application.

What's New in v2?

Shaka v2 has several improvements over v1, including:

  • Support for multiple DASH Periods
  • Support for DASH Location elements
  • Support for DASH UTCTiming elements for clock synchronization
  • Lower-latency startup
  • Simplified API
  • Better browser compatibility
  • More detailed browser support test
  • Numerical error code system
  • Clears old data from the buffer to conserve memory
  • Buffering state is independent of play/pause
  • Distinguishes between subtitle and caption tracks
  • Separate audio & text language preferences
  • New plugin and build system to extend Shaka
  • Cache-friendly networking
  • Simpler, mobile-friendly demo app
  • HLS support (VOD, Event, and Live)
  • DASH trick mode support
  • Support for jumping gaps in the timeline
  • Additional stats and events from Player
  • Indication of critical errors vs recoverable errors
  • Allowing applications to render their own text tracks
  • Making the default ABR manager more configurable
  • Adding channel count and bandwidth info to variant tracks
  • Xlink support in DASH
  • New option for offline protected content without persistent licensing
  • MPEG-2 TS content can be transmuxed to MP4 for playback on all browsers
  • Captions are not streamed until they are shown
  • Use NetworkInformation API to get initial bandwidth estimate
  • The demo app is now a Progressive Web App (PWA) and can be used offline
  • Support for CEA captions in TS content
  • Support for TTML and VTT regions
  • A video element is no longer required when Player is constructed
  • New attach() and detach() methods have been added to Player to manage attachment to video elements
  • Fetch is now preferred over XHR when available
  • Network requests are now abortable
  • Live stream playback can begin at a negative offset from the live edge

Shaka Plugins

Shaka v2 has a new, cleaner architecture than v1 based on plugins. In v2, networking, manifest parsing, and subtitle/caption parsing are all plugins.

We bundle some default plugins (HTTP support, DASH support, and WebVTT), and we plan to expand this list in future releases. Application developers can write their own plugins as well. Plugins can either be compiled into the library, or they can live outside the library in the application. Application developers can also customize the build to exclude any default plugins they don't need.

For a more in-depth discussion of plugins, check out Plugins and Customizing the Build.

Namespace

In v1, the Player class was namespaced as shaka.player.Player. In v2, this has been simplified to shaka.Player.

load()

Before, you needed a DashVideoSource or other IVideoSource subclass to pass to player.load(). The video source was constructed with a manifest URL:

// v1:
var player = new shaka.player.Player(video);
var videoSource = new shaka.player.DashVideoSource(manifestUri);
player.load(videoSource);

In v2, the entire video source concept is gone from the API. Now, you pass the URL directly to the player, and it decides which manifest parser plugin to use based on the file extension or MIME type:

// v2:
var player = new shaka.Player(video);
player.load(manifestUri);

ContentProtection callbacks

Shaka v1's DashVideoSource had a parameter for a ContentProtection callback. This callback was required to play protected content because Shaka did not interpret ContentProtection elements in the DASH manifest and could not derive the license server URI automatically:

// v1:
function interpretContentProtection(schemeIdUri, contentProtectionElement) {
  if (schemeIdUri.toLowerCase() ==
      'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed') {
    // This is the UUID which represents Widevine.
    return [{
      'keySystem': 'com.widevine.alpha',
      'licenseServerUrl': 'https://proxy.uat.widevine.com/proxy'
    }];
  } else if (schemeIdUri.toLowerCase() ==
      'urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95') {
    // This is the UUID which represents PlayReady.
    return [{
      'keySystem': 'com.microsoft.playready',
      'licenseServerUrl':
          'https://playready.directtaps.net/pr/svc/rightsmanager.asmx'
    }];
  } else {
    return null;
  }
}
var player = new shaka.player.Player(video);
var videoSource = new shaka.player.DashVideoSource(
    manifestUri, interpretContentProtection);
player.load(videoSource);

In v2, these callbacks are only required for non-standard ContentProtection schemes, such as that used by YouTube's demo assets. For the 99% of you who are using standard schemes, no callback is required. Simply configure() the player with your license servers:

// v2:
var player = new shaka.Player(video);
player.configure({
  drm: {
    servers: {
      'com.widevine.alpha': 'https://proxy.uat.widevine.com/proxy'
      'com.microsoft.playready':
          'https://playready.directtaps.net/pr/svc/rightsmanager.asmx'
    }
  }
});
player.load(manifestUri);

For a more in-depth discussion of DRM configuration, see DRM Configuration.

If you need to support a custom ContentProtection scheme, you can still do so with a callback set through player.configure():

// v2:
function interpretContentProtection(contentProtectionElement) {
  if (contentProtectionElement.getAttribute('schemeIdUri') ==
      'http://youtube.com/drm/2012/10/10') {
    var configs = [];
    for (....) {
      configs.push({
        'keySystem': keySystem,
        // WATCH OUT: now called URI not URL
        'licenseServerUri': licenseServerUri
      });
    }
    return configs;
  }
}

var player = new shaka.Player(video);
player.configure({
  manifest: {
    dash: {
      customScheme: interpretContentProtection
    }
  }
});
player.load(manifestUri);

For more on what you can specify for a custom scheme, see the docs for shaka.extern.DrmInfo.

Detailed DrmInfo

Shaka v1's ContentProtection callbacks could return a detailed DrmInfo object with lots of EME-related and license-request-related settings:

// v1:
function interpretContentProtection(schemeIdUri, contentProtectionElement) {
  if (schemeIdUri.toLowerCase() ==
      'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed') {
    return [{
      'keySystem': 'com.widevine.alpha',
      'licenseServerUrl': 'https://proxy.uat.widevine.com/proxy',

      'distinctiveIdentifierRequired': true,
      'persistentStateRequired': false,
      'serverCertificate': certificateUint8Array,
      'audioRobustness': 'HW_SECURE_ALL',
      'videoRobustness': 'HW_SECURE_ALL',
      'initData': { 'initDataType': 'cenc', 'initData': initDataUint8Array },

      'licensePreProcessor': licensePreProcessor,
      'licensePostProcessor': licensePostProcessor,
      'withCredentials': true
    }];
  } else {
    return null;
  }
}
function licensePreProcessor(requestInfo) {
  // Only called for license requests.

  // Wrap the body, which is an ArrayBuffer:
  var newBody = wrapLicenseRequest(requestInfo.body);
  requestInfo.body = newBody;

  // Add a header:
  requestInfo.headers['foo'] = 'bar';
}
function licensePostProcessor(license) {
  // Only called for license responses.
  // Unwrap the license, which is a Uint8Array, use/store the extra data:
  var rawLicense = unwrapLicense(license);
  // Now return the raw license, which is also a Uint8Array:
  return rawLicense;
}

In v2, the EME settings have moved to the drm.advanced field of the config object:

// v2:
player.configure({
  drm: {
    advanced: {
      'com.widevine.alpha': {
        distinctiveIdentifierRequired: true,
        persistentStateRequired: true,
        serverCertificate: certificateUint8Array,
        audioRobustness: 'HW_SECURE_ALL',
        videoRobustness: 'HW_SECURE_ALL',
        // NOTE: initData is now an array of one or more overrides:
        initData: [{ initDataType: 'cenc', initData: initDataUint8Array }]
      }
    }
  }
});

For a discussion of advanced DRM configuration, see DRM Configuration.

Shaka v1's license-request-related settings have moved to v2's network filters.

Network filters are a generic filtering system for all networking, including license requests and responses. They are more general and flexible, so they take slightly more effort than the old preprocessor/postprocessor system. However, in v2, you only need to filter your license traffic for two reasons:

  • if you use cross-site credentials (v1's "withCredentials" flag)
  • if you wrap/unwrap license requests and responses into some other format
// v2:
player.getNetworkingEngine().registerRequestFilter(licensePreProcessor);
player.getNetworkingEngine().registerResponseFilter(licensePostProcessor);

function licensePreProcessor(type, request) {
  // A generic filter for all requests, so filter on type LICENSE:
  if (type != shaka.net.NetworkingEngine.RequestType.LICENSE) return;

  // Equivalent to v1's 'withCredentials': true
  request.allowCrossSiteCredentials = true;

  // Wrap the request data, which is an ArrayBuffer:
  var newData = wrapLicenseRequest(request.data);
  request.data = newData;

  // Add a header:
  request.headers['foo'] = 'bar';
}
function licensePostProcessor(type, response) {
  // A generic filter for all responses, so filter on type LICENSE:
  if (type != shaka.net.NetworkingEngine.RequestType.LICENSE) return;

  // Unwrap the response, which is an ArrayBuffer, use/store the extra data:
  var rawLicense = unwrapLicense(response.data);
  // Instead of returning the raw license, store it back to the response:
  response.data = rawLicense;
  // Return nothing.
}

For more on request filters, see the docs for shaka.net.NetworkingEngine.RequestFilter, shaka.extern.Request, shaka.net.NetworkingEngine.ResponseFilter, shaka.extern.Response.

ClearKey configuration

Shaka v1's ContentProtection callbacks could be used for ClearKey, but it required you to craft both a data URI and fake init data in the correct format:

// v1:
function interpretContentProtection(schemeIdUri, contentProtectionElement) {
  var keyid;  // as Uint8Array
  var key;  // as Uint8Array
  var keyObj = {
    kty: 'oct',
    kid: Uint8ArrayUtils.toBase64(keyid, false),
    k: Uint8ArrayUtils.toBase64(key, false)
  };
  var jwkSet = {keys: [keyObj]};
  var license = JSON.stringify(jwkSet);
  var initData = {
    'initData': keyid,
    'initDataType': 'webm'
  };
  var licenseServerUrl = 'data:application/json;base64,' +
      window.btoa(license);
  return [{
    'keySystem': 'org.w3.clearkey',
    'licenseServerUrl': licenseServerUrl,
    'initData': initData
  }];
}

In v2, this has been simplified through player.configure():

// v2:
player.configure({
  drm: {
    clearKeys: {
      'deadbeefdeadbeefdeadbeefdeadbeef': '18675309186753091867530918675309',
      '02030507011013017019023029031037': '03050701302303204201080425098033'
    }
  }
});

For more on ClearKey setup, see DRM Configuration.

BandwidthEstimator and AbrManager

Shaka v1's DashVideoSource had parameters for applications to inject custom BandwidthEstimator and AbrManager implementations. We even recommended injecting BandwithEstimator to get persisted estimates across playbacks:

// v1:
var player = new shaka.player.Player(video);
var bandwidthEstimator = new shaka.util.EWMABandwidthEstimator();
var abrManager = new shaka.media.SimpleAbrManager();
var videoSource = new shaka.player.DashVideoSource(
    manifestUri, /* interpretContentProtection */ null, estimator, abrManager);
player.load(videoSource);

In v2, we rolled the BandwidthEstimator concept into AbrManager. It is no longer necessary to inject an instance to persist estimates across playbacks, and custom AbrManagers are now provided via player.configure():

// v2:
var player = new shaka.Player(video);
player.configure({
  abrFactory: MyCustomAbrManager
});
player.load(manifestUri);

For more on the AbrManager interface, see the docs for shaka.extern.AbrManager.

Selecting tracks

Shaka v1 had separate methods for each type of tracks: getVideoTracks(), getAudioTracks(), and getTextTracks(), as well as selectVideoTrack(), selectAudioTrack(), and selectTextTrack(). Tracks were selected by ID.

// v1:
var videoTracks = player.getVideoTracks();
var i = /* choose an index somehow */;
player.selectVideoTrack(videoTracks[i].id);  // id, specifically video

In Shaka v2, audio and video tracks are combined into a variant track. It is not possible to select individual audio/video streams, you can only select a specific variant as specified by the manifest.

You can get the currently available tracks using getVariantTracks() and getTextTracks(). To switch tracks, use selectVariantTrack() and selectTextTrack(), passing in the whole track object.

// v2:
var variantTracks = player.getVariantTracks();
var i = /* choose an index somehow */;
player.selectVariantTrack(variantTracks[i]);  // whole track

In v1, you could show or hide text tracks with player.enableTextTrack():

// v1:
player.enableTextTrack(true);

In v2, this becomes player.setTextTrackVisibility():

// v2:
player.setTextTrackVisibility(true);

See also the shaka.extern.Track structure which is used for all track types (variant and text).

Side-loading captions/subtitles

In Shaka v1, you could side-load subtitles that were not present in the manifest by calling addExternalCaptions() on the DashVideoSource before load():

// v1:
var player = new shaka.player.Player(video);
var videoSource = new shaka.player.DashVideoSource(manifestUri);
videoSource.addExternalCaptions(textStreamUri, 'fr-CA', 'text/vtt');
player.load(videoSource);

In v2, this is done on with player.addTextTrack() after load() is complete:

// v2:
var player = new shaka.Player(video);
player.load(manifestUri).then(function() {
  player.addTextTrack(textStreamUri, 'fr-CA', 'caption', 'text/vtt');
});

Playback start time

Shaka v1's player.setPlaybackStartTime() would let you start playback at an arbitrary timestamp. It had to be called before load():

// v1:
player.setPlaybackStartTime(123.45);
player.load(manifestUri);

In v2, this is done through an optional parameter on load():

// v2:
player.load(manifestUri, 123.45);

Trick play

Shaka v1 had player.setPlaybackRate() that could be used for trick play by emulating negative rate support in video.playbackRate. If you used v1's setPlaybackRate() for trick play, use v2's player.trickPlay(). For other purposes, use video.playbackRate directly.

configure()

Shaka v1 and v2 both have a player.configure() method. Here is a map of settings in v1 and their equivalents in v2 (most of which are at a different level of the configuration hierarchy):

  • enableAdaptation => abr.enabled
  • streamBufferSize => streaming.bufferingGoal
  • licenseRequestTimeout => drm.retryParameters.timeout
  • mpdRequestTimeout => manifest.retryParameters.timeout
  • segmentRequestTimeout => streaming.retryParameters.timeout
  • preferredLanguage => split into preferredAudioLanguage and preferredTextLanguage
  • restrictions => (same name, see below)
  • liveStreamEndTimeout => (not needed in v2)
  • disableCacheBustingEvenThoughItMayAffectBandwidthEstimation => (not needed, always cache-friendly)

The shaka.player.Restriction type was replaced by a simple record type. So instead of constructing an object, simply create an anonymous JavaScript object. minPixels/maxPixels were added to limit total pixels. Also minBandwidth and maxBandwidth were split into minAudioBandwidth, maxAudioBandwidth, minVideoBandwidth, and maxVideoBandwidth, see shaka.extern.Restrictions.

For more information on configuration in v2, see Configuration, Network and Buffering Configuration, and DRM Configuration.

getStats()

Shaka v1 had player.getStats(). Shaka v2 has a similar method, but it returns a somewhat different structure.

// v1:
player.getStats()

=> Object
  streamStats: StreamStats  // refers to currently selected video stream
    videoWidth: number  // pixels
    videoHeight: number  // pixels
    videoMimeType: string
    videoBandwidth: number  // bits/sec
  decodedFrames: number
  droppedFrames: number
  estimatedBandwidth: number  // bits/sec
  playTime: number  // seconds
  bufferingTime: number  // seconds
  playbackLatency: number  // seconds
  bufferingHistory: Array  // of timestamps when we started buffering
  bandwidthHistory: Array of Objects
    timestamp: number  // seconds, when bandwidth estimate was made
    value: number  // bandwidth estimate, bits/sec
  streamHistory: Array of Objects
    timestamp: number  // seconds, when video stream changed
    value: StreamStats  // information about the selected stream, video only

Shaka v2 does not expose playback latency or a history of bandwidth estimates. v2's switchHistory is more general than v1's streamHistory, and covers all stream types:

// v2:
player.getStats()

=> Object
  width: number // pixels, current video track
  height: number  // pixels, current video track
  streamBandwidth: number  // bits/sec, total for all current streams
  decodedFrames: number  // same as v1
  droppedFrames: number  // same as v1
  estimatedBandwidth: number  // bits/sec, same as v1
  loadLatency: number,  // seconds between load() and the video's loadend event
  playTime: number  // seconds, same as v1
  bufferingTime: number  // seconds, same as v1
  switchHistory: Array of Objects  // replaces v1's streamHistory
    timestamp: number  // seconds, when the stream was selected
    id: number  // track ID
    type: string  // 'variant' or 'text'
    fromAdaptation: boolean  // distinguishes between ABR and manual choices
    bandwidth: ?number // track's bandwidth (null for text tracks)
  stateChange: Array of Objects
    timestamp: number  // seconds, when the state changed
    state: string  // 'buffering', 'playing', 'paused', or 'ended'
    duration: number  // seconds in this state

For more on stats in v2, see shaka.extern.Stats.

Player events

In both Shaka v1 and v2, Player emits events. Here is a map of events and their properties in v1 and their equivalents in v2:

  • error => error
  • bufferingStart and bufferingEnd => combined into buffering
    • (no v1 equivalent) => buffering (boolean, true when we enter buffering state, false when we leave)
  • (no v1 equivalent) => texttrackvisibility
  • trackschanged => trackschanged
  • seekrangechanged => (no v2 equivalent, use player.seekRange() when updating the UI)
  • adaptation => adaptation
    • contentType (string) => (no v2 equivalent)
    • size (width:number, height:number) => (no v2 equivalent)
    • bandwidth (number) => (no v2 equivalent)

For more information on events, see the Events section of shaka.Player.

Browser support testing

In Shaka v1, you could check if a browser was supported or not using shaka.player.Player.isBrowserSupported():

// v1:
if (!shaka.player.Player.isBrowserSupported()) {
  // Show an error and abort.
}

In v2, the same method exists to detect support. For diagnostics there is a new method that will get more detailed information about the browser. This will involve making a number of queries to EME which may result in user prompts, so it is suggested this only be used for diagnostics:

// v2:
if (!shaka.Player.isBrowserSupported()) {
  // Show an error and abort.
} else {
  // Only call this method if the browser is supported.
  shaka.Player.probeSupport().then(function(support) {
    // The check is asynchronous because the EME API is asynchronous.
    // The support object contains much more information about what the browser
    // offers, if you need it.  For example, if you require both Widevine and
    // WebM/VP9:
    if (!support.drm['com.widevine.alpha'] ||
        !support.media['video/webm; codecs="vp9"']) {
      // Show an error and abort.
    }
  });
}

For more on the support object, check out shaka.extern.SupportType. You can also see the full probeSupport() report for your browser at: http://shaka-player-demo.appspot.com/support.html

HttpVideoSource

There is no equivalent to v1's HttpVideoSource. Shaka v2 only supports playback via MSE. This is much simpler and allowed us to remove many special cases from the code.

Offline storage

In v1, to store offline content you used an OfflineVideoSource.

// v1:
function chooseStreams() {
  return Promise.resolve(trackIds);
}

function onProgress(evt) {
  console.log('Stored ' + evt.detail + '%');
}

var videoSource = new shaka.player.OfflineVideoSource(null, null);
videoSource.addEventListener('progress', onProgress);

var promise = videoSource.store('https://url', 'en-US', null, chooseStreams);
promise.then(function(groupId) {
  console.log('Stored at group ID ' + groupId);
});

In v2 you use the Storage class to store offline content.

// v2:
function chooseTracks(allTracks) {
  return filteredTracks;
}

function onProgress(storedContent, percent) {
  console.log('Stored ' + percent + '%');
}

var storage = new shaka.offline.Storage(player);
// Optional
storage.configure({
  trackSelectionCallback: chooseTracks,
  progressCallback: onProgress
});

var promise = storage.store('https://url', {extra: 'data'});
promise.then(function(storedContent) {
  console.log('Can be loaded using url: ' + storedContent.offlineUri);
});

Offline playback

In v1, you also used OfflineVideoSource to load the stored content.

// v1:
var videoSource = new shaka.player.OfflineVideoSource(groupId, null);
player.load(videoSource);

In v2, you don't have to use any special types so long as you know the URL to use. When storing the content, you get a special URL that is simply passed to load like any other URL. You can also get the info by listing all stored content.

// v2:
player.load(offlineUri);

Offline listing and deleting

In v1, you used OfflineVideoSource to list and delete content.

// v1:
var videoSource = new shaka.player.OfflineVideoSource(null, null);

videoSource.retrieveGroupIds().then(function(groupIds) {
  console.log(groupIds[0]);
});

var sourceToDelete = new shaka.player.OfflineVideoSource(groupId, null);
sourceToDelete.deleteGroup().then(function() {
  console.log('Done');
});

In v2, you use Storage to list and delete stored content.

// v2:
var storage = new shaka.offline.Storage(player);

storage.list().then(function(storedContents) {
  var firstInfo = storedContents[0];
  var url = firstInfo.offlineUri;
  player.load(url);
});

storage.remove(storedContent.offlineUri).then(function() {
  console.log('Done');
});