Skip to content

Commit 891604c

Browse files
committed
Add software sync support
Software sync brings two cameras frames into synchronisation. It takes some time, however, and image metadata tells us when this synchronisation moment has been reached. The extra functionality includes: * The ability for encoders to ignore frames until sync is achieved. * Applications can wait for the encoder to signal that a synchronised recording has actually started. * We can easily capture the very first request where sync was achieved. Some examples and tests also been included. Signed-off-by: David Plowman <david.plowman@raspberrypi.com>
1 parent b894c08 commit 891604c

File tree

6 files changed

+159
-0
lines changed

6 files changed

+159
-0
lines changed

examples/sync_capture.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/python3
2+
3+
from libcamera import controls
4+
5+
from picamera2 import Picamera2
6+
7+
# Show how to do software-synchronised capture.
8+
#
9+
# This code is for the server (which controls the synchronisation), but the code
10+
# for the client is almost identical. We just change controls.rpi.SyncModeEnum.Server
11+
# into controls.rpi.SyncModeEnum.Client and it will listen out for and follow
12+
# the server.
13+
#
14+
# Usually the best advice is to start the client first. It will sit there doing
15+
# nothing at all until the server has been started and tells it "now".
16+
17+
picam2 = Picamera2()
18+
ctrls = {'FrameRate': 30.0, 'SyncMode': controls.rpi.SyncModeEnum.Server}
19+
# You can use a still capture mode, but would probably need more buffers so that
20+
# you don't get lots of frame drops.
21+
config = picam2.create_preview_configuration(controls=ctrls)
22+
23+
picam2.start(config)
24+
25+
req = picam2.capture_sync_request()
26+
# THe 'timer' tells us how many microseconds we were away from the synchronisation point.
27+
# Normally this should be small, but may be much larger if frames are being dropped.
28+
print("Lag:", req.get_metadata()['SyncTimer'])

examples/sync_recording.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/python3
2+
3+
import time
4+
5+
from libcamera import controls
6+
7+
from picamera2 import Picamera2
8+
from picamera2.encoders import H264Encoder
9+
10+
# Show how to do software-synchronised camera recordings.
11+
#
12+
# This code is for the server (which controls the synchronisation), but the code
13+
# for the client is almost identical. We just change controls.rpi.SyncModeEnum.Server
14+
# into controls.rpi.SyncModeEnum.Client and it will listen out for and follow
15+
# the server.
16+
#
17+
# Usually the best advice is to start the client first. It will sit there recording
18+
# nothing at all until the server has been started and tells it to "go now".
19+
20+
picam2 = Picamera2()
21+
ctrls = {'SyncMode': controls.rpi.SyncModeEnum.Server}
22+
config = picam2.create_video_configuration(controls=ctrls)
23+
encoder = H264Encoder(bitrate=5000000)
24+
encoder.sync_enable = True # this tells the encoder to wait until synchronisation
25+
output = "server.h264"
26+
27+
picam2.start(config)
28+
picam2.start_encoder(encoder, output)
29+
30+
# This event being signalled tells us that recording has started.
31+
encoder.sync.wait()
32+
print("Recording has started")
33+
34+
time.sleep(5)
35+
36+
picam2.stop_encoder(encoder)
37+
picam2.stop()

picamera2/encoders/encoder.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ def __init__(self):
6969
self.audio_sync = -100000 # in us, so by default, delay audio by 100ms
7070
self._audio_start = threading.Event()
7171
self.frames_encoded = 0
72+
# For camera sync.
73+
self.sync_enable = False
74+
self.sync = threading.Event()
7275

7376
@property
7477
def running(self):
@@ -239,6 +242,17 @@ def encode(self, stream, request):
239242
"""
240243
if self.audio:
241244
self._audio_start.set() # Signal the audio encode thread to start.
245+
246+
# If "sync" has been requested, we must wait for the image metadata to say that we
247+
# don't need to wait any more. While waiting, we simply don't encode any frames.
248+
if self.sync_enable:
249+
metadata = request.get_metadata()
250+
if metadata.get('SyncReady', False):
251+
self.sync_enable = False
252+
self.sync.set()
253+
else:
254+
return
255+
242256
if self._skip_count == 0:
243257
with self._lock:
244258
if not self._running:

picamera2/picamera2.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,6 +1528,18 @@ def captured_request(self, wait=None, flush=None):
15281528
finally:
15291529
request.release()
15301530

1531+
@contextlib.contextmanager
1532+
def captured_sync_request(self, wait=None):
1533+
"""Capture the first synchronised request using the context manager which guarantees its release.
1534+
1535+
Only for use when running with the software sync algorith.
1536+
"""
1537+
request = self.capture_sync_request(wait=wait)
1538+
try:
1539+
yield request
1540+
finally:
1541+
request.release()
1542+
15311543
def capture_metadata_(self):
15321544
if not self.completed_requests:
15331545
return (False, None)
@@ -2006,3 +2018,24 @@ def wait_for_af_state(self, states):
20062018
partial(wait_for_af_state, self,
20072019
{controls.AfStateEnum.Focused, controls.AfStateEnum.Failed, controls.AfStateEnum.Idle})]
20082020
return self.dispatch_functions(functions, wait, signal_function)
2021+
2022+
def capture_sync_request(self, wait=None, signal_function=None):
2023+
"""Return the first request when the camera system has reached sychronisation point.
2024+
2025+
This method can be used when this camera is the sychronisation server or client
2026+
for the software sync algorithm.
2027+
"""
2028+
2029+
def capture_sync_request_(self):
2030+
if not self.completed_requests:
2031+
return (False, None)
2032+
req = self.completed_requests.pop(0)
2033+
sync_ready = req.get_metadata().get('SyncReady', False)
2034+
if not sync_ready:
2035+
# Not yet synced. Discard this request and wait some more.
2036+
req.release()
2037+
return (False, None)
2038+
# Sync achieved. Return this request.
2039+
return (True, req)
2040+
2041+
return self.dispatch_functions([partial(capture_sync_request_, self)], wait, signal_function)

tests/sync_test.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/python3
2+
3+
import io
4+
import time
5+
6+
from libcamera import controls
7+
8+
from picamera2 import Picamera2
9+
from picamera2.encoders import H264Encoder
10+
from picamera2.outputs import FileOutput
11+
12+
picam2 = Picamera2()
13+
ctrls = {'SyncMode': controls.rpi.SyncModeEnum.Server, 'SyncFrames': 300}
14+
config = picam2.create_video_configuration(controls=ctrls)
15+
encoder = H264Encoder(bitrate=5000000)
16+
encoder.sync_enable = True # this tells the encoder to wait until synchronisation
17+
buffer = io.BytesIO()
18+
output = FileOutput(buffer)
19+
20+
picam2.start(config)
21+
picam2.start_encoder(encoder, output)
22+
23+
# The synchronisation delay is now 300 frames, or 10 seconds at 30fps, so
24+
# for 5 seconds we can be quite sure that nothing should be recorded.
25+
26+
time.sleep(5)
27+
if buffer.tell():
28+
print("ERROR: bytes recorded before synchronisation")
29+
else:
30+
print("No bytes during synchronisation period")
31+
32+
encoder.sync.wait()
33+
print("Recording has started")
34+
35+
# Now, if we wait a bit longer, there had better be some data!
36+
37+
time.sleep(5)
38+
if buffer.tell():
39+
print(buffer.tell(), "bytes record after synchronisation period")
40+
else:
41+
print("ERROR: still no bytes after synchronisation")
42+
43+
picam2.stop_encoder(encoder)
44+
picam2.stop()

tests/test_list.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ examples/still_during_video.py
4343
examples/switch_mode.py
4444
examples/switch_mode_2.py
4545
examples/switch_mode_persist.py
46+
examples/sync_capture.py
47+
examples/sync_recording.py
4648
examples/timestamp_capture.py
4749
examples/title_bar.py
4850
examples/tuning_file.py
@@ -85,6 +87,7 @@ tests/preview_location_test.py
8587
tests/preview_start_stop.py
8688
tests/quality_check.py
8789
tests/qt_gl_preview_test.py
90+
tests/sync_test.py
8891
tests/stop_slow_framerate.py
8992
tests/allocator_test.py
9093
tests/allocator_leak_test.py

0 commit comments

Comments
 (0)