Skip to content

Commit

Permalink
Add native support for the raspberry pi camera
Browse files Browse the repository at this point in the history
  • Loading branch information
aler9 committed Aug 11, 2022
1 parent a35cc53 commit 3638b27
Show file tree
Hide file tree
Showing 15 changed files with 948 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
/release
/coverage*.txt
/apidocs/*.html
/rtsp-simple-server-rpi
/rtsp-simple-server-rpi64
92 changes: 92 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,95 @@ dockerhub:

docker buildx rm builder
rm -rf $$HOME/.docker/manifests/*

define DOCKERFILE_RPI
FROM balenalib/raspberrypi3:buster-run AS camera

RUN ["cross-build-start"]

RUN apt update && apt install -y g++ libraspberrypi-dev libcamera-dev

WORKDIR /s/internal/rpicamera

COPY internal/rpicamera/*.cpp internal/rpicamera/*.c ./

RUN gcc \
rpicamera_legacy.c \
-o rpicamera_legacy \
-Ofast \
-Werror \
-Wall \
-Wextra \
-Wno-unused-parameter \
-I/opt/vc/include \
-L/opt/vc/lib -lmmal_core -lmmal_util -lmmal_vc_client -lvcsm -lvcos -lvchiq_arm

RUN g++ \
rpicamera_libcamera.cpp \
-o rpicamera_libcamera \
-Ofast \
-Werror \
-Wall \
-Wextra \
-Wno-unused-parameter \
$(pkg-config --cxxflags --libs libcamera)

FROM $(BASE_IMAGE)

WORKDIR /s

COPY go.mod go.sum ./
RUN go mod download

COPY --from=camera /s/internal/rpicamera/rpicamera_legacy /s/internal/rpicamera/
COPY --from=camera /s/internal/rpicamera/rpicamera_libcamera /s/internal/rpicamera/

COPY . .

RUN GOOS=linux GOARCH=arm GOARM=6 go build -tags rpilibcamera -o /out
endef
export DOCKERFILE_RPI

rpi:
echo "$$DOCKERFILE_RPI" | docker build . -f - -t temp
docker run --rm -it -v $(PWD):/o temp sh -c "mv /out /o/rtsp-simple-server-rpi"

define DOCKERFILE_RPI64
FROM balenalib/raspberrypi3-64:buster-run AS camera

RUN ["cross-build-start"]

RUN apt update && apt install -y g++ libcamera-dev

WORKDIR /s/internal/rpicamera

COPY internal/rpicamera/*.cpp ./

RUN g++ \
rpicamera_libcamera.cpp \
-o rpicamera_libcamera \
-Ofast \
-Werror \
-Wall \
-Wextra \
-Wno-unused-parameter \
$(pkg-config --cxxflags --libs libcamera)

FROM $(BASE_IMAGE)

WORKDIR /s

COPY go.mod go.sum ./
RUN go mod download

COPY --from=camera /s/internal/rpicamera/rpicamera_libcamera /s/internal/rpicamera/

COPY . .

RUN GOOS=linux GOARCH=arm64 go build -tags rpilibcamera -o /out
endef
export DOCKERFILE_RPI64

rpi64:
echo "$$DOCKERFILE_RPI64" | docker build . -f - -t temp
docker run --rm -it -v $(PWD):/o temp sh -c "mv /out /o/rtsp-simple-server-rpi64"
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
2 changes: 2 additions & 0 deletions internal/conf/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ 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":

default:
return fmt.Errorf("invalid source: '%s'", pconf.Source)
}
Expand Down
3 changes: 2 additions & 1 deletion internal/core/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,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 Down
107 changes: 107 additions & 0 deletions internal/core/rpicamera_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package core

import (
"context"
"time"

"github.com/aler9/gortsplib"
"github.com/aler9/gortsplib/pkg/h264"
"github.com/aler9/gortsplib/pkg/rtph264"

"github.com/aler9/rtsp-simple-server/internal/logger"
"github.com/aler9/rtsp-simple-server/internal/rpicamera"
)

type rpiCameraSourceParent interface {
log(logger.Level, string, ...interface{})
sourceStaticImplSetReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes
sourceStaticImplSetNotReady(req pathSourceStaticSetNotReadyReq)
}

type rpiCameraSource struct {
parent rpiCameraSourceParent
}

func newRPICameraSource(
parent rpiCameraSourceParent,
) *rpiCameraSource {
return &rpiCameraSource{
parent: parent,
}
}

func (s *rpiCameraSource) Log(level logger.Level, format string, args ...interface{}) {
s.parent.log(level, "[rpicamera source] "+format, args...)
}

// run implements sourceStaticImpl.
func (s *rpiCameraSource) run(ctx context.Context) error {
track := &gortsplib.TrackH264{PayloadType: 96}
enc := &rtph264.Encoder{PayloadType: 96}
enc.Init()
var stream *stream
var start time.Time

onData := func(nalus [][]byte) {
if stream == nil {
res := s.parent.sourceStaticImplSetReady(pathSourceStaticSetReadyReq{
tracks: gortsplib.Tracks{track},
})
if res.err != nil {
return
}

s.Log(logger.Info, "ready")
stream = res.stream
start = time.Now()
}

pts := time.Since(start)

pkts, err := enc.Encode(nalus, pts)
if err != nil {
return
}

lastPkt := len(pkts) - 1
for i, pkt := range pkts {
if i != lastPkt {
stream.writeData(&data{
trackID: 0,
rtp: pkt,
ptsEqualsDTS: false,
})
} else {
stream.writeData(&data{
trackID: 0,
rtp: pkt,
ptsEqualsDTS: h264.IDRPresent(nalus),
h264NALUs: nalus,
h264PTS: pts,
})
}
}
}

cam, err := rpicamera.New(onData)
if err != nil {
return err
}
defer cam.Close()

defer func() {
if stream != nil {
s.parent.sourceStaticImplSetNotReady(pathSourceStaticSetNotReadyReq{})
}
}()

<-ctx.Done()
return nil
}

// apiSourceDescribe implements sourceStaticImpl.
func (*rpiCameraSource) apiSourceDescribe() interface{} {
return struct {
Type string `json:"type"`
}{"rpiCameraSource"}
}
4 changes: 4 additions & 0 deletions internal/core/source_static.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ func newSourceStatic(
s.ur,
s.fingerprint,
s)

case s.ur == "rpicamera":
s.impl = newRPICameraSource(
s)
}

return s
Expand Down
80 changes: 80 additions & 0 deletions internal/rpicamera/embeddedexecutable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//go:build rpilibcamera || rpilegacy
// +build rpilibcamera rpilegacy

package rpicamera

import (
"fmt"
"os"
"os/exec"
"runtime"
)

const (
embeddedExecutableTempPath = "/dev/shm/rtspss-embeddedbin"
)

func getKernelArch() (string, error) {
cmd := exec.Command("uname", "-m")

byts, err := cmd.Output()
if err != nil {
return "", err
}

return string(byts[:len(byts)-1]), nil
}

// 32-bit embedded binaries can't run on 64-bit.
func checkArch() error {
if runtime.GOARCH != "arm" {
return nil
}

arch, err := getKernelArch()
if err != nil {
return err
}

if arch == "aarch64" {
return fmt.Errorf("you need the arm64 version")
}

return nil
}

type embeddedExecutable struct {
cmd *exec.Cmd
}

func newEmbeddedExecutable(content []byte, arg string) (*embeddedExecutable, error) {
err := checkArch()
if err != nil {
return nil, err
}

err = os.WriteFile(embeddedExecutableTempPath, content, 0o755)
if err != nil {
return nil, err
}

cmd := exec.Command(embeddedExecutableTempPath, arg)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err = cmd.Start()
if err != nil {
os.Remove(embeddedExecutableTempPath)
return nil, err
}

return &embeddedExecutable{
cmd: cmd,
}, nil
}

func (e *embeddedExecutable) close() {
e.cmd.Process.Kill()
e.cmd.Wait()
os.Remove(embeddedExecutableTempPath)
}

0 comments on commit 3638b27

Please sign in to comment.