bmdwire 1.5.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.5.0/bmdwire.egg-info → bmdwire-1.5.1}/PKG-INFO +2 -2
- {bmdwire-1.5.0 → bmdwire-1.5.1}/README.md +1 -1
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/docs/MEDIA_LOCK.md +52 -1
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/docs/SURFACE.md +15 -2
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/protocol.py +129 -43
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/transport.py +24 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1/bmdwire.egg-info}/PKG-INFO +2 -2
- {bmdwire-1.5.0 → bmdwire-1.5.1}/pyproject.toml +1 -1
- {bmdwire-1.5.0 → bmdwire-1.5.1}/LICENSE +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/LICENSE +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/LICENSE-gpl3.txt +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/NOTICE +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/__init__.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/_socketqueue.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/_transfer.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/atem.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/connection.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/docs/MACRO_FORMAT.md +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/docs/PROFILE_FORMAT.md +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/docs/PROFILE_SAVE_RESTORE.md +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/helpers.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/imaging.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/macrotransfer/__init__.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/macrotransfer/_helpers.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/macrotransfer/_io.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/macrotransfer/downstream_keyer.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/macrotransfer/fairlight.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/macrotransfer/flow.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/macrotransfer/hyperdeck.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/macrotransfer/media.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/macrotransfer/switching.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/macrotransfer/upstream_keyer.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/mediaconvertmodule.c +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/__init__.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/_dsl.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/audio_legacy.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/camera_control.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/color_generator.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/downstream_keyer.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/fade_to_black.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/fairlight.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/file_transfer.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/hyperdeck.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/input_video.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/lock.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/macros.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/manual.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/media.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/meters.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/multiviewer.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/recording.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/streaming.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/supersource.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/switching.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/system_info.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/tally.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/transition.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/messages/upstream_keyer.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/pool.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/probe.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/profile/__init__.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/profile/_common.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/profile/_enums.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/profile/_xml.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/profile/apply.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/profile/options.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/profile/save.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/py.typed +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/ready.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/state.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/atemwire/testing.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/bmdwire.egg-info/SOURCES.txt +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/bmdwire.egg-info/dependency_links.txt +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/bmdwire.egg-info/requires.txt +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/bmdwire.egg-info/top_level.txt +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/hyperdeckwire/LICENSE +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/hyperdeckwire/__init__.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/hyperdeckwire/client.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/hyperdeckwire/setup.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/hyperdeckwire/upload.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/setup.cfg +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/setup.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/ultimattewire/LICENSE +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/ultimattewire/__init__.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/ultimattewire/_preamble.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/ultimattewire/_protocol.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/ultimattewire/network.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/ultimattewire/profile/__init__.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/ultimattewire/profile/_ranges.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/ultimattewire/profile/archive.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/ultimattewire/profile/restore.py +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/videohubwire/LICENSE +0 -0
- {bmdwire-1.5.0 → bmdwire-1.5.1}/videohubwire/__init__.py +0 -0
- {bmdwire-1.5.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.5.
|
|
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.5.
|
|
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.5.
|
|
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
|
|
|
@@ -26,6 +26,15 @@ one) did not hold on this hardware.
|
|
|
26
26
|
on `LKOB`, not only on the release broadcast.
|
|
27
27
|
* A `LOCK` (full store) or `PLCK` sent by a session that **already holds**
|
|
28
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").
|
|
29
38
|
* A `LOCK state=0` from a session that does not hold the store does
|
|
30
39
|
nothing visible (this was tested only while a foreign client held it).
|
|
31
40
|
|
|
@@ -180,5 +189,47 @@ holder, which the switcher ignores (see "Requests and grants"). Waiting
|
|
|
180
189
|
for the echo hid it, at a frame per transfer at best and up to 6.6 s
|
|
181
190
|
measured, during a 20-still restore by bmdctl, which mirrors this code.
|
|
182
191
|
Since `bd6ae77` atemwire holds the lock from `LKOB` to its own release, as
|
|
183
|
-
ASC does
|
|
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.
|
|
184
235
|
|
|
@@ -500,7 +500,9 @@ in the transfer flow. At field level, the `CKDV`/`CTDv` fields are all echoed, b
|
|
|
500
500
|
repeated within 1.0 s (`LOCK_REREQUEST_SECONDS`). There is no timer-driven retry: a dropped
|
|
501
501
|
request is re-sent only on the next event. Since `bd6ae77` the release and the next request go
|
|
502
502
|
in one packet, as ATEM Software Control sends them, and a release broadcast while the store is
|
|
503
|
-
held changes nothing; after an FTDE 6 the release goes alone (§5.4 item 21).
|
|
503
|
+
held changes nothing; after an FTDE 6 the release goes alone (§5.4 item 21). Since `5827626`
|
|
504
|
+
the release goes alone and the next request waits for the store to be announced free and a
|
|
505
|
+
frame more, never while another client holds it (§5.4 item 26).
|
|
504
506
|
- **`FTDE` status handling:**
|
|
505
507
|
- 1 (try again): re-send the request with the same tid.
|
|
506
508
|
- 5 (no lock): re-request the lock.
|
|
@@ -915,7 +917,8 @@ Items 1, 2 and 4 were fixed after 1.2.0, in the commits named in each; they stil
|
|
|
915
917
|
1.2.0 and earlier, including the 1.1.0 that WebATEM pins. The fly-key bullets of items 9 and
|
|
916
918
|
10, the split-mono bullet of item 8, the float and `sourceId` bullets of item 9, items 15, 16,
|
|
917
919
|
18, 19 and 20, and part of item 17 were fixed after 1.3.0 and still apply to it. Items 21 to 25
|
|
918
|
-
were fixed after 1.4.0 and still apply to it.
|
|
920
|
+
were fixed after 1.4.0 and still apply to it. Item 26 was fixed after 1.5.0 and still applies to
|
|
921
|
+
it.
|
|
919
922
|
|
|
920
923
|
1. **Profile video mode does not round-trip at fractional rates** (1.2.0). Save writes
|
|
921
924
|
`get_label()` (`1080p59.94`); apply only knows `1080p5994`-style names, so it skips the mode as
|
|
@@ -1076,6 +1079,16 @@ were fixed after 1.4.0 and still apply to it.
|
|
|
1076
1079
|
the switcher sees no gap, and an upload loses a chunk without a word. *Fixed in `bd6ae77`:*
|
|
1077
1080
|
`ENOBUFS` and `EAGAIN` are retried for up to 20 ms, and a reliable packet still refused keeps
|
|
1078
1081
|
its number and its copy for retransmission.
|
|
1082
|
+
26. **Another client's turn is cut short** (1.5.0). The release and the next lock request go in
|
|
1083
|
+
one packet (item 21). Another client's request waiting for the store is handed it at the
|
|
1084
|
+
release, unannounced; the next request is granted beside it (a `PLCK` beside a `LOCK`) and
|
|
1085
|
+
its transfer aborts theirs with `FTDE 6`. ATEM Software Control, which tries each still once
|
|
1086
|
+
when it connects, is left without its thumbnails (`docs/MEDIA_LOCK.md`, "Sharing the store
|
|
1087
|
+
with ATEM Software Control"). *Fixed in `5827626`:* the release goes alone; the next request
|
|
1088
|
+
waits for the store to be announced free (`LKST`) and `LOCK_COURTESY` (60 ms) more, never
|
|
1089
|
+
while another client is seen holding the store, and with no announcement waits while anyone
|
|
1090
|
+
is seen at work, asking after `LOCK_QUIET` (10 s) of quiet. `UdpProtocol.wake_at` wakes the
|
|
1091
|
+
protocol for it.
|
|
1079
1092
|
|
|
1080
1093
|
## Counts
|
|
1081
1094
|
|
|
@@ -33,10 +33,34 @@ def _is_tcp_transport(transport) -> bool:
|
|
|
33
33
|
|
|
34
34
|
# A lock request (LOCK / PLCK) with no LKOB back after this long is sent
|
|
35
35
|
# again on the next trigger. Measured: a granted request answers in ~1 ms;
|
|
36
|
-
# a queued PLCK
|
|
37
|
-
#
|
|
36
|
+
# a queued one (PLCK, and a full LOCK too: ATEM Software Control's, in a
|
|
37
|
+
# capture of 2026-10-15) the moment the holder releases.
|
|
38
38
|
LOCK_REREQUEST_SECONDS = 1.0
|
|
39
39
|
|
|
40
|
+
# Another client's turn. A request of another client's that waited for
|
|
41
|
+
# the store is granted the moment we release it, and the switcher grants a
|
|
42
|
+
# request of ours sent soon after too (two sessions asking within ~150 ms
|
|
43
|
+
# can both get LKOB; MEDIA_LOCK.md). Our transfer then aborts theirs with
|
|
44
|
+
# FTDE 6, and ATEM Software Control, which tries each still once when it
|
|
45
|
+
# connects and again only when the media pool changes, is left without
|
|
46
|
+
# its thumbnails (a capture, 2026-10-15: 20 stills, 20 FTDE 6, while a
|
|
47
|
+
# client of this package downloaded back to back; 1.5.0 sent the release
|
|
48
|
+
# and the next request in one packet). A request handed the store at our
|
|
49
|
+
# release isn't announced at all (no LKST either way), and a PLCK of ours
|
|
50
|
+
# is granted beside another client's LOCK even half a second into its
|
|
51
|
+
# turn, its transfer aborted (a second capture, the same day). So after a
|
|
52
|
+
# per-frame release with more work queued for the store, the next request
|
|
53
|
+
# waits for the store to be announced free (LKST), then LOCK_COURTESY more
|
|
54
|
+
# (a frame, for anyone else to take it); a store another client is seen
|
|
55
|
+
# holding (its LKST) is not asked for until announced free. No
|
|
56
|
+
# announcement means someone was handed the store: we wait, as long as
|
|
57
|
+
# anyone is seen at work (another session's FTDC, any LKST), and ask only
|
|
58
|
+
# after LOCK_QUIET with no sign of anyone. bmdwire 1.4.0 waited the same
|
|
59
|
+
# way for the release's echo, by accident: its long waits in a restore
|
|
60
|
+
# were WebATEM refreshing each new still's thumbnail.
|
|
61
|
+
LOCK_COURTESY = 0.06
|
|
62
|
+
LOCK_QUIET = 10.0
|
|
63
|
+
|
|
40
64
|
# A state dump that ends (InCm) without a video mode has lost its first
|
|
41
65
|
# packet for good: the transport takes the first reliable packet that
|
|
42
66
|
# arrives as the session's baseline, so one lost before it is never resent
|
|
@@ -174,6 +198,13 @@ class AtemProtocol:
|
|
|
174
198
|
# Cleared by the LKOB, by the store's LKST release broadcast (the
|
|
175
199
|
# pounce re-requests anyway), and by every lane reset.
|
|
176
200
|
self._lock_requested = {}
|
|
201
|
+
# store -> [since, announced]: for a store we released and have more
|
|
202
|
+
# work for (LOCK_COURTESY), when we released it or last saw anyone
|
|
203
|
+
# at work, and when it was announced free (None until it is).
|
|
204
|
+
self._yielding = {}
|
|
205
|
+
# Stores another client holds, from its LKST until its release's:
|
|
206
|
+
# we don't ask for them meanwhile (LOCK_COURTESY).
|
|
207
|
+
self._foreign_held = set()
|
|
177
208
|
self.mode = None
|
|
178
209
|
self.transfer_queue = {}
|
|
179
210
|
self.transfer_id = 42
|
|
@@ -221,7 +252,9 @@ class AtemProtocol:
|
|
|
221
252
|
if isinstance(packet, Wakeup):
|
|
222
253
|
# Caller-side wake-up to drain a pending outbound command — the
|
|
223
254
|
# worker thread (ATEMConnection._run) will pick up the queue
|
|
224
|
-
# right after this returns
|
|
255
|
+
# right after this returns — or the transport's, for the lock
|
|
256
|
+
# yield (wake_at): a store left free may be asked for now.
|
|
257
|
+
self._service_yields()
|
|
225
258
|
return
|
|
226
259
|
self.connected = True
|
|
227
260
|
if isinstance(packet, TransferQueueFlushed):
|
|
@@ -336,8 +369,13 @@ class AtemProtocol:
|
|
|
336
369
|
# and silent, so mixerstate['lock-state'] only ever showed the
|
|
337
370
|
# last release (bmdwire#15). The packet doesn't name the holder:
|
|
338
371
|
# held by someone else is state=True while self.locks.get(store)
|
|
339
|
-
# is falsy.
|
|
340
|
-
|
|
372
|
+
# is falsy. Held by someone else, we don't ask for it until they
|
|
373
|
+
# release it (LOCK_COURTESY), and a store we were leaving free
|
|
374
|
+
# is theirs now.
|
|
375
|
+
if not self.locks.get(contents.store):
|
|
376
|
+
self._foreign_held.add(contents.store)
|
|
377
|
+
if contents.store in self._yielding:
|
|
378
|
+
self._yielding[contents.store] = [time.monotonic(), None]
|
|
341
379
|
elif key == 'lock-state' and self.locks.get(contents.store):
|
|
342
380
|
# A release broadcast while we hold the store is the late echo
|
|
343
381
|
# of our own previous release: the switcher announces lock
|
|
@@ -349,6 +387,22 @@ class AtemProtocol:
|
|
|
349
387
|
f'our own earlier release, announced late')
|
|
350
388
|
elif key == 'lock-state':
|
|
351
389
|
self.log.debug(contents)
|
|
390
|
+
self._foreign_held.discard(contents.store)
|
|
391
|
+
# A release announced, ours or another client's: the store is
|
|
392
|
+
# seen free, and a request waits LOCK_COURTESY more for anyone
|
|
393
|
+
# else to take it. Another client releasing and taking the store
|
|
394
|
+
# again for its next transfer (ASC does, in one packet) is
|
|
395
|
+
# announced as a release then a lock, often in one packet: a
|
|
396
|
+
# request sent on the release would be granted alongside theirs
|
|
397
|
+
# and abort their transfer (TEST ATEM 2, 2026-10-15).
|
|
398
|
+
y = self._yielding.get(contents.store)
|
|
399
|
+
now = time.monotonic()
|
|
400
|
+
if y is None and self.transfer_queue.get(contents.store) and self._can_wake():
|
|
401
|
+
self._yielding[contents.store] = [now, now]
|
|
402
|
+
self._wake_at(now + LOCK_COURTESY)
|
|
403
|
+
elif y is not None and y[1] is None:
|
|
404
|
+
y[1] = now
|
|
405
|
+
self._wake_at(now + LOCK_COURTESY)
|
|
352
406
|
# The lock is free: whatever request we had outstanding is
|
|
353
407
|
# either about to be granted (a queued PLCK — the switcher
|
|
354
408
|
# grants it directly, sometimes without even broadcasting the
|
|
@@ -491,8 +545,10 @@ class AtemProtocol:
|
|
|
491
545
|
# this is another session's transfer (watcher, uploader, a
|
|
492
546
|
# sibling operator) finishing. Expected on a shared setup.
|
|
493
547
|
self.log.debug("FTDC for another session's transfer — ignoring")
|
|
548
|
+
self._someone_at_work()
|
|
494
549
|
return
|
|
495
550
|
if contents.transfer != self.transfer.tid:
|
|
551
|
+
self._someone_at_work()
|
|
496
552
|
return
|
|
497
553
|
# Remove current item from the transfer queue
|
|
498
554
|
store = self.transfer.store
|
|
@@ -649,6 +705,8 @@ class AtemProtocol:
|
|
|
649
705
|
self.transfer_buffer_bytes = 0
|
|
650
706
|
self.locks = {}
|
|
651
707
|
self._lock_requested.clear()
|
|
708
|
+
self._yielding.clear()
|
|
709
|
+
self._foreign_held.clear()
|
|
652
710
|
|
|
653
711
|
def release_locks_now(self):
|
|
654
712
|
"""Give back every store lock we hold, SYNCHRONOUSLY, for teardown.
|
|
@@ -686,6 +744,7 @@ class AtemProtocol:
|
|
|
686
744
|
exc_info=True)
|
|
687
745
|
self.locks[store] = False
|
|
688
746
|
self._lock_requested.clear()
|
|
747
|
+
self._yielding.clear()
|
|
689
748
|
|
|
690
749
|
def abort_transfers(self):
|
|
691
750
|
"""Best-effort reset of the transfer state machine, for callers that
|
|
@@ -700,9 +759,11 @@ class AtemProtocol:
|
|
|
700
759
|
self.transfer_requested = False
|
|
701
760
|
self.transfer_buffer = []
|
|
702
761
|
self.transfer_buffer_bytes = 0
|
|
703
|
-
# A lock request in flight
|
|
704
|
-
# was for is gone, and the next caller's
|
|
762
|
+
# A lock request in flight, or a store left free, must not outlive
|
|
763
|
+
# the abort: the queue it was for is gone, and the next caller's
|
|
764
|
+
# request must not wait on it.
|
|
705
765
|
self._lock_requested.clear()
|
|
766
|
+
self._yielding.clear()
|
|
706
767
|
for lock in list(self.locks):
|
|
707
768
|
if self.locks[lock]:
|
|
708
769
|
try:
|
|
@@ -872,13 +933,11 @@ class AtemProtocol:
|
|
|
872
933
|
LOCKED and its preview fetches starve (observed live 2026-07-07 on a
|
|
873
934
|
freshly rebooted switcher). ASC holds per frame; now so do we.
|
|
874
935
|
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
request in
|
|
880
|
-
echo, which arrives a frame later. The request is the switcher's
|
|
881
|
-
to queue if another client asked first, as a PLCK is queued.
|
|
936
|
+
With more work queued for the store, its next lock request waits
|
|
937
|
+
for the store's next LKST (see LOCK_COURTESY): another client that
|
|
938
|
+
was waiting gets the store at our release, and a request of ours
|
|
939
|
+
soon after would abort its transfer. 1.5.0 sent the release and the
|
|
940
|
+
next request in one packet, as ASC sends them; that shut ASC out.
|
|
882
941
|
``request_next=False`` sends the release alone and leaves the next
|
|
883
942
|
request to the store's release broadcast (the pounce): after an
|
|
884
943
|
FTDE 6 another session is queued for the store, and asking again
|
|
@@ -892,35 +951,49 @@ class AtemProtocol:
|
|
|
892
951
|
if store != 0xffff and self.locks.get(store):
|
|
893
952
|
self.log.info(f'Releasing lock {store} (per-frame)')
|
|
894
953
|
self.locks[store] = False
|
|
895
|
-
|
|
954
|
+
try:
|
|
955
|
+
self.send_commands([LockCommand(store, False)])
|
|
956
|
+
except Exception:
|
|
957
|
+
self.log.exception('per-frame unlock send failed')
|
|
896
958
|
if not request_next:
|
|
897
|
-
try:
|
|
898
|
-
self.send_commands([release])
|
|
899
|
-
except Exception:
|
|
900
|
-
self.log.exception('per-frame unlock send failed')
|
|
901
959
|
return # the store's release broadcast continues the queue
|
|
902
|
-
self.
|
|
903
|
-
|
|
960
|
+
if self.transfer_queue.get(store) and self._can_wake():
|
|
961
|
+
# The trigger below defers the request and asks to be woken.
|
|
962
|
+
self._yielding[store] = [time.monotonic(), None]
|
|
904
963
|
self._transfer_trigger(store)
|
|
905
964
|
|
|
906
|
-
def
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
965
|
+
def _can_wake(self):
|
|
966
|
+
return hasattr(self.transport, 'wake_at')
|
|
967
|
+
|
|
968
|
+
def _wake_at(self, when):
|
|
969
|
+
wake = getattr(self.transport, 'wake_at', None)
|
|
970
|
+
if wake is not None:
|
|
971
|
+
wake(when)
|
|
972
|
+
|
|
973
|
+
def _yield_due(self, store):
|
|
974
|
+
"""When a store we are leaving free may be asked for again."""
|
|
975
|
+
since, announced = self._yielding[store]
|
|
976
|
+
if announced is not None:
|
|
977
|
+
return announced + LOCK_COURTESY
|
|
978
|
+
return since + LOCK_QUIET
|
|
979
|
+
|
|
980
|
+
def _someone_at_work(self):
|
|
981
|
+
"""Another session's transfer ended: a store we wait for, not yet
|
|
982
|
+
announced free, is busy, and LOCK_QUIET starts again."""
|
|
983
|
+
now = time.monotonic()
|
|
984
|
+
for y in self._yielding.values():
|
|
985
|
+
if y[1] is None:
|
|
986
|
+
y[0] = now
|
|
987
|
+
|
|
988
|
+
def _service_yields(self):
|
|
989
|
+
"""Ask for each store we left free whose yield is over."""
|
|
990
|
+
for store in list(self._yielding):
|
|
991
|
+
if not self.transfer_queue.get(store):
|
|
992
|
+
del self._yielding[store]
|
|
993
|
+
elif not self.transfer_requested and not self.locks.get(store):
|
|
994
|
+
self._transfer_trigger(store)
|
|
995
|
+
|
|
996
|
+
def _transfer_trigger(self, store, retry=False):
|
|
924
997
|
next = None
|
|
925
998
|
|
|
926
999
|
self.log.info(f'transfer trigger for store {store} (retry={retry})')
|
|
@@ -945,11 +1018,24 @@ class AtemProtocol:
|
|
|
945
1018
|
if self.locks[lock]:
|
|
946
1019
|
self.log.info('Releasing lock {}'.format(lock))
|
|
947
1020
|
cmd = LockCommand(lock, False)
|
|
948
|
-
|
|
1021
|
+
self.send_commands([cmd])
|
|
949
1022
|
return
|
|
950
1023
|
|
|
951
1024
|
# Request a lock if needed
|
|
952
1025
|
if next.store != 0xffff and (next.store not in self.locks or not self.locks[next.store]):
|
|
1026
|
+
if next.store in self._foreign_held:
|
|
1027
|
+
# Another client's turn: its release's LKST asks for us.
|
|
1028
|
+
self.log.debug(f'Store {next.store} is another client\'s: '
|
|
1029
|
+
f'asking once they release it')
|
|
1030
|
+
return
|
|
1031
|
+
if next.store in self._yielding:
|
|
1032
|
+
due = self._yield_due(next.store)
|
|
1033
|
+
if time.monotonic() < due:
|
|
1034
|
+
self.log.debug(f'Leaving store {next.store} free a moment '
|
|
1035
|
+
f'for other clients')
|
|
1036
|
+
self._wake_at(due)
|
|
1037
|
+
return
|
|
1038
|
+
del self._yielding[next.store]
|
|
953
1039
|
asked = self._lock_requested.get(next.store)
|
|
954
1040
|
if asked is not None and time.monotonic() - asked < LOCK_REREQUEST_SECONDS:
|
|
955
1041
|
self.log.debug(
|
|
@@ -966,7 +1052,7 @@ class AtemProtocol:
|
|
|
966
1052
|
else:
|
|
967
1053
|
cmd = PartialLockCommand(next.store, next.slot)
|
|
968
1054
|
self._lock_requested[next.store] = time.monotonic()
|
|
969
|
-
|
|
1055
|
+
self.send_commands([cmd])
|
|
970
1056
|
return
|
|
971
1057
|
|
|
972
1058
|
# A transfer request is already running, don't start a new one
|
|
@@ -985,7 +1071,7 @@ class AtemProtocol:
|
|
|
985
1071
|
self.transfer_queue[next.store] = queue[1:]
|
|
986
1072
|
self.log.info(
|
|
987
1073
|
'Clearing {}:{} under store lock'.format(next.store, next.slot))
|
|
988
|
-
|
|
1074
|
+
self.send_commands([ClearStillCommand(slot=next.slot)])
|
|
989
1075
|
self._raise('clear-dispatched', next.store, next.slot)
|
|
990
1076
|
self._release_then_continue(next.store)
|
|
991
1077
|
return
|
|
@@ -1039,4 +1125,4 @@ class AtemProtocol:
|
|
|
1039
1125
|
description=self.transfer.description,
|
|
1040
1126
|
))
|
|
1041
1127
|
self.transfer.metadata_sent = True
|
|
1042
|
-
|
|
1128
|
+
self.send_commands(outbound)
|
|
@@ -376,6 +376,10 @@ class UdpProtocol(BaseProtocol):
|
|
|
376
376
|
# when what it hasn't acknowledged is resent (RESEND_AFTER).
|
|
377
377
|
self._acked_through = None
|
|
378
378
|
self._resend_due = None
|
|
379
|
+
# When receive_packet next returns a Wakeup for the protocol's own
|
|
380
|
+
# timed work (wake_at); set from the protocol's thread.
|
|
381
|
+
self._wake_due = None
|
|
382
|
+
self._wake_lock = threading.Lock()
|
|
379
383
|
|
|
380
384
|
self.thread_queue = SocketQueue()
|
|
381
385
|
self.thread_recv_queue = Queue()
|
|
@@ -478,8 +482,12 @@ class UdpProtocol(BaseProtocol):
|
|
|
478
482
|
continue
|
|
479
483
|
if self._resend_due is not None:
|
|
480
484
|
remaining = max(0.0, min(remaining, self._resend_due - time.monotonic()))
|
|
485
|
+
wake = self._wake_due
|
|
486
|
+
if wake is not None:
|
|
487
|
+
remaining = max(0.0, min(remaining, wake - time.monotonic()))
|
|
481
488
|
readable, _, _ = select.select([self.sock, self.thread_queue], [], [], remaining)
|
|
482
489
|
self._resend_unacked()
|
|
490
|
+
self._fire_wake()
|
|
483
491
|
for queue in readable:
|
|
484
492
|
if queue is self.sock:
|
|
485
493
|
packet = self._receive_packet_low()
|
|
@@ -840,6 +848,22 @@ class UdpProtocol(BaseProtocol):
|
|
|
840
848
|
raise
|
|
841
849
|
time.sleep(0.001)
|
|
842
850
|
|
|
851
|
+
def wake_at(self, when):
|
|
852
|
+
"""Have receive_packet return a Wakeup at ``when`` (monotonic): the
|
|
853
|
+
protocol's timed work, which its packets alone can't drive (the
|
|
854
|
+
switcher's pings never reach it). The earliest of several wins."""
|
|
855
|
+
with self._wake_lock:
|
|
856
|
+
if self._wake_due is None or when < self._wake_due:
|
|
857
|
+
self._wake_due = when
|
|
858
|
+
self.thread_queue.put(None) # select() takes the new timeout
|
|
859
|
+
|
|
860
|
+
def _fire_wake(self):
|
|
861
|
+
with self._wake_lock:
|
|
862
|
+
if self._wake_due is None or time.monotonic() < self._wake_due:
|
|
863
|
+
return
|
|
864
|
+
self._wake_due = None
|
|
865
|
+
self.thread_recv_queue.put(Wakeup())
|
|
866
|
+
|
|
843
867
|
def _on_ack(self, number):
|
|
844
868
|
"""The switcher has our reliable packets through ``number``: the
|
|
845
869
|
resend timer starts again while some remain unacknowledged."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bmdwire
|
|
3
|
-
Version: 1.5.
|
|
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.5.
|
|
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
|
|
|
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
|
|
|
13
13
|
|
|
14
14
|
[project]
|
|
15
15
|
name = "bmdwire"
|
|
16
|
-
version = "1.5.
|
|
16
|
+
version = "1.5.1"
|
|
17
17
|
description = "Python libraries for Blackmagic Design broadcast devices: ATEM switchers, HyperDeck recorders, Ultimatte keyers and Videohub routers"
|
|
18
18
|
readme = "README.md"
|
|
19
19
|
# atemwire is a fork of pyatem and stays LGPL-3.0-only; the other three were
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|