vsjetengine 1.0.0__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.
- vsengine/__init__.py +28 -0
- vsengine/_futures.py +372 -0
- vsengine/_helpers.py +34 -0
- vsengine/_hospice.py +121 -0
- vsengine/_nodes.py +116 -0
- vsengine/_version.py +2 -0
- vsengine/adapters/__init__.py +6 -0
- vsengine/adapters/asyncio.py +85 -0
- vsengine/adapters/trio.py +107 -0
- vsengine/loops.py +269 -0
- vsengine/policy.py +395 -0
- vsengine/py.typed +0 -0
- vsengine/video.py +180 -0
- vsengine/vpy.py +441 -0
- vsjetengine-1.0.0.dist-info/METADATA +350 -0
- vsjetengine-1.0.0.dist-info/RECORD +18 -0
- vsjetengine-1.0.0.dist-info/WHEEL +4 -0
- vsjetengine-1.0.0.dist-info/licenses/COPYING +287 -0
vsengine/policy.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# vs-engine
|
|
2
|
+
# Copyright (C) 2022 cid-chan
|
|
3
|
+
# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
|
|
4
|
+
# This project is licensed under the EUPL-1.2
|
|
5
|
+
# SPDX-License-Identifier: EUPL-1.2
|
|
6
|
+
"""
|
|
7
|
+
This module implements VapourSynth's Environment Policy system,
|
|
8
|
+
allowing you to manage multiple VapourSynth environments within a single application.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import threading
|
|
14
|
+
from abc import ABC, abstractmethod
|
|
15
|
+
from collections.abc import Iterator
|
|
16
|
+
from contextlib import AbstractContextManager, contextmanager
|
|
17
|
+
from contextvars import ContextVar
|
|
18
|
+
from logging import getLogger
|
|
19
|
+
from types import MappingProxyType, TracebackType
|
|
20
|
+
from typing import TYPE_CHECKING, Self
|
|
21
|
+
from weakref import ReferenceType, ref
|
|
22
|
+
|
|
23
|
+
from vapoursynth import (
|
|
24
|
+
AudioNode,
|
|
25
|
+
Core,
|
|
26
|
+
Environment,
|
|
27
|
+
EnvironmentData,
|
|
28
|
+
EnvironmentPolicy,
|
|
29
|
+
EnvironmentPolicyAPI,
|
|
30
|
+
VideoOutputTuple,
|
|
31
|
+
core,
|
|
32
|
+
get_outputs,
|
|
33
|
+
register_policy,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from ._hospice import admit_environment
|
|
37
|
+
|
|
38
|
+
__all__ = ["ContextVarStore", "GlobalStore", "ManagedEnvironment", "Policy", "ThreadLocalStore"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
logger = getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class EnvironmentStore(ABC):
|
|
45
|
+
"""
|
|
46
|
+
Environment Stores manage which environment is currently active.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
@abstractmethod
|
|
50
|
+
def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Set the current environment in the store.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def get_current_environment(self) -> ReferenceType[EnvironmentData] | None:
|
|
57
|
+
"""
|
|
58
|
+
Retrieve the current environment from the store (if any)
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class GlobalStore(EnvironmentStore):
|
|
63
|
+
"""
|
|
64
|
+
This is the simplest store: It just stores the environment in a variable.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
_current: ReferenceType[EnvironmentData] | None
|
|
68
|
+
__slots__ = ("_current",)
|
|
69
|
+
|
|
70
|
+
def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None:
|
|
71
|
+
self._current = environment
|
|
72
|
+
|
|
73
|
+
def get_current_environment(self) -> ReferenceType[EnvironmentData] | None:
|
|
74
|
+
return getattr(self, "_current", None)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ThreadLocalStore(EnvironmentStore):
|
|
78
|
+
"""
|
|
79
|
+
For simple threaded applications, use this store.
|
|
80
|
+
|
|
81
|
+
It will store the environment in a thread-local variable.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
_current: threading.local
|
|
85
|
+
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
self._current = threading.local()
|
|
88
|
+
|
|
89
|
+
def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None:
|
|
90
|
+
self._current.environment = environment
|
|
91
|
+
|
|
92
|
+
def get_current_environment(self) -> ReferenceType[EnvironmentData] | None:
|
|
93
|
+
return getattr(self._current, "environment", None)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ContextVarStore(EnvironmentStore):
|
|
97
|
+
"""
|
|
98
|
+
If you are using AsyncIO or similar frameworks, use this store.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
_current: ContextVar[ReferenceType[EnvironmentData] | None]
|
|
102
|
+
|
|
103
|
+
def __init__(self, name: str = "vapoursynth") -> None:
|
|
104
|
+
self._current = ContextVar(name)
|
|
105
|
+
|
|
106
|
+
def set_current_environment(self, environment: ReferenceType[EnvironmentData] | None) -> None:
|
|
107
|
+
self._current.set(environment)
|
|
108
|
+
|
|
109
|
+
def get_current_environment(self) -> ReferenceType[EnvironmentData] | None:
|
|
110
|
+
return self._current.get(None)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class _ManagedPolicy(EnvironmentPolicy):
|
|
114
|
+
"""
|
|
115
|
+
This class directly interfaces with VapourSynth.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
__slots__ = ("_api", "_local", "_mutex", "_store")
|
|
119
|
+
|
|
120
|
+
def __init__(self, store: EnvironmentStore) -> None:
|
|
121
|
+
self._store = store
|
|
122
|
+
self._mutex = threading.Lock()
|
|
123
|
+
self._local = threading.local()
|
|
124
|
+
|
|
125
|
+
# For engine-calls that require vapoursynth but
|
|
126
|
+
# should not make their switch observable from the outside.
|
|
127
|
+
|
|
128
|
+
# Start the section.
|
|
129
|
+
def inline_section_start(self, environment: EnvironmentData) -> None:
|
|
130
|
+
self._local.environment = environment
|
|
131
|
+
|
|
132
|
+
# End the section.
|
|
133
|
+
def inline_section_end(self) -> None:
|
|
134
|
+
del self._local.environment
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def api(self) -> EnvironmentPolicyAPI:
|
|
138
|
+
if hasattr(self, "_api"):
|
|
139
|
+
return self._api
|
|
140
|
+
|
|
141
|
+
raise RuntimeError("Invalid state: No access to the current API")
|
|
142
|
+
|
|
143
|
+
def on_policy_registered(self, special_api: EnvironmentPolicyAPI) -> None:
|
|
144
|
+
self._api = special_api
|
|
145
|
+
logger.debug("Environment policy %r successfully registered with VapourSynth.", special_api)
|
|
146
|
+
|
|
147
|
+
def on_policy_cleared(self) -> None:
|
|
148
|
+
del self._api
|
|
149
|
+
logger.debug("Environment policy successfully cleared.")
|
|
150
|
+
|
|
151
|
+
def get_current_environment(self) -> EnvironmentData | None:
|
|
152
|
+
# For small segments, allow switching the environment inline.
|
|
153
|
+
# This is useful for vsengine-functions that require access to the
|
|
154
|
+
# vapoursynth api, but don't want to invoke the store for it.
|
|
155
|
+
if (env := getattr(self._local, "environment", None)) is not None and self.is_alive(env):
|
|
156
|
+
return env
|
|
157
|
+
|
|
158
|
+
# We wrap everything in a mutex to make sure
|
|
159
|
+
# no context-switch can reliably happen in this section.
|
|
160
|
+
with self._mutex:
|
|
161
|
+
current_environment = self._store.get_current_environment()
|
|
162
|
+
if current_environment is None:
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
if current_environment() is None:
|
|
166
|
+
logger.warning("Environment reference from store resolved to dead object: %r", current_environment)
|
|
167
|
+
self._store.set_current_environment(None)
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
received_environment = current_environment()
|
|
171
|
+
|
|
172
|
+
if TYPE_CHECKING:
|
|
173
|
+
assert received_environment
|
|
174
|
+
|
|
175
|
+
if not self.is_alive(received_environment):
|
|
176
|
+
logger.warning(
|
|
177
|
+
"Received environment object is not alive (Garbage collected?): %r",
|
|
178
|
+
received_environment,
|
|
179
|
+
)
|
|
180
|
+
# Remove the environment.
|
|
181
|
+
self._store.set_current_environment(None)
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
return received_environment
|
|
185
|
+
|
|
186
|
+
def set_environment(self, environment: EnvironmentData | None) -> EnvironmentData | None:
|
|
187
|
+
with self._mutex:
|
|
188
|
+
previous_environment = self._store.get_current_environment()
|
|
189
|
+
|
|
190
|
+
if environment is not None and not self.is_alive(environment):
|
|
191
|
+
logger.warning("Attempted to set environment which is not alive: %r", environment)
|
|
192
|
+
self._store.set_current_environment(None)
|
|
193
|
+
else:
|
|
194
|
+
logger.debug("Environment successfully set to: %r", environment)
|
|
195
|
+
if environment is None:
|
|
196
|
+
self._store.set_current_environment(None)
|
|
197
|
+
else:
|
|
198
|
+
self._store.set_current_environment(ref(environment))
|
|
199
|
+
|
|
200
|
+
if previous_environment is not None:
|
|
201
|
+
return previous_environment()
|
|
202
|
+
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ManagedEnvironment(AbstractContextManager["ManagedEnvironment"]):
|
|
207
|
+
"""
|
|
208
|
+
Represents a VapourSynth environment that is managed by a policy.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
__slots__ = ("_data", "_environment", "_policy")
|
|
212
|
+
|
|
213
|
+
def __init__(self, environment: Environment, data: EnvironmentData, policy: Policy) -> None:
|
|
214
|
+
self._environment = environment
|
|
215
|
+
self._data = data
|
|
216
|
+
self._policy = policy
|
|
217
|
+
|
|
218
|
+
def __enter__(self) -> Self:
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
def __exit__(self, exc: type[BaseException] | None, val: BaseException | None, tb: TracebackType | None) -> None:
|
|
222
|
+
self.dispose()
|
|
223
|
+
|
|
224
|
+
@property
|
|
225
|
+
def core(self) -> Core:
|
|
226
|
+
"""
|
|
227
|
+
Returns the core representing this environment.
|
|
228
|
+
"""
|
|
229
|
+
with self.inline_section():
|
|
230
|
+
return core.core
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def disposed(self) -> bool:
|
|
234
|
+
"""
|
|
235
|
+
Checks if the environment is disposed
|
|
236
|
+
"""
|
|
237
|
+
return not hasattr(self, "_data")
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def outputs(self) -> MappingProxyType[int, VideoOutputTuple | AudioNode]:
|
|
241
|
+
"""
|
|
242
|
+
Returns the outputs within this environment.
|
|
243
|
+
"""
|
|
244
|
+
with self.inline_section():
|
|
245
|
+
return get_outputs()
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def vs_environment(self) -> Environment:
|
|
249
|
+
"""
|
|
250
|
+
Returns the vapoursynth.Environment-object representing this environment.
|
|
251
|
+
"""
|
|
252
|
+
return self._environment
|
|
253
|
+
|
|
254
|
+
def switch(self) -> None:
|
|
255
|
+
"""
|
|
256
|
+
Switches to the given environment without storing
|
|
257
|
+
which environment has been defined previously.
|
|
258
|
+
"""
|
|
259
|
+
self._environment.use().__enter__()
|
|
260
|
+
|
|
261
|
+
def dispose(self) -> None:
|
|
262
|
+
"""
|
|
263
|
+
Disposes of the environment.
|
|
264
|
+
"""
|
|
265
|
+
if self.disposed:
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
logger.debug("Starting disposal of environment: %r", self._data)
|
|
269
|
+
|
|
270
|
+
admit_environment(self._data, self.core)
|
|
271
|
+
self._policy.api.destroy_environment(self._data)
|
|
272
|
+
del self._data
|
|
273
|
+
|
|
274
|
+
@contextmanager
|
|
275
|
+
def inline_section(self) -> Iterator[None]:
|
|
276
|
+
"""
|
|
277
|
+
Private API!
|
|
278
|
+
|
|
279
|
+
Switches to the given environment within the block without
|
|
280
|
+
notifying the store.
|
|
281
|
+
|
|
282
|
+
If you follow the rules below, switching the environment
|
|
283
|
+
will be invisible to the caller.
|
|
284
|
+
|
|
285
|
+
Rules for safely calling this function:
|
|
286
|
+
- Do not suspend greenlets within the block!
|
|
287
|
+
- Do not yield or await within the block!
|
|
288
|
+
- Do not use __enter__ and __exit__ directly.
|
|
289
|
+
- This function is not reentrant.
|
|
290
|
+
"""
|
|
291
|
+
self._policy.managed.inline_section_start(self._data)
|
|
292
|
+
try:
|
|
293
|
+
yield
|
|
294
|
+
finally:
|
|
295
|
+
self._policy.managed.inline_section_end()
|
|
296
|
+
|
|
297
|
+
@contextmanager
|
|
298
|
+
def use(self) -> Iterator[None]:
|
|
299
|
+
"""
|
|
300
|
+
Switches to this environment within a block.
|
|
301
|
+
"""
|
|
302
|
+
# prev_environment = self._policy.managed._store.get_current_environment()
|
|
303
|
+
with self._environment.use():
|
|
304
|
+
yield
|
|
305
|
+
|
|
306
|
+
# FIXME
|
|
307
|
+
# # Workaround: On 32bit systems, environment policies do not reset.
|
|
308
|
+
# self._policy.managed.set_environment(prev_environment)
|
|
309
|
+
|
|
310
|
+
def __del__(self) -> None:
|
|
311
|
+
if self.disposed:
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
import warnings
|
|
315
|
+
|
|
316
|
+
warnings.warn(f"Disposing {self!r} inside __del__. This might cause leaks.", ResourceWarning)
|
|
317
|
+
self.dispose()
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class Policy(AbstractContextManager["Policy"]):
|
|
321
|
+
"""
|
|
322
|
+
A managed policy is a very simple policy that just stores the environment
|
|
323
|
+
data within the given store.
|
|
324
|
+
|
|
325
|
+
For convenience (especially for testing), this is a context manager that
|
|
326
|
+
makes sure policies are being unregistered when leaving a block.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
_managed: _ManagedPolicy
|
|
330
|
+
|
|
331
|
+
def __init__(self, store: EnvironmentStore, flags_creation: int = 0) -> None:
|
|
332
|
+
"""
|
|
333
|
+
Initializes a new Policy
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
store: The store to use for managing environments.
|
|
337
|
+
flags_creation: The flags to use when creating environments.
|
|
338
|
+
See vapoursynth.CoreCreationFlags for more information.
|
|
339
|
+
"""
|
|
340
|
+
self._managed = _ManagedPolicy(store)
|
|
341
|
+
self.flags_creation = flags_creation
|
|
342
|
+
|
|
343
|
+
def __enter__(self) -> Self:
|
|
344
|
+
self.register()
|
|
345
|
+
return self
|
|
346
|
+
|
|
347
|
+
def __exit__(self, _: type[BaseException] | None, __: BaseException | None, ___: TracebackType | None) -> None:
|
|
348
|
+
self.unregister()
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def api(self) -> EnvironmentPolicyAPI:
|
|
352
|
+
"""
|
|
353
|
+
Returns the API instance for more complex interactions.
|
|
354
|
+
|
|
355
|
+
You will rarely need to use this directly.
|
|
356
|
+
"""
|
|
357
|
+
return self._managed.api
|
|
358
|
+
|
|
359
|
+
@property
|
|
360
|
+
def managed(self) -> _ManagedPolicy:
|
|
361
|
+
"""
|
|
362
|
+
Returns the actual policy within VapourSynth.
|
|
363
|
+
|
|
364
|
+
You will rarely need to use this directly.
|
|
365
|
+
"""
|
|
366
|
+
return self._managed
|
|
367
|
+
|
|
368
|
+
def register(self) -> None:
|
|
369
|
+
"""
|
|
370
|
+
Registers the policy with VapourSynth.
|
|
371
|
+
"""
|
|
372
|
+
register_policy(self._managed)
|
|
373
|
+
|
|
374
|
+
def unregister(self) -> None:
|
|
375
|
+
"""
|
|
376
|
+
Unregisters the policy from VapourSynth.
|
|
377
|
+
"""
|
|
378
|
+
self._managed.api.unregister_policy()
|
|
379
|
+
|
|
380
|
+
def new_environment(self) -> ManagedEnvironment:
|
|
381
|
+
"""
|
|
382
|
+
Creates a new VapourSynth core.
|
|
383
|
+
|
|
384
|
+
You need to call `dispose()` on this environment when you are done
|
|
385
|
+
using the new environment.
|
|
386
|
+
|
|
387
|
+
For convenience, a managed environment will also serve as a
|
|
388
|
+
context-manager that disposes the environment automatically.
|
|
389
|
+
"""
|
|
390
|
+
data = self.api.create_environment(self.flags_creation)
|
|
391
|
+
env = self.api.wrap_environment(data)
|
|
392
|
+
|
|
393
|
+
menv = ManagedEnvironment(env, data, self)
|
|
394
|
+
logger.debug("Successfully created new environment %r", data)
|
|
395
|
+
return menv
|
vsengine/py.typed
ADDED
|
File without changes
|
vsengine/video.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# vs-engine
|
|
2
|
+
# Copyright (C) 2022 cid-chan
|
|
3
|
+
# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
|
|
4
|
+
# This project is licensed under the EUPL-1.2
|
|
5
|
+
# SPDX-License-Identifier: EUPL-1.2
|
|
6
|
+
"""
|
|
7
|
+
vsengine.render renders video frames for you.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterator, Sequence
|
|
11
|
+
from concurrent.futures import Future
|
|
12
|
+
|
|
13
|
+
import vapoursynth as vs
|
|
14
|
+
|
|
15
|
+
from ._futures import UnifiedFuture, unified
|
|
16
|
+
from ._helpers import use_inline
|
|
17
|
+
from ._nodes import buffer_futures, close_when_needed
|
|
18
|
+
from .policy import ManagedEnvironment
|
|
19
|
+
|
|
20
|
+
__all__ = ["frame", "frames", "planes", "render"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@unified(kind="future")
|
|
24
|
+
def frame(
|
|
25
|
+
node: vs.VideoNode, frameno: int, env: vs.Environment | ManagedEnvironment | None = None
|
|
26
|
+
) -> Future[vs.VideoFrame]:
|
|
27
|
+
"""
|
|
28
|
+
Request a specific frame from a node.
|
|
29
|
+
|
|
30
|
+
:param node: The node to request the frame from.
|
|
31
|
+
:param frameno: The frame number to request.
|
|
32
|
+
:param env: The environment to use for the request.
|
|
33
|
+
:return: A future that resolves to the frame.
|
|
34
|
+
"""
|
|
35
|
+
with use_inline("frame", env):
|
|
36
|
+
return node.get_frame_async(frameno)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@unified(kind="future")
|
|
40
|
+
def planes(
|
|
41
|
+
node: vs.VideoNode,
|
|
42
|
+
frameno: int,
|
|
43
|
+
env: vs.Environment | ManagedEnvironment | None = None,
|
|
44
|
+
*,
|
|
45
|
+
planes: Sequence[int] | None = None,
|
|
46
|
+
) -> Future[tuple[bytes, ...]]:
|
|
47
|
+
"""
|
|
48
|
+
Request a specific frame from a node and return the planes as bytes.
|
|
49
|
+
|
|
50
|
+
:param node: The node to request the frame from.
|
|
51
|
+
:param frameno: The frame number to request.
|
|
52
|
+
:param env: The environment to use for the request.
|
|
53
|
+
:param planes: The planes to return. If None, all planes are returned.
|
|
54
|
+
:return: A future that resolves to a tuple of bytes.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def _extract(frame: vs.VideoFrame) -> tuple[bytes, ...]:
|
|
58
|
+
try:
|
|
59
|
+
# This might be a variable format clip.
|
|
60
|
+
# extract the plane as late as possible.
|
|
61
|
+
ps = range(len(frame)) if planes is None else planes
|
|
62
|
+
return tuple(bytes(frame[p]) for p in ps)
|
|
63
|
+
finally:
|
|
64
|
+
frame.close()
|
|
65
|
+
|
|
66
|
+
return frame(node, frameno, env).map(_extract)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@unified(kind="generator")
|
|
70
|
+
def frames(
|
|
71
|
+
node: vs.VideoNode,
|
|
72
|
+
env: vs.Environment | ManagedEnvironment | None = None,
|
|
73
|
+
*,
|
|
74
|
+
prefetch: int = 0,
|
|
75
|
+
backlog: int | None = None,
|
|
76
|
+
# Unlike the implementation provided by VapourSynth,
|
|
77
|
+
# we don't have to care about backwards compatibility and
|
|
78
|
+
# can just do the right thing from the beginning.
|
|
79
|
+
close: bool = True,
|
|
80
|
+
) -> Iterator[Future[vs.VideoFrame]]:
|
|
81
|
+
"""
|
|
82
|
+
Iterate over the frames of a node.
|
|
83
|
+
|
|
84
|
+
:param node: The node to iterate over.
|
|
85
|
+
:param env: The environment to use for the request.
|
|
86
|
+
:param prefetch: The number of frames to prefetch.
|
|
87
|
+
:param backlog: The maximum number of frames to keep in the backlog.
|
|
88
|
+
:param close: Whether to close the frames automatically.
|
|
89
|
+
:return: An iterator of futures that resolve to the frames.
|
|
90
|
+
"""
|
|
91
|
+
with use_inline("frames", env):
|
|
92
|
+
length = len(node)
|
|
93
|
+
|
|
94
|
+
it = (frame(node, n, env) for n in range(length))
|
|
95
|
+
|
|
96
|
+
# If backlog is zero, skip.
|
|
97
|
+
if backlog is None or backlog > 0:
|
|
98
|
+
it = buffer_futures(it, prefetch=prefetch, backlog=backlog)
|
|
99
|
+
|
|
100
|
+
if close:
|
|
101
|
+
it = close_when_needed(it)
|
|
102
|
+
|
|
103
|
+
return it
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@unified(kind="generator")
|
|
107
|
+
def render(
|
|
108
|
+
node: vs.VideoNode,
|
|
109
|
+
env: vs.Environment | ManagedEnvironment | None = None,
|
|
110
|
+
*,
|
|
111
|
+
prefetch: int = 0,
|
|
112
|
+
backlog: int | None = 0,
|
|
113
|
+
y4m: bool = False,
|
|
114
|
+
) -> Iterator[Future[tuple[int, bytes]]]:
|
|
115
|
+
"""
|
|
116
|
+
Render a node to a stream of bytes.
|
|
117
|
+
|
|
118
|
+
:param node: The node to render.
|
|
119
|
+
:param env: The environment to use for the request.
|
|
120
|
+
:param prefetch: The number of frames to prefetch.
|
|
121
|
+
:param backlog: The maximum number of frames to keep in the backlog.
|
|
122
|
+
:param y4m: Whether to output a Y4M header.
|
|
123
|
+
:return: An iterator of futures that resolve to a tuple of the frame number and the frame data.
|
|
124
|
+
"""
|
|
125
|
+
frame_count = len(node)
|
|
126
|
+
|
|
127
|
+
if y4m:
|
|
128
|
+
match node.format.color_family:
|
|
129
|
+
case vs.GRAY:
|
|
130
|
+
y4mformat = "mono"
|
|
131
|
+
case vs.YUV:
|
|
132
|
+
match (node.format.subsampling_w, node.format.subsampling_h):
|
|
133
|
+
case (1, 1):
|
|
134
|
+
y4mformat = "420"
|
|
135
|
+
case (1, 0):
|
|
136
|
+
y4mformat = "422"
|
|
137
|
+
case (0, 0):
|
|
138
|
+
y4mformat = "444"
|
|
139
|
+
case (2, 2):
|
|
140
|
+
y4mformat = "410"
|
|
141
|
+
case (2, 0):
|
|
142
|
+
y4mformat = "411"
|
|
143
|
+
case (0, 1):
|
|
144
|
+
y4mformat = "440"
|
|
145
|
+
case _:
|
|
146
|
+
raise NotImplementedError
|
|
147
|
+
case _:
|
|
148
|
+
raise ValueError("Can only use GRAY and YUV for Y4M-Streams")
|
|
149
|
+
|
|
150
|
+
if node.format.bits_per_sample > 8:
|
|
151
|
+
y4mformat += f"p{node.format.bits_per_sample}"
|
|
152
|
+
|
|
153
|
+
y4mformat = "C" + y4mformat + " "
|
|
154
|
+
|
|
155
|
+
data = "YUV4MPEG2 {y4mformat}W{width} H{height} F{fps_num}:{fps_den} Ip A0:0 XLENGTH={length}\n".format( # noqa: UP032
|
|
156
|
+
y4mformat=y4mformat,
|
|
157
|
+
width=node.width,
|
|
158
|
+
height=node.height,
|
|
159
|
+
fps_num=node.fps_num,
|
|
160
|
+
fps_den=node.fps_den,
|
|
161
|
+
length=frame_count,
|
|
162
|
+
)
|
|
163
|
+
yield UnifiedFuture.resolve((0, data.encode("ascii")))
|
|
164
|
+
|
|
165
|
+
current_frame = 0
|
|
166
|
+
|
|
167
|
+
def render_single_frame(frame: vs.VideoFrame) -> tuple[int, bytes]:
|
|
168
|
+
buf = list[bytes]()
|
|
169
|
+
|
|
170
|
+
if y4m:
|
|
171
|
+
buf.append(b"FRAME\n")
|
|
172
|
+
|
|
173
|
+
for plane in iter(frame):
|
|
174
|
+
buf.append(bytes(plane))
|
|
175
|
+
|
|
176
|
+
return current_frame, b"".join(buf)
|
|
177
|
+
|
|
178
|
+
for frame, fut in enumerate(frames(node, env, prefetch=prefetch, backlog=backlog).futures, 1):
|
|
179
|
+
current_frame = frame
|
|
180
|
+
yield UnifiedFuture.from_future(fut).map(render_single_frame)
|