{
    "items": [
        {
            "type": [
                "h-entry"
            ],
            "properties": {
                "author": [
                    {
                        "value": "Ryan Parman",
                        "type": [
                            "h-card"
                        ],
                        "properties": {
                            "family-name": [
                                "Parman"
                            ],
                            "given-name": [
                                "Ryan"
                            ],
                            "name": [
                                "Ryan Parman"
                            ],
                            "note": [
                                "Ryan Parman is an engineering manager with over 20 years of experience across software development, site reliability engineering, and security. He is the creator of SimplePie and AWS SDK for PHP, patented multifactor-authentication-as-a-service at WePay, defined much of the CI/CD and SRE disciplines at McGraw-Hill Education, and came up with the idea of “serverless, event-driven, responsive functions in the cloud” while at Amazon Web Services in 2010. Ryan's aptly-named blog, Flailing Wildly, is where he writes about ideas longer than 280 characters. Ambivert. Curious. Not a coffee drinker."
                            ],
                            "photo": [
                                "https://cdn.ryanparman.com/hugo/gravatars/current/8c73b5fc88b88ac0d92fec541c6a4413890df291.jpg",
                                "https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/"
                            ]
                        }
                    }
                ],
                "category": [
                    "Projects and Code"
                ],
                "content": [
                    {
                        "html": "\u003cp itemprop=\"description\" class=\"f5 f4-m f3-l mt0 lh-copy p-summary entry-summary\"\u003e\nWhile YouTube is free (as in money) to use, the cost is paid in terms of privacy and advertising analytics. So I've decided to investigate self-hosting my video content.\n\u003c/p\u003e\n\n\u003ch2 id=\"the-cost-of-youtube\"\u003eThe Cost of YouTube\u003c/h2\u003e\n\u003cp\u003eWith YouTube, you sacrifice privacy in favor of cost. YouTube is the very best at what they do (serve video to all resolutions and bandwidths), and they are backed by Google who is the very best at what they do (collect data in order to facilitate selling a primed audience to advertisers).\u003c/p\u003e\n\u003cdiv class=\"pa2-ns\"\u003e\n    \u003cpicture\u003e\u003csource type=\"image/webp\" srcset=\"https://cdn.ryanparman.com/hugo/posts/2018/youtube-2017.webp\" alt=\"\" class=\"db fullimage\" decoding=\"async\"\u003e\n        \u003cimg src=\"https://cdn.ryanparman.com/hugo/posts/2018/youtube-2017.png\" alt=\"\" class=\"db fullimage\" decoding=\"async\"\u003e\n    \u003c/picture\u003e\n    \u003cp class=\"f6 gray tc db\"\u003e\u003c/p\u003e\n\u003c/div\u003e\n\n\u003cp\u003eThere’s nothing inherently wrong with that. We live in a capitalistic society; there is money to be made; Google/YouTube is providing a service to advertisers; many consumers will (knowingly or unknowingly) give up their privacy in exchange for free-as-in-money services.\u003c/p\u003e\n\u003cp\u003eBut as I’ve gotten older and started to realize just \u003cem\u003ehow much\u003c/em\u003e data Google has on each and every one of us, I’ve started valuing my privacy a lot more. I’d like to provide an option for you to protect your privacy as well.\u003c/p\u003e\n\u003ch2 id=\"self-hosting-video-content\"\u003eSelf-Hosting Video Content\u003c/h2\u003e\n\u003cp\u003eEven with efficient video codecs, video can still cost a lot of money to serve.\u003c/p\u003e\n\u003cp\u003eMany websites provide \u003cem\u003ea video\u003c/em\u003e to their users, wherein this video is a single file, and the browser will begin loading and playing the video from start to finish. This means that even if the user only watches the first few seconds of a 5 minute video, it’s possible that the video is downloaded in its entirety — which is an unnecessary cost.\u003c/p\u003e\n\u003cp\u003eHowever, we can provide a \u003cem\u003ebetter user experience\u003c/em\u003e as well as \u003cem\u003ereduce hosting costs\u003c/em\u003e by leveraging the ability to serve bandwidth-adaptive chunks of video to players on-demand.\u003c/p\u003e\n\u003ch3 id=\"adaptive-bitrate-streaming\"\u003eAdaptive Bitrate Streaming\u003c/h3\u003e\n\u003cp\u003eThere are two major, semi-compatible approaches to \u003ca href=\"https://en.wikipedia.org/wiki/Adaptive_bitrate_streaming\"\u003eadaptive bitrate streaming\u003c/a\u003e over HTTP. One is called \u003ca href=\"https://web.archive.org/web/20180909031833/https://en.wikipedia.org/wiki/HTTP_Live_Streaming\"\u003eHTTP Live Streaming\u003c/a\u003e (“HLS”), and the other is called \u003ca href=\"https://web.archive.org/web/20180909031833/https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP\"\u003eDynamic Adaptive Streaming over HTTP\u003c/a\u003e (“MPEG-DASH”).\u003c/p\u003e\n\u003cdiv class=\"pa2-ns\"\u003e\n    \u003cpicture\u003e\u003csource type=\"image/webp\" srcset=\"https://cdn.ryanparman.com/hugo/posts/2018/adaptive-bitrate-streaming.webp\" alt=\"\" class=\"db fullimage\" decoding=\"async\"\u003e\n        \u003cimg src=\"https://cdn.ryanparman.com/hugo/posts/2018/adaptive-bitrate-streaming.png\" alt=\"\" class=\"db fullimage\" decoding=\"async\"\u003e\n    \u003c/picture\u003e\n    \u003cp class=\"f6 gray tc db\"\u003e\u003c/p\u003e\n\u003c/div\u003e\n\n\u003cp\u003eFrom \u003ca href=\"https://web.archive.org/web/20180909031833/https://en.wikipedia.org/wiki/Adaptive_bitrate_streaming\"\u003eWikipedia\u003c/a\u003e:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eAdaptive bitrate streaming is a technique used in streaming multimedia over computer networks. While in the past most video or audio streaming technologies utilized streaming protocols such as RTP with RTSP, today’s adaptive streaming technologies are almost exclusively based on HTTP and designed to work efficiently over large distributed HTTP networks such as the Internet.\u003c/p\u003e\n\u003cp\u003eIt works by detecting a user’s bandwidth and CPU capacity in real time and adjusting the quality of the media stream accordingly. It requires the use of an encoder which can encode a single source media (video or audio) at multiple bit rates. The player client switches between streaming the different encodings depending on available resources. “The result: very little buffering, fast start time and a good experience for both high-end and low-end connections.” […]\u003c/p\u003e\n\u003cp\u003eHTTP-based adaptive bitrate streaming technologies yield additional benefits over traditional server-driven adaptive bitrate streaming. First, since the streaming technology is built on top of HTTP, contrary to RTP-based adaptive streaming, the packets have no difficulties traversing firewall and NAT devices. Second, since HTTP streaming is purely client-driven, all adaptation logic resides at the client. This reduces the requirement of persistent connections between server and client application. Furthermore, the server is not required to maintain session state information on each client, increasing scalability. Finally, existing HTTP delivery infrastructure, such as HTTP caches and servers can be seamlessly adopted.\u003c/p\u003e\n\u003cp\u003eA scalable CDN is used to deliver media streaming to an Internet audience. The CDN receives the stream from the source at its Origin server, then replicates it to many or all of its Edge cache servers. The end-user requests the stream and is redirected to the “closest” Edge server. […] The use of HTTP-based adaptive streaming allows the Edge server to run a simple HTTP server software, whose licence cost is cheap or free, reducing software licensing cost, compared to costly media server licences (e.g. Adobe Flash Media Streaming Server). The CDN cost for HTTP streaming media is then similar to HTTP web caching CDN cost.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eThis means that we can use off-the-shelf services like \u003ca href=\"https://aws.amazon.com/s3\"\u003eAmazon S3\u003c/a\u003e and \u003ca href=\"https://aws.amazon.com/cloudfront\"\u003eAmazon CloudFront\u003c/a\u003e to serve video, which are relatively inexpensive and have large user-bases who can answer questions when you run into issues.\u003c/p\u003e\n\u003ch3 id=\"http-live-streaming-hls\"\u003eHTTP Live Streaming (HLS)\u003c/h3\u003e\n\u003cp\u003eAfter doing some research, I came across a blog post that was particularly helpful — “\u003ca href=\"https://web.archive.org/web/20180909031833/https://vincent.bernat.ch/en/blog/2018-self-hosted-videos\"\u003eSelf-hosted videos with HLS\u003c/a\u003e” by Vincent Bernat.\u003c/p\u003e\n\u003cp\u003eVincent writes:\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eTo serve HLS videos, you need three kinds of files:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003ethe media segments (encoded with different bitrates/resolutions),\u003c/li\u003e\n\u003cli\u003ea media playlist for each variant, listing the media segments, and\u003c/li\u003e\n\u003cli\u003ea master playlist, listing the media playlists.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eMedia segments can come in two formats:\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eMPEG-2 Transport Streams (TS), or\u003c/li\u003e\n\u003cli\u003eFragmented MP4.\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eFragmented 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 playlists are different). Also, they can be served from the same file with range requests. However, if you want to target older versions of iOS, you need to stick with MPEG-2 TS.\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003eAt the time of this writing, iOS 12 will be out in a week or two. A quick search tells me that \u003ca href=\"https://web.archive.org/web/20180909031833/https://data.apteligent.com/ios/\"\u003eiOS 10 and newer make up 85% of all iOS users\u003c/a\u003e. This means that I can pretty safely use the \u003cem\u003eFragmented MP4\u003c/em\u003e method which, according to \u003ca href=\"https://web.archive.org/web/20180909031833/https://bitmovin.com/hls-news-wwdc-2016/\"\u003ethese\u003c/a\u003e \u003ca href=\"https://web.archive.org/web/20180909031833/http://www.streamingmedia.com/Articles/ReadArticle.aspx?ArticleID=111796\"\u003esources\u003c/a\u003e, is more compatible with MPEG-DASH for some cross-over implementations in the future.\u003c/p\u003e\n\u003ch2 id=\"sample-video\"\u003eSample Video\u003c/h2\u003e\n\u003cdiv style=\"position: relative; padding-bottom: 56.25%; overflow: hidden;\"\u003e\n    \u003cvideo poster=\"https://cdn.ryanparman.com/hls/hallelujah.fmp4/poster.jpg\" controls=\"\" preload=\"none\" style=\"position: absolute; width: 100%; height: 100%;\"\u003e\n      \u003csource src=\"https://cdn.ryanparman.com/hls/hallelujah.fmp4/index.m3u8\" type=\"application/vnd.apple.mpegurl\"\u003e\n      \u003csource src=\"https://cdn.ryanparman.com/hls/hallelujah.fmp4/progressive.mp4\" type=\"video/mp4; codecs=\u0026#34;avc1.4d401f, mp4a.40.2\u0026#34;\"\u003e\n    \u003c/video\u003e\n\u003c/div\u003e\u003cp class=\"tc black-60 db mb2\"\u003e\u003csmall\u003e\u003cb\u003eSource:\u003c/b\u003e \u003ca href=\"https://youtu.be/e1C9kpMV2e8\"\u003eHallelujah - Brooklyn Duo (Piano + Cello)\u003c/a\u003e\u003c/small\u003e\u003c/p\u003e\n\u003ch2 id=\"implementation\"\u003eImplementation\u003c/h2\u003e\n\u003ch3 id=\"encoding-and-deploying-video\"\u003eEncoding and Deploying Video\u003c/h3\u003e\n\u003cp\u003eVincent Bernat provides a tool on GitHub which greatly simplifies the process of creating the various video fragments called \u003ca href=\"https://web.archive.org/web/20180909031833/https://github.com/vincentbernat/video2hls\"\u003evideo2hls\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eFor \u003ca href=\"https://github.com/skyzyx/blog/blob/master/Makefile\"\u003ethis website\u003c/a\u003e, I have put together a workflow for creating and serving HLS video content.\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eI use \u003cstrong\u003eH.264\u003c/strong\u003e video with \u003cstrong\u003eAAC\u003c/strong\u003e audio wrapped inside an \u003cstrong\u003eMP4\u003c/strong\u003e container, \u003cem\u003eexclusively\u003c/em\u003e. These are all defined as part of the MPEG-4 specification, and is the \u003ca href=\"https://caniuse.com/#search=H.264\"\u003ebest-supported grouping of codecs and containers across all browsers and devices\u003c/a\u003e.\u003c/p\u003e\n\u003cp\u003eHardware-level decoders are commonplace inside computers, phones, tablets, and set-top boxes like Xbox, PlayStation, and Apple TV.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eI have a directory called \u003ccode\u003estreaming-video\u003c/code\u003e, which is separate from the images that I use and push to S3. Video files are large, and I don’t want to accidentally push partially-completed video data to my caching CDN before they’re ready.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eI have a command which takes any video file inside the \u003ccode\u003estreaming-video\u003c/code\u003e folder, with a filename ending in \u003ccode\u003e-source.mp4\u003c/code\u003e, and passes it through \u003ccode\u003evideo2hls\u003c/code\u003e, creating a folder called \u003ccode\u003e{video}.fmp4\u003c/code\u003e which contains all of the video and playlist files I need across a large variety of bandwidths and resolutions.\u003c/p\u003e\n\u003cp\u003eIt will only do the work to create the directory and all of the fragmented files if the directory doesn’t already exist.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003efind ./streaming-video -type f -name \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;*-source.mp4\u0026#34;\u003c/span\u003e | xargs -I \u003cspan style=\"color:#f92672\"\u003e{}\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    bash -c \u003cspan style=\"color:#e6db74\"\u003e'if [ ! -d \u0026#34;${1%-source.mp4}.fmp4\u0026#34; ]; then \\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e        video2hls --debug --output \u0026#34;${1%-source.mp4}.fmp4\u0026#34; --hls-type fmp4 \u0026#34;$1\u0026#34;; \\\n\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e    fi;'\u003c/span\u003e _ \u003cspan style=\"color:#f92672\"\u003e{}\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eI find all of the \u003ccode\u003e.m3u8\u003c/code\u003e playlist files and gzip them (since they’re just text). This is essentially an in-place rewrite of the files.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003efind ./streaming-video -type f -name \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;*.m3u8\u0026#34;\u003c/span\u003e | xargs -P \u003cspan style=\"color:#ae81ff\"\u003e8\u003c/span\u003e -I \u003cspan style=\"color:#f92672\"\u003e{}\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    bash -c \u003cspan style=\"color:#e6db74\"\u003e'! gunzip -t $1 2\u0026gt;/dev/null \u0026amp;\u0026amp; gzip -v $1 \u0026amp;\u0026amp; mv -v $1.gz $1;'\u003c/span\u003e _ \u003cspan style=\"color:#f92672\"\u003e{}\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eLastly, I push all of the files up to the \u003ccode\u003ehls\u003c/code\u003e folder in my S3 bucket using the \u003ca href=\"https://github.com/aws/aws-cli\"\u003eAWS Unified CLI Tools\u003c/a\u003e, setting the correct \u003ccode\u003eContent-Type\u003c/code\u003e and \u003ccode\u003eContent-Encoding\u003c/code\u003e headers.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"color:#75715e\"\u003e# The .m3u8 playlists that we gzipped\u003c/span\u003e\naws s3 sync ./streaming-video s3://blog.ryanparman.com/hls \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --exclude \u003cspan style=\"color:#e6db74\"\u003e'*.*'\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --include \u003cspan style=\"color:#e6db74\"\u003e'*.m3u8'\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --acl\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003epublic-read \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --cache-control max-age\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e31536000,public \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --content-type \u003cspan style=\"color:#e6db74\"\u003e'application/vnd.apple.mpegurl'\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --content-encoding \u003cspan style=\"color:#e6db74\"\u003e'gzip'\u003c/span\u003e\n\n\u003cspan style=\"color:#75715e\"\u003e# The video \u0026#34;posters\u0026#34;\u003c/span\u003e\naws s3 sync ./streaming-video s3://blog.ryanparman.com/hls \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --exclude \u003cspan style=\"color:#e6db74\"\u003e'*.*'\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --include \u003cspan style=\"color:#e6db74\"\u003e'*.jpg'\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --acl\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003epublic-read \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --cache-control max-age\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e31536000,public \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --content-type \u003cspan style=\"color:#e6db74\"\u003e'image/jpeg'\u003c/span\u003e\n\n\u003cspan style=\"color:#75715e\"\u003e# The fragmented MP4 files\u003c/span\u003e\naws s3 sync ./streaming-video s3://blog.ryanparman.com/hls \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --exclude \u003cspan style=\"color:#e6db74\"\u003e'*.*'\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --include \u003cspan style=\"color:#e6db74\"\u003e'*.mp4'\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --acl\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003epublic-read \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --cache-control max-age\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e31536000,public \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e\u003c/span\u003e    --content-type \u003cspan style=\"color:#e6db74\"\u003e'video/mp4'\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"the-client-side-code\"\u003eThe Client-Side Code\u003c/h3\u003e\n\u003cp\u003eAfter pushing the content to our CDN, we can use the standard HTML5 \u003ccode\u003e\u0026lt;video\u0026gt;\u003c/code\u003e tag to tell browsers how to load the requested assets.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u0026lt;\u003cspan style=\"color:#f92672\"\u003evideo\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eposter\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;https://cdn.ryanparman.com/hls/hallelujah.fmp4/poster.jpg\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003econtrols\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003epreload\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;none\u0026#34;\u003c/span\u003e\u0026gt;\n    \u0026lt;\u003cspan style=\"color:#f92672\"\u003esource\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esrc\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;https://cdn.ryanparman.com/hls/hallelujah.fmp4/index.m3u8\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003etype\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;application/vnd.apple.mpegurl\u0026#34;\u003c/span\u003e\u0026gt;\n    \u0026lt;\u003cspan style=\"color:#f92672\"\u003esource\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esrc\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;https://cdn.ryanparman.com/hls/hallelujah.fmp4/progressive.mp4\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003etype\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e'video/mp4; codecs=\u0026#34;avc1.4d401f, mp4a.40.2\u0026#34;'\u003c/span\u003e\u0026gt;\n\u0026lt;/\u003cspan style=\"color:#f92672\"\u003evideo\u003c/span\u003e\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003col\u003e\n\u003cli\u003e\n\u003cp\u003eHere, we have a static \u003cem\u003eposter\u003c/em\u003e image that the \u003ccode\u003e\u0026lt;video\u0026gt;\u003c/code\u003e element loads by default.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eNext, we have an HLS-compatible playlist file (\u003ccode\u003e.m3u8\u003c/code\u003e), which ultimately points to the correct \u003ccode\u003e.mp4\u003c/code\u003e files.\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eLastly, we have a standard \u003ccode\u003e.mp4\u003c/code\u003e fallback.\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"enabling-chrome-firefox-and-edge-using-hlsjs\"\u003eEnabling Chrome, Firefox, and Edge using hls.js\u003c/h3\u003e\n\u003cp\u003eDailymotion has released a JavaScript library called \u003ca href=\"https://github.com/video-dev/hls.js\"\u003ehls.js\u003c/a\u003e which enables HLS playback on browsers like Chrome, Firefox, and Edge using Fragmented MP4 sources.\u003c/p\u003e\n\u003cp\u003eYou can load the script from the CDN:\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"\u003e\u003ccode class=\"language-html\" data-lang=\"html\"\u003e\u0026lt;\u003cspan style=\"color:#f92672\"\u003escript\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esrc\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;https://cdn.jsdelivr.net/npm/hls.js@latest\u0026#34;\u003c/span\u003e\u0026gt;\u0026lt;/\u003cspan style=\"color:#f92672\"\u003escript\u003c/span\u003e\u0026gt;\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAfter that, we have the implementation. Here, we start with a working \u003ccode\u003e\u0026lt;video\u0026gt;\u003c/code\u003e element, then use JavaScript to swap over to HLS.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"\u003e\u003ccode class=\"language-javascript\" data-lang=\"javascript\"\u003e(() =\u0026gt; {\n  \u003cspan style=\"color:#e6db74\"\u003e'use strict'\u003c/span\u003e;\n\n  \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003eHls\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eisSupported\u003c/span\u003e()) {\n    \u003cspan style=\"color:#66d9ef\"\u003elet\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eselector\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;video source[type='application/vnd.apple.mpegurl']\u0026#34;\u003c/span\u003e,\n        \u003cspan style=\"color:#a6e22e\"\u003evideoSources\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e document.\u003cspan style=\"color:#a6e22e\"\u003equerySelectorAll\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003eselector\u003c/span\u003e);\n\n    \u003cspan style=\"color:#a6e22e\"\u003evideoSources\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eforEach\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003evideoSource\u003c/span\u003e =\u0026gt; {\n      \u003cspan style=\"color:#66d9ef\"\u003elet\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003em3u8\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003evideoSource\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003esrc\u003c/span\u003e,\n          \u003cspan style=\"color:#a6e22e\"\u003eonce\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e;\n\n      \u003cspan style=\"color:#75715e\"\u003e// Clone the video to remove any source\n\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e      \u003cspan style=\"color:#66d9ef\"\u003elet\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eoldVideo\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003evideoSource\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eparentNode\u003c/span\u003e,\n          \u003cspan style=\"color:#a6e22e\"\u003enewVideo\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eoldVideo\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003ecloneNode\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e);\n\n      \u003cspan style=\"color:#75715e\"\u003e// Replace video tag with our clone.\n\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e      \u003cspan style=\"color:#a6e22e\"\u003eoldVideo\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eparentNode\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003ereplaceChild\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003enewVideo\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003eoldVideo\u003c/span\u003e);\n\n      \u003cspan style=\"color:#75715e\"\u003e// On play, initialize hls.js, once.\n\u003c/span\u003e\u003cspan style=\"color:#75715e\"\u003e\u003c/span\u003e      \u003cspan style=\"color:#a6e22e\"\u003enewVideo\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eaddEventListener\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e'play'\u003c/span\u003e, () =\u0026gt; {\n        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (\u003cspan style=\"color:#a6e22e\"\u003eonce\u003c/span\u003e) {\n          \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e;\n        };\n        \u003cspan style=\"color:#a6e22e\"\u003eonce\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e;\n\n        \u003cspan style=\"color:#66d9ef\"\u003evar\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ehls\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eHls\u003c/span\u003e({\n          \u003cspan style=\"color:#a6e22e\"\u003ecapLevelToPlayerSize\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e\n        });\n        \u003cspan style=\"color:#a6e22e\"\u003ehls\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eattachMedia\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003enewVideo\u003c/span\u003e);\n        \u003cspan style=\"color:#a6e22e\"\u003ehls\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eloadSource\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003em3u8\u003c/span\u003e);\n        \u003cspan style=\"color:#a6e22e\"\u003ehls\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eon\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003eHls\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eEvents\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eMANIFEST_PARSED\u003c/span\u003e, (\u003cspan style=\"color:#a6e22e\"\u003eevent\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003edata\u003c/span\u003e) =\u0026gt; {\n          \u003cspan style=\"color:#a6e22e\"\u003enewVideo\u003c/span\u003e.\u003cspan style=\"color:#a6e22e\"\u003eplay\u003c/span\u003e();\n        });\n      }, \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e);\n    });\n  }\n})();\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"cors\"\u003eCORS\u003c/h3\u003e\n\u003cp\u003eIf you are serving the files from a third-party host (such as Amazon S3), you will need to enable \u003ca href=\"https://caniuse.com/#search=cors\"\u003eCORS\u003c/a\u003e support on your bucket.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"color:#75715e\"\u003e\u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt;\u003c/span\u003e\n\u003cspan style=\"color:#f92672\"\u003e\u0026lt;CORSConfiguration\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003exmlns=\u003c/span\u003e\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;http://s3.amazonaws.com/doc/2006-03-01/\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e\n  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;CORSRule\u0026gt;\u003c/span\u003e\n    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;AllowedHeader\u0026gt;\u003c/span\u003e*\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/AllowedHeader\u0026gt;\u003c/span\u003e\n    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;AllowedOrigin\u0026gt;\u003c/span\u003e*\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/AllowedOrigin\u0026gt;\u003c/span\u003e\n    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;AllowedMethod\u0026gt;\u003c/span\u003eGET\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/AllowedMethod\u0026gt;\u003c/span\u003e\n    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;AllowedMethod\u0026gt;\u003c/span\u003eHEAD\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/AllowedMethod\u0026gt;\u003c/span\u003e\n  \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/CORSRule\u0026gt;\u003c/span\u003e\n\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/CORSConfiguration\u0026gt;\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003eAdditionally, if you have a CDN cache in front of that S3 bucket (e.g., Amazon CloudFront), you’ll need to make sure that it is configured to allow the \u003ccode\u003eOrigin\u003c/code\u003e headers through and also respond to the HTTP \u003ccode\u003eOPTIONS\u003c/code\u003e verb.\u003c/p\u003e\n\u003cp\u003eYou can find more information about solving this problem with CloudFront at “\u003ca href=\"https://web.archive.org/web/20180909031833/https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/header-caching.html#header-caching-web-cors\"\u003eConfiguring CloudFront to Respect CORS Settings\u003c/a\u003e”.\u003c/p\u003e\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/hls.js@latest\" type=\"c9d15be256f7a6671e26a23e-text/javascript\"\u003e\u003c/script\u003e\n\u003cscript type=\"c9d15be256f7a6671e26a23e-text/javascript\" src=\"https://ryanparman.com/js/stream-hls.min.270faa3c315322e584846228fe3da0897e67544a45ac2646b9050c886baa54b9.js\" integrity=\"sha256-Jw+qPDFTIuWEhGIo/j2giX5nVEpFrCZGuQUMiGuqVLk=\"\u003e\u003c/script\u003e",
                        "value": "While YouTube is free (as in money) to use, the cost is paid in terms of privacy and advertising analytics. So I've decided to investigate self-hosting my video content.\n\n\nThe Cost of YouTube\nWith YouTube, you sacrifice privacy in favor of cost. YouTube is the very best at what they do (serve video to all resolutions and bandwidths), and they are backed by Google who is the very best at what they do (collect data in order to facilitate selling a primed audience to advertisers).\n\n    \n        \n    \n    \n\n\nThere’s nothing inherently wrong with that. We live in a capitalistic society; there is money to be made; Google/YouTube is providing a service to advertisers; many consumers will (knowingly or unknowingly) give up their privacy in exchange for free-as-in-money services.\nBut as I’ve gotten older and started to realize just how much data Google has on each and every one of us, I’ve started valuing my privacy a lot more. I’d like to provide an option for you to protect your privacy as well.\nSelf-Hosting Video Content\nEven with efficient video codecs, video can still cost a lot of money to serve.\nMany websites provide a video to their users, wherein this video is a single file, and the browser will begin loading and playing the video from start to finish. This means that even if the user only watches the first few seconds of a 5 minute video, it’s possible that the video is downloaded in its entirety — which is an unnecessary cost.\nHowever, we can provide a better user experience as well as reduce hosting costs by leveraging the ability to serve bandwidth-adaptive chunks of video to players on-demand.\nAdaptive Bitrate Streaming\nThere are two major, semi-compatible approaches to adaptive bitrate streaming over HTTP. One is called HTTP Live Streaming (“HLS”), and the other is called Dynamic Adaptive Streaming over HTTP (“MPEG-DASH”).\n\n    \n        \n    \n    \n\n\nFrom Wikipedia:\n\nAdaptive bitrate streaming is a technique used in streaming multimedia over computer networks. While in the past most video or audio streaming technologies utilized streaming protocols such as RTP with RTSP, today’s adaptive streaming technologies are almost exclusively based on HTTP and designed to work efficiently over large distributed HTTP networks such as the Internet.\nIt works by detecting a user’s bandwidth and CPU capacity in real time and adjusting the quality of the media stream accordingly. It requires the use of an encoder which can encode a single source media (video or audio) at multiple bit rates. The player client switches between streaming the different encodings depending on available resources. “The result: very little buffering, fast start time and a good experience for both high-end and low-end connections.” […]\nHTTP-based adaptive bitrate streaming technologies yield additional benefits over traditional server-driven adaptive bitrate streaming. First, since the streaming technology is built on top of HTTP, contrary to RTP-based adaptive streaming, the packets have no difficulties traversing firewall and NAT devices. Second, since HTTP streaming is purely client-driven, all adaptation logic resides at the client. This reduces the requirement of persistent connections between server and client application. Furthermore, the server is not required to maintain session state information on each client, increasing scalability. Finally, existing HTTP delivery infrastructure, such as HTTP caches and servers can be seamlessly adopted.\nA scalable CDN is used to deliver media streaming to an Internet audience. The CDN receives the stream from the source at its Origin server, then replicates it to many or all of its Edge cache servers. The end-user requests the stream and is redirected to the “closest” Edge server. […] The use of HTTP-based adaptive streaming allows the Edge server to run a simple HTTP server software, whose licence cost is cheap or free, reducing software licensing cost, compared to costly media server licences (e.g. Adobe Flash Media Streaming Server). The CDN cost for HTTP streaming media is then similar to HTTP web caching CDN cost.\n\nThis means that we can use off-the-shelf services like Amazon S3 and Amazon CloudFront to serve video, which are relatively inexpensive and have large user-bases who can answer questions when you run into issues.\nHTTP Live Streaming (HLS)\nAfter doing some research, I came across a blog post that was particularly helpful — “Self-hosted videos with HLS” by Vincent Bernat.\nVincent writes:\n\nTo serve HLS videos, you need three kinds of files:\n\nthe media segments (encoded with different bitrates/resolutions),\na media playlist for each variant, listing the media segments, and\na master playlist, listing the media playlists.\n\nMedia segments can come in two formats:\n\nMPEG-2 Transport Streams (TS), or\nFragmented MP4.\n\nFragmented 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 playlists are different). Also, they can be served from the same file with range requests. However, if you want to target older versions of iOS, you need to stick with MPEG-2 TS.\n\nAt the time of this writing, iOS 12 will be out in a week or two. A quick search tells me that iOS 10 and newer make up 85% of all iOS users. This means that I can pretty safely use the Fragmented MP4 method which, according to these sources, is more compatible with MPEG-DASH for some cross-over implementations in the future.\nSample Video\n\n    \n      \n      \n    \nSource: Hallelujah - Brooklyn Duo (Piano + Cello)\nImplementation\nEncoding and Deploying Video\nVincent Bernat provides a tool on GitHub which greatly simplifies the process of creating the various video fragments called video2hls.\nFor this website, I have put together a workflow for creating and serving HLS video content.\n\n\nI use H.264 video with AAC audio wrapped inside an MP4 container, exclusively. These are all defined as part of the MPEG-4 specification, and is the best-supported grouping of codecs and containers across all browsers and devices.\nHardware-level decoders are commonplace inside computers, phones, tablets, and set-top boxes like Xbox, PlayStation, and Apple TV.\n\n\nI have a directory called streaming-video, which is separate from the images that I use and push to S3. Video files are large, and I don’t want to accidentally push partially-completed video data to my caching CDN before they’re ready.\n\n\nI have a command which takes any video file inside the streaming-video folder, with a filename ending in -source.mp4, and passes it through video2hls, creating a folder called {video}.fmp4 which contains all of the video and playlist files I need across a large variety of bandwidths and resolutions.\nIt will only do the work to create the directory and all of the fragmented files if the directory doesn’t already exist.\nfind ./streaming-video -type f -name \"*-source.mp4\" | xargs -I {} \\\n    bash -c 'if [ ! -d \"${1%-source.mp4}.fmp4\" ]; then \\\n        video2hls --debug --output \"${1%-source.mp4}.fmp4\" --hls-type fmp4 \"$1\"; \\\n    fi;' _ {} \\;\n\n\nI find all of the .m3u8 playlist files and gzip them (since they’re just text). This is essentially an in-place rewrite of the files.\nfind ./streaming-video -type f -name \"*.m3u8\" | xargs -P 8 -I {} \\\n    bash -c '! gunzip -t $1 2\u003e/dev/null \u0026\u0026 gzip -v $1 \u0026\u0026 mv -v $1.gz $1;' _ {} \\;\n\n\nLastly, I push all of the files up to the hls folder in my S3 bucket using the AWS Unified CLI Tools, setting the correct Content-Type and Content-Encoding headers.\n# The .m3u8 playlists that we gzipped\naws s3 sync ./streaming-video s3://blog.ryanparman.com/hls \\\n    --exclude '*.*' \\\n    --include '*.m3u8' \\\n    --acl=public-read \\\n    --cache-control max-age=31536000,public \\\n    --content-type 'application/vnd.apple.mpegurl' \\\n    --content-encoding 'gzip'\n\n# The video \"posters\"\naws s3 sync ./streaming-video s3://blog.ryanparman.com/hls \\\n    --exclude '*.*' \\\n    --include '*.jpg' \\\n    --acl=public-read \\\n    --cache-control max-age=31536000,public \\\n    --content-type 'image/jpeg'\n\n# The fragmented MP4 files\naws s3 sync ./streaming-video s3://blog.ryanparman.com/hls \\\n    --exclude '*.*' \\\n    --include '*.mp4' \\\n    --acl=public-read \\\n    --cache-control max-age=31536000,public \\\n    --content-type 'video/mp4'\n\n\nThe Client-Side Code\nAfter pushing the content to our CDN, we can use the standard HTML5 \u003cvideo\u003e tag to tell browsers how to load the requested assets.\n\u003cvideo poster=\"https://cdn.ryanparman.com/hls/hallelujah.fmp4/poster.jpg\" controls preload=\"none\"\u003e\n    \u003csource src=\"https://cdn.ryanparman.com/hls/hallelujah.fmp4/index.m3u8\" type=\"application/vnd.apple.mpegurl\"\u003e\n    \u003csource src=\"https://cdn.ryanparman.com/hls/hallelujah.fmp4/progressive.mp4\" type='video/mp4; codecs=\"avc1.4d401f, mp4a.40.2\"'\u003e\n\u003c/video\u003e\n\n\nHere, we have a static poster image that the \u003cvideo\u003e element loads by default.\n\n\nNext, we have an HLS-compatible playlist file (.m3u8), which ultimately points to the correct .mp4 files.\n\n\nLastly, we have a standard .mp4 fallback.\n\n\nEnabling Chrome, Firefox, and Edge using hls.js\nDailymotion has released a JavaScript library called hls.js which enables HLS playback on browsers like Chrome, Firefox, and Edge using Fragmented MP4 sources.\nYou can load the script from the CDN:\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/hls.js@latest\"\u003e\u003c/script\u003e\nAfter that, we have the implementation. Here, we start with a working \u003cvideo\u003e element, then use JavaScript to swap over to HLS.\n(() =\u003e {\n  'use strict';\n\n  if (Hls.isSupported()) {\n    let selector = \"video source[type='application/vnd.apple.mpegurl']\",\n        videoSources = document.querySelectorAll(selector);\n\n    videoSources.forEach(videoSource =\u003e {\n      let m3u8 = videoSource.src,\n          once = false;\n\n      // Clone the video to remove any source\n      let oldVideo = videoSource.parentNode,\n          newVideo = oldVideo.cloneNode(false);\n\n      // Replace video tag with our clone.\n      oldVideo.parentNode.replaceChild(newVideo, oldVideo);\n\n      // On play, initialize hls.js, once.\n      newVideo.addEventListener('play', () =\u003e {\n        if (once) {\n          return;\n        };\n        once = true;\n\n        var hls = new Hls({\n          capLevelToPlayerSize: false\n        });\n        hls.attachMedia(newVideo);\n        hls.loadSource(m3u8);\n        hls.on(Hls.Events.MANIFEST_PARSED, (event, data) =\u003e {\n          newVideo.play();\n        });\n      }, false);\n    });\n  }\n})();\nCORS\nIf you are serving the files from a third-party host (such as Amazon S3), you will need to enable CORS support on your bucket.\n\u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\n\u003cCORSConfiguration xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"\u003e\n  \u003cCORSRule\u003e\n    \u003cAllowedHeader\u003e*\u003c/AllowedHeader\u003e\n    \u003cAllowedOrigin\u003e*\u003c/AllowedOrigin\u003e\n    \u003cAllowedMethod\u003eGET\u003c/AllowedMethod\u003e\n    \u003cAllowedMethod\u003eHEAD\u003c/AllowedMethod\u003e\n  \u003c/CORSRule\u003e\n\u003c/CORSConfiguration\u003e\nAdditionally, if you have a CDN cache in front of that S3 bucket (e.g., Amazon CloudFront), you’ll need to make sure that it is configured to allow the Origin headers through and also respond to the HTTP OPTIONS verb.\nYou can find more information about solving this problem with CloudFront at “Configuring CloudFront to Respect CORS Settings”."
                    }
                ],
                "name": [
                    "Serving Bandwidth-Friendly Video with HTTP Live Streaming (HLS)"
                ],
                "published": [
                    "2018-09-09T03:18:33Z"
                ],
                "summary": [
                    "While YouTube is free (as in money) to use, the cost is paid in terms of privacy and advertising analytics. So I've decided to investigate self-hosting my video content."
                ]
            }
        }
    ],
    "rels": {
        "alternate": [
            "https://instapaper.com/text?u=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/",
            "https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/index.json",
            "https://feed.ryanparman.com/",
            "https://feed.ryanparman.com/conglomo",
            "https://feed.ryanparman.com/content",
            "https://feed.ryanparman.com/linkblog",
            "https://feed.ryanparman.com/media",
            "https://feed.ryanparman.com/movies",
            "https://feed.ryanparman.com/tv",
            "https://feed.ryanparman.com/youtube",
            "https://metadata.ryanparman.com/oembed/?url=https%3a%2f%2fryanparman.com%2fposts%2f2018%2fserving-bandwidth-friendly-video-with-hls%2f"
        ],
        "amphtml": [
            "https://mercury.postlight.com/amp?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/"
        ],
        "apple-touch-icon-precomposed": [
            "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-57x57.png",
            "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-114x114.png",
            "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-72x72.png",
            "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-144x144.png",
            "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-60x60.png",
            "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-120x120.png",
            "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-76x76.png",
            "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-152x152.png"
        ],
        "archives": [
            "https://ryanparman.com/posts/"
        ],
        "canonical": [
            "https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/"
        ],
        "dns-prefetch": [
            "https://cdn.ryanparman.com",
            "https://metadata.ryanparman.com",
            "https://ajax.cloudflare.com",
            "https://www.googletagmanager.com"
        ],
        "gravatar": [
            "https://cdn.ryanparman.com/hugo/gravatars/current/8c73b5fc88b88ac0d92fec541c6a4413890df291@4x.jpg"
        ],
        "home": [
            "https://ryanparman.com"
        ],
        "icon": [
            "https://cdn.ryanparman.com/hugo/favicons/favicon.ico",
            "https://cdn.ryanparman.com/hugo/favicons/favicon-196x196.png",
            "https://cdn.ryanparman.com/hugo/favicons/favicon-96x96.png",
            "https://cdn.ryanparman.com/hugo/favicons/favicon-32x32.png",
            "https://cdn.ryanparman.com/hugo/favicons/favicon-16x16.png",
            "https://cdn.ryanparman.com/hugo/favicons/favicon-128.png"
        ],
        "index": [
            "https://ryanparman.com/posts/"
        ],
        "license": [
            "https://ryanparman.com/content-license/"
        ],
        "me": [
            "https://codepen.io/skyzyx/",
            "https://coderwall.com/skyzyx",
            "https://hub.docker.com/u/skyzyx/",
            "https://github.com/skyzyx",
            "https://libraries.io/github/skyzyx",
            "https://www.npmjs.com/~skyzyx",
            "https://pypi.org/user/skyzyx/",
            "https://forums.aws.amazon.com/profile.jspa?userID=62398",
            "https://dev.to/skyzyx",
            "https://news.ycombinator.com/user?id=skyzyx",
            "https://www.reddit.com/user/skyzyx",
            "https://stackoverflow.com/users/228514/ryan-parman",
            "https://cash.me/$rparman",
            "https://www.paypal.me/rparman",
            "https://venmo.com/skyzyx",
            "https://rparman.yelp.com/",
            "https://itunes.apple.com/profile/skyzyx",
            "https://bandcamp.com/skyzyx",
            "https://www.last.fm/user/skyzyx",
            "https://pandora.com/profile/ryan4516",
            "https://soundcloud.com/skyzyx",
            "https://open.spotify.com/user/skyzyx",
            "https://www.airbnb.com/users/show/47877510",
            "https://amazon.com/wishlist/KAFYR57E8R81",
            "https://browser.geekbench.com/user/skyzyx",
            "https://homescreen.me/skyzyx",
            "https://www.kickstarter.com/profile/skyzyx",
            "https://unsplash.com/@skyzyx",
            "https://angel.co/skyzyx",
            "https://betalist.com/@skyzyx",
            "https://www.linkedin.com/in/rparman",
            "https://feedly.com/skyzyx",
            "https://www.goodreads.com/user/show/7693108-ryan-parman",
            "https://medium.com/@skyzyx",
            "https://hackerone.com/skyzyx",
            "https://keybase.io/skyzyx",
            "https://twitter.com/skyzyx",
            "https://imdb.com/user/ur24445966/",
            "https://letterboxd.com/skyzyx/",
            "https://trakt.tv/users/skyzyx",
            "https://www.youtube.com/user/Skyzyx",
            "https://ryanparman.com"
        ],
        "next": [
            "https://ryanparman.com/posts/2018/clueless-recruiters-issue-8/"
        ],
        "openid2.local_id": [
            "https://openid.stackexchange.com/user/934ca319-0966-4032-8ff0-3111ad438835"
        ],
        "openid2.provider": [
            "https://openid.stackexchange.com/openid/provider"
        ],
        "pavatar": [
            "https://cdn.ryanparman.com/hugo/gravatars/current/8c73b5fc88b88ac0d92fec541c6a4413890df291@4x.jpg"
        ],
        "preload": [
            "https://cdn.ryanparman.com/fonts/MesloLGS-Regular.woff2",
            "https://cdn.ryanparman.com/fonts/SFProDisplay-Regular.woff2",
            "https://cdn.ryanparman.com/fonts/SFProText-Regular.woff2"
        ],
        "prev": [
            "https://ryanparman.com/posts/2018/the-hiring-process-part-i-what-i-look-for-in-a-cv-resume-remastered/"
        ],
        "profile": [
            "https://gmpg.org/xfn/11",
            "https://microformats.org/profile/h-card",
            "https://microformats.org/profile/h-entry"
        ],
        "read-later": [
            "https://instapaper.com/edit?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026title=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29\u0026description=While%20YouTube%20is%20free%20%28as%20in%20money%29%20to%20use,%20the%20cost%20is%20paid%20in%20terms%20of%20privacy%20and%20advertising%20analytics.%20So%20I%27ve%20decided%20to%20investigate%20self-hosting%20my%20video%20content.%20The%20Cost%20of%20YouTube%20With%20YouTube,%20you%20sacrifice%20privacy%20in%20favor%20of%20cost.%20YouTube%20is%20the%20very%20best%20at%20what%20they%20do%20%28serve%20video%20to%20all%20resolutions%20and%20bandwidths%29,%20and%20they%20are%20backed%20by%20Google%20who%20is%20the%20very%20best%20at%20what%20they%20do%20%28collect%20data%20in%20order%20to%20facilitate%20selling%20a%20primed%20audience%20to%20advertisers%29.",
            "https://pinboard.in/add?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026title=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29\u0026description=While%20YouTube%20is%20free%20%28as%20in%20money%29%20to%20use,%20the%20cost%20is%20paid%20in%20terms%20of%20privacy%20and%20advertising%20analytics.%20So%20I%27ve%20decided%20to%20investigate%20self-hosting%20my%20video%20content.%20The%20Cost%20of%20YouTube%20With%20YouTube,%20you%20sacrifice%20privacy%20in%20favor%20of%20cost.%20YouTube%20is%20the%20very%20best%20at%20what%20they%20do%20%28serve%20video%20to%20all%20resolutions%20and%20bandwidths%29,%20and%20they%20are%20backed%20by%20Google%20who%20is%20the%20very%20best%20at%20what%20they%20do%20%28collect%20data%20in%20order%20to%20facilitate%20selling%20a%20primed%20audience%20to%20advertisers%29.",
            "https://getpocket.com/edit?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026title=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29"
        ],
        "safety": [
            "https://transparencyreport.google.com/https/certificates?cert_search=include_expired:false;include_subdomains:true;domain:ryanparman.com",
            "https://transparencyreport.google.com/copyright/explore?copyright_data_exploration=ce:domain;size:10;q:ryanparman.com",
            "https://transparencyreport.google.com/safe-browsing/search?url=ryanparman.com",
            "https://safeweb.norton.com/report/show?url=ryanparman.com",
            "https://www.siteadvisor.com/sitereport.html?url=ryanparman.com"
        ],
        "share": [
            "https://facebook.com/sharer/sharer.php?u=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/",
            "https://www.linkedin.com/sharing/share-offsite/?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026title=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29",
            "https://pinterest.com/pin/create/button/?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026description=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29\u0026media=https://cdn.ryanparman.com/hugo/posts/2018/adaptive-bitrate-streaming.png",
            "https://www.tumblr.com/share/link?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026name=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29",
            "https://twitter.com/intent/tweet?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026text=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29\u0026via=skyzyx"
        ],
        "shortcut": [
            "https://cdn.ryanparman.com/hugo/favicons/favicon.ico"
        ],
        "structured-data": [
            "https://developers.facebook.com/tools/debug/og/object/?q=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/",
            "https://search.google.com/structured-data/testing-tool#url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/",
            "https://developers.pinterest.com/tools/url-debugger/?link=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/"
        ],
        "stylesheet": [
            "https://ryanparman.com/dist/css/app.ce143f640e183efa5ec41587f838e7c7.css"
        ],
        "work": [
            "https://aws.amazon.com/sdk-for-php/",
            "https://www.mheducation.com",
            "https://github.com/simplepie",
            "https://wepay.com"
        ]
    },
    "rel-urls": {
        "https://ajax.cloudflare.com": {
            "rels": [
                "dns-prefetch"
            ]
        },
        "https://amazon.com/wishlist/KAFYR57E8R81": {
            "rels": [
                "me"
            ],
            "title": "Amazon wishlist",
            "type": "text/html"
        },
        "https://angel.co/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Angel.co",
            "type": "text/html"
        },
        "https://aws.amazon.com/sdk-for-php/": {
            "rels": [
                "work"
            ],
            "title": "AWS SDK for PHP",
            "type": "text/html"
        },
        "https://bandcamp.com/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Bandcamp",
            "type": "text/html"
        },
        "https://betalist.com/@skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Betalist",
            "type": "text/html"
        },
        "https://browser.geekbench.com/user/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Geekbench",
            "type": "text/html"
        },
        "https://cash.me/$rparman": {
            "rels": [
                "me"
            ],
            "title": "Cash.me",
            "type": "text/html"
        },
        "https://cdn.ryanparman.com": {
            "rels": [
                "dns-prefetch"
            ]
        },
        "https://cdn.ryanparman.com/fonts/MesloLGS-Regular.woff2": {
            "rels": [
                "preload"
            ]
        },
        "https://cdn.ryanparman.com/fonts/SFProDisplay-Regular.woff2": {
            "rels": [
                "preload"
            ]
        },
        "https://cdn.ryanparman.com/fonts/SFProText-Regular.woff2": {
            "rels": [
                "preload"
            ]
        },
        "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-114x114.png": {
            "rels": [
                "apple-touch-icon-precomposed"
            ]
        },
        "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-120x120.png": {
            "rels": [
                "apple-touch-icon-precomposed"
            ]
        },
        "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-144x144.png": {
            "rels": [
                "apple-touch-icon-precomposed"
            ]
        },
        "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-152x152.png": {
            "rels": [
                "apple-touch-icon-precomposed"
            ]
        },
        "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-57x57.png": {
            "rels": [
                "apple-touch-icon-precomposed"
            ]
        },
        "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-60x60.png": {
            "rels": [
                "apple-touch-icon-precomposed"
            ]
        },
        "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-72x72.png": {
            "rels": [
                "apple-touch-icon-precomposed"
            ]
        },
        "https://cdn.ryanparman.com/hugo/favicons/apple-touch-icon-76x76.png": {
            "rels": [
                "apple-touch-icon-precomposed"
            ]
        },
        "https://cdn.ryanparman.com/hugo/favicons/favicon-128.png": {
            "rels": [
                "icon"
            ],
            "type": "image/png"
        },
        "https://cdn.ryanparman.com/hugo/favicons/favicon-16x16.png": {
            "rels": [
                "icon"
            ],
            "type": "image/png"
        },
        "https://cdn.ryanparman.com/hugo/favicons/favicon-196x196.png": {
            "rels": [
                "icon"
            ],
            "type": "image/png"
        },
        "https://cdn.ryanparman.com/hugo/favicons/favicon-32x32.png": {
            "rels": [
                "icon"
            ],
            "type": "image/png"
        },
        "https://cdn.ryanparman.com/hugo/favicons/favicon-96x96.png": {
            "rels": [
                "icon"
            ],
            "type": "image/png"
        },
        "https://cdn.ryanparman.com/hugo/favicons/favicon.ico": {
            "rels": [
                "shortcut",
                "icon"
            ],
            "type": "image/x-icon"
        },
        "https://cdn.ryanparman.com/hugo/gravatars/current/8c73b5fc88b88ac0d92fec541c6a4413890df291@4x.jpg": {
            "rels": [
                "gravatar"
            ]
        },
        "https://codepen.io/skyzyx/": {
            "rels": [
                "me"
            ],
            "title": "Codepen",
            "type": "text/html"
        },
        "https://coderwall.com/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Coderwall",
            "type": "text/html"
        },
        "https://dev.to/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Dev.to",
            "type": "text/html"
        },
        "https://developers.facebook.com/tools/debug/og/object/?q=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/": {
            "rels": [
                "structured-data"
            ],
            "title": "Facebook",
            "type": "text/html"
        },
        "https://developers.pinterest.com/tools/url-debugger/?link=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/": {
            "rels": [
                "structured-data"
            ],
            "title": "Pinterest",
            "type": "text/html"
        },
        "https://facebook.com/sharer/sharer.php?u=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/": {
            "rels": [
                "share"
            ],
            "title": "Facebook",
            "type": "text/html"
        },
        "https://feed.ryanparman.com/": {
            "rels": [
                "alternate"
            ],
            "title": ": Blog",
            "type": "application/rss+xml"
        },
        "https://feed.ryanparman.com/conglomo": {
            "rels": [
                "alternate"
            ],
            "title": ": Blog + Link Blog + YouTube + Movies + TV",
            "type": "application/rss+xml"
        },
        "https://feed.ryanparman.com/content": {
            "rels": [
                "alternate"
            ],
            "title": ": Blog + Link Blog",
            "type": "application/rss+xml"
        },
        "https://feed.ryanparman.com/linkblog": {
            "rels": [
                "alternate"
            ],
            "title": ": Link Blog",
            "type": "application/rss+xml"
        },
        "https://feed.ryanparman.com/media": {
            "rels": [
                "alternate"
            ],
            "title": ": YouTube + Movies + TV",
            "type": "application/rss+xml"
        },
        "https://feed.ryanparman.com/movies": {
            "rels": [
                "alternate"
            ],
            "title": ": Movies",
            "type": "application/rss+xml"
        },
        "https://feed.ryanparman.com/tv": {
            "rels": [
                "alternate"
            ],
            "title": ": TV Shows",
            "type": "application/rss+xml"
        },
        "https://feed.ryanparman.com/youtube": {
            "rels": [
                "alternate"
            ],
            "title": ": YouTube Favorites",
            "type": "application/rss+xml"
        },
        "https://feedly.com/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Feedly",
            "type": "text/html"
        },
        "https://forums.aws.amazon.com/profile.jspa?userID=62398": {
            "rels": [
                "me"
            ],
            "title": "AWS Forums",
            "type": "text/html"
        },
        "https://getpocket.com/edit?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026title=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29": {
            "rels": [
                "read-later"
            ],
            "title": "Pocket",
            "type": "text/html"
        },
        "https://github.com/simplepie": {
            "rels": [
                "work"
            ],
            "title": "SimplePie",
            "type": "text/html"
        },
        "https://github.com/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "GitHub",
            "type": "text/html"
        },
        "https://gmpg.org/xfn/11": {
            "rels": [
                "profile"
            ]
        },
        "https://hackerone.com/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "HackerOne",
            "type": "text/html"
        },
        "https://homescreen.me/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Homescreen",
            "type": "text/html"
        },
        "https://hub.docker.com/u/skyzyx/": {
            "rels": [
                "me"
            ],
            "title": "Docker Hub",
            "type": "text/html"
        },
        "https://imdb.com/user/ur24445966/": {
            "rels": [
                "me"
            ],
            "title": "IMDb",
            "type": "text/html"
        },
        "https://instapaper.com/edit?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026title=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29\u0026description=While%20YouTube%20is%20free%20%28as%20in%20money%29%20to%20use,%20the%20cost%20is%20paid%20in%20terms%20of%20privacy%20and%20advertising%20analytics.%20So%20I%27ve%20decided%20to%20investigate%20self-hosting%20my%20video%20content.%20The%20Cost%20of%20YouTube%20With%20YouTube,%20you%20sacrifice%20privacy%20in%20favor%20of%20cost.%20YouTube%20is%20the%20very%20best%20at%20what%20they%20do%20%28serve%20video%20to%20all%20resolutions%20and%20bandwidths%29,%20and%20they%20are%20backed%20by%20Google%20who%20is%20the%20very%20best%20at%20what%20they%20do%20%28collect%20data%20in%20order%20to%20facilitate%20selling%20a%20primed%20audience%20to%20advertisers%29.": {
            "rels": [
                "read-later"
            ],
            "title": "Instapaper",
            "type": "text/html"
        },
        "https://instapaper.com/text?u=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/": {
            "rels": [
                "alternate"
            ],
            "title": "Instapaper Text Reader",
            "type": "text/html"
        },
        "https://itunes.apple.com/profile/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Apple Music",
            "type": "text/html"
        },
        "https://keybase.io/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Keybase",
            "type": "text/html"
        },
        "https://letterboxd.com/skyzyx/": {
            "rels": [
                "me"
            ],
            "title": "Letterboxd",
            "type": "text/html"
        },
        "https://libraries.io/github/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Libraries.io",
            "type": "text/html"
        },
        "https://medium.com/@skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Medium",
            "type": "text/html"
        },
        "https://mercury.postlight.com/amp?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/": {
            "rels": [
                "amphtml"
            ],
            "title": "AMP",
            "type": "text/html"
        },
        "https://metadata.ryanparman.com": {
            "rels": [
                "dns-prefetch"
            ]
        },
        "https://metadata.ryanparman.com/oembed/?url=https%3a%2f%2fryanparman.com%2fposts%2f2018%2fserving-bandwidth-friendly-video-with-hls%2f": {
            "rels": [
                "alternate"
            ],
            "title": "oEmbed",
            "type": "application/json+oembed"
        },
        "https://microformats.org/profile/h-card": {
            "rels": [
                "profile"
            ]
        },
        "https://microformats.org/profile/h-entry": {
            "rels": [
                "profile"
            ]
        },
        "https://news.ycombinator.com/user?id=skyzyx": {
            "rels": [
                "me"
            ],
            "title": "HackerNews",
            "type": "text/html"
        },
        "https://open.spotify.com/user/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Spotify",
            "type": "text/html"
        },
        "https://openid.stackexchange.com/openid/provider": {
            "rels": [
                "openid2.provider"
            ]
        },
        "https://openid.stackexchange.com/user/934ca319-0966-4032-8ff0-3111ad438835": {
            "rels": [
                "openid2.local_id"
            ]
        },
        "https://pandora.com/profile/ryan4516": {
            "rels": [
                "me"
            ],
            "title": "Pandora",
            "type": "text/html"
        },
        "https://pinboard.in/add?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026title=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29\u0026description=While%20YouTube%20is%20free%20%28as%20in%20money%29%20to%20use,%20the%20cost%20is%20paid%20in%20terms%20of%20privacy%20and%20advertising%20analytics.%20So%20I%27ve%20decided%20to%20investigate%20self-hosting%20my%20video%20content.%20The%20Cost%20of%20YouTube%20With%20YouTube,%20you%20sacrifice%20privacy%20in%20favor%20of%20cost.%20YouTube%20is%20the%20very%20best%20at%20what%20they%20do%20%28serve%20video%20to%20all%20resolutions%20and%20bandwidths%29,%20and%20they%20are%20backed%20by%20Google%20who%20is%20the%20very%20best%20at%20what%20they%20do%20%28collect%20data%20in%20order%20to%20facilitate%20selling%20a%20primed%20audience%20to%20advertisers%29.": {
            "rels": [
                "read-later"
            ],
            "title": "Pinboard",
            "type": "text/html"
        },
        "https://pinterest.com/pin/create/button/?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026description=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29\u0026media=https://cdn.ryanparman.com/hugo/posts/2018/adaptive-bitrate-streaming.png": {
            "rels": [
                "share"
            ],
            "title": "Pinterest",
            "type": "text/html"
        },
        "https://pypi.org/user/skyzyx/": {
            "rels": [
                "me"
            ],
            "title": "Pypi",
            "type": "text/html"
        },
        "https://rparman.yelp.com/": {
            "rels": [
                "me"
            ],
            "title": "Yelp!",
            "type": "text/html"
        },
        "https://ryanparman.com": {
            "rels": [
                "home"
            ]
        },
        "https://ryanparman.com/content-license/": {
            "rels": [
                "license"
            ],
            "text": "Content License"
        },
        "https://ryanparman.com/dist/css/app.ce143f640e183efa5ec41587f838e7c7.css": {
            "rels": [
                "stylesheet"
            ]
        },
        "https://ryanparman.com/posts/": {
            "rels": [
                "archives"
            ]
        },
        "https://ryanparman.com/posts/2018/clueless-recruiters-issue-8/": {
            "rels": [
                "next"
            ],
            "title": "Clueless Recruiters, Issue #8"
        },
        "https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/": {
            "rels": [
                "canonical"
            ]
        },
        "https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/index.json": {
            "rels": [
                "alternate"
            ],
            "title": "JSON variant",
            "type": "application/json"
        },
        "https://ryanparman.com/posts/2018/the-hiring-process-part-i-what-i-look-for-in-a-cv-resume-remastered/": {
            "rels": [
                "prev"
            ],
            "title": "The Hiring Process, Part I"
        },
        "https://safeweb.norton.com/report/show?url=ryanparman.com": {
            "rels": [
                "safety"
            ],
            "title": "Norton Safeweb",
            "type": "text/html"
        },
        "https://search.google.com/structured-data/testing-tool#url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/": {
            "rels": [
                "structured-data"
            ],
            "title": "Google",
            "type": "text/html"
        },
        "https://soundcloud.com/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "SoundCloud",
            "type": "text/html"
        },
        "https://stackoverflow.com/users/228514/ryan-parman": {
            "rels": [
                "me"
            ],
            "title": "Stack Overflow",
            "type": "text/html"
        },
        "https://trakt.tv/users/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "trakt.tv",
            "type": "text/html"
        },
        "https://transparencyreport.google.com/copyright/explore?copyright_data_exploration=ce:domain;size:10;q:ryanparman.com": {
            "rels": [
                "safety"
            ],
            "title": "Google Copyright Infringement Report",
            "type": "text/html"
        },
        "https://transparencyreport.google.com/https/certificates?cert_search=include_expired:false;include_subdomains:true;domain:ryanparman.com": {
            "rels": [
                "safety"
            ],
            "title": "Google Certificate Transparency Report",
            "type": "text/html"
        },
        "https://transparencyreport.google.com/safe-browsing/search?url=ryanparman.com": {
            "rels": [
                "safety"
            ],
            "title": "Google Safe Browsing",
            "type": "text/html"
        },
        "https://twitter.com/intent/tweet?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026text=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29\u0026via=skyzyx": {
            "rels": [
                "share"
            ],
            "title": "Twitter",
            "type": "text/html"
        },
        "https://twitter.com/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Twitter",
            "type": "text/html"
        },
        "https://unsplash.com/@skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Unsplash",
            "type": "text/html"
        },
        "https://venmo.com/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Venmo",
            "type": "text/html"
        },
        "https://wepay.com": {
            "rels": [
                "work"
            ],
            "title": "WePay",
            "type": "text/html"
        },
        "https://www.airbnb.com/users/show/47877510": {
            "rels": [
                "me"
            ],
            "title": "Airbnb",
            "type": "text/html"
        },
        "https://www.goodreads.com/user/show/7693108-ryan-parman": {
            "rels": [
                "me"
            ],
            "title": "Goodreads",
            "type": "text/html"
        },
        "https://www.googletagmanager.com": {
            "rels": [
                "dns-prefetch"
            ]
        },
        "https://www.kickstarter.com/profile/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Kickstarter",
            "type": "text/html"
        },
        "https://www.last.fm/user/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Last.fm",
            "type": "text/html"
        },
        "https://www.linkedin.com/in/rparman": {
            "rels": [
                "me"
            ],
            "title": "LinkedIn",
            "type": "text/html"
        },
        "https://www.linkedin.com/sharing/share-offsite/?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026title=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29": {
            "rels": [
                "share"
            ],
            "title": "LinkedIn",
            "type": "text/html"
        },
        "https://www.mheducation.com": {
            "rels": [
                "work"
            ],
            "title": "McGraw-Hill Education",
            "type": "text/html"
        },
        "https://www.npmjs.com/~skyzyx": {
            "rels": [
                "me"
            ],
            "title": "npm",
            "type": "text/html"
        },
        "https://www.paypal.me/rparman": {
            "rels": [
                "me"
            ],
            "title": "Paypal",
            "type": "text/html"
        },
        "https://www.reddit.com/user/skyzyx": {
            "rels": [
                "me"
            ],
            "title": "Reddit",
            "type": "text/html"
        },
        "https://www.siteadvisor.com/sitereport.html?url=ryanparman.com": {
            "rels": [
                "safety"
            ],
            "title": "Site Advisor",
            "type": "text/html"
        },
        "https://www.tumblr.com/share/link?url=https://ryanparman.com/posts/2018/serving-bandwidth-friendly-video-with-hls/\u0026name=Serving%20Bandwidth-Friendly%20Video%20with%20HTTP%20Live%20Streaming%20%28HLS%29": {
            "rels": [
                "share"
            ],
            "title": "Tumblr",
            "type": "text/html"
        },
        "https://www.youtube.com/user/Skyzyx": {
            "rels": [
                "me"
            ],
            "title": "YouTube",
            "type": "text/html"
        }
    }
}