185 lines
6.5 KiB
Bash
185 lines
6.5 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
# scripts/manual/client_rct_remote_capture.sh
|
||
|
|
# Remote Tethys-side UVC/UAC capture helper for run_client_to_rct_transport_probe.sh.
|
||
|
|
set -euo pipefail
|
||
|
|
remote_capture=$1
|
||
|
|
capture_seconds=$2
|
||
|
|
width=$3
|
||
|
|
height=$4
|
||
|
|
fps=$5
|
||
|
|
capture_stack=$6
|
||
|
|
pulse_tool=$7
|
||
|
|
video_mode=$8
|
||
|
|
anchor_silence=$9
|
||
|
|
preroll_discard=${10}
|
||
|
|
preroll_settle=${11}
|
||
|
|
ready_settle=${12}
|
||
|
|
ready_marker=${13}
|
||
|
|
|
||
|
|
resolve_video_device() {
|
||
|
|
find /dev/v4l/by-id -maxdepth 1 -type l \
|
||
|
|
-name 'usb-Lesavka_Lesavka_Composite*video-index0' | sort | head -n 1
|
||
|
|
}
|
||
|
|
|
||
|
|
resolve_pulse_source() {
|
||
|
|
pactl list short sources 2>/dev/null \
|
||
|
|
| awk '
|
||
|
|
/alsa_input\..*Lesavka_Lesavka_Composite/ { print $2; found=1; exit }
|
||
|
|
/Lesavka_Lesavka_Composite/ && $2 !~ /\.monitor$/ && !fallback { fallback=$2 }
|
||
|
|
END {
|
||
|
|
if (found) exit 0
|
||
|
|
if (fallback != "") { print fallback; exit 0 }
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
'
|
||
|
|
}
|
||
|
|
|
||
|
|
current_video_profile() {
|
||
|
|
v4l2-ctl -d "${video_device}" --all 2>/dev/null \
|
||
|
|
| awk '
|
||
|
|
/Width\/Height[[:space:]]*:/ {
|
||
|
|
split($0, a, ":")
|
||
|
|
gsub(/^[ \t]+/, "", a[2])
|
||
|
|
split(a[2], wh, "/")
|
||
|
|
width=wh[1]
|
||
|
|
height=wh[2]
|
||
|
|
next
|
||
|
|
}
|
||
|
|
/Frames per second[[:space:]]*:/ {
|
||
|
|
split($0, a, ":")
|
||
|
|
gsub(/^[ \t]+/, "", a[2])
|
||
|
|
split(a[2], fps_parts, "\\.")
|
||
|
|
fps=fps_parts[1]
|
||
|
|
}
|
||
|
|
END {
|
||
|
|
if (width && height && fps) {
|
||
|
|
printf "%s %s %s\n", width, height, fps
|
||
|
|
exit 0
|
||
|
|
}
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
'
|
||
|
|
}
|
||
|
|
|
||
|
|
gst_audio_mixer_element() {
|
||
|
|
if gst-inspect-1.0 audiomixer 2>/dev/null | grep -q 'ignore-inactive-pads'; then
|
||
|
|
printf 'audiomixer name=amix ignore-inactive-pads=true'
|
||
|
|
else
|
||
|
|
printf 'audiomixer name=amix'
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
run_preroll() {
|
||
|
|
local video_device=$1
|
||
|
|
local seconds=$2
|
||
|
|
[[ "${seconds}" =~ ^[0-9]+$ && "${seconds}" -gt 0 ]] || return 0
|
||
|
|
printf 'discarding %ss of post-enumeration capture before probe\n' "${seconds}" >&2
|
||
|
|
timeout --kill-after=5 --signal=INT "$((seconds + 5))" \
|
||
|
|
gst-launch-1.0 -q -e v4l2src device="${video_device}" do-timestamp=true num-buffers="$((fps * seconds))" \
|
||
|
|
! "image/jpeg,width=${width},height=${height},framerate=${fps}/1" ! fakesink \
|
||
|
|
>/dev/null 2>&1 || true
|
||
|
|
if [[ "${preroll_settle}" =~ ^[0-9]+$ && "${preroll_settle}" -gt 0 ]]; then
|
||
|
|
printf 'settling %ss after preroll discard\n' "${preroll_settle}" >&2
|
||
|
|
sleep "${preroll_settle}"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
run_gst_pulse_capture() {
|
||
|
|
local video_device=$1
|
||
|
|
local pulse_source=$2
|
||
|
|
local video_caps="image/jpeg,width=${width},height=${height},framerate=${fps}/1"
|
||
|
|
local decode_chain="jpegdec !"
|
||
|
|
local audio_mixer
|
||
|
|
audio_mixer="$(gst_audio_mixer_element)"
|
||
|
|
local audio_anchor=()
|
||
|
|
if [[ "${anchor_silence}" != "0" ]]; then
|
||
|
|
printf 'anchoring Pulse capture audio timeline with generated silence\n' >&2
|
||
|
|
audio_anchor=(audiotestsrc wave=silence is-live=true do-timestamp=true ! "audio/x-raw,rate=48000,channels=2" ! queue ! amix.)
|
||
|
|
fi
|
||
|
|
printf 'capture_start_unix_ns=%s\n' "$(date +%s%N)" >&2
|
||
|
|
if [[ "${video_mode}" == "cfr" ]]; then
|
||
|
|
timeout --kill-after=5 --signal=INT "$((capture_seconds + 3))" \
|
||
|
|
gst-launch-1.0 -q -e \
|
||
|
|
matroskamux name=mux ! filesink location="${remote_capture}" \
|
||
|
|
v4l2src device="${video_device}" do-timestamp=true ! ${video_caps} ! \
|
||
|
|
${decode_chain} videoconvert ! videorate ! video/x-raw,framerate="${fps}"/1 ! \
|
||
|
|
x264enc tune=zerolatency speed-preset=ultrafast key-int-max=1 bitrate=5000 ! \
|
||
|
|
h264parse ! queue ! mux. \
|
||
|
|
${audio_mixer} ! audio/x-raw,rate=48000,channels=2 ! queue ! mux. \
|
||
|
|
"${audio_anchor[@]}" \
|
||
|
|
pulsesrc device="${pulse_source}" do-timestamp=true ! audio/x-raw,rate=48000,channels=2 ! \
|
||
|
|
audioconvert ! audioresample ! audio/x-raw,rate=48000,channels=2 ! queue ! amix. &
|
||
|
|
else
|
||
|
|
timeout --kill-after=5 --signal=INT "$((capture_seconds + 3))" \
|
||
|
|
gst-launch-1.0 -q -e \
|
||
|
|
matroskamux name=mux ! filesink location="${remote_capture}" \
|
||
|
|
v4l2src device="${video_device}" do-timestamp=true ! ${video_caps} ! queue ! mux. \
|
||
|
|
${audio_mixer} ! audio/x-raw,rate=48000,channels=2 ! queue ! mux. \
|
||
|
|
"${audio_anchor[@]}" \
|
||
|
|
pulsesrc device="${pulse_source}" do-timestamp=true ! audio/x-raw,rate=48000,channels=2 ! \
|
||
|
|
audioconvert ! audioresample ! audio/x-raw,rate=48000,channels=2 ! queue ! amix. &
|
||
|
|
fi
|
||
|
|
local capture_pid=$!
|
||
|
|
sleep "${ready_settle}"
|
||
|
|
printf '%s\n' "${ready_marker}" >&2
|
||
|
|
wait "${capture_pid}"
|
||
|
|
}
|
||
|
|
|
||
|
|
run_ffmpeg_pulse_capture() {
|
||
|
|
local video_device=$1
|
||
|
|
local pulse_source=$2
|
||
|
|
printf 'capture_start_unix_ns=%s\n' "$(date +%s%N)" >&2
|
||
|
|
timeout --kill-after=5 --signal=INT "$((capture_seconds + 5))" \
|
||
|
|
ffmpeg -nostdin -hide_banner -loglevel error -y \
|
||
|
|
-thread_queue_size 1024 -f video4linux2 -framerate "${fps}" \
|
||
|
|
-video_size "${width}x${height}" -input_format mjpeg -i "${video_device}" \
|
||
|
|
-thread_queue_size 1024 -f pulse -i "${pulse_source}" \
|
||
|
|
-map 0:v:0 -map 1:a:0 -t "${capture_seconds}" \
|
||
|
|
-c:v copy -c:a pcm_s16le "${remote_capture}" </dev/null &
|
||
|
|
local capture_pid=$!
|
||
|
|
sleep "${ready_settle}"
|
||
|
|
printf '%s\n' "${ready_marker}" >&2
|
||
|
|
wait "${capture_pid}"
|
||
|
|
}
|
||
|
|
|
||
|
|
rm -f "${remote_capture}"
|
||
|
|
video_device="$(resolve_video_device)"
|
||
|
|
if [[ -z "${video_device}" ]]; then
|
||
|
|
printf 'Lesavka UVC video device not found on RCT host; refusing unrelated capture devices.\n' >&2
|
||
|
|
exit 2
|
||
|
|
fi
|
||
|
|
if [[ "${width}" == "0" || "${height}" == "0" || "${fps}" == "0" ]]; then
|
||
|
|
if read -r width height fps < <(current_video_profile); then
|
||
|
|
:
|
||
|
|
else
|
||
|
|
printf 'unable to auto-detect current UVC mode; set LESAVKA_CLIENT_RCT_MODE=WIDTHxHEIGHT@FPS\n' >&2
|
||
|
|
exit 2
|
||
|
|
fi
|
||
|
|
fi
|
||
|
|
printf 'using video device: %s\n' "${video_device}" >&2
|
||
|
|
printf 'using video mode: %sx%s @ %s fps (mjpeg)\n' "${width}" "${height}" "${fps}" >&2
|
||
|
|
|
||
|
|
case "${capture_stack}" in
|
||
|
|
pulse)
|
||
|
|
pulse_source="$(resolve_pulse_source)"
|
||
|
|
if [[ -z "${pulse_source}" ]]; then
|
||
|
|
printf 'Lesavka Pulse audio source not found; refusing timing-sensitive fallback.\n' >&2
|
||
|
|
exit 2
|
||
|
|
fi
|
||
|
|
printf 'using Pulse source: %s\n' "${pulse_source}" >&2
|
||
|
|
run_preroll "${video_device}" "${preroll_discard}"
|
||
|
|
case "${pulse_tool}" in
|
||
|
|
gst) run_gst_pulse_capture "${video_device}" "${pulse_source}" ;;
|
||
|
|
ffmpeg) run_ffmpeg_pulse_capture "${video_device}" "${pulse_source}" ;;
|
||
|
|
*)
|
||
|
|
printf 'unsupported REMOTE_PULSE_CAPTURE_TOOL=%s for client-to-RCT probe; use gst or ffmpeg\n' "${pulse_tool}" >&2
|
||
|
|
exit 2
|
||
|
|
;;
|
||
|
|
esac
|
||
|
|
;;
|
||
|
|
*)
|
||
|
|
printf 'unsupported REMOTE_CAPTURE_STACK=%s for client-to-RCT probe\n' "${capture_stack}" >&2
|
||
|
|
exit 2
|
||
|
|
;;
|
||
|
|
esac
|