mirror of
https://github.com/pikvm/ustreamer.git
synced 2026-02-27 20:26:31 +00:00
Compare commits
21 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 |
@@ -1,7 +1,7 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
commit = True
|
commit = True
|
||||||
tag = True
|
tag = True
|
||||||
current_version = 5.22
|
current_version = 5.26
|
||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)
|
parse = (?P<major>\d+)\.(?P<minor>\d+)
|
||||||
serialize =
|
serialize =
|
||||||
{major}.{minor}
|
{major}.{minor}
|
||||||
|
|||||||
@@ -7,3 +7,4 @@
|
|||||||
!python/**
|
!python/**
|
||||||
!janus/**
|
!janus/**
|
||||||
!man/**
|
!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
|
# These are supported funding model platforms
|
||||||
|
|
||||||
patreon: pikvm
|
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```.
|
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
|
# Raspberry Pi Camera Example
|
||||||
|
|
||||||
Example usage for the Raspberry Pi v1 camera:
|
Example usage for the Raspberry Pi v1 camera:
|
||||||
```bash
|
```bash
|
||||||
$ sudo modprobe bcm2835-v4l2
|
$ sudo modprobe bcm2835-v4l2
|
||||||
|
|||||||
29
docs/h264.md
29
docs/h264.md
@@ -78,6 +78,17 @@ memsink: {
|
|||||||
EOF
|
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
|
### 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:
|
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. 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:
|
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
|
- `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.
|
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.
|
- 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 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 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 µ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. The client attaches the video stream to the `<video>` element, rendering the video stream in the browser window.
|
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
|
### Sample Code
|
||||||
|
|
||||||
@@ -176,7 +188,7 @@ The client-side JavaScript application uses the following control flow:
|
|||||||
// successfully.
|
// successfully.
|
||||||
success: function (pluginHandle) {
|
success: function (pluginHandle) {
|
||||||
uStreamerPluginHandle = 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" } });
|
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) {
|
onremotetrack: function (mediaStreamTrack, mediaId, isAdded) {
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
// Attach the received media track to the video element. Cloning the
|
// Attach the received media track to the video element. Cloning the
|
||||||
// mediaStreamTrack creates a new object with a distinct, globally
|
// mediaStreamTrack creates a new object with a distinct, globally
|
||||||
// unique stream identifier.
|
// unique stream identifier.
|
||||||
const videoElement = document.getElementById("webrtc-output");
|
const videoElement = document.getElementById("webrtc-output");
|
||||||
const stream = new MediaStream();
|
if (videoElement.srcObject === null) {
|
||||||
stream.addTrack(mediaStreamTrack.clone());
|
videoElement.srcObject = new MediaStream();
|
||||||
videoElement.srcObject = stream;
|
}
|
||||||
|
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;
|
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_s *frame = us_frame_init();
|
||||||
us_frame_set_data(frame, mem->data, mem->used);
|
us_frame_set_data(frame, mem->data, mem->used);
|
||||||
US_FRAME_COPY_META(mem, frame);
|
US_FRAME_COPY_META(mem, frame);
|
||||||
*frame_id = mem->id;
|
*frame_id = mem->id;
|
||||||
mem->last_client_ts = us_get_now_monotonic();
|
mem->last_client_ts = us_get_now_monotonic();
|
||||||
|
if (key_required) {
|
||||||
|
mem->key_requested = true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
if (frame->format != V4L2_PIX_FMT_H264) {
|
if (frame->format != V4L2_PIX_FMT_H264) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <unistd.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);
|
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_ready = false;
|
||||||
static atomic_bool _g_stop = false;
|
static atomic_bool _g_stop = false;
|
||||||
static atomic_bool _g_has_watchers = false;
|
static atomic_bool _g_has_watchers = false;
|
||||||
|
static atomic_bool _g_key_required = false;
|
||||||
|
|
||||||
|
|
||||||
#define _LOCK_VIDEO US_MUTEX_LOCK(_g_video_lock)
|
#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) {
|
while (!_STOP && _HAS_WATCHERS) {
|
||||||
const int result = us_memsink_fd_wait_frame(fd, mem, frame_id);
|
const int result = us_memsink_fd_wait_frame(fd, mem, frame_id);
|
||||||
if (result == 0) {
|
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) {
|
if (frame == NULL) {
|
||||||
goto close_memsink;
|
goto close_memsink;
|
||||||
}
|
}
|
||||||
|
// if (frame->key) {
|
||||||
|
// atomic_store(&_g_key_required, false);
|
||||||
|
// }
|
||||||
if (us_queue_put(_g_video_queue, frame, 0) != 0) {
|
if (us_queue_put(_g_video_queue, frame, 0) != 0) {
|
||||||
_IF_NOT_REPORTED({ US_JLOG_PERROR("video", "Video queue is full"); });
|
_IF_NOT_REPORTED({ US_JLOG_PERROR("video", "Video queue is full"); });
|
||||||
us_frame_destroy(frame);
|
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.rmem_max=1000000
|
||||||
// sysctl -w net.core.wmem_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)) {
|
if (gw == NULL || config_dir_path == NULL || ((_g_config = us_config_init(config_dir_path)) == NULL)) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -423,6 +427,7 @@ static struct janus_plugin_result *_plugin_handle_message(
|
|||||||
} else if (!strcmp(request_str, "watch")) {
|
} else if (!strcmp(request_str, "watch")) {
|
||||||
char *sdp;
|
char *sdp;
|
||||||
{
|
{
|
||||||
|
// atomic_store(&_g_key_required, true);
|
||||||
char *const video_sdp = us_rtpv_make_sdp(_g_rtpv);
|
char *const video_sdp = us_rtpv_make_sdp(_g_rtpv);
|
||||||
if (video_sdp == NULL) {
|
if (video_sdp == NULL) {
|
||||||
PUSH_ERROR(503, "Haven't received SPS/PPS from memsink yet");
|
PUSH_ERROR(503, "Haven't received SPS/PPS from memsink yet");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.\" Manpage for ustreamer-dump.
|
.\" Manpage for ustreamer-dump.
|
||||||
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
|
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
|
||||||
.TH USTREAMER-DUMP 1 "version 5.22" "January 2021"
|
.TH USTREAMER-DUMP 1 "version 5.26" "January 2021"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
ustreamer-dump \- Dump uStreamer's memory sink to file
|
ustreamer-dump \- Dump uStreamer's memory sink to file
|
||||||
@@ -44,6 +44,9 @@ Limit the number of frames. Default: 0 (infinite).
|
|||||||
.TP
|
.TP
|
||||||
.BR \-i ", "\-\-interval\ \fIsec
|
.BR \-i ", "\-\-interval\ \fIsec
|
||||||
Delay between reading frames (float). Default: 0.
|
Delay between reading frames (float). Default: 0.
|
||||||
|
.TP
|
||||||
|
.BR \-k ", " \-\-key
|
||||||
|
Request keyframe from the sink. Default: disabled.
|
||||||
|
|
||||||
.SS "Logging options"
|
.SS "Logging options"
|
||||||
.TP
|
.TP
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.\" Manpage for ustreamer.
|
.\" Manpage for ustreamer.
|
||||||
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
|
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
|
||||||
.TH USTREAMER 1 "version 5.22" "November 2020"
|
.TH USTREAMER 1 "version 5.26" "November 2020"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
ustreamer \- stream MJPEG video from any V4L2 device to the network
|
ustreamer \- stream MJPEG video from any V4L2 device to the network
|
||||||
@@ -203,6 +203,9 @@ Default: disabled.
|
|||||||
.BR \-\-allow\-origin\ \fIstr
|
.BR \-\-allow\-origin\ \fIstr
|
||||||
Set Access\-Control\-Allow\-Origin header. Default: disabled.
|
Set Access\-Control\-Allow\-Origin header. Default: disabled.
|
||||||
.TP
|
.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
|
.BR \-\-server\-timeout\ \fIsec
|
||||||
Timeout for client connections. Default: 10.
|
Timeout for client connections. Default: 10.
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
pkgname=ustreamer
|
pkgname=ustreamer
|
||||||
pkgver=5.22
|
pkgver=5.26
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Lightweight and fast MJPEG-HTTP streamer"
|
pkgdesc="Lightweight and fast MJPEG-HTTP streamer"
|
||||||
url="https://github.com/pikvm/ustreamer"
|
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
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=ustreamer
|
PKG_NAME:=ustreamer
|
||||||
PKG_VERSION:=5.22
|
PKG_VERSION:=5.26
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=1
|
||||||
PKG_MAINTAINER:=Maxim Devaev <mdevaev@gmail.com>
|
PKG_MAINTAINER:=Maxim Devaev <mdevaev@gmail.com>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ def _find_sources(suffix: str) -> list[str]:
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
setup(
|
setup(
|
||||||
name="ustreamer",
|
name="ustreamer",
|
||||||
version="5.22",
|
version="5.26",
|
||||||
description="uStreamer tools",
|
description="uStreamer tools",
|
||||||
author="Maxim Devaev",
|
author="Maxim Devaev",
|
||||||
author_email="mdevaev@gmail.com",
|
author_email="mdevaev@gmail.com",
|
||||||
|
|||||||
@@ -181,12 +181,18 @@ static int _wait_frame(_MemsinkObject *self) {
|
|||||||
return -2;
|
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) {
|
if (self->mem == NULL || self->fd <= 0) {
|
||||||
PyErr_SetString(PyExc_RuntimeError, "Closed");
|
PyErr_SetString(PyExc_RuntimeError, "Closed");
|
||||||
return NULL;
|
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)) {
|
switch (_wait_frame(self)) {
|
||||||
case 0: break;
|
case 0: break;
|
||||||
case -2: Py_RETURN_NONE;
|
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_id = _MEM(id);
|
||||||
self->frame_ts = us_get_now_monotonic();
|
self->frame_ts = us_get_now_monotonic();
|
||||||
_MEM(last_client_ts) = self->frame_ts;
|
_MEM(last_client_ts) = self->frame_ts;
|
||||||
|
if (key_required) {
|
||||||
|
_MEM(key_requested) = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (flock(self->fd, LOCK_UN) < 0) {
|
if (flock(self->fd, LOCK_UN) < 0) {
|
||||||
return PyErr_SetFromErrno(PyExc_OSError);
|
return PyErr_SetFromErrno(PyExc_OSError);
|
||||||
@@ -260,7 +269,7 @@ static PyMethodDef _MemsinkObject_methods[] = {
|
|||||||
ADD_METHOD("close", close, METH_NOARGS),
|
ADD_METHOD("close", close, METH_NOARGS),
|
||||||
ADD_METHOD("__enter__", enter, METH_NOARGS),
|
ADD_METHOD("__enter__", enter, METH_NOARGS),
|
||||||
ADD_METHOD("__exit__", exit, METH_VARARGS),
|
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),
|
ADD_METHOD("is_opened", is_opened, METH_NOARGS),
|
||||||
{},
|
{},
|
||||||
# undef ADD_METHOD
|
# undef ADD_METHOD
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ enum _OPT_VALUES {
|
|||||||
_O_OUTPUT_JSON = 'j',
|
_O_OUTPUT_JSON = 'j',
|
||||||
_O_COUNT = 'c',
|
_O_COUNT = 'c',
|
||||||
_O_INTERVAL = 'i',
|
_O_INTERVAL = 'i',
|
||||||
|
_O_KEY = 'k',
|
||||||
|
|
||||||
_O_HELP = 'h',
|
_O_HELP = 'h',
|
||||||
_O_VERSION = 'v',
|
_O_VERSION = 'v',
|
||||||
@@ -67,6 +68,7 @@ static const struct option _LONG_OPTS[] = {
|
|||||||
{"output-json", no_argument, NULL, _O_OUTPUT_JSON},
|
{"output-json", no_argument, NULL, _O_OUTPUT_JSON},
|
||||||
{"count", required_argument, NULL, _O_COUNT},
|
{"count", required_argument, NULL, _O_COUNT},
|
||||||
{"interval", required_argument, NULL, _O_INTERVAL},
|
{"interval", required_argument, NULL, _O_INTERVAL},
|
||||||
|
{"key", no_argument, NULL, _O_KEY},
|
||||||
|
|
||||||
{"log-level", required_argument, NULL, _O_LOG_LEVEL},
|
{"log-level", required_argument, NULL, _O_LOG_LEVEL},
|
||||||
{"perf", no_argument, NULL, _O_PERF},
|
{"perf", no_argument, NULL, _O_PERF},
|
||||||
@@ -98,6 +100,7 @@ static void _install_signal_handlers(void);
|
|||||||
static int _dump_sink(
|
static int _dump_sink(
|
||||||
const char *sink_name, unsigned sink_timeout,
|
const char *sink_name, unsigned sink_timeout,
|
||||||
long long count, long double interval,
|
long long count, long double interval,
|
||||||
|
bool key_required,
|
||||||
_output_context_s *ctx);
|
_output_context_s *ctx);
|
||||||
|
|
||||||
static void _help(FILE *fp);
|
static void _help(FILE *fp);
|
||||||
@@ -113,6 +116,7 @@ int main(int argc, char *argv[]) {
|
|||||||
bool output_json = false;
|
bool output_json = false;
|
||||||
long long count = 0;
|
long long count = 0;
|
||||||
long double interval = 0;
|
long double interval = 0;
|
||||||
|
bool key_required = false;
|
||||||
|
|
||||||
# define OPT_SET(_dest, _value) { \
|
# define OPT_SET(_dest, _value) { \
|
||||||
_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_OUTPUT_JSON: OPT_SET(output_json, true);
|
||||||
case _O_COUNT: OPT_NUMBER("--count", count, 0, LLONG_MAX, 0);
|
case _O_COUNT: OPT_NUMBER("--count", count, 0, LLONG_MAX, 0);
|
||||||
case _O_INTERVAL: OPT_LDOUBLE("--interval", interval, 0, 60);
|
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_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);
|
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();
|
_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) {
|
if (ctx.v_output && ctx.destroy) {
|
||||||
ctx.destroy(ctx.v_output);
|
ctx.destroy(ctx.v_output);
|
||||||
}
|
}
|
||||||
@@ -223,6 +228,7 @@ static void _install_signal_handlers(void) {
|
|||||||
static int _dump_sink(
|
static int _dump_sink(
|
||||||
const char *sink_name, unsigned sink_timeout,
|
const char *sink_name, unsigned sink_timeout,
|
||||||
long long count, long double interval,
|
long long count, long double interval,
|
||||||
|
bool key_required,
|
||||||
_output_context_s *ctx) {
|
_output_context_s *ctx) {
|
||||||
|
|
||||||
if (count == 0) {
|
if (count == 0) {
|
||||||
@@ -245,8 +251,10 @@ static int _dump_sink(
|
|||||||
long double last_ts = 0;
|
long double last_ts = 0;
|
||||||
|
|
||||||
while (!_g_stop) {
|
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) {
|
if (error == 0) {
|
||||||
|
key_required = false;
|
||||||
|
|
||||||
const long double now = us_get_now_monotonic();
|
const long double now = us_get_now_monotonic();
|
||||||
const long long now_second = us_floor_ms(now);
|
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(" -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(" -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(" -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("Logging options:");
|
||||||
SAY("════════════════");
|
SAY("════════════════");
|
||||||
SAY(" --log-level <N> ──── Verbosity level of messages from 0 (info) to 3 (debug).");
|
SAY(" --log-level <N> ──── Verbosity level of messages from 0 (info) to 3 (debug).");
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define US_VERSION_MAJOR 5
|
#define US_VERSION_MAJOR 5
|
||||||
#define US_VERSION_MINOR 22
|
#define US_VERSION_MINOR 26
|
||||||
|
|
||||||
#define US_MAKE_VERSION2(_major, _minor) #_major "." #_minor
|
#define US_MAKE_VERSION2(_major, _minor) #_major "." #_minor
|
||||||
#define US_MAKE_VERSION1(_major, _minor) US_MAKE_VERSION2(_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));;
|
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);
|
assert(sink->server);
|
||||||
|
|
||||||
const long double now = us_get_now_monotonic();
|
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->last_id = us_get_now_id();
|
||||||
sink->mem->id = sink->last_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);
|
memcpy(sink->mem->data, frame->data, frame->used);
|
||||||
sink->mem->used = 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->magic = US_MEMSINK_MAGIC;
|
||||||
sink->mem->version = US_MEMSINK_VERSION;
|
sink->mem->version = US_MEMSINK_VERSION;
|
||||||
|
|
||||||
atomic_store(&sink->has_clients, (sink->mem->last_client_ts + sink->client_ttl > us_get_now_monotonic()));
|
atomic_store(&sink->has_clients, (sink->mem->last_client_ts + sink->client_ttl > us_get_now_monotonic()));
|
||||||
|
|
||||||
if (flock(sink->fd, LOCK_UN) < 0) {
|
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;
|
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
|
assert(!sink->server); // Client only
|
||||||
|
|
||||||
if (us_flock_timedwait_monotonic(sink->fd, sink->timeout) < 0) {
|
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;
|
retval = 0;
|
||||||
}
|
}
|
||||||
sink->mem->last_client_ts = us_get_now_monotonic();
|
sink->mem->last_client_ts = us_get_now_monotonic();
|
||||||
|
if (key_required) {
|
||||||
|
sink->mem->key_requested = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
done:
|
done:
|
||||||
|
|||||||
@@ -63,6 +63,6 @@ us_memsink_s *us_memsink_init(
|
|||||||
void us_memsink_destroy(us_memsink_s *sink);
|
void us_memsink_destroy(us_memsink_s *sink);
|
||||||
|
|
||||||
bool us_memsink_server_check(us_memsink_s *sink, const us_frame_s *frame);
|
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_MAGIC ((uint64_t)0xCAFEBABECAFEBABE)
|
||||||
#define US_MEMSINK_VERSION ((uint32_t)2)
|
#define US_MEMSINK_VERSION ((uint32_t)3)
|
||||||
|
|
||||||
#ifndef US_CFG_MEMSINK_MAX_DATA
|
#ifndef US_CFG_MEMSINK_MAX_DATA
|
||||||
# define US_CFG_MEMSINK_MAX_DATA 33554432
|
# define US_CFG_MEMSINK_MAX_DATA 33554432
|
||||||
@@ -57,6 +57,7 @@ typedef struct {
|
|||||||
long double encode_end_ts;
|
long double encode_end_ts;
|
||||||
|
|
||||||
long double last_client_ts;
|
long double last_client_ts;
|
||||||
|
bool key_requested;
|
||||||
|
|
||||||
uint8_t data[US_MEMSINK_MAX_DATA];
|
uint8_t data[US_MEMSINK_MAX_DATA];
|
||||||
} us_memsink_shared_s;
|
} 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);
|
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;
|
bool online = false;
|
||||||
if (!us_m2m_encoder_compress(h264->enc, frame, h264->dest, force_key)) {
|
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);
|
atomic_store(&h264->online, online);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
us_memsink_s *sink;
|
us_memsink_s *sink;
|
||||||
|
bool key_requested;
|
||||||
us_frame_s *tmp_src;
|
us_frame_s *tmp_src;
|
||||||
us_frame_s *dest;
|
us_frame_s *dest;
|
||||||
us_m2m_encoder_s *enc;
|
us_m2m_encoder_s *enc;
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ us_server_s *us_server_init(us_stream_s *stream) {
|
|||||||
server->passwd = "";
|
server->passwd = "";
|
||||||
server->static_path = "";
|
server->static_path = "";
|
||||||
server->allow_origin = "";
|
server->allow_origin = "";
|
||||||
|
server->instance_id = "";
|
||||||
server->timeout = 10;
|
server->timeout = 10;
|
||||||
server->run = run;
|
server->run = run;
|
||||||
|
|
||||||
@@ -400,7 +401,9 @@ static void _http_callback_state(struct evhttp_request *request, void *v_server)
|
|||||||
|
|
||||||
_A_EVBUFFER_ADD_PRINTF(buf,
|
_A_EVBUFFER_ADD_PRINTF(buf,
|
||||||
"{\"ok\": true, \"result\": {"
|
"{\"ok\": true, \"result\": {"
|
||||||
|
" \"instance_id\": \"%s\","
|
||||||
" \"encoder\": {\"type\": \"%s\", \"quality\": %u},",
|
" \"encoder\": {\"type\": \"%s\", \"quality\": %u},",
|
||||||
|
server->instance_id,
|
||||||
us_encoder_type_to_string(enc_type),
|
us_encoder_type_to_string(enc_type),
|
||||||
enc_quality
|
enc_quality
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ typedef struct us_server_sx {
|
|||||||
char *passwd;
|
char *passwd;
|
||||||
char *static_path;
|
char *static_path;
|
||||||
char *allow_origin;
|
char *allow_origin;
|
||||||
|
char *instance_id;
|
||||||
|
|
||||||
unsigned drop_same_frames;
|
unsigned drop_same_frames;
|
||||||
unsigned fake_width;
|
unsigned fake_width;
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ static void _m2m_encoder_prepare(us_m2m_encoder_s *enc, const us_frame_s *frame)
|
|||||||
SET_OPTION(V4L2_CID_MPEG_VIDEO_H264_LEVEL, V4L2_MPEG_VIDEO_H264_LEVEL_5_1);
|
SET_OPTION(V4L2_CID_MPEG_VIDEO_H264_LEVEL, V4L2_MPEG_VIDEO_H264_LEVEL_5_1);
|
||||||
}
|
}
|
||||||
SET_OPTION(V4L2_CID_MPEG_VIDEO_REPEAT_SEQ_HEADER, 1);
|
SET_OPTION(V4L2_CID_MPEG_VIDEO_REPEAT_SEQ_HEADER, 1);
|
||||||
SET_OPTION(V4L2_CID_MPEG_VIDEO_H264_MIN_QP, 20);
|
SET_OPTION(V4L2_CID_MPEG_VIDEO_H264_MIN_QP, 16);
|
||||||
SET_OPTION(V4L2_CID_MPEG_VIDEO_H264_MAX_QP, 32);
|
SET_OPTION(V4L2_CID_MPEG_VIDEO_H264_MAX_QP, 32);
|
||||||
} else if (enc->output_format == V4L2_PIX_FMT_MJPEG) {
|
} else if (enc->output_format == V4L2_PIX_FMT_MJPEG) {
|
||||||
SET_OPTION(V4L2_CID_MPEG_VIDEO_BITRATE, enc->bitrate);
|
SET_OPTION(V4L2_CID_MPEG_VIDEO_BITRATE, enc->bitrate);
|
||||||
@@ -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.colorspace = V4L2_COLORSPACE_DEFAULT;
|
||||||
fmt.fmt.pix_mp.num_planes = 1;
|
fmt.fmt.pix_mp.num_planes = 1;
|
||||||
// fmt.fmt.pix_mp.plane_fmt[0].bytesperline = 0;
|
// 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_LOG_DEBUG("Configuring OUTPUT format ...");
|
||||||
_E_XIOCTL(VIDIOC_S_FMT, &fmt, "Can't set OUTPUT format");
|
_E_XIOCTL(VIDIOC_S_FMT, &fmt, "Can't set OUTPUT format");
|
||||||
if (fmt.fmt.pix_mp.pixelformat != enc->output_format) {
|
if (fmt.fmt.pix_mp.pixelformat != enc->output_format) {
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ enum _US_OPT_VALUES {
|
|||||||
_O_PASSWD,
|
_O_PASSWD,
|
||||||
_O_STATIC,
|
_O_STATIC,
|
||||||
_O_ALLOW_ORIGIN,
|
_O_ALLOW_ORIGIN,
|
||||||
|
_O_INSTANCE_ID,
|
||||||
_O_TCP_NODELAY,
|
_O_TCP_NODELAY,
|
||||||
_O_SERVER_TIMEOUT,
|
_O_SERVER_TIMEOUT,
|
||||||
|
|
||||||
@@ -177,6 +178,7 @@ static const struct option _LONG_OPTS[] = {
|
|||||||
{"static", required_argument, NULL, _O_STATIC},
|
{"static", required_argument, NULL, _O_STATIC},
|
||||||
{"drop-same-frames", required_argument, NULL, _O_DROP_SAME_FRAMES},
|
{"drop-same-frames", required_argument, NULL, _O_DROP_SAME_FRAMES},
|
||||||
{"allow-origin", required_argument, NULL, _O_ALLOW_ORIGIN},
|
{"allow-origin", required_argument, NULL, _O_ALLOW_ORIGIN},
|
||||||
|
{"instance-id", required_argument, NULL, _O_INSTANCE_ID},
|
||||||
{"fake-resolution", required_argument, NULL, _O_FAKE_RESOLUTION},
|
{"fake-resolution", required_argument, NULL, _O_FAKE_RESOLUTION},
|
||||||
{"tcp-nodelay", no_argument, NULL, _O_TCP_NODELAY},
|
{"tcp-nodelay", no_argument, NULL, _O_TCP_NODELAY},
|
||||||
{"server-timeout", required_argument, NULL, _O_SERVER_TIMEOUT},
|
{"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 _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 _features(void);
|
||||||
static void _help(FILE *fp, us_device_s *dev, us_encoder_s *enc, us_stream_s *stream, us_server_s *server);
|
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_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_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_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_TCP_NODELAY: OPT_SET(server->tcp_nodelay, true);
|
||||||
case _O_SERVER_TIMEOUT: OPT_NUMBER("--server-timeout", server->timeout, 1, 60, 0);
|
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);
|
options->blank = us_blank_frame_init(blank_path);
|
||||||
stream->blank = options->blank;
|
stream->blank = options->blank;
|
||||||
|
|
||||||
@@ -530,6 +542,18 @@ static int _parse_resolution(const char *str, unsigned *width, unsigned *height,
|
|||||||
return 0;
|
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) {
|
static void _features(void) {
|
||||||
# ifdef WITH_GPIO
|
# ifdef WITH_GPIO
|
||||||
puts("+ 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(" --tcp-nodelay ────────────── Set TCP_NODELAY flag to the client /stream socket. Only for TCP socket.");
|
||||||
SAY(" Default: disabled.\n");
|
SAY(" Default: disabled.\n");
|
||||||
SAY(" --allow-origin <str> ─────── Set Access-Control-Allow-Origin header. 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);
|
SAY(" --server-timeout <sec> ───── Timeout for client connections. Default: %u.\n", server->timeout);
|
||||||
# define ADD_SINK(x_name, x_opt) \
|
# define ADD_SINK(x_name, x_opt) \
|
||||||
SAY(x_name " sink options:"); \
|
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) { \
|
#define _SINK_PUT(x_sink, x_frame) { \
|
||||||
if (stream->x_sink && us_memsink_server_check(stream->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); \
|
||||||
} \
|
} \
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user