nvidia-codec 0.0.22__tar.gz → 0.0.24__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.22/src/nvidia_codec.egg-info → nvidia_codec-0.0.24}/PKG-INFO +19 -2
  2. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/README.md +18 -1
  3. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/setup.cfg +1 -1
  4. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/include/libavformat.py +1 -0
  5. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/include/libavutil.py +7 -5
  6. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/libavformat.py +0 -10
  7. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/utils/decode.py +50 -2
  8. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/utils/demux.py +29 -9
  9. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/utils/player.py +4 -1
  10. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24/src/nvidia_codec.egg-info}/PKG-INFO +19 -2
  11. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/tests/test_smoke.py +80 -0
  12. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/LICENSE +0 -0
  13. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/pyproject.toml +0 -0
  14. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/__init__.py +0 -0
  15. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/common.py +0 -0
  16. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/core/__init__.py +0 -0
  17. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/core/common.py +0 -0
  18. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/core/cuda.py +0 -0
  19. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/core/cuviddec.py +0 -0
  20. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/core/decode.py +0 -0
  21. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/core/nvcuvid.py +0 -0
  22. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/__init__.py +0 -0
  23. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/adapt.py +0 -0
  24. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/common.py +0 -0
  25. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/include/__init__.py +0 -0
  26. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/include/libavcodec.py +0 -0
  27. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/libavcodec.py +0 -0
  28. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/libavutil.py +0 -0
  29. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/test.py +0 -0
  30. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/utils/__init__.py +0 -0
  31. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/utils/color.py +0 -0
  32. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/utils/compat.py +0 -0
  33. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec.egg-info/SOURCES.txt +0 -0
  34. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec.egg-info/dependency_links.txt +0 -0
  35. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec.egg-info/requires.txt +0 -0
  36. {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/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.22
3
+ Version: 0.0.24
4
4
  Summary: Pythonic interface to Nvidia Codec
5
5
  Home-page: https://github.com/riaqn/python-nvidia-codec
6
6
  Author: Zesen Qian
@@ -124,6 +124,23 @@ Codec support depends on your GPU:
124
124
  - VPF allocates the bare minimum of resources needed for most decoding tasks. PNC allows the user to specify the amount of resources to be allocated for advanced applications. Users own the resources and decide when and whether to deal with them.
125
125
  - Managing resources is not painful: similar to `pycuda`, we shift the burden of managing host/device resources to the Python garbage collector. Resources (such as `Picture` and `Surface`) are automatically freed when the user drops the reference.
126
126
 
127
+ ## Limitations
128
+
129
+ - **One consumer per `Decoder`.** A `Decoder`/`Player` instance serves one
130
+ `frames()`, `screenshots()` or `screenshot()` call at a time. Use separate
131
+ instances (from any threads) for concurrent decodes of the same or different
132
+ files.
133
+ - **No mid-stream resolution or codec changes.** The NVDEC decoder is created
134
+ from the first sequence header; a later sequence with a different coded
135
+ size, chroma format or bit depth raises `decoder already initialized with
136
+ different format`. Split such files per segment.
137
+ - **Timestamp-less elementary streams** (e.g. raw Annex-B `.h264`/`.hevc`)
138
+ carry no pts: `frames()` yields `AV_NOPTS_VALUE` for every frame,
139
+ `pts2time` returns `None`, and `screenshot()` cannot locate a time. Mux
140
+ into a container first.
141
+ - **Embedded cover art** (`attached_pic` streams) is skipped by `Player`
142
+ and reported by `VideoTrack.attached_pic` from `parse()`.
143
+
127
144
  ## Roadmap
128
145
 
129
146
  - [x] Decoding
@@ -133,7 +150,7 @@ Codec support depends on your GPU:
133
150
  - Color Ranges: MPEG (limited), JPEG (full)
134
151
  - Color Space: BT.601, BT.709, BT.2020
135
152
  - [x] Built-in cropping and scaling via NV decoder
136
- - [x] Thread-safe and thread-friendly
153
+ - [x] Thread-friendly (one consumer per decoder; decoders are independent)
137
154
  - [ ] Encoder
138
155
  - [x] PyTorch integration
139
156
 
@@ -107,6 +107,23 @@ Codec support depends on your GPU:
107
107
  - VPF allocates the bare minimum of resources needed for most decoding tasks. PNC allows the user to specify the amount of resources to be allocated for advanced applications. Users own the resources and decide when and whether to deal with them.
108
108
  - Managing resources is not painful: similar to `pycuda`, we shift the burden of managing host/device resources to the Python garbage collector. Resources (such as `Picture` and `Surface`) are automatically freed when the user drops the reference.
109
109
 
110
+ ## Limitations
111
+
112
+ - **One consumer per `Decoder`.** A `Decoder`/`Player` instance serves one
113
+ `frames()`, `screenshots()` or `screenshot()` call at a time. Use separate
114
+ instances (from any threads) for concurrent decodes of the same or different
115
+ files.
116
+ - **No mid-stream resolution or codec changes.** The NVDEC decoder is created
117
+ from the first sequence header; a later sequence with a different coded
118
+ size, chroma format or bit depth raises `decoder already initialized with
119
+ different format`. Split such files per segment.
120
+ - **Timestamp-less elementary streams** (e.g. raw Annex-B `.h264`/`.hevc`)
121
+ carry no pts: `frames()` yields `AV_NOPTS_VALUE` for every frame,
122
+ `pts2time` returns `None`, and `screenshot()` cannot locate a time. Mux
123
+ into a container first.
124
+ - **Embedded cover art** (`attached_pic` streams) is skipped by `Player`
125
+ and reported by `VideoTrack.attached_pic` from `parse()`.
126
+
110
127
  ## Roadmap
111
128
 
112
129
  - [x] Decoding
@@ -116,7 +133,7 @@ Codec support depends on your GPU:
116
133
  - Color Ranges: MPEG (limited), JPEG (full)
117
134
  - Color Space: BT.601, BT.709, BT.2020
118
135
  - [x] Built-in cropping and scaling via NV decoder
119
- - [x] Thread-safe and thread-friendly
136
+ - [x] Thread-friendly (one consumer per decoder; decoders are independent)
120
137
  - [ ] Encoder
121
138
  - [x] PyTorch integration
122
139
 
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = nvidia-codec
3
- version = 0.0.22
3
+ version = 0.0.24
4
4
  author = Zesen Qian
5
5
  author_email = nvidia-codec@riaqn.org
6
6
  description = Pythonic interface to Nvidia Codec
@@ -19,6 +19,7 @@ class AVIndexEntry(Structure):
19
19
  return (self.flags_size >> 2) & 0x3FFFFFFF
20
20
 
21
21
  AVINDEX_KEYFRAME = 0x0001
22
+ AV_DISPOSITION_ATTACHED_PIC = 0x0400
22
23
 
23
24
  class AVStream(Structure):
24
25
  _fields_ = [
@@ -63,10 +63,12 @@ class AVColorRange(TypedCEnumeration(c_int)):
63
63
  # NB = 3
64
64
 
65
65
  class AVPixelFormat(TypedCEnumeration(c_int)):
66
- NONE = -1,
67
- RGB24 = 0,
68
- YUV420P = 1,
69
- YUV444P = 5,
70
- YUV420P16LE = 45,
66
+ # values per libavutil/pixfmt.h (ffmpeg 9); no trailing commas — they
67
+ # would make the members 1-tuples, not ints
68
+ NONE = -1
69
+ YUV420P = 0
70
+ RGB24 = 2
71
+ YUV444P = 5
72
+ YUV420P16LE = 45
71
73
  YUV444P16LE = 49
72
74
  # other omitted
@@ -3,7 +3,6 @@
3
3
  # not pyav
4
4
 
5
5
  from ctypes import *
6
- from fractions import Fraction
7
6
 
8
7
  from .include.libavutil import *
9
8
  from .include.libavcodec import *
@@ -104,15 +103,6 @@ class FormatContext:
104
103
  return result.contents
105
104
  return None
106
105
 
107
- def infer_start_time(self):
108
- # log.warning('infering start time from first packet')
109
- # in this case, we get the first packet
110
- pkt = Packet()
111
- self.read_packet(pkt)
112
- start_time = pkt.av.pts
113
- time_base = pkt.av.time_base
114
- return start_time * Fraction(time_base.num, time_base.den)
115
-
116
106
  @property
117
107
  def streams(self):
118
108
  for i in range(self.av.nb_streams):
@@ -2,6 +2,7 @@
2
2
 
3
3
  import collections
4
4
  from datetime import timedelta
5
+ from fractions import Fraction
5
6
  import numpy as np
6
7
  from queue import Queue, ShutDown
7
8
  import threading
@@ -9,6 +10,7 @@ import weakref
9
10
 
10
11
  from ..ffmpeg.libavcodec import BSFContext
11
12
  from ..ffmpeg.libavformat import FormatContext, AVCodecID
13
+ from ..ffmpeg.include.libavcodec import AV_PKT_FLAG_KEY
12
14
  from ..ffmpeg.include.libavutil import (
13
15
  AV_NOPTS_VALUE,
14
16
  AVColorRange,
@@ -180,6 +182,11 @@ class Decoder(BaseDecoder):
180
182
  coded_height=track.height,
181
183
  )
182
184
 
185
+ @property
186
+ def duration(self):
187
+ """Track duration as a timedelta, or None if the container doesn't say."""
188
+ return self.track.duration
189
+
183
190
  def pre_decode(self, idx):
184
191
  """NVDEC reclaims slot idx. Clear tracking, wait for consumer if held."""
185
192
  self._slots.pop(idx, None)
@@ -247,8 +254,7 @@ class Decoder(BaseDecoder):
247
254
  packet/frame traversal. Only if avformat cannot seek the container at all do we
248
255
  fall back to the in-memory-index path (then BOF)."""
249
256
  try:
250
- self.track.fc.seek_file(self.track.stream, target_pts,
251
- min_ts=-(2**63), max_ts=target_pts)
257
+ self._native_seek_to_keyframe(target_pts)
252
258
  except AVException:
253
259
  kts = self._keyframe_before(target_pts)
254
260
  if kts is None:
@@ -258,6 +264,48 @@ class Decoder(BaseDecoder):
258
264
  self.with_callbacks(lambda: self.send(None), post_decode=lambda pic: None)
259
265
  self._recent.clear()
260
266
 
267
+ def _native_seek_to_keyframe(self, target_pts, max_tries=16):
268
+ """avformat_seek_file(max_ts=target) positions on the last packet with
269
+ dts <= target. Indexed containers (MP4 / MKV) snap that to a keyframe.
270
+ Index-less ones (MPEG-TS / M2TS) use the generic dts bisection — which
271
+ is NOT keyframe-aware ("FIXME keyframe?" in mpegts.c) — and land
272
+ mid-GOP; NVDEC then discards everything up to the NEXT IDR, so the
273
+ frame we hand back is up to a GOP late. (ffmpeg's CLI papers over this
274
+ with a fixed 3/23 s dts heuristic.)
275
+
276
+ Peek at the landing packet; if it isn't a keyframe — OR it is one but
277
+ its PTS is past the target (dts-space seek with B-frames: a keyframe's
278
+ dts precedes its pts by video_delay frames, so for a target inside
279
+ that window the seek picks a keyframe whose first displayed frame is
280
+ already too late; the on-screen frame belongs to the previous GOP) —
281
+ back off by a growing number of frame-durations and retry. Landing on
282
+ an earlier keyframe than strictly needed only costs decode-forward
283
+ time, so overshooting backwards is safe. The peek consumes a packet,
284
+ so the final seek re-runs to restore the position. Raises AVException
285
+ if the container can't seek at all (caller falls back to the
286
+ in-memory index).
287
+ """
288
+ fc, stream = self.track.fc, self.track.stream
289
+ tb = self.track._time_base
290
+ afr = stream.avg_frame_rate
291
+ fps = Fraction(afr.num, afr.den) if afr.num > 0 and afr.den > 0 else Fraction(25)
292
+ step = max(1, int(1 / (fps * tb))) # one frame, in pts units
293
+ ts = target_pts
294
+ for _ in range(max_tries):
295
+ fc.seek_file(stream, ts, min_ts=-(2**63), max_ts=ts)
296
+ first = next(fc.read_packets(stream), None)
297
+ if first is None:
298
+ break # EOF — nothing to fix
299
+ pts_ok = first.av.pts == AV_NOPTS_VALUE or first.av.pts <= target_pts
300
+ if first.av.flags & AV_PKT_FLAG_KEY and pts_ok:
301
+ break # keyframe that displays at/before target
302
+ landed = first.av.dts if first.av.dts != AV_NOPTS_VALUE else first.av.pts
303
+ if landed == AV_NOPTS_VALUE:
304
+ break # can't reason about position
305
+ ts = min(ts, landed) - step
306
+ step *= 2
307
+ fc.seek_file(stream, ts, min_ts=-(2**63), max_ts=ts)
308
+
261
309
  def _map_and_convert(self, pic, dtype, pixel_format='rgb'):
262
310
  """Map a Picture (or OwnedPicture) to a tensor. Frees pic and surface.
263
311
  Returns (timedelta, frame) on success, (timedelta, exception) on error.
@@ -10,6 +10,7 @@ from fractions import Fraction
10
10
  import functools
11
11
 
12
12
  from ..ffmpeg.libavformat import FormatContext, AVMediaType, AVCodecID
13
+ from ..ffmpeg.include.libavformat import AV_DISPOSITION_ATTACHED_PIC
13
14
  from ..ffmpeg.include.libavutil import (
14
15
  AV_NOPTS_VALUE,
15
16
  AV_TIME_BASE,
@@ -36,6 +37,7 @@ class VideoTrack:
36
37
  extradata: Codec extradata bytes, or None.
37
38
  color_space: AVColorSpace enum value.
38
39
  color_range: AVColorRange enum value.
40
+ attached_pic: True if this is embedded cover art, not real video.
39
41
  """
40
42
 
41
43
  def __init__(self, fc, stream):
@@ -51,20 +53,33 @@ class VideoTrack:
51
53
  # Time base
52
54
  self._time_base = Fraction(stream.time_base.num, stream.time_base.den)
53
55
 
54
- # Start time
56
+ # Start time. Lazy containers (MPEG-TS, raw elementary streams) leave
57
+ # both the stream and the format start_time unset until probed;
58
+ # probing buffers the packets it reads, so nothing is lost. (The old
59
+ # path read the first packet directly — which both computed 0 via the
60
+ # packet's unset 0/1 time_base AND swallowed the IDR, dropping the
61
+ # whole first GOP from decode.)
55
62
  self._start_time = stream.start_time
56
63
  if self._start_time == AV_NOPTS_VALUE:
57
- self._start_time = fc.av.start_time
58
- if self._start_time == AV_NOPTS_VALUE:
59
- start_time = fc.infer_start_time()
60
- self._start_time = int(start_time / self._time_base)
61
- else:
64
+ fc.probe()
65
+ self._start_time = stream.start_time
66
+ if self._start_time == AV_NOPTS_VALUE:
67
+ if fc.av.start_time != AV_NOPTS_VALUE:
62
68
  self._start_time = int(
63
- self._start_time / AV_TIME_BASE / self._time_base
69
+ fc.av.start_time / AV_TIME_BASE / self._time_base
64
70
  )
71
+ else:
72
+ # Timestamp-less stream (e.g. raw Annex-B .h264): packets carry
73
+ # no pts at all; frames() yields AV_NOPTS_VALUE and pts2time
74
+ # returns None. Anchor at 0 so time2pts is at least defined.
75
+ self._start_time = 0
65
76
 
66
- # Duration (lazy — probes only if needed)
67
77
 
78
+ @property
79
+ def attached_pic(self):
80
+ """True for cover-art / thumbnail "video" streams (AV_DISPOSITION_ATTACHED_PIC):
81
+ a single still image, not a decodable video track."""
82
+ return bool(self.stream.disposition & AV_DISPOSITION_ATTACHED_PIC)
68
83
 
69
84
  @functools.cached_property
70
85
  def width(self):
@@ -231,7 +246,12 @@ class VideoTrack:
231
246
  return default if r == AVColorRange.UNSPECIFIED else r
232
247
 
233
248
  def time2pts(self, time: timedelta):
234
- return int(time.total_seconds() / self._time_base) + self._start_time
249
+ """timedelta -> track pts (floor). Exact rational arithmetic: timedelta has
250
+ microsecond resolution, so 1.16 s is 1160000 us and lands on pts 14848 in a
251
+ 1/12800 base; via float it is 14847.999... and int() would floor to the
252
+ PREVIOUS frame for any target exactly on a frame boundary."""
253
+ us = time // timedelta(microseconds=1)
254
+ return int(Fraction(us, 1_000_000) / self._time_base) + self._start_time
235
255
 
236
256
  def pts2time(self, pts: int):
237
257
  if pts == AV_NOPTS_VALUE:
@@ -22,7 +22,10 @@ class Player(Decoder):
22
22
  track_idx=None,
23
23
  **decoder_kwargs,
24
24
  ):
25
- tracks = [t for t in parse(url) if isinstance(t, VideoTrack)]
25
+ # Cover art (attached_pic) is a "video" stream to ffmpeg but a single
26
+ # still image; never pick it, and don't count it as ambiguity.
27
+ tracks = [t for t in parse(url)
28
+ if isinstance(t, VideoTrack) and not t.attached_pic]
26
29
  if not tracks:
27
30
  raise ValueError(f"{url} has no video stream")
28
31
  if track_idx is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nvidia-codec
3
- Version: 0.0.22
3
+ Version: 0.0.24
4
4
  Summary: Pythonic interface to Nvidia Codec
5
5
  Home-page: https://github.com/riaqn/python-nvidia-codec
6
6
  Author: Zesen Qian
@@ -124,6 +124,23 @@ Codec support depends on your GPU:
124
124
  - VPF allocates the bare minimum of resources needed for most decoding tasks. PNC allows the user to specify the amount of resources to be allocated for advanced applications. Users own the resources and decide when and whether to deal with them.
125
125
  - Managing resources is not painful: similar to `pycuda`, we shift the burden of managing host/device resources to the Python garbage collector. Resources (such as `Picture` and `Surface`) are automatically freed when the user drops the reference.
126
126
 
127
+ ## Limitations
128
+
129
+ - **One consumer per `Decoder`.** A `Decoder`/`Player` instance serves one
130
+ `frames()`, `screenshots()` or `screenshot()` call at a time. Use separate
131
+ instances (from any threads) for concurrent decodes of the same or different
132
+ files.
133
+ - **No mid-stream resolution or codec changes.** The NVDEC decoder is created
134
+ from the first sequence header; a later sequence with a different coded
135
+ size, chroma format or bit depth raises `decoder already initialized with
136
+ different format`. Split such files per segment.
137
+ - **Timestamp-less elementary streams** (e.g. raw Annex-B `.h264`/`.hevc`)
138
+ carry no pts: `frames()` yields `AV_NOPTS_VALUE` for every frame,
139
+ `pts2time` returns `None`, and `screenshot()` cannot locate a time. Mux
140
+ into a container first.
141
+ - **Embedded cover art** (`attached_pic` streams) is skipped by `Player`
142
+ and reported by `VideoTrack.attached_pic` from `parse()`.
143
+
127
144
  ## Roadmap
128
145
 
129
146
  - [x] Decoding
@@ -133,7 +150,7 @@ Codec support depends on your GPU:
133
150
  - Color Ranges: MPEG (limited), JPEG (full)
134
151
  - Color Space: BT.601, BT.709, BT.2020
135
152
  - [x] Built-in cropping and scaling via NV decoder
136
- - [x] Thread-safe and thread-friendly
153
+ - [x] Thread-friendly (one consumer per decoder; decoders are independent)
137
154
  - [ ] Encoder
138
155
  - [x] PyTorch integration
139
156
 
@@ -86,6 +86,86 @@ def test_screenshots(clip):
86
86
  assert frame.shape == (3, H, W)
87
87
 
88
88
 
89
+ def test_duration_property(clip):
90
+ with Player(clip) as p:
91
+ assert abs(p.duration.total_seconds() - SECONDS) < 0.2
92
+
93
+
94
+ def test_cover_art_skipped(clip):
95
+ """A PNG attached_pic stream must not be selected or count as ambiguity."""
96
+ d = tempfile.mkdtemp(prefix="nvc-smoke-cover-")
97
+ try:
98
+ png = os.path.join(d, "cover.png")
99
+ out = os.path.join(d, "cover.mp4")
100
+ subprocess.run(["ffmpeg", "-v", "error", "-y", "-f", "lavfi",
101
+ "-i", "color=c=red:size=64x64:duration=1", "-frames:v", "1", png],
102
+ check=True, timeout=60)
103
+ subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", clip, "-i", png,
104
+ "-map", "0", "-map", "1", "-c", "copy",
105
+ "-disposition:v:1", "attached_pic", out], check=True, timeout=60)
106
+ vids = [t for t in parse(out) if isinstance(t, VideoTrack)]
107
+ assert [t.attached_pic for t in vids] == [False, True]
108
+ with Player(out) as p:
109
+ assert p.track.index == 0
110
+ n = sum(1 for _ in p.frames(torch.uint8))
111
+ assert n == FPS * SECONDS
112
+ finally:
113
+ shutil.rmtree(d, ignore_errors=True)
114
+
115
+
116
+ def test_mpegts_start_time_and_first_gop(clip):
117
+ """MPEG-TS leaves start_time unset until probed. Probing must not swallow
118
+ the IDR (which used to drop the first GOP) and start_time must be real."""
119
+ d = tempfile.mkdtemp(prefix="nvc-smoke-ts-")
120
+ try:
121
+ out = os.path.join(d, "clip.ts")
122
+ subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", clip, "-c", "copy", out],
123
+ check=True, timeout=60)
124
+ with Player(out) as p:
125
+ assert p.track._start_time > 0 # ffmpeg's TS muxer starts at 1.4 s
126
+ frames = list(p.frames(torch.uint8))
127
+ assert len(frames) == FPS * SECONDS
128
+ first_t = p.track.pts2time(frames[0][0])
129
+ assert abs(first_t.total_seconds()) < 1 / FPS
130
+ # TS seeks by dts with no keyframe awareness; the decoder must still
131
+ # produce the frame actually on screen at 1.0 s, not the next IDR.
132
+ t, _ = p.screenshot(timedelta(seconds=1), torch.uint8, accurate=True)
133
+ assert abs(t.total_seconds() - 1.0) < 1 / FPS + 1e-3
134
+ pts0, _ = next(iter(p.frames(torch.uint8, start=timedelta(seconds=1))))
135
+ assert abs(p.track.pts2time(pts0).total_seconds() - 1.0) < 1 / FPS + 1e-3
136
+ # DTS-vs-PTS window: with B-frames the GOP-2 keyframe (pts 0.96 s) has
137
+ # dts ~0.88 s. A target in between must still yield the previous GOP's
138
+ # frame, not the keyframe (which displays after the target).
139
+ for target in (0.90, 0.94):
140
+ expect = int(target * FPS + 1e-6) / FPS
141
+ t, _ = p.screenshot(timedelta(seconds=target), torch.uint8, accurate=True)
142
+ assert abs(t.total_seconds() - expect) < 1e-3, (target, t)
143
+ pts0, _ = next(iter(p.frames(torch.uint8, start=timedelta(seconds=target))))
144
+ assert abs(p.track.pts2time(pts0).total_seconds() - expect) < 1e-3, (target, pts0)
145
+ finally:
146
+ shutil.rmtree(d, ignore_errors=True)
147
+
148
+
149
+ def test_time2pts_exact_on_frame_boundaries(clip):
150
+ """time2pts must not floor a target that is exactly on a frame boundary to
151
+ the previous frame (float 1.16 * 12800 = 14847.999...)."""
152
+ with Player(clip) as p:
153
+ tb = p.track._time_base
154
+ for i in range(FPS * SECONDS):
155
+ td = timedelta(seconds=i / FPS)
156
+ expect = int(round(float(td.total_seconds() / tb))) + p.track._start_time
157
+ assert p.track.time2pts(td) == expect, i
158
+ # between two frames the pts lands strictly between their pts values
159
+ a, b = p.track.time2pts(timedelta(seconds=1.16)), p.track.time2pts(timedelta(seconds=1.20))
160
+ assert a < p.track.time2pts(timedelta(seconds=1.17)) < b
161
+
162
+
163
+ def test_pixel_format_enum_values():
164
+ from nvidia_codec.ffmpeg.include.libavutil import AVPixelFormat
165
+ assert AVPixelFormat.YUV420P == 0 and AVPixelFormat.RGB24 == 2
166
+ assert isinstance(AVPixelFormat.YUV444P, int)
167
+
168
+
89
169
  def _has_encoder(name):
90
170
  out = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
91
171
  capture_output=True, text=True, timeout=30).stdout
File without changes