Building a Live HLS Video Stream Plugin for WordPress: A complete build journey

Yesterday my after-hours work started with fixing a video streaming feature I had been experimenting with. What began as a simple idea—showing a live feed of my office during work hours—turned into a multi-week journey through Raspberry Pi hardware, streaming protocols, WordPress plugin development, and some surprisingly tricky JavaScript debugging.

Table of Contents


Why Build This?

I’ve got a Raspberry Pi 5 in my office that I had bought for a dungeons and dragons coding project with my daughter. That project came and went, and now I needed to find a good use for it.

Since my site got hacked, I’ve been thinking about how I want to run Click Foundry moving forward. The biggest idea I wanted to communicate was “building in public.” I’m not a SAAS guy—I like making websites, plugins, and things that solve problems. As GPTs get better at coding, I’m able to turn small ideas into weekend projects.

And this project is exactly about that. It communicates that I’m out here working. Monday through Friday, I’m out here making stuff.

I remembered somewhere around 2014, there was a marketing agency I was too intimidated to apply for. Their home header was a video of their open office plan, with a bunch of people on laptops. And it affected me.

Looking back, I don’t think it was live, but I decided to do it live for a few reasons:

  1. Building in Public—visually represented
  2. Self-imposed digital panopticon—Maybe I behave better when I know someone is watching me, whether or not someone actually is
  3. I have this Raspberry Pi collecting dust—Might as well use it

The Hardware Setup

The first part was the hardest: accepting that I would not be able to repurpose my baby webcam or any Arlo webcam quickly enough to get this project moving.

This drove me mad because at first I thought the Raspberry Pi should at least recognize the camera, and then we could go from there. But cameras made for certain purposes are locked from tampering—whether for security or to keep users within their ecosystem. I couldn’t get it to work.

So I bought a $30 Logitech webcam.

My Pi instantly recognized it. From there, it was a matter of figuring out how to turn that data into streaming data.

The GStreamer Pipeline

I ended up using FFmpeg and GStreamer, which are powerful tools for video processing. The challenge was:

  1. Getting the Pi to recognize /dev/video0 (the webcam)
  2. Finding the right resolution (1920×1080 didn’t work—camera maxed at 5fps)
  3. Settling on 640×360 @ 30fps for reliability
  4. Encoding to VP8 for web streaming

The test pattern stream helped me debug without the webcam:

gst-launch-1.0 videotestsrc ! videoconvert ! \
  vp8enc deadline=1 ! rtpvp8pay ! udpsink host=127.0.0.1 port=8006

Once I confirmed the pipeline worked, I switched to the actual webcam feed:

gst-launch-1.0 v4l2src device=/dev/video0 ! \
  video/x-raw,width=640,height=360,framerate=30/1 ! \
  videoconvert ! \
  vp8enc deadline=1 ! \
  rtpvp8pay ! \
  udpsink host=127.0.0.1 port=8004

Getting the Stream to Work

Initially, I tried Janus WebRTC Server for ultra-low-latency streaming, but configuration issues and complex RTP relay setups made it too finicky.

I pivoted to RTMP streaming to Cloudflare Stream, which offered:

  • Reliable CDN distribution
  • Built-in transcoding
  • HLS output for web playback
  • 8-20 second latency (acceptable for this use case)

The final streaming command:

gst-launch-1.0 v4l2src device=/dev/video0 ! \
  video/x-raw,width=640,height=360,framerate=30/1 ! \
  videoconvert ! \
  x264enc tune=zerolatency speed-preset=ultrafast bitrate=500 key-int-max=15 ! \
  flvmux streamable=true ! \
  rtmpsink location='rtmp://live.cloudflare.com/live/[KEY]' sync=false async=false

And just like that—live video from my office to the world.

The First WordPress Integration

At first, I tried embedding the stream with an iframe. Simple, right?

<iframe src="https://cloudflare-stream-url" width="800" height="450"></iframe>

The problem? The lag was several minutes, and sometimes it wouldn’t play at all. It wasn’t playing nicely with Barba.js, which handles page transitions on my site.

So I rebuilt it as a WordPress plugin with a shortcode that generates an HLS stream player using HLS.js.

The Plugin Architecture

The plugin had:

  • A shortcode:
  • Settings for autoplay, mute, and control visibility
  • Cloudflare Stream integration
  • A custom video player wrapper

The shortcode would output a <video> element, and JavaScript would initialize an HLS player:

function render_stream_shortcode() {
    $uid = get_option('cf_stream_uid');
    $customer_code = get_option('cf_stream_customer_code');
    $video_id = "cfStream_{$uid}";
    
    return sprintf(
        '<div class="cf-stream"><video id="%s" width="800" height="450" autoplay muted playsinline></video></div>',
        esc_attr($video_id)
    );
}

This worked great—until Barba.js page transitions broke everything.

Office Hours Automation

I didn’t want the stream running 24/7, so I built an office hours automation script (office_stream.sh) that:

  1. Checks the current time every 5 minutes
  2. Only streams Monday-Friday, 9am-5pm (with a lunch break)
  3. Prevents duplicate stream processes
  4. Logs all activity

The Script Logic

# Split schedule: 9am-1pm, break, 1:01pm-5pm weekdays
HOUR=$(date +%H)
DAY=$(date +%u)  # 1=Monday, 7=Sunday

if [ $DAY -ge 1 ] && [ $DAY -le 5 ]; then
    if [ $HOUR -ge 9 ] && [ $HOUR -lt 13 ]; then
        # Morning session
        start_stream
    elif [ $HOUR -ge 13 ] && [ $HOUR -lt 17 ]; then
        # Afternoon session
        start_stream
    else
        stop_stream
    fi
else
    stop_stream
fi

This script runs via cron every 5 minutes, ensuring the stream only runs during office hours.

Making It Work with Barba.js

The iframe approach didn’t work, and even the custom player broke after Barba.js page transitions. Why? Because Barba replaces the DOM, destroying initialized JavaScript instances.

The video element stayed in the HTML, but the HLS player instance? Gone.

The Problem in Detail

Barba.js creates smooth transitions by:

  1. Fetching new page content
  2. Replacing the DOM content inside a container
  3. Triggering animations

But this means any JavaScript that initialized on page load—like our HLS video player—gets wiped out.

The Solution

I refactored the plugin into a class-based architecture with proper lifecycle management. This is covered in depth in a separate technical post:

Read the full technical breakdown: “Fixing HLS Video Players with Barba.js”

Quick summary:

  • Built CFStreamPlayer class for individual player instances
  • Built CFStreamManager class to handle multiple players
  • Added destroy() methods for cleanup before page transitions
  • Hooked into Barba’s lifecycle events (leave and afterEnter)
  • Used a global manager instance to reinitialize players after transitions

The result? Smooth page transitions with working video every time.

Building the Smart Video Router

Once the player was stable, I realized I needed intelligent video routing. The stream shouldn’t always show the same thing—it should adapt based on:

  • Time of day (office hours vs after hours)
  • Manual overrides (lunch break, technical difficulties)
  • Operational status (stream enabled/disabled)

The Requirements

I needed:

  1. Time-aware routing that automatically shows an “After Hours” video outside 9am-5pm LA time
  2. Admin controls for non-technical users to toggle the stream on/off
  3. Fallback scenarios for lunch breaks and technical issues
  4. Multi-platform support (Cloudflare for live, Vimeo for fallbacks)

The Implementation

I built a WordPress admin settings panel where administrators could:

  • Toggle the live stream on/off
  • Enable/disable office hours automation
  • Select fallback video scenarios
  • Configure Cloudflare and Vimeo video IDs

The routing priority:

  1. Office Hours Check → If outside 9-5 LA time, show “After Hours” video
  2. Stream Toggle Check → If disabled, show selected fallback (lunch or technical)
  3. Default → Show live Cloudflare stream

This gave content managers full control without touching code, while the system automatically handled time-based routing.

→ Read the full build story: “Building a Smart Video Router: Time-Aware Content Delivery”

Quick summary:

  • Created WordPress admin menu with settings page
  • Built time detection logic using PHP DateTime with LA timezone
  • Implemented priority-based video routing
  • Integrated Vimeo embeds for fallback videos with background mode
  • Added responsive CSS with 16:9 aspect ratio enforcement

What I Learned

1. Cameras Are Finicky

Vendor-locked devices (baby monitors, security cameras) are often impossible to repurpose. A cheap generic webcam solved everything instantly.

2. Test Patterns Are Your Friend

The GStreamer color bars test stream was invaluable for debugging. It separated hardware issues from software issues.

3. SPAs Break Everything

Page transition libraries like Barba.js require careful lifecycle management. Always plan for destroy/reinit patterns.

4. Latency Is a Tradeoff

I wanted ultra-low latency (WebRTC), but the complexity wasn’t worth it. 8-20 second latency via Cloudflare HLS was perfectly acceptable for this use case.

5. Progressive Enhancement Works

I kept the original functionality intact while adding new features. Existing shortcodes still work—nothing broke.

6. Admin UIs Matter

Non-technical users need simple controls. A few checkboxes and dropdowns made the difference between “unusable” and “useful.”


The Final Result

What visitors see:

  • Live office stream during work hours (9am-5pm, Monday-Friday)
  • “After Hours” video outside office hours
  • Fallback videos when I manually disable the stream
  • Seamless playback with no configuration needed

What I control:

  • Simple WordPress admin toggle to turn stream on/off
  • Office hours automation that I can enable/disable
  • Choice between lunch break or technical difficulty fallbacks
  • All video IDs configurable in one place

The tech stack:

  • Raspberry Pi 5 + Logitech webcam
  • GStreamer → RTMP → Cloudflare Stream
  • Bash script for office hours automation
  • WordPress plugin with shortcode
  • HLS.js for playback with stall recovery
  • Barba.js compatibility with lifecycle management
  • Vimeo for fallback videos

Try It Yourself

The plugin is designed to be reusable. If you want to embed Cloudflare Stream videos in WordPress with:

  • HLS.js playback
  • Automatic stall recovery
  • Barba.js compatibility
  • Time-based routing
  • Admin controls

…you can adapt this approach for your own projects.

Key components:

  1. WordPress plugin with admin settings page
  2. JavaScript classes for player management
  3. Priority-based routing logic
  4. Lifecycle hooks for SPA compatibility

Building in public means showing the messy parts. This project wasn’t clean or linear—it was a series of dead ends, pivots, and small wins. But that’s the point. You’re watching me figure it out in real time.

And now, you can literally watch me work: clickfoundry.co

💬