nvidia-codec 0.0.20__tar.gz → 0.0.21__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 (36) hide show
  1. {nvidia_codec-0.0.20/src/nvidia_codec.egg-info → nvidia_codec-0.0.21}/PKG-INFO +1 -1
  2. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/setup.cfg +1 -1
  3. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/core/decode.py +69 -17
  4. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/ffmpeg/include/libavcodec.py +9 -4
  5. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/ffmpeg/libavcodec.py +4 -1
  6. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/utils/color.py +38 -0
  7. nvidia_codec-0.0.21/src/nvidia_codec/utils/decode.py +768 -0
  8. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/utils/demux.py +83 -22
  9. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21/src/nvidia_codec.egg-info}/PKG-INFO +1 -1
  10. nvidia_codec-0.0.20/src/nvidia_codec/utils/decode.py +0 -474
  11. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/LICENSE +0 -0
  12. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/README.md +0 -0
  13. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/pyproject.toml +0 -0
  14. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/__init__.py +0 -0
  15. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/common.py +0 -0
  16. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/core/__init__.py +0 -0
  17. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/core/common.py +0 -0
  18. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/core/cuda.py +0 -0
  19. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/core/cuviddec.py +0 -0
  20. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/core/nvcuvid.py +0 -0
  21. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/ffmpeg/__init__.py +0 -0
  22. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/ffmpeg/adapt.py +0 -0
  23. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/ffmpeg/common.py +0 -0
  24. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/ffmpeg/include/__init__.py +0 -0
  25. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/ffmpeg/include/libavformat.py +0 -0
  26. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/ffmpeg/include/libavutil.py +0 -0
  27. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/ffmpeg/libavformat.py +0 -0
  28. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/ffmpeg/libavutil.py +0 -0
  29. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/ffmpeg/test.py +0 -0
  30. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/utils/__init__.py +0 -0
  31. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/utils/compat.py +0 -0
  32. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec/utils/player.py +0 -0
  33. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec.egg-info/SOURCES.txt +0 -0
  34. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec.egg-info/dependency_links.txt +0 -0
  35. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/src/nvidia_codec.egg-info/requires.txt +0 -0
  36. {nvidia_codec-0.0.20 → nvidia_codec-0.0.21}/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.20
3
+ Version: 0.0.21
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.20
3
+ version = 0.0.21
4
4
  author = Zesen Qian
5
5
  author_email = nvidia-codec@riaqn.org
6
6
  description = Pythonic interface to Nvidia Codec
@@ -17,6 +17,7 @@ from .common import *
17
17
  from .. import CodecNotSupportedError
18
18
  import numpy as np
19
19
  from ctypes import *
20
+ import weakref
20
21
 
21
22
  import logging
22
23
 
@@ -123,12 +124,19 @@ class Surface:
123
124
  class Picture:
124
125
  """Decoded frame before post-processing.
125
126
 
126
- A data object — does not own the picture slot. Slot ownership is
127
- managed by the caller via pre_decode/pic_release callbacks.
127
+ A pure data object — index, proc params, pts. Deliberately holds NO
128
+ reference to the decoder: the decoder tracks live Pictures in
129
+ _slots/_recent, and a back-reference here would form a cycle that
130
+ keeps the decoder (and its large NVDEC context) alive past the
131
+ refcount-zero point, deferring cleanup to the cyclic GC. To turn a
132
+ Picture into a Surface use Decoder.map(pic) (the decoder is always
133
+ in scope where mapping happens).
134
+
135
+ Slot ownership is managed by the caller via pre_decode/pic_release
136
+ callbacks.
128
137
  """
129
138
 
130
- def __init__(self, decoder, index, proc_params, pts):
131
- self.decoder = decoder
139
+ def __init__(self, index, proc_params, pts):
132
140
  self.index = index
133
141
  self.params = proc_params
134
142
  self.pts = pts
@@ -137,9 +145,6 @@ class Picture:
137
145
  """No-op. Slot ownership is managed by the caller (player.py)."""
138
146
  pass
139
147
 
140
- def map(self, stream: int = 0):
141
- return Surface(self.decoder, self.index, self.params, stream, self.pts)
142
-
143
148
 
144
149
  def decide_surface_format(
145
150
  chroma_format, bit_depth, supported_surface_formats, allow_high=False
@@ -192,12 +197,27 @@ class BaseDecoder:
192
197
  """
193
198
 
194
199
  def catch_exception(self, func, return_on_error=0):
200
+ # `func` is a bound method of self, and the wrapper records errors
201
+ # on self.exception — both strong refs to self. The wrapper is
202
+ # wrapped in a CFUNCTYPE and stored on self (self.handleXxxCallback),
203
+ # so capturing self strongly here forms a self-cycle that makes the
204
+ # decoder uncollectable by refcounting (only the cyclic GC can take
205
+ # it — which under streaming load lags and balloons VRAM). Hold
206
+ # self weakly instead; the callback only ever fires during a
207
+ # send()/cuvidParseVideoData while a caller keeps the decoder
208
+ # alive, so the weakref always resolves then. The None branch is
209
+ # just belt-and-suspenders for a callback racing finalization.
210
+ selfref = weakref.ref(self)
211
+ fn = func.__func__
195
212
  def wrapper(*args, **kwargs):
213
+ s = selfref()
214
+ if s is None:
215
+ return return_on_error
196
216
  try:
197
- return func(*args, **kwargs)
217
+ return fn(s, *args, **kwargs)
198
218
  except BaseException as e:
199
219
  log.debug(f"callback exception logged {e}")
200
- self.exception = e
220
+ s.exception = e
201
221
  log.debug(f"returning error code {return_on_error}")
202
222
  return return_on_error
203
223
 
@@ -410,10 +430,16 @@ class BaseDecoder:
410
430
  unpaired_field=di.repeat_first_field < 0,
411
431
  )
412
432
 
413
- picture = Picture(self, di.picture_index, params, di.timestamp)
433
+ picture = Picture(di.picture_index, params, di.timestamp)
414
434
  self.post_decode(picture)
415
435
  return 1
416
436
 
437
+ def map(self, pic, stream: int = 0):
438
+ """Map a decoded Picture to a Surface (post-processed frame).
439
+ Lives on the decoder (not Picture.map) so Picture needn't hold a
440
+ decoder back-reference — see Picture's docstring."""
441
+ return Surface(self, pic.index, pic.params, stream, pic.pts)
442
+
417
443
  def handleOperatingPoint(self, pUserData, pOPInfo):
418
444
  opi = pOPInfo.contents
419
445
  if opi.codec == cudaVideoCodec.AV1:
@@ -528,18 +554,41 @@ class BaseDecoder:
528
554
  payload=data.ctypes.data_as(POINTER(c_uint8)),
529
555
  timestamp=pts,
530
556
  )
557
+ # self.exception relays an error out of a ctypes callback (which
558
+ # can't propagate through the C parser). Clear it as we consume
559
+ # it: a retained exception keeps its __traceback__ alive, whose
560
+ # frames reference self, pinning the decoder. Raise it WITHOUT
561
+ # binding it to a local here — a `e = self.exception; raise e`
562
+ # would put `e` in this frame, and the raised exception's
563
+ # traceback includes this same frame → e ↔ frame cycle, defeating
564
+ # the point. `try: raise self.exception finally: clear` keeps the
565
+ # exception out of this frame's locals.
531
566
  self.exception = None
532
- with cuda.Device(self.device):
533
- try:
567
+ try:
568
+ with cuda.Device(self.device):
534
569
  cuda.check(nvcuvid.cuvidParseVideoData(self.cuvid_parser, byref(p)))
535
- except cuda.CUError:
536
- if self.exception is not None:
537
- raise self.exception
570
+ except cuda.CUError:
571
+ if self.exception is None:
538
572
  raise
573
+ # else: a callback captured a more specific exception — it's
574
+ # raised (and cleared) below in preference to the CUError.
539
575
  if self.exception is not None:
540
- raise self.exception
576
+ try:
577
+ raise self.exception
578
+ finally:
579
+ self.exception = None
541
580
 
542
581
  def free(self):
582
+ # __del__ can call this during interpreter shutdown, by which
583
+ # point module globals (nvcuvid, cuda, the ctypes types) may be
584
+ # torn down to None — calling through them then raises
585
+ # "NoneType is not callable". The OS reclaims the GPU context on
586
+ # process exit regardless, so just bail if our entry points are
587
+ # gone. (Now reachable because the decoder is refcount-collectable
588
+ # — its self-reference cycles were removed — so __del__ actually
589
+ # runs at shutdown instead of the object leaking.)
590
+ if nvcuvid is None or cuda is None:
591
+ return
543
592
  if getattr(self, 'cuvid_parser', None):
544
593
  cuda.check(nvcuvid.cuvidDestroyVideoParser(self.cuvid_parser))
545
594
  self.cuvid_parser = CUvideoparser()
@@ -549,7 +598,10 @@ class BaseDecoder:
549
598
  self.cuvid_decoder = CUvideodecoder()
550
599
 
551
600
  def __del__(self):
552
- self.free()
601
+ try:
602
+ self.free()
603
+ except Exception:
604
+ pass # best-effort during finalization / shutdown
553
605
 
554
606
  def __enter__(self):
555
607
  return self
@@ -15,6 +15,10 @@ class AVMediaType(TypedCEnumeration(c_int)):
15
15
  NB = 5
16
16
 
17
17
  class AVCodecID(TypedCEnumeration(c_int)):
18
+ # Values track the ffmpeg 9 enum (libavcodec 63, pinned via the versioned
19
+ # libavcodec.so.63 load in ffmpeg/libavcodec.py — ffmpeg 8 removed entries,
20
+ # shifting WEBP/VP9/HEVC/HQ_HQA/AV1 down). A mismatch surfaces as
21
+ # CodecNotSupportedError ("<AVCodecID.(unknown): N>") on NVDEC-supported files.
18
22
  MPEG1 = 1
19
23
  MPEG2 = 2
20
24
  MPEG4 = 12
@@ -30,11 +34,12 @@ class AVCodecID(TypedCEnumeration(c_int)):
30
34
  VP6F = 92
31
35
  GIF = 97
32
36
  PRORES = 147
33
- WEBP = 171
37
+ WEBP = 170
34
38
  VP8 = 139
35
- VP9 = 167
36
- HEVC = 173
37
- AV1 = 225
39
+ VP9 = 166
40
+ HEVC = 172
41
+ HQ_HQA = 185
42
+ AV1 = 222
38
43
  # Audio codecs — values must match ffmpeg's avcodec.h
39
44
  PCM_S16LE = 0x10000
40
45
  PCM_S16BE = 0x10001
@@ -8,7 +8,10 @@ from errno import EAGAIN
8
8
  import logging
9
9
  log = logging.getLogger(__name__)
10
10
 
11
- lib = cdll.LoadLibrary('libavcodec.so')
11
+ # Pin the ABI major: enum values + struct layouts in include/libavcodec.py
12
+ # track ffmpeg 9 (libavcodec 63). Loading the versioned .so makes a system
13
+ # ffmpeg upgrade fail loudly here instead of mis-decoding with shifted enums.
14
+ lib = cdll.LoadLibrary('libavcodec.so.63')
12
15
 
13
16
 
14
17
  av_packet_alloc = lib.av_packet_alloc
@@ -23,6 +23,44 @@ from ..ffmpeg.include.libavutil import AVColorRange, AVColorSpace
23
23
  from ..core.cuviddec import cudaVideoSurfaceFormat
24
24
  from ..core.decode import Surface
25
25
 
26
+ def to_yuv420p(surface : Surface):
27
+ """De-interleave/subsample a decoded surface into a contiguous planar I420
28
+ (yuv420p) uint8 CUDA tensor: Y (H·W) ++ U (H·W/4) ++ V (H·W/4).
29
+
30
+ Unlike convert() — which applies the YUV→RGB color matrix — this does NO
31
+ color math, only plane layout: for NV12 (semi-planar 4:2:0) it splits the
32
+ interleaved chroma; for YUV444 (planar 4:4:4) it box-averages the full-res
33
+ chroma 2×2 down to 4:2:0. 8-bit only (NV12 / YUV444). Half the bytes of rgb24
34
+ and no matrix round-trip; the encoder consumes yuv420p directly. Returns a 1D
35
+ tensor of length H·W·3//2, ready for `.cpu().numpy().tobytes()`."""
36
+ fmt = surface.format
37
+ width, height = surface.width, surface.height
38
+ YUV = torch.as_tensor(surface, device=f'cuda:{surface.decoder.device}')
39
+ if fmt == cudaVideoSurfaceFormat.NV12:
40
+ Y = YUV[:height, :] # (H, W)
41
+ UV = YUV[height:, :] # (H/2, W): U,V interleaved
42
+ U = UV[:, 0::2] # (H/2, W/2)
43
+ V = UV[:, 1::2] # (H/2, W/2)
44
+ elif fmt == cudaVideoSurfaceFormat.YUV444:
45
+ # planar 4:4:4 → 4:2:0: keep Y full-res, box-average chroma 2×2. Plane
46
+ # layout mirrors convert() (3-plane tensor, or 3 planes stacked H·3).
47
+ if YUV.ndim == 3:
48
+ Y, U444, V444 = YUV[0], YUV[1], YUV[2] # each (H, W)
49
+ else:
50
+ Y = YUV[:height, :]
51
+ U444 = YUV[height:height * 2, :]
52
+ V444 = YUV[height * 2:height * 3, :]
53
+ eh, ew = height // 2 * 2, width // 2 * 2
54
+ def _sub(c): # (H, W) 4:4:4 → (H/2, W/2) box-average, uint8
55
+ c = c[:eh, :ew].contiguous().to(torch.float32).reshape(eh // 2, 2, ew // 2, 2)
56
+ return c.mean(dim=(1, 3)).round().clamp(0, 255).to(torch.uint8)
57
+ U, V = _sub(U444), _sub(V444)
58
+ else:
59
+ raise ValueError(f"to_yuv420p: NV12/YUV444 8-bit only, got {fmt}")
60
+ # reshape(-1) copies the (possibly pitch-strided) planes to contiguous.
61
+ return torch.cat([Y.reshape(-1), U.reshape(-1), V.reshape(-1)])
62
+
63
+
26
64
  def convert(surface : Surface,
27
65
  source_space : AVColorSpace,
28
66
  source_range : AVColorRange,