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.
- {nvidia_codec-0.0.22/src/nvidia_codec.egg-info → nvidia_codec-0.0.24}/PKG-INFO +19 -2
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/README.md +18 -1
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/setup.cfg +1 -1
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/include/libavformat.py +1 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/include/libavutil.py +7 -5
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/libavformat.py +0 -10
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/utils/decode.py +50 -2
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/utils/demux.py +29 -9
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/utils/player.py +4 -1
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24/src/nvidia_codec.egg-info}/PKG-INFO +19 -2
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/tests/test_smoke.py +80 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/LICENSE +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/pyproject.toml +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/__init__.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/common.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/core/__init__.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/core/common.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/core/cuda.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/core/cuviddec.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/core/decode.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/core/nvcuvid.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/__init__.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/adapt.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/common.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/include/__init__.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/include/libavcodec.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/libavcodec.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/libavutil.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/ffmpeg/test.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/utils/__init__.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/utils/color.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec/utils/compat.py +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec.egg-info/SOURCES.txt +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec.egg-info/dependency_links.txt +0 -0
- {nvidia_codec-0.0.22 → nvidia_codec-0.0.24}/src/nvidia_codec.egg-info/requires.txt +0 -0
- {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.
|
|
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-
|
|
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-
|
|
136
|
+
- [x] Thread-friendly (one consumer per decoder; decoders are independent)
|
|
120
137
|
- [ ] Encoder
|
|
121
138
|
- [x] PyTorch integration
|
|
122
139
|
|
|
@@ -63,10 +63,12 @@ class AVColorRange(TypedCEnumeration(c_int)):
|
|
|
63
63
|
# NB = 3
|
|
64
64
|
|
|
65
65
|
class AVPixelFormat(TypedCEnumeration(c_int)):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|