mirror of
https://github.com/pikvm/ustreamer.git
synced 2026-02-19 16:26:30 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f83ff439c4 | ||
|
|
983796e952 | ||
|
|
40be0c20e2 | ||
|
|
f1e9d4568c | ||
|
|
f4f57cce38 | ||
|
|
c87ad5703c | ||
|
|
95df13b7cb | ||
|
|
bfa1516491 | ||
|
|
36c9ff22b3 | ||
|
|
17f54a7977 | ||
|
|
574986d0fd | ||
|
|
d5ce2e835f | ||
|
|
6201554ba1 | ||
|
|
75dee4e91d | ||
|
|
e1b4e0db66 | ||
|
|
0d37b09bb4 | ||
|
|
18767b68ff | ||
|
|
7975615c6c | ||
|
|
90f09b197e | ||
|
|
687e97d523 | ||
|
|
c1675001fa | ||
|
|
69cc45a2a0 | ||
|
|
ece96b5834 | ||
|
|
383ed7530b | ||
|
|
7f620c758f |
@@ -1,7 +1,7 @@
|
||||
[bumpversion]
|
||||
commit = True
|
||||
tag = True
|
||||
current_version = 5.21
|
||||
current_version = 5.26
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)
|
||||
serialize =
|
||||
{major}.{minor}
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
!python/**
|
||||
!janus/**
|
||||
!man/**
|
||||
!pkg/docker/entry.sh
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,4 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
patreon: pikvm
|
||||
#custom: https://www.paypal.me/mdevaev
|
||||
custom: https://paypal.me/pikvm
|
||||
|
||||
81
.github/workflows/docker-alpine-image.yaml
vendored
Normal file
81
.github/workflows/docker-alpine-image.yaml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: Build Alpine
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, build_*]
|
||||
tags: [v*]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Login to DockerHub Container Registry
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/ustreamer,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
ghcr.io/${{ github.repository }},enable=${{ github.event_name != 'pull_request' }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=sha,format=long
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
driver-opts: image=moby/buildkit:master
|
||||
-
|
||||
name: Build and export to Docker
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
load: true
|
||||
tags: ustreamer
|
||||
file: pkg/docker/Dockerfile.alpine
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
echo version: $(docker run --rm -t ustreamer --version)
|
||||
echo -e "features:\n$(docker run --rm -t ustreamer --features)"
|
||||
-
|
||||
name: Build multi arch
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/arm64,linux/amd64,linux/arm/v7
|
||||
file: pkg/docker/Dockerfile.alpine
|
||||
-
|
||||
name: Push
|
||||
if: steps.meta.outputs.tags != ''
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/arm64,linux/amd64,linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
file: pkg/docker/Dockerfile.alpine
|
||||
49
README.md
49
README.md
@@ -81,8 +81,57 @@ $ ./ustreamer \
|
||||
|
||||
You can always view the full list of options with ```ustreamer --help```.
|
||||
|
||||
-----
|
||||
# Docker (Raspberry Pi 4 HDMI)
|
||||
|
||||
## Preparations
|
||||
Add following lines to /boot/firmware/usercfg.txt:
|
||||
|
||||
```
|
||||
gpu_mem=128
|
||||
dtoverlay=tc358743
|
||||
```
|
||||
|
||||
Check size of CMA:
|
||||
|
||||
```bash
|
||||
$ dmesg | grep cma-reserved
|
||||
[ 0.000000] Memory: 7700524K/8244224K available (11772K kernel code, 1278K rwdata, 4320K rodata, 4096K init, 1077K bss, 281556K reserved, 262144K cma-reserved)
|
||||
```
|
||||
|
||||
If it is smaller than 128M add following to /boot/firmware/cmdline.txt:
|
||||
|
||||
```
|
||||
cma=128M
|
||||
```
|
||||
|
||||
Save changes and reboot.
|
||||
|
||||
## Launch
|
||||
Start container:
|
||||
|
||||
```bash
|
||||
$ docker run --device /dev/video0:/dev/video0 -e EDID=1 -p 8080:8080 pikvm/ustreamer:latest
|
||||
```
|
||||
|
||||
Then access the web interface at port 8080 (e.g. http://raspberrypi.local:8080).
|
||||
|
||||
## Custom config
|
||||
```bash
|
||||
$ docker run --rm pikvm/ustreamer:latest \
|
||||
--format=uyvy \
|
||||
--workers=3 \
|
||||
--persistent \
|
||||
--dv-timings \
|
||||
--drop-same-frames=30
|
||||
```
|
||||
|
||||
## EDID
|
||||
Add `-e EDID=1` to set HDMI EDID before starging ustreamer. Use together with `-e EDID_HEX=xx` to specify custom EDID data.
|
||||
|
||||
-----
|
||||
# Raspberry Pi Camera Example
|
||||
|
||||
Example usage for the Raspberry Pi v1 camera:
|
||||
```bash
|
||||
$ sudo modprobe bcm2835-v4l2
|
||||
|
||||
29
docs/h264.md
29
docs/h264.md
@@ -78,6 +78,17 @@ memsink: {
|
||||
EOF
|
||||
```
|
||||
|
||||
If you're using a TC358743-based video capture device that supports audio capture, run the following command to enable audio streaming:
|
||||
|
||||
```sh
|
||||
cat << EOF >> /opt/janus/lib/janus/configs/janus.plugin.ustreamer.jcfg
|
||||
audio: {
|
||||
device = "hw:1"
|
||||
tc358743 = "/dev/video0"
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
### Start µStreamer and the Janus WebRTC Server
|
||||
|
||||
For µStreamer to share the video stream with the µStreamer Janus plugin, µStreamer must run with the following command-line flags:
|
||||
@@ -111,13 +122,14 @@ The client-side JavaScript application uses the following control flow:
|
||||
1. The client instructs the Janus server to attach the µStreamer Janus plugin.
|
||||
1. On success, the client obtains a plugin handle through which it can send requests directly to the µStreamer Janus plugin. The client processes responses via the `attach` callbacks:
|
||||
- `onmessage` for general messages
|
||||
- `onremotetrack` for the H.264 video stream
|
||||
- `onremotetrack` for the H.264 video stream and (optionally) an Opus audio stream
|
||||
1. The client issues a `watch` request to the µStreamer Janus plugin, which initiates the H.264 stream in the plugin itself.
|
||||
- It takes a few seconds for uStreamer's video stream to become available to Janus. The first `watch` request may fail, so the client must retry the `watch` request.
|
||||
1. The client and server negotiate the underlying parameters of the WebRTC session. This procedure is called JavaScript Session Establishment Protocol (JSEP). The server makes a `jsepOffer` to the client, and the client responds with a `jsepAnswer`.
|
||||
1. The client issues a `start` request to the µStreamer Janus plugin to indicate that the client wants to begin consuming the video stream.
|
||||
1. The µStreamer Janus plugin delivers the H.264 video stream to the client via WebRTC.
|
||||
1. The Janus client library invokes the `onremotetrack` callback. The client attaches the video stream to the `<video>` element, rendering the video stream in the browser window.
|
||||
1. The µStreamer Janus plugin delivers the H.264 video stream and (optionally) an Opus audio stream to the client via WebRTC.
|
||||
1. The Janus client library invokes the `onremotetrack` callback with the video stream. The client attaches the video stream to the `<video>` element, rendering the video stream in the browser window.
|
||||
1. (if an audio track is available) The Janus client library invokes the `onremotetrack` callback with the Opus audio stream. The client adds the audio stream to the `<video>` element, rendering the audio in the browser window.
|
||||
|
||||
### Sample Code
|
||||
|
||||
@@ -176,7 +188,7 @@ The client-side JavaScript application uses the following control flow:
|
||||
// successfully.
|
||||
success: function (pluginHandle) {
|
||||
uStreamerPluginHandle = pluginHandle;
|
||||
// Instruct the µStreamer Janus plugin to initiate the video stream.
|
||||
// Instruct the µStreamer Janus plugin to initiate streaming.
|
||||
uStreamerPluginHandle.send({ message: { request: "watch" } });
|
||||
},
|
||||
|
||||
@@ -211,16 +223,17 @@ The client-side JavaScript application uses the following control flow:
|
||||
}
|
||||
},
|
||||
|
||||
// Callback function, for when the video stream arrives.
|
||||
// Callback function, for when a media stream arrives.
|
||||
onremotetrack: function (mediaStreamTrack, mediaId, isAdded) {
|
||||
if (isAdded) {
|
||||
// Attach the received media track to the video element. Cloning the
|
||||
// mediaStreamTrack creates a new object with a distinct, globally
|
||||
// unique stream identifier.
|
||||
const videoElement = document.getElementById("webrtc-output");
|
||||
const stream = new MediaStream();
|
||||
stream.addTrack(mediaStreamTrack.clone());
|
||||
videoElement.srcObject = stream;
|
||||
if (videoElement.srcObject === null) {
|
||||
videoElement.srcObject = new MediaStream();
|
||||
}
|
||||
videoElement.srcObject.addTrack(mediaStreamTrack.clone());
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -46,12 +46,15 @@ int us_memsink_fd_wait_frame(int fd, us_memsink_shared_s* mem, uint64_t last_id)
|
||||
return -2;
|
||||
}
|
||||
|
||||
us_frame_s *us_memsink_fd_get_frame(int fd, us_memsink_shared_s *mem, uint64_t *frame_id) {
|
||||
us_frame_s *us_memsink_fd_get_frame(int fd, us_memsink_shared_s *mem, uint64_t *frame_id, bool key_required) {
|
||||
us_frame_s *frame = us_frame_init();
|
||||
us_frame_set_data(frame, mem->data, mem->used);
|
||||
US_FRAME_COPY_META(mem, frame);
|
||||
*frame_id = mem->id;
|
||||
mem->last_client_ts = us_get_now_monotonic();
|
||||
if (key_required) {
|
||||
mem->key_requested = true;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
if (frame->format != V4L2_PIX_FMT_H264) {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -35,4 +36,4 @@
|
||||
|
||||
|
||||
int us_memsink_fd_wait_frame(int fd, us_memsink_shared_s* mem, uint64_t last_id);
|
||||
us_frame_s *us_memsink_fd_get_frame(int fd, us_memsink_shared_s *mem, uint64_t *frame_id);
|
||||
us_frame_s *us_memsink_fd_get_frame(int fd, us_memsink_shared_s *mem, uint64_t *frame_id, bool key_required);
|
||||
|
||||
@@ -76,6 +76,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_key_required = false;
|
||||
|
||||
|
||||
#define _LOCK_VIDEO US_MUTEX_LOCK(_g_video_lock)
|
||||
@@ -149,10 +150,13 @@ static void *_video_sink_thread(UNUSED void *arg) {
|
||||
while (!_STOP && _HAS_WATCHERS) {
|
||||
const int result = us_memsink_fd_wait_frame(fd, mem, frame_id);
|
||||
if (result == 0) {
|
||||
us_frame_s *const frame = us_memsink_fd_get_frame(fd, mem, &frame_id);
|
||||
us_frame_s *const frame = us_memsink_fd_get_frame(fd, mem, &frame_id, atomic_load(&_g_key_required));
|
||||
if (frame == NULL) {
|
||||
goto close_memsink;
|
||||
}
|
||||
// if (frame->key) {
|
||||
// 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_frame_destroy(frame);
|
||||
@@ -250,7 +254,7 @@ static int _plugin_init(janus_callbacks *gw, const char *config_dir_path) {
|
||||
// sysctl -w net.core.rmem_max=1000000
|
||||
// sysctl -w net.core.wmem_max=1000000
|
||||
|
||||
US_JLOG_INFO("main", "Initializing plugin ...");
|
||||
US_JLOG_INFO("main", "Initializing PiKVM uStreamer plugin %s ...", US_VERSION);
|
||||
if (gw == NULL || config_dir_path == NULL || ((_g_config = us_config_init(config_dir_path)) == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
@@ -423,6 +427,7 @@ static struct janus_plugin_result *_plugin_handle_message(
|
||||
} else if (!strcmp(request_str, "watch")) {
|
||||
char *sdp;
|
||||
{
|
||||
// atomic_store(&_g_key_required, true);
|
||||
char *const video_sdp = us_rtpv_make_sdp(_g_rtpv);
|
||||
if (video_sdp == NULL) {
|
||||
PUSH_ERROR(503, "Haven't received SPS/PPS from memsink yet");
|
||||
|
||||
@@ -3,7 +3,7 @@ FROM archlinux/archlinux:base-devel
|
||||
RUN mkdir -p /etc/pacman.d/hooks \
|
||||
&& ln -s /dev/null /etc/pacman.d/hooks/30-systemd-tmpfiles.hook
|
||||
|
||||
RUN echo "Server = http://mirror.yandex.ru/archlinux/\$repo/os/\$arch" > /etc/pacman.d/mirrorlist
|
||||
RUN echo 'Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch' > /etc/pacman.d/mirrorlist
|
||||
|
||||
RUN pacman -Syu --noconfirm \
|
||||
&& pacman -S --needed --noconfirm \
|
||||
|
||||
@@ -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.21" "January 2021"
|
||||
.TH USTREAMER-DUMP 1 "version 5.26" "January 2021"
|
||||
|
||||
.SH NAME
|
||||
ustreamer-dump \- Dump uStreamer's memory sink to file
|
||||
@@ -44,6 +44,9 @@ Limit the number of frames. Default: 0 (infinite).
|
||||
.TP
|
||||
.BR \-i ", "\-\-interval\ \fIsec
|
||||
Delay between reading frames (float). Default: 0.
|
||||
.TP
|
||||
.BR \-k ", " \-\-key
|
||||
Request keyframe from the sink. Default: disabled.
|
||||
|
||||
.SS "Logging options"
|
||||
.TP
|
||||
|
||||
@@ -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.21" "November 2020"
|
||||
.TH USTREAMER 1 "version 5.26" "November 2020"
|
||||
|
||||
.SH NAME
|
||||
ustreamer \- stream MJPEG video from any V4L2 device to the network
|
||||
@@ -203,6 +203,9 @@ Default: disabled.
|
||||
.BR \-\-allow\-origin\ \fIstr
|
||||
Set Access\-Control\-Allow\-Origin header. Default: disabled.
|
||||
.TP
|
||||
.BR \-\-instance\-id\ \fIstr
|
||||
A short string identifier to be displayed in the /state handle. It must satisfy regexp ^[a-zA-Z0-9\./+_-]*$. Default: an empty string.
|
||||
.TP
|
||||
.BR \-\-server\-timeout\ \fIsec
|
||||
Timeout for client connections. Default: 10.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
|
||||
pkgname=ustreamer
|
||||
pkgver=5.21
|
||||
pkgver=5.26
|
||||
pkgrel=1
|
||||
pkgdesc="Lightweight and fast MJPEG-HTTP streamer"
|
||||
url="https://github.com/pikvm/ustreamer"
|
||||
|
||||
34
pkg/docker/Dockerfile.alpine
Normal file
34
pkg/docker/Dockerfile.alpine
Normal file
@@ -0,0 +1,34 @@
|
||||
FROM alpine:3.16 as build
|
||||
RUN apk add --no-cache \
|
||||
alpine-sdk \
|
||||
linux-headers \
|
||||
libjpeg-turbo-dev \
|
||||
libevent-dev \
|
||||
libbsd-dev \
|
||||
libgpiod-dev
|
||||
|
||||
WORKDIR /build/ustreamer/
|
||||
COPY . .
|
||||
RUN make -j5 WITH_GPIO=1
|
||||
|
||||
FROM alpine:3.16 as run
|
||||
|
||||
RUN apk add --no-cache \
|
||||
libevent \
|
||||
libjpeg-turbo \
|
||||
libevent \
|
||||
libgpiod \
|
||||
libbsd \
|
||||
v4l-utils
|
||||
|
||||
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
|
||||
COPY pkg/docker/entry.sh /
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/entry.sh"]
|
||||
CMD ["--dv-timings", "--format", "UYVY"]
|
||||
|
||||
# vim: syntax=dockerfile
|
||||
14
pkg/docker/entry.sh
Executable file
14
pkg/docker/entry.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
[ -n "$EDID" ] && {
|
||||
[ -n "$EDID_HEX" ] && echo "$EDID_HEX" > /edid.hex
|
||||
while true; do
|
||||
v4l2-ctl --device=/dev/video0 --set-edid=file=/edid.hex --fix-edid-checksums --info-edid && break
|
||||
echo 'Failed to set EDID. Reetrying...'
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
./ustreamer --host=0.0.0.0 $@
|
||||
@@ -6,7 +6,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=ustreamer
|
||||
PKG_VERSION:=5.21
|
||||
PKG_VERSION:=5.26
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=Maxim Devaev <mdevaev@gmail.com>
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import os
|
||||
|
||||
from typing import List
|
||||
|
||||
from setuptools import Extension
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
# =====
|
||||
def _find_sources(suffix: str) -> List[str]:
|
||||
sources: List[str] = []
|
||||
def _find_sources(suffix: str) -> list[str]:
|
||||
sources: list[str] = []
|
||||
for (root_path, _, names) in os.walk("src"):
|
||||
for name in names:
|
||||
if name.endswith(suffix):
|
||||
@@ -19,7 +17,7 @@ def _find_sources(suffix: str) -> List[str]:
|
||||
if __name__ == "__main__":
|
||||
setup(
|
||||
name="ustreamer",
|
||||
version="5.21",
|
||||
version="5.26",
|
||||
description="uStreamer tools",
|
||||
author="Maxim Devaev",
|
||||
author_email="mdevaev@gmail.com",
|
||||
|
||||
@@ -181,12 +181,18 @@ static int _wait_frame(_MemsinkObject *self) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
static PyObject *_MemsinkObject_wait_frame(_MemsinkObject *self, PyObject *Py_UNUSED(ignored)) {
|
||||
static PyObject *_MemsinkObject_wait_frame(_MemsinkObject *self, PyObject *args, PyObject *kwargs) {
|
||||
if (self->mem == NULL || self->fd <= 0) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Closed");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool key_required = false;
|
||||
static char *kws[] = {"key_required", NULL};
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|p", kws, &key_required)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
switch (_wait_frame(self)) {
|
||||
case 0: break;
|
||||
case -2: Py_RETURN_NONE;
|
||||
@@ -198,6 +204,9 @@ static PyObject *_MemsinkObject_wait_frame(_MemsinkObject *self, PyObject *Py_UN
|
||||
self->frame_id = _MEM(id);
|
||||
self->frame_ts = us_get_now_monotonic();
|
||||
_MEM(last_client_ts) = self->frame_ts;
|
||||
if (key_required) {
|
||||
_MEM(key_requested) = true;
|
||||
}
|
||||
|
||||
if (flock(self->fd, LOCK_UN) < 0) {
|
||||
return PyErr_SetFromErrno(PyExc_OSError);
|
||||
@@ -260,7 +269,7 @@ static PyMethodDef _MemsinkObject_methods[] = {
|
||||
ADD_METHOD("close", close, METH_NOARGS),
|
||||
ADD_METHOD("__enter__", enter, METH_NOARGS),
|
||||
ADD_METHOD("__exit__", exit, METH_VARARGS),
|
||||
ADD_METHOD("wait_frame", wait_frame, METH_NOARGS),
|
||||
ADD_METHOD("wait_frame", wait_frame, METH_VARARGS | METH_KEYWORDS),
|
||||
ADD_METHOD("is_opened", is_opened, METH_NOARGS),
|
||||
{},
|
||||
# undef ADD_METHOD
|
||||
|
||||
@@ -48,6 +48,7 @@ enum _OPT_VALUES {
|
||||
_O_OUTPUT_JSON = 'j',
|
||||
_O_COUNT = 'c',
|
||||
_O_INTERVAL = 'i',
|
||||
_O_KEY = 'k',
|
||||
|
||||
_O_HELP = 'h',
|
||||
_O_VERSION = 'v',
|
||||
@@ -67,6 +68,7 @@ static const struct option _LONG_OPTS[] = {
|
||||
{"output-json", no_argument, NULL, _O_OUTPUT_JSON},
|
||||
{"count", required_argument, NULL, _O_COUNT},
|
||||
{"interval", required_argument, NULL, _O_INTERVAL},
|
||||
{"key", no_argument, NULL, _O_KEY},
|
||||
|
||||
{"log-level", required_argument, NULL, _O_LOG_LEVEL},
|
||||
{"perf", no_argument, NULL, _O_PERF},
|
||||
@@ -98,6 +100,7 @@ static void _install_signal_handlers(void);
|
||||
static int _dump_sink(
|
||||
const char *sink_name, unsigned sink_timeout,
|
||||
long long count, long double interval,
|
||||
bool key_required,
|
||||
_output_context_s *ctx);
|
||||
|
||||
static void _help(FILE *fp);
|
||||
@@ -113,6 +116,7 @@ int main(int argc, char *argv[]) {
|
||||
bool output_json = false;
|
||||
long long count = 0;
|
||||
long double interval = 0;
|
||||
bool key_required = false;
|
||||
|
||||
# define OPT_SET(_dest, _value) { \
|
||||
_dest = _value; \
|
||||
@@ -150,6 +154,7 @@ int main(int argc, char *argv[]) {
|
||||
case _O_OUTPUT_JSON: OPT_SET(output_json, true);
|
||||
case _O_COUNT: OPT_NUMBER("--count", count, 0, LLONG_MAX, 0);
|
||||
case _O_INTERVAL: OPT_LDOUBLE("--interval", interval, 0, 60);
|
||||
case _O_KEY: OPT_SET(key_required, true);
|
||||
|
||||
case _O_LOG_LEVEL: OPT_NUMBER("--log-level", us_g_log_level, US_LOG_LEVEL_INFO, US_LOG_LEVEL_DEBUG, 0);
|
||||
case _O_PERF: OPT_SET(us_g_log_level, US_LOG_LEVEL_PERF);
|
||||
@@ -186,7 +191,7 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
|
||||
_install_signal_handlers();
|
||||
const int retval = abs(_dump_sink(sink_name, sink_timeout, count, interval, &ctx));
|
||||
const int retval = abs(_dump_sink(sink_name, sink_timeout, count, interval, key_required, &ctx));
|
||||
if (ctx.v_output && ctx.destroy) {
|
||||
ctx.destroy(ctx.v_output);
|
||||
}
|
||||
@@ -223,6 +228,7 @@ static void _install_signal_handlers(void) {
|
||||
static int _dump_sink(
|
||||
const char *sink_name, unsigned sink_timeout,
|
||||
long long count, long double interval,
|
||||
bool key_required,
|
||||
_output_context_s *ctx) {
|
||||
|
||||
if (count == 0) {
|
||||
@@ -245,8 +251,10 @@ static int _dump_sink(
|
||||
long double last_ts = 0;
|
||||
|
||||
while (!_g_stop) {
|
||||
int error = us_memsink_client_get(sink, frame);
|
||||
const int error = us_memsink_client_get(sink, frame, key_required);
|
||||
if (error == 0) {
|
||||
key_required = false;
|
||||
|
||||
const long double now = us_get_now_monotonic();
|
||||
const long long now_second = us_floor_ms(now);
|
||||
|
||||
@@ -322,6 +330,7 @@ static void _help(FILE *fp) {
|
||||
SAY(" -j|--output-json ──────── Format output as JSON. Required option --output. Default: disabled.\n");
|
||||
SAY(" -c|--count <N> ───────── Limit the number of frames. Default: 0 (infinite).\n");
|
||||
SAY(" -i|--interval <sec> ───── Delay between reading frames (float). Default: 0.\n");
|
||||
SAY(" -k|--key ──────────────── Request keyframe from the sink. Default: disabled.\n");
|
||||
SAY("Logging options:");
|
||||
SAY("════════════════");
|
||||
SAY(" --log-level <N> ──── Verbosity level of messages from 0 (info) to 3 (debug).");
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#pragma once
|
||||
|
||||
#define US_VERSION_MAJOR 5
|
||||
#define US_VERSION_MINOR 21
|
||||
#define US_VERSION_MINOR 26
|
||||
|
||||
#define US_MAKE_VERSION2(_major, _minor) #_major "." #_minor
|
||||
#define US_MAKE_VERSION1(_major, _minor) US_MAKE_VERSION2(_major, _minor)
|
||||
|
||||
@@ -117,7 +117,7 @@ bool us_memsink_server_check(us_memsink_s *sink, const us_frame_s *frame) {
|
||||
return (has_clients || !US_FRAME_COMPARE_META_USED_NOTS(sink->mem, frame));;
|
||||
}
|
||||
|
||||
int us_memsink_server_put(us_memsink_s *sink, const us_frame_s *frame) {
|
||||
int us_memsink_server_put(us_memsink_s *sink, const us_frame_s *frame, bool *const key_requested) {
|
||||
assert(sink->server);
|
||||
|
||||
const long double now = us_get_now_monotonic();
|
||||
@@ -133,6 +133,12 @@ int us_memsink_server_put(us_memsink_s *sink, const us_frame_s *frame) {
|
||||
|
||||
sink->last_id = us_get_now_id();
|
||||
sink->mem->id = sink->last_id;
|
||||
if (sink->mem->key_requested && frame->key) {
|
||||
sink->mem->key_requested = false;
|
||||
}
|
||||
if (key_requested != NULL) {
|
||||
*key_requested = sink->mem->key_requested;
|
||||
}
|
||||
|
||||
memcpy(sink->mem->data, frame->data, frame->used);
|
||||
sink->mem->used = frame->used;
|
||||
@@ -140,6 +146,7 @@ int us_memsink_server_put(us_memsink_s *sink, const us_frame_s *frame) {
|
||||
|
||||
sink->mem->magic = US_MEMSINK_MAGIC;
|
||||
sink->mem->version = US_MEMSINK_VERSION;
|
||||
|
||||
atomic_store(&sink->has_clients, (sink->mem->last_client_ts + sink->client_ttl > us_get_now_monotonic()));
|
||||
|
||||
if (flock(sink->fd, LOCK_UN) < 0) {
|
||||
@@ -159,7 +166,7 @@ int us_memsink_server_put(us_memsink_s *sink, const us_frame_s *frame) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int us_memsink_client_get(us_memsink_s *sink, us_frame_s *frame) { // cppcheck-suppress unusedFunction
|
||||
int us_memsink_client_get(us_memsink_s *sink, us_frame_s *frame, bool key_required) { // cppcheck-suppress unusedFunction
|
||||
assert(!sink->server); // Client only
|
||||
|
||||
if (us_flock_timedwait_monotonic(sink->fd, sink->timeout) < 0) {
|
||||
@@ -185,6 +192,9 @@ int us_memsink_client_get(us_memsink_s *sink, us_frame_s *frame) { // cppcheck-s
|
||||
retval = 0;
|
||||
}
|
||||
sink->mem->last_client_ts = us_get_now_monotonic();
|
||||
if (key_required) {
|
||||
sink->mem->key_requested = true;
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
|
||||
@@ -63,6 +63,6 @@ us_memsink_s *us_memsink_init(
|
||||
void us_memsink_destroy(us_memsink_s *sink);
|
||||
|
||||
bool us_memsink_server_check(us_memsink_s *sink, const us_frame_s *frame);
|
||||
int us_memsink_server_put(us_memsink_s *sink, const us_frame_s *frame);
|
||||
int us_memsink_server_put(us_memsink_s *sink, const us_frame_s *frame, bool *const key_requested);
|
||||
|
||||
int us_memsink_client_get(us_memsink_s *sink, us_frame_s *frame);
|
||||
int us_memsink_client_get(us_memsink_s *sink, us_frame_s *frame, bool key_required);
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
|
||||
#define US_MEMSINK_MAGIC ((uint64_t)0xCAFEBABECAFEBABE)
|
||||
#define US_MEMSINK_VERSION ((uint32_t)2)
|
||||
#define US_MEMSINK_VERSION ((uint32_t)3)
|
||||
|
||||
#ifndef US_CFG_MEMSINK_MAX_DATA
|
||||
# define US_CFG_MEMSINK_MAX_DATA 33554432
|
||||
@@ -57,6 +57,7 @@ typedef struct {
|
||||
long double encode_end_ts;
|
||||
|
||||
long double last_client_ts;
|
||||
bool key_requested;
|
||||
|
||||
uint8_t data[US_MEMSINK_MAX_DATA];
|
||||
} us_memsink_shared_s;
|
||||
|
||||
@@ -56,9 +56,15 @@ void us_h264_stream_process(us_h264_stream_s *h264, const us_frame_s *frame, boo
|
||||
US_LOG_VERBOSE("H264: JPEG decoded; time=%.3Lf", us_get_now_monotonic() - now);
|
||||
}
|
||||
|
||||
if (h264->key_requested) {
|
||||
US_LOG_INFO("H264: Requested keyframe by a sink client");
|
||||
h264->key_requested = false;
|
||||
force_key = true;
|
||||
}
|
||||
|
||||
bool online = false;
|
||||
if (!us_m2m_encoder_compress(h264->enc, frame, h264->dest, force_key)) {
|
||||
online = !us_memsink_server_put(h264->sink, h264->dest);
|
||||
online = !us_memsink_server_put(h264->sink, h264->dest, &h264->key_requested);
|
||||
}
|
||||
atomic_store(&h264->online, online);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
|
||||
typedef struct {
|
||||
us_memsink_s *sink;
|
||||
bool key_requested;
|
||||
us_frame_s *tmp_src;
|
||||
us_frame_s *dest;
|
||||
us_m2m_encoder_s *enc;
|
||||
|
||||
@@ -75,6 +75,7 @@ us_server_s *us_server_init(us_stream_s *stream) {
|
||||
server->passwd = "";
|
||||
server->static_path = "";
|
||||
server->allow_origin = "";
|
||||
server->instance_id = "";
|
||||
server->timeout = 10;
|
||||
server->run = run;
|
||||
|
||||
@@ -400,7 +401,9 @@ static void _http_callback_state(struct evhttp_request *request, void *v_server)
|
||||
|
||||
_A_EVBUFFER_ADD_PRINTF(buf,
|
||||
"{\"ok\": true, \"result\": {"
|
||||
" \"instance_id\": \"%s\","
|
||||
" \"encoder\": {\"type\": \"%s\", \"quality\": %u},",
|
||||
server->instance_id,
|
||||
us_encoder_type_to_string(enc_type),
|
||||
enc_quality
|
||||
);
|
||||
|
||||
@@ -149,6 +149,7 @@ typedef struct us_server_sx {
|
||||
char *passwd;
|
||||
char *static_path;
|
||||
char *allow_origin;
|
||||
char *instance_id;
|
||||
|
||||
unsigned drop_same_frames;
|
||||
unsigned fake_width;
|
||||
|
||||
@@ -220,7 +220,11 @@ static void _m2m_encoder_prepare(us_m2m_encoder_s *enc, const us_frame_s *frame)
|
||||
fmt.fmt.pix_mp.colorspace = V4L2_COLORSPACE_DEFAULT;
|
||||
fmt.fmt.pix_mp.num_planes = 1;
|
||||
// fmt.fmt.pix_mp.plane_fmt[0].bytesperline = 0;
|
||||
// fmt.fmt.pix_mp.plane_fmt[0].sizeimage = 512 << 10;
|
||||
if (enc->output_format == V4L2_PIX_FMT_H264) {
|
||||
// https://github.com/pikvm/ustreamer/issues/169
|
||||
// https://github.com/raspberrypi/linux/pull/5232
|
||||
fmt.fmt.pix_mp.plane_fmt[0].sizeimage = (1024 + 512) << 10; // 1.5Mb
|
||||
}
|
||||
_E_LOG_DEBUG("Configuring OUTPUT format ...");
|
||||
_E_XIOCTL(VIDIOC_S_FMT, &fmt, "Can't set OUTPUT format");
|
||||
if (fmt.fmt.pix_mp.pixelformat != enc->output_format) {
|
||||
|
||||
@@ -82,6 +82,7 @@ enum _US_OPT_VALUES {
|
||||
_O_PASSWD,
|
||||
_O_STATIC,
|
||||
_O_ALLOW_ORIGIN,
|
||||
_O_INSTANCE_ID,
|
||||
_O_TCP_NODELAY,
|
||||
_O_SERVER_TIMEOUT,
|
||||
|
||||
@@ -177,6 +178,7 @@ static const struct option _LONG_OPTS[] = {
|
||||
{"static", required_argument, NULL, _O_STATIC},
|
||||
{"drop-same-frames", required_argument, NULL, _O_DROP_SAME_FRAMES},
|
||||
{"allow-origin", required_argument, NULL, _O_ALLOW_ORIGIN},
|
||||
{"instance-id", required_argument, NULL, _O_INSTANCE_ID},
|
||||
{"fake-resolution", required_argument, NULL, _O_FAKE_RESOLUTION},
|
||||
{"tcp-nodelay", no_argument, NULL, _O_TCP_NODELAY},
|
||||
{"server-timeout", required_argument, NULL, _O_SERVER_TIMEOUT},
|
||||
@@ -228,6 +230,7 @@ static const struct option _LONG_OPTS[] = {
|
||||
|
||||
|
||||
static int _parse_resolution(const char *str, unsigned *width, unsigned *height, bool limited);
|
||||
static int _check_instance_id(const char *str);
|
||||
|
||||
static void _features(void);
|
||||
static void _help(FILE *fp, us_device_s *dev, us_encoder_s *enc, us_stream_s *stream, us_server_s *server);
|
||||
@@ -419,6 +422,13 @@ int options_parse(us_options_s *options, us_device_s *dev, us_encoder_s *enc, us
|
||||
case _O_DROP_SAME_FRAMES: OPT_NUMBER("--drop-same-frames", server->drop_same_frames, 0, US_VIDEO_MAX_FPS, 0);
|
||||
case _O_FAKE_RESOLUTION: OPT_RESOLUTION("--fake-resolution", server->fake_width, server->fake_height, false);
|
||||
case _O_ALLOW_ORIGIN: OPT_SET(server->allow_origin, optarg);
|
||||
case _O_INSTANCE_ID:
|
||||
if (_check_instance_id(optarg) != 0) {
|
||||
printf("Invalid instance ID, it should be like: ^[a-zA-Z0-9\\./+_-]*$\n");
|
||||
return -1;
|
||||
}
|
||||
server->instance_id = optarg;
|
||||
break;
|
||||
case _O_TCP_NODELAY: OPT_SET(server->tcp_nodelay, true);
|
||||
case _O_SERVER_TIMEOUT: OPT_NUMBER("--server-timeout", server->timeout, 1, 60, 0);
|
||||
|
||||
@@ -473,6 +483,8 @@ int options_parse(us_options_s *options, us_device_s *dev, us_encoder_s *enc, us
|
||||
}
|
||||
}
|
||||
|
||||
US_LOG_INFO("Starting PiKVM uStreamer %s ...", US_VERSION);
|
||||
|
||||
options->blank = us_blank_frame_init(blank_path);
|
||||
stream->blank = options->blank;
|
||||
|
||||
@@ -530,6 +542,18 @@ static int _parse_resolution(const char *str, unsigned *width, unsigned *height,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int _check_instance_id(const char *str) {
|
||||
for (const char *ptr = str; *ptr; ++ptr) {
|
||||
if (!(isascii(*ptr) && (
|
||||
isalpha(*ptr) || isdigit(*ptr)
|
||||
|| *ptr == '.' || *ptr == '/' || *ptr == '+' || *ptr == '_' || *ptr == '-'
|
||||
))) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void _features(void) {
|
||||
# ifdef WITH_GPIO
|
||||
puts("+ WITH_GPIO");
|
||||
@@ -657,6 +681,8 @@ static void _help(FILE *fp, us_device_s *dev, us_encoder_s *enc, us_stream_s *st
|
||||
SAY(" --tcp-nodelay ────────────── Set TCP_NODELAY flag to the client /stream socket. Only for TCP socket.");
|
||||
SAY(" Default: disabled.\n");
|
||||
SAY(" --allow-origin <str> ─────── Set Access-Control-Allow-Origin header. Default: disabled.\n");
|
||||
SAY(" --instance-id <str> ──────── A short string identifier to be displayed in the /state handle.");
|
||||
SAY(" It must satisfy regexp ^[a-zA-Z0-9\\./+_-]*$. Default: an empty string.\n");
|
||||
SAY(" --server-timeout <sec> ───── Timeout for client connections. Default: %u.\n", server->timeout);
|
||||
# define ADD_SINK(x_name, x_opt) \
|
||||
SAY(x_name " sink options:"); \
|
||||
|
||||
@@ -32,7 +32,7 @@ static void _stream_expose_frame(us_stream_s *stream, us_frame_s *frame, unsigne
|
||||
|
||||
#define _SINK_PUT(x_sink, x_frame) { \
|
||||
if (stream->x_sink && us_memsink_server_check(stream->x_sink, x_frame)) {\
|
||||
us_memsink_server_put(stream->x_sink, x_frame); \
|
||||
us_memsink_server_put(stream->x_sink, x_frame, NULL); \
|
||||
} \
|
||||
}
|
||||
|
||||
|
||||
@@ -26,13 +26,11 @@ import os
|
||||
import io
|
||||
import struct
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
import common
|
||||
|
||||
|
||||
# =====
|
||||
def _get_jpeg_size(data: bytes) -> Tuple[int, int]:
|
||||
def _get_jpeg_size(data: bytes) -> tuple[int, int]:
|
||||
# https://sheep.horse/2013/9/finding_the_dimensions_of_a_jpeg_file_in_python.html
|
||||
|
||||
stream = io.BytesIO(data)
|
||||
|
||||
Reference in New Issue
Block a user