Skip to content

SEB scenes and Event Player in FW 0.12.11

Use Berry SEB.land and SEB.emit in firmware 0.12.11 for causal scenes and timed Event-value segments.

SEB (Spectoda Event Batch) is a small binary file containing discrete Event values/records. Each record contains a native value, label, Spectoda ID, priority and an absolute time offset inside the segment. Landing or emitting a record produces normal EventStateUpdate processing through EventStore.

SEB is not a Player authoring or Project format. It contains no scene name, Track name, loop, or seek rules. Project Berry owns those policies. The static player.show v3 file is a compact manifest for one global mixed-ID stream of fixed player.NNN NetworkStorage slots.

One SEB segment contains at most 340 records, occupies at most 4,092 bytes and lasts at most 65,535 ms. This fills one 4,096-byte LittleFS data-block budget; filesystem metadata is stored separately. The runtime still accepts at most 340 due events in one atomic contribution, so one maximum same-time Cue Group across all IDs can commit atomically. A 341st pending event fails before any queue, EventStore, or cursor mutation. A Cue Group is never split. Long shows are ordered collections of bounded segments. Firmware owns the normative byte layout and validation rules in components/spectoda-library/docs/SEB_V1.md; this is the exact FW 0.12.11 review revision.

Use SEB.land on every prepared controller for a distributed Player. Events enter each local EventStore with the same Network time without an immediate execute-bytecode broadcast. Normal EventStore synchronization can later bring a controller that missed the landing up to date.

Use SEB.emit only when exactly one authoritative source exists. Calling it on every controller would multiply network event traffic.

The scalar API remains available in FW 0.12.11:

spectoda.landEvent(label, value, ids, vtype)
spectoda.landEvent(label, value, ids, vtype, event_millis)
spectoda.emitEvent(label, value, ids, vtype)
spectoda.emitEvent(label, value, ids, vtype, event_millis)

ids may be one ID, a list of IDs, or nil for broadcast. Optional event_millis preserves the exact causal Controller-local time of the source event. Use the SEB component for batches; it is not a plural overload of the scalar method.

var result = SEB.land("segment-000.seb", {
"source": "networkStorage",
"at": segment_at,
"cursor": cursor,
"until": position
})
if result["ok"]
cursor = result["cursor"]
else
print("SEB error", result["error"])
end
  • at is the wrapping local controller.millis() token corresponding to offset zero. Timed segments require it.
  • cursor is the first source record that has not been consumed yet. It defaults to 0.
  • until is the inclusive due offset. It defaults to 0.
  • source: "networkStorage" makes the first argument a filename. Without it, the first argument remains a Berry bytes value.

One call consumes the maximal due prefix. It validates and reserves the whole contribution before applying it atomically. A failure leaves EventStore, queues and cursor unchanged, so Berry can retry safely.

One args.at contract covers three explicit time domains:

  • timeline.at(segmentStart) projects a Player timeline position to a local wrapping millis token;
  • event_millis from an EVS.cb trigger preserves an event’s Network-clock causality for scene recall;
  • controller.millis() means now on this Controller.

Network clock remains the EventStore ordering authority. SEB only accepts the local token and has no timeline-specific overload.

SEB retains no Berry pointer, filename, file handle, cursor or Player state. The Project must keep at bit-identical for the exact source/cursor pair. It resets or replaces the cursor when changing segment, seeking, looping or changing the timing projection.

The reference Player handles pause/resume as explicit outer policy. It derives a fresh filename/at pair with the new projection and carries only the known first-unconsumed source index. Do not reuse that number with a different source or an arbitrary at.

A scene is an ordinary mixed-ID SEB with duration_ms = 0 and all offsets at zero. A trigger Event value can carry the scene label. Its EVS.cb callback also receives the exact local time of the source event; pass that value as at.

var scene = EVS("scene", ID255)
scene.cb = def(value, event_id, event_millis)
if event_millis == nil || !value.is(31)
return
end
var name = value.get(31) + ".seb"
var result = SEB.land(name, {
"source": "networkStorage",
"at": event_millis
})
if !result["ok"]
print("Invalid scene", result["error"])
end
end

Do not compile the $scene[ID255] trigger itself into the scene file, or the recall would recurse.

Scene recall never calls timeline.at(). It stays causal while the timeline is paused, because the callback’s event_millis came from the trigger event.

The copy-ready Player reads player.show and polls timeline.getState(). The exact-EOF manifest has a 12-byte header followed by one 10-byte entry for each fixed segment slot:

  • exact required NetworkStorage version as u48 little-endian;
  • absolute segment start on the show timeline as u32 little-endian.

Entry index 0 means player.000, index 1 means player.001, and so on up to player.999. Names are not stored. Berry obtains each local version from spectoda.listNetworkStorageData() as a fixed-width lowercase 12-character hexadecimal string and compares it exactly without converting it to a 32-bit number.

Studio publishes a newer manifest first and changed slots afterwards. This is not a hot-update protocol: a Controller stays inactive until every requested version is present and every SEB header/EOF validates. Ordinary NetworkStorage synchronization supplies missing files. Upload never changes timeline state.

Normal forward playback selects the current segment from its absolute start, calculates at through timeline.at(segmentStart), and passes the current relative position as until. Every Controller lands every ID from the same stream. An ordinary pause without a timeline discontinuity makes no SEB call.

timeline.at(position) records that the current timeline reference position P was valid at Network clock C, projects C + (position - P), and converts that clock to this Controller’s signed wrapping local token. The same formula is used while running and paused, so Controllers with different command processing latency still resolve the same Network timestamp. The call returns nil when the target clock is invalid or outside the unambiguous ±2^31 ms wrapping window.

Timeline position is a separate 24-hour ring, 0..86,399,999; 86,400,000 normalizes to 0. Midnight therefore remains continuous for the Player, Layers and animations. Projection selects the nearest occurrence within ±12 hours; an exact 12-hour delta is ambiguous and returns nil.

Pause/resume keeps the timeline epoch but resume creates a new causal projection; the Player derives a fresh at for the current segment while retaining its already-consumed source prefix. Seek and loop change the epoch and force cursor positioning again.

On seek, loop, rewind, or a 24-hour wrap, the Player selects the exact-target Cue Group or the immediately preceding Group. It lands that one sparse Group once at the causal seek time, including while paused, and advances the cursor past it. Resume therefore does not repeat it and no future Cue is selected.

This is sparse reconciliation, not complete-state reconstruction. The Player does not clear EventStore or replay older history. Events absent from the selected Cue keep their existing EventStates. Use a complete Cue where an arbitrary seek must recreate a complete visual state.

SEB never mutates EventState behind EventStore. The latest EventStateUpdate therefore remains authoritative after historical synchronization, and an externally emitted event can change Player state through the normal path.

The examples use synthetic IDs and exact NetworkStorage artifacts for FW 0.12.11. To author a show in Studio, continue to Event Player in Spectoda Studio.