nvidia-codec 0.0.16__tar.gz → 0.0.18__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 (37) hide show
  1. {nvidia_codec-0.0.16/src/nvidia_codec.egg-info → nvidia_codec-0.0.18}/PKG-INFO +1 -1
  2. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/setup.cfg +1 -1
  3. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/core/decode.py +89 -279
  4. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/ffmpeg/libavformat.py +39 -20
  5. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/ffmpeg/libavutil.py +13 -6
  6. nvidia_codec-0.0.18/src/nvidia_codec/utils/__init__.py +10 -0
  7. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/utils/compat.py +21 -10
  8. nvidia_codec-0.0.18/src/nvidia_codec/utils/decode.py +467 -0
  9. nvidia_codec-0.0.18/src/nvidia_codec/utils/demux.py +260 -0
  10. nvidia_codec-0.0.18/src/nvidia_codec/utils/player.py +42 -0
  11. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18/src/nvidia_codec.egg-info}/PKG-INFO +1 -1
  12. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec.egg-info/SOURCES.txt +2 -0
  13. nvidia_codec-0.0.16/src/nvidia_codec/utils/__init__.py +0 -12
  14. nvidia_codec-0.0.16/src/nvidia_codec/utils/player.py +0 -762
  15. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/LICENSE +0 -0
  16. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/README.md +0 -0
  17. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/pyproject.toml +0 -0
  18. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/__init__.py +0 -0
  19. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/common.py +0 -0
  20. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/core/__init__.py +0 -0
  21. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/core/common.py +0 -0
  22. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/core/cuda.py +0 -0
  23. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/core/cuviddec.py +0 -0
  24. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/core/nvcuvid.py +0 -0
  25. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/ffmpeg/__init__.py +0 -0
  26. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/ffmpeg/adapt.py +0 -0
  27. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/ffmpeg/common.py +0 -0
  28. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/ffmpeg/include/__init__.py +0 -0
  29. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/ffmpeg/include/libavcodec.py +0 -0
  30. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/ffmpeg/include/libavformat.py +0 -0
  31. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/ffmpeg/include/libavutil.py +0 -0
  32. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/ffmpeg/libavcodec.py +0 -0
  33. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/ffmpeg/test.py +0 -0
  34. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec/utils/color.py +0 -0
  35. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec.egg-info/dependency_links.txt +0 -0
  36. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec.egg-info/requires.txt +0 -0
  37. {nvidia_codec-0.0.16 → nvidia_codec-0.0.18}/src/nvidia_codec.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nvidia-codec
3
- Version: 0.0.16
3
+ Version: 0.0.18
4
4
  Summary: Pythonic interface to Nvidia Codec
5
5
  Home-page: https://github.com/riaqn/python-nvidia-codec
6
6
  Author: Zesen Qian
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = nvidia-codec
3
- version = 0.0.16
3
+ version = 0.0.18
4
4
  author = Zesen Qian
5
5
  author_email = nvidia-codec@riaqn.org
6
6
  description = Pythonic interface to Nvidia Codec
@@ -1,46 +1,22 @@
1
- """Low-level NVDEC decoder with Picture/Surface memory management.
2
-
3
- This module provides the core decoding interface using NVIDIA's CUVID API.
4
- The decoding pipeline has two stages:
5
-
6
- 1. **Picture**: Raw decoded frame in GPU memory (not directly accessible)
7
- 2. **Surface**: Post-processed frame that can be accessed as a CUDA array
8
-
9
- Workflow:
10
- 1. Send compressed packets to BaseDecoder.send()
11
- 2. Receive Picture objects via the on_recv callback
12
- 3. Call Picture.map() to get a Surface (applies cropping/scaling)
13
- 4. Access Surface data via __cuda_array_interface__ (works with PyTorch, CuPy)
14
- 5. Call Surface.free() and Picture.free() when done (or let them be garbage collected)
15
-
16
- Memory Management:
17
- - Pictures and Surfaces are backed by pre-allocated GPU memory pools
18
- - `num_pictures` controls the decode buffer size (max 32)
19
- - `num_surfaces` controls the output buffer size (no hard limit)
20
- - Calling free() marks the slot as reusable, but memory stays allocated
21
- - The decoder owns all memory until BaseDecoder.free() is called
22
-
23
- Example:
24
- decoder = BaseDecoder(cudaVideoCodec.H264, decide=my_decide_callback)
25
- def on_recv(picture, pts, result):
26
- if picture is None: # End of stream
27
- return result
28
- surface = picture.map(cuda_stream)
29
- tensor = torch.as_tensor(surface, device='cuda')
30
- picture.free()
31
- surface.free()
32
- return tensor
33
- result = decoder.send(packet_data, on_recv, pts=12345)
1
+ """Low-level NVDEC decoder — thin wrapper around CUVID API.
2
+
3
+ This module provides the core decoding interface. It does NOT manage
4
+ threading or slot ownership; that is the caller's responsibility
5
+ (see nvidia_codec.utils.player for the higher-level interface).
6
+
7
+ Callbacks:
8
+ pre_decode(pic_idx): Called before cuvidDecodePicture. The caller
9
+ should block here if the picture slot is in use.
10
+ post_decode(pic): Called when a decoded frame is ready for display
11
+ (the "display callback"). pic is a Picture or None (EOS).
34
12
  """
35
13
 
36
14
  from . import cuda
37
15
  from .nvcuvid import *
38
16
  from .common import *
39
17
  from .. import CodecNotSupportedError
40
- from queue import Empty, Queue
41
18
  import numpy as np
42
19
  from ctypes import *
43
- from threading import Condition, Semaphore
44
20
 
45
21
  import logging
46
22
 
@@ -52,87 +28,57 @@ nvcuvid = cdll.LoadLibrary("libnvcuvid.so")
52
28
  class Surface:
53
29
  """Post-processed decoded frame accessible as a CUDA array.
54
30
 
55
- A Surface contains the final decoded frame data after cropping and scaling.
56
- It implements __cuda_array_interface__ for zero-copy access from PyTorch,
57
- CuPy, and other CUDA-aware libraries.
58
-
59
31
  Do not instantiate directly; use Picture.map() instead.
60
-
61
- Attributes:
62
- format: Surface pixel format (NV12, P016, YUV444, etc.)
63
- width: Frame width in pixels
64
- height: Frame height in pixels
65
- shape: Array shape for __cuda_array_interface__
66
32
  """
67
33
 
68
- def __init__(self, decoder, index, params, stream):
69
- """Internal constructor. Use Picture.map() instead."""
70
-
34
+ def __init__(self, decoder, index, params, stream, pts):
71
35
  self.decoder = decoder
36
+ self.pts = pts
72
37
 
73
38
  self.params = CUVIDPROCPARAMS()
74
39
  memmove(byref(self.params), byref(params), sizeof(CUVIDEOFORMAT))
75
40
 
76
41
  self.params.output_stream = stream
77
42
 
78
- self.c_devptr = (
79
- c_ulonglong()
80
- ) # according to cuviddec, the argument type of cuvidmapvideoframe64
43
+ self.c_devptr = c_ulonglong()
81
44
  self.c_pitch = c_uint()
82
45
 
83
- with self.decoder.condition:
84
- self.decoder.condition.wait_for(lambda: self.decoder.surfaces_sem > 0)
85
- with cuda.Device(self.decoder.device):
86
- cuda.check(
87
- nvcuvid.cuvidMapVideoFrame64(
88
- self.decoder.cuvid_decoder,
89
- c_int(index),
90
- byref(self.c_devptr),
91
- byref(self.c_pitch),
92
- byref(self.params),
93
- )
46
+ with cuda.Device(self.decoder.device):
47
+ cuda.check(
48
+ nvcuvid.cuvidMapVideoFrame64(
49
+ self.decoder.cuvid_decoder,
50
+ c_int(index),
51
+ byref(self.c_devptr),
52
+ byref(self.c_pitch),
53
+ byref(self.params),
94
54
  )
95
- # log.warning('mapped')
96
- self.decoder.surfaces_sem -= 1
97
- self.decoder.condition.notify_all()
55
+ )
98
56
 
99
57
  @property
100
58
  def format(self) -> cudaVideoSurfaceFormat:
101
- """Surface pixel format (NV12, P016, YUV444, or YUV444_16Bit)."""
102
59
  return self.decoder.surface_format
103
60
 
104
61
  @property
105
62
  def height(self):
106
- """Frame height in pixels."""
107
63
  return self.decoder.target_height
108
64
 
109
65
  @property
110
66
  def width(self):
111
- """Frame width in pixels."""
112
67
  return self.decoder.target_width
113
68
 
114
69
  @property
115
70
  def size(self):
116
- """Frame dimensions as {'width': int, 'height': int}."""
117
71
  return {"width": self.width, "height": self.height}
118
72
 
119
73
  def free(self):
120
- """Release this surface back to the decoder's pool.
121
-
122
- The underlying GPU memory remains allocated by the decoder and will be
123
- reused for future frames. Called automatically on garbage collection.
124
- """
125
- with self.decoder.condition:
126
- if self.c_devptr and self.decoder.cuvid_decoder:
127
- with cuda.Device(self.decoder.device):
128
- cuda.check(
129
- nvcuvid.cuvidUnmapVideoFrame64(
130
- self.decoder.cuvid_decoder, self.c_devptr
131
- )
74
+ if self.c_devptr and self.decoder.cuvid_decoder:
75
+ with cuda.Device(self.decoder.device):
76
+ cuda.check(
77
+ nvcuvid.cuvidUnmapVideoFrame64(
78
+ self.decoder.cuvid_decoder, self.c_devptr
132
79
  )
133
- self.decoder.surfaces_sem += 1
134
- self.c_devptr = c_ulonglong()
135
- self.decoder.condition.notify_all()
80
+ )
81
+ self.c_devptr = c_ulonglong()
136
82
 
137
83
  def __del__(self):
138
84
  self.free()
@@ -169,7 +115,7 @@ class Surface:
169
115
  "typestr": typestr,
170
116
  "strides": strides,
171
117
  "version": 3,
172
- "data": (self.c_devptr.value, False), # false = not read-only
118
+ "data": (self.c_devptr.value, False),
173
119
  "stream": self.params.output_stream,
174
120
  }
175
121
 
@@ -177,75 +123,28 @@ class Surface:
177
123
  class Picture:
178
124
  """Decoded frame before post-processing.
179
125
 
180
- A Picture holds a reference to a decoded frame in the decoder's internal
181
- buffer. The frame data is not directly accessible; call map() to create
182
- a Surface that can be read.
183
-
184
- Do not instantiate directly; Pictures are created by the decoder and
185
- passed to your on_recv callback.
126
+ A data object — does not own the picture slot. Slot ownership is
127
+ managed by the caller via pre_decode/pic_release callbacks.
186
128
  """
187
129
 
188
- def __init__(self, decoder, index, proc_params):
189
- """Internal constructor. Pictures are created by the decoder."""
130
+ def __init__(self, decoder, index, proc_params, pts):
190
131
  self.decoder = decoder
191
132
  self.index = index
192
133
  self.params = proc_params
193
- with self.decoder.condition:
194
- self.decoder.pictures_used.add(self.index)
134
+ self.pts = pts
195
135
 
196
136
  def free(self):
197
- """Release this picture back to the decoder's pool.
198
-
199
- The underlying GPU memory remains allocated by the decoder and will be
200
- reused for future frames. Called automatically on garbage collection.
201
- """
202
- log.debug(f"freeing picture {self.index}")
203
-
204
- with self.decoder.condition:
205
- # discard instead of remove
206
- # so this won't trigger exception
207
- # which is possible
208
- self.decoder.pictures_used.discard(self.index)
209
- self.decoder.condition.notify_all()
210
-
211
- def __del__(self):
212
- self.free()
137
+ """No-op. Slot ownership is managed by the caller (player.py)."""
138
+ pass
213
139
 
214
140
  def map(self, stream: int = 0):
215
- """Post-process this picture and create an accessible Surface.
216
-
217
- Applies cropping, scaling, and color format conversion as configured
218
- in the decoder. The operation is queued on the specified CUDA stream.
219
-
220
- Args:
221
- stream: CUDA stream for the operation. Pass 0 or 2 for the
222
- per-thread default stream, or a stream pointer/handle.
223
-
224
- Returns:
225
- Surface that can be accessed via __cuda_array_interface__.
226
- """
227
- return Surface(self.decoder, self.index, self.params, stream)
141
+ return Surface(self.decoder, self.index, self.params, stream, self.pts)
228
142
 
229
143
 
230
144
  def decide_surface_format(
231
145
  chroma_format, bit_depth, supported_surface_formats, allow_high=False
232
146
  ):
233
- """Select the best surface format for the given video parameters.
234
-
235
- Args:
236
- chroma_format: Video chroma subsampling (YUV420, YUV444, etc.)
237
- bit_depth: Bits per color channel (usually 8 or 10)
238
- supported_surface_formats: Formats supported by this GPU/codec combination
239
- allow_high: If True, use 16-bit output for >8-bit video. If False,
240
- always use 8-bit output (lossy for HDR content).
241
-
242
- Returns:
243
- The selected cudaVideoSurfaceFormat.
244
-
245
- Raises:
246
- Exception: If the chroma format is unsupported or no compatible
247
- surface format is available.
248
- """
147
+ """Select the best surface format for the given video parameters."""
249
148
  if chroma_format in [
250
149
  cudaVideoChromaFormat.YUV420,
251
150
  cudaVideoChromaFormat.MONOCHROME,
@@ -266,7 +165,6 @@ def decide_surface_format(
266
165
  else:
267
166
  raise Exception(f"unexpected chroma format {chroma_format}")
268
167
 
269
- # check if the selected format is supported. If not, check fallback options
270
168
  if f not in supported_surface_formats:
271
169
  if cudaVideoSurfaceFormat.NV12 in supported_surface_formats:
272
170
  f = cudaVideoSurfaceFormat.NV12
@@ -286,26 +184,19 @@ def decide_surface_format(
286
184
 
287
185
 
288
186
  class BaseDecoder:
289
- """NVDEC hardware decoder with callback-based frame delivery.
290
-
291
- Decodes video packets using NVIDIA's CUVID API. For each decoded frame,
292
- the provided on_recv callback is invoked with a Picture object. This
293
- is the low-level decoder; most users should use Player
294
- from nvidia_codec.utils instead.
187
+ """NVDEC hardware decoder — thin wrapper around CUVID API.
295
188
 
296
- The decoder manages GPU memory pools for Pictures and Surfaces. Configure
297
- pool sizes via the decide callback to balance memory usage and throughput.
189
+ Threading and slot ownership are NOT managed here. The caller provides
190
+ callbacks (pre_decode, pic_release, surface_acquire, surface_release)
191
+ to handle synchronization.
298
192
  """
299
193
 
300
194
  def catch_exception(self, func, return_on_error=0):
301
- """Wrap callbacks to capture exceptions and return error codes."""
302
-
303
195
  def wrapper(*args, **kwargs):
304
196
  try:
305
197
  return func(*args, **kwargs)
306
- except BaseException as e: # catch keyboard interrupt as well
198
+ except BaseException as e:
307
199
  log.debug(f"callback exception logged {e}")
308
- # we record the exception; to be checked when nvcuvid propagate the error return code
309
200
  self.exception = e
310
201
  log.debug(f"returning error code {return_on_error}")
311
202
  return return_on_error
@@ -317,8 +208,6 @@ class BaseDecoder:
317
208
  vf = pVideoFormat.contents
318
209
 
319
210
  if self.cuvid_decoder:
320
- # Only compare fields that affect decoder configuration
321
- # frame_rate and other metadata can vary after seek within the same video
322
211
  essential = [
323
212
  "codec",
324
213
  "coded_width",
@@ -339,7 +228,6 @@ class BaseDecoder:
339
228
  f"decoder already initialized with different format: {diff}"
340
229
  )
341
230
  memmove(byref(self.video_format), byref(vf), sizeof(CUVIDEOFORMAT))
342
- # save for user use
343
231
 
344
232
  caps = CUVIDDECODECAPS(
345
233
  eCodecType=vf.codec,
@@ -362,7 +250,7 @@ class BaseDecoder:
362
250
  ) <= caps.nMaxMBCount, "too many macroblocks"
363
251
 
364
252
  supported_surface_formats = []
365
- for surface_format in range(4): # cudaVideoSurfaceFormat:
253
+ for surface_format in range(4):
366
254
  if caps.nOutputFormatMask & (1 << surface_format):
367
255
  supported_surface_formats.append(cudaVideoSurfaceFormat(surface_format))
368
256
 
@@ -373,14 +261,13 @@ class BaseDecoder:
373
261
  "min_num_pictures": vf.min_num_decode_surfaces,
374
262
  }
375
263
 
376
- # the default values
377
264
  decision = {
378
- "num_pictures": p["min_num_pictures"] + 1, # for simplicity
379
- "num_surfaces": 1 + 1, # for simplicity
265
+ "num_pictures": p["min_num_pictures"] + 1,
266
+ "num_surfaces": 1 + 1,
380
267
  "surface_format": decide_surface_format(
381
268
  p["chroma_format"], p["bit_depth"], p["supported_surface_formats"]
382
269
  ),
383
- "cropping": lambda height, width: { # by default no cropping
270
+ "cropping": lambda height, width: {
384
271
  "left": 0,
385
272
  "right": width,
386
273
  "top": 0,
@@ -389,8 +276,8 @@ class BaseDecoder:
389
276
  "target_size": lambda cropped_height, cropped_width: (
390
277
  cropped_height,
391
278
  cropped_width,
392
- ), # by default no scaling
393
- "target_rect": lambda target_height, target_width: { # by default no margin
279
+ ),
280
+ "target_rect": lambda target_height, target_width: {
394
281
  "left": 0,
395
282
  "right": target_width,
396
283
  "top": 0,
@@ -413,7 +300,6 @@ class BaseDecoder:
413
300
  ), f"surface format {decision['surface_format']} not supported for codec {caps.eCodecType} chroma {caps.eChromaFormat} depth {caps.nBitDepthMinus8 + 8}"
414
301
  assert decision["num_surfaces"] >= 0, "number of surfaces must be non-negative"
415
302
  da = vf.display_area
416
- # the provided cropping is offsetted
417
303
  display_area = SRECT(
418
304
  left=da.left + c["left"],
419
305
  top=da.top + c["top"],
@@ -450,9 +336,6 @@ class BaseDecoder:
450
336
  enableHistogram=0,
451
337
  )
452
338
 
453
- # for field_name, filed_type in p._fields_:
454
- # print(field_name, getattr(p, field_name))
455
-
456
339
  with cuda.Device(self.device):
457
340
  cuda.check(
458
341
  nvcuvid.cuvidCreateDecoder(
@@ -460,77 +343,64 @@ class BaseDecoder:
460
343
  )
461
344
  )
462
345
 
463
- # this contains all picture indices that are still being used
464
- # including the ones in above, and the ones passed to user via on_recv
465
- self.pictures_used = set()
466
-
467
- self.surfaces_sem = decision["num_surfaces"] # number of surfaces available
468
-
469
346
  log.debug("sequence successful")
470
347
  return decision["num_pictures"]
471
348
 
472
349
  @property
473
350
  def codec(self):
474
- """Video codec (cudaVideoCodec enum)."""
475
351
  return self.decode_create_info.CodecType
476
352
 
477
353
  @property
478
354
  def height(self):
479
- """Original video height in pixels (before scaling)."""
480
355
  return (
481
356
  self.video_format.display_area.bottom - self.video_format.display_area.top
482
357
  )
483
358
 
484
359
  @property
485
360
  def width(self):
486
- """Original video width in pixels (before scaling)."""
487
361
  return (
488
362
  self.video_format.display_area.right - self.video_format.display_area.left
489
363
  )
490
364
 
491
365
  @property
492
366
  def target_width(self):
493
- """Output width in pixels (after scaling)."""
494
367
  return self.decode_create_info.ulTargetWidth
495
368
 
496
369
  @property
497
370
  def target_height(self):
498
- """Output height in pixels (after scaling)."""
499
371
  return self.decode_create_info.ulTargetHeight
500
372
 
501
373
  @property
502
374
  def surface_format(self):
503
- """Output surface format (NV12, P016, YUV444, etc.)."""
504
375
  return self.decode_create_info.OutputFormat
505
376
 
377
+ def pre_decode(self, idx):
378
+ """NVDEC is reclaiming picture slot idx. After this returns,
379
+ slot ownership is transferred back to NVDEC."""
380
+ raise NotImplementedError
381
+
382
+ def post_decode(self, pic):
383
+ """NVDEC is releasing picture slot for display.
384
+ pic is a Picture or None (EOS)."""
385
+ raise NotImplementedError
386
+
506
387
  def handlePictureDecode(self, pUserData, pPicParams):
507
388
  log.debug("decode")
508
389
  pp = pPicParams.contents
509
- # assert self.cuvid_decoder, "decoder not initialized"
510
390
  log.debug(f"decode picture index: {pp.CurrPicIdx}")
511
391
 
512
- if pp.CurrPicIdx in self.pictures_used:
513
- log.info(f"{self.pictures_used}")
514
- with self.condition:
515
- # wait until picture slot is available again
516
- log.debug("wait_for_pictures started")
517
- self.condition.wait_for(lambda: pp.CurrPicIdx not in self.pictures_used)
518
- log.debug("wait_for_pictures finished")
392
+ self.pre_decode(pp.CurrPicIdx)
519
393
 
520
- # now we know that the user no longer need this picture
521
- # we can overwrite it
522
- with cuda.Device(self.device):
523
- cuda.check(nvcuvid.cuvidDecodePicture(self.cuvid_decoder, byref(pp)))
394
+ with cuda.Device(self.device):
395
+ cuda.check(nvcuvid.cuvidDecodePicture(self.cuvid_decoder, byref(pp)))
524
396
 
525
397
  return 1
526
398
 
527
399
  def handlePictureDisplay(self, pUserData, pDispInfo):
528
400
  log.debug("display")
529
401
  if not bool(pDispInfo):
530
- # EOS notification
531
- self.on_recv(None, 0)
402
+ self.post_decode(None)
532
403
  return 1
533
- # di = cast(pDispInfo, POINTER(CUVIDPARSERDISPINFO)).contents
534
404
  di = pDispInfo.contents
535
405
 
536
406
  params = CUVIDPROCPARAMS(
@@ -540,16 +410,11 @@ class BaseDecoder:
540
410
  unpaired_field=di.repeat_first_field < 0,
541
411
  )
542
412
 
543
- picture = Picture(self, di.picture_index, params)
544
- try:
545
- self.on_recv(picture, di.timestamp)
546
- except:
547
- picture.free()
548
- raise
413
+ picture = Picture(self, di.picture_index, params, di.timestamp)
414
+ self.post_decode(picture)
549
415
  return 1
550
416
 
551
417
  def handleOperatingPoint(self, pUserData, pOPInfo):
552
- """Handle AV1 operating point selection (internal callback)."""
553
418
  opi = pOPInfo.contents
554
419
  if opi.codec == cudaVideoCodec.AV1:
555
420
  if opi.av1.operating_points_cnt > 1:
@@ -567,40 +432,17 @@ class BaseDecoder:
567
432
  coded_width=0,
568
433
  coded_height=0,
569
434
  ):
570
- """Initialize the decoder.
571
-
572
- Args:
573
- codec: Video codec (e.g., cudaVideoCodec.H264, cudaVideoCodec.HEVC)
574
- decide: Callback to configure decoder parameters. Called with a dict:
575
- - 'chroma_format': cudaVideoChromaFormat (YUV420, YUV444, etc.)
576
- - 'bit_depth': int (8 or 10)
577
- - 'supported_surface_formats': list of cudaVideoSurfaceFormat
578
- - 'min_num_pictures': int (minimum decode buffer size)
579
-
580
- Return a dict with any of these optional overrides:
581
- - 'num_pictures': Decode buffer size (default: min + 1, max: 32)
582
- - 'num_surfaces': Output buffer size (default: 2)
583
- - 'surface_format': Output format (default: auto-selected)
584
- - 'cropping': Function (h, w) -> {'left', 'top', 'right', 'bottom'}
585
- - 'target_size': Function (h, w) -> (new_h, new_w) for scaling
586
- - 'target_rect': Function (h, w) -> {'left', 'top', 'right', 'bottom'}
587
-
588
- device: CUDA device ID (default: current device)
589
- extradata: Codec sequence header from container (required for VC1/WMV3)
590
- coded_width: Video width hint for codecs needing extradata
591
- coded_height: Video height hint for codecs needing extradata
592
- """
593
435
  self.dirty = False
594
436
  self.decide = decide
595
437
  self.exception = None
596
- self.cuvid_parser = CUvideoparser() # NULL
597
- self.cuvid_decoder = CUvideodecoder() # NULL
438
+ self.cuvid_parser = CUvideoparser()
439
+ self.cuvid_decoder = CUvideodecoder()
440
+
598
441
 
599
442
  self.operating_point = 0
600
443
  self.disp_all_layers = False
601
- self.condition = Condition()
602
444
 
603
- self.video_format = CUVIDEOFORMAT() # NULL, to be filled in later
445
+ self.video_format = CUVIDEOFORMAT()
604
446
 
605
447
  self.handleVideoSequenceCallback = PFNVIDSEQUENCECALLBACK(
606
448
  self.catch_exception(self.handleVideoSequence)
@@ -615,17 +457,14 @@ class BaseDecoder:
615
457
  self.catch_exception(self.handleOperatingPoint, -1)
616
458
  )
617
459
 
618
- # Prepare extended video info with sequence header for codecs like VC1/WMV3
619
460
  self.ext_video_info = None
620
461
  pExtVideoInfo = None
621
462
  if extradata is not None and len(extradata) > 0:
622
463
  self.ext_video_info = CUVIDEOFORMATEX()
623
- # Set format info from container (needed for VC1 Simple/Main which lacks in-band headers)
624
464
  self.ext_video_info.format.codec = codec
625
465
  self.ext_video_info.format.coded_width = coded_width
626
466
  self.ext_video_info.format.coded_height = coded_height
627
- self.ext_video_info.format.chroma_format = 1 # YUV420 - most common
628
- # Copy extradata into raw_seqhdr_data (max 1024 bytes)
467
+ self.ext_video_info.format.chroma_format = 1
629
468
  copy_len = min(len(extradata), 1024)
630
469
  memmove(self.ext_video_info.raw_seqhdr_data, extradata, copy_len)
631
470
  self.ext_video_info.format.seqhdr_data_length = copy_len
@@ -649,11 +488,8 @@ class BaseDecoder:
649
488
 
650
489
  self.device = cuda.get_current_device(device)
651
490
 
652
- # Early codec support check before creating parser
653
- # (cuvidCreateVideoParser may succeed even for unsupported codecs,
654
- # then cuvidParseVideoData fails with an unhelpful "unknown error")
655
491
  caps = CUVIDDECODECAPS(
656
- eCodecType=codec, eChromaFormat=1, nBitDepthMinus8=0 # YUV420 — most common
492
+ eCodecType=codec, eChromaFormat=1, nBitDepthMinus8=0
657
493
  )
658
494
  with cuda.Device(self.device):
659
495
  cuda.check(nvcuvid.cuvidGetDecoderCaps(byref(caps)))
@@ -662,34 +498,20 @@ class BaseDecoder:
662
498
  f"Codec not supported: {cudaVideoCodec(codec)}"
663
499
  )
664
500
 
665
- with self.condition:
666
- cuda.check(
667
- nvcuvid.cuvidCreateVideoParser(byref(self.cuvid_parser), byref(p))
668
- )
669
- self.condition.notify_all()
501
+ cuda.check(
502
+ nvcuvid.cuvidCreateVideoParser(byref(self.cuvid_parser), byref(p))
503
+ )
670
504
 
671
- def send(self, packet, on_recv, pts=0):
505
+ def send(self, packet):
672
506
  """Send a compressed packet to the decoder.
673
507
 
674
- The decoder will parse the packet and invoke on_recv for each decoded
675
- frame. Frames are delivered in display order (after B-frame reordering).
508
+ Calls self.pre_decode(idx) before each decode and
509
+ self.post_decode(pic) for each displayed frame.
676
510
 
677
511
  Args:
678
- packet: Compressed video data as a 1D numpy array, or None to
679
- signal end-of-stream (triggers EOS callback with picture=None).
680
- on_recv: Callback function(picture, pts) called for each decoded frame.
681
- - picture: Picture object, or None at end of stream
682
- - pts: Presentation timestamp passed to send()
683
- pts: Presentation timestamp for this packet (passed to on_recv).
684
-
685
- Note:
686
- The packet buffer can be reused immediately after send() returns.
687
- Call flush() to drain any remaining frames from the reorder buffer.
512
+ packet: (pts, data) tuple or None for end-of-stream.
688
513
  """
689
-
690
514
  if packet is None:
691
- # this will reset the parser internal state
692
- # also triggers dummy display callback
693
515
  p = CUVIDSOURCEDATAPACKET(
694
516
  flags=(
695
517
  CUvideopacketflags.ENDOFSTREAM | CUvideopacketflags.NOTIFY_EOS
@@ -699,44 +521,32 @@ class BaseDecoder:
699
521
  timestamp=0,
700
522
  )
701
523
  else:
524
+ pts, data = packet
702
525
  p = CUVIDSOURCEDATAPACKET(
703
526
  flags=CUvideopacketflags.TIMESTAMP.value,
704
- payload_size=packet.shape[0],
705
- payload=packet.ctypes.data_as(POINTER(c_uint8)),
527
+ payload_size=data.shape[0],
528
+ payload=data.ctypes.data_as(POINTER(c_uint8)),
706
529
  timestamp=pts,
707
530
  )
708
531
  self.exception = None
709
- self.on_recv = on_recv
710
532
  with cuda.Device(self.device):
711
533
  try:
712
534
  cuda.check(nvcuvid.cuvidParseVideoData(self.cuvid_parser, byref(p)))
713
535
  except cuda.CUError:
714
- # If a callback stored an exception, raise that instead of CUError
715
536
  if self.exception is not None:
716
537
  raise self.exception
717
538
  raise
718
- # catch: cuvidParseVideoData will not propagate error return code 0 of HandleVideoSequenceCallback
719
- # it still returns CUDA_SUCCESS and simply ignore all future incoming packets
720
- # therefore we must check the exception here, even if the last call succeeded
721
539
  if self.exception is not None:
722
- # the exception is caused by our callback, not by cuvid
723
540
  raise self.exception
724
541
 
725
542
  def free(self):
726
- """Release all decoder resources.
727
-
728
- Destroys the CUVID parser and decoder, freeing all GPU memory.
729
- The decoder cannot be used after calling free().
730
- """
731
- with self.condition:
732
- if self.cuvid_parser:
733
- cuda.check(nvcuvid.cuvidDestroyVideoParser(self.cuvid_parser))
734
- self.cuvid_parser = CUvideoparser()
735
- if self.cuvid_decoder:
736
- with cuda.Device(self.device):
737
- cuda.check(nvcuvid.cuvidDestroyDecoder(self.cuvid_decoder))
738
- self.cuvid_decoder = CUvideodecoder()
739
- self.condition.notify_all()
543
+ if getattr(self, 'cuvid_parser', None):
544
+ cuda.check(nvcuvid.cuvidDestroyVideoParser(self.cuvid_parser))
545
+ self.cuvid_parser = CUvideoparser()
546
+ if getattr(self, 'cuvid_decoder', None):
547
+ with cuda.Device(self.device):
548
+ cuda.check(nvcuvid.cuvidDestroyDecoder(self.cuvid_decoder))
549
+ self.cuvid_decoder = CUvideodecoder()
740
550
 
741
551
  def __del__(self):
742
552
  self.free()