Introduction
High-bandwidth Digital Content Protection (HDCP) is a form of digital copy protection used to protect high definition (HD) video and audio signals from being copied on unauthorized devices. The transmitting device first checks if the receiver is authorized to receive data. If yes, then the transmitter sends encrypted data to prevent eavesdropping.
The receiving setup must be HDCP compliant, including devices, cables, adaptors, and software drivers. If the receiver is not HDCP compliant, then video will play in standard definition (SD) only. In general, newer HDTVs and HDMI or DVI cables should be HDCP compliant.
For details, see the Fallback HDCP document.
Requirements
The following requirements are needed to support this feature:
Brightcove SDK version
- Native SDK for Android 6.17.1 and newer
- Native SDK for iOS 6.10.1 and newer
Android implementation
When playing a video with a mix of SD and HD renditions, you will want to do the following:
-
On devices that can support both HD and SD playback, you want to ensure that the player can switch to an appropriate rendition when needed, for example when network bandwidth changes. This rendition switch may require the player to select an SD rendition, where before it was playing HD.
-
On devices without HDCP support, for example older Android mobile devices with older OS levels, you want to ensure appropriate rendition switching within SD renditions, as well as guarding that the player will not attempt to load an HDCP-protected rendition, at which point the license request will fail, with an error message like this:
Error message
2021-11-01 19:01:36.943 30131-30131/com.brightcove.player.samples.exoplayer.basic E/VideoDisplayComponent: onPlayerError com.google.android.exoplayer2.ExoPlaybackException: MediaCodecVideoRenderer error, index=0, format=Format(bf310894-59b5-4f1b-9a37-e110a3d6121d, null, null, video/avc, avc1.4D401F, 1712000, null, [1280, 720, 30.0], [-1, -1]), format_supported=YES at com.google.android.exoplayer2.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:555) at android.os.Handler.dispatchMessage(Handler.java:98) at android.os.Looper.loop(Looper.java:148) at android.os.HandlerThread.run(HandlerThread.java:61) Caused by: android.media.MediaCodec$CryptoException: Unknown Error at android.media.MediaCodec.native_queueSecureInputBuffer(Native Method) at android.media.MediaCodec.queueSecureInputBuffer(MediaCodec.java:2292) at com.google.android.exoplayer2.mediacodec.SynchronousMediaCodecAdapter.queueSecureInputBuffer(SynchronousMediaCodecAdapter.java:143) at com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.feedInputBuffer(MediaCodecRenderer.java:1380) at com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.render(MediaCodecRenderer.java:845) at com.google.android.exoplayer2.ExoPlayerImplInternal.doSomeWork(ExoPlayerImplInternal.java:945) at com.google.android.exoplayer2.ExoPlayerImplInternal.handleMessage(ExoPlayerImplInternal.java:478) at android.os.Handler.dispatchMessage(Handler.java:98) at android.os.Looper.loop(Looper.java:148) at android.os.HandlerThread.run(HandlerThread.java:61)
Content with Fallback HDCP carries a separate DRM key per rendition tier. The SD key plays on any output, while the HD and UHD keys require a protected one. The Widevine CDM enforces that on the device, and it does so by refusing the key at output time, so playback fails with the decrypt error above rather than dropping to SD. Your application therefore has to keep an unprotected output away from the HD renditions.
Two device classes have to work, and they fail in opposite ways:
- A device on a protected output must be free to adapt up into the HD renditions when bandwidth allows.
- A device on an unprotected output must never request or decode an HD rendition.
Read the HDCP level of the connected output and constrain the track selector to match. Use an EventListener on the SET_SOURCE event, as the constraint has to be in place before the ExoPlayer instance is created.
An example like this would be added to your player’s Activity class, in the onCreate method:
eventEmitter.on(EventType.SET_SOURCE, event -> {
// Each rendition tier has a DRM key of its own, so moving from an SD rendition to an HD one
// needs a second concurrent DRM session. Without this property the player keeps the session
// it opened for the SD key and has no key for the HD renditions.
Object source = event.getProperties().get(Event.SOURCE);
if (source instanceof Source
&& ((Source) source).hasKeySystem(Source.Fields.WIDEVINE_KEY_SYSTEM)) {
((Source) source).getProperties().put(Source.Fields.MULTI_SESSION, "true");
}
if (!hasProtectedOutput()) {
Log.v(TAG, "Restricting rendition selection to SD");
// Constrain the track selector the SDK already uses, rather than replacing it, so that
// any other constraint your application or the SDK applied stays in place. Before the
// first playback the SDK has not created its selector yet, so install one.
DefaultTrackSelector trackSelector = videoDisplayComponent.getTrackSelector();
if (trackSelector == null) {
trackSelector = new DefaultTrackSelector(this, new AdaptiveTrackSelection.Factory());
videoDisplayComponent.setTrackSelector(trackSelector);
}
trackSelector.setParameters(trackSelector.buildUponParameters()
.setMaxVideoSizeSd()
// The size constraint on its own is a preference: the player still selects an
// HD rendition when no rendition fits the constraint. Output protection has to
// fail closed, so turn the preference into a limit.
.setExceedVideoConstraintsIfNecessary(false)
.build());
}
});
The HDCP level of the connected output comes from MediaDrm. Compare it with the level your account’s licence policy requires for HD:
private boolean hasProtectedOutput() {
MediaDrm mediaDrm = null;
try {
mediaDrm = new MediaDrm(Constants.WIDEVINE_UUID);
int connectedLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
? mediaDrm.getConnectedHdcpLevel()
: parseHdcpLevel(mediaDrm.getPropertyString("hdcpLevel"));
Log.v(TAG, "Connected HDCP level: " + connectedLevel);
// The MediaDrm levels rise with protection, so a single comparison is enough. Raise the
// minimum to MediaDrm.HDCP_V2_2 when your licence policy asks for HDCP 2.2, as UHD
// content usually does.
return connectedLevel >= MediaDrm.HDCP_V1;
} catch (UnsupportedSchemeException exception) {
Log.e(TAG, "UnsupportedSchemeException: " + exception.getLocalizedMessage());
return false;
} finally {
if (mediaDrm != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
mediaDrm.close();
} else {
mediaDrm.release();
}
}
}
}
Two further points complete the implementation:
- Re-read the level when a display is attached or detached, with a
DisplayManager.DisplayListener. Otherwise an HD stream keeps playing after an HDMI cable is plugged into an unprotected receiver. - A size constraint governs automatic selection only. If your application offers a quality menu, it has to refuse the renditions above the cap as well, because an explicit track selection override bypasses a size constraint.
For a complete, runnable version of the above, including the display listener and the parseHdcpLevel method that maps the hdcpLevel property string of API 27 and below, see the HdcpFallbackSampleApp sample. It ships in Java and Kotlin.
iOS implementation
Fallback HDCP is supported with the Native SDK for iOS/tvOS, but is only enforced for content protected using FairPlay.
You can detect the non-compliance of the device, by using KVC to detect changes to the value isOutputObscuredDueToInsufficientExternalProtection on the AVPlayer.
AVPlayer.isOutputObscuredDueToInsufficientExternalProtection == true
The above property's value changes to true for the following reasons:
- The current item requires external protection
- The device does not meet the protection level
- The user observes video loss
Since not all users have an HDCP compatable setup, Apple recommends including a variant (rendition) in the video manifest which does not require HDCP protection. Brightcove handles this for you when your account is enabled for Fallback HDCP.