Handle events

React to dotLottie playback events from C by draining the event queue each frame, and read the frame number and loop count each event carries.

The runtime reports what the player is doing — loaded, started playing, completed a loop — through a queue you drain, rather than through callbacks you register.

Why polling instead of callbacks

Callbacks across an FFI boundary mean handing a function pointer to the library and having it invoked from whatever thread and stack the library happens to be on. That constrains what you can safely do inside the handler and complicates lifetime management on both sides.

Queued events avoid all of it. Events accumulate as playback progresses, and you consume them at a point you choose, on your own thread, where your application state is already accessible.

Draining the queue

dotlottie_poll_event() writes one event into a struct you provide and tells you whether it wrote anything:

ReturnMeaning
1An event was written
0The queue is empty
-1A null player or event pointer

Drain it fully with a while loop, since more than one event can be queued per tick:

dotlottieDotLottiePlayerEvent event;

while (dotlottie_poll_event(player, &event) == 1) {
  switch (event.event_type) {
    case Load:
      printf("animation loaded\n");
      break;
    case LoadError:
      fprintf(stderr, "animation failed to load\n");
      break;
    case Loop:
      printf("completed loop %u\n", event.data.loop_count);
      break;
    case Complete:
      printf("playback finished\n");
      break;
    default:
      break;
  }
}

Poll once per iteration of your render loop, after dotlottie_tick(). Events that are never polled stay queued and accumulate, so drain the queue even if you only care about a couple of event types.

Event types

TypeNumericFires whenPayload
Load0An animation finished loading
LoadError1An animation failed to load
Play2Playback started
Pause3Playback paused
Stop4Playback stopped and reset
Frame5The current frame advancedframe_no
Render6A frame was drawnframe_no
Loop7A loop iteration completedloop_count
Complete8Playback finished and will not continue
Event constants have no prefix

The values are emitted bare — Load, Frame, Loop, Complete — not DotLottiePlayerEventType_Load. Some doc comments inside the generated header show the prefixed spelling, but those identifiers do not exist and will not compile. Because names this common share the global namespace, watch for collisions with your own macros and enums.

Reading the payload

Two event types carry data, in a union whose active member is determined by the event type:

switch (event.event_type) {
  case Frame:
  case Render:
    printf("frame %.2f\n", event.data.frame_no);   /* float */
    break;
  case Loop:
    printf("loop %u\n", event.data.loop_count);    /* uint32_t */
    break;
  default:
    break;                                         /* no payload */
}

Reading frame_no on a Loop event, or loop_count on a Frame event, reinterprets unrelated bytes and yields nonsense. Always branch on event_type first.

Frame and Render fire every time the animation advances or draws — many times per second. Logging from those handlers will flood your output.

Where events fit in the loop

double last = now_seconds();

for (;;) {
  double current = now_seconds();
  float dt = (float)(current - last);
  last = current;

  bool rendered = false;
  dotlottie_tick(player, dt, &rendered);

  dotlottieDotLottiePlayerEvent event;
  while (dotlottie_poll_event(player, &event) == 1) {
    handle_event(&event);
  }

  if (rendered) {
    present(buffer, width, height);
  }
}

For the common cases you don't need events at all — dotlottie_is_complete(), dotlottie_status(), and dotlottie_get_current_loop_count() answer the same questions by direct query. Events are most useful when you want to act at the exact moment something changes, such as starting a sound on Loop or advancing UI state on Complete.

State machine events

State machines have their own separate queues, polled with dotlottie_state_machine_poll_event() and dotlottie_state_machine_poll_internal_event(). Their events report transitions and input changes rather than playback progress, and unlike player events they contain string pointers with a limited lifetime. See State machines for details.

Learn more

Last updated: August 13, 2026 at 9:17 AMEdit this page