ComSkip - Docker

So for a while I have wanted com skip to work but

Vero 4k with TVHeadend is in one room and records to Server in another. Also didnt want the Vero 4k processing this

So the solution was Vero 4k to record to the NAS then the NAS to be triggered to process the file

In TVHeadend set the

Post Processing command
“/your network location/comskip.sh” “%f”

#!/bin/bash

# The URL of your Flask application running in the Docker container
FLASK_URL="http://192.168.0.115:5555/trigger"

# The path to the recorded file, provided by TVHeadend
FILE_PATH="$1"

# Convert the TVHeadend path to the Docker container path
# Assuming /mnt/TVHeadend/Recordings maps to /input in the Docker container
DOCKER_PATH="${FILE_PATH//mnt\/TVHeadend\/Recordings/input}"

# URL-encode the Docker path to handle special characters
# Note: Python's urllib.parse.quote function handles special characters and spaces
ENCODED_DOCKER_PATH=$(python3 -c "import urllib.parse; import sys; print(urllib.parse.quote(sys.argv[1], safe=''))" "$DOCKER_PATH")

# Debug output to verify paths
echo "FILE_PATH: $FILE_PATH"
echo "DOCKER_PATH: $DOCKER_PATH"
echo "ENCODED_DOCKER_PATH: $ENCODED_DOCKER_PATH"

# Send a POST request to the Flask application with the converted file path
curl -X POST -F "file_path=${ENCODED_DOCKER_PATH}" "${FLASK_URL}"

Remove Processing command
“/your network location/cleanup.sh” “%f”

#!/bin/bash
# Get the file path of the recording being deleted
INPUT_FILE="$1"

# Derive the paths for the associated .processed and .txt files

TXT_FILE="${INPUT_FILE%.ts}.txt"
LOG_FILE="${INPUT_FILE%.ts}.log"
VPrj_FILE="${INPUT_FILE%.ts}.VPrj"
logo_FILE="${INPUT_FILE%.ts}.logo.txt"

FOLDER_PATH=$(dirname "$INPUT_FILE")

if [ -f "$logo_FILE" ]; then
  echo "Removing $logo_FILE"
  rm "$logo_FILE"
else
  echo "$Vlogo_FILE does not exist"
fi

if [ -f "$VPrj_FILE" ]; then
  echo "Removing $VPrj_FILE"
  rm "$VPrj_FILE"
else
  echo "$VPrj_FILE does not exist"
fi

if [ -f "$LOG_FILE" ]; then
  echo "Removing $LOG_FILE"
  rm "$LOG_FILE"
else
  echo "$LOG_FILE does not exist"
fi

if [ -f "$TXT_FILE" ]; then
  echo "Removing $TXT_FILE"
  rm "$TXT_FILE"
else
  echo "$TXT_FILE does not exist"
fi

# Check if the folder is empty and remove it
if [ -d "$FOLDER_PATH" ]; then
  if [ "$(ls -A "$FOLDER_PATH")" == "" ]; then
    echo "Folder is empty, removing $FOLDER_PATH"
    rmdir "$FOLDER_PATH"
  else
    echo "Folder is not empty: $FOLDER_PATH"
  fi
else
  echo "Folder does not exist: $FOLDER_PATH"
fi


# Final log entry for the script run
echo "Cleanup complete for $INPUT_FILE"

The Dockerfile should be (this includes nvidia dont think its used) but lines can be removed

# Use a CUDA image with runtime libraries for GPU acceleration
FROM nvidia/cuda:12.0.0-base-ubuntu20.04

# Set non-interactive mode to avoid prompts during the build
ENV DEBIAN_FRONTEND=noninteractive

# Ensure all required repositories are available
RUN apt-get update && \
    apt-get install -y software-properties-common && \
    add-apt-repository universe && \
    add-apt-repository multiverse && \
    apt-get update

# Install core dependencies
RUN apt-get install -y \
    git \
    build-essential \
    autoconf \
    automake \
    libtool \
    pkg-config \
    libargtable2-dev \
    libavformat-dev \
    libavcodec-dev \
    libswscale-dev \
    && apt-get clean

# Install SDL libraries
RUN apt-get install -y \
    libsdl1.2-dev \
    libsdl2-dev \
    && apt-get clean

# Install FFmpeg and other utilities
RUN apt-get install -y \
    ffmpeg \
    inotify-tools \
    cron \
    && apt-get clean

# Install Python and Flask
RUN apt-get update && \
    apt-get install -y python3 python3-pip && \
    pip3 install Flask

# Install Nginx
RUN apt-get install -y nginx

# Clone and build Comskip
RUN git clone https://github.com/erikkaashoek/Comskip /opt/comskip && \
    cd /opt/comskip && \
    ./autogen.sh && \
    ./configure && \
    make

# Create a directory where videos will be stored
RUN mkdir /input

# Copy the monitor script into the container
COPY monitor.sh /usr/local/bin/monitor.sh

# Make the monitor script executable
RUN chmod +x /usr/local/bin/monitor.sh

# Copy Nginx configuration file
COPY nginx.conf /etc/nginx/nginx.conf

# Expose port 80 for Nginx
EXPOSE 80

# Set the default command to run the monitor script
CMD ["/usr/local/bin/monitor.sh"]

create the “monitor.sh”

#!/bin/bash

# Start Nginx in the background
service nginx start

# Start Flask in the background
python3 /usr/share/nginx/html/trigger_comskip.py &

# Define the input directory where .ts files are stored
INPUT_DIR="/input"

# Function to process .ts files with Comskip
process_files() {
  # Recursively find all .ts files in the input directory
  find "$INPUT_DIR" -type f -name "*.ts" | while read -r file; do
    # Strip leading/trailing whitespace from filenames (just in case)
    file=$(echo "$file" | xargs)
    
    # Marker file to track processing, stored next to the .ts file
    marker_file="${file%.ts}.log"
    
    # Check if the file has already been processed (i.e., marker file exists)
    if [ ! -f "$marker_file" ]; then
      echo "Processing $file with Comskip..."
      
      # Run Comskip on the .ts file
      /opt/comskip/comskip --ini=/Scripts/default.ini "$file"
      
      # Create a marker file to indicate the file has been processed
      touch "$marker_file"
    else
      echo "$file has already been processed."
    fi
  done
}

# Run the file processing once
process_files

# Keep the container running
tail -f /dev/null

Create “nginx.conf”

user www-data;
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
}

http {
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;

    types {
        text/html html htm shtml;
        text/plain txt;
        text/xml xml;
        image/gif gif;
        image/jpeg jpg jpeg;
        application/x-javascript js;
        text/css css;
    }

    server {
        listen 80 default_server;
        listen [::]:80 default_server;

        root /usr/share/nginx/html;
        index index.html;

        server_name _;

        location / {
            try_files $uri $uri/ =404;
        }

        # Proxy requests to /trigger to the Flask server running on port 5000
        location /trigger {
            proxy_pass http://localhost:5000;  # Flask app is running on port 5000
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}

I used my NAS OMV v6 to create and build the container file

services:
  comskip:
    container_name: "comskip"
    image: "comskip"  # Ensure this image is built with CUDA and Nvidia support
    network_mode: "bridge"
    restart: "always"
    labels:
      - "com.centurylinklabs.watchtower.enable=false"
    volumes:
      - Path to Recordings:/input # SKIP_BACKUP
      - Path to HTML:/usr/share/nginx/html
      - Path to Scripts:/Scripts
    deploy:
      resources:
        reservations:
          devices:
            - driver: "nvidia"
              count: all
              capabilities: [ "compute", "utility" ]
    runtime: nvidia  # Specify the Nvidia runtime
    ports:
      - "5555:80"
      - "5000:5000"

HTML file “index.html”, you can load this to manually trigger files to be processed, but on container start it checked for unprocesed files

/mnt/TVHeadend/Recordings/ can be changed to your path, this means you can copy and paste links as they appear on computer etc

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Comskip Trigger</title>
    <script>
        function adjustFilePath(event) {
            const filePathInput = document.getElementById("file_path");
            let currentValue = filePathInput.value;
            
            // Replace "/mnt/TVHeadend/Recordings/" with "/input/"
            const searchPath = "/mnt/TVHeadend/Recordings/";
            if (currentValue.startsWith(searchPath)) {
                currentValue = currentValue.replace(searchPath, "/input/");
            }
            // Add "/input" to the beginning if necessary
            else if (!currentValue.startsWith("/input")) {
                currentValue = "/input" + currentValue;
            }

            filePathInput.value = currentValue;
        }
    </script>
</head>
<body>
    <h1>Comskip Trigger</h1>
    <form action="/trigger" method="post" onsubmit="adjustFilePath(event)">
        <label for="file_path">File Path:</label>
        <input type="text" id="file_path" name="file_path" required>
        <button type="submit">Trigger Processing</button>
    </form>
</body>
</html>

this goes in same folder as html “trigger_comskip.py”

from flask import Flask, request, jsonify
import subprocess
import urllib.parse

app = Flask(__name__)

@app.route('/trigger', methods=['POST'])
def trigger_comskip():
    file_path = request.form.get('file_path')
    if not file_path:
        return jsonify({"status": "error", "message": "No file path provided"}), 400

    # Decode the file path and log it
    decoded_file_path = urllib.parse.unquote(file_path)
    app.logger.info(f"Received file path: {decoded_file_path}")

    # Construct the Comskip command
    comskip_command = [
        '/opt/comskip/comskip', 
        '--ini=/Scripts/default.ini', 
        decoded_file_path
    ]

    try:
        result = subprocess.run(comskip_command, check=True, text=True, capture_output=True)
        app.logger.info(result.stdout)
        return jsonify({"status": "success", "message": f"Processing started for {decoded_file_path}"}), 200
    except subprocess.CalledProcessError as e:
        app.logger.error(f"Error running Comskip: {e}")
        app.logger.error(e.stderr)
        return jsonify({"status": "error", "message": str(e)}), 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

in the scripts folder add “default.ini” others can be found here Tuning link

detect_method=111			;1=black frame, 2=logo, 4=scene change, 8=fuzzy logic, 16=closed captions, 32=aspect ration, 64=silence, 128=cutscenes, 255=all
validate_silence=1			; Default, set to 0 to force using this clues if selected above.
validate_uniform=1			; Default, set to 0 to force using this clues (like pure white frames) if blackframe is selected above.
validate_scenechange=1		; Default, set to 0 to force using this clues if selected above.
verbose=10				;show a lot of extra info, level 5 is also OK, set to 0 to disable
max_brightness=60      			;frame not black if any pixels checked are greater than this (scale 0 to 255)
test_brightness=40      		;frame not pure black if any pixels checked are greater than this, will check average brightness (scale 0 to 255)
max_avg_brightness=25			;maximum average brightness for a dim frame to be considered black (scale 0 to 255) 0 means autosetting
max_commercialbreak=600 		;maximum length in seconds to consider a segment a commercial break
min_commercialbreak=25			;minimum length in seconds to consider a segment a commercial break
max_commercial_size=125			;maximum time in seconds for a single commercial or multiple commercials if no breaks in between
min_commercial_size=4   		;mimimum time in seconds for a single commercial
min_show_segment_length=125 	; any segment longer than this will be scored towards show.
non_uniformity=500			; Set to 0 to disable cutpoints based on uniform frames
max_volume=500				; any frame with sound volume larger than this will not be regarded as black frame
min_silence=12				; Any deep silence longer than this amount  of frames is a possible cutpoint
ticker_tape=0				; Amount of pixels from bottom to ignore in all processing 
logo_at_bottom=0			; Set to 1 to search only for logo at the lower half of the video, do not combine with subtitle setting
punish=0					; Compare to average for sum of 1=brightness, 2=uniform 4=volume, 8=silence, 16=schange, set to 0 to disable
punish_threshold=1.3		; Multiply when amount is above average * punish_threshold
punish_modifier=2			; When above average * threshold multiply score by this value
intelligent_brightness=0 		; Set to 1 to use a USA specific algorithm to tune some of the settings, not adviced outside the USA
logo_percentile=0.92			; if more then this amount of logo is found then logo detection will be disabled
logo_threshold=0.75
punish_no_logo=1			; Default, set to 0 to avoid show segments without logo to be scored towards commercial
aggressive_logo_rejection=0
connect_blocks_with_logo=1		; set to 1 if you want successive blocks with logo on the transition to be regarded as connected, set to 0 to disable
logo_filter=0               ; set the size of the filter to apply to bad logo detection, 4 seems to be a good value.
cut_on_ar_change=1			; set to 1 if you want to cut also on aspect ratio changes when logo is present, set to 2 to force cuts on aspect ratio changes. set to 0 to disable
delete_show_after_last_commercial=0	; set to 1 if you want to delete the last block if its a show and after a commercial
delete_show_before_or_after_current=0	; set to 1 if you want to delete the previous and the next show in the recording, this can lead to the deletion of trailers of next show
delete_block_after_commercial=0	;set to max size of block in seconds to be discarded, set to 0 to disable 
remove_before=1				; amount of seconds of show to be removed before ALL commercials 0
remove_after=1				; amount of seconds of show to be removed after ALL commercials 0
shrink_logo=5				; Reduce the duration of the logo with this amount of seconds
after_logo=0		; set to number of seconds after logo disappears comskip should start to search for silence to insert an additional cutpoint
padding=0
ms_audio_delay=5
volume_slip=20
max_repair_size=200			; Will repair maximum 200 missing MPEG frames in the timeline, set to 0 to disable repairing for players that don't use PTS. 
disable_heuristics=4		bit pattern for disabling heuristics, adding 1 disables heristics 1, adding 2 disables heristics 2, adding 4 disables heristics 3, 255  disables all heuristics 
delete_logo_file=0			; set to 1 if you want comskip to tidy up after finishing
output_framearray=0			; create a big excel file for detailed analysis, set to 0 to disable
output_videoredo=1
output_womble=0
output_mls=0			; set to 1 if you want MPeg Video Wizard bookmark file output
output_cuttermaran=0
output_mpeg2schnitt=0
output_mpgtx=0
output_dvrcut=0
output_zoomplayer_chapter=0
output_zoomplayer_cutlist=0
output_edl=0
output_edlx=0
output_vcf=0
output_bsplayer=0
output_btv=0				; set to 1 if you want Beyond TV chapter cutlist output
output_projectx=0			; set to 1 if you want ProjectX cutlist output (Xcl)
output_avisynth=0
output_vdr=0				; set to 1 if you want XBMC to skipping commercials
output_demux=0				; set to 1 if you want comskip to demux the mpeg file while scanning
sage_framenumber_bug=0
sage_minute_bug=0
live_tv=0					; set to 1 if you use parallelprocessing and need the output while recording
live_tv_retries=4			; change to 16 when using live_tv in BTV, set to 120 when using on dvr-ms
standoff=0					; change to 8000000 when using live_tv in BTV
cuttermaran_options="cut=\"true\" unattended=\"true\" muxResult=\"false\" snapToCutPoints=\"true\" closeApp=\"true\""
mpeg2schnitt_options="mpeg2schnitt.exe /S /E /R25  /Z %2 %1"
avisynth_options="LoadPlugin(\"MPEG2Dec3.dll\") \nMPEG2Source(\"%s\")\n"
dvrcut_options="dvrcut \"%s.dvr-ms\" \"%s_clean.dvr-ms\" "
windowtitle="Comskip - %s"

If after all the above is working, the file played should look like this

When watching the red sections are skipped

Hope this helps someone

Where I got my help from
other users guide & chatgpt :slight_smile: