raspberrypi / picamera2

New libcamera based python library
BSD 2-Clause "Simplified" License
889 stars 188 forks source link

[HOW-TO] Write h264 encoded data to named pipe #982

Open dwoja22 opened 8 months ago

dwoja22 commented 8 months ago

I wrote a script that accomplishes this using a circular buffer, unfortunately I'm having sync issues with reading the buffer and writing to the pipe.

My loop that handles the read/write blocks because I do not know how to sync these two operations.

I managed to solve it with a dirty hack by introducing a 10ms sleep inside the loop.

The question is how do I sync reading the buffer and writing to the pipe? Is using a circular buffer the correct approach?

Below the python script for context

from picamera2 import Picamera2
from picamera2.encoders import H264Encoder
from picamera2.outputs import FileOutput
from picamera2.outputs import CircularOutput
import time
import sys
import os
import io
import signal

def signal_handler(sig, frame):
    print('Received CTRL+C, exiting...')
    sys.exit(0)

picam2=Picamera2()
sensor_modes = picam2.sensor_modes

#loop through the sensor modes and print them
print('Sensor Modes:')
for mode in sensor_modes:
    print(mode)

raw_modes = picam2._get_raw_modes()
#loop through the raw modes and print them
print('Raw Modes:')
for mode in raw_modes:
    print(mode)

config = picam2.create_video_configuration(main={'size': (640, 480), 'format':'XBGR8888'}, raw={'size': (1640,1232)})
picam2.configure(config)

fifo_path = os.path.abspath('../pipe1')
#check if the fifo exists
if not os.path.exists(fifo_path):
    print('error: fifo does not exist')
    sys.exit(1)
print ('opening fifo')
try:
    fifo = io.open(fifo_path, 'wb', buffering=0)
except FileNotFoundError:
    print("error: fifo '{}' does not exist.".format(fifo_path))
    sys.exit(1)
except PermissionError:
    print("error: permission denied to write to FIFO '{}'.".format(fifo_path))
    sys.exit(1)
except Exception as e:
    print("error:", e)
    sys.exit(1)
else:
    print("fifo opened successfully.")

encoder = H264Encoder(bitrate=2000000, repeat=True, iperiod=7, framerate=30)

buffer = io.BytesIO()
output = CircularOutput(buffer, buffersize=1)
encoder.output = output

print('starting stream')
picam2.start()
picam2.start_encoder(encoder)

signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

#Continuously write the buffer to the fifo
try:
    while True:
        time.sleep(10/1000)
        buffer.seek(0)
        data = buffer.read()

        fifo.write(data)
        buffer.truncate(0)
except Exception as e:
    print("error:", e)
finally:
    picam2.stop_encoder()
    picam2.stop()
    fifo.close()
    print('fifo closed')
    sys.exit(0)
davidplowman commented 8 months ago

Hi, I wasn't quite sure what you were trying to accomplish. If the purpose is to write encoded video to the pipe, can't you just give the pipe name to the encoder object? i.e.

encoder.output = FileOutput(name_of_pipe)

Or perhaps there's some reason that doesn't work, in which case please let me know.

Trying to synchronise what the background thread is doing with your application loop sounds more than a little hair-raising. I'm impressed you got it to work at all, but I'd be surprised if it proved to be reliable!

dwoja22 commented 8 months ago

Hi,

Yes FileOutput works.

On the other end of the pipe I have a golang app that consumes the data from the pipe and streams it to a browser via webrtc.

I must have messed up earlier, I initially tried to implement using FileOutput but I got an error saying I have to provide BufferedIOBase.

Should have been more careful reading the documentation in the code, it clearly states it accepts a path as well.

:type file: str or BufferedIOBase or Path, optional

Interestingly enough, both solutions work equally well and both have a very strange symptom.

The stream always starts with around 5-10 seconds of latency and around after 30s it stabilizes and the stream is real time as expected.

Might be an issue in the golang app, so I leave it at that.

Below the corrected and much shorter script. Issue is resolved.


from picamera2.encoders import H264Encoder
from picamera2.outputs import FileOutput
import sys
import os
import io
import signal

def signal_handler(sig, frame):
    print('Received CTRL+C, exiting...')
    picam2.stop_encoder()
    picam2.stop()
    sys.exit(0)

picam2=Picamera2()
sensor_modes = picam2.sensor_modes

#loop through the sensor modes and print them
print('Sensor Modes:')
for mode in sensor_modes:
    print(mode)

raw_modes = picam2._get_raw_modes()
#loop through the raw modes and print them
print('Raw Modes:')
for mode in raw_modes:
    print(mode)

config = picam2.create_video_configuration(main={'size': (640, 480), 'format':'XBGR8888'}, raw={'size': (1640,1232)})
picam2.configure(config)

fifo_path = os.path.abspath('pipe1')
encoder = H264Encoder(bitrate=2000000, repeat=True, iperiod=7, framerate=30)

buffer = io.BytesIO()
encoder.output = FileOutput(fifo_path)

print('starting stream')
picam2.start()
picam2.start_encoder(encoder)

signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
signal.pause()
davidplowman commented 8 months ago

Cool, glad it works! On the camera side, there should be no meaningful latency in sending stuff to your pipe so far as I know, so I guess you need to look at the buffering in the receiving end. To see what should be achievable, maybe check out a simple a example like https://github.com/raspberrypi/picamera2/blob/main/examples/mjpeg_server_2.py which I think runs with fairly low latency.