<?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: David Liman</title>
    <description>The latest articles on DEV Community by David Liman (@dvliman).</description>
    <link>https://dev.to/dvliman</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%2F62%2F0517f017-c5b2-49b0-a173-b7b1cdf6b270.jpeg</url>
      <title>DEV Community: David Liman</title>
      <link>https://dev.to/dvliman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dvliman"/>
    <language>en</language>
    <item>
      <title>Don't use clj-time, use clojure.java-time instead</title>
      <dc:creator>David Liman</dc:creator>
      <pubDate>Wed, 10 Jan 2024 03:41:00 +0000</pubDate>
      <link>https://dev.to/dvliman/dont-use-clj-time-use-clojurejava-time-instead-4l26</link>
      <guid>https://dev.to/dvliman/dont-use-clj-time-use-clojurejava-time-instead-4l26</guid>
      <description>&lt;h2&gt;
  
  
  clj-time
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;A date and time library for Clojure, wrapping the Joda Time library. DEPRECATED &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A big warning sign on a project's README should have deterred anyone from using the library but my goldfish memory can't remember why&lt;/p&gt;

&lt;p&gt;I have used &lt;code&gt;clj-time&lt;/code&gt; in my previous project before. I did not recall anything bad about it. Time, just like a logger, in Java has a long history. For the most part, when you have to deal with time, you google around, find which method to call, and move on. &lt;/p&gt;

&lt;p&gt;Until this one fine afternoon, I saw lots of new error logs. The stacktrace looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#error {
 :cause "The datetime zone id 'America/Ciudad_Juarez' is not recognised"
 :via
 [{:type java.lang.IllegalArgumentException
   :message "The datetime zone id 'America/Ciudad_Juarez' is not recognised"
   :at [org.joda.time.DateTimeZone forID "DateTimeZone.java" 234]}]
 :trace
 [[org.joda.time.DateTimeZone forID "DateTimeZone.java" 234]
  [clj_time.core$time_zone_for_id invokeStatic "core.clj" 411]
  [clj_time.core$time_zone_for_id invoke "core.clj" 406]
  [app.domain.hotel$time_zone invokeStatic "hotel.clj" 129]
   ...
  [clojure.core$reduce1 invokeStatic "core.clj" 944]
  [clojure.core$set invokeStatic "core.clj" 4101]
  [org.eclipse.jetty.util.thread.QueuedThreadPool$Runner run "QueuedThreadPool.java" 1034]
  [java.lang.Thread run nil -1]]}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It turns out a partner system sends an update with timezone record like 'America/Ciudad_Juarez'&lt;/p&gt;

&lt;p&gt;Wait, what is wrong with that time zone? Let's fire up a REPL and check that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(require '[clj-time.core :as time])

(time/time-zone-for-id "America/Ciudad_Juarez')
Execution error (IllegalArgumentException) at org.joda.time.DateTimeZone/forID (DateTimeZone.java:234).
The datetime zone id 'America/Ciudad_Juarez' is not recognised
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Uh oh, wait that can't be right. How about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(time/time-zone-for-id "America/Chicago")
;; =&amp;gt; #&amp;lt;org.joda.time.tz.CachedDateTimeZone@23749549 America/Chicago&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one is fine. Quick Google-ing tells me Ciudad Juarez was only &lt;a href="https://mm.icann.org/pipermail/tz-announce/2022-November/000076.html"&gt;recently added to a list of timezones back in 2022&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Okay, how do we upgrade our Clojure / Java system with that latest timezone? Does Java reads the timezone database somewhere? That leads me to &lt;a href="https://www.oracle.com/java/technologies/javase/tzupdater-readme.html"&gt;Oracle TZUpdater&lt;/a&gt; app&lt;/p&gt;

&lt;p&gt;Fair enough. Let's update our Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM debian:bullseye-slim as final
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y curl wget unzip

ENV JAVA_HOME=/usr/local/openjdk-17

RUN wget --header "Cookie: oraclelicense=accept-securebackup-cookie" https://download.oracle.com/otn-pub/java/tzupdater/2.3.2/tzupdater-2.3.2.zip &amp;amp;&amp;amp; \
    unzip tzupdater-2.3.2.zip &amp;amp;&amp;amp; \
    $JAVA_HOME/bin/java -jar tzupdater-2.3.2/tzupdater.jar -f -l https://www.iana.org/time-zones/repository/tzdata-latest.tar.gz

ENTRYPOINT ["java", "-jar", "app-standalone.jar"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a big deal. That should be it, right? Not too soon! Turns out &lt;a href="https://github.com/clj-time/clj-time/"&gt;clj-time&lt;/a&gt; is a wrapper of &lt;a href="https://www.joda.org/joda-time/"&gt;Joda Time&lt;/a&gt; and the project's README says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you are using Java 8 or later, please use the built-in Java Time instead of Joda Time -- or look at clojure.java-time if you want a Clojure wrapper for that, or cljc.java-time for a thin Clojure(Script) wrapper, or juxt/tick for another cross-platform option. See Converting from Joda Time to java.time for more details about the similarities and differences between the two libraries. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Okay so clj-time is deprecated, latest version is 0.15.2. Let's try &lt;a href="https://github.com/dm3/clojure.java-time"&gt;dm3/clojure.java-time&lt;/a&gt;. Sure enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(jt/local-date (jt/instant) "America/Ciudad_Juarez")
;; =&amp;gt; #&amp;lt;java.time.LocalDate@5bfc8967 2024-01-09&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It picks up the new timezone. Now why wouldn't Joda pick up the latest timezone? To be fair, Joda time does. Here is the &lt;a href="https://www.joda.org/joda-time/changes-report.html"&gt;release report&lt;/a&gt;. It bundles new &lt;a href="https://github.com/JodaOrg/global-tz"&gt;Global Tz&lt;/a&gt; which is derived from &lt;a href="https://data.iana.org/time-zones/"&gt;IANA Time Zone database&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;clj-time&lt;/code&gt; wouldn't. It is marked as deprecated. Now my only option is to wholesale adopt java-time, change hundreds of lines of code, or fork clj-time and upgrade to the latest Joda time. Note to myself, pick the newer library for new project guys.&lt;/p&gt;

</description>
      <category>clojure</category>
      <category>java</category>
    </item>
    <item>
      <title>Building a Live Streaming app in Clojure</title>
      <dc:creator>David Liman</dc:creator>
      <pubDate>Tue, 01 Mar 2022 03:13:56 +0000</pubDate>
      <link>https://dev.to/dvliman/building-a-live-streaming-app-in-clojure-329m</link>
      <guid>https://dev.to/dvliman/building-a-live-streaming-app-in-clojure-329m</guid>
      <description>&lt;p&gt;I want to echo &lt;a href="https://twitter.com/ID_AA_Carmack" rel="noopener noreferrer"&gt;John Carmack&lt;/a&gt;’s &lt;a href="https://twitter.com/ID_AA_Carmack/status/1258531455220609025" rel="noopener noreferrer"&gt;tweet that all giant companies use open-source FFmpeg in the backends&lt;/a&gt;. &lt;a href="https://www.ffmpeg.org/" rel="noopener noreferrer"&gt;FFmpeg&lt;/a&gt; is a core piece of technology that powers our live-streaming and recording system at Inspire Fitness. It certainly is high-quality open-source software that we use to record and stream countless hours of workout videos.&lt;/p&gt;

&lt;p&gt;It looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdvliman%2Fdvliman.github.io%2Fmaster%2Fresources%2Fpublic%2Fimages%2Fsession-detail.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdvliman%2Fdvliman.github.io%2Fmaster%2Fresources%2Fpublic%2Fimages%2Fsession-detail.png" alt="session details"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdvliman%2Fdvliman.github.io%2Fmaster%2Fresources%2Fpublic%2Fimages%2Flive-sessions.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fdvliman%2Fdvliman.github.io%2Fmaster%2Fresources%2Fpublic%2Fimages%2Flive-sessions.png" alt="live sessions"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Users can: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;watch live-streaming content, or&lt;/li&gt;
&lt;li&gt;playback on-demand videos from our content library&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;High level&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Behind the scene, we have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;IP cameras are wired up in each studio room.&lt;/li&gt;
&lt;li&gt;The cameras support the RTSP protocol.&lt;/li&gt;
&lt;li&gt;A software pipeline integrated with our custom CMS to broadcast (stream) our cameras feed to the internet while simultaneously recording and storing the content to AWS S3 storage for on-demand playback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;How it all works together:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We configure classes (the recordings) to start at a particular time in our dashboard. The time aligns with our studio schedules, where gym members would often join our classes to work out alongside the instructors.&lt;/li&gt;
&lt;li&gt;We &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/ec2/run-instances.html" rel="noopener noreferrer"&gt;kick off a dedicated ec2 instance with FFmpeg baked in an AMI image&lt;/a&gt; when the class starts. We call this our encoder/transcoder.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight clojure"&gt;&lt;code&gt;&lt;span class="c1"&gt;;; vm.clj&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;aws/invoke&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state/ec2&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;:op&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="no"&gt;:RunInstances&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="no"&gt;:request&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="no"&gt;:InstanceType&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="s"&gt;"c5d.2xlarge"&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="no"&gt;:MaxCount&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="no"&gt;:MinCount&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="no"&gt;:SubnetId&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="s"&gt;"subnet-id"&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="no"&gt;:ImageId&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="s"&gt;"ami-id"&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="no"&gt;:SecurityGroupIds&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"sg-id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="no"&gt;:UserData&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;build-user-data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;class-id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="no"&gt;:TagSpecifications&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="no"&gt;:ResourceType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"instance"&lt;/span&gt;&lt;span class="w"&gt;
                                  &lt;/span&gt;&lt;span class="no"&gt;:Tags&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="no"&gt;:Key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Name"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;:Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;make-class-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;class-id&lt;/span&gt;&lt;span class="p"&gt;)}]}]}})&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;As soon as the ec2 boots up, it runs the cloud-init script, which starts the Clojure process and &lt;a href="https://github.com/tolitius/mount" rel="noopener noreferrer"&gt;mount&lt;/a&gt; (a state management library) that would then starts the dependencies:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight clojure"&gt;&lt;code&gt;&lt;span class="c1"&gt;;; core.clj&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;defn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;run-encoder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mount/start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="ss"&gt;'state/s3&lt;/span&gt;&lt;span class="w"&gt;
               &lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="ss"&gt;'state/db&lt;/span&gt;&lt;span class="w"&gt;
               &lt;/span&gt;&lt;span class="o"&gt;#&lt;/span&gt;&lt;span class="ss"&gt;'encoder/encoder&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;defn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;-main&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;run-encoder&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;This would in turn calls &lt;code&gt;start-stream&lt;/code&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight clojure"&gt;&lt;code&gt;&lt;span class="c1"&gt;;; encoder.clj&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;defstate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="no"&gt;:start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;try&lt;/span&gt;&lt;span class="w"&gt;
           &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;start-stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
           &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;catch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="c1"&gt;;; error-handling here&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="no"&gt;:stop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stop-stream&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;start-stream&lt;/code&gt; logic is actually pretty simple. It pulls feed from our camera and egress out our CDN partner
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight clojure"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;defn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start-stream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;:encoder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;:connect-to-camera&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;make-event-details&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;...&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;manager/register&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;:connect-to-camera&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sh/proc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"ffmpeg"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-hide_banner"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-re"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-rtsp_transport"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"tcp"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-i"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;config/encoder-rtsp-endpoint&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s"&gt;"-c:a"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"aac"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-ar"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"48000"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-b:a"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"128k"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s"&gt;"-c:v"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"h264"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-profile:v"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"high"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s"&gt;"-g"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"48"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-keyint_min"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"48"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-sc_threshold"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-b:v"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"3072k"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s"&gt;"-maxrate"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"3500k"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-vcodec"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"libx264"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-bufsize"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"3072k"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s"&gt;"-hls_time"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"6"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s"&gt;"-hls_playlist_type"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"event"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s"&gt;"-hls_segment_filename"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;segment-file-pattern&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;manifest-file-path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;redirect-stdout-stderr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;:connect-to-camera&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hls-file-path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="c1"&gt;;; it takes some time to pull from the RTSP stream and write to the m3u8 file&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="c1"&gt;;; the HLS egress looks at the m3u8 file; if it can't find it, the process will exit - so wait for the file&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;:encoder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;:wait-for-manifest-file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;make-event-details&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;...&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;:encoder&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;:egress&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;make-event-details&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;...&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;manager/register&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;:egress&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sh/proc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"ffmpeg"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-hide_banner"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-re"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-i"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;manifest-file-path&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s"&gt;"-c:v"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"copy"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-c:a"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"aac"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-ar"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"48000"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-b:a"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"128k"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"-f"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"flv"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;config/encoder-ingress-endpoint&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;redirect-stdout-stderr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="no"&gt;:egress&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;egress-file-path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;The output would be a playback URL that our video player would be pulling from.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Notice we are essentially invoking the FFmpeg that we bundled earlier to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;take input from RTSP transport protocol&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ffmpeg.org/ffmpeg-formats.html" rel="noopener noreferrer"&gt;argument flags for the video/audio codec&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;-re&lt;/td&gt;
&lt;td&gt;read input at native frame rate. Mainly used to simulate a grab device i.e if you wanted to stream a video file, then you would want this, otherwise it might stream too fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-c:a aac&lt;/td&gt;
&lt;td&gt;transcode to AAC codec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-ar 48000&lt;/td&gt;
&lt;td&gt;set the audio sample rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-b:a 128k&lt;/td&gt;
&lt;td&gt;set the audio bitrate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-c:v h264 or copy&lt;/td&gt;
&lt;td&gt;transcode to h264 codec or simply send the frame verbatim to output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-hls_time&lt;/td&gt;
&lt;td&gt;the duration for video segment length&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-f flv&lt;/td&gt;
&lt;td&gt;says to deliver the output stream in an flv wrapper&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;rtmp://&lt;/td&gt;
&lt;td&gt;is where the transcoded video stream get pushed to&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The code is essentially a shell wrapper to FFmpeg command-line arguments. FFmpeg is the swiss-army tool for all video/audio codecs&lt;/p&gt;

&lt;p&gt;The whole &lt;code&gt;encoder.clj&lt;/code&gt; is about 300 lines long with error handling. It handles file uploads (video segment files, FFmpeg logs for debugging), egress to primary and secondary/fallback RTMP slot, shutdown processes, and the ec2 instance when we are done with the recording.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson learned&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This was a rescue project from Go to Clojure. The previous architecture had too many moving pieces, making HTTP requests across multiple micro-services. The main server would crash daily due to improper handling of WebSocket messages, causing messages to be lost and encoder instances not starting up on time.&lt;/p&gt;

&lt;p&gt;The rewrite reduced the complexities. &lt;a href="https://www.youtube.com/watch?v=SxdOUGdseq4" rel="noopener noreferrer"&gt;Simple Made Easy&lt;/a&gt; as &lt;a href="https://clojure.org/about/history" rel="noopener noreferrer"&gt;Rich Hickey&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Rewriting a project is never a good approach considering the opportunity cost. I evaluated a few offerings: &lt;a href="http://mux.com" rel="noopener noreferrer"&gt;mux.com&lt;/a&gt;, Cloudflare Stream, Amazon IVS. On paper, they have all the building blocks we need. In addition, some have features like video analytics, policing/signing playback URL, which would be useful for us.&lt;/p&gt;

&lt;p&gt;Ultimately, the fact that we still had 2 years contract with the CDN company was why we still manage our encoder.&lt;/p&gt;

&lt;p&gt;In hindsight, if we consider the storage costs and S3 egress bandwidth fee on top of the CDN costs, I would probably go for ready-made solutions for our company stage. I would start optimizing when we have more traffic.&lt;/p&gt;

&lt;p&gt;The good thing is that this system works really well for live-streaming workload with programmatic access. (Sidenote: barring occasional internet hiccups in our studio). &lt;/p&gt;

&lt;p&gt;If you have a video production pipeline that involves heavy video editing, going with prebuilt software could be more flexible until you solidify the core functionalities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Special thanks to:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://github.com/crinklywrappr" rel="noopener noreferrer"&gt;Daniel Fitzpatrick&lt;/a&gt; and Vincent Ho. My coworkers helped proofread this article and maintain the broadcast system. I enjoy working with you both ❤️. We even have automated tests to prove the camera stream is working end to end!&lt;/li&gt;
&lt;li&gt;Neil, our product manager, understands tech trade-offs and works with me to balance the product roadmap.&lt;/li&gt;
&lt;li&gt;Daniel Glauser, who hired me for this Clojure gig&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>clojure</category>
      <category>ffmpeg</category>
      <category>rtmp</category>
    </item>
  </channel>
</rss>
