webatem 2.0.2__tar.gz → 2.0.3__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.
Files changed (134) hide show
  1. {webatem-2.0.2 → webatem-2.0.3}/PKG-INFO +1 -1
  2. {webatem-2.0.2 → webatem-2.0.3}/atem_control/control/consumer.py +54 -19
  3. {webatem-2.0.2 → webatem-2.0.3}/pyproject.toml +1 -1
  4. {webatem-2.0.2 → webatem-2.0.3}/webatem.egg-info/PKG-INFO +1 -1
  5. {webatem-2.0.2 → webatem-2.0.3}/LICENSE +0 -0
  6. {webatem-2.0.2 → webatem-2.0.3}/README.md +0 -0
  7. {webatem-2.0.2 → webatem-2.0.3}/atem_control/__init__.py +0 -0
  8. {webatem-2.0.2 → webatem-2.0.3}/atem_control/activity.py +0 -0
  9. {webatem-2.0.2 → webatem-2.0.3}/atem_control/apps.py +0 -0
  10. {webatem-2.0.2 → webatem-2.0.3}/atem_control/control/__init__.py +0 -0
  11. {webatem-2.0.2 → webatem-2.0.3}/atem_control/control/activity.py +0 -0
  12. {webatem-2.0.2 → webatem-2.0.3}/atem_control/control/commands.py +0 -0
  13. {webatem-2.0.2 → webatem-2.0.3}/atem_control/control/device_api.py +0 -0
  14. {webatem-2.0.2 → webatem-2.0.3}/atem_control/control/logging.py +0 -0
  15. {webatem-2.0.2 → webatem-2.0.3}/atem_control/control/views.py +0 -0
  16. {webatem-2.0.2 → webatem-2.0.3}/atem_control/discovery.py +0 -0
  17. {webatem-2.0.2 → webatem-2.0.3}/atem_control/hooks.py +0 -0
  18. {webatem-2.0.2 → webatem-2.0.3}/atem_control/hyperdeck/__init__.py +0 -0
  19. {webatem-2.0.2 → webatem-2.0.3}/atem_control/hyperdeck/connection.py +0 -0
  20. {webatem-2.0.2 → webatem-2.0.3}/atem_control/hyperdeck/views.py +0 -0
  21. {webatem-2.0.2 → webatem-2.0.3}/atem_control/ip_upload_lock.py +0 -0
  22. {webatem-2.0.2 → webatem-2.0.3}/atem_control/media_pool/__init__.py +0 -0
  23. {webatem-2.0.2 → webatem-2.0.3}/atem_control/media_pool/broadcast.py +0 -0
  24. {webatem-2.0.2 → webatem-2.0.3}/atem_control/media_pool/views.py +0 -0
  25. {webatem-2.0.2 → webatem-2.0.3}/atem_control/media_pool/watcher.py +0 -0
  26. {webatem-2.0.2 → webatem-2.0.3}/atem_control/migrations/0001_initial.py +0 -0
  27. {webatem-2.0.2 → webatem-2.0.3}/atem_control/migrations/0002_legacy_log.py +0 -0
  28. {webatem-2.0.2 → webatem-2.0.3}/atem_control/migrations/__init__.py +0 -0
  29. {webatem-2.0.2 → webatem-2.0.3}/atem_control/models.py +0 -0
  30. {webatem-2.0.2 → webatem-2.0.3}/atem_control/netutil.py +0 -0
  31. {webatem-2.0.2 → webatem-2.0.3}/atem_control/profile/__init__.py +0 -0
  32. {webatem-2.0.2 → webatem-2.0.3}/atem_control/profile/dialog.py +0 -0
  33. {webatem-2.0.2 → webatem-2.0.3}/atem_control/profile/export.py +0 -0
  34. {webatem-2.0.2 → webatem-2.0.3}/atem_control/profile/views.py +0 -0
  35. {webatem-2.0.2 → webatem-2.0.3}/atem_control/routing.py +0 -0
  36. {webatem-2.0.2 → webatem-2.0.3}/atem_control/sightings.py +0 -0
  37. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/apple-touch-icon.png +0 -0
  38. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/brand.css +0 -0
  39. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/favicon-32.png +0 -0
  40. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/favicon.ico +0 -0
  41. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/Archivo-400-latin-ext.woff2 +0 -0
  42. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/Archivo-400-latin.woff2 +0 -0
  43. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/Archivo-500-latin-ext.woff2 +0 -0
  44. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/Archivo-500-latin.woff2 +0 -0
  45. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/Archivo-600-latin-ext.woff2 +0 -0
  46. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/Archivo-600-latin.woff2 +0 -0
  47. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/Archivo-800-latin-ext.woff2 +0 -0
  48. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/Archivo-800-latin.woff2 +0 -0
  49. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/IBMPlexMono-400-latin-ext.woff2 +0 -0
  50. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/IBMPlexMono-400-latin.woff2 +0 -0
  51. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/IBMPlexMono-500-latin-ext.woff2 +0 -0
  52. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/IBMPlexMono-500-latin.woff2 +0 -0
  53. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/IBMPlexMono-600-latin-ext.woff2 +0 -0
  54. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/IBMPlexMono-600-latin.woff2 +0 -0
  55. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/OFL-Archivo.txt +0 -0
  56. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/fonts/OFL-IBMPlexMono.txt +0 -0
  57. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/icon-192.png +0 -0
  58. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/icon-512.png +0 -0
  59. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/icon-maskable-512.png +0 -0
  60. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/manifest.webmanifest +0 -0
  61. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/mark.svg +0 -0
  62. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/tray/linux-24.png +0 -0
  63. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/tray/mac-22.png +0 -0
  64. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/tray/mac-44.png +0 -0
  65. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/tray/win-16.png +0 -0
  66. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/brand/tray/win-32.png +0 -0
  67. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/css/atem_connect.css +0 -0
  68. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/css/atem_control.css +0 -0
  69. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/icon.png +0 -0
  70. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/js/atem_connect.js +0 -0
  71. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/js/atem_control.js +0 -0
  72. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/js/atem_control_page.js +0 -0
  73. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/js/atem_discovery.js +0 -0
  74. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/js/atem_hyperdeck.js +0 -0
  75. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/js/atem_profile.js +0 -0
  76. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/js/atem_realtime_slider.js +0 -0
  77. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/js/custom-scrollbar.js +0 -0
  78. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/js/recent_label.js +0 -0
  79. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/js/wa-select.js +0 -0
  80. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/vendor/README.md +0 -0
  81. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/vendor/alpine.min.js +0 -0
  82. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/vendor/alpinejs-collapse-3.14.3.min.js +0 -0
  83. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/vendor/bootstrap-icons/bootstrap-icons.min.css +0 -0
  84. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  85. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  86. {webatem-2.0.2 → webatem-2.0.3}/atem_control/static/vendor/webatem.css +0 -0
  87. {webatem-2.0.2 → webatem-2.0.3}/atem_control/storage.py +0 -0
  88. {webatem-2.0.2 → webatem-2.0.3}/atem_control/tally.py +0 -0
  89. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/connect.html +0 -0
  90. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_atem_info.html +0 -0
  91. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_fade_to_black.html +0 -0
  92. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_fairlight.html +0 -0
  93. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_header.html +0 -0
  94. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_hyperdeck_panel.html +0 -0
  95. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_macros.html +0 -0
  96. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_media_pool.html +0 -0
  97. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_profile_modal.html +0 -0
  98. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_settings_dsk.html +0 -0
  99. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_settings_transition.html +0 -0
  100. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_settings_usk.html +0 -0
  101. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_slider.html +0 -0
  102. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_transition_controls.html +0 -0
  103. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_transition_style.html +0 -0
  104. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_usk_fly.html +0 -0
  105. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_usk_keyframe.html +0 -0
  106. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_usk_mask.html +0 -0
  107. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control/_usk_slider.html +0 -0
  108. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templates/control.html +0 -0
  109. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templatetags/__init__.py +0 -0
  110. {webatem-2.0.2 → webatem-2.0.3}/atem_control/templatetags/static_v.py +0 -0
  111. {webatem-2.0.2 → webatem-2.0.3}/atem_control/uploader.py +0 -0
  112. {webatem-2.0.2 → webatem-2.0.3}/atem_control/urls.py +0 -0
  113. {webatem-2.0.2 → webatem-2.0.3}/setup.cfg +0 -0
  114. {webatem-2.0.2 → webatem-2.0.3}/webatem/__init__.py +0 -0
  115. {webatem-2.0.2 → webatem-2.0.3}/webatem/__main__.py +0 -0
  116. {webatem-2.0.2 → webatem-2.0.3}/webatem/applog.py +0 -0
  117. {webatem-2.0.2 → webatem-2.0.3}/webatem/asgi.py +0 -0
  118. {webatem-2.0.2 → webatem-2.0.3}/webatem/context.py +0 -0
  119. {webatem-2.0.2 → webatem-2.0.3}/webatem/launcher.py +0 -0
  120. {webatem-2.0.2 → webatem-2.0.3}/webatem/server.py +0 -0
  121. {webatem-2.0.2 → webatem-2.0.3}/webatem/settings.py +0 -0
  122. {webatem-2.0.2 → webatem-2.0.3}/webatem/templates/base.html +0 -0
  123. {webatem-2.0.2 → webatem-2.0.3}/webatem/templates/brand/_mark.svg +0 -0
  124. {webatem-2.0.2 → webatem-2.0.3}/webatem/templates/launcher.html +0 -0
  125. {webatem-2.0.2 → webatem-2.0.3}/webatem/uninstall.py +0 -0
  126. {webatem-2.0.2 → webatem-2.0.3}/webatem/updates.py +0 -0
  127. {webatem-2.0.2 → webatem-2.0.3}/webatem/urls.py +0 -0
  128. {webatem-2.0.2 → webatem-2.0.3}/webatem/views.py +0 -0
  129. {webatem-2.0.2 → webatem-2.0.3}/webatem/websocket.py +0 -0
  130. {webatem-2.0.2 → webatem-2.0.3}/webatem.egg-info/SOURCES.txt +0 -0
  131. {webatem-2.0.2 → webatem-2.0.3}/webatem.egg-info/dependency_links.txt +0 -0
  132. {webatem-2.0.2 → webatem-2.0.3}/webatem.egg-info/entry_points.txt +0 -0
  133. {webatem-2.0.2 → webatem-2.0.3}/webatem.egg-info/requires.txt +0 -0
  134. {webatem-2.0.2 → webatem-2.0.3}/webatem.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webatem
3
- Version: 2.0.2
3
+ Version: 2.0.3
4
4
  Summary: Browser-based control for Blackmagic ATEM switchers: run it, open the browser, your switchers are there
5
5
  Author: Lucas Romanenko
6
6
  License: MIT
@@ -134,10 +134,12 @@ class ATEMConsumer(ATEMConnectionLoggingMixin, ATEMStateMixin, AsyncWebsocketCon
134
134
  # is sent to the ATEM exactly on the 0↔1 transitions.
135
135
  self._audio_subscribed = False
136
136
  self._meter_handler_ids: list = []
137
- # The AtemProtocol object the meter handlers were registered on —
138
- # a pooled re-handshake builds a NEW protocol (and a NEW ATEM
139
- # session that never got SFLN), so identity change means the
140
- # meters must re-arm.
137
+ # The AtemProtocol object the meter handlers were registered on.
138
+ # A transport re-handshake keeps it (and starts a new switcher
139
+ # session that has not had SFLN): the 'connected' handler re-arms
140
+ # for that. Only a fresh connect() on this connection, after its
141
+ # worker died, builds a new protocol; the poll loop's identity
142
+ # check re-registers the handlers on it.
141
143
  self._meter_protocol = None
142
144
  self._meter_loop = None
143
145
  self._meter_batch_strips: dict = {} # strip_id → latest payload this tick
@@ -649,8 +651,10 @@ class ATEMConsumer(ATEMConnectionLoggingMixin, ATEMStateMixin, AsyncWebsocketCon
649
651
  # SYN handshakes after a cable yank, so ``is_connected`` stays
650
652
  # True for a while even with no live ATEM). The real signal is
651
653
  # ``build_full_state`` returning ``is_connected: False``, which
652
- # fires when atemwire's protocol layer cleared mixerstate after
653
- # giving up on the failed reconnects.
654
+ # fires when atemwire's protocol layer clears mixerstate: 5 s
655
+ # after the switcher's last packet, when the transport starts
656
+ # re-handshaking (and keeps at it). Since bmdwire 1.4.0 that
657
+ # holds even while this side keeps sending (bmdwire#11).
654
658
  #
655
659
  # Starts at ``None`` (unknown). The first observation seeds it
656
660
  # without firing a notification — otherwise the first poll
@@ -674,9 +678,11 @@ class ATEMConsumer(ATEMConnectionLoggingMixin, ATEMStateMixin, AsyncWebsocketCon
674
678
  await self._auto_disconnect_inactive()
675
679
  break
676
680
 
677
- # Meters must survive a pooled in-place reconnect
678
- # (new protocol object + new ATEM session) — cheap
679
- # identity check, no-op when nothing changed.
681
+ # Meters must survive a connection that built a new
682
+ # protocol object (a fresh connect() after the worker
683
+ # died). Cheap identity check, a no-op when nothing
684
+ # changed. A transport re-handshake keeps the protocol
685
+ # and is re-armed by the 'connected' handler instead.
680
686
  self._rearm_audio_meters_if_reconnected()
681
687
 
682
688
  # Get current state
@@ -685,8 +691,8 @@ class ATEMConsumer(ATEMConnectionLoggingMixin, ATEMStateMixin, AsyncWebsocketCon
685
691
  t_build = time.monotonic() - t0
686
692
 
687
693
  # ATEM-side reachability transition: atemwire's protocol clears
688
- # mixerstate when it gives up on reconnects, which makes
689
- # build_full_state return ``{'is_connected': False}``.
694
+ # mixerstate when the transport starts re-handshaking, which
695
+ # makes build_full_state return ``{'is_connected': False}``.
690
696
  # Treat that as "ATEM dropped mid-session" and notify the
691
697
  # frontend so the disconnect banner
692
698
  # appears. Without this, the worker keeps retrying SYN
@@ -982,6 +988,13 @@ class ATEMConsumer(ATEMConnectionLoggingMixin, ATEMStateMixin, AsyncWebsocketCon
982
988
  self._on_master_meter)
983
989
  self._meter_handler_ids.append(('change:fairlight-master-levels', mid))
984
990
 
991
+ # A new switcher session (the transport re-handshaking after a
992
+ # silence) has not had SFLN, so it streams no meters until it
993
+ # gets one. It keeps this protocol object, so the identity check
994
+ # can't see it: re-arm on the session's 'connected' event.
995
+ cid = protocol.on('connected', self._on_session_connected)
996
+ self._meter_handler_ids.append(('connected', cid))
997
+
985
998
  # Per-IP subscriber refcount. First subscriber tells the ATEM to
986
999
  # start streaming meters — without SFLN(True) the ATEM emits zero
987
1000
  # FMLv/FDLv packets even with handlers registered.
@@ -999,14 +1012,17 @@ class ATEMConsumer(ATEMConnectionLoggingMixin, ATEMStateMixin, AsyncWebsocketCon
999
1012
  self._meter_protocol = protocol
1000
1013
 
1001
1014
  def _rearm_audio_meters_if_reconnected(self):
1002
- """A pooled re-handshake builds a NEW AtemProtocol and a NEW
1003
- ATEM session — the meter handlers were registered on the old
1004
- object and the new session never received SFLN(enable), so the
1005
- meters froze at the old levels (looking live) until every
1006
- subscriber unsubscribed. Detect the protocol identity change from
1007
- the poll loop and re-arm: re-register the handlers on the new
1008
- protocol and re-send the enable (idempotent on the ATEM; the
1009
- per-IP refcount is untouched — this consumer never unsubscribed)."""
1015
+ """Re-arm the meters on a NEW AtemProtocol. A transport
1016
+ re-handshake does not build one: it keeps the protocol and is
1017
+ re-armed by ``_on_session_connected``. Only a fresh ``connect()``
1018
+ on this connection after its worker died does, and then the meter
1019
+ handlers are still on the old object and the new session never
1020
+ received SFLN(enable), so the meters would freeze at the old
1021
+ levels (looking live) until every subscriber unsubscribed. Detect
1022
+ the protocol identity change from the poll loop and re-arm:
1023
+ re-register the handlers on the new protocol and re-send the
1024
+ enable (idempotent on the ATEM; the per-IP refcount is untouched,
1025
+ since this consumer never unsubscribed)."""
1010
1026
  if not self._audio_subscribed:
1011
1027
  return
1012
1028
  protocol = getattr(self.connection, 'protocol', None) if self.connection else None
@@ -1019,12 +1035,31 @@ class ATEMConsumer(ATEMConnectionLoggingMixin, ATEMStateMixin, AsyncWebsocketCon
1019
1035
  self._meter_handler_ids.append(('change:fairlight-meter-levels', sid))
1020
1036
  mid = protocol.on('change:fairlight-master-levels', self._on_master_meter)
1021
1037
  self._meter_handler_ids.append(('change:fairlight-master-levels', mid))
1038
+ cid = protocol.on('connected', self._on_session_connected)
1039
+ self._meter_handler_ids.append(('connected', cid))
1022
1040
  self._meter_protocol = protocol
1023
1041
  try:
1024
1042
  enable_fairlight_levels(self.connection, enable=True)
1025
1043
  except Exception as e:
1026
1044
  logger.warning(f"SFLN re-arm failed: {e}")
1027
1045
 
1046
+ def _on_session_connected(self):
1047
+ """Handler for the protocol's 'connected' event, which fires when
1048
+ a new switcher session's state dump is complete: after a transport
1049
+ re-handshake on the same protocol object (webatem#38). That session
1050
+ has not had SFLN, so re-send it. Runs on the connection's worker
1051
+ thread; ``ATEMConnection.send`` is thread-safe. Every subscribed
1052
+ consumer sends one, which is idempotent on the switcher; the
1053
+ per-IP refcount only gates the 0→1 send at subscribe time, so with
1054
+ two tabs open a re-subscribe alone would send nothing."""
1055
+ if not self._audio_subscribed or self.connection is None:
1056
+ return
1057
+ logger.info(f"Re-arming audio meters for the new switcher session ({self.current_ip})")
1058
+ try:
1059
+ enable_fairlight_levels(self.connection, enable=True)
1060
+ except Exception as e:
1061
+ logger.warning(f"SFLN re-arm after reconnect failed: {e}")
1062
+
1028
1063
  def _unsubscribe_audio_meters(self):
1029
1064
  """Idempotent. Unregister per-consumer meter listeners and
1030
1065
  decrement the per-IP refcount; send SFLN(enable=False) on 1→0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "webatem"
7
- version = "2.0.2"
7
+ version = "2.0.3"
8
8
  description = "Browser-based control for Blackmagic ATEM switchers: run it, open the browser, your switchers are there"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webatem
3
- Version: 2.0.2
3
+ Version: 2.0.3
4
4
  Summary: Browser-based control for Blackmagic ATEM switchers: run it, open the browser, your switchers are there
5
5
  Author: Lucas Romanenko
6
6
  License: MIT
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