Building a Smart Video Router: From Basic Player to Intelligent Stream Manager

How we transformed a simple video player into a time-aware content delivery system

The Challenge

We started with a functional WordPress plugin that embedded Cloudflare Stream videos using HLS.js with stall recovery. It worked well, but had a critical limitation: it could only show one type of video with no flexibility for different scenarios.

The business need was clear: we needed intelligent video routing that could automatically display different content based on time, availability, and operational status.

The problem:

  • Live stream shouldn’t play outside office hours
  • Need manual control for breaks and technical issues
  • Different videos needed for different scenarios
  • Non-technical users need to manage this without touching code

What We Built

We transformed a basic video player into a smart video management system with three key capabilities:

1. Time-Aware Routing

The system needed to know when the office was open (9am-5pm Los Angeles time, Monday-Friday). Outside those hours, visitors should see an “After Hours” message video instead of the live stream.

Why this matters:

  • No one wants to watch an empty office at night
  • Automatic behavior = zero maintenance
  • Sets proper expectations for visitors
  • Saves bandwidth when stream isn’t relevant

2. Manual Override Controls

Operations teams needed a simple toggle to turn the stream on/off without touching code. When disabled, they could choose between preset scenarios:

  • Lunch Break – “We’ll be back soon”
  • Technical Difficulties – “Experiencing issues”

Why this matters:

  • Quick response to technical problems
  • Professional messaging during breaks
  • No need to SSH into the server
  • Content managers can handle it

3. Dual Platform Support

While the main stream lives on Cloudflare, the fallback videos are hosted on Vimeo. The plugin needed to seamlessly switch between both platforms with different embed requirements.

Why this matters:

  • Don’t put all eggs in one basket
  • Vimeo’s background mode perfect for looping fallbacks
  • Cloudflare optimized for live streaming
  • Each platform does what it’s best at

The Implementation Journey

Step 1: Admin Settings Panel

First, we added a WordPress admin menu page where administrators could configure everything:

public function add_admin_menu() {
    add_menu_page(
        'CF Stream Settings',
        'CF Stream',
        'manage_options',
        'cf-stream-settings',
        [ $this, 'render_admin_page' ],
        'dashicons-video-alt3',
        100
    );
}

This gave us a dedicated settings page accessible from the WordPress admin sidebar with a clean video icon.

Design considerations:

  • Put it in the main admin menu (not buried in Settings)
  • Use a recognizable icon (dashicons-video-alt3)
  • Clear, descriptive page title
  • Only visible to users with manage_options capability

Step 2: Settings Structure

We designed a settings array that captured all configuration needs:

private function get_default_settings() {
    return [
        'cf_uid' => '',                      // Cloudflare Stream UID
        'cf_customer_code' => '',            // Cloudflare customer code
        'stream_enabled' => true,            // Master on/off switch
        'office_hours_enabled' => true,      // Time-based routing
        'fallback_video' => 'lunch',         // Which fallback to show
        'vimeo_after_hours' => '',           // After hours video ID
        'vimeo_lunch' => '',                 // Lunch break video ID
        'vimeo_technical' => '',             // Technical difficulties video ID
    ];
}

Why this structure works:

  • All related settings in one array
  • Clear naming convention
  • Boolean flags for easy toggling
  • Separate IDs for each scenario
  • Sensible defaults

All settings are sanitized on save and stored in a single WordPress option for efficient retrieval:

public function save_settings() {
    check_admin_referer('cf_stream_save_settings');
    
    $settings = [
        'cf_uid' => sanitize_text_field($_POST['cf_uid']),
        'cf_customer_code' => sanitize_text_field($_POST['cf_customer_code']),
        'stream_enabled' => isset($_POST['stream_enabled']),
        'office_hours_enabled' => isset($_POST['office_hours_enabled']),
        'fallback_video' => sanitize_text_field($_POST['fallback_video']),
        'vimeo_after_hours' => sanitize_text_field($_POST['vimeo_after_hours']),
        'vimeo_lunch' => sanitize_text_field($_POST['vimeo_lunch']),
        'vimeo_technical' => sanitize_text_field($_POST['vimeo_technical']),
    ];
    
    update_option('cf_stream_settings', $settings);
}

Security first:

  • Nonce verification prevents CSRF attacks
  • Every input is sanitized
  • Checkboxes handled properly (isset check)
  • Single option update = atomic save

Step 3: Time Detection Logic

The office hours check was critical. We used PHP’s DateTime with the Los Angeles timezone:

private function is_office_hours() {
    $la_time = new DateTime('now', new DateTimeZone('America/Los_Angeles'));
    $hour = (int) $la_time->format('G');
    $day_of_week = (int) $la_time->format('N');
    
    return ($day_of_week >= 1 && $day_of_week <= 5)
        && ($hour >= 9 && $hour < 17);
}

Breaking this down:

  • DateTimeZone('America/Los_Angeles') – Always use explicit timezone (no server timezone assumptions)
  • format('G') – 24-hour format without leading zeros (9, not 09)
  • format('N') – ISO-8601 day of week (1=Monday, 7=Sunday)
  • Simple boolean logic for weekdays 9am-5pm

Why explicit timezone matters: Your server might be in Virginia, but your office is in Los Angeles. Using server time would show the wrong videos for 3 hours every day.

This returns a simple boolean that’s checked every time someone loads the page. No caching, no complexity—just “are we open right now?”

Step 4: Priority-Based Video Routing

The shortcode renderer became a decision engine with clear priority order:

public function render_shortcode($atts) {
    $options = get_option('cf_stream_settings', $this->get_default_settings());
    
    $video_type = 'cloudflare';
    $video_id = $options['cf_uid'];
    
    // Priority 1: Office Hours Check (if enabled)
    if ($options['office_hours_enabled'] && !$this->is_office_hours()) {
        $video_type = 'vimeo';
        $video_id = $options['vimeo_after_hours'];
    }
    
    // Priority 2: Stream Toggle Check
    elseif (!$options['stream_enabled']) {
        $video_type = 'vimeo';
        $video_id = ($options['fallback_video'] === 'lunch')
            ? $options['vimeo_lunch']
            : $options['vimeo_technical'];
    }
    
    // Priority 3: Default Behavior - Show live stream
    
    return $this->render_player($video_type, $video_id, $options);
}

The decision cascade:

Priority 1: Office Hours Check (if enabled)

  • If outside 9am-5pm LA time → Vimeo “After Hours” video

Priority 2: Stream Toggle Check

  • If stream disabled → Selected fallback video (Lunch or Technical)

Priority 3: Default Behavior

  • Show live Cloudflare stream

Why this order matters:

  • Most restrictive condition wins
  • Office hours trump manual controls (prevents accidentally showing live stream at 2am)
  • Clear fallthrough logic—easy to debug
  • No nested ifs—readable code

Example scenarios:

TimeStream ToggleOffice HoursResult
2pm TuesdayOnEnabledLive stream
11pm TuesdayOnEnabledAfter hours video
2pm TuesdayOffEnabledFallback video (lunch/technical)
2pm TuesdayOffDisabledFallback video (lunch/technical)
11pm TuesdayOffEnabledAfter hours video

The office hours check always wins when enabled, regardless of the stream toggle.

Step 5: Vimeo Integration

Vimeo embeds required different handling than Cloudflare. We needed:

  • Background mode for seamless looping
  • No controls for a clean presentation
  • Auto-loop and muted playback
  • Responsive 16:9 aspect ratio
private function render_vimeo_player($video_id) {
    if (empty($video_id)) {
        return '<p>Video not configured.</p>';
    }
    
    $iframe_src = sprintf(
        'https://player.vimeo.com/video/%s?background=1&autoplay=1&loop=1&muted=1&controls=0',
        esc_attr($video_id)
    );
    
    return sprintf(
        '<div class="cf-vimeo-wrapper">
            <iframe class="cf-vimeo-iframe" 
                    src="%s" 
                    frameborder="0" 
                    allow="autoplay; fullscreen" 
                    allowfullscreen>
            </iframe>
        </div>',
        esc_url($iframe_src)
    );
}

The Vimeo URL parameters:

  • background=1 – Vimeo’s special background video mode (crucial for auto-looping)
  • autoplay=1 – Start immediately
  • loop=1 – Continuous playback
  • muted=1 – No audio (required for autoplay)
  • controls=0 – Clean presentation

The background=1 parameter is key—it’s Vimeo’s special mode for background videos that automatically handles continuous playback without showing any UI.

Cloudflare vs Vimeo rendering:

private function render_player($video_type, $video_id, $options) {
    if ($video_type === 'vimeo') {
        return $this->render_vimeo_player($video_id);
    }
    
    // Cloudflare Stream (HLS.js handles this via JavaScript)
    return sprintf(
        '<div class="cf-stream">
            <video id="cfStream_%s" 
                   width="800" 
                   height="450" 
                   autoplay 
                   muted 
                   playsinline>
            </video>
        </div>',
        esc_attr($video_id)
    );
}

Cloudflare outputs a simple <video> element that gets initialized by our HLS.js player class. Vimeo outputs a fully-functional iframe with all parameters baked in.

Step 6: Styling Architecture

To meet the design requirements, we created two semantic class names:

.cf-vimeo-wrapper {
    aspect-ratio: 16 / 9 !important;
    height: -webkit-fill-available;
    width: 100%;
    border: 1px solid var(--cf-gray-dark);
    border-radius: 0.5rem;
    overflow: hidden;
}

.cf-vimeo-iframe {
    width: 100%;
    height: 100%;
    display: block;
}

Why this CSS works:

  • aspect-ratio: 16 / 9 – Modern browsers handle this perfectly
  • -webkit-fill-available – Safari/Chrome height filling
  • overflow: hidden – Border-radius clips the iframe corners
  • display: block – Removes iframe inline spacing
  • Custom properties for theming (var(--cf-gray-dark))

The wrapper maintains aspect ratio while the iframe fills it completely. No JavaScript calculations needed.

Step 7: The Admin Interface

We built a clean, accessible form:

public function render_admin_page() {
    $options = get_option('cf_stream_settings', $this->get_default_settings());
    ?>
    <div class="wrap">
        <h1>CF Stream Settings</h1>
        <form method="post" action="">
            <?php wp_nonce_field('cf_stream_save_settings'); ?>
            
            <h2>Cloudflare Stream Settings</h2>
            <table class="form-table">
                <tr>
                    <th><label for="cf_uid">Stream UID</label></th>
                    <td>
                        <input type="text" 
                               id="cf_uid" 
                               name="cf_uid" 
                               value="<?php echo esc_attr($options['cf_uid']); ?>" 
                               class="regular-text">
                    </td>
                </tr>
                <tr>
                    <th><label for="cf_customer_code">Customer Code</label></th>
                    <td>
                        <input type="text" 
                               id="cf_customer_code" 
                               name="cf_customer_code" 
                               value="<?php echo esc_attr($options['cf_customer_code']); ?>" 
                               class="regular-text">
                    </td>
                </tr>
            </table>
            
            <h2>Stream Controls</h2>
            <table class="form-table">
                <tr>
                    <th>Enable Stream</th>
                    <td>
                        <label>
                            <input type="checkbox" 
                                   name="stream_enabled" 
                                   <?php checked($options['stream_enabled']); ?>>
                            Show live stream (when unchecked, shows fallback video)
                        </label>
                    </td>
                </tr>
                <tr>
                    <th>Enable Office Hours</th>
                    <td>
                        <label>
                            <input type="checkbox" 
                                   name="office_hours_enabled" 
                                   <?php checked($options['office_hours_enabled']); ?>>
                            Automatically show after-hours video outside 9am-5pm LA time
                        </label>
                    </td>
                </tr>
                <tr>
                    <th>Fallback Video</th>
                    <td>
                        <label>
                            <input type="radio" 
                                   name="fallback_video" 
                                   value="lunch" 
                                   <?php checked($options['fallback_video'], 'lunch'); ?>>
                            Lunch Break
                        </label><br>
                        <label>
                            <input type="radio" 
                                   name="fallback_video" 
                                   value="technical" 
                                   <?php checked($options['fallback_video'], 'technical'); ?>>
                            Technical Difficulties
                        </label>
                    </td>
                </tr>
            </table>
            
            <h2>Vimeo Fallback Videos</h2>
            <table class="form-table">
                <tr>
                    <th><label for="vimeo_after_hours">After Hours Video ID</label></th>
                    <td>
                        <input type="text" 
                               id="vimeo_after_hours" 
                               name="vimeo_after_hours" 
                               value="<?php echo esc_attr($options['vimeo_after_hours']); ?>" 
                               class="regular-text">
                        <p class="description">Shown outside office hours (9am-5pm LA time)</p>
                    </td>
                </tr>
                <tr>
                    <th><label for="vimeo_lunch">Lunch Break Video ID</label></th>
                    <td>
                        <input type="text" 
                               id="vimeo_lunch" 
                               name="vimeo_lunch" 
                               value="<?php echo esc_attr($options['vimeo_lunch']); ?>" 
                               class="regular-text">
                    </td>
                </tr>
                <tr>
                    <th><label for="vimeo_technical">Technical Difficulties Video ID</label></th>
                    <td>
                        <input type="text" 
                               id="vimeo_technical" 
                               name="vimeo_technical" 
                               value="<?php echo esc_attr($options['vimeo_technical']); ?>" 
                               class="regular-text">
                    </td>
                </tr>
            </table>
            
            <?php submit_button('Save Settings'); ?>
        </form>
    </div>
    <?php
}

UX considerations:

  • Grouped settings by category (Cloudflare, Controls, Vimeo)
  • Clear labels and descriptions
  • Standard WordPress form styling
  • Helper text for each input
  • Visual hierarchy with headings

The Result

For Content Managers

A simple admin interface where they can:

  • Toggle the live stream on/off instantly
  • Set office hours automation
  • Choose fallback scenarios
  • Configure all video IDs in one place

No code. No SSH. Just checkboxes.

For Developers

Clean separation of concerns:

  • PHP handles routing logic and admin UI
  • JavaScript handles Cloudflare HLS playback (from our previous Barba.js integration)
  • CSS handles responsive presentation
  • Vimeo handles fallback video playback

Each layer does one thing well.

For End Users

Seamless video delivery with no manual intervention. The system automatically shows the right video based on time and operational status.

They never see:

  • Empty office streams at night
  • Error messages during technical issues
  • Awkward “stream will return” messages

They always see:

  • Appropriate content for the time of day
  • Professional messaging during breaks
  • Working video (never a broken player)

Key Takeaways

1. Progressive Enhancement

We kept the original Cloudflare Stream functionality intact while adding new capabilities. Existing shortcodes still work—nothing broke.

Migration path:

  • Old shortcode:
    → still works
  • New features: opt-in via admin panel
  • No database migrations needed

2. Priority-Based Logic

Clear decision trees prevent conflicts. Most restrictive conditions are evaluated first.

The rule: If it’s outside office hours, that wins. If the stream is disabled, that wins next. Otherwise, show the live stream.

3. Admin-Friendly Controls

Non-technical users can manage video routing through WordPress admin without touching code.

The test: Can someone who’s never seen PHP configure this? Yes.

4. Timezone Awareness

Always use explicit timezone objects when time-based routing is critical.

The mistake: Using server time when your office is in a different timezone. Don’t do this.

5. Platform Flexibility

Don’t lock yourself into one video platform. Design for multiple providers from the start.

The benefit: Cloudflare for live streaming, Vimeo for polished fallbacks. Each does what it’s best at.

Real-World Usage

Here’s how it actually works in practice:

Monday 10am (office hours, stream enabled):

User loads page → Check office hours (true) → Check stream toggle (true) → Show Cloudflare live stream

Monday 11pm (outside office hours):

User loads page → Check office hours (false) → Show Vimeo after-hours video

Monday 12:30pm (office hours, but stream disabled for lunch):

User loads page → Check office hours (true) → Check stream toggle (false) → Show Vimeo lunch video

Monday 2pm (office hours, technical difficulties):

Admin unchecks "Enable Stream" → Selects "Technical Difficulties" → User sees Vimeo technical video

No page refreshes needed. The decision happens server-side on every page load.

What’s Next?

Future enhancements could include:

Custom office hours – Not just 9-5, configure specific times

Multiple timezone support – Offices in different cities

Scheduled maintenance windows – Plan downtime in advance

Analytics tracking per video type – Which videos are people seeing most?

Preview mode before saving changes – See what users will see

More fallback video options – Weekend video, holiday video, etc.

Stream health monitoring – Automatic fallback if stream goes down

Webhook integration – Trigger stream toggles from external systems

Common Customizations

Change Office Hours

Edit the is_office_hours() method:

private function is_office_hours() {
    $la_time = new DateTime('now', new DateTimeZone('America/Los_Angeles'));
    $hour = (int) $la_time->format('G');
    $day_of_week = (int) $la_time->format('N');
    
    // Change to 8am-6pm
    return ($day_of_week >= 1 && $day_of_week <= 5)
        && ($hour >= 8 && $hour < 18);
}

Add a Third Fallback Option

Extend the settings array and admin form:

// Add to default settings
'fallback_video' => 'lunch', // Change to support 'lunch', 'technical', 'weekend'
'vimeo_weekend' => '',

// Update the routing logic
if ($options['fallback_video'] === 'lunch') {
    $video_id = $options['vimeo_lunch'];
} elseif ($options['fallback_video'] === 'weekend') {
    $video_id = $options['vimeo_weekend'];
} else {
    $video_id = $options['vimeo_technical'];
}

Support Multiple Timezones

Make timezone configurable:

// Add to settings
'office_timezone' => 'America/Los_Angeles',

// Update the check
private function is_office_hours() {
    $options = get_option('cf_stream_settings', $this->get_default_settings());
    $office_time = new DateTime('now', new DateTimeZone($options['office_timezone']));
    // ... rest of logic
}

Conclusion

What started as a basic video player evolved into an intelligent content delivery system. By adding administrative controls, time-based logic, and multi-platform support, we created a tool that adapts to business needs automatically while remaining simple for content managers to control.

The best plugins solve real problems with minimal complexity.

This one does exactly that—smart video routing with a few checkboxes and text fields.

The architecture is extensible, the code is maintainable, and the user experience is seamless. That’s the goal.

Want to implement something similar?

The key is starting with your core requirements and building features that serve actual workflow needs, not just technical possibilities.

Ask yourself:

  • What decisions do humans make repeatedly?
  • Can the system make those decisions automatically?
  • When automation isn’t right, what’s the simplest manual control?

Build that. Ship it. Iterate.

← Back to main build journey

💬