<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://bjia56.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://bjia56.github.io/" rel="alternate" type="text/html" /><updated>2026-01-18T13:15:02+00:00</updated><id>https://bjia56.github.io/feed.xml</id><title type="html">Brett’s Blog</title><author><name>Brett Jia</name></author><entry><title type="html">Coding with Samsung DeX</title><link href="https://bjia56.github.io/development/coding-with-samsung-dex/" rel="alternate" type="text/html" title="Coding with Samsung DeX" /><published>2025-07-11T03:30:00+00:00</published><updated>2025-07-11T03:30:00+00:00</updated><id>https://bjia56.github.io/development/coding-with-samsung-dex</id><content type="html" xml:base="https://bjia56.github.io/development/coding-with-samsung-dex/"><![CDATA[<p>One of my favorite features of Samsung smartphones is <a href="https://www.samsung.com/us/apps/dex/">DeX</a>, an Android-based
desktop environment available when the phone is connected to an external monitor.
Though DeX can be used to get more screen real estate for watching videos or running
apps, I’ve recently begun using it as a minimal but functional coding environment.</p>

<p>In this post, I’ll walk through a basic Linux-like development setup and talk
about <a href="#is-it-worth-it">my opinions</a> at the ends.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/coding-with-samsung-dex/cosmotop.jpg" alt="" />
<br /><em>Developing</em> <a href="https://github.com/bjia56/cosmotop"><code class="language-plaintext highlighter-rouge">cosmotop</code></a> <em>on DeX</em></p>

<h2 id="hardware-and-dex">Hardware and DeX</h2>

<p>Unsurprisingly, Samsung DeX requires a Samsung device, not just any Android.
I’ve tested on a Samsung Galaxy phone, but Samsung tablets should also work.</p>

<p>For DeX to activate, you will need an external monitor and a USB-C cable capable
of transmitting video signal. The USB-C cable connects to the phone, and the
other end connects to the monitor. When the phone is plugged into a display, the
DeX desktop will show up on the monitor, and the phone turns into a touchpad for
mouse controls.</p>

<p>When connected to a portable USB monitor, the phone can act as a power source
and power the monitor through the cable, though at the cost of faster battery
drain.
For advanced usage, a laptop USB-C docking station or hub with support for
monitors also works, and can also provide power back to the phone to charge it
while in use. However, multi-display docks will not provide an extended desktop,
instead mirroring a single desktop on all screens. The dock can also provide
extra USB ports, which can handle useful peripherals like a physical keyboard
and mouse.</p>

<h2 id="installing-termux">Installing Termux</h2>

<p>To get a Linux environment on the phone, we will use the <a href="https://github.com/termux/termux-app">Termux app</a>
and <a href="https://wiki.termux.com/wiki/PRoot">proot-distro</a> to install a full Debian distribution. This will allow
us to download and install <a href="https://code.visualstudio.com">Visual Studio Code</a> and access it through
the browser.</p>

<p>To install Termux, get the app from <a href="https://f-droid.org/en/packages/com.termux/">F-Droid</a>. The Google Play
Store version is <a href="https://github.com/termux/termux-app/discussions/4000">not recommended</a>.</p>

<p>Configure Termux to access the phone’s storage with the following command. This
will be used to download and install Visual Studio Code later.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>termux-setup-storage
</code></pre></div></div>

<p class="notice--warning"><strong>WARNING:</strong> Setting up software can involve downloading a large amount of files.
Ensure your device is connected to Wi-Fi to minimize data charges.</p>

<p>While Termux provides a usable Linux environment, a more complete distribution
can be installed with <code class="language-plaintext highlighter-rouge">proot-distro</code>.
Install <code class="language-plaintext highlighter-rouge">proot-distro</code> and Debian as follows:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pkg <span class="nb">install </span>proot-distro
proot-distro <span class="nb">install </span>debian
</code></pre></div></div>

<p class="notice--info"><strong>NOTE:</strong> For other available Linux distributions, run <code class="language-plaintext highlighter-rouge">proot-distro list</code>.</p>

<p>Once complete, a root shell can be launched in the distribution:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>proot-distro login debian
</code></pre></div></div>

<h2 id="installing-visual-studio-code">Installing Visual Studio Code</h2>

<p>To install Visual Studio Code, get the <code class="language-plaintext highlighter-rouge">.deb</code> package for Linux Arm64 from
<a href="https://code.visualstudio.com/Download">this page</a>. Inside the <code class="language-plaintext highlighter-rouge">proot-distro</code> shell, update <code class="language-plaintext highlighter-rouge">apt</code>
and install the package.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt update
<span class="c"># Note: Your downloaded file name may differ</span>
apt <span class="nb">install</span> /data/data/com.termux/files/home/storage/downloads/code_1.102.0-1752099871_arm64.deb
</code></pre></div></div>

<p>Sit back for a few moments and let the install finish.</p>

<p class="notice--info"><strong>NOTE:</strong> If your downloads folder is inaccessible, ensure you have set up
Termux’s access to your phone’s storage with <code class="language-plaintext highlighter-rouge">termux-setup-storage</code> outside of
<code class="language-plaintext highlighter-rouge">proot-distro</code>.</p>

<p>Once completed, run Visual Studio Code in browser mode. This will allow you to
use it through a web browser, without the need for a graphical environment
within Termux.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">DOTNET_GCHeapHardLimit</span><span class="o">=</span>0x400000000 code serve-web
</code></pre></div></div>

<p class="notice--info"><strong>NOTE:</strong> In this command, a dotnet environment variable is set to ensure that Visual
Studio Code extensions can be installed correctly. More specifically, the
program that verifies extension checksums relies on dotnet, and will crash
without setting this memory limit.</p>

<p>The command will output a localhost url, containing a token query parameter.
Copy the entire url and open it in the browser.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/coding-with-samsung-dex/landing.jpg" alt="" />
<br /><em>Running Visual Studio Code through the browser on DeX</em></p>

<p>That’s it! You can now access the shell, install extensions, and write code.</p>

<h2 id="is-it-worth-it">Is it worth it?</h2>

<p>While doing some limited coding via DeX is doable, I would not recommend ditching
your primary development machine for the latest Galaxy smartphone. At the end of the day,
though Termux and <code class="language-plaintext highlighter-rouge">proot-distro</code> provides a level of compatibility with traditional Linux distributions via emulation,
DeX still operates under the limitations and restrictions imposed by unrooted Android.
For example, access to system paths such as <code class="language-plaintext highlighter-rouge">/proc</code> and <code class="language-plaintext highlighter-rouge">/sys</code> is limited, and the performance hit of <code class="language-plaintext highlighter-rouge">proot</code>
is noticeable when running Visual Studio Code in more complex repos. Running a few C++ compilation jobs
in parallel can also cause Termux to exit due to high resource usage, though this is fixable through tweaking <a href="https://github.com/agnostic-apollo/Android-Docs/blob/master/en/docs/apps/processes/phantom-cached-and-empty-processes.md#commands-for-android-14-and-higher">Developer options</a>.</p>

<p>Additionally, using the phone as a touchpad instead of a dedicated mouse can be a challenge
to get used to.
For example, scrolling on the touchpad can cause Chrome’s navigation bar to show and hide, which can be
disorienting when compared to a fixed bar when scrolling on a traditional desktop.
The virtual keyboard also sometimes pops up when text entry widgets are focused, even if a physical
keyboard is present. Though the virtual keyboard goes away when something is typed on the
physical keyboard, the virtual keyboard makes it difficult to swipe on the touchpad and
can lead to unintended extra letters typed.</p>

<p>Despite these challenges, I have been able to develop C++ applications and use
Intellisense and GitHub Copilot through the Visual Studio Code setup. Some parts
are slow, but in general, DeX is usable - in fact, this entire blog post was
written through this setup!
I would treat it as a last-resort setup for coding and
certainly not a replacement for developing on a proper laptop or desktop.</p>]]></content><author><name>Brett Jia</name></author><category term="development" /><summary type="html"><![CDATA[One of my favorite features of Samsung smartphones is DeX, an Android-based desktop environment available when the phone is connected to an external monitor. Though DeX can be used to get more screen real estate for watching videos or running apps, I’ve recently begun using it as a minimal but functional coding environment.]]></summary></entry><entry><title type="html">FFmpeg Transcoding Helper</title><link href="https://bjia56.github.io/ffmpeg/ffmpeg-transcoding-helper/" rel="alternate" type="text/html" title="FFmpeg Transcoding Helper" /><published>2025-05-27T18:00:00+00:00</published><updated>2025-05-27T18:00:00+00:00</updated><id>https://bjia56.github.io/ffmpeg/ffmpeg-transcoding-helper</id><content type="html" xml:base="https://bjia56.github.io/ffmpeg/ffmpeg-transcoding-helper/"><![CDATA[<p>This tool generates FFmpeg output arguments for transcoding video streams, for use cases such as Scrypted’s rebroadcast FFmpeg output arguments.</p>

<div>
  <form id="ffmpeg-form" style="background:rgba(0,0,0,0); padding: 0px 0px 22px 0px" onsubmit="event.preventDefault(); generateClicked();">
    <label style="display:block;margin-bottom:18px;">
      <span style="display:block;margin-bottom:5px;font-weight:600;">Input Video Codec:</span>
      <select id="inputCodec" style="width:100%;padding:7px 10px;border:1px solid #ccc;border-radius:4px;font-size:15px;color: #000000">
        <option value="h264">H.264</option>
        <option value="h265">H.265</option>
        <option value="other">Other</option>
      </select>
    </label>

    <label style="display:block;margin-bottom:18px;">
      <span style="display:block;margin-bottom:5px;font-weight:600;">Desired Video Codec:</span>
      <select id="codec" style="width:100%;padding:7px 10px;border:1px solid #ccc;border-radius:4px;font-size:15px;color: #000000">
        <option value="h264">H.264</option>
        <option value="h265">H.265</option>
      </select>
    </label>

    <label style="display:block;margin-bottom:18px;">
      <span style="display:block;margin-bottom:5px;font-weight:600;">Hardware Acceleration:</span>
      <select id="hwAccel" style="width:100%;padding:7px 10px;border:1px solid #ccc;border-radius:4px;font-size:15px;color: #000000">
        <option value="cpu">CPU Only (libx264/libx265)</option>
        <option value="nvenc">NVIDIA GPU (NVENC)</option>
        <option value="amf">AMD GPU (AMF)</option>
        <option value="qsv">Intel GPU (QSV)</option>
        <option value="videotoolbox">Apple VideoToolbox (macOS)</option>
        <option value="vaapi">VAAPI (Linux Intel/AMD)</option>
      </select>
    </label>

    <label style="display:block;margin-bottom:18px;">
      <span style="display:block;margin-bottom:5px;font-weight:600;">Resolution:</span>
      <select id="resolution" style="width:100%;padding:7px 10px;border:1px solid #ccc;border-radius:4px;font-size:15px;color: #000000">
        <option value="original">Original</option>
        <option value="1920:1080">1080p (1920x1080)</option>
        <option value="1280:720">720p (1280x720)</option>
        <option value="640:480">VGA (640x480)</option>
      </select>
    </label>

    <label style="display:flex;align-items:center;margin-bottom:22px;">
      <input type="checkbox" id="aspectRatio" style="margin-right:8px;" />
      <span style="font-weight:600;">Keep Input Aspect Ratio</span>
    </label>

    <label style="display:block;margin-bottom:18px;">
      <span style="display:block;margin-bottom:5px;font-weight:600;">Rotate/Transpose:</span>
      <select id="transpose" style="width:100%;padding:7px 10px;border:1px solid #ccc;border-radius:4px;font-size:15px;color: #000000">
        <option value="none">None</option>
        <option value="1">90° Right (clockwise)</option>
        <option value="2">90° Left (counterclockwise)</option>
        <option value="180">180°</option>
      </select>
    </label>

    <label style="display:flex;align-items:center;margin-bottom:22px;">
      <input type="checkbox" id="mirror" style="margin-right:8px;" />
      <span style="font-weight:600;">Mirror Video</span>
    </label>

    <button type="submit" style="width:100%;padding:12px 0;background:#0057d9;color:white;font-weight:700;border:none;border-radius:6px;font-size:16px;cursor:pointer;transition:background 0.2s;">
      Generate
    </button>
  </form>

  <!-- Output Arguments Line with Floating Copy Icon -->
  <div style="display:block;position:relative;padding: 0px 0px">
    <input id="ffmpeg-output" type="text" readonly="" style="width:100%;padding:10px 44px 10px 8px;font-size:15px;border:1px solid #bbb;border-radius:4px;background:#ffffff;color:#000000" placeholder="FFmpeg arguments" />
    <button id="ffmpeg-copy-btn" type="button" style="
        position:absolute;top:50%;right:10px;transform:translateY(-50%);
        background:transparent;border:none;outline:none;cursor:pointer;
        padding:0;margin:0;height:28px;width:28px;display:flex;align-items:center;justify-content:center;
      " title="Copy to clipboard">
      <!-- Clipboard SVG Icon -->
      <svg id="copy-icon" xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="none" viewBox="0 0 24 24">
        <rect x="9" y="9" width="10" height="12" rx="2" fill="#008060" opacity="0.15" />
        <rect x="5" y="3" width="10" height="12" rx="2" stroke="#008060" stroke-width="2" fill="none" />
        <rect x="9" y="7" width="10" height="12" rx="2" stroke="#008060" stroke-width="2" fill="none" />
      </svg>
    </button>
  </div>

  <script>
    // Copy to clipboard logic
    document.getElementById('ffmpeg-copy-btn').onclick = function () {
      const output = document.getElementById('ffmpeg-output');
      const text = output.value;

      if (navigator.clipboard) {
        navigator.clipboard.writeText(text)
          .then(() => {
            const icon = document.getElementById('copy-icon');
            icon.style.filter = "drop-shadow(0 0 2px #0f0)";
            setTimeout(() => { icon.style.filter = ""; }, 1200);
          })
          .catch(() => {
            alert('Failed to copy!');
          });
      } else {
        // Fallback to legacy method
        output.select();
        output.setSelectionRange(0, 99999);
        let success = false;
        try {
          success = document.execCommand('copy');
        } catch (err) {}
        const icon = document.getElementById('copy-icon');
        if (success) {
          icon.style.filter = "drop-shadow(0 0 2px #0f0)";
          setTimeout(() => { icon.style.filter = ""; }, 1200);
        } else {
          alert('Failed to copy!');
        }
      }
    };

    function generateClicked() {
      // Fetch the selected values from each form control
      const inputCodec = document.getElementById("inputCodec").value;
      const codec = document.getElementById("codec").value;
      const hwAccel = document.getElementById("hwAccel").value;
      const resolution = document.getElementById("resolution").value;
      const keepAspectRatio = document.getElementById("aspectRatio").checked;
      const transpose = document.getElementById("transpose").value;
      const mirror = document.getElementById("mirror").checked;

      // Video filters
      let vf = [];

      // Resolution
      if (resolution !== "original") {
        // e.g., "1280x720"
        if (keepAspectRatio) {
          const parts = resolution.split(":");
          vf.push(`scale=${parts[0]}:-1`);
        } else {
          vf.push(`scale=${resolution}`);
        }
      }

      // Transpose (rotation)
      if (transpose !== "none") {
        if (transpose === "180") {
          vf.push("transpose=1,transpose=1");
        } else {
          vf.push(`transpose=${transpose}`);
        }
      }

      // Mirror (horizontal flip)
      if (mirror) {
        vf.push("hflip");
      }

      // Start building ffmpeg arguments
      let args = [];

      // VAAPI requires special handling
      if (hwAccel === "vaapi") {
        args.push("-vaapi_device /dev/dri/renderD128");
        if (vf.length > 0) {
          vf.unshift("format=nv12,hwupload");
        } else {
          vf.push("format=nv12,hwupload");
        }
      }

      // Video codec
      if (inputCodec === codec && vf.length === 0 && hwAccel === "cpu") {
        args.push("-vcodec copy");
      } else {
        // Select encoder based on hardware acceleration and codec
        let encoder = "";
        let encoderParams = "";

        if (hwAccel === "cpu") {
          if (codec === "h264") {
            encoder = "libx264";
            encoderParams = "-crf 23 -preset ultrafast";
          } else if (codec === "h265") {
            encoder = "libx265";
            encoderParams = "-crf 28 -preset ultrafast";
          }
        } else if (hwAccel === "nvenc") {
          if (codec === "h264") {
            encoder = "h264_nvenc";
            encoderParams = "-preset p4 -rc vbr -cq 23";
          } else if (codec === "h265") {
            encoder = "hevc_nvenc";
            encoderParams = "-preset p4 -rc vbr -cq 23";
          }
        } else if (hwAccel === "amf") {
          if (codec === "h264") {
            encoder = "h264_amf";
            encoderParams = "-quality balanced -rc cqp -qp_i 23";
          } else if (codec === "h265") {
            encoder = "hevc_amf";
            encoderParams = "-quality balanced -rc cqp -qp_i 23";
          }
        } else if (hwAccel === "qsv") {
          if (codec === "h264") {
            encoder = "h264_qsv";
            encoderParams = "-preset medium -global_quality 23";
          } else if (codec === "h265") {
            encoder = "hevc_qsv";
            encoderParams = "-preset medium -global_quality 23";
          }
        } else if (hwAccel === "videotoolbox") {
          if (codec === "h264") {
            encoder = "h264_videotoolbox";
            encoderParams = "-q:v 23 -realtime 1";
          } else if (codec === "h265") {
            encoder = "hevc_videotoolbox";
            encoderParams = "-q:v 23 -realtime 1";
          }
        } else if (hwAccel === "vaapi") {
          if (codec === "h264") {
            encoder = "h264_vaapi";
            encoderParams = "-qp 23";
          } else if (codec === "h265") {
            encoder = "hevc_vaapi";
            encoderParams = "-qp 23";
          }
        }

        args.push(`-c:v ${encoder} ${encoderParams}`);

        // Add common parameters for non-copy encoding
        if (hwAccel === "cpu") {
          args.push("-pix_fmt yuvj420p -bf 0 -g 60 -r 15");
        } else {
          // Hardware encoders typically use different pixel formats and settings
          args.push("-g 60 -r 15");
        }
      }

      if (vf.length > 0) {
        args.push(`-vf ${vf.join(',')}`);
      }

      // Join the arguments string
      const ffmpegArgs = args.join(" ");

      // Output to the arguments box
      document.getElementById("ffmpeg-output").value = ffmpegArgs;
    }
  </script>
</div>]]></content><author><name>Brett Jia</name></author><category term="ffmpeg" /><summary type="html"><![CDATA[This tool generates FFmpeg output arguments for transcoding video streams, for use cases such as Scrypted’s rebroadcast FFmpeg output arguments.]]></summary></entry><entry><title type="html">Packaging terminal apps for Scrypted</title><link href="https://bjia56.github.io/scrypted/packaging-terminal-apps-for-scrypted/" rel="alternate" type="text/html" title="Packaging terminal apps for Scrypted" /><published>2025-01-17T00:00:00+00:00</published><updated>2025-01-17T00:00:00+00:00</updated><id>https://bjia56.github.io/scrypted/packaging-terminal-apps-for-scrypted</id><content type="html" xml:base="https://bjia56.github.io/scrypted/packaging-terminal-apps-for-scrypted/"><![CDATA[<p>The major reimplementation of Scrypted’s management UI in 2024 introduced a way
for plugins to expose custom interactive terminals to users. Similar to how
Scrypted’s management UI provides shell access via a web terminal (which itself
allows for some <a href="/scrypted/diving-into-the-scrypted-terminal#using-scrypted-as-a-tcp-tunnel">neat tricks</a>), this feature provides an xterm.js window on the
plugin’s device page, allowing plugins to render interactive terminal apps directly
to the browser.</p>

<p>Looking to add new terminal apps? Skip to the relevant <a href="#adding-additional-applications">technical part</a>.</p>

<h2 id="examples">Examples</h2>

<table style="margin-bottom: 0">
    <tr>
        <td style="border-right: 1px solid #3d4046; border-bottom: 0">
            <a href="/assets/images/packaging-terminal-apps-for-scrypted/terminalservice.png">
                <img src="/assets/images/packaging-terminal-apps-for-scrypted/terminalservice.png" />
            </a>
        </td>
        <td style="border-right: 1px solid #3d4046; border-bottom: 0">
            <a href="/assets/images/packaging-terminal-apps-for-scrypted/btop.png">
                <img src="/assets/images/packaging-terminal-apps-for-scrypted/btop.png" />
            </a>
        </td>
        <td style="border-right: 1px solid #3d4046; border-bottom: 0">
            <a href="/assets/images/packaging-terminal-apps-for-scrypted/fastfetch.png">
                <img src="/assets/images/packaging-terminal-apps-for-scrypted/fastfetch.png" />
            </a>
        </td>
        <td style="border-right: 1px solid #3d4046; border-bottom: 0">
            <a href="/assets/images/packaging-terminal-apps-for-scrypted/2048.png">
                <img src="/assets/images/packaging-terminal-apps-for-scrypted/2048.png" />
            </a>
        </td>
        <td style="border-bottom: 0">
            <a href="/assets/images/packaging-terminal-apps-for-scrypted/pipes.png">
                <img src="/assets/images/packaging-terminal-apps-for-scrypted/pipes.png" />
            </a>
        </td>
    </tr>
</table>
<p style="font-size: 80%; text-align: center;"><em>Some examples of terminal applications in Scrypted.</em></p>

<p>The most well-known usage of Scrypted’s terminal feature is <code class="language-plaintext highlighter-rouge">TerminalService</code>, provided
by <code class="language-plaintext highlighter-rouge">@scrypted/core</code>. This device connects the browser to an interactive shell and is
the backend to the management UI’s main web terminal.</p>

<p><code class="language-plaintext highlighter-rouge">btop</code>, a system monitoring tool, can be installed by <code class="language-plaintext highlighter-rouge">@scrypted/btop</code>. Notice that the
plugin’s device page renders the terminal app directly, which supports both key strokes
and mouse clicks in the browser.</p>

<p><code class="language-plaintext highlighter-rouge">fastfetch</code>, a system information tool, can be installed by <code class="language-plaintext highlighter-rouge">@scrypted/fastfetch</code>. Again,
loading the plugin’s device page runs the tool, printing its output to the screen.</p>

<p>There are also less serious plugins that use this custom terminal. <code class="language-plaintext highlighter-rouge">@bjia56/scrypted-2048</code>
allows users to play the classic 2048 game in the browser. <code class="language-plaintext highlighter-rouge">@bjia56/scrypted-pipes-screensaver</code>
renders a 2D version of the classic pipes screensaver in the same terminal.</p>

<h2 id="how-it-works">How it works</h2>

<p>The new Scrypted management UI added support for two device interfaces: <code class="language-plaintext highlighter-rouge">StreamService</code> and
<code class="language-plaintext highlighter-rouge">TTY</code>. <code class="language-plaintext highlighter-rouge">StreamService</code> has existed for some time now, being used as the transit protocol of the
web terminal, plugin console, and plugin REPL. The <code class="language-plaintext highlighter-rouge">TTY</code> interface is comparatively newer and
signals to the management UI that a Scrypted device supports interactive terminals. When both
interfaces are present, the UI renders an xterm.js window and calls <code class="language-plaintext highlighter-rouge">connectStream</code> (a <code class="language-plaintext highlighter-rouge">StreamService</code>
function) to create a bidirectional terminal connection, sending application output to the browser
and user input to the application.</p>

<h2 id="adding-additional-applications">Adding additional applications</h2>

<p>Before packaging an application in a plugin, it’s important to consider the platforms that will
be able to run the program. As of this writing, Scrypted’s supported platforms include Linux
(x86_64 and arm64), MacOS (x86_64 and arm64), and Windows (x86_64). Ideally, a program should have
available distributions for each of these platforms for the broadest user accessibility, but it is
not required. If a platform is unsupported, the best practice is for the plugin to state which ones
it <em>does</em> support in documentation and display an appropriate error on unsupported platforms.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// sample code to display an unsupported platform alert in the management UI</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">platform</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">win32</span><span class="dl">"</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">log</span><span class="p">.</span><span class="nx">a</span><span class="p">(</span><span class="dl">"</span><span class="s2">Windows is not supported</span><span class="dl">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="distributing-the-application">Distributing the application</h3>

<p>If the application is a compiled binary, consider downloading it at plugin launch from a trusted location,
such as GitHub releases, over HTTPS. The binary can be downloaded to <code class="language-plaintext highlighter-rouge">process.env.SCRYPTED_PLUGIN_VOLUME</code>,
which contains the root directory of the plugin under Scrypted’s volume directory tree. It’s recommended to
name the download file with the host OS and architecture (or otherwise store such information on disk) and
check it on plugin startup, to account for users zipping up their volumes directory and moving it to a different
host. Additionally, consider storing version or checksum information, so the binary does not need to
be redownloaded on every plugin startup while still retaining the ability to update it when a newer version is
available.</p>

<p>Scrypted plugins are published as npm packages, so a possible (but not recommended) method is to bundle the
application with the plugin bundle. This can be done by placing the application file(s) under the <code class="language-plaintext highlighter-rouge">fs</code> directory
before building and publishing. After installation, the files will be placed under the following path:</p>

<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">SCRYPTED_PLUGIN_VOLUME</span> <span class="o">+</span> <span class="dl">"</span><span class="s2">/zip/unzipped/fs/</span><span class="dl">"</span> <span class="o">+</span> <span class="nx">your</span> <span class="nx">file</span></code></pre></figure>

<p>While this approach works well for platform-agnostic applications (such as scripts), it’s not ideal for compiled
binaries, since multiple binaries may need to be included in the same bundle to cover all supported platforms.</p>

<h3 id="running-the-application-in-scrypted">Running the application in Scrypted</h3>

<p>As mentioned earlier, Scrypted devices must implement the <code class="language-plaintext highlighter-rouge">StreamService</code> and <code class="language-plaintext highlighter-rouge">TTY</code> devices. The
core functionality lies in the <code class="language-plaintext highlighter-rouge">connectStream</code> function of <code class="language-plaintext highlighter-rouge">StreamService</code>, where implementations
must launch the application as a subprocess, create a pty, then connect the pty with the management
UI’s stream. It’s also good practice to implement flow control in this stream, so both the terminal
application and the management UI are not overloaded with large amounts of data.</p>

<p>This all sounds like a lot of work to get a simple application running. Thankfully, we have an easier
way: leverage <code class="language-plaintext highlighter-rouge">TerminalService</code> under <code class="language-plaintext highlighter-rouge">@scrypted/core</code> to handle this for us! We can simply fetch
a reference to <code class="language-plaintext highlighter-rouge">TerminalService</code> (which implements <code class="language-plaintext highlighter-rouge">StreamService</code> and already provides flow control)
through the Scrypted SDK, tell it which program to execute, then bridge the two <code class="language-plaintext highlighter-rouge">connectStream</code> functions.</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">MyDevice</span> <span class="k">implements</span> <span class="nx">StreamService</span> <span class="p">{</span>
    <span class="k">async</span> <span class="nx">connectStream</span><span class="p">(</span><span class="nx">input</span><span class="p">:</span> <span class="nx">AsyncGenerator</span><span class="o">&lt;</span><span class="kr">any</span><span class="p">,</span> <span class="k">void</span><span class="o">&gt;</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">AsyncGenerator</span><span class="o">&lt;</span><span class="kr">any</span><span class="p">,</span> <span class="k">void</span><span class="o">&gt;&gt;</span> <span class="p">{</span>
        <span class="c1">// get a reference to @scrypted/core</span>
        <span class="kd">const</span> <span class="nx">core</span> <span class="o">=</span> <span class="nx">sdk</span><span class="p">.</span><span class="nx">systemManager</span><span class="p">.</span><span class="nx">getDeviceByName</span><span class="o">&lt;</span><span class="nx">DeviceProvider</span><span class="o">&gt;</span><span class="p">(</span><span class="dl">"</span><span class="s2">@scrypted/core</span><span class="dl">"</span><span class="p">);</span>
        <span class="c1">// get a reference to TerminalService</span>
        <span class="kd">const</span> <span class="nx">terminalService</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">core</span><span class="p">.</span><span class="nx">getDevice</span><span class="p">(</span><span class="dl">"</span><span class="s2">terminalservice</span><span class="dl">"</span><span class="p">);</span>
        <span class="c1">// recommended: connect directly to the device to skip a round trip through Scrypted server</span>
        <span class="kd">const</span> <span class="nx">terminalServiceDirect</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">sdk</span><span class="p">.</span><span class="nx">connectRPCObject</span><span class="p">(</span><span class="nx">terminalService</span><span class="p">);</span>
        <span class="c1">// bridge the connection</span>
        <span class="k">return</span> <span class="k">await</span> <span class="nx">terminalServiceDirect</span><span class="p">.</span><span class="nx">connectStream</span><span class="p">(</span><span class="nx">input</span><span class="p">,</span> <span class="p">{</span>
            <span class="na">cmd</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">path/to/executable</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">arg1</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">arg2</span><span class="dl">"</span><span class="p">]</span>
        <span class="p">});</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Sometimes, it might be useful for the downloaded application to be added to the user’s <code class="language-plaintext highlighter-rouge">PATH</code>
for convenient use in the main Scrypted interactive terminal. To do this, implement the optional
<code class="language-plaintext highlighter-rouge">TTYSettings</code> interface and provide a <code class="language-plaintext highlighter-rouge">getTTYSettings</code> function:</p>

<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">MyDevice</span> <span class="k">implements</span> <span class="nx">TTYSettings</span> <span class="p">{</span>
    <span class="k">async</span> <span class="kd">function</span> <span class="nx">getTTYSettings</span><span class="p">():</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="p">{</span>
        <span class="nx">paths</span><span class="p">?:</span> <span class="kr">string</span><span class="p">[];</span>
    <span class="p">}</span><span class="o">&gt;</span> <span class="p">{</span>
        <span class="k">return</span> <span class="p">{</span>
            <span class="na">paths</span><span class="p">:</span> <span class="p">[</span><span class="dl">"</span><span class="s2">directory/containing/executable</span><span class="dl">"</span><span class="p">],</span>
        <span class="p">};</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Finally, the top-level device of the plugin (i.e. the plugin itself) must implement <code class="language-plaintext highlighter-rouge">DeviceProvider</code>
and return in <code class="language-plaintext highlighter-rouge">getDevice</code> a reference to the device in that handles the terminal applications. This is to allow the
Scrypted management UI to construct a unique websocket connection for the terminal connection,
ensuring the UI remains responsive even when the terminal connection processes a large amount of
data. For plugins where only one device exists (i.e. the plugin itself implements <code class="language-plaintext highlighter-rouge">StreamService</code>
and <code class="language-plaintext highlighter-rouge">TTY</code>), it’s sufficient to implement a <code class="language-plaintext highlighter-rouge">getDevice</code> function that returns <code class="language-plaintext highlighter-rouge">this</code> (or <code class="language-plaintext highlighter-rouge">self</code> in
Python).</p>

<p>For live examples, check out the source code of <a href="https://github.com/scryptedapp/btop"><code class="language-plaintext highlighter-rouge">@scrypted/btop</code></a>
and <a href="https://github.com/scryptedapp/fastfetch"><code class="language-plaintext highlighter-rouge">@scrypted/fastfetch</code></a>.</p>]]></content><author><name>Brett Jia</name></author><category term="scrypted" /><summary type="html"><![CDATA[The major reimplementation of Scrypted’s management UI in 2024 introduced a way for plugins to expose custom interactive terminals to users. Similar to how Scrypted’s management UI provides shell access via a web terminal (which itself allows for some neat tricks), this feature provides an xterm.js window on the plugin’s device page, allowing plugins to render interactive terminal apps directly to the browser.]]></summary></entry><entry><title type="html">Streaming your terminal apps as video</title><link href="https://bjia56.github.io/ffmpeg/streaming-your-terminal-apps-as-video/" rel="alternate" type="text/html" title="Streaming your terminal apps as video" /><published>2024-07-13T17:30:00+00:00</published><updated>2024-07-13T17:30:00+00:00</updated><id>https://bjia56.github.io/ffmpeg/streaming-your-terminal-apps-as-video</id><content type="html" xml:base="https://bjia56.github.io/ffmpeg/streaming-your-terminal-apps-as-video/"><![CDATA[<p>If you’ve ever looked into how to record your terminal and share it with a friend, chances
are you’ve come across tools like <a href="https://manpages.ubuntu.com/manpages/trusty/man1/ttyrec.1.html"><code class="language-plaintext highlighter-rouge">ttyrec</code></a> or <a href="https://github.com/asciinema/asciinema"><code class="language-plaintext highlighter-rouge">asciinema</code></a>. These
store the inputs and outputs of your terminal session in text files that can be
shared and replayed.</p>

<p>What if you want a video of your terminal instead? Or, perhaps, a video <em>stream</em> of a
single program, running unattended on your computer?</p>

<p>It turns out, with some clever uses of <a href="https://manpages.ubuntu.com/manpages/xenial/man1/xvfb-run.1.html"><code class="language-plaintext highlighter-rouge">xvfb-run</code></a>, <a href="https://linux.die.net/man/1/xterm"><code class="language-plaintext highlighter-rouge">xterm</code></a>, and
FFmpeg’s <a href="https://ffmpeg.org/ffmpeg.html#X11-grabbing">x11grab</a> screen recorder, we can do just that - convert a
terminal app’s output into a live video stream.</p>

<p>For this example, let’s stream the system monitoring tool <a href="https://github.com/aristocratos/btop"><code class="language-plaintext highlighter-rouge">btop</code></a> on Ubuntu
22.04. In one terminal window, run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>xvfb-run <span class="nt">--server-args</span><span class="o">=</span><span class="s2">"-screen 0 1024x720x24"</span> <span class="nt">-f</span> /tmp/auth.txt xterm <span class="nt">-maximized</span> <span class="nt">-e</span> btop
</code></pre></div></div>

<p>This will launch a virtual X11 display with screen dimensions of 1024x720. The XAUTH
cookie is stored at <code class="language-plaintext highlighter-rouge">/tmp/auth.txt</code>, which will be used later. The program to run under
the virtual display is <code class="language-plaintext highlighter-rouge">xterm</code>, which will start up maximized and automatically run <code class="language-plaintext highlighter-rouge">btop</code> on startup. By default, <code class="language-plaintext highlighter-rouge">xvfb-run</code> will use display number 99.</p>

<p>In another terminal windows, run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">XAUTHORITY</span><span class="o">=</span>/tmp/auth.txt ffmpeg <span class="nt">-video_size</span> 1024x720 <span class="nt">-f</span> x11grab <span class="nt">-draw_mouse</span> 0 <span class="nt">-i</span> :99 <span class="nt">-f</span> flv <span class="nt">-listen</span> 1 rtmp://localhost:4444/stream
</code></pre></div></div>

<p>This will tell <code class="language-plaintext highlighter-rouge">ffmpeg</code> to use the XAUTH cookie from the previous step and read from
the X11 display with <code class="language-plaintext highlighter-rouge">x11grab</code>. The flag <code class="language-plaintext highlighter-rouge">-draw_mouse 0</code> disables rendering of the
mouse pointer on the screen. The input device is <code class="language-plaintext highlighter-rouge">:99</code>, referring to display number 99.
Finally, the input stream is converted into FLV video and streamed at the given url.</p>

<p>Finally, use <code class="language-plaintext highlighter-rouge">ffplay</code> to view the stream:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffplay rtmp://localhost:4444/stream
</code></pre></div></div>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/streaming-your-terminal-apps-as-video/screencap.png" alt="" />
<br /><code class="language-plaintext highlighter-rouge">btop</code> streaming to <code class="language-plaintext highlighter-rouge">ffplay</code></p>]]></content><author><name>Brett Jia</name></author><category term="ffmpeg" /><summary type="html"><![CDATA[If you’ve ever looked into how to record your terminal and share it with a friend, chances are you’ve come across tools like ttyrec or asciinema. These store the inputs and outputs of your terminal session in text files that can be shared and replayed.]]></summary></entry><entry><title type="html">Hosting Scrypted on the Orange Pi 5</title><link href="https://bjia56.github.io/scrypted/hosting-scrypted-on-the-orange-pi-5/" rel="alternate" type="text/html" title="Hosting Scrypted on the Orange Pi 5" /><published>2024-07-09T14:30:00+00:00</published><updated>2024-07-09T14:30:00+00:00</updated><id>https://bjia56.github.io/scrypted/hosting-scrypted-on-the-orange-pi-5</id><content type="html" xml:base="https://bjia56.github.io/scrypted/hosting-scrypted-on-the-orange-pi-5/"><![CDATA[<p>When setting up the <a href="https://github.com/koush/scrypted">Scrypted</a> home video platform, it’s important
to consider what kind of computer to use as the host. Scrypted benefits
from being a pluggable system that can cater to a variety of use cases, so
there is flexibility in weighing desired performance and functionality
against cost of hardware. <a href="https://docs.scrypted.app/scrypted-nvr/">NVR</a>, for example, benefits from having fast
GPUs and AI accelerators for object detection, as well as ample reliable hard
disk storage. Using Scrypted as a hub for many high-resolution cameras benefits
from extra RAM and good hardware-accelerated decoders.</p>

<p>To help with navigating the many choices for home servers, Scrypted provides
hardware recommendations in its <a href="https://docs.scrypted.app/buyers-guide/servers.html">docs</a>. In this tutorial,
we explore an alternate option: the <a href="http://www.orangepi.org/html/hardWare/computerAndMicrocontrollers/details/Orange-Pi-5.html">Orange Pi 5</a>.</p>

<h2 id="why-the-orange-pi-5">Why the Orange Pi 5?</h2>

<p>The Orange Pi 5 is a single-board computer sporting the 8-core ARM64
Rockchip <a href="https://www.cnx-software.com/2022/01/12/rockchip-rk3588s-cost-optimized-cortex-a76-a55-processor/">RK3588S</a> processor. What’s special about this CPU is its built-in
NPU (neural processing unit) with 3 cores at 2 TOPS each, giving up to 6
TOPS of AI processing power.</p>

<style>
table {
  display: table;
  width: 100%;
}
</style>

<table>
  <thead>
    <tr>
      <th>Server</th>
      <th>Time to run benchmark (yolov6n)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Apple Silicon M1 Ultra</td>
      <td>5 seconds</td>
    </tr>
    <tr>
      <td>NVIDIA 4090</td>
      <td>7 seconds</td>
    </tr>
    <tr>
      <td>Intel 13500H</td>
      <td>11 seconds</td>
    </tr>
    <tr>
      <td><strong>Orange Pi 5</strong></td>
      <td><strong>22 seconds</strong></td>
    </tr>
    <tr>
      <td>Intel N100</td>
      <td>27 seconds</td>
    </tr>
  </tbody>
</table>

<p style="font-size: 80%; text-align: center;"><em>Object detection benchmarks with data sourced from <a href="https://scripts.scrypted.app/object-detection-benchmark.html">Scrypted</a>. Only yolov6n is currently supported by the</em> <code class="language-plaintext highlighter-rouge">@scrypted/rknn</code> <em>plugin.</em></p>

<p>The integrated Rockchip VPU (video processing unit) is capable of hardware
acceleration, supporting transcoding up to <a href="https://jellyfin.org/docs/general/administration/hardware-acceleration/rockchip#speed-and-quality">1080p@480fps or 4k@120fps</a>
using the Rockchip MPP media stack. With RAM configurations up to 16 GB (32 GB soon available),
the Orange Pi 5 is a small but capable device for running Scrypted.</p>

<p class="notice--info"><strong>NOTE:</strong> While Rockchip devices sport decent processing power for their price
point, they are more barebones than other home servers like the Intel N100.
Setup and maintenance will be more hands-on, with potential for errors when
performing live kernel upgrades, so casual users should opt for
hardware on the recommendations list with better system software support.</p>

<p>Though this tutorial focuses on the Orange Pi 5, setup for other Rockchip RK3588(S)
devices should be similar, with differences in OS installation and setup.
Assuming a recent-enough Linux kernel, Scrypted should be able to
leverage the NPU and VPU on these other Rockchip boards. It is not recommended to
use an Android-based OS for these devices.</p>

<h2 id="hardware">Hardware</h2>

<p>Required hardware:</p>
<ul>
  <li>Orange Pi 5 (at least 8 GB RAM)</li>
  <li>Orange Pi 5 USB-C power supply <sup>a</sup></li>
  <li>microSD card (at least 8 GB)</li>
  <li>Computer with a microSD card writer (or SD card writer and a microSD-to-SD card adapter)</li>
  <li>Ethernet cable</li>
</ul>

<p>Recommended hardware:</p>
<ul>
  <li>Orange Pi 5 enclosure</li>
  <li>NVMe M.2 2242 SSD (at least 256 GB) <sup>b</sup></li>
  <li>External HDD (for NVR users, at least 2 TB)</li>
  <li>External HDD enclosure (for NVR users) <sup>c</sup></li>
</ul>

<div style="font-size:80%"><sup>a</sup> The Orange Pi 5 does not support USB-C PD negotiation. Get a power supply that supports 5V 4A.</div>
<div style="font-size:80%"><sup>b</sup> If you are getting an Orange Pi 5 enclosure, check that it has enough physical space to fit the M.2 SSD.</div>
<div style="font-size:80%"><sup>c</sup> Sabrent enclosures are known to fail frequently and are not recommended.</div>
<p><br />
Assembly of the Orange Pi 5 depends on the specific enclosure used. Refer to the manufacturer’s
manual for exact steps.</p>

<h2 id="setting-up-the-microsd-card">Setting up the microSD card</h2>

<p>On a computer with a microSD card writer, download and install <a href="https://etcher.balena.io/">balenaEtcher</a>.
We will use it to write an Armbian Linux image to the microSD card.</p>

<p>Navigate to the <a href="https://www.armbian.com/orangepi-5/">Armbian Linux Orange Pi 5 page</a> and download a Server/CLI
Ubuntu image. Choose any stable build with kernel version 6.1 or above. The rest of this
tutorial will use Armbian Linux 6.1 Ubuntu 22.04 Server/CLI.</p>

<p class="notice--info"><strong>NOTE:</strong> Though not required, it is usually recommended to verify the checksum of the downloaded image. To do so, follow the steps from the
<a href="https://docs.armbian.com/User-Guide_Getting-Started/#how-to-check-download-authenticity">Armbian docs</a>.</p>

<p>Plug in the microSD card and launch balenaEtcher. Choose the Armbian Linux image and your microSD card drive.
<em>Double check that the drive is correct!</em></p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/balenaEtcher.png" alt="" />
<br /><em>Imaging the microSD card</em></p>

<p>Click <code class="language-plaintext highlighter-rouge">Flash!</code>, wait for it to finish, then eject the microSD card.</p>

<h2 id="initial-boot">Initial boot</h2>

<p>Plug in the microSD card into the Orange Pi 5. Connect the Orange Pi 5
to your network router with an Ethernet cable. We will perform
setup over SSH, though on-device setup with HDMI and keyboard is also possible.</p>

<p>Power on the Orange Pi 5 to boot from the microSD card. Find its IP address
(most router admin pages will list the IP under the hostname <code class="language-plaintext highlighter-rouge">orangepi5</code>)
and SSH in with the username <code class="language-plaintext highlighter-rouge">root</code> and password <code class="language-plaintext highlighter-rouge">1234</code>. The initial login
will prompt you to change the root password, create a local non-root
user, and perform additional system setup.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/orangepi5_initial_login.png" alt="" />
<br /><em>Initial login screen</em></p>

<p class="notice--info"><strong>NOTE:</strong> If you made a mistake during the initial login setup or exited out of it early,
run <code class="language-plaintext highlighter-rouge">touch /root/.not_logged_in_yet</code> then reboot. The next login will prompt you for
setup once again.</p>

<p>Once setup is complete, update the system by running <code class="language-plaintext highlighter-rouge">apt update &amp;&amp; apt -y upgrade</code>.</p>

<h2 id="installing-the-os-to-nvme">Installing the OS to NVMe</h2>

<p>Installing Linux to and booting from NVMe grants greater storage capacity
and faster filesystem I/O. We will reformat the NVMe drive then move the
OS install stored on the microSD card to NVMe.</p>

<p>In a root shell on the Orange Pi 5, find the NVMe drive with <code class="language-plaintext highlighter-rouge">lsblk</code>.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/nvme_lsblk.png" alt="" />
<br /><code class="language-plaintext highlighter-rouge">lsblk</code> <em>output</em></p>

<p>Usually, the NVMe drive will show up as <code class="language-plaintext highlighter-rouge">nvme0n1</code>. In the above example, there is an
existing partition, <code class="language-plaintext highlighter-rouge">nvme01n1p1</code>. We will delete this and recreate it during
reformatting. Run <code class="language-plaintext highlighter-rouge">parted</code>, then peform the following operations:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">select /dev/nvme0n1</code> - select the device to operate on (replace with the correct device if <code class="language-plaintext highlighter-rouge">lsblk</code> shows a different NVMe drive, but keep the <code class="language-plaintext highlighter-rouge">/dev/</code> prefix)</li>
  <li><code class="language-plaintext highlighter-rouge">mktable gpt</code> - reformats the disk by creating or replacing the partition table</li>
  <li><code class="language-plaintext highlighter-rouge">mkpart armbi_root ext4 0% 100%</code> - creates a partition spanning the entire disk</li>
  <li><code class="language-plaintext highlighter-rouge">quit</code> - exit <code class="language-plaintext highlighter-rouge">parted</code> and commit changes</li>
</ul>

<p>Fixup the new partition as ext4 by running <code class="language-plaintext highlighter-rouge">mkfs.ext4 /dev/nvme0n1p1</code>.
Label the partition with <code class="language-plaintext highlighter-rouge">e2label /dev/nvme0n1p1 armbi_root</code>.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/nvme_reformatting.png" alt="" />
<br /><em>Preparing the NVMe drive with</em> <code class="language-plaintext highlighter-rouge">parted</code><em>,</em> <code class="language-plaintext highlighter-rouge">mkfs.ext4</code><em>, and</em> <code class="language-plaintext highlighter-rouge">e2label</code></p>

<p>Now, run <code class="language-plaintext highlighter-rouge">armbian-install</code> and select option 4. This will copy the OS from
the microSD card onto the new partition on the NVMe drive. When selecting
the target drive, select the NVMe formatted in the steps above. The installer
may ask to reformat the disk - select <strong>ext4</strong>.</p>

<table style="margin-bottom: 0">
    <tr>
        <td style="border-right: 1px solid #3d4046">
              <img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/armbian_install_1.png" />
        </td>
        <td>
              <img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/armbian_install_2.png" />
        </td>
    </tr>
    <tr>
        <td style="border-bottom: 0; border-right: 1px solid #3d4046">
              <img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/armbian_install_3.png" />
        </td>
        <td style="border-bottom: 0">
              <img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/armbian_install_4.png" />
        </td>
    </tr>
</table>
<p style="font-size: 80%; text-align: center;"><em>Various screens of</em> <code class="language-plaintext highlighter-rouge">armbian-install</code></p>

<p>Once OS install completes, ensure the bootloader is also installed. This will
take a rather long time with no visible progress. Once the install completes,
<code class="language-plaintext highlighter-rouge">armbian-install</code> will prompt to power off. Select <code class="language-plaintext highlighter-rouge">Power off</code> from the installer screen,
or manually run <code class="language-plaintext highlighter-rouge">poweroff</code> in the shell.</p>

<p>Remove the microSD card and turn the board back on. It should now boot up from NVMe.</p>

<p class="notice--info"><strong>NOTE:</strong> The IP address may change after switching the boot disk from microSD to NVMe.
Check your router for the new IP if the host is unreachable over SSH.</p>

<h2 id="installing-scrypted">Installing Scrypted</h2>

<p>Log in as the user account created during installation.
We will install Scrypted using the <a href="https://docs.scrypted.app/installation.html#linux-docker">official Linux Docker instructions</a>.
The install script has been reproduced below (check the docs
for the latest edition):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-s</span> https://raw.githubusercontent.com/koush/scrypted/main/install/docker/install-scrypted-docker-compose.sh <span class="o">&gt;</span> ~/install-scrypted-docker-compose.sh
<span class="nb">sudo </span><span class="nv">SERVICE_USER</span><span class="o">=</span><span class="nv">$USER</span> bash ~/install-scrypted-docker-compose.sh
<span class="nb">rm</span> ~/install-scrypted-docker-compose.sh
</code></pre></div></div>

<p>When prompted, install Docker and Avahi.</p>

<p>Open up <code class="language-plaintext highlighter-rouge">~/.scrypted/docker-compose.yml</code> and make a few modifications:</p>
<ul>
  <li>Under <code class="language-plaintext highlighter-rouge">devices</code>, uncomment the line that maps <code class="language-plaintext highlighter-rouge">"/dev/dri:/dev/dri"</code>.</li>
  <li>Under <code class="language-plaintext highlighter-rouge">devices</code>, add the following device mappings: <code class="language-plaintext highlighter-rouge">"/dev/dma_heap:/dev/dma_heap"</code>, <code class="language-plaintext highlighter-rouge">"/dev/rga:/dev/rga"</code>, <code class="language-plaintext highlighter-rouge">"/dev/mpp_service:/dev/mpp_service"</code>.</li>
  <li>Under <code class="language-plaintext highlighter-rouge">security_opt</code>, add <code class="language-plaintext highlighter-rouge">systempaths=unconfined</code>.</li>
  <li>For NVR users, under <code class="language-plaintext highlighter-rouge">environment</code>, uncomment the line that sets the <code class="language-plaintext highlighter-rouge">SCRYPTED_NVR_VOLUME</code> environment variable.</li>
  <li>For NVR users, under <code class="language-plaintext highlighter-rouge">volumes</code>, uncomment the line that mounts the host path <code class="language-plaintext highlighter-rouge">/mnt/media/video</code> to <code class="language-plaintext highlighter-rouge">/nvr</code> inside the container.</li>
</ul>

<p>Check out the <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> <a href="#final-docker-compose">below</a> as a
reference.
Restart the Docker containers with <code class="language-plaintext highlighter-rouge">cd ~/.scrypted &amp;&amp; docker compose up --force-recreate -d</code>, or reboot.</p>

<h3 id="setting-up-a-filesystem-mount-for-nvr">Setting up a filesystem mount for NVR</h3>

<p>If you are using Scrypted NVR, it is highly recommended to get an external HDD. Now’s the time to plug it in.</p>

<p>If your drive is brand new or recycled from a different computer, chances are that it will need to be reformatted. Skip the following reformatting steps if you are transferring over
an existing NVR drive from a different computer.</p>

<p>Similar to NVMe installation, we will use <code class="language-plaintext highlighter-rouge">lsblk</code> and <code class="language-plaintext highlighter-rouge">parted</code>.</p>

<p>In a root shell on the Orange Pi 5, find the HDD drive with <code class="language-plaintext highlighter-rouge">lsblk</code>.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/sda_lsblk.png" alt="" />
<br /><code class="language-plaintext highlighter-rouge">lsblk</code> <em>output</em></p>

<p>Usually, the HDD drive will show up as <code class="language-plaintext highlighter-rouge">sda</code>. In the above example, there is an existing partition, <code class="language-plaintext highlighter-rouge">sda1</code>. We will delete this and recreate it during reformatting. Run <code class="language-plaintext highlighter-rouge">parted</code>,
then peform the following operations:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">select /dev/sda</code> - select the device to operate on (replace with the correct device if <code class="language-plaintext highlighter-rouge">lsblk</code> shows a different HDD drive, but keep the <code class="language-plaintext highlighter-rouge">/dev/</code> prefix)</li>
  <li><code class="language-plaintext highlighter-rouge">mktable gpt</code> - reformats the disk by creating or replacing the partition table</li>
  <li><code class="language-plaintext highlighter-rouge">mkpart drive ext4 0% 100%</code> - creates a partition spanning the entire disk</li>
  <li><code class="language-plaintext highlighter-rouge">quit</code> - exit <code class="language-plaintext highlighter-rouge">parted</code> and commit changes</li>
</ul>

<p>Fixup the new partition as ext4 by running <code class="language-plaintext highlighter-rouge">mkfs.ext4 /dev/sda1</code>.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/sda_reformatting.png" alt="" />
<br /><em>Preparing the HDD drive with</em> <code class="language-plaintext highlighter-rouge">parted</code><em>,</em> <code class="language-plaintext highlighter-rouge">mkfs.ext4</code><em>, and</em> <code class="language-plaintext highlighter-rouge">e2label</code></p>

<p>Now, we need to mount the drive. Find the partition’s UUID with <code class="language-plaintext highlighter-rouge">blkid</code>:</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/sda_blkid.png" alt="" />
<br /><code class="language-plaintext highlighter-rouge">blkid</code> output</p>

<p>For the example above, the UUID of <code class="language-plaintext highlighter-rouge">/dev/sda1</code> is <code class="language-plaintext highlighter-rouge">cacb76b6-f0d5-4d31-b2dd-98efdf199ace</code>. Open up <code class="language-plaintext highlighter-rouge">/etc/fstab</code> and add a new
line (replace the UUID with yours):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>UUID=cacb76b6-f0d5-4d31-b2dd-98efdf199ace /mnt/media/video ext4 defaults,nofail,x-systemd.device-timeout=30 0 0
</code></pre></div></div>

<p>Reboot, and your drive will be automatically mounted on startup.</p>

<h3 id="plugin-configuration">Plugin configuration</h3>

<p>Navigate to the Scrypted management console at <code class="language-plaintext highlighter-rouge">https://&lt;your orange pi 5 ip&gt;:10443</code>.
Ensure that <code class="language-plaintext highlighter-rouge">https</code> is used. You may be prompted to accept an insecure connection,
since Scrypted generates its own self-signed certificates for HTTPS. Create the initial
admin user account.</p>

<p class="notice--info"><strong>NOTE:</strong>  For users looking to migrate an existing setup, restore
a backup from the <strong>Settings</strong> section of the sidebar. Once the restore
succeeds, log in with the account <em>of the restored backup</em>, not the account
created for the fresh Scrypted install.</p>

<p>Install the <code class="language-plaintext highlighter-rouge">@scrypted/rockchip-essentials</code> plugin.
This plugin will download a special build of FFmpeg that can leverage
the Rockchip VPU for hardware acceleration. The plugin will also
automatically install <code class="language-plaintext highlighter-rouge">@scrypted/rknn</code>, which provides object detection
capabilities via the Rockchip NPU.</p>

<p>The <code class="language-plaintext highlighter-rouge">@scrypted/rockchip-essentials</code> plugin will show the path, inside the
Scrypted container, that contains the downloaded FFmpeg. Typically, this is
at <code class="language-plaintext highlighter-rouge">/server/volume/plugins/@scrypted/rockchip-essentials/files/ffmpeg</code>.
Modify <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> to set the environment variable
<code class="language-plaintext highlighter-rouge">SCRYPTED_FFMPEG_PATH</code> to this path.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/rockchip_essentials.png" alt="" />
<br /><em>The path to the downloaded FFmpeg is displayed under the plugin settings</em></p>

<p>The final <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> may look something like this:</p>

<details id="final-docker-compose" style="margin-bottom: 20px">
  <summary style="font-size: 80%">Click to expand</summary>
  <div><div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># The Scrypted docker-compose.yml file typically resides at:</span>
<span class="c1"># ~/.scrypted/docker-compose.yml</span>


<span class="c1"># Scrypted NVR Storage (Optional Network Volume: Part 1 of 3)</span>
<span class="c1"># Example volumes SMB (CIFS) and NFS.</span>
<span class="c1"># Uncomment only one.</span>
<span class="c1"># volumes:</span>
<span class="c1">#     nvr:</span>
<span class="c1">#         driver_opts:</span>
<span class="c1">#             type: cifs</span>
<span class="c1">#             o: username=[username],password=[password],vers=3.0,file_mode=0777,dir_mode=0777</span>
<span class="c1">#             device: //[ip-address]/[path-to-directory]</span>
<span class="c1">#     nvr:</span>
<span class="c1">#         driver_opts:</span>
<span class="c1">#             type: "nfs"</span>
<span class="c1">#             o: "addr=[ip-address],nolock,soft,rw"</span>
<span class="c1">#             device: ":[path-to-directory]"</span>

<span class="na">services</span><span class="pi">:</span>
    <span class="na">scrypted</span><span class="pi">:</span>
        <span class="na">environment</span><span class="pi">:</span>
            <span class="c1"># Scrypted NVR Storage (Part 2 of 3)</span>

            <span class="c1"># Uncomment the next line to configure the NVR plugin to store recordings</span>
            <span class="c1"># use the /nvr directory within the container. This can also be configured</span>
            <span class="c1"># within the plugin manually.</span>
            <span class="c1"># The drive or network share will ALSO need to be configured in the volumes</span>
            <span class="c1"># section below.</span>
            <span class="pi">-</span> <span class="s">SCRYPTED_NVR_VOLUME=/nvr</span>

            <span class="pi">-</span> <span class="s">SCRYPTED_WEBHOOK_UPDATE_AUTHORIZATION=Bearer f528318087bec447bf0027423fbd364a</span>
            <span class="pi">-</span> <span class="s">SCRYPTED_WEBHOOK_UPDATE=http://localhost:10444/v1/update</span>
            <span class="pi">-</span> <span class="s">SCRYPTED_FFMPEG_PATH=/server/volume/plugins/@scrypted/rockchip-essentials/files/ffmpeg</span>

            <span class="c1"># Avahi can be used for network discovery by passing in the host daemon</span>
            <span class="c1"># or running the daemon inside the container. Choose one or the other.</span>
            <span class="c1"># Uncomment next line to run avahi-daemon inside the container.</span>
            <span class="c1"># See volumes and security_opt section below to use the host daemon.</span>
            <span class="c1"># - SCRYPTED_DOCKER_AVAHI=true</span>

            <span class="c1"># NVIDIA (Part 1 of 4)</span>
            <span class="c1"># - NVIDIA_VISIBLE_DEVICES=all</span>
            <span class="c1"># - NVIDIA_DRIVER_CAPABILITIES=all</span>

        <span class="c1"># NVIDIA (Part 2 of 4)</span>
        <span class="c1"># runtime: nvidia</span>

        <span class="c1"># NVIDIA (Part 3 of 4) - Use NVIDIA image, and remove subsequent default image.</span>
        <span class="c1"># image: ghcr.io/koush/scrypted:nvidia</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/koush/scrypted</span>

        <span class="na">volumes</span><span class="pi">:</span>
            <span class="c1"># NVIDIA (Part 4 of 4)</span>
            <span class="c1"># - /etc/OpenCL/vendors/nvidia.icd:/etc/OpenCL/vendors/nvidia.icd</span>

            <span class="c1"># Scrypted NVR Storage (Part 3 of 3)</span>

            <span class="c1"># Modify to add the additional volume for Scrypted NVR.</span>
            <span class="c1"># The following example would mount the /mnt/sda/video path on the host</span>
            <span class="c1"># to the /nvr path inside the docker container.</span>
            <span class="pi">-</span> <span class="s">/mnt/media/video:/nvr</span>

            <span class="c1"># Or use a network mount from one of the CIFS/NFS examples at the top of this file.</span>
            <span class="c1"># - type: volume</span>
            <span class="c1">#   source: nvr</span>
            <span class="c1">#   target: /nvr</span>
            <span class="c1">#   volume:</span>
            <span class="c1">#     nocopy: true</span>

            <span class="c1"># Uncomment the following lines to use Avahi daemon from the host.</span>
            <span class="c1"># Ensure Avahi is running on the host machine:</span>
            <span class="c1"># It can be installed with: sudo apt-get install avahi-daemon</span>
            <span class="c1"># This is not compatible with running avahi inside the container (see above).</span>
            <span class="c1"># Also, uncomment the lines under security_opt</span>
            <span class="pi">-</span> <span class="s">/var/run/dbus:/var/run/dbus</span>
            <span class="pi">-</span> <span class="s">/var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket</span>

            <span class="c1"># Default volume for the Scrypted database. Typically should not be changed.</span>
            <span class="pi">-</span> <span class="s">~/.scrypted/volume:/server/volume</span>
        <span class="c1"># Uncomment the following lines to use Avahi daemon from the host</span>
        <span class="c1"># Without this, AppArmor will block the container's attempt to talk to Avahi via dbus</span>
        <span class="na">security_opt</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">apparmor:unconfined</span>
            <span class="pi">-</span> <span class="s">systempaths=unconfined</span>
        <span class="na">devices</span><span class="pi">:</span> <span class="pi">[</span>
            <span class="c1"># uncomment the common systems devices to pass</span>
            <span class="c1"># them through to docker.</span>

            <span class="c1"># all usb devices, such as coral tpu</span>
            <span class="c1"># "/dev/bus/usb:/dev/bus/usb",</span>

            <span class="c1"># hardware accelerated video decoding, opencl, etc.</span>
            <span class="s2">"</span><span class="s">/dev/dri:/dev/dri"</span><span class="pi">,</span>
            <span class="s2">"</span><span class="s">/dev/dma_heap:/dev/dma_heap"</span><span class="pi">,</span>
            <span class="s2">"</span><span class="s">/dev/rga:/dev/rga"</span><span class="pi">,</span>
            <span class="s2">"</span><span class="s">/dev/mpp_service:/dev/mpp_service"</span><span class="pi">,</span>

            <span class="c1"># uncomment below as necessary.</span>
            <span class="c1"># zwave usb serial device</span>

            <span class="c1"># "/dev/ttyACM0:/dev/ttyACM0",</span>

            <span class="c1"># coral PCI devices</span>
            <span class="c1"># "/dev/apex_0:/dev/apex_0",</span>
            <span class="c1"># "/dev/apex_1:/dev/apex_1",</span>
        <span class="pi">]</span>

        <span class="na">container_name</span><span class="pi">:</span> <span class="s">scrypted</span>
        <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
        <span class="na">network_mode</span><span class="pi">:</span> <span class="s">host</span>

        <span class="c1"># logging is noisy and will unnecessarily wear on flash storage.</span>
        <span class="c1"># scrypted has per device in memory logging that is preferred.</span>
        <span class="c1"># enable the log file if enhanced debugging is necessary.</span>
        <span class="na">logging</span><span class="pi">:</span>
            <span class="na">driver</span><span class="pi">:</span> <span class="s2">"</span><span class="s">none"</span>
            <span class="c1"># driver: "json-file"</span>
            <span class="c1"># options:</span>
            <span class="c1">#     max-size: "10m"</span>
            <span class="c1">#     max-file: "10"</span>
        <span class="na">labels</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s2">"</span><span class="s">com.centurylinklabs.watchtower.scope=scrypted"</span>

    <span class="c1"># watchtower manages updates for Scrypted.</span>
    <span class="na">watchtower</span><span class="pi">:</span>
        <span class="na">environment</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">WATCHTOWER_HTTP_API_TOKEN=f528318087bec447bf0027423fbd364a</span>
            <span class="pi">-</span> <span class="s">WATCHTOWER_HTTP_API_UPDATE=true</span>
            <span class="pi">-</span> <span class="s">WATCHTOWER_SCOPE=scrypted</span>
            <span class="c1"># remove the following line to never allow docker to auto update.</span>
            <span class="c1"># this is not recommended.</span>
            <span class="pi">-</span> <span class="s">WATCHTOWER_HTTP_API_PERIODIC_POLLS=true</span>
        <span class="na">image</span><span class="pi">:</span> <span class="s">containrrr/watchtower</span>
        <span class="na">container_name</span><span class="pi">:</span> <span class="s">scrypted-watchtower</span>
        <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>
        <span class="na">volumes</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s">/var/run/docker.sock:/var/run/docker.sock</span>
        <span class="na">labels</span><span class="pi">:</span>
            <span class="pi">-</span> <span class="s2">"</span><span class="s">com.centurylinklabs.watchtower.scope=scrypted"</span>
        <span class="na">ports</span><span class="pi">:</span>
            <span class="c1"># The auto update port 10444 can be configured</span>
            <span class="c1"># Must match the port in the auto update url above.</span>
            <span class="pi">-</span> <span class="s">10444:8080</span>
        <span class="c1"># check for updates once an hour (interval is in seconds)</span>
        <span class="na">command</span><span class="pi">:</span> <span class="s">--interval 3600 --cleanup --scope scrypted</span>
</code></pre></div></div>
</div>
</details>

<p>Restart the Docker containers with <code class="language-plaintext highlighter-rouge">cd ~/.scrypted &amp;&amp; docker compose up --force-recreate -d</code>, or reboot.</p>

<p>Finally, let’s enable the Rockchip VPU for transcoding
and decoding. Install the <code class="language-plaintext highlighter-rouge">@scrypted/prebuffer-mixin</code> plugin.
Inside Scrypted, under the <strong>Settings</strong> option
on the sidebar, modify the H264 Encoder Arguments and replace <code class="language-plaintext highlighter-rouge">libx264</code> with <code class="language-plaintext highlighter-rouge">h264_rkmpp</code>. For
NVR users, under the WebAssembly Decoder device,
set both H264 Decoder Arguments and H265 Decoder Arguments to <code class="language-plaintext highlighter-rouge">-hwaccel rkmpp</code>.</p>

<table style="margin-bottom: 0">
    <tr>
        <td style="border-bottom: 0; border-right: 1px solid #3d4046">
              <img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/rkmpp_server.png" />
        </td>
        <td style="border-bottom: 0">
              <img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/rkmpp_wasm.png" />
        </td>
    </tr>
</table>
<p style="font-size: 80%; text-align: center;"><em>Enabling hardware acceleration</em></p>

<h3 id="final-setup-and-verification">Final setup and verification</h3>

<p>At this point, Scrypted is fully installed and configured.
Using a custom tool, <a href="https://github.com/ramonbroox/rknputop"><code class="language-plaintext highlighter-rouge">rknputop</code></a>, we can
monitor NPU usage.</p>

<p>To install <code class="language-plaintext highlighter-rouge">rknputop</code>, open a root shell on the Orange Pi 5 and run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt <span class="nb">install </span>python3-pip
python3 <span class="nt">-m</span> pip <span class="nb">install </span>plotext psutil
wget https://raw.githubusercontent.com/ramonbroox/rknputop/main/rknputop
<span class="nb">chmod</span> +x rknputop
<span class="nb">chmod </span>755 rknputop
<span class="nb">mv </span>rknputop /usr/local/bin/
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">rknputop</code> must be run as root to read NPU data.</p>

<p>To test, go back into Scrypted and
create a new <strong>Script</strong> from the sidebar. Copy the below detection benchmark (adapted from the Scrypted <a href="https://scripts.scrypted.app/object-detection-benchmark.html">scripts site</a>):</p>

<details style="margin-bottom: 20px">
  <summary style="font-size: 80%">Click to expand</summary>
  <div><div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">mo</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">mediaManager</span><span class="p">.</span><span class="nx">createMediaObjectFromUrl</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://user-images.githubusercontent.com/73924/230690188-7a25983a-0630-44e9-9e2d-b4ac150f1524.jpg</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">image</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">mediaManager</span><span class="p">.</span><span class="nx">convertMediaObject</span><span class="o">&lt;</span><span class="nx">Image</span> <span class="o">&amp;</span> <span class="nx">MediaObject</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">mo</span><span class="p">,</span> <span class="dl">'</span><span class="s1">x-scrypted/x-scrypted-image</span><span class="dl">'</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">detectors</span> <span class="o">=</span> <span class="p">[</span>
    <span class="c1">// '@scrypted/coreml',</span>
    <span class="c1">// '@scrypted/onnx',</span>
    <span class="c1">// '@scrypted/openvino',</span>
    <span class="c1">// '@scrypted/tensorflow-lite',</span>
    <span class="dl">'</span><span class="s1">@scrypted/rknn</span><span class="dl">'</span><span class="p">,</span>
<span class="p">];</span>

<span class="kd">const</span> <span class="nx">simulatedCameras</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">batch</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">batchesPerCamera</span> <span class="o">=</span> <span class="mi">125</span><span class="p">;</span>

<span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">id</span> <span class="k">of</span> <span class="nx">detectors</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">d</span><span class="p">:</span> <span class="nx">ObjectDetection</span> <span class="o">=</span> <span class="nx">systemManager</span><span class="p">.</span><span class="nx">getDeviceById</span><span class="o">&lt;</span><span class="nx">ObjectDetection</span><span class="o">&gt;</span><span class="p">(</span><span class="nx">id</span><span class="p">);</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">starting</span><span class="dl">'</span><span class="p">,</span> <span class="nx">id</span><span class="p">);</span>
    <span class="c1">// await d.detectObjects(image);</span>

    <span class="kd">const</span> <span class="nx">model</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">d</span><span class="p">.</span><span class="nx">getDetectionModel</span><span class="p">();</span>
    <span class="kd">const</span> <span class="nx">bytes</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">image</span><span class="p">.</span><span class="nx">toBuffer</span><span class="p">({</span>
        <span class="na">resize</span><span class="p">:</span> <span class="p">{</span>
            <span class="na">width</span><span class="p">:</span> <span class="nx">model</span><span class="p">.</span><span class="nx">inputSize</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
            <span class="na">height</span><span class="p">:</span> <span class="nx">model</span><span class="p">.</span><span class="nx">inputSize</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
        <span class="p">},</span>
        <span class="na">format</span><span class="p">:</span> <span class="nx">model</span><span class="p">.</span><span class="nx">inputFormat</span><span class="p">,</span>
    <span class="p">});</span>

    <span class="c1">// cache a preconverted image to remove that from benchmark.</span>
    <span class="kd">const</span> <span class="nx">media</span><span class="p">:</span> <span class="nx">Image</span> <span class="o">&amp;</span> <span class="nx">MediaObject</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">sdk</span><span class="p">.</span><span class="nx">mediaManager</span><span class="p">.</span><span class="nx">createMediaObject</span><span class="p">(</span><span class="nx">bytes</span><span class="p">,</span> <span class="dl">'</span><span class="s1">x-scrypted/x-scrypted-image</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">sourceId</span><span class="p">:</span> <span class="nx">image</span><span class="p">.</span><span class="nx">sourceId</span><span class="p">,</span>
        <span class="na">width</span><span class="p">:</span> <span class="nx">model</span><span class="p">.</span><span class="nx">inputSize</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
        <span class="na">height</span><span class="p">:</span> <span class="nx">model</span><span class="p">.</span><span class="nx">inputSize</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span>
        <span class="na">format</span><span class="p">:</span> <span class="kc">null</span><span class="p">,</span>
        <span class="na">toBuffer</span><span class="p">:</span> <span class="k">async</span> <span class="p">(</span><span class="na">options</span><span class="p">:</span> <span class="nx">ImageOptions</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">bytes</span><span class="p">,</span>
        <span class="na">toImage</span><span class="p">:</span> <span class="kc">undefined</span><span class="p">,</span>
        <span class="na">close</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">image</span><span class="p">.</span><span class="nx">close</span><span class="p">(),</span>
    <span class="p">})</span>

    <span class="kd">const</span> <span class="nx">start</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span>

    <span class="kd">let</span> <span class="nx">detections</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kd">const</span> <span class="nx">simulateCameraDetections</span> <span class="o">=</span> <span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">batchesPerCamera</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">([</span>
                <span class="nx">d</span><span class="p">.</span><span class="nx">detectObjects</span><span class="p">(</span><span class="nx">media</span><span class="p">,</span> <span class="p">{</span> <span class="nx">batch</span> <span class="p">}),</span>
                <span class="nx">d</span><span class="p">.</span><span class="nx">detectObjects</span><span class="p">(</span><span class="nx">media</span><span class="p">),</span>
                <span class="nx">d</span><span class="p">.</span><span class="nx">detectObjects</span><span class="p">(</span><span class="nx">media</span><span class="p">),</span>
                <span class="nx">d</span><span class="p">.</span><span class="nx">detectObjects</span><span class="p">(</span><span class="nx">media</span><span class="p">),</span>
            <span class="p">]);</span>
            <span class="nx">detections</span> <span class="o">+=</span> <span class="nx">batch</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">};</span>

    <span class="kd">const</span> <span class="nx">simulated</span><span class="p">:</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="k">void</span><span class="o">&gt;</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[];</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">simulatedCameras</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">simulated</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">simulateCameraDetections</span><span class="p">());</span>
    <span class="p">}</span>

    <span class="k">await</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span><span class="nx">simulated</span><span class="p">);</span>

    <span class="kd">const</span> <span class="nx">end</span> <span class="o">=</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">();</span>
    <span class="kd">const</span> <span class="nx">ms</span> <span class="o">=</span> <span class="nx">end</span> <span class="o">-</span> <span class="nx">start</span><span class="p">;</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">id</span><span class="p">,</span> <span class="dl">'</span><span class="s1">done</span><span class="dl">'</span><span class="p">,</span> <span class="nx">ms</span><span class="p">,</span> <span class="dl">'</span><span class="s1">ms</span><span class="dl">'</span><span class="p">,</span> <span class="nx">detections</span><span class="p">,</span> <span class="dl">'</span><span class="s1">detections</span><span class="dl">'</span><span class="p">,</span> <span class="nx">detections</span> <span class="o">/</span> <span class="p">(</span><span class="nx">ms</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">),</span> <span class="dl">'</span><span class="s1">detections per second</span><span class="dl">'</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
</div>
</details>

<p>In a shell window, run <code class="language-plaintext highlighter-rouge">sudo rknputop</code>. In a Scrypted window, run the script. You should see
the NPU usage increase as the detection benchmark runs.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-the-orange-pi-5/rknputop.png" alt="" />
<br /><code class="language-plaintext highlighter-rouge">rknputop</code> <em>output during object detection</em></p>

<p class="notice--info"><strong>NOTE:</strong> Due to driver limitations, the maximum reported NPU usage is capped at around
60-70%. The object detection benchmark script does not usually cause the NPU to
reach this maximum utilization.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The Orange Pi 5 is a small but capable device, with specialized hardware to accelerate
two of the most compute-intensive tasks used by Scrypted: object detection and video
decoding. Depending on your needs, it may be a suitable upgrade from a
Raspberry Pi or old Intel PC.</p>]]></content><author><name>Brett Jia</name></author><category term="scrypted" /><summary type="html"><![CDATA[When setting up the Scrypted home video platform, it’s important to consider what kind of computer to use as the host. Scrypted benefits from being a pluggable system that can cater to a variety of use cases, so there is flexibility in weighing desired performance and functionality against cost of hardware. NVR, for example, benefits from having fast GPUs and AI accelerators for object detection, as well as ample reliable hard disk storage. Using Scrypted as a hub for many high-resolution cameras benefits from extra RAM and good hardware-accelerated decoders.]]></summary></entry><entry><title type="html">translate.ttf: A translation engine in your font</title><link href="https://bjia56.github.io/ai/translate-ttf-a-translation-engine-in-your-font/" rel="alternate" type="text/html" title="translate.ttf: A translation engine in your font" /><published>2024-07-08T11:30:00+00:00</published><updated>2024-07-08T11:30:00+00:00</updated><id>https://bjia56.github.io/ai/translate-ttf-a-translation-engine-in-your-font</id><content type="html" xml:base="https://bjia56.github.io/ai/translate-ttf-a-translation-engine-in-your-font/"><![CDATA[<p>A few weeks ago, <a href="https://github.com/fuglede">@fuglede</a> demonstrated that it was possible to
<a href="https://fuglede.github.io/llama.ttf/">embed llama2</a> generative AI models inside TrueType fonts using the
experimental <a href="https://github.com/harfbuzz/harfbuzz/blob/main/docs/wasm-shaper.md">HarfBuzz WASM shaper engine</a>. Today, we take things a step
further by embedding T5 (text-to-text transfer transformer) AI models
inside fonts using the same method, granting our fonts the power of text
translation. No more localization files or language packs required
to port apps to other languages!</p>

<table style="margin-bottom: 0">
    <tr>
        <td style="border-right: 1px solid #3d4046">
            <a href="/assets/images/translate-ttf-a-translation-engine-in-your-font/frame1_en.png">
                <img src="/assets/images/translate-ttf-a-translation-engine-in-your-font/frame1_en.png" />
            </a>
        </td>
        <td>
            <a href="/assets/images/translate-ttf-a-translation-engine-in-your-font/frame1_de.png">
                <img src="/assets/images/translate-ttf-a-translation-engine-in-your-font/frame1_de.png" />
            </a>
        </td>
    </tr>
    <tr>
        <td style="border-bottom: 0; border-right: 1px solid #3d4046">
            <a href="/assets/images/translate-ttf-a-translation-engine-in-your-font/frame2_en.png">
                <img src="/assets/images/translate-ttf-a-translation-engine-in-your-font/frame2_en.png" />
            </a>
        </td>
        <td style="border-bottom: 0">
            <a href="/assets/images/translate-ttf-a-translation-engine-in-your-font/frame2_de.png">
                <img src="/assets/images/translate-ttf-a-translation-engine-in-your-font/frame2_de.png" />
            </a>
        </td>
    </tr>
</table>
<p style="font-size: 80%; text-align: center;"><em>Subtitles from <a href="https://studio.blender.org/films/sintel/">Sintel</a>. Original <a href="https://durian.blender.org/wp-content/content/subtitles/sintel_en.srt">English</a> vs font-translated German.</em></p>

<p>The source code for <code class="language-plaintext highlighter-rouge">translate.ttf</code> can be found <a href="https://github.com/bjia56/translate.ttf">here</a>.</p>

<h2 id="installation-and-usage">Installation and usage</h2>

<p><code class="language-plaintext highlighter-rouge">translate_de.ttf</code> can be downloaded from <a href="https://github.com/bjia56/translate.ttf/raw/main/translatettf/translate_de.ttf">here</a>. This
font renders the original English text in German. On Ubuntu,
the font can be installed to <code class="language-plaintext highlighter-rouge">~/.local/share/fonts/</code>.</p>

<p>Much like <code class="language-plaintext highlighter-rouge">llama.ttf</code>, we will need to build <a href="https://github.com/bytecodealliance/wasm-micro-runtime">wasm-micro-runtime</a> (WAMR)
and <a href="https://github.com/harfbuzz/harfbuzz">HarfBuzz</a>. To take full advantage of WebAssembly SIMD instructions,
we will also need to enable the LLVM JIT compiler in WAMR.</p>

<p>The following assumes you are on Ubuntu 22.04 or similar. Adjust as needed
for your platform.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">WORKDIR</span><span class="o">=</span><span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span>

<span class="c"># Install dependencies</span>
<span class="nb">sudo </span>apt <span class="nb">install </span>git cmake ccache pkg-config libtool build-essential <span class="se">\</span>
  python3-pip patchelf ragel llvm-15-dev libfreetype-dev
python3 <span class="nt">-m</span> pip <span class="nb">install </span>ninja

<span class="c"># Let's install all libs under /opt</span>
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /opt/translate.ttf
<span class="nb">export </span><span class="nv">LD_LIBRARY_PATH</span><span class="o">=</span>/opt/translate.ttf/lib:<span class="nv">$LD_LIBRARY_PATH</span>
<span class="nb">export </span><span class="nv">CFLAGS</span><span class="o">=</span><span class="nt">-I</span>/opt/translate.ttf/include
<span class="nb">export </span><span class="nv">CXXFLAGS</span><span class="o">=</span><span class="nt">-I</span>/opt/translate.ttf/include
<span class="nb">export </span><span class="nv">LDFLAGS</span><span class="o">=</span><span class="nt">-L</span>/opt/translate.ttf/lib

<span class="c"># Build WAMR</span>
<span class="nb">cd</span> <span class="k">${</span><span class="nv">WORKDIR</span><span class="k">}</span>
git clone https://github.com/bytecodealliance/wasm-micro-runtime.git <span class="se">\</span>
  <span class="nt">--branch</span> WAMR-2.1.0 <span class="nt">--single-branch</span> <span class="nt">--depth</span> 1
<span class="nb">cd </span>wasm-micro-runtime/wamr-compiler
./build_llvm.sh
<span class="nb">cd</span> ..
cmake <span class="nt">-B</span> build <span class="se">\</span>
  <span class="nt">-DCMAKE_INSTALL_PREFIX</span><span class="o">=</span>/opt/translate.ttf <span class="se">\</span>
  <span class="nt">-DWAMR_BUILD_REF_TYPES</span><span class="o">=</span>1 <span class="se">\</span>
  <span class="nt">-DWAMR_BUILD_LIBC_WASI</span><span class="o">=</span>1 <span class="se">\</span>
  <span class="nt">-DWAMR_BUILD_JIT</span><span class="o">=</span>1 <span class="se">\</span>
  <span class="nt">-DWAMR_BUILD_FAST_JIT</span><span class="o">=</span>0 <span class="se">\</span>
  <span class="nt">-DWAMR_BUILD_LAZY_JIT</span><span class="o">=</span>0 <span class="se">\</span>
  <span class="nt">-DWAMR_BUILD_STATIC</span><span class="o">=</span>0
cmake <span class="nt">--build</span> build <span class="nt">--config</span> Release <span class="nt">--parallel</span>
<span class="nb">sudo </span>cmake <span class="nt">--build</span> build <span class="nt">--config</span> Release <span class="nt">--target</span> <span class="nb">install</span>

<span class="c"># The WAMR shared object doesn't appear to pull in libLLVM even though it's</span>
<span class="c"># required at runtime, so add it here</span>
<span class="nb">sudo </span>patchelf <span class="nt">--add-needed</span> libLLVM-15.so /opt/translate.ttf/lib/libiwasm.so

<span class="c"># Build HarfBuzz</span>
<span class="nb">cd</span> <span class="k">${</span><span class="nv">WORKDIR</span><span class="k">}</span>
git clone https://github.com/harfbuzz/harfbuzz.git <span class="nt">--branch</span> 8.5.0 <span class="se">\</span>
  <span class="nt">--single-branch</span> <span class="nt">--depth</span> 1
<span class="nb">cd </span>harfbuzz
./autogen.sh
./configure <span class="nt">--with-wasm</span><span class="o">=</span><span class="nb">yes</span> <span class="nt">--prefix</span><span class="o">=</span>/opt/translate.ttf
make <span class="nt">-j4</span>
<span class="nb">sudo </span>make <span class="nb">install</span>
</code></pre></div></div>

<p>When running applications such as gedit, set <code class="language-plaintext highlighter-rouge">LD_LIBRARY_PATH</code> or use <code class="language-plaintext highlighter-rouge">LD_PRELOAD</code> with the compiled <code class="language-plaintext highlighter-rouge">libharfbuzz.so</code> and <code class="language-plaintext highlighter-rouge">libiwasm.so</code>.</p>

<h3 id="translating-subtitles-with-ffmpeg">Translating subtitles with FFmpeg</h3>

<p>To showcase the possible applications of this text translation font, let’s
do some movie subtitle translation with FFmpeg. FFmpeg’s <a href="https://trac.ffmpeg.org/wiki/HowToBurnSubtitlesIntoVideo">subtitle burn-in</a>
feature takes a video and its subtitles file, then renders a new file
with the subtitles drawn directly onto the video. When using <code class="language-plaintext highlighter-rouge">libass</code> as the subtitle
filter, FFmpeg will invoke HarfBuzz to shape the text. That’s where our
custom font will come in.</p>

<p>We can test this with <a href="https://studio.blender.org/films/sintel/">Sintel</a>, a 2010 animated short film by the Blender Foundation.
The film and .srt subtitles files can be downloaded from
<a href="https://durian.blender.org/download/">durian.blender.org</a>. For this test, we will use the 720p MKV render
and the English subtitles.</p>

<p>First, convert the subtitles file to <code class="language-plaintext highlighter-rouge">ass</code> (Advanced SubStation Alpha) format:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg <span class="nt">-i</span> sintel_en.srt sintel_en.ass
</code></pre></div></div>

<p>Open <code class="language-plaintext highlighter-rouge">sintel_en.ass</code> and modify the requested font from <code class="language-plaintext highlighter-rouge">Arial</code> to <code class="language-plaintext highlighter-rouge">OpenSans</code>. Make sure
you have <code class="language-plaintext highlighter-rouge">translate_de.ttf</code> installed locally.</p>

<p>Ensure that <code class="language-plaintext highlighter-rouge">LD_LIBRARY_PATH</code> or <code class="language-plaintext highlighter-rouge">LD_PRELOAD</code> is set to pull in <code class="language-plaintext highlighter-rouge">libharfbuzz</code>
with WASM support. Then, subtitles can be generated with:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg <span class="nt">-i</span> Sintel.2010.720p.mkv <span class="nt">-vf</span> <span class="s2">"ass=sintel_en.ass"</span> sintel-subtitled.mp4
</code></pre></div></div>

<p>As the video renders, FFmpeg should print to console that the translation font
is being used. You should also see WASM debug logs appear in the console.
These logs will show what text is being “shaped” by HarfBuzz and the
corresponding results from translation.</p>

<p>To extract a single frame from the render instead:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ffmpeg <span class="nt">-i</span> Sintel.2010.720p.mkv <span class="nt">-vf</span> <span class="s2">"ass=sintel_en.ass,select=eq(n</span><span class="se">\,</span><span class="s2">3122)"</span> <span class="nt">-vframes</span> 1 frame.png
</code></pre></div></div>

<h2 id="technical-notes">Technical notes</h2>

<p><code class="language-plaintext highlighter-rouge">translate.ttf</code> uses the <a href="https://github.com/huggingface/candle">Candle</a> ML framework to perform inference
with <a href="https://huggingface.co/lmz/candle-quantized-t5"><code class="language-plaintext highlighter-rouge">candle-quantized-t5</code></a>. Candle’s T5 WASM example
was ported over to support WAMR without a dependency on a JavaScript enviroment. Specifically, the dependency on <code class="language-plaintext highlighter-rouge">js-sys</code> was removed and
<code class="language-plaintext highlighter-rouge">getrandom</code> was patched to use WASI APIs instead.</p>

<p>The WASM module produced by <code class="language-plaintext highlighter-rouge">translate.ttf</code> uses WASM SIMD instructions,
so WAMR must be built with LLVM JIT support. WAMR does have <a href="https://bytecodealliance.github.io/wamr.dev/blog/introduction-to-wamr-running-modes/">tiered running modes</a>,
which could be enabled when building WAMR, though they will not work with
<code class="language-plaintext highlighter-rouge">translate.ttf</code>’s SIMD instructions.</p>

<p>By following the strings submitted to HarfBuzz, I observed that gedit implements text wrapping by first shaping the entire paragraph block
to get the full length, then calculating how much text should be rendered per line, then re-shaping
each line. The way <code class="language-plaintext highlighter-rouge">translate.ttf</code> currently performs inference is by breaking up a block of
text into sentences, translating each sentence separately. This produces the desired
result for the initial shaping request with the entire paragraph, but breaks when the second
pass provides partial sentences due to line breaks inserted by text wrapping.</p>]]></content><author><name>Brett Jia</name></author><category term="ai" /><summary type="html"><![CDATA[A few weeks ago, @fuglede demonstrated that it was possible to embed llama2 generative AI models inside TrueType fonts using the experimental HarfBuzz WASM shaper engine. Today, we take things a step further by embedding T5 (text-to-text transfer transformer) AI models inside fonts using the same method, granting our fonts the power of text translation. No more localization files or language packs required to port apps to other languages!]]></summary></entry><entry><title type="html">Hosting Scrypted on Android</title><link href="https://bjia56.github.io/scrypted/hosting-scrypted-on-android/" rel="alternate" type="text/html" title="Hosting Scrypted on Android" /><published>2024-06-16T19:00:00+00:00</published><updated>2024-06-16T19:00:00+00:00</updated><id>https://bjia56.github.io/scrypted/hosting-scrypted-on-android</id><content type="html" xml:base="https://bjia56.github.io/scrypted/hosting-scrypted-on-android/"><![CDATA[<p>The <a href="https://github.com/koush/scrypted">Scrypted</a> home video platform is typically installed on
a Linux, MacOS, or Windows server, with optional viewer apps available for
Android and iOS as part of the paid <a href="https://docs.scrypted.app/scrypted-nvr/">NVR</a> plugin. Historically,
Scrypted was available as an <a href="https://www.xda-developers.com/scrypted-home-automation-android-app-integrates-google-assistant/">Android app</a>. Although this Android
app is no longer offered, in this blog we’ll take a look at what it
takes to go back to Scrypted’s roots and get Scrypted server running on
Android. While these steps should work for most Android devices, the test
device I used is a <a href="https://www.walmart.com/ip/seort/2835618394">$20 Walmart onn Android TV</a> - just for fun. (Some
<a href="#caveats">caveats</a> are listed at the very end.)</p>

<p>Screenshots taken for this article were done through <a href="https://www.vysor.io">Vysor</a>.</p>

<p class="notice--info"><strong>NOTE:</strong> This tutorial is mostly a proof of technical capabilities. Running
Scrypted on Android as a production solution is not recommended or supported.
Check out the Scrypted docs for <a href="https://docs.scrypted.app/buyers-guide/servers.html">server hardware recommendations</a>.</p>

<h2 id="installing-termux">Installing Termux</h2>

<p>We will be using the <a href="https://github.com/termux/termux-app">Termux app</a> and <a href="https://wiki.termux.com/wiki/PRoot">proot-distro</a> to install
a full Ubuntu 22.04 distribution to run Scrypted.</p>

<p>To install Termux, get the app from <a href="https://f-droid.org/en/packages/com.termux/">F-Droid</a>. The Google Play
Store version is <a href="https://github.com/termux/termux-app/discussions/4000">not recommended</a>.</p>

<p>Launching the app should give you a shell:</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-android/termux_landing.png" alt="" />
<br /><em>The Termux landing screen</em></p>

<p>Now, install proot-distro with the command:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pkg <span class="nb">install </span>proot-distro
</code></pre></div></div>

<p>Then install Ubuntu 22.04:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>proot-distro <span class="nb">install </span>ubuntu-oldlts
</code></pre></div></div>

<p class="notice--info"><strong>NOTE:</strong> As of this writing, Ubuntu 22.04 is identified in proot-distro as
<code class="language-plaintext highlighter-rouge">ubuntu-oldlts</code>. To show all available distributions, run <code class="language-plaintext highlighter-rouge">proot-distro list</code>.</p>

<p>To launch a shell inside proot-distro:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>proot-distro login ubuntu-oldlts
</code></pre></div></div>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-android/termux_proot_distro_install.png" alt="" />
<br /><em>Installing and logging into Ubuntu 22.04</em></p>

<h3 id="extra-steps-for-32-bit-android">Extra steps for 32-bit Android</h3>

<p>If your device runs 32-bit Android, the following extra steps may be required.</p>

<h4 id="fixing-processor-identification">Fixing processor identification</h4>

<p>On some Android devices, the kernel and userspace applications run in 32-bit
mode on a 64-bit processor. Processor identification within proot-distro may
show <code class="language-plaintext highlighter-rouge">armv8l</code>:</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-android/termux_armv8l.png" alt="" />
<br /><em>Default</em> <code class="language-plaintext highlighter-rouge">uname</code> <em>output</em></p>

<p>We want this to show <code class="language-plaintext highlighter-rouge">armv7l</code> instead, to match what would be shown on
other 32-bit Arm machines like the Raspberry Pi. This is important for some
dependencies (especially Python packages) to be installed correctly. To do
this, exit proot-distro and download and install a patched version of proot:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pkg <span class="nb">install </span>wget openssl
wget https://github.com/bjia56/proot/releases/download/5.1.107.1-64/proot_5.1.107.1-64_arm.deb
dpkg <span class="nt">-i</span> proot_5.1.107.1-64_arm.deb
</code></pre></div></div>

<p>Now, processor identification shows <code class="language-plaintext highlighter-rouge">armv7l</code>:</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-android/termux_armv7l.png" alt="" />
<br /><em>Patched</em> <code class="language-plaintext highlighter-rouge">uname</code> <em>output</em></p>

<h4 id="configuring-an-alternative-python-package-index">Configuring an alternative Python package index</h4>

<p>Scrypted’s Python plugins will typically pull pre-built packages from
<a href="https://pypi.org">pypi.org</a>. However, armv7l packages are rarely published
there, so we will need to use an alternate package index. A couple of options
include:</p>
<ul>
  <li><a href="https://piwheels.org/">piwheels</a>: This index provides automated builds of
nearly every package hosted on pypi.org.</li>
  <li><a href="https://bjia56.github.io/armv7l-wheels/">armv7l-wheels</a>: My own index
hosting a manually-curated list of packages built with external
dependencies bundled together.</li>
</ul>

<p>Inside proot-distro, add the file <code class="language-plaintext highlighter-rouge">/etc/pip.conf</code> with the following contents
for piwheels:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[global]
extra-index-url=https://www.piwheels.org/simple/
</code></pre></div></div>

<p>or the following for armv7l-wheels:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[global]
extra-index-url=https://bjia56.github.io/armv7l-wheels/
</code></pre></div></div>

<h2 id="installing-scrypted-server">Installing Scrypted server</h2>

<p>If you haven’t already, enter into proot-distro with
<code class="language-plaintext highlighter-rouge">proot-distro login ubuntu-oldlts</code>. Update the system and install some initial
dependencies:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apt update <span class="o">&amp;&amp;</span> apt <span class="nt">-y</span> upgrade
apt <span class="nt">-y</span> <span class="nb">install </span>curl <span class="nb">sudo </span>nscd
<span class="nb">mkdir</span> <span class="nt">-p</span> /var/run/nscd
</code></pre></div></div>

<p>Docker is not supported within Termux, so we will need to use a modified
version of the Linux local installation instructions. Download and run
the install script:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">unset </span>ANDROID_DATA
<span class="nb">unset </span>ANDROID_ROOT
curl <span class="nt">-s</span> https://raw.githubusercontent.com/koush/scrypted/main/install/local/install-scrypted-dependencies-linux.sh | <span class="nv">SERVICE_USER</span><span class="o">=</span><span class="nv">$USER</span> <span class="nv">SERVICE_USER_ROOT</span><span class="o">=</span>1 bash
</code></pre></div></div>

<p class="notice--info"><strong>NOTE:</strong> If curl reports the error <code class="language-plaintext highlighter-rouge">CANNOT LINK EXECUTABLE "curl": library "libssl.so.1.1" not found</code>,
you may be referencing curl libraries from Termux instead of Ubuntu. Exit out
of proot-distro and log back in, then retry.</p>

<p class="notice--info"><strong>NOTE:</strong> You may get an npm error saying the module <code class="language-plaintext highlighter-rouge">node-addon-api</code> cannot
be found while building the <code class="language-plaintext highlighter-rouge">@scrypted/node-pty</code> package. Run
<code class="language-plaintext highlighter-rouge">npm --prefix /root/.scrypted install node-addon-api</code> to install the
missing module, then rerun the Scrypted install command above.</p>

<p>On 32-bit Android, install a custom build of ffmpeg:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-L</span> <span class="nt">--output</span> <span class="s2">"/root/.scrypted/node_modules/@scrypted/ffmpeg-static/artifacts/ffmpeg-linux-arm"</span> https://github.com/eugeneware/ffmpeg-static/releases/download/b6.0/ffmpeg-linux-arm
</code></pre></div></div>

<p>There is no systemd within proot-distro, so we can use a script to do the same
thing that the Scrypted systemd service will do:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nb">unset </span>ANDROID_DATA
<span class="nb">unset </span>ANDROID_ROOT
<span class="nb">export </span><span class="nv">NODE_OPTIONS</span><span class="o">=</span><span class="nt">--dns-result-order</span><span class="o">=</span>ipv4first
<span class="nb">export </span><span class="nv">SCRYPTED_INSTALL_ENVIRONMENT</span><span class="o">=</span><span class="nb">local</span>

<span class="c"># nscd is required for ffmpeg-static to do</span>
<span class="c"># hostname resolution on Ubuntu 22.04</span>
nscd <span class="nt">-d</span> &amp;

npx <span class="nt">-y</span> scrypted serve
</code></pre></div></div>

<p>Save this file as <code class="language-plaintext highlighter-rouge">/usr/local/bin/scrypted-serve.sh</code> and make it executable with <code class="language-plaintext highlighter-rouge">chmod +x /usr/local/bin/scrypted-serve.sh</code>. We will use this in the next
section to start Scrypted automatically.</p>

<p>Test that everything installed correctly by running <code class="language-plaintext highlighter-rouge">scrypted-serve.sh</code>.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/hosting-scrypted-on-android/termux_scrypted.png" alt="" />
<br /><em>Scrypted running on Android</em></p>

<h2 id="starting-scrypted-on-boot">Starting Scrypted on boot</h2>

<p>Up to date instructions for how to set up Termux to start on boot can be found
on the <a href="https://wiki.termux.com/wiki/Termux:Boot">Termux wiki</a>, however the general steps are reproduced
here for completeness.</p>

<p>Install the <a href="https://f-droid.org/en/packages/com.termux.boot/">Termux:Boot</a> app from F-Droid. Then, go to
Android settings and turn off battery or power saving optimizations for both
Termux and Termux:Boot. Launch the Termux:Boot app to allow it to run on boot.</p>

<p>Launch Termux, and create the file <code class="language-plaintext highlighter-rouge">~/.termux/boot/start-scrypted</code>:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/data/data/com.termux/files/usr/bin/sh</span>
termux-wake-lock
proot-distro login ubuntu-oldlts <span class="nt">--</span> scrypted-serve.sh
</code></pre></div></div>

<p>Mark it executable with <code class="language-plaintext highlighter-rouge">chmod +x ~/.termux/boot/start-scrypted</code>.</p>

<h2 id="caveats">Caveats</h2>

<p>While an Android TV may be a good starting point to run Scrypted, due to
being plugged into the wall, it’s worth noting that most Android TVs run in
32-bit mode, even if the processor supports 64 bits. Only a select few devices
on the market (such as NVIDIA’s Shield TV Pro - the tower model, not the stick)
run applications in 64-bit mode. Scrypted has long phased out official support
for 32-bit platforms, so some functionality may fail.</p>

<h2 id="conclusion">Conclusion</h2>

<p>We’ve covered the basic steps to install Scrypted server on Android.
While the server runs, functionality of popular plugins have not been fully
tested exhaustively, but this serves as a fun proof of concept for running
Scrypted on its original platform.</p>]]></content><author><name>Brett Jia</name></author><category term="scrypted" /><summary type="html"><![CDATA[The Scrypted home video platform is typically installed on a Linux, MacOS, or Windows server, with optional viewer apps available for Android and iOS as part of the paid NVR plugin. Historically, Scrypted was available as an Android app. Although this Android app is no longer offered, in this blog we’ll take a look at what it takes to go back to Scrypted’s roots and get Scrypted server running on Android. While these steps should work for most Android devices, the test device I used is a $20 Walmart onn Android TV - just for fun. (Some caveats are listed at the very end.)]]></summary></entry><entry><title type="html">Making a portable CPython interpreter</title><link href="https://bjia56.github.io/python/scrypted/making-a-portable-cpython-interpreter/" rel="alternate" type="text/html" title="Making a portable CPython interpreter" /><published>2024-05-19T03:00:00+00:00</published><updated>2024-05-19T03:00:00+00:00</updated><id>https://bjia56.github.io/python/scrypted/making-a-portable-cpython-interpreter</id><content type="html" xml:base="https://bjia56.github.io/python/scrypted/making-a-portable-cpython-interpreter/"><![CDATA[<p>In the <a href="https://github.com/koush/scrypted">Scrypted</a> home video platform, Python is one of the scripting languages used
for implementing Scrypted <a href="https://developer.scrypted.app/">plugins</a>, predominantly used by plugins that provide
motion detection, object detection, and other AI inference services (a byproduct
of the heavily reliance on Python in existing machine learning communities). As
such an ubiquitous language, Python has interpreters available for nearly every conceivable operating system and hardware architecture, though
these distributions can vary in Python version, Python
implementation (e.g. CPython vs PyPy), installation method, compatibility
with Scrypted plugins, or even permissions required to install on the host. Being
able to easily bundle a vetted Python distribution with a Scrypted installation
would reduce the amount of variability that this critical dependency has on the
platform.</p>

<p>Enter <a href="https://github.com/bjia56/portable-python">portable-python</a>. This project is my attempt at configuring and packaging
a distribution of CPython for Linux, Windows, and MacOS, across different architectures, that fulfills the needs
of Scrypted’s growing set of Python plugins while still being suitable for
general-purpose use. CPython was chosen as it is the one used
by existing Scrypted plugins. The rest of this blog
documents my experiences with building this portable distribution, as well as the
technical challenges and design choices made to address them. This is by no means
a testament into the most “correct” or “superior” method of achieving this goal -
if there are better ways, please let me know by raising an issue in the repo.</p>

<h2 id="evaluating-static-linking">Evaluating static linking</h2>

<p>Since Scrypted is primarily a nodejs program, I was initially inspired by the
<a href="https://github.com/eugeneware/ffmpeg-static">ffmpeg-static</a> project’s method of using an <a href="https://docs.npmjs.com/cli/v10/using-npm/scripts#npm-install">npm install hook</a> and dynamically detecting, at install time, the
target OS and architecture so the correct ffmpeg build can be selected and
downloaded. A statically linked ffmpeg makes this straightforward to do,
since a static binary removes any dependency on the host’s shared libraries and,
more importantly, makes the binary relocatable. In other words, the ffmpeg binary
can be installed to anywhere on the filesystem, and it will still function properly. This is
important for an installer like ffmpeg-static to work - a nodejs package
could exist anywhere on the filesystem, and could be installed by an unprivileged
user account, so the included ffmpeg must not depend on hardcoded file paths or
require installing anything into privileged system paths such as <code class="language-plaintext highlighter-rouge">/usr</code>.
(Technically, static linking is not enough to solve the problem for more complex
applications - more on that in a <a href="#relaxing-the-application-prefix">later section</a>.)</p>

<p>It seems natural, therefore, to consider building a statically linked CPython interpreter. To do this, I chose to use <a href="https://github.com/python-cmake-buildsystem/python-cmake-buildsystem">python-cmake-buildsystem</a>,
leveraging its simple configuration options to build all standard Python extensions and
link dependencies statically on Linux, Windows, and MacOS. While it was easy to
get static CPython built with this buildsystem, a significant downside is that
statically linking libc on Linux renders <code class="language-plaintext highlighter-rouge">dlopen</code> <a href="https://www.openwall.com/lists/musl/2012/12/08/4">error-prone</a> and no longer portable.
<code class="language-plaintext highlighter-rouge">dlopen</code> is an important dynamic loader function used by the Python <code class="language-plaintext highlighter-rouge">ctypes</code>
module as well as any Python modules with native extensions, so
ensuring it works properly is important. Therefore, for portable-python, static
linking is no longer a suitable approach - a dynamically linked executable is required.</p>

<h2 id="loading-shared-libraries">Loading shared libraries</h2>

<p>Binary executable files are stored on disk in certain standardized formats, which grant
the operating system a way to determine how the executable is laid out in memory and how it
should be executed. Executables are in ELF format on Linux, Mach-O on MacOS, and PE on Windows.</p>

<p>A dynamically linked CPython interpreter, especially
one built with the full set of standard library modules, depends on may shared libraries.
Some examples of such
libraries are libssl and libcrypto for the <code class="language-plaintext highlighter-rouge">ssl</code> module and liblzma for the <code class="language-plaintext highlighter-rouge">lzma</code>
module. Compiled executables contains references to shared libraries
in its headers, and when the executable starts running, the operating system’s dynamic linker (a program
responsible for loading the executable and resolving symbols at runtime) reads these
headers to determine which libraries to load.</p>

<p>For PE executables on Windows, the <a href="https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#standard-search-order-for-packaged-apps">DLL search path</a> includes the
executable’s source directory. This is convenient for portable-python, since any dependencies
just need to be copied to the directory containing the CPython interpreter.</p>

<p>Both ELF and Mach-O executables search for shared libraries in a fixed number of default system
directories (see docs for <a href="https://man7.org/linux/man-pages/man8/ldconfig.8.html">Linux</a> and <a href="https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/UsingDynamicLibraries.html#//apple_ref/doc/uid/TP40002182-SW10">MacOS</a>). This
won’t work for portable-python, since there’s no guarantee that the shared library dependencies
will exist on the host. There are environment variables that could be set to instruct the
dynamic linker to search other paths, but expecting the end user to set those variables before
using portable-python is bad UX.</p>

<p>Borrowing a technique used by <a href="https://github.com/pypa/auditwheel">auditwheel</a> (a tool that creates Python wheels
with all dependency shared libraries copied into the wheel itself), I considered replacing all
references to shared libraries to a path relative to the executable. Conveniently, both ELF
and Mach-O support constructing relative paths by using <code class="language-plaintext highlighter-rouge">$ORIGIN</code> in ELF and <code class="language-plaintext highlighter-rouge">@executable_path</code> in Mach-O to resolve
the path to the executable file. Replacing the references can then be done with
<a href="https://github.com/NixOS/patchelf">patchelf</a> on Linux and <a href="https://www.unix.com/man-page/osx/1/install_name_tool/">install_name_tool</a> on MacOS. Solving this
should be simple with a <a href="https://github.com/bjia56/portable-python/blob/e5c6e94eb29374fec61059dad86b90be8cc5d5ff/scripts/patch_libpython.sh">script</a> that enumerates all shared libraries and
patches them, right?</p>

<p>Unfortunately, in my testing, patchelf proved to be unreliable and buggy. Patching each shared
library individually sometimes results in a broken executable, unable to be loaded by
the dynamic linker. Behavior across different architectures also varied, as the same version of
patchelf may work for x86_64, but then break the executable on arm64.</p>

<p>Readers familiar with dynamic linking may be aware that there is a simple and obvious solution
to this, one that I initially overlooked: <code class="language-plaintext highlighter-rouge">rpath</code>. <code class="language-plaintext highlighter-rouge">rpath</code>, or “run-path,” is a way to specify,
as part of the executable, additional directories to look for shared libraries - without the
need to export environment variables! Better yet, <code class="language-plaintext highlighter-rouge">rpath</code> also supports <code class="language-plaintext highlighter-rouge">$ORIGIN</code> and
<code class="language-plaintext highlighter-rouge">@executable_path</code>, meaning it can be used to reference relative directories! This proved to be
the key solution to bundling shared libraries in portable-python: All dependencies could
be simply installed with the interpreter under the same folder structure, and <code class="language-plaintext highlighter-rouge">rpath</code> tells
the dynamic linker to look for libraries in the installation directory, no matter where it is
on the filesystem.</p>

<h2 id="relaxing-the-application-prefix">Relaxing the application <code class="language-plaintext highlighter-rouge">prefix</code></h2>

<p>It turns out, pointing an executable to its shared libraries is only half the battle. Inside
the program binary, there could be hardcoded references to the path where it <em>expects</em> to be
installed. This is where <code class="language-plaintext highlighter-rouge">prefix</code> comes into play.</p>

<p>When compiling applications on Unix systems, many build script generators (like autoconf and
cmake) take in a <code class="language-plaintext highlighter-rouge">prefix</code> flag. This parameter tells the build scripts where the application
will eventually be installed - for example, a <code class="language-plaintext highlighter-rouge">prefix</code> set to <code class="language-plaintext highlighter-rouge">/usr/local</code> will install
executables under <code class="language-plaintext highlighter-rouge">/usr/local/bin</code>, libraries under <code class="language-plaintext highlighter-rouge">/usr/local/lib</code>, and so on.</p>

<p>As previously discussed, setting lookup paths for shared libraries can be done easily with <code class="language-plaintext highlighter-rouge">rpath</code>. What
about other resources and assets required by CPython, such as Python files part of the standard
library? <code class="language-plaintext highlighter-rouge">prefix</code> is used to resolve these. When CPython is compiled, the <code class="language-plaintext highlighter-rouge">prefix</code> value is stored as part of the
<code class="language-plaintext highlighter-rouge">sys</code> module, with additional references to the path inside <code class="language-plaintext highlighter-rouge">sysconfig</code>. This <code class="language-plaintext highlighter-rouge">prefix</code> is also used by Python wheel builders to
point native extension compilers to headers shipped with the Python distribution through
static pkgconfig scripts.</p>

<p>To solve this, I made minor modifications to the <a href="https://github.com/bjia56/portable-python-cmake-buildsystem/blob/9467dedff93fd351e57cf4ca91e6c34a9c4f9ddc/patches/3.9/portable/01-getpath-portable-prefix.patch">CPython source code</a> and
<a href="https://github.com/bjia56/portable-python-cmake-buildsystem/blob/9467dedff93fd351e57cf4ca91e6c34a9c4f9ddc/patches/3.9/portable/02-sysconfig-build-vars.patch">Python standard library</a> to rewrite hardcoded <code class="language-plaintext highlighter-rouge">prefix</code> values with dynamic
runtime resolution. I also made modifications to <a href="https://github.com/bjia56/portable-python-cmake-buildsystem/blob/9467dedff93fd351e57cf4ca91e6c34a9c4f9ddc/patches/3.9/portable/03-sysconfig-pkgconfig.patch">dynamically generate pkgconfig scripts</a>, allowing native extension compilers to find headers properly. Additionally,
<code class="language-plaintext highlighter-rouge">pip</code>, the standard Python module installer, comes with a CLI script that references the path
to the Python interpreter - the shebang of this script also needed to be <a href="https://github.com/bjia56/portable-python/blob/bc6d1a8d1d5f81138a6ea061520b8b01962d9fd5/scripts/patch_pip_script.py">patched</a>.</p>

<p>Finally, one dependency on <code class="language-plaintext highlighter-rouge">prefix</code> that could not be easily patched is OpenSSL, a dependency
for CPython’s <code class="language-plaintext highlighter-rouge">ssl</code> module. When installed,
OpenSSL places a certificate bundle in a path under its <code class="language-plaintext highlighter-rouge">prefix</code>. This certificate bundle contains
many well-known certificates for trusted public certificate authorities, and is used by
OpenSSL to verify server certificates for any SSL connection (e.g. when connecting to a site
over HTTPS). As a workaround, I chose to include <code class="language-plaintext highlighter-rouge">certifi</code> (a Python package containing
Mozilla’s trusted certficate bundle) with the portable-python install, and <a href="https://github.com/bjia56/portable-python-cmake-buildsystem/blob/9467dedff93fd351e57cf4ca91e6c34a9c4f9ddc/patches/3.9/portable/04-ssl.patch">patch</a>
the <code class="language-plaintext highlighter-rouge">ssl</code> module to look for trusted certificates under where <code class="language-plaintext highlighter-rouge">certifi</code> is installed. The
result works well and allows programs to connect to HTTPS sites without manually supplying
trusted certificates.</p>

<h2 id="compiling-for-different-architectures">Compiling for different architectures</h2>

<p>Scrypted is supported on x86_64 and arm64 for both Linux and MacOS, and x86_64 for Windows.
As such, portable-python needs to provide binaries that work on each of these
platforms. Builds are produced on <a href="https://github.com/bjia56/portable-python/tree/main/.github/workflows">GitHub Actions</a>, which, at
the start of the portable-python project,
only provided x86_64 runners (though as of this writing, <a href="https://github.blog/changelog/2024-01-30-github-actions-introducing-the-new-m1-macos-runner-available-to-open-source/">MacOS arm64 runners</a> are now available).
The project needed some method of producing arm64 distributions that will still run on GitHub Actions.</p>

<p>On MacOS, I wanted to produce <a href="https://developer.apple.com/documentation/apple-silicon/building-a-universal-macos-binary">universal</a> binaries, which are binaries that contain both x86_64
and arm64 code. This means that a single distribution would be able to run on both
architectures without using Rosetta translation. For the most part, this was straightforward to do by adding <code class="language-plaintext highlighter-rouge">-arch x86_64 -arch arm64</code>
to <code class="language-plaintext highlighter-rouge">CFLAGS</code> or setting <code class="language-plaintext highlighter-rouge">CMAKE_OSX_ARCHITECTURES=arm64;x86_64</code> in cmake. Some dependencies,
such as libffi, were less compliant with these flags, and I had to build separately for x86_64
and arm64, then use <a href="https://www.unix.com/man-page/osx/1/lipo/">lipo</a> to merge them together. Finally, to ensure that the result will
work with older MacOS versions, I set the environment variable <code class="language-plaintext highlighter-rouge">MACOSX_DEPLOYMENT_TARGET=10.9</code>
to tell the compiler to produce binaries compatible with OS X Mavericks.</p>

<p>On Linux, there are two common approaches to compiling for alternative architectures: cross compilation
and Docker with QEMU emulation. Cross compilation is fast since the compiler
runs natively on the build host, but all libraries and dependencies required to build the
program must also exist for the target architecture. These libraries and dependencies are
typically laid out in a sysroot, a directory structure that mirrors the one found in a host
running the target architecture.</p>

<p>Docker, on the other hand, is able to run containers of a different architecture than the host
through <a href="https://docs.docker.com/build/building/multi-platform/">QEMU user-mode emulation</a>. This is great for compilation, since the
compiler running within Docker sees a proper host filesystem (no sysroot needed) and can emit
binary code that it thinks is “native.” However, as can be expected of emulation, this is quite slow.</p>

<p>Like MacOS, I wanted to ensure that the Linux CPython builds are compatible with older Linux
distributions. Generally, this backward compatibility is done at the glibc level, since glibc
is required by most programs. The library’s
symbols are versioned, and great care is taken by its authors to ensure backward compatibility,
so a program built against one glibc release will work for any glibc released afterwards.
Taking inspiration from Python’s <a href="https://peps.python.org/pep-0599/">manylinux2014</a> platform tag, which specifies
that binary Python wheels should target glibc 2.17 (as of this writing, <a href="https://mayeut.github.io/manylinux-timeline/">65%</a>
of packages on PyPI target this), portable-python is also compiled for glibc 2.17.</p>

<p>The easiest way to ensure glibc compatibility is to use Docker and compile inside a CentOS 7
container. This is the approach that <a href="https://github.com/pypa/manylinux">manylinux</a> and <a href="https://github.com/pypa/cibuildwheel">cibuildwheel</a>
take. Early iterations of portable-python also took this approach.</p>

<p>Even though using a CentOS 7 container is convenient, I do not believe it is a sustainable
solution for building programs compatible with older Linux distributions. One reason is the
upcoming CentOS 7 <a href="https://endoflife.date/centos">EOL date</a>. Another is the difficulty of compiling modern
programs - CentOS 7 ships with an ancient gcc 4.8.5, and only some architectures get gcc 10
through Red Hat’s devtoolset-10 package. Modern programs may use newer C++ standards, requiring
newer compilers. Though newer compilers can be built for CentOS 7 (I’ve built
<a href="https://github.com/bjia56/armv7l-wheel-builder/blob/514f8fd6d0fcc0bf0a0aae9775a4e0130105eb0b/build_rpms.sh">devtoolset-10 on armv7l</a> before), the cost of maintaining these compilers
will grow over time.</p>

<p>The solution currently employed by portable-python is to use the <a href="https://github.com/ziglang/zig">zig</a> compiler. Built on
LLVM, the zig compiler exposes two commands, <code class="language-plaintext highlighter-rouge">zig cc</code> and <code class="language-plaintext highlighter-rouge">zig c++</code>, which are drop-in
replacements for C and C++ compilers. Additionally, zig’s use of LLVM allows it to target
different architectures and cross compile with ease. Finally - and this is the key feature -
zig is able to target a particular glibc release version while compiling. With this combination,
CPython and its dependencies can be cross compiled for a variety of architectures, targeting
glibc 2.17 without maintaining any extra containers or sysroot filesystems. Check out this
awesome <a href="https://andrewkelley.me/post/zig-cc-powerful-drop-in-replacement-gcc-clang.html">writeup</a> by Andrew Kelley that expands more on the capabilities of <code class="language-plaintext highlighter-rouge">zig cc</code>.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Building portable-python has been an interesting experience, and I’ve learned much from the
process. If you use the Scrypted NVR desktop app, chances are that some of your Scrypted
plugins are running on portable-python builds. To get a copy of portable CPython, check
out the <a href="https://github.com/bjia56/portable-python">project repo</a>, where you can download zips from the releases or
install via the nodejs installers.</p>]]></content><author><name>Brett Jia</name></author><category term="python" /><category term="scrypted" /><summary type="html"><![CDATA[In the Scrypted home video platform, Python is one of the scripting languages used for implementing Scrypted plugins, predominantly used by plugins that provide motion detection, object detection, and other AI inference services (a byproduct of the heavily reliance on Python in existing machine learning communities). As such an ubiquitous language, Python has interpreters available for nearly every conceivable operating system and hardware architecture, though these distributions can vary in Python version, Python implementation (e.g. CPython vs PyPy), installation method, compatibility with Scrypted plugins, or even permissions required to install on the host. Being able to easily bundle a vetted Python distribution with a Scrypted installation would reduce the amount of variability that this critical dependency has on the platform.]]></summary></entry><entry><title type="html">Diving into the Scrypted terminal</title><link href="https://bjia56.github.io/scrypted/diving-into-the-scrypted-terminal/" rel="alternate" type="text/html" title="Diving into the Scrypted terminal" /><published>2024-05-04T20:00:00+00:00</published><updated>2024-05-04T20:00:00+00:00</updated><id>https://bjia56.github.io/scrypted/diving-into-the-scrypted-terminal</id><content type="html" xml:base="https://bjia56.github.io/scrypted/diving-into-the-scrypted-terminal/"><![CDATA[<p><a href="https://github.com/koush/scrypted">Scrypted</a> is an awesome high performance and pluggable home video and automation platform.
If you’ve never used it before, go check it out! Its media pipeline is optimized for low-latency
camera streaming, and its <a href="https://developer.scrypted.app/">flexible plugin system</a> allows for integrations for countless external services.</p>

<p>In this blog, I’ll go over one of Scrypted’s underrated features: the terminal.</p>

<h2 id="what-is-the-scrypted-terminal">What is the Scrypted terminal?</h2>

<p>The Scrypted terminal provides a shell environment to the Scrypted server, accessible through the
browser via the Scrypted management UI. Access is only granted to authenticated admin users;
non-admins do not have access. On the left side of the management UI screen, we can see the “Terminal” tab.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/diving-into-the-scrypted-terminal/dashboard.png" alt="" />
<br /><em>Scrypted dashboard</em></p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/diving-into-the-scrypted-terminal/terminal.png" alt="" />
<br /><em>Terminal screen</em></p>

<p>Here, we are presented with a window hosting a terminal shell. The shell program used is dependent
on the <code class="language-plaintext highlighter-rouge">SHELL</code> environment variable of the Scrypted server process. This shell instance is launched as
a subprocess of the <code class="language-plaintext highlighter-rouge">@scrypted/core</code> plugin (see <a href="#architecture-of-the-scrypted-terminal">architecture</a>), so it inherits Scrypted’s user and
group. Additonally, if Scrypted is installed as a container (e.g. Docker, LXC), the terminal gives
a handy way of landing a window into the container itself.</p>

<p>The terminal window is rendered on the web with <a href="https://xtermjs.org/">xterm.js</a> and connects to an instance of <a href="https://github.com/microsoft/node-pty">node-pty</a> on the
server side, providing a functional terminal experience. For instance, full-screen terminal applications
like <code class="language-plaintext highlighter-rouge">htop</code> work, including mouse functionality.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/diving-into-the-scrypted-terminal/scrypted_htop.png" alt="" />
<br /><code class="language-plaintext highlighter-rouge">htop</code> <em>in the Scrypted terminal</em></p>

<h2 id="accessing-the-terminal-via-the-scrypted-cli">Accessing the terminal via the Scrypted CLI</h2>

<p>Scrypted CLI version 1.3.3 introduced the <code class="language-plaintext highlighter-rouge">shell</code> subcommand to enable the CLI to connect to a
Scrypted server’s terminal. Instead of using xterm.js, the CLI will connect the local terminal to
the remote node-pty, giving the same look-and-feel as if you’ve logged into the remote server over
something like SSH.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/diving-into-the-scrypted-terminal/cli.png" alt="" />
<br /><em>Scrypted CLI subcommands</em></p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/diving-into-the-scrypted-terminal/cli_shell.png" alt="" />
<br /><em>Connecting to the terminal</em></p>

<p class="notice--info"><strong>NOTE:</strong> When using the <code class="language-plaintext highlighter-rouge">npx scrypted shell</code> command, ensure you have logged into the Scrypted server beforehand
with <code class="language-plaintext highlighter-rouge">npx scrypted login</code>. Normally, the CLI will prompt for server credentials if not previously logged in;
however, this has the side effect of breaking the terminal’s stdin and stdout, causing the CLI shell to
get stuck. If you find this happens, force kill the CLI node process.</p>

<p>Like in the web, the <code class="language-plaintext highlighter-rouge">shell</code> subcommand gives a fully-functional terminal experience,
with full-screen applications and mouse events handled properly.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/diving-into-the-scrypted-terminal/cli_htop.png" alt="" />
<br /><code class="language-plaintext highlighter-rouge">htop</code> <em>in the Scrypted CLI</em></p>

<p>The <code class="language-plaintext highlighter-rouge">shell</code> command contains more capabilities than the web terminal, in that it can be used to specify the command
to launch on the server. Place the command you want to run after a <code class="language-plaintext highlighter-rouge">--</code> separator.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/diving-into-the-scrypted-terminal/cli_date.png" alt="" />
<br /><em>Two ways to run a remote command</em></p>

<p>Additionally, <code class="language-plaintext highlighter-rouge">shell</code> can be used in both interactive and non-interactive modes.
Normally, <code class="language-plaintext highlighter-rouge">shell</code> is started in interactive mode. However, if the CLI detects that its stdin is not a
terminal, it will fall back to non-interactive mode. For example, if stdin is a pipe from another process,
<code class="language-plaintext highlighter-rouge">shell</code> will be started in non-interactive mode.</p>

<p>Functionally, the difference in interactive and non-interactive mode is whether or not node-pty is started on the
server side. In interactive mode, node-pty is started to handle terminal control sequences, such as mouse
events. In non-interactive mode, node-pty is not used, and a plain subprocess takes in data from the CLI’s
stdin as its own stdin.</p>

<p>Combining the ability to launch a remote command and pipe data into <code class="language-plaintext highlighter-rouge">shell</code> grants us a way to do some
interesting things…</p>

<h2 id="using-scrypted-as-a-tcp-tunnel">Using Scrypted as a TCP tunnel</h2>

<p><code class="language-plaintext highlighter-rouge">socat</code> is a powerful utility with flexible socket manipulation capabilities. One of its uses is to act
as a TCP server, exec a subcommand, and pipe data received by the server to the subcommand’s stdin.
We can use this feature to tell <code class="language-plaintext highlighter-rouge">socat</code> to listen on a local port, then spawn a Scrypted CLI shell
in non-interactive mode to connect to a remote server.</p>

<p>When the Scrypted CLI shell connection is used non-interactively, data sent across the shell connection
is written directly to the remote process’s stdin. There is no requirement that this data consists of
printable characters - any binary data will do.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/diving-into-the-scrypted-terminal/socat_date.png" alt="" />
<br /><em>Using</em> <code class="language-plaintext highlighter-rouge">socat</code> <em>to run a remote command</em></p>

<p>We can use another instance of <code class="language-plaintext highlighter-rouge">socat</code> on the remote side to forward data sent over the Scrypted shell
connection to a host and port accessible from the Scrypted server. For example, the Scrypted server might
be hosted on your private network, with external HTTPS access available via a Cloudflare Tunnel. A Scrypted
CLI command outside the private network can authenticate as normal over HTTPS, then provide shell access
to the Scrypted server. With non-interactive <code class="language-plaintext highlighter-rouge">socat</code> running as the shell’s remote command, this allows
an authenticated CLI to tunnel into the private network and connect to any private network host and port.</p>

<p class="notice--warning"><strong>WARNING:</strong> Using the Scrypted CLI shell to connect to the server via a Cloudflare Tunnel means all data
is decrypted when traveling through Cloudflare’s servers. For any sensitive information, it is recommended
to ensure encryption on either side of the Scrypted TCP tunnel, such as with SSH (see below). Additionally,
Cloudflare may impose restrictions or cut access if too much data is transferred across their connection, so
be mindful of usage and try to avoid copying large files across the tunnel.</p>

<p>A simple script to grant this access can look like:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c"># Replace this with your public IP or Cloudflare Tunnel address</span>
<span class="nv">SERVER</span><span class="o">=</span>192.168.24.90
<span class="c"># Replace this with your public port</span>
<span class="nv">SERVER_PORT</span><span class="o">=</span>10443

<span class="c"># First argument is the port forwarded from localhost</span>
<span class="nv">LOCAL_PORT</span><span class="o">=</span><span class="nv">$1</span>
<span class="c"># Second argument is the host to forward to, relative to the Scrypted server</span>
<span class="nv">HOST</span><span class="o">=</span><span class="nv">$2</span>
<span class="c"># Third argument is the port of the host to forward to</span>
<span class="nv">PORT</span><span class="o">=</span><span class="nv">$3</span>

<span class="c"># This assumes that socat is available on the Scrypted server and in your PATH</span>
socat tcp-listen:<span class="nv">$LOCAL_PORT</span>,fork,reuseaddr,nodelay <span class="se">\</span>
    <span class="nb">exec</span>:<span class="s2">"npx scrypted shell </span><span class="nv">$SERVER</span><span class="se">\:</span><span class="nv">$SERVER_PORT</span><span class="s2"> -- socat - tcp</span><span class="se">\:</span><span class="nv">$HOST</span><span class="se">\:</span><span class="nv">$PORT</span><span class="s2">"</span>
</code></pre></div></div>

<p>Save this to <code class="language-plaintext highlighter-rouge">/usr/local/bin/scrypted-tunnel</code> and make the script executable.
To use, run it from the command line. For example, to make a TCP tunnel for SSH:</p>

<figure class="highlight"><pre><code class="language-shell" data-lang="shell"><span class="nv">$ </span>scrypted-tunnel 12345 localhost 22</code></pre></figure>

<p>This will forward the local port 12345 to port 22 of localhost <em>of the Scrypted server</em>.</p>

<p style="font-size: 80%; text-align: center;"><img src="/assets/images/diving-into-the-scrypted-terminal/ssh_over_tunnel.png" alt="" />
<br /><em>Using SSH over the TCP tunnel</em></p>

<h2 id="architecture-of-the-scrypted-terminal">Architecture of the Scrypted terminal</h2>

<p>The backend of the Scrypted terminal resides in the <code class="language-plaintext highlighter-rouge">@scrypted/core</code> plugin’s “Terminal Service” device.
This device is responsible for handling terminal connections, managing node-pty, and spawning the
appropriate subprocesses requested by terminal clients.</p>

<p>The Terminal Service device implements the StreamService interface:</p>

<figure class="highlight"><pre><code class="language-typescript" data-lang="typescript"><span class="cm">/**
 * Generic bidirectional stream connection.
 */</span>
<span class="k">export</span> <span class="kr">interface</span> <span class="nx">StreamService</span> <span class="p">{</span>
    <span class="nx">connectStream</span><span class="p">(</span><span class="nx">input</span><span class="p">:</span> <span class="nx">AsyncGenerator</span><span class="o">&lt;</span><span class="kr">any</span><span class="p">,</span> <span class="k">void</span><span class="o">&gt;</span><span class="p">):</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="nx">AsyncGenerator</span><span class="o">&lt;</span><span class="kr">any</span><span class="p">,</span> <span class="k">void</span><span class="o">&gt;&gt;</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>

<p>As the documentation suggests, StreamService is a generic representation of a bidirectional
connection, where both sides can stream data at any time. A StreamService object lives on the
server side, and clients can request to connect by calling connectStream. As such, when a client
wants to form a stream connection with a server, the client first gets a remote reference to the
server’s StreamService via the Scrypted RPC framework, then creates a local AsyncGenerator,
then sends the AsyncGenerator across the Scrypted RPC framework to the server with connectStream. Finally, the
server sends back its own AsyncGenerator as a response, and the two sides can communicate with
the pair of AsyncGenerators: the client sends information by queuing on its local AsyncGenerator,
and receives information by awaiting the server’s AsyncGenerator.</p>

<p>This design of representing streams as AsyncGenerators makes the connection consumer-centric, since
data is only sent over the connection if the consumer is ready to receive it. Queued data can sit on
the producer side of the connection and be batched when the consumer is ready for it.</p>

<p>However, in the case of terminal access, programs can still produce data faster than the receiving end
can process it. For example, xterm.js can be overwhelmed by large amounts of data, and recommends
adding <a href="https://xtermjs.org/docs/guides/flowcontrol/">flow control</a>. In Scrypted’s terminal, flow control is added on both sides
of the connection by queuing up to 64 kB of data before pausing reads from the producer. This is implemented
in <a href="https://github.com/koush/scrypted/blob/9a0c88ac61feb052997a8f47112579ddb6e630ff/plugins/core/src/terminal-service.ts#L124-L136">Terminal Service</a>, in the <a href="https://github.com/koush/scrypted/blob/9a0c88ac61feb052997a8f47112579ddb6e630ff/packages/cli/src/shell.ts#L27-L38">CLI</a>, and even the
<a href="https://github.com/koush/scrypted/blob/9a0c88ac61feb052997a8f47112579ddb6e630ff/plugins/core/ui/src/components/builtin/PtyComponent.vue#L118-L129">management UI</a>.</p>

<p class="notice--info"><strong>NOTE:</strong> StreamService and flow control isn’t just for the Scrypted terminal - it’s also the underlying architecture for
the device console and REPL in the management UI.</p>

<p>An aspect of terminal clients that isn’t sent as binary data across the connection is the terminal
resize event. If you use the Scrypted CLI’s shell subcommand and run a full-screen application (such as <code class="language-plaintext highlighter-rouge">htop</code>),
you can see that the program adapts to the available screen size - and is responsive when the terminal client
is resized. To implement this, Scrypted’s Terminal Service distinguishes between two different types of input:
control messages and generic terminal data.</p>

<p>Control messages are JSON-formatted strings containing metadata about the stream itself. As of this writing, there are
three kinds of control messages:</p>
<ul>
  <li>Stream start</li>
  <li>Resize</li>
  <li>EOF</li>
</ul>

<p>The “stream start” control message dictates how the Terminal Service should initialize the terminal, and is typically
the first message sent by the client across the stream connection. This message specifies if the terminal should
be started in interactive or non-interactive mode, as well as the command (and arguments) to run.</p>

<p>The “resize” control message contains the new dimensions (in rows and columns) of the client terminal. One instance
of this message is sent at the start of the stream to set the initial dimensions, and additional messages are sent
when the client terminal resizes.</p>

<p class="notice--info"><strong>NOTE:</strong> Resize events are available in xterm.js as well, but as of this writing, handling resizing is
unimplemented in the management UI.</p>

<p>The “EOF” control message is sent when the client wants to send EOF to the remote process.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The Scrypted terminal is a neat feature of the platform as a whole, and gives a flexible way to access
the Scrypted server environment. Its interactive and non-interactive modes allow for unique ways to use the
connection. I hope this post has been helpful in peeling back the curtains on how this feature works under the hood.</p>]]></content><author><name>Brett Jia</name></author><category term="scrypted" /><summary type="html"><![CDATA[Scrypted is an awesome high performance and pluggable home video and automation platform. If you’ve never used it before, go check it out! Its media pipeline is optimized for low-latency camera streaming, and its flexible plugin system allows for integrations for countless external services.]]></summary></entry></feed>