imsh-clients

Clients for imsh screenshot/screencast sharing service
git clone https://git.echoz.io/imsh-clients.git
Log | Files | Refs

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