Compare commits

...

46 Commits
v0.16 ... v0.27

Author SHA1 Message Date
Devaev Maxim
a68e27f09e Bump version: 0.26 → 0.27 2018-11-03 04:50:13 +03:00
Devaev Maxim
4af8c6a121 moved option --extra-stream-headers to http params 2018-11-03 04:49:33 +03:00
Devaev Maxim
693c89ae6b improved index page 2018-11-03 04:29:28 +03:00
Devaev Maxim
8be5d6d370 separate html_index.h 2018-11-03 04:10:02 +03:00
Devaev Maxim
5a7f3d30a7 Complete workaround for Chrome frames processor
https://bugs.chromium.org/p/chromium/issues/detail?id=527446
2018-11-03 02:54:58 +03:00
Devaev Maxim
fb6331b64a first implementation of --soft-fps 2018-11-02 02:32:28 +03:00
Devaev Maxim
77b5e6eabc Bump version: 0.25 → 0.26 2018-10-28 14:02:50 +03:00
Devaev Maxim
ca07a9155b --device-persistent 2018-10-28 14:02:23 +03:00
Devaev Maxim
6cc202133e using 127.0.0.1 by default 2018-10-28 07:12:19 +03:00
Devaev Maxim
797e9427e5 Bump version: 0.24 → 0.25 2018-10-21 08:15:40 +03:00
Devaev Maxim
b624ea2005 fixed usage of strerror_r() 2018-10-21 08:15:23 +03:00
Devaev Maxim
189aba1488 Bump version: 0.23 → 0.24 2018-10-16 03:26:11 +03:00
Maxim Devaev
5f43fb7d0a Update README.md 2018-10-16 03:24:06 +03:00
Maxim Devaev
86fada184c Merge pull request #1 from goblinqueen/master
Readme English translation
2018-10-16 00:38:42 +03:00
Eltha Black
f0eece484f README.md English translation 2018-10-16 00:11:36 +03:00
Eltha Black
67f8e6665b README.md English translation 2018-10-16 00:06:43 +03:00
Maxim Devaev
6569e27312 Update README.md 2018-10-15 06:50:38 +03:00
Maxim Devaev
4284ad3734 Update README.md 2018-10-15 02:22:24 +03:00
Maxim Devaev
1c2f85cd59 Update README.md 2018-10-15 01:27:25 +03:00
Devaev Maxim
2f83742f1d s/with/+/ 2018-10-14 20:55:31 +03:00
Maxim Devaev
26d884c2da Update README.md 2018-10-13 05:36:19 +03:00
Devaev Maxim
2d054fa9a8 Bump version: 0.22 → 0.23 2018-10-10 00:08:13 +03:00
Devaev Maxim
f2863d1108 --version 2018-10-10 00:07:59 +03:00
Devaev Maxim
281e1b36ce Bump version: 0.21 → 0.22 2018-10-08 16:33:16 +03:00
Devaev Maxim
160ad5c10f updated dependencies info 2018-10-08 15:39:33 +03:00
Devaev Maxim
667137638b Bump version: 0.20 → 0.21 2018-10-07 11:32:18 +03:00
Devaev Maxim
b5dffa927a changed /ping format 2018-10-07 11:32:07 +03:00
Devaev Maxim
651a82d1ca Bump version: 0.19 → 0.20 2018-10-06 01:17:38 +03:00
Devaev Maxim
d963c95af8 refactoring 2018-10-06 01:12:41 +03:00
Devaev Maxim
7c524a1196 calculating client fps 2018-10-06 00:50:30 +03:00
Maxim Devaev
0c85aad5a2 Update README.md 2018-10-05 11:12:38 +03:00
Devaev Maxim
5515654497 Bump version: 0.18 → 0.19 2018-10-04 21:19:21 +03:00
Devaev Maxim
5955310bf3 updated readme 2018-10-04 21:19:09 +03:00
Devaev Maxim
a9df6da912 improved fps counters 2018-10-04 19:43:58 +03:00
Devaev Maxim
f3c56d5774 Bump version: 0.17 → 0.18 2018-10-04 16:03:29 +03:00
Devaev Maxim
9a86793923 Workaround for Chrome bug: https://bugs.chromium.org/p/chromium/issues/detail?id=527446 2018-10-04 15:59:40 +03:00
Devaev Maxim
93c6248fdb better msg 2018-10-04 11:35:18 +03:00
Devaev Maxim
ec738b18dc fps -> cps 2018-10-04 09:20:36 +03:00
Devaev Maxim
6029408564 Bump version: 0.16 → 0.17 2018-10-04 01:33:30 +03:00
Devaev Maxim
8734834341 refactoring 2018-10-04 01:32:02 +03:00
Devaev Maxim
809f86955d refactoring 2018-10-04 01:29:38 +03:00
Devaev Maxim
2f557617d8 --add-x-timings -> --extra-stream-headers 2018-10-04 01:07:06 +03:00
Devaev Maxim
35c8196103 extra headers 2018-10-04 00:59:59 +03:00
Devaev Maxim
c71df1bb25 fixed time rounding 2018-10-03 20:46:13 +03:00
Devaev Maxim
bc107d2870 eps 2018-10-03 20:37:39 +03:00
Devaev Maxim
294ed36b8f clients count in /ping 2018-10-03 19:29:19 +03:00
18 changed files with 489 additions and 163 deletions

View File

@@ -1,7 +1,7 @@
[bumpversion]
commit = True
tag = True
current_version = 0.16
current_version = 0.27
parse = (?P<major>\d+)\.(?P<minor>\d+)(\.(?P<patch>\d+)(\-(?P<release>[a-z]+))?)?
serialize =
{major}.{minor}

View File

@@ -6,7 +6,7 @@ LDFLAGS ?=
# =====
CC = gcc
LIBS = -lm -ljpeg -pthread -levent -levent_pthreads
LIBS = -lm -ljpeg -pthread -levent -levent_pthreads -luuid
override CFLAGS += -c -std=c99 -Wall -Wextra -D_GNU_SOURCE
SOURCES = $(shell ls src/*.c src/jpeg/*.c)
OBJECTS = $(SOURCES:.c=.o)

View File

@@ -3,13 +3,13 @@
pkgname=ustreamer
pkgver=0.16
pkgver=0.27
pkgrel=1
pkgdesc="Lightweight and fast MJPG-HTTP streamer"
url="https://github.com/pi-kvm/ustreamer"
license=(GPL)
arch=(i686 x86_64 armv6h armv7h)
depends=(libjpeg libevent)
depends=(libjpeg libevent libutil-linux)
# optional: raspberrypi-firmware for OMX JPEG compressor
makedepends=(gcc make)
source=("$url/archive/v$pkgver.tar.gz")

View File

@@ -1,39 +1,41 @@
# µStreamer
[[Русская версия]](README.ru.md)
µStreamer - это маленький и очень быстрый сервер, который позволяет организовать трансляцию видео в формате [MJPG](https://en.wikipedia.org/wiki/Motion_JPEG) с любого устройства V4L2 в сеть. Этот формат нативно поддерживается всеми современными браузерами и большинством приложений для просмотра видео (mplayer, VLC и так далее). µStreamer был разработан в рамках проекта [Pi-KVM](https://github.com/pi-kvm) специально для стриминга с устройств видеозахвата [VGA](https://www.amazon.com/dp/B0126O0RDC) и [HDMI](https://auvidea.com/b101-hdmi-to-csi-2-bridge-15-pin-fpc/) с максимально возможным разрешением и FPS, которые только позволяет железо.
µStreamer is a lightweight and very quick server to broadcast [MJPG](https://en.wikipedia.org/wiki/Motion_JPEG) video from any V4L2 device to the net. All new browsers have native support of this video format, as well as most video players such as mplayer, VLC etc.
µStreamer is a part of the [Pi-KVM](https://github.com/pi-kvm) project designed to stream [VGA](https://www.amazon.com/dp/B0126O0RDC) and [HDMI](https://auvidea.com/b101-hdmi-to-csi-2-bridge-15-pin-fpc/) screencast hardware data with the highest resolution and FPS possible.
Функционально µStreamer очень похож на [mjpg-streamer](https://github.com/jacksonliam/mjpg-streamer) при использовании им плагинов ```input_uvc.so``` и ```output_http.so```, однако имеет ряд серьезных отличий. Основные приведены в этой таблице:
µStreamer is very similar to [mjpg-streamer](https://github.com/jacksonliam/mjpg-streamer) with ```input_uvc.so``` and ```output_http.so``` plugins, however, there are some major differences. The key ones are:
| **Фича** | **µStreamer** | **mjpg-streamer** |
| **Feature** | **µStreamer** | **mjpg-streamer** |
|----------|---------------|-------------------|
| Многопоточное кодирование JPEG | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Аппаратное кодирование с помощью [OpenMAX IL](https://www.khronos.org/openmaxil) на Raspberry Pi | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Поведение при физическом отключении устройства<br>от сервера во время работы | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Транслирует черный экран<br>с надписью ```NO SIGNAL```,<br>пока устройство не будет подключено снова | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Необратимо зависает <sup>1</sup> |
| Поддержка [DV-таймингов](https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/dv-timings.html) - возможности изменения <br>параметров разрешения трансляции на лету<br>по сигналу источника (устройства видеозахвата) | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет <sup>1</sup> |
| Возможность пропуска фреймов при передаче<br>статического изображения по HTTP<br>для экономии трафика | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть <sup>2</sup> | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Дебаг-логи без перекомпиляции,<br>логгирование статистики производительности,<br>возможность получения параметров<br>трансляции по HTTP | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Поддерживаемые входные форматы устройств | ![#ffaa00](https://placehold.it/15/ffaa00/000000?text=+) YUYV, UYVY,<br>RGB565, ~~MJPG~~ <sup>3</sup> | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) YUYV, UYVY,<br>RGB565, MJPG |
| Поддержка контролов веб-камер (фокус,<br> движение сервами) и всяких настроек,<br> типа яркости, через HTTP | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть |
| Возможность сервить файлы встроенным<br>HTTP-сервером, настройки авторизации | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет <sup>4</sup> | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть |
| Multithreaded JPEG encoding | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No |
| [OpenMAX IL](https://www.khronos.org/openmaxil) hardware acceleration<br>on Raspberry Pi | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No |
| Behavior when the device<br>is disconnected while streaming | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Shows a black screen<br>with ```NO SIGNAL``` on it<br>until reconnected | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Stops the broadcast<sup>1</sup> |
| [DV-timings](https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/dv-timings.html) support -<br>the ability to change resolution<br>on the fly by source signal | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | ![#ffaa00](https://placehold.it/15/ffaa00/000000?text=+) Partially yes <sup>1</sup> |
| Option to skip frames when streaming<br>static images by HTTP to save traffic | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes <sup>2</sup> | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No |
| Debug logs without recompiling,<br>performance statistics log,<br>access to HTTP broadcast parameters | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No |
| Supported input devices | ![#ffaa00](https://placehold.it/15/ffaa00/000000?text=+) YUYV, UYVY,<br>RGB565, ~~MJPG~~ <sup>3</sup> | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) YUYV, UYVY,<br>RGB565, MJPG |
| Access to webcam controls (focus, servos)<br>and settings such as brightness via HTTP | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes |
| Option to serve files<br>with a built-in HTTP server, auth settings | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) No <sup>4</sup> | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Yes |
Сносочки:
* ```1``` Для mjpg-streamer существует [мой патч](https://github.com/jacksonliam/mjpg-streamer/pull/164), предотвращающий зависание при отключении устройства и добавляющий поддержку DV-таймингов, однако трансляция при этом все равно прерывается. В данный момент этот патч не принят в апстрим, и я даже не гарантирую его стопроцентную работоспособность. Код mjpg-streamer очень плохо структурирован и чрезвычайно запутан, и я мог что-то упустить. Собственно, это одна из причин, почему µStreamer был написан с нуля.
Footnotes:
* ```1``` Long before µStreamer, I made a [patch](https://github.com/jacksonliam/mjpg-streamer/pull/164) to add DV-timings support to mjpg-streamer and to keep it from hanging up no device disconnection. Alas, the patch is far from perfect and I can't guarantee it will work every time - mjpg-streamer's source code is very complicated and its structure is hard to understand. With this in mind, along with needing multithreading and JPEG hardware acceleration in the future, I decided to make my own stream server from scratch instead of supporting legacy code.
* ```2``` Это фича позволяет в несколько раз снизить объем исходящего трафика при трансляции HDMI, однако увеличивает загрузку процессора и добавляет небольшую задержку. Суть в том, что HDMI - полностью цифровой интерфейс, и новый захваченный фрейм может быть идентичен предыдущему в точности до байта. В этом случае нет нужды передавать одну и ту же картинку по сети несколько раз в секунду. При использовании опции `--drop-same-frames=20`, µStreamer будет дропать все одинаковые фреймы, но не более 20. Новый фрейм сравнивается с предыдущим сначала по длине, а затем помощью ```memcmp()```.
* ```2``` This feature allows to cut down outgoing traffic several-fold when broadcasting HDMI, but it increases CPU usage a little bit. The idea is that HDMI is a fully digital interface and each captured frame can be identical to the previous one byte-wise. There's no need to broadcast the same image over the net several times a second. With the `--drop-same-frames=20` option enabled, µStreamer will drop all the matching frames (with a limit of 20 in a row). Each new frame is matched with the previous one first by length, then using ```memcmp()```.
* ```3``` Поскольку µStreamer писался в первую очередь для устройств видеозахвата, в нем реализованы только те форматы, которые для них были нужны. MJPG в контексте входных данных означает, что устройство умеет самостоятельно сжимать картинку в JPEG и отдавать ее программе, что позволяет значительно снизить загрузку процессора и избавить его от необходимости кодировать картинку софтом. Этот формат поддерживается большинством веб-камер, но не поддерживается ни одним из встреченных мной устройств видеозахвата; его не умеет ни [Auvidea B101](https://auvidea.com/b101-hdmi-to-csi-2-bridge-15-pin-fpc/), ни [EasyCap UTV 007](https://www.amazon.com/dp/B0126O0RDC). Нет никаких технических сложностей добавить поддержку аппаратного MJPG источника, но у меня просто пока не дошли до этого руки.
* ```3``` As µStreamer was made mainly to be used with screencast hardware it supports video formats they usually need. MJPG input means that the screencast device can compress images to JPEG and feed it to the software, which allows to cut down CPU usage and avoid software image encoding. This video format is supported by most webcams, but I've never seen it supported by screencast hardware: neither [Auvidea B101](https://auvidea.com/b101-hdmi-to-csi-2-bridge-15-pin-fpc/), nor [EasyCap UTV 007](https://www.amazon.com/dp/B0126O0RDC) offer such support. It's not hard to add hardware MJPG sources support, it's just not done yet.
* ```4``` ... и не будет. µStreamer придерживается концепции UNIX-way, так что если вам нужно нарисовать маленький сайтик со встроенной трансляцией - просто поставьте NGINX.
* ```4``` ...and there'll never be. µStreamer is designed UNIX-way, so if you need a small website with your broadcast, install NGINX.
-----
# TL;DR
Если вам нужно вещать стрим с уличной камеры и управлять ее параметрами - возьмите mjpg-streamer. Если же вам нужно очень качественное изображение с высоким FPS - µStreamer ваш бро.
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.
-----
# Сборка
Для сборки вам понадобятся ```make```, ```gcc```, ```libevent``` с поддержкой ```pthreads``` и ```libjpeg8```/```libjpeg-turbo```.
# Building
You'll need ```make```, ```gcc```, ```libevent``` with ```pthreads``` support, ```libjpeg8```/```libjpeg-turbo``` and ```libuuid```.
На Raspberry Pi програма автоматически собирается с поддержкой OpenMAX, если обнаружит нужные хедеры в ```/opt/vc/include```.
It should compile automatically with OpenMAX IL on Raspberry Pi, if the corresponding headers are present in ```/opt/vc/include```.
```
$ git clone --depth=1 https://github.com/pi-kvm/ustreamer
@@ -42,27 +44,29 @@ $ make
$ ./ustreamer --help
```
Для Arch Linux в AUR есть готовый пакет: https://aur.archlinux.org/packages/ustreamer
AUR has a package for Arch Linux: https://aur.archlinux.org/packages/ustreamer
-----
# Использование
Будучи запущенным без аргументов, ```ustreamer``` попробует открыть устройство ```/dev/video0``` с разрешением 640x480 и начать трансляцию на ```http://localhost:8080```. Это поведение может быть изменено с помощью опций ```--device```, ```--host``` и ```--port```. Пример вещания на всю сеть по 80 порту:
# Usage
Without arguments, ```ustreamer``` will try to open ```/dev/video0``` with 640x480 resolution and start broadcasting on ```http://127.0.0.1:8080```. You can override this behavior using parameters ```--device```, ```--host``` and ```--port```. For example, to broadcast to the world, run:
```
# ./ustreamer --device=/dev/video1 --host=0.0.0.0 --port=80
```
Рекомендуемый способ запуска µStreamer для работы с [Auvidea B101](https://www.raspberrypi.org/forums/viewtopic.php?f=38&t=120702&start=400#p1339178) на Raspberry Pi:
The recommended way of running µStreamer with [Auvidea B101](https://www.raspberrypi.org/forums/viewtopic.php?f=38&t=120702&start=400#p1339178) on Raspberry Pi:
```bash
$ ./ustreamer \
--format=uyvy \ # Настройка входного формата устройства
--encoder=omx \ # Использование аппаратного кодирования с помощью OpenMAX
--dv-timings # Включение DV-таймингов
--format=uyvy \ # Device input format
--encoder=omx \ # Hardware encoding with OpenMAX
--dv-timings \ # use DV-timings
--quality=20 \ # OpenMAX has a non-linear quality scale
--drop-same-frames=30 # Save that traffic
```
За полным списком опций обращайтесь ко встроенной справке: ```ustreamer --help```.
You can always view the full list of options with ```ustreamer --help```.
-----
# Лицензия
# License
Copyright (C) 2018 by Maxim Devaev mdevaev@gmail.com
This program is free software: you can redistribute it and/or modify

83
README.ru.md Normal file
View File

@@ -0,0 +1,83 @@
# µStreamer
[[English version]](README.md)
µStreamer - это маленький и очень быстрый сервер, который позволяет организовать трансляцию видео в формате [MJPG](https://en.wikipedia.org/wiki/Motion_JPEG) с любого устройства V4L2 в сеть. Этот формат нативно поддерживается всеми современными браузерами и большинством приложений для просмотра видео (mplayer, VLC и так далее). µStreamer был разработан в рамках проекта [Pi-KVM](https://github.com/pi-kvm) специально для стриминга с устройств видеозахвата [VGA](https://www.amazon.com/dp/B0126O0RDC) и [HDMI](https://auvidea.com/b101-hdmi-to-csi-2-bridge-15-pin-fpc/) с максимально возможным разрешением и FPS, которые только позволяет железо.
Функционально µStreamer очень похож на [mjpg-streamer](https://github.com/jacksonliam/mjpg-streamer) при использовании им плагинов ```input_uvc.so``` и ```output_http.so```, однако имеет ряд серьезных отличий. Основные приведены в этой таблице:
| **Фича** | **µStreamer** | **mjpg-streamer** |
|----------|---------------|-------------------|
| Многопоточное кодирование JPEG | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Аппаратное кодирование с помощью [OpenMAX IL](https://www.khronos.org/openmaxil) на Raspberry Pi | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Поведение при физическом отключении устройства<br>от сервера во время работы | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Транслирует черный экран<br>с надписью ```NO SIGNAL```,<br>пока устройство не будет подключено снова | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Прерывает трансляцию <sup>1</sup> |
| Поддержка [DV-таймингов](https://linuxtv.org/downloads/v4l-dvb-apis/uapi/v4l/dv-timings.html) - возможности изменения <br>параметров разрешения трансляции на лету<br>по сигналу источника (устройства видеозахвата) | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#ffaa00](https://placehold.it/15/ffaa00/000000?text=+) Условно есть <sup>1</sup> |
| Возможность пропуска фреймов при передаче<br>статического изображения по HTTP<br>для экономии трафика | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть <sup>2</sup> | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Дебаг-логи без перекомпиляции,<br>логгирование статистики производительности,<br>возможность получения параметров<br>трансляции по HTTP | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет |
| Поддерживаемые входные форматы устройств | ![#ffaa00](https://placehold.it/15/ffaa00/000000?text=+) YUYV, UYVY,<br>RGB565, ~~MJPG~~ <sup>3</sup> | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) YUYV, UYVY,<br>RGB565, MJPG |
| Поддержка контролов веб-камер (фокус,<br> движение сервами) и всяких настроек,<br> типа яркости, через HTTP | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть |
| Возможность сервить файлы встроенным<br>HTTP-сервером, настройки авторизации | ![#f03c15](https://placehold.it/15/f03c15/000000?text=+) Нет <sup>4</sup> | ![#00aa00](https://placehold.it/15/00aa00/000000?text=+) Есть |
Сносочки:
* ```1``` Еще до написания µStreamer, я запилил [патч](https://github.com/jacksonliam/mjpg-streamer/pull/164), добавляющий в mjpg-streamer поддержку DV-таймингов и предотвращающий его зависание при отключении устройства. Однако патч, увы, далек от совершенства и я не гарантирую его стопроцентную работоспособность, поскольку код mjpg-streamer чрезвычайно запутан и очень плохо структурирован. Учитывая это, а также то, что в дальнейшем мне потребовались многопоточность и аппаратное кодирование JPEG, было принято решение написать свой стрим-сервер с нуля, чтобы не тратить силы на поддержку лишнего легаси.
* ```2``` Это фича позволяет в несколько раз снизить объем исходящего трафика при трансляции HDMI, однако немного увеличивает загрузку процессора. Суть в том, что HDMI - полностью цифровой интерфейс, и новый захваченный фрейм может быть идентичен предыдущему в точности до байта. В этом случае нет нужды передавать одну и ту же картинку по сети несколько раз в секунду. При использовании опции `--drop-same-frames=20`, µStreamer будет дропать все одинаковые фреймы, но не более 20 подряд. Новый фрейм сравнивается с предыдущим сначала по длине, а затем помощью ```memcmp()```.
* ```3``` Поскольку µStreamer писался в первую очередь для устройств видеозахвата, в нем реализованы только те форматы, которые для них были нужны. MJPG в контексте входных данных означает, что устройство умеет самостоятельно сжимать картинку в JPEG и отдавать ее программе, что позволяет значительно снизить загрузку процессора и избавить его от необходимости кодировать картинку софтом. Этот формат поддерживается большинством веб-камер, но не поддерживается ни одним из встреченных мной устройств видеозахвата; его не умеет ни [Auvidea B101](https://auvidea.com/b101-hdmi-to-csi-2-bridge-15-pin-fpc/), ни [EasyCap UTV 007](https://www.amazon.com/dp/B0126O0RDC). Нет никаких технических сложностей добавить поддержку аппаратного MJPG источника, но у меня просто пока не дошли до этого руки.
* ```4``` ... и не будет. µStreamer придерживается концепции UNIX-way, так что если вам нужно нарисовать маленький сайтик со встроенной трансляцией - просто поставьте NGINX.
-----
# TL;DR
Если вам нужно вещать стрим с уличной камеры и управлять ее параметрами - возьмите mjpg-streamer. Если же вам нужно очень качественное изображение с высоким FPS - µStreamer ваш бро.
-----
# Сборка
Для сборки вам понадобятся ```make```, ```gcc```, ```libevent``` с поддержкой ```pthreads```, ```libjpeg8```/```libjpeg-turbo``` и ```libuuid```.
На Raspberry Pi програма автоматически собирается с поддержкой OpenMAX IL, если обнаружит нужные хедеры в ```/opt/vc/include```.
```
$ git clone --depth=1 https://github.com/pi-kvm/ustreamer
$ cd ustreamer
$ make
$ ./ustreamer --help
```
Для Arch Linux в AUR есть готовый пакет: https://aur.archlinux.org/packages/ustreamer
-----
# Использование
Будучи запущенным без аргументов, ```ustreamer``` попробует открыть устройство ```/dev/video0``` с разрешением 640x480 и начать трансляцию на ```http://127.0.0.1:8080```. Это поведение может быть изменено с помощью опций ```--device```, ```--host``` и ```--port```. Пример вещания на всю сеть по 80-м порту:
```
# ./ustreamer --device=/dev/video1 --host=0.0.0.0 --port=80
```
Рекомендуемый способ запуска µStreamer для работы с [Auvidea B101](https://www.raspberrypi.org/forums/viewtopic.php?f=38&t=120702&start=400#p1339178) на Raspberry Pi:
```bash
$ ./ustreamer \
--format=uyvy \ # Настройка входного формата устройства
--encoder=omx \ # Использование аппаратного кодирования с помощью OpenMAX
--dv-timings \ # Включение DV-таймингов
--quality=20 \ # У OpenMAX нелинейная шкала качества
--drop-same-frames=30 # Экономим трафик
```
За полным списком опций обращайтесь ко встроенной справке: ```ustreamer --help```.
-----
# Лицензия
Copyright (C) 2018 by 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/.

View File

@@ -21,4 +21,4 @@
#pragma once
#define VERSION "0.16"
#define VERSION "0.27"

72
src/data/html_index.h Normal file
View File

@@ -0,0 +1,72 @@
/*****************************************************************************
# 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 "../config.h"
const char *HTML_INDEX_PAGE = " \
<!DOCTYPE html> \
<html> \
<head> \
<meta charset=\"utf-8\"> \
<title>uStreamer</title> \
</head> \
<body> \
<h3>&micro;Streamer v" VERSION "</h3> \
<hr> \
<ul> \
<li> \
<a href=\"/ping\"><b>/ping</b></a><br> \
Get JSON structure with state of the server. \
</li> \
<br> \
<li> \
<a href=\"/snapshot\"><b>/snapshot</b></a><br> \
Get a current actual image from server. \
</li> \
<br> \
<li> \
<a href=\"/stream\"><b>/stream</b></a><br> \
Get a live stream. Query params:<br> \
<br> \
<ul> \
<li> \
<i>extra_headers=1</i><br> \
Add X-UStreamer-* headers to /stream handle (like on <a href=\"/snapshot\">/snapshot</a>). \
</li> \
<br> \
<li> \
<i>advance_headers=1</i><br> \
Enable workaround for Chromium/Blink \
<a href=\"https://bugs.chromium.org/p/chromium/issues/detail?id=527446\">Bug #527446</a>. \
</li> \
</ul> \
</li> \
<br> \
</ul> \
<br> \
<hr> \
<a href=\"https://github.com/pi-kvm/ustreamer\">Sources &amp; docs</a> \
</body> \
</html> \
";

View File

@@ -84,8 +84,9 @@ struct device_t *device_init() {
dev->standard = V4L2_STD_UNKNOWN;
dev->n_buffers = max_u(sysconf(_SC_NPROCESSORS_ONLN), 1) + 1;
dev->n_workers = dev->n_buffers;
dev->soft_fps = 30;
dev->timeout = 1;
dev->error_timeout = 1;
dev->error_delay = 1;
dev->run = run;
return dev;
}

View File

@@ -72,10 +72,12 @@ struct device_t {
bool dv_timings;
unsigned n_buffers;
unsigned n_workers;
unsigned soft_fps;
unsigned every_frame;
unsigned min_frame_size;
bool persistent;
unsigned timeout;
unsigned error_timeout;
unsigned error_delay;
struct device_runtime_t *run;
sig_atomic_t volatile stop;

View File

@@ -131,7 +131,7 @@ void encoder_prepare_for_device(struct encoder_t *encoder, struct device_t *dev)
int encoder_compress_buffer(struct encoder_t *encoder, struct device_t *dev, const unsigned index) {
assert(encoder->type != ENCODER_TYPE_UNKNOWN);
dev->run->pictures[index].encode_begin_time = now_monotonic_ms();
dev->run->pictures[index].encode_begin_time = get_now_monotonic();
if (encoder->type == ENCODER_TYPE_CPU) {
jpeg_encoder_compress_buffer(dev, index, encoder->quality);
@@ -144,7 +144,7 @@ int encoder_compress_buffer(struct encoder_t *encoder, struct device_t *dev, con
}
# endif
dev->run->pictures[index].encode_end_time = now_monotonic_ms();
dev->run->pictures[index].encode_end_time = get_now_monotonic();
return 0;

View File

@@ -22,6 +22,7 @@
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <strings.h>
#include <assert.h>
#include <event2/event.h>
@@ -29,6 +30,9 @@
#include <event2/http.h>
#include <event2/buffer.h>
#include <event2/bufferevent.h>
#include <event2/keyvalq_struct.h>
#include <uuid/uuid.h>
#ifndef EVTHREAD_USE_PTHREADS_IMPLEMENTED
# error Required libevent-pthreads support
@@ -39,9 +43,12 @@
#include "stream.h"
#include "http.h"
#include "data/html_index.h"
#include "data/blank.h"
static bool _http_get_param_true(struct evkeyvalq *params, const char *key);
static void _http_callback_root(struct evhttp_request *request, void *arg);
static void _http_callback_ping(struct evhttp_request *request, void *v_server);
static void _http_callback_snapshot(struct evhttp_request *request, void *v_server);
@@ -61,7 +68,6 @@ struct http_server_t *http_server_init(struct stream_t *stream) {
struct http_server_runtime_t *run;
struct http_server_t *server;
struct exposed_t *exposed;
struct timeval refresh_interval;
A_CALLOC(exposed, 1);
@@ -71,7 +77,7 @@ struct http_server_t *http_server_init(struct stream_t *stream) {
run->drop_same_frames_blank = 10;
A_CALLOC(server, 1);
server->host = "localhost";
server->host = "127.0.0.1";
server->port = 8080;
server->timeout = 10;
server->run = run;
@@ -88,17 +94,15 @@ struct http_server_t *http_server_init(struct stream_t *stream) {
assert(!evhttp_set_cb(run->http, "/snapshot", _http_callback_snapshot, (void *)server));
assert(!evhttp_set_cb(run->http, "/stream", _http_callback_stream, (void *)server));
refresh_interval.tv_sec = 0;
refresh_interval.tv_usec = 30000; // ~30 refreshes per second
assert((run->refresh = event_new(run->base, -1, EV_PERSIST, _http_exposed_refresh, server)));
assert(!event_add(run->refresh, &refresh_interval));
return server;
}
void http_server_destroy(struct http_server_t *server) {
event_del(server->run->refresh);
event_free(server->run->refresh);
if (server->run->refresh) {
event_del(server->run->refresh);
event_free(server->run->refresh);
}
evhttp_free(server->run->http);
event_base_free(server->run->base);
libevent_global_shutdown();
@@ -110,6 +114,13 @@ void http_server_destroy(struct http_server_t *server) {
}
int http_server_listen(struct http_server_t *server) {
struct timeval refresh_interval;
refresh_interval.tv_sec = 0;
refresh_interval.tv_usec = 1000000 / (server->run->stream->dev->soft_fps * 2);
assert((server->run->refresh = event_new(server->run->base, -1, EV_PERSIST, _http_exposed_refresh, server)));
assert(!event_add(server->run->refresh, &refresh_interval));
server->run->drop_same_frames_blank = max_u(server->drop_same_frames, server->run->drop_same_frames_blank);
LOG_DEBUG("Binding HTTP to [%s]:%d ...", server->host, server->port);
@@ -135,6 +146,17 @@ void http_server_loop_break(struct http_server_t *server) {
}
static bool _http_get_param_true(struct evkeyvalq *params, const char *key) {
const char *value_str;
if ((value_str = evhttp_find_header(params, key)) != NULL) {
if (!strcasecmp(value_str, "true") || !strcasecmp(value_str, "yes") || value_str[0] == '1') {
return true;
}
}
return false;
}
#define ADD_HEADER(_key, _value) \
assert(!evhttp_add_header(evhttp_request_get_output_headers(request), _key, _value))
@@ -151,14 +173,7 @@ static void _http_callback_root(struct evhttp_request *request, UNUSED void *arg
PROCESS_HEAD_REQUEST;
assert((buf = evbuffer_new()));
assert(evbuffer_add_printf(buf,
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
"<title>uStreamer</title></head><body><ul>"
"<li><a href=\"/ping\">/ping</a></li>"
"<li><a href=\"/snapshot\">/snapshot</a></li>"
"<li><a href=\"/stream\">/stream</a></li>"
"</body></html>"
));
assert(evbuffer_add_printf(buf, HTML_INDEX_PAGE));
ADD_HEADER("Content-Type", "text/html");
evhttp_send_reply(request, HTTP_OK, "OK", buf);
evbuffer_free(buf);
@@ -172,13 +187,28 @@ static void _http_callback_ping(struct evhttp_request *request, void *v_server)
assert((buf = evbuffer_new()));
assert(evbuffer_add_printf(buf,
"{\"stream\": {\"resolution\":"
" {\"width\": %u, \"height\": %u},"
" \"fps\": %u, \"online\": %s}}",
"{\"source\": {\"resolution\": {\"width\": %u, \"height\": %u},"
" \"online\": %s, \"quality\": %u, \"captured_fps\": %u},"
" \"stream\": {\"queued_fps\": %u, \"clients\": %u, \"clients_stat\": {",
(server->fake_width ? server->fake_width : server->run->exposed->width),
(server->fake_height ? server->fake_height : server->run->exposed->height),
server->run->exposed->fps, (server->run->exposed->online ? "true" : "false")
(server->run->exposed->online ? "true" : "false"),
server->run->stream->encoder->quality,
server->run->exposed->captured_fps,
server->run->exposed->queued_fps,
server->run->stream_clients_count
));
for (struct stream_client_t * client = server->run->stream_clients; client != NULL; client = client->next) {
assert(evbuffer_add_printf(buf,
"\"%s\": {\"fps\": %u, \"advance_headers\": %s}%s",
client->id,
client->fps,
(client->advance_headers ? "true" : "false"),
(client->next ? ", " : "")
));
}
assert(evbuffer_add_printf(buf, "}}}"));
ADD_HEADER("Content-Type", "application/json");
evhttp_send_reply(request, HTTP_OK, "OK", buf);
evbuffer_free(buf);
@@ -204,16 +234,16 @@ static void _http_callback_snapshot(struct evhttp_request *request, void *v_serv
# define ADD_TIME_HEADER(_key, _value) \
{ sprintf(time_buf, "%.06Lf", _value); ADD_HEADER(_key, time_buf); }
ADD_TIME_HEADER("X-Timestamp", now_real_ms());
ADD_TIME_HEADER("X-Timestamp", get_now_real());
if (server->add_x_timings) {
ADD_TIME_HEADER("X-UStreamer-Grab-Time", EXPOSED(picture.grab_time));
ADD_TIME_HEADER("X-UStreamer-Encode-Begin-Time", EXPOSED(picture.encode_begin_time));
ADD_TIME_HEADER("X-UStreamer-Encode-End-Time", EXPOSED(picture.encode_end_time));
ADD_TIME_HEADER("X-UStreamer-Expose-Begin-Time", EXPOSED(expose_begin_time));
ADD_TIME_HEADER("X-UStreamer-Expose-End-Time", EXPOSED(expose_end_time));
ADD_TIME_HEADER("X-UStreamer-Send-Time", now_monotonic_ms());
}
ADD_HEADER("X-UStreamer-Online", (EXPOSED(online) ? "true" : "false"));
ADD_TIME_HEADER("X-UStreamer-Grab-Time", EXPOSED(picture.grab_time));
ADD_TIME_HEADER("X-UStreamer-Encode-Begin-Time", EXPOSED(picture.encode_begin_time));
ADD_TIME_HEADER("X-UStreamer-Encode-End-Time", EXPOSED(picture.encode_end_time));
ADD_TIME_HEADER("X-UStreamer-Expose-Begin-Time", EXPOSED(expose_begin_time));
ADD_TIME_HEADER("X-UStreamer-Expose-Cmp-Time", EXPOSED(expose_cmp_time));
ADD_TIME_HEADER("X-UStreamer-Expose-End-Time", EXPOSED(expose_end_time));
ADD_TIME_HEADER("X-UStreamer-Send-Time", get_now_monotonic());
# undef ADD_TIME_HEADER
@@ -236,10 +266,12 @@ static void _http_callback_stream(struct evhttp_request *request, void *v_server
struct http_server_t *server = (struct http_server_t *)v_server;
struct evhttp_connection *conn;
struct evkeyvalq params;
struct bufferevent *buf_event;
struct stream_client_t *client;
char *client_addr;
unsigned short client_port;
uuid_t uuid;
PROCESS_HEAD_REQUEST;
@@ -251,6 +283,14 @@ static void _http_callback_stream(struct evhttp_request *request, void *v_server
client->need_initial = true;
client->need_first_frame = true;
evhttp_parse_query(evhttp_request_get_uri(request), &params);
client->extra_headers = _http_get_param_true(&params, "extra_headers");
client->advance_headers = _http_get_param_true(&params, "advance_headers");
evhttp_clear_headers(&params);
uuid_generate(uuid);
uuid_unparse_lower(uuid, client->id);
if (server->run->stream_clients == NULL) {
server->run->stream_clients = client;
} else {
@@ -264,8 +304,8 @@ static void _http_callback_stream(struct evhttp_request *request, void *v_server
evhttp_connection_get_peer(conn, &client_addr, &client_port);
LOG_INFO(
"HTTP: Registered the new stream client: [%s]:%u; clients now: %u",
client_addr, client_port, server->run->stream_clients_count
"HTTP: Registered the new stream client: [%s]:%u; id=%s; clients now: %u",
client_addr, client_port, client->id, server->run->stream_clients_count
);
buf_event = evhttp_connection_get_bufferevent(conn);
@@ -284,9 +324,41 @@ static void _http_callback_stream_write(struct bufferevent *buf_event, void *v_c
struct stream_client_t *client = (struct stream_client_t *)v_client;
struct evbuffer *buf;
long double now = get_now_monotonic();
long long now_second = floor_ms(now);
if (now_second != client->fps_accum_second) {
client->fps = client->fps_accum;
client->fps_accum = 0;
client->fps_accum_second = now_second;
}
client->fps_accum += 1;
assert((buf = evbuffer_new()));
// В хроме и его производных есть фундаментальный баг: он отрисовывает
// фрейм с задержкой на один, как только ему придут заголовки следующего.
// В сочетании с drop_same_frames это дает значительный лаг стрима
// при большом количестве дропов (на статичном изображении, где внезапно
// что-то изменилось.
//
// https://bugs.chromium.org/p/chromium/issues/detail?id=527446
//
// Включение advance_headers заставляет стример отсылать заголовки
// будущего фрейма сразу после данных текущего, чтобы триггернуть отрисовку.
// Естественным следствием этого является невозможность установки заголовка
// Content-Length, так как предсказывать будущее мы еще не научились.
// Его наличие не требуется RFC, однако никаких стандартов на MJPG over HTTP
// в природе не существует, и никто не может гарантировать, что отсутствие
// Content-Length не сломает вещание для каких-нибудь маргинальных браузеров.
//
// Кроме того, advance_headers форсит отключение заголовков X-UStreamer-*
// по тем же причинам, по которым у нас нет Content-Length.
# define ADD_ADVANCE_HEADERS \
{ assert(evbuffer_add_printf(buf, \
"Content-Type: image/jpeg" RN "X-Timestamp: %.06Lf" RN RN, get_now_real())); }
if (client->need_initial) {
assert(evbuffer_add_printf(buf,
"HTTP/1.0 200 OK" RN
@@ -294,40 +366,55 @@ static void _http_callback_stream_write(struct bufferevent *buf_event, void *v_c
"Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0" RN
"Pragma: no-cache" RN
"Expires: Mon, 3 Jan 2000 12:34:56 GMT" RN
"Set-Cookie: stream_client_id=%s; path=/; max-age=30" RN
"Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY RN
RN
"--" BOUNDARY RN
"--" BOUNDARY RN,
client->id
));
if (client->advance_headers) {
ADD_ADVANCE_HEADERS;
}
assert(!bufferevent_write_buffer(buf_event, buf));
client->need_initial = false;
}
# define EXPOSED(_next) client->server->run->exposed->_next
assert(evbuffer_add_printf(buf,
"Content-Type: image/jpeg" RN
"Content-Length: %lu" RN
"X-Timestamp: %.06Lf" RN
"%s",
EXPOSED(picture.size) * sizeof(*EXPOSED(picture.data)),
now_real_ms(), (client->server->add_x_timings ? "" : RN)
));
if (client->server->add_x_timings) {
if (!client->advance_headers) {
assert(evbuffer_add_printf(buf,
"X-UStreamer-Grab-Time: %.06Lf" RN
"X-UStreamer-Encode-Begin-Time: %.06Lf" RN
"X-UStreamer-Encode-End-Time: %.06Lf" RN
"X-UStreamer-Expose-Begin-Time: %.06Lf" RN
"X-UStreamer-Expose-End-Time: %.06Lf" RN
"X-UStreamer-Send-Time: %.06Lf" RN
RN,
EXPOSED(picture.grab_time),
EXPOSED(picture.encode_begin_time),
EXPOSED(picture.encode_end_time),
EXPOSED(expose_begin_time),
EXPOSED(expose_end_time),
now_monotonic_ms()
"Content-Type: image/jpeg" RN
"Content-Length: %lu" RN
"X-Timestamp: %.06Lf" RN
"%s",
EXPOSED(picture.size) * sizeof(*EXPOSED(picture.data)),
get_now_real(), (client->extra_headers ? "" : RN)
));
if (client->extra_headers) {
assert(evbuffer_add_printf(buf,
"X-UStreamer-Online: %s" RN
"X-UStreamer-Client-FPS: %u" RN
"X-UStreamer-Grab-Time: %.06Lf" RN
"X-UStreamer-Encode-Begin-Time: %.06Lf" RN
"X-UStreamer-Encode-End-Time: %.06Lf" RN
"X-UStreamer-Expose-Begin-Time: %.06Lf" RN
"X-UStreamer-Expose-Cmp-Time: %.06Lf" RN
"X-UStreamer-Expose-End-Time: %.06Lf" RN
"X-UStreamer-Send-Time: %.06Lf" RN
RN,
(EXPOSED(online) ? "true" : "false"),
client->fps,
EXPOSED(picture.grab_time),
EXPOSED(picture.encode_begin_time),
EXPOSED(picture.encode_end_time),
EXPOSED(expose_begin_time),
EXPOSED(expose_cmp_time),
EXPOSED(expose_end_time),
now
));
}
}
assert(!evbuffer_add(buf,
@@ -336,6 +423,10 @@ static void _http_callback_stream_write(struct bufferevent *buf_event, void *v_c
));
assert(evbuffer_add_printf(buf, RN "--" BOUNDARY RN));
if (client->advance_headers) {
ADD_ADVANCE_HEADERS;
}
assert(!bufferevent_write_buffer(buf_event, buf));
evbuffer_free(buf);
@@ -344,6 +435,7 @@ static void _http_callback_stream_write(struct bufferevent *buf_event, void *v_c
# undef BOUNDARY
# undef RN
# undef ADD_ADVANCE_HEADERS
# undef EXPOSED
}
@@ -379,35 +471,44 @@ static void _http_callback_stream_error(UNUSED struct bufferevent *buf_event, UN
}
static void _http_queue_send_stream(struct http_server_t *server, const bool updated) {
struct stream_client_t *client;
struct evhttp_connection *conn;
struct bufferevent *buf_event;
long long now;
bool queued = false;
static unsigned queued_fps_accum = 0;
static long long queued_fps_second = 0;
for (client = server->run->stream_clients; client != NULL; client = client->next) {
for (struct stream_client_t *client = server->run->stream_clients; client != NULL; client = client->next) {
conn = evhttp_request_get_connection(client->request);
if (conn != NULL && (updated || client->need_first_frame)) {
buf_event = evhttp_connection_get_bufferevent(conn);
bufferevent_setcb(buf_event, NULL, _http_callback_stream_write, _http_callback_stream_error, (void *)client);
bufferevent_enable(buf_event, EV_READ|EV_WRITE);
client->need_first_frame = false;
queued = true;
}
}
if (queued) {
if ((now = floor_ms(get_now_monotonic())) != queued_fps_second) {
server->run->exposed->queued_fps = queued_fps_accum;
queued_fps_accum = 0;
queued_fps_second = now;
}
queued_fps_accum += 1;
}
}
static void _http_exposed_refresh(UNUSED int fd, UNUSED short what, void *v_server) {
struct http_server_t *server = (struct http_server_t *)v_server;
bool updated = false;
bool queue_send = false;
#define LOCK_STREAM \
A_PTHREAD_M_LOCK(&server->run->stream->mutex);
#define UNLOCK_STREAM \
{ server->run->stream->updated = false; A_PTHREAD_M_UNLOCK(&server->run->stream->mutex); }
# define UNLOCK_STREAM \
{ server->run->stream->updated = false; A_PTHREAD_M_UNLOCK(&server->run->stream->mutex); }
if (server->run->stream->updated) {
LOG_DEBUG("Refreshing HTTP exposed ...");
LOCK_STREAM;
A_PTHREAD_M_LOCK(&server->run->stream->mutex);
if (server->run->stream->picture.size > 0) { // If online
updated = _expose_new_picture(server);
UNLOCK_STREAM;
@@ -415,19 +516,14 @@ static void _http_exposed_refresh(UNUSED int fd, UNUSED short what, void *v_serv
UNLOCK_STREAM;
updated = _expose_blank_picture(server);
}
queue_send = true;
} else if (!server->run->exposed->online) {
LOG_DEBUG("Refreshing HTTP exposed (BLANK) ...");
updated = _expose_blank_picture(server);
queue_send = true;
}
if (queue_send) {
_http_queue_send_stream(server, updated);
}
# undef LOCK_STREAM
# undef UNLOCK_STREAM
_http_queue_send_stream(server, updated);
}
static bool _expose_new_picture(struct http_server_t *server) {
@@ -435,8 +531,8 @@ static bool _expose_new_picture(struct http_server_t *server) {
# define EXPOSED(_next) server->run->exposed->_next
assert(STREAM(picture.size) > 0);
EXPOSED(fps) = STREAM(fps);
EXPOSED(expose_begin_time) = now_monotonic_ms();
EXPOSED(captured_fps) = STREAM(captured_fps);
EXPOSED(expose_begin_time) = get_now_monotonic();
# define MEM_STREAM_TO_EXPOSED \
EXPOSED(picture.data), STREAM(picture.data), \
@@ -449,10 +545,20 @@ static bool _expose_new_picture(struct http_server_t *server) {
&& EXPOSED(picture.size) == STREAM(picture.size)
&& !memcmp(MEM_STREAM_TO_EXPOSED)
) {
LOG_PERF("HTTP: dropped same frame number %u", EXPOSED(dropped));
EXPOSED(expose_cmp_time) = get_now_monotonic();
EXPOSED(expose_end_time) = EXPOSED(expose_cmp_time);
LOG_VERBOSE(
"HTTP: dropped same frame number %u; comparsion time = %.06Lf",
EXPOSED(dropped), EXPOSED(expose_cmp_time) - EXPOSED(expose_begin_time)
);
EXPOSED(dropped) += 1;
EXPOSED(expose_end_time) = now_monotonic_ms();
return false; // Not updated
} else {
EXPOSED(expose_cmp_time) = get_now_monotonic();
LOG_VERBOSE(
"HTTP: passed same frame check (frames are differ); comparsion time = %.06Lf",
EXPOSED(expose_cmp_time) - EXPOSED(expose_begin_time)
);
}
}
@@ -475,7 +581,13 @@ static bool _expose_new_picture(struct http_server_t *server) {
EXPOSED(height) = STREAM(height);
EXPOSED(online) = true;
EXPOSED(dropped) = 0;
EXPOSED(expose_end_time) = now_monotonic_ms();
EXPOSED(expose_cmp_time) = EXPOSED(expose_begin_time);
EXPOSED(expose_end_time) = get_now_monotonic();
LOG_VERBOSE(
"HTTP: exposed new frame; full exposition time = %.06Lf",
EXPOSED(expose_end_time) - EXPOSED(expose_begin_time)
);
# undef STREAM
# undef EXPOSED
@@ -485,7 +597,8 @@ static bool _expose_new_picture(struct http_server_t *server) {
static bool _expose_blank_picture(struct http_server_t *server) {
# define EXPOSED(_next) server->run->exposed->_next
EXPOSED(expose_begin_time) = now_monotonic_ms();
EXPOSED(expose_begin_time) = get_now_monotonic();
EXPOSED(expose_cmp_time) = EXPOSED(expose_begin_time);
if (EXPOSED(online) || EXPOSED(picture.size) == 0) {
if (EXPOSED(picture.allocated) < BLANK_JPG_SIZE) {
@@ -506,7 +619,7 @@ static bool _expose_blank_picture(struct http_server_t *server) {
EXPOSED(width) = BLANK_JPG_WIDTH;
EXPOSED(height) = BLANK_JPG_HEIGHT;
EXPOSED(fps) = 0;
EXPOSED(captured_fps) = 0;
EXPOSED(online) = false;
goto updated;
}
@@ -514,13 +627,13 @@ static bool _expose_blank_picture(struct http_server_t *server) {
if (EXPOSED(dropped) < server->run->drop_same_frames_blank) {
LOG_PERF("HTTP: dropped same frame (BLANK) number %u", EXPOSED(dropped));
EXPOSED(dropped) += 1;
EXPOSED(expose_end_time) = now_monotonic_ms();
EXPOSED(expose_end_time) = get_now_monotonic();
return false; // Not updated
}
updated:
EXPOSED(dropped) = 0;
EXPOSED(expose_end_time) = now_monotonic_ms();
EXPOSED(expose_end_time) = get_now_monotonic();
return true; // Updated
# undef EXPOSED

View File

@@ -31,8 +31,14 @@
struct stream_client_t {
struct http_server_t *server;
struct evhttp_request *request;
char id[37]; // ex. "1b4e28ba-2fa1-11d2-883f-0016d3cca427" + "\0"
bool extra_headers;
bool advance_headers;
bool need_initial;
bool need_first_frame;
unsigned fps;
unsigned fps_accum;
long long fps_accum_second;
struct stream_client_t *prev;
struct stream_client_t *next;
@@ -42,10 +48,12 @@ struct exposed_t {
struct picture_t picture;
unsigned width;
unsigned height;
unsigned fps;
unsigned captured_fps;
unsigned queued_fps;
bool online;
unsigned dropped;
long double expose_begin_time;
long double expose_cmp_time;
long double expose_end_time;
};
@@ -64,7 +72,6 @@ struct http_server_t {
char *host;
unsigned port;
unsigned drop_same_frames;
bool add_x_timings;
unsigned fake_width;
unsigned fake_height;
unsigned timeout;

View File

@@ -71,7 +71,7 @@ pthread_mutex_t log_mutex;
}
#define LOG_PRINTF_NOLOCK(_label, _msg, ...) { \
printf("-- " _label " [%.03Lf tid=%ld] -- " _msg "\n", now_monotonic_ms(), syscall(SYS_gettid), ##__VA_ARGS__); \
printf("-- " _label " [%.03Lf tid=%ld] -- " _msg "\n", get_now_monotonic(), syscall(SYS_gettid), ##__VA_ARGS__); \
fflush(stdout); \
}
@@ -83,9 +83,9 @@ pthread_mutex_t log_mutex;
#define LOG_PERROR(_msg, ...) { \
char _buf[1024] = ""; \
strerror_r(errno, _buf, 1024); \
char *_ptr = strerror_r(errno, _buf, 1024); \
LOGGING_LOCK; \
printf("-- ERROR [%.03Lf tid=%ld] -- " _msg ": %s\n", now_monotonic_ms(), syscall(SYS_gettid), ##__VA_ARGS__, _buf); \
printf("-- ERROR [%.03Lf tid=%ld] -- " _msg ": %s\n", get_now_monotonic(), syscall(SYS_gettid), ##__VA_ARGS__, _ptr); \
fflush(stdout); \
LOGGING_UNLOCK; \
}

View File

@@ -25,7 +25,7 @@
#endif
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <signal.h>
#include <getopt.h>
@@ -48,6 +48,7 @@ static const struct option _long_opts[] = {
{"height", required_argument, NULL, 'y'},
{"format", required_argument, NULL, 'f'},
{"tv-standard", required_argument, NULL, 'a'},
{"soft-fps", required_argument, NULL, 'm'},
{"every-frame", required_argument, NULL, 'e'},
{"min-frame-size", required_argument, NULL, 'z'},
{"dv-timings", no_argument, NULL, 't'},
@@ -59,12 +60,12 @@ static const struct option _long_opts[] = {
{"encoder-omx-use-ijg", required_argument, NULL, 500},
# endif
{"device-timeout", required_argument, NULL, 1000},
{"device-error-timeout", required_argument, NULL, 1001},
{"device-persistent", no_argument, NULL, 1001},
{"device-error-delay", required_argument, NULL, 1002},
{"host", required_argument, NULL, 's'},
{"port", required_argument, NULL, 'p'},
{"drop-same-frames", required_argument, NULL, 'r'},
{"add-x-timings", no_argument, NULL, 2000},
{"fake-width", required_argument, NULL, 2001},
{"fake-height", required_argument, NULL, 2002},
{"server-timeout", required_argument, NULL, 2003},
@@ -74,13 +75,26 @@ static const struct option _long_opts[] = {
{"debug", no_argument, NULL, 5002},
{"log-level", required_argument, NULL, 5010},
{"help", no_argument, NULL, 'h'},
{"version", no_argument, NULL, 6000},
{NULL, 0, NULL, 0},
};
static void _version(bool nl) {
printf(VERSION);
# ifdef OMX_ENCODER
printf(" + OMX");
# endif
if (nl) {
putchar('\n');
}
}
static void _help(struct device_t *dev, struct encoder_t *encoder, struct http_server_t *server) {
printf("\nuStreamer - Lightweight and fast MJPG-HTTP streamer\n");
printf("===================================================\n\n");
printf("Version: %s; license: GPLv3\n", VERSION);
printf("Version: ");
_version(false);
printf("; license: GPLv3\n");
printf("Copyright (C) 2018 Maxim Devaev <mdevaev@gmail.com>\n\n");
printf("Capturing options:\n");
printf("------------------\n");
@@ -92,6 +106,7 @@ static void _help(struct device_t *dev, struct encoder_t *encoder, struct http_s
printf(" Available: %s; default: YUYV.\n\n", FORMATS_STR);
printf(" -a|--tv-standard <std> -- Force TV standard.\n");
printf(" Available: %s; default: disabled.\n\n", STANDARDS_STR);
printf(" -m|--soft-fps <N> -- Soft FPS limit; default: %u.\n\n", dev->soft_fps);
printf(" -e|--every-frame <N> -- Drop all input frames except specified. Default: disabled.\n\n");
printf(" -z|--min-frame-size <N> -- Drop frames smaller then this limit.\n");
printf(" Useful if the device produces small-sized garbage frames.\n\n");
@@ -109,8 +124,9 @@ static void _help(struct device_t *dev, struct encoder_t *encoder, struct http_s
printf(" Default: disabled.\n\n");
# endif
printf(" --device-timeout <seconds> -- Timeout for device querying. Default: %d\n\n", dev->timeout);
printf(" --device-error-timeout <seconds> -- Delay before trying to connect to the device again\n");
printf(" after a timeout. Default: %d\n\n", dev->error_timeout);
printf(" --device-persistent -- Don't re-initialize device on timeout. Default: disabled.\n\n");
printf(" --device-error-delay <seconds> -- Delay before trying to connect to the device again\n");
printf(" after a timeout. Default: %d\n\n", dev->error_delay);
printf("HTTP server options:\n");
printf("--------------------\n");
printf(" --host <address> -- Listen on Hostname or IP. Default: %s\n\n", server->host);
@@ -119,8 +135,6 @@ static void _help(struct device_t *dev, struct encoder_t *encoder, struct http_s
printf(" It can significantly reduce the outgoing traffic, but will increase\n");
printf(" the CPU loading. Don't use this option with analog signal sources\n");
printf(" or webcams, it's useless. Default: disabled.\n\n");
printf(" --add-x-timings -- Add X-UStreamer-*-Time headers to the /stream and /snapshot handles.\n");
printf(" Default: disabled.\n\n");
printf(" --fake-width <N> -- Override image width for /ping. Default: disabled\n\n");
printf(" --fake-height <N> -- Override image height for /ping. Default: disabled.\n\n");
printf(" --server-timeout <seconds> -- Timeout for client connections. Default: %d\n\n", server->timeout);
@@ -166,6 +180,7 @@ static int _parse_options(int argc, char *argv[], struct device_t *dev, struct e
case 'f': OPT_PARSE(dev->format, device_parse_format, FORMAT_UNKNOWN, "pixel format");
# pragma GCC diagnostic pop
case 'a': OPT_PARSE(dev->standard, device_parse_standard, STANDARD_UNKNOWN, "TV standard");
case 'm': OPT_UNSIGNED(dev->soft_fps, "--soft-fps", 1, 30);
case 'e': OPT_UNSIGNED(dev->every_frame, "--every-frame", 1, 30);
case 'z': OPT_UNSIGNED(dev->min_frame_size, "--min-frame-size", 0, 8192);
case 't': OPT_SET(dev->dv_timings, true);
@@ -176,13 +191,13 @@ static int _parse_options(int argc, char *argv[], struct device_t *dev, struct e
# ifdef OMX_ENCODER
case 500: OPT_SET(encoder->omx_use_ijg, true);
# endif
case 1000: OPT_UNSIGNED(dev->timeout, "--timeout", 1, 60);
case 1001: OPT_UNSIGNED(dev->error_timeout, "--error-timeout", 1, 60);
case 1000: OPT_UNSIGNED(dev->timeout, "--device-timeout", 1, 60);
case 1001: OPT_SET(dev->persistent, true);
case 1002: OPT_UNSIGNED(dev->error_delay, "--device-error-delay", 1, 60);
case 's': OPT_SET(server->host, optarg);
case 'p': OPT_UNSIGNED(server->port, "--port", 1, 65535);
case 'r': OPT_UNSIGNED(server->drop_same_frames, "--drop-same-frames", 0, 30);
case 2000: OPT_SET(server->add_x_timings, true);
case 2001: OPT_UNSIGNED(server->fake_width, "--fake-width", 0, 1920);
case 2002: OPT_UNSIGNED(server->fake_height, "--fake-height", 0, 1200);
case 2003: OPT_UNSIGNED(server->timeout, "--server-timeout", 1, 60);
@@ -191,8 +206,10 @@ static int _parse_options(int argc, char *argv[], struct device_t *dev, struct e
case 5001: OPT_SET(log_level, LOG_LEVEL_VERBOSE);
case 5002: OPT_SET(log_level, LOG_LEVEL_DEBUG);
case 5010: OPT_UNSIGNED(log_level, "--log-level", 0, 3);
case 'h': _help(dev, encoder, server); return 1;
case 6000: _version(true); return 1;
case 0: break;
case 'h': default: _help(dev, encoder, server); return -1;
default: _help(dev, encoder, server); return -1;
}
}
@@ -294,5 +311,5 @@ int main(int argc, char *argv[]) {
device_destroy(dev);
LOGGING_DESTROY;
return abs(exit_code);
return (exit_code < 0 ? 1 : 0);
}

View File

@@ -35,7 +35,7 @@
#define LOG_OMX_ERROR(_error, _msg, ...) { \
LOGGING_LOCK; \
printf("-- ERROR [%.03Lf tid=%ld] -- " _msg ": %s\n", now_monotonic_ms(), \
printf("-- ERROR [%.03Lf tid=%ld] -- " _msg ": %s\n", get_now_monotonic(), \
syscall(SYS_gettid), ##__VA_ARGS__, omx_error_to_string(_error)); \
LOGGING_UNLOCK; \
}

View File

@@ -85,12 +85,15 @@ void stream_loop(struct stream_t *stream) {
unsigned frames_count = 0;
long double grab_after = 0;
unsigned fluency_passed = 0;
unsigned fps = 0;
long long fps_second = 0;
unsigned captured_fps_accum = 0;
long long captured_fps_second = 0;
bool persistent_timeout_reported = false;
LOG_DEBUG("Allocation memory for stream picture ...");
A_CALLOC(stream->picture.data, stream->dev->run->max_picture_size);
LOG_INFO("Capturing ...");
while (!stream->dev->stop) {
int free_worker_number = -1;
@@ -111,7 +114,7 @@ void stream_loop(struct stream_t *stream) {
free_worker_number = oldest_worker->ctx.number;
oldest_worker = oldest_worker->order_next;
LOG_PERF("##### ACCEPT : %u", free_worker_number);
LOG_PERF("##### Raw frame accepted; worker = %u", free_worker_number);
} else {
for (unsigned number = 0; number < stream->dev->run->n_workers; ++number) {
if (!pool.workers[number].has_job && (free_worker_number == -1
@@ -125,7 +128,7 @@ void stream_loop(struct stream_t *stream) {
assert(free_worker_number >= 0);
assert(!pool.workers[free_worker_number].has_job);
LOG_PERF("----- DROP : %u", free_worker_number);
LOG_PERF("----- Raw frame dropped; worker = %u", free_worker_number);
}
if (stream->dev->stop) {
@@ -154,15 +157,26 @@ void stream_loop(struct stream_t *stream) {
}
} else if (retval == 0) {
LOG_ERROR("Mainloop select() timeout");
break;
if (stream->dev->persistent) {
if (!persistent_timeout_reported) {
LOG_ERROR("Mainloop select() timeout, polling ...")
persistent_timeout_reported = true;
}
continue;
} else {
LOG_ERROR("Mainloop select() timeout");
break;
}
} else {
persistent_timeout_reported = false;
if (FD_ISSET(stream->dev->run->fd, &read_fds)) {
LOG_DEBUG("Frame is ready");
struct v4l2_buffer buf_info;
long double now = now_monotonic_ms();
long double now = get_now_monotonic();
long long now_second = floor_ms(now);
if (_stream_grab_buffer(stream->dev, &buf_info) < 0) {
break;
@@ -196,13 +210,13 @@ void stream_loop(struct stream_t *stream) {
}
fluency_passed = 0;
if ((long long)now != fps_second) {
LOG_PERF("Oldest worker complete, encoding FPS = %u", fps);
stream->fps = fps;
fps = 0;
fps_second = (long long)now;
if (now_second != captured_fps_second) {
stream->captured_fps = captured_fps_accum;
captured_fps_accum = 0;
captured_fps_second = now_second;
LOG_PERF("Oldest worker complete, Captured-FPS = %u", stream->captured_fps);
}
fps += 1;
captured_fps_accum += 1;
long double fluency_delay = _stream_get_fluency_delay(stream->dev, &pool);
@@ -307,17 +321,26 @@ static void _stream_expose_picture(struct stream_t *stream, unsigned buf_index)
}
static long double _stream_get_fluency_delay(struct device_t *dev, struct workers_pool_t *pool) {
long double delay = 0;
long double comp_time = 0;
long double min_delay;
long double soft_delay;
for (unsigned number = 0; number < dev->run->n_workers; ++number) {
A_PTHREAD_M_LOCK(&pool->workers[number].last_comp_time_mutex);
if (pool->workers[number].last_comp_time > 0) {
delay += pool->workers[number].last_comp_time;
comp_time += pool->workers[number].last_comp_time;
}
A_PTHREAD_M_UNLOCK(&pool->workers[number].last_comp_time_mutex);
}
// Среднее арифметическое деленное на количество воркеров
return delay / dev->run->n_workers / dev->run->n_workers;
comp_time = comp_time / dev->run->n_workers; // Среднее время работы воркеров
min_delay = comp_time / dev->run->n_workers; // Минимальное время работы размазывается на N воркеров
soft_delay = ((long double)1) / dev->soft_fps; // Искусственное время задержки на основе желаемого FPS
if (min_delay > 0) {
return (min_delay > soft_delay ? min_delay : soft_delay);
}
return min_delay;
}
static int _stream_init_loop(struct device_t *dev, struct workers_pool_t *pool) {
@@ -326,8 +349,8 @@ static int _stream_init_loop(struct device_t *dev, struct workers_pool_t *pool)
LOG_DEBUG("%s: *dev->stop = %d", __FUNCTION__, dev->stop);
while (!dev->stop) {
if ((retval = _stream_init(dev, pool)) < 0) {
LOG_INFO("Sleeping %d seconds before new stream init ...", dev->error_timeout);
sleep(dev->error_timeout);
LOG_INFO("Sleeping %d seconds before new stream init ...", dev->error_delay);
sleep(dev->error_delay);
} else {
break;
}

View File

@@ -86,7 +86,7 @@ struct stream_t {
struct picture_t picture;
unsigned width;
unsigned height;
unsigned fps;
unsigned captured_fps;
bool updated;
pthread_mutex_t mutex;
struct device_t *dev;

View File

@@ -61,7 +61,11 @@ INLINE unsigned max_u(unsigned a, unsigned b) {
return (a > b ? a : b);
}
INLINE void now_ms(clockid_t clk_id, time_t *sec, long *msec) {
INLINE long long floor_ms(long double now) {
return (long long) now - (now < (long long) now); // floor()
}
INLINE void get_now(clockid_t clk_id, time_t *sec, long *msec) {
struct timespec spec;
assert(!clock_gettime(clk_id, &spec));
@@ -74,18 +78,18 @@ INLINE void now_ms(clockid_t clk_id, time_t *sec, long *msec) {
}
}
INLINE long double now_monotonic_ms(void) {
INLINE long double get_now_monotonic(void) {
time_t sec;
long msec;
now_ms(CLOCK_MONOTONIC_RAW, &sec, &msec);
get_now(CLOCK_MONOTONIC_RAW, &sec, &msec);
return (long double)sec + ((long double)msec) / 1000;
}
INLINE long double now_real_ms(void) {
INLINE long double get_now_real(void) {
time_t sec;
long msec;
now_ms(CLOCK_REALTIME, &sec, &msec);
get_now(CLOCK_REALTIME, &sec, &msec);
return (long double)sec + ((long double)msec) / 1000;
}