@desklink/host 0.2.0 → 0.3.1
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.
- package/NOTICE +26 -0
- package/README.md +134 -56
- package/bin/desklink-host.mjs +13 -5
- package/dist/bridge.d.ts +1 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +12 -4
- package/dist/bridge.js.map +1 -1
- package/dist/engineProcess.d.ts +6 -1
- package/dist/engineProcess.d.ts.map +1 -1
- package/dist/engineProcess.js +35 -4
- package/dist/engineProcess.js.map +1 -1
- package/dist/protocol.d.ts +58 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js.map +1 -1
- package/dist/resolveEngine.d.ts +7 -1
- package/dist/resolveEngine.d.ts.map +1 -1
- package/dist/resolveEngine.js +66 -14
- package/dist/resolveEngine.js.map +1 -1
- package/docs/BOUNDARY.md +15 -7
- package/docs/PROTOCOL.md +267 -27
- package/engine/Cargo.lock +47 -2
- package/engine/Cargo.toml +18 -13
- package/engine/build.rs +86 -4
- package/engine/native/agent_overlay_mac.m +51 -4
- package/engine/native/agent_overlay_wayland.c +145 -12
- package/engine/native/agent_overlay_x11.c +72 -0
- package/engine/native/nvenc_shim.c +453 -0
- package/engine/native/vaapi_shim.c +727 -0
- package/engine/native/vpx_shim.c +54 -4
- package/engine/native/vt_shim.c +448 -0
- package/engine/src/capture.rs +167 -58
- package/engine/src/capture_mac.rs +24 -14
- package/engine/src/capture_win.rs +23 -0
- package/engine/src/clipboard_win.rs +8 -0
- package/engine/src/convert.rs +286 -61
- package/engine/src/cursor.rs +118 -0
- package/engine/src/encoder.rs +122 -13
- package/engine/src/h264.rs +188 -0
- package/engine/src/h264_nvenc.rs +308 -0
- package/engine/src/h264_openh264.rs +735 -0
- package/engine/src/h264_vaapi.rs +263 -0
- package/engine/src/h264_vt.rs +368 -0
- package/engine/src/indicator.rs +104 -3
- package/engine/src/input_win.rs +91 -0
- package/engine/src/keymap_win.rs +39 -0
- package/engine/src/mac.rs +34 -9
- package/engine/src/main.rs +186 -26
- package/engine/src/peer.rs +466 -28
- package/engine/src/portal.rs +42 -4
- package/engine/src/portal_win.rs +37 -0
- package/engine/src/protocol.rs +64 -2
- package/engine/src/session.rs +1252 -105
- package/engine/src/win.rs +110 -0
- package/engine/src/x11.rs +225 -9
- package/engine/src/x11_mac.rs +12 -0
- package/engine/vendor/libva/COPYING +19 -0
- package/engine/vendor/libva/va/va.h +5450 -0
- package/engine/vendor/libva/va/va_dec_av1.h +695 -0
- package/engine/vendor/libva/va/va_dec_hevc.h +616 -0
- package/engine/vendor/libva/va/va_dec_jpeg.h +157 -0
- package/engine/vendor/libva/va/va_dec_vp8.h +249 -0
- package/engine/vendor/libva/va/va_dec_vp9.h +312 -0
- package/engine/vendor/libva/va/va_dec_vvc.h +688 -0
- package/engine/vendor/libva/va/va_drm.h +61 -0
- package/engine/vendor/libva/va/va_enc_av1.h +1010 -0
- package/engine/vendor/libva/va/va_enc_h264.h +659 -0
- package/engine/vendor/libva/va/va_enc_hevc.h +958 -0
- package/engine/vendor/libva/va/va_enc_jpeg.h +162 -0
- package/engine/vendor/libva/va/va_enc_mpeg2.h +308 -0
- package/engine/vendor/libva/va/va_enc_vp8.h +350 -0
- package/engine/vendor/libva/va/va_enc_vp9.h +639 -0
- package/engine/vendor/libva/va/va_fei.h +170 -0
- package/engine/vendor/libva/va/va_fei_h264.h +505 -0
- package/engine/vendor/libva/va/va_fei_hevc.h +254 -0
- package/engine/vendor/libva/va/va_prot.h +525 -0
- package/engine/vendor/libva/va/va_version.h +87 -0
- package/engine/vendor/libva/va/va_vpp.h +1590 -0
- package/engine/vendor/nv-codec-headers/LICENSE +24 -0
- package/engine/vendor/nv-codec-headers/README +7 -0
- package/engine/vendor/nv-codec-headers/include/ffnvcodec/nvEncodeAPI.h +3825 -0
- package/examples/reference-client.html +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Thin C surface over NVIDIA's NVENC H.264 encoder.
|
|
3
|
+
*
|
|
4
|
+
* NVENC is configured through large versioned structs from nvEncodeAPI.h;
|
|
5
|
+
* doing that configuration in C, where the compiler and the header agree,
|
|
6
|
+
* keeps a wrong offset from producing a broken stream. The Rust side sees an
|
|
7
|
+
* opaque handle and a byte slice.
|
|
8
|
+
*
|
|
9
|
+
* Nothing is linked: libcuda and libnvidia-encode ship with the NVIDIA driver,
|
|
10
|
+
* so both are opened with dlopen when an encoder is created, and a machine
|
|
11
|
+
* without them only gets an error back. The header is nv-codec-headers' copy
|
|
12
|
+
* of the Video Codec SDK 10.0 API (MIT, vendored under vendor/), the oldest
|
|
13
|
+
* with the P1-P7 presets and tuning info; any Linux driver from 450.51 on
|
|
14
|
+
* accepts it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
#include <dlfcn.h>
|
|
18
|
+
#include <pthread.h>
|
|
19
|
+
#include <stdarg.h>
|
|
20
|
+
#include <stdint.h>
|
|
21
|
+
#include <stdio.h>
|
|
22
|
+
#include <stdlib.h>
|
|
23
|
+
#include <string.h>
|
|
24
|
+
|
|
25
|
+
#include <ffnvcodec/nvEncodeAPI.h>
|
|
26
|
+
|
|
27
|
+
/* The CUDA driver API calls used here, declared rather than included: the
|
|
28
|
+
* five signatures are stable ABI, and the SDK header is not redistributable. */
|
|
29
|
+
typedef int CUresult;
|
|
30
|
+
typedef int CUdevice;
|
|
31
|
+
typedef void *CUcontext;
|
|
32
|
+
|
|
33
|
+
typedef struct {
|
|
34
|
+
CUcontext cuda;
|
|
35
|
+
CUresult (*cu_ctx_push)(CUcontext);
|
|
36
|
+
CUresult (*cu_ctx_pop)(CUcontext *);
|
|
37
|
+
CUresult (*cu_primary_release)(CUdevice);
|
|
38
|
+
CUdevice device;
|
|
39
|
+
NV_ENCODE_API_FUNCTION_LIST api;
|
|
40
|
+
void *session;
|
|
41
|
+
NV_ENC_INITIALIZE_PARAMS init;
|
|
42
|
+
NV_ENC_CONFIG config;
|
|
43
|
+
NV_ENC_INPUT_PTR input;
|
|
44
|
+
NV_ENC_OUTPUT_PTR output;
|
|
45
|
+
int width;
|
|
46
|
+
int height;
|
|
47
|
+
uint64_t frame_index;
|
|
48
|
+
/* The last access unit, copied out so the bitstream buffer can be unlocked
|
|
49
|
+
* before encode returns. */
|
|
50
|
+
uint8_t *packet;
|
|
51
|
+
size_t packet_size;
|
|
52
|
+
size_t packet_capacity;
|
|
53
|
+
int packet_is_key;
|
|
54
|
+
char error[256];
|
|
55
|
+
} dl_nvenc_encoder;
|
|
56
|
+
|
|
57
|
+
static void fail(char *error, size_t len, const char *fmt, ...) {
|
|
58
|
+
if (error == NULL || len == 0) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
va_list args;
|
|
62
|
+
va_start(args, fmt);
|
|
63
|
+
vsnprintf(error, len, fmt, args);
|
|
64
|
+
va_end(args);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static const char *nvenc_reason(dl_nvenc_encoder *self) {
|
|
68
|
+
const char *text = NULL;
|
|
69
|
+
if (self->session != NULL && self->api.nvEncGetLastErrorString != NULL) {
|
|
70
|
+
text = self->api.nvEncGetLastErrorString(self->session);
|
|
71
|
+
}
|
|
72
|
+
return text != NULL && text[0] != '\0' ? text : "no detail from the driver";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
static void destroy(dl_nvenc_encoder *self);
|
|
76
|
+
|
|
77
|
+
/* Rate control for `bitrate_kbps`, with a quantizer ceiling when `max_qp` > 0.
|
|
78
|
+
* CBR with a one-second buffer, like the VP9 path: a single-frame buffer
|
|
79
|
+
* would starve every IDR, and a desktop's IDRs are rare and must be legible. */
|
|
80
|
+
static void set_rate(dl_nvenc_encoder *self, int bitrate_kbps, int max_qp) {
|
|
81
|
+
NV_ENC_RC_PARAMS *rc = &self->config.rcParams;
|
|
82
|
+
uint32_t bits = (uint32_t)bitrate_kbps * 1000u;
|
|
83
|
+
rc->rateControlMode = NV_ENC_PARAMS_RC_CBR;
|
|
84
|
+
rc->averageBitRate = bits;
|
|
85
|
+
rc->maxBitRate = bits;
|
|
86
|
+
rc->vbvBufferSize = bits;
|
|
87
|
+
rc->vbvInitialDelay = bits / 2;
|
|
88
|
+
/* The ceiling stays enabled and is lifted by raising it to 51: the 610
|
|
89
|
+
* driver ignores enableMaxQP going back to 0 and keeps the last cap. */
|
|
90
|
+
rc->enableMaxQP = 1;
|
|
91
|
+
rc->maxQP.qpInterP = rc->maxQP.qpInterB = rc->maxQP.qpIntra =
|
|
92
|
+
max_qp > 0 ? (uint32_t)max_qp : 51;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Opening and closing sessions is serialized process-wide. With two sessions
|
|
96
|
+
* set up or torn down at the same time, the 610 driver was seen to fault later
|
|
97
|
+
* in one of its worker threads in about half of the runs. Serialized, a probe
|
|
98
|
+
* beside a running stream (the engine's case: one session at a time, one
|
|
99
|
+
* cached probe) did not fault in 35 runs; several threads churning sessions
|
|
100
|
+
* at once still faulted in a few runs in 25, so this narrows a driver bug the
|
|
101
|
+
* engine does not otherwise reach rather than fixing it. */
|
|
102
|
+
static pthread_mutex_t lifecycle = PTHREAD_MUTEX_INITIALIZER;
|
|
103
|
+
|
|
104
|
+
static dl_nvenc_encoder *create(int width, int height, int bitrate_kbps, int fps,
|
|
105
|
+
char *error, size_t error_len) {
|
|
106
|
+
if (width <= 0 || height <= 0 || (width | height) & 1 || bitrate_kbps <= 0 || fps <= 0) {
|
|
107
|
+
fail(error, error_len, "invalid geometry or rate");
|
|
108
|
+
return NULL;
|
|
109
|
+
}
|
|
110
|
+
dl_nvenc_encoder *self = calloc(1, sizeof(dl_nvenc_encoder));
|
|
111
|
+
if (self == NULL) {
|
|
112
|
+
fail(error, error_len, "out of memory");
|
|
113
|
+
return NULL;
|
|
114
|
+
}
|
|
115
|
+
self->width = width;
|
|
116
|
+
self->height = height;
|
|
117
|
+
|
|
118
|
+
void *cuda_lib = dlopen("libcuda.so.1", RTLD_NOW | RTLD_LOCAL);
|
|
119
|
+
if (cuda_lib == NULL) {
|
|
120
|
+
fail(error, error_len, "libcuda.so.1 not found (no NVIDIA driver)");
|
|
121
|
+
goto failed;
|
|
122
|
+
}
|
|
123
|
+
CUresult (*cu_init)(unsigned) = (CUresult(*)(unsigned))dlsym(cuda_lib, "cuInit");
|
|
124
|
+
CUresult (*cu_device_get)(CUdevice *, int) =
|
|
125
|
+
(CUresult(*)(CUdevice *, int))dlsym(cuda_lib, "cuDeviceGet");
|
|
126
|
+
CUresult (*cu_primary_retain)(CUcontext *, CUdevice) =
|
|
127
|
+
(CUresult(*)(CUcontext *, CUdevice))dlsym(cuda_lib, "cuDevicePrimaryCtxRetain");
|
|
128
|
+
self->cu_ctx_push = (CUresult(*)(CUcontext))dlsym(cuda_lib, "cuCtxPushCurrent_v2");
|
|
129
|
+
self->cu_ctx_pop = (CUresult(*)(CUcontext *))dlsym(cuda_lib, "cuCtxPopCurrent_v2");
|
|
130
|
+
self->cu_primary_release =
|
|
131
|
+
(CUresult(*)(CUdevice))dlsym(cuda_lib, "cuDevicePrimaryCtxRelease_v2");
|
|
132
|
+
if (!cu_init || !cu_device_get || !cu_primary_retain || !self->cu_ctx_push ||
|
|
133
|
+
!self->cu_ctx_pop || !self->cu_primary_release) {
|
|
134
|
+
fail(error, error_len, "libcuda.so.1 lacks the CUDA driver API");
|
|
135
|
+
goto failed;
|
|
136
|
+
}
|
|
137
|
+
/* The device's primary context, shared by every session in the process,
|
|
138
|
+
* made current around each call since the encoder moves between threads. */
|
|
139
|
+
CUresult cu = cu_init(0);
|
|
140
|
+
if (cu == 0) {
|
|
141
|
+
cu = cu_device_get(&self->device, 0);
|
|
142
|
+
}
|
|
143
|
+
if (cu == 0) {
|
|
144
|
+
cu = cu_primary_retain(&self->cuda, self->device);
|
|
145
|
+
}
|
|
146
|
+
if (cu != 0) {
|
|
147
|
+
self->cuda = NULL;
|
|
148
|
+
fail(error, error_len, "no usable CUDA device (CUDA error %d)", cu);
|
|
149
|
+
goto failed;
|
|
150
|
+
}
|
|
151
|
+
void *enc_lib = dlopen("libnvidia-encode.so.1", RTLD_NOW | RTLD_LOCAL);
|
|
152
|
+
if (enc_lib == NULL) {
|
|
153
|
+
fail(error, error_len, "libnvidia-encode.so.1 not found");
|
|
154
|
+
goto failed;
|
|
155
|
+
}
|
|
156
|
+
NVENCSTATUS (*max_version)(uint32_t *) =
|
|
157
|
+
(NVENCSTATUS(*)(uint32_t *))dlsym(enc_lib, "NvEncodeAPIGetMaxSupportedVersion");
|
|
158
|
+
NVENCSTATUS (*create_instance)(NV_ENCODE_API_FUNCTION_LIST *) =
|
|
159
|
+
(NVENCSTATUS(*)(NV_ENCODE_API_FUNCTION_LIST *))dlsym(enc_lib,
|
|
160
|
+
"NvEncodeAPICreateInstance");
|
|
161
|
+
uint32_t supported = 0;
|
|
162
|
+
if (!max_version || !create_instance || max_version(&supported) != NV_ENC_SUCCESS) {
|
|
163
|
+
fail(error, error_len, "libnvidia-encode.so.1 is not a usable NVENC library");
|
|
164
|
+
goto failed;
|
|
165
|
+
}
|
|
166
|
+
if (supported < ((NVENCAPI_MAJOR_VERSION << 4) | NVENCAPI_MINOR_VERSION)) {
|
|
167
|
+
fail(error, error_len, "the NVIDIA driver supports NVENC API %u.%u; %d.%d is needed",
|
|
168
|
+
supported >> 4, supported & 15, NVENCAPI_MAJOR_VERSION, NVENCAPI_MINOR_VERSION);
|
|
169
|
+
goto failed;
|
|
170
|
+
}
|
|
171
|
+
self->api.version = NV_ENCODE_API_FUNCTION_LIST_VER;
|
|
172
|
+
if (create_instance(&self->api) != NV_ENC_SUCCESS) {
|
|
173
|
+
fail(error, error_len, "NvEncodeAPICreateInstance failed");
|
|
174
|
+
goto failed;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (self->cu_ctx_push(self->cuda) != 0) {
|
|
178
|
+
fail(error, error_len, "the CUDA context could not be made current");
|
|
179
|
+
goto failed;
|
|
180
|
+
}
|
|
181
|
+
NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS open = {0};
|
|
182
|
+
open.version = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER;
|
|
183
|
+
open.deviceType = NV_ENC_DEVICE_TYPE_CUDA;
|
|
184
|
+
open.device = self->cuda;
|
|
185
|
+
open.apiVersion = NVENCAPI_VERSION;
|
|
186
|
+
NVENCSTATUS status = self->api.nvEncOpenEncodeSessionEx(&open, &self->session);
|
|
187
|
+
if (status != NV_ENC_SUCCESS) {
|
|
188
|
+
/* Consumer GPUs cap concurrent sessions; this is where that shows. */
|
|
189
|
+
fail(error, error_len, "NVENC refused a session (status %d)", (int)status);
|
|
190
|
+
self->session = NULL;
|
|
191
|
+
goto failed_pushed;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* P3 under ultra-low-latency tuning codes 4K60 within a frame interval
|
|
195
|
+
* with room to spare; the higher presets add little on screen content. */
|
|
196
|
+
NV_ENC_PRESET_CONFIG preset = {0};
|
|
197
|
+
preset.version = NV_ENC_PRESET_CONFIG_VER;
|
|
198
|
+
preset.presetCfg.version = NV_ENC_CONFIG_VER;
|
|
199
|
+
status = self->api.nvEncGetEncodePresetConfigEx(self->session, NV_ENC_CODEC_H264_GUID,
|
|
200
|
+
NV_ENC_PRESET_P3_GUID,
|
|
201
|
+
NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY, &preset);
|
|
202
|
+
if (status != NV_ENC_SUCCESS) {
|
|
203
|
+
fail(error, error_len, "NVENC has no H.264 preset (status %d): %s", (int)status, nvenc_reason(self));
|
|
204
|
+
goto failed_pushed;
|
|
205
|
+
}
|
|
206
|
+
self->config = preset.presetCfg;
|
|
207
|
+
self->config.version = NV_ENC_CONFIG_VER;
|
|
208
|
+
self->config.profileGUID = NV_ENC_H264_PROFILE_BASELINE_GUID;
|
|
209
|
+
/* IDRs only when asked for; P frames only, so nothing is ever reordered. */
|
|
210
|
+
self->config.gopLength = NVENC_INFINITE_GOPLENGTH;
|
|
211
|
+
self->config.frameIntervalP = 1;
|
|
212
|
+
set_rate(self, bitrate_kbps, 0);
|
|
213
|
+
self->config.rcParams.enableLookahead = 0;
|
|
214
|
+
self->config.rcParams.zeroReorderDelay = 1;
|
|
215
|
+
NV_ENC_CONFIG_H264 *h264 = &self->config.encodeCodecConfig.h264Config;
|
|
216
|
+
h264->idrPeriod = NVENC_INFINITE_GOPLENGTH;
|
|
217
|
+
h264->repeatSPSPPS = 1;
|
|
218
|
+
h264->disableSPSPPS = 0;
|
|
219
|
+
h264->outputAUD = 0;
|
|
220
|
+
h264->enableIntraRefresh = 0;
|
|
221
|
+
h264->sliceMode = 0;
|
|
222
|
+
h264->sliceModeData = 0;
|
|
223
|
+
h264->level = NV_ENC_LEVEL_AUTOSELECT;
|
|
224
|
+
h264->chromaFormatIDC = 1;
|
|
225
|
+
/* Constrained Baseline: CAVLC and no 8x8 transform. */
|
|
226
|
+
h264->entropyCodingMode = NV_ENC_H264_ENTROPY_CODING_MODE_CAVLC;
|
|
227
|
+
h264->adaptiveTransformMode = NV_ENC_H264_ADAPTIVE_TRANSFORM_DISABLE;
|
|
228
|
+
|
|
229
|
+
NV_ENC_INITIALIZE_PARAMS *init = &self->init;
|
|
230
|
+
init->version = NV_ENC_INITIALIZE_PARAMS_VER;
|
|
231
|
+
init->encodeGUID = NV_ENC_CODEC_H264_GUID;
|
|
232
|
+
init->presetGUID = NV_ENC_PRESET_P3_GUID;
|
|
233
|
+
init->tuningInfo = NV_ENC_TUNING_INFO_ULTRA_LOW_LATENCY;
|
|
234
|
+
/* Any even size: NVENC pads to macroblocks and writes the SPS crop. */
|
|
235
|
+
init->encodeWidth = init->darWidth = init->maxEncodeWidth = (uint32_t)width;
|
|
236
|
+
init->encodeHeight = init->darHeight = init->maxEncodeHeight = (uint32_t)height;
|
|
237
|
+
init->frameRateNum = (uint32_t)fps;
|
|
238
|
+
init->frameRateDen = 1;
|
|
239
|
+
init->enablePTD = 1;
|
|
240
|
+
init->enableEncodeAsync = 0;
|
|
241
|
+
init->encodeConfig = &self->config;
|
|
242
|
+
status = self->api.nvEncInitializeEncoder(self->session, init);
|
|
243
|
+
if (status != NV_ENC_SUCCESS) {
|
|
244
|
+
fail(error, error_len, "NVENC refused %dx%d H.264: %s", width, height,
|
|
245
|
+
nvenc_reason(self));
|
|
246
|
+
goto failed_pushed;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
NV_ENC_CREATE_INPUT_BUFFER input = {0};
|
|
250
|
+
input.version = NV_ENC_CREATE_INPUT_BUFFER_VER;
|
|
251
|
+
input.width = (uint32_t)width;
|
|
252
|
+
input.height = (uint32_t)height;
|
|
253
|
+
input.bufferFmt = NV_ENC_BUFFER_FORMAT_IYUV;
|
|
254
|
+
status = self->api.nvEncCreateInputBuffer(self->session, &input);
|
|
255
|
+
if (status != NV_ENC_SUCCESS) {
|
|
256
|
+
fail(error, error_len, "NVENC input buffer (status %d): %s", (int)status, nvenc_reason(self));
|
|
257
|
+
goto failed_pushed;
|
|
258
|
+
}
|
|
259
|
+
self->input = input.inputBuffer;
|
|
260
|
+
NV_ENC_CREATE_BITSTREAM_BUFFER output = {0};
|
|
261
|
+
output.version = NV_ENC_CREATE_BITSTREAM_BUFFER_VER;
|
|
262
|
+
status = self->api.nvEncCreateBitstreamBuffer(self->session, &output);
|
|
263
|
+
if (status != NV_ENC_SUCCESS) {
|
|
264
|
+
fail(error, error_len, "NVENC bitstream buffer (status %d): %s", (int)status, nvenc_reason(self));
|
|
265
|
+
goto failed_pushed;
|
|
266
|
+
}
|
|
267
|
+
self->output = output.bitstreamBuffer;
|
|
268
|
+
self->cu_ctx_pop(NULL);
|
|
269
|
+
return self;
|
|
270
|
+
|
|
271
|
+
failed_pushed:
|
|
272
|
+
self->cu_ctx_pop(NULL);
|
|
273
|
+
failed:
|
|
274
|
+
destroy(self);
|
|
275
|
+
return NULL;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/* Copy tightly packed I420 into the locked input buffer, whose rows are
|
|
279
|
+
* `pitch` bytes for luma and half that for chroma, planes back to back. */
|
|
280
|
+
static int upload(dl_nvenc_encoder *self, const uint8_t *i420) {
|
|
281
|
+
NV_ENC_LOCK_INPUT_BUFFER lock = {0};
|
|
282
|
+
lock.version = NV_ENC_LOCK_INPUT_BUFFER_VER;
|
|
283
|
+
lock.inputBuffer = self->input;
|
|
284
|
+
NVENCSTATUS status = self->api.nvEncLockInputBuffer(self->session, &lock);
|
|
285
|
+
if (status != NV_ENC_SUCCESS) {
|
|
286
|
+
fail(self->error, sizeof self->error, "lock input (status %d): %s", (int)status, nvenc_reason(self));
|
|
287
|
+
return -1;
|
|
288
|
+
}
|
|
289
|
+
int w = self->width, h = self->height;
|
|
290
|
+
size_t pitch = lock.pitch;
|
|
291
|
+
uint8_t *dst = lock.bufferDataPtr;
|
|
292
|
+
for (int y = 0; y < h; y++) {
|
|
293
|
+
memcpy(dst + y * pitch, i420 + (size_t)y * w, (size_t)w);
|
|
294
|
+
}
|
|
295
|
+
const uint8_t *src = i420 + (size_t)w * h;
|
|
296
|
+
dst += pitch * h;
|
|
297
|
+
for (int plane = 0; plane < 2; plane++) {
|
|
298
|
+
for (int y = 0; y < h / 2; y++) {
|
|
299
|
+
memcpy(dst + y * (pitch / 2), src + (size_t)y * (w / 2), (size_t)(w / 2));
|
|
300
|
+
}
|
|
301
|
+
src += (size_t)(w / 2) * (h / 2);
|
|
302
|
+
dst += (pitch / 2) * (h / 2);
|
|
303
|
+
}
|
|
304
|
+
self->api.nvEncUnlockInputBuffer(self->session, self->input);
|
|
305
|
+
return (int)lock.pitch;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* Encode one tightly packed I420 frame at the opened size; the access unit is
|
|
309
|
+
* complete when this returns. Returns 1 with a packet, -1 on failure. */
|
|
310
|
+
int dl_nvenc_encode(dl_nvenc_encoder *self, const uint8_t *i420, int force_idr) {
|
|
311
|
+
if (self == NULL || i420 == NULL) {
|
|
312
|
+
return -1;
|
|
313
|
+
}
|
|
314
|
+
if (self->cu_ctx_push(self->cuda) != 0) {
|
|
315
|
+
fail(self->error, sizeof self->error, "the CUDA context could not be made current");
|
|
316
|
+
return -1;
|
|
317
|
+
}
|
|
318
|
+
int result = -1;
|
|
319
|
+
int pitch = upload(self, i420);
|
|
320
|
+
if (pitch < 0) {
|
|
321
|
+
goto done;
|
|
322
|
+
}
|
|
323
|
+
NV_ENC_PIC_PARAMS pic = {0};
|
|
324
|
+
pic.version = NV_ENC_PIC_PARAMS_VER;
|
|
325
|
+
pic.inputWidth = (uint32_t)self->width;
|
|
326
|
+
pic.inputHeight = (uint32_t)self->height;
|
|
327
|
+
pic.inputPitch = (uint32_t)pitch;
|
|
328
|
+
pic.inputBuffer = self->input;
|
|
329
|
+
pic.outputBitstream = self->output;
|
|
330
|
+
pic.bufferFmt = NV_ENC_BUFFER_FORMAT_IYUV;
|
|
331
|
+
pic.pictureStruct = NV_ENC_PIC_STRUCT_FRAME;
|
|
332
|
+
pic.inputTimeStamp = self->frame_index++;
|
|
333
|
+
if (force_idr) {
|
|
334
|
+
pic.encodePicFlags = NV_ENC_PIC_FLAG_FORCEIDR | NV_ENC_PIC_FLAG_OUTPUT_SPSPPS;
|
|
335
|
+
}
|
|
336
|
+
NVENCSTATUS status = self->api.nvEncEncodePicture(self->session, &pic);
|
|
337
|
+
if (status != NV_ENC_SUCCESS) {
|
|
338
|
+
fail(self->error, sizeof self->error, "encode (status %d): %s", (int)status, nvenc_reason(self));
|
|
339
|
+
goto done;
|
|
340
|
+
}
|
|
341
|
+
NV_ENC_LOCK_BITSTREAM lock = {0};
|
|
342
|
+
lock.version = NV_ENC_LOCK_BITSTREAM_VER;
|
|
343
|
+
lock.outputBitstream = self->output;
|
|
344
|
+
status = self->api.nvEncLockBitstream(self->session, &lock);
|
|
345
|
+
if (status != NV_ENC_SUCCESS) {
|
|
346
|
+
fail(self->error, sizeof self->error, "lock bitstream (status %d): %s", (int)status, nvenc_reason(self));
|
|
347
|
+
goto done;
|
|
348
|
+
}
|
|
349
|
+
size_t size = lock.bitstreamSizeInBytes;
|
|
350
|
+
if (size > self->packet_capacity) {
|
|
351
|
+
uint8_t *grown = realloc(self->packet, size);
|
|
352
|
+
if (grown == NULL) {
|
|
353
|
+
self->api.nvEncUnlockBitstream(self->session, self->output);
|
|
354
|
+
fail(self->error, sizeof self->error, "out of memory");
|
|
355
|
+
goto done;
|
|
356
|
+
}
|
|
357
|
+
self->packet = grown;
|
|
358
|
+
self->packet_capacity = size;
|
|
359
|
+
}
|
|
360
|
+
memcpy(self->packet, lock.bitstreamBufferPtr, size);
|
|
361
|
+
self->packet_size = size;
|
|
362
|
+
self->packet_is_key = lock.pictureType == NV_ENC_PIC_TYPE_IDR;
|
|
363
|
+
self->api.nvEncUnlockBitstream(self->session, self->output);
|
|
364
|
+
result = size > 0 ? 1 : -1;
|
|
365
|
+
if (size == 0) {
|
|
366
|
+
fail(self->error, sizeof self->error, "empty access unit");
|
|
367
|
+
}
|
|
368
|
+
done:
|
|
369
|
+
self->cu_ctx_pop(NULL);
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/* Change the rate target and, with `max_qp` > 0, cap the quantizer, for the
|
|
374
|
+
* frames that follow — in place, without an IDR or a rate-control reset.
|
|
375
|
+
* Returns 0 on success. */
|
|
376
|
+
int dl_nvenc_reconfigure(dl_nvenc_encoder *self, int bitrate_kbps, int max_qp) {
|
|
377
|
+
if (self == NULL) {
|
|
378
|
+
return -1;
|
|
379
|
+
}
|
|
380
|
+
if (bitrate_kbps <= 0) {
|
|
381
|
+
fail(self->error, sizeof self->error, "a rate of %d kbps", bitrate_kbps);
|
|
382
|
+
return -1;
|
|
383
|
+
}
|
|
384
|
+
if (self->cu_ctx_push(self->cuda) != 0) {
|
|
385
|
+
fail(self->error, sizeof self->error, "the CUDA context could not be made current");
|
|
386
|
+
return -1;
|
|
387
|
+
}
|
|
388
|
+
set_rate(self, bitrate_kbps, max_qp);
|
|
389
|
+
NV_ENC_RECONFIGURE_PARAMS params = {0};
|
|
390
|
+
params.version = NV_ENC_RECONFIGURE_PARAMS_VER;
|
|
391
|
+
params.reInitEncodeParams = self->init;
|
|
392
|
+
params.resetEncoder = 0;
|
|
393
|
+
params.forceIDR = 0;
|
|
394
|
+
NVENCSTATUS status = self->api.nvEncReconfigureEncoder(self->session, ¶ms);
|
|
395
|
+
self->cu_ctx_pop(NULL);
|
|
396
|
+
if (status != NV_ENC_SUCCESS) {
|
|
397
|
+
fail(self->error, sizeof self->error, "reconfigure (status %d): %s", (int)status, nvenc_reason(self));
|
|
398
|
+
return -1;
|
|
399
|
+
}
|
|
400
|
+
return 0;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const uint8_t *dl_nvenc_packet_data(const dl_nvenc_encoder *self) { return self->packet; }
|
|
404
|
+
|
|
405
|
+
size_t dl_nvenc_packet_size(const dl_nvenc_encoder *self) { return self->packet_size; }
|
|
406
|
+
|
|
407
|
+
int dl_nvenc_packet_is_key(const dl_nvenc_encoder *self) { return self->packet_is_key; }
|
|
408
|
+
|
|
409
|
+
const char *dl_nvenc_error(const dl_nvenc_encoder *self) { return self->error; }
|
|
410
|
+
|
|
411
|
+
static void destroy(dl_nvenc_encoder *self) {
|
|
412
|
+
if (self == NULL) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (self->session != NULL && self->cu_ctx_push(self->cuda) == 0) {
|
|
416
|
+
if (self->input != NULL) {
|
|
417
|
+
/* End of stream: the encoder is flushed before it is closed. */
|
|
418
|
+
NV_ENC_PIC_PARAMS eos = {0};
|
|
419
|
+
eos.version = NV_ENC_PIC_PARAMS_VER;
|
|
420
|
+
eos.encodePicFlags = NV_ENC_PIC_FLAG_EOS;
|
|
421
|
+
self->api.nvEncEncodePicture(self->session, &eos);
|
|
422
|
+
self->api.nvEncDestroyInputBuffer(self->session, self->input);
|
|
423
|
+
}
|
|
424
|
+
if (self->output != NULL) {
|
|
425
|
+
self->api.nvEncDestroyBitstreamBuffer(self->session, self->output);
|
|
426
|
+
}
|
|
427
|
+
self->api.nvEncDestroyEncoder(self->session);
|
|
428
|
+
self->cu_ctx_pop(NULL);
|
|
429
|
+
}
|
|
430
|
+
if (self->cuda != NULL) {
|
|
431
|
+
self->cu_primary_release(self->device);
|
|
432
|
+
}
|
|
433
|
+
/* The driver libraries stay loaded (no dlclose): driver worker threads
|
|
434
|
+
* can outlive a session, and the next session skips the load. */
|
|
435
|
+
free(self->packet);
|
|
436
|
+
free(self);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* Returns NULL with the reason in `error` when the driver, the device or the
|
|
440
|
+
* session is unavailable; nothing is left open in that case. */
|
|
441
|
+
dl_nvenc_encoder *dl_nvenc_create(int width, int height, int bitrate_kbps, int fps,
|
|
442
|
+
char *error, size_t error_len) {
|
|
443
|
+
pthread_mutex_lock(&lifecycle);
|
|
444
|
+
dl_nvenc_encoder *self = create(width, height, bitrate_kbps, fps, error, error_len);
|
|
445
|
+
pthread_mutex_unlock(&lifecycle);
|
|
446
|
+
return self;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
void dl_nvenc_destroy(dl_nvenc_encoder *self) {
|
|
450
|
+
pthread_mutex_lock(&lifecycle);
|
|
451
|
+
destroy(self);
|
|
452
|
+
pthread_mutex_unlock(&lifecycle);
|
|
453
|
+
}
|