Skip to content

HLS Video+Audio Muxing Guide

Combine a looping video background with live streaming audio into a single HLS stream for Roku playback.


Why HLS Muxing?

Roku devices have a platform limitation: only one media decoder instance can be active at a time. This means you can't play a video file and a separate audio stream simultaneously—you'll get an error like:

"player: only one playing instance supported"

The Solution: Combine (mux) your video and audio into a single HLS stream on the server side. Roku plays this as one unified stream—video and audio perfectly synchronized.


Architecture Overview

┌─────────────────┐     ┌─────────────────┐
│  Video Loop     │────▶│                 │
│  (MP4 file)     │     │   Muxing        │     ┌─────────────────┐
└─────────────────┘     │   Server        │────▶│   Roku App      │
                        │   (Restreamer)  │     │  (Single HLS)   │
┌─────────────────┐     │                 │     └─────────────────┘
│  Audio Stream   │────▶│                 │
│  (Icecast/MP3)  │     └─────────────────┘
└─────────────────┘

Prerequisites

Before you begin, you'll need:

  • A video file: MP4, H.264 encoded, ideally with no audio track (video-only)
  • An audio stream: Icecast, Shoutcast, or any HTTP audio stream (MP3, AAC)
  • A server: To run the muxing software (Docker recommended)
  • HTTPS: SSL certificates for your muxing server (or Cloudflare Tunnel)

Creating a Video-Only File

Use FFmpeg to strip audio from your video:

ffmpeg -i input_video.mp4 -an -c:v copy output_video_only.mp4


Restreamer is an open-source, Docker-based streaming server with a web UI. It's the easiest way to get started.

Step 1: Deploy Restreamer

Add to your docker-compose.yml:

services:
  restreamer:
    image: datarhei/restreamer:latest
    container_name: restreamer
    restart: unless-stopped
    ports:
      - "8282:8080"      # Web UI
      - "1935:1935"      # RTMP
      - "1936:1936"      # RTMPS  
      - "6000:6000/udp"  # SRT
    volumes:
      - restreamer_config:/core/config
      - restreamer_data:/core/data
    environment:
      - RS_API_AUTH_ENABLE=true
      - RS_API_AUTH_USERNAME=admin
      - RS_API_AUTH_PASSWORD=your_secure_password
      - RS_API_AUTH_JWT_SECRET=generate_random_string_here

volumes:
  restreamer_config:
  restreamer_data:

Start the container:

docker-compose up -d restreamer

Step 2: Access the Web UI

Navigate to http://your-server:8282 and log in with your credentials.

Step 3: Create a Channel

  1. Click "+" to create a new channel
  2. Give it a descriptive name (e.g., "Radio Visualizer")

Step 4: Configure Video Source

Setting Value
Source Type Loop
Upload Upload your video file (or paste a URL)
Encoding Passthrough (copy)
Filter None

Use Passthrough

Always select Passthrough (copy) for encoding. This prevents re-encoding and preserves quality while minimizing CPU usage.

Step 5: Configure Audio Source

Setting Value
Source Type Network source
URL Your Icecast/Shoutcast stream URL
Encoding Passthrough (copy)
Filter None

Example audio URLs:

https://your-icecast-server.com:8000/stream
https://your-azuracast.com/listen/station/stream.mp3

Step 6: Start the Channel

Click Start and Restreamer will begin muxing your video and audio into an HLS stream.

Step 7: Get Your HLS URL

Your muxed stream URL will be:

https://your-restreamer-server/memfs/{channel-uuid}.m3u8

Find the exact URL in the Restreamer UI under the channel's output settings.


Option 2: Liquidsoap + Nginx

For more control and integration with existing Liquidsoap infrastructure, you can generate HLS directly from Liquidsoap.

Liquidsoap Configuration

#!/usr/bin/liquidsoap

set("frame.audio.samplerate", 48000)

# Your audio source
radio = mksafe(input.jack(id="liquidsoap"))

# HLS output directory
output.file.hls(
    playlist = "stream.m3u8",
    segment_duration = 6.0,
    segments = 5,
    "/var/www/hls",
    %mp3(bitrate=320),
    radio
)

Nginx Configuration

server {
    listen 8080;

    location /hls {
        alias /var/www/hls;

        # CORS headers
        add_header Access-Control-Allow-Origin *;
        add_header Cache-Control no-cache;

        types {
            application/vnd.apple.mpegurl m3u8;
            video/mp2t ts;
        }
    }
}

Creating the Master Manifest

To combine video with the HLS audio, create a master manifest:

#EXTM3U
#EXT-X-VERSION:4

#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="Live",DEFAULT=YES,URI="audio/stream.m3u8"

#EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=1280x720,AUDIO="audio"
video/playlist.m3u8

Option 3: Apache (Alternative to Nginx)

If you prefer Apache:

Listen 8080

<VirtualHost *:8080>
    DocumentRoot /var/www

    Alias /hls /var/www/hls

    <Directory /var/www/hls>
        Header set Access-Control-Allow-Origin "*"
        Header set Cache-Control "no-cache"
    </Directory>

    <FilesMatch "\.m3u8$">
        ForceType application/vnd.apple.mpegurl
    </FilesMatch>

    <FilesMatch "\.ts$">
        ForceType video/mp2t
    </FilesMatch>
</VirtualHost>

CastConductor Configuration

Once your muxed HLS stream is running, configure CastConductor to use it.

Scene Background Settings

  1. Go to CastConductor → Scene Manager
  2. Select your scene
  3. Click the Appearance tab
  4. Set Background Type to Video
  5. Enter your HLS URL:
    https://mux.yourdomain.com/memfs/{channel-uuid}.m3u8
    
  6. Enable Loop (handled by the muxing server)
  7. Uncheck Muted ← Critical! Audio is in the stream!

Don't Forget: Muted = OFF

Since the audio is now part of the video stream, you must uncheck the Muted option. If muted is enabled, you'll have video but no audio.

What happens if your muxing server goes down? Without configuration, viewers would see a static fallback image but hear silence.

CastConductor v5.7.45+ supports smart fallback: configure both a muxed HLS stream AND a fallback audio URL. The app will:

  1. Play the muxed HLS video (with embedded audio) as primary
  2. If the muxed stream fails → show fallback image AND start the fallback audio stream
  3. Viewers never experience silence!

To configure fallback audio:

  1. Go to CastConductor → Settings
  2. Set Stream URL to your Icecast/Shoutcast fallback:
    https://your-icecast-server.com:8000/stream
    
  3. Set Playback Mode to any mode (the app is smart enough to figure it out)

How the Smart Fallback Works

When video is unmuted (muxed HLS mode), the Roku app:

  • ✅ Plays muxed HLS video (audio comes from video stream)
  • ✅ Keeps the fallback audio URL ready but doesn't start it
  • ✅ If video fails → starts fallback audio automatically

This means you can safely configure both without conflicts!

Configuration Summary

Setting Location Value
Background Type Scene Manager → Appearance Video
Video URL Scene Manager → Appearance https://mux.yourdomain.com/memfs/{uuid}.m3u8
Muted Scene Manager → Appearance OFF (unchecked)
Stream URL Settings Your Icecast/Shoutcast fallback URL
Playback Mode Settings Any (app auto-detects muxed mode)

Playback Mode Notes

Mode Behavior with Muxed HLS
Standard ✅ Plays muxed stream, audio from video
Video Only ✅ Same behavior
Dual Source ✅ App detects unmuted video and skips separate audio
Audio Only ⚠️ No video, only plays fallback audio

Dual Source Mode is Safe

Even if you select "Dual Source (Video Muted + Audio)", the app is smart enough to detect that the video is unmuted and will NOT try to start a separate audio stream. The Playback Mode setting is primarily for non-muxed configurations.


Verifying Your Stream

Check the HLS Manifest

curl -s "https://mux.yourdomain.com/memfs/{channel-uuid}.m3u8"

Expected output:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=3480505,RESOLUTION=1280x720,CODECS="avc1.4d001f,mp4a.40.34"
{channel-uuid}_output_0.m3u8

Verify Audio Codec

The CODECS field tells you what's in the stream:

Codec Meaning
avc1.4d001f H.264 video (Main Profile)
mp4a.40.34 MP3 audio (passthrough)
mp4a.40.2 AAC audio (transcoded)

MP3 Passthrough = No Quality Loss

If you see mp4a.40.34, your audio is passing through without re-encoding. This means zero quality loss from your original stream!

Test in VLC

Before testing on Roku, verify the stream works in VLC:

  1. Open VLC
  2. Media → Open Network Stream
  3. Paste your HLS URL
  4. Confirm video and audio play correctly

Troubleshooting

No Audio on Roku

  1. Check Muted setting - Ensure "Muted" is OFF in Scene Manager
  2. Verify stream has audio - Test in VLC first
  3. Check manifest - Look for audio codec in CODECS field
  4. Rebuild Roku app - If you just changed the muted setting, rebuild

Video Immediately Falls Back to Static Image

Symptom: Video starts playing, then immediately switches to static background image with audio.

Cause: You have both a muxed HLS video AND a Stream URL configured, but you're running a Roku app version older than v5.7.45.

Solution: Update to v5.7.45+ which has smart muxed HLS detection. The app will: - Play muxed HLS video (with embedded audio) - Skip starting the separate audio stream - Only start fallback audio if video actually fails

Silence When Muxing Server Fails

Symptom: Muxing server goes down, video falls back to static image, but no audio plays.

Cause: No fallback audio URL configured.

Solution: Configure a Stream URL in Settings as fallback audio. With v5.7.45+, this will only be used when the muxed video fails.

Video Not Looping

  1. Confirm Restreamer source type is Loop (not File)
  2. Check the video file is valid and not corrupted
  3. Verify video has no audio track (use video-only file)

High CPU Usage

  1. Ensure encoding is set to Passthrough (copy)
  2. Transcoding uses significantly more CPU
  3. Consider a more powerful server if transcoding is required

Stream Latency

HLS inherently has 10-30 seconds of latency. To reduce:

  • Decrease HLS segment duration (but increases overhead)
  • Consider SRT input for lower-latency audio source
  • Enable Low-Latency HLS if your player supports it

Connection Refused

  1. Check firewall allows traffic on port 8282 (or your configured port)
  2. Verify Docker container is running: docker ps
  3. Check Restreamer logs: docker logs restreamer

Resource Requirements

Resource Requirement
CPU ~2-5% (no transcoding)
RAM ~256MB
Storage 512MB for HLS segment cache
Bandwidth Sum of video + audio bitrates

Transcoding Mode

Resource Requirement
CPU 1-2 cores per stream
RAM ~512MB-1GB
Storage 512MB for HLS segment cache
Bandwidth Output bitrate (configurable)

Security Considerations

Authentication

Restreamer supports authentication for both the admin UI and stream access:

environment:
  - RS_API_AUTH_ENABLE=true
  - RS_API_AUTH_USERNAME=admin
  - RS_API_AUTH_PASSWORD=strong_password_here
  - RS_STORAGE_MEMORY_AUTH_ENABLE=true
  - RS_STORAGE_MEMORY_AUTH_USERNAME=token
  - RS_STORAGE_MEMORY_AUTH_PASSWORD=stream_access_token

HTTPS

Always serve HLS over HTTPS in production:

  • Use a reverse proxy (Nginx, Caddy, Traefik)
  • Or Cloudflare Tunnel for automatic SSL

Rate Limiting

Consider implementing rate limiting if the stream is public to prevent abuse.


Example: Complete Setup

Here's a complete example configuration for a radio station with a visualizer:

Video: 13-second loop, 1280x720, H.264, no audio
Audio: Icecast MP3 stream, 320kbps, 48kHz
Server: Ubuntu 22.04 with Docker

docker-compose.yml

version: '3.8'

services:
  restreamer:
    image: datarhei/restreamer:latest
    container_name: restreamer
    restart: unless-stopped
    ports:
      - "8282:8080"
      - "1935:1935"
    volumes:
      - restreamer_config:/core/config
      - restreamer_data:/core/data
    environment:
      - RS_API_AUTH_ENABLE=true
      - RS_API_AUTH_USERNAME=admin
      - RS_API_AUTH_PASSWORD=MySecurePassword123!
      - RS_API_AUTH_JWT_SECRET=random-jwt-secret-string
      - RS_FFMPEG_LOG_MAXLINES=100

volumes:
  restreamer_config:
  restreamer_data:

Cloudflare Tunnel Config

ingress:
  - hostname: mux.yourdomain.com
    service: http://localhost:8282

CastConductor Scene Settings

Setting Value
Background Type Video
Video URL https://mux.yourdomain.com/memfs/{uuid}.m3u8
Loop On
Muted Off

Result

Roku displays the looping visualizer video with synchronized live radio audio—all from a single HLS stream, no "only one playing instance" errors!


Summary

Approach Best For Complexity
Restreamer Quick setup, GUI config Low
Liquidsoap + Nginx Full control, existing Liquidsoap High
Liquidsoap + Apache Apache-based infrastructure High

All approaches achieve the same result: a unified HLS stream that Roku can play with synchronized video and audio. Choose based on your existing infrastructure and comfort level.