Stream AWS S3 HLS Videos in iOS Browsers

青春壹個敷衍的年華 提交于 2021-02-02 09:26:18

问题


How can I stream HLS(.m3u8) in iOS Safari Browsers? My videos are stored in AWS S3 Bucket and the only way to access the video and audio .m3u8 is to pass a signed URL.

I am using videojs to stream videos. videojs.Hls.xhr.beforeRequest is not working on iOS browsers. I also read that MSE is not supported in iOS, is there any alternative I can use to pass a signed URL to be able to stream my videos on iOS browsers?

Here are my sample codes and screenshot of error:

videojs.Hls.xhr.beforeRequest = function(options) {

               
                if (options.uri.includes('Audio')) {
                    options.uri = options.uri + '?Policy=' + policy + '&Key-Pair-Id=' + keyPairId + '&Signature=' + signature;

                }
                else if (options.uri.includes('Video')) {
                    options.uri = options.uri + '?Policy=' + policy + '&Key-Pair-Id=' + keyPairId + '&Signature=' + signature;
                   
                }

                return options
}

var overrideNative = false;

var player = videojs('video-test', {
  "controls": true,
        "fluid": true,
        "preload": 'none',
        "techOrder": ["html5"],
        "html5": {
            "hls": {
                "withCredentials": true,
                 overrideNative: overrideNative,
                        
                 
                }, 
        },
            nativeVideoTracks: !overrideNative,
            nativeAudioTracks: !overrideNative,
            nativeTextTracks: !overrideNative
        });

player.src(
{
   src: url, type: "application/x-mpegURL", withCredentials: true
});


回答1:


Exact same issue, except implemented in ReactJS the videojs vhs overrides do not work, as it has to do with Safari and the parsing (or not) of the options to see the security parameters for subsequent calls past the register m3u8.

There are a few other people dealing with this, such as https://github.com/awslabs/unicornflix/issues/15

i've tried everything, from amazon IVS+VideoJS attempts, to re-writing my class modules as functional to try examples I've found; and basically always end up right back at this issue

---------------UPDATE BELOW--------------- (and grab a comfy seat)

Delivering protected video from S3 via Cloudfront using secure cookies (for iOS based browsers + all Safari) and secure urls for Chrome and everything else.

website architecture:

  • Frontend: ReactJS
  • Backend: NodeJS
  • cloud service architecture: https://aws.amazon.com/blogs/media/creating-a-secure-video-on-demand-vod-platform-using-aws/ (and attached lab guide)

Presumptions: equivalent setup to above cloud architecture, specifically the IAM configuration for CF to S3 bucket, and the related S3 security configurations for IAM and CORS.

TL/DR:

NON-SAFARI aka Chrome etc - use secure urls (VERY easy OOTB); the above guide worked for chrome, but not for safari.

Safari requires secure cookies for streaming hls natively, and won’t let recognize xhr.beforeRequest overloads at all. SAFARI / iOS BROWSERS BASED ON SAFARI - use secure cookies Everything below, explains this.

Setting cookies, is easy enough sounding! Its probably why there is no end to end example anywhere in AWS CloudFront, AWS Forums, or AWSDeveloper Slack channel, that its presumed to be easy because, hey its just cookies right?

Right. END TL/DR

Solution Details

The ‘AH-HA!’ moment was finally understanding that for this to work, you need to be able to set a cookie for a cloudfront server, from your own server, which is basically an enormous web security no-no. aka - ‘domains need to be the same, all the way down/up the network call’

comments here https://jwplayer-support-archive.netlify.app/questions/16356614-signed-cookies-on-cloudfront-with-hls-and-dash

and here link https://www.spacevatican.org/2015/5/1/using-cloudfront-signed-cookies/

both combined with original AWS documentation about signed cookies with a cname of a domain to apply to subdomains, all combined for me finally.

The solution is:

  1. setup a CNAME for your cloudfront instance; ie: you can’t set a cookie against 5j1h24j1j.cloudfront.net as you don’t own it, but you can CNAME something like cloudfront.<your-domain>.com in your DNS. good docs exists for this particular step at https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-cloudfront-distribution.html
  2. Important - you need to also setup a reference for this CNAME in your CF Distribution. (if you want SSL, you need to re-sign your domain cert with cloudfront.<your-domain>.com, then upload this cert to AWS Certificate Manager, so it will be referencable in the CF Distribution edit screen drop down list (which, you can’t save unless you select something).
  3. for your local development box, set up a hosts file overload for whatever the NodeJS listening/bound IP is. ie: if your node is bound to 0.0.0.0, then edit your /etc/hosts to have a line 0.0.0.0 dev.<your-domain>.com - when you deploy to your production host, the domain will obviously work there.
  4. Now, in your backend (aka server side) code, where you will set the cookies, you need to set the domain parameter, and you can’t directly wildcard but you can leave it as <your-domain.com>(which in a browser, if you inspect using developer tools, you will see listed as .<your-domain>.com NOTE THE LEADING DOT. This is fine, and expected behaviour for a modern browser; essentially saying ‘any subdomain of <your-domain>.com will have these cookies accessible for.

What the above does, is make sure that END TO END, your are able to send the cookie, assigned to the .<your-domain>.com from a call starting in dev.<your-domain>.com or your future production <your-domain>.com through to the same uri but on a different port for your backend, then on to CF via your CNAME which is a subdomain the cookie can see now. At this point, its up to CF to pass on the required headers to the S3 instance.

But wait, there is more to do client side first. A thing that blocked me even seeing the cookies in the first place, was the fact they don’t get set unless the requestor/initiator uses a ‘withCredentials: true’ flag in the network call that starts it. In my code, that is a ReactJS componentDidMount() based Axios network REST GET call to my backend nodeJS endpoint for the video list (which the nodeJS gets from graphQL in AWS, but thats not needed for this explanation of my fix).

componentDidMount() {
        axios.get('http://dev.<your-domain>.com:3000/api/my-data-endpoint'
        ,{
          withCredentials: true,
        })
         .then(vidData => {
          this.setState({
            ....//set stuff  for player component include to use
          });

        })
    }

When my axios call did not have ‘withCredentials: true’, the cookies were never sent back; as soon as i had that? my cookies were at least sent back to the first caller, localhost (with no domain parameter in the cookie, it defaults to calling, which i had as local host at the time), which therefore meant it would never pass it to CF, which was the 2435h23l4jjfsj.cloudfront.net name at that point.

So, updating axios to use dev.<your-domain>.com for server access, and the withCredentials flag, my cookies were set, on the call to my backend info about the videos. As AWS documentation does point out, the cookies need to be fully set BEFORE the call for secure content, so this is accomplished.

In the above described call to my api, i get back something like

{src:’https://cloudfront.<your-domain>.com/path-to-secure-register-m3u8-file’, qps:’?policy=x&signature=y&key-pair-id=z’, blah blah}

[sidebar - signed urls are all generated in the cloud by a lambda] For Chrome, the player code will append the two together, then Wherever you instantiate your video.js player, overload the videojs.Hls.xhr.beforeRequest as follows

videojs.Hls.xhr.beforeRequest = function (options) {
  options.uri = `${options.uri}${videojs.getAllPlayers()[0].options().token}`;
  return options;
 };

which puts the query string of ?policy=x&signature=y&Key-Pair-ID=z on the end of every sub-file in the stream after the register m3u8 file kicks it off.

the backend call to the api described above, also tears apart the QP’s to set the cookies before the json is sent as a response, as follows

res.cookie("CloudFront-Key-Pair-Id", keypair, {httpOnly: true, path: "/", domain: ‘<your-domain>.com'});
res.cookie("CloudFront-Signature", sig, {httpOnly: true, path: "/", domain: ‘<your-domain>.com'});
res.cookie("CloudFront-Policy", poli, {httpOnly: true, path: "/", domain: ‘<your-domain>.com'});

INTERRUPT - now we have set withCredentials to true, you probably see CORS issues; fun. in your server side code (my reactJS) i set a few headers in my nodejs router

res.header("Access-Control-Allow-Credentials", "true");
res.header("Access-Control-Allow-Origin", "http://dev.<your-domain>.com:8080"); // will be set to just <your-domain>.com for production

At this point, stuff still wasn’t working though. This is because the cloud code was putting the CF 234hgjghg.cloudfront.net domain into the policy, and not my CNAME mapping. I updated this in the cloud. So now my calls for video data, returned urls to the secure m3u8 using cloudfront.<your-domain>.com and not the cloudfront.net which is described here https://forums.aws.amazon.com/thread.jspa?messageID=610961&#610961 in the last response step 3.

At THIS point, if i used safari debug tools, I knew i was close, because my responses to attempted streaming changed from the no key or cookie xml, to

<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>

error, and in it, was a reference to my S3 bucket. This meant to me, that my CF distribution was essentially happy with the cookie based policy, key-id, and signature, and had passed me on to S3, but S3 told me to get lost.

The good thing at this point though, was that the 3 required cloudfront cookies were set from dev.<your-domain>.com all the way through to the cloudfront.<your-domain>.com calls for the m3u8 register file, and then in all the subsequent calls to a .ts or .m3u8

OK, so I spent a bit of time in the s3 config (not editing anything, just reviewing everything… which looked 100% fine to me), and then went back to CF distribution behaviours edit page, where you setup headers to forward. settings (listed below, then a screenshot of mine):

  • cache and origin request settings: use legacy cache settings
  • cache based on selected request headers - whitelist
    • add origin, access-control-request-headers, access-control-request-method. you will need explicitly type the last 2 in, they didn’t auto-complete for me nor show in the suggestion list, but add custom button worked.
    • object caching: use origin cache headers
    • forward cookies/query strings - none(improves caching) on both
    • restrict viewer access (use signed urls or cookies) - yes (this is the entire point of this headache lol)
    • trusted signer, self

After the distribution had saved and propagated, Safari and Chrome video playing both worked!

This was quite a rabbits hole and a degree (or 15) more difficult than I anticipated, but of course once its all written out, it all seems so logical and obvious. I hope this at least partially helps the others i found on the internet with secure streaming private content across all major browsers using AWS Cloudfront infront of S3




回答2:


This seems promising but I am still trying to figure out what the hls version of this looks like (this is an example for dash): https://github.com/videojs/video.js/issues/5247#issuecomment-735299266



来源:https://stackoverflow.com/questions/64166589/stream-aws-s3-hls-videos-in-ios-browsers

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!