Compare commits

...

23 Commits
v5.31 ... v5.36

Author SHA1 Message Date
Maxim Devaev
31219358c5 Bump version: 5.35 → 5.36 2022-12-27 19:11:09 +03:00
Maxim Devaev
4adfadfaea DOCKERHUB_REPO 2022-12-27 18:45:44 +03:00
Maxim Devaev
6174edcf0d Bump version: 5.34 → 5.35 2022-12-21 22:03:12 +03:00
Maxim Devaev
05d9322404 using archlinux/archlinux:base for testenv 2022-12-21 22:00:18 +03:00
Maxim Devaev
bf78d8f562 Bump version: 5.33 → 5.34 2022-11-29 16:07:18 +03:00
Maxim Devaev
77a5dbfeae janus: probe alsa capture device 2022-11-29 16:06:38 +03:00
Maxim Devaev
3eebeaeedd fixed edid 2022-11-29 08:04:34 +03:00
Maxim Devaev
aba8396d60 pinned flake8==5.0.4 due zheller/flake8-quotes#110 2022-11-29 05:16:15 +03:00
Maxim Devaev
5b33246b6b Bump version: 5.32 → 5.33 2022-11-28 03:33:42 +03:00
Maxim Devaev
7f3f480d92 janus: fixed features response 2022-11-28 03:16:10 +03:00
Maxim Devaev
86fef47018 Bump version: 5.31 → 5.32 2022-11-27 12:05:21 +03:00
Maxim Devaev
f09bb1ade9 janus: features request 2022-11-27 12:03:43 +03:00
Maxim Devaev
fa030147e8 janus: improved audio switching 2022-11-27 09:34:14 +03:00
Maxim Devaev
f88333b6bf refactoring 2022-11-27 08:00:58 +03:00
Maxim Devaev
9b4f3229f2 janus: optional audio for each client 2022-11-27 07:01:23 +03:00
Maxim Devaev
900d7e1112 Merge branch 'auto-playout-delay' 2022-11-27 03:34:58 +03:00
Maxim Devaev
c0588c6736 janus: commented zero_playout_delay opts 2022-11-27 03:34:46 +03:00
Maxim Devaev
5bf8c97a1c int once 2022-11-19 04:21:37 +03:00
Maxim Devaev
7335a5d2df US_ONCE macro 2022-11-19 00:25:21 +03:00
Maxim Devaev
839804b476 indentation fix 2022-11-18 22:34:52 +03:00
Marcel Stör
5bb223506a Extend the installation instructions (#190) 2022-11-14 23:33:37 +03:00
Maxim Devaev
414f536ace auto playout delay 2022-11-14 19:58:12 +03:00
Maxim Devaev
c1363d55e0 pass gop to memsink 2022-11-13 05:26:30 +03:00
32 changed files with 195 additions and 122 deletions

View File

@@ -1,7 +1,7 @@
[bumpversion]
commit = True
tag = True
current_version = 5.31
current_version = 5.36
parse = (?P<major>\d+)\.(?P<minor>\d+)
serialize =
{major}.{minor}

View File

@@ -35,7 +35,7 @@ jobs:
uses: docker/metadata-action@v4
with:
images: |
${{ secrets.DOCKERHUB_USERNAME }}/ustreamer,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
${{ secrets.DOCKERHUB_REPO }}/ustreamer,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
ghcr.io/${{ github.repository }},enable=${{ github.event_name != 'pull_request' }}
tags: |
type=ref,event=tag

View File

@@ -34,16 +34,31 @@ Footnotes:
If you're going to live-stream from your backyard webcam and need to control it, use mjpg-streamer. If you need a high-quality image with high FPS - µStreamer for the win.
-----
# Building
# Installation
## Building
You need to download the µStreamer onto your system and build it from the sources.
AUR has a package for Arch Linux: https://aur.archlinux.org/packages/ustreamer.
FreeBSD port: https://www.freshports.org/multimedia/ustreamer.
### Preconditions
You'll need ```make```, ```gcc```, ```libevent``` with ```pthreads``` support, ```libjpeg9```/```libjpeg-turbo``` and ```libbsd``` (only for Linux).
* Arch: `sudo pacman -S libevent libjpeg-turbo libutil-linux libbsd`.
* Raspbian: `sudo apt install libevent-dev libjpeg8-dev libbsd-dev`. Add `libgpiod-dev` for `WITH_GPIO=1` and `libsystemd-dev` for `WITH_SYSTEMD=1` and `libasound2-dev libspeex-dev libspeexdsp-dev libopus-dev` for `WITH_JANUS=1`.
* Raspbian: `sudo apt install libevent-dev libjpeg9-dev libbsd-dev`. Add `libgpiod-dev` for `WITH_GPIO=1` and `libsystemd-dev` for `WITH_SYSTEMD=1` and `libasound2-dev libspeex-dev libspeexdsp-dev libopus-dev` for `WITH_JANUS=1`.
* Debian/Ubuntu: `sudo apt install build-essential libevent-dev libjpeg-dev libbsd-dev`.
* Alpine: `sudo apk add libevent-dev libbsd-dev libjpeg-turbo-dev musl-dev`. Build with `WITH_PTHREAD_NP=0`.
To enable GPIO support install [libgpiod](https://git.kernel.org/pub/scm/libs/libgpiod/libgpiod.git/about) and pass option ```WITH_GPIO=1```. If the compiler reports about a missing function ```pthread_get_name_np()``` (or similar), add option ```WITH_PTHREAD_NP=0``` (it's enabled by default). For the similar error with ```setproctitle()``` add option ```WITH_SETPROCTITLE=0```.
> **Note**
> Raspian: In case your version of Raspian is too told for there to be a libjpeg9 package, use `libjpeg8-dev` instead: `E: Package 'libjpeg9-dev' has no installation candidate`.
### Make
The most convenient process is to clone the µStreamer Git repository onto your system. If you don't have Git installed and don't want to install it either, you can download and unzip the sources from GitHub using `wget https://github.com/pikvm/ustreamer/archive/refs/heads/master.zip`.
```
$ git clone --depth=1 https://github.com/pikvm/ustreamer
$ cd ustreamer
@@ -51,9 +66,15 @@ $ make
$ ./ustreamer --help
```
AUR has a package for Arch Linux: https://aur.archlinux.org/packages/ustreamer.
## Update
Assuming you have a µStreamer clone as discussed above you can update µStreamer as follows.
FreeBSD port: https://www.freshports.org/multimedia/ustreamer.
```
$ cd ustreamer
$ git pull
$ make clean
$ make
```
-----
# Usage

View File

@@ -189,7 +189,7 @@ The client-side JavaScript application uses the following control flow:
success: function (pluginHandle) {
uStreamerPluginHandle = pluginHandle;
// Instruct the µStreamer Janus plugin to initiate streaming.
uStreamerPluginHandle.send({ message: { request: "watch" } });
uStreamerPluginHandle.send({ message: { request: "watch", params: {audio: true} } });
},
// Callback function if the server fails to attach the plugin.
@@ -197,13 +197,6 @@ The client-side JavaScript application uses the following control flow:
// Callback function for processing messages from the Janus server.
onmessage: function (msg, jsepOffer) {
// 503 indicates that the plugin is not ready to stream yet. Retry the
// watch request until the video stream is available.
if (msg.error_code === 503) {
uStreamerPluginHandle.send({ message: { request: "watch" } });
return;
}
// If there is a JSEP offer, respond to it. This starts the WebRTC
// connection.
if (jsepOffer) {

View File

@@ -55,6 +55,19 @@ static void *_pcm_thread(void *v_audio);
static void *_encoder_thread(void *v_audio);
bool us_audio_probe(const char *name) {
snd_pcm_t *pcm;
int err;
US_JLOG_INFO("audio", "Probing PCM capture ...");
if ((err = snd_pcm_open(&pcm, name, SND_PCM_STREAM_CAPTURE, 0)) < 0) {
_JLOG_PERROR_ALSA(err, "audio", "Can't probe PCM capture");
return false;
}
snd_pcm_close(pcm);
US_JLOG_INFO("audio", "PCM capture is available");
return true;
}
us_audio_s *us_audio_init(const char *name, unsigned pcm_hz) {
us_audio_s *audio;
US_CALLOC(audio, 1);

View File

@@ -52,8 +52,8 @@ typedef struct {
SpeexResamplerState *res;
OpusEncoder *enc;
us_queue_s *pcm_queue;
us_queue_s *enc_queue;
us_queue_s *pcm_queue;
us_queue_s *enc_queue;
uint32_t pts;
pthread_t pcm_tid;
@@ -63,6 +63,8 @@ typedef struct {
} us_audio_s;
bool us_audio_probe(const char *name);
us_audio_s *us_audio_init(const char *name, unsigned pcm_hz);
void us_audio_destroy(us_audio_s *audio);

View File

@@ -28,53 +28,50 @@ static void *_audio_thread(void *v_client);
static void *_common_thread(void *v_client, bool video);
us_janus_client_s *us_janus_client_init(janus_callbacks *gw, janus_plugin_session *session, bool has_audio) {
us_janus_client_s *us_janus_client_init(janus_callbacks *gw, janus_plugin_session *session) {
us_janus_client_s *client;
US_CALLOC(client, 1);
client->gw = gw;
client->session = session;
atomic_init(&client->transmit, true);
atomic_init(&client->transmit, false);
atomic_init(&client->transmit_audio, false);
atomic_init(&client->stop, false);
client->video_queue = us_queue_init(1024);
US_THREAD_CREATE(client->video_tid, _video_thread, client);
if (has_audio) {
client->audio_queue = us_queue_init(64);
US_THREAD_CREATE(client->audio_tid, _audio_thread, client);
}
client->audio_queue = us_queue_init(64);
US_THREAD_CREATE(client->audio_tid, _audio_thread, client);
return client;
}
void us_janus_client_destroy(us_janus_client_s *client) {
atomic_store(&client->stop, true);
us_queue_put(client->video_queue, NULL, 0);
if (client->audio_queue != NULL) {
us_queue_put(client->audio_queue, NULL, 0);
}
us_queue_put(client->audio_queue, NULL, 0);
US_THREAD_JOIN(client->video_tid);
US_QUEUE_DELETE_WITH_ITEMS(client->video_queue, us_rtp_destroy);
if (client->audio_queue != NULL) {
US_THREAD_JOIN(client->audio_tid);
US_QUEUE_DELETE_WITH_ITEMS(client->audio_queue, us_rtp_destroy);
}
US_THREAD_JOIN(client->audio_tid);
US_QUEUE_DELETE_WITH_ITEMS(client->audio_queue, us_rtp_destroy);
free(client);
}
void us_janus_client_send(us_janus_client_s *client, const us_rtp_s *rtp) {
if (
!atomic_load(&client->transmit)
|| (!rtp->video && client->audio_queue == NULL)
atomic_load(&client->transmit)
&& (rtp->video || atomic_load(&client->transmit_audio))
) {
return;
}
us_rtp_s *const new = us_rtp_dup(rtp);
if (us_queue_put((new->video ? client->video_queue : client->audio_queue), new, 0) != 0) {
US_JLOG_ERROR("client", "Session %p %s queue is full",
client->session, (new->video ? "video" : "audio"));
us_rtp_destroy(new);
us_rtp_s *const new = us_rtp_dup(rtp);
if (us_queue_put((new->video ? client->video_queue : client->audio_queue), new, 0) != 0) {
US_JLOG_ERROR("client", "Session %p %s queue is full",
client->session, (new->video ? "video" : "audio"));
us_rtp_destroy(new);
}
}
}
@@ -97,7 +94,11 @@ static void *_common_thread(void *v_client, bool video) {
if (rtp == NULL) {
break;
}
if (atomic_load(&client->transmit)) {
if (
atomic_load(&client->transmit)
&& (video || atomic_load(&client->transmit_audio))
) {
janus_plugin_rtp packet = {0};
packet.video = rtp->video;
packet.buffer = (char *)rtp->datagram;
@@ -107,14 +108,17 @@ static void *_common_thread(void *v_client, bool video) {
// (if available) in stream index 1.
packet.mindex = (rtp->video ? 0 : 1);
# endif
janus_plugin_rtp_extensions_reset(&packet.extensions);
// FIXME: Это очень эффективный способ уменьшить задержку, но WebRTC стек в хроме и фоксе
// слишком корявый, чтобы обработать это, из-за чего на кейфреймах начинаются заикания.
// - https://github.com/Glimesh/janus-ftl-plugin/issues/101
if (rtp->zero_playout_delay) {
/*if (rtp->zero_playout_delay) {
// https://github.com/pikvm/pikvm/issues/784
packet.extensions.min_delay = 0;
packet.extensions.max_delay = 0;
}
} else {
packet.extensions.min_delay = 0;
packet.extensions.max_delay = 1000;
}*/
client->gw->relay_rtp(client->session, &packet);
}
us_rtp_destroy(rtp);

View File

@@ -41,21 +41,22 @@
typedef struct us_janus_client_sx {
janus_callbacks *gw;
janus_plugin_session *session;
atomic_bool transmit;
janus_plugin_session *session;
atomic_bool transmit;
atomic_bool transmit_audio;
pthread_t video_tid;
pthread_t audio_tid;
atomic_bool stop;
pthread_t video_tid;
pthread_t audio_tid;
atomic_bool stop;
us_queue_s *video_queue;
us_queue_s *audio_queue;
us_queue_s *video_queue;
us_queue_s *audio_queue;
US_LIST_STRUCT(struct us_janus_client_sx);
} us_janus_client_s;
us_janus_client_s *us_janus_client_init(janus_callbacks *gw, janus_plugin_session *session, bool has_audio);
us_janus_client_s *us_janus_client_init(janus_callbacks *gw, janus_plugin_session *session);
void us_janus_client_destroy(us_janus_client_s *client);
void us_janus_client_send(us_janus_client_s *client, const us_rtp_s *rtp);

View File

@@ -24,7 +24,7 @@
static char *_get_value(janus_config *jcfg, const char *section, const char *option);
static bool _get_bool(janus_config *jcfg, const char *section, const char *option, bool def);
// static bool _get_bool(janus_config *jcfg, const char *section, const char *option, bool def);
us_config_s *us_config_init(const char *config_dir_path) {
@@ -51,11 +51,7 @@ us_config_s *us_config_init(const char *config_dir_path) {
US_JLOG_ERROR("config", "Missing config value: video.sink (ex. memsink.object)");
goto error;
}
if ((config->video_zero_playout_delay = _get_bool(jcfg, "video", "zero_playout_delay", false)) == true) {
US_JLOG_INFO("config", "Enabled the experimental Playout-Delay=0 RTP extension for VIDEO");
}
if ((config->audio_dev_name = _get_value(jcfg, "audio", "device")) != NULL) {
US_JLOG_INFO("config", "Enabled the experimental AUDIO feature");
if ((config->tc358743_dev_path = _get_value(jcfg, "audio", "tc358743")) == NULL) {
US_JLOG_INFO("config", "Missing config value: audio.tc358743");
goto error;
@@ -88,7 +84,7 @@ static char *_get_value(janus_config *jcfg, const char *section, const char *opt
return us_strdup(option_obj->value);
}
static bool _get_bool(janus_config *jcfg, const char *section, const char *option, bool def) {
/*static bool _get_bool(janus_config *jcfg, const char *section, const char *option, bool def) {
char *const tmp = _get_value(jcfg, section, option);
bool value = def;
if (tmp != NULL) {
@@ -96,4 +92,4 @@ static bool _get_bool(janus_config *jcfg, const char *section, const char *optio
free(tmp);
}
return value;
}
}*/

View File

@@ -36,7 +36,6 @@
typedef struct {
char *video_sink_name;
bool video_zero_playout_delay;
char *audio_dev_name;
char *tc358743_dev_path;

View File

@@ -36,3 +36,11 @@
JANUS_LOG(LOG_ERR, "[%s/%-9s] " x_msg ": %s\n", US_PLUGIN_NAME, x_prefix, ##__VA_ARGS__, m_perror_str); \
free(m_perror_str); \
}
#define US_ONCE(...) { \
const int m_reported = __LINE__; \
if (m_reported != once) { \
__VA_ARGS__; \
once = m_reported; \
} \
}

View File

@@ -77,6 +77,7 @@ static pthread_mutex_t _g_audio_lock = PTHREAD_MUTEX_INITIALIZER;
static atomic_bool _g_ready = false;
static atomic_bool _g_stop = false;
static atomic_bool _g_has_watchers = false;
static atomic_bool _g_has_listeners = false;
static atomic_bool _g_key_required = false;
@@ -92,16 +93,12 @@ static atomic_bool _g_key_required = false;
#define _READY atomic_load(&_g_ready)
#define _STOP atomic_load(&_g_stop)
#define _HAS_WATCHERS atomic_load(&_g_has_watchers)
#define _HAS_LISTENERS atomic_load(&_g_has_listeners)
janus_plugin *create(void);
#define _IF_NOT_REPORTED(...) { \
const unsigned _error_code = __LINE__; \
if (error_reported != _error_code) { __VA_ARGS__; error_reported = _error_code; } \
}
static void *_video_rtp_thread(UNUSED void *arg) {
US_THREAD_RENAME("us_video_rtp");
atomic_store(&_g_video_rtp_tid_created, true);
@@ -123,11 +120,11 @@ static void *_video_sink_thread(UNUSED void *arg) {
atomic_store(&_g_video_sink_tid_created, true);
uint64_t frame_id = 0;
unsigned error_reported = 0;
int once = 0;
while (!_STOP) {
if (!_HAS_WATCHERS) {
_IF_NOT_REPORTED({ US_JLOG_INFO("video", "No active watchers, memsink disconnected"); });
US_ONCE({ US_JLOG_INFO("video", "No active watchers, memsink disconnected"); });
usleep(_g_watchers_polling);
continue;
}
@@ -136,16 +133,16 @@ static void *_video_sink_thread(UNUSED void *arg) {
us_memsink_shared_s *mem = NULL;
if ((fd = shm_open(_g_config->video_sink_name, O_RDWR, 0)) <= 0) {
_IF_NOT_REPORTED({ US_JLOG_PERROR("video", "Can't open memsink"); });
US_ONCE({ US_JLOG_PERROR("video", "Can't open memsink"); });
goto close_memsink;
}
if ((mem = us_memsink_shared_map(fd)) == NULL) {
_IF_NOT_REPORTED({ US_JLOG_PERROR("video", "Can't map memsink"); });
US_ONCE({ US_JLOG_PERROR("video", "Can't map memsink"); });
goto close_memsink;
}
error_reported = 0;
once = 0;
US_JLOG_INFO("video", "Memsink opened; reading frames ...");
while (!_STOP && _HAS_WATCHERS) {
@@ -159,7 +156,7 @@ static void *_video_sink_thread(UNUSED void *arg) {
atomic_store(&_g_key_required, false);
}
if (us_queue_put(_g_video_queue, frame, 0) != 0) {
_IF_NOT_REPORTED({ US_JLOG_PERROR("video", "Video queue is full"); });
US_ONCE({ US_JLOG_PERROR("video", "Video queue is full"); });
us_frame_destroy(frame);
}
} else if (result == -1) {
@@ -186,10 +183,10 @@ static void *_audio_thread(UNUSED void *arg) {
assert(_g_config->audio_dev_name != NULL);
assert(_g_config->tc358743_dev_path != NULL);
unsigned error_reported = 0;
int once = 0;
while (!_STOP) {
if (!_HAS_WATCHERS) {
if (!_HAS_WATCHERS || !_HAS_LISTENERS) {
usleep(_g_watchers_polling);
continue;
}
@@ -201,17 +198,17 @@ static void *_audio_thread(UNUSED void *arg) {
goto close_audio;
}
if (!info.has_audio) {
_IF_NOT_REPORTED({ US_JLOG_INFO("audio", "No audio presented from the host"); });
US_ONCE({ US_JLOG_INFO("audio", "No audio presented from the host"); });
goto close_audio;
}
_IF_NOT_REPORTED({ US_JLOG_INFO("audio", "Detected host audio"); });
US_ONCE({ US_JLOG_INFO("audio", "Detected host audio"); });
if ((audio = us_audio_init(_g_config->audio_dev_name, info.audio_hz)) == NULL) {
goto close_audio;
}
error_reported = 0;
once = 0;
while (!_STOP && _HAS_WATCHERS) {
while (!_STOP && _HAS_WATCHERS && _HAS_LISTENERS) {
if (
us_tc358743_read_info(_g_config->tc358743_dev_path, &info) < 0
|| !info.has_audio
@@ -240,8 +237,6 @@ static void *_audio_thread(UNUSED void *arg) {
return NULL;
}
#undef _IF_NOT_REPORTED
static void _relay_rtp_clients(const us_rtp_s *rtp) {
US_LIST_ITERATE(_g_clients, client, {
us_janus_client_send(client, rtp);
@@ -262,8 +257,8 @@ static int _plugin_init(janus_callbacks *gw, const char *config_dir_path) {
_g_gw = gw;
_g_video_queue = us_queue_init(1024);
_g_rtpv = us_rtpv_init(_relay_rtp_clients, _g_config->video_zero_playout_delay);
if (_g_config->audio_dev_name != NULL) {
_g_rtpv = us_rtpv_init(_relay_rtp_clients);
if (_g_config->audio_dev_name != NULL && us_audio_probe(_g_config->audio_dev_name)) {
_g_rtpa = us_rtpa_init(_relay_rtp_clients);
US_THREAD_CREATE(_g_audio_tid, _audio_thread, NULL);
}
@@ -302,7 +297,7 @@ static void _plugin_create_session(janus_plugin_session *session, int *err) {
_IF_DISABLED({ *err = -1; return; });
_LOCK_ALL;
US_JLOG_INFO("main", "Creating session %p ...", session);
us_janus_client_s *const client = us_janus_client_init(_g_gw, session, (_g_config->audio_dev_name != NULL));
us_janus_client_s *const client = us_janus_client_init(_g_gw, session);
US_LIST_APPEND(_g_clients, client);
atomic_store(&_g_has_watchers, true);
_UNLOCK_ALL;
@@ -313,6 +308,7 @@ static void _plugin_destroy_session(janus_plugin_session* session, int *err) {
_LOCK_ALL;
bool found = false;
bool has_watchers = false;
bool has_listeners = false;
US_LIST_ITERATE(_g_clients, client, {
if (client->session == session) {
US_JLOG_INFO("main", "Removing session %p ...", session);
@@ -321,6 +317,7 @@ static void _plugin_destroy_session(janus_plugin_session* session, int *err) {
found = true;
} else {
has_watchers = (has_watchers || atomic_load(&client->transmit));
has_listeners = (has_listeners || atomic_load(&client->transmit_audio));
}
});
if (!found) {
@@ -328,6 +325,7 @@ static void _plugin_destroy_session(janus_plugin_session* session, int *err) {
*err = -2;
}
atomic_store(&_g_has_watchers, has_watchers);
atomic_store(&_g_has_listeners, has_listeners);
_UNLOCK_ALL;
}
@@ -396,40 +394,54 @@ static struct janus_plugin_result *_plugin_handle_message(
json_decref(m_event); \
}
json_t *const request_obj = json_object_get(msg, "request");
if (request_obj == NULL) {
json_t *const request = json_object_get(msg, "request");
if (request == NULL) {
PUSH_ERROR(400, "Request missing");
goto ok_wait;
}
const char *const request_str = json_string_value(request_obj);
const char *const request_str = json_string_value(request);
if (request_str == NULL) {
PUSH_ERROR(400, "Request not a string");
goto ok_wait;
}
// US_JLOG_INFO("main", "Message: %s", request_str);
# define PUSH_STATUS(x_status, x_jsep) { \
# define PUSH_STATUS(x_status, x_payload, x_jsep) { \
json_t *const m_event = json_object(); \
json_object_set_new(m_event, "ustreamer", json_string("event")); \
json_t *const m_result = json_object(); \
json_object_set_new(m_result, "status", json_string(x_status)); \
if (x_payload != NULL) { \
json_object_set_new(m_result, x_status, x_payload); \
} \
json_object_set_new(m_event, "result", m_result); \
_g_gw->push_event(session, create(), transaction, m_event, x_jsep); \
json_decref(m_event); \
}
if (!strcmp(request_str, "start")) {
PUSH_STATUS("started", NULL);
PUSH_STATUS("started", NULL, NULL);
} else if (!strcmp(request_str, "stop")) {
PUSH_STATUS("stopped", NULL);
PUSH_STATUS("stopped", NULL, NULL);
} else if (!strcmp(request_str, "watch")) {
char *sdp;
bool with_audio = false;
{
json_t *const params = json_object_get(msg, "params");
if (params != NULL) {
json_t *const audio = json_object_get(params, "audio");
if (audio != NULL && json_is_boolean(audio)) {
with_audio = (_g_rtpa != NULL && json_boolean_value(audio));
}
}
}
{
char *sdp;
char *const video_sdp = us_rtpv_make_sdp(_g_rtpv);
char *const audio_sdp = (_g_rtpa ? us_rtpa_make_sdp(_g_rtpa) : us_strdup(""));
char *const audio_sdp = (with_audio ? us_rtpa_make_sdp(_g_rtpa) : us_strdup(""));
US_ASPRINTF(sdp,
"v=0" RN
"o=- %" PRIu64 " 1 IN IP4 0.0.0.0" RN
@@ -447,13 +459,31 @@ static struct janus_plugin_result *_plugin_handle_message(
audio_sdp, video_sdp
# endif
);
json_t *const offer_jsep = json_pack("{ssss}", "type", "offer", "sdp", sdp);
PUSH_STATUS("started", NULL, offer_jsep);
json_decref(offer_jsep);
free(audio_sdp);
free(video_sdp);
free(sdp);
}
json_t *const offer_jsep = json_pack("{ssss}", "type", "offer", "sdp", sdp);
free(sdp);
PUSH_STATUS("started", offer_jsep);
json_decref(offer_jsep);
{
_LOCK_ALL;
bool has_listeners = false;
US_LIST_ITERATE(_g_clients, client, {
if (client->session == session) {
atomic_store(&client->transmit_audio, with_audio);
}
has_listeners = (has_listeners || atomic_load(&client->transmit_audio));
});
atomic_store(&_g_has_listeners, has_listeners);
_UNLOCK_ALL;
}
} else if (!strcmp(request_str, "features")) {
json_t *const features = json_pack("{sb}", "audio", (_g_rtpa != NULL));
PUSH_STATUS("features", features, NULL);
json_decref(features);
} else if (!strcmp(request_str, "key_required")) {
// US_JLOG_INFO("main", "Got key_required message");

View File

@@ -26,12 +26,11 @@
#include "rtp.h"
us_rtp_s *us_rtp_init(unsigned payload, bool video, bool zero_playout_delay) {
us_rtp_s *us_rtp_init(unsigned payload, bool video) {
us_rtp_s *rtp;
US_CALLOC(rtp, 1);
rtp->payload = payload;
rtp->video = video;
rtp->zero_playout_delay = zero_playout_delay; // See client.c
rtp->ssrc = us_triple_u32(us_get_now_monotonic_u64());
return rtp;
}

View File

@@ -39,18 +39,18 @@
typedef struct {
unsigned payload;
bool video;
bool zero_playout_delay;
uint32_t ssrc;
uint16_t seq;
uint8_t datagram[US_RTP_DATAGRAM_SIZE];
size_t used;
bool zero_playout_delay;
} us_rtp_s;
typedef void (*us_rtp_callback_f)(const us_rtp_s *rtp);
us_rtp_s *us_rtp_init(unsigned payload, bool video, bool zero_playout_delay);
us_rtp_s *us_rtp_init(unsigned payload, bool video);
us_rtp_s *us_rtp_dup(const us_rtp_s *rtp);
void us_rtp_destroy(us_rtp_s *rtp);

View File

@@ -26,7 +26,7 @@
us_rtpa_s *us_rtpa_init(us_rtp_callback_f callback) {
us_rtpa_s *rtpa;
US_CALLOC(rtpa, 1);
rtpa->rtp = us_rtp_init(111, false, false);
rtpa->rtp = us_rtp_init(111, false);
rtpa->callback = callback;
return rtpa;
}

View File

@@ -31,10 +31,10 @@ void _rtpv_process_nalu(us_rtpv_s *rtpv, const uint8_t *data, size_t size, uint3
static ssize_t _find_annexb(const uint8_t *data, size_t size);
us_rtpv_s *us_rtpv_init(us_rtp_callback_f callback, bool zero_playout_delay) {
us_rtpv_s *us_rtpv_init(us_rtp_callback_f callback) {
us_rtpv_s *rtpv;
US_CALLOC(rtpv, 1);
rtpv->rtp = us_rtp_init(96, true, zero_playout_delay);
rtpv->rtp = us_rtp_init(96, true);
rtpv->callback = callback;
return rtpv;
}
@@ -59,12 +59,11 @@ char *us_rtpv_make_sdp(us_rtpv_s *rtpv) {
"a=rtcp-fb:%u nack pli" RN
"a=rtcp-fb:%u goog-remb" RN
"a=ssrc:%" PRIu32 " cname:ustreamer" RN
"%s" // playout-delay
"a=extmap:1 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" RN
"a=sendonly" RN,
PAYLOAD, PAYLOAD, PAYLOAD, PAYLOAD,
PAYLOAD, PAYLOAD, PAYLOAD,
rtpv->rtp->ssrc,
(rtpv->rtp->zero_playout_delay ? "a=extmap:1 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" RN : "")
rtpv->rtp->ssrc
);
return sdp;
# undef PAYLOAD
@@ -78,6 +77,8 @@ void us_rtpv_wrap(us_rtpv_s *rtpv, const us_frame_s *frame) {
assert(frame->format == V4L2_PIX_FMT_H264);
rtpv->rtp->zero_playout_delay = (frame->gop == 0);
const uint32_t pts = us_get_now_monotonic_u64() * 9 / 100; // PTS units are in 90 kHz
ssize_t last_offset = -_PRE;

View File

@@ -43,7 +43,7 @@ typedef struct {
} us_rtpv_s;
us_rtpv_s *us_rtpv_init(us_rtp_callback_f callback, bool zero_playout_delay);
us_rtpv_s *us_rtpv_init(us_rtp_callback_f callback);
void us_rtpv_destroy(us_rtpv_s *rtpv);
char *us_rtpv_make_sdp(us_rtpv_s *rtpv);

View File

@@ -1,4 +1,4 @@
FROM archlinux/archlinux:base-devel
FROM archlinux/archlinux:base
RUN mkdir -p /etc/pacman.d/hooks \
&& ln -s /dev/null /etc/pacman.d/hooks/30-systemd-tmpfiles.hook

View File

@@ -26,7 +26,7 @@ commands = cppcheck \
whitelist_externals = bash
commands = bash -c 'flake8 --config=linters/flake8.ini tools/*.py' python/*.py
deps =
flake8
flake8==5.0.4
flake8-quotes
[testenv:pylint]

View File

@@ -1,6 +1,6 @@
.\" Manpage for ustreamer-dump.
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
.TH USTREAMER-DUMP 1 "version 5.31" "January 2021"
.TH USTREAMER-DUMP 1 "version 5.36" "January 2021"
.SH NAME
ustreamer-dump \- Dump uStreamer's memory sink to file

View File

@@ -1,6 +1,6 @@
.\" Manpage for ustreamer.
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
.TH USTREAMER 1 "version 5.31" "November 2020"
.TH USTREAMER 1 "version 5.36" "November 2020"
.SH NAME
ustreamer \- stream MJPEG video from any V4L2 device to the network

View File

@@ -3,7 +3,7 @@
pkgname=ustreamer
pkgver=5.31
pkgver=5.36
pkgrel=1
pkgdesc="Lightweight and fast MJPEG-HTTP streamer"
url="https://github.com/pikvm/ustreamer"

View File

@@ -24,7 +24,7 @@ RUN apk add --no-cache \
WORKDIR /ustreamer
COPY --from=build /build/ustreamer/src/ustreamer.bin ustreamer
RUN wget https://raw.githubusercontent.com/pikvm/kvmd/master/configs/kvmd/tc358743-edid.hex -O /edid.hex
RUN wget https://raw.githubusercontent.com/pikvm/kvmd/master/configs/kvmd/edid/v3-hdmi.hex -O /edid.hex
COPY pkg/docker/entry.sh /
EXPOSE 8080

View File

@@ -6,7 +6,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=ustreamer
PKG_VERSION:=5.31
PKG_VERSION:=5.36
PKG_RELEASE:=1
PKG_MAINTAINER:=Maxim Devaev <mdevaev@gmail.com>

View File

@@ -17,7 +17,7 @@ def _find_sources(suffix: str) -> list[str]:
if __name__ == "__main__":
setup(
name="ustreamer",
version="5.31",
version="5.36",
description="uStreamer tools",
author="Maxim Devaev",
author_email="mdevaev@gmail.com",

View File

@@ -236,6 +236,7 @@ static PyObject *_MemsinkObject_wait_frame(_MemsinkObject *self, PyObject *args,
SET_NUMBER(stride, Long, Long);
SET_NUMBER(online, Long, Bool);
SET_NUMBER(key, Long, Bool);
SET_NUMBER(gop, Long, Long);
SET_NUMBER(grab_ts, Double, Float);
SET_NUMBER(encode_begin_ts, Double, Float);
SET_NUMBER(encode_end_ts, Double, Float);

View File

@@ -52,11 +52,11 @@ void us_output_file_write(void *v_output, const us_frame_s *frame) {
us_base64_encode(frame->data, frame->used, &output->base64_data, &output->base64_allocated);
fprintf(output->fp,
"{\"size\": %zu, \"width\": %u, \"height\": %u,"
" \"format\": %u, \"stride\": %u, \"online\": %u, \"key\": %u,"
" \"format\": %u, \"stride\": %u, \"online\": %u, \"key\": %u, \"gop\": %u,"
" \"grab_ts\": %.3Lf, \"encode_begin_ts\": %.3Lf, \"encode_end_ts\": %.3Lf,"
" \"data\": \"%s\"}\n",
frame->used, frame->width, frame->height,
frame->format, frame->stride, frame->online, frame->key,
frame->format, frame->stride, frame->online, frame->key, frame->gop,
frame->grab_ts, frame->encode_begin_ts, frame->encode_end_ts,
output->base64_data);
} else {

View File

@@ -260,16 +260,16 @@ static int _dump_sink(
const long long now_second = us_floor_ms(now);
char fourcc_str[8];
US_LOG_VERBOSE("Frame: res=%ux%u, fmt=%s, stride=%u, online=%d, key=%d, kr=%d, latency=%.3Lf, diff=%.3Lf, size=%zu",
frame->width, frame->height,
US_LOG_VERBOSE("Frame: %s - %ux%u -- online=%d, key=%d, kr=%d, gop=%u, latency=%.3Lf, backlog=%.3Lf, size=%zu",
us_fourcc_to_string(frame->format, fourcc_str, 8),
frame->stride, frame->online, frame->key, key_requested,
frame->width, frame->height,
frame->online, frame->key, key_requested, frame->gop,
now - frame->grab_ts, (last_ts ? now - last_ts : 0),
frame->used);
last_ts = now;
US_LOG_DEBUG(" grab_ts=%.3Lf, encode_begin_ts=%.3Lf, encode_end_ts=%.3Lf",
frame->grab_ts, frame->encode_begin_ts, frame->encode_end_ts);
US_LOG_DEBUG(" stride=%u, grab_ts=%.3Lf, encode_begin_ts=%.3Lf, encode_end_ts=%.3Lf",
frame->stride, frame->grab_ts, frame->encode_begin_ts, frame->encode_end_ts);
if (now_second != fps_second) {
fps = fps_accum;

View File

@@ -23,7 +23,7 @@
#pragma once
#define US_VERSION_MAJOR 5
#define US_VERSION_MINOR 31
#define US_VERSION_MINOR 36
#define US_MAKE_VERSION2(_major, _minor) #_major "." #_minor
#define US_MAKE_VERSION1(_major, _minor) US_MAKE_VERSION2(_major, _minor)

View File

@@ -50,6 +50,7 @@ typedef struct {
bool online;
bool key;
unsigned gop;
long double grab_ts;
long double encode_begin_ts;
@@ -64,6 +65,7 @@ typedef struct {
x_dest->stride = x_src->stride; \
x_dest->online = x_src->online; \
x_dest->key = x_src->key; \
x_dest->gop = x_src->gop; \
x_dest->grab_ts = x_src->grab_ts; \
x_dest->encode_begin_ts = x_src->encode_begin_ts; \
x_dest->encode_end_ts = x_src->encode_end_ts; \
@@ -81,6 +83,7 @@ static inline void us_frame_copy_meta(const us_frame_s *src, us_frame_s *dest) {
&& x_a->stride == x_b->stride \
&& x_a->online == x_b->online \
&& x_a->key == x_b->key \
&& x_a->gop == x_b->gop \
)

View File

@@ -30,7 +30,7 @@
#define US_MEMSINK_MAGIC ((uint64_t)0xCAFEBABECAFEBABE)
#define US_MEMSINK_VERSION ((uint32_t)3)
#define US_MEMSINK_VERSION ((uint32_t)4)
#ifndef US_CFG_MEMSINK_MAX_DATA
# define US_CFG_MEMSINK_MAX_DATA 33554432
@@ -51,6 +51,7 @@ typedef struct {
unsigned stride;
bool online;
bool key;
unsigned gop;
long double grab_ts;
long double encode_begin_ts;

View File

@@ -476,6 +476,7 @@ static int _m2m_encoder_compress_raw(us_m2m_encoder_s *enc, const us_frame_s *sr
} else {
us_frame_set_data(dest, _RUN(output_bufs[output_buf.index].data), output_plane.bytesused);
dest->key = output_buf.flags & V4L2_BUF_FLAG_KEYFRAME;
dest->gop = enc->gop;
done = true;
}