<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Caleb Cho</title>
    <description>The latest articles on DEV Community by Caleb Cho (@chowder).</description>
    <link>https://dev.to/chowder</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F273802%2Fb09af9a4-1f4e-4595-8018-a831a872d06d.jpeg</url>
      <title>DEV Community: Caleb Cho</title>
      <link>https://dev.to/chowder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chowder"/>
    <language>en</language>
    <item>
      <title>Setting Up Mirrors for YouTube Livestreams</title>
      <dc:creator>Caleb Cho</dc:creator>
      <pubDate>Fri, 04 Jun 2021 21:08:57 +0000</pubDate>
      <link>https://dev.to/chowder/setting-up-mirrors-for-youtube-livestreams-4b</link>
      <guid>https://dev.to/chowder/setting-up-mirrors-for-youtube-livestreams-4b</guid>
      <description>&lt;p&gt;Many such as myself like to have lo-fi music streams playing in the background while I'm coding or working. The subtle rhythms of the genre gives a perfect blend of ambience and presence while working on other tasks. &lt;/p&gt;

&lt;p&gt;Though recently I've been finding YouTube to be taking up too much of my already pitiful bandwidth. Chrome DevTools reveals that YouTube was transferring &lt;strong&gt;6.4MB worth of data per minute&lt;/strong&gt; (which admittably isn't a lot, but having to work from home using my phone's cellular network has made this very noticeable). &lt;/p&gt;

&lt;h2&gt;
  
  
  Why not use youtube-dl?
&lt;/h2&gt;

&lt;p&gt;It would seem that &lt;a href="https://github.com/ytdl-org/youtube-dl"&gt;&lt;code&gt;youtube-dl&lt;/code&gt;&lt;/a&gt; would be a perfect solution to the problem, simply selecting the audio playlist of the stream and chucking it into a compatible player like VLC or MPV.&lt;/p&gt;

&lt;p&gt;Unfortunately YouTube livestreams, unlike normal videos, don't have separate audio tracks. A simple &lt;code&gt;youtube-dl --list-formats&lt;/code&gt; will reveal this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chowder@blade:~$ youtube-dl -F https://www.youtube.com/watch?v=5qap5aO4i9A
format code  extension  resolution note
91           mp4        256x144     197k , avc1.42c00b, 30.0fps, mp4a.40.5
92           mp4        426x240     338k , avc1.4d4015, 30.0fps, mp4a.40.5
93           mp4        640x360     829k , avc1.4d401e, 30.0fps, mp4a.40.2
94           mp4        854x480    1380k , avc1.4d401f, 30.0fps, mp4a.40.2
95           mp4        1280x720   2593k , avc1.4d401f, 30.0fps, mp4a.40.2
96           mp4        1920x1080  4715k , avc1.640028, 30.0fps, mp4a.40.2 (best)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Even at the lowest resolution, each 5-second segment of the stream is approximately ~80KB with the audio being only ~30KB of that.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chowder@blade:~$ ls -lh | grep seg.ts
-rw-r--r-- 1 chowder chowder  80K Jun  4 17:32 seg.ts
chowder@blade:~$ ffmpeg -i seg.ts -vn -acodec copy audio-only.ts
...
video:0kB audio:29kB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Obviously there's still some savings to be made if we could somehow obtain &lt;strong&gt;only&lt;/strong&gt; the audio part of a live stream. &lt;/p&gt;
&lt;h2&gt;
  
  
  Setting up a mirror
&lt;/h2&gt;

&lt;p&gt;The idea that came to me was to setup a mirror of the YouTube livestream that would strip the video from the stream segments, passing through only the audio component. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mUFbdxNl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8ixxz81h8s65uplumfdg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mUFbdxNl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8ixxz81h8s65uplumfdg.png" alt="simple-architecture-diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(This would obviously have to be hosted from outside my home network to avoid the bandwidth costs I'm trying to shave in the first place.)&lt;/p&gt;
&lt;h3&gt;
  
  
  FFmpeg to the rescue
&lt;/h3&gt;

&lt;p&gt;Instead of writing an HLS playlist parser, then mapping the download URLs for each segment to URLs from your mirror, I realised &lt;a href="https://www.ffmpeg.org/"&gt;FFmpeg&lt;/a&gt; already provided &lt;strong&gt;all&lt;/strong&gt; of this functionality by being able to &lt;a href="https://ffmpeg.org/ffmpeg-formats.html#hls-1"&gt;ingest an HLS playlist URL&lt;/a&gt; as input, and &lt;a href="https://ffmpeg.org/ffmpeg-formats.html#hls-2"&gt;output a local HLS playlist&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;To do this, I first retrieved the YouTube HLS playlist URL with &lt;code&gt;youtube-dl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;HLS_PLAYLIST_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;youtube-dl &lt;span class="nt"&gt;-f&lt;/span&gt; worst &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$YOUTUBE_LIVESTREAM_URL&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The &lt;code&gt;-f worst&lt;/code&gt; parameter simply instructs youtube-dl to use the worst quality playlist offered, since the audio quality across all playlists are the same.&lt;/p&gt;

&lt;p&gt;Then I passed the URL obtained to FFmpeg to build a local HLS playlist:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HLS_PLAYLIST_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-ac&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-vn&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-f&lt;/span&gt; hls &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-hls_time&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-hls_flags&lt;/span&gt; delete_segments &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-hls_list_size&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
    stream.m3u8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Breaking down the parameters used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-c:a copy&lt;/code&gt; - Use &lt;a href="https://ffmpeg.org/ffmpeg.html#toc-Stream-copy"&gt;stream copy mode&lt;/a&gt; for the audio, which stop ffmpeg from re-encoding the audio stream&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-ac 2&lt;/code&gt; - Use 2 audio channels &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-vn&lt;/code&gt; - No video output &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-f hls&lt;/code&gt; - Output a HLS playlist &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-hls_time 5&lt;/code&gt; - The length of each stream segment in seconds&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-hls_flags delete_segments&lt;/code&gt; - Deletes any segment files no longer referenced by the playlist file, think of it as "garbage collection" for the segment files &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stream.m3u8&lt;/code&gt; - This is the name of the local HLS playlist file generated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running the above command generated a &lt;code&gt;stream.m3u8&lt;/code&gt; file in the current directory, and a series of &lt;code&gt;stream&amp;lt;n&amp;gt;.ts&lt;/code&gt; files each corresponding to a segment of the stream. The &lt;code&gt;stream.m3u8&lt;/code&gt; file was also periodically replaced as the livestream progressed.&lt;/p&gt;
&lt;h3&gt;
  
  
  Hosting the playlist
&lt;/h3&gt;

&lt;p&gt;I booted into an EC2 instance that I had, and wrapped the commands above into a simple shell script:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nb"&gt;cd&lt;/span&gt; /var/www/html/hls-mirror

&lt;span class="nv"&gt;youtube_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://www.youtube.com/watch?v=dQw4w9WgXcQ"&lt;/span&gt;

&lt;span class="nv"&gt;hls_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;youtube-dl &lt;span class="nt"&gt;-f&lt;/span&gt; worst &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$youtube_url&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;exec &lt;/span&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$hls_url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-c&lt;/span&gt;:a copy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-ac&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-vn&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-f&lt;/span&gt; hls &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-hls_time&lt;/span&gt; 5 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-hls_flags&lt;/span&gt; delete_segments &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-hls_list_size&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-hide_banner&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-loglevel&lt;/span&gt; error &lt;span class="se"&gt;\&lt;/span&gt;
    stream.m3u8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And setup the script as a daemon service with &lt;code&gt;systemd&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;HLS Mirror&lt;/span&gt;

&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/local/bin/run-hls-mirror&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;multi-user.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The playlist and segment files were then served through Apache. Though you might find this &lt;a href="https://gist.github.com/willurd/5720255"&gt;list of static HTTP server one-liners&lt;/a&gt; useful for similar purposes.&lt;/p&gt;

&lt;p&gt;That's pretty much it - at this point I was able to open a network stream to the URL of the &lt;code&gt;stream.m3u8&lt;/code&gt; file in VLC, and start enjoying my low-footprint audio livestream.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Vw_K2bdm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ctdpwe5q4qfth81qurq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Vw_K2bdm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0ctdpwe5q4qfth81qurq.png" alt="vlc-stats"&gt;&lt;/a&gt; &lt;/p&gt;
&lt;h2&gt;
  
  
  Setting up a frontend player
&lt;/h2&gt;

&lt;p&gt;I also decided to setup a web-based frontend so that I can easily access the livestream without needing to download another app for it.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://videojs.com/"&gt;Video.js&lt;/a&gt; seemed like a good choice for this sort of thing, and so I hand-rolled some HTML, CSS and JS to &lt;a href="https://chowder.github.io/lofi-radio/"&gt;put together a simple site&lt;/a&gt; hosted on GitHub Pages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--PN0ixAyH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ccslu3mnkpj3bqgkwy3o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PN0ixAyH--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ccslu3mnkpj3bqgkwy3o.png" alt="frontend-player"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, I can continue having lo-fi music in the background of my Zoom calls without getting cut off... 😃&lt;/p&gt;
&lt;h2&gt;
  
  
  Can you do this with AWS Lambda?
&lt;/h2&gt;

&lt;p&gt;Requiring an entire VPS for this purpose may be a little overkill for some – in my case I had already been running an instance for other reasons and so the entire project came at "zero" additional cost; though it had me thinking if it was possible to reduce the cost of hosting this by going serverless.&lt;/p&gt;

&lt;p&gt;The first thing I considered was to define a set of Lambda functions that would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse the playlist file from YouTube &lt;/li&gt;
&lt;li&gt;Generate and store a mapping for each segment file &lt;/li&gt;
&lt;li&gt;Return a playlist file with the segment URLs mapped to your own &lt;/li&gt;
&lt;li&gt;At request, fetch the original segment files, and return them with the video components stripped &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Unfortunately the HLS API for YouTube is setup in such a way that you can only stream it from a single source IP address, while each invocation of a Lambda would be from a different IP address. &lt;/p&gt;

&lt;p&gt;While there are multiple ways to &lt;a href="https://dev.to/alexanderdamiani/aws-lambda-with-a-static-ip-2g3l"&gt;setup static IP addresses for Lambdas&lt;/a&gt;, all of them require setting up a VPC with a NAT Gateway, which will cost you at minimum $30/month/availability zone. &lt;/p&gt;

&lt;p&gt;In the end, with EC2 instances being as cheap as they are at the low end (especially for &lt;a href="https://aws.amazon.com/ec2/pricing/reserved-instances/"&gt;reserved instances&lt;/a&gt;) I didn't see a compelling reason to further cost optimise. &lt;/p&gt;
&lt;h2&gt;
  
  
  Source
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--i3JOwpme--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev.to/assets/github-logo-ba8488d21cd8ee1fee097b8410db9deaa41d0ca30b004c0c63de0a479114156f.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/chowder"&gt;
        chowder
      &lt;/a&gt; / &lt;a href="https://github.com/chowder/lofi-radio"&gt;
        lofi-radio
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      LoFi Radio powered by a HLS mirror
    &lt;/h3&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Thanks Sam Corbett for reviewing this post)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
