From 174c4243d50b77845edddeab875dc19e840b2301 Mon Sep 17 00:00:00 2001 From: Eran Date: Mon, 23 Aug 2021 14:51:34 +0300 Subject: [PATCH] added syncer/test-ts-playback.py to test playback behavior at EOF --- unit-tests/syncer/sw.py | 106 ++++++++++++++++-- unit-tests/syncer/test-ts-playback.py | 150 ++++++++++++++++++++++++++ 2 files changed, 247 insertions(+), 9 deletions(-) create mode 100644 unit-tests/syncer/test-ts-playback.py diff --git a/unit-tests/syncer/sw.py b/unit-tests/syncer/sw.py index cff1264ee87..f4b0a1f3813 100644 --- a/unit-tests/syncer/sw.py +++ b/unit-tests/syncer/sw.py @@ -3,6 +3,7 @@ import pyrealsense2 as rs from rspy import log, test +import time # Constants @@ -15,6 +16,8 @@ w = 640 h = 480 bpp = 2 # bytes +# +playback_status = None def init(): @@ -61,6 +64,46 @@ def init(): # global syncer syncer = rs.syncer( 100 ) # We don't want to lose any frames so uses a big queue size (default is 1) + # + global playback_status + playback_status = None + + +def playback_callback( status ): + """ + """ + global playback_status + playback_status = status + log.d( "...", status ) + + +def playback( filename, use_syncer = True ): + """ + """ + ctx = rs.context() + # + global device + device = rs.playback( ctx.load_device( filename ) ) + device.set_real_time( False ) + device.set_status_changed_callback( playback_callback ) + # + global depth_sensor, color_sensor + sensors = device.query_sensors() + depth_sensor = next( s for s in sensors if s.name == "Depth" ) + color_sensor = next( s for s in sensors if s.name == "Color" ) + # + global depth_profile, color_profile + depth_profile = next( p for p in depth_sensor.profiles if p.stream_type() == rs.stream.depth ) + color_profile = next( p for p in color_sensor.profiles if p.stream_type() == rs.stream.color ) + # + global syncer + if use_syncer: + syncer = rs.syncer( 100 ) # We don't want to lose any frames so uses a big queue size (default is 1) + else: + syncer = rs.frame_queue( 100 ) + # + global playback_status + playback_status = rs.playback_status.unknown def start(): @@ -73,6 +116,32 @@ def start(): color_sensor.start( syncer ) +def stop(): + """ + """ + global depth_profile, color_profile, depth_sensor, color_sensor + color_sensor.stop() + depth_sensor.stop() + color_sensor.close() + depth_sensor.close() + + +def reset(): + """ + """ + global depth_profile, color_profile, depth_sensor, color_sensor + color_sensor = None + depth_sensor = None + depth_profile = None + color_profile = None + # + global device + device = None + # + global syncer + syncer = None + + def generate_depth_frame( frame_number, timestamp ): """ """ @@ -116,31 +185,50 @@ def expect( depth_frame = None, color_frame = None, nothing_else = False ): Looks at the syncer queue and gets the next frame from it if available, checking its contents against the expected frame numbers. """ - global syncer - fs = syncer.poll_for_frames() # NOTE: will never be None - if not fs: + global syncer, playback_status + f = syncer.poll_for_frame() + if playback_status is not None: + countdown = 50 # 5 seconds + while not f and playback_status != rs.playback_status.stopped: + countdown -= 1 + if countdown == 0: + break + time.sleep( 0.1 ) + f = syncer.poll_for_frame() + # NOTE: fs will never be None + if not f: test.check( depth_frame is None, "expected a depth frame" ) test.check( color_frame is None, "expected a color frame" ) return False - log.d( "Got", fs ) + log.d( "Got", f ) + + fs = rs.composite_frame( f ) - depth = fs.get_depth_frame() + if fs: + depth = fs.get_depth_frame() + else: + depth = rs.depth_frame( f ) test.info( "actual depth", depth ) test.check_equal( depth_frame is None, not depth ) if depth_frame is not None and depth: test.check_equal( depth.get_frame_number(), depth_frame ) - color = fs.get_color_frame() + if fs: + color = fs.get_color_frame() + elif not depth: + color = rs.video_frame( f ) + else: + color = None test.info( "actual color", color ) test.check_equal( color_frame is None, not color ) if color_frame is not None and color: test.check_equal( color.get_frame_number(), color_frame ) if nothing_else: - fs = syncer.poll_for_frames() - test.info( "Expected nothing else; actual", fs ) - test.check( not fs ) + f = syncer.poll_for_frame() + test.info( "Expected nothing else; actual", f ) + test.check( not f ) return True diff --git a/unit-tests/syncer/test-ts-playback.py b/unit-tests/syncer/test-ts-playback.py new file mode 100644 index 00000000000..efbd0388084 --- /dev/null +++ b/unit-tests/syncer/test-ts-playback.py @@ -0,0 +1,150 @@ +# License: Apache 2.0. See LICENSE file in root directory. +# Copyright(c) 2021 Intel Corporation. All Rights Reserved. + +import pyrealsense2 as rs +from rspy import log, test +import sw + + +# The timestamp jumps are closely correlated to the FPS passed to the video streams: +# syncer expects frames to arrive every 1000/FPS milliseconds! +sw.fps_c = sw.fps_d = 60 +sw.init() + +import tempfile, os +temp_dir = tempfile.TemporaryDirectory( prefix = 'recordings_' ) +filename = os.path.join( temp_dir.name, 'rec.bag' ) +recorder = rs.recorder( filename, sw.device ) + +sw.start() + +############################################################################################# +# +test.start( "Init" ) + +# It can take a few frames for the syncer to actually produce a matched frameset (it doesn't +# know what to match to in the beginning) + +sw.generate_depth_and_color( frame_number = 0, timestamp = 0 ) +sw.expect( depth_frame = 0 ) # syncer doesn't know about color yet +sw.expect( color_frame = 0, nothing_else = True ) # less than next expected of D +# +# NOTE: if the syncer queue wasn't 100 (see above) then we'd only get the color frame! +# +sw.generate_depth_and_color( 1, sw.gap_d * 1 ) +sw.expect( depth_frame = 1, color_frame = 1, nothing_else = True ) # frameset 1 + +test.finish() +# +############################################################################################# +# +test.start( "Keep going" ) + +sw.generate_depth_and_color( 2, sw.gap_d * 2 ) +sw.expect( depth_frame = 2, color_frame = 2, nothing_else = True ) # frameset 2 + +test.finish() +# +############################################################################################# +# +test.start( "Stop giving color; nothing output" ) + +sw.generate_depth_frame( 3, sw.gap_d * 3 ) + +# The depth frame will be kept in the syncer, and never make it out (no matching color frame +# and we're not going to push additional frames that would cause it to eventually flush): +# +sw.expect_nothing() +# +# ... BUT the file should still have it! + +test.finish() +# +############################################################################################# +# +test.start( "Dump the file" ) + +recorder.pause() +recorder = None # otherwise the file will be open when we exit +log.d( "filename=", filename ) +sw.stop() +sw.reset() +# +# Dump it... should look like: +# [Depth/0 #0 @0.000000] +# [Color/1 #0 @0.000000] +# [Depth/0 #1 @16.666667] +# [Color/1 #1 @16.666667] +# [Depth/0 #2 @33.333333] +# [Color/1 #2 @33.333333] +# [Depth/0 #3 @50.000000] <--- the frame that was "lost" +# +import subprocess, sys +for p in sys.path: + rs_convert = os.path.join( p, 'rs-convert.exe' ) + if os.path.isfile( rs_convert ): + subprocess.run( [rs_convert, '-i', filename, '-T'], + stdout=None, + stderr=subprocess.STDOUT, + universal_newlines=True, + timeout=10, + check=True ) + break + +test.finish() +# +############################################################################################# +# +test.start( "Play it back, with syncer -- lose last frame" ) + +sw.playback( filename ) +sw.start() + +sw.expect( depth_frame = 0 ) # syncer doesn't know about color yet +sw.expect( color_frame = 0 ) # less than next expected of D +sw.expect( depth_frame = 1, color_frame = 1 ) +sw.expect( depth_frame = 2, color_frame = 2 ) + +# We know there should be another frame in the file: +# [Depth/0 #3 @50.000000] +# ... but the syncer is keeping it inside, waiting for a matching color frame, and does not +# know that we've reached the EOF. There is a flush when we reach the EOF, but not on the +# syncer -- the playback device knows not that its client is a syncer! +# +#sw.expect( depth_frame = 3 ) +sw.expect_nothing() +# +# There is no API to flush the syncer, but it can easily be added. Or we can implement a +# special frame type, an "end-of-file frame", which would cause the syncer to flush... + +sw.stop() +sw.reset() + +test.finish() +# +############################################################################################# +# +test.start( "Play it back, without syncer -- and now expect the lost frame" ) + +sw.playback( filename, use_syncer = False ) +sw.start() + +sw.expect( depth_frame = 0 ) # none of these is synced (no syncer) +sw.expect( color_frame = 0 ) +sw.expect( depth_frame = 1 ) +sw.expect( color_frame = 1 ) +sw.expect( depth_frame = 2 ) +sw.expect( color_frame = 2 ) + +# This line is the difference from the last test: +# +sw.expect( depth_frame = 3 ) + +sw.expect_nothing() +sw.stop() +sw.reset() + +test.finish() +# +############################################################################################# +test.print_results_and_exit()