mirror of
https://github.com/pikvm/ustreamer.git
synced 2025-12-24 03:00:01 +00:00
raw sink
This commit is contained in:
parent
847726c0d7
commit
338389c219
7
Makefile
7
Makefile
@ -28,6 +28,13 @@ $(filter $(shell echo $(1) | tr A-Z a-z), yes on 1)
|
||||
endef
|
||||
|
||||
|
||||
ifneq ($(call optbool,$(WITH_RAWSINK)),)
|
||||
_LIBS += -lrt
|
||||
override CFLAGS += -DWITH_RAWSINK
|
||||
_SRCS += $(shell ls src/rawsink/*.c)
|
||||
endif
|
||||
|
||||
|
||||
ifneq ($(call optbool,$(WITH_OMX)),)
|
||||
_LIBS += -lbcm_host -lvcos -lopenmaxil -L$(RPI_VC_LIBS)
|
||||
override CFLAGS += -DWITH_OMX -DOMX_SKIP64BIT -I$(RPI_VC_HEADERS)
|
||||
|
||||
201
src/rawsink/rawsink.c
Normal file
201
src/rawsink/rawsink.c
Normal file
@ -0,0 +1,201 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#include "rawsink.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <semaphore.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/mman.h>
|
||||
|
||||
#include "../common/tools.h"
|
||||
#include "../common/logging.h"
|
||||
|
||||
|
||||
struct rawsink_t *rawsink_init(const char *name, mode_t mode, bool rm) {
|
||||
struct rawsink_t *rawsink;
|
||||
|
||||
A_CALLOC(rawsink, 1);
|
||||
rawsink->fd = -1;
|
||||
rawsink->picture = MAP_FAILED;
|
||||
rawsink->signal_sem = SEM_FAILED;
|
||||
rawsink->lock_sem = SEM_FAILED;
|
||||
rawsink->rm = rm;
|
||||
|
||||
A_CALLOC(rawsink->mem_name, strlen(name) + 8);
|
||||
A_CALLOC(rawsink->signal_name, strlen(name) + 8);
|
||||
A_CALLOC(rawsink->lock_name, strlen(name) + 8);
|
||||
|
||||
sprintf(rawsink->mem_name, "%s.mem", name);
|
||||
sprintf(rawsink->signal_name, "%s.sig", name);
|
||||
sprintf(rawsink->lock_name, "%s.lock", name);
|
||||
|
||||
LOG_INFO("Using RAW sink: %s.{mem,sig,lock}", name);
|
||||
|
||||
{ // Shared memory
|
||||
if ((rawsink->fd = shm_open(rawsink->mem_name, O_RDWR | O_CREAT, mode)) == -1) {
|
||||
LOG_PERROR("Can't open RAW sink memory");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (ftruncate(rawsink->fd, sizeof(struct rawsink_picture_t)) < 0) {
|
||||
LOG_PERROR("Can't truncate RAW sink memory");
|
||||
goto error;
|
||||
}
|
||||
|
||||
if ((rawsink->picture = mmap(
|
||||
NULL,
|
||||
sizeof(struct rawsink_picture_t),
|
||||
PROT_READ | PROT_WRITE,
|
||||
MAP_SHARED,
|
||||
rawsink->fd,
|
||||
0
|
||||
)) == MAP_FAILED) {
|
||||
LOG_PERROR("Can't mmap RAW sink memory");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
# define OPEN_SEM(_role, _default) { \
|
||||
if ((rawsink->_role##_sem = sem_open(rawsink->_role##_name, O_RDWR | O_CREAT, mode, _default)) == SEM_FAILED) { \
|
||||
LOG_PERROR("Can't open RAW sink " #_role " semaphore"); \
|
||||
goto error; \
|
||||
} \
|
||||
}
|
||||
|
||||
OPEN_SEM(signal, 0);
|
||||
OPEN_SEM(lock, 1);
|
||||
|
||||
# undef OPEN_SEM
|
||||
|
||||
return rawsink;
|
||||
|
||||
error:
|
||||
rawsink_destroy(rawsink);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void rawsink_destroy(struct rawsink_t *rawsink) {
|
||||
# define CLOSE_SEM(_role) { \
|
||||
if (rawsink->_role##_sem != SEM_FAILED) { \
|
||||
if (sem_close(rawsink->_role##_sem) < 0) { \
|
||||
LOG_PERROR("Can't close RAW sink " #_role " semaphore"); \
|
||||
} \
|
||||
if (rawsink->rm && sem_unlink(rawsink->_role##_name) < 0) { \
|
||||
if (errno != ENOENT) { \
|
||||
LOG_PERROR("Can't remove RAW sink " #_role " semaphore"); \
|
||||
} \
|
||||
} \
|
||||
} \
|
||||
}
|
||||
|
||||
CLOSE_SEM(lock);
|
||||
CLOSE_SEM(signal);
|
||||
|
||||
# undef CLOSE_SEM
|
||||
|
||||
if (rawsink->picture != MAP_FAILED) {
|
||||
if (munmap(rawsink->picture, sizeof(struct rawsink_picture_t)) < 0) {
|
||||
LOG_PERROR("Can't unmap RAW sink memory");
|
||||
}
|
||||
}
|
||||
|
||||
if (rawsink->fd >= 0) {
|
||||
if (close(rawsink->fd) < 0) {
|
||||
LOG_PERROR("Can't close RAW sink fd");
|
||||
}
|
||||
if (rawsink->rm && shm_unlink(rawsink->mem_name) < 0) {
|
||||
if (errno != ENOENT) {
|
||||
LOG_PERROR("Can't remove RAW sink memory");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(rawsink->lock_name);
|
||||
free(rawsink->signal_name);
|
||||
free(rawsink->mem_name);
|
||||
free(rawsink);
|
||||
}
|
||||
|
||||
void rawsink_put(
|
||||
struct rawsink_t *rawsink,
|
||||
const unsigned char *data, size_t size,
|
||||
unsigned format, unsigned width, unsigned height,
|
||||
long double grab_ts) {
|
||||
|
||||
long double now = get_now_monotonic();
|
||||
|
||||
if (rawsink->failed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (size > RAWSINK_MAX_DATA) {
|
||||
LOG_ERROR("RAWSINK: Can't put RAW frame: is too big (%zu > %zu)", size, RAWSINK_MAX_DATA);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sem_trywait(rawsink->lock_sem) == 0) {
|
||||
LOG_PERF("RAWSINK: >>>>> Exposing new frame ...");
|
||||
|
||||
if (sem_trywait(rawsink->signal_sem) < 0 && errno != EAGAIN) {
|
||||
LOG_PERROR("RAWSINK: Can't wait %s", rawsink->signal_name);
|
||||
goto error;
|
||||
}
|
||||
|
||||
# define PICTURE(_next) rawsink->picture->_next
|
||||
PICTURE(format) = format;
|
||||
PICTURE(width) = width;
|
||||
PICTURE(height) = height;
|
||||
PICTURE(grab_ts) = grab_ts;
|
||||
PICTURE(used) = size;
|
||||
memcpy(PICTURE(data), data, size);
|
||||
# undef PICTURE
|
||||
|
||||
if (sem_post(rawsink->signal_sem) < 0) {
|
||||
LOG_PERROR("RAWSINK: Can't post %s", rawsink->signal_name);
|
||||
goto error;
|
||||
}
|
||||
if (sem_post(rawsink->lock_sem) < 0) {
|
||||
LOG_PERROR("RAWSINK: Can't post %s", rawsink->lock_name);
|
||||
goto error;
|
||||
}
|
||||
LOG_VERBOSE("RAWSINK: Exposed new frame; full exposition time = %Lf", get_now_monotonic() - now);
|
||||
|
||||
} else if (errno == EAGAIN) {
|
||||
LOG_PERF("RAWSINK: ===== Shared memory is busy now; frame skipped");
|
||||
|
||||
} else {
|
||||
LOG_PERROR("RAWSINK: Can't wait %s", rawsink->lock_name);
|
||||
goto error;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
error:
|
||||
LOG_ERROR("RAW sink completely disabled due error");
|
||||
rawsink->failed = true;
|
||||
}
|
||||
70
src/rawsink/rawsink.h
Normal file
70
src/rawsink/rawsink.h
Normal file
@ -0,0 +1,70 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# uStreamer - Lightweight and fast MJPG-HTTP streamer. #
|
||||
# #
|
||||
# Copyright (C) 2018 Maxim Devaev <mdevaev@gmail.com> #
|
||||
# #
|
||||
# This program is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# This program is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the GNU General Public License #
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <semaphore.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
|
||||
#ifndef CFG_RAWSINK_MAX_DATA
|
||||
# define CFG_RAWSINK_MAX_DATA 33554432
|
||||
#endif
|
||||
#define RAWSINK_MAX_DATA ((size_t)(CFG_RAWSINK_MAX_DATA))
|
||||
|
||||
|
||||
struct rawsink_picture_t {
|
||||
unsigned format;
|
||||
unsigned width;
|
||||
unsigned height;
|
||||
long double grab_ts;
|
||||
size_t used;
|
||||
unsigned char data[RAWSINK_MAX_DATA];
|
||||
};
|
||||
|
||||
struct rawsink_t {
|
||||
char *mem_name;
|
||||
char *signal_name;
|
||||
char *lock_name;
|
||||
|
||||
int fd;
|
||||
struct rawsink_picture_t *picture;
|
||||
|
||||
sem_t *signal_sem;
|
||||
sem_t *lock_sem;
|
||||
|
||||
bool rm;
|
||||
bool failed;
|
||||
};
|
||||
|
||||
|
||||
struct rawsink_t *rawsink_init(const char *name, mode_t mode, bool rm);
|
||||
void rawsink_destroy(struct rawsink_t *rawsink);
|
||||
|
||||
void rawsink_put(
|
||||
struct rawsink_t *rawsink,
|
||||
const unsigned char *data, size_t size,
|
||||
unsigned format, unsigned witdh, unsigned height,
|
||||
long double grab_ts);
|
||||
@ -120,12 +120,16 @@ struct device_t *device_init(void) {
|
||||
dev->height = 480;
|
||||
dev->format = V4L2_PIX_FMT_YUYV;
|
||||
dev->standard = V4L2_STD_UNKNOWN;
|
||||
dev->io_method = V4L2_MEMORY_MMAP;
|
||||
dev->n_buffers = cores_available + 1;
|
||||
dev->n_workers = min_u(cores_available, dev->n_buffers);
|
||||
dev->min_frame_size = 128;
|
||||
dev->timeout = 1;
|
||||
dev->error_delay = 1;
|
||||
dev->io_method = V4L2_MEMORY_MMAP;
|
||||
dev->error_delay = 1; // XXX: not device param
|
||||
# ifdef WITH_RAWSINK // XXX: not device param
|
||||
dev->rawsink_name = "";
|
||||
dev->rawsink_mode = 0660;
|
||||
# endif
|
||||
dev->run = run;
|
||||
return dev;
|
||||
}
|
||||
|
||||
@ -28,6 +28,10 @@
|
||||
#include <pthread.h>
|
||||
#include <linux/videodev2.h>
|
||||
|
||||
#ifdef WITH_RAWSINK
|
||||
# include "../rawsink/rawsink.h"
|
||||
#endif
|
||||
|
||||
#include "picture.h"
|
||||
|
||||
|
||||
@ -116,7 +120,12 @@ struct device_t {
|
||||
size_t min_frame_size;
|
||||
bool persistent;
|
||||
unsigned timeout;
|
||||
unsigned error_delay;
|
||||
unsigned error_delay; // XXX: not device param
|
||||
# ifdef WITH_RAWSINK // XXX: not device params
|
||||
char *rawsink_name;
|
||||
mode_t rawsink_mode;
|
||||
bool rawsink_rm;
|
||||
# endif
|
||||
|
||||
struct controls_t ctl;
|
||||
|
||||
|
||||
@ -104,6 +104,12 @@ enum _OPT_VALUES {
|
||||
_O_TCP_NODELAY,
|
||||
_O_SERVER_TIMEOUT,
|
||||
|
||||
#ifdef WITH_RAWSINK
|
||||
_O_RAWSINK,
|
||||
_O_RAWSINK_MODE,
|
||||
_O_RAWSINK_RM,
|
||||
#endif
|
||||
|
||||
#ifdef WITH_GPIO
|
||||
_O_GPIO_DEVICE,
|
||||
_O_GPIO_CONSUMER_PREFIX,
|
||||
@ -182,6 +188,12 @@ static const struct option _LONG_OPTS[] = {
|
||||
{"tcp-nodelay", no_argument, NULL, _O_TCP_NODELAY},
|
||||
{"server-timeout", required_argument, NULL, _O_SERVER_TIMEOUT},
|
||||
|
||||
#ifdef WITH_RAWSINK
|
||||
{"raw-sink", required_argument, NULL, _O_RAWSINK},
|
||||
{"raw-sink-mode", required_argument, NULL, _O_RAWSINK_MODE},
|
||||
{"raw-sink-rm", no_argument, NULL, _O_RAWSINK_RM},
|
||||
#endif
|
||||
|
||||
#ifdef WITH_GPIO
|
||||
{"gpio-device", required_argument, NULL, _O_GPIO_DEVICE},
|
||||
{"gpio-consumer-prefix", required_argument, NULL, _O_GPIO_CONSUMER_PREFIX},
|
||||
@ -405,6 +417,12 @@ int options_parse(struct options_t *options, struct device_t *dev, struct encode
|
||||
case _O_TCP_NODELAY: OPT_SET(server->tcp_nodelay, true);
|
||||
case _O_SERVER_TIMEOUT: OPT_NUMBER("--server-timeout", server->timeout, 1, 60, 0);
|
||||
|
||||
# ifdef WITH_RAWSINK
|
||||
case _O_RAWSINK: OPT_SET(dev->rawsink_name, optarg);
|
||||
case _O_RAWSINK_MODE: OPT_NUMBER("--raw-sink-mode", dev->rawsink_mode, INT_MIN, INT_MAX, 8);
|
||||
case _O_RAWSINK_RM: OPT_SET(dev->rawsink_rm, true);
|
||||
# endif
|
||||
|
||||
# ifdef WITH_GPIO
|
||||
case _O_GPIO_DEVICE: OPT_SET(gpio.path, optarg);
|
||||
case _O_GPIO_CONSUMER_PREFIX: OPT_SET(gpio.consumer_prefix, optarg);
|
||||
@ -534,6 +552,12 @@ static void _features(void) {
|
||||
puts("- WITH_OMX");
|
||||
# endif
|
||||
|
||||
# ifdef WITH_RAWSINK
|
||||
puts("+ WITH_RAWSINK");
|
||||
# else
|
||||
puts("- WITH_RAWSINK");
|
||||
# endif
|
||||
|
||||
# ifdef WITH_GPIO
|
||||
puts("+ WITH_GPIO");
|
||||
# else
|
||||
@ -651,6 +675,14 @@ static void _help(struct device_t *dev, struct encoder_t *encoder, struct http_s
|
||||
printf(" Default: disabled.\n\n");
|
||||
printf(" --allow-origin <str> ─────── Set Access-Control-Allow-Origin header. Default: disabled.\n\n");
|
||||
printf(" --server-timeout <sec> ───── Timeout for client connections. Default: %u.\n\n", server->timeout);
|
||||
#ifdef WITH_RAWSINK
|
||||
printf("RAW sink options:\n");
|
||||
printf("═════════════════\n");
|
||||
printf(" --raw-sink <name> ────── Use the shared memory to sink RAW frames before encoding.\n");
|
||||
printf(" Most likely you will never need it. Default: disabled.\n\n");
|
||||
printf(" --raw-sink-mode <mode> ─ Set RAW sink permissions (like 777). Default: %o.\n\n", dev->rawsink_mode);
|
||||
printf(" --raw-sink-rm ────────── Remove shared memory on stop. Default: disabled.\n\n");
|
||||
#endif
|
||||
#ifdef WITH_GPIO
|
||||
printf("GPIO options:\n");
|
||||
printf("═════════════\n");
|
||||
|
||||
@ -38,6 +38,9 @@
|
||||
#include "picture.h"
|
||||
#include "device.h"
|
||||
#include "encoder.h"
|
||||
#ifdef WITH_RAWSINK
|
||||
# include "../rawsink/rawsink.h"
|
||||
#endif
|
||||
#ifdef WITH_GPIO
|
||||
# include "gpio/gpio.h"
|
||||
#endif
|
||||
@ -128,10 +131,18 @@ void stream_destroy(struct stream_t *stream) {
|
||||
}
|
||||
|
||||
void stream_loop(struct stream_t *stream) {
|
||||
# define DEV(_next) stream->dev->_next
|
||||
struct _workers_pool_t *pool;
|
||||
|
||||
LOG_INFO("Using V4L2 device: %s", stream->dev->path);
|
||||
LOG_INFO("Using desired FPS: %u", stream->dev->desired_fps);
|
||||
LOG_INFO("Using V4L2 device: %s", DEV(path));
|
||||
LOG_INFO("Using desired FPS: %u", DEV(desired_fps));
|
||||
|
||||
# ifdef WITH_RAWSINK
|
||||
struct rawsink_t *rawsink = NULL;
|
||||
if (DEV(rawsink_name[0]) != '\0') {
|
||||
rawsink = rawsink_init(DEV(rawsink_name), DEV(rawsink_mode), DEV(rawsink_rm));
|
||||
}
|
||||
# endif
|
||||
|
||||
while ((pool = _stream_init_loop(stream)) != NULL) {
|
||||
long double grab_after = 0;
|
||||
@ -143,7 +154,7 @@ void stream_loop(struct stream_t *stream) {
|
||||
LOG_INFO("Capturing ...");
|
||||
|
||||
LOG_DEBUG("Pre-allocating memory for stream picture ...");
|
||||
picture_realloc_data(stream->picture, picture_get_generous_size(stream->dev->run->width, stream->dev->run->height));
|
||||
picture_realloc_data(stream->picture, picture_get_generous_size(DEV(run->width), DEV(run->height)));
|
||||
|
||||
while (!atomic_load(&stream->proc->stop)) {
|
||||
struct _worker_t *ready_worker;
|
||||
@ -201,7 +212,8 @@ void stream_loop(struct stream_t *stream) {
|
||||
if (buf_index >= 0) {
|
||||
if (now < grab_after) {
|
||||
fluency_passed += 1;
|
||||
LOG_VERBOSE("Passed %u frames for fluency: now=%.03Lf, grab_after=%.03Lf", fluency_passed, now, grab_after);
|
||||
LOG_VERBOSE("Passed %u frames for fluency: now=%.03Lf, grab_after=%.03Lf",
|
||||
fluency_passed, now, grab_after);
|
||||
if (device_release_buffer(stream->dev, buf_index) < 0) {
|
||||
break;
|
||||
}
|
||||
@ -221,6 +233,20 @@ void stream_loop(struct stream_t *stream) {
|
||||
grab_after = now + fluency_delay;
|
||||
LOG_VERBOSE("Fluency: delay=%.03Lf, grab_after=%.03Lf", fluency_delay, grab_after);
|
||||
|
||||
# ifdef WITH_RAWSINK
|
||||
if (rawsink) {
|
||||
rawsink_put(
|
||||
rawsink,
|
||||
DEV(run->hw_buffers[buf_index].data),
|
||||
DEV(run->hw_buffers[buf_index].used),
|
||||
DEV(run->format),
|
||||
DEV(run->width),
|
||||
DEV(run->height),
|
||||
DEV(run->pictures[buf_index]->grab_ts)
|
||||
);
|
||||
}
|
||||
# endif
|
||||
|
||||
_workers_pool_assign(pool, ready_worker, buf_index);
|
||||
}
|
||||
} else if (buf_index != -2) { // -2 for broken frame
|
||||
@ -255,6 +281,14 @@ void stream_loop(struct stream_t *stream) {
|
||||
gpio_set_stream_online(false);
|
||||
# endif
|
||||
}
|
||||
|
||||
# ifdef WITH_RAWSINK
|
||||
if (rawsink) {
|
||||
rawsink_destroy(rawsink);
|
||||
}
|
||||
# endif
|
||||
|
||||
# undef DEV
|
||||
}
|
||||
|
||||
void stream_loop_break(struct stream_t *stream) {
|
||||
|
||||
19
ustreamer.1
19
ustreamer.1
@ -153,9 +153,9 @@ Bind to this TCP port. Default: 8080.
|
||||
.TP
|
||||
.BR \-U\ \fIpath ", " \-\-unix\ \fIpath
|
||||
Bind to UNIX domain socket. Default: disabled.
|
||||
.TP
|
||||
.BR \-D ", " \-\-unix\-rm
|
||||
Try to remove old UNIX socket file before binding. Default: disabled.
|
||||
.tp
|
||||
.br \-d ", " \-\-unix\-rm
|
||||
try to remove old unix socket file before binding. default: disabled.
|
||||
.TP
|
||||
.BR \-M\ \fImode ", " \-\-unix\-mode\ \fImode
|
||||
Set UNIX socket file permissions (like 777). Default: disabled.
|
||||
@ -194,6 +194,19 @@ Set Access\-Control\-Allow\-Origin header. Default: disabled.
|
||||
.BR \-\-server\-timeout\ \fIsec
|
||||
Timeout for client connections. Default: 10.
|
||||
|
||||
.SS "RAW sink options"
|
||||
Available only if \fBWITH_RAWSINK\fR feature enabled.
|
||||
.TP
|
||||
.BR \-\-raw\-sink\ \fIname
|
||||
Use the specified shared memory object to sink RAW frames before encoding.
|
||||
Most likely you will never need it. Default: disabled.
|
||||
.TP
|
||||
.BR \-\-raw\-sink\-mode\ \fImode
|
||||
Set RAW sink permissions (like 777). Default: 660.
|
||||
.TP
|
||||
.BR \-\-raw\-sink\-rm
|
||||
Remove shared memory on stop. Default: disabled.
|
||||
|
||||
.SS "Process options"
|
||||
.BR \-\-exit\-on\-parent\-death
|
||||
Exit the program if the parent process is dead. Required \fBHAS_PDEATHSIG\fR feature. Default: disabled.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user