Pedro
Pedro

Reputation: 21

Is it possible to stop a pipeline when pressing a key in bash?

I am using a Rpi zero 2w and an old Rpi camera using Rpi OS (Legacy, 64-bit) Lite (i.e. a server linux) to stream a video through my local net. I want to stop the stream remotely from windows using mobaxterm. With this code it streams well but does not detect the key press:

#!/bin/bash

WIDTH=1280
HEIGHT=720
FRAMERATE=30
SERVER_PORT=8111
STOP_KEY="p"

libcamera-vid -n -t 0 --inline -o - --width $WIDTH --height $HEIGHT --framerate $FRAMERATE --libav-format h264 | nc -l -p $SERVER_PORT | {
    while true; do
        echo "test"
        
        read -rsn1 keypress

        if [ "$keypress" == "$STOP_KEY" ]; then
            echo "Stopping pipeline"
            pkill -INT -f "libcamera-vid"
            exit 0
        fi
    done
}

Maybe I do not understand well how pipelines work, but right now it only displays "test" once.

Any ideas?

Upvotes: 1

Views: 102

Answers (2)

F. Hauri  - Give Up GitHub
F. Hauri - Give Up GitHub

Reputation: 70957

Run a process in background, then interact with through STDIO

Under I often use /proc/$pid for this...

But care: $! return only pid of last run command:

Sample:

sleep infinity | nc -l -p 12345 &
while read pid cmd _; do
    case $cmd in
        ps ) ;;
        *  ) pids+=($pid) ;;
    esac
done < <(ps --ppid $$ ho pid,cmd)
while [[ -d /proc/${pids[@]: -1} ]]; do
    read -rsn 1 -t 2 char &&
        case $char in
            q | $'\e' )
                kill ${pids[@]}
                echo break ${pids[@]}
                wait ${pids[@]};;
            s ) echo run from $SECONDS seconds.;;
        esac
done

Should by run in a new bash session in order to initialize $SECONDS:

bash -c 'sleep infinity|nc -lp12345&while read pid cmd _;do case $cmd in ps);;*)
pids+=($pid);;esac;done< <(ps --ppid $$ ho pid,cmd);while [[ -d /proc/${pids[@]:
-1} ]];do read -rsn 1 -t 2 char && case $char in q|$'\''\e'\'') kill ${pids[@]};
echo break ${pids[@]};wait ${pids[@]};;s)echo run from $SECONDS secs.;;esac;done'

Regarding your sample script,

First, notice that nc's ouput is request from nc's client network connected! So if you expect read from STDIN, you have to not read input from nc's output!

Because I dislike long lines in script, I'v grouped libcamera-vid's arguments into an array.

Nota: As I don't know your tool (libcamera-vid), assuming they could expect interaction from STDIN, you may have to ensure they won't stole your keyboard interactions. For this you could redirect his STDIN to an empty line: <<<'' (or to /dev/null: <dev/null if you tool quit immediately when run them with <<<'').

Then script could become something like:

#!/bin/bash

WIDTH=1280
HEIGHT=720
FRAMERATE=30
SERVER_PORT=8111
STOP_KEY="p"
STAT_KEY="s"

cmdArgs=( -n -t 0 --inline -o -  --width $WIDTH --height $HEIGHT --
          framerate $FRAMERATE   --libav-format h264 )
libcamera-vid "${cmdArgs[@]}" <<<'' |
    nc -q 0 -l -p $SERVER_PORT >/dev/null &
camPid=$!

while [[ -d /proc/$camPid ]];do
    read -rsn 1 -t 2 char &&
        case $char in
            $STOP_KEY )
                kill $camPid
                echo "Kill: $camPid"
                wait camPid
                ;;
            $STAT_KEY )
                echo "Process $$ and $camPid run from $SECONDS seconds."
                ;;
        esac
done

Upvotes: 0

Paul Hodges
Paul Hodges

Reputation: 15418

The pipeline is replacing the STDIN of your loop with the STDOUT of the preceding command. It isn't reading the terminal anymore.

$: yes| for i in {1..4}; do read -rsn1 input; echo "$i: $input"; done
1: y
2:
3: y
4:

What's happening in the above example is that yes is sending out an endless stream of y's, each followed by a newline character, and the read -rsn1 is explicitly reading ONE character from the input, so the first iteration gets a y, and the second gets a newline - 3 is a y, 4 is a newline, then my loop exits. It doesn't wait for me to type anything, because it's reading the pipe.

To do what you are trying to do, you will need to somehow differentiate the streams. There are several ways to do this. Here's one -

$: for i in {1..4}        # limiting to four iterations again here
>  do read -rsn1 keypress # this reads one character, as expected
>     read -r stream <&3  # this reads from stream 3, declared below
>     echo "$i: keyboard='$keypress' pipe='$stream'" # show what we got
> done 3< <(printf "%s\n" W X Y Z) # declare stream 3 
1: keyboard='a' pipe='W'
2: keyboard='b' pipe='X'
3: keyboard='c' pipe='Y'
4: keyboard='d' pipe='Z'

Each iteration stopped and waited for me to type (I entered the lowercase letters in order) and continued without waiting for me to hit the return (the -n1 on the read).

...but I think you have another problem. If I read that right, you are streaming the data to a socket port, so while the loop is issuing the echo "test" and then getting to the read -rsn1 keypress successfully, in your case there is no data coming in through the pipeline, because your command pipeline isn't sending anything to STDOUT, so it just waits.

You could also use a FIFO, a coprocess, or a more complicated set of shuffled redirections, etc. You just have to differentiate the data streams. As Philippe said, you could just use read -rsn1 keypress < /dev/tty :)

But to modify your idea to actually work as intended...
Try this -

libcamera-vid -n -t 0 --inline -o - --width $WIDTH --height $HEIGHT --framerate $FRAMERATE --libav-format h264 | 
  nc -l -p $SERVER_PORT & pid=$!
until read -r -s -n1 -t5 any
do ps -p $pid >/dev/null 2>&1 || break
done
if ps -p $pid >/dev/null 2>&1
then echo "Stopping stream"
     kill $pid
fi

Upvotes: 1

Related Questions