How to Serve HLS Video from an S3 Bucket

This post will describe how to configure an S3 bucket to serve video using HLS.

I’ve also written some example code that creates a bucket and configures it as outlined in this post.

Amazon S3 is a storage solution that allows you to store large amounts of data for relatively little cost, perfect for something like video files, which tend to be quite large. Files (or objects as they are often referred to) are accessed over HTTP, which makes it a great solution for storing (and serving) your HLS videos.

You’ll need an Amazon Web Services (AWS) account to use S3. If you haven’t got one, you can sign up here. Sign in to your account and navigate to the S3 console. Create a bucket. Next, upload your video segments and playlist etc. to the bucket.

Make sure the content type of the playlist and the video segments (.ts) is set to “application/x-mpegURL” and “video/MP2T” respectively. You can do this by selecting the Properties tab for each file and then clicking on Metadata.

Before you can start serving your videos, you need to grant read access to the files in the bucket; files are private by default. Select the Permissions tab and set the bucket policy to the following:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadForGetBucketObjects",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::<your bucket name>/*"
        }
    ]
}

Most browsers don’t support HLS natively, so HLS video playback is typically achieved by using some sort of Javascript-based video player like Video.js, for example. To play an HLS video all you typically need to do is configure the player with the playlist URL and it takes care of the rest. If you serve your videos from the same domain as the video player, there are no additional steps for you to do. However, for the purposes of this post I’m going to assume the video player references the playlist stored in the S3 bucket.

This presents a bit of a problem. If you request a resource (a file) from a different domain, you need permission to do so; it violates the same-origin policy that browsers enforce. Cross-origin resource sharing (CORS) is a mechanism that allows user agents to request permission to access resources that reside on a different server. In this instance, you need to grant permission to the player to allow it to access to the video(s) in the S3 bucket.

Thankfully, Amazon S3 supports CORS so you can selectively allow cross-origin access to the files in your S3 bucket. To enable CORS on your bucket, select the Permissons tab then click on CORS configuration. By default, the configuration file allows access from anywhere. If I wanted to restrict access to the videos to this domain, I would modify the CORS configuration to look like this:

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
   <AllowedOrigin>http://hlsbook.net</AllowedOrigin>
   <AllowedMethod>GET</AllowedMethod>
   <MaxAgeSeconds>3000</MaxAgeSeconds>
   <AllowedHeader>Authorization</AllowedHeader>
</CORSRule>
</CORSConfiguration>

Once you have granted public access to files in the S3 bucket and configured CORS appropriately, you should now be able to serve your HLS videos from an S3 bucket.

Restricting Access

Setting the allowed origin(s) in the CORS policy will prevent somebody from embedding your video on their website if they use a player like VideoJS, but it won’t prevent it if the browser supports HLS natively because in this instance, the same-origin policy doesn’t apply. One thing you can do is check for the presence of the Referer header and if the request hasn’t come from your domain, you block it. You can do this by modifying the bucket policy. Here’s the policy for my bucket:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadForGetBucketObjects",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::net.hlsbook/*"
        },
        {
            "Sid": "Explicit deny to ensure requests are allowed only from a specific domain.",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::net.hlsbook/*",
            "Condition": {
                "StringNotLike": {
                    "aws:Referer": [
                        "http://hlsbook.net/*"
                    ]
                }
            }
        }
    ]
}

This policy will check the referrer header and if it doesn’t match or it’s missing, access to the requested resource will be blocked. (You could include the check in the allowed action but I prefer to use an explicit deny because it overrides any allows.) However, bear in mind that the check can be circumvented because the referrer header can be spoofed.

Unfortunately, Chrome Mobile doesn’t include the referrer header so the above policy will block access to your videos on that particular browser. Fortunately there is a workaround (or “hack” if you prefer).

The solution is to tell the player to use the Media Source Extensions (if the platform supports it) for HLS playback instead of playing it natively. You can do this with VideoJS by setting the overrideNative property to true. If you view the source for this page, you’ll see this:

videojs.options.hls.overrideNative = true;
videojs.options.html5.nativeAudioTracks = false;
videojs.options.html5.nativeTextTracks = false;

var player = videojs('example-video');
player.src('https://s3.eu-west-2.amazonaws.com/net.hlsbook/prog_index.m3u8');

The referrer header will now be included in the requests so access to the video will no longer be blocked on Chrome Mobile.

One More Thing

HLS playlists support the EXT-X-BYTERANGE tag. This indicates to the player that each video segment is part of a (larger) resource. (There’s more about this in the book if you are interested.) Here’s an example from a playlist that uses the tag:

#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10.00000,
#EXT-X-BYTERANGE:808400@0
main.ts
#EXTINF:10.00000,
#EXT-X-BYTERANGE:848068@808400
main.ts
#EXTINF:10.00000,
#EXT-X-BYTERANGE:811784@1656468
main.ts

With the current configuration, any requests for the video file (main.ts) will result in a 403 Access Denied response from S3. To grant access to the file, you need to allow the use of the HTTP Range header. Modify the CORS configuration file so it looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
   <AllowedOrigin>http://hlsbook.net</AllowedOrigin>
   <AllowedMethod>GET</AllowedMethod>
   <MaxAgeSeconds>3000</MaxAgeSeconds>
   <AllowedHeader>Authorization</AllowedHeader>
   <AllowedHeader>Range</AllowedHeader>
</CORSRule>
</CORSConfiguration>

Now your video player will be able to make range requests for parts of the video file.

Example

Finally, here’s an example of playing an HLS video from an S3 bucket:



Conclusion

Amazon S3 is a cost effective solution for storing videos as video files can take up a lot of space. Because access to files in S3 is over HTTP and with support for CORS, it also makes it a viable solution for using it to serve your HLS videos to your viewers.

S3 also has other features that I haven’t mentioned here, such as versioning and archiving, that could be useful for managing your video assets. Check out the S3 documentation for more information.

46 thoughts on “How to Serve HLS Video from an S3 Bucket

    1. Simon Post author

      Sure. I can’t think of a reason why not. The thing you may want to check is latency: the time it takes to copy the segments from the origin to the bucket. You may want to consider using CloudFront as well.

      Reply
  1. anil v

    Thank you simon,
    But we observed delays in response and the number of requests per second 100 concurrent sessions only supporting.

    Reply
    1. Simon Post author

      You are right, there are limits to how many requests an S3 bucket can serve. (More info here.) If the number of requests you need to serve at any one time exceeds the limit then you’ve answered your original question: you can’t use an S3 bucket for live streams, unless you use a CDN like CloudFront.

      Reply
  2. Leonardo Hammer

    I put m3u8 file and *.ts files into AWS S3, it’s all private. When I pre-signed the m3u8 file, open url, I got error with .ts file, 403 error. How can I fix it? Thanks help.

    Reply
    1. Simon Post author

      This is to be expected. If the bucket is private, you will need to create pre-signed URLs for each .ts file as well as the m3u8 file; otherwise, you will get 403 errors.

      An alternative solution is to encrypt the .ts files, put them in a public bucket, then generate a pre-signed URL for the decryption key. The advantage to this approach is that you only have to create a pre-signed URL for the playlist and the decryption key and not every .ts file.

      Reply
      1. Matt

        You can also set the behavior of the cloudfront instance to allow subsequent *.ts requests to passthrough w/o additional authorization

        Reply
        1. Ekaterina tsapaeva

          Hello Matt,

          Could you please give some details of how I can utilize this approach? I am working on securing videos on our S3 cloud and this thing sounds like exactly that we need. Is there any way cloudfront can recognise subsequent request coming from the same user that initial m3u8 file?

          Thank you,
          Ekaterina

          Reply
          1. Puneet

            Hello Ekaterina,

            We also need to serve the *.ts files from inside a private bucket. Were you able to find a solution? Please share with me.

            Thanks!
            Puneet

          2. Otsubo

            We faced the same problem but downloaded only the m3u8 file and rewrote the .ts file references to the signed URL which is part of the content of the m3u8 file and played it.

      2. ASam

        Hey Simon
        Thanks a lot for this post.
        I’ve an m3u8 file and m4s file (using single fragmented mp4 rather than multiple .ts file) uploaded on S3.
        I can generate presigned url for both the m3u8 file and the m4s file, is it possible to specify to the hls player, the URL to query for the video file?
        Thanks a lot!

        Reply
  3. Betty

    When i open this page on my Android and Mobile Chrome 72, this video cannot be loaded. This is only on Android/Chrome. Does anyone have the same problem?

    Reply
    1. Simon Post author

      It should work now. I’d included a check for the referrer header in the bucket policy to prevent hotlinking. After looking at the access logs, it appears that Mobile Chrome 72 doesn’t include this header in the request so access to the files was denied.

      Reply
        1. Simon Post author

          I’ve updated the article to include the bucket policy with the referrer header check and also a workaround for Chrome Mobile.

          Reply
  4. Onur

    Hey thank you for this. I have implemented this. Everthing works fine on (FireFox, Safari), however if I run it on Chrome the movie is not loading. If I however run Chrome in Incognito mode it will start the movie from Chrome as well? Any Idea?

    Reply
  5. Solana

    Hi Simon! This article is very clear and I’ve implemented it.
    With a regular hls video works fine, but with an encrypted one (I followed “how-to-encrypt-hls-video-with-ffmpeg” instructions ) it doesn’t work.

    In the -hls_key_info_file I specified my url as the the URI of the key, it look like this:

    https://arquitecturabkp/enc.key
    enc.key
    ecd0d06eaf884d8226c33928e87efa33

    So I save a copy of the enc.key file in my server and save my hls files (the .ts and the m3u8 one) in an Amazon S3 bucket, but it doesn’t load in the player … What Am I missing?

    Reply
    1. Simon Post author

      I tested the set-up you described and it worked for me. I used VideoJS for the player.

      Here are a few things you can try:

      – Use Developer tools to check for any errors.

      – Clear your browser cache

      – Check the URL of the key in the playlist. It should be absolute and the domain name (your server) should be resolvable.

      – If you are serving your pages over HTTPS and you are referencing a player hosted by a third-party (e.g. CDN), make sure the URL to the player is over HTTPS too or your browser may block access to it (mixed content).

      Reply
  6. Julio

    Hello Simon, it is not possible to play the video on Brave browser. I am facing a similar problem If you can share the way you solve it, I’ll really appreciate it!

    Reply
    1. Simon Post author

      The reason the video doesn’t play in Brave is because access to the playlist is blocked. When shields are up, Brave sets the referrer header on the XHR request to “https://s3.eu-west-2.amazonaws.com/”. When shields are down, the referrer header is set to “https://hlsbook.net/how-to-serve-hls-video-from-an-s3-bucket/”, which is what it should be. I have a bucket policy on the bucket that checks the value of the referrer header and if it doesn’t match the expected value it blocks access to the files (see Restricting Access). If you turn shields down, you should be able to play the video.

      Reply
  7. Onur

    Hey Simon,

    I was using your solution. It works in the browser, but not on the SmartTV browser…
    I get following error:

    Fialed to execute ‘appendBuffer’ on ‘SourceBuffer’: The SourceBuffer is full, and cannot free space to append additional buffers

    Thank you in Advance…

    Best regards stay healthy

    Reply
  8. yves

    I didn’t use AWS VOD CF FORMATION.
    It is a custom cloudfront distribution and custom elastic transcoder job.

    The transcoded files are stored in a private s3 bucket and He use CloudFront to distribute these files by sgned url.
    All work like charm when it not a video distribution.

    I can read image file, html file except video streaming.

    The first request to myplaylist.m3u8 it ok with 200 status code but the second file return 403 status code and try to get this second file again and again.

    When a read directly a mp4 file it ok

    Please we need your help.

    Reply
    1. Rob Little

      I have this same setup and config, and have the same issue. I can’t figure out how to return the subsequent files from the initial m3u8 register file, using a secure url, when safari can’t set the hls.xhr.beforeRequest to achieve this. help please

      Reply
      1. Simon Post author

        I outlined the issue in a previous comment.

        One possible solution is to create a separate cache behaviour for the playlist and only restrict access to it (using signed URLs), not the segments. If somebody can’t access the playlist then they won’t know what the URL of the media segments will be. However, this isn’t the most secure solution because if somebody can guess the URL of the media segments then they’ll be able to access them freely.

        If you want something more robust, then you could take a look at using CloudFront and Lambda@Edge. Here’s an example that uses Cognito to restrict access to files in a CloudFront distribution.

        Reply
  9. Gabriel

    Hello Simon !

    Thank you for the post.

    I getting this exception in console of browsers:

    “Refused to load media from ‘blob:https://…’ because it violates the following Content Security Policy directive: “default-src * data: ‘unsafe-eval’ ‘unsafe-inline'”. Note that ‘media-src’ was not explicitly set, so ‘default-src’ is used as a fallback.”

    I tried several solutions, including adding meta tags on site http-equiv = “Content-Security-Policy”. Do you have any tips?

    Reply
  10. Ubaid Ullah

    Hello Simon,

    I have implemented your article but Internet Download Manager still able to download my video. Can you please guide how can I prevent the user from the download.

    Reply
    1. Simon Post author

      The purpose of the section on restricting access was to show how to prevent other websites from embedding your videos. I don’t know how Internet Download Manager works but you could try using something like HTTP Basic authentication or setting a cookie for example.

      Reply
  11. Kirk

    Hi Simon,

    I followed your article. I have set the *m3u8 anf *.ts files with the relevant metadata with buck policy and CORS set to https://hlsplayer.net/. However, hlsplayer.net is not able to play. Getting a network error.

    What am i missing?

    Reply
  12. Jesus Salazar

    Hello, thank you very much for the content it helped me a lot. A query. Is there any other way to make chrome mobile include the referrer header ?. Since I use my page it is in wordpress and I use a player plugin and I cannot make it work in chrome mobile. If you could help me I would be grateful. Excuse the google translation. Greetings!

    Reply
  13. Laurent

    Hello Simon, thanks for this very clear post ! Works beautifully !
    However, the security part with the “Deny” bucket policy has a side effect on my Laravel app : my users cannot upload anymore any files to the S3.
    I setup the CORS with “AllowedMethods”: [“GET”, “POST”], of course, but I get an error 500. This is really weirds as my users do the POST via the only domain / referrer allowed by the Bucket Policy.
    If I delete the Deny Policy
    {
    “Sid”: “Explicit deny to ensure requests are allowed only from a specific domain.”,
    “Effect”: “Deny”,
    “Principal”: “*”,
    “Action”: “s3:GetObject”,
    “Resource”: “arn:aws:s3:::awkn/*”,
    “Condition”: {
    “StringNotLike”: {
    “aws:Referer”: [
    “https://mydomain.com*”
    ]
    }
    }
    },
    Then all of the sudden I get my upload feature working again… But of course anybody can then access the S3 files with their direct url, via any browser…

    Any idea what could be wrong ?

    Reply
  14. Ankit

    In flutter how to play a private s3 video url. Currently if video url is public it is working fine but with private s3 url it is not working

    Reply
  15. Madiyor Abdukhashimov

    Hi, I think this post’s comment section is still active
    Thanks for sharing the way to stream video through S3. I see you are playing encrypted video. How to play AES-128 encrypted video with video js. Any instructions and resources will be awesome.

    Reply
  16. Wilson Li

    Hello Simon, I have followed your steps and have setup a livestream using IVS and recorded the video to be store in s3 Bucket. I have also setup Cloudfront, but when trying to playback using the master.m3u8 as suggested by Amazon Documentation, nothing is being played. Upon inspecting the m3u8 file, I see that the resolution does not point to any playlist m3u8.

    This is what my m3u8 file shows:
    #EXTM3U
    #EXT-X-SESSION-DATA:DATA-ID=”net.live-video.customer.id”,VALUE=”361107206646″
    #EXT-X-SESSION-DATA:DATA-ID=”net.live-video.content.id”,VALUE=”eRzklk7YtD58″
    #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=”1080p”,NAME=”1080p”,AUTOSELECT=YES,DEFAULT=YES
    #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=4502400,CODECS=”avc1.428028,mp4a.40.2″,RESOLUTION=1920×1080,VIDEO=”1080p”
    1080p/playlist.m3u8
    #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=”720p30″,NAME=”720p”,AUTOSELECT=YES,DEFAULT=YES
    #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2415000,CODECS=”avc1.4D401F,mp4a.40.2″,RESOLUTION=1280×720,VIDEO=”720p30″
    720p30/playlist.m3u8
    #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=”480p30″,NAME=”480p”,AUTOSELECT=YES,DEFAULT=YES
    #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1469999,CODECS=”avc1.4D401F,mp4a.40.2″,RESOLUTION=852×480,VIDEO=”480p30″
    480p30/playlist.m3u8
    #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=”360p30″,NAME=”360p”,AUTOSELECT=YES,DEFAULT=YES
    #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=630000,CODECS=”avc1.4D401F,mp4a.40.2″,RESOLUTION=640×360,VIDEO=”360p30″
    360p30/playlist.m3u8
    #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=”160p30″,NAME=”160p”,AUTOSELECT=YES,DEFAULT=YES
    #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=230000,CODECS=”avc1.4D401F,mp4a.40.2″,RESOLUTION=284×160,VIDEO=”160p30″
    160p30/playlist.m3u8

    and this is what I get when I try to lay this file:
    amazon-ivs-wasmworker.min.js:5 HTTP Response Error: TypeError Failed to fetch
    amazon-ivs-wasmworker.min.js:5 Player stopping playback – error MasterPlaylist:9 (ErrorNetworkIO code -1 – Failed to fetch)

    Would you happen to know what I am doing wrong ?

    Thank you

    Reply
  17. Pablo Batista

    Hi bro, i config all. But work only audio
    see – https://www.hlsplayer.net/#type=m3u8&src=https%3A%2F%2Fagorastoragecomunidade.s3.amazonaws.com%2Fcomunidadetadashi%2F26Fevereiro6horas%2Fd56ed5f72f4d1e893d4575b8b0b21f61_1674740885909x561282782993317900__uid_s_10235__uid_e_audio.m3u8

    Video no play See: https://www.hlsplayer.net/#type=m3u8&src=https%3A%2F%2Fagorastoragecomunidade.s3.amazonaws.com%2Fcomunidadetadashi%2F26Fevereiro6horas%2Fd56ed5f72f4d1e893d4575b8b0b21f61_1674740885909x561282782993317900__uid_s_10235__uid_e_video.m3u8

    You help me? please

    Reply
    1. Simon Post author

      I had a look at your video playlist and you are using a container format that isn’t supported (webm) and a codec that also isn’t supported (VP8) by HLS. You must encode your video with H.264 or HEVC/H.265. For H.264 encoded video the container format must be MPEG transport stream (like your audio stream) or fragmented MP4. The container format for HEVC/H.265 must be fMP4.

      Reply
  18. Alok

    Hi Simon,
    thank you for this. We have implemented same architecture and serving videos through cloud front. But some users are complaining about buffering issues and when I have checked console then it’s showing some .ts file requests are cancelled while it was buffering. We already tried clearing browser cache, changing internet provider etc but it sometimes work but sometimes doesn’t work. Any suggestion will be greatly appreciated.

    Reply

Leave a Reply to Jesus Salazar Cancel reply