<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Filemoon Video Hosting]]></title><description><![CDATA[See the complete Raising Cane’s menu with prices for 2026, including Caniac Combo cost, box meals, chicken fingers, desserts, and updated calorie information..]]></description><link>https://filemoon.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Filemoon Video Hosting</title><link>https://filemoon.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 13 Oct 2026 20:48:55 GMT</lastBuildDate><atom:link href="https://filemoon.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building a Restaurant Menu Price Website: Architecture, Schema and SEO Lessons]]></title><description><![CDATA[Restaurant menu websites look like the simplest projects on the internet: a list of items and a list of prices. In practice they combine data modeling, performance, search visibility and trust in a sm]]></description><link>https://filemoon.hashnode.dev/building-a-restaurant-menu-price-website-architecture-schema-and-seo-lessons</link><guid isPermaLink="true">https://filemoon.hashnode.dev/building-a-restaurant-menu-price-website-architecture-schema-and-seo-lessons</guid><category><![CDATA[SEO]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[JSON-LD]]></category><category><![CDATA[Case Study]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[ssvtiktok]]></dc:creator><pubDate>Thu, 08 Oct 2026 02:52:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/699cf33ecf5def0f6ace7ea1/74431faf-0877-4233-baec-9d197231b631.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Restaurant menu websites look like the simplest projects on the internet: a list of items and a list of prices. In practice they combine data modeling, performance, search visibility and trust in a small package. This guide walks through the decisions that matter, using my work on a <strong>Bojangles menu with prices</strong> site, <a href="https://bojanglesprice.com">bojanglesprice.com</a>, as the running example.</p>
<p>If you are building any content site around frequently changing data, most of this applies directly.</p>
<h2>1. Start With What the Visitor Wants</h2>
<p>Before choosing tools, I wrote down the three things a visitor wants when they search for the <strong>Bojangles menu with prices 2026</strong>:</p>
<ol>
<li><p>See the items and prices fast, usually on a phone.</p>
</li>
<li><p>Trust that the numbers are reasonably current.</p>
</li>
<li><p>Compare options (combos, sizes, categories) without hunting.</p>
</li>
</ol>
<p>Every later decision, from layout to caching, comes back to these three.</p>
<h2>2. Choose a Simple Architecture</h2>
<p>For content like this, you do not need a heavy stack. The main options are:</p>
<ul>
<li><p><strong>Static site generator:</strong> fastest, cheapest and easiest to keep secure. Great when data changes weekly, not minute by minute.</p>
</li>
<li><p><strong>CMS such as WordPress:</strong> easier for non-developers to edit, but needs care around plugins, caching and page weight.</p>
</li>
<li><p><strong>Custom app with a database:</strong> flexible, but usually overkill for a price reference site.</p>
</li>
</ul>
<p>Whichever you choose, keep prices in <strong>one structured source</strong> (a spreadsheet, JSON file or database table) so pages are generated from data instead of hand-edited.</p>
<h2>3. Model the Menu Data</h2>
<p>A clean data model makes everything else easier:</p>
<pre><code class="language-json">{
  "category": "Chicken Combos",
  "items": [
    {
      "name": "Example Combo",
      "price_low": 7.49,
      "price_high": 9.99,
      "last_checked": "2026-10-01"
    }
  ]
}
</code></pre>
<p>I store a price <strong>range</strong> and a <strong>last checked</strong> date instead of a single number. Prices differ by location and franchise, so a range is more honest, and the date tells visitors how fresh the information is.</p>
<h2>4. Use Semantic HTML for the Page</h2>
<p>Search engines and screen readers both rely on structure. For price data, use real tables with a caption and headers:</p>
<pre><code class="language-html">&lt;table&gt;
  &lt;caption&gt;Chicken combos: typical price range&lt;/caption&gt;
  &lt;thead&gt;
    &lt;tr&gt;&lt;th&gt;Item&lt;/th&gt;&lt;th&gt;Price range&lt;/th&gt;&lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;td&gt;Example Combo&lt;/td&gt;&lt;td&gt;$7.49 - $9.99&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
</code></pre>
<p>Add one clear <code>&lt;h1&gt;</code>, logical <code>&lt;h2&gt;</code> sections per category, and wrap wide tables in a container with <code>overflow-x: auto</code> so they scroll cleanly on small screens.</p>
<h2>5. Add Structured Data (Carefully)</h2>
<p>Schema.org has types for menus, which can help search engines understand your content:</p>
<pre><code class="language-html">&lt;script type="application/ld+json"&gt;
{
  "@context": "https://schema.org",
  "@type": "Menu",
  "name": "Example Chicken Combos",
  "hasMenuSection": {
    "@type": "MenuSection",
    "name": "Chicken Combos",
    "hasMenuItem": {
      "@type": "MenuItem",
      "name": "Example Combo",
      "offers": {
        "@type": "Offer",
        "price": "7.49",
        "priceCurrency": "USD"
      }
    }
  }
}
&lt;/script&gt;
</code></pre>
<p>Two honest caveats. First, structured data does not guarantee a rich result, so treat it as clarity for crawlers rather than a ranking trick. Second, only mark up what is actually visible on the page, and keep it in sync with your data. Mismatched markup does more harm than none.</p>
<h2>6. Performance Is Part of the Product</h2>
<p>Most visitors arrive on a phone, often while deciding where to eat. Practical wins:</p>
<ul>
<li><p>Compress images and set explicit width and height to avoid layout shift</p>
</li>
<li><p>Lazy-load images below the fold</p>
</li>
<li><p>Keep JavaScript minimal, since a price table needs almost none</p>
</li>
<li><p>Serve fonts efficiently or use system fonts</p>
</li>
<li><p>Cache aggressively, since pages change rarely</p>
</li>
</ul>
<p>Test with Lighthouse and PageSpeed Insights, and focus on real-world mobile results rather than a perfect lab score.</p>
<h2>7. Build Trust Into the Content</h2>
<p>For a site about prices, trust is the whole point:</p>
<ul>
<li><p>Show a visible <strong>last updated</strong> date</p>
</li>
<li><p>Say clearly that prices vary by location</p>
</li>
<li><p>Point visitors to the official app or website to confirm</p>
</li>
<li><p>Add short, genuinely useful context around the tables, such as value tips and combo comparisons</p>
</li>
</ul>
<p>Content that only repeats a table is thin. Content that helps someone decide is useful.</p>
<h2>8. Internal Linking and Site Structure</h2>
<p>Group pages into clear topics (category pages, a main menu overview, a short FAQ) and link between them with descriptive anchor text. This helps visitors move around and helps crawlers understand how your pages relate.</p>
<h2>9. Measure What Actually Happens</h2>
<p>After launch:</p>
<ol>
<li><p>Verify the site in Google Search Console and submit your sitemap.</p>
</li>
<li><p>Watch which queries bring impressions, then improve those pages.</p>
</li>
<li><p>Check mobile usability and Core Web Vitals reports.</p>
</li>
<li><p>Review pages quarterly and update prices and dates.</p>
</li>
</ol>
<p>Rankings take time, and the useful signal early on is whether impressions and clicks are slowly growing, not whether you hit page one in a week.</p>
<h2>Launch Checklist</h2>
<ul>
<li><p>[ ] Prices stored in one data source</p>
</li>
<li><p>[ ] Real HTML tables with captions</p>
</li>
<li><p>[ ] Unique title and meta description on every page</p>
</li>
<li><p>[ ] Visible update date and location disclaimer</p>
</li>
<li><p>[ ] Sitemap submitted and site verified</p>
</li>
<li><p>[ ] Mobile speed tested</p>
</li>
<li><p>[ ] Structured data matches visible content</p>
</li>
</ul>
<h2>Conclusion</h2>
<p>A menu price website is a small project with real engineering behind it: clean data, semantic markup, fast pages and honest content. None of it is exotic, but doing all of it consistently is what separates a useful reference from a thin one.</p>
<p>You can see how I applied these ideas in practice on the <strong>Bojangles menu with prices</strong> pages at <a href="https://bojanglesprice.com">bojanglesprice.com</a>.</p>
<p>What is the most useful thing you have learned from building a data-driven content site? Let me know in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[Adding FAQ Schema and Speed Fixes to a Menu Price Website]]></title><description><![CDATA[Content sites look easy until you try to make them fast, clear and trustworthy. I recently spent time improving the pages for the Carl's Jr menu on carlsjrprices.com, and most of the wins came from sm]]></description><link>https://filemoon.hashnode.dev/adding-faq-schema-and-speed-fixes-to-a-menu-price-website</link><guid isPermaLink="true">https://filemoon.hashnode.dev/adding-faq-schema-and-speed-fixes-to-a-menu-price-website</guid><category><![CDATA[SEO]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[JSON-LD]]></category><category><![CDATA[performance]]></category><dc:creator><![CDATA[ssvtiktok]]></dc:creator><pubDate>Thu, 08 Oct 2026 02:42:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/699cf33ecf5def0f6ace7ea1/6e2e90d5-8607-4a7e-9b48-212c59ebf243.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Content sites look easy until you try to make them fast, clear and trustworthy. I recently spent time improving the pages for the <strong>Carl's Jr menu</strong> on <a href="https://carlsjrprices.com">carlsjrprices.com</a>, and most of the wins came from small technical details, not big redesigns.</p>
<p>Here is what worked, in case you are building a similar data-heavy site.</p>
<h2>The Goal</h2>
<p>Visitors searching for the <strong>Carl's Jr menu with prices 2026</strong> usually want one thing: a quick, accurate answer on their phone. That means the page has to load fast, show a clean table, and answer common questions without scrolling forever.</p>
<h2>1. Use Real Tables for Price Data</h2>
<p>It is tempting to build price lists with styled <code>div</code> elements. A real table is better for accessibility and for search engines, because the relationship between item, category and price is clear.</p>
<pre><code class="language-html">&lt;table&gt;
  &lt;caption&gt;Burgers: typical price range&lt;/caption&gt;
  &lt;thead&gt;
    &lt;tr&gt;&lt;th&gt;Item&lt;/th&gt;&lt;th&gt;Price range&lt;/th&gt;&lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;&lt;td&gt;Example burger&lt;/td&gt;&lt;td&gt;$5 - $9&lt;/td&gt;&lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
</code></pre>
<h2>2. Add FAQ Structured Data</h2>
<p>Common questions (like whether prices differ by location) can be marked up with JSON-LD so search engines understand them:</p>
<pre><code class="language-html">&lt;script type="application/ld+json"&gt;
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [{
    "@type": "Question",
    "name": "Are Carl's Jr. prices the same everywhere?",
    "acceptedAnswer": {
      "@type": "Answer",
      "text": "No. Prices vary by city, state and franchise."
    }
  }]
}
&lt;/script&gt;
</code></pre>
<p>Only mark up questions that are actually visible on the page. Hidden or mismatched FAQ markup can hurt more than help.</p>
<h2>3. Fix the Mobile Basics</h2>
<p>Most people check a menu while deciding where to eat, so mobile speed matters more than desktop polish:</p>
<ul>
<li><p>Compress and properly size images</p>
</li>
<li><p>Use <code>loading="lazy"</code> for images below the fold</p>
</li>
<li><p>Avoid heavy scripts that block rendering</p>
</li>
<li><p>Keep layout stable so tables do not jump while loading</p>
</li>
</ul>
<h2>4. Show Freshness Honestly</h2>
<p>Prices change, so I display an "updated" date on each page and remind readers to confirm in the official app. A guide to the <strong>Carl's Jr menu</strong> is only useful if visitors can trust it, and honest disclaimers build that trust.</p>
<h2>Key Takeaways</h2>
<ol>
<li><p>Semantic HTML beats clever styling for data-heavy pages.</p>
</li>
<li><p>Structured data helps, but only when it matches visible content.</p>
</li>
<li><p>Mobile speed is a feature, not an afterthought.</p>
</li>
<li><p>Trust signals like update dates matter as much as design.</p>
</li>
</ol>
<p>If you want to see how these ideas look in a real project, the finished pages are at <a href="https://carlsjrprices.com">carlsjrprices.com</a>.</p>
<p>What is the biggest performance or SEO fix you have made on a content site? Share it in the comments.</p>
]]></content:encoded></item><item><title><![CDATA[How to Build a Scalable Video Streaming Architecture with HLS and CDN]]></title><description><![CDATA[Adding a video player to a website is easy.
Building a video streaming system that continues to perform when hundreds or thousands of users start watching at the same time is a very different problem.]]></description><link>https://filemoon.hashnode.dev/how-to-build-a-scalable-video-streaming-architecture-with-hls-and-cdn</link><guid isPermaLink="true">https://filemoon.hashnode.dev/how-to-build-a-scalable-video-streaming-architecture-with-hls-and-cdn</guid><dc:creator><![CDATA[ssvtiktok]]></dc:creator><pubDate>Tue, 11 Aug 2026 16:42:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/699cf33ecf5def0f6ace7ea1/989afc89-0dc2-49d7-bb01-bb9a01bb714a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Adding a video player to a website is easy.</p>
<p>Building a video streaming system that continues to perform when hundreds or thousands of users start watching at the same time is a very different problem.</p>
<p>A basic implementation might look like this:</p>
<pre><code class="language-text">User
  |
  v
Web Server
  |
  v
video.mp4
</code></pre>
<p>For a small application, this may be perfectly acceptable.</p>
<p>But as traffic increases, the architecture starts dealing with several challenges:</p>
<ul>
<li><p>large media files</p>
</li>
<li><p>bandwidth consumption</p>
</li>
<li><p>concurrent viewers</p>
</li>
<li><p>buffering</p>
</li>
<li><p>video transcoding</p>
</li>
<li><p>multiple resolutions</p>
</li>
<li><p>geographic latency</p>
</li>
<li><p>storage capacity</p>
</li>
<li><p>playback authorization</p>
</li>
</ul>
<p>This article explores how to design a more scalable video delivery architecture using HLS, object storage, a CDN, background workers, and a web player.</p>
<h2>Start by Separating the Application and Media Layers</h2>
<p>One of the first architectural improvements is separating normal application traffic from media delivery.</p>
<p>Instead of:</p>
<pre><code class="language-text">             Web Server
                 |
     +-----------+-----------+
     |           |           |
    API       Database     Videos
</code></pre>
<p>consider:</p>
<pre><code class="language-text">                 User
                   |
          +--------+--------+
          |                 |
          v                 v
    Web Application      CDN
          |                 |
          v                 v
       Database         Video Storage
</code></pre>
<p>The application server handles:</p>
<pre><code class="language-text">authentication
authorization
metadata
business logic
API requests
</code></pre>
<p>The media infrastructure handles:</p>
<pre><code class="language-text">large video files
streaming
bandwidth
caching
video segments
</code></pre>
<p>This separation becomes increasingly useful as traffic grows.</p>
<h2>Why Direct MP4 Delivery Can Become Limiting</h2>
<p>An MP4 file can work well for many websites.</p>
<p>With HTTP Range Requests, a browser can request only a portion of the file:</p>
<pre><code class="language-http">Range: bytes=5000000-9999999
</code></pre>
<p>So direct MP4 delivery should not automatically be considered bad architecture.</p>
<p>However, streaming applications often need more flexibility.</p>
<p>Imagine a user on a 30 Mbps connection and another on a 2 Mbps mobile connection.</p>
<p>If both users receive the same high-bitrate video, the slower connection may experience repeated buffering.</p>
<p>This is where adaptive streaming becomes useful.</p>
<h2>Enter HLS</h2>
<p>HLS stands for HTTP Live Streaming.</p>
<p>Instead of treating a video as one large file, HLS uses playlists and smaller media segments.</p>
<p>A simplified structure might look like:</p>
<pre><code class="language-text">video/
├── master.m3u8
├── 1080p/
│   ├── playlist.m3u8
│   ├── segment001.ts
│   ├── segment002.ts
│   └── ...
├── 720p/
│   ├── playlist.m3u8
│   └── ...
└── 480p/
    ├── playlist.m3u8
    └── ...
</code></pre>
<p>The master playlist describes the available versions of the video.</p>
<p>A player can then select an appropriate stream.</p>
<h2>Adaptive Bitrate Streaming</h2>
<p>Suppose we encode three variants:</p>
<pre><code class="language-text">1080p -&gt; 5 Mbps

720p  -&gt; 2.5 Mbps

480p  -&gt; 1 Mbps
</code></pre>
<p>A viewer with a fast connection may receive 1080p.</p>
<p>If network conditions become worse, the player can switch to 720p or 480p.</p>
<p>Conceptually:</p>
<pre><code class="language-text">Fast Network
     |
     v
   1080p

Network drops
     |
     v
    720p

Network drops again
     |
     v
    480p
</code></pre>
<p>The goal is not simply maximum image quality.</p>
<p>The goal is to balance quality and uninterrupted playback.</p>
<h2>Build an Encoding Pipeline</h2>
<p>Uploaded videos normally need processing before they become HLS streams.</p>
<p>A practical pipeline might be:</p>
<pre><code class="language-text">Upload
  |
  v
Original Storage
  |
  v
Queue
  |
  v
Encoding Worker
  |
  v
FFmpeg
  |
  +---- 1080p
  |
  +---- 720p
  |
  +---- 480p
  |
  v
HLS Packaging
  |
  v
Media Storage
</code></pre>
<p>The upload request should not wait for this entire process.</p>
<p>Instead:</p>
<pre><code class="language-text">POST /videos
      |
      v
Store original
      |
      v
Create processing job
      |
      v
Return 202 Accepted
</code></pre>
<p>The conversion happens asynchronously.</p>
<h2>Track Processing State</h2>
<p>A database record can represent the current video state.</p>
<p>For example:</p>
<pre><code class="language-json">{
  "id": 845,
  "status": "processing",
  "progress": 61
}
</code></pre>
<p>Useful states might include:</p>
<pre><code class="language-text">uploaded

queued

processing

ready

failed
</code></pre>
<p>The frontend can poll an API or receive updates through WebSockets.</p>
<p>Once the video becomes ready:</p>
<pre><code class="language-json">{
  "id": 845,
  "status": "ready",
  "playback_url": "/streams/845/master.m3u8"
}
</code></pre>
<p>Now the player can load the stream.</p>
<h2>Don't Upscale Videos Unnecessarily</h2>
<p>Before encoding, inspect the input.</p>
<p>FFprobe is useful:</p>
<pre><code class="language-bash">ffprobe \
  -v quiet \
  -print_format json \
  -show_streams \
  -show_format \
  input.mp4
</code></pre>
<p>Suppose the input is only 720p.</p>
<p>Generating a 1080p version does not create additional real visual detail.</p>
<p>A better ladder could be:</p>
<pre><code class="language-text">4K Input
├── 1080p
├── 720p
└── 480p

1080p Input
├── 1080p
├── 720p
└── 480p

720p Input
├── 720p
└── 480p
</code></pre>
<p>This avoids unnecessary processing and storage.</p>
<h2>Object Storage Is a Natural Fit</h2>
<p>Media files can consume enormous amounts of disk space.</p>
<p>Instead of keeping every video on the application server:</p>
<pre><code class="language-text">Web Server
└── 10 TB videos
</code></pre>
<p>we can separate them:</p>
<pre><code class="language-text">Web Application
      |
      v
   Metadata DB

Object Storage
      |
      +--- originals
      |
      +--- HLS
      |
      +--- thumbnails
</code></pre>
<p>The application database only needs references to the objects.</p>
<p>For example:</p>
<pre><code class="language-json">{
  "video_id": 845,
  "master_playlist":
    "videos/845/hls/master.m3u8",
  "thumbnail":
    "videos/845/thumb.webp"
}
</code></pre>
<p>This keeps the application layer much lighter.</p>
<h2>Put a CDN in Front of Storage</h2>
<p>Serving every HLS segment directly from origin storage can generate a huge number of origin requests.</p>
<p>Consider a 30-minute video divided into 6-second segments:</p>
<pre><code class="language-text">30 minutes
=
1800 seconds

1800 / 6
=
300 segments
</code></pre>
<p>One viewer may request hundreds of objects.</p>
<p>Now multiply that by thousands of viewers.</p>
<p>A CDN can cache frequently requested segments closer to users.</p>
<pre><code class="language-text">              Origin
                |
                v
               CDN
          /      |      \
         /       |       \
        v        v        v
      User     User      User
</code></pre>
<p>If multiple users request the same segment, the CDN may serve cached copies rather than repeatedly contacting the origin.</p>
<h2>Cache Immutable Segments Aggressively</h2>
<p>HLS VOD segments generally do not change after encoding.</p>
<p>That makes them good caching candidates.</p>
<p>Conceptually:</p>
<pre><code class="language-http">Cache-Control:
public, max-age=31536000, immutable
</code></pre>
<p>Whether this exact policy is appropriate depends on your URL/versioning strategy.</p>
<p>The important idea is that immutable media objects can usually use long cache lifetimes.</p>
<h2>Use Unique Paths for New Encodes</h2>
<p>Suppose you replace a video's encoding but keep:</p>
<pre><code class="language-text">/video/845/segment001.ts
</code></pre>
<p>A CDN may still have the old object cached.</p>
<p>A safer approach can be versioned paths:</p>
<pre><code class="language-text">/video/845/v1/segment001.ts

/video/845/v2/segment001.ts
</code></pre>
<p>Now new media receives a new URL.</p>
<p>This reduces cache invalidation problems.</p>
<h2>Keep the Player Independent of Storage</h2>
<p>The frontend should not need to understand the internal storage provider.</p>
<p>Instead of returning:</p>
<pre><code class="language-text">bucket-name.internal-provider.example/...
</code></pre>
<p>return a stable playback URL:</p>
<pre><code class="language-json">{
  "playback_url":
    "https://media.example.com/v/845/master.m3u8"
}
</code></pre>
<p>Now the backend infrastructure can change:</p>
<pre><code class="language-text">Local Storage
      ↓
Object Storage A
      ↓
Object Storage B
</code></pre>
<p>without requiring major frontend changes.</p>
<h2>HLS Playback in the Browser</h2>
<p>Safari supports HLS natively.</p>
<p>Other modern browsers can commonly use <code>hls.js</code>.</p>
<p>A basic implementation:</p>
<pre><code class="language-javascript">import Hls from 'hls.js';

const video =
    document.querySelector('#player');

const source =
    'https://media.example.com/v/845/master.m3u8';

if (Hls.isSupported()) {

    const hls = new Hls();

    hls.loadSource(source);
    hls.attachMedia(video);

} else if (
    video.canPlayType(
        'application/vnd.apple.mpegurl'
    )
) {

    video.src = source;

}
</code></pre>
<p>This provides a basic cross-browser strategy.</p>
<h2>Startup Time Matters</h2>
<p>A streaming system can technically work and still feel slow.</p>
<p>One of the most noticeable metrics is startup time:</p>
<pre><code class="language-text">User clicks Play
       |
       v
Manifest request
       |
       v
Segments requested
       |
       v
Enough buffer
       |
       v
First frame
</code></pre>
<p>If this takes several seconds, users notice.</p>
<p>Useful measurements include:</p>
<pre><code class="language-text">manifest latency

segment latency

time to first frame

buffer level

CDN cache hit ratio
</code></pre>
<p>Optimizing the player alone may not solve slow startup.</p>
<h2>Segment Duration Is a Tradeoff</h2>
<p>Suppose HLS segments are:</p>
<pre><code class="language-text">2 seconds
</code></pre>
<p>You get many small files and more HTTP requests.</p>
<p>With:</p>
<pre><code class="language-text">10 seconds
</code></pre>
<p>you get fewer, larger segments.</p>
<p>There is no universal perfect value.</p>
<p>The right choice depends on:</p>
<pre><code class="language-text">latency requirements

content type

CDN behavior

encoding setup

player behavior
</code></pre>
<p>For normal video-on-demand, several-second segments are a common starting point.</p>
<p>Measure rather than guessing.</p>
<h2>Protect Private Streams</h2>
<p>If a video is private, putting it on an obscure URL is not sufficient.</p>
<p>A better architecture is:</p>
<pre><code class="language-text">User
 |
 v
Authentication
 |
 v
Authorization
 |
 v
Temporary Playback Access
 |
 v
CDN
 |
 v
HLS
</code></pre>
<p>The backend checks whether the user can watch the video.</p>
<p>Only then does it issue temporary playback access.</p>
<h2>Signed URLs and Tokens</h2>
<p>A playback URL might include:</p>
<pre><code class="language-text">video ID
expiration
signature
</code></pre>
<p>For example:</p>
<pre><code class="language-text">/stream/845/master.m3u8
?expires=1786400000
&amp;signature=...
</code></pre>
<p>The media layer verifies the request before serving content.</p>
<p>For private subscription services, temporary access is generally more useful than permanent public media URLs.</p>
<h2>Protect More Than the Master Playlist</h2>
<p>HLS consists of multiple resources.</p>
<pre><code class="language-text">master.m3u8
     |
     v
playlist.m3u8
     |
     v
segment001.ts
segment002.ts
segment003.ts
</code></pre>
<p>If only <code>master.m3u8</code> is protected while all segments remain permanently public, the protection may be incomplete.</p>
<p>Access-control architecture should account for the entire media path.</p>
<h2>Self-Hosted Streaming vs a Video Platform</h2>
<p>Once we combine everything, our infrastructure includes:</p>
<pre><code class="language-text">Upload API

Queue

FFmpeg workers

Object storage

HLS packaging

CDN

Playback security

Player

Monitoring

Cleanup
</code></pre>
<p>For a product where video infrastructure is a core engineering competency, owning this stack may be worthwhile.</p>
<p>For other applications, developers can evaluate a dedicated <a href="https://filemoon.org/en/video-streaming-platform"><strong>video streaming platform</strong></a> such as <strong>FileMoon</strong>, alongside other available media infrastructure providers.</p>
<p>The architectural decision should be based on operational requirements rather than simply whether self-hosting is technically possible.</p>
<p>Ask:</p>
<pre><code class="language-text">Do we need custom encoding?

How much traffic do we expect?

Who manages storage?

Who manages CDN delivery?

How important is private playback?

How much engineering time can we spend on media infrastructure?
</code></pre>
<p>The answers determine whether building or outsourcing parts of the streaming stack makes more sense.</p>
<h2>Monitor Playback, Not Just Servers</h2>
<p>Traditional monitoring might tell you:</p>
<pre><code class="language-text">CPU = 45%

RAM = 62%

Server = online
</code></pre>
<p>But users may still be experiencing terrible video playback.</p>
<p>Streaming-specific metrics are also useful:</p>
<pre><code class="language-text">startup_time

buffering_count

buffering_duration

playback_errors

average_bitrate

watch_time

completion_rate
</code></pre>
<p>For example:</p>
<pre><code class="language-text">Server uptime:
99.99%

Average startup:
8.2 seconds
</code></pre>
<p>Technically the system is available.</p>
<p>From the user's perspective, it may still be poor.</p>
<h2>Measure Buffer Ratio</h2>
<p>A useful metric is:</p>
<pre><code class="language-text">buffer ratio
=
buffering duration
/
watch duration
</code></pre>
<p>Suppose:</p>
<pre><code class="language-text">Watch time:
600 seconds

Buffering:
30 seconds
</code></pre>
<p>Then:</p>
<pre><code class="language-text">30 / 600
=
5%
</code></pre>
<p>Tracking this across users, countries, ISPs, devices, and video qualities can reveal delivery problems.</p>
<h2>Monitor the Encoding Queue</h2>
<p>Playback is only one part of the system.</p>
<p>Upload-to-ready time matters too.</p>
<p>Suppose:</p>
<pre><code class="language-text">Upload completed:
10:00

Encoding started:
10:28

Video ready:
10:36
</code></pre>
<p>The encoding itself took only eight minutes.</p>
<p>But the user waited 36 minutes.</p>
<p>The real problem was queue delay.</p>
<p>Track:</p>
<pre><code class="language-text">queue_wait_time

encoding_time

total_processing_time
</code></pre>
<p>separately.</p>
<h2>Avoid Unlimited Encoding Concurrency</h2>
<p>A common scaling mistake is simply adding more FFmpeg processes.</p>
<p>Suppose one server has:</p>
<pre><code class="language-text">8 CPU cores
</code></pre>
<p>and you run:</p>
<pre><code class="language-text">8 simultaneous FFmpeg jobs
</code></pre>
<p>Each FFmpeg process may itself use multiple threads.</p>
<p>The result can be:</p>
<pre><code class="language-text">CPU saturation
      |
      v
slower encoding
      |
      v
longer queues
      |
      v
unstable server
</code></pre>
<p>Benchmark different concurrency levels.</p>
<p>More simultaneous jobs do not always mean higher total throughput.</p>
<h2>Separate Worker Capacity From Web Capacity</h2>
<p>A useful production architecture might be:</p>
<pre><code class="language-text">              Application
                   |
        +----------+----------+
        |                     |
        v                     v
    Web Servers             Queue
                              |
                  +-----------+-----------+
                  |                       |
                  v                       v
              Worker A                Worker B
                  |                       |
                  +-----------+-----------+
                              |
                              v
                          Storage
                              |
                              v
                             CDN
</code></pre>
<p>Now video processing can scale independently from API traffic.</p>
<p>If encoding demand increases, add worker capacity.</p>
<p>If website traffic increases, scale the web tier.</p>
<h2>Design for Failure</h2>
<p>Distributed video systems will eventually experience failures.</p>
<p>Examples:</p>
<pre><code class="language-text">upload interrupted

worker crashes

FFmpeg fails

storage request times out

CDN origin unavailable

database update fails
</code></pre>
<p>Jobs should be retryable where appropriate.</p>
<p>But retries should also be bounded.</p>
<p>For example:</p>
<pre><code class="language-text">Attempt 1
   |
 failure
   |
 wait 60 sec

Attempt 2
   |
 failure
   |
 wait 5 min

Attempt 3
   |
 failure
   |
 mark failed
</code></pre>
<p>Permanent errors should not consume worker capacity forever.</p>
<h2>Clean Up Temporary Files</h2>
<p>Encoding workers can generate large temporary files.</p>
<p>Always account for cleanup:</p>
<pre><code class="language-text">Download source
      |
      v
Encode
      |
      v
Upload result
      |
      v
Verify upload
      |
      v
Delete temporary files
</code></pre>
<p>Cleanup should run on success and failure.</p>
<p>Otherwise, a worker with hundreds of failed jobs can eventually run out of disk space.</p>
<h2>A More Complete Architecture</h2>
<p>Putting everything together:</p>
<pre><code class="language-text">                    Users
                      |
                      v
                  Application
                      |
          +-----------+-----------+
          |                       |
          v                       v
       Database                Upload
                                  |
                                  v
                           Original Storage
                                  |
                                  v
                                Queue
                                  |
                       +----------+----------+
                       |                     |
                       v                     v
                    Worker                Worker
                       |                     |
                       +----------+----------+
                                  |
                                  v
                               FFmpeg
                                  |
                                  v
                           HLS Variants
                                  |
                                  v
                           Object Storage
                                  |
                                  v
                                 CDN
                                  |
                                  v
                         Playback Security
                                  |
                                  v
                                Player
                                  |
                                  v
                                 User
</code></pre>
<p>Each layer has a clear responsibility.</p>
<h2>Final Thoughts</h2>
<p>A scalable video streaming system is much more than a <code>&lt;video&gt;</code> element.</p>
<p>As an application grows, the architecture may need to handle:</p>
<ul>
<li><p>asynchronous transcoding</p>
</li>
<li><p>HLS packaging</p>
</li>
<li><p>multiple qualities</p>
</li>
<li><p>object storage</p>
</li>
<li><p>CDN caching</p>
</li>
<li><p>secure playback</p>
</li>
<li><p>queue management</p>
</li>
<li><p>monitoring</p>
</li>
<li><p>failure recovery</p>
</li>
</ul>
<p>But this does not mean every project should start with a complex distributed architecture.</p>
<p>For a small application:</p>
<pre><code class="language-text">MP4
+
Object Storage
+
CDN
</code></pre>
<p>may be enough.</p>
<p>As requirements grow:</p>
<pre><code class="language-text">Upload
↓
Queue
↓
FFmpeg
↓
HLS
↓
Storage
↓
CDN
↓
Player
</code></pre>
<p>can become the next stage.</p>
<p>The most important principle is separation of responsibilities.</p>
<p>Keep application logic, CPU-intensive processing, media storage, delivery, and playback loosely coupled.</p>
<p>Then each part of the system can evolve as the product grows instead of forcing the entire video stack to be rebuilt at once.</p>
]]></content:encoded></item><item><title><![CDATA[How to Secure Private Video Streaming in a Web Application]]></title><description><![CDATA[Serving public video is relatively straightforward.
Serving a video that should only be available to a specific user, subscriber, customer, or organization introduces a different set of engineering pr]]></description><link>https://filemoon.hashnode.dev/how-to-secure-private-video-streaming-in-a-web-application</link><guid isPermaLink="true">https://filemoon.hashnode.dev/how-to-secure-private-video-streaming-in-a-web-application</guid><dc:creator><![CDATA[ssvtiktok]]></dc:creator><pubDate>Tue, 11 Aug 2026 16:36:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/699cf33ecf5def0f6ace7ea1/29f5944e-1a76-4606-aa74-ee260f3bdffa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Serving public video is relatively straightforward.</p>
<p>Serving a video that should only be available to a specific user, subscriber, customer, or organization introduces a different set of engineering problems.</p>
<p>A common first implementation looks like this:</p>
<pre><code class="language-html">&lt;video controls&gt;
    &lt;source
        src="https://cdn.example.com/videos/course-101.mp4"
        type="video/mp4"
    &gt;
&lt;/video&gt;
</code></pre>
<p>The problem is not the <code>&lt;video&gt;</code> element.</p>
<p>The problem is that the media URL may be publicly accessible.</p>
<p>If someone copies that URL and shares it, your application-level permission checks may no longer matter.</p>
<p>In this article, we'll design a more practical architecture for private video delivery.</p>
<h2>Start With the Threat Model</h2>
<p>Before implementing security controls, define what you are trying to prevent.</p>
<p>For a subscription video application, common concerns might include:</p>
<pre><code class="language-text">Unauthorized users

Shared media URLs

Expired subscriptions

Automated scraping

Excessive account sharing

Permanent public links
</code></pre>
<p>Notice that "prevent anyone from ever copying a video" is not on the list.</p>
<p>If a legitimate user can watch media on a device, completely preventing capture is not a realistic browser security model.</p>
<p>The goal is to control access and make unauthorized reuse more difficult.</p>
<h2>Authentication Is Not Authorization</h2>
<p>These two concepts are often mixed together.</p>
<p>Authentication answers:</p>
<pre><code class="language-text">Who is the user?
</code></pre>
<p>Authorization answers:</p>
<pre><code class="language-text">Can this user watch this video?
</code></pre>
<p>A logged-in user should not automatically have access to every video.</p>
<p>A typical request flow might be:</p>
<pre><code class="language-text">User
 |
 v
Authentication
 |
 v
Video Request
 |
 v
Authorization
 |
 v
Playback Permission
</code></pre>
<p>Your backend should make the authorization decision.</p>
<h2>A Simple Laravel Example</h2>
<p>Suppose we have:</p>
<pre><code class="language-text">users
videos
subscriptions
</code></pre>
<p>A playback endpoint could look like:</p>
<pre><code class="language-php">Route::get(
    '/videos/{video}/play',
    [VideoPlaybackController::class, 'show']
)-&gt;middleware('auth');
</code></pre>
<p>Then:</p>
<pre><code class="language-php">public function show(
    Request $request,
    Video $video
) {
    $user = $request-&gt;user();

    if (! $user-&gt;can('view', $video)) {
        abort(403);
    }

    return response()-&gt;json([
        'playback_url' =&gt;
            $this-&gt;createPlaybackUrl($video)
    ]);
}
</code></pre>
<p>The frontend never decides whether the user is allowed to watch the video.</p>
<p>It only requests permission from the backend.</p>
<h2>Use Policies for Authorization</h2>
<p>Laravel Policies can keep access rules out of controllers.</p>
<p>For example:</p>
<pre><code class="language-php">class VideoPolicy
{
    public function view(
        User $user,
        Video $video
    ): bool {
        if ($video-&gt;is_public) {
            return true;
        }

        return $user
            -&gt;subscriptions()
            -&gt;where('status', 'active')
            -&gt;exists();
    }
}
</code></pre>
<p>Now the authorization logic is reusable.</p>
<pre><code class="language-php">$this-&gt;authorize(
    'view',
    $video
);
</code></pre>
<p>This becomes especially useful when access rules become more complicated.</p>
<h2>Avoid Permanent Public Media URLs</h2>
<p>Consider:</p>
<pre><code class="language-text">https://media.example.com/private/video-101/master.m3u8
</code></pre>
<p>If that URL works indefinitely for anyone, your application loses much of its control once the URL leaks.</p>
<p>A better model is temporary access.</p>
<p>For example:</p>
<pre><code class="language-text">https://media.example.com/play/video-101
?expires=1786400000
&amp;signature=abc...
</code></pre>
<p>The URL works only until a particular time.</p>
<p>After that:</p>
<pre><code class="language-text">Request
  |
  v
Expiration Check
  |
  +--- valid ---&gt; Video
  |
  +--- expired -&gt; 403
</code></pre>
<p>This doesn't make copying impossible.</p>
<p>It limits how useful a leaked URL remains.</p>
<h2>Signed URLs</h2>
<p>A signed URL normally contains information that the server can verify.</p>
<p>Conceptually:</p>
<pre><code class="language-text">video_id
+
expiration
+
secret
=
signature
</code></pre>
<p>A simplified PHP example:</p>
<pre><code class="language-php">function createSignature(
    int $videoId,
    int $expires,
    string $secret
): string {
    return hash_hmac(
        'sha256',
        $videoId . ':' . $expires,
        $secret
    );
}
</code></pre>
<p>Generate an expiration time:</p>
<pre><code class="language-php">$expires = now()
    -&gt;addMinutes(10)
    -&gt;timestamp;
</code></pre>
<p>Then:</p>
<pre><code class="language-php">$signature = createSignature(
    $video-&gt;id,
    $expires,
    config('video.secret')
);
</code></pre>
<p>The media layer can verify the signature before allowing playback.</p>
<p>In production, use the signing implementation provided by your CDN, object storage, or media infrastructure where possible rather than inventing your own protocol.</p>
<h2>Never Send Signing Secrets to the Browser</h2>
<p>This would defeat the entire design:</p>
<pre><code class="language-javascript">const VIDEO_SECRET =
    'super-secret-key';
</code></pre>
<p>Anything sent to browser JavaScript should be treated as visible to the user.</p>
<p>The correct flow is:</p>
<pre><code class="language-text">Browser
   |
   | video ID
   v
Backend
   |
   | authorization
   |
   | signing secret
   v
Temporary Playback URL
   |
   v
Browser
</code></pre>
<p>The secret remains on trusted server infrastructure.</p>
<h2>Short-Lived Playback Tokens</h2>
<p>Instead of putting authorization data directly into a URL, another option is a short-lived token.</p>
<p>For example:</p>
<pre><code class="language-json">{
  "video_id": 101,
  "user_id": 42,
  "expires_at": 1786400000
}
</code></pre>
<p>The media server validates the token before delivering content.</p>
<p>The exact mechanism could involve:</p>
<pre><code class="language-text">JWT

opaque token

signed query parameters

CDN signed cookies

application session
</code></pre>
<p>Each approach has different tradeoffs.</p>
<p>The important principle is that playback permission should expire.</p>
<h2>Protect HLS Playlists and Segments</h2>
<p>With HLS, protecting only <code>master.m3u8</code> may not be enough.</p>
<p>Consider:</p>
<pre><code class="language-text">master.m3u8
   |
   +--- 1080p/index.m3u8
   |       |
   |       +--- segment001.ts
   |       +--- segment002.ts
   |
   +--- 720p/index.m3u8
           |
           +--- segment001.ts
</code></pre>
<p>If the playlists require authorization but every segment is permanently public, someone who discovers the segment URLs may still request them directly.</p>
<p>Your security design needs to consider the complete delivery path.</p>
<p>Depending on the infrastructure, authorization might happen through:</p>
<pre><code class="language-text">signed CDN requests

signed cookies

tokenized paths

edge authorization

temporary URLs
</code></pre>
<h2>Don't Treat Hidden URLs as Security</h2>
<p>This is not access control:</p>
<pre><code class="language-javascript">const hiddenVideoUrl =
    '/secret-folder/x92hd82/video.mp4';
</code></pre>
<p>Neither is:</p>
<pre><code class="language-text">robots.txt
</code></pre>
<p>or disabling right-click.</p>
<p>Obscurity can make a URL less obvious, but it should not be the authorization layer.</p>
<p>Assume that a determined user can inspect browser network requests.</p>
<h2>Separate Application and Video Responsibilities</h2>
<p>A clean architecture could look like:</p>
<pre><code class="language-text">              User
                |
                v
        Web Application
                |
       Authentication
                |
       Authorization
                |
                v
        Playback Access
                |
                v
          Video Layer
                |
        +-------+-------+
        |               |
      Storage          CDN
</code></pre>
<p>The application answers:</p>
<pre><code class="language-text">Who is this user?

What can they watch?
</code></pre>
<p>The media layer answers:</p>
<pre><code class="language-text">How should the video be delivered efficiently?
</code></pre>
<p>Keeping these responsibilities separate makes the system easier to reason about.</p>
<h2>Build the Video Layer or Use Dedicated Infrastructure?</h2>
<p>A private video system can eventually require:</p>
<pre><code class="language-text">Upload handling
Storage
Transcoding
HLS packaging
CDN delivery
Signed playback
Expiration
Access control
Monitoring
Cleanup
</code></pre>
<p>If video infrastructure is central to the product, operating this stack yourself may make sense.</p>
<p>For applications where video is only one feature, developers may instead evaluate a dedicated <a href="https://filemoon.org/en/secure-video-hosting"><strong>secure video hosting</strong></a> service such as FileMoon or another media provider and keep authorization and application-specific business rules within their own backend.</p>
<p>The useful architectural question is not whether one approach is universally better.</p>
<p>It is which responsibilities your team actually needs to own.</p>
<h2>Rate-Limit Playback Authorization</h2>
<p>Suppose your application exposes:</p>
<pre><code class="language-text">POST /api/videos/101/playback-token
</code></pre>
<p>Without rate limiting, an authenticated account might generate huge numbers of tokens.</p>
<p>Laravel provides rate-limiting tools that can help control this.</p>
<p>Conceptually:</p>
<pre><code class="language-text">User
 |
 v
Rate Limit
 |
 v
Authorization
 |
 v
Generate Token
</code></pre>
<p>For example, you might limit how frequently a client can request fresh playback authorization.</p>
<p>The exact threshold should reflect your player behavior and expected user activity.</p>
<h2>Log Important Playback Events</h2>
<p>Security controls become much more useful when you can observe what is happening.</p>
<p>Useful events may include:</p>
<pre><code class="language-text">user_id

video_id

token_created_at

token_expires_at

authorization_result

IP address

user agent

playback request
</code></pre>
<p>Be deliberate about privacy and retention when collecting these fields.</p>
<p>The purpose is not to collect everything forever.</p>
<p>The purpose is to have enough visibility to investigate abuse and operational problems.</p>
<h2>Detect Suspicious Patterns</h2>
<p>Once useful events are available, you can look for patterns.</p>
<p>For example:</p>
<pre><code class="language-text">Account A

10:00 -&gt; IP A
10:01 -&gt; IP B
10:02 -&gt; IP C
10:03 -&gt; IP D
</code></pre>
<p>This does not automatically prove abuse.</p>
<p>Users can legitimately change networks or use VPNs.</p>
<p>But unusual activity can contribute to a risk signal.</p>
<p>Other patterns might include:</p>
<pre><code class="language-text">hundreds of playback tokens per minute

many simultaneous sessions

repeated authorization failures

extremely high playback volume
</code></pre>
<p>Avoid building aggressive blocking rules from one weak signal.</p>
<p>Combine context.</p>
<h2>Subscription Expiration</h2>
<p>Suppose a user has a subscription that expires at 12:00.</p>
<p>If you issue a playback URL valid for 24 hours at 11:59, that user may retain access long after the subscription ends.</p>
<p>A better expiration rule could be:</p>
<pre><code class="language-text">token expiration
=
min(
    default token lifetime,
    subscription expiration
)
</code></pre>
<p>For example:</p>
<pre><code class="language-php">$expiresAt = min(
    now()-&gt;addMinutes(15)-&gt;timestamp,
    $subscription-&gt;expires_at-&gt;timestamp
);
</code></pre>
<p>Now playback authorization cannot outlive the user's entitlement.</p>
<h2>Revocation Is Harder Than Expiration</h2>
<p>Short-lived tokens are useful because they naturally expire.</p>
<p>But what if access must be revoked immediately?</p>
<p>For example:</p>
<pre><code class="language-text">User account suspended
</code></pre>
<p>A stateless signed URL that remains valid for another hour may continue working.</p>
<p>Possible solutions include:</p>
<pre><code class="language-text">very short token lifetimes

server-side token records

revocation lists

edge authorization

session validation
</code></pre>
<p>Each adds operational complexity.</p>
<p>This is a tradeoff between performance, simplicity, and immediate control.</p>
<h2>Domain Restrictions Are an Additional Layer</h2>
<p>Some video systems allow playback to be restricted to specific domains.</p>
<p>For example:</p>
<pre><code class="language-text">Allowed:
app.example.com

Not expected:
random-site.example
</code></pre>
<p>This can help discourage simple hotlinking.</p>
<p>But it should not replace proper authentication and authorization.</p>
<p>Request headers are not a strong identity mechanism by themselves.</p>
<p>Think of domain restrictions as defense in depth.</p>
<h2>CORS Is Not Video Authorization</h2>
<p>CORS is frequently misunderstood.</p>
<p>Configuring:</p>
<pre><code class="language-http">Access-Control-Allow-Origin:
https://app.example.com
</code></pre>
<p>controls which browser origins can access certain responses through browser cross-origin rules.</p>
<p>It does not transform a public media URL into a securely authenticated resource.</p>
<p>Someone can still make requests outside normal browser JavaScript.</p>
<p>Use CORS for its intended browser security purpose, not as your primary media authorization system.</p>
<h2>Consider Concurrent Session Limits Carefully</h2>
<p>A subscription service may want to limit simultaneous playback.</p>
<p>You could maintain active playback sessions:</p>
<pre><code class="language-text">user_id
video_id
session_id
started_at
last_seen_at
</code></pre>
<p>Then apply a policy such as:</p>
<pre><code class="language-text">Maximum active sessions = 2
</code></pre>
<p>However, session tracking has edge cases:</p>
<pre><code class="language-text">browser crashes

device sleeps

network disconnects

user closes laptop

heartbeat request fails
</code></pre>
<p>So concurrent-session enforcement needs expiration and cleanup logic.</p>
<h2>Player Heartbeats</h2>
<p>One possible approach is a heartbeat.</p>
<pre><code class="language-text">Player
  |
  | every 30-60 seconds
  v
Playback API
</code></pre>
<p>The server updates:</p>
<pre><code class="language-text">last_seen_at
</code></pre>
<p>A session that hasn't sent a heartbeat for a defined period can be considered inactive.</p>
<p>This can also provide useful watch-time information.</p>
<p>But avoid extremely frequent heartbeats; they can create unnecessary backend load.</p>
<h2>Security Is Layered</h2>
<p>A practical private-video architecture may combine several controls:</p>
<pre><code class="language-text">Authentication
      |
      v
Authorization
      |
      v
Rate Limiting
      |
      v
Short-Lived Access
      |
      v
Media Delivery
      |
      v
Logging
      |
      v
Abuse Detection
</code></pre>
<p>No single feature solves every problem.</p>
<p>Signed URLs alone do not verify subscription logic.</p>
<p>Authentication alone does not protect a permanently public media URL.</p>
<p>Domain restrictions alone do not prove user identity.</p>
<p>Layering controls produces a stronger design.</p>
<h2>Don't Promise Impossible Protection</h2>
<p>A web application should avoid claims such as:</p>
<pre><code class="language-text">"This video can never be downloaded."
</code></pre>
<p>A legitimate viewer ultimately receives enough information to render the media.</p>
<p>A determined user may capture network traffic, use recording tools, or otherwise reproduce what is displayed.</p>
<p>The realistic goals are:</p>
<pre><code class="language-text">prevent unauthorized access

limit URL sharing

make access temporary

enforce subscriptions

reduce hotlinking

detect abuse

revoke access when practical
</code></pre>
<p>Those are engineering problems we can meaningfully address.</p>
<h2>A Practical Final Architecture</h2>
<p>A more mature system could look like:</p>
<pre><code class="language-text">                     User
                       |
                       v
                Authentication
                       |
                       v
                 Application
                       |
                  Authorization
                       |
                 Rate Limiting
                       |
                       v
              Playback Token API
                       |
                       v
                Signed Access
                       |
                       v
                    CDN
                       |
                       v
                HLS Playlist
                       |
                       v
                 HLS Segments
                       |
                       v
                    Player

                       |
                       v
                Access Logging
</code></pre>
<p>This keeps application-specific permission logic under your control while allowing the media delivery layer to focus on streaming.</p>
<h2>Final Thoughts</h2>
<p>Secure private video delivery is not about hiding an MP4 URL.</p>
<p>It is an authorization architecture.</p>
<p>Start with:</p>
<pre><code class="language-text">Authentication
+
Authorization
</code></pre>
<p>Then add controls according to the actual threat model:</p>
<pre><code class="language-text">short-lived playback URLs

signed requests

rate limiting

subscription-aware expiration

access logging

domain restrictions

session controls
</code></pre>
<p>And remember that browser-delivered video cannot be made impossible to capture.</p>
<p>The practical objective is to make sure the right users can access the right videos for the right amount of time, while keeping unauthorized access observable and difficult to reuse.</p>
<p>That is a much more useful definition of secure video streaming.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Scalable Video Upload and Processing Pipeline with Laravel, Queues, and HLS]]></title><description><![CDATA[Uploading a video to a Laravel application is easy.
Processing that video reliably at scale is where things become more interesting.
A simple application may start with:
User
  |
  v
Laravel
  |
  v
v]]></description><link>https://filemoon.hashnode.dev/building-a-scalable-video-upload-and-processing-pipeline-with-laravel-queues-and-hls</link><guid isPermaLink="true">https://filemoon.hashnode.dev/building-a-scalable-video-upload-and-processing-pipeline-with-laravel-queues-and-hls</guid><dc:creator><![CDATA[ssvtiktok]]></dc:creator><pubDate>Tue, 11 Aug 2026 16:27:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/699cf33ecf5def0f6ace7ea1/0b569dcc-ec01-4086-ae63-eaa5bec879c4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Uploading a video to a Laravel application is easy.</p>
<p>Processing that video reliably at scale is where things become more interesting.</p>
<p>A simple application may start with:</p>
<pre><code class="language-text">User
  |
  v
Laravel
  |
  v
video.mp4
</code></pre>
<p>This works when the application handles a few small videos.</p>
<p>But once users begin uploading larger files, several problems appear:</p>
<ul>
<li><p>HTTP requests can time out</p>
</li>
<li><p>FFmpeg can consume significant CPU</p>
</li>
<li><p>large uploads consume local disk space</p>
</li>
<li><p>multiple conversions can overload the server</p>
</li>
<li><p>failed jobs need retry logic</p>
</li>
<li><p>users need to know whether a video is ready</p>
</li>
<li><p>streaming a large MP4 is not always ideal</p>
</li>
</ul>
<p>A more maintainable system separates uploading, processing, storage, and playback into different responsibilities.</p>
<p>In this article, we will design a practical Laravel video pipeline using queues, FFmpeg, HLS, and dedicated media infrastructure.</p>
<h2>The Architecture</h2>
<p>A reasonable starting architecture looks like this:</p>
<pre><code class="language-text">                 User
                   |
                   v
             Upload API
                   |
          +--------+--------+
          |                 |
          v                 v
     Original File      Database
                            |
                            v
                        Queue Job
                            |
                            v
                       FFmpeg Worker
                            |
                            v
                        HLS Output
                            |
                            v
                         Storage
                            |
                            v
                           CDN
                            |
                            v
                         Player
</code></pre>
<p>The most important principle is simple:</p>
<p><strong>Do not make the web request responsible for the entire video-processing workflow.</strong></p>
<h2>Step 1: Accept the Upload</h2>
<p>A simplified Laravel controller might look like this:</p>
<pre><code class="language-php">public function store(Request $request)
{
    $request-&gt;validate([
        'video' =&gt; [
            'required',
            'file',
            'mimetypes:video/mp4,video/quicktime'
        ]
    ]);

    $path = $request
        -&gt;file('video')
        -&gt;store('videos/originals');

    $video = Video::create([
        'source_path' =&gt; $path,
        'status' =&gt; 'pending',
    ]);

    ProcessVideo::dispatch($video-&gt;id);

    return response()-&gt;json([
        'id' =&gt; $video-&gt;id,
        'status' =&gt; 'pending'
    ], 202);
}
</code></pre>
<p>Notice that we are not running FFmpeg here.</p>
<p>The upload request performs only a few responsibilities:</p>
<ol>
<li><p>validate the file</p>
</li>
<li><p>store the original</p>
</li>
<li><p>create a database record</p>
</li>
<li><p>dispatch a background job</p>
</li>
<li><p>return a response</p>
</li>
</ol>
<p>The user does not need to keep the HTTP request open while the server processes the video.</p>
<h2>Step 2: Track Processing State</h2>
<p>Video processing is asynchronous, so the application needs to know what is happening.</p>
<p>A simple status model might be:</p>
<pre><code class="language-text">pending
processing
ready
failed
</code></pre>
<p>A database table could include:</p>
<pre><code class="language-php">Schema::create('videos', function (Blueprint $table) {
    $table-&gt;id();

    $table-&gt;string('source_path');

    $table-&gt;string('playback_path')
        -&gt;nullable();

    $table-&gt;string('status')
        -&gt;default('pending');

    $table-&gt;unsignedTinyInteger('progress')
        -&gt;default(0);

    $table-&gt;text('error_message')
        -&gt;nullable();

    $table-&gt;timestamps();
});
</code></pre>
<p>Now the frontend can request:</p>
<pre><code class="language-text">GET /api/videos/123
</code></pre>
<p>and receive:</p>
<pre><code class="language-json">{
  "id": 123,
  "status": "processing",
  "progress": 42
}
</code></pre>
<p>The UI can show a proper processing state instead of presenting a broken player.</p>
<h2>Step 3: Process the Video in a Queue</h2>
<p>Create a Laravel job:</p>
<pre><code class="language-bash">php artisan make:job ProcessVideo
</code></pre>
<p>A simplified version:</p>
<pre><code class="language-php">class ProcessVideo implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public int $tries = 3;

    public function __construct(
        public int $videoId
    ) {
    }

    public function handle(): void
    {
        $video = Video::findOrFail(
            $this-&gt;videoId
        );

        $video-&gt;update([
            'status' =&gt; 'processing'
        ]);

        // FFmpeg processing
    }
}
</code></pre>
<p>Passing the ID instead of relying on a large serialized model also keeps the job payload simple.</p>
<h2>Step 4: Inspect the File Before Encoding</h2>
<p>Before running an expensive transcode, inspect the source with <code>ffprobe</code>.</p>
<p>Example:</p>
<pre><code class="language-bash">ffprobe \
  -v quiet \
  -print_format json \
  -show_format \
  -show_streams \
  input.mp4
</code></pre>
<p>This can tell us:</p>
<pre><code class="language-text">duration
codec
width
height
bitrate
frame rate
audio streams
</code></pre>
<p>That information can influence the encoding pipeline.</p>
<p>For example, there is usually no reason to upscale a 720p source into 1080p.</p>
<p>A simple rule might be:</p>
<pre><code class="language-text">2160p source
  -&gt; 1080p
  -&gt; 720p
  -&gt; 480p

1080p source
  -&gt; 1080p
  -&gt; 720p
  -&gt; 480p

720p source
  -&gt; 720p
  -&gt; 480p
</code></pre>
<p>This saves processing time and storage.</p>
<h2>Step 5: Generate HLS</h2>
<p>For a basic test, FFmpeg can produce HLS output with:</p>
<pre><code class="language-bash">ffmpeg \
  -i input.mp4 \
  -c:v libx264 \
  -c:a aac \
  -hls_time 6 \
  -hls_playlist_type vod \
  output.m3u8
</code></pre>
<p>This generates a playlist and smaller media segments.</p>
<p>Conceptually:</p>
<pre><code class="language-text">video-123/
├── playlist.m3u8
├── segment000.ts
├── segment001.ts
├── segment002.ts
└── ...
</code></pre>
<p>For adaptive bitrate streaming, the structure becomes more complex:</p>
<pre><code class="language-text">video-123/
├── master.m3u8
├── 1080p/
│   ├── playlist.m3u8
│   └── segments...
├── 720p/
│   ├── playlist.m3u8
│   └── segments...
└── 480p/
    ├── playlist.m3u8
    └── segments...
</code></pre>
<p>The player can then choose an appropriate quality based on network conditions.</p>
<h2>Step 6: Execute FFmpeg Safely</h2>
<p>Avoid constructing a shell command by concatenating raw user input.</p>
<p>Using Symfony Process is cleaner:</p>
<pre><code class="language-php">use Symfony\Component\Process\Process;

$process = new Process([
    'ffmpeg',
    '-y',
    '-i',
    $inputPath,
    '-c:v',
    'libx264',
    '-c:a',
    'aac',
    '-hls_time',
    '6',
    '-hls_playlist_type',
    'vod',
    $outputPath,
]);

$process-&gt;setTimeout(3600);

$process-&gt;run();

if (! $process-&gt;isSuccessful()) {
    throw new RuntimeException(
        $process-&gt;getErrorOutput()
    );
}
</code></pre>
<p>The job can then mark the video as ready:</p>
<pre><code class="language-php">$video-&gt;update([
    'status' =&gt; 'ready',
    'progress' =&gt; 100,
    'playback_path' =&gt; $relativePath
]);
</code></pre>
<h2>Step 7: Handle Failures Properly</h2>
<p>Encoding can fail for many reasons:</p>
<pre><code class="language-text">corrupt file
unsupported codec
disk full
timeout
FFmpeg crash
worker restart
storage failure
</code></pre>
<p>Do not leave the video permanently stuck in <code>processing</code>.</p>
<pre><code class="language-php">try {

    $this-&gt;encodeVideo($video);

    $video-&gt;update([
        'status' =&gt; 'ready',
        'progress' =&gt; 100,
    ]);

} catch (Throwable $e) {

    $video-&gt;update([
        'status' =&gt; 'failed',
        'error_message' =&gt; mb_substr(
            $e-&gt;getMessage(),
            0,
            5000
        )
    ]);

    throw $e;
}
</code></pre>
<p>Users and administrators can now distinguish a failed conversion from one that is simply still running.</p>
<h2>Retry With Backoff</h2>
<p>Laravel jobs support retry strategies.</p>
<pre><code class="language-php">public int $tries = 3;

public function backoff(): array
{
    return [
        60,
        300,
        900
    ];
}
</code></pre>
<p>That gives us:</p>
<pre><code class="language-text">Failure
  |
  v
Wait 60 seconds
  |
Retry
  |
  v
Wait 300 seconds
  |
Retry
</code></pre>
<p>But not every error should be retried.</p>
<p>A temporary storage error may succeed later.</p>
<p>A corrupt input file probably will not.</p>
<p>Production systems should eventually classify retryable and permanent failures separately.</p>
<h2>Step 8: Keep Heavy Processing Away From the Web Server</h2>
<p>At first, the queue worker may run on the same machine:</p>
<pre><code class="language-text">Server
├── nginx
├── PHP-FPM
├── Database
└── FFmpeg Worker
</code></pre>
<p>This is easy to operate.</p>
<p>As traffic grows, FFmpeg becomes a noisy neighbor.</p>
<p>A more scalable architecture is:</p>
<pre><code class="language-text">Web Server
├── nginx
└── PHP-FPM

Worker Server
├── Queue Worker
└── FFmpeg

Storage
└── Video Files
</code></pre>
<p>Now CPU-intensive encoding does not directly compete with web requests.</p>
<h2>Be Careful With Worker Concurrency</h2>
<p>More workers do not automatically mean more throughput.</p>
<p>Imagine an 8-core server running:</p>
<pre><code class="language-text">Worker 1 -&gt; FFmpeg
Worker 2 -&gt; FFmpeg
Worker 3 -&gt; FFmpeg
Worker 4 -&gt; FFmpeg
Worker 5 -&gt; FFmpeg
Worker 6 -&gt; FFmpeg
</code></pre>
<p>FFmpeg itself can use multiple threads.</p>
<p>Running too many simultaneous jobs may result in:</p>
<pre><code class="language-text">CPU = 100%
load average increases
encoding slows down
web requests suffer
</code></pre>
<p>Start with conservative concurrency and measure.</p>
<p>For example:</p>
<pre><code class="language-text">2 FFmpeg jobs
</code></pre>
<p>may complete more reliably than six jobs fighting over the same CPU resources.</p>
<h2>Step 9: Move Video Storage Out of the App Server</h2>
<p>Video files grow quickly.</p>
<p>A better long-term model is:</p>
<pre><code class="language-text">Application Server
       |
       v
Metadata Database

Object Storage
       |
       +--- originals
       |
       +--- streams
</code></pre>
<p>The application server does not need to hold every media file locally.</p>
<p>Temporary local storage can still be useful during processing.</p>
<p>After encoding:</p>
<pre><code class="language-text">Local Temporary Files
       |
       v
Upload to Object Storage
       |
       v
Delete Temporary Files
</code></pre>
<p>Always clean up temporary data, even when a job fails.</p>
<h2>Use <code>finally</code> for Cleanup</h2>
<p>Example:</p>
<pre><code class="language-php">try {

    $this-&gt;processVideo();

} finally {

    $this-&gt;cleanupTemporaryFiles();
}
</code></pre>
<p>A forgotten temporary directory can eventually fill the entire worker disk.</p>
<p>Video processing systems need explicit storage lifecycle management.</p>
<h2>Step 10: Keep Playback Infrastructure Abstract</h2>
<p>The frontend should not need to know whether media is stored:</p>
<pre><code class="language-text">locally
on object storage
behind a CDN
on another media server
</code></pre>
<p>Have your backend return a playback URL:</p>
<pre><code class="language-json">{
  "id": 123,
  "title": "Example Video",
  "playback_url": "https://media.example.com/123/master.m3u8"
}
</code></pre>
<p>Then the frontend simply loads it.</p>
<pre><code class="language-javascript">async function loadVideo(id) {
  const response =
    await fetch(`/api/videos/${id}`);

  const data =
    await response.json();

  player.src =
    data.playback_url;
}
</code></pre>
<p>This abstraction allows the video layer to evolve without rewriting the frontend.</p>
<h2>Build the Entire Pipeline or Use Dedicated Infrastructure?</h2>
<p>At this point, notice how many components we have introduced:</p>
<pre><code class="language-text">Upload API
Database
Queue
FFprobe
FFmpeg
Workers
HLS
Object Storage
CDN
Cleanup
Monitoring
Retries
</code></pre>
<p>For applications where video infrastructure is core technology, building and operating this pipeline may make sense.</p>
<p>For applications where video is only one feature, maintaining the whole media stack may be unnecessary.</p>
<p>In that case, developers can also evaluate a dedicated <a href="https://filemoon.org/en/video-hosting-platform"><strong>video hosting platform</strong></a> such as FileMoon, or another media service, and keep their application focused on authentication, business logic, and user experience.</p>
<p>The architectural decision is not simply “self-hosted vs platform.”</p>
<p>The real question is:</p>
<p><strong>Which parts of the video pipeline provide unique value to your application, and which parts are infrastructure you would rather not operate?</strong></p>
<h2>Step 11: Add Access Control</h2>
<p>If videos are private or paid, avoid exposing permanent public URLs.</p>
<p>A better request flow is:</p>
<pre><code class="language-text">User
  |
  v
Authentication
  |
  v
Authorization
  |
  v
Playback Access
  |
  v
Video
</code></pre>
<p>Depending on the infrastructure, this might use:</p>
<pre><code class="language-text">signed URLs
expiring tokens
session validation
domain restrictions
</code></pre>
<p>Do not treat a hidden frontend URL as security.</p>
<p>Authorization should happen on trusted backend infrastructure.</p>
<h2>Step 12: Monitor the Pipeline</h2>
<p>A video system needs more than uptime monitoring.</p>
<p>Useful metrics include:</p>
<pre><code class="language-text">upload failures
queue wait time
encoding duration
failed jobs
CPU usage
memory usage
disk usage
output size
playback errors
startup time
buffering
</code></pre>
<p>Imagine the average queue wait grows from:</p>
<pre><code class="language-text">20 seconds
   ↓
5 minutes
   ↓
35 minutes
</code></pre>
<p>The player may still work perfectly, but users are waiting too long for uploaded videos to become ready.</p>
<p>That is an infrastructure problem worth detecting.</p>
<h2>A Practical Final Architecture</h2>
<p>A more mature version might look like:</p>
<pre><code class="language-text">                    User
                      |
                      v
                Web Application
                      |
          +-----------+-----------+
          |                       |
          v                       v
      Database                Upload API
                                  |
                                  v
                            Original Storage
                                  |
                                  v
                               Queue
                                  |
                       +----------+----------+
                       |                     |
                       v                     v
                   Worker 1              Worker 2
                       |                     |
                       +----------+----------+
                                  |
                                  v
                              FFmpeg
                                  |
                                  v
                              HLS Output
                                  |
                                  v
                           Object Storage
                                  |
                                  v
                                 CDN
                                  |
                                  v
                               Player
</code></pre>
<p>This gives every part of the system a clear responsibility.</p>
<h2>Final Thoughts</h2>
<p>A scalable video pipeline is not really an FFmpeg problem.</p>
<p>FFmpeg is only one component.</p>
<p>The difficult parts are usually:</p>
<ul>
<li><p>controlling asynchronous work</p>
</li>
<li><p>managing CPU-intensive jobs</p>
</li>
<li><p>tracking state</p>
</li>
<li><p>handling failures</p>
</li>
<li><p>managing storage lifecycle</p>
</li>
<li><p>separating infrastructure responsibilities</p>
</li>
<li><p>monitoring the user experience</p>
</li>
</ul>
<p>Start with the simplest architecture that works.</p>
<p>For a small project:</p>
<pre><code class="language-text">MP4
+
Object Storage
+
CDN
</code></pre>
<p>may be enough.</p>
<p>When the product needs adaptive streaming and automated processing:</p>
<pre><code class="language-text">Upload
↓
Queue
↓
FFmpeg
↓
HLS
↓
Storage
↓
CDN
</code></pre>
<p>becomes a reasonable next step.</p>
<p>And when maintaining that pipeline starts consuming more engineering time than your actual product, it may be worth deciding whether the media infrastructure should remain an internal system at all.</p>
]]></content:encoded></item></channel></rss>