|
#!/bin/bash |
|
# Find V4L2 capture device and stream with minimal latency |
|
# Usage: ./mpv-stream.sh [device_pattern] |
|
|
|
set -e |
|
|
|
# Default device pattern (common capture card patterns) |
|
DEFAULT_PATTERNS=("capture" "video" "usb" "cam" "card" "easycap") |
|
DEVICE_PATTERN="${1:-capture}" |
|
|
|
# Function to find V4L2 devices - returns only device path |
|
find_v4l2_device() { |
|
local pattern="$1" |
|
echo "Searching for V4L2 devices with pattern: $pattern" >&2 |
|
|
|
# Check all video devices |
|
for device in /dev/video*; do |
|
if [ -c "$device" ]; then |
|
# Use v4l2-ctl to get device info |
|
if v4l2-ctl -d "$device" --all 2>/dev/null | grep -q -i "$pattern"; then |
|
echo "Found device: $device" >&2 |
|
v4l2-ctl -d "$device" --info | head -n 5 >&2 |
|
# Return only the device path (no echo) |
|
printf "%s" "$device" |
|
return 0 |
|
fi |
|
fi |
|
done |
|
|
|
return 1 |
|
} |
|
|
|
# Function to list all available V4L2 devices |
|
list_all_devices() { |
|
echo "Available V4L2 devices:" |
|
for device in /dev/video*; do |
|
if [ -c "$device" ]; then |
|
echo "=== $device ===" |
|
v4l2-ctl -d "$device" --info 2>/dev/null | head -n 3 || true |
|
echo |
|
fi |
|
done |
|
} |
|
|
|
# Try to find device with specified pattern |
|
FOUND_DEVICE="" |
|
for pattern in "$DEVICE_PATTERN" "${DEFAULT_PATTERNS[@]}"; do |
|
echo "Trying pattern: $pattern" |
|
FOUND_DEVICE=$(find_v4l2_device "$pattern") |
|
if [ -n "$FOUND_DEVICE" ]; then |
|
break |
|
fi |
|
done |
|
|
|
# If no device found, show available devices and exit |
|
if [ -z "$FOUND_DEVICE" ]; then |
|
echo "Error: No V4L2 capture device found with pattern: $DEVICE_PATTERN" |
|
echo |
|
list_all_devices |
|
echo "Please specify a different pattern or check your device connection" |
|
exit 1 |
|
fi |
|
|
|
echo "Using device: $FOUND_DEVICE" |
|
|
|
# # Configure the V4L2 device to 1080p60 upfront so we don't need global demuxer opts |
|
if command -v v4l2-ctl >/dev/null 2>&1; then |
|
echo "Configuring $FOUND_DEVICE to 1920x1080@60..." |
|
v4l2-ctl -d "$FOUND_DEVICE" --set-fmt-video=width=1920,height=1080,pixelformat=MJPG 2>/dev/null || \ |
|
# v4l2-ctl -d "$FOUND_DEVICE" --set-fmt-video=width=1920,height=1080,pixelformat=YUYV 2>/dev/null || \ |
|
echo "Warning: Could not set pixel format to MJPG/YUYV" |
|
v4l2-ctl -d "$FOUND_DEVICE" --set-parm=60 2>/dev/null || echo "Warning: Could not set framerate to 60" |
|
fi |
|
|
|
# Check if audio is available and ensure it matches the selected V4L2 device |
|
|
|
# Prefer PulseAudio for audio capture, fallback to ALSA only if PulseAudio is unavailable or no suitable source is found |
|
AUDIO_DEVICE="" |
|
AUDIO_TYPE="" |
|
|
|
# Resolve the hardware root (USB/PCI) for a sysfs path |
|
get_hw_root() { |
|
local p="$1" |
|
while [ "$p" != "/" ]; do |
|
if [ -f "$p/idVendor" ] || [ -f "$p/vendor" ]; then |
|
echo "$p" |
|
return 0 |
|
fi |
|
p=$(dirname "$p") |
|
done |
|
echo "" |
|
} |
|
|
|
# Find ALSA card number that shares the same hardware root as the given V4L2 device |
|
find_alsa_card_for_video_device() { |
|
local video="$1" |
|
local vpath |
|
vpath=$(readlink -f "/sys/class/video4linux/$(basename "$video")/device" 2>/dev/null || true) |
|
[ -z "$vpath" ] && return 1 |
|
local vroot |
|
vroot=$(get_hw_root "$vpath") |
|
[ -z "$vroot" ] && return 1 |
|
for dev in /sys/class/sound/card*/device; do |
|
[ -e "$dev" ] || continue |
|
local cpath |
|
cpath=$(readlink -f "$dev" 2>/dev/null || true) |
|
[ -z "$cpath" ] && continue |
|
local croot |
|
croot=$(get_hw_root "$cpath") |
|
[ -z "$croot" ] && continue |
|
if [ "$croot" = "$vroot" ]; then |
|
local carddir |
|
carddir=$(basename "$(dirname "$dev")") # e.g., card2 |
|
printf "%s" "${carddir#card}" |
|
return 0 |
|
fi |
|
done |
|
return 1 |
|
} |
|
|
|
# Try PulseAudio first |
|
|
|
PULSE_DEVICE_CANDIDATE="" |
|
if command -v pactl >/dev/null 2>&1; then |
|
# Try to match PulseAudio source to V4L2 device hardware root |
|
V4L2_HW_ROOT="$(get_hw_root $(readlink -f "/sys/class/video4linux/$(basename "$FOUND_DEVICE")/device" 2>/dev/null || true))" |
|
# List all sources and try to match by USB/PCI ID in the name |
|
PULSE_MATCHED="" |
|
if [ -n "$V4L2_HW_ROOT" ]; then |
|
# Extract USB/PCI ID from the hardware root path |
|
USB_ID="$(basename "$V4L2_HW_ROOT")" |
|
# Try to find a source whose name contains the USB/PCI ID |
|
PULSE_MATCHED="$(pactl list sources | awk -v RS= -v id="$USB_ID" ' |
|
$0 ~ /Source #/ && $0 !~ /monitor/ && $0 ~ /Name:[[:space:]]+.*alsa_input/ { |
|
if ($0 ~ id) { |
|
if (match($0, /Name:[[:space:]]+([^\n]+)/, n)) { print n[1]; exit } |
|
} |
|
}' | head -n1)" |
|
fi |
|
# Fallback to first non-monitor alsa_input source if no match |
|
if [ -z "$PULSE_MATCHED" ]; then |
|
PULSE_MATCHED="$(pactl list sources | awk -v RS= ' |
|
$0 ~ /Source #/ && $0 !~ /monitor/ && $0 ~ /Name:[[:space:]]+.*alsa_input/ { |
|
if (match($0, /Name:[[:space:]]+([^\n]+)/, n)) { print n[1]; exit } |
|
}' | head -n1)" |
|
fi |
|
if [ -n "$PULSE_MATCHED" ]; then |
|
AUDIO_DEVICE="$PULSE_MATCHED" |
|
AUDIO_TYPE="pulse" |
|
fi |
|
fi |
|
|
|
# If PulseAudio is not available or no suitable source, try ALSA |
|
if [ -z "$AUDIO_DEVICE" ]; then |
|
ALSA_CARD_NUM="$(find_alsa_card_for_video_device "$FOUND_DEVICE" || true)" |
|
ALSA_DEVICE_CANDIDATE="" |
|
[ -n "$ALSA_CARD_NUM" ] && ALSA_DEVICE_CANDIDATE="plughw:$ALSA_CARD_NUM,0" |
|
if [ -n "$ALSA_DEVICE_CANDIDATE" ]; then |
|
AUDIO_DEVICE="$ALSA_DEVICE_CANDIDATE" |
|
AUDIO_TYPE="alsa" |
|
fi |
|
fi |
|
|
|
[ -n "$AUDIO_DEVICE" ] && [ -n "$AUDIO_TYPE" ] && echo "Selected audio ($AUDIO_TYPE): $AUDIO_DEVICE" |
|
|
|
# MPV options for lowest latency |
|
MPV_OPTIONS=( |
|
"--profile=low-latency" |
|
"--untimed" |
|
"--video-sync=display-resample" |
|
"--interpolation=no" |
|
"--video-latency-hacks=yes" |
|
"--opengl-swapinterval=0" |
|
"--hwdec=auto-copy" # Hardware decoding if available |
|
"--gpu-context=auto" |
|
"--cache=no" |
|
"--audio-buffer=0.01" # Very small audio buffer |
|
"--log-file=./log.txt" |
|
"--demuxer-lavf-o-add=use_wallclock_as_timestamps=yes" |
|
|
|
) |
|
|
|
# Build the mpv command and ensure a valid audio output backend is selected |
|
MPV_CMD=("mpv" "${MPV_OPTIONS[@]}") |
|
if command -v pactl >/dev/null 2>&1; then |
|
MPV_CMD+=("--ao=pulse" "--audio-device=auto") |
|
else |
|
MPV_CMD+=("--audio-device=auto") |
|
fi |
|
|
|
# Clean up any potential whitespace in device paths |
|
FOUND_DEVICE=$(echo "$FOUND_DEVICE" | tr -d '[:space:]') |
|
|
|
printf 'Found device \n%s\n' $FOUND_DEVICE |
|
printf 'Found audio \n%s\n' $AUDIO_DEVICE |
|
printf 'Found audio type \n%s\n' $AUDIO_TYPE |
|
if [ -n "$AUDIO_DEVICE" ]; then |
|
# Clean audio device path too |
|
AUDIO_DEVICE=$(echo "$AUDIO_DEVICE" | tr -d '[:space:]') |
|
# Stream with audio |
|
echo "Starting stream with audio (lowest latency)... with $AUDIO_TYPE audio device: $AUDIO_DEVICE" |
|
if [ "$AUDIO_TYPE" = "alsa" ]; then |
|
ENABLE_LSFG=1 "${MPV_CMD[@]}" \ |
|
--aid=1 \ |
|
--audio-file="av://alsa:$AUDIO_DEVICE" \ |
|
"av://v4l2:$FOUND_DEVICE" |
|
else |
|
ENABLE_LSFG=1 "${MPV_CMD[@]}" \ |
|
--aid=1 \ |
|
--audio-file="av://pulse:$AUDIO_DEVICE" \ |
|
"av://v4l2:$FOUND_DEVICE" |
|
fi |
|
else |
|
# Stream without audio |
|
echo "Starting video-only stream (lowest latency)..." |
|
echo "Note: No audio device found. If you need audio, check your audio setup." |
|
"${MPV_CMD[@]}" \ |
|
"av://v4l2:$FOUND_DEVICE" |
|
fi |