Repository navigation
Releases: lucas-romanenko/bmdwire
Release list
1.5.1: leave the store to another client's turn
Fixes 1.5.0's lock handover cutting another client's turn short (#38). ATEM Software Control connecting while this package transferred stills lost its thumbnails: the switcher hands the store to a waiting request at a release without announcing it, and 1.5.0's next request was granted beside it and aborted it (FTDE 6). Now the next request waits for the store to be announced free and a frame more, never while another client holds it, and waits while anyone is seen at work. On a 1 M/E Constellation HD, ASC connecting during a back-to-back download loaded all 20 thumbnails (0 with 1.5.0). Uploads alone cost about 0.1 s more per still: 20 take 14.2 s.
New transport API UdpProtocol.wake_at, which the fix uses; nothing removed. MEDIA_LOCK.md: "Sharing the store with ATEM Software Control"; a LOCK sent while another session holds the store is queued, not dropped.
1.5.0: uploads as ATEM Software Control sends them
Uploads and the media lock as ATEM Software Control does them (#36, from a capture of its restore): the release and the next lock request in one packet, FTFD with the first budget, each budget sent whole. 20 photographic 1080p stills upload in 12.7 s on a 1 M/E Constellation HD (ASC about 12 s; 1.4.0 took 23 s a still).
More reliable too: what the switcher hasn't acknowledged in 250 ms is resent, and a send the kernel refuses is retried and keeps its sequence number instead of silently losing an upload chunk. A late release broadcast no longer costs the new lock grant. aggressive_drain is still accepted and changes nothing.
Also: MEDIA_LOCK.md on what ATEM Software Control does with the media pool (#35).
1.4.0: transport recovery, HyperDeck answers per command, macro and profile fixes
Fixes for issues #8 to #21, most of them found while porting bmdwire to Go (bmdctl). atemwire and hyperdeckwire change; ultimattewire and videohubwire are unchanged. Nothing is removed. Four of the issues stay open, for a check on hardware or a packet trace; they are listed at the end.
Behaviour changes
- atemwire: the silence timeout now fires for a client that keeps sending (#11). The transport re-handshakes after 5 s without hearing from the switcher. Until now, the client's own outgoing packets, and datagrams it then dropped, restarted those 5 s. So a client that sent at least once every 5 s to a switcher that had gone away never timed out: no
disconnectedevent,is_readystayed True over stale state, and commands went nowhere. Now only a packet accepted from the switcher, or a new handshake, restarts the clock. A switcher that stops answering a control surface, a script or a profile apply is noticed within 5 to 6 s, so expectdisconnected(in WebATEM, "connection lost") where there was none before. - hyperdeckwire: a timeout ends the connection (#18). When an answer doesn't come within
read_timeout,request()closes the connection before raisingTimeoutError, and so does every method, since they all go through it. Later calls raiseOSError('not connected')until you callconnect()again. Before, the connection stayed open: the late answer was returned to the next command, and every answer after it was one behind. Code that catches the timeout and carries on with the sameHyperdeckmust now reconnect.
Changes, by issue
atemwire: connection and transport
#11: The silence timeout counts only the switcher (#25). See Behaviour changes. last_rx_monotonic keeps its meaning: the time of the last packet from the switcher.
#12: A re-handshake ignores the session it replaces (#26). After a silence, the switcher goes on resending the old session for a few seconds. Those packets restarted the silence window. The first of them could also become the new session's sequence baseline, so the new state dump was dropped as duplicates and connected didn't come until the next timeout. And one could be adopted as the session id. Now packets carrying the replaced session's id are dropped before any receive state sees them, and so is anything but the handshake reply while the handshake is out.
#13: A state dump without a video mode re-handshakes at once (#27). If the first packet of a state dump is lost, it is lost for good. When that packet carried the video mode, ATEMConnection.connect waited out its 15 s and failed. Now the protocol raises disconnected and re-handshakes for a fresh dump as soon as a dump ends (InCm) without a video mode, up to 3 times in a row, then carries on as before. This is the fail-fast half of the issue; the real fix is still open (below).
#14: *XFC on a UDP session is ignored (#28). This OpenSwitcher TCP-relay event is never sent by a switcher. If one arrived anyway, its handler either raised KeyError, losing the rest of the packet, or popped a real transfer in flight. On UDP it is now dropped before parsing.
#15: A lock taken is stored and raised (#29). LKST was stored only for a release, so mixerstate['lock-state'] never showed the media store held. Both states are now stored and raise change:lock-state. The transfer engine still reacts to releases only. Another client holds the store when state is True and protocol.locks.get(store) is falsy. The key is still bare: the latest LKST wins, whatever its store.
#8: Transfers are refused while the session is re-established (#31). download_still, download_macro and clear_still now check is_ready, and raise ConnectionDeadError at once during a transport re-handshake. Before, they queued into the session being replaced and waited out 30 s, 10 s or 5 s, and a still's timeout blamed "another client" for the lock. profile.save.download_media_pool_images refuses up front, and stops when the session goes, returning the slots done so far. send() still checks is_connected.
atemwire: macros and profiles
#16: A Fairlight strip macro op with no sourceId encodes the stereo strip (#30). The default was 0xFFFFFFFFFFFF0000, an address no switcher uses. It is now 0xFFFFFFFFFFFF0100, which ATEM Software Control (ASC) writes as 18446744073709486336.
#17: Macro floats decode to six significant digits, as ASC writes them (#24). A silenced fader now decodes to -327.68, not -327.679993. Across every macro op in twelve ASC exports, all 399 distinct values now match ASC's strings; 349 did before.
- Every float the decoder writes changes form.
- Above 10, six significant digits are coarser than the switcher's fixed point, so a value decoded and encoded again can move by a few 1/65536 steps, as it does when you save and restore through ASC.
#9: Profiles save and apply dual-mono Fairlight inputs per half (#23). Apply used to send both halves of a split-mono input to the stereo strip, the second overwriting the first. Save wrote them as one stereo input.
- Save now writes
DualMono, with ASC's id for each half (−256 left, −255 right), and apply sends each half to its own channel. - The XLR input's strip (id −256) is now addressed as channel 0, as ASC's export names it, rather than −1.
- Older saves of a dual-mono input apply as before.
- Apply doesn't switch an input to Dual Mono; it must already be in Dual Mono.
#10: Fly-key locations are one enum, with MiddleCentre at 5 (#22). A capture of ASC settled it: RFlK byte 5, KeFS byte 5 and macro op 0x0056 share one table, atemwire.helpers.FLYKEY_LOCATION_NAMES (0 CentreOfKey, then 1 to 9 for the 3×3 grid, row by row). The macro codec now encodes MiddleCentre, and decodes byte 5 to it rather than '5'. run_flying_key_infinite_direction takes ASC's location names as well as the number.
hyperdeckwire
#18: A timeout ends the connection (#32). See Behaviour changes.
#19: A response nobody asked for is dropped (#32). Responses that arrive before a command is sent are no longer returned as its answer; they are logged and dropped. One that arrives after the command is sent still can't be told from the real answer, since the protocol has no request ids.
#20: A command with a line break in it is refused (#32). A clip name or record prefix containing CR or LF used to be sent as two commands: clips_add('a.mov\nclips clear') cleared the timeline. Every method, and request(), now raises ValueError before anything is sent.
#21: connect() fails when the deck refuses the connection (#32). A deck at its client limit answers 120 connection failed and closes. connect() used to succeed anyway, with an empty model. Any 1xx greeting now raises HyperdeckError, and any greeting other than 500 raises OSError.
atemwire/docs/SURFACE.md §5.4 records each atemwire fix with its commit, and hyperdeckwire/docs/API.md has a new section, "One answer per command".
Still open
- #8, #11, #12: fixed here; each closes after a check on a bench switcher (a cable pull).
- #13: the fail-fast guard is in this release. The real fix is to know a new session's first sequence number, so that a lost first packet is resent like any other. It needs a packet trace of a few connects first.
Wheels for Linux x86_64 and aarch64, macOS arm64 and x86_64, Windows x86 and x86_64 on CPython 3.10 to 3.14, plus the sdist.
1.3.0: profile video mode, reconnect readiness, macro slot 0
Three atemwire fixes and one new property. Two known problems stay open, listed at the end with the capture each needs. Nothing is removed; hyperdeckwire, ultimattewire and videohubwire are unchanged.
Behaviour changes
ATEM.connectedandprobe()report not-connected during a reconnect gap. After about 5 s without packets the transport re-handshakes on its own, and the switcher's state is empty until the new state dump completes. During that gap,ATEM.connectedis now False, andprobe()returnsconnection_status=Falsewith the other fields None; both used to report a connected switcher with no state.ATEMConnection.is_connectedkeeps its meaning (worker alive, handshake done):send()and the pool's dead-worker eviction rely on it.- Profiles saved by 1.3.0 use the canonical video-mode names. These are the spellings ATEM Software Control uses:
1080p5994rather than1080p59.94, andNTSC_widescreen/PAL_widescreenfor 16:9 SD.
Fixes
Profile video mode round-trips at every rate. Save used to write the display label (1080p59.94) while apply only knew 1080p5994-style names, so every fractional-rate mode was skipped on apply as "unrecognized mode". Both SD aspects were also saved as one string. Save, apply and the macro codec now share one table, atemwire.messages.input_video.VIDEO_MODE_XML_NAMES, covering all 30 modes. Apply compares mode numbers, so a profile at the current mode no longer re-sends the mode change and waits 3 s.
Files saved by earlier versions still apply. The decimal spellings are read, except the two SD labels that never recorded 4:3 or 16:9:
525i59.94leaves the video mode unchanged and logs a warning.625i50, which is also ASC's own name for PAL 4:3, applies as 4:3 with a warning.
ATEMConnection.is_ready is new: True while connected and the switcher's state is current, False from the protocol's disconnected event until the next connected. It is what ATEM.connected and probe() now read.
Macro slot 0 is latched as the last-run macro. The play-status handler turned index 0 into "idle", so lastRunMacro stayed empty whenever macro 1 ran.
Also new: atemwire/docs/SURFACE.md, an inventory of the library's whole surface (commands, state, transfers, connection lifecycle, gaps), read from the code.
Not fixed
Split-mono Fairlight strips in profile apply. Apply sends both halves of a split-mono strip with channel −1, so the second overwrites the first. Save writes the same AudioSource id for both halves and records no subchannel, and how ATEM Software Control marks the second half is unknown. Capture needed: an ATEM Software Control "Save Switcher State" XML from a switcher with one input set to dual-mono (split).
Fly-key infinite-run direction, centre position. run_flying_key_infinite_direction documents 5 as the centre; the macro codec's table has 0 = CentreOfKey and no 5. They agree on every other position. ASC treats CentreOfKey and MiddleCentre as different locations. Both sites carry a TODO; behaviour is unchanged. Capture needed: a Wireshark capture of ATEM Software Control's RFlK packets (byte 5) as it runs a fly-enabled key to each of its ten locations (CentreOfKey and the 3×3 grid including MiddleCentre). Ideally also record those ten runs into a macro and download it, to compare the location byte of op 0x0056.
Wheels for Linux x86_64 and aarch64, macOS arm64 and x86_64, Windows x86 and x86_64 on CPython 3.10 to 3.14, plus the sdist.
1.2.0: hyperdeckwire.setup, the deck's configuration API
hyperdeckwire.setup: a client for the HyperDeck's configuration API.
Everything HyperDeck Setup shows for a networked deck lives on a REST API on the deck's HTTP port (/admin/api/v1/). None of it exists in the 9993 Ethernet Protocol, whose configuration verb covers codec, inputs and timecode only. HyperdeckSetup covers:
- name and identify;
- the network interface (address, netmask, gateway, DNS, DHCP);
- the FTP and Web Media Manager switches (Disabled / Enabled / SecureOnly);
- the HyperDeck Ethernet Protocol switch (TCP 9993 on or off);
- certificate, users, date and time, NTP, timezone, reboot.
Every setter reads the setting back and raises HyperdeckSetupError when the deck holds something else. A network change is read back until the interface is live on the new values, at the new address if the address changed.
"Configure via USB and Ethernet" has no setter. While it is off, the deck refuses every PUT with 401, including the one that would turn it on. So it can only be enabled over USB, and turning it off over Ethernet would cut off the channel doing it. remote_admin() reads it so a 401 can be explained before it happens.
Measured on two HyperDeck Studio HD Mini units on software 9.0.2: every read endpoint, and the write paths as same-value round trips (name, network, network access, Ethernet protocol, NTP, timezone, identify).
Wheels for Linux x86_64 and aarch64, macOS arm64 and x86_64, Windows x86 and x86_64 on CPython 3.10 to 3.14, plus the sdist.
1.1.0: one distribution
pip install bmdwire now installs all four libraries at one version: atemwire, hyperdeckwire, ultimattewire and videohubwire. The import names are unchanged; only what you install and pin changed. Code is that of atemwire 1.0.3, hyperdeckwire 1.0.1, ultimattewire 1.0.1 and videohubwire 1.0.1.
The four old PyPI projects were deleted the same day; bmdwire is the only package. If an environment still has any of them installed from before, uninstall them before installing bmdwire. A fresh virtualenv, pipx or a container build is unaffected.
Wheels for Linux x86_64 and aarch64, macOS arm64 and x86_64, Windows x86_64 on CPython 3.10 to 3.14, plus the sdist. Licence expression LGPL-3.0-only AND MIT; every licence file ships in the wheel.
atemwire 1.0.3
The lock we held without knowing, and what the switcher really does. Measured on the wire; the table is in atemwire/atemwire/docs/MEDIA_LOCK.md.
- The transport dropped the first packet after every state dump (and any packet displaced by the flush sentinel mid-upload). When a lock had been requested during the dump that packet was the LKOB, so the session held the media lock without knowing it, for its whole life, and refused every other client. Displaced packets are now kept.
- Ready now means the state dump is complete (InCm seen), not that its first packet arrived. The pool's connect timeout grows to 15 s to match.
- FTDE 6 is a two-client collision, not a rejection: the loser still holds the lock. It now releases, keeps the task and re-requests when the store frees. Two sessions interleave one frame at a time, like two ATEM Software Control instances.
- Reliable packets are ACKed as soon as the session is established; one lock request per store per second; the lock is released before the RLE decode; a fatal transfer error fails the caller at once.
atemwire 1.0.2
Gives the media store lock back before a session ends.
A lock still held when a connection tears down stayed with the switcher for the rest of that session's life, and for a session that was abandoned rather than closed, for the switcher's own timeout of around five minutes. Every client that asked for the media store during that window was refused. In practice that looks like a switcher that will not let you load a still, or an ATEM Software Control media pool slot spinning forever on a thumbnail it cannot download.
The lock is held per frame during any upload or download, so any teardown landing mid-transfer could leave it behind: closing a control page, an idle disconnect, a pooled connection being reclaimed.
The release is written straight to the socket, the same way the protocol goodbye is, and before it. Releasing through the normal command path does not work on a close path, because that queues the packet for a worker thread which is already exiting.
videohubwire 1.0.1
Documentation only, no code change. Publishes the corrected package description: the page no longer calls 1.0.0 a pre-release, the install examples pin 1.0.x, and atemwire's page no longer claims a C compiler is required now that it ships prebuilt wheels for Linux, macOS and Windows on CPython 3.10 to 3.14.
videohubwire 1.0.0
Control Blackmagic Videohub routers from Python over TCP/9990.
Read the routing table and the input, output and monitoring labels, change a route, and rename a port. Connections are per request and short lived, which suits a router that is asked a question now and then rather than driven continuously.
MIT.