pyobs-gui 2.0.0.dev18__tar.gz → 2.0.0.dev19__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 (143) hide show
  1. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/PKG-INFO +1 -1
  2. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/videowidget.py +61 -10
  3. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyproject.toml +1 -1
  4. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/uv.lock +1 -1
  5. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/.claude/skills/verify/SKILL.md +0 -0
  6. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/.github/dependabot.yml +0 -0
  7. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/.github/workflows/pypi.yml +0 -0
  8. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/.github/workflows/pyrefly.yml +0 -0
  9. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/.github/workflows/ruff.yml +0 -0
  10. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/.gitignore +0 -0
  11. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/.pre-commit-config.yaml +0 -0
  12. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/.readthedocs.yml +0 -0
  13. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/CHANGELOG.rst +0 -0
  14. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/LICENSE +0 -0
  15. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/README.md +0 -0
  16. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/bin/mypyuic5 +0 -0
  17. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/docs/Makefile +0 -0
  18. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/docs/make.bat +0 -0
  19. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/docs/requirements.txt +0 -0
  20. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/docs/source/_static/pyobs.gif +0 -0
  21. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/docs/source/conf.py +0 -0
  22. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/docs/source/index.rst +0 -0
  23. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/mypy.ini +0 -0
  24. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/__init__.py +0 -0
  25. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/__main__.py +0 -0
  26. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/_nuitka_astropy_patch.py +0 -0
  27. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/accounts.py +0 -0
  28. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/acquisitionwidget.py +0 -0
  29. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/autofocuswidget.py +0 -0
  30. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/autoguidingwidget.py +0 -0
  31. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/base.py +0 -0
  32. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/camerawidget.py +0 -0
  33. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/commandinputwidget.py +0 -0
  34. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/compassmovewidget.py +0 -0
  35. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/coolingwidget.py +0 -0
  36. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/datadisplaywidget.py +0 -0
  37. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/eventswidget.py +0 -0
  38. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/filterwidget.py +0 -0
  39. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/fitsheaderswidget.py +0 -0
  40. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/focuswidget.py +0 -0
  41. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/gui.py +0 -0
  42. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/login.py +0 -0
  43. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/loginwindow.py +0 -0
  44. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/logmodel.py +0 -0
  45. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/mainwindow.py +0 -0
  46. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/modewidget.py +0 -0
  47. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/modifiedmixin.py +0 -0
  48. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/modulegui.py +0 -0
  49. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/__init__.py +0 -0
  50. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
  51. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
  52. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/autofocuswidget.ui +0 -0
  53. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
  54. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
  55. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
  56. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/camerawidget.ui +0 -0
  57. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/camerawidget_ui.py +0 -0
  58. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  59. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/compassmovewidget.py +0 -0
  60. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  61. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  62. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/compile.sh +0 -0
  63. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/coolingwidget.ui +0 -0
  64. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  65. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  66. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  67. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  68. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/designer.sh +0 -0
  69. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/eventswidget.ui +0 -0
  70. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  71. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/filterwidget.ui +0 -0
  72. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/filterwidget_ui.py +0 -0
  73. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
  74. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
  75. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/focuswidget.ui +0 -0
  76. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/focuswidget_ui.py +0 -0
  77. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/mainwindow.ui +0 -0
  78. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/mainwindow_ui.py +0 -0
  79. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/modewidget.ui +0 -0
  80. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/modewidget_ui.py +0 -0
  81. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  82. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  83. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  84. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  85. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  86. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  87. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  88. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  89. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  90. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  91. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  92. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  93. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources.qrc +0 -0
  94. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/resources_rc.py +0 -0
  95. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/roofwidget.ui +0 -0
  96. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  97. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/shellwidget.ui +0 -0
  98. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  99. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  100. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  101. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/telescopewidget.ui +0 -0
  102. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
  103. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  104. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  105. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  106. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  107. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/videowidget.ui +0 -0
  108. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/videowidget_ui.py +0 -0
  109. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/weatherwidget.ui +0 -0
  110. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  111. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/roofwidget.py +0 -0
  112. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/shellwidget.py +0 -0
  113. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/spectrographwidget.py +0 -0
  114. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/statuswidget.py +0 -0
  115. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/telescopewidget.py +0 -0
  116. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/temperaturesplotwidget.py +0 -0
  117. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/temperatureswidget.py +0 -0
  118. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/utils.py +0 -0
  119. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/watchedlabel.py +0 -0
  120. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pyobs_gui/weatherwidget.py +0 -0
  121. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/pysidedeploy.spec +0 -0
  122. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/specs/2026-08-20-gui-remote-call-error-handling.md +0 -0
  123. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/specs/index.md +0 -0
  124. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/spike_standalone/main.py +0 -0
  125. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/spike_standalone/pysidedeploy.spec +0 -0
  126. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/test/acquisition.yaml +0 -0
  127. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/test/autofocus.yaml +0 -0
  128. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/test/camera.yaml +0 -0
  129. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/test/full.yaml +0 -0
  130. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/test/guiding.yaml +0 -0
  131. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/test/mode.yaml +0 -0
  132. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/test/roof.yaml +0 -0
  133. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/test/spectrograph.yaml +0 -0
  134. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/test/telescope.yaml +0 -0
  135. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/test/telescope_acl.yaml +0 -0
  136. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/test/telescope_acl_denied.yaml +0 -0
  137. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/test/video.yaml +0 -0
  138. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/tests/__init__.py +0 -0
  139. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/tests/conftest.py +0 -0
  140. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/tests/test_accounts.py +0 -0
  141. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/tests/test_camerawidget.py +0 -0
  142. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/tests/test_loginwindow.py +0 -0
  143. {pyobs_gui-2.0.0.dev18 → pyobs_gui-2.0.0.dev19}/tests/test_videowidget.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyobs-gui
3
- Version: 2.0.0.dev18
3
+ Version: 2.0.0.dev19
4
4
  Summary: A remote GUI for pyobs
5
5
  Author-email: Tim-Oliver Husser <thusser@uni-goettingen.de>
6
6
  License-Expression: MIT
@@ -22,7 +22,6 @@ from PySide6 import QtCore, QtGui, QtNetwork, QtWidgets # type: ignore
22
22
  from .base import BaseWidget
23
23
  from .qt.videowidget_ui import Ui_VideoWidget
24
24
 
25
-
26
25
  log = logging.getLogger(__name__)
27
26
 
28
27
 
@@ -75,9 +74,16 @@ class VideoWidget(BaseWidget, Ui_VideoWidget):
75
74
  # init buffer
76
75
  self.buffer = b""
77
76
 
78
- # socket
79
- self.socket = QtNetwork.QTcpSocket()
80
- self.socket.readyRead.connect(self._received_data)
77
+ # raw socket for the MJPEG stream; created once the video URL is known
78
+ # (see _init), because its type depends on the URL scheme -- https needs
79
+ # a TLS socket so the plaintext HTTP request survives a TLS-terminating
80
+ # reverse proxy (e.g. nginx)
81
+ self.socket: QtNetwork.QAbstractSocket | None = None
82
+ self.scheme: str | None = None
83
+
84
+ # whether the HTTP response headers of the current stream connection have
85
+ # been stripped from self.buffer yet (see _received_data)
86
+ self._headers_received = False
81
87
 
82
88
  # set exposure types
83
89
  image_types = sorted([it.name for it in ImageType])
@@ -137,7 +143,7 @@ class VideoWidget(BaseWidget, Ui_VideoWidget):
137
143
 
138
144
  # parse URL
139
145
  o = urlparse(video_file.url)
140
- if o.scheme != "http":
146
+ if o.scheme not in ["http", "https"]:
141
147
  log.error("URL scheme to video of module %s must be HTTP.", self.module)
142
148
  return
143
149
 
@@ -145,8 +151,17 @@ class VideoWidget(BaseWidget, Ui_VideoWidget):
145
151
  s = o.netloc.split(":")[:2]
146
152
  self.host, self.port = s[0], int(s[1])
147
153
  else:
148
- self.host, self.port = (o.netloc, 80)
154
+ self.host, self.port = (o.netloc, 443 if o.scheme == "https" else 80)
149
155
  self.path = o.path
156
+ self.scheme = o.scheme
157
+
158
+ # create the raw socket for the stream; https URLs need a TLS socket so
159
+ # the plaintext MJPEG request works through a TLS-terminating reverse
160
+ # proxy (a plain QTcpSocket can only reach a plain-HTTP endpoint)
161
+ self.socket = QtNetwork.QSslSocket() if self.scheme == "https" else QtNetwork.QTcpSocket()
162
+ self.socket.readyRead.connect(self._received_data)
163
+ if isinstance(self.socket, QtNetwork.QSslSocket):
164
+ self.socket.sslErrors.connect(self._on_ssl_errors)
150
165
 
151
166
  # subscribe to state
152
167
  if has_exposure_time:
@@ -173,19 +188,38 @@ class VideoWidget(BaseWidget, Ui_VideoWidget):
173
188
  await BaseWidget._showEvent(self, event)
174
189
 
175
190
  # connect socket
176
- if self.host is not None and self.port is not None and self.path is not None:
177
- self.socket.connectToHost(self.host, self.port)
191
+ if self.host is not None and self.port is not None and self.path is not None and self.socket is not None:
192
+ if self.scheme == "https":
193
+ if not isinstance(self.socket, QtNetwork.QSslSocket):
194
+ log.error("Video URL of %s is https but no TLS socket was created.", self.module)
195
+ return
196
+ self.socket.connectToHostEncrypted(self.host, self.port)
197
+ else:
198
+ self.socket.connectToHost(self.host, self.port)
178
199
  host_header = self.host if self.port == 80 else f"{self.host}:{self.port}"
200
+ # HTTP/1.0 on purpose: HTTP/1.1 responses to an endless MJPEG stream use chunked
201
+ # transfer encoding, and this raw-socket parser doesn't decode chunk framing --
202
+ # the hex chunk-size lines would land inside the JPEG data and corrupt every
203
+ # frame they fall in. HTTP/1.0 forbids chunked encoding, so the body arrives
204
+ # as the plain MJPEG byte stream (with Connection: close, which just means the
205
+ # stream runs until the client disconnects).
179
206
  self.socket.write(
180
- b"GET %s HTTP/1.1\r\nHost: %s\r\n\r\n" % (bytes(self.path, "UTF-8"), bytes(host_header, "UTF-8"))
207
+ b"GET %s HTTP/1.0\r\nHost: %s\r\n\r\n" % (bytes(self.path, "UTF-8"), bytes(host_header, "UTF-8"))
181
208
  )
209
+ # new connection: the next bytes start with the HTTP response headers
210
+ self._headers_received = False
211
+
212
+ def _on_ssl_errors(self, errors: list) -> None:
213
+ """Log SSL errors from the video-stream socket instead of failing silently."""
214
+ log.error("SSL errors connecting to video stream of %s: %s", self.module, errors)
182
215
 
183
216
  def hideEvent(self, event: QtGui.QHideEvent) -> None:
184
217
  # call base
185
218
  BaseWidget.hideEvent(self, event)
186
219
 
187
220
  # disconnect socket
188
- self.socket.disconnectFromHost()
221
+ if self.socket is not None:
222
+ self.socket.disconnectFromHost()
189
223
 
190
224
  def update_gui(self) -> None:
191
225
  """Update the GUI."""
@@ -203,8 +237,21 @@ class VideoWidget(BaseWidget, Ui_VideoWidget):
203
237
  self.labelExposuresLeft.setText("")
204
238
 
205
239
  def _received_data(self) -> None:
240
+ if self.socket is None:
241
+ return
206
242
  boundary = b"--jpgboundary\r\n"
207
243
  self.buffer += bytes(self.socket.readAll())
244
+
245
+ # strip the HTTP response headers before parsing the MJPEG body -- the
246
+ # boundary string also appears in the "Content-Type: ... boundary=--jpgboundary"
247
+ # header line, so parsing it as a frame would produce garbage
248
+ if not self._headers_received:
249
+ pos = self.buffer.find(b"\r\n\r\n")
250
+ if pos == -1:
251
+ return # headers not complete yet
252
+ self.buffer = self.buffer[pos + 4 :]
253
+ self._headers_received = True
254
+
208
255
  while boundary in self.buffer:
209
256
  # find boundary
210
257
  pos = self.buffer.find(boundary)
@@ -217,6 +264,10 @@ class VideoWidget(BaseWidget, Ui_VideoWidget):
217
264
 
218
265
  # find end of frame
219
266
  image_data = frame[frame.find(b"\r\n\r\n") + 4 :]
267
+ if not image_data:
268
+ # preamble before the first boundary (e.g. HTTP response headers)
269
+ # or a frame with no payload yet -- nothing to show
270
+ continue
220
271
 
221
272
  # to pixmap and show it
222
273
  qp = QtGui.QPixmap()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyobs-gui"
3
- version = "2.0.0.dev18"
3
+ version = "2.0.0.dev19"
4
4
  description = "A remote GUI for pyobs"
5
5
  authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }]
6
6
  requires-python = ">=3.11"
@@ -3295,7 +3295,7 @@ wheels = [
3295
3295
 
3296
3296
  [[package]]
3297
3297
  name = "pyobs-gui"
3298
- version = "2.0.0.dev18"
3298
+ version = "2.0.0.dev19"
3299
3299
  source = { editable = "." }
3300
3300
  dependencies = [
3301
3301
  { name = "astroplan" },
File without changes