Compare commits

...

7 Commits
v5.42 ... v5.43

Author SHA1 Message Date
Maxim Devaev
a4b4dd3932 Bump version: 5.42 → 5.43 2023-10-04 02:46:33 +03:00
Maxim Devaev
e952f787a0 moved ssl docs 2023-10-04 02:43:28 +03:00
Maxim Devaev
b3e4ea9c0f issue #230: processing any freshest valid buffer 2023-10-04 02:41:55 +03:00
Maxim Devaev
22a816b9b5 issue #230: fixed possible memory error 2023-10-04 02:41:55 +03:00
Stargirl Flowers
c96559e4ac Discard truncated JPEG frames (#230)
Hello! This patch works around an issue encountered with [ELP-USB100W03M]
cameras where they send a vast amount of invalid JPEGs when capturing
their MJPEG streams. These bad frames account for about 87% of captured
frames and cause issues for browsers and downstream applications.

Replaces #229

[ELP-USB100W03M]: https://www.webcamerausb.com/elp-10mp-free-driver-usb20-ov9712-cmos-sensor-hd-mjpeg-web-camera-board-720p-36mm-lens-p-116.html
2023-10-04 02:41:55 +03:00
Maxim Devaev
a52df47b29 skip broken frames and save only good 2023-10-04 02:41:55 +03:00
tallman5
68e7e97e74 SSL Proxy Scripts (#226)
* adding basic ssl steps

* added down the road section
2023-10-04 02:41:39 +03:00
11 changed files with 171 additions and 41 deletions

View File

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

50
docs/ssl/README.md Normal file
View File

@@ -0,0 +1,50 @@
# Adding SSL
These days, browsers are not happy if you have HTTP content on an HTTPS page.
The browser will not show an HTTP stream on a page if the parent page is from a site which is using HTTPS.
The files in this folder configure an Nginx proxy in front of the µStreamer stream.
Using certbot, an SSL cert is created from Let's Encrypt and installed.
These scripts can be modified to add SSL to just about any HTTP server.
The scripts are not fire and forget.
They will require some pre-configuration and are interactive (you'll be asked questions while they're running).
They have been tested using the following setup.
1. A Raspberry Pi 4
1. µStreamer set up and running as a service
1. Internally on port 8080
1. Public port will be 5101
1. Verizon home Wi-Fi router
1. Domain registration from GoDaddy
## The Script
Below is an overview of the steps performed by `ssl-config.sh` (for Raspberry OS):
1. Install snapd - certbot uses this for installation
1. Install certbot
1. Get a free cert from Let's Encrypt using certbot
1. Install nginx
1. Configures nginx to proxy for µStreamer
## Steps
1. Create a public DNS entry.
1. Pointing to the Pi itself or the public IP of the router behind which the Pi sits.
1. This would be managed in the domain registrar, such as GoDaddy.
1. Use a subdomain, such as `webcam.domain.com`
1. Port Forwarding
1. If using a Wi-Fi router, create a port forwarding rule which passes traffic from port 80 to the Pi. This is needed for certbot to ensure your DNS entry reaches the Pi, even if your final port will be something else.
1. Create a second rule for your final setup. For example, forward traffic from the router on port 5101 to the Pi's IP port 8080.
1. Update the ustreamer-proxy file in this folder
1. Replace `your.domain.com` with a fully qualified domain, it's three places in the proxy file.
1. Modify the line `listen 5101 ssl` port if needed. This is the public port, not the port on which the µStreamer service is running
1. Modify `proxy_pass http://127.0.0.1:8080;` with the working address of the internal µStreamer service.
1. Run the script
1. Stand buy, certbot asks some basic questions, such as email, domain, agree to terms, etc.
1. `bash ssl-config.sh`
1. Test your URL!
## Down the Road
Two important points to keep in mind for the future:
1. Dynamic IP - Most routers do not have a static IP address on the WAN side. So, if you reboot your router or if your internet provider gives you a new IP, you'll have to update the DNS entry.
1. Many routers have some sort of dynamic DNS feature. This would automatically update the DNS entry for you. That functionality is outside the scope of this document.
1. SSL Renewals - certbot automatically creates a task to renew the SSL cert before it expires. Assuming the Pi is running all the time, this shouldn't be an issue.
## Enjoy!

20
docs/ssl/ssl-config.sh Normal file
View File

@@ -0,0 +1,20 @@
#!/bin/sh
echo -e "\e[32mInstalling snapd...\e[0m"
sudo apt install snapd -y
sudo snap install core
echo -e "\e[32mInstalling certbot, don't leave, it's going to ask questions...\e[0m"
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
sudo certbot certonly --standalone
sudo certbot renew --dry-run
echo -e "\e[32mInstalling nginx...\e[0m"
sudo apt-get install nginx -y
sudo cp ustreamer-proxy /etc/nginx/sites-available/ustreamer-proxy
sudo ln -s /etc/nginx/sites-available/ustreamer-proxy /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

13
docs/ssl/ustreamer-proxy Normal file
View File

@@ -0,0 +1,13 @@
server {
listen 5101 ssl;
server_name your.domain.com;
ssl_certificate /etc/letsencrypt/live/your.domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080; # Change this to the uStreamer server address
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}

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.42" "January 2021"
.TH USTREAMER-DUMP 1 "version 5.43" "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.42" "November 2020"
.TH USTREAMER 1 "version 5.43" "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.42
pkgver=5.43
pkgrel=1
pkgdesc="Lightweight and fast MJPEG-HTTP streamer"
url="https://github.com/pikvm/ustreamer"

View File

@@ -6,7 +6,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=ustreamer
PKG_VERSION:=5.42
PKG_VERSION:=5.43
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.42",
version="5.43",
description="uStreamer tools",
author="Maxim Devaev",
author_email="mdevaev@gmail.com",

View File

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

View File

@@ -54,6 +54,7 @@ static const struct {
};
static bool _device_is_buffer_valid(us_device_s *dev, const struct v4l2_buffer *buf, const uint8_t *data);
static int _device_open_check_cap(us_device_s *dev);
static int _device_open_dv_timings(us_device_s *dev);
static int _device_apply_dv_timings(us_device_s *dev);
@@ -312,6 +313,7 @@ int us_device_grab_buffer(us_device_s *dev, us_hw_buffer_s **hw) {
struct v4l2_buffer buf = {0};
bool buf_got = false;
unsigned skipped = 0;
bool broken = false;
US_LOG_DEBUG("Grabbing device buffer ...");
@@ -322,55 +324,61 @@ int us_device_grab_buffer(us_device_s *dev, us_hw_buffer_s **hw) {
const bool new_got = (_D_XIOCTL(VIDIOC_DQBUF, &new) >= 0);
if (new_got) {
if (new.index >= _RUN(n_bufs)) {
US_LOG_ERROR("V4L2 error: grabbed invalid device buffer=%u, n_bufs=%u", new.index, _RUN(n_bufs));
return -1;
}
# define GRABBED(x_buf) _RUN(hw_bufs)[x_buf.index].grabbed
# define FRAME_DATA(x_buf) _RUN(hw_bufs)[x_buf.index].raw.data
if (GRABBED(new)) {
US_LOG_ERROR("V4L2 error: grabbed device buffer=%u is already used", new.index);
return -1;
}
GRABBED(new) = true;
broken = !_device_is_buffer_valid(dev, &new, FRAME_DATA(new));
if (broken) {
US_LOG_DEBUG("Releasing device buffer=%u (broken frame) ...", new.index);
if (_D_XIOCTL(VIDIOC_QBUF, &new) < 0) {
US_LOG_PERROR("Can't release device buffer=%u (broken frame)", new.index);
return -1;
}
GRABBED(new) = false;
continue;
}
if (buf_got) {
if (_D_XIOCTL(VIDIOC_QBUF, &buf) < 0) {
US_LOG_PERROR("Can't release device buffer=%u (skipped frame)", buf.index);
return -1;
}
GRABBED(buf) = false;
++skipped;
// buf_got = false;
}
# undef GRABBED
# undef FRAME_DATA
memcpy(&buf, &new, sizeof(struct v4l2_buffer));
buf_got = true;
} else {
if (buf_got && errno == EAGAIN) {
break;
} else {
US_LOG_PERROR("Can't grab device buffer");
return -1;
if (errno == EAGAIN) {
if (buf_got) {
break; // Process any latest valid frame
} else if (broken) {
return -2; // If we have only broken frames on this capture session
}
}
US_LOG_PERROR("Can't grab device buffer");
return -1;
}
} while (true);
if (buf.index >= _RUN(n_bufs)) {
US_LOG_ERROR("V4L2 error: grabbed invalid device buffer=%u, n_bufs=%u", buf.index, _RUN(n_bufs));
return -1;
}
// Workaround for broken, corrupted frames:
// Under low light conditions corrupted frames may get captured.
// The good thing is such frames are quite small compared to the regular frames.
// For example a VGA (640x480) webcam frame is normally >= 8kByte large,
// corrupted frames are smaller.
if (buf.bytesused < dev->min_frame_size) {
US_LOG_DEBUG("Dropped too small frame, assuming it was broken: buffer=%u, bytesused=%u",
buf.index, buf.bytesused);
US_LOG_DEBUG("Releasing device buffer=%u (broken frame) ...", buf.index);
if (_D_XIOCTL(VIDIOC_QBUF, &buf) < 0) {
US_LOG_PERROR("Can't release device buffer=%u (broken frame)", buf.index);
return -1;
}
return -2;
}
# define HW(x_next) _RUN(hw_bufs)[buf.index].x_next
if (HW(grabbed)) {
US_LOG_ERROR("V4L2 error: grabbed device buffer=%u is already used", buf.index);
return -1;
}
HW(grabbed) = true;
HW(raw.dma_fd) = HW(dma_fd);
HW(raw.used) = buf.bytesused;
HW(raw.width) = _RUN(width);
@@ -382,8 +390,8 @@ int us_device_grab_buffer(us_device_s *dev, us_hw_buffer_s **hw) {
HW(raw.grab_ts)= (long double)((buf.timestamp.tv_sec * (uint64_t)1000) + (buf.timestamp.tv_usec / 1000)) / 1000;
US_LOG_DEBUG("Grabbed new frame: buffer=%u, bytesused=%u, grab_ts=%.3Lf, latency=%.3Lf, skipped=%u",
buf.index, buf.bytesused, HW(raw.grab_ts), us_get_now_monotonic() - HW(raw.grab_ts), skipped);
# undef HW
*hw = &_RUN(hw_bufs[buf.index]);
return buf.index;
}
@@ -419,6 +427,45 @@ int us_device_consume_event(us_device_s *dev) {
return 0;
}
bool _device_is_buffer_valid(us_device_s *dev, const struct v4l2_buffer *buf, const uint8_t *data) {
// Workaround for broken, corrupted frames:
// Under low light conditions corrupted frames may get captured.
// The good thing is such frames are quite small compared to the regular frames.
// For example a VGA (640x480) webcam frame is normally >= 8kByte large,
// corrupted frames are smaller.
if (buf->bytesused < dev->min_frame_size) {
US_LOG_DEBUG("Dropped too small frame, assuming it was broken: buffer=%u, bytesused=%u",
buf->index, buf->bytesused);
return false;
}
// Workaround for truncated JPEG frames:
// Some inexpensive CCTV-style USB webcams such as the ELP-USB100W03M send
// large amounts of these frames when using MJPEG streams. Checks that the
// buffer ends with either the JPEG end of image marker (0xFFD9), the last
// marker byte plus a padding byte (0xD900), or just padding bytes (0x0000)
// A more sophisticated method would scan for the end of image marker, but
// that takes precious CPU cycles and this should be good enough for most
// cases.
if (us_is_jpeg(dev->run->format)) {
if (buf->bytesused < 125) {
// https://stackoverflow.com/questions/2253404/what-is-the-smallest-valid-jpeg-file-size-in-bytes
US_LOG_DEBUG("Discarding invalid frame, too small to be a valid JPEG: bytesused=%u", buf->bytesused);
return false;
}
const uint8_t *const end_ptr = data + buf->bytesused;
const uint8_t *const eoi_ptr = end_ptr - 2;
const uint16_t eoi_marker = (((uint16_t)(eoi_ptr[0]) << 8) | eoi_ptr[1]);
if (eoi_marker != 0xFFD9 && eoi_marker != 0xD900 && eoi_marker != 0x0000) {
US_LOG_DEBUG("Discarding truncated JPEG frame: eoi_marker=0x%04x, bytesused=%u", eoi_marker, buf->bytesused);
return false;
}
}
return true;
}
static int _device_open_check_cap(us_device_s *dev) {
struct v4l2_capability cap = {0};