diff --git a/examples/playback_external_sync/main.c b/examples/playback_external_sync/main.c
index 49de96f6..03d5e4e0 100644
--- a/examples/playback_external_sync/main.c
+++ b/examples/playback_external_sync/main.c
@@ -51,8 +51,7 @@ static void print_capture_info(recording_t *file)
{
if (images[i] != NULL)
{
- uint64_t timestamp = k4a_image_get_device_timestamp_usec(images[i]) +
- (uint64_t)file->record_config.start_timestamp_offset_usec;
+ uint64_t timestamp = k4a_image_get_device_timestamp_usec(images[i]);
printf(" %7ju usec", timestamp);
k4a_image_release(images[i]);
images[i] = NULL;
@@ -162,11 +161,7 @@ int main(int argc, char **argv)
{
if (files[i].capture != NULL)
{
- // All recording files start at timestamp 0, however the first timestamp off the camera is usually
- // non-zero. We need to add the recording "start offset" back to the recording timestamp to recover
- // the original timestamp from the device, and synchronize the files.
- uint64_t timestamp = first_capture_timestamp(files[i].capture) +
- files[i].record_config.start_timestamp_offset_usec;
+ uint64_t timestamp = first_capture_timestamp(files[i].capture);
if (timestamp < min_timestamp)
{
min_timestamp = timestamp;
diff --git a/examples/transformation/main.cpp b/examples/transformation/main.cpp
index f699983f..246a8558 100644
--- a/examples/transformation/main.cpp
+++ b/examples/transformation/main.cpp
@@ -273,7 +273,7 @@ static int playback(char *input_path, int timestamp = 1000, std::string output_f
}
printf("Seeking to timestamp: %d/%d (ms)\n",
timestamp,
- (int)(k4a_playback_get_last_timestamp_usec(playback) / 1000));
+ (int)(k4a_playback_get_recording_length_usec(playback) / 1000));
stream_result = k4a_playback_get_next_capture(playback, &capture);
if (stream_result != K4A_STREAM_RESULT_SUCCEEDED || capture == NULL)
diff --git a/include/k4ainternal/matroska_read.h b/include/k4ainternal/matroska_read.h
index 61901326..04bdbb02 100644
--- a/include/k4ainternal/matroska_read.h
+++ b/include/k4ainternal/matroska_read.h
@@ -121,7 +121,7 @@ typedef struct _k4a_playback_context_t
uint64_t attachments_offset;
uint64_t tags_offset;
- uint64_t last_timestamp_ns;
+ uint64_t last_file_timestamp_ns; // Relative to start of file.
// Stats
uint64_t seek_count, load_count, cache_hits;
diff --git a/include/k4arecord/playback.h b/include/k4arecord/playback.h
index ef75f7fd..1a92300f 100644
--- a/include/k4arecord/playback.h
+++ b/include/k4arecord/playback.h
@@ -376,10 +376,11 @@ K4ARECORD_EXPORT k4a_stream_result_t k4a_playback_get_previous_imu_sample(k4a_pl
* Handle obtained by k4a_playback_open().
*
* \param offset_usec
- * The timestamp offset to seek to relative to \p origin
+ * The timestamp offset to seek to, relative to \p origin
*
* \param origin
- * Specifies if the seek operation should be done relative to the beginning or end of the recording.
+ * Specifies how the given timestamp should be interpreted. Seek can be done relative to the beginning or end of the
+ * recording, or using an absolute device timestamp.
*
* \returns
* ::K4A_RESULT_SUCCEEDED if the seek operation was successful, or ::K4A_RESULT_FAILED if an error occured. The current
@@ -388,6 +389,10 @@ K4ARECORD_EXPORT k4a_stream_result_t k4a_playback_get_previous_imu_sample(k4a_pl
* \relates k4a_playback_t
*
* \remarks
+ * The first device timestamp in a recording is usually non-zero. The recording file starts at the device timestamp
+ * defined by start_timestamp_offset_usec, which is accessible via k4a_playback_get_record_configuration().
+ *
+ * \remarks
* The first call to k4a_playback_get_next_capture() after k4a_playback_seek_timestamp() will return the first capture
* containing an image timestamp greater than or equal to the seek time.
*
@@ -415,18 +420,19 @@ K4ARECORD_EXPORT k4a_result_t k4a_playback_seek_timestamp(k4a_playback_t playbac
int64_t offset_usec,
k4a_playback_seek_origin_t origin);
-/** Gets the last timestamp in a recording.
+/** Returns the length of the recording in microseconds.
*
* \param playback_handle
* Handle obtained by k4a_playback_open().
*
* \returns
- * The timestamp of the last capture image or IMU sample in microseconds.
+ * The recording length, calculated as the difference between the first and last timestamp in the file.
*
* \relates k4a_playback_t
*
* \remarks
- * Recordings start at timestamp 0, and end at the timestamp returned by k4a_playback_get_last_timestamp_usec().
+ * The recording length may be longer than an individual track if, for example, the IMU continues to run after the last
+ * color image is recorded.
*
* \xmlonly
*
@@ -436,7 +442,34 @@ K4ARECORD_EXPORT k4a_result_t k4a_playback_seek_timestamp(k4a_playback_t playbac
*
* \endxmlonly
*/
-K4ARECORD_EXPORT uint64_t k4a_playback_get_last_timestamp_usec(k4a_playback_t playback_handle);
+K4ARECORD_EXPORT uint64_t k4a_playback_get_recording_length_usec(k4a_playback_t playback_handle);
+
+/** Gets the last timestamp in a recording, relative to the start of the recording.
+ *
+ * \param playback_handle
+ * Handle obtained by k4a_playback_open().
+ *
+ * \returns
+ * The file timestamp of the last capture image or IMU sample in microseconds.
+ *
+ * \relates k4a_playback_t
+ *
+ * \remarks
+ * This function returns a file timestamp, not an absolute device timestamp, meaning it is relative to the start of the
+ * recording. This function is equivalent to the length of the recording.
+ *
+ * \deprecated
+ * Deprecated starting in 1.2.0. Please use k4a_playback_get_recording_length_usec().
+ *
+ * \xmlonly
+ *
+ * playback.h (include k4arecord/playback.h)
+ * k4arecord.lib
+ * k4arecord.dll
+ *
+ * \endxmlonly
+ */
+K4ARECORD_DEPRECATED_EXPORT uint64_t k4a_playback_get_last_timestamp_usec(k4a_playback_t playback_handle);
/** Closes a recording playback handle.
*
diff --git a/include/k4arecord/playback.hpp b/include/k4arecord/playback.hpp
index 5d5cd858..dc21174c 100644
--- a/include/k4arecord/playback.hpp
+++ b/include/k4arecord/playback.hpp
@@ -288,11 +288,11 @@ public:
/** Get the last valid timestamp in the recording
*
- * \sa k4a_playback_get_last_timestamp_usec
+ * \sa k4a_playback_get_recording_length_usec
*/
- std::chrono::microseconds get_last_timestamp() const noexcept
+ std::chrono::microseconds get_recording_length() const noexcept
{
- return std::chrono::microseconds(k4a_playback_get_last_timestamp_usec(m_handle));
+ return std::chrono::microseconds(k4a_playback_get_recording_length_usec(m_handle));
}
/** Set the image format that color captures will be converted to. By default the conversion format will be the same
diff --git a/include/k4arecord/types.h b/include/k4arecord/types.h
index 1baa7c9e..4ffb61c2 100644
--- a/include/k4arecord/types.h
+++ b/include/k4arecord/types.h
@@ -80,8 +80,9 @@ typedef enum
*/
typedef enum
{
- K4A_PLAYBACK_SEEK_BEGIN, /**< Seek relative to the beginning of a recording. */
- K4A_PLAYBACK_SEEK_END /**< Seek relative to the end of a recording. */
+ K4A_PLAYBACK_SEEK_BEGIN, /**< Seek relative to the beginning of a recording. */
+ K4A_PLAYBACK_SEEK_END, /**< Seek relative to the end of a recording. */
+ K4A_PLAYBACK_SEEK_DEVICE_TIME /**< Seek to an absolute device timestamp. */
} k4a_playback_seek_origin_t;
/**
diff --git a/src/record/internal/matroska_read.cpp b/src/record/internal/matroska_read.cpp
index 20b27c7b..40e46758 100644
--- a/src/record/internal/matroska_read.cpp
+++ b/src/record/internal/matroska_read.cpp
@@ -229,7 +229,7 @@ k4a_result_t parse_mkv(k4a_playback_context_t *context)
RETURN_IF_ERROR(populate_cluster_cache(context));
// Find the last timestamp in the file
- context->last_timestamp_ns = 0;
+ context->last_file_timestamp_ns = 0;
cluster_info_t *cluster_info = find_cluster(context, UINT64_MAX);
if (cluster_info == NULL)
{
@@ -251,9 +251,9 @@ k4a_result_t parse_mkv(k4a_playback_context_t *context)
{
simple_block->SetParent(*last_cluster);
uint64_t block_timestamp_ns = simple_block->GlobalTimecode();
- if (block_timestamp_ns > context->last_timestamp_ns)
+ if (block_timestamp_ns > context->last_file_timestamp_ns)
{
- context->last_timestamp_ns = block_timestamp_ns;
+ context->last_file_timestamp_ns = block_timestamp_ns;
}
}
else if (check_element_type(e, &block_group))
@@ -281,13 +281,13 @@ k4a_result_t parse_mkv(k4a_playback_context_t *context)
block_timestamp_ns += block_duration_ns - 1;
}
}
- if (block_timestamp_ns > context->last_timestamp_ns)
+ if (block_timestamp_ns > context->last_file_timestamp_ns)
{
- context->last_timestamp_ns = block_timestamp_ns;
+ context->last_file_timestamp_ns = block_timestamp_ns;
}
}
}
- LOG_TRACE("Found last timestamp: %llu", context->last_timestamp_ns);
+ LOG_TRACE("Found last file timestamp: %llu", context->last_file_timestamp_ns);
return K4A_RESULT_SUCCEEDED;
}
@@ -1750,7 +1750,9 @@ k4a_result_t convert_block_to_image(k4a_playback_context_t *context,
&free_vector_buffer,
buffer,
image_out));
- k4a_image_set_device_timestamp_usec(*image_out, in_block->timestamp_ns / 1000);
+ uint64_t device_timestamp_usec = in_block->timestamp_ns / 1000 +
+ (uint64_t)context->record_config.start_timestamp_offset_usec;
+ k4a_image_set_device_timestamp_usec(*image_out, device_timestamp_usec);
}
if (K4A_FAILED(result) && buffer != NULL)
@@ -2041,6 +2043,11 @@ k4a_stream_result_t get_imu_sample(k4a_playback_context_t *context, k4a_imu_samp
else
{
// The timestamp we're looking for is within the found block.
+ // IMU timestamps within the sample buffer are device timestamps, not relative to start of file.
+ // The seek timestamp needs to be converted to a device timestamp when comparing.
+ uint64_t seek_device_timestamp_ns = context->seek_timestamp_ns +
+ ((uint64_t)context->record_config.start_timestamp_offset_usec *
+ 1000);
context->imu_sample_index = -1;
for (size_t i = 0; i < sample_count; i++)
{
@@ -2051,7 +2058,7 @@ k4a_stream_result_t get_imu_sample(k4a_playback_context_t *context, k4a_imu_samp
*imu_sample = { 0 };
return K4A_STREAM_RESULT_FAILED;
}
- else if (sample->acc_timestamp_ns >= context->seek_timestamp_ns)
+ else if (sample->acc_timestamp_ns >= seek_device_timestamp_ns)
{
context->imu_sample_index = next ? (int)i : (int)i - 1;
break;
diff --git a/src/record/sdk/playback.cpp b/src/record/sdk/playback.cpp
index 0de4fb23..26c26e5b 100644
--- a/src/record/sdk/playback.cpp
+++ b/src/record/sdk/playback.cpp
@@ -297,7 +297,16 @@ k4a_result_t k4a_playback_seek_timestamp(k4a_playback_t playback_handle,
k4a_playback_context_t *context = k4a_playback_t_get_context(playback_handle);
RETURN_VALUE_IF_ARG(K4A_RESULT_FAILED, context == NULL);
RETURN_VALUE_IF_ARG(K4A_RESULT_FAILED, context->segment == nullptr);
- RETURN_VALUE_IF_ARG(K4A_RESULT_FAILED, origin != K4A_PLAYBACK_SEEK_BEGIN && origin != K4A_PLAYBACK_SEEK_END);
+ RETURN_VALUE_IF_ARG(K4A_RESULT_FAILED,
+ origin != K4A_PLAYBACK_SEEK_BEGIN && origin != K4A_PLAYBACK_SEEK_END &&
+ origin != K4A_PLAYBACK_SEEK_DEVICE_TIME);
+
+ // If seeking to a device timestamp, calculate the offset relative to the start of file.
+ if (origin == K4A_PLAYBACK_SEEK_DEVICE_TIME)
+ {
+ origin = K4A_PLAYBACK_SEEK_BEGIN;
+ offset_usec -= (int64_t)context->record_config.start_timestamp_offset_usec;
+ }
// Clamp the offset timestamp so the seek direction is correct reletive to the specified origin.
if (origin == K4A_PLAYBACK_SEEK_BEGIN && offset_usec < 0)
@@ -313,14 +322,14 @@ k4a_result_t k4a_playback_seek_timestamp(k4a_playback_t playback_handle,
if (origin == K4A_PLAYBACK_SEEK_END)
{
uint64_t offset_ns = (uint64_t)(-offset_usec * 1000);
- if (offset_ns > context->last_timestamp_ns)
+ if (offset_ns > context->last_file_timestamp_ns)
{
// If the target timestamp is negative, clamp to 0 so we don't underflow.
target_time_ns = 0;
}
else
{
- target_time_ns = context->last_timestamp_ns + 1 - offset_ns;
+ target_time_ns = context->last_file_timestamp_ns + 1 - offset_ns;
}
}
else
@@ -348,13 +357,22 @@ k4a_result_t k4a_playback_seek_timestamp(k4a_playback_t playback_handle,
return K4A_RESULT_SUCCEEDED;
}
+uint64_t k4a_playback_get_recording_length_usec(k4a_playback_t playback_handle)
+{
+ RETURN_VALUE_IF_HANDLE_INVALID(0, k4a_playback_t, playback_handle);
+
+ k4a_playback_context_t *context = k4a_playback_t_get_context(playback_handle);
+ RETURN_VALUE_IF_ARG(0, context == NULL);
+ return context->last_file_timestamp_ns / 1000;
+}
+
uint64_t k4a_playback_get_last_timestamp_usec(k4a_playback_t playback_handle)
{
RETURN_VALUE_IF_HANDLE_INVALID(0, k4a_playback_t, playback_handle);
k4a_playback_context_t *context = k4a_playback_t_get_context(playback_handle);
RETURN_VALUE_IF_ARG(0, context == NULL);
- return context->last_timestamp_ns / 1000;
+ return context->last_file_timestamp_ns / 1000;
}
void k4a_playback_close(const k4a_playback_t playback_handle)
diff --git a/src/record/sdk/record.cpp b/src/record/sdk/record.cpp
index b645db54..36f03028 100644
--- a/src/record/sdk/record.cpp
+++ b/src/record/sdk/record.cpp
@@ -543,6 +543,13 @@ k4a_result_t k4a_record_write_imu_sample(const k4a_record_t recording_handle, k4
k4a_record_context_t *context = k4a_record_t_get_context(recording_handle);
RETURN_VALUE_IF_ARG(K4A_RESULT_FAILED, context == NULL);
+ if (!context->imu_track)
+ {
+ LOG_ERROR("The IMU track needs to be added with k4a_record_add_imu_track() before IMU samples can be written.",
+ 0);
+ return K4A_RESULT_FAILED;
+ }
+
if (!context->header_written)
{
LOG_ERROR("The recording header needs to be written before any imu samples.", 0);
diff --git a/tests/RecordTests/UnitTest/playback_perf.cpp b/tests/RecordTests/UnitTest/playback_perf.cpp
index 8cea8547..bd76cb90 100644
--- a/tests/RecordTests/UnitTest/playback_perf.cpp
+++ b/tests/RecordTests/UnitTest/playback_perf.cpp
@@ -110,6 +110,28 @@ TEST_F(playback_perf, test_open)
}
}
+ if (config.imu_track_enabled)
+ {
+ k4a_imu_sample_t imu_sample = { 0 };
+ k4a_stream_result_t playback_result = k4a_playback_get_next_imu_sample(handle, &imu_sample);
+ ASSERT_NE(playback_result, K4A_STREAM_RESULT_FAILED);
+ if (playback_result == K4A_STREAM_RESULT_EOF)
+ {
+ std::cout << "No IMU data in recording." << std::endl;
+ }
+ else
+ {
+ std::cout << std::endl;
+ std::cout << "First IMU sample:" << std::endl;
+ std::cout << " Accel Timestamp: " << imu_sample.acc_timestamp_usec << " usec" << std::endl;
+ std::cout << " Accel Data: (" << imu_sample.acc_sample.xyz.x << ", " << imu_sample.acc_sample.xyz.y
+ << ", " << imu_sample.acc_sample.xyz.z << ")" << std::endl;
+ std::cout << " Gyro Timestamp: " << imu_sample.gyro_timestamp_usec << " usec" << std::endl;
+ std::cout << " Gyro Data: (" << imu_sample.gyro_sample.xyz.x << ", " << imu_sample.gyro_sample.xyz.y
+ << ", " << imu_sample.gyro_sample.xyz.z << ")" << std::endl;
+ }
+ }
+
k4a_playback_close(handle);
}
diff --git a/tests/RecordTests/UnitTest/playback_ut.cpp b/tests/RecordTests/UnitTest/playback_ut.cpp
index 9d20126c..3419a8f4 100644
--- a/tests/RecordTests/UnitTest/playback_ut.cpp
+++ b/tests/RecordTests/UnitTest/playback_ut.cpp
@@ -93,7 +93,7 @@ TEST_F(playback_ut, open_large_file)
k4a_stream_result_t stream_result = K4A_STREAM_RESULT_FAILED;
uint64_t timestamps[3] = { 0, 1000, 1000 };
uint64_t timestamp_delta = 1000000 / k4a_convert_fps_to_uint(config.camera_fps);
- int i = 0;
+ size_t i = 0;
for (; i < 50; i++)
{
stream_result = k4a_playback_get_next_capture(handle, &capture);
@@ -130,7 +130,7 @@ TEST_F(playback_ut, open_large_file)
timestamps[1] += timestamp_delta;
timestamps[2] += timestamp_delta;
}
- for (; i < 100; i++)
+ for (; i < test_frame_count; i++)
{
stream_result = k4a_playback_get_next_capture(handle, &capture);
ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
@@ -180,7 +180,7 @@ TEST_F(playback_ut, open_delay_offset_file)
uint64_t timestamp_delta = 1000000 / k4a_convert_fps_to_uint(config.camera_fps);
// Read forward
- for (int i = 0; i < 100; i++)
+ for (size_t i = 0; i < test_frame_count; i++)
{
stream_result = k4a_playback_get_next_capture(handle, &capture);
ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
@@ -199,7 +199,7 @@ TEST_F(playback_ut, open_delay_offset_file)
ASSERT_EQ(capture, (k4a_capture_t)NULL);
// Read backward
- for (int i = 0; i < 100; i++)
+ for (size_t i = 0; i < test_frame_count; i++)
{
timestamps[0] -= timestamp_delta;
timestamps[1] -= timestamp_delta;
@@ -241,6 +241,16 @@ TEST_F(playback_ut, open_subordinate_delay_file)
ASSERT_EQ(config.depth_delay_off_color_usec, 0);
ASSERT_EQ(config.wired_sync_mode, K4A_WIRED_SYNC_MODE_SUBORDINATE);
ASSERT_EQ(config.subordinate_delay_off_master_usec, (uint32_t)10000);
+ ASSERT_EQ(config.start_timestamp_offset_usec, (uint32_t)10000);
+
+ uint64_t timestamps[3] = { 10000, 10000, 10000 };
+
+ k4a_capture_t capture = NULL;
+ k4a_stream_result_t stream_result = k4a_playback_get_next_capture(handle, &capture);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
+ ASSERT_TRUE(
+ validate_test_capture(capture, timestamps, config.color_format, config.color_resolution, config.depth_mode));
+ k4a_capture_release(capture);
k4a_playback_close(handle);
}
@@ -295,7 +305,7 @@ TEST_F(playback_ut, playback_seek_test)
ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
ASSERT_TRUE(validate_imu_sample(imu_sample, imu_timestamp));
- int64_t recording_length = (int64_t)k4a_playback_get_last_timestamp_usec(handle) + 1;
+ int64_t recording_length = (int64_t)k4a_playback_get_recording_length_usec(handle) + 1;
std::pair start_seek_combinations[] = { // Beginning
{ 0, K4A_PLAYBACK_SEEK_BEGIN },
{ -recording_length,
@@ -535,9 +545,7 @@ TEST_F(playback_ut, open_skipped_frames_file)
k4a_capture_t capture = NULL;
k4a_stream_result_t stream_result = K4A_STREAM_RESULT_FAILED;
- uint64_t timestamps[3] = { 1000000 - config.start_timestamp_offset_usec,
- 1001000 - config.start_timestamp_offset_usec,
- 1001000 - config.start_timestamp_offset_usec };
+ uint64_t timestamps[3] = { 1000000, 1001000, 1001000 };
uint64_t timestamp_delta = 1000000 / k4a_convert_fps_to_uint(config.camera_fps);
// Test initial state
@@ -570,7 +578,7 @@ TEST_F(playback_ut, open_skipped_frames_file)
// Test seek past beginning
result = k4a_playback_seek_timestamp(handle,
- -(int64_t)k4a_playback_get_last_timestamp_usec(handle) - 10,
+ -(int64_t)k4a_playback_get_recording_length_usec(handle) - 10,
K4A_PLAYBACK_SEEK_END);
ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
@@ -601,7 +609,7 @@ TEST_F(playback_ut, open_skipped_frames_file)
// Test seek to end, relative to start
result = k4a_playback_seek_timestamp(handle,
- (int64_t)k4a_playback_get_last_timestamp_usec(handle) + 1,
+ (int64_t)k4a_playback_get_recording_length_usec(handle) + 1,
K4A_PLAYBACK_SEEK_BEGIN);
ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
@@ -620,7 +628,7 @@ TEST_F(playback_ut, open_skipped_frames_file)
timestamps[0] -= timestamp_delta * 50;
timestamps[1] -= timestamp_delta * 50;
timestamps[2] -= timestamp_delta * 50;
- result = k4a_playback_seek_timestamp(handle, (int64_t)timestamps[0], K4A_PLAYBACK_SEEK_BEGIN);
+ result = k4a_playback_seek_timestamp(handle, (int64_t)timestamps[0], K4A_PLAYBACK_SEEK_DEVICE_TIME);
ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
stream_result = k4a_playback_get_next_capture(handle, &capture);
@@ -631,7 +639,7 @@ TEST_F(playback_ut, open_skipped_frames_file)
k4a_capture_release(capture);
// Test seek to middle of the recording, then read backward
- result = k4a_playback_seek_timestamp(handle, (int64_t)timestamps[0], K4A_PLAYBACK_SEEK_BEGIN);
+ result = k4a_playback_seek_timestamp(handle, (int64_t)timestamps[0], K4A_PLAYBACK_SEEK_DEVICE_TIME);
ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
timestamps[0] -= timestamp_delta;
@@ -646,7 +654,7 @@ TEST_F(playback_ut, open_skipped_frames_file)
k4a_capture_release(capture);
// Read the rest of the file
- for (int i = 49; i < 100; i++)
+ for (size_t i = 49; i < test_frame_count; i++)
{
timestamps[0] += timestamp_delta;
timestamps[1] += timestamp_delta;
@@ -721,11 +729,11 @@ TEST_F(playback_ut, open_imu_playback_file)
k4a_imu_sample_t imu_sample = { 0 };
k4a_stream_result_t stream_result = K4A_STREAM_RESULT_FAILED;
uint64_t imu_timestamp = 1150;
- uint64_t last_timestamp = k4a_playback_get_last_timestamp_usec(handle);
- ASSERT_EQ(last_timestamp, 3333150);
+ uint64_t recording_length = k4a_playback_get_recording_length_usec(handle);
+ ASSERT_EQ(recording_length, 3333150);
// Read forward
- while (imu_timestamp <= last_timestamp)
+ while (imu_timestamp <= recording_length)
{
stream_result = k4a_playback_get_next_imu_sample(handle, &imu_sample);
ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
@@ -749,7 +757,7 @@ TEST_F(playback_ut, open_imu_playback_file)
ASSERT_TRUE(validate_null_imu_sample(imu_sample));
// Test seeking to first 100 samples (covers edge cases around block boundaries)
- for (int i = 0; i < 100; i++)
+ for (size_t i = 0; i < test_frame_count; i++)
{
// Seek to before sample
result = k4a_playback_seek_timestamp(handle, (int64_t)imu_timestamp - 100, K4A_PLAYBACK_SEEK_BEGIN);
@@ -781,6 +789,204 @@ TEST_F(playback_ut, open_imu_playback_file)
k4a_playback_close(handle);
}
+TEST_F(playback_ut, open_start_offset_file)
+{
+ k4a_playback_t handle = NULL;
+ k4a_result_t result = k4a_playback_open("record_test_offset.mkv", &handle);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ // Read recording configuration
+ k4a_record_configuration_t config;
+ result = k4a_playback_get_record_configuration(handle, &config);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+ ASSERT_EQ(config.color_format, K4A_IMAGE_FORMAT_COLOR_MJPG);
+ ASSERT_EQ(config.color_resolution, K4A_COLOR_RESOLUTION_1080P);
+ ASSERT_EQ(config.depth_mode, K4A_DEPTH_MODE_NFOV_UNBINNED);
+ ASSERT_EQ(config.camera_fps, K4A_FRAMES_PER_SECOND_30);
+ ASSERT_TRUE(config.color_track_enabled);
+ ASSERT_TRUE(config.depth_track_enabled);
+ ASSERT_TRUE(config.ir_track_enabled);
+ ASSERT_TRUE(config.imu_track_enabled);
+ ASSERT_EQ(config.depth_delay_off_color_usec, 0);
+ ASSERT_EQ(config.wired_sync_mode, K4A_WIRED_SYNC_MODE_STANDALONE);
+ ASSERT_EQ(config.subordinate_delay_off_master_usec, (uint32_t)0);
+ ASSERT_EQ(config.start_timestamp_offset_usec, (uint32_t)1000000);
+
+ k4a_capture_t capture = NULL;
+ k4a_imu_sample_t imu_sample = { 0 };
+ k4a_stream_result_t stream_result = K4A_STREAM_RESULT_FAILED;
+ uint64_t timestamps[3] = { 1000000, 1000000, 1000000 };
+ uint64_t imu_timestamp = 1001150;
+ uint64_t timestamp_delta = 1000000 / k4a_convert_fps_to_uint(config.camera_fps);
+ uint64_t last_timestamp = k4a_playback_get_recording_length_usec(handle) +
+ (uint64_t)config.start_timestamp_offset_usec;
+ ASSERT_EQ(last_timestamp, (uint64_t)config.start_timestamp_offset_usec + 3333150);
+
+ // Read capture forward
+ for (size_t i = 0; i < test_frame_count; i++)
+ {
+ stream_result = k4a_playback_get_next_capture(handle, &capture);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
+ ASSERT_TRUE(validate_test_capture(capture,
+ timestamps,
+ config.color_format,
+ config.color_resolution,
+ config.depth_mode));
+ k4a_capture_release(capture);
+ timestamps[0] += timestamp_delta;
+ timestamps[1] += timestamp_delta;
+ timestamps[2] += timestamp_delta;
+ }
+ stream_result = k4a_playback_get_next_capture(handle, &capture);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_EOF);
+ ASSERT_EQ(capture, (k4a_capture_t)NULL);
+
+ // Read capture backward
+ for (size_t i = 0; i < test_frame_count; i++)
+ {
+ timestamps[0] -= timestamp_delta;
+ timestamps[1] -= timestamp_delta;
+ timestamps[2] -= timestamp_delta;
+ stream_result = k4a_playback_get_previous_capture(handle, &capture);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
+ ASSERT_TRUE(validate_test_capture(capture,
+ timestamps,
+ config.color_format,
+ config.color_resolution,
+ config.depth_mode));
+ k4a_capture_release(capture);
+ }
+ stream_result = k4a_playback_get_previous_capture(handle, &capture);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_EOF);
+ ASSERT_EQ(capture, (k4a_capture_t)NULL);
+
+ // Read IMU forward
+ while (imu_timestamp <= last_timestamp)
+ {
+ stream_result = k4a_playback_get_next_imu_sample(handle, &imu_sample);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
+ ASSERT_TRUE(validate_imu_sample(imu_sample, imu_timestamp));
+ imu_timestamp += 1000;
+ }
+ stream_result = k4a_playback_get_next_imu_sample(handle, &imu_sample);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_EOF);
+ ASSERT_TRUE(validate_null_imu_sample(imu_sample));
+
+ // Read IMU backward
+ while (imu_timestamp > 1001150)
+ {
+ imu_timestamp -= 1000;
+ stream_result = k4a_playback_get_previous_imu_sample(handle, &imu_sample);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
+ ASSERT_TRUE(validate_imu_sample(imu_sample, imu_timestamp));
+ }
+ stream_result = k4a_playback_get_previous_imu_sample(handle, &imu_sample);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_EOF);
+ ASSERT_TRUE(validate_null_imu_sample(imu_sample));
+
+ // Test seeking to first 100 samples (covers edge cases around block boundaries)
+ for (size_t i = 0; i < test_frame_count; i++)
+ {
+ // Seek to before sample
+ result = k4a_playback_seek_timestamp(handle, (int64_t)imu_timestamp - 100, K4A_PLAYBACK_SEEK_DEVICE_TIME);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ stream_result = k4a_playback_get_next_imu_sample(handle, &imu_sample);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
+ ASSERT_TRUE(validate_imu_sample(imu_sample, imu_timestamp));
+
+ // Seek exactly to sample
+ result = k4a_playback_seek_timestamp(handle, (int64_t)imu_timestamp, K4A_PLAYBACK_SEEK_DEVICE_TIME);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ stream_result = k4a_playback_get_next_imu_sample(handle, &imu_sample);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
+ ASSERT_TRUE(validate_imu_sample(imu_sample, imu_timestamp));
+
+ // Seek to after sample
+ result = k4a_playback_seek_timestamp(handle, (int64_t)imu_timestamp + 100, K4A_PLAYBACK_SEEK_DEVICE_TIME);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ stream_result = k4a_playback_get_previous_imu_sample(handle, &imu_sample);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
+ ASSERT_TRUE(validate_imu_sample(imu_sample, imu_timestamp));
+
+ imu_timestamp += 1000;
+ }
+
+ k4a_playback_close(handle);
+}
+
+TEST_F(playback_ut, open_color_only_file)
+{
+ k4a_playback_t handle = NULL;
+ k4a_result_t result = k4a_playback_open("record_test_color_only.mkv", &handle);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ // Read recording configuration
+ k4a_record_configuration_t config;
+ result = k4a_playback_get_record_configuration(handle, &config);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+ ASSERT_EQ(config.color_format, K4A_IMAGE_FORMAT_COLOR_MJPG);
+ ASSERT_EQ(config.color_resolution, K4A_COLOR_RESOLUTION_1080P);
+ ASSERT_EQ(config.depth_mode, K4A_DEPTH_MODE_OFF);
+ ASSERT_EQ(config.camera_fps, K4A_FRAMES_PER_SECOND_30);
+ ASSERT_TRUE(config.color_track_enabled);
+ ASSERT_FALSE(config.depth_track_enabled);
+ ASSERT_FALSE(config.ir_track_enabled);
+ ASSERT_FALSE(config.imu_track_enabled);
+ ASSERT_EQ(config.depth_delay_off_color_usec, 0);
+ ASSERT_EQ(config.wired_sync_mode, K4A_WIRED_SYNC_MODE_STANDALONE);
+ ASSERT_EQ(config.subordinate_delay_off_master_usec, (uint32_t)0);
+ ASSERT_EQ(config.start_timestamp_offset_usec, (uint32_t)0);
+
+ uint64_t timestamps[3] = { 0, 0, 0 };
+
+ k4a_capture_t capture = NULL;
+ k4a_stream_result_t stream_result = k4a_playback_get_next_capture(handle, &capture);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
+ ASSERT_TRUE(
+ validate_test_capture(capture, timestamps, config.color_format, config.color_resolution, config.depth_mode));
+ k4a_capture_release(capture);
+
+ k4a_playback_close(handle);
+}
+
+TEST_F(playback_ut, open_depth_only_file)
+{
+ k4a_playback_t handle = NULL;
+ k4a_result_t result = k4a_playback_open("record_test_depth_only.mkv", &handle);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ // Read recording configuration
+ k4a_record_configuration_t config;
+ result = k4a_playback_get_record_configuration(handle, &config);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+ ASSERT_EQ(config.color_format, K4A_IMAGE_FORMAT_CUSTOM);
+ ASSERT_EQ(config.color_resolution, K4A_COLOR_RESOLUTION_OFF);
+ ASSERT_EQ(config.depth_mode, K4A_DEPTH_MODE_NFOV_UNBINNED);
+ ASSERT_EQ(config.camera_fps, K4A_FRAMES_PER_SECOND_30);
+ ASSERT_FALSE(config.color_track_enabled);
+ ASSERT_TRUE(config.depth_track_enabled);
+ ASSERT_TRUE(config.ir_track_enabled);
+ ASSERT_FALSE(config.imu_track_enabled);
+ ASSERT_EQ(config.depth_delay_off_color_usec, 0);
+ ASSERT_EQ(config.wired_sync_mode, K4A_WIRED_SYNC_MODE_STANDALONE);
+ ASSERT_EQ(config.subordinate_delay_off_master_usec, (uint32_t)0);
+ ASSERT_EQ(config.start_timestamp_offset_usec, (uint32_t)0);
+
+ uint64_t timestamps[3] = { 0, 0, 0 };
+
+ k4a_capture_t capture = NULL;
+ k4a_stream_result_t stream_result = k4a_playback_get_next_capture(handle, &capture);
+ ASSERT_EQ(stream_result, K4A_STREAM_RESULT_SUCCEEDED);
+ ASSERT_TRUE(
+ validate_test_capture(capture, timestamps, config.color_format, config.color_resolution, config.depth_mode));
+ k4a_capture_release(capture);
+
+ k4a_playback_close(handle);
+}
+
int main(int argc, char **argv)
{
k4a_unittest_init();
diff --git a/tests/RecordTests/UnitTest/sample_recordings.cpp b/tests/RecordTests/UnitTest/sample_recordings.cpp
index b158b38f..4bf7cfce 100644
--- a/tests/RecordTests/UnitTest/sample_recordings.cpp
+++ b/tests/RecordTests/UnitTest/sample_recordings.cpp
@@ -29,6 +29,12 @@ void SampleRecordings::SetUp()
record_config_sub.wired_sync_mode = K4A_WIRED_SYNC_MODE_SUBORDINATE;
record_config_sub.subordinate_delay_off_master_usec = 10000; // 10ms
+ k4a_device_configuration_t record_config_color_only = record_config_full;
+ record_config_color_only.depth_mode = K4A_DEPTH_MODE_OFF;
+
+ k4a_device_configuration_t record_config_depth_only = record_config_full;
+ record_config_depth_only.color_resolution = K4A_COLOR_RESOLUTION_OFF;
+
{
k4a_record_t handle = NULL;
k4a_result_t result = k4a_record_create("record_test_empty.mkv", NULL, record_config_empty, &handle);
@@ -57,7 +63,7 @@ void SampleRecordings::SetUp()
uint64_t imu_timestamp = 1150;
uint32_t timestamp_delta = 1000000 / k4a_convert_fps_to_uint(record_config_full.camera_fps);
k4a_capture_t capture = NULL;
- for (int i = 0; i < 100; i++)
+ for (size_t i = 0; i < test_frame_count; i++)
{
capture = create_test_capture(timestamps,
record_config_full.color_format,
@@ -100,7 +106,7 @@ void SampleRecordings::SetUp()
(uint64_t)record_config_delay.depth_delay_off_color_usec };
uint32_t timestamp_delta = 1000000 / k4a_convert_fps_to_uint(record_config_delay.camera_fps);
k4a_capture_t capture = NULL;
- for (int i = 0; i < 100; i++)
+ for (size_t i = 0; i < test_frame_count; i++)
{
capture = create_test_capture(timestamps,
record_config_delay.color_format,
@@ -120,7 +126,7 @@ void SampleRecordings::SetUp()
k4a_record_close(handle);
}
- {
+ { // Create a recording file with a subordinate delay off master
k4a_record_t handle = NULL;
k4a_result_t result = k4a_record_create("record_test_sub.mkv", NULL, record_config_sub, &handle);
ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
@@ -128,7 +134,9 @@ void SampleRecordings::SetUp()
result = k4a_record_write_header(handle);
ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
- uint64_t timestamps[3] = { 0, 0, 0 };
+ uint64_t timestamps[3] = { record_config_sub.subordinate_delay_off_master_usec,
+ record_config_sub.subordinate_delay_off_master_usec,
+ record_config_sub.subordinate_delay_off_master_usec };
k4a_capture_t capture = create_test_capture(timestamps,
record_config_sub.color_format,
record_config_sub.color_resolution,
@@ -158,7 +166,7 @@ void SampleRecordings::SetUp()
uint64_t timestamps[3] = { 1000000, 1001000, 1001000 }; // Start recording at 1s
uint32_t timestamp_delta = 1000000 / k4a_convert_fps_to_uint(record_config_full.camera_fps);
- for (int i = 0; i < 100; i++)
+ for (size_t i = 0; i < test_frame_count; i++)
{
// Create a known pattern of dropped / missing frames that can be tested against
// The pattern is repeated every 4 captures until the end of the file.
@@ -208,6 +216,95 @@ void SampleRecordings::SetUp()
result = k4a_record_flush(handle);
ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+ k4a_record_close(handle);
+ }
+ { // Create a recording file with a start offset and all tracks enabled
+ k4a_record_t handle = NULL;
+ k4a_result_t result = k4a_record_create("record_test_offset.mkv", NULL, record_config_full, &handle);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ result = k4a_record_add_imu_track(handle);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ result = k4a_record_write_header(handle);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ uint64_t timestamps[3] = { 1000000, 1000000, 1000000 };
+ uint64_t imu_timestamp = 1001150;
+ uint32_t timestamp_delta = 1000000 / k4a_convert_fps_to_uint(record_config_delay.camera_fps);
+ k4a_capture_t capture = NULL;
+ for (size_t i = 0; i < test_frame_count; i++)
+ {
+ capture = create_test_capture(timestamps,
+ record_config_delay.color_format,
+ record_config_delay.color_resolution,
+ record_config_delay.depth_mode);
+ result = k4a_record_write_capture(handle, capture);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+ k4a_capture_release(capture);
+
+ timestamps[0] += timestamp_delta;
+ timestamps[1] += timestamp_delta;
+ timestamps[2] += timestamp_delta;
+
+ while (imu_timestamp < timestamps[0])
+ {
+ k4a_imu_sample_t imu_sample = create_test_imu_sample(imu_timestamp);
+ result = k4a_record_write_imu_sample(handle, imu_sample);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ // Write IMU samples at ~1000 samples per second (this is an arbitrary rate for testing)
+ imu_timestamp += 1000; // 1ms
+ }
+ }
+
+ result = k4a_record_flush(handle);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ k4a_record_close(handle);
+ }
+ { // Create a recording file with only the color camera enabled
+ k4a_record_t handle = NULL;
+ k4a_result_t result = k4a_record_create("record_test_color_only.mkv", NULL, record_config_color_only, &handle);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ result = k4a_record_write_header(handle);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ uint64_t timestamps[3] = { 0, 0, 0 };
+ k4a_capture_t capture = create_test_capture(timestamps,
+ record_config_color_only.color_format,
+ record_config_color_only.color_resolution,
+ record_config_color_only.depth_mode);
+ result = k4a_record_write_capture(handle, capture);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+ k4a_capture_release(capture);
+
+ result = k4a_record_flush(handle);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ k4a_record_close(handle);
+ }
+ { // Create a recording file with only the depth camera enabled
+ k4a_record_t handle = NULL;
+ k4a_result_t result = k4a_record_create("record_test_depth_only.mkv", NULL, record_config_depth_only, &handle);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ result = k4a_record_write_header(handle);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
+ uint64_t timestamps[3] = { 0, 0, 0 };
+ k4a_capture_t capture = create_test_capture(timestamps,
+ record_config_depth_only.color_format,
+ record_config_depth_only.color_resolution,
+ record_config_depth_only.depth_mode);
+ result = k4a_record_write_capture(handle, capture);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+ k4a_capture_release(capture);
+
+ result = k4a_record_flush(handle);
+ ASSERT_EQ(result, K4A_RESULT_SUCCEEDED);
+
k4a_record_close(handle);
}
}
@@ -219,4 +316,7 @@ void SampleRecordings::TearDown()
ASSERT_EQ(std::remove("record_test_delay.mkv"), 0);
ASSERT_EQ(std::remove("record_test_skips.mkv"), 0);
ASSERT_EQ(std::remove("record_test_sub.mkv"), 0);
+ ASSERT_EQ(std::remove("record_test_offset.mkv"), 0);
+ ASSERT_EQ(std::remove("record_test_color_only.mkv"), 0);
+ ASSERT_EQ(std::remove("record_test_depth_only.mkv"), 0);
}
diff --git a/tests/RecordTests/UnitTest/test_helpers.h b/tests/RecordTests/UnitTest/test_helpers.h
index e65a79d0..1d030461 100644
--- a/tests/RecordTests/UnitTest/test_helpers.h
+++ b/tests/RecordTests/UnitTest/test_helpers.h
@@ -24,6 +24,13 @@ static const char *const fps_names[] = { "K4A_FRAMES_PER_SECOND_5",
"K4A_FRAMES_PER_SECOND_15",
"K4A_FRAMES_PER_SECOND_30" };
+// Testing values
+static const uint32_t test_depth_width = 640;
+static const uint32_t test_depth_height = 576;
+static const uint32_t test_camera_fps = 30;
+static const uint32_t test_timestamp_delta_usec = 33333;
+static const size_t test_frame_count = 100;
+
k4a_capture_t create_test_capture(uint64_t timestamp_us[3],
k4a_image_format_t color_format,
k4a_color_resolution_t resolution,
@@ -33,6 +40,7 @@ bool validate_test_capture(k4a_capture_t capture,
k4a_image_format_t color_format,
k4a_color_resolution_t resolution,
k4a_depth_mode_t mode);
+
k4a_image_t
create_test_image(uint64_t timestamp_us, k4a_image_format_t format, uint32_t width, uint32_t height, uint32_t stride);
bool validate_test_image(k4a_image_t image,
@@ -41,6 +49,7 @@ bool validate_test_image(k4a_image_t image,
uint32_t width,
uint32_t height,
uint32_t stride);
+
k4a_imu_sample_t create_test_imu_sample(uint64_t timestamp_us);
bool validate_imu_sample(k4a_imu_sample_t &imu_sample, uint64_t timestamp_us);
bool validate_null_imu_sample(k4a_imu_sample_t &imu_sample);
diff --git a/tools/k4aviewer/k4arecordingdockcontrol.cpp b/tools/k4aviewer/k4arecordingdockcontrol.cpp
index b2ab449d..01f58d03 100644
--- a/tools/k4aviewer/k4arecordingdockcontrol.cpp
+++ b/tools/k4aviewer/k4arecordingdockcontrol.cpp
@@ -112,8 +112,7 @@ K4ARecordingDockControl::K4ARecordingDockControl(std::string &&path, k4a::playba
m_subordinateDelayOffMasterUsec = m_recordConfiguration.subordinate_delay_off_master_usec;
m_startTimestampOffsetUsec = m_recordConfiguration.start_timestamp_offset_usec;
- m_playbackThreadState.TimestampOffset = std::chrono::microseconds(m_startTimestampOffsetUsec);
- m_recordingLengthUsec = static_cast(recording.get_last_timestamp().count());
+ m_recordingLengthUsec = static_cast(recording.get_recording_length().count());
// Device info
//
@@ -361,12 +360,6 @@ bool K4ARecordingDockControl::PlaybackThreadFn(PlaybackThreadState *state)
break;
}
- // Update the timestamps on the IMU samples using the timing data embedded in the recording
- // so we show comparable timestamps when playing back synchronized recordings
- //
- nextImuSample.acc_timestamp_usec += static_cast(state->TimestampOffset.count());
- nextImuSample.gyro_timestamp_usec += static_cast(state->TimestampOffset.count());
-
state->ImuDataSource.NotifyObservers(nextImuSample);
}
}
@@ -392,7 +385,7 @@ bool K4ARecordingDockControl::PlaybackThreadFn(PlaybackThreadState *state)
{
if (image)
{
- image.set_timestamp(image.get_device_timestamp() + state->TimestampOffset);
+ image.set_timestamp(image.get_device_timestamp());
}
}
diff --git a/tools/k4aviewer/k4arecordingdockcontrol.h b/tools/k4aviewer/k4arecordingdockcontrol.h
index caea746d..053fd9ed 100644
--- a/tools/k4aviewer/k4arecordingdockcontrol.h
+++ b/tools/k4aviewer/k4arecordingdockcontrol.h
@@ -54,7 +54,6 @@ private:
// Constant state (expected to be set once, accessible without synchronization)
//
std::chrono::microseconds TimePerFrame;
- std::chrono::microseconds TimestampOffset = std::chrono::microseconds(0);
// Recording state
//