videojs / videojs-contrib-eme

Supports Encrypted Media Extensions for playback of encrypted content in Video.js
Other
200 stars 71 forks source link

FairPlay: ArrayBuffer length minus the byteOffset is not a multiple (but other issues) #118

Open AwokeKnowing opened 4 years ago

AwokeKnowing commented 4 years ago

Hello, I've been using videojs for years and videojs-contrib-eme for Widevine for a while. Now I'm trying to get FairPlay working.

I'm getting 'array errors' though, when trying to play the video. I'm using default videojs / eme settings. It gets the cert, but not the license.

https://videojs-http-streaming.netlify.app/?debug=true&autoplay=false&muted=false&minified=false&liveui=false&partial=false&url=https%3A%2F%2Fcdn0.knowledgecity.com%2Fvendors%2Fvideojs-contrib-eme%2FFPSTEST2%2Fen%2FFPSTEST2-m2u1p%2Ffp_hls%2F1587654321%2FFPSTEST2-m2u1p--v123445.m3u8&type=application%2Fx-mpegurl&keysystems=%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22com.apple.fps.1_0%22%3A%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22certificateUri%22%3A%20%22https%3A%2F%2Fcontentlicense-js9gan4za5h4mz7f6.knowledgecity.com%2Ffpscert%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22licenseUri%22%3A%20%22https%3A%2F%2Fcontentlicense-js9gan4za5h4mz7f6.knowledgecity.com%3Fassetid%3D173d970b1d8eba9687b21d403e9854c3%22%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%7D

It is pulling the cert correctly, but the initData seems to come as utf8 yet the arrayToString method assumes utf16. That seems to be the error at least at the start. I can fix it by manually chaniging the implementation of arrayTostring:

var uint8ArrayToString = function uint8ArrayToString(array) {
    return String.fromCharCode.apply(null, new Uint16Array(array.buffer));
  };

in the above if I change to Uint8Array it actually parses to string. So maybe the above method has a bug.

Anyways, if I fix that, later along the lines I also get an array error when concatenating the data id and cert.

For the array errors, I think it may have to do with this:

https://github.com/videojs/videojs-contrib-eme/pull/86

I'm not sure how to add the sinf to keysystems so havn't tried that.

it also seems related to what shaka packager fixed recently:

https://github.com/google/shaka-player/commit/25bea9fac68c2b4fc405878ad1226417e996ec2e

I generated the files like this:

in=/media/FPSTEST2-m2u1p.mp4,stream=audio,output=/media/fp_hls/1587654321/FPSTEST2-m2u1p_audio.mp4,drm_label=AUDIO \
in=/media/FPSTEST2-m2u1p.mp4,stream=video,output=/media/fp_hls/1587654321/FPSTEST2-m2u1p_video.mp4,drm_label=HD \
--protection_scheme cbcs \
--enable_raw_key_encryption \
--keys label=AUDIO:key_id=557e2e0d1a5529a6d2da3af090c54b88:key=841f3536dc9b7c158cb56d633866cc5b,label=HD:key_id=557e2e0d1a5529a6d2da3af090c54b88:key=841f3536dc9b7c158cb56d633866cc5b \
--protection_systems FairPlay \
--iv ae80c2eee02548246b8a7627dd5d8e35 \
--hls_master_playlist_output /media/fp_hls/1587654321/FPSTEST2-m2u1p.m3u8 \
--hls_key_uri skd://173d970b1d8eba9687b21d403e9854c3

I am viewing this on: Safari 13.1.2 (desktop)

Another note is that if fix the init data with Uint8 then later at the concat part, the next error is here:

var idArray = new Uint16Array(buffer, offset, id.length);

the above is where it's concating the initdata id and cert. It throws an error that the offset is not aligned

The other thing I will mention is that while it's common to send the license request to/from the license server in b64, in this case I noticed the default methods in videojs (fairplay.js) were sending in binary, so currently the license server is expecting a binary request/response rather than the typical spc= / .. serialized method. However, at this point it's not even getting to the getlicense part because it can't seem to decode the init data. I think my next step at moment is to wrangle the bytes here and handle the skd extraction which honestly should be in the default methods.

any help is appreciated.

Fundamentally, I'm just trying to encode videos with FairPlay and play back.

AwokeKnowing commented 4 years ago

So it seems that contrib-eme only supports default .ts encoded streams which with the prefixed eme would generate the normal skd:// initData instead of the sinf initData.

I'm reading through : https://www.w3.org/TR/encrypted-media/#mediaencryptedevent for info on handling the sinf style

AwokeKnowing commented 3 years ago

Well it seems the deal with we getting the sinf init data instead of skd:// was caused by me setting video/mp4 as mime type instead of application/x-mpegURL.

in the end I found it's totally possible to do MSE in safari but as it doesn't really work on iPhone at moment, the skd:// form is better and is currently the vjs default.

So concretely, this page worked perfectly for me :

<!DOCTYPE html>
<html>
<head></head>
<body>
<link rel="stylesheet" href="https://vjs.zencdn.net/7.6.6/video-js.min.css">
<script src="https://vjs.zencdn.net/7.6.6/video.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/videojs-contrib-eme@3.7.0/dist/videojs-contrib-eme.js"></script>
<video preload="auto" width=960 id="myvideo" class="video-js vjs-default-skin" controls data-setup='{"plugins":{"eme":{}}}'></video>
<script>
    var serverProcessSPCPath  = 'https://fps.exmple.com'; 
    var serverCertificateURI = 'fps.cer';
    var m3u8Url = 'https://cdn.example.com/video.m3u8';
    player = videojs('myvideo');
    player.src({
        src: m3u8Url,
        type: 'application/x-mpegURL',
        keySystems: {
            'com.apple.fps.1_0': { 
                certificateUri: serverCertificateURI,
                getContentId: function(emeOptions, initData) {
                    return new TextDecoder('utf-16').decode(initData.slice(16));
                },
                getLicense: function(emeOptions, contentId, spc, callback)
                {
                    fetch(serverProcessSPCPath, {
                        method:  "post",
                        headers:{"Content-type":"application/x-www-form-urlencoded"},
                        body:    'spc='+encodeURIComponent(btoa(String.fromCharCode(...spc))) +
                                 '&assetId='+encodeURIComponent(contentId)
                    }).then(r=>r.text().then(ckcText => {
                        let b64key = ckcText.trim().slice(5,-6);// get AAA= from <ckc>AAA=</ckc>
                        let key = new Uint8Array(atob(b64key).split('').map(c=>c.charCodeAt(0)));
                        callback(null, key)
                    }));
                }
            } 
        }
    });
</script>
</body>
</html>

maybe a full example like the above would be good to post somewhere, though I understand that getLicense is not the same for everyone, but for people who get it without ckc or whatever it should be super simple to change.

I imagine in the end there's probably only 6 or 7 different common formats and maybe vjs can just build them all in, and have an atribute to select eg {responseIsb64WrappedWithCKCTag=true}