-1

I see weird behavior in a program that I am writing using the NewTek NDI SDK for receiving video. It basically receives frames from a video source, and I can then handle each frame individually:

void handle_frame(NDIlib_video_frame_v2_t &video_frame)
{
  printf(
    "Frame received (%dx%d), timestamp: %ld, timecode: %ld, timestamp_ms: %f, timecode_ms: %f, metadata: %s",
    video_frame.xres,
    video_frame.yres,
    video_frame.timestamp,
    video_frame.timecode,
    (float)video_frame.timestamp / 10000.0f,
    (float)video_frame.timecode / 10000.0f,
    video_frame.p_metadata
  );
}

void receive_frames(NDIlib_recv_instance_t &pNDI_recv)
{
  while (true) {
    NDIlib_video_frame_v2_t video_frame;
    NDIlib_frame_type_e frame_type = NDIlib_recv_capture_v2(pNDI_recv, &video_frame, nullptr, nullptr, 5000);
    switch (frame_type) {
    case NDIlib_frame_type_video:
      handle_frame(video_frame);
      NDIlib_recv_free_video_v2(pNDI_recv, &video_frame);
      break;
    default:
      break;
    }
  }
}

Now, this is the output I get for a few subsequent calls:

Frame received (1920x1080), timestamp: 16170169126246802, timecode: 16170169126639307, timestamp_ms: 1617016913920.000000, timecode_ms: 1617016913920.000000, metadata: (null)

Frame received (1920x1080), timestamp: 16170169126593547, timecode: 16170169126972974, timestamp_ms: 1617016913920.000000, timecode_ms: 1617016913920.000000, metadata: (null)

Frame received (1920x1080), timestamp: 16170169126832240, timecode: 16170169127306641, timestamp_ms: 1617016913920.000000, timecode_ms: 1617016913920.000000, metadata: (null)

Notice that the timestamp/timecode values change on every call, but once I divide them and log them, they are … always the same! They always take the value of the first time I made the division (1617016913920.000000).

What's even weirder is that they take the same value, despite using timestamp and timecode for both.

How can this be? How could the function use the value of a previous call?

Could it be that the video_frame instance underneath was changed somehow? But how could the other printed values then be correct in the context?

Even when I print the (divided) values directly inside the case branch, the result is the same.

I feel like I'm missing something obvious here, but I cannot explain it.

slhck
  • 36,575
  • 28
  • 148
  • 201
  • `float`s are too inaccurate for numbers this large so you will get a lot of rounded results. – perivesta Mar 29 '21 at 11:40
  • @dave Ohh, gotcha, the value being "reused" was a red herring. Could you post that as an answer? Using `double` seems to do the trick. – slhck Mar 29 '21 at 11:44
  • `float` has a precision of `6` significant digits, on all the platforms I tested (granted, it might be different on yours: `std::numeric_limits::digits10`). The value, that you are printing has more than `6` significant digits. Hence, it didn't change: it was truncated to nearest value, that can be represented with `float`). Related: [Is floating point math broken?](https://stackoverflow.com/questions/588004/is-floating-point-math-broken) – Algirdas Preidžius Mar 29 '21 at 11:45
  • 1
    It is often a bad idea to hold time values as `double`s. Keep the API's in-built return value -- presumably chosen for a reason. Or translate it to a standard type for your language; for C++ that might be [`chrono::time_point`](https://en.cppreference.com/w/cpp/chrono/time_point). – ariels Mar 29 '21 at 11:48

1 Answers1

1

float values get increasingly inaccurate for large values (due to the representation in memory). In your case the next/previous representable values are spaced quite far apart:

#include <cmath>
#include <iostream>
#include <iomanip>

int main()
{
        std::cout << std::setprecision(20)
                << std::nextafter(1617016913920.f, 0.f) << "\n"
                << 1617016913920.f << "\n"
                << std::nextafter(1617016913920.f, 100000000000000.f) << std::endl;
}

Output:

1617016782848
1617016913920
1617017044992

Using double will increase the precision but it is probably still the wrong datatype to use in this situation. Use integer types or something like chrono::duration instead to avoid this issue.

perivesta
  • 3,417
  • 1
  • 10
  • 25
  • Timestamp is `int64_t` and it signifies 100 ns intervals (weird non-SI unit choice, but whatever), so how do I precisely divide by 10000 then? You mean I could create a `time_point` from `timestamp`, then cast it using `duration_cast`? – slhck Mar 29 '21 at 11:53
  • What I would probably do is create a `std::chrono::duration>` and then use `std::chrono::duration_cast` for conversions – perivesta Mar 29 '21 at 12:01
  • Note that `std::chrono::duration<>` is explicitly intended to support floating-point types. That won't avoid the issue you mentioned. But in this case, with the `/ 10000.0f` suggesting a 1E-4 resolution, I'd use a duration which counts exactly that: `std::chrono::duration>`. With `int32_t`, you could count up to 596 hours. That might be enough. – MSalters Mar 29 '21 at 12:22
  • @dave Thanks, I see the usefulness of creating a dedicated type for the "100 ns units". In this puzzle I kind of miss the way how to instantiate the duration from the nanoseconds timestamp I have. All I see is how to instantiate it from seconds. How would function `double timecode_to_ms(int64_t timestamp) { … }` look like if I wanted to improve on returning `(double)timestamp / 10000.0`? – slhck Mar 29 '21 at 18:54