---
title: "Self-hosting videos with HLS"
description: "Putting videos on YouTube is a convenient solution to host a video, but not a privacy-friendly one. Using HLS, it's possible to self-host videos while being bandwidth-friendly."
date: 2018-02-07
author: "Vincent Bernat"
tags: ["tutorial"]
url: https://www.exoscale.com/blog/self-hosted-videos/
---
# Self-hosting videos with HLS

**Quick summary of this article**

- Self-hosting videos preserves privacy compared to YouTube but increases setup complexity and bandwidth costs.
- Use HLS for widest compatibility today; MPEG-DASH is technically superior but less natively supported.
- Adaptive streaming needs media segments, per-variant media playlists, and a master playlist to coordinate variants.
- FFmpeg or the video2hls script can produce segments, playlists, a progressive MP4, and a poster image.
- Serve files with correct MIME types, CORS headers, and optional gzip compression using object storage like Exoscale.

Hosting videos on [YouTube][] is convenient for several reasons:
pretty good player, free bandwidth, mobile-friendly, network effect
and, optionally, no ads. On the other hand, this is one of the less
privacy-friendly solution. Most other providers share the
same characteristics, except the ability to disable ads for free.

![Self-hosting videos](https://www.exoscale.com/blog/self-hosted-videos/self-hosted-videos-with-hls.jpg)

With the [`<video>` tag][video], self-hosting a video is
simple:

```html
<video controls>
  <source src="../videos/big_buck_bunny.webm" type="video/webm">
  <source src="../videos/big_buck_bunny.mp4" type="video/mp4">
</video>
```

However, while it is possible to provide a [different videos depending
on the screen width][], **adapting the video to the available
bandwidth** is trickier. There are two solutions:

 - HLS ([RFC 8216][], informational), and
 - MPEG-DASH ([ISO/IEC 23009-1:2014][]).

They are both **adaptive bitrate streaming protocols**: the video is
sliced in small segments and made available at a variety of different
bitrates. Depending on current network conditions, the player
automatically selects the appropriate bitrate to download the next
segment.

**HLS** was initially implemented by Apple but is now also supported
natively by Microsoft Edge and Chrome on Android. [hls.js][] is a
JavaScript library bringing HLS support to other browsers. **MPEG-DASH**
is technically superior (codec-agnostic) but only works through a
JavaScript library, like [dash.js][]. In both cases, support of the
[Media Source Extensions][] is needed when native support is
absent. Safari on iOS [doesn't have this feature][caniuse] and
therefore cannot use *MPEG-DASH*. Consequently, the most compatible
solution is currently *HLS*.

## Encoding

To serve HLS videos, you need three kinds of files:

 - the **media segments** (encoded with different bitrates/resolutions),
 - a **media playlist** for each variant, listing the media segments, and
 - a **master playlist**, listing the media playlists.

Media segments can come in two formats:

 - MPEG-2 Transport Streams (TS), or
 - Fragmented MP4.

Fragmented MP4 media segments are supported since iOS 10. They are a
bit more efficient and can be reused to serve the same content as
MPEG-DASH (only the manifests are different). However, if you want to
target older versions of iOS, you need to stick with MPEG-2 TS.

[FFmpeg][] is able to convert a video to media segments and generate
the associated media playlists. [Peer5's documentation][] explains the
suitable commands. I have put together an handy (Python 3.6) script,
[video2hls][], stitching together all the steps. After executing it on
your target video, you get a directory containing:

 - media segments for each resolution (`1080p_1_001.ts`, `720p_2_001.ts`, ...)
 - media playlists for each resolution (`1080p_1.m3u8`, `720p_2.m3u8`, ...)
 - master playlist (`index.m3u8`)
 - progressive (streamable) MP4 version of your video (`progressive.mp4`)
 - poster (`poster.jpg`)

The script accepts a lot of options for customization. Use the
`--help` flag to discover them. Run it with `--debug` to get the
`ffmpeg` commands executed with an explanation for each flag. For
example, the poster is built with this command:

```sh
ffmpeg \
  `# seek to the given position (5%)` \
   -ss 4 \
  `# load input file` \
   -i ../input.mp4 \
  `# take only one frame` \
   -frames:v 1 \
  `# filter to select an I-frame and scale` \
   -vf 'select=eq(pict_type\,I),scale=1280:720' \
  `# request a JPEG quality ~ 10` \
   -qscale:v 28 \
  `# output file` \
   poster.jpg
```

## Serving

So, we got a bunch of static files we can upload anywhere. Yet two
details are important:

 - When serving from another domain, CORS needs to be configured to
   allow `GET` requests. Adding `Access-Control-Allow-Origin: *` to
   response headers is enough.
 - Some clients may be picky about the MIME types. Ensure files are
   served with the ones in the table below.

Kind              | Extension | MIME type
----------------- | --------- | --------------
Playlists         | `.m3u8`   | `application/vnd.apple.mpegurl`
MPEG2-TS segments | `.ts`     | `video/mp2t`
fMP4 segments     | `.mp4`    | `video/mp4`
Progressive MP4   | `.mp4`    | `video/mp4`
Poster            | `.jpg`    | `image/jpeg`

Let's host our files on [Exoscale's Object Storage][] which is
compatible with S3 and located in Switzerland. As an example, the
[Tears of Steel][video] video is about 2 GiB (six sizes for HLS,
including 4K, and one progressive MP4). It would cost us about
0.09 € per month for storage and 6.35 € for bandwidth if 1000 people
watch the 1080p version from beginning to end—unlikely.

We use `s3cmd` to upload files. First, you need to recover your [API
credentials][] from the portal and put them in `~/.s3cfg`:

```ini
[default]
host_base = sos-ch-dk-2.exo.io
host_bucket = %(bucket)s.sos-ch-dk-2.exo.io
access_key = EXO.....
secret_key = ....
use_https = True
bucket_location = ch-dk-2
```

The second step is to create a bucket:

```console
$ s3cmd mb s3://hls-videos
Bucket 's3://hls-videos/' created
```

You need to configure the CORS policy for this bucket. First, define
the policy in a `cors.xml` file (you may want to restrict the allowed
origin):

```xml
<CORSConfiguration>
 <CORSRule>
   <AllowedOrigin>*</AllowedOrigin>
   <AllowedMethod>GET</AllowedMethod>
 </CORSRule>
</CORSConfiguration>
```

Then, apply the policy to the bucket:

```console
$ s3cmd setcors cors.xml s3://hls-videos
```

The last step is to copy the static files. For each video, inside the
directory containing all the generated files, use the following
command:

```sh
while read extension mime gz; do
  [ -z "$gz" ] || {
    # gzip compression (if not already done)
    for f in *.${extension}; do
      ! gunzip -t $f 2> /dev/null || continue
      gzip $f
      mv $f.gz $f
    done
  }
  s3cmd --no-preserve -F -P \
        ${gz:+--add-header=Content-Encoding:gzip} \
        --mime-type=${mime} \
        --encoding=UTF-8 \
        --exclude=* --include=*.${extension} \
        --delete-removed \
    sync . s3://hls-videos/video1/
done <<EOF
m3u8  application/vnd.apple.mpegurl true
jpg   image/jpeg
mp4   video/mp4
ts    video/mp2t
EOF
```

The files are now available at
`https://hls-videos.sos-ch-dk-2.exo.io/video1/`.

## HTML

We can insert our video in a document with the following markup:

```html
<video poster="https://hls-videos.sos-ch-dk-2.exo.io/video1/poster.jpg"
       controls preload="none">
  <source src="https://hls-videos.sos-ch-dk-2.exo.io/video1/index.m3u8"
          type="application/vnd.apple.mpegurl">
  <source src="https://hls-videos.sos-ch-dk-2.exo.io/video1/progressive.mp4"
          type='video/mp4; codecs="avc1.4d401f, mp4a.40.2"'>
</video>
```

Browsers with native support use the HLS version while others would
fall back to the progressive MP4 version. However, with the help of
[hls.js][], we can ensure most browsers benefit from the HLS version
too:

```html
<script src="https://cdn.jsdelivr.net/npm/hls.js@lastest"></script>
<script>
    if(Hls.isSupported()) {
        var selector = "video source[type='application/vnd.apple.mpegurl']",
            videoSources = document.querySelectorAll(selector);
        videoSources.forEach(function(videoSource) {
            var once = false;

            // Clone the video to remove any source
            var oldVideo = videoSource.parentNode,
                newVideo = oldVideo.cloneNode(false);

            // Replace video tag with our clone.
            oldVideo.parentNode.replaceChild(newVideo, oldVideo);

            // On play, initialize hls.js, once.
            newVideo.addEventListener('play',function() {
                if (once) return;
                once = true;

                var hls = new Hls({ capLevelToPlayerSize: true });
                hls.attachMedia(newVideo);
                hls.loadSource(videoSource.src);
            }, false);
        });
    }
</script>
```

Here is the result, featuring [Tears of Steel][video], a video
produced by the [Blender Foundation][] and released under the
[Creative Commons Attribution 3.0][] license:

<div style="width:100%;height:auto;padding-top:56.25%;position:relative;">
<video poster="https://hls-videos.sos-ch-dk-2.exo.io/video1/poster.jpg"
       controls preload="none"
       style="width:100%;height:100%;background-color:black;position:absolute;top:0;left:0;">
  <source src="https://hls-videos.sos-ch-dk-2.exo.io/video1/index.m3u8"
          type="application/vnd.apple.mpegurl">
  <source src="https://hls-videos.sos-ch-dk-2.exo.io/video1/progressive.mp4"
          type='video/mp4; codecs="avc1.4d401f, mp4a.40.2"'>
</video>
</div>
<script src="https://cdn.jsdelivr.net/npm/hls.js@0.8.9"></script>
<script>
    if(Hls.isSupported()) {
        var selector = "video source[type='application/vnd.apple.mpegurl']",
            videoSources = document.querySelectorAll(selector);
        videoSources.forEach(function(videoSource) {
            var once = false;
            var oldVideo = videoSource.parentNode,
                newVideo = oldVideo.cloneNode(false);
            oldVideo.parentNode.replaceChild(newVideo, oldVideo);
            newVideo.addEventListener('play',function() {
                if (once) return;
                once = true;
                var hls = new Hls({ capLevelToPlayerSize: true });
                hls.attachMedia(newVideo);
                hls.loadSource(videoSource.src);
            }, false);
        });
    }
</script>

The player is different from one browser to another but provides the
basic needs. You can upgrade to a more advanced player, like
[video.js][] or [MediaElements.js][]. They also handle HLS videos
through *hls.js*.

[YouTube]: https://www.youtube.com/ "YouTube"
[advanced settings]: https://www.youtube.com/advanced_settings "YouTube advanced settings"
[video]: https://www.html5rocks.com/en/tutorials/video/basics/ "HTML5 Video"
[video.js]: https://videojs.com/ "Video.js: The Player Framework"
[MediaElements.js]: http://www.mediaelementjs.com/ "MediaElements.js: HTML5 video and audio unification framework"
[different videos depending on the screen width]: https://www.iandevlin.com/blog/2012/08/html5/responsive-html5-video/ "Responsive HTML5 video"
[RFC 8216]: https://www.rfc-editor.org/rfc/rfc8216 "RFC 8216: HTTP Live Streaming"
[ISO/IEC 23009-1:2014]: https://www.iso.org/standard/65274.html "Dynamic adaptive streaming over HTTP (DASH)"
[hls.js]: https://github.com/video-dev/hls.js/ "JavaScript HLS client using Media Source Extension"
[dash.js]: https://github.com/Dash-Industry-Forum/dash.js "Reference client implementation for the playback of MPEG DASH via Javascript and compliant browsers"
[Media Source Extensions]: https://w3c.github.io/media-source/ "W3C: Media Source Extensions"
[caniuse]: https://caniuse.com/#feat=mediasource "Can I use Media Source Extensions"
[Exoscale's Object Storage]: https://www.exoscale.com/object-storage/ "Exoscale: Object Storage"
[API credentials]: https://portal.exoscale.com/account/profile/api "API credentials for Exoscale"
[FFmpeg]: https://ffmpeg.org/ "A complete, cross-platform solution to record, convert and stream audio and video"
[Peer5's documentation]: https://docs.peer5.com/guides/production-ready-hls-vod/ "Creating A Production Ready Multi Bitrate HLS VOD stream"
[StackOverflow question]: https://stackoverflow.com/questions/16363167/html5-video-tag-codecs-attribute "StackOverflow: html5 video tag codecs attribute"
[video2hls]: https://github.com/vincentbernat/video2hls "Prepare a video to be streamed with HLS"
[video]: https://mango.blender.org/ "Tears of Steel | Mago Open Movie Project"
[Blender Foundation]: https://www.blender.org/about/foundation/ "Blender Foundation"
[Creative Commons Attribution 3.0]: https://creativecommons.org/licenses/by/3.0/ "Creative Commons Attribution 3.0 Unported (CC BY 3.0)"

