bmdwire 1.2.0__tar.gz → 1.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bmdwire-1.2.0/bmdwire.egg-info → bmdwire-1.5.0}/PKG-INFO +2 -2
- {bmdwire-1.2.0 → bmdwire-1.5.0}/README.md +1 -1
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/_transfer.py +9 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/atem.py +4 -1
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/connection.py +49 -5
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/docs/MACRO_FORMAT.md +3 -2
- bmdwire-1.5.0/atemwire/docs/MEDIA_LOCK.md +184 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/docs/PROFILE_FORMAT.md +31 -17
- bmdwire-1.5.0/atemwire/docs/SURFACE.md +1096 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/helpers.py +42 -1
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/_helpers.py +19 -26
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/fairlight.py +9 -6
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/switching.py +4 -17
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/input_video.py +19 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/upstream_keyer.py +20 -8
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/pool.py +3 -2
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/probe.py +9 -2
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/_enums.py +36 -2
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/apply.py +63 -13
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/save.py +61 -15
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/protocol.py +176 -69
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/transport.py +166 -13
- {bmdwire-1.2.0 → bmdwire-1.5.0/bmdwire.egg-info}/PKG-INFO +2 -2
- {bmdwire-1.2.0 → bmdwire-1.5.0}/bmdwire.egg-info/SOURCES.txt +1 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/hyperdeckwire/client.py +81 -3
- {bmdwire-1.2.0 → bmdwire-1.5.0}/pyproject.toml +1 -1
- bmdwire-1.2.0/atemwire/docs/MEDIA_LOCK.md +0 -85
- {bmdwire-1.2.0 → bmdwire-1.5.0}/LICENSE +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/LICENSE +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/LICENSE-gpl3.txt +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/NOTICE +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/__init__.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/_socketqueue.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/docs/PROFILE_SAVE_RESTORE.md +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/imaging.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/__init__.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/_io.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/downstream_keyer.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/flow.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/hyperdeck.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/media.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/upstream_keyer.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/mediaconvertmodule.c +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/__init__.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/_dsl.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/audio_legacy.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/camera_control.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/color_generator.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/downstream_keyer.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/fade_to_black.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/fairlight.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/file_transfer.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/hyperdeck.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/lock.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/macros.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/manual.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/media.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/meters.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/multiviewer.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/recording.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/streaming.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/supersource.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/switching.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/system_info.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/tally.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/transition.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/__init__.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/_common.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/_xml.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/options.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/py.typed +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/ready.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/state.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/testing.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/bmdwire.egg-info/dependency_links.txt +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/bmdwire.egg-info/requires.txt +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/bmdwire.egg-info/top_level.txt +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/hyperdeckwire/LICENSE +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/hyperdeckwire/__init__.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/hyperdeckwire/setup.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/hyperdeckwire/upload.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/setup.cfg +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/setup.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/LICENSE +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/__init__.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/_preamble.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/_protocol.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/network.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/profile/__init__.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/profile/_ranges.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/profile/archive.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/profile/restore.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/videohubwire/LICENSE +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/videohubwire/__init__.py +0 -0
- {bmdwire-1.2.0 → bmdwire-1.5.0}/videohubwire/client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bmdwire
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Python libraries for Blackmagic Design broadcast devices: ATEM switchers, HyperDeck recorders, Ultimatte keyers and Videohub routers
|
|
5
5
|
Author: Lucas Romanenko, Martijn Braam and the OpenAtem contributors
|
|
6
6
|
License-Expression: LGPL-3.0-only AND MIT
|
|
@@ -49,7 +49,7 @@ pip install bmdwire
|
|
|
49
49
|
| **`ultimattewire`** | Ultimatte 12 keyers: archive and restore over the native TCP protocol, Smart Remote 4 compatible zips | MIT | [ultimattewire/README.md](ultimattewire/README.md) |
|
|
50
50
|
| **`videohubwire`** | Videohub routers: state snapshot, crosspoint routing, labels over TCP 9990 | MIT | [videohubwire/README.md](videohubwire/README.md) |
|
|
51
51
|
|
|
52
|
-
All four are pure standard library except atemwire, whose small C extension ships prebuilt for Linux, macOS and Windows on CPython 3.10 to 3.14, so installing needs no compiler. Pin the version in a requirements file (`bmdwire==1.
|
|
52
|
+
All four are pure standard library except atemwire, whose small C extension ships prebuilt for Linux, macOS and Windows on CPython 3.10 to 3.14, so installing needs no compiler. Pin the version in a requirements file (`bmdwire==1.5.0`) so a later release cannot change your install under you.
|
|
53
53
|
|
|
54
54
|
## atemwire stands on pyatem
|
|
55
55
|
|
|
@@ -15,7 +15,7 @@ pip install bmdwire
|
|
|
15
15
|
| **`ultimattewire`** | Ultimatte 12 keyers: archive and restore over the native TCP protocol, Smart Remote 4 compatible zips | MIT | [ultimattewire/README.md](ultimattewire/README.md) |
|
|
16
16
|
| **`videohubwire`** | Videohub routers: state snapshot, crosspoint routing, labels over TCP 9990 | MIT | [videohubwire/README.md](videohubwire/README.md) |
|
|
17
17
|
|
|
18
|
-
All four are pure standard library except atemwire, whose small C extension ships prebuilt for Linux, macOS and Windows on CPython 3.10 to 3.14, so installing needs no compiler. Pin the version in a requirements file (`bmdwire==1.
|
|
18
|
+
All four are pure standard library except atemwire, whose small C extension ships prebuilt for Linux, macOS and Windows on CPython 3.10 to 3.14, so installing needs no compiler. Pin the version in a requirements file (`bmdwire==1.5.0`) so a later release cannot change your install under you.
|
|
19
19
|
|
|
20
20
|
## atemwire stands on pyatem
|
|
21
21
|
|
|
@@ -29,6 +29,15 @@ class TransferTask:
|
|
|
29
29
|
|
|
30
30
|
self.name = None
|
|
31
31
|
self.description = None
|
|
32
|
+
# FTFD (name, description, MD5) goes out with the first FTCD budget
|
|
33
|
+
# of each attempt, before the data, as ATEM Software Control sends
|
|
34
|
+
# it; cleared when the upload is (re)requested.
|
|
35
|
+
self.metadata_sent = False
|
|
36
|
+
# Bytes of ``data`` already cut into chunks in this attempt. Chunks
|
|
37
|
+
# used to be cut by slicing them off the front of ``data``, which
|
|
38
|
+
# copied the rest each time (some 25 GB for the ~6,000 chunks of a
|
|
39
|
+
# 1080p still) and left a retried upload without its start.
|
|
40
|
+
self.offset = 0
|
|
32
41
|
|
|
33
42
|
def calculate_hash(self):
|
|
34
43
|
hasher = hashlib.md5(self.data)
|
|
@@ -221,8 +221,11 @@ class ATEM:
|
|
|
221
221
|
|
|
222
222
|
@property
|
|
223
223
|
def connected(self) -> bool:
|
|
224
|
+
"""True while the session is up AND its state is current — False
|
|
225
|
+
during a transport re-handshake, when the state is empty (see
|
|
226
|
+
``ATEMConnection.is_ready``)."""
|
|
224
227
|
conn = self._conn
|
|
225
|
-
return bool(conn and conn.
|
|
228
|
+
return bool(conn and conn.is_ready)
|
|
226
229
|
|
|
227
230
|
@property
|
|
228
231
|
def video_mode(self) -> str:
|
|
@@ -131,6 +131,12 @@ class ATEMConnection:
|
|
|
131
131
|
# immediately reflects the post-disconnect state.
|
|
132
132
|
self._handshake_done: bool = False
|
|
133
133
|
|
|
134
|
+
# ``_session_ready`` follows the protocol's 'connected' /
|
|
135
|
+
# 'disconnected' events: False from the moment a transport
|
|
136
|
+
# re-handshake empties mixerstate until the new state dump is
|
|
137
|
+
# complete. Read through ``is_ready``.
|
|
138
|
+
self._session_ready: bool = False
|
|
139
|
+
|
|
134
140
|
self._lifecycle_lock = threading.Lock() # serializes connect/disconnect calls
|
|
135
141
|
self._transfer_serial_lock = threading.Lock() # one native transfer at a time
|
|
136
142
|
|
|
@@ -168,6 +174,19 @@ class ATEMConnection:
|
|
|
168
174
|
worker = self._worker
|
|
169
175
|
return worker is not None and worker.is_alive()
|
|
170
176
|
|
|
177
|
+
@property
|
|
178
|
+
def is_ready(self) -> bool:
|
|
179
|
+
"""True iff ``is_connected`` AND the switcher's state is current.
|
|
180
|
+
|
|
181
|
+
After ~5 s without packets the transport re-handshakes on its own:
|
|
182
|
+
the protocol raises 'disconnected' and empties ``mixerstate``, the
|
|
183
|
+
same worker carries on, and 'connected' fires again once the new
|
|
184
|
+
state dump is complete. ``is_connected`` stays True through that
|
|
185
|
+
gap (the worker is alive and recovering, so the pool must not evict
|
|
186
|
+
it); ``is_ready`` is False for its whole length.
|
|
187
|
+
"""
|
|
188
|
+
return self._session_ready and self.is_connected
|
|
189
|
+
|
|
171
190
|
@property
|
|
172
191
|
def mixerstate(self) -> dict:
|
|
173
192
|
"""Live state dict from atemwire. Safe to read from any thread — CPython
|
|
@@ -213,9 +232,13 @@ class ATEMConnection:
|
|
|
213
232
|
is dequeued so it cannot fire much later against a slot an
|
|
214
233
|
operator may have since reused.
|
|
215
234
|
"""
|
|
216
|
-
|
|
235
|
+
# is_ready, not is_connected: during a transport re-handshake the
|
|
236
|
+
# worker is alive but the session is being replaced, and a clear
|
|
237
|
+
# queued into it waited out its whole timeout (bmdwire#8).
|
|
238
|
+
if not self.is_ready:
|
|
217
239
|
raise ConnectionDeadError(
|
|
218
|
-
f"clear_still: connection to {self.ip_address} is not ready"
|
|
240
|
+
f"clear_still: connection to {self.ip_address} is not ready "
|
|
241
|
+
f"(no session, or the session is being re-established)")
|
|
219
242
|
ip = self.ip_address
|
|
220
243
|
protocol = self._protocol
|
|
221
244
|
done = threading.Event()
|
|
@@ -280,9 +303,14 @@ class ATEMConnection:
|
|
|
280
303
|
the transfer so the connection's transfer lane can't stay wedged
|
|
281
304
|
for later callers).
|
|
282
305
|
"""
|
|
283
|
-
|
|
306
|
+
# is_ready, not is_connected: a transfer started during a transport
|
|
307
|
+
# re-handshake queued its lock and transfer requests into the
|
|
308
|
+
# session being replaced and waited out its whole timeout, blaming
|
|
309
|
+
# "another client" for a lock nobody held (bmdwire#8).
|
|
310
|
+
if not self.is_ready:
|
|
284
311
|
raise ConnectionDeadError(
|
|
285
|
-
f"download: connection to {self.ip_address} is not ready"
|
|
312
|
+
f"download: connection to {self.ip_address} is not ready "
|
|
313
|
+
f"(no session, or the session is being re-established)")
|
|
286
314
|
# Captured up front: a disconnect racing the timeout wait clears
|
|
287
315
|
# self.ip_address, and the error would read "on None".
|
|
288
316
|
ip = self.ip_address
|
|
@@ -405,6 +433,7 @@ class ATEMConnection:
|
|
|
405
433
|
self._ready_event.clear()
|
|
406
434
|
self._connect_error = None
|
|
407
435
|
self._handshake_done = False
|
|
436
|
+
self._session_ready = False
|
|
408
437
|
self._died_signalled = False
|
|
409
438
|
self.ip_address = ip_address
|
|
410
439
|
|
|
@@ -478,12 +507,27 @@ class ATEMConnection:
|
|
|
478
507
|
# bare (non-indexed) field so the event name is 'change:macro-play-status'.
|
|
479
508
|
def _on_macro_play(contents):
|
|
480
509
|
running = bool(getattr(contents, 'running', False))
|
|
481
|
-
|
|
510
|
+
# Slot 0 is a real macro; only 0xFFFF (or no index) is idle.
|
|
511
|
+
idx = getattr(contents, 'index', None)
|
|
512
|
+
idx = 0xFFFF if idx is None else int(idx)
|
|
482
513
|
if running and idx != 0xFFFF and idx >= 0:
|
|
483
514
|
self.last_run_macro_index = idx
|
|
484
515
|
|
|
485
516
|
self._protocol.on('change:macro-play-status', _on_macro_play)
|
|
486
517
|
|
|
518
|
+
# Session readiness (``is_ready``): 'disconnected' is raised when
|
|
519
|
+
# the transport gives up on the session and mixerstate is
|
|
520
|
+
# emptied; 'connected' when a state dump is complete, at the
|
|
521
|
+
# first connect and again after every re-handshake.
|
|
522
|
+
def _on_connected():
|
|
523
|
+
self._session_ready = True
|
|
524
|
+
|
|
525
|
+
def _on_disconnected():
|
|
526
|
+
self._session_ready = False
|
|
527
|
+
|
|
528
|
+
self._protocol.on('connected', _on_connected)
|
|
529
|
+
self._protocol.on('disconnected', _on_disconnected)
|
|
530
|
+
|
|
487
531
|
try:
|
|
488
532
|
self._protocol.connect()
|
|
489
533
|
except Exception as e:
|
|
@@ -612,12 +612,13 @@ corresponding op was recorded).
|
|
|
612
612
|
| 0x00DA | `MediaPlayerSourceStillIndex` | `u8 mp, _, u16 LE index` | XML-verified |
|
|
613
613
|
| 0x00E1 | `MediaPlayerSourceStill` | `u8 mp, _, _, _` | XML-verified |
|
|
614
614
|
| 0x014A | `FairlightAudioMixerInputSourceMixType` | `u16 LE source, u16 marker(0x4346), i64 LE sourceId, u16 LE state, u16 marker(0x000c)` (state: 1=Off, 2=On, 4=AFV; sourceId is the universal stereo sentinel `0xFFFFFFFFFFFF0100` = i64 -65280) | live-verified |
|
|
615
|
-
| 0x014B | `FairlightAudioMixerInputSourceFaderGain` | `u16 LE source, u16 marker(0x4346), i64 LE sourceId, i32 LE gain × 65536` (XML scalar = value / 65536,
|
|
615
|
+
| 0x014B | `FairlightAudioMixerInputSourceFaderGain` | `u16 LE source, u16 marker(0x4346), i64 LE sourceId, i32 LE gain × 65536` (XML scalar = value / 65536, six significant digits: -21474836 is `-327.68`, silenced) | live-verified |
|
|
616
616
|
| 0x016B | `FairlightAudioMixerMasterOutFaderGain` | `i32 LE gain × 65536` | live-verified |
|
|
617
617
|
|
|
618
618
|
The strip ops (0x014A/0x014B) carry a sourceId field — Software Control's
|
|
619
619
|
universal stereo sentinel is `0xFFFFFFFFFFFF0100` (= i64 -65280),
|
|
620
|
-
representing "all channels of this source". The
|
|
620
|
+
representing "all channels of this source". The encoder writes it for an
|
|
621
|
+
op with no `sourceId` attribute (or an empty one). The 0x4346 u16 at param
|
|
621
622
|
offset 2 is constant in every captured recording but its meaning isn't
|
|
622
623
|
known; the decoder ignores it and the encoder emits the constant for
|
|
623
624
|
byte-identical round-trip.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# The media store lock, as the switcher actually behaves
|
|
2
|
+
|
|
3
|
+
Everything below was measured on the wire on 2026-09-16 against an ATEM
|
|
4
|
+
1 M/E Constellation HD, with a session that logged every `LKOB` / `LKST` /
|
|
5
|
+
`FTDE` / `FTDC` as it was decoded and every `LOCK` / `PLCK` / `FTSU` as it
|
|
6
|
+
was sent. Other models may differ; nothing here is inferred from
|
|
7
|
+
documentation. The OpenSwitcher protocol notes describe the same commands
|
|
8
|
+
but several of their claims (the automatic release of a partial lock, for
|
|
9
|
+
one) did not hold on this hardware.
|
|
10
|
+
|
|
11
|
+
## Requests and grants
|
|
12
|
+
|
|
13
|
+
* `LOCK store=0 state=1` is answered by `LKOB` in about 1 ms and then an
|
|
14
|
+
`LKST locked` broadcast to every session.
|
|
15
|
+
* `PLCK store=0 slot=n` is answered the same way. It is **not released
|
|
16
|
+
automatically** after one transfer: the explicit `LOCK state=0` is
|
|
17
|
+
required, and its `LKST unlocked` echo arrives about 40 ms later.
|
|
18
|
+
* `LKST` goes out at the switcher's frame tick (every 40 ms at 25p), so a
|
|
19
|
+
request sent with a release, or right after it, is granted (`LKOB`)
|
|
20
|
+
**before** the release is broadcast. See "What ATEM Software Control
|
|
21
|
+
does with an upload".
|
|
22
|
+
* A `PLCK` sent while another session holds the store is **queued by the
|
|
23
|
+
switcher** and granted the moment the holder releases. In that case the
|
|
24
|
+
requester receives `LKOB` directly and there may be **no `LKST unlocked`
|
|
25
|
+
broadcast at all** between the two holders. A client must therefore act
|
|
26
|
+
on `LKOB`, not only on the release broadcast.
|
|
27
|
+
* A `LOCK` (full store) or `PLCK` sent by a session that **already holds**
|
|
28
|
+
the store gets no answer.
|
|
29
|
+
* A `LOCK state=0` from a session that does not hold the store does
|
|
30
|
+
nothing visible (this was tested only while a foreign client held it).
|
|
31
|
+
|
|
32
|
+
## Two sessions at once
|
|
33
|
+
|
|
34
|
+
* Two sessions requesting within ~150 ms can **both** receive `LKOB`. The
|
|
35
|
+
first `FTSU` to be serviced wins; the other transfer is aborted with
|
|
36
|
+
**`FTDE status 6`**. That session **still holds the lock** after the 6;
|
|
37
|
+
the winner's `PLCK` stays queued behind it. The only correct reaction to
|
|
38
|
+
a 6 is: release, keep the task, and re-request when the store frees.
|
|
39
|
+
Re-requesting without releasing deadlocks both sessions until their
|
|
40
|
+
timeouts.
|
|
41
|
+
* With that rule two sessions interleave one frame at a time, exactly like
|
|
42
|
+
two ATEM Software Control instances: four 1080p downloads across two
|
|
43
|
+
sessions complete in 8.2 s, the wire floor.
|
|
44
|
+
* `FTDC` for a transfer is broadcast to **every** session, with the owner's
|
|
45
|
+
transfer id. A session must match the id against its own transfer.
|
|
46
|
+
|
|
47
|
+
## Session end
|
|
48
|
+
|
|
49
|
+
* A lock still held when the session says goodbye (hello opcode 0x04) is
|
|
50
|
+
released by the switcher within **10 ms**.
|
|
51
|
+
* A lock held by a session whose socket simply closed (no goodbye) is
|
|
52
|
+
released when the switcher reaps the session, about **5 s** later. (The
|
|
53
|
+
"five minutes" that older comments in this package repeated was never
|
|
54
|
+
measured.)
|
|
55
|
+
|
|
56
|
+
## The state dump, and the bug that looked like a foreign lock
|
|
57
|
+
|
|
58
|
+
* The switcher answers a request sent **during** its initial state dump
|
|
59
|
+
normally; the `LKOB` is the first packet after the dump.
|
|
60
|
+
* `atemwire.transport.receive_packet` used to throw that packet away: it
|
|
61
|
+
pulled a packet from the queue, saw it owed the caller the
|
|
62
|
+
`ConnectionReady` sentinel (the packet after `InCm`), returned the
|
|
63
|
+
sentinel and dropped the packet. The switcher retransmitted the `LKOB`
|
|
64
|
+
every 80 ms, and every copy was deduplicated as already received. The
|
|
65
|
+
session then **held the media lock without knowing it** for the rest of
|
|
66
|
+
its life, and every other client, ATEM Software Control included, was
|
|
67
|
+
refused the store. From the outside that is "locked by another instance"
|
|
68
|
+
with nobody else there. The same shape existed for the
|
|
69
|
+
`TransferQueueFlushed` sentinel mid-upload. Fixed in 1.0.3: a displaced
|
|
70
|
+
packet is kept and handed out on the next call.
|
|
71
|
+
* `atemwire.ready.wait_ready` used to return on the **first** packet of the
|
|
72
|
+
dump (`protocol.connected` flips on any data packet; `video-mode` is in
|
|
73
|
+
that first packet). Since 1.0.3 it waits for the whole dump
|
|
74
|
+
(`protocol.initialized`, set when `InCm` has been processed). A session
|
|
75
|
+
on a LAN reports ready in ~0.5 s with ~107 state keys, where it used to
|
|
76
|
+
report ready in 4 ms with 25.
|
|
77
|
+
* Until 1.0.3 the dump itself was not ACKed until the switcher's first
|
|
78
|
+
no-data packet after it. Any dump slower than the switcher's ~80 ms
|
|
79
|
+
retransmit timer was resent in full, repeatedly. Reliable packets are now
|
|
80
|
+
ACKed as soon as the session is established.
|
|
81
|
+
|
|
82
|
+
## Timing that is ours, not the switcher's
|
|
83
|
+
|
|
84
|
+
* A 1080p still downloads in ~1.75 s from `FTSU` to `FTDC`. The RLE decode
|
|
85
|
+
that follows is pure Python and takes ~250 ms on the connection's worker
|
|
86
|
+
thread. Since 1.0.3 the lock is released **before** that decode; before,
|
|
87
|
+
every frame held the store a quarter second longer than the transfer
|
|
88
|
+
needed. The decode still blocks the worker for those 250 ms; moving it
|
|
89
|
+
into the C extension is the remaining improvement.
|
|
90
|
+
|
|
91
|
+
## What ATEM Software Control does with the media pool
|
|
92
|
+
|
|
93
|
+
A Wireshark capture on the ASC machine, 2026-10-14: ATEM Software Control
|
|
94
|
+
connecting to the same model (firmware 9.2.1, 1080p25) with 20 stills in
|
|
95
|
+
the pool, 8 different pictures, then saving a profile.
|
|
96
|
+
|
|
97
|
+
* **On connecting, ASC downloads every used still in full**, one after
|
|
98
|
+
another, starting 0.75 s after its hello (once the dump is in): 20
|
|
99
|
+
transfers of 8.32 MB, about 5,940 `FTDa` chunks of about 1,400 bytes
|
|
100
|
+
each, **1.78 to 1.82 s apiece from `FTSU` to `FTDC`**, with 17 to 39 ms
|
|
101
|
+
(median 24) from one `FTDC` to the next `FTSU`. It downloads repeated
|
|
102
|
+
pictures too: 20 transfers for 8 pictures.
|
|
103
|
+
* It takes the **whole store** for each still, `LOCK store=0 state=1`
|
|
104
|
+
(the last byte varied: `0x64`, `0xff`), and releases it with
|
|
105
|
+
`LOCK store=0 state=0` after each `FTDC`. It never sent a `PLCK`.
|
|
106
|
+
* Every `FTDa` is acknowledged with an `FTUA`; its second field is
|
|
107
|
+
nonzero and varies, where atemwire sends 0. Up to 153 chunks were in
|
|
108
|
+
flight before an acknowledgement.
|
|
109
|
+
* `FTSU`'s last four bytes were `00 ef 67 70` for a still and
|
|
110
|
+
`03 xx xx 00` for a macro (atemwire sends `00 d0 9b 8c` for a still).
|
|
111
|
+
* **Saving the profile, 61 s in, downloaded only the macros**: the six
|
|
112
|
+
used macro slots (three name-only, no ops), in 0.5 s, and **no still**.
|
|
113
|
+
ASC's quick save writes the stills it downloaded on connecting.
|
|
114
|
+
|
|
115
|
+
So a still comes off this switcher at its own pace, about 1.8 s for 1080p,
|
|
116
|
+
for ATEM Software Control as for atemwire (a client of this package,
|
|
117
|
+
bmdctl, measured 1.80 to 1.83 s on the same pool). The wire floor above
|
|
118
|
+
holds for one session too. Nothing in the capture is a faster transfer:
|
|
119
|
+
the whole-store lock, the acknowledgement's second field and `FTSU`'s
|
|
120
|
+
suffix cost the same time. A client that wants a quick save keeps the
|
|
121
|
+
stills it has downloaded, which is what `download_media_pool_images`'s
|
|
122
|
+
`cache_get` and `cache_put` are for (WebATEM 2.1.0 keeps them on disk by
|
|
123
|
+
hash), rather than looking for a faster download.
|
|
124
|
+
|
|
125
|
+
## What ATEM Software Control does with an upload
|
|
126
|
+
|
|
127
|
+
A Wireshark capture on the ASC machine, 2026-10-15: ATEM Software Control
|
|
128
|
+
restoring a profile with 20 stills (8 pictures) to the same model
|
|
129
|
+
(firmware 9.2.1, 1080p25), into a pool already holding them but for two
|
|
130
|
+
slots changed beforehand.
|
|
131
|
+
|
|
132
|
+
* **It uploads every still the profile names**, changed or not: 20
|
|
133
|
+
uploads. It doesn't skip a slot that already holds its picture.
|
|
134
|
+
* **Twenty photographic 1080p stills in about 12 s.** Each sends 0.31 s of
|
|
135
|
+
data (5,955 to 5,958 chunks, 8.29 MB: RLE barely shortens a
|
|
136
|
+
photograph), takes 0.52 to 0.60 s from `FTSD` to `FTDC`, and the next
|
|
137
|
+
`FTSD` follows 35 to 54 ms after the `FTDC`. A client of this package,
|
|
138
|
+
pacing five chunks per packet from the switcher, took 23 s a still.
|
|
139
|
+
* **The handover.** 1 to 5 ms after each `FTDC`, `LOCK store=0 state=0`;
|
|
140
|
+
the next still's `PLCK` in the same packet (8 of 19 times) or the next
|
|
141
|
+
one, 3 to 21 ms after the `FTDC`. `LKOB` comes 0.3 to 0.7 ms after the
|
|
142
|
+
`PLCK`, every time, before any `LKST`. The release's `LKST` comes at the
|
|
143
|
+
switcher's next frame tick (they sit on a 40 ms grid), after that
|
|
144
|
+
`LKOB` and often after the next `FTSD`: `state=0` with the released slot
|
|
145
|
+
in byte 3, then `state=1`, or two `state=1` when the release and the
|
|
146
|
+
request shared a packet. ASC holds the lock from `LKOB` to its own
|
|
147
|
+
release whatever `LKST` says.
|
|
148
|
+
* Its downloads on connecting hand over the same way with the whole store
|
|
149
|
+
(the 2026-10-14 capture): `LOCK state=0` and `LOCK state=1` in one
|
|
150
|
+
packet, `LKOB` 0.8 ms later.
|
|
151
|
+
* **`FTSD`** follows the `LKOB` by 10 to 35 ms (ASC's own pace), not the
|
|
152
|
+
`LKST`. Its bytes are atemwire's: transfer id, store 0, slot,
|
|
153
|
+
`0x007e9000` (the 8,294,400-byte frame), mode 1.
|
|
154
|
+
* **`FTFD` goes with the first budget**: 0.1 to 1.6 ms after the first
|
|
155
|
+
`FTCD` when that came about 30 ms after the `FTSD`, or among the first
|
|
156
|
+
150 to 424 chunks when it came within 6 ms. Never after the data. Its
|
|
157
|
+
name and description fields carry leftover memory after the NUL.
|
|
158
|
+
* **Budgets.** `FTCD` grants 768 chunks of 1,396 bytes (a few 601 to 760),
|
|
159
|
+
nine per still. ASC sends each whole, one `FTDa` per packet, back to
|
|
160
|
+
back: 768 chunks in about 35 ms, 24 µs apart (median). The next `FTCD`
|
|
161
|
+
comes about 5 ms after the last chunk. The switcher acknowledged about
|
|
162
|
+
every 16 packets, at most 88 behind.
|
|
163
|
+
* `PLCK`'s bytes 4 to 7 were `ab 01 a9 9b` in every request, where the
|
|
164
|
+
2026-09-16 notes found `ff 01 00 00`: byte 5 is the constant.
|
|
165
|
+
* Nothing was lost or resent either way. ASC's acknowledgement-only
|
|
166
|
+
packets carry sequence number 0, so nothing tells the switcher that a
|
|
167
|
+
last packet went missing until it is sent again: a client resends what
|
|
168
|
+
isn't acknowledged on a timer, as the switcher does (every ~80 ms).
|
|
169
|
+
|
|
170
|
+
### The lock request that seemed to be eaten
|
|
171
|
+
|
|
172
|
+
From 2026-07-07 atemwire held back a lock request until the `LKST` echo of
|
|
173
|
+
its own release, believing the switcher silently drops a request that
|
|
174
|
+
arrives while an unlock is in flight. ASC sends exactly that request, every
|
|
175
|
+
time, and the switcher grants it. What happened was the order above: the
|
|
176
|
+
release's broadcast comes after the grant, and atemwire's lock-state
|
|
177
|
+
handler took a release broadcast as the end of its own hold. It forgot the
|
|
178
|
+
new lock, skipped the release after that transfer, and asked again as the
|
|
179
|
+
holder, which the switcher ignores (see "Requests and grants"). Waiting
|
|
180
|
+
for the echo hid it, at a frame per transfer at best and up to 6.6 s
|
|
181
|
+
measured, during a 20-still restore by bmdctl, which mirrors this code.
|
|
182
|
+
Since `bd6ae77` atemwire holds the lock from `LKOB` to its own release, as
|
|
183
|
+
ASC does, and sends the release and the next request in one packet.
|
|
184
|
+
|
|
@@ -531,7 +531,7 @@ not the wait — implement your own settle if you need it.
|
|
|
531
531
|
<Limiter enabled="..." threshold="..." attack="..." hold="..." release="..."/>
|
|
532
532
|
</MasterOutDynamicsProcessor>
|
|
533
533
|
<AudioInputs>
|
|
534
|
-
<AudioInput configuration="Stereo|Mono" id="...">
|
|
534
|
+
<AudioInput configuration="Stereo|Mono|DualMono" id="...">
|
|
535
535
|
<Analog level="ProLine|Mic"/> <!-- only on inputs that have analog config -->
|
|
536
536
|
<AudioSource id="-65280" inputGain="..." pan="..." faderGain="..." mixOption="..." delayFrames="...">
|
|
537
537
|
<Equalizer enabled="..." gain="...">
|
|
@@ -614,15 +614,26 @@ configuration (i.e. the TRS jack, source 1401). Levels: `ProLine`
|
|
|
614
614
|
(line) or `Mic`. Maps to `mixerstate['fairlight-audio-input'].level`
|
|
615
615
|
(1=mic, 2=line).
|
|
616
616
|
|
|
617
|
-
`<AudioSource id="...">`
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
617
|
+
`<AudioSource id="...">` is one strip of the input. Its id is the
|
|
618
|
+
strip's 8-byte wire address (the one `CFSP`, `CEBP` and `FASP` carry:
|
|
619
|
+
six `0xFF` bytes, the split byte, the channel byte) read as a signed
|
|
620
|
+
i64. Captured from ATEM Software Control with one input in Dual Mono
|
|
621
|
+
(bmdwire#9, fixture `tests/profile_fairlight_dual_mono_asc.xml`):
|
|
622
|
+
|
|
623
|
+
| id | address | strip | setter `channel` |
|
|
624
|
+
|---|---|---|---|
|
|
625
|
+
| `-65280` | `ff…ff 01 00` | a stereo input's strip | `-1` |
|
|
626
|
+
| `-256` | `ff…ff ff 00` | a dual-mono input's left half; the mono XLR (1301) | `0` |
|
|
627
|
+
| `-255` | `ff…ff ff 01` | a dual-mono input's right half | `1` |
|
|
628
|
+
|
|
629
|
+
A dual-mono input is `configuration="DualMono"` with two
|
|
630
|
+
`<AudioSource>` elements, left first, each with its own fader, pan, EQ
|
|
631
|
+
and dynamics. Save writes that when the input reports two split strips
|
|
632
|
+
(`FASP.is_split == 0xff`, strip ids `"<source>.0"` and `"<source>.1"`),
|
|
633
|
+
in subchannel order; apply sends each `<AudioSource>` to the channel its
|
|
634
|
+
id names (`_enums.audio_source_channel`). Apply doesn't change an
|
|
635
|
+
input's configuration (atemwire has no command for it), so a dual-mono
|
|
636
|
+
profile restores onto an input that is already in Dual Mono.
|
|
626
637
|
|
|
627
638
|
| AudioSource attr | Source | Op |
|
|
628
639
|
|------------------|--------|-----|
|
|
@@ -799,13 +810,16 @@ Each Op uses a small grammar of attributes:
|
|
|
799
810
|
- `clip` / `gain` / `xSize` / `ySize` / etc.: floats matching the
|
|
800
811
|
units used elsewhere in the file (e.g. `clip="0.300003"` is a 0..1
|
|
801
812
|
scalar; `gain="-0.0800018"` is dB; `xPosition="0"` is unit-scaled).
|
|
802
|
-
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
813
|
+
- Floats are written to six significant digits (`%g`), as ASC writes
|
|
814
|
+
them: Q16.16 -5243 is `-0.0800018`, 471859 is `7.2` (bmdwire#17).
|
|
815
|
+
- Audio-op `gain` is in dB. `-327.68` is the silenced fader, Q16.16
|
|
816
|
+
-21474836 (bytes `ec 51 b8 fe`), ≡ `-inf` in the declarative
|
|
817
|
+
`<AudioSource>` representation.
|
|
818
|
+
- Audio-op `sourceId` is the `<AudioSource id>` value read unsigned:
|
|
819
|
+
`18446744073709486336` = `0xFFFFFFFFFFFF0100` is -65280 (a stereo
|
|
820
|
+
strip), and a dual-mono input's halves are `18446744073709551360`
|
|
821
|
+
(-256, left) and `18446744073709551361` (-255, right), as ASC
|
|
822
|
+
recorded them (bmdwire#9).
|
|
809
823
|
|
|
810
824
|
**Macro round-trip uses bytecode upload (partial-upload policy) as of
|
|
811
825
|
2026-04-29.**
|