pyglet 2.1.12__py3-none-any.whl → 3.0.dev1__py3-none-any.whl

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 (267) hide show
  1. pyglet/__init__.py +67 -61
  2. pyglet/__init__.pyi +15 -8
  3. pyglet/app/__init__.py +22 -13
  4. pyglet/app/async_app.py +212 -0
  5. pyglet/app/base.py +2 -1
  6. pyglet/app/{xlib.py → linux.py} +3 -3
  7. pyglet/config/__init__.py +101 -0
  8. pyglet/config/gl/__init__.py +30 -0
  9. pyglet/config/gl/egl.py +120 -0
  10. pyglet/config/gl/macos.py +262 -0
  11. pyglet/config/gl/windows.py +267 -0
  12. pyglet/config/gl/x11.py +142 -0
  13. pyglet/customtypes.py +43 -2
  14. pyglet/display/__init__.py +8 -6
  15. pyglet/display/base.py +3 -63
  16. pyglet/display/cocoa.py +12 -17
  17. pyglet/display/emscripten.py +39 -0
  18. pyglet/display/headless.py +23 -30
  19. pyglet/display/wayland.py +157 -0
  20. pyglet/display/win32.py +4 -17
  21. pyglet/display/xlib.py +19 -27
  22. pyglet/display/xlib_vidmoderestore.py +2 -2
  23. pyglet/enums.py +183 -0
  24. pyglet/event.py +0 -1
  25. pyglet/experimental/geoshader_sprite.py +15 -13
  26. pyglet/experimental/hidraw.py +6 -15
  27. pyglet/experimental/multitexture_sprite.py +31 -19
  28. pyglet/experimental/particles.py +13 -35
  29. pyglet/font/__init__.py +251 -85
  30. pyglet/font/base.py +116 -61
  31. pyglet/font/dwrite/__init__.py +349 -204
  32. pyglet/font/dwrite/dwrite_lib.py +27 -5
  33. pyglet/font/fontconfig.py +14 -6
  34. pyglet/font/freetype.py +138 -87
  35. pyglet/font/freetype_lib.py +19 -0
  36. pyglet/font/group.py +179 -0
  37. pyglet/font/harfbuzz/__init__.py +3 -3
  38. pyglet/font/pyodide_js.py +310 -0
  39. pyglet/font/quartz.py +319 -126
  40. pyglet/font/ttf.py +45 -3
  41. pyglet/font/user.py +14 -19
  42. pyglet/font/win32.py +45 -21
  43. pyglet/graphics/__init__.py +8 -787
  44. pyglet/graphics/allocation.py +115 -1
  45. pyglet/graphics/api/__init__.py +77 -0
  46. pyglet/graphics/api/base.py +299 -0
  47. pyglet/graphics/api/gl/__init__.py +58 -0
  48. pyglet/graphics/api/gl/base.py +24 -0
  49. pyglet/graphics/{vertexbuffer.py → api/gl/buffer.py} +104 -159
  50. pyglet/graphics/api/gl/cocoa/context.py +76 -0
  51. pyglet/graphics/api/gl/context.py +391 -0
  52. pyglet/graphics/api/gl/default_shaders.py +0 -0
  53. pyglet/graphics/api/gl/draw.py +627 -0
  54. pyglet/graphics/api/gl/egl/__init__.py +0 -0
  55. pyglet/graphics/api/gl/egl/context.py +92 -0
  56. pyglet/graphics/api/gl/enums.py +76 -0
  57. pyglet/graphics/api/gl/framebuffer.py +315 -0
  58. pyglet/graphics/api/gl/gl.py +5463 -0
  59. pyglet/graphics/api/gl/gl_info.py +188 -0
  60. pyglet/graphics/api/gl/global_opengl.py +226 -0
  61. pyglet/{gl → graphics/api/gl}/lib.py +34 -18
  62. pyglet/graphics/api/gl/shader.py +1476 -0
  63. pyglet/graphics/api/gl/shapes.py +55 -0
  64. pyglet/graphics/api/gl/sprite.py +102 -0
  65. pyglet/graphics/api/gl/state.py +219 -0
  66. pyglet/graphics/api/gl/text.py +190 -0
  67. pyglet/graphics/api/gl/texture.py +1526 -0
  68. pyglet/graphics/{vertexarray.py → api/gl/vertexarray.py} +11 -13
  69. pyglet/graphics/api/gl/vertexdomain.py +751 -0
  70. pyglet/graphics/api/gl/win32/__init__.py +0 -0
  71. pyglet/graphics/api/gl/win32/context.py +108 -0
  72. pyglet/graphics/api/gl/win32/wgl_info.py +24 -0
  73. pyglet/graphics/api/gl/xlib/__init__.py +0 -0
  74. pyglet/graphics/api/gl/xlib/context.py +174 -0
  75. pyglet/{gl → graphics/api/gl/xlib}/glx_info.py +26 -31
  76. pyglet/graphics/api/gl1/__init__.py +0 -0
  77. pyglet/{gl → graphics/api/gl1}/gl_compat.py +3 -2
  78. pyglet/graphics/api/gl2/__init__.py +0 -0
  79. pyglet/graphics/api/gl2/buffer.py +320 -0
  80. pyglet/graphics/api/gl2/draw.py +600 -0
  81. pyglet/graphics/api/gl2/global_opengl.py +122 -0
  82. pyglet/graphics/api/gl2/shader.py +200 -0
  83. pyglet/graphics/api/gl2/shapes.py +51 -0
  84. pyglet/graphics/api/gl2/sprite.py +79 -0
  85. pyglet/graphics/api/gl2/text.py +175 -0
  86. pyglet/graphics/api/gl2/vertexdomain.py +364 -0
  87. pyglet/graphics/api/webgl/__init__.py +233 -0
  88. pyglet/graphics/api/webgl/buffer.py +302 -0
  89. pyglet/graphics/api/webgl/context.py +234 -0
  90. pyglet/graphics/api/webgl/draw.py +590 -0
  91. pyglet/graphics/api/webgl/enums.py +76 -0
  92. pyglet/graphics/api/webgl/framebuffer.py +360 -0
  93. pyglet/graphics/api/webgl/gl.py +1537 -0
  94. pyglet/graphics/api/webgl/gl_info.py +130 -0
  95. pyglet/graphics/api/webgl/shader.py +1346 -0
  96. pyglet/graphics/api/webgl/shapes.py +92 -0
  97. pyglet/graphics/api/webgl/sprite.py +102 -0
  98. pyglet/graphics/api/webgl/state.py +227 -0
  99. pyglet/graphics/api/webgl/text.py +187 -0
  100. pyglet/graphics/api/webgl/texture.py +1227 -0
  101. pyglet/graphics/api/webgl/vertexarray.py +54 -0
  102. pyglet/graphics/api/webgl/vertexdomain.py +616 -0
  103. pyglet/graphics/api/webgl/webgl_js.pyi +307 -0
  104. pyglet/{image → graphics}/atlas.py +33 -32
  105. pyglet/graphics/base.py +10 -0
  106. pyglet/graphics/buffer.py +245 -0
  107. pyglet/graphics/draw.py +578 -0
  108. pyglet/graphics/framebuffer.py +26 -0
  109. pyglet/graphics/instance.py +178 -69
  110. pyglet/graphics/shader.py +267 -1553
  111. pyglet/graphics/state.py +83 -0
  112. pyglet/graphics/texture.py +703 -0
  113. pyglet/graphics/vertexdomain.py +695 -538
  114. pyglet/gui/ninepatch.py +10 -10
  115. pyglet/gui/widgets.py +120 -10
  116. pyglet/image/__init__.py +20 -1973
  117. pyglet/image/animation.py +12 -12
  118. pyglet/image/base.py +730 -0
  119. pyglet/image/codecs/__init__.py +9 -0
  120. pyglet/image/codecs/bmp.py +53 -30
  121. pyglet/image/codecs/dds.py +53 -31
  122. pyglet/image/codecs/gdiplus.py +38 -14
  123. pyglet/image/codecs/gdkpixbuf2.py +0 -2
  124. pyglet/image/codecs/js_image.py +99 -0
  125. pyglet/image/codecs/ktx2.py +161 -0
  126. pyglet/image/codecs/pil.py +1 -1
  127. pyglet/image/codecs/png.py +1 -1
  128. pyglet/image/codecs/wic.py +11 -2
  129. pyglet/info.py +26 -24
  130. pyglet/input/__init__.py +8 -0
  131. pyglet/input/base.py +163 -105
  132. pyglet/input/controller.py +13 -19
  133. pyglet/input/controller_db.py +39 -24
  134. pyglet/input/emscripten/__init__.py +18 -0
  135. pyglet/input/emscripten/gamepad_js.py +397 -0
  136. pyglet/input/linux/__init__.py +11 -5
  137. pyglet/input/linux/evdev.py +10 -11
  138. pyglet/input/linux/x11_xinput.py +2 -2
  139. pyglet/input/linux/x11_xinput_tablet.py +1 -1
  140. pyglet/input/macos/__init__.py +7 -2
  141. pyglet/input/macos/darwin_gc.py +559 -0
  142. pyglet/input/win32/__init__.py +1 -1
  143. pyglet/input/win32/directinput.py +34 -29
  144. pyglet/input/win32/xinput.py +11 -61
  145. pyglet/lib.py +3 -3
  146. pyglet/libs/__init__.py +1 -1
  147. pyglet/{gl → libs/darwin}/agl.py +1 -1
  148. pyglet/libs/darwin/cocoapy/__init__.py +2 -2
  149. pyglet/libs/darwin/cocoapy/cocoahelpers.py +181 -0
  150. pyglet/libs/darwin/cocoapy/cocoalibs.py +31 -0
  151. pyglet/libs/darwin/cocoapy/cocoatypes.py +27 -0
  152. pyglet/libs/darwin/cocoapy/runtime.py +81 -45
  153. pyglet/libs/darwin/coreaudio.py +4 -4
  154. pyglet/{gl → libs/darwin}/lib_agl.py +9 -8
  155. pyglet/libs/darwin/quartzkey.py +1 -3
  156. pyglet/libs/egl/__init__.py +2 -0
  157. pyglet/libs/egl/egl_lib.py +576 -0
  158. pyglet/libs/egl/eglext.py +51 -5
  159. pyglet/libs/linux/__init__.py +0 -0
  160. pyglet/libs/linux/egl/__init__.py +0 -0
  161. pyglet/libs/linux/egl/eglext.py +22 -0
  162. pyglet/libs/linux/glx/__init__.py +0 -0
  163. pyglet/{gl → libs/linux/glx}/glx.py +13 -14
  164. pyglet/{gl → libs/linux/glx}/glxext_arb.py +408 -192
  165. pyglet/{gl → libs/linux/glx}/glxext_mesa.py +1 -1
  166. pyglet/{gl → libs/linux/glx}/glxext_nv.py +345 -164
  167. pyglet/{gl → libs/linux/glx}/lib_glx.py +3 -2
  168. pyglet/libs/linux/wayland/__init__.py +0 -0
  169. pyglet/libs/linux/wayland/client.py +1068 -0
  170. pyglet/libs/linux/wayland/lib_wayland.py +207 -0
  171. pyglet/libs/linux/wayland/wayland_egl.py +38 -0
  172. pyglet/libs/{wayland → linux/wayland}/xkbcommon.py +26 -0
  173. pyglet/libs/{x11 → linux/x11}/xf86vmode.py +4 -4
  174. pyglet/libs/{x11 → linux/x11}/xinerama.py +2 -2
  175. pyglet/libs/{x11 → linux/x11}/xinput.py +10 -10
  176. pyglet/libs/linux/x11/xrandr.py +0 -0
  177. pyglet/libs/{x11 → linux/x11}/xrender.py +1 -1
  178. pyglet/libs/shared/__init__.py +0 -0
  179. pyglet/libs/shared/spirv/__init__.py +0 -0
  180. pyglet/libs/shared/spirv/lib_shaderc.py +85 -0
  181. pyglet/libs/shared/spirv/lib_spirv_cross.py +126 -0
  182. pyglet/libs/win32/__init__.py +27 -5
  183. pyglet/libs/win32/constants.py +59 -48
  184. pyglet/libs/win32/context_managers.py +20 -3
  185. pyglet/libs/win32/dinput.py +105 -88
  186. pyglet/{gl → libs/win32}/lib_wgl.py +52 -26
  187. pyglet/libs/win32/types.py +58 -23
  188. pyglet/{gl → libs/win32}/wgl.py +32 -25
  189. pyglet/{gl → libs/win32}/wglext_arb.py +364 -2
  190. pyglet/media/__init__.py +9 -10
  191. pyglet/media/codecs/__init__.py +12 -1
  192. pyglet/media/codecs/base.py +99 -96
  193. pyglet/media/codecs/ffmpeg.py +2 -2
  194. pyglet/media/codecs/ffmpeg_lib/libavformat.py +3 -8
  195. pyglet/media/codecs/webaudio_pyodide.py +111 -0
  196. pyglet/media/drivers/__init__.py +9 -4
  197. pyglet/media/drivers/base.py +4 -4
  198. pyglet/media/drivers/openal/__init__.py +1 -1
  199. pyglet/media/drivers/openal/adaptation.py +3 -3
  200. pyglet/media/drivers/pulse/__init__.py +1 -1
  201. pyglet/media/drivers/pulse/adaptation.py +3 -3
  202. pyglet/media/drivers/pyodide_js/__init__.py +8 -0
  203. pyglet/media/drivers/pyodide_js/adaptation.py +288 -0
  204. pyglet/media/drivers/xaudio2/adaptation.py +3 -3
  205. pyglet/media/player.py +276 -193
  206. pyglet/media/player_worker_thread.py +1 -1
  207. pyglet/model/__init__.py +39 -29
  208. pyglet/model/codecs/base.py +4 -4
  209. pyglet/model/codecs/gltf.py +3 -3
  210. pyglet/model/codecs/obj.py +71 -43
  211. pyglet/resource.py +129 -78
  212. pyglet/shapes.py +147 -177
  213. pyglet/sprite.py +47 -164
  214. pyglet/text/__init__.py +44 -54
  215. pyglet/text/caret.py +12 -7
  216. pyglet/text/document.py +19 -17
  217. pyglet/text/formats/html.py +2 -2
  218. pyglet/text/formats/structured.py +10 -40
  219. pyglet/text/layout/__init__.py +20 -13
  220. pyglet/text/layout/base.py +176 -287
  221. pyglet/text/layout/incremental.py +9 -10
  222. pyglet/text/layout/scrolling.py +7 -95
  223. pyglet/window/__init__.py +183 -172
  224. pyglet/window/cocoa/__init__.py +62 -51
  225. pyglet/window/cocoa/pyglet_delegate.py +2 -25
  226. pyglet/window/cocoa/pyglet_view.py +9 -8
  227. pyglet/window/dialog/__init__.py +184 -0
  228. pyglet/window/dialog/base.py +99 -0
  229. pyglet/window/dialog/darwin.py +121 -0
  230. pyglet/window/dialog/linux.py +72 -0
  231. pyglet/window/dialog/windows.py +194 -0
  232. pyglet/window/emscripten/__init__.py +779 -0
  233. pyglet/window/headless/__init__.py +44 -28
  234. pyglet/window/key.py +2 -0
  235. pyglet/window/mouse.py +2 -2
  236. pyglet/window/wayland/__init__.py +377 -0
  237. pyglet/window/win32/__init__.py +101 -46
  238. pyglet/window/xlib/__init__.py +104 -66
  239. {pyglet-2.1.12.dist-info → pyglet-3.0.dev1.dist-info}/METADATA +2 -3
  240. pyglet-3.0.dev1.dist-info/RECORD +322 -0
  241. {pyglet-2.1.12.dist-info → pyglet-3.0.dev1.dist-info}/WHEEL +1 -1
  242. pyglet/gl/__init__.py +0 -208
  243. pyglet/gl/base.py +0 -499
  244. pyglet/gl/cocoa.py +0 -309
  245. pyglet/gl/gl.py +0 -4625
  246. pyglet/gl/gl.pyi +0 -2320
  247. pyglet/gl/gl_compat.pyi +0 -3097
  248. pyglet/gl/gl_info.py +0 -190
  249. pyglet/gl/headless.py +0 -166
  250. pyglet/gl/wgl_info.py +0 -36
  251. pyglet/gl/wglext_nv.py +0 -1096
  252. pyglet/gl/win32.py +0 -268
  253. pyglet/gl/xlib.py +0 -295
  254. pyglet/image/buffer.py +0 -274
  255. pyglet/image/codecs/s3tc.py +0 -354
  256. pyglet/libs/x11/xrandr.py +0 -166
  257. pyglet-2.1.12.dist-info/RECORD +0 -234
  258. /pyglet/{libs/wayland → graphics/api/gl/cocoa}/__init__.py +0 -0
  259. /pyglet/libs/{egl → linux/egl}/egl.py +0 -0
  260. /pyglet/libs/{egl → linux/egl}/lib.py +0 -0
  261. /pyglet/libs/{ioctl.py → linux/ioctl.py} +0 -0
  262. /pyglet/libs/{wayland → linux/wayland}/gbm.py +0 -0
  263. /pyglet/libs/{x11 → linux/x11}/__init__.py +0 -0
  264. /pyglet/libs/{x11 → linux/x11}/cursorfont.py +0 -0
  265. /pyglet/libs/{x11 → linux/x11}/xlib.py +0 -0
  266. /pyglet/libs/{x11 → linux/x11}/xsync.py +0 -0
  267. {pyglet-2.1.12.dist-info/licenses → pyglet-3.0.dev1.dist-info}/LICENSE +0 -0
pyglet/media/player.py CHANGED
@@ -3,20 +3,18 @@ from __future__ import annotations
3
3
 
4
4
  import time
5
5
  from collections import deque
6
-
7
- from typing import TYPE_CHECKING, Iterable, Generator
6
+ from typing import TYPE_CHECKING, Generator, Iterable
8
7
 
9
8
  import pyglet
10
-
11
- from pyglet.gl import GL_TEXTURE_2D
12
- from pyglet.media import buffered_logger as bl
13
- from pyglet.media.drivers import get_audio_driver
14
9
  from pyglet.media.codecs.base import Source, SourceGroup
10
+ from pyglet.media.drivers import get_audio_driver
11
+ from pyglet.media.exceptions import MediaException
12
+ from pyglet.util import debug_print
15
13
 
16
- _debug = pyglet.options['debug_media']
14
+ _debug = debug_print('debug_media')
17
15
 
18
16
  if TYPE_CHECKING:
19
- from pyglet.image import Texture
17
+ from pyglet.graphics import Texture
20
18
 
21
19
 
22
20
  class PlaybackTimer:
@@ -54,8 +52,7 @@ class PlaybackTimer:
54
52
  return time.perf_counter() - self._started_at + self._elapsed
55
53
 
56
54
  def set_time(self, value: float) -> None:
57
- """
58
- Manually set the elapsed time.
55
+ """Manually set the elapsed time.
59
56
 
60
57
  Args:
61
58
  value (float): the new elapsed time value
@@ -94,21 +91,21 @@ class _PlayerProperty:
94
91
  getattr(obj._audio_player, self.setter_name)(value)
95
92
 
96
93
 
97
- class Player(pyglet.event.EventDispatcher):
98
- """High-level sound and video player."""
94
+ class AudioPlayer(pyglet.event.EventDispatcher):
95
+ """High-level sound player."""
99
96
 
100
97
  # Specialisation attributes, preserved between audio players
101
98
  _volume = 1.0
102
99
  _min_distance = 1.0
103
- _max_distance = 100000000.
100
+ _max_distance = 100000000.0
104
101
 
105
102
  _position = (0, 0, 0)
106
103
  _pitch = 1.0
107
104
 
108
105
  _cone_orientation = (0, 0, 1)
109
- _cone_inner_angle = 360.
110
- _cone_outer_angle = 360.
111
- _cone_outer_gain = 1.
106
+ _cone_inner_angle = 360.0
107
+ _cone_outer_angle = 360.0
108
+ _cone_outer_gain = 1.0
112
109
 
113
110
  def __init__(self) -> None:
114
111
  """Initialize the Player with a MasterClock."""
@@ -116,9 +113,6 @@ class Player(pyglet.event.EventDispatcher):
116
113
  self._playlists = deque()
117
114
  self._audio_player = None
118
115
 
119
- self._context = pyglet.gl.current_context
120
- self._texture = None
121
-
122
116
  # Desired play state (not an indication of actual state).
123
117
  self._playing = False
124
118
 
@@ -149,8 +143,7 @@ class Player(pyglet.event.EventDispatcher):
149
143
  try:
150
144
  source = iter(source)
151
145
  except TypeError:
152
- raise TypeError("source must be either a Source or an iterable."
153
- " Received type {0}".format(type(source)))
146
+ raise TypeError(f"source must be either a Source or an iterable. Received type {type(source)}")
154
147
  self._playlists.append(source)
155
148
 
156
149
  if self.source is None:
@@ -164,6 +157,41 @@ class Player(pyglet.event.EventDispatcher):
164
157
  else:
165
158
  self._source = new_source.get_queue_source()
166
159
 
160
+ def _check_resource_reuse(self, old_source: Source) -> None:
161
+ """Determine if a resource can be re-used based on the old source."""
162
+ if self._audio_player is not None:
163
+ # If current source format properties match the old source, then the resource can be re-used.
164
+ if self._source.audio_format == old_source.audio_format:
165
+ self._audio_player.set_source(self._source)
166
+ else:
167
+ # Delete the resource so a matching resource can be created.
168
+ self._audio_player.delete()
169
+ self._audio_player = None
170
+
171
+ def _create_player_resources(self, starting: bool) -> None:
172
+ """Creates driver resources for the particular format."""
173
+ if self._source.audio_format is not None:
174
+ if was_created := self._audio_player is None:
175
+ self._create_audio_player()
176
+ if self._audio_player is not None and (was_created or starting):
177
+ self._audio_player.prefill_audio()
178
+
179
+ def _start_player_resources(self) -> None:
180
+ """Start any driver related resources required for playback."""
181
+ if self._audio_player is not None:
182
+ self._audio_player.play()
183
+
184
+ def _stop_player_resources(self) -> None:
185
+ """Stop any driver related resources required for playback."""
186
+ if self._audio_player is not None:
187
+ self._audio_player.stop()
188
+
189
+ def _seek_player_resources(self) -> None:
190
+ if self._audio_player is not None:
191
+ # XXX: According to docstring in AbstractAudioPlayer this cannot
192
+ # be called when the player is not stopped
193
+ self._audio_player.clear()
194
+
167
195
  def _set_playing(self, playing: bool) -> None:
168
196
  # stopping = self._playing and not playing
169
197
  starting = not self._playing and playing
@@ -172,44 +200,22 @@ class Player(pyglet.event.EventDispatcher):
172
200
  source = self.source
173
201
 
174
202
  if playing and source:
175
- if source.audio_format is not None:
176
- if (was_created := self._audio_player is None):
177
- self._create_audio_player()
178
- if self._audio_player is not None and (was_created or starting):
179
- self._audio_player.prefill_audio()
180
-
181
- if bl.logger is not None:
182
- bl.logger.init_wall_time()
183
- bl.logger.log("p.P._sp", 0.0)
184
-
185
- if source.video_format is not None:
186
- if self._texture is None:
187
- self._create_texture()
188
-
189
- if self._audio_player is not None:
190
- self._audio_player.play()
191
- if source.video_format is not None:
192
- pyglet.clock.schedule_once(self.update_texture, 0)
193
- # For audio synchronization tests, the following will
194
- # add a delay to de-synchronize the audio.
195
- # Negative number means audio runs ahead.
196
- # self._mclock._systime += -0.3
203
+ self._create_player_resources(starting)
204
+ self._start_player_resources()
197
205
  self._timer.start()
206
+
207
+ # If the driver is silent, it will not create any players, dispatch an end event.
198
208
  if self._audio_player is None and source.video_format is None:
199
209
  pyglet.clock.schedule_once(lambda dt: self.dispatch_event("on_eos"), source.duration)
200
-
201
210
  else:
202
- if self._audio_player is not None:
203
- self._audio_player.stop()
204
-
205
- pyglet.clock.unschedule(self.update_texture)
211
+ self._stop_player_resources()
206
212
  self._timer.pause()
207
213
 
208
214
  @property
209
215
  def playing(self) -> bool:
210
216
  """The current playing state.
211
217
 
212
- The *playing* property is irrespective of whether or not there is
218
+ The *playing* property is irrespective of whether there is
213
219
  actually a source to play. If *playing* is ``True`` and a source is
214
220
  queued, it will begin to play immediately. If *playing* is ``False``,
215
221
  it is implied that the player is paused. There is no other possible
@@ -241,8 +247,6 @@ class Player(pyglet.event.EventDispatcher):
241
247
  if self._audio_player is not None:
242
248
  self._audio_player.delete()
243
249
  self._audio_player = None
244
- if self._texture is not None:
245
- self._texture = None
246
250
 
247
251
  def next_source(self) -> None:
248
252
  """Move immediately to the next source in the current playlist.
@@ -285,15 +289,7 @@ class Player(pyglet.event.EventDispatcher):
285
289
  old_source = self._source
286
290
  self._set_source(new_source)
287
291
 
288
- if self._audio_player is not None:
289
- if self._source.audio_format == old_source.audio_format:
290
- self._audio_player.set_source(self._source)
291
- else:
292
- self._audio_player.delete()
293
- self._audio_player = None
294
- if self._source.video_format != old_source.video_format:
295
- self._texture = None
296
- pyglet.clock.unschedule(self.update_texture)
292
+ self._check_resource_reuse(old_source)
297
293
 
298
294
  del old_source
299
295
 
@@ -312,9 +308,6 @@ class Player(pyglet.event.EventDispatcher):
312
308
  if self.source is None:
313
309
  return
314
310
 
315
- if bl.logger is not None:
316
- bl.logger.log("p.P.sk", timestamp)
317
-
318
311
  timestamp = max(timestamp, 0.0)
319
312
  if self._source.duration is not None:
320
313
  # TODO: If the duration is reported as None and the source clamps anyways,
@@ -326,13 +319,8 @@ class Player(pyglet.event.EventDispatcher):
326
319
  self._source.seek(timestamp)
327
320
  self.last_seek_time = timestamp
328
321
 
329
- if self._audio_player is not None:
330
- # XXX: According to docstring in AbstractAudioPlayer this cannot
331
- # be called when the player is not stopped
332
- self._audio_player.clear()
333
- if self.source.video_format is not None:
334
- self.update_texture()
335
- pyglet.clock.unschedule(self.update_texture)
322
+ self._seek_player_resources()
323
+
336
324
  self._set_playing(playing)
337
325
 
338
326
  def _create_audio_player(self) -> None:
@@ -348,9 +336,17 @@ class Player(pyglet.event.EventDispatcher):
348
336
  self._audio_player = audio_driver.create_audio_player(source, self)
349
337
 
350
338
  # Set the audio player attributes
351
- for attr in ('volume', 'min_distance', 'max_distance', 'position',
352
- 'pitch', 'cone_orientation', 'cone_inner_angle',
353
- 'cone_outer_angle', 'cone_outer_gain'):
339
+ for attr in (
340
+ 'volume',
341
+ 'min_distance',
342
+ 'max_distance',
343
+ 'position',
344
+ 'pitch',
345
+ 'cone_orientation',
346
+ 'cone_inner_angle',
347
+ 'cone_outer_angle',
348
+ 'cone_outer_gain',
349
+ ):
354
350
  value = getattr(self, attr)
355
351
  setattr(self, attr, value)
356
352
 
@@ -370,107 +366,6 @@ class Player(pyglet.event.EventDispatcher):
370
366
  """
371
367
  return self._timer.get_time()
372
368
 
373
- def _create_texture(self) -> None:
374
- video_format = self.source.video_format
375
- self._texture = pyglet.image.Texture.create(video_format.width, video_format.height, GL_TEXTURE_2D)
376
- self._texture = self._texture.get_transform(flip_y=True)
377
- # After flipping the texture along the y axis, the anchor_y is set
378
- # to the top of the image. We want to keep it at the bottom.
379
- self._texture.anchor_y = 0
380
- return self._texture
381
-
382
- @property
383
- def texture(self) -> Texture | None:
384
- """Get the texture for the current video frame, if any.
385
-
386
- You should query this property every time you display a frame of video,
387
- as multiple textures might be used. This property will be ``None`` if
388
- the current Source does not contain video.
389
- """
390
- return self._texture
391
-
392
- def seek_next_frame(self) -> None:
393
- """Step forwards one video frame in the current source."""
394
- time = self.source.get_next_video_timestamp()
395
- if time is None:
396
- return
397
- self.seek(time)
398
-
399
- def update_texture(self, dt: float | None = None) -> None:
400
- """Manually update the texture from the current source.
401
-
402
- This happens automatically, so you shouldn't need to call this method.
403
-
404
- Args:
405
- dt:
406
- The time elapsed since the last call to ``update_texture``.
407
- """
408
- # self.pr.disable()
409
- # if dt > 0.05:
410
- # print("update_texture dt:", dt)
411
- # import pstats
412
- # ps = pstats.Stats(self.pr).sort_stats("cumulative")
413
- # ps.print_stats()
414
- source = self.source
415
- time = self.time
416
- if bl.logger is not None:
417
- bl.logger.log(
418
- "p.P.ut.1.0", dt, time,
419
- self._audio_player.get_time() if self._audio_player else 0,
420
- bl.logger.rebased_wall_time()
421
- )
422
-
423
- frame_rate = source.video_format.frame_rate
424
- frame_duration = 1 / frame_rate
425
- ts = source.get_next_video_timestamp()
426
- # Allow up to frame_duration difference
427
- while ts is not None and ts + frame_duration < time:
428
- source.get_next_video_frame() # Discard frame
429
- if bl.logger is not None:
430
- bl.logger.log("p.P.ut.1.5", ts)
431
- ts = source.get_next_video_timestamp()
432
-
433
- if bl.logger is not None:
434
- bl.logger.log("p.P.ut.1.6", ts)
435
-
436
- if ts is None:
437
- # No more video frames to show. End of video stream.
438
- if bl.logger is not None:
439
- bl.logger.log("p.P.ut.1.7", frame_duration)
440
-
441
- pyglet.clock.schedule_once(self._video_finished, 0)
442
- return
443
- elif ts > time:
444
- # update_texture called too early (probably manually!)
445
- pyglet.clock.schedule_once(self.update_texture, ts - time)
446
- return
447
-
448
-
449
- image = source.get_next_video_frame()
450
- if image is not None:
451
- with self._context:
452
- if self._texture is None:
453
- self._create_texture()
454
- self._texture.blit_into(image, 0, 0, 0)
455
- elif bl.logger is not None:
456
- bl.logger.log("p.P.ut.1.8")
457
-
458
- ts = source.get_next_video_timestamp()
459
- if ts is None:
460
- delay = frame_duration
461
- else:
462
- delay = ts - time
463
-
464
- delay = max(0.0, delay)
465
- if bl.logger is not None:
466
- bl.logger.log("p.P.ut.1.9", delay, ts)
467
- pyglet.clock.schedule_once(self.update_texture, delay)
468
- # self.pr.enable()
469
-
470
- def _video_finished(self, _dt: float) -> None:
471
- if self._audio_player is None:
472
- self.dispatch_event("on_eos")
473
-
474
369
  volume = _PlayerProperty('volume', doc="""
475
370
  The volume level of sound playback.
476
371
 
@@ -546,8 +441,7 @@ class Player(pyglet.event.EventDispatcher):
546
441
 
547
442
  def on_player_eos(self):
548
443
  """The player ran out of sources. The playlist is empty."""
549
- if _debug:
550
- print('Player.on_player_eos')
444
+ assert _debug('Player.on_player_eos')
551
445
 
552
446
  def on_eos(self):
553
447
  """The current source ran out of data.
@@ -558,11 +452,7 @@ class Player(pyglet.event.EventDispatcher):
558
452
  will start to play again until :meth:`next_source` is called or
559
453
  :attr:`.loop` is set to ``False``.
560
454
  """
561
- if _debug:
562
- print('Player.on_eos')
563
- if bl.logger is not None:
564
- bl.logger.log("p.P.oe")
565
- bl.logger.close()
455
+ assert _debug('Player.on_eos')
566
456
 
567
457
  if self.loop:
568
458
  was_playing = self._playing
@@ -587,7 +477,7 @@ class Player(pyglet.event.EventDispatcher):
587
477
  def on_driver_reset(self):
588
478
  """The audio driver has been reset.
589
479
 
590
- By default this will kill the current audio player, create a new one,
480
+ By default, this will kill the current audio player, create a new one,
591
481
  and requeue the buffers. Any buffers that may have been queued in a
592
482
  player will be resubmitted. It will continue from the last buffers
593
483
  submitted, not played, and may cause sync issues if using video.
@@ -598,8 +488,17 @@ class Player(pyglet.event.EventDispatcher):
598
488
  self._audio_player.on_driver_reset()
599
489
 
600
490
  # Voice has been changed, will need to reset all options on the voice.
601
- for attr in ('volume', 'min_distance', 'max_distance', 'position', 'pitch',
602
- 'cone_orientation', 'cone_inner_angle', 'cone_outer_angle', 'cone_outer_gain'):
491
+ for attr in (
492
+ 'volume',
493
+ 'min_distance',
494
+ 'max_distance',
495
+ 'position',
496
+ 'pitch',
497
+ 'cone_orientation',
498
+ 'cone_inner_angle',
499
+ 'cone_outer_angle',
500
+ 'cone_outer_gain',
501
+ ):
603
502
  value = getattr(self, attr)
604
503
  setattr(self, attr, value)
605
504
 
@@ -607,16 +506,196 @@ class Player(pyglet.event.EventDispatcher):
607
506
  self._audio_player.play()
608
507
 
609
508
 
610
- Player.register_event_type('on_eos')
611
- Player.register_event_type('on_player_eos')
612
- Player.register_event_type('on_player_next_source')
613
- Player.register_event_type('on_driver_reset')
509
+ AudioPlayer.register_event_type('on_eos')
510
+ AudioPlayer.register_event_type('on_player_eos')
511
+ AudioPlayer.register_event_type('on_player_next_source')
512
+ AudioPlayer.register_event_type('on_driver_reset')
614
513
 
615
514
 
616
515
  def _one_item_playlist(source: Source) -> Generator:
617
516
  yield source
618
517
 
619
518
 
519
+ class VideoPlayer(AudioPlayer):
520
+ """High-level sound and video player."""
521
+ _sprite: pyglet.sprite.Sprite | None
522
+
523
+ def __init__(self, batch: pyglet.graphics.Batch | None = None) -> None:
524
+ super().__init__()
525
+ self._texture = None
526
+ self._sprite = None
527
+ self._position = (0, 0)
528
+ self._width = 0
529
+ self._height = 0
530
+ self.batch = batch or pyglet.graphics.get_default_batch()
531
+ self._context = batch._context # noqa: SLF001
532
+
533
+ self._check_ffmpeg_availability()
534
+
535
+ @staticmethod
536
+ def _check_ffmpeg_availability() -> None:
537
+ # Check if a source is FFmpeg instead?
538
+ from pyglet.media import have_ffmpeg
539
+ if not have_ffmpeg():
540
+ raise MediaException("VideoPlayer requires FFmpeg to be installed.")
541
+
542
+ @property
543
+ def position(self) -> tuple[int, int]:
544
+ """Get the position of the video player."""
545
+ return self._position
546
+
547
+ @position.setter
548
+ def position(self, value: tuple[int, int]) -> None:
549
+ self._position = value
550
+ if self._sprite:
551
+ self._sprite.position = (*value, 0)
552
+
553
+ @property
554
+ def width(self) -> int:
555
+ """Get the width of the video player."""
556
+ return self._width
557
+
558
+ @width.setter
559
+ def width(self, width: int) -> None:
560
+ self._width = width
561
+ if self._sprite:
562
+ self._sprite.width = width
563
+
564
+ @property
565
+ def height(self) -> int:
566
+ """Get the height of the video player."""
567
+ return self._height
568
+
569
+ @height.setter
570
+ def height(self, height: int) -> None:
571
+ self._height = height
572
+ if self._sprite:
573
+ self._sprite.height = height
574
+
575
+ def _seek_player_resources(self) -> None:
576
+ super()._seek_player_resources()
577
+ if self.source.video_format is not None:
578
+ self.update_texture()
579
+ pyglet.clock.unschedule(self.update_texture)
580
+
581
+ def _check_resource_reuse(self, old_source: Source) -> None:
582
+ super()._check_resource_reuse(old_source)
583
+ if self._source.video_format != old_source.video_format:
584
+ pyglet.clock.unschedule(self.update_texture)
585
+ self._texture = None
586
+ self._sprite = None
587
+
588
+ def _create_player_resources(self, starting: bool) -> None:
589
+ super()._create_player_resources(starting)
590
+ if self._source.video_format is not None:
591
+ if self._texture is None:
592
+ with self._context:
593
+ self._create_texture()
594
+ self._create_sprite()
595
+
596
+ def _create_texture(self) -> pyglet.graphics.Texture:
597
+ video_format = self.source.video_format
598
+ self._texture = pyglet.graphics.Texture.create(video_format.width, video_format.height)
599
+
600
+ # Flip coordinates so output is correct orientation.
601
+ self._texture = self._texture.get_transform(flip_y=True)
602
+ # After flipping, the anchor_y is set to the top of the image. We want to keep it at the bottom.
603
+ self._texture.anchor_y = 0
604
+ return self._texture
605
+
606
+ def _create_sprite(self) -> None:
607
+ if not self._sprite:
608
+ self._sprite = pyglet.sprite.Sprite(self._texture, x=self._position[0], y=self._position[1], batch=self.batch)
609
+
610
+ def _start_player_resources(self) -> None:
611
+ super()._start_player_resources()
612
+ if self._source.video_format is not None:
613
+ pyglet.clock.schedule_once(self.update_texture, 0)
614
+
615
+ def _stop_player_resources(self):
616
+ super()._stop_player_resources()
617
+ pyglet.clock.unschedule(self.update_texture)
618
+
619
+ def draw(self):
620
+ assert pyglet.graphics.api.core.current_context == self._context, "Drawing in the wrong context."
621
+ if self._source:
622
+ self.batch.draw()
623
+
624
+ def update_texture(self, dt: float | None = None) -> None:
625
+ """Manually update the texture from the current source.
626
+
627
+ This happens automatically, so you shouldn't need to call this method.
628
+
629
+ Args:
630
+ dt:
631
+ The time elapsed since the last call to ``update_texture``.
632
+ """
633
+ source = self.source
634
+ _time = self.time
635
+
636
+ frame_rate = source.video_format.frame_rate
637
+ frame_duration = 1 / frame_rate
638
+ ts = source.get_next_video_timestamp()
639
+ # Allow up to frame_duration difference
640
+ while ts is not None and ts + frame_duration < _time:
641
+ source.get_next_video_frame() # Discard frame
642
+ ts = source.get_next_video_timestamp()
643
+
644
+ if ts is None:
645
+ # No more video frames to show. End of video stream.
646
+ pyglet.clock.schedule_once(self._video_finished, 0)
647
+ return
648
+ if ts > _time:
649
+ # update_texture called too early (probably manually!)
650
+ pyglet.clock.schedule_once(self.update_texture, ts - _time)
651
+ return
652
+
653
+ image = source.get_next_video_frame()
654
+ if image is not None:
655
+ with self._context:
656
+ if self._texture is None:
657
+ self._create_texture()
658
+ self._texture.bind()
659
+ self._texture.upload(image, 0, 0, 0)
660
+
661
+ ts = source.get_next_video_timestamp()
662
+ if ts is None:
663
+ delay = frame_duration
664
+ else:
665
+ delay = ts - _time
666
+
667
+ delay = max(0.0, delay)
668
+ pyglet.clock.schedule_once(self.update_texture, delay)
669
+
670
+ def _video_finished(self, _dt: float) -> None:
671
+ if self._audio_player is None:
672
+ self.dispatch_event("on_eos")
673
+
674
+ @property
675
+ def texture(self) -> Texture | None:
676
+ """Get the texture for the current video frame, if any.
677
+
678
+ You should query this property every time you display a frame of video,
679
+ as multiple textures might be used. This property will be ``None`` if
680
+ the current Source does not contain video.
681
+ """
682
+ return self._texture
683
+
684
+ def seek_next_frame(self) -> None:
685
+ """Step forwards one video frame in the current source."""
686
+ ts = self.source.get_next_video_timestamp()
687
+ if ts is None:
688
+ return
689
+ self.seek(ts)
690
+
691
+ def delete(self) -> None:
692
+ super().delete()
693
+ if self._texture is not None:
694
+ self._texture = None
695
+ if self._sprite is not None:
696
+ self._sprite = None
697
+
698
+
620
699
  class PlayerGroup:
621
700
  """Group of players that can be played and paused simultaneously.
622
701
 
@@ -625,14 +704,13 @@ class PlayerGroup:
625
704
  All players in the group must currently not belong to any other group.
626
705
  """
627
706
 
628
- def __init__(self, players: Iterable[Player]) -> None:
707
+ def __init__(self, players: Iterable[AudioPlayer]) -> None:
629
708
  """Initialize the PlayerGroup with the players."""
630
709
  self.players = list(players)
631
710
 
632
711
  def play(self) -> None:
633
712
  """Begin playing all players in the group simultaneously."""
634
- audio_players = [p._audio_player
635
- for p in self.players if p._audio_player]
713
+ audio_players = [p._audio_player for p in self.players if p._audio_player]
636
714
  if audio_players:
637
715
  audio_players[0]._play_group(audio_players)
638
716
  for player in self.players:
@@ -640,9 +718,14 @@ class PlayerGroup:
640
718
 
641
719
  def pause(self) -> None:
642
720
  """Pause all players in the group simultaneously."""
643
- audio_players = [p._audio_player
644
- for p in self.players if p._audio_player]
721
+ audio_players = [p._audio_player for p in self.players if p._audio_player]
645
722
  if audio_players:
646
723
  audio_players[0]._stop_group(audio_players)
647
724
  for player in self.players:
648
725
  player.pause()
726
+
727
+
728
+ AudioPlayer.register_event_type('on_eos')
729
+ AudioPlayer.register_event_type('on_player_eos')
730
+ AudioPlayer.register_event_type('on_player_next_source')
731
+ AudioPlayer.register_event_type('on_driver_reset')
@@ -44,7 +44,7 @@ class PlayerWorkerThread(threading.Thread):
44
44
  self.players: Set[AbstractAudioPlayer] = set()
45
45
 
46
46
  def run(self) -> None:
47
- if pyglet.options['debug_trace']:
47
+ if pyglet.options.debug_trace:
48
48
  pyglet._install_trace()
49
49
 
50
50
  sleep_time = None