Server Version#: 1.40.4.8679
Tuner: HDHomeRun FLEX DUO, OTA Antenna
Host System: Debian 12
I want to save space by storing my PVR recordings in h.265 encoding rather than mpeg-2.
Plex only encodes in h.264 as far as I can tell. I’d be fine with that except transcoding live recordings uses 100% CPU load instead of getting passed to my GPU (I know this is still a beta feature). Optimizing a pre-recorded video does use my GPU for encoding, but it keeps the original file.
My solution was to write a postprocessing script for PVR recordings and save it as “/config/Library/Application Support/Plex Media Server/Scripts/ts-to-h265.sh” I’ve entered “ts-to-h265.sh” in the postprocessing script menu in Plex, which later updated to show the full path.
Testing the script in terminal, “sh ts-to-h265 path/recording_name.ts” will use my GPU to transcode “path/recording_name.ts” to hevc, save it as “path/recording_name.mkv”, then delete “path/recording_name.ts”. It writes each step to a logfile with timestamps, including “script started”. As far as I can tell, Plex is not running the script. The script’s logfile shows no activity and there’s no transcoded files.
Make sure your script is executable. Also, ensure that you’re using either #!/usr/bin/sh or #!/usr/bin/bash as the interpreter in your script (and not /bin/*). At least if you’re using the Plex official container.
I worked through this earlier this year - a couple tips:
I mounted the scripts directory as a folder on my smb share - it means I can edit the file in VSCode on my windows machine without any weirdness about moving the script, having to rebless the permissions, etc
Depending on how you have pms installed (for instance if you’re running it in a docker container as I am), you may need to install some of the supporting infrastructure like ffmpeg in the container
Similarly, if you want NVenc, you’ll need to run install the nvidia container toolkit and enable the gpu in the container. There are lots of docker compose examples out there
I was running into a problem where encodings weren’t running - I made my script write the log file in a directory and retain it for 36 hours - that way I can check what happened
Be sure to test your script against a dummy recording - you can do this by ./myScript.sh myRecording.ts - that’s a good way of checking. If you’re running it in docker, you need to run the script there to be sure that dependencies like ffmpeg are there and registered in the path
I found ffmpeg was notably faster than handbrake with nvec with the same quality settings - that surprised me
If you’re running plex in a docker container, you might consider creating a script that you can run every time you recreate the container with a new version. I’ll append the script I use every time I rebuild the container at the end of this post
I’m not an awesome scripter in bash - I found working with CoPilot or ChatGPT invaluable to fix errors or work through concepts
And my transcode script. I do some logic to strip out any extraneous audio streams for smaller encoding sizes.
#!/bin/bash
# Define the directory where logs are stored
LOG_DIR="/transcode-logs"
# Delete log files older than 36 hours
find "$LOG_DIR" -type f -name '*.log' -mmin +2160 -delete
# Define variables
INPUT_FILE="$1"
OUTPUT_FILE="${INPUT_FILE%.*}.mkv" # Output file in H.265 (HEVC) format with MKV container
# Use ffprobe to get the list of audio streams, their channel count, bitrate, and language
AUDIO_STREAMS_INFO=$(ffprobe -select_streams a -show_entries stream=index,channels,bit_rate,tags:language -of csv=p=0 "$INPUT_FILE")
# Initialize variables to find the highest bitrate audio stream
HIGHEST_BITRATE=0
SELECTED_STREAM_INDEX=""
SELECTED_STREAM_LANGUAGE=""
for STREAM_INFO in $AUDIO_STREAMS_INFO; do
IFS=',' read -r INDEX CHANNELS BITRATE LANGUAGE <<< "$STREAM_INFO"
# Convert bitrate to an integer for comparison
BITRATE=${BITRATE%.*}
if [[ "$LANGUAGE" == "eng" && BITRATE -gt HIGHEST_BITRATE ]]; then
HIGHEST_BITRATE=$BITRATE
SELECTED_STREAM_INDEX=$INDEX
SELECTED_STREAM_LANGUAGE=$LANGUAGE
elif [[ -z "$SELECTED_STREAM_LANGUAGE" && BITRATE -gt HIGHEST_BITRATE ]]; then
HIGHEST_BITRATE=$BITRATE
SELECTED_STREAM_INDEX=$INDEX
fi
done
# Log file path
LOG_FILE="$LOG_DIR/$(basename "${INPUT_FILE%.*}").log"
# Run ffmpeg with the selected streams and re-encode video with Nvidia H265
ffmpeg -y -i "$INPUT_FILE" -map 0:v -map 0:a:$SELECTED_STREAM_INDEX -c:v hevc_nvenc -rc vbr -cq 26 -qmin 26 -qmax 26 -c:a aac -b:a 320k -max_muxing_queue_size 9999 "$OUTPUT_FILE" 2>&1 | tee -a "$LOG_FILE"
# Check the exit status of ffmpeg
if [ ${PIPESTATUS[0]} -eq 0 ]; then
echo "Conversion completed successfully: $OUTPUT_FILE" | tee -a "$LOG_FILE"
# Optionally remove the original input file after successful conversion
rm "$INPUT_FILE"
else
echo "Conversion failed: $INPUT_FILE" | tee -a "$LOG_FILE"
# Keep the original input file in case of failure
fi