bmdwire 1.3.0__tar.gz → 1.5.1__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.3.0/bmdwire.egg-info → bmdwire-1.5.1}/PKG-INFO +2 -2
- {bmdwire-1.3.0 → bmdwire-1.5.1}/README.md +1 -1
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/_transfer.py +9 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/connection.py +13 -4
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/docs/MACRO_FORMAT.md +3 -2
- bmdwire-1.5.1/atemwire/docs/MEDIA_LOCK.md +235 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/docs/PROFILE_FORMAT.md +31 -17
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/docs/SURFACE.md +140 -26
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/helpers.py +42 -1
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/_helpers.py +19 -39
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/fairlight.py +9 -6
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/upstream_keyer.py +20 -18
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/pool.py +3 -2
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/_enums.py +36 -2
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/apply.py +12 -9
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/save.py +50 -11
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/protocol.py +256 -63
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/transport.py +190 -13
- {bmdwire-1.3.0 → bmdwire-1.5.1/bmdwire.egg-info}/PKG-INFO +2 -2
- {bmdwire-1.3.0 → bmdwire-1.5.1}/hyperdeckwire/client.py +81 -3
- {bmdwire-1.3.0 → bmdwire-1.5.1}/pyproject.toml +1 -1
- bmdwire-1.3.0/atemwire/docs/MEDIA_LOCK.md +0 -85
- {bmdwire-1.3.0 → bmdwire-1.5.1}/LICENSE +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/LICENSE +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/LICENSE-gpl3.txt +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/NOTICE +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/__init__.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/_socketqueue.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/atem.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/docs/PROFILE_SAVE_RESTORE.md +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/imaging.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/__init__.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/_io.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/downstream_keyer.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/flow.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/hyperdeck.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/media.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/switching.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/upstream_keyer.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/mediaconvertmodule.c +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/__init__.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/_dsl.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/audio_legacy.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/camera_control.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/color_generator.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/downstream_keyer.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/fade_to_black.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/fairlight.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/file_transfer.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/hyperdeck.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/input_video.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/lock.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/macros.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/manual.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/media.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/meters.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/multiviewer.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/recording.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/streaming.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/supersource.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/switching.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/system_info.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/tally.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/transition.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/probe.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/__init__.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/_common.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/_xml.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/options.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/py.typed +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/ready.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/state.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/testing.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/bmdwire.egg-info/SOURCES.txt +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/bmdwire.egg-info/dependency_links.txt +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/bmdwire.egg-info/requires.txt +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/bmdwire.egg-info/top_level.txt +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/hyperdeckwire/LICENSE +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/hyperdeckwire/__init__.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/hyperdeckwire/setup.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/hyperdeckwire/upload.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/setup.cfg +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/setup.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/LICENSE +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/__init__.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/_preamble.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/_protocol.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/network.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/profile/__init__.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/profile/_ranges.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/profile/archive.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/profile/restore.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/videohubwire/LICENSE +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/videohubwire/__init__.py +0 -0
- {bmdwire-1.3.0 → bmdwire-1.5.1}/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.1
|
|
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.1`) 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.1`) 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)
|
|
@@ -232,9 +232,13 @@ class ATEMConnection:
|
|
|
232
232
|
is dequeued so it cannot fire much later against a slot an
|
|
233
233
|
operator may have since reused.
|
|
234
234
|
"""
|
|
235
|
-
|
|
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:
|
|
236
239
|
raise ConnectionDeadError(
|
|
237
|
-
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)")
|
|
238
242
|
ip = self.ip_address
|
|
239
243
|
protocol = self._protocol
|
|
240
244
|
done = threading.Event()
|
|
@@ -299,9 +303,14 @@ class ATEMConnection:
|
|
|
299
303
|
the transfer so the connection's transfer lane can't stay wedged
|
|
300
304
|
for later callers).
|
|
301
305
|
"""
|
|
302
|
-
|
|
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:
|
|
303
311
|
raise ConnectionDeadError(
|
|
304
|
-
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)")
|
|
305
314
|
# Captured up front: a disconnect racing the timeout wait clears
|
|
306
315
|
# self.ip_address, and the error would read "on None".
|
|
307
316
|
ip = self.ip_address
|
|
@@ -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,235 @@
|
|
|
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` sent while another session holds the store is **queued** too,
|
|
30
|
+
like a `PLCK` (ATEM Software Control's, 2026-10-15; this package's
|
|
31
|
+
comments used to say it was dropped). Handed the store at the holder's
|
|
32
|
+
release, the requester gets `LKOB` and **nothing is announced**, no
|
|
33
|
+
`LKST` either way.
|
|
34
|
+
* A `PLCK` is granted beside another session's `LOCK`, even half a second
|
|
35
|
+
into its turn; the `FTSU` that follows aborts the other's transfer in
|
|
36
|
+
progress with `FTDE 6` (see "Sharing the store with ATEM Software
|
|
37
|
+
Control").
|
|
38
|
+
* A `LOCK state=0` from a session that does not hold the store does
|
|
39
|
+
nothing visible (this was tested only while a foreign client held it).
|
|
40
|
+
|
|
41
|
+
## Two sessions at once
|
|
42
|
+
|
|
43
|
+
* Two sessions requesting within ~150 ms can **both** receive `LKOB`. The
|
|
44
|
+
first `FTSU` to be serviced wins; the other transfer is aborted with
|
|
45
|
+
**`FTDE status 6`**. That session **still holds the lock** after the 6;
|
|
46
|
+
the winner's `PLCK` stays queued behind it. The only correct reaction to
|
|
47
|
+
a 6 is: release, keep the task, and re-request when the store frees.
|
|
48
|
+
Re-requesting without releasing deadlocks both sessions until their
|
|
49
|
+
timeouts.
|
|
50
|
+
* With that rule two sessions interleave one frame at a time, exactly like
|
|
51
|
+
two ATEM Software Control instances: four 1080p downloads across two
|
|
52
|
+
sessions complete in 8.2 s, the wire floor.
|
|
53
|
+
* `FTDC` for a transfer is broadcast to **every** session, with the owner's
|
|
54
|
+
transfer id. A session must match the id against its own transfer.
|
|
55
|
+
|
|
56
|
+
## Session end
|
|
57
|
+
|
|
58
|
+
* A lock still held when the session says goodbye (hello opcode 0x04) is
|
|
59
|
+
released by the switcher within **10 ms**.
|
|
60
|
+
* A lock held by a session whose socket simply closed (no goodbye) is
|
|
61
|
+
released when the switcher reaps the session, about **5 s** later. (The
|
|
62
|
+
"five minutes" that older comments in this package repeated was never
|
|
63
|
+
measured.)
|
|
64
|
+
|
|
65
|
+
## The state dump, and the bug that looked like a foreign lock
|
|
66
|
+
|
|
67
|
+
* The switcher answers a request sent **during** its initial state dump
|
|
68
|
+
normally; the `LKOB` is the first packet after the dump.
|
|
69
|
+
* `atemwire.transport.receive_packet` used to throw that packet away: it
|
|
70
|
+
pulled a packet from the queue, saw it owed the caller the
|
|
71
|
+
`ConnectionReady` sentinel (the packet after `InCm`), returned the
|
|
72
|
+
sentinel and dropped the packet. The switcher retransmitted the `LKOB`
|
|
73
|
+
every 80 ms, and every copy was deduplicated as already received. The
|
|
74
|
+
session then **held the media lock without knowing it** for the rest of
|
|
75
|
+
its life, and every other client, ATEM Software Control included, was
|
|
76
|
+
refused the store. From the outside that is "locked by another instance"
|
|
77
|
+
with nobody else there. The same shape existed for the
|
|
78
|
+
`TransferQueueFlushed` sentinel mid-upload. Fixed in 1.0.3: a displaced
|
|
79
|
+
packet is kept and handed out on the next call.
|
|
80
|
+
* `atemwire.ready.wait_ready` used to return on the **first** packet of the
|
|
81
|
+
dump (`protocol.connected` flips on any data packet; `video-mode` is in
|
|
82
|
+
that first packet). Since 1.0.3 it waits for the whole dump
|
|
83
|
+
(`protocol.initialized`, set when `InCm` has been processed). A session
|
|
84
|
+
on a LAN reports ready in ~0.5 s with ~107 state keys, where it used to
|
|
85
|
+
report ready in 4 ms with 25.
|
|
86
|
+
* Until 1.0.3 the dump itself was not ACKed until the switcher's first
|
|
87
|
+
no-data packet after it. Any dump slower than the switcher's ~80 ms
|
|
88
|
+
retransmit timer was resent in full, repeatedly. Reliable packets are now
|
|
89
|
+
ACKed as soon as the session is established.
|
|
90
|
+
|
|
91
|
+
## Timing that is ours, not the switcher's
|
|
92
|
+
|
|
93
|
+
* A 1080p still downloads in ~1.75 s from `FTSU` to `FTDC`. The RLE decode
|
|
94
|
+
that follows is pure Python and takes ~250 ms on the connection's worker
|
|
95
|
+
thread. Since 1.0.3 the lock is released **before** that decode; before,
|
|
96
|
+
every frame held the store a quarter second longer than the transfer
|
|
97
|
+
needed. The decode still blocks the worker for those 250 ms; moving it
|
|
98
|
+
into the C extension is the remaining improvement.
|
|
99
|
+
|
|
100
|
+
## What ATEM Software Control does with the media pool
|
|
101
|
+
|
|
102
|
+
A Wireshark capture on the ASC machine, 2026-10-14: ATEM Software Control
|
|
103
|
+
connecting to the same model (firmware 9.2.1, 1080p25) with 20 stills in
|
|
104
|
+
the pool, 8 different pictures, then saving a profile.
|
|
105
|
+
|
|
106
|
+
* **On connecting, ASC downloads every used still in full**, one after
|
|
107
|
+
another, starting 0.75 s after its hello (once the dump is in): 20
|
|
108
|
+
transfers of 8.32 MB, about 5,940 `FTDa` chunks of about 1,400 bytes
|
|
109
|
+
each, **1.78 to 1.82 s apiece from `FTSU` to `FTDC`**, with 17 to 39 ms
|
|
110
|
+
(median 24) from one `FTDC` to the next `FTSU`. It downloads repeated
|
|
111
|
+
pictures too: 20 transfers for 8 pictures.
|
|
112
|
+
* It takes the **whole store** for each still, `LOCK store=0 state=1`
|
|
113
|
+
(the last byte varied: `0x64`, `0xff`), and releases it with
|
|
114
|
+
`LOCK store=0 state=0` after each `FTDC`. It never sent a `PLCK`.
|
|
115
|
+
* Every `FTDa` is acknowledged with an `FTUA`; its second field is
|
|
116
|
+
nonzero and varies, where atemwire sends 0. Up to 153 chunks were in
|
|
117
|
+
flight before an acknowledgement.
|
|
118
|
+
* `FTSU`'s last four bytes were `00 ef 67 70` for a still and
|
|
119
|
+
`03 xx xx 00` for a macro (atemwire sends `00 d0 9b 8c` for a still).
|
|
120
|
+
* **Saving the profile, 61 s in, downloaded only the macros**: the six
|
|
121
|
+
used macro slots (three name-only, no ops), in 0.5 s, and **no still**.
|
|
122
|
+
ASC's quick save writes the stills it downloaded on connecting.
|
|
123
|
+
|
|
124
|
+
So a still comes off this switcher at its own pace, about 1.8 s for 1080p,
|
|
125
|
+
for ATEM Software Control as for atemwire (a client of this package,
|
|
126
|
+
bmdctl, measured 1.80 to 1.83 s on the same pool). The wire floor above
|
|
127
|
+
holds for one session too. Nothing in the capture is a faster transfer:
|
|
128
|
+
the whole-store lock, the acknowledgement's second field and `FTSU`'s
|
|
129
|
+
suffix cost the same time. A client that wants a quick save keeps the
|
|
130
|
+
stills it has downloaded, which is what `download_media_pool_images`'s
|
|
131
|
+
`cache_get` and `cache_put` are for (WebATEM 2.1.0 keeps them on disk by
|
|
132
|
+
hash), rather than looking for a faster download.
|
|
133
|
+
|
|
134
|
+
## What ATEM Software Control does with an upload
|
|
135
|
+
|
|
136
|
+
A Wireshark capture on the ASC machine, 2026-10-15: ATEM Software Control
|
|
137
|
+
restoring a profile with 20 stills (8 pictures) to the same model
|
|
138
|
+
(firmware 9.2.1, 1080p25), into a pool already holding them but for two
|
|
139
|
+
slots changed beforehand.
|
|
140
|
+
|
|
141
|
+
* **It uploads every still the profile names**, changed or not: 20
|
|
142
|
+
uploads. It doesn't skip a slot that already holds its picture.
|
|
143
|
+
* **Twenty photographic 1080p stills in about 12 s.** Each sends 0.31 s of
|
|
144
|
+
data (5,955 to 5,958 chunks, 8.29 MB: RLE barely shortens a
|
|
145
|
+
photograph), takes 0.52 to 0.60 s from `FTSD` to `FTDC`, and the next
|
|
146
|
+
`FTSD` follows 35 to 54 ms after the `FTDC`. A client of this package,
|
|
147
|
+
pacing five chunks per packet from the switcher, took 23 s a still.
|
|
148
|
+
* **The handover.** 1 to 5 ms after each `FTDC`, `LOCK store=0 state=0`;
|
|
149
|
+
the next still's `PLCK` in the same packet (8 of 19 times) or the next
|
|
150
|
+
one, 3 to 21 ms after the `FTDC`. `LKOB` comes 0.3 to 0.7 ms after the
|
|
151
|
+
`PLCK`, every time, before any `LKST`. The release's `LKST` comes at the
|
|
152
|
+
switcher's next frame tick (they sit on a 40 ms grid), after that
|
|
153
|
+
`LKOB` and often after the next `FTSD`: `state=0` with the released slot
|
|
154
|
+
in byte 3, then `state=1`, or two `state=1` when the release and the
|
|
155
|
+
request shared a packet. ASC holds the lock from `LKOB` to its own
|
|
156
|
+
release whatever `LKST` says.
|
|
157
|
+
* Its downloads on connecting hand over the same way with the whole store
|
|
158
|
+
(the 2026-10-14 capture): `LOCK state=0` and `LOCK state=1` in one
|
|
159
|
+
packet, `LKOB` 0.8 ms later.
|
|
160
|
+
* **`FTSD`** follows the `LKOB` by 10 to 35 ms (ASC's own pace), not the
|
|
161
|
+
`LKST`. Its bytes are atemwire's: transfer id, store 0, slot,
|
|
162
|
+
`0x007e9000` (the 8,294,400-byte frame), mode 1.
|
|
163
|
+
* **`FTFD` goes with the first budget**: 0.1 to 1.6 ms after the first
|
|
164
|
+
`FTCD` when that came about 30 ms after the `FTSD`, or among the first
|
|
165
|
+
150 to 424 chunks when it came within 6 ms. Never after the data. Its
|
|
166
|
+
name and description fields carry leftover memory after the NUL.
|
|
167
|
+
* **Budgets.** `FTCD` grants 768 chunks of 1,396 bytes (a few 601 to 760),
|
|
168
|
+
nine per still. ASC sends each whole, one `FTDa` per packet, back to
|
|
169
|
+
back: 768 chunks in about 35 ms, 24 µs apart (median). The next `FTCD`
|
|
170
|
+
comes about 5 ms after the last chunk. The switcher acknowledged about
|
|
171
|
+
every 16 packets, at most 88 behind.
|
|
172
|
+
* `PLCK`'s bytes 4 to 7 were `ab 01 a9 9b` in every request, where the
|
|
173
|
+
2026-09-16 notes found `ff 01 00 00`: byte 5 is the constant.
|
|
174
|
+
* Nothing was lost or resent either way. ASC's acknowledgement-only
|
|
175
|
+
packets carry sequence number 0, so nothing tells the switcher that a
|
|
176
|
+
last packet went missing until it is sent again: a client resends what
|
|
177
|
+
isn't acknowledged on a timer, as the switcher does (every ~80 ms).
|
|
178
|
+
|
|
179
|
+
### The lock request that seemed to be eaten
|
|
180
|
+
|
|
181
|
+
From 2026-07-07 atemwire held back a lock request until the `LKST` echo of
|
|
182
|
+
its own release, believing the switcher silently drops a request that
|
|
183
|
+
arrives while an unlock is in flight. ASC sends exactly that request, every
|
|
184
|
+
time, and the switcher grants it. What happened was the order above: the
|
|
185
|
+
release's broadcast comes after the grant, and atemwire's lock-state
|
|
186
|
+
handler took a release broadcast as the end of its own hold. It forgot the
|
|
187
|
+
new lock, skipped the release after that transfer, and asked again as the
|
|
188
|
+
holder, which the switcher ignores (see "Requests and grants"). Waiting
|
|
189
|
+
for the echo hid it, at a frame per transfer at best and up to 6.6 s
|
|
190
|
+
measured, during a 20-still restore by bmdctl, which mirrors this code.
|
|
191
|
+
Since `bd6ae77` atemwire holds the lock from `LKOB` to its own release, as
|
|
192
|
+
ASC does. That commit also sent the release and the next request in one
|
|
193
|
+
packet, as ASC does; that shut ASC out, and since `5827626` the next
|
|
194
|
+
request waits its turn (below).
|
|
195
|
+
|
|
196
|
+
## Sharing the store with ATEM Software Control
|
|
197
|
+
|
|
198
|
+
Three tests on 2026-10-15, ATEM Software Control connecting to the same
|
|
199
|
+
switcher while a client of this package downloaded stills back to back,
|
|
200
|
+
two of them captured on the ASC machine.
|
|
201
|
+
|
|
202
|
+
* When it connects, ASC fetches every used still for its thumbnails, one
|
|
203
|
+
at a time, each under a whole-store `LOCK`. A `LOCK` sent while the
|
|
204
|
+
other client held the store was queued and handed to ASC at that
|
|
205
|
+
client's release, with `LKOB` and no `LKST`.
|
|
206
|
+
* With 1.5.0 the other client then asked again at once (its release and
|
|
207
|
+
next request in one packet), or within half a second: its `PLCK` was
|
|
208
|
+
granted beside ASC's `LOCK`, and its `FTSU` aborted ASC's download in
|
|
209
|
+
progress, 230 to 500 ms in, with `FTDE 6`.
|
|
210
|
+
* ASC reacts to the 6 as the notes above say a client should: it
|
|
211
|
+
releases and asks again in one packet, is queued, and is handed the
|
|
212
|
+
store at the next release, to be aborted again. **But it tries each
|
|
213
|
+
still once**: after the 6 it moves on to the next. After 20 stills and
|
|
214
|
+
20 `FTDE 6` it released the store and asked for nothing more, its
|
|
215
|
+
thumbnails left empty. It fetched them again only when the media pool
|
|
216
|
+
changed (a still cleared from another client).
|
|
217
|
+
* Between its own stills ASC releases and asks again in one packet; the
|
|
218
|
+
switcher announces that as a release and a lock, often in one packet.
|
|
219
|
+
* What lets ASC have its turn (`5827626`): after a release with more
|
|
220
|
+
work queued, ask again only once the store is announced free, and a
|
|
221
|
+
frame later (`LOCK_COURTESY`, 60 ms), so a lock announced right behind
|
|
222
|
+
the release (another client's next transfer) cancels the request;
|
|
223
|
+
never ask while another client is seen holding the store; and with no
|
|
224
|
+
announcement at all (the store handed to a waiting request), wait as
|
|
225
|
+
long as anyone is seen at work, another session's `FTDC` or any `LKST`,
|
|
226
|
+
asking after `LOCK_QUIET` (10 s) of quiet. With it ASC loaded all 20
|
|
227
|
+
thumbnails while the other client downloaded; the download paused 38 s
|
|
228
|
+
for ASC's 20 and went on. Alone, 20 uploads take 14.2 s, against
|
|
229
|
+
12.7 s with 1.5.0 and about 12 s for ASC.
|
|
230
|
+
* atemwire 1.4.0 waited for its own release's `LKST` echo before asking
|
|
231
|
+
again, so it waited out a client handed the store too, by accident.
|
|
232
|
+
bmdctl, which mirrors this code, saw that as unexplained pauses of 2 to
|
|
233
|
+
6.6 s between stills in a restore: WebATEM, open on the same switcher,
|
|
234
|
+
was downloading each newly uploaded still for its thumbnail.
|
|
235
|
+
|
|
@@ -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.**
|