Playing Widevine (DRM) enabled DASH Streams with Exoplayer on Android

Burak Oguz
5 min readMar 12, 2021

--

Digital rights management (DRM) is a set of tools and techniques to create access control generally around proprietary hardware or copyright materials online. Dynamic Adaptive Streaming over HTTP is an adaptive bitrate streaming technique similar to Apple’s HTTP Live Streaming (HLS) solution that serves high quality streaming over HTTP by dividing the content into a sequence of smaller segments and making this content available at different bit rates. Exoplayer is a popular application level video player alternative to Android’s MediaPlayer framework, and Widevine is the most popular digital rights management technology from Google. Widevine is currently being used by Google Chrome, Firefox, Android, and Android TV. In this article, we will focus on explaining how DRM works and how to integrate Exoplayer and Wideview content using MediaDRM with online and downloadable content use cases.

Please note that the code samples in this article rely on the knowledge of Exoplayer and dependency injection. They are not full examples, due to the complexities of setting up license and content servers.

DRM Workflow and License Exchange

There are four main components of Widevine streaming:

  1. The client attempting to play the content
  2. The license server that generates the decryption keys based on the request parameters from the client
  3. The provisioning server if it is required to distribute device unique credentials
  4. The content server which serves the encrypted content

When the client tries to play protected content from the content server via a DashMediaSource with a provided DrmSessionManager, the client will automatically initiate a DRM license request via MediaDrmCallback. In the meantime, if the device needs provisioning, a request to the provisioning server will be done via callback. After the MediaDRM client receives the license, it will pass it to Exoplayer via the media source and media playback will begin. For non-persistent licenses, this procedure will be repeated with any media playback request. For persistent license cases, applications may save the persistent license and can reuse the license until it expires. Also for persistent license requests, the license can be fetched before the video playback starts with OfflineLicenseHelper so that video initialization will not depend on callback’s successful license fetch operation. Now we will detail how these classes will be utilized.

Playing DASH content in Exoplayer

According to product and system requirements, it is possible to implement the business logic that manages the license exchange in very different ways. However, the most basic way of doing the license exchange is using HttpMediaDrmCallback. This method can be utilized if your license servers don’t require any custom processing on the license exchange request. As you can see below, it is quite straightforward to build a DashMediaSource with a DefaultDrmSessionManager using a HttpMediaDrmCallback. In the example, when the player loads the DashMediaSource, if it encounters encrypted content that requires a license to play, it will ask DefaultDrmSessionManager to provide a license and DefaultDrmSessionManager will use HttpMediaDrmCallback to fetch the license from the provided license URL. Calling setMode with null license data will force DefaultDrmSessionManager to utilize HttpMediaDrmCallback when there is a license request. If a license is provided to setMode, DefaultDrmSessionManager will try to use it first, and if the license doesn’t successfully decrypt the content, it will again try to utilize the provided MediaDrmCallback to fetch a new license.

@Singletoninternal class FooDownloader @Inject constructor(private val customHttpDrmMediaCallback: CustomHttpDrmMediaCallback) {fun download(context: Context, downloadUrl: Uri) {val sessionManager = DefaultDrmSessionManager.Builder().build(customHttpDrmMediaCallback)sessionManager.setMode(DefaultDrmSessionManager.MODE_DOWNLOAD, null)DownloadHelper.forMediaItem(MediaItem.fromUri(downloadUrl),DefaultTrackSelector.Parameters.getDefaults(context),DefaultRenderersFactory(context),DefaultHttpDataSourceFactory("userAgent"),sessionManager)}}

Custom MediaDrmCallback

Of course, the example above is not functionally complete. In most cases, the license server will need an authentication token or other information based on the media subject to the license exchange request. In order to achieve this, it is possible to use built-in HttpMediaDrmCallback or implement a custom MediaDrmCallback. It is also possible to build a custom MediaDrmCallback using HttpMediaDrmCallback. In the callback below, executeKeyRequest and executeProvisioningRequest methods will be executed in a background thread. This allows additional business logic around network calls or database/file IO access to be done without thread switching. As you can see below, when an executeKeyRequest is done, the authentication token is fetched from a remote API and the key request is executed with an updated license URL. The same flow applies to executeProvisioningRequest as well.

@Singletoninternal class CustomHttpDrmMediaCallback @Inject constructor(@Named(“drm”) private val factory: OkHttpDataSourceFactory,private val apiClient: VideoApiClient) : MediaDrmCallback {companion object {private const val defaultLicenseUrl = “https://license.url/server/fetch-license"}private val httpMediaDrmCallback = HttpMediaDrmCallback(defaultLicenseUrl, false, factory)override fun executeKeyRequest(uuid: UUID, request: ExoMediaDrm.KeyRequest): ByteArray {val authenticationToken = apiClient.getLicenseAuthenticationToken()val licenseUrl = Uri.parse(defaultLicenseUrl).buildUpon().appendQueryParameter(“token”, authenticationToken).build().toString()val updatedRequest = ExoMediaDrm.KeyRequest(request.data, licenseUrl)return httpMediaDrmCallback.executeKeyRequest(uuid, updatedRequest)}override fun executeProvisionRequest(uuid: UUID, request: ExoMediaDrm.ProvisionRequest): ByteArray {val authenticationToken = apiClient.getLicenseAuthenticationToken()val licenseUrl = Uri.parse(defaultLicenseUrl).buildUpon().appendQueryParameter(“token”, authenticationToken).build().toString()val updatedRequest = ExoMediaDrm.ProvisionRequest(request.data, drmLicenseUrl)return httpMediaDrmCallback.executeProvisionRequest(uuid, updatedRequest)}}

Utilizing Offline Licenses

In the cases covered so far, the application needed online access for license exchange operations at the time of playback. Depending on the product and system requirements, it is possible to fetch licenses prior to video playback and cache them for later use. This will also have a positive impact on video playback start-up times. In order to fetch licenses without DrmSessionManager or MediaDrmCallback, it is possible to build a custom client to execute license exchange operations. However, there is already a class called OfflineLicenseHelper to expedite this process. As illustrated below, with a given DRM license fetch URL and URI to the video path, first it is needed to create a new instance that supports Wideview license download and then it is possible to download license with downloadLicense method and extract the expiration date with getLicenseDurationRemainingSec method.

@Singletoninternal class OfflineRemoteLicenseFetcher @Inject constructor(@Named(“drm”) private val okHttpDataSourceFactory: OkHttpDataSourceFactory,) {fun downloadLicense(drmLicenseUrl: String, videoPath: Uri): Pair<ByteArray?, Long>? {val offlineLicenseHelper = OfflineLicenseHelper.newWidevineInstance(drmLicenseUrl,okHttpDataSourceFactory, DrmSessionEventListener.EventDispatcher())val dataSource = okHttpDataSourceFactory.createDataSource()val dashManifest = DashUtil.loadManifest(dataSource, videoPath)val drmInitData = DashUtil.loadFormatWithDrmInitData(dataSource, dashManifest.getPeriod(0))val licenseData = drmInitData?.let {offlineLicenseHelper.downloadLicense(it)}val licenseExpiration = if (licenseData != null) {System.currentTimeMillis() + (offlineLicenseHelper.getLicenseDurationRemainingSec(licenseData).first * 1000)} else {0}return Pair(licenseData, licenseExpiration)}}

After fetching the license, it is possible to cache it.The next time applicable content is accessed, the cached license can be provided to the DrmSessionManager via the setMode method shown below. As mentioned above, if the cached license data becomes invalid, DrmSessionManager will use MediaDrmCallback to fetch a new license.

sessionManager.setMode(DefaultDrmSessionManager.MODE_PLAYBACK, licenseData)

Handling Errors

It is important to keep in mind that the added layer of operations may cause an added number of error cases and these errors need to be handled properly. DRM license related playback errors will end up in Exoplayer’s Player.EventListener.onPlayerError method. MediaCodec.CryptoException and DrmSession.DrmSessionException are common exceptions that need to be handled in the player flow. Additionally, if it is preferred to implement a custom DrmSessionManager, it will be necessary to register an event handler for DrmSessionEventListener and handle errors in onDrmSessionManagerError.

Utilizing DrmSessionManager with Exoplayer DownloadHelper

Another important feature that Exoplayer provides is being able to download adaptive streams to the client. Exoplayer supports downloading of both HLS and DASH streams. Even though DownloadHelper has a basic interface, integrating it with product and system requirements can become complicated, especially while downloading DRM enabled DASH content. Keep in mind that a DrmSessionManager has to be provided to the DownloadHelper.forMediaItem method.

@Singletoninternal class DashDownloader @Inject constructor(private val customHttpDrmMediaCallback: CustomHttpDrmMediaCallback) {fun download(context: Context, downloadUrl: Uri) {val sessionManager = DefaultDrmSessionManager.Builder().build(customHttpDrmMediaCallback)sessionManager.setMode(DefaultDrmSessionManager.MODE_DOWNLOAD, null)DownloadHelper.forMediaItem(MediaItem.fromUri(downloadUrl),DefaultTrackSelector.Parameters.getDefaults(context),DefaultRenderersFactory(context),DefaultHttpDataSourceFactory(“userAgent”),sessionManager)}}

Another important thing to remember related to downloaded DRM content, is to cache the license data to enable users to play the encrypted content when they don’t have a connection. Lastly, don’t forget to set up a periodic worker via WorkManager to refresh offline licenses whenever a user has a connection according to license expiration requirements.

--

--