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
});
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:
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:
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򕊑 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):
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With