Interface control document
1. Identification
| Item | Value |
|---|---|
| Document | ICD-CMTOBII-001 rev 1 |
| Product | Tobii module (Tobii Module in the module registry) |
| Product versions covered | aa2ba9b |
| Peer | A screen-based Tobii eye tracker, reached through the Tobii Stream Engine native library tobii_stream_engine.dll |
| Host | HEAT Capture, HEAT.Capture.ModuleSDK 2.0.3 |
| Settings key | TobiiSDK, configuration section HEAT.TobiiModule |
| Declared capability | process.unrestricted |
| Runtime | net10.0, Windows only (Media Foundation and the vendored Stream Engine DLL) |
| Data model | Two distinct SimulationFrame shapes: a gaze frame and a video frame, both on entity Tobii_Data |
| Date | 2026-09-02 |
| Owner | VRAI Simulation |
2. Scope
This document defines the Tobii Stream Engine surface the module actually calls, the callback data it consumes, the two frame shapes it publishes to HEAT Capture, and the H.264 camera stream contract.
Audit finding TOBII-02 recorded that the module’s data page described a video and stream contract without specifying it as an interface. Section 7 is that specification. It is constrained to what source in this repository shows, and section 7.6 states explicitly which stream parameters this repository does not define. Where a parameter is decided by the vendor runtime or by the Media Foundation encoder selected at run time, that is said rather than filled in with a plausible value.
The module reads measurement data and writes device configuration. It does not expose any remote or network interface of its own; every peer call is an in-process P/Invoke.
3. Interface inventory
| ID | Name | Direction | Transport | Peer |
|---|---|---|---|---|
| IF-CMTOBII-01 | Discovery, licensing and connection | Module to engine | P/Invoke into tobii_stream_engine | Tobii Stream Engine |
| IF-CMTOBII-02 | Gaze, eye-openness and head-pose subscriptions | Engine to module | Stream Engine callbacks, pumped on a module thread | Tobii device |
| IF-CMTOBII-03 | Primary camera image subscription | Engine to module | Stream Engine callback, same pump | Tobii device |
| IF-CMTOBII-04 | Device configuration | Module to engine | P/Invoke into tobii_stream_engine | Tobii device |
| IF-CMTOBII-05 | Gaze frame publication | Module to host | Module SDK interval publication | HEAT Capture |
| IF-CMTOBII-06 | Video frame publication | Module to host | Module SDK Publish, out of band | HEAT Capture |
3.1 Native entry points used
Every entry point below is declared with CallingConvention.Cdecl against library name
tobii_stream_engine. The DLL is vendored in this repository and copied to the package output
root, from where the module loader resolves it.
| Entry point | Interface | Purpose in this module |
|---|---|---|
tobii_api_create | IF-CMTOBII-01 | Creates the API context. Passed a null custom-log callback, so Stream Engine internal logging is not routed into the HEAT logger. |
tobii_find_all_eyetrackers | IF-CMTOBII-01 | Enumerates trackers into a managed array of tobii_eyetracker_t, each carrying a url string. |
tobii_connect_eyetracker | IF-CMTOBII-01 | Connects with a licence key, yielding the device context. |
tobii_disconnect | IF-CMTOBII-01 | Disconnects at capture end. |
tobii_api_destroy | IF-CMTOBII-01 | Destroys the API context at capture end. |
tobii_error_message | all | Renders an error code for the log. |
tobii_wait_for_callbacks | IF-CMTOBII-02, 03 | Blocks the pump thread until there is something to process. |
tobii_device_process_callbacks | IF-CMTOBII-02, 03 | Invokes the registered callbacks on the pump thread. |
tobii_device_reconnect | IF-CMTOBII-02, 03 | Called after a connection failure from the pump. |
tobii_gaze_data_subscribe / _unsubscribe | IF-CMTOBII-02 | Per-eye gaze data. |
tobii_absolute_eye_openness_subscribe / _unsubscribe | IF-CMTOBII-02 | Eyelid openness. |
tobii_head_pose_subscribe / _unsubscribe | IF-CMTOBII-02 | Head position and rotation. |
tobii_gaze_point_subscribe / _unsubscribe | IF-CMTOBII-02 | Combined, lightly filtered screen-space gaze point. |
tobii_primary_camera_image_subscribe / _unsubscribe | IF-CMTOBII-03 | Raw camera frames. |
tobii_get_geometry_mounting | IF-CMTOBII-04 | Reads the tracker’s physical mounting geometry. |
tobii_calculate_display_area_basic | IF-CMTOBII-04 | Derives a display area from settings plus that geometry. |
tobii_set_display_area | IF-CMTOBII-04 | Applies the derived display area. |
tobii_set_output_frequency | IF-CMTOBII-04 | Requests 33 Hz. |
tobii_calibration_apply | IF-CMTOBII-04 | Applies the bundled calibration blob. |
The bindings in this repository also declare presence, user presence, user position guide, gaze origin, normalised eye position, track box, digital syncport, illumination mode, face type, diagnostics image, power save and remote wake entry points. None of them is called by the module in this revision. They are dormant binding surface, not part of this interface.
The module never calls tobii_get_api_version, so it neither reads nor asserts the version of the
Stream Engine library it loads.
4. IF-CMTOBII-01 Discovery, licensing and connection
4.1 Addressing and device selection
| Item | Value |
|---|---|
| Addressing | The Stream Engine url string returned by enumeration. It is not a setting and is not filtered. |
| Selection rule | The module connects to eyetrackers[0], the first entry of the enumerated array. There is no setting to choose among several trackers, and the array order is whatever the Stream Engine returns. |
| No device found | No eye trackers found is logged as an error and InitialiseModule returns false, so the module does not start. |
The connected URL is logged twice during startup, as Connecting to eye tracker: {Url} and then
Device context created for {Url}.
4.2 Authentication and authorisation
There is no authentication of HEAT Capture to the tracker and none of the tracker to HEAT Capture. Any local Tobii device that enumeration returns is accepted.
What the LicenceFile setting provides is authorisation for privileged Stream Engine feature
groups, not identity. The file is read from the module assembly’s own directory joined with the
setting value, decoded as UTF-16, and passed as a tobii_license_key_t whose size_in_bytes is
the character count times two.
The licence levels the vendor headers require for the calls this module makes:
| Call | Required feature group, per the vendor header |
|---|---|
tobii_gaze_data_subscribe / _unsubscribe | Professional |
tobii_primary_camera_image_subscribe / _unsubscribe | Internal |
tobii_set_output_frequency | Not stated in the header as a feature group. The module handles TOBII_ERROR_INSUFFICIENT_LICENSE from it specifically and describes it in that log line as requiring a Config-level licence. |
tobii_gaze_point_subscribe, tobii_head_pose_subscribe, tobii_absolute_eye_openness_subscribe | No licence level stated in the header. |
LicenceFile defaults to an empty string. With the default, the joined path resolves to the module
assembly’s own directory, and the file read is attempted against a directory. The module performs no
existence check before that read, unlike the calibration path which does check. The resulting I/O
exception propagates out of InitialiseModule, where the SDK catches it and logs
{Module} InitialiseModule failed., and the module does not start. A missing or unset licence file
is therefore a hard startup failure reported as an exception, not a diagnosable configuration error.
4.3 Connection lifecycle
InitialiseModule runs, in order: create the API context, enumerate trackers, read the licence file
and connect to the first tracker, configure the display area (IF-CMTOBII-04), then apply the
calibration blob. All of it is bounded by the SDK StartupTimeout. The module does not override
that property, so the bound is the SDK default of 5 seconds, covering enumeration, connection,
licence file I/O, three device configuration round trips and a calibration apply.
StartCapture assigns the five callback delegates to fields so they are not collected while native
code holds them, issues the four data subscriptions, optionally starts the video path
(IF-CMTOBII-03), and then starts the pump thread TobiiCallbackThread.
Subscription failures do not fail the start. Each subscribe is wrapped in a helper that logs at
critical level and returns, so StartCapture returns true even when every subscription was
rejected. The observable result is a module that runs and publishes frames containing only default
values. Distinguish this case by the presence of Failed subscribing to ... critical log lines.
EndCapture cancels the pump token and joins the pump thread for up to 2 seconds, cancels the
encode token, completes the camera queue and joins the encode thread for up to 2 seconds, logging
Encode thread did not stop in time if that join fails. It then unsubscribes the four data streams,
unsubscribes the camera, disconnects, destroys the API context, and zeroes both handles. Failures at
this stage are logged as errors and do not stop teardown.
The SDK clears its initialised flag on stop, so a subsequent start re-runs InitialiseModule and
rebuilds both contexts. Enumeration, connection, display-area configuration and calibration are
therefore repeated per capture session, not once per process.
The camera unsubscribe is guarded by the current value of CaptureVideoFeed rather than by the
value that was in effect when the subscription was made. Toggling the setting during a session makes
teardown either skip a live camera subscription or attempt to unsubscribe one that was never made.
4.4 Reconnect
Reconnect is handled inside the pump loop, not by a connection supervisor.
| Pump result | Module behaviour |
|---|---|
TOBII_ERROR_NO_ERROR from the wait | Calls tobii_device_process_callbacks. |
TOBII_ERROR_TIMED_OUT from the wait | Continues immediately. The vendor documents this as a housekeeping opportunity after a timeout of some hundred milliseconds, not a failure. |
| Any other error from the wait | Logs tobii_wait_for_callbacks returned: {Error} as a warning and continues, with no backoff. |
TOBII_ERROR_CONNECTION_FAILED from processing | Logs a warning containing Tobii device connection lost and attempting reconnect, then calls tobii_device_reconnect. The return value of that call is discarded, so a failed reconnect is not reported and the loop retries on the next iteration. |
| Any other error from processing | Logs tobii_device_process_callbacks returned: {Error} as a warning and continues. |
| Managed exception in the loop body | Logged as Exception in Tobii callback processing loop and the loop continues. |
The loop also exits if the device context has been zeroed, which happens only during teardown.
5. IF-CMTOBII-02 Gaze, eye-openness and head-pose subscriptions
5.1 Delivery model
Four callbacks are registered and all are invoked from the module’s own pump thread during
tobii_device_process_callbacks. Each callback receives its vendor struct by reference, validates
per-field validity flags, and writes into three long-lived component objects held by the module. No
queue, ring buffer or timestamp history is kept: each callback overwrites the retained value.
The callback bodies are individually wrapped in try/catch and log
Exception in OnGazeDataCallback, Exception in OnGazePointCallback,
Exception in OnHeadPoseCallback or Exception in OnEyePpennessCallback. The last of those log
strings contains a typo in this revision and is reproduced here as it appears, because log-based
diagnostics have to match the emitted text.
Each callback returns immediately without writing anything if the device context has been zeroed.
5.2 Wire format consumed
| Struct | Fields the module reads |
|---|---|
tobii_gaze_data_t | Per eye: gaze_origin_validity, gaze_origin_from_eye_tracker_mm_xyz, eye_position_validity, eye_position_in_track_box_normalized_xyz, pupil_validity, pupil_diameter_mm |
tobii_gaze_point_t | validity, position |
tobii_head_pose_t | position_validity, position_xyz, rotation_x_validity, rotation_y_validity, rotation_z_validity, rotation_xyz |
tobii_absolute_eye_openness_t | left_validity, left_eye_openness, right_validity, right_eye_openness |
The module reads no timestamp from any of these structs. timestamp_tracker_us and
timestamp_system_us on gaze data and eye openness, and timestamp_us on gaze point and head pose,
are all ignored. It also declares but never reads gaze_point_validity,
gaze_point_from_eye_tracker_mm_xyz, gaze_point_on_display_normalized_xy,
eyeball_center_validity and eyeball_center_from_eye_tracker_mm_xyz: two local validity variables
are computed from the first and fourth of those and then never used.
6. IF-CMTOBII-05 Gaze frame publication and data model reference
6.1 Frame shape
Every capture cycle builds and returns one SimulationFrame with exactly one entity.
| Item | Value |
|---|---|
| Entity name | Tobii_Data |
| Entity id | (ulong)Guid.NewGuid().GetHashCode() & 0x7FFFFFFF, evaluated once when the module object is constructed |
| Components | GazeComponent, PupillometryDataComponent, SpatialComponent |
| Capture time | UTC Unix milliseconds read at frame build |
The entity id is derived from a fresh GUID, so it is a fresh unpredictable value on every host process start. It is stable for the lifetime of a module instance, including across stop and restart of capture within one process, but it is not reproducible and carries no relationship to the device or to the entity name. Correlating entities across sessions requires the entity name, not the id.
HandleCapture never returns null, so a frame is published on every cycle whether or not any
callback has fired. Before the first callback the frame carries default component values, and the
components are never reset at start or end of capture, so the first frames of a second session in
the same process carry the previous session’s last values. There is no timestamp, sequence number or
validity flag on the published data to distinguish a fresh reading from a retained one; only
left_eye_occluded and right_eye_occluded carry any liveness signal, and those default to false.
HandleCapture returns the retained component objects themselves rather than copies, so
serialisation by the SDK reads objects that the pump thread is concurrently mutating, with no lock
or exchange between the two threads. A frame can therefore mix fields from two different device
samples.
6.2 Published fields
| Proto field | Component | Source | Written when |
|---|---|---|---|
left_gaze_origin | GazeComponent | gaze_data.left.gaze_origin_from_eye_tracker_mm_xyz | gaze_origin_validity is valid |
right_gaze_origin | GazeComponent | gaze_data.right.gaze_origin_from_eye_tracker_mm_xyz | gaze_origin_validity is valid |
left_eye_position | GazeComponent | gaze_data.left.eye_position_in_track_box_normalized_xyz | eye_position_validity is valid |
right_eye_position | GazeComponent | gaze_data.right.eye_position_in_track_box_normalized_xyz | eye_position_validity is valid |
left_eye_occluded | GazeComponent | Negation of gaze_data.left.eye_position_validity being valid | Every gaze-data callback |
right_eye_occluded | GazeComponent | Negation of gaze_data.right.eye_position_validity being valid | Every gaze-data callback |
gaze_screen_point | GazeComponent | gaze_point.position x and y, copied unchanged | gaze_point.validity is valid |
left_eye_pupil_diameter | PupillometryDataComponent | gaze_data.left.pupil_diameter_mm | pupil_validity is valid |
right_eye_pupil_diameter | PupillometryDataComponent | gaze_data.right.pupil_diameter_mm | pupil_validity is valid |
left_eyelid_openness | PupillometryDataComponent | eye_openness.left_eye_openness | Every eye-openness callback, regardless of validity |
left_eyelid_openness_quality | PupillometryDataComponent | 1.0 if left_validity is valid, otherwise 0.0 | Every eye-openness callback |
right_eyelid_openness | PupillometryDataComponent | eye_openness.right_eye_openness | Every eye-openness callback, regardless of validity |
right_eyelid_openness_quality | PupillometryDataComponent | 1.0 if right_validity is valid, otherwise 0.0 | Every eye-openness callback |
pose.position | SpatialComponent | head_pose.position_xyz | position_validity is valid |
pose.orientation | SpatialComponent | Quaternion converted from head_pose.rotation_xyz | All three of rotation_x_validity, rotation_y_validity, rotation_z_validity are valid |
left_eyelid_openness and right_eyelid_openness are the only readings written without checking
their validity flag. A consumer must gate them on the paired quality field, which is the binary
1.0 or 0.0 carrying that flag, and not treat a 0.0 quality reading as a measured value of an
eyelid.
The pose is rebuilt from an empty builder on each head-pose callback and assigned only if position or rotation was valid that callback. When only one of the two is valid, the other half of the pose is the builder default rather than its previous value, so a position update during a rotation dropout publishes a default orientation rather than the last known one.
Not populated by this module: gaze_direction, left_gaze_direction, right_gaze_direction,
combined_eye_position, gaze_point, blink, has_glasses, and every gaze quality field.
SpatialComponent carries only pose; the module sets no image, velocity or other spatial field.
PupillometryDataComponent combined fields and other_pupil_data are not populated.
6.3 Cadence
Gaze publication is driven entirely by the SDK capture loop, so it runs at Interval, default
1000 ms, re-read every cycle. The device is asked to run at 33 Hz (IF-CMTOBII-04), so at default
settings the callbacks update the retained values roughly 33 times for every frame published, and 32
of those 33 samples are discarded. The module offers no native-rate publication path and no
per-sample buffering. To approach the device rate, Interval must be lowered.
7. IF-CMTOBII-03 and IF-CMTOBII-06 Camera stream
Active only when CaptureVideoFeed is true, which is the default.
7.1 Ingest
tobii_primary_camera_image_subscribe is called with a null user-data pointer. The callback receives
tobii_primary_camera_image_t with timestamp_us, bits_per_pixel, width, height, stride,
data_size and data.
The vendor header documents timestamp_us as having an undefined epoch, useful only for measuring
elapsed time between a pair of values. The module does not use it. It stamps each frame with
DateTimeOffset.UtcNow at callback receipt instead, and that host-clock value is what reaches the
consumer as capture_time. Video timestamps are therefore not comparable with the tracker clock, and
they include the callback delivery latency of the pump thread.
Accepted frames must have non-zero data_size, width and height, bits_per_pixel equal to 8,
and a stride that is not smaller than width; a stride of zero or less is treated as equal to
width. Any other pixel format logs Unsupported camera pixel format: {Bpp}bpp, video disabled once
and disables video for the session. The pixel data is copied out of the native buffer before the
callback returns.
7.2 Queue and back pressure
| Item | Value |
|---|---|
| Queue | Bounded channel, single reader, single writer |
| Capacity | 66 frames, documented in source as about 2 seconds at 33 Hz |
| Full mode | Drop oldest |
| Overflow action | The drop callback requests a forced keyframe, so the next access unit re-anchors the stream after the lost history |
7.3 Encode
A dedicated background thread named TobiiEncodeThread drains the queue. The encoder is created
lazily on the first frame, because the frame dimensions are not known until then, and it is created,
used and disposed only on that thread.
| Item | Value |
|---|---|
| Backend | Media Foundation transform, CPU sample path. Hardware encoders enumerated first, then the Microsoft software H.264 transform. Reported as MFT-HW or MFT-SW. |
| Input media type | MFVideoFormat_NV12 in system memory, progressive, frame size and frame rate set to the encode size and 33 |
| Greyscale conversion | Y plane receives the camera bytes row by row, zero-padded to the encode size; the UV plane is filled with the constant 0x80 |
| Encode size | Both source dimensions rounded up to a multiple of 16 |
| Output media type | MFVideoFormat_H264, average bitrate attribute 2 000 000, progressive, pixel aspect ratio 1:1 |
| Codec API requests | Constant bitrate mode, mean bitrate 2 000 000, GOP size 33, zero B-pictures, low-latency mode. All five are best-effort; a rejection is logged at debug level and encoding continues with the transform’s own default. |
| Sample timing | Each input sample is given a time of the frame’s host timestamp in 100 ns units and a duration of one frame at 33 fps |
| Timestamp carry | Submitted timestamps are queued and dequeued FIFO as access units emerge, so each published access unit carries the host timestamp of the camera frame it came from |
The initialisation log line is H264 encoder: backend={Backend} {W}x{H}@{Fps} and reports the
16-aligned encode size, not the camera’s native size.
7.4 Published frame shape
One SimulationFrame per completed access unit, published immediately through the SDK Publish
path from the encode thread. It is not produced by HandleCapture and is not gated by Interval.
| Item | Value |
|---|---|
| Entity | Same id and name as the gaze frame, Tobii_Data |
| Components | StreamComponent only, with one StreamSource holding one VideoSource. No gaze, pupillometry or spatial component. |
VideoSource.name | camera.ir |
VideoSource.codec | The literal string H264 |
VideoSource.width / height | The encoder’s 16-aligned encode dimensions, so not the camera’s native dimensions unless those are already multiples of 16 |
VideoSource.frame_rate | The integer constant 33, always, independent of the rate the device actually delivers |
VideoSource.video_data | The bytes of exactly one access unit |
capture_time | The host-clock timestamp of the camera frame, not the encode completion time |
There is no keyframe flag on the message. Identifying an IDR requires inspecting NAL unit types in
video_data. A consumer must decode in arrival order per stream name and cannot begin decoding
before an IDR arrives.
7.5 Cadence
Video cadence is the rate at which the device delivers camera frames, reduced by any frames dropped
at ingest, at the queue or by the encoder. The module never resamples or pads, so the emitted rate is
not held to the declared frame_rate of 33. The declared value is a constant in source and is not
measured, so it should be treated as a nominal hint rather than as the stream’s true rate.
7.6 Parameters this repository does not define
The following are outside what source in this repository fixes, and are recorded here as undetermined rather than assumed:
- Camera native resolution and frame rate. The module accepts whatever the first callback reports and derives its encode size from that. No resolution or rate is asserted anywhere in the module. The authority is the connected Tobii device and its runtime.
- Bitstream framing and parameter-set placement. The module sets no bitstream-format attribute on either media type. Annex-B framing with SPS and PPS present on IDR frames is asserted by the encoder’s own documentation comment and by the module’s design specification as the behaviour of the Media Foundation H.264 transform. It is neither configured nor verified by code in this repository, and the transform chosen varies by machine because hardware encoders are preferred over the Microsoft software fallback. Verify framing against the actual capture before relying on it.
- Profile and level. Not set. Whatever the selected transform defaults to.
- Achieved bitrate. 2 000 000 is a requested average and a best-effort constant-bitrate request. A hardware transform may reject the rate-control mode, in which case its own default applies.
- Keyframe interval. GOP size 33 is requested best-effort, and forced keyframes are additionally
requested on queue overflow, which itself may be rejected and is then logged as
ForceKeyframe request rejected. - Colour metadata. None is set on either media type. The source is 8-bit greyscale and the chroma plane is synthetic neutral, so colour primaries, transfer characteristics and matrix coefficients are whatever the transform writes by default.
- Access-unit-to-frame ratio. Timestamp carry assumes one output access unit per submitted
frame. If a transform emits an additional standalone output, the FIFO shifts and later access units
carry the timestamp of an earlier frame. When the timestamp queue is empty, the access unit is
published with a
capture_timeof 0.
8. IF-CMTOBII-04 Device configuration
Applied once per capture session during InitialiseModule, before any subscription.
tobii_get_geometry_mountingreads the tracker’s own mounting geometry. Its log containsTracker geometry,{Guides},{Width}mmand{Angle}deg. Failure logsFailed retrieving geometry mountingand aborts initialisation.tobii_calculate_display_area_basiccombinesScreenWidth,ScreenHeightandTrackerOffsetXwith that geometry. Failure logsFailed calculating display areaand aborts initialisation.tobii_set_display_areaapplies the result, and the three computed corners are logged. Failure logsFailed setting display areaand aborts initialisation.tobii_set_output_frequencyrequests 33 Hz.TOBII_ERROR_INSUFFICIENT_LICENSElogs a warning stating the device will run at its default frequency; any other error logsFailed setting output frequency. Neither aborts initialisation, so a session can run at a device frequency other than 33 Hz while the module still declaresframe_rate33 on video and still assumes 33 fps for encoder timing.tobii_calibration_applyapplies the blob atCalibrationBlobPath, resolved relative to the module assembly directory and defaulting todata/calib_default.blob, a 345-byte file bundled in the package. A missing file logs a message containingNo calibration blob found at '{Path}'andproceeding without applying calibration, then continues uncalibrated. An apply failure logsFailed applying calibration bloband also continues.
The same bundled default blob is applied on every session. The module provides no per-user or per-session calibration step, so gaze accuracy reflects that default calibration and not the current operator.
9. Units and conventions
| Concept | Convention |
|---|---|
| Gaze origin | Millimetres from the eye tracker, as reported by the device. Passed through unscaled. |
| Eye position | Normalised track-box coordinates in the range 0 to 1, per the vendor header. Passed through. |
| Pupil diameter | Millimetres, as reported by the device. |
| Eyelid openness | Millimetres. The vendor header defines it as the diameter of the largest sphere that fits between the upper and lower eyelids, so it is an absolute distance rather than a normalised ratio. |
| Openness quality | Binary 1.0 or 0.0, a re-encoding of the vendor validity flag. It is not a graduated confidence, even though the proto documents the field as a [0..1] quality. |
| Head position | Millimetres from the centre of the display, per the vendor header. |
| Head rotation | The vendor header describes right-handed Euler rotations about each axis but does not state the angle unit. The module’s conversion applies trigonometric functions directly to the values, which treats them as radians. Confirm against the vendor documentation for the specific device before deriving anything from the published orientation. |
| Euler-to-quaternion mapping | The module maps rotation_xyz.x to pitch, .y to yaw and .z to roll, then composes a quaternion. The vendor header states only that the z rotation is about the vector pointing towards the user. |
| Gaze screen point | Normalised, and may fall outside 0 to 1 when the user looks off screen. The vendor header defines the vertical axis with 0.0 at the top edge and 1.0 at the bottom edge. The HEAT gaze_screen_point field is documented as normalised with (0,0) at bottom-left and (1,1) at top-right. The module copies x and y unchanged, applying no vertical flip, so the published vertical component follows the Tobii top-down convention and not the convention the proto field documents. Treat the published y as top-down until this is reconciled. |
| Gaze and video time base | Host wall clock, UTC Unix milliseconds. No device timestamp is carried on any published field. Gaze frames are stamped at frame build; video frames are stamped when the camera callback is received. |
| Video pixel data | 8-bit greyscale from the device, encoded through NV12 with a synthetic neutral chroma plane. The decoded image is greyscale content in a nominally colour format. |
10. Configuration affecting the interfaces
| Setting | Default | Interface effect |
|---|---|---|
LicenceFile | empty | Determines which Stream Engine feature groups are authorised. Empty or missing is a hard startup failure, see 4.2. Read only during InitialiseModule. |
ScreenWidth | 527.0 mm | Input to the display-area calculation. A wrong value produces a valid but physically incorrect display area and therefore a wrong gaze-to-screen mapping, with no error. |
ScreenHeight | 296.0 mm | As above. |
TrackerOffsetX | 0.0 mm | As above. There is no equivalent Y or Z offset. |
CaptureVideoFeed | true | Whether IF-CMTOBII-03 and IF-CMTOBII-06 exist at all. Read at StartCapture for the subscription and again at EndCapture for the unsubscribe, see 4.3. |
CalibrationBlobPath | data/calib_default.blob | Which calibration blob is applied. Hidden from the settings UI. |
Interval | 1000 ms | Gaze frame cadence only. Does not affect the device output frequency and does not affect video. |
Enabled | true | Whether the module runs. |
OverrideNodeId, NodeId | false, 0 | Node id stamped on published packets by the SDK. |
Timeout | 30 s | Inherited from the SDK and not read by this module. |
The output frequency request, the video frame rate, the encoder bitrate, the GOP size, the queue
capacity, the thread join timeouts and the reconnect behaviour are all compile-time constants with
no setting. Settings changed mid-session have no effect on any subscription, because subscriptions
are made only at StartCapture.
See Manage capture modules for the generic settings and the SDK ICD for the lifecycle and publication contract this module implements.
11. Error behaviour
| Condition | Detection | Module behaviour | Observable effect |
|---|---|---|---|
| API context creation fails | tobii_api_create error | Logs Failed Creating Tobii API Context at critical level and continues | Initialisation proceeds with a zero API context and fails later, at the display-area calculation |
| Enumeration fails | tobii_find_all_eyetrackers error | Logs Failed enumerating eyetrackers at critical level; the array is empty | Falls through to the no-device path |
| No tracker present | Empty enumeration array | Logs No eye trackers found as an error, returns false | Module does not start; no retry |
| Licence file missing or unset | Exception from the file read | Uncaught in the module; the SDK logs {Module} InitialiseModule failed. | Module does not start |
| Connection fails | tobii_connect_eyetracker error | Logs Failed creating device context at critical level; the zero context is then caught by an explicit check that logs Failed to create device context and returns false | Module does not start |
| Geometry, display-area calculation or display-area apply fails | Vendor error code | Logs the specific failure and returns false | Module does not start |
| Output frequency rejected | TOBII_ERROR_INSUFFICIENT_LICENSE or other error | Warning only | Session runs at the device’s default frequency while still declaring 33 |
| Calibration blob missing or apply fails | File check or vendor error | Warning or error, continues | Session runs uncalibrated |
| Any subscription rejected | Vendor error code | Logs Failed subscribing to ... at critical level and continues | Module runs and publishes frames whose corresponding fields never update |
| Connection lost while capturing | TOBII_ERROR_CONNECTION_FAILED from the pump | Warns and calls tobii_device_reconnect, discarding the result | Retries every pump iteration indefinitely; gaze frames keep publishing the last retained values with no staleness indicator |
| Exception inside a data callback | try/catch per callback | Logged, retained values left as they were | That sample is lost |
| Unsupported camera pixel format | bits_per_pixel not 8, or stride below width | Logs Unsupported camera pixel format: {Bpp}bpp, video disabled once and disables video for the session | Video stops, gaze continues |
| No usable H.264 transform | Enumeration and configuration all fail | Logs No usable H.264 encoder MFT found; video disabled from the encoder and H264 encoder unavailable; video disabled for this session from the module, completes the queue and exits the encode thread | Video never starts, gaze continues |
| Encoder busy or input rejected | Async token exhausted or ProcessInput failure | Frame dropped; H264 encoder submit failed (hr=...); frames will be dropped logged once per session | Gap in the video stream, not re-anchored |
| Output stalled | ProcessOutput failure | Warning logged once per session | Video output stops for the session |
| Camera frames arriving faster than encode | Queue full | Oldest frame dropped and a keyframe forced | Gap in the video stream, re-anchored at the next access unit |
| Transient exception around encode or publish | try/catch in the encode loop | Transient error encoding/publishing camera frame; continuing logged once per session, loop continues | Isolated frames lost |
| Fatal exception in the encode loop | try/catch around the loop | Encode loop failed; video disabled for this session, queue completed, encoder disposed | Video stops, gaze continues |
Exception thrown by HandleCapture | SDK | Logged as Error in module, loop continues | Cycle produces no frame |
No error condition in this module raises a HEAT-visible fault, and none stops the capture session. Every failure short of a startup failure degrades silently into missing or frozen data, so log inspection is the only reliable way to establish that a session’s data is trustworthy.
12. Compatibility and versioning
There is no version negotiation on either side. The module does not query the Stream Engine version
and does not read the device model or firmware. Compatibility rests on the vendored
tobii_stream_engine.dll in this repository matching the layout of the binding structs alongside it;
a vendor DLL swap that changes any consumed struct layout produces misread fields rather than an
error, because the bindings are sequential-layout structs marshalled directly.
Device requirements that follow from source rather than from documentation: the tracker must be screen-based, since the module uses the screen-based API and not the wearable API whose bindings are also present but unused; it must supply an 8-bit greyscale primary camera image for video to work; and the licence must reach at least Professional for gaze data and Internal for the camera image.
The host contract is HEAT.Capture.ModuleSDK 2.0.3, targeting net10.0. The module is Windows-only
in practice: Media Foundation for encoding and the vendored Windows Stream Engine DLL.
Audit finding TOBII-01 applies to pinning an artifact to this revision. In this revision, non-main
builds select a full SemVer package version but the generated release location and tag use only
majorMinorPatch, so a prerelease package is published under a tag that looks like a stable release
and collides with the eventual stable release of the same version. Pin by commit, and verify the
package version recorded inside the artifact rather than trusting the release tag, when confirming
that a deployed module is this revision.
13. Security considerations
Gaze, pupillometry, head pose and the infrared camera image are all personal data. The camera feed in particular is imagery of a person’s eyes and face region, and gaze data is behavioural biometric data from which attention and identity inferences can be drawn. Both are published into HEAT Capture and persisted with the session.
| Item | Property |
|---|---|
| Transport | In-process P/Invoke to a local native library and local device I/O. No network interface is exposed by this module. |
| Authentication | None in either direction, see 4.2. |
| Encryption | None; not applicable to in-process calls, and the module applies none to the data it publishes. |
| Licence file | Plain UTF-16 text read from the module directory. It grants access to privileged Stream Engine feature groups, including the camera image, so treat it as a credential and restrict read access to the module directory accordingly. |
| Declared capability | process.unrestricted, so the module runs without the host’s process restrictions. |
| Video content | Infrared imagery of the operator’s eyes. Retained in captured sessions as H.264. There is no setting to blur, crop or redact it; the only control is turning CaptureVideoFeed off. |
| Data minimisation | Turning CaptureVideoFeed off removes the camera subscription entirely, so no imagery reaches the host. Use that when the scenario does not need imagery. |
Restrict access to the workstation, the module directory and stored captures according to the
deployment’s own data-handling policy, and treat any exported session containing camera.ir as
containing identifiable imagery.
14. Conformance
- Place a valid Tobii licence file in the module directory and set
LicenceFileto it. Confirm the log recordsLicense file loaded from '{Path}'andDevice context created for {Url}. Confirm noFailed subscribing to ...critical line appears for any of gaze data, eye openness, head pose or gaze point. - Confirm the startup log records the tracker geometry line, the three display-area corners, and
either
Output frequency set to 33Hzor the insufficient-licence warning. If the warning appears, record the device’s actual frequency, because the declared videoframe_rateof 33 will be wrong for that session. - Confirm one entity named
Tobii_Dataappears in the captured data, and that both frame shapes arrive on the same entity id: gaze frames carrying gaze, pupillometry and spatial components, and video frames carrying only a stream component. - With the operator looking at known screen corners, confirm the sign convention of
gaze_screen_point. Looking at the top of the screen should be checked against the y value: a value near 0 confirms the top-down Tobii convention described in section 9, and near 1 would contradict it. Fix downstream interpretation to match what you observe, not to what the proto field documents. - Confirm eyelid openness is only trusted when its quality field is
1.0. Cover one eye and check that the corresponding quality drops to0.0while the openness field still carries a number. - With
Intervalat the default, confirm gaze frames arrive at about 1 Hz and not at the device rate. LowerIntervaland confirm the rate follows it. - With
CaptureVideoFeedon, dump thevideo_dataof consecutivecamera.irframes to a raw.h264file in arrival order and probe it. Record the actual resolution, the observed keyframe cadence and the achieved bitrate, and confirm whether framing is Annex-B with parameter sets on IDR frames. This is the step that resolves the parameters section 7.6 leaves undetermined, and it has to be repeated per machine class, because the transform selected differs between rigs with and without a usable hardware encoder. Note the backend from theH264 encoder: backend={Backend} {W}x{H}@{Fps}line alongside the result. - Confirm the encode size in that log line against the camera’s native size, and expect the declared
widthandheightto be rounded up to a multiple of 16. - Disconnect the tracker while capturing. Confirm a warning containing
Tobii device connection lostandattempting reconnectappears, and confirm that gaze frames keep publishing the last values with no staleness indicator. Reconnect and confirm values resume changing without restarting the capture session. - Stop and restart capture within the same host process. Confirm initialisation runs again, and confirm that the first frames of the second session carry the previous session’s retained values before the first callback of the new session arrives.
- Start with
CaptureVideoFeedoff and confirm nocamera.irstream is published and no encode thread starts, while gaze, pupillometry and head pose publish normally.
15. Revision history
| Rev | Date | Product version | Change |
|---|---|---|---|
| 1 | 2026-09-02 | aa2ba9b | Initial issue. Supersedes the placeholder that deferred the stream contract to the data page, closing audit finding TOBII-02. |