Thanks to visit codestin.com
Credit goes to github.com

Skip to content

How to play non-DRM encrypted HLS? #9116

@hethon

Description

@hethon

I was using ClearKey DRM to protect my content, then I discovered ClearKey DRM is not supported on iOS devices so I decided to fallback to using encrypted HLS whenever ClearKey DRM is unavailable.

On Apple platforms, native HLS playback bypasses JavaScript request hooks (like VHS's beforeRequest), preventing the use of authentication headers. My workaround is to use token authentication via query parameters, with a server that dynamically generates HLS manifests to include the token in the URIs for the media playlists and encryption key.

The not working code (It works for the DASH ClearKey DRM path):

<!DOCTYPE html>
<html lang="en" data-theme="dark">

<head>
  <title>Test</title>
  <link href="https://vjs.zencdn.net/8.23.4/video-js.css" rel="stylesheet" />
</head>

<body>
  <main>
    <video-js
        id="my-video"
        class="vjs-default-skin vjs-16-9 vjs-fluid"
        controls
    ></video-js>
  </main>
  <script src="https://vjs.zencdn.net/8.23.4/video.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/videojs-contrib-eme.min.js"></script>
  <script>
    async function supportsClearKey() {
      const keySystem = 'org.w3.clearkey';
      const config = [{
        initDataTypes: ['cenc'],
        videoCapabilities: [{ contentType: 'video/mp4; codecs="avc1.42E01E"' }]
      }];
      if (!navigator.requestMediaKeySystemAccess) return false;
      try {
        await navigator.requestMediaKeySystemAccess(keySystem, config);
        return true;
      } catch (error) {
        return false;
      }
    }

    async function setupPlayer() {
        const player = videojs('my-video');
        window.player = player; // for debugging

        const canPlayClearKey = await supportsClearKey();

        const token= "auth_token";

        if (!canPlayClearKey || true) { // intentinally forcing this path for testing purpose
            console.log("Strategy: Falling back to native HLS.");
            player.src({
                src: `master.m3u8?token=${token}`,
                type: 'application/x-mpegURL',
            });
        }
        else {
            console.log("Strategy: Using DASH with Clear Key.");
            player.on('xhr-hooks-ready', () => {
                player.tech(false).vhs.xhr.onRequest((options) => {
                    options.headers = options.headers || {};
                    options.headers['X-Token'] = token;
                    return options;
                });
            });
            player.eme();
            player.src({
                src: 'manifest.mpd',
                type: 'application/dash+xml',
                keySystems: {
                    'org.w3.clearkey': {
                        getLicense: function(emeOptions, keyMessage, callback) {
                            try {
                                const msgUint8 = new Uint8Array(keyMessage);
                                console.log("typeof keyMessage:", typeof keyMessage);
                                fetch('http://127.0.0.1:5000/license', {
                                    method: 'POST',
                                    headers: {
                                        'Content-Type': 'application/octet-stream',
                                        'X-Telegram-Init-Data': telegramInitData,
                                    },
                                    body: msgUint8 // send raw bytes
                                })
                                .then(res => {
                                    if (!res.ok) {
                                        return res.text().then(text => {
                                            throw new Error(text);
                                        });
                                    }
                                    return res.arrayBuffer();
                                })
                                .then(licenseArrayBuffer => {
                                    callback(null, new Uint8Array(licenseArrayBuffer));
                                })
                                .catch(err => {
                                    console.error('License fetch error', err);
                                    callback(err);
                                });
                            } catch (err) {
                                callback(err);
                            }
                        }
                    }
                },
            });
        }
    }
    (async function() {
        try {
            await setupPlayer();
            console.log("Player setup process completed successfully.");
        } catch (error) {
            console.error("A critical error occurred during player setup:", error);
        }
    })();
  </script>
</body>

</html>

Trackback:

Image

How did I know this was not caused by the way I am serving the playlist files (.m3u8)? I built another player that does the same thing using Shaka player and it seems to be working fine for both paths. If there was any issue with the playlists or the way I am serving the decryption link this code wouldn't work as well.

The Shaka player code:

<!DOCTYPE html>
<html lang="en" data-theme="dark">

<head>
  <title>Test</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/shaka-player/4.16.3/shaka-player.ui.min.js"></script>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/shaka-player/4.16.3/controls.min.css">
</head>

<body>
  <main>
    <div id="my-video" data-shaka-player-container>
      <!-- Shaka will use this video element -->
      <video autoplay data-shaka-player id="video"></video>
    </div>
  </main>
  <script>
    async function init() {
        async function supportsClearKey() {
            const keySystem = 'org.w3.clearkey';
            const config = [{
                initDataTypes: ['cenc'],
                videoCapabilities: [{ contentType: 'video/mp4; codecs="avc1.42E01E"' }]
            }];
            if (!navigator.requestMediaKeySystemAccess) return false;
            try {
                await navigator.requestMediaKeySystemAccess(keySystem, config);
                return true;
            } catch (error) {
                return false;
            }
        }

        const video = document.getElementById('video');
        const ui = video['ui'];
        const controls = ui.getControls();
        const player = controls.getPlayer();

        window.player = player;
        window.ui = ui;

        // Listen for error events.
        player.addEventListener('error', onPlayerErrorEvent);
        controls.addEventListener('error', onUIErrorEvent);

        const canPlayClearKey = await supportsClearKey();

        const token = "auth_token"

        if (!canPlayClearKey || true) { // intentinally forcing this path for testing purpose
            console.log("Strategy: Falling back to native HLS.");
            try {
                await player.load(`master.m3u8?initData=${token}`);
                console.log('The video has now been loaded!');
            } catch (error) {
                onPlayerError(error);
            }
        } else {
            console.log("Strategy: Using DASH with Clear Key.");
            // Attach initData header to every manifest, segment and license request
            player.getNetworkingEngine().registerRequestFilter((type, request) => {
              if (type === shaka.net.NetworkingEngine.RequestType.MANIFEST ||
                  type === shaka.net.NetworkingEngine.RequestType.SEGMENT ||
                  type === shaka.net.NetworkingEngine.RequestType.LICENSE) {
                  request.headers['X-Telegram-Init-Data'] = telegramInitData;
              }
            });

            player.configure({
                drm: {
                    servers: {
                        'org.w3.clearkey': 'http://127.0.0.1:5000/license'
                    }
                }
            });

            try {
                await player.load('manifest.mpd');
                // This runs if the asynchronous load is successful.
                console.log('The video has now been loaded!');
            } catch (error) {
                onPlayerError(error);
            }
        }
    }

    function onPlayerErrorEvent(errorEvent) {
        // Extract the shaka.util.Error object from the event.
        onPlayerError(errorEvent.detail);
    }

    function onPlayerError(errorEvent) {
        // Handle player error
        console.error('Error code', errorEvent.code, 'object', errorEvent);
    }

    function onUIErrorEvent(errorEvent) {
        // Extract the shaka.util.Error object from the event.
        onPlayerError(errorEvent.detail);
    }

    function initFailed(errorEvent) {
        console.error('Unable to load the UI library!', errorEvent.detail);
    }

    // Listen to the custom shaka-ui-loaded event, to wait until the UI is loaded.
    document.addEventListener('shaka-ui-loaded', init);
    // Listen to the custom shaka-ui-load-failed event, in case Shaka Player fails
    // to load (e.g. due to lack of browser support).
    document.addEventListener('shaka-ui-load-failed', initFailed);
  </script>
</body>

</html>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions