shaka-project / shaka-player

JavaScript player library / DASH & HLS client / MSE-EME player
Apache License 2.0
7.07k stars 1.33k forks source link

Fairplay + HLS stream doesn't work on Mac Safari. #5997

Closed lalit-tudip closed 6 months ago

lalit-tudip commented 9 months ago

Have you read the Tutorials?

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

What version of Shaka Player are you using?

Please ask your question I'm facing an issue with Fairplay + HLS in my Mac Safari which is related to an existing thread https://github.com/shaka-project/shaka-player/issues/4829. So the first time it loads the stream into the player but as soon as we load it for the second time, doesn't matter same or different stream. The license request fails with the below error.

{ "success": false, "message": "Unable to parse SPC. See 'innerException' for more information." }

I checked and the same issue(https://github.com/shaka-project/shaka-player/issues/4829#issuecomment-1402029425) is occurring in my case as initially, the SPC message starts with "AAAAA..." but after the first load, it changes to "eyA..." which looks more like the authentication token.

There is one more issue I'm facing, on the first load I'm not able to see video and not able to hear audio but able to see the captions. Below is the debug logs of the first load

Log] loading media – "https://nwtel-selive003.tv2consulting.com:6443/CBNEWSH/archive-1701860400-now.m3u8" (bundle.js, line 116) [Info] Using Apple-prefixed EME (bundle.js, line 38908) [Log] PatchedMediaKeysApple.requestMediaKeySystemAccess (bundle.js, line 38911) [Log] PatchedMediaKeysApple.MediaKeySystemAccess (bundle.js, line 38911) [Log] PatchedMediaKeysApple.MediaKeySystemAccess.getConfiguration (bundle.js, line 38911) [Log] PatchedMediaKeysApple.MediaKeySystemAccess.createMediaKeys (bundle.js, line 38911) [Log] PatchedMediaKeysApple.MediaKeys (bundle.js, line 38911) [Info] Created MediaKeys object for key system – "com.apple.fps" (bundle.js, line 38908) [Log] PatchedMediaKeysApple.MediaKeys.setServerCertificate (bundle.js, line 38911) [Warning] Server certificates are not supported by the key system. The server certificate has been ignored. (bundle.js, line 38905) [Log] PatchedMediaKeysApple.setMediaKeys (bundle.js, line 38911) [Info] Creating new – "temporary" – "session" (bundle.js, line 38908) [Log] PatchedMediaKeysApple.MediaKeys.createSession (bundle.js, line 38911) [Log] PatchedMediaKeysApple.MediaKeySession (bundle.js, line 38911) [Log] Unable to find byte-order-mark, making an educated guess. (bundle.js, line 38911) [Log] Unable to find byte-order-mark, making an educated guess. (bundle.js, line 38911) [Log] PatchedMediaKeysApple.MediaKeySession.generateRequest (bundle.js, line 38911) [Log] PatchedMediaKeysApple.onWebkitKeyMessage – WebKitMediaKeyMessageEvent {isTrusted: true, message: Uint8Array, destinationURL: "this is a test string", …} (bundle.js, line 38911) WebKitMediaKeyMessageEvent {isTrusted: true, message: Uint8Array, destinationURL: "this is a test string", type: "webkitkeymessage", target: WebKitMediaKeySession, …}WebKitMediaKeyMessageEvent [Log] license request (bundle.js, line 171) [Log] license passed (bundle.js, line 187) [Log] PatchedMediaKeysApple.MediaKeySession.update (bundle.js, line 38911) [Log] PatchedMediaKeysApple.onWebkitKeyAdded – Event {isTrusted: true, type: "webkitkeyadded", target: WebKitMediaKeySession, …} (bundle.js, line 38911) Event {isTrusted: true, type: "webkitkeyadded", target: WebKitMediaKeySession, currentTarget: WebKitMediaKeySession, eventPhase: 2, …}Event

Screenshot of the first load:

blank_player

Below is my config for Fairplay: ` shaka.polyfill.PatchedMediaKeysApple.install();

        const certi = await fetch(cert).then(response => response.arrayBuffer());
        let certificate = new Uint8Array(certi)

        this.player.configure({
            drm: {
                servers: {
                    'com.apple.fps': `URL`,
                },
                advanced: {
                    'com.apple.fps': {
                        serverCertificate: certificate,
                    },
                },
            },
        });

        this.player.getNetworkingEngine().registerRequestFilter((type, request) => {
            console.log("request", type != shaka.net.NetworkingEngine.RequestType.LICENSE)
            if (type != shaka.net.NetworkingEngine.RequestType.LICENSE) {
                return;
            }
            const originalPayload = new Uint8Array(request.body);
            request.headers['Content-Type'] = 'application/json;api-version=5.1 ';
            let pl = window.btoa(
                Array(originalPayload.length).fill('').map((_, i) => String.fromCharCode(originalPayload[i])).join('')
            )
            const params = `{ "spc": "${pl}"}`;
            request.body = shaka.util.StringUtils.toUTF8(params);
            request.headers['Authorization'] = `token`
        });

        this.player.getNetworkingEngine().registerResponseFilter((type, response) => {
            if (type != shaka.net.NetworkingEngine.RequestType.LICENSE) {
                return;
            }
            console.log("license passed", type != shaka.net.NetworkingEngine.RequestType.LICENSE)
            let responseText = shaka.util.StringUtils.fromUTF8(response.data);
            responseText = responseText.trim();
            responseText = responseText.replace(/["{}]/g, '');
            responseText = responseText.slice(4);
            let ap = window.atob(responseText);
            let l = ap.length;
            let array = new Uint8Array(new ArrayBuffer(l));

            for (let i = 0; i < l; i++)
                array[i] = ap.charCodeAt(i);

            // console.log(responseText, "responseText")
            response.data = shaka.util.Uint8ArrayUtils.fromBase64(responseText).buffer;

        });

        this.player.load(url).then(() => {
        }).catch((error) => {
            console.log("error", error);
        });`

@joeyparrish @avelad @TheModMaker Could you please help me out with this? We are migrating from videojs player to Shaka player. The DASH + Widevine part is done, the issue is with Fairplay + HLS.

Note: The same URL is streaming fine on videojs js player with the same creds(media URL, certificate, license, token, etc).

Buttering-dev commented 9 months ago

I had same problem. when i tried to put this codeshaka.polyfill.PatchedMediaKeysApple.install(); it worked well

lalit-tudip commented 9 months ago

I had same problem. when i tried to put this codeshaka.polyfill.PatchedMediaKeysApple.install(); it worked well

Thanks for the suggestion @Buttering-dev but if you check the code I posted, shaka.polyfill.PatchedMediaKeysApple.install(); is already available in it.

If you don't mind can you share your implementation and remove sensitive details from it (like I did) or check my code to see what changes can i make?

Update:

I'm still facing this blank screen + no audio issue, as only captions are visible. I have also added some event listeners to the player (loadeddata, playing, timeupdate) and all of them are successfully triggering.

@joeyparrish @avelad @TheModMaker What could be the issue here?

lalit-tudip commented 9 months ago

Update: Changing com.apple.fps to com.apple.fps.1_0 and adding initDataTransform to the player config did the trick and the video and audio are accessible.

So now my player supports both HLS + Fairplay and DASH + Widevine. I still need to support the HLS + AES128 and I think for this to work, we just need to pass authorization to aes128 license call which is currently failing.

There was a thread https://github.com/shaka-project/shaka-player/issues/850 regarding the support of AES128 in Shaka. Is it available in the latest version and how can we pass the authorization to the AES128 license call?

In the Videojs player, we get an XHR handler which we can use to intercept the license request and add the authorization to it.

clearInterval(this.addAuth);
this.addAuth = setInterval(() => {
    if (player.tech(true) && player.tech(true).vhs) {
        player.tech(true).vhs.xhr.beforeRequest = (options) => {
            let headers = options.headers || {};
            if (options.uri?.includes("drm/aes-128/license?passphrase")) {
                headers["Authorization"] = `JWT ${testToken.access_token}`;
            }
            options.headers = headers;
            return options;
        };
    }
}, 100);
Screenshot 2023-12-07 at 4 17 27 PM
TheModMaker commented 9 months ago

Please don't mention specific people unless you are working directly with them. I no longer work on the project. Please just post the comment/issue, the team is watching the project and will get a notification anyway and will get to it when they have a chance.

baobao-jane commented 9 months ago

I made sample code of fairplay. you can compare with your code. https://github.com/jane-c-dev/nextjs-shaka-player-example/blob/main/src/component/shaka-player/shaka-player-fairplay-example.tsx

shaka-bot commented 8 months ago

Closing due to inactivity. If this is still an issue for you or if you have further questions, the OP can ask shaka-bot to reopen it by including @shaka-bot reopen in a comment.

lalit-tudip commented 8 months ago

I made sample code of fairplay. you can compare with your code. https://github.com/jane-c-dev/nextjs-shaka-player-example/blob/main/src/component/shaka-player/shaka-player-fairplay-example.tsx

This doesn't work as the license call requires an auth token and this request can't be intercepted with the registerRequestFilter method, as it is made by the player automatically.

@shaka-bot reopen

avelad commented 8 months ago

Using the setting useNativeHlsOnSafari = true implies that it is Safari's own video element that is in charge of managing the video, in this case Shaka only manages the license requests (DRM).

lalit-tudip commented 8 months ago

Using the setting useNativeHlsOnSafari = true implies that it is Safari's video element that is in charge of managing the video, in this case, Shaka only manages the license requests (DRM).

@avelad Can you please elaborate on it? Do I need to use the useNativeHlsOnSafari in my demo app to run HLS + AES128? and how can I pass auth headers to the AES128 DRM license that the player is automatically doing?

If I use the useNativeHlsOnSafari, will it affect my HLS + Fairplay streams?

In the below code for AES128, the code goes into the if condition and for Chrome it successfully intercepts the license request but for Safari the console.log("registerRequestFilter") is not even logging. What could be the issue here and why is the registerRequestFilter not even called?

 if (this.props.isAesChannel) {
            this.player.unload().then(async () => {
                this.player.getNetworkingEngine().clearAllRequestFilters();
                this.player.getNetworkingEngine().clearAllResponseFilters();
                this.player.getNetworkingEngine().registerRequestFilter((type, request) => {
                    console.log("registerRequestFilter");
                    const url = request?.uris[0]?.[0] || request?.uris[0];
                    if (url.includes("drm/aes-128/license")) {
                        console.log("lic...", url, type, shaka.net.NetworkingEngine.RequestType.LICENSE);
                    } else {
                        return;
                    }
                    request.headers['Content-Type'] = 'application/json;api-version=5.1 ';
                    request.headers['Authorization'] = `JWT ${testToken.access_token}`;
                });

                this.player.load(url).then(() => {
                }).catch((error) => {
                    console.log("error", error);
                });
            })
        } else if (!isSafari) {
            this.player.configure({
                drm: {
                    servers: {
                        'com.widevine.alpha': 'URL',
                    },
                    advanced: {
                        'com.widevine.alpha': {
                            'videoRobustness': 'SW_SECURE_CRYPTO',
                            'audioRobustness': 'SW_SECURE_CRYPTO'
                        }
                    }
                }
            })
            this.player
                .getNetworkingEngine()
                .registerRequestFilter(function (type, request) {
                    // Only add headers to license requests:
                    if (type === shaka.net.NetworkingEngine.RequestType.LICENSE) {
                        // This is the specific header name and value the server wants:
                        request.headers["Content-Type"] = "application/octet-stream";
                    }
                });

            try {
                const that = this;
                await this.player.load(url)
                    .then(function () {
                        // This runs if the asynchronous load is successful.
                        console.log("The video has now been loaded! ", shaka);

                        // Code to display the captions;
                        that.player.setTextTrackVisibility(true)
                    })
                    .catch(this.onError);
            } catch (e) {
                console.error(e);
            }
        } else {
            shaka.polyfill.PatchedMediaKeysApple.install();

            this.player.unload().then(async () => {
                // Unregister existing filters
                this.player.getNetworkingEngine().clearAllRequestFilters();
                this.player.getNetworkingEngine().clearAllResponseFilters();
                const certi = await fetch(cert).then(response => response.arrayBuffer());
                let certificate = new Uint8Array(certi)
                let that = this;

                this.player.configure({
                    drm: {
                        servers: {
                            "com.apple.fps.1_0": 'url',
                        },
                        advanced: {
                            'com.apple.fps.1_0': {
                                serverCertificate: certificate,
                            },
                        },
                    },
                });

                this.player.configure('drm.initDataTransform', (initData, initDataType, drmInfo) => {
                    // Custom code
                });

                this.player.getNetworkingEngine().registerRequestFilter((type, request) => {
                    if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
                        return;
                    }
                    // Custom code
                });

                this.player.getNetworkingEngine().registerResponseFilter((type, response) => {
                    if (type !== shaka.net.NetworkingEngine.RequestType.LICENSE) {
                        return;
                    }
                    // Custom code
                });

                this.player.load(url).then(() => {
                }).catch((error) => {
                    console.log("error", error);
                });
            }).catch((e) => this.onError(e))
        }
joeyparrish commented 6 months ago

If you want control over requests, you have to disable "native" HLS on Safari. Otherwise, Safari is in control of everything but DRM licenses, which are distinct from raw AES-128 keys.

Then you will need to have your filter check for RequestType.KEY to intercept requests for AES-128 keys. Because these keys are distinct from DRM licenses (Fairplay), they have their own RequestType.

Does this answer your question?

lalit-tudip commented 6 months ago

Sorry for not updating the thread before, But followed this same approach already where:

Thanks @avelad @joeyparrish for the guidance.