nvidia-codec 0.0.17__tar.gz → 0.0.19__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.17/src/nvidia_codec.egg-info → nvidia_codec-0.0.19}/PKG-INFO +1 -1
  2. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/setup.cfg +1 -1
  3. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/core/decode.py +83 -270
  4. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/ffmpeg/libavformat.py +39 -20
  5. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/ffmpeg/libavutil.py +13 -6
  6. nvidia_codec-0.0.19/src/nvidia_codec/utils/__init__.py +10 -0
  7. nvidia_codec-0.0.19/src/nvidia_codec/utils/decode.py +470 -0
  8. nvidia_codec-0.0.19/src/nvidia_codec/utils/demux.py +260 -0
  9. nvidia_codec-0.0.19/src/nvidia_codec/utils/player.py +42 -0
  10. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19/src/nvidia_codec.egg-info}/PKG-INFO +1 -1
  11. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec.egg-info/SOURCES.txt +2 -0
  12. nvidia_codec-0.0.17/src/nvidia_codec/utils/__init__.py +0 -12
  13. nvidia_codec-0.0.17/src/nvidia_codec/utils/player.py +0 -849
  14. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/LICENSE +0 -0
  15. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/README.md +0 -0
  16. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/pyproject.toml +0 -0
  17. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/__init__.py +0 -0
  18. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/common.py +0 -0
  19. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/core/__init__.py +0 -0
  20. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/core/common.py +0 -0
  21. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/core/cuda.py +0 -0
  22. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/core/cuviddec.py +0 -0
  23. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/core/nvcuvid.py +0 -0
  24. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/ffmpeg/__init__.py +0 -0
  25. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/ffmpeg/adapt.py +0 -0
  26. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/ffmpeg/common.py +0 -0
  27. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/ffmpeg/include/__init__.py +0 -0
  28. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/ffmpeg/include/libavcodec.py +0 -0
  29. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/ffmpeg/include/libavformat.py +0 -0
  30. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/ffmpeg/include/libavutil.py +0 -0
  31. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/ffmpeg/libavcodec.py +0 -0
  32. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/ffmpeg/test.py +0 -0
  33. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/utils/color.py +0 -0
  34. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec/utils/compat.py +0 -0
  35. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec.egg-info/dependency_links.txt +0 -0
  36. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/src/nvidia_codec.egg-info/requires.txt +0 -0
  37. {nvidia_codec-0.0.17 → nvidia_codec-0.0.19}/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.17
3
+ Version: 0.0.19
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.17
3
+ version = 0.0.19
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,76 +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
130
  def __init__(self, decoder, index, proc_params, pts):
189
- """Internal constructor. Pictures are created by the decoder."""
190
131
  self.decoder = decoder
191
132
  self.index = index
192
133
  self.params = proc_params
193
134
  self.pts = pts
194
- with self.decoder.condition:
195
- self.decoder.pictures_used.add(self.index)
196
135
 
197
136
  def free(self):
198
- """Release this picture back to the decoder's pool.
199
-
200
- The underlying GPU memory remains allocated by the decoder and will be
201
- reused for future frames. Called automatically on garbage collection.
202
- """
203
- log.debug(f"freeing picture {self.index}")
204
-
205
- with self.decoder.condition:
206
- # discard instead of remove
207
- # so this won't trigger exception
208
- # which is possible
209
- self.decoder.pictures_used.discard(self.index)
210
- self.decoder.condition.notify_all()
211
-
212
- def __del__(self):
213
- self.free()
137
+ """No-op. Slot ownership is managed by the caller (player.py)."""
138
+ pass
214
139
 
215
140
  def map(self, stream: int = 0):
216
- """Post-process this picture and create an accessible Surface.
217
-
218
- Applies cropping, scaling, and color format conversion as configured
219
- in the decoder. The operation is queued on the specified CUDA stream.
220
-
221
- Args:
222
- stream: CUDA stream for the operation. Pass 0 or 2 for the
223
- per-thread default stream, or a stream pointer/handle.
224
-
225
- Returns:
226
- Surface that can be accessed via __cuda_array_interface__.
227
- """
228
- return Surface(self.decoder, self.index, self.params, stream)
141
+ return Surface(self.decoder, self.index, self.params, stream, self.pts)
229
142
 
230
143
 
231
144
  def decide_surface_format(
232
145
  chroma_format, bit_depth, supported_surface_formats, allow_high=False
233
146
  ):
234
- """Select the best surface format for the given video parameters.
235
-
236
- Args:
237
- chroma_format: Video chroma subsampling (YUV420, YUV444, etc.)
238
- bit_depth: Bits per color channel (usually 8 or 10)
239
- supported_surface_formats: Formats supported by this GPU/codec combination
240
- allow_high: If True, use 16-bit output for >8-bit video. If False,
241
- always use 8-bit output (lossy for HDR content).
242
-
243
- Returns:
244
- The selected cudaVideoSurfaceFormat.
245
-
246
- Raises:
247
- Exception: If the chroma format is unsupported or no compatible
248
- surface format is available.
249
- """
147
+ """Select the best surface format for the given video parameters."""
250
148
  if chroma_format in [
251
149
  cudaVideoChromaFormat.YUV420,
252
150
  cudaVideoChromaFormat.MONOCHROME,
@@ -267,7 +165,6 @@ def decide_surface_format(
267
165
  else:
268
166
  raise Exception(f"unexpected chroma format {chroma_format}")
269
167
 
270
- # check if the selected format is supported. If not, check fallback options
271
168
  if f not in supported_surface_formats:
272
169
  if cudaVideoSurfaceFormat.NV12 in supported_surface_formats:
273
170
  f = cudaVideoSurfaceFormat.NV12
@@ -287,26 +184,19 @@ def decide_surface_format(
287
184
 
288
185
 
289
186
  class BaseDecoder:
290
- """NVDEC hardware decoder with callback-based frame delivery.
291
-
292
- Decodes video packets using NVIDIA's CUVID API. For each decoded frame,
293
- the provided on_recv callback is invoked with a Picture object. This
294
- is the low-level decoder; most users should use Player
295
- from nvidia_codec.utils instead.
187
+ """NVDEC hardware decoder — thin wrapper around CUVID API.
296
188
 
297
- The decoder manages GPU memory pools for Pictures and Surfaces. Configure
298
- 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.
299
192
  """
300
193
 
301
194
  def catch_exception(self, func, return_on_error=0):
302
- """Wrap callbacks to capture exceptions and return error codes."""
303
-
304
195
  def wrapper(*args, **kwargs):
305
196
  try:
306
197
  return func(*args, **kwargs)
307
- except BaseException as e: # catch keyboard interrupt as well
198
+ except BaseException as e:
308
199
  log.debug(f"callback exception logged {e}")
309
- # we record the exception; to be checked when nvcuvid propagate the error return code
310
200
  self.exception = e
311
201
  log.debug(f"returning error code {return_on_error}")
312
202
  return return_on_error
@@ -318,8 +208,6 @@ class BaseDecoder:
318
208
  vf = pVideoFormat.contents
319
209
 
320
210
  if self.cuvid_decoder:
321
- # Only compare fields that affect decoder configuration
322
- # frame_rate and other metadata can vary after seek within the same video
323
211
  essential = [
324
212
  "codec",
325
213
  "coded_width",
@@ -340,7 +228,6 @@ class BaseDecoder:
340
228
  f"decoder already initialized with different format: {diff}"
341
229
  )
342
230
  memmove(byref(self.video_format), byref(vf), sizeof(CUVIDEOFORMAT))
343
- # save for user use
344
231
 
345
232
  caps = CUVIDDECODECAPS(
346
233
  eCodecType=vf.codec,
@@ -363,7 +250,7 @@ class BaseDecoder:
363
250
  ) <= caps.nMaxMBCount, "too many macroblocks"
364
251
 
365
252
  supported_surface_formats = []
366
- for surface_format in range(4): # cudaVideoSurfaceFormat:
253
+ for surface_format in range(4):
367
254
  if caps.nOutputFormatMask & (1 << surface_format):
368
255
  supported_surface_formats.append(cudaVideoSurfaceFormat(surface_format))
369
256
 
@@ -374,14 +261,13 @@ class BaseDecoder:
374
261
  "min_num_pictures": vf.min_num_decode_surfaces,
375
262
  }
376
263
 
377
- # the default values
378
264
  decision = {
379
- "num_pictures": p["min_num_pictures"] + 1, # for simplicity
380
- "num_surfaces": 1 + 1, # for simplicity
265
+ "num_pictures": p["min_num_pictures"] + 1,
266
+ "num_surfaces": 1 + 1,
381
267
  "surface_format": decide_surface_format(
382
268
  p["chroma_format"], p["bit_depth"], p["supported_surface_formats"]
383
269
  ),
384
- "cropping": lambda height, width: { # by default no cropping
270
+ "cropping": lambda height, width: {
385
271
  "left": 0,
386
272
  "right": width,
387
273
  "top": 0,
@@ -390,8 +276,8 @@ class BaseDecoder:
390
276
  "target_size": lambda cropped_height, cropped_width: (
391
277
  cropped_height,
392
278
  cropped_width,
393
- ), # by default no scaling
394
- "target_rect": lambda target_height, target_width: { # by default no margin
279
+ ),
280
+ "target_rect": lambda target_height, target_width: {
395
281
  "left": 0,
396
282
  "right": target_width,
397
283
  "top": 0,
@@ -414,7 +300,6 @@ class BaseDecoder:
414
300
  ), f"surface format {decision['surface_format']} not supported for codec {caps.eCodecType} chroma {caps.eChromaFormat} depth {caps.nBitDepthMinus8 + 8}"
415
301
  assert decision["num_surfaces"] >= 0, "number of surfaces must be non-negative"
416
302
  da = vf.display_area
417
- # the provided cropping is offsetted
418
303
  display_area = SRECT(
419
304
  left=da.left + c["left"],
420
305
  top=da.top + c["top"],
@@ -451,9 +336,6 @@ class BaseDecoder:
451
336
  enableHistogram=0,
452
337
  )
453
338
 
454
- # for field_name, filed_type in p._fields_:
455
- # print(field_name, getattr(p, field_name))
456
-
457
339
  with cuda.Device(self.device):
458
340
  cuda.check(
459
341
  nvcuvid.cuvidCreateDecoder(
@@ -461,77 +343,64 @@ class BaseDecoder:
461
343
  )
462
344
  )
463
345
 
464
- # this contains all picture indices that are still being used
465
- # including the ones in above, and the ones passed to user via on_recv
466
- self.pictures_used = set()
467
-
468
- self.surfaces_sem = decision["num_surfaces"] # number of surfaces available
469
-
470
346
  log.debug("sequence successful")
471
347
  return decision["num_pictures"]
472
348
 
473
349
  @property
474
350
  def codec(self):
475
- """Video codec (cudaVideoCodec enum)."""
476
351
  return self.decode_create_info.CodecType
477
352
 
478
353
  @property
479
354
  def height(self):
480
- """Original video height in pixels (before scaling)."""
481
355
  return (
482
356
  self.video_format.display_area.bottom - self.video_format.display_area.top
483
357
  )
484
358
 
485
359
  @property
486
360
  def width(self):
487
- """Original video width in pixels (before scaling)."""
488
361
  return (
489
362
  self.video_format.display_area.right - self.video_format.display_area.left
490
363
  )
491
364
 
492
365
  @property
493
366
  def target_width(self):
494
- """Output width in pixels (after scaling)."""
495
367
  return self.decode_create_info.ulTargetWidth
496
368
 
497
369
  @property
498
370
  def target_height(self):
499
- """Output height in pixels (after scaling)."""
500
371
  return self.decode_create_info.ulTargetHeight
501
372
 
502
373
  @property
503
374
  def surface_format(self):
504
- """Output surface format (NV12, P016, YUV444, etc.)."""
505
375
  return self.decode_create_info.OutputFormat
506
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
+
507
387
  def handlePictureDecode(self, pUserData, pPicParams):
508
388
  log.debug("decode")
509
389
  pp = pPicParams.contents
510
- # assert self.cuvid_decoder, "decoder not initialized"
511
390
  log.debug(f"decode picture index: {pp.CurrPicIdx}")
512
391
 
513
- if pp.CurrPicIdx in self.pictures_used:
514
- log.info(f"{self.pictures_used}")
515
- with self.condition:
516
- # wait until picture slot is available again
517
- log.debug("wait_for_pictures started")
518
- self.condition.wait_for(lambda: pp.CurrPicIdx not in self.pictures_used)
519
- log.debug("wait_for_pictures finished")
392
+ self.pre_decode(pp.CurrPicIdx)
520
393
 
521
- # now we know that the user no longer need this picture
522
- # we can overwrite it
523
- with cuda.Device(self.device):
524
- 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)))
525
396
 
526
397
  return 1
527
398
 
528
399
  def handlePictureDisplay(self, pUserData, pDispInfo):
529
400
  log.debug("display")
530
401
  if not bool(pDispInfo):
531
- # EOS notification
532
- self.on_recv(None)
402
+ self.post_decode(None)
533
403
  return 1
534
- # di = cast(pDispInfo, POINTER(CUVIDPARSERDISPINFO)).contents
535
404
  di = pDispInfo.contents
536
405
 
537
406
  params = CUVIDPROCPARAMS(
@@ -542,15 +411,10 @@ class BaseDecoder:
542
411
  )
543
412
 
544
413
  picture = Picture(self, di.picture_index, params, di.timestamp)
545
- try:
546
- self.on_recv(picture)
547
- except:
548
- picture.free()
549
- raise
414
+ self.post_decode(picture)
550
415
  return 1
551
416
 
552
417
  def handleOperatingPoint(self, pUserData, pOPInfo):
553
- """Handle AV1 operating point selection (internal callback)."""
554
418
  opi = pOPInfo.contents
555
419
  if opi.codec == cudaVideoCodec.AV1:
556
420
  if opi.av1.operating_points_cnt > 1:
@@ -568,40 +432,17 @@ class BaseDecoder:
568
432
  coded_width=0,
569
433
  coded_height=0,
570
434
  ):
571
- """Initialize the decoder.
572
-
573
- Args:
574
- codec: Video codec (e.g., cudaVideoCodec.H264, cudaVideoCodec.HEVC)
575
- decide: Callback to configure decoder parameters. Called with a dict:
576
- - 'chroma_format': cudaVideoChromaFormat (YUV420, YUV444, etc.)
577
- - 'bit_depth': int (8 or 10)
578
- - 'supported_surface_formats': list of cudaVideoSurfaceFormat
579
- - 'min_num_pictures': int (minimum decode buffer size)
580
-
581
- Return a dict with any of these optional overrides:
582
- - 'num_pictures': Decode buffer size (default: min + 1, max: 32)
583
- - 'num_surfaces': Output buffer size (default: 2)
584
- - 'surface_format': Output format (default: auto-selected)
585
- - 'cropping': Function (h, w) -> {'left', 'top', 'right', 'bottom'}
586
- - 'target_size': Function (h, w) -> (new_h, new_w) for scaling
587
- - 'target_rect': Function (h, w) -> {'left', 'top', 'right', 'bottom'}
588
-
589
- device: CUDA device ID (default: current device)
590
- extradata: Codec sequence header from container (required for VC1/WMV3)
591
- coded_width: Video width hint for codecs needing extradata
592
- coded_height: Video height hint for codecs needing extradata
593
- """
594
435
  self.dirty = False
595
436
  self.decide = decide
596
437
  self.exception = None
597
- self.cuvid_parser = CUvideoparser() # NULL
598
- self.cuvid_decoder = CUvideodecoder() # NULL
438
+ self.cuvid_parser = CUvideoparser()
439
+ self.cuvid_decoder = CUvideodecoder()
440
+
599
441
 
600
442
  self.operating_point = 0
601
443
  self.disp_all_layers = False
602
- self.condition = Condition()
603
444
 
604
- self.video_format = CUVIDEOFORMAT() # NULL, to be filled in later
445
+ self.video_format = CUVIDEOFORMAT()
605
446
 
606
447
  self.handleVideoSequenceCallback = PFNVIDSEQUENCECALLBACK(
607
448
  self.catch_exception(self.handleVideoSequence)
@@ -616,17 +457,14 @@ class BaseDecoder:
616
457
  self.catch_exception(self.handleOperatingPoint, -1)
617
458
  )
618
459
 
619
- # Prepare extended video info with sequence header for codecs like VC1/WMV3
620
460
  self.ext_video_info = None
621
461
  pExtVideoInfo = None
622
462
  if extradata is not None and len(extradata) > 0:
623
463
  self.ext_video_info = CUVIDEOFORMATEX()
624
- # Set format info from container (needed for VC1 Simple/Main which lacks in-band headers)
625
464
  self.ext_video_info.format.codec = codec
626
465
  self.ext_video_info.format.coded_width = coded_width
627
466
  self.ext_video_info.format.coded_height = coded_height
628
- self.ext_video_info.format.chroma_format = 1 # YUV420 - most common
629
- # Copy extradata into raw_seqhdr_data (max 1024 bytes)
467
+ self.ext_video_info.format.chroma_format = 1
630
468
  copy_len = min(len(extradata), 1024)
631
469
  memmove(self.ext_video_info.raw_seqhdr_data, extradata, copy_len)
632
470
  self.ext_video_info.format.seqhdr_data_length = copy_len
@@ -650,11 +488,8 @@ class BaseDecoder:
650
488
 
651
489
  self.device = cuda.get_current_device(device)
652
490
 
653
- # Early codec support check before creating parser
654
- # (cuvidCreateVideoParser may succeed even for unsupported codecs,
655
- # then cuvidParseVideoData fails with an unhelpful "unknown error")
656
491
  caps = CUVIDDECODECAPS(
657
- eCodecType=codec, eChromaFormat=1, nBitDepthMinus8=0 # YUV420 — most common
492
+ eCodecType=codec, eChromaFormat=1, nBitDepthMinus8=0
658
493
  )
659
494
  with cuda.Device(self.device):
660
495
  cuda.check(nvcuvid.cuvidGetDecoderCaps(byref(caps)))
@@ -663,28 +498,19 @@ class BaseDecoder:
663
498
  f"Codec not supported: {cudaVideoCodec(codec)}"
664
499
  )
665
500
 
666
- with self.condition:
667
- cuda.check(
668
- nvcuvid.cuvidCreateVideoParser(byref(self.cuvid_parser), byref(p))
669
- )
670
- self.condition.notify_all()
501
+ cuda.check(
502
+ nvcuvid.cuvidCreateVideoParser(byref(self.cuvid_parser), byref(p))
503
+ )
671
504
 
672
- def send(self, packet, on_recv):
505
+ def send(self, packet):
673
506
  """Send a compressed packet to the decoder.
674
507
 
675
- The decoder will parse the packet and invoke on_recv for each decoded
676
- 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.
677
510
 
678
511
  Args:
679
- packet: Either (pts, data) tuple where data is a 1D numpy array,
680
- or None to signal end-of-stream (triggers EOS callback with picture=None).
681
- on_recv: Callback function(picture) called for each decoded frame.
682
- - picture: Picture object (with .pts field), or None at end of stream
683
-
684
- Note:
685
- The packet buffer can be reused immediately after send() returns.
512
+ packet: (pts, data) tuple or None for end-of-stream.
686
513
  """
687
-
688
514
  if packet is None:
689
515
  p = CUVIDSOURCEDATAPACKET(
690
516
  flags=(
@@ -703,37 +529,24 @@ class BaseDecoder:
703
529
  timestamp=pts,
704
530
  )
705
531
  self.exception = None
706
- self.on_recv = on_recv
707
532
  with cuda.Device(self.device):
708
533
  try:
709
534
  cuda.check(nvcuvid.cuvidParseVideoData(self.cuvid_parser, byref(p)))
710
535
  except cuda.CUError:
711
- # If a callback stored an exception, raise that instead of CUError
712
536
  if self.exception is not None:
713
537
  raise self.exception
714
538
  raise
715
- # catch: cuvidParseVideoData will not propagate error return code 0 of HandleVideoSequenceCallback
716
- # it still returns CUDA_SUCCESS and simply ignore all future incoming packets
717
- # therefore we must check the exception here, even if the last call succeeded
718
539
  if self.exception is not None:
719
- # the exception is caused by our callback, not by cuvid
720
540
  raise self.exception
721
541
 
722
542
  def free(self):
723
- """Release all decoder resources.
724
-
725
- Destroys the CUVID parser and decoder, freeing all GPU memory.
726
- The decoder cannot be used after calling free().
727
- """
728
- with self.condition:
729
- if self.cuvid_parser:
730
- cuda.check(nvcuvid.cuvidDestroyVideoParser(self.cuvid_parser))
731
- self.cuvid_parser = CUvideoparser()
732
- if self.cuvid_decoder:
733
- with cuda.Device(self.device):
734
- cuda.check(nvcuvid.cuvidDestroyDecoder(self.cuvid_decoder))
735
- self.cuvid_decoder = CUvideodecoder()
736
- 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()
737
550
 
738
551
  def __del__(self):
739
552
  self.free()