imsh-cast.sh (8671B)
1 #!/usr/bin/env bash 2 3 argv0="imsh-cast" 4 5 usage() { 6 cat <<EOF 7 $argv0 - Capture and optionally upload a screencast to an imsh server 8 9 Usage: $argv0 [options...] 10 11 Options: 12 -u, --upload 13 Uploads the screencast to imsh. 14 15 -a, --api-key=key|@file|%cmd 16 API key for uploading to imsh. 17 Prefix with "@" to read from a file. 18 Prefix with "%" to run a command. 19 20 -e, --endpoint=url 21 The imsh endpoint to upload to. 22 Defaults to https://scrn.is/api/v1/upload 23 24 -o, --output=path-pattern 25 Path to write captured screencast to. Gets passed through date and 26 supports the same format strings. 27 28 -U, --utc 29 Call date with -u to use UTC for added privacy when sharing. 30 31 -C, --copy 32 Copy the absolute filesystem path of the screencast to the 33 clipboard. If uploading, instead copy the returned URL. 34 35 --pid-file=path 36 Specify the path to read/write PID from. If a file is found at this 37 location any other arguments will be ignored, and the PID contained 38 within will be sent a SIGINT. 39 Defaults to: \$XDG_RUNTIME_DIR/$argv0-0.pid 40 41 -t, --filetype=ext 42 Specifies the file extension for the temporary file used when no 43 output is specified. This has no effect when an output is specified. 44 Default: mp4 45 46 -A, --audio 47 Records audio in addition to video. To specify a device to record 48 from use --audio-device instead. 49 50 --audio-device=name 51 Specify the device to record audio from. Implies --audio. 52 53 -c, --codec=codec 54 Specifies the codec of the video. These can be found by using: 55 ffmpeg -encoders 56 57 -r, --framerate=number 58 Specifies a constant framerate. 59 60 -d, --device=name 61 Selects the device to use when encoding the video. 62 63 --no-dmabuf 64 Forces CPU copy while for recording. 65 66 -D, --no-damage 67 Continuously records even when there are no new frames. 68 69 -m, --muxer=name 70 Specifies the output format to a specific muxer instead of detecting 71 it from the filename. 72 73 -x, --pixel-format=name 74 Specifies the output pixel format. These can be found by running: 75 ffmpeg -pix_fmts 76 77 -O, --display-output 78 Specifies the display output to record. 79 80 -p, --codec-param=option=value 81 Change video codec parameters. 82 83 -F, --filter=option=value 84 Specifies the ffmpeg filter string to use. 85 86 -b, --bframes=number 87 Specifies the max number of b-frames to be used. If b-frames are not 88 supported by your hardware, set this to 0. 89 90 -B, --buffrate=number 91 Specifies the buffer's expected framerate. 92 93 --audio-backend=name 94 Specifies the audio bakcned to use, e.g. pipewire. 95 96 --audio-codec=codec 97 Specifies the audio codec. These can be found by running: 98 ffmpeg -encoders 99 100 -X, --sample-format=fmt 101 Specifies the audio sample format. These can be found by running: 102 ffmpeg -sample_fmts 103 104 -R, --sample-rate=number 105 Specifies the audio sample rate in Hz. 106 Default: 48000 107 108 -P, --audio-codec-param=option=value 109 Change audio codec parameters. 110 111 -y, --overwrite 112 Force overwriting the output file without prompting. 113 114 -h, --help 115 Display this message. 116 EOF 117 } 118 119 info() { 120 printf "Info: %s\n" "$1" >&2 121 } 122 123 warn() { 124 printf "Warning: %s\n" "$1" >&2 125 } 126 127 fatal() { 128 printf "Error: %s\n" "$1" >&2 129 exit 1 130 } 131 132 fatalUser() { 133 cat >&2 <<EOF 134 Error: $1 135 136 See $argv0 --help for more information. 137 EOF 138 exit 2 139 } 140 141 checkInput() { 142 err=() 143 while [[ $# -gt 0 ]]; do 144 msg="$1"; shift 145 cond="$1"; shift 146 value="$1"; shift 147 [[ "$cond" -ne 0 ]] && [[ -z "$value" ]] && err+=("$msg" ", ") 148 done 149 if [[ ${#err[@]} -gt 0 ]]; then 150 IFS="" 151 fatalUser "${err[*]::${#err[@]}-1}" >&2 152 fi 153 } 154 155 optsWithArgs=( 156 -a --api-key 157 -e --endpoint 158 -o --output 159 --audio-device 160 -c --codec 161 -r --framerate 162 -d --device 163 -t --filetype 164 -m --muxer 165 -x --pixel-format 166 -p --codec-param 167 -F --filter 168 -b --bframes 169 -B --buffrate 170 --audio-backend 171 --audio-codec 172 -X --sample-format 173 -R --sample-rate 174 -P --audio-codec-param 175 ) 176 177 # Preprocess options: 178 # Expand -fff to -f -f -f. 179 # Expand -kv to -k v. 180 # Expand --key=value to --key value. 181 # Stop preprocessing once -- is observed. 182 argv=() 183 ignoreOpts= 184 while [[ $# -gt 0 ]]; do 185 case "$ignoreOpts$1" in 186 --) argv+=("$1"); ignoreOpts=1;; 187 --*=*) 188 argv+=("${1%%=*}" "${1#*=}") 189 (IFS="|"; [[ "|${optsWithArgs[*]}" = *"|${1%%=*}"* ]]) \ 190 || fatalUser "invalid option: $1" 191 ;; 192 --*) argv+=("$1");; 193 -*) 194 for ((i=1; i < ${#1}; ++i)); do 195 argv+=("-${1:i:1}"); 196 if (IFS="|"; [[ "|${optsWithArgs[*]}" = *"|-${1:i:1}"* ]]) && [[ -n "${1:i+1}" ]]; then 197 argv+=("${1:i+1}") 198 break 199 fi 200 done;; 201 *) argv+=("$1");; 202 esac 203 shift 204 done 205 set -- "${argv[@]}" 206 207 upload= 208 apiKey= 209 endpoint=https://scrn.is/api/v1/upload 210 out= 211 save= 212 copy= 213 pidFile="$XDG_RUNTIME_DIR/$argv0-0.pid" 214 filetype=mp4 215 dateArgs=() 216 wfRecorderArgs=() 217 218 positional=() 219 ignoreOpts= 220 while [[ $# -gt 0 ]]; do 221 case "$ignoreOpts$1" in 222 -u|--upload) upload=1;; 223 -a|--api-key) apiKey="$2"; shift;; 224 -e|--endpoint) endpoint="$2"; shift;; 225 -o|--output) out="$2"; save=1; shift;; 226 -U|--utc) dateArgs+=(-u);; 227 -C|--copy) copy=1;; 228 --pid-file) pidFile="$2"; shift;; 229 -t|--filetype) filetype="$2"; shift;; 230 -A|--audio) wfRecorderArgs+=(--audio);; 231 --audio-device) wfRecorderArgs+=("--audio=$2"); shift;; 232 -c|--codec) wfRecorderArgs+=(--codec "$2"); shift;; 233 -r|--framerate) wfRecorderArgs+=(--framerate "$2"); shift;; 234 -d|--device) wfRecorderArgs+=(--device "$2"); shift;; 235 --no-dmabuf) wfRecorderArgs+=(--no-dmabuf);; 236 -D|--no-damage) wfRecorderArgs+=(--no-damage);; 237 -m|--muxer) wfRecorderArgs+=(--muxer "$2"); shift;; 238 -x|--pixel-format) wfRecorderArgs+=(--pixel-format "$2"); shift;; 239 -O|--display-output) wfRecorderArgs+=(--output "$2"); shift;; 240 -p|--codec-param) wfRecorderArgs+=(--codec-param "$2"); shift;; 241 -F|--filter) wfRecorderArgs+=(--filter "$2"); shift;; 242 -b|--bframes) wfRecorderArgs+=(--bframes "$2"); shift;; 243 -B|--buffrate) wfRecorderArgs+=(--buffrate "$2"); shift;; 244 --audio-backend) wfRecorderArgs+=(--audio-backend "$2"); shift;; 245 --audio-codec) wfRecorderArgs+=(--audio-codec "$2"); shift;; 246 -X|--sample-format) wfRecorderArgs+=(--sample-format "$2"); shift;; 247 -R|--sample-rate) wfRecorderArgs+=(--sample-rate "$2"); shift;; 248 -P|--audio-codec-param) wfRecorderArgs+=(--audio-codec-param "$2"); shift;; 249 -y|--overwrite) wfRecorderArgs+=(--overwrite);; 250 -h|--help) usage; exit 0;; 251 --) ignoreOpts=1;; 252 -*) fatalUser "invalid option: $1";; 253 *) positional+=("$1");; 254 esac 255 shift 256 done 257 set -- "${positional[@]}" 258 259 if [[ -e "$pidFile" ]]; then 260 set -e 261 info "found PID file: $pidFile" 262 pid="$(tr -d '[:space:]' <"$pidFile")" 263 if [[ "$pid" -gt 0 ]]; then 264 info "sending SIGINT to PID $pid" 265 kill -INT "$pid" 266 else 267 warn "PID file empty or malformed: $pid" 268 fi 269 info "deleting PID file: $pidFile" 270 rm -f "$pidFile" 271 exit 0 272 fi 273 274 checkInput \ 275 "extraneous positional arguments ($*)" $# ""\ 276 "missing one of --output --upload" 1 "$save$out$upload" \ 277 "missing value for --api-key" "$upload" "$apiKey" \ 278 "missing value for --output" "$save" "$out" \ 279 280 if [[ $((!save && upload)) -ne 0 ]]; then 281 out="$(mktemp --suffix ".$filetype" "$(printf 'X%.0s' {1..8})" --tmpdir)" 282 wfRecorderArgs+=(--overwrite) 283 trap "$(trap -P EXIT)"$'\nrm -f "$out"' EXIT 284 else 285 out="$(date "${dateArgs[@]}" "+${out/#\~/$HOME}")" \ 286 || fatal "date returned non-zero status" 287 fi 288 wfRecorderArgs+=(-f "$out") 289 290 if [[ "$upload" -ne 0 ]]; then 291 case "${apiKey:0:1}" in 292 @|%) apiKey=${apiKey:1};;& 293 @) 294 apiKeyRaw="$(<"${apiKey/#\~/$HOME}")" \ 295 || fatal "could not read API key from file: ${apiKey}" 296 ;; 297 %) 298 apiKeyRaw="$(bash -c "${apiKey}")" \ 299 || fatal "API key command returned non-zero status: ${apiKey}" 300 ;; 301 *) apiKeyRaw="$apiKey";; 302 esac 303 apiKey="$(tr -d '[:space:]' <<<"$apiKeyRaw")" 304 fi 305 306 set -eo pipefail 307 308 area="$(slurp)" 309 wfRecorderArgs+=(--geometry "$area") 310 311 mkdir -p "$(dirname "$out")" 312 wf-recorder "${wfRecorderArgs[@]}" >/dev/null & 313 pid=$! 314 trap "$(trap -P EXIT)"$'\nfor pid in $(jobs -p); do kill $pid; done' EXIT 315 printf "%d" $pid >"$pidFile" \ 316 || fatal "wf-recorder unable to write PID: $pidFile" 317 wait $pid \ 318 || fatal "wf-recorder returned non-zero status" 319 320 if [[ "$upload" -ne 0 ]]; then 321 response="$(curl -sS --fail-with-body \ 322 "$endpoint" \ 323 -F "image=@$out" \ 324 -H "Authorization: Bearer $apiKey")" 325 url="$(jq -r .url <<<"$response")" 326 printf "%s" "$url" 327 if [[ "$copy" -ne 0 ]]; then 328 tr -d '[:space:]' <<<"$url" | wl-copy 329 fi 330 elif [[ "$copy" -ne 0 ]]; then 331 printf "%s" "$out" | wl-copy 332 fi 333