Skip to content

Fairplay playback works in Safari on MacOS but not on iOS #7890

Open
@jakubvojacek

Description

@jakubvojacek

Have you read the FAQ and checked for duplicate open issues?
yes

If the problem is related to FairPlay, have you read the tutorial?
yes

What version of Shaka Player are you using?
I tried latest as well master. In the demo I am using the last version on cdnjs (4.12.6)

Can you reproduce the issue with our latest release version?
yes

Can you reproduce the issue with the latest code from main?
yes

Are you using the demo app or your own custom app?
own

If custom app, can you reproduce the issue using our demo app?
it uses custom DRM request/response filter, couldn't use demo app

What browser and OS are you using?
I tested on latest Safari on

  • macos 15.1.1 - works
  • ios 18.1.1 - does not work (tested on multiple phones in the office, behaved the same)

What are the manifest and license server URIs?
Its all part of the demo sample below

What configuration are you using? What is the output of player.getNonDefaultConfiguration()?
Its all part of the demo sample below

What did you do?

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Shaka Player Example</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/shaka-player/4.12.6/shaka-player.ui.debug.min.js" integrity="sha512-dZZCdD5vzAd9E9kDye1xY/JzzflJW5CyHq3KIm+YT0KClWlD3AWfUmbJHvOSqbooxSrjNa556ng+I6GomKXjgA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>
<body>
    <div id="consoleOutput" style="background-color: #f0f0f0; padding: 10px; height: 500px; overflow-y: scroll; border: 1px solid #ccc; font-size: 0.6em;"></div>
    <script>
            // Custom function to handle all console outputs
            (function() {
                // Get the consoleOutput div
                var outputDiv = document.getElementById('consoleOutput');

                // Function to handle logging to the div
                function safeStringify(obj, space = 2) {
                    const seen = new WeakSet();
                    return JSON.stringify(obj, (key, value) => {
                        if (typeof value === "object" && value !== null) {
                            if (seen.has(value)) {
                                return "[Circular]";
                            }
                            seen.add(value);
                        }
                        return value;
                    }, space);
                }


                function logToDiv(type, args) {
                    // Convert arguments to a string representation
                    var msg = Array.from(args).map(arg => (typeof arg === 'string' ? arg : safeStringify(arg))).join(' ');

                    // Create a new div element for each log entry
                    var logEntry = document.createElement('div');
                    logEntry.textContent = msg;

                    // Style log entry based on the log type
                    if (type === 'warn') {
                        logEntry.style.color = 'orange';
                    } else if (type === 'error') {
                        logEntry.style.color = 'red';
                    } else if (type === 'info') {
                        logEntry.style.color = 'blue';
                    } else if (type === 'debug') {
                        logEntry.style.color = 'green';
                    } else {
                        logEntry.style.color = 'black'; // Default for log
                    }

                    // Append the log entry to the consoleOutput div
                    outputDiv.appendChild(logEntry);

                    // Scroll to the bottom of the div to see the latest log
                    outputDiv.scrollTop = outputDiv.scrollHeight;
                }

                // List of console methods to override
                var methods = ['log', 'warn', 'error', 'info', 'debug'];

                methods.forEach(function(method) {
                    var oldMethod = console[method];
                    console[method] = function() {
                        logToDiv(method, arguments);
                        oldMethod.apply(console, arguments); // Call the original method
                    };
                });
            })();
        </script>
    <video id="video" width="640" height="360" controls autoplay style="max-width: 100%; height: auto;"></video>
    <script>
        function uInt8ArrayToString(array) {
            return String.fromCharCode.apply(null, Array.from(array));
        }

        function base64EncodeUint8Array(input) {
            return btoa(uInt8ArrayToString(input));
        }

        function base64ToUint8Array(base64) {
            const binaryString = atob(base64);
            const len = binaryString.length;
            const bytes = new Uint8Array(len);
            for (let i = 0; i < len; i++) {
                bytes[i] = binaryString.charCodeAt(i);
            }
            return bytes;
        }

        document.addEventListener('DOMContentLoaded', () => {
            const video = document.getElementById('video');
            const base64der = "MIIE2jCCA8KgAwIBAgIIKtRnQQ5QBBgwDQYJKoZIhvcNAQEFBQAwfzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MTMwMQYDVQQDDCpBcHBsZSBLZXkgU2VydmljZXMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjEwNjE0MDU1MjE2WhcNMjMwNjE1MDU1MjE1WjBoMQswCQYDVQQGEwJDWjEXMBUGA1UECgwOTW90di5ldSBzLnIuby4xEzARBgNVBAsMCko1WTZFQkJUUUsxKzApBgNVBAMMIkZhaXJQbGF5IFN0cmVhbWluZzogTW90di5ldSBzLnIuby4wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAPCylmldNcZ4rG/MsXB/Lt09zz4C6MQw+oD8DRUtv8PCHbZfWejhrJqlDISeFRFboE95EyDsTR8aSFqz5qoNVHksvf+pLs87OVIeKcNaXzd0kNjdD2sy0ND9/atVsBEwGjS8hDnGLoxiPOIF+uM/yP8KfePUQZvPPVe1dczMeQ7zAgMBAAGjggHzMIIB7zAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFGPkR1TLhXFZRiyDrMxEMWRnAyy+MIHiBgNVHSAEgdowgdcwgdQGCSqGSIb3Y2QFATCBxjCBwwYIKwYBBQUHAgIwgbYMgbNSZWxpYW5jZSBvbiB0aGlzIGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRlIHBvbGljeSBhbmQgY2VydGlmaWNhdGlvbiBwcmFjdGljZSBzdGF0ZW1lbnRzLjA1BgNVHR8ELjAsMCqgKKAmhiRodHRwOi8vY3JsLmFwcGxlLmNvbS9rZXlzZXJ2aWNlcy5jcmwwHQYDVR0OBBYEFAIWSJaVyWs0pYaJ+U3B9Q9zg9/UMA4GA1UdDwEB/wQEAwIFIDBJBgsqhkiG92NkBg0BAwEB/wQ3AWp4aHc0bjVic3d4dG1qc3g5dWF3Z2R6Nnk4Y29semtuNWlhcWlseXYzYWJycGpwcTZob3M0MzAoBgsqhkiG92NkBg0BBAEB/wQWATZmNm56bTl3d2F6Z3Y3MDgzZ28weDANBgkqhkiG9w0BAQUFAAOCAQEAuvQFOR1to43xCjRRZssfRkJNY62qILxRN2zgG1QrfpP+Sqr93DRVEk1jym5iZOpWcH/XzK3w703+osbRWM4ADM1doInQ+1NtikfKTE1Lrcns+QNOeeFSx1datI7YSjrO/dLsilLAGWXk4J3jPA0ngSm69E+4eMQZhJj2h9ujfKJ2FNp/uSNt0Gm6XFY1KOTPaQ44xRTWcQ19Fj05FRvW8ZXD6UNl80EG6rggB58AURcsVtWsjGIJkrjtqsE/wI+ik2u8m/3zR2kCBap4+d4GGlAfz6AhR9zphGSz7fhiF4H5loHgqe0Srv62YbQGC3U+wHGKM77M2c0LWuE/cb+9mg==";
            const cert = base64ToUint8Array(base64der);

            if (shaka.Player.isBrowserSupported()) {
                shaka.polyfill.installAll();
                const player = new shaka.Player();
                player.attach(video)

                config = {
                    drm: {
                        servers: {
                            "com.apple.fps": "https://mw.motv.eu/ksm",
                            "com.widevine.alpha": "https://mw.motv.eu/widevine_proxy",
                        },
                        advanced: {
                            "com.apple.fps": {
                                serverCertificate: cert
                            }
                        }
                    }
                };

                const deviceHash = "sdfssdfdsf";
                const version = "0.1";
                const profilesId = 274483;
                const devicesType = "ios";
                const deviceInfo = "test";
                const customerToken = "glf40bnmzmmwiw85glre6gqy8qjjvnhy99g90ul8";

                player.configure(config);

                player.getNetworkingEngine().registerRequestFilter((type, request) => {
                    if (type === shaka.net.NetworkingEngine.RequestType.LICENSE) {
                        request.headers.Authorization = `Bearer ${customerToken}`;
                        request.headers.profilesId = btoa(profilesId.toString());
                        request.headers.devicesType = btoa(devicesType);
                        request.headers.version = btoa(version);

                        const wrapped = {
                            devices_hash: deviceHash,
                            devices_identification: deviceInfo,
                            edges_id: 1,
                            offset: 0,
                            // rawLicense: shaka.util.Uint8ArrayUtils.toBase64(new Uint8Array(request.body), false), // for widevine
                            rawLicense: base64EncodeUint8Array(new Uint8Array(request.body)), // for fairplay
                            timestamp: new Date().getTime() / 1000,
                        };
                        const wrappedJson = JSON.stringify(wrapped);
                        request.body = shaka.util.StringUtils.toUTF8(wrappedJson);
                    }
                });

                player.getNetworkingEngine().registerResponseFilter((type, response) => {
                    if (type === shaka.net.NetworkingEngine.RequestType.LICENSE) {
                        const wrappedString = shaka.util.StringUtils.fromUTF8(response.data);
                        const wrapped = JSON.parse(wrappedString);
                        const rawLicense = wrapped.rawLicense;
                        response.data = shaka.util.Uint8ArrayUtils.fromBase64(rawLicense);
                    }
                });

                player.addEventListener('error', (event) => {
                    console.error('Shaka Player Error', event.detail);
                });

                player.load("https://edge1.motv.eu/s1/vods/30/neni-houba-jako-houba-3852/66c72001de232-2635-mp4-cbcs.m3u8", null).then(() => {
                    video.play();
                }).catch((error) => {
                    console.error('Error loading video', error);
                });
            } else {
                console.error("Browser not supported by Shaka Player");
            }
        });
    </script>
</body>
</html>

What did you expect to happen?
I expected playback on iOS

What actually happened?
it fails with error 3016, Media failed to decode. You should be easily able to reproduce the same using the code sample above or its available on URL https://mw.motv.eu/static/player.html

I tested on multiple mac's and it worked everywhere, I would expect that the some code would work on iOS on Safari but it does not. However the error reported is not very descriptive and I am not sure where to look now, could you please help me find the cause?

Image

Are you planning to send a PR to fix it?
No, I am not sure what is wrong.

Metadata

Metadata

Assignees

No one assigned

    Labels

    component: FairPlayThe issue involves the FairPlay DRMplatform: iOSIssues affecting iOStype: bugSomething isn't working correctly

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions