Skip to content

Commit 382be3a

Browse files
committed
Add SplittableOutput
The SplittableOutput allows switching seamlessly from one output to another without dropping any frames. The switch-overs can be arranged to happen at video keyframes, so as to ensure that the final pieces can be played in isolation. Signed-off-by: David Plowman <david.plowman@raspberrypi.com>
1 parent 8cff1ef commit 382be3a

File tree

5 files changed

+155
-0
lines changed

5 files changed

+155
-0
lines changed

examples/split_output.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#!/usr/bin/python3
2+
3+
import time
4+
5+
from picamera2 import Picamera2
6+
from picamera2.encoders import H264Encoder
7+
from picamera2.outputs import PyavOutput, SplittableOutput
8+
9+
picam2 = Picamera2()
10+
config = picam2.create_video_configuration()
11+
picam2.configure(config)
12+
encoder = H264Encoder(bitrate=5000000)
13+
14+
splitter = SplittableOutput(output=PyavOutput("test0.mp4"))
15+
16+
# Start writing initially to one file, and then we'll switch seamlessly to another.
17+
# You can omit the initial output, and use split_output later to start the first output.
18+
picam2.start_recording(encoder, splitter)
19+
20+
time.sleep(5)
21+
22+
# This returns only when the split has happened and the first file has been closed.
23+
# The second file is guaranteed to continue at an I frame with no frame drops.
24+
print("Waiting for switchover...")
25+
splitter.split_output(PyavOutput("test1.mp4"))
26+
print("Switched to new output!")
27+
28+
time.sleep(5)
29+
30+
picam2.stop_recording()

picamera2/outputs/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
from .fileoutput import FileOutput
55
from .output import Output
66
from .pyavoutput import PyavOutput
7+
from .splittableoutput import SplittableOutput

picamera2/outputs/splittableoutput.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from threading import Event
2+
3+
from .output import Output
4+
5+
6+
class SplittableOutput(Output):
7+
"""
8+
The SplittableOutput passes the encoded bitstream to another output (or drops them if there isn't one).
9+
10+
It can be told to "split" the current output that it's been writing to, to another one. This means
11+
it switches seamlessly from the current output, which is closed, to a new one, without dropping
12+
any frames. By default, it performs the switch at a video keyframem though it can be told not to
13+
wait for one (by setting wait_for_keyframe to False).
14+
"""
15+
16+
def __init__(self, output=None, *args, **kwargs):
17+
super().__init__(*args, **kwargs)
18+
self._output = output
19+
self._new_output = None
20+
self._split_done = Event()
21+
self._streams = []
22+
23+
def split_output(self, new_output, wait_for_keyframe=True):
24+
"""
25+
Terminate the current output, switching seamlessly to the new one.
26+
27+
If wait_for_keyframe is True, the switch only occurs when the next video keyframe is
28+
seen. Otherwise the switch happens immediately.
29+
30+
THe function returns only when the switch has happened, which may entail waiting for a
31+
video keyframe, and the old output has been closed.
32+
"""
33+
old_output = self._output
34+
# Start the new outoput in this thread, then schedule outputframe to make the switch.
35+
new_output.start()
36+
for encoder_stream, codec, kwargs in self._streams:
37+
new_output._add_stream(encoder_stream, codec, **kwargs)
38+
self._wait_for_keyframe = wait_for_keyframe
39+
self._new_output = new_output
40+
# Wait for the switch-over to happen, and close the old output in this thread too.
41+
self._split_done.wait()
42+
self._split_done.clear()
43+
if old_output:
44+
old_output.stop()
45+
46+
def outputframe(self, frame, keyframe=True, timestamp=None, packet=None, audio=False):
47+
# Audio frames probably always say they're keyframes, but we must wait for a video one.
48+
if self._new_output and (not self._wait_for_keyframe or (not audio and keyframe)):
49+
self._split_done.set()
50+
# split_output will close the old output.
51+
self._output = self._new_output
52+
self._new_output = None
53+
if self._output:
54+
self._output.outputframe(frame, keyframe, timestamp, packet, audio)
55+
56+
def start(self):
57+
super().start()
58+
if self._output:
59+
self._output.start()
60+
61+
def stop(self):
62+
super().stop()
63+
if self._output:
64+
self._output.stop()
65+
66+
def _add_stream(self, encoder_stream, codec_name, **kwargs):
67+
# The underlying output may need to know what streams it's dealing with, so we must
68+
# remember them.
69+
self._streams.append((encoder_stream, codec_name, kwargs))
70+
# Forward immediately to the output if we were given one initially.
71+
if self._output:
72+
self._output._add_stream(encoder_stream, codec_name, **kwargs)

tests/split_output_test.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/python3
2+
3+
# We're going to encode a video stream, splitting it seamlessly into a pair of
4+
# files using the SplittableOutput. At the same time we'll write a single output
5+
# file as well, and check that the split files contain as much as the single file.
6+
7+
import io
8+
import time
9+
10+
from picamera2 import Picamera2
11+
from picamera2.encoders import H264Encoder
12+
from picamera2.outputs import FileOutput, SplittableOutput
13+
14+
picam2 = Picamera2()
15+
config = picam2.create_video_configuration()
16+
picam2.configure(config)
17+
18+
first_half = io.BytesIO()
19+
second_half = io.BytesIO()
20+
whole = io.BytesIO()
21+
22+
encoder = H264Encoder(bitrate=5000000)
23+
splitter = SplittableOutput(output=FileOutput(first_half))
24+
output = FileOutput(whole)
25+
encoder.output = [splitter, output]
26+
27+
picam2.start_encoder(encoder)
28+
picam2.start()
29+
30+
time.sleep(5)
31+
32+
splitter.split_output(FileOutput(second_half))
33+
34+
time.sleep(5)
35+
36+
picam2.stop_recording()
37+
38+
first_half_len = first_half.getbuffer().nbytes
39+
second_half_len = second_half.getbuffer().nbytes
40+
combined_len = first_half_len + second_half_len
41+
whole_len = whole.getbuffer().nbytes
42+
43+
print("First half:", first_half_len)
44+
print("Second half:", second_half_len)
45+
print("Both halves combined:", combined_len)
46+
print("Whole output:", whole_len)
47+
48+
if combined_len != whole_len:
49+
print("Error: split files do not match the whole file")
50+
else:
51+
print("Files match!")

tests/test_list.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,6 @@ tests/stop_slow_framerate.py
9494
tests/allocator_test.py
9595
tests/allocator_leak_test.py
9696
tests/wait_cancel_test.py
97+
tests/split_output_test.py
9798
tests/stride_test.py
9899
tests/yuv_capture.py

0 commit comments

Comments
 (0)