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 //