Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add native support for the Raspberry Pi camera #1057

Merged
merged 1 commit into from
Aug 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# do not add .git, since it is needed to extract the tag

/tmp
/release
/coverage*.txt
/apidocs/*.html
86 changes: 52 additions & 34 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
BASE_IMAGE = golang:1.18-alpine3.15
LINT_IMAGE = golangci/golangci-lint:v1.45.2
NODE_IMAGE = node:16-alpine3.15
RPI32_IMAGE = balenalib/raspberrypi3:buster-run
RPI64_IMAGE = balenalib/raspberrypi3-64:buster-run

.PHONY: $(shell ls)

Expand Down Expand Up @@ -162,17 +164,6 @@ apidocs-lint:
docker run --rm -v $(PWD)/apidocs:/s -w /s temp \
sh -c "openapi lint openapi.yaml"

define DOCKERFILE_RELEASE
FROM $(BASE_IMAGE)
RUN apk add --no-cache zip make git tar
WORKDIR /s
COPY go.mod go.sum ./
RUN go mod download
COPY . ./
RUN make release-nodocker
endef
export DOCKERFILE_RELEASE

define DOCKERFILE_APIDOCS_GEN
FROM $(NODE_IMAGE)
RUN yarn global add redoc-cli@0.13.7
Expand All @@ -184,36 +175,63 @@ apidocs-gen:
docker run --rm -v $(PWD)/apidocs:/s -w /s temp \
sh -c "redoc-cli bundle openapi.yaml"

release:
echo "$$DOCKERFILE_RELEASE" | docker build . -f - -t temp
docker run --rm -v $(PWD):/out \
temp sh -c "rm -rf /out/release && cp -r /s/release /out/"
define DOCKERFILE_RELEASE
FROM $(RPI32_IMAGE) AS rpicamera32
RUN ["cross-build-start"]
RUN apt update && apt install -y g++ pkg-config make libcamera-dev
WORKDIR /s/internal/rpicamera
COPY internal/rpicamera .
RUN cd exe && make

FROM $(RPI64_IMAGE) AS rpicamera64
RUN ["cross-build-start"]
RUN apt update && apt install -y g++ pkg-config make libcamera-dev
WORKDIR /s/internal/rpicamera
COPY internal/rpicamera .
RUN cd exe && make

release-nodocker:
$(eval export CGO_ENABLED=0)
$(eval VERSION := $(shell git describe --tags))
$(eval GOBUILD := go build -ldflags '-X github.com/aler9/rtsp-simple-server/internal/core.version=$(VERSION)')
rm -rf tmp && mkdir tmp
rm -rf release && mkdir release
cp rtsp-simple-server.yml tmp/
FROM $(BASE_IMAGE)
RUN apk add --no-cache zip make git tar
WORKDIR /s
COPY go.mod go.sum ./
RUN go mod download
COPY . ./

GOOS=windows GOARCH=amd64 $(GOBUILD) -o tmp/rtsp-simple-server.exe
cd tmp && zip -q $(PWD)/release/rtsp-simple-server_$(VERSION)_windows_amd64.zip rtsp-simple-server.exe rtsp-simple-server.yml
ENV VERSION $(shell git describe --tags)
ENV CGO_ENABLED 0
RUN mkdir tmp release
RUN cp rtsp-simple-server.yml tmp/

GOOS=linux GOARCH=amd64 $(GOBUILD) -o tmp/rtsp-simple-server
tar -C tmp -czf $(PWD)/release/rtsp-simple-server_$(VERSION)_linux_amd64.tar.gz --owner=0 --group=0 rtsp-simple-server rtsp-simple-server.yml
RUN GOOS=windows GOARCH=amd64 go build -ldflags "-X github.com/aler9/rtsp-simple-server/internal/core.version=$$VERSION" -o tmp/rtsp-simple-server.exe
RUN cd tmp && zip -q ../release/rtsp-simple-server_$${VERSION}_windows_amd64.zip rtsp-simple-server.exe rtsp-simple-server.yml

GOOS=linux GOARCH=arm GOARM=6 $(GOBUILD) -o tmp/rtsp-simple-server
tar -C tmp -czf $(PWD)/release/rtsp-simple-server_$(VERSION)_linux_armv6.tar.gz --owner=0 --group=0 rtsp-simple-server rtsp-simple-server.yml
RUN GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/aler9/rtsp-simple-server/internal/core.version=$$VERSION" -o tmp/rtsp-simple-server
RUN tar -C tmp -czf release/rtsp-simple-server_$${VERSION}_linux_amd64.tar.gz --owner=0 --group=0 rtsp-simple-server rtsp-simple-server.yml

GOOS=linux GOARCH=arm GOARM=7 $(GOBUILD) -o tmp/rtsp-simple-server
tar -C tmp -czf $(PWD)/release/rtsp-simple-server_$(VERSION)_linux_armv7.tar.gz --owner=0 --group=0 rtsp-simple-server rtsp-simple-server.yml
RUN GOOS=darwin GOARCH=amd64 go build -ldflags "-X github.com/aler9/rtsp-simple-server/internal/core.version=$$VERSION" -o tmp/rtsp-simple-server
RUN tar -C tmp -czf release/rtsp-simple-server_$${VERSION}_darwin_amd64.tar.gz --owner=0 --group=0 rtsp-simple-server rtsp-simple-server.yml

GOOS=linux GOARCH=arm64 $(GOBUILD) -o tmp/rtsp-simple-server
tar -C tmp -czf $(PWD)/release/rtsp-simple-server_$(VERSION)_linux_arm64v8.tar.gz --owner=0 --group=0 rtsp-simple-server rtsp-simple-server.yml
COPY --from=rpicamera32 /s/internal/rpicamera/exe/exe internal/rpicamera/exe/
RUN GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "-X github.com/aler9/rtsp-simple-server/internal/core.version=$$VERSION" -o tmp/rtsp-simple-server -tags rpicamera
RUN tar -C tmp -czf release/rtsp-simple-server_$${VERSION}_linux_armv6.tar.gz --owner=0 --group=0 rtsp-simple-server rtsp-simple-server.yml
RUN rm internal/rpicamera/exe/exe

GOOS=darwin GOARCH=amd64 $(GOBUILD) -o tmp/rtsp-simple-server
tar -C tmp -czf $(PWD)/release/rtsp-simple-server_$(VERSION)_darwin_amd64.tar.gz --owner=0 --group=0 rtsp-simple-server rtsp-simple-server.yml
COPY --from=rpicamera32 /s/internal/rpicamera/exe/exe internal/rpicamera/exe/
RUN GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "-X github.com/aler9/rtsp-simple-server/internal/core.version=$$VERSION" -o tmp/rtsp-simple-server -tags rpicamera
RUN tar -C tmp -czf release/rtsp-simple-server_$${VERSION}_linux_armv7.tar.gz --owner=0 --group=0 rtsp-simple-server rtsp-simple-server.yml
RUN rm internal/rpicamera/exe/exe

COPY --from=rpicamera64 /s/internal/rpicamera/exe/exe internal/rpicamera/exe/
RUN GOOS=linux GOARCH=arm64 go build -ldflags "-X github.com/aler9/rtsp-simple-server/internal/core.version=$$VERSION" -o tmp/rtsp-simple-server -tags rpicamera
RUN tar -C tmp -czf release/rtsp-simple-server_$${VERSION}_linux_arm64v8.tar.gz --owner=0 --group=0 rtsp-simple-server rtsp-simple-server.yml
RUN rm internal/rpicamera/exe/exe
endef
export DOCKERFILE_RELEASE

release:
echo "$$DOCKERFILE_RELEASE" | DOCKER_BUILDKIT=1 docker build . -f - -t temp
docker run --rm -v $(PWD):/out \
temp sh -c "rm -rf /out/release && cp -r /s/release /out/"

define DOCKERFILE_DOCKERHUB
FROM --platform=linux/amd64 $(BASE_IMAGE) AS build
Expand Down
36 changes: 19 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ Features:

* Publish live streams to the server
* Read live streams from the server
* Act as a proxy and serve streams from other servers or cameras, always or on-demand
* Each stream can have multiple video and audio tracks, encoded with any codec, including H264, H265, VP8, VP9, MPEG2, MP3, AAC, Opus, PCM, JPEG
* Proxy streams from other servers or cameras, always or on-demand
* Each stream can have multiple video and audio tracks, encoded with any RTP-compatible codec, including H264, H265, VP8, VP9, MPEG2, MP3, AAC, Opus, PCM, JPEG
* Streams are automatically converted from a protocol to another. For instance, it's possible to publish a stream with RTSP and read it with HLS
* Serve multiple streams at once in separate paths
* Authenticate users; use internal or external authentication
* Redirect readers to other RTSP servers (load balancing)
* Query and control the server through an HTTP API
* Reload the configuration without disconnecting existing clients (hot reloading)
* Read Prometheus-compatible metrics
* Redirect readers to other RTSP servers (load balancing)
* Run external commands when clients connect, disconnect, read or publish streams
* Reload the configuration without disconnecting existing clients (hot reloading)
* Natively compatible with the Raspberry Pi Camera
* Compatible with Linux, Windows and macOS, does not require any dependency or interpreter, it's a single executable

[![Test](https://github.com/aler9/rtsp-simple-server/workflows/test/badge.svg)](https://github.com/aler9/rtsp-simple-server/actions?query=workflow:test)
Expand Down Expand Up @@ -361,7 +362,7 @@ The command inserted into `runOnDemand` will start only when a client requests t

#### Linux

Systemd is the service manager used by Ubuntu, Debian and many other Linux distributions, and allows to launch rtsp-simple-server on boot.
Systemd is the service manager used by Ubuntu, Debian and many other Linux distributions, and allows to launch _rtsp-simple-server_ on boot.

Download a release bundle from the [release page](https://github.com/aler9/rtsp-simple-server/releases), unzip it, and move the executable and configuration in the system:

Expand Down Expand Up @@ -523,26 +524,27 @@ After starting the server, the webcam can be reached on `rtsp://localhost:8554/c

### From a Raspberry Pi Camera

To publish the video stream of a Raspberry Pi Camera to the server, install a couple of dependencies:
_rtsp-simple-server_ natively support the Raspberry Pi Camera, enabling high-quality and low-latency video streaming from the camera to any user. To make the video stream of a Raspberry Pi Camera available on the server:

1. The server must be installed on a Raspberry Pi, with Raspberry Pi OS bullseye or newer as operative system, and must be installed by using the standard method (Docker is not actually supported). If you're using the 64-bit version of the operative system, you need to pick the `arm64` variant of the server.

1. _GStreamer_ and _h264parse_:
2. Make sure that the legacy camera stack is disabled. Type:

```
sudo apt install -y gstreamer1.0-tools gstreamer1.0-rtsp gstreamer1.0-plugins-bad
sudo raspi-config
```

2. _gst-rpicamsrc_, by following [instruction here](https://github.com/thaytan/gst-rpicamsrc)
Then go to `Interfacing options`, `enable/disable legacy camera support`, choose `no`. Reboot the system.

Then edit `rtsp-simple-server.yml` and replace everything inside section `paths` with the following content:
3. edit `rtsp-simple-server.yml` and replace everything inside section `paths` with the following content:

```yml
paths:
cam:
runOnInit: gst-launch-1.0 rpicamsrc preview=false bitrate=2000000 keyframe-interval=50 ! video/x-h264,width=1920,height=1080,framerate=25/1 ! h264parse ! rtspclientsink location=rtsp://localhost:$RTSP_PORT/$RTSP_PATH
runOnInitRestart: yes
```
```yml
paths:
cam:
source: rpiCamera
```

After starting the server, the camera is available on `rtsp://localhost:8554/cam`.
After starting the server, the camera can be reached on `rtsp://raspberry-pi:8554/cam` or `http://raspberry-pi:8888/cam`.

### From OBS Studio

Expand Down
8 changes: 8 additions & 0 deletions apidocs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ components:
- $ref: '#/components/schemas/PathSourceRTSPSource'
- $ref: '#/components/schemas/PathSourceRTMPSource'
- $ref: '#/components/schemas/PathSourceHLSSource'
- $ref: '#/components/schemas/PathSourceRPICameraSource'
sourceReady:
type: boolean
readers:
Expand Down Expand Up @@ -258,6 +259,13 @@ components:
type: string
enum: [hlsSource]

PathSourceRPICameraSource:
type: object
properties:
type:
type: string
enum: [rpiCameraSource]

PathReaderRTSPSession:
type: object
properties:
Expand Down
13 changes: 13 additions & 0 deletions internal/conf/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,17 @@ import (
"github.com/stretchr/testify/require"
)

type subStruct struct {
// int
MyParam int
}

type mapEntry struct {
// string
MyValue string

// struct
MyStruct subStruct
}

type testStruct struct {
Expand Down Expand Up @@ -48,6 +57,9 @@ func TestEnvironment(t *testing.T) {
os.Setenv("MYPREFIX_MYMAP_MYKEY2_MYVALUE", "asd")
defer os.Unsetenv("MYPREFIX_MYMAP_MYKEY2_MYVALUE")

os.Setenv("MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM", "456")
defer os.Unsetenv("MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM")

var s testStruct
err := loadFromEnvironment("MYPREFIX", &s)
require.NoError(t, err)
Expand All @@ -63,4 +75,5 @@ func TestEnvironment(t *testing.T) {
v, ok := s.MyMap["mykey2"]
require.Equal(t, true, ok)
require.Equal(t, "asd", v.MyValue)
require.Equal(t, 456, v.MyStruct.MyParam)
}
31 changes: 31 additions & 0 deletions internal/conf/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ type PathConf struct {
SourceRedirect string `json:"sourceRedirect"`
DisablePublisherOverride bool `json:"disablePublisherOverride"`
Fallback string `json:"fallback"`
RPICameraCamID int `json:"rpiCameraCamID"`
RPICameraWidth int `json:"rpiCameraWidth"`
RPICameraHeight int `json:"rpiCameraHeight"`
RPICameraFPS int `json:"rpiCameraFPS"`
RPICameraIDRPeriod int `json:"rpiCameraIDRPeriod"`
RPICameraBitrate int `json:"rpiCameraBitrate"`
RPICameraProfile string `json:"rpiCameraProfile"`
RPICameraLevel string `json:"rpiCameraLevel"`

// authentication
PublishUser Credential `json:"publishUser"`
Expand Down Expand Up @@ -165,6 +173,29 @@ func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error {
return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.SourceRedirect)
}

case pconf.Source == "rpiCamera":
if pconf.RPICameraWidth == 0 {
pconf.RPICameraWidth = 1280
}
if pconf.RPICameraHeight == 0 {
pconf.RPICameraHeight = 720
}
if pconf.RPICameraFPS == 0 {
pconf.RPICameraFPS = 30
}
if pconf.RPICameraIDRPeriod == 0 {
pconf.RPICameraIDRPeriod = 60
}
if pconf.RPICameraBitrate == 0 {
pconf.RPICameraBitrate = 1000000
}
if pconf.RPICameraProfile == "" {
pconf.RPICameraProfile = "main"
}
if pconf.RPICameraLevel == "" {
pconf.RPICameraLevel = "4.1"
}

default:
return fmt.Errorf("invalid source: '%s'", pconf.Source)
}
Expand Down
8 changes: 8 additions & 0 deletions internal/core/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,14 @@ func loadConfPathData(ctx *gin.Context) (interface{}, error) {
SourceRedirect *string `json:"sourceRedirect"`
DisablePublisherOverride *bool `json:"disablePublisherOverride"`
Fallback *string `json:"fallback"`
RPICameraCamID *int `json:"rpiCameraCamID"`
RPICameraWidth *int `json:"rpiCameraWidth"`
RPICameraHeight *int `json:"rpiCameraHeight"`
RPICameraFPS *int `json:"rpiCameraFPS"`
RPICameraIDRPeriod *int `json:"rpiCameraIDRPeriod"`
RPICameraBitrate *int `json:"rpiCameraBitrate"`
RPICameraProfile *string `json:"rpiCameraProfile"`
RPICameraLevel *string `json:"rpiCameraLevel"`

// authentication
PublishUser *conf.Credential `json:"publishUser"`
Expand Down
2 changes: 1 addition & 1 deletion internal/core/hls_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (s *hlsSource) run(ctx context.Context) error {
return res.err
}

s.Log(logger.Info, "proxying %s", sourceTrackInfo(tracks))
s.Log(logger.Info, "ready: %s", sourceTrackInfo(tracks))
stream = res.stream

return nil
Expand Down
8 changes: 3 additions & 5 deletions internal/core/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,8 @@ func (pa *path) hasStaticSource() bool {
strings.HasPrefix(pa.conf.Source, "rtsps://") ||
strings.HasPrefix(pa.conf.Source, "rtmp://") ||
strings.HasPrefix(pa.conf.Source, "http://") ||
strings.HasPrefix(pa.conf.Source, "https://")
strings.HasPrefix(pa.conf.Source, "https://") ||
pa.conf.Source == "rpiCamera"
}

func (pa *path) hasOnDemandStaticSource() bool {
Expand All @@ -353,10 +354,7 @@ func (pa *path) run() {
pa.source = &sourceRedirect{}
} else if pa.hasStaticSource() {
pa.source = newSourceStatic(
pa.conf.Source,
pa.conf.SourceProtocol,
pa.conf.SourceAnyPortEnable,
pa.conf.SourceFingerprint,
pa.conf,
pa.readTimeout,
pa.writeTimeout,
pa.readBufferCount,
Expand Down