Multichannel view, an alternative approach

I just tried this on a $100 Beelink with about 50% CPU load using h264_vaapi combining 4 1280x720 tiles, just like the Mac version.

Hardware used:

Intel(R) Celeron(R) J4125 CPU @ 2.00GHz / UHD Graphics 600

The code https://pastebin.com/HD5WGTZe

2 Likes

@hal9000 Any chance of adding a small channel number overlay to each stream in the mosaic? Also, could a more descriptive name be attached to each audio track . I'm seeing Track 1, Track 2, Track 3 and English.

If a channel number overlay can be added, then that number would be a good reference. Something like Track 6199, or failing that, maybe Track Upper Left and so on?

1 Like

I don't think audio tracks can be labelled with random labels. Added overlay 'CH 6088' in upper left corner.

https://pastebin.com/sEvcPa9Q

2 Likes

Since not all arm64 users are running Apple hardware, I built bnhf/multiview:latest (aka bnhf/multiview:2025.07.02) for both amd64 and arm64. Tested on my Mac-Mini-M2 and it runs fine with the software codec. CPU usage was mostly in the 400-500% range (so 4-5 cores).

Still best for Apple users to install this directly (with the hardware codec) for regular use, but for occasional multiview use on Apple, or for arm64 use on other platforms (with reasonably powerful processors) the container works well.

This latest build, pushed today, also includes the channel number overlay on each tile in the mosaic. Thanks @hal9000!

I tried the one-click but got the following error:

exit status 1


+ dvr=192.168.150.242:8089
++ basename /config/multichannelview.sh
+ extension=multichannelview.sh
+ extension=multichannelview
+ cp /config/multichannelview.env /tmp
+ envFile=/tmp/multichannelview.env
+ [[ -n 192.168.150.242 ]]
+ extensionURL=192.168.150.242:5002
+ [[ #1 == \# ]]
+ cdvrStartingChannel='#1'
+ [[ -n #1 ]]
+ cdvrIgnoreM3UNumbers=ignore
+ ch1=6001
+ ch2=18423
+ ch3=18424
+ ch4=18421
+ curl -s -o /dev/null http://192.168.150.242:5002
+ envVars=("TAG=$2" "DEVICES=$3" "HOST_PORT=$4" "CDVR_HOST=${dvr%%:*}" "CDVR_PORT=${dvr##*:}" "CODEC=$5")
+ printf '%s\n' TAG=latest DEVICES=# HOST_PORT=5002 CDVR_HOST=192.168.150.242 CDVR_PORT=8089 CODEC=libx264
+ sed -i /=#/d /tmp/multichannelview.env
+ /config/portainerstack.sh multichannelview
+ stackName=multichannelview
+ portainerHost=192.168.150.242
+ portainerToken=ptr_TP3q1aaHghJfhw94AMoa6ulP04EI93wfbogZlsKSb3c=
+ [[ -n 9443 ]]
+ portainerPort=9443
+ yamlCopied=
++ curl -s -k -H 'X-API-Key: ptr_TP3q1aaHghJfhw94AMoa6ulP04EI93wfbogZlsKSb3c=' http://192.168.150.242:9000/api/endpoints
++ jq '.[] | select(.Name=="local") | .Id'
jq: error (at <stdin>:1): Cannot index string with string "Name"
+ portainerEnv=
+ curl -s -o /dev/null http://192.168.150.242:9000
+ portainerURL='http://192.168.150.242:9000/api/stacks/create/standalone/string?endpointId='
+ [[ '' != \t\r\u\e ]]
+ cp /config/multichannelview.yaml /tmp
+ stackFile=/tmp/multichannelview.yaml
+ envFile=/tmp/multichannelview.env
+ dirsFile=/tmp/multichannelview.dirs
++ grep DVR_SHARE= /tmp/multichannelview.env
++ grep -v /
++ awk -F= '{print $2}'
+ dockerVolume=
++ grep VOL_EXTERNAL= /tmp/multichannelview.env
++ grep -v '#'
++ awk -F= '{print $2}'
+ volumeExternal=
++ grep VOL_NAME= /tmp/multichannelview.env
++ grep -v '#'
++ awk -F= '{print $2}'
+ volumeName=
++ grep NETWORK_MODE= /tmp/multichannelview.env
++ grep -v '#'
++ awk -F= '{print $2}'
+ networkMode=
++ grep DEVICES= /tmp/multichannelview.env
++ grep -v '#'
++ awk -F= '{print $2}'
+ transcoderDevice=
++ grep CDVR_CONTAINER= /tmp/multichannelview.env
++ grep -v '#'
++ awk -F= '{print $2}'
+ stackNumber=
+ [[ -n '' ]]
+ [[ -n '' ]]
+ [[ -n '' ]]
+ [[ -n '' ]]
++ sed 's/\\/\\\\/g' /tmp/multichannelview.yaml
++ sed 's/"/\\"/g'
++ awk '{printf "%s\\n", $0}'
+ stackContent='version: '\''3.9'\''\nservices:\n  multichannelview:\n    # 2025.07.01\n    # Github home for this project: https://github.com/bnhf/multichannelview.\n    # Docker container home for this project: https://hub.docker.com/r/bnhf/multichannelview.\n    image: bnhf/multichannelview:${TAG} # Add the tag like latest or test to the environment variables below.\n    container_name: multichannelview\n    #devices:\n      #- /dev/dri:/dev/dri # Uncomment this and the line above to use Intel Quicksync hardware acceleration. Plus use h26_qsv as the codec.\n    ports:\n      - ${HOST_PORT}:5001 # Use the same port number the container is using, or optionally change it if the port is already in use on your host.\n    environment:\n      - CDVR_HOST=${CDVR_HOST} # Hostname/IP of Channels DVR server.\n      - CDVR_PORT=${CDVR_PORT} # Port of Channels DVR server.\n      - CODEC=${CODEC} # Use h264_qsv (hardware) or libx264 (software).\n    restart: unless-stopped\n'
+ stackEnvVars='['
+ IFS=
+ read -r line
+ key=TAG
+ value=latest
+ stackEnvVars='[{"name": "TAG", "value": "latest"},'
+ IFS=
+ read -r line
+ key=HOST_PORT
+ value=5002
+ stackEnvVars='[{"name": "TAG", "value": "latest"},{"name": "HOST_PORT", "value": "5002"},'
+ IFS=
+ read -r line
+ key=CDVR_HOST
+ value=192.168.150.242
+ stackEnvVars='[{"name": "TAG", "value": "latest"},{"name": "HOST_PORT", "value": "5002"},{"name": "CDVR_HOST", "value": "192.168.150.242"},'
+ IFS=
+ read -r line
+ key=CDVR_PORT
+ value=8089
+ stackEnvVars='[{"name": "TAG", "value": "latest"},{"name": "HOST_PORT", "value": "5002"},{"name": "CDVR_HOST", "value": "192.168.150.242"},{"name": "CDVR_PORT", "value": "8089"},'
+ IFS=
+ read -r line
+ key=CODEC
+ value=libx264
+ stackEnvVars='[{"name": "TAG", "value": "latest"},{"name": "HOST_PORT", "value": "5002"},{"name": "CDVR_HOST", "value": "192.168.150.242"},{"name": "CDVR_PORT", "value": "8089"},{"name": "CODEC", "value": "libx264"},'
+ IFS=
+ read -r line
+ stackEnvVars='[{"name": "TAG", "value": "latest"},{"name": "HOST_PORT", "value": "5002"},{"name": "CDVR_HOST", "value": "192.168.150.242"},{"name": "CDVR_PORT", "value": "8089"},{"name": "CODEC", "value": "libx264"}]'
++ cat
+ stackJSON='{
  "Name": "multichannelview",
  "SwarmID": "",
  "StackFileContent": "version: '\''3.9'\''\nservices:\n  multichannelview:\n    # 2025.07.01\n    # Github home for this project: https://github.com/bnhf/multichannelview.\n    # Docker container home for this project: https://hub.docker.com/r/bnhf/multichannelview.\n    image: bnhf/multichannelview:${TAG} # Add the tag like latest or test to the environment variables below.\n    container_name: multichannelview\n    #devices:\n      #- /dev/dri:/dev/dri # Uncomment this and the line above to use Intel Quicksync hardware acceleration. Plus use h26_qsv as the codec.\n    ports:\n      - ${HOST_PORT}:5001 # Use the same port number the container is using, or optionally change it if the port is already in use on your host.\n    environment:\n      - CDVR_HOST=${CDVR_HOST} # Hostname/IP of Channels DVR server.\n      - CDVR_PORT=${CDVR_PORT} # Port of Channels DVR server.\n      - CODEC=${CODEC} # Use h264_qsv (hardware) or libx264 (software).\n    restart: unless-stopped\n",
  "Env": [{"name": "TAG", "value": "latest"},{"name": "HOST_PORT", "value": "5002"},{"name": "CDVR_HOST", "value": "192.168.150.242"},{"name": "CDVR_PORT", "value": "8089"},{"name": "CODEC", "value": "libx264"}]
}'
+ echo 'JSON response from http://192.168.150.242:9000/api/stacks/create/standalone/string?endpointId=:'
++ curl -k -X POST -H 'Content-Type: application/json' -H 'X-API-Key: ptr_TP3q1aaHghJfhw94AMoa6ulP04EI93wfbogZlsKSb3c=' -d '{
  "Name": "multichannelview",
  "SwarmID": "",
  "StackFileContent": "version: '\''3.9'\''\nservices:\n  multichannelview:\n    # 2025.07.01\n    # Github home for this project: https://github.com/bnhf/multichannelview.\n    # Docker container home for this project: https://hub.docker.com/r/bnhf/multichannelview.\n    image: bnhf/multichannelview:${TAG} # Add the tag like latest or test to the environment variables below.\n    container_name: multichannelview\n    #devices:\n      #- /dev/dri:/dev/dri # Uncomment this and the line above to use Intel Quicksync hardware acceleration. Plus use h26_qsv as the codec.\n    ports:\n      - ${HOST_PORT}:5001 # Use the same port number the container is using, or optionally change it if the port is already in use on your host.\n    environment:\n      - CDVR_HOST=${CDVR_HOST} # Hostname/IP of Channels DVR server.\n      - CDVR_PORT=${CDVR_PORT} # Port of Channels DVR server.\n      - CODEC=${CODEC} # Use h264_qsv (hardware) or libx264 (software).\n    restart: unless-stopped\n",
  "Env": [{"name": "TAG", "value": "latest"},{"name": "HOST_PORT", "value": "5002"},{"name": "CDVR_HOST", "value": "192.168.150.242"},{"name": "CDVR_PORT", "value": "8089"},{"name": "CODEC", "value": "libx264"}]
}' 'http://192.168.150.242:9000/api/stacks/create/standalone/string?endpointId='
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed

  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100  1263  100    19  100  1244  19000  1214k --:--:-- --:--:-- --:--:-- 1233k
+ portainerResponse='404 page not found'
+ [[ -z 404 page not found ]]
+ echo 404 page not found
+ echo '404 page not found'
+ jq -e '.Id != null'
jq: error (at <stdin>:1): Cannot index number with string "Id"
parse error: Invalid numeric literal at line 1, column 9
+ exit 1
+ [[ 1 == 1 ]]
+ exit 1

This should either a # for CDVR to choose the channel number, or a 1 if you want to start numbering multiview channels at 1 -- not both combined.

This is the bigger problem though, as there's something up with your OliveTin installation. Please run the OliveTin Post-Install Healthcheck, and post the results in the Project One-Click thread:

1 Like

@hal9000 This app is working well with Quicksync on my 10th gen i7, however it fails on a 12th gen i3 and a 14th gen i9, both with the same error:

[h264_qsv @ 0x60177c396340] Error initializing an internal MFX session: unsupported (-3)

Error initializing output stream 0:4 -- Error while opening encoder for output stream #0:4 - maybe incorrect parameters such as bit_rate, rate, width or height

172.19.0.1 - - [02/Jul/2025 21:20:34] "GET /combine?ch=6142&ch=6144&ch=6147&ch=6199 HTTP/1.1" 200 -

The Intel UHD architecture changed with gen 12, so I believe this may be the cause. Any thoughts on whether this can be addressed through the script?

I didn't do anything with quicksync, it was your thing. Have you tried vaapi from post #9?

Chatgpt suggests trying this script https://pastebin.com/45kqyNCH to resolve any quicksync issues.

Different error with the h264_vaapi codec:

Impossible to convert between the formats supported by the filter 'Parsed_xstack_12' and the filter 'auto_scale_0'

Error reinitializing filters!

Failed to inject frame into filter network: Function not implemented

Error while processing the decoded data for stream #3:0

172.19.0.1 - - [02/Jul/2025 23:52:11] "GET /combine?ch=6142&ch=6144&ch=6147&ch=6199 HTTP/1.1" 200 -

Am I interpreting this correctly, that you're not interested in getting this to work with various generations of Intel GPUs? I figured since you tested this on a low end Intel processor, you had some motivation, but if not -- no worries.

I've been trying to get quick sync to work on Linux with a 12th gen CPU all day and nothing seems to work. Windows does not seem to have this issue. I am starting to think it is a issue with the Linux driver. Somewhere between ffmpeg and the VA-API library interaction is not working in newer Intel.

On Windows it works like a champ.

We are duplicating the work already done by the CDVR team. They can use hardware encoding on newer Intels CPUs. I would just take a look at how ffmpeg is invoked during encoding and just copy the important parameters.

Could it be as simple as just using their custom version of ffmpeg? I already download that when building OliveTin, so I'll give it a try.

I don't think it is a simple fix as this is a more complex process than usual. VA-API is having to do a back and forth with the video between the CPU and GPU. I could be wrong, but I think this is where the issue is happening at. It's well above my pay grade though.

I doubt anything special is being done with h264 in the special version of ffmpeg. CDVR is probably maintaining a list of parameters for each gen of CPU. Just start an encoding session from the client and see how ffmpeg is invoked. I used to drop a script in place to log all the parameters it gets called with and then call the real ffmpeg.

#!/bin/bash
LOGFILE="/tmp/ffmpeg_wrapper.log"
REAL_FFMPEG="$0.org"

echo "[$(date)] $0 $@" >> "$LOGFILE"

exec "$REAL_FFMPEG" "$@"

I made a list of what I tried and why I think I failed. Hopefully this will help.

Attempt 1: Enable Hardware Encoding
My first attempt was to simply tell ffmpeg to use the VA-API hardware encoder for the final output.

The idea was that ffmpeg would perform all the software filtering (scaling, stacking) on the CPU and then hand off the final, composited video to the GPU for encoding.

Why It Failed: This failed with the error A hardware device reference is required to upload frames to. This meant that just selecting the h264_vaapi encoder wasn't enough, the hwupload filter needed to know which specific hardware device to use.

Attempt 2: Initialize the Hardware Device
To fix the "missing hardware device" error, I added global flags to the beginning of the ffmpeg command to initialize the VA-API device.

I added -hwaccel vaapi, -hwaccel_device /dev/dri/renderD128, and -hwaccel_output_format vaapi to the start of the ffmpeg command. I was trying to tell ffmpeg to use the GPU for hardware acceleration from the very beginning.

Why It Failed: This created the opposite problem, with the error Impossible to convert between the formats supported by the filter 'Parsed_fps_0' and the filter 'auto_scale_0'. By initializing the hardware at the start, ffmpeg decoded the incoming streams on the GPU. The video frames were now "stuck" on the GPU in a hardware-specific format that the software-based fps filter couldn't understand.

Attempt 3: The Full CPU-to-GPU Pipeline
To resolve the format conflict, I tried to create a full, explicit pipeline to manage the data flow between the CPU and GPU.

I kept the initial -hwaccel flags to decode on the GPU, and then in the complex filter, I added a hwdownload filter for each input stream. This was supposed to move the decoded video from the GPU back to the CPU's memory before the software filters were applied.

Why It Failed: This is where I hit the core of the problem. The process of decoding on the GPU, downloading to the CPU for filtering, and then uploading back to the GPU for encoding is complex. This failed with the error Impossible to convert between the formats supported by the filter 'graph 0 input from stream 1:1' and the filter 'auto_scale_0', indicating that the hand-off between the hardware decoder and the hwdownload filter was failing due to an incompatible pixel format.

I'm gonna try oneVPL tomorrow and see if it works better.

I've pushed an update, bnhf/olivetin:latest (aka bnhf/olivetin:2025.07.03), primarily aimed at improving the initial Custom Channel setup for this project in CDVR-Sources -- when spun-up through Project One-Click.

For example, this set of values:

adds a Multichannel View stack to Portainer, and creates and initial Custom Channel Source M3U:

with EPG data that looks like this:

Also, for @chDVRuser, this is the first test of having comments duplicated with the env vars in Portainer. If you're willing, give it a try, and let me know what you think back in the Project One-Click thread:

This is what I was able to capture when running Transcoder Probe from Advanced settings and choosing hardware transcoder and hardware deinterlacer on J4125


[Thu Jul  3 08:34:08 PM PDT 2025] /home/channels/channels-dvr/2025.07.03.0419/ffmpeg -hide_banner -nostats -color_primaries bt2020 -color_trc smpte2084 -colorspace bt2020nc -progress pipe:3 -loglevel verbose -f lavfi -t 1 -i testsrc=size=1280x720:rate=60,format=nv12,colorspace=all=bt2020:iall=bt2020:fast=0 -c:v h264_vaapi -profile:v high -level 42 -b:v 4000k -maxrate:v 6000k -init_hw_device vaapi=intel:/dev/dri/renderD128 -filter_hw_device intel -vf format=nv12,hwupload -f null -y /dev/null
[Thu Jul  3 08:34:08 PM PDT 2025] /home/channels/channels-dvr/2025.07.03.0419/ffmpeg -hide_banner -nostats -color_primaries bt2020 -color_trc smpte2084 -colorspace bt2020nc -progress pipe:3 -loglevel verbose -f lavfi -t 1 -i testsrc=size=1280x720:rate=60,format=nv12,colorspace=all=bt2020:iall=bt2020:fast=0 -c:v h264_vaapi -profile:v high -level 42 -b:v 4000k -maxrate:v 6000k -init_hw_device vaapi=intel:/dev/dri/renderD128 -filter_hw_device intel -vf format=nv12,hwupload,deinterlace_vaapi -f null -y /dev/null
[Thu Jul  3 08:34:09 PM PDT 2025] /home/channels/channels-dvr/2025.07.03.0419/ffmpeg -hide_banner -nostats -color_primaries bt2020 -color_trc smpte2084 -colorspace bt2020nc -progress pipe:3 -loglevel verbose -f lavfi -t 1 -i testsrc=size=1280x720:rate=60,format=nv12,colorspace=all=bt2020:iall=bt2020:fast=0 -c:v h264_vaapi -profile:v high -level 42 -b:v 4000k -maxrate:v 6000k -init_hw_device vaapi=intel:/dev/dri/card0 -filter_hw_device intel -vf format=nv12,hwupload -f null -y /dev/null
[Thu Jul  3 08:34:10 PM PDT 2025] /home/channels/channels-dvr/2025.07.03.0419/ffmpeg -hide_banner -nostats -color_primaries bt2020 -color_trc smpte2084 -colorspace bt2020nc -progress pipe:3 -loglevel verbose -f lavfi -t 1 -i testsrc=size=1280x720:rate=60,format=nv12,colorspace=all=bt2020:iall=bt2020:fast=0 -c:v h264_vaapi -profile:v high -level 42 -b:v 4000k -maxrate:v 6000k -init_hw_device vaapi=intel:/dev/dri/card0 -filter_hw_device intel -vf format=nv12,hwupload,deinterlace_vaapi -f null -y /dev/null

The first run can be simplified as

ffmpeg \
  -vaapi_device /dev/dri/renderD128 \
  -f lavfi -t 1 -i "testsrc=size=1280x720:rate=60,format=nv12,colorspace=all=bt2020:iall=bt2020:fast=0" \
  -vf format=nv12,hwupload \
  -c:v h264_vaapi -profile:v high -level 4.2 -b:v 4000k -maxrate 6000k \
  -color_primaries bt2020 -color_trc smpte2084 -colorspace bt2020nc \
  -f null -y /dev/null

Would something like this work in ffmpeg? Or does it just not show up in Channels?

-metadata:s:a:0 title="Channel 6608"

I tried -metadata:s:a:0 title="Audio CH 101" but even VLC wouldn't show it.

I believe audio labels are based on 3-letter language codes