Skip to content

Commit

Permalink
added syncer/test-ts-playback.py to test playback behavior at EOF
Browse files Browse the repository at this point in the history
  • Loading branch information
maloel committed Aug 23, 2021
1 parent 6f452cf commit 174c424
Show file tree
Hide file tree
Showing 2 changed files with 247 additions and 9 deletions.
106 changes: 97 additions & 9 deletions unit-tests/syncer/sw.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import pyrealsense2 as rs
from rspy import log, test
import time


# Constants
Expand All @@ -15,6 +16,8 @@
w = 640
h = 480
bpp = 2 # bytes
#
playback_status = None


def init():
Expand Down Expand Up @@ -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():
Expand All @@ -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 ):
"""
"""
Expand Down Expand Up @@ -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

Expand Down
150 changes: 150 additions & 0 deletions unit-tests/syncer/test-ts-playback.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 174c424

Please sign in to comment.