Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
jakubvojacek opened this issue Jan 15, 2025 · 0 comments
Open

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

jakubvojacek opened this issue Jan 15, 2025 · 0 comments
Labels
component: FairPlay The issue involves the FairPlay DRM platform: iOS Issues affecting iOS type: bug Something isn't working correctly
Milestone

Comments

@jakubvojacek
Copy link

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.

@jakubvojacek jakubvojacek added the type: bug Something isn't working correctly label Jan 15, 2025
@avelad avelad added platform: iOS Issues affecting iOS component: FairPlay The issue involves the FairPlay DRM labels Jan 15, 2025
@shaka-bot shaka-bot added this to the v4.13 milestone Jan 15, 2025
@avelad avelad modified the milestones: v4.13, v4.14 Jan 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: FairPlay The issue involves the FairPlay DRM platform: iOS Issues affecting iOS type: bug Something isn't working correctly
Projects
None yet
Development

No branches or pull requests

3 participants