Browse Source

rpicamera: support changing parameters without interrupting the stream (#1463)

pull/1468/head
Alessandro Ros 4 years ago committed by GitHub
parent
commit
b461f15a4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      internal/conf/path.go
  2. 2
      internal/core/hls_muxer.go
  3. 6
      internal/core/hls_server.go
  4. 17
      internal/core/hls_source.go
  5. 54
      internal/core/path.go
  6. 89
      internal/core/path_manager.go
  7. 51
      internal/core/rpicamera_source.go
  8. 12
      internal/core/rtmp_conn.go
  9. 16
      internal/core/rtmp_source.go
  10. 12
      internal/core/rtsp_session.go
  11. 28
      internal/core/rtsp_source.go
  12. 124
      internal/core/source_static.go
  13. 2
      internal/core/webrtc_conn.go
  14. 2
      internal/core/webrtc_server.go
  15. 49
      internal/rpicamera/embeddedexe.go
  16. 1
      internal/rpicamera/exe/Makefile
  17. 173
      internal/rpicamera/exe/camera.cpp
  18. 3
      internal/rpicamera/exe/camera.h
  19. 4
      internal/rpicamera/exe/encoder.c
  20. 2
      internal/rpicamera/exe/encoder.h
  21. 110
      internal/rpicamera/exe/main.c
  22. 155
      internal/rpicamera/exe/parameters.c
  23. 20
      internal/rpicamera/exe/parameters.h
  24. 44
      internal/rpicamera/exe/pipe.c
  25. 12
      internal/rpicamera/exe/pipe.h
  26. 15
      internal/rpicamera/exe/sensor_mode.c
  27. 2
      internal/rpicamera/exe/sensor_mode.h
  28. 12
      internal/rpicamera/exe/window.c
  29. 2
      internal/rpicamera/exe/window.h
  30. 20
      internal/rpicamera/pipe.go
  31. 234
      internal/rpicamera/rpicamera.go
  32. 4
      internal/rpicamera/rpicamera_disabled.go
  33. 4
      rtsp-simple-server.yml

6
internal/conf/path.go

@ -1,9 +1,9 @@
package conf package conf
import ( import (
"encoding/json"
"fmt" "fmt"
gourl "net/url" gourl "net/url"
"reflect"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -320,7 +320,5 @@ func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error {
// Equal checks whether two PathConfs are equal. // Equal checks whether two PathConfs are equal.
func (pconf *PathConf) Equal(other *PathConf) bool { func (pconf *PathConf) Equal(other *PathConf) bool {
a, _ := json.Marshal(pconf) return reflect.DeepEqual(pconf, other)
b, _ := json.Marshal(other)
return string(a) == string(b)
} }

2
internal/core/hls_muxer.go

@ -555,7 +555,7 @@ func (m *hlsMuxer) handleRequest(req *hlsMuxerRequest) func() *hls.MuxerFileResp
} }
func (m *hlsMuxer) authenticate(ctx *gin.Context) error { func (m *hlsMuxer) authenticate(ctx *gin.Context) error {
pathConf := m.path.Conf() pathConf := m.path.safeConf()
pathIPs := pathConf.ReadIPs pathIPs := pathConf.ReadIPs
pathUser := pathConf.ReadUser pathUser := pathConf.ReadUser
pathPass := pathConf.ReadPass pathPass := pathConf.ReadPass

6
internal/core/hls_server.go

@ -204,15 +204,15 @@ outer:
select { select {
case pa := <-s.chPathSourceReady: case pa := <-s.chPathSourceReady:
if s.alwaysRemux { if s.alwaysRemux {
s.createMuxer(pa.Name(), "", nil) s.createMuxer(pa.name, "", nil)
} }
case pa := <-s.chPathSourceNotReady: case pa := <-s.chPathSourceNotReady:
if s.alwaysRemux { if s.alwaysRemux {
c, ok := s.muxers[pa.Name()] c, ok := s.muxers[pa.name]
if ok { if ok {
c.close() c.close()
delete(s.muxers, pa.Name()) delete(s.muxers, pa.name)
} }
} }

17
internal/core/hls_source.go

@ -7,6 +7,7 @@ import (
"github.com/aler9/gortsplib/v2/pkg/format" "github.com/aler9/gortsplib/v2/pkg/format"
"github.com/aler9/gortsplib/v2/pkg/media" "github.com/aler9/gortsplib/v2/pkg/media"
"github.com/aler9/rtsp-simple-server/internal/conf"
"github.com/aler9/rtsp-simple-server/internal/formatprocessor" "github.com/aler9/rtsp-simple-server/internal/formatprocessor"
"github.com/aler9/rtsp-simple-server/internal/hls" "github.com/aler9/rtsp-simple-server/internal/hls"
"github.com/aler9/rtsp-simple-server/internal/logger" "github.com/aler9/rtsp-simple-server/internal/logger"
@ -19,19 +20,13 @@ type hlsSourceParent interface {
} }
type hlsSource struct { type hlsSource struct {
ur string
fingerprint string
parent hlsSourceParent parent hlsSourceParent
} }
func newHLSSource( func newHLSSource(
ur string,
fingerprint string,
parent hlsSourceParent, parent hlsSourceParent,
) *hlsSource { ) *hlsSource {
return &hlsSource{ return &hlsSource{
ur: ur,
fingerprint: fingerprint,
parent: parent, parent: parent,
} }
} }
@ -41,7 +36,7 @@ func (s *hlsSource) Log(level logger.Level, format string, args ...interface{})
} }
// run implements sourceStaticImpl. // run implements sourceStaticImpl.
func (s *hlsSource) run(ctx context.Context) error { func (s *hlsSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan *conf.PathConf) error {
var stream *stream var stream *stream
defer func() { defer func() {
@ -51,8 +46,8 @@ func (s *hlsSource) run(ctx context.Context) error {
}() }()
c, err := hls.NewClient( c, err := hls.NewClient(
s.ur, cnf.Source,
s.fingerprint, cnf.SourceFingerprint,
s, s,
) )
if err != nil { if err != nil {
@ -137,16 +132,20 @@ func (s *hlsSource) run(ctx context.Context) error {
c.Start() c.Start()
for {
select { select {
case err := <-c.Wait(): case err := <-c.Wait():
return err return err
case <-reloadConf:
case <-ctx.Done(): case <-ctx.Done():
c.Close() c.Close()
<-c.Wait() <-c.Wait()
return nil return nil
} }
} }
}
// apiSourceDescribe implements sourceStaticImpl. // apiSourceDescribe implements sourceStaticImpl.
func (*hlsSource) apiSourceDescribe() interface{} { func (*hlsSource) apiSourceDescribe() interface{} {

54
internal/core/path.go

@ -201,6 +201,7 @@ type path struct {
ctx context.Context ctx context.Context
ctxCancel func() ctxCancel func()
confMutex sync.RWMutex
source source source source
bytesReceived *uint64 bytesReceived *uint64
stream *stream stream *stream
@ -217,6 +218,7 @@ type path struct {
onDemandPublisherCloseTimer *time.Timer onDemandPublisherCloseTimer *time.Timer
// in // in
chReloadConf chan *conf.PathConf
chSourceStaticSetReady chan pathSourceStaticSetReadyReq chSourceStaticSetReady chan pathSourceStaticSetReadyReq
chSourceStaticSetNotReady chan pathSourceStaticSetNotReadyReq chSourceStaticSetNotReady chan pathSourceStaticSetNotReadyReq
chDescribe chan pathDescribeReq chDescribe chan pathDescribeReq
@ -227,6 +229,9 @@ type path struct {
chReaderAdd chan pathReaderAddReq chReaderAdd chan pathReaderAddReq
chReaderRemove chan pathReaderRemoveReq chReaderRemove chan pathReaderRemoveReq
chAPIPathsList chan pathAPIPathsListSubReq chAPIPathsList chan pathAPIPathsListSubReq
// out
done chan struct{}
} }
func newPath( func newPath(
@ -236,7 +241,7 @@ func newPath(
writeTimeout conf.StringDuration, writeTimeout conf.StringDuration,
readBufferCount int, readBufferCount int,
confName string, confName string,
conf *conf.PathConf, cnf *conf.PathConf,
name string, name string,
matches []string, matches []string,
wg *sync.WaitGroup, wg *sync.WaitGroup,
@ -251,7 +256,7 @@ func newPath(
writeTimeout: writeTimeout, writeTimeout: writeTimeout,
readBufferCount: readBufferCount, readBufferCount: readBufferCount,
confName: confName, confName: confName,
conf: conf, conf: cnf,
name: name, name: name,
matches: matches, matches: matches,
wg: wg, wg: wg,
@ -265,6 +270,7 @@ func newPath(
onDemandStaticSourceCloseTimer: newEmptyTimer(), onDemandStaticSourceCloseTimer: newEmptyTimer(),
onDemandPublisherReadyTimer: newEmptyTimer(), onDemandPublisherReadyTimer: newEmptyTimer(),
onDemandPublisherCloseTimer: newEmptyTimer(), onDemandPublisherCloseTimer: newEmptyTimer(),
chReloadConf: make(chan *conf.PathConf),
chSourceStaticSetReady: make(chan pathSourceStaticSetReadyReq), chSourceStaticSetReady: make(chan pathSourceStaticSetReadyReq),
chSourceStaticSetNotReady: make(chan pathSourceStaticSetNotReadyReq), chSourceStaticSetNotReady: make(chan pathSourceStaticSetNotReadyReq),
chDescribe: make(chan pathDescribeReq), chDescribe: make(chan pathDescribeReq),
@ -275,6 +281,7 @@ func newPath(
chReaderAdd: make(chan pathReaderAddReq), chReaderAdd: make(chan pathReaderAddReq),
chReaderRemove: make(chan pathReaderRemoveReq), chReaderRemove: make(chan pathReaderRemoveReq),
chAPIPathsList: make(chan pathAPIPathsListSubReq), chAPIPathsList: make(chan pathAPIPathsListSubReq),
done: make(chan struct{}),
} }
pa.log(logger.Debug, "created") pa.log(logger.Debug, "created")
@ -289,26 +296,15 @@ func (pa *path) close() {
pa.ctxCancel() pa.ctxCancel()
} }
func (pa *path) wait() {
<-pa.done
}
// Log is the main logging function. // Log is the main logging function.
func (pa *path) log(level logger.Level, format string, args ...interface{}) { func (pa *path) log(level logger.Level, format string, args ...interface{}) {
pa.parent.log(level, "[path "+pa.name+"] "+format, args...) pa.parent.log(level, "[path "+pa.name+"] "+format, args...)
} }
// ConfName returns the configuration name of this path.
func (pa *path) ConfName() string {
return pa.confName
}
// Conf returns the configuration of this path.
func (pa *path) Conf() *conf.PathConf {
return pa.conf
}
// Name returns the name of this path.
func (pa *path) Name() string {
return pa.name
}
func (pa *path) hasStaticSource() bool { func (pa *path) hasStaticSource() bool {
return strings.HasPrefix(pa.conf.Source, "rtsp://") || return strings.HasPrefix(pa.conf.Source, "rtsp://") ||
strings.HasPrefix(pa.conf.Source, "rtsps://") || strings.HasPrefix(pa.conf.Source, "rtsps://") ||
@ -327,7 +323,14 @@ func (pa *path) hasOnDemandPublisher() bool {
return pa.conf.RunOnDemand != "" return pa.conf.RunOnDemand != ""
} }
func (pa *path) safeConf() *conf.PathConf {
pa.confMutex.RLock()
defer pa.confMutex.RUnlock()
return pa.conf
}
func (pa *path) run() { func (pa *path) run() {
defer close(pa.done)
defer pa.wg.Done() defer pa.wg.Done()
if pa.conf.Source == "redirect" { if pa.conf.Source == "redirect" {
@ -410,6 +413,15 @@ func (pa *path) run() {
return fmt.Errorf("not in use") return fmt.Errorf("not in use")
} }
case newConf := <-pa.chReloadConf:
if pa.hasStaticSource() {
go pa.source.(*sourceStatic).reloadConf(newConf)
}
pa.confMutex.Lock()
pa.conf = newConf
pa.confMutex.Unlock()
case req := <-pa.chSourceStaticSetReady: case req := <-pa.chSourceStaticSetReady:
err := pa.sourceSetReady(req.medias, req.generateRTPPackets) err := pa.sourceSetReady(req.medias, req.generateRTPPackets)
if err != nil { if err != nil {
@ -920,6 +932,14 @@ func (pa *path) handleAPIPathsList(req pathAPIPathsListSubReq) {
close(req.res) close(req.res)
} }
// reloadConf is called by pathManager.
func (pa *path) reloadConf(newConf *conf.PathConf) {
select {
case pa.chReloadConf <- newConf:
case <-pa.ctx.Done():
}
}
// sourceStaticSetReady is called by sourceStatic. // sourceStaticSetReady is called by sourceStatic.
func (pa *path) sourceStaticSetReady(sourceStaticCtx context.Context, req pathSourceStaticSetReadyReq) { func (pa *path) sourceStaticSetReady(sourceStaticCtx context.Context, req pathSourceStaticSetReadyReq) {
select { select {

89
internal/core/path_manager.go

@ -10,6 +10,24 @@ import (
"github.com/aler9/rtsp-simple-server/internal/logger" "github.com/aler9/rtsp-simple-server/internal/logger"
) )
func pathConfCanBeUpdated(oldPathConf *conf.PathConf, newPathConf *conf.PathConf) bool {
var copy conf.PathConf
cloneStruct(&copy, oldPathConf)
copy.RPICameraBrightness = newPathConf.RPICameraBrightness
copy.RPICameraContrast = newPathConf.RPICameraContrast
copy.RPICameraSaturation = newPathConf.RPICameraSaturation
copy.RPICameraSharpness = newPathConf.RPICameraSharpness
copy.RPICameraExposure = newPathConf.RPICameraExposure
copy.RPICameraAWB = newPathConf.RPICameraAWB
copy.RPICameraDenoise = newPathConf.RPICameraDenoise
copy.RPICameraMetering = newPathConf.RPICameraMetering
copy.RPICameraEV = newPathConf.RPICameraEV
copy.RPICameraFPS = newPathConf.RPICameraFPS
return newPathConf.Equal(&copy)
}
type pathManagerHLSServer interface { type pathManagerHLSServer interface {
pathSourceReady(*path) pathSourceReady(*path)
pathSourceNotReady(*path) pathSourceNotReady(*path)
@ -34,6 +52,7 @@ type pathManager struct {
wg sync.WaitGroup wg sync.WaitGroup
hlsServer pathManagerHLSServer hlsServer pathManagerHLSServer
paths map[string]*path paths map[string]*path
pathsByConf map[string]map[*path]struct{}
// in // in
chConfReload chan map[string]*conf.PathConf chConfReload chan map[string]*conf.PathConf
@ -72,6 +91,7 @@ func newPathManager(
ctx: ctx, ctx: ctx,
ctxCancel: ctxCancel, ctxCancel: ctxCancel,
paths: make(map[string]*path), paths: make(map[string]*path),
pathsByConf: make(map[string]map[*path]struct{}),
chConfReload: make(chan map[string]*conf.PathConf), chConfReload: make(chan map[string]*conf.PathConf),
chPathClose: make(chan *path), chPathClose: make(chan *path),
chPathSourceReady: make(chan *path), chPathSourceReady: make(chan *path),
@ -118,36 +138,34 @@ func (pm *pathManager) run() {
outer: outer:
for { for {
select { select {
case pathConfs := <-pm.chConfReload: case newPathConfs := <-pm.chConfReload:
// remove confs for confName, pathConf := range pm.pathConfs {
for pathConfName := range pm.pathConfs { if newPathConf, ok := newPathConfs[confName]; ok {
if _, ok := pathConfs[pathConfName]; !ok { // configuration has changed
delete(pm.pathConfs, pathConfName) if !newPathConf.Equal(pathConf) {
if pathConfCanBeUpdated(pathConf, newPathConf) { // paths associated with the configuration can be updated
for pa := range pm.pathsByConf[confName] {
go pa.reloadConf(newPathConf)
}
} else { // paths associated with the configuration must be recreated
for pa := range pm.pathsByConf[confName] {
pm.removePath(pa)
pa.close()
pa.wait() // avoid conflicts between sources
} }
} }
// update confs
for pathConfName, oldConf := range pm.pathConfs {
if !oldConf.Equal(pathConfs[pathConfName]) {
pm.pathConfs[pathConfName] = pathConfs[pathConfName]
} }
} else {
// configuration has been deleted, remove associated paths
for pa := range pm.pathsByConf[confName] {
pm.removePath(pa)
pa.close()
pa.wait() // avoid conflicts between sources
} }
// add confs
for pathConfName, pathConf := range pathConfs {
if _, ok := pm.pathConfs[pathConfName]; !ok {
pm.pathConfs[pathConfName] = pathConf
} }
} }
// remove paths associated with a conf which doesn't exist anymore pm.pathConfs = newPathConfs
// or has changed
for _, pa := range pm.paths {
if pathConf, ok := pm.pathConfs[pa.ConfName()]; !ok || pathConf != pa.Conf() {
delete(pm.paths, pa.Name())
pa.close()
}
}
// add new paths // add new paths
for pathConfName, pathConf := range pm.pathConfs { for pathConfName, pathConf := range pm.pathConfs {
@ -157,11 +175,10 @@ outer:
} }
case pa := <-pm.chPathClose: case pa := <-pm.chPathClose:
if pmpa, ok := pm.paths[pa.Name()]; !ok || pmpa != pa { if pmpa, ok := pm.paths[pa.name]; !ok || pmpa != pa {
continue continue
} }
delete(pm.paths, pa.Name()) pm.removePath(pa)
pa.close()
case pa := <-pm.chPathSourceReady: case pa := <-pm.chPathSourceReady:
if pm.hlsServer != nil { if pm.hlsServer != nil {
@ -278,7 +295,7 @@ func (pm *pathManager) createPath(
name string, name string,
matches []string, matches []string,
) { ) {
pm.paths[name] = newPath( pa := newPath(
pm.ctx, pm.ctx,
pm.rtspAddress, pm.rtspAddress,
pm.readTimeout, pm.readTimeout,
@ -291,6 +308,21 @@ func (pm *pathManager) createPath(
&pm.wg, &pm.wg,
pm.externalCmdPool, pm.externalCmdPool,
pm) pm)
pm.paths[name] = pa
if _, ok := pm.pathsByConf[pathConfName]; !ok {
pm.pathsByConf[pathConfName] = make(map[*path]struct{})
}
pm.pathsByConf[pathConfName][pa] = struct{}{}
}
func (pm *pathManager) removePath(pa *path) {
delete(pm.pathsByConf[pa.confName], pa)
if len(pm.pathsByConf[pa.confName]) == 0 {
delete(pm.pathsByConf, pa.confName)
}
delete(pm.paths, pa.name)
} }
func (pm *pathManager) findPathConf(name string) (string, *conf.PathConf, []string, error) { func (pm *pathManager) findPathConf(name string) (string, *conf.PathConf, []string, error) {
@ -330,6 +362,7 @@ func (pm *pathManager) pathSourceReady(pa *path) {
select { select {
case pm.chPathSourceReady <- pa: case pm.chPathSourceReady <- pa:
case <-pm.ctx.Done(): case <-pm.ctx.Done():
case <-pa.ctx.Done(): // in case pathManager is closing the path
} }
} }
@ -338,6 +371,7 @@ func (pm *pathManager) pathSourceNotReady(pa *path) {
select { select {
case pm.chPathSourceNotReady <- pa: case pm.chPathSourceNotReady <- pa:
case <-pm.ctx.Done(): case <-pm.ctx.Done():
case <-pa.ctx.Done(): // in case pathManager is closing the path
} }
} }
@ -346,6 +380,7 @@ func (pm *pathManager) onPathClose(pa *path) {
select { select {
case pm.chPathClose <- pa: case pm.chPathClose <- pa:
case <-pm.ctx.Done(): case <-pm.ctx.Done():
case <-pa.ctx.Done(): // in case pathManager is closing the path
} }
} }

51
internal/core/rpicamera_source.go

@ -7,11 +7,46 @@ import (
"github.com/aler9/gortsplib/v2/pkg/format" "github.com/aler9/gortsplib/v2/pkg/format"
"github.com/aler9/gortsplib/v2/pkg/media" "github.com/aler9/gortsplib/v2/pkg/media"
"github.com/aler9/rtsp-simple-server/internal/conf"
"github.com/aler9/rtsp-simple-server/internal/formatprocessor" "github.com/aler9/rtsp-simple-server/internal/formatprocessor"
"github.com/aler9/rtsp-simple-server/internal/logger" "github.com/aler9/rtsp-simple-server/internal/logger"
"github.com/aler9/rtsp-simple-server/internal/rpicamera" "github.com/aler9/rtsp-simple-server/internal/rpicamera"
) )
func paramsFromConf(cnf *conf.PathConf) rpicamera.Params {
return rpicamera.Params{
CameraID: cnf.RPICameraCamID,
Width: cnf.RPICameraWidth,
Height: cnf.RPICameraHeight,
HFlip: cnf.RPICameraHFlip,
VFlip: cnf.RPICameraVFlip,
Brightness: cnf.RPICameraBrightness,
Contrast: cnf.RPICameraContrast,
Saturation: cnf.RPICameraSaturation,
Sharpness: cnf.RPICameraSharpness,
Exposure: cnf.RPICameraExposure,
AWB: cnf.RPICameraAWB,
Denoise: cnf.RPICameraDenoise,
Shutter: cnf.RPICameraShutter,
Metering: cnf.RPICameraMetering,
Gain: cnf.RPICameraGain,
EV: cnf.RPICameraEV,
ROI: cnf.RPICameraROI,
TuningFile: cnf.RPICameraTuningFile,
Mode: cnf.RPICameraMode,
FPS: cnf.RPICameraFPS,
IDRPeriod: cnf.RPICameraIDRPeriod,
Bitrate: cnf.RPICameraBitrate,
Profile: cnf.RPICameraProfile,
Level: cnf.RPICameraLevel,
AfMode: cnf.RPICameraAfMode,
AfRange: cnf.RPICameraAfRange,
AfSpeed: cnf.RPICameraAfSpeed,
LensPosition: cnf.RPICameraLensPosition,
AfWindow: cnf.RPICameraAfWindow,
}
}
type rpiCameraSourceParent interface { type rpiCameraSourceParent interface {
log(logger.Level, string, ...interface{}) log(logger.Level, string, ...interface{})
sourceStaticImplSetReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes sourceStaticImplSetReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes
@ -19,16 +54,13 @@ type rpiCameraSourceParent interface {
} }
type rpiCameraSource struct { type rpiCameraSource struct {
params rpicamera.Params
parent rpiCameraSourceParent parent rpiCameraSourceParent
} }
func newRPICameraSource( func newRPICameraSource(
params rpicamera.Params,
parent rpiCameraSourceParent, parent rpiCameraSourceParent,
) *rpiCameraSource { ) *rpiCameraSource {
return &rpiCameraSource{ return &rpiCameraSource{
params: params,
parent: parent, parent: parent,
} }
} }
@ -38,7 +70,7 @@ func (s *rpiCameraSource) Log(level logger.Level, format string, args ...interfa
} }
// run implements sourceStaticImpl. // run implements sourceStaticImpl.
func (s *rpiCameraSource) run(ctx context.Context) error { func (s *rpiCameraSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan *conf.PathConf) error {
medi := &media.Media{ medi := &media.Media{
Type: media.TypeVideo, Type: media.TypeVideo,
Formats: []format.Format{&format.H264{ Formats: []format.Format{&format.H264{
@ -73,7 +105,7 @@ func (s *rpiCameraSource) run(ctx context.Context) error {
} }
} }
cam, err := rpicamera.New(s.params, onData) cam, err := rpicamera.New(paramsFromConf(cnf), onData)
if err != nil { if err != nil {
return err return err
} }
@ -85,9 +117,16 @@ func (s *rpiCameraSource) run(ctx context.Context) error {
} }
}() }()
<-ctx.Done() for {
select {
case cnf := <-reloadConf:
cam.ReloadParams(paramsFromConf(cnf))
case <-ctx.Done():
return nil return nil
} }
}
}
// apiSourceDescribe implements sourceStaticImpl. // apiSourceDescribe implements sourceStaticImpl.
func (*rpiCameraSource) apiSourceDescribe() interface{} { func (*rpiCameraSource) apiSourceDescribe() interface{} {

12
internal/core/rtmp_conn.go

@ -422,14 +422,16 @@ func (c *rtmpConn) runRead(ctx context.Context, u *url.URL) error {
defer res.stream.readerRemove(c) defer res.stream.readerRemove(c)
c.log(logger.Info, "is reading from path '%s', %s", c.log(logger.Info, "is reading from path '%s', %s",
path.Name(), sourceMediaInfo(medias)) path.name, sourceMediaInfo(medias))
if path.Conf().RunOnRead != "" { pathConf := path.safeConf()
if pathConf.RunOnRead != "" {
c.log(logger.Info, "runOnRead command started") c.log(logger.Info, "runOnRead command started")
onReadCmd := externalcmd.NewCmd( onReadCmd := externalcmd.NewCmd(
c.externalCmdPool, c.externalCmdPool,
path.Conf().RunOnRead, pathConf.RunOnRead,
path.Conf().RunOnReadRestart, pathConf.RunOnReadRestart,
path.externalCmdEnv(), path.externalCmdEnv(),
func(co int) { func(co int) {
c.log(logger.Info, "runOnRead command exited with code %d", co) c.log(logger.Info, "runOnRead command exited with code %d", co)
@ -530,7 +532,7 @@ func (c *rtmpConn) runPublish(ctx context.Context, u *url.URL) error {
} }
c.log(logger.Info, "is publishing to path '%s', %s", c.log(logger.Info, "is publishing to path '%s', %s",
path.Name(), path.name,
sourceMediaInfo(medias)) sourceMediaInfo(medias))
// disable write deadline to allow outgoing acknowledges // disable write deadline to allow outgoing acknowledges

16
internal/core/rtmp_source.go

@ -30,23 +30,17 @@ type rtmpSourceParent interface {
} }
type rtmpSource struct { type rtmpSource struct {
ur string
fingerprint string
readTimeout conf.StringDuration readTimeout conf.StringDuration
writeTimeout conf.StringDuration writeTimeout conf.StringDuration
parent rtmpSourceParent parent rtmpSourceParent
} }
func newRTMPSource( func newRTMPSource(
ur string,
fingerprint string,
readTimeout conf.StringDuration, readTimeout conf.StringDuration,
writeTimeout conf.StringDuration, writeTimeout conf.StringDuration,
parent rtmpSourceParent, parent rtmpSourceParent,
) *rtmpSource { ) *rtmpSource {
return &rtmpSource{ return &rtmpSource{
ur: ur,
fingerprint: fingerprint,
readTimeout: readTimeout, readTimeout: readTimeout,
writeTimeout: writeTimeout, writeTimeout: writeTimeout,
parent: parent, parent: parent,
@ -58,10 +52,10 @@ func (s *rtmpSource) Log(level logger.Level, format string, args ...interface{})
} }
// run implements sourceStaticImpl. // run implements sourceStaticImpl.
func (s *rtmpSource) run(ctx context.Context) error { func (s *rtmpSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan *conf.PathConf) error {
s.Log(logger.Debug, "connecting") s.Log(logger.Debug, "connecting")
u, err := url.Parse(s.ur) u, err := url.Parse(cnf.Source)
if err != nil { if err != nil {
return err return err
} }
@ -86,7 +80,7 @@ func (s *rtmpSource) run(ctx context.Context) error {
h := sha256.New() h := sha256.New()
h.Write(cs.PeerCertificates[0].Raw) h.Write(cs.PeerCertificates[0].Raw)
hstr := hex.EncodeToString(h.Sum(nil)) hstr := hex.EncodeToString(h.Sum(nil))
fingerprintLower := strings.ToLower(s.fingerprint) fingerprintLower := strings.ToLower(cnf.SourceFingerprint)
if hstr != fingerprintLower { if hstr != fingerprintLower {
return fmt.Errorf("server fingerprint do not match: expected %s, got %s", return fmt.Errorf("server fingerprint do not match: expected %s, got %s",
@ -213,17 +207,21 @@ func (s *rtmpSource) run(ctx context.Context) error {
}() }()
}() }()
for {
select { select {
case err := <-readDone: case err := <-readDone:
nconn.Close() nconn.Close()
return err return err
case <-reloadConf:
case <-ctx.Done(): case <-ctx.Done():
nconn.Close() nconn.Close()
<-readDone <-readDone
return nil return nil
} }
} }
}
// apiSourceDescribe implements sourceStaticImpl. // apiSourceDescribe implements sourceStaticImpl.
func (*rtmpSource) apiSourceDescribe() interface{} { func (*rtmpSource) apiSourceDescribe() interface{} {

12
internal/core/rtsp_session.go

@ -270,16 +270,18 @@ func (s *rtspSession) onPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Respo
if s.session.State() == gortsplib.ServerSessionStatePrePlay { if s.session.State() == gortsplib.ServerSessionStatePrePlay {
s.log(logger.Info, "is reading from path '%s', with %s, %s", s.log(logger.Info, "is reading from path '%s', with %s, %s",
s.path.Name(), s.path.name,
s.session.SetuppedTransport(), s.session.SetuppedTransport(),
sourceMediaInfo(s.session.SetuppedMedias())) sourceMediaInfo(s.session.SetuppedMedias()))
if s.path.Conf().RunOnRead != "" { pathConf := s.path.safeConf()
if pathConf.RunOnRead != "" {
s.log(logger.Info, "runOnRead command started") s.log(logger.Info, "runOnRead command started")
s.onReadCmd = externalcmd.NewCmd( s.onReadCmd = externalcmd.NewCmd(
s.externalCmdPool, s.externalCmdPool,
s.path.Conf().RunOnRead, pathConf.RunOnRead,
s.path.Conf().RunOnReadRestart, pathConf.RunOnReadRestart,
s.path.externalCmdEnv(), s.path.externalCmdEnv(),
func(co int) { func(co int) {
s.log(logger.Info, "runOnRead command exited with code %d", co) s.log(logger.Info, "runOnRead command exited with code %d", co)
@ -311,7 +313,7 @@ func (s *rtspSession) onRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.R
} }
s.log(logger.Info, "is publishing to path '%s', with %s, %s", s.log(logger.Info, "is publishing to path '%s', with %s, %s",
s.path.Name(), s.path.name,
s.session.SetuppedTransport(), s.session.SetuppedTransport(),
sourceMediaInfo(s.session.AnnouncedMedias())) sourceMediaInfo(s.session.AnnouncedMedias()))

28
internal/core/rtsp_source.go

@ -27,10 +27,6 @@ type rtspSourceParent interface {
} }
type rtspSource struct { type rtspSource struct {
ur string
proto conf.SourceProtocol
anyPortEnable bool
fingerprint string
readTimeout conf.StringDuration readTimeout conf.StringDuration
writeTimeout conf.StringDuration writeTimeout conf.StringDuration
readBufferCount int readBufferCount int
@ -38,20 +34,12 @@ type rtspSource struct {
} }
func newRTSPSource( func newRTSPSource(
ur string,
proto conf.SourceProtocol,
anyPortEnable bool,
fingerprint string,
readTimeout conf.StringDuration, readTimeout conf.StringDuration,
writeTimeout conf.StringDuration, writeTimeout conf.StringDuration,
readBufferCount int, readBufferCount int,
parent rtspSourceParent, parent rtspSourceParent,
) *rtspSource { ) *rtspSource {
return &rtspSource{ return &rtspSource{
ur: ur,
proto: proto,
anyPortEnable: anyPortEnable,
fingerprint: fingerprint,
readTimeout: readTimeout, readTimeout: readTimeout,
writeTimeout: writeTimeout, writeTimeout: writeTimeout,
readBufferCount: readBufferCount, readBufferCount: readBufferCount,
@ -64,18 +52,18 @@ func (s *rtspSource) Log(level logger.Level, format string, args ...interface{})
} }
// run implements sourceStaticImpl. // run implements sourceStaticImpl.
func (s *rtspSource) run(ctx context.Context) error { func (s *rtspSource) run(ctx context.Context, cnf *conf.PathConf, reloadConf chan *conf.PathConf) error {
s.Log(logger.Debug, "connecting") s.Log(logger.Debug, "connecting")
var tlsConfig *tls.Config var tlsConfig *tls.Config
if s.fingerprint != "" { if cnf.SourceFingerprint != "" {
tlsConfig = &tls.Config{ tlsConfig = &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
VerifyConnection: func(cs tls.ConnectionState) error { VerifyConnection: func(cs tls.ConnectionState) error {
h := sha256.New() h := sha256.New()
h.Write(cs.PeerCertificates[0].Raw) h.Write(cs.PeerCertificates[0].Raw)
hstr := hex.EncodeToString(h.Sum(nil)) hstr := hex.EncodeToString(h.Sum(nil))
fingerprintLower := strings.ToLower(s.fingerprint) fingerprintLower := strings.ToLower(cnf.SourceFingerprint)
if hstr != fingerprintLower { if hstr != fingerprintLower {
return fmt.Errorf("server fingerprint do not match: expected %s, got %s", return fmt.Errorf("server fingerprint do not match: expected %s, got %s",
@ -88,12 +76,12 @@ func (s *rtspSource) run(ctx context.Context) error {
} }
c := &gortsplib.Client{ c := &gortsplib.Client{
Transport: s.proto.Transport, Transport: cnf.SourceProtocol.Transport,
TLSConfig: tlsConfig, TLSConfig: tlsConfig,
ReadTimeout: time.Duration(s.readTimeout), ReadTimeout: time.Duration(s.readTimeout),
WriteTimeout: time.Duration(s.writeTimeout), WriteTimeout: time.Duration(s.writeTimeout),
ReadBufferCount: s.readBufferCount, ReadBufferCount: s.readBufferCount,
AnyPortEnable: s.anyPortEnable, AnyPortEnable: cnf.SourceAnyPortEnable,
OnRequest: func(req *base.Request) { OnRequest: func(req *base.Request) {
s.Log(logger.Debug, "c->s %v", req) s.Log(logger.Debug, "c->s %v", req)
}, },
@ -105,7 +93,7 @@ func (s *rtspSource) run(ctx context.Context) error {
}, },
} }
u, err := url.Parse(s.ur) u, err := url.Parse(cnf.Source)
if err != nil { if err != nil {
return err return err
} }
@ -238,16 +226,20 @@ func (s *rtspSource) run(ctx context.Context) error {
}() }()
}() }()
for {
select { select {
case err := <-readErr: case err := <-readErr:
return err return err
case <-reloadConf:
case <-ctx.Done(): case <-ctx.Done():
c.Close() c.Close()
<-readErr <-readErr
return nil return nil
} }
} }
}
// apiSourceDescribe implements sourceStaticImpl. // apiSourceDescribe implements sourceStaticImpl.
func (*rtspSource) apiSourceDescribe() interface{} { func (*rtspSource) apiSourceDescribe() interface{} {

124
internal/core/source_static.go

@ -8,7 +8,6 @@ import (
"github.com/aler9/rtsp-simple-server/internal/conf" "github.com/aler9/rtsp-simple-server/internal/conf"
"github.com/aler9/rtsp-simple-server/internal/logger" "github.com/aler9/rtsp-simple-server/internal/logger"
"github.com/aler9/rtsp-simple-server/internal/rpicamera"
) )
const ( const (
@ -17,7 +16,7 @@ const (
type sourceStaticImpl interface { type sourceStaticImpl interface {
Log(logger.Level, string, ...interface{}) Log(logger.Level, string, ...interface{})
run(context.Context) error run(context.Context, *conf.PathConf, chan *conf.PathConf) error
apiSourceDescribe() interface{} apiSourceDescribe() interface{}
} }
@ -29,6 +28,7 @@ type sourceStaticParent interface {
// sourceStatic is a static source. // sourceStatic is a static source.
type sourceStatic struct { type sourceStatic struct {
conf *conf.PathConf
parent sourceStaticParent parent sourceStaticParent
ctx context.Context ctx context.Context
@ -36,86 +36,53 @@ type sourceStatic struct {
impl sourceStaticImpl impl sourceStaticImpl
running bool running bool
done chan struct{} // in
chReloadConf chan *conf.PathConf
chSourceStaticImplSetReady chan pathSourceStaticSetReadyReq chSourceStaticImplSetReady chan pathSourceStaticSetReadyReq
chSourceStaticImplSetNotReady chan pathSourceStaticSetNotReadyReq chSourceStaticImplSetNotReady chan pathSourceStaticSetNotReadyReq
// out
done chan struct{}
} }
func newSourceStatic( func newSourceStatic(
conf *conf.PathConf, cnf *conf.PathConf,
readTimeout conf.StringDuration, readTimeout conf.StringDuration,
writeTimeout conf.StringDuration, writeTimeout conf.StringDuration,
readBufferCount int, readBufferCount int,
parent sourceStaticParent, parent sourceStaticParent,
) *sourceStatic { ) *sourceStatic {
s := &sourceStatic{ s := &sourceStatic{
conf: cnf,
parent: parent, parent: parent,
chReloadConf: make(chan *conf.PathConf),
chSourceStaticImplSetReady: make(chan pathSourceStaticSetReadyReq), chSourceStaticImplSetReady: make(chan pathSourceStaticSetReadyReq),
chSourceStaticImplSetNotReady: make(chan pathSourceStaticSetNotReadyReq), chSourceStaticImplSetNotReady: make(chan pathSourceStaticSetNotReadyReq),
} }
switch { switch {
case strings.HasPrefix(conf.Source, "rtsp://") || case strings.HasPrefix(cnf.Source, "rtsp://") ||
strings.HasPrefix(conf.Source, "rtsps://"): strings.HasPrefix(cnf.Source, "rtsps://"):
s.impl = newRTSPSource( s.impl = newRTSPSource(
conf.Source,
conf.SourceProtocol,
conf.SourceAnyPortEnable,
conf.SourceFingerprint,
readTimeout, readTimeout,
writeTimeout, writeTimeout,
readBufferCount, readBufferCount,
s) s)
case strings.HasPrefix(conf.Source, "rtmp://") || case strings.HasPrefix(cnf.Source, "rtmp://") ||
strings.HasPrefix(conf.Source, "rtmps://"): strings.HasPrefix(cnf.Source, "rtmps://"):
s.impl = newRTMPSource( s.impl = newRTMPSource(
conf.Source,
conf.SourceFingerprint,
readTimeout, readTimeout,
writeTimeout, writeTimeout,
s) s)
case strings.HasPrefix(conf.Source, "http://") || case strings.HasPrefix(cnf.Source, "http://") ||
strings.HasPrefix(conf.Source, "https://"): strings.HasPrefix(cnf.Source, "https://"):
s.impl = newHLSSource( s.impl = newHLSSource(
conf.Source,
conf.SourceFingerprint,
s) s)
case conf.Source == "rpiCamera": case cnf.Source == "rpiCamera":
s.impl = newRPICameraSource( s.impl = newRPICameraSource(
rpicamera.Params{
CameraID: conf.RPICameraCamID,
Width: conf.RPICameraWidth,
Height: conf.RPICameraHeight,
HFlip: conf.RPICameraHFlip,
VFlip: conf.RPICameraVFlip,
Brightness: conf.RPICameraBrightness,
Contrast: conf.RPICameraContrast,
Saturation: conf.RPICameraSaturation,
Sharpness: conf.RPICameraSharpness,
Exposure: conf.RPICameraExposure,
AWB: conf.RPICameraAWB,
Denoise: conf.RPICameraDenoise,
Shutter: conf.RPICameraShutter,
Metering: conf.RPICameraMetering,
Gain: conf.RPICameraGain,
EV: conf.RPICameraEV,
ROI: conf.RPICameraROI,
TuningFile: conf.RPICameraTuningFile,
Mode: conf.RPICameraMode,
FPS: conf.RPICameraFPS,
IDRPeriod: conf.RPICameraIDRPeriod,
Bitrate: conf.RPICameraBitrate,
Profile: conf.RPICameraProfile,
Level: conf.RPICameraLevel,
AfMode: conf.RPICameraAfMode,
AfRange: conf.RPICameraAfRange,
AfSpeed: conf.RPICameraAfSpeed,
LensPosition: conf.RPICameraLensPosition,
AfWindow: conf.RPICameraAfWindow,
},
s) s)
} }
@ -163,33 +130,43 @@ func (s *sourceStatic) log(level logger.Level, format string, args ...interface{
func (s *sourceStatic) run() { func (s *sourceStatic) run() {
defer close(s.done) defer close(s.done)
outer: var innerCtx context.Context
for { var innerCtxCancel func()
s.runInner() implErr := make(chan error)
innerReloadConf := make(chan *conf.PathConf)
select { recreate := func() {
case <-time.After(sourceStaticRetryPause): innerCtx, innerCtxCancel = context.WithCancel(context.Background())
case <-s.ctx.Done(): go func() {
break outer implErr <- s.impl.run(innerCtx, s.conf, innerReloadConf)
} }()
} }
s.ctxCancel() recreate()
}
func (s *sourceStatic) runInner() { recreating := false
innerCtx, innerCtxCancel := context.WithCancel(context.Background()) recreateTimer := newEmptyTimer()
implErr := make(chan error)
go func() {
implErr <- s.impl.run(innerCtx)
}()
for { for {
select { select {
case err := <-implErr: case err := <-implErr:
innerCtxCancel() innerCtxCancel()
s.impl.Log(logger.Info, "ERR: %v", err) s.impl.Log(logger.Info, "ERR: %v", err)
return recreating = true
recreateTimer = time.NewTimer(sourceStaticRetryPause)
case newConf := <-s.chReloadConf:
s.conf = newConf
if !recreating {
cReloadConf := innerReloadConf
cInnerCtx := innerCtx
go func() {
select {
case cReloadConf <- newConf:
case <-cInnerCtx.Done():
}
}()
}
case req := <-s.chSourceStaticImplSetReady: case req := <-s.chSourceStaticImplSetReady:
s.parent.sourceStaticSetReady(s.ctx, req) s.parent.sourceStaticSetReady(s.ctx, req)
@ -197,14 +174,27 @@ func (s *sourceStatic) runInner() {
case req := <-s.chSourceStaticImplSetNotReady: case req := <-s.chSourceStaticImplSetNotReady:
s.parent.sourceStaticSetNotReady(s.ctx, req) s.parent.sourceStaticSetNotReady(s.ctx, req)
case <-recreateTimer.C:
recreate()
recreating = false
case <-s.ctx.Done(): case <-s.ctx.Done():
if !recreating {
innerCtxCancel() innerCtxCancel()
<-implErr <-implErr
}
return return
} }
} }
} }
func (s *sourceStatic) reloadConf(newConf *conf.PathConf) {
select {
case s.chReloadConf <- newConf:
case <-s.ctx.Done():
}
}
// apiSourceDescribe implements source. // apiSourceDescribe implements source.
func (s *sourceStatic) apiSourceDescribe() interface{} { func (s *sourceStatic) apiSourceDescribe() interface{} {
return s.impl.apiSourceDescribe() return s.impl.apiSourceDescribe()

2
internal/core/webrtc_conn.go

@ -507,7 +507,7 @@ outer:
defer res.stream.readerRemove(c) defer res.stream.readerRemove(c)
c.log(logger.Info, "is reading from path '%s', %s", c.log(logger.Info, "is reading from path '%s', %s",
path.Name(), sourceMediaInfo(gatherMedias(tracks))) path.name, sourceMediaInfo(gatherMedias(tracks)))
go func() { go func() {
for { for {

2
internal/core/webrtc_server.go

@ -421,7 +421,7 @@ func (s *webRTCServer) newConn(dir string, wsconn *websocket.ServerConn) *webRTC
} }
func (s *webRTCServer) authenticate(pa *path, ctx *gin.Context) error { func (s *webRTCServer) authenticate(pa *path, ctx *gin.Context) error {
pathConf := pa.Conf() pathConf := pa.safeConf()
pathIPs := pathConf.ReadIPs pathIPs := pathConf.ReadIPs
pathUser := pathConf.ReadUser pathUser := pathConf.ReadUser
pathPass := pathConf.ReadPass pathPass := pathConf.ReadPass

49
internal/rpicamera/embeddedexe.go

@ -1,49 +0,0 @@
//go:build rpicamera
// +build rpicamera
package rpicamera
import (
"os"
"os/exec"
"strconv"
"time"
)
const (
tempPathPrefix = "/dev/shm/rtspss-embeddedexe-"
)
type embeddedExe struct {
cmd *exec.Cmd
}
func newEmbeddedExe(content []byte, env []string) (*embeddedExe, error) {
tempPath := tempPathPrefix + strconv.FormatInt(time.Now().UnixNano(), 10)
err := os.WriteFile(tempPath, content, 0o755)
if err != nil {
return nil, err
}
cmd := exec.Command(tempPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = env
err = cmd.Start()
os.Remove(tempPath)
if err != nil {
return nil, err
}
return &embeddedExe{
cmd: cmd,
}, nil
}
func (e *embeddedExe) close() {
e.cmd.Process.Kill()
e.cmd.Wait()
}

1
internal/rpicamera/exe/Makefile

@ -26,6 +26,7 @@ OBJS = \
encoder.o \ encoder.o \
main.o \ main.o \
parameters.o \ parameters.o \
pipe.o \
window.o \ window.o \
sensor_mode.o sensor_mode.o

173
internal/rpicamera/exe/camera.cpp

@ -3,6 +3,7 @@
#include <cstring> #include <cstring>
#include <sys/mman.h> #include <sys/mman.h>
#include <iostream> #include <iostream>
#include <mutex>
#include <libcamera/camera_manager.h> #include <libcamera/camera_manager.h>
#include <libcamera/camera.h> #include <libcamera/camera.h>
@ -71,13 +72,15 @@ static PixelFormat mode_to_pixel_format(sensor_mode_t *mode) {
} }
struct CameraPriv { struct CameraPriv {
parameters_t *params; const parameters_t *params;
camera_frame_cb frame_cb; camera_frame_cb frame_cb;
std::unique_ptr<CameraManager> camera_manager; std::unique_ptr<CameraManager> camera_manager;
std::shared_ptr<Camera> camera; std::shared_ptr<Camera> camera;
Stream *video_stream; Stream *video_stream;
std::unique_ptr<FrameBufferAllocator> allocator; std::unique_ptr<FrameBufferAllocator> allocator;
std::vector<std::unique_ptr<Request>> requests; std::vector<std::unique_ptr<Request>> requests;
std::mutex ctrls_mutex;
std::unique_ptr<ControlList> ctrls;
}; };
static int get_v4l2_colorspace(std::optional<ColorSpace> const &cs) { static int get_v4l2_colorspace(std::optional<ColorSpace> const &cs) {
@ -87,7 +90,7 @@ static int get_v4l2_colorspace(std::optional<ColorSpace> const &cs) {
return V4L2_COLORSPACE_SMPTE170M; return V4L2_COLORSPACE_SMPTE170M;
} }
bool camera_create(parameters_t *params, camera_frame_cb frame_cb, camera_t **cam) { bool camera_create(const parameters_t *params, camera_frame_cb frame_cb, camera_t **cam) {
// We make sure to set the environment variable before libcamera init // We make sure to set the environment variable before libcamera init
setenv("LIBCAMERA_RPI_TUNING_FILE", params->tuning_file, 1); setenv("LIBCAMERA_RPI_TUNING_FILE", params->tuning_file, 1);
@ -223,6 +226,13 @@ static void on_request_complete(Request *request) {
camp->frame_cb(buffer->planes()[0].fd.get(), size, ts); camp->frame_cb(buffer->planes()[0].fd.get(), size, ts);
request->reuse(Request::ReuseFlag::ReuseBuffers); request->reuse(Request::ReuseFlag::ReuseBuffers);
{
std::lock_guard<std::mutex> lock(camp->ctrls_mutex);
request->controls() = *camp->ctrls;
camp->ctrls->clear();
}
camp->camera->queueRequest(request); camp->camera->queueRequest(request);
} }
@ -236,143 +246,146 @@ int camera_get_mode_colorspace(camera_t *cam) {
return get_v4l2_colorspace(camp->video_stream->configuration().colorSpace); return get_v4l2_colorspace(camp->video_stream->configuration().colorSpace);
} }
bool camera_start(camera_t *cam) { static void fill_dynamic_controls(ControlList *ctrls, const parameters_t *params) {
CameraPriv *camp = (CameraPriv *)cam; ctrls->set(controls::Brightness, params->brightness);
ctrls->set(controls::Contrast, params->contrast);
ControlList ctrls = ControlList(controls::controls); ctrls->set(controls::Saturation, params->saturation);
ctrls->set(controls::Sharpness, params->sharpness);
ctrls.set(controls::Brightness, camp->params->brightness);
ctrls.set(controls::Contrast, camp->params->contrast);
ctrls.set(controls::Saturation, camp->params->saturation);
ctrls.set(controls::Sharpness, camp->params->sharpness);
int exposure_mode; int exposure_mode;
if (strcmp(camp->params->exposure, "short") == 0) { if (strcmp(params->exposure, "short") == 0) {
exposure_mode = controls::ExposureShort; exposure_mode = controls::ExposureShort;
} else if (strcmp(camp->params->exposure, "long") == 0) { } else if (strcmp(params->exposure, "long") == 0) {
exposure_mode = controls::ExposureLong; exposure_mode = controls::ExposureLong;
} else if (strcmp(camp->params->exposure, "custom") == 0) { } else if (strcmp(params->exposure, "custom") == 0) {
exposure_mode = controls::ExposureCustom; exposure_mode = controls::ExposureCustom;
} else { } else {
exposure_mode = controls::ExposureNormal; exposure_mode = controls::ExposureNormal;
} }
ctrls.set(controls::AeExposureMode, exposure_mode); ctrls->set(controls::AeExposureMode, exposure_mode);
int awb_mode; int awb_mode;
if (strcmp(camp->params->awb, "incandescent") == 0) { if (strcmp(params->awb, "incandescent") == 0) {
awb_mode = controls::AwbIncandescent; awb_mode = controls::AwbIncandescent;
} else if (strcmp(camp->params->awb, "tungsten") == 0) { } else if (strcmp(params->awb, "tungsten") == 0) {
awb_mode = controls::AwbTungsten; awb_mode = controls::AwbTungsten;
} else if (strcmp(camp->params->awb, "fluorescent") == 0) { } else if (strcmp(params->awb, "fluorescent") == 0) {
awb_mode = controls::AwbFluorescent; awb_mode = controls::AwbFluorescent;
} else if (strcmp(camp->params->awb, "indoor") == 0) { } else if (strcmp(params->awb, "indoor") == 0) {
awb_mode = controls::AwbIndoor; awb_mode = controls::AwbIndoor;
} else if (strcmp(camp->params->awb, "daylight") == 0) { } else if (strcmp(params->awb, "daylight") == 0) {
awb_mode = controls::AwbDaylight; awb_mode = controls::AwbDaylight;
} else if (strcmp(camp->params->awb, "cloudy") == 0) { } else if (strcmp(params->awb, "cloudy") == 0) {
awb_mode = controls::AwbCloudy; awb_mode = controls::AwbCloudy;
} else if (strcmp(camp->params->awb, "custom") == 0) { } else if (strcmp(params->awb, "custom") == 0) {
awb_mode = controls::AwbCustom; awb_mode = controls::AwbCustom;
} else { } else {
awb_mode = controls::AwbAuto; awb_mode = controls::AwbAuto;
} }
ctrls.set(controls::AwbMode, awb_mode); ctrls->set(controls::AwbMode, awb_mode);
int denoise_mode; int denoise_mode;
if (strcmp(camp->params->denoise, "cdn_off") == 0) { if (strcmp(params->denoise, "cdn_off") == 0) {
denoise_mode = controls::draft::NoiseReductionModeMinimal; denoise_mode = controls::draft::NoiseReductionModeMinimal;
} else if (strcmp(camp->params->denoise, "cdn_hq") == 0) { } else if (strcmp(params->denoise, "cdn_hq") == 0) {
denoise_mode = controls::draft::NoiseReductionModeHighQuality; denoise_mode = controls::draft::NoiseReductionModeHighQuality;
} else if (strcmp(camp->params->denoise, "cdn_fast") == 0) { } else if (strcmp(params->denoise, "cdn_fast") == 0) {
denoise_mode = controls::draft::NoiseReductionModeFast; denoise_mode = controls::draft::NoiseReductionModeFast;
} else { } else {
denoise_mode = controls::draft::NoiseReductionModeOff; denoise_mode = controls::draft::NoiseReductionModeOff;
} }
ctrls.set(controls::draft::NoiseReductionMode, denoise_mode); ctrls->set(controls::draft::NoiseReductionMode, denoise_mode);
if (camp->params->shutter != 0) {
ctrls.set(controls::ExposureTime, camp->params->shutter);
}
int metering_mode; int metering_mode;
if (strcmp(camp->params->metering, "spot") == 0) { if (strcmp(params->metering, "spot") == 0) {
metering_mode = controls::MeteringSpot; metering_mode = controls::MeteringSpot;
} else if (strcmp(camp->params->metering, "matrix") == 0) { } else if (strcmp(params->metering, "matrix") == 0) {
metering_mode = controls::MeteringMatrix; metering_mode = controls::MeteringMatrix;
} else if (strcmp(camp->params->metering, "custom") == 0) { } else if (strcmp(params->metering, "custom") == 0) {
metering_mode = controls::MeteringCustom; metering_mode = controls::MeteringCustom;
} else { } else {
metering_mode = controls::MeteringCentreWeighted; metering_mode = controls::MeteringCentreWeighted;
} }
ctrls.set(controls::AeMeteringMode, metering_mode); ctrls->set(controls::AeMeteringMode, metering_mode);
if (camp->params->gain > 0) { ctrls->set(controls::ExposureValue, params->ev);
ctrls.set(controls::AnalogueGain, camp->params->gain);
int64_t frame_time = 1000000 / params->fps;
ctrls->set(controls::FrameDurationLimits, Span<const int64_t, 2>({ frame_time, frame_time }));
} }
ctrls.set(controls::ExposureValue, camp->params->ev); bool camera_start(camera_t *cam) {
CameraPriv *camp = (CameraPriv *)cam;
if (camp->params->roi != NULL) { camp->ctrls = std::make_unique<ControlList>(controls::controls);
std::optional<Rectangle> opt = camp->camera->properties().get(properties::ScalerCropMaximum);
Rectangle sensor_area;
try {
sensor_area = opt.value();
} catch(const std::bad_optional_access& exc) {
set_error("get(ScalerCropMaximum) failed");
return false;
}
Rectangle crop( fill_dynamic_controls(camp->ctrls.get(), camp->params);
camp->params->roi->x * sensor_area.width,
camp->params->roi->y * sensor_area.height, if (camp->params->shutter != 0) {
camp->params->roi->width * sensor_area.width, camp->ctrls->set(controls::ExposureTime, camp->params->shutter);
camp->params->roi->height * sensor_area.height);
crop.translateBy(sensor_area.topLeft());
ctrls.set(controls::ScalerCrop, crop);
} }
int64_t frame_time = 1000000 / camp->params->fps; if (camp->params->gain > 0) {
ctrls.set(controls::FrameDurationLimits, Span<const int64_t, 2>({ frame_time, frame_time })); camp->ctrls->set(controls::AnalogueGain, camp->params->gain);
}
if (camp->camera->controls().count(&controls::AfMode) > 0) {
int af_mode; int af_mode;
if (strcmp(camp->params->af_mode, "manual") == 0) { if (strcmp(camp->params->af_mode, "manual") == 0) {
af_mode = controls::AfModeManual; af_mode = controls::AfModeManual;
} else if (strcmp(camp->params->af_mode, "auto") == 0) {
af_mode = controls::AfModeAuto;
} else if (strcmp(camp->params->af_mode, "continuous") == 0) { } else if (strcmp(camp->params->af_mode, "continuous") == 0) {
af_mode = controls::AfModeContinuous; af_mode = controls::AfModeContinuous;
} else { } else {
af_mode = controls::AfModeManual; af_mode = controls::AfModeAuto;
}
camp->ctrls->set(controls::AfMode, af_mode);
if (af_mode == controls::AfModeManual) {
camp->ctrls->set(controls::LensPosition, camp->params->lens_position);
}
} }
ctrls.set(controls::AfMode, af_mode);
if (camp->camera->controls().count(&controls::AfRange) > 0) {
int af_range; int af_range;
if (strcmp(camp->params->af_range, "normal") == 0) { if (strcmp(camp->params->af_range, "macro") == 0) {
af_range = controls::AfRangeNormal;
} else if (strcmp(camp->params->af_range, "macro") == 0) {
af_range = controls::AfRangeMacro; af_range = controls::AfRangeMacro;
} else if (strcmp(camp->params->af_range, "full") == 0) { } else if (strcmp(camp->params->af_range, "full") == 0) {
af_range = controls::AfRangeFull; af_range = controls::AfRangeFull;
} else { } else {
af_range = controls::AfRangeNormal; af_range = controls::AfRangeNormal;
} }
ctrls.set(controls::AfRange, af_range); camp->ctrls->set(controls::AfRange, af_range);
}
if (camp->camera->controls().count(&controls::AfSpeed) > 0) {
int af_speed; int af_speed;
if (strcmp(camp->params->af_range, "normal") == 0) { if (strcmp(camp->params->af_range, "fast") == 0) {
af_speed = controls::AfSpeedNormal;
} else if (strcmp(camp->params->af_range, "fast") == 0) {
af_speed = controls::AfSpeedFast; af_speed = controls::AfSpeedFast;
} else { } else {
af_speed = controls::AfSpeedNormal; af_speed = controls::AfSpeedNormal;
} }
ctrls.set(controls::AfSpeed, af_speed); camp->ctrls->set(controls::AfSpeed, af_speed);
}
if (camp->params->roi != NULL) {
std::optional<Rectangle> opt = camp->camera->properties().get(properties::ScalerCropMaximum);
Rectangle sensor_area;
try {
sensor_area = opt.value();
} catch(const std::bad_optional_access& exc) {
set_error("get(ScalerCropMaximum) failed");
return false;
}
// Lens Position Rectangle crop(
ctrls.set(controls::LensPosition, camp->params->lens_position); camp->params->roi->x * sensor_area.width,
camp->params->roi->y * sensor_area.height,
camp->params->roi->width * sensor_area.width,
camp->params->roi->height * sensor_area.height);
crop.translateBy(sensor_area.topLeft());
camp->ctrls->set(controls::ScalerCrop, crop);
}
// Af Window
if (camp->params->af_window != NULL) { if (camp->params->af_window != NULL) {
std::optional<Rectangle> opt = camp->camera->properties().get(properties::ScalerCropMaximum); std::optional<Rectangle> opt = camp->camera->properties().get(properties::ScalerCropMaximum);
Rectangle sensor_area; Rectangle sensor_area;
@ -392,19 +405,18 @@ bool camera_start(camera_t *cam) {
camp->params->af_window->height * sensor_area.height); camp->params->af_window->height * sensor_area.height);
afwindows_rectangle[0].translateBy(sensor_area.topLeft()); afwindows_rectangle[0].translateBy(sensor_area.topLeft());
//activate the AfMeteringWindows camp->ctrls->set(controls::AfMetering, controls::AfMeteringWindows);
ctrls.set(controls::AfMetering, controls::AfMeteringWindows); camp->ctrls->set(controls::AfWindows, afwindows_rectangle);
//set window
ctrls.set(controls::AfWindows, afwindows_rectangle);
} }
int res = camp->camera->start(camp->ctrls.get());
int res = camp->camera->start(&ctrls);
if (res != 0) { if (res != 0) {
set_error("Camera.start() failed"); set_error("Camera.start() failed");
return false; return false;
} }
camp->ctrls->clear();
camp->camera->requestCompleted.connect(on_request_complete); camp->camera->requestCompleted.connect(on_request_complete);
for (std::unique_ptr<Request> &request : camp->requests) { for (std::unique_ptr<Request> &request : camp->requests) {
@ -417,3 +429,10 @@ bool camera_start(camera_t *cam) {
return true; return true;
} }
void camera_reload_params(camera_t *cam, const parameters_t *params) {
CameraPriv *camp = (CameraPriv *)cam;
std::lock_guard<std::mutex> lock(camp->ctrls_mutex);
fill_dynamic_controls(camp->ctrls.get(), params);
}

3
internal/rpicamera/exe/camera.h

@ -10,10 +10,11 @@ extern "C" {
#endif #endif
const char *camera_get_error(); const char *camera_get_error();
bool camera_create(parameters_t *params, camera_frame_cb frame_cb, camera_t **cam); bool camera_create(const parameters_t *params, camera_frame_cb frame_cb, camera_t **cam);
int camera_get_mode_stride(camera_t *cam); int camera_get_mode_stride(camera_t *cam);
int camera_get_mode_colorspace(camera_t *cam); int camera_get_mode_colorspace(camera_t *cam);
bool camera_start(camera_t *cam); bool camera_start(camera_t *cam);
void camera_reload_params(camera_t *cam, const parameters_t *params);
#ifdef __cplusplus #ifdef __cplusplus
} }

4
internal/rpicamera/exe/encoder.c

@ -33,7 +33,7 @@ const char *encoder_get_error() {
} }
typedef struct { typedef struct {
parameters_t *params; const parameters_t *params;
int fd; int fd;
void **capture_buffers; void **capture_buffers;
int cur_buffer; int cur_buffer;
@ -112,7 +112,7 @@ static void *output_thread(void *userdata) {
return NULL; return NULL;
} }
bool encoder_create(parameters_t *params, int stride, int colorspace, encoder_output_cb output_cb, encoder_t **enc) { bool encoder_create(const parameters_t *params, int stride, int colorspace, encoder_output_cb output_cb, encoder_t **enc) {
*enc = malloc(sizeof(encoder_priv_t)); *enc = malloc(sizeof(encoder_priv_t));
encoder_priv_t *encp = (encoder_priv_t *)(*enc); encoder_priv_t *encp = (encoder_priv_t *)(*enc);

2
internal/rpicamera/exe/encoder.h

@ -6,7 +6,7 @@ typedef void encoder_t;
typedef void (*encoder_output_cb)(uint64_t ts, const uint8_t *buf, uint64_t size); typedef void (*encoder_output_cb)(uint64_t ts, const uint8_t *buf, uint64_t size);
const char *encoder_get_error(); const char *encoder_get_error();
bool encoder_create(parameters_t *params, int stride, int colorspace, encoder_output_cb output_cb, encoder_t **enc); bool encoder_create(const parameters_t *params, int stride, int colorspace, encoder_output_cb output_cb, encoder_t **enc);
void encoder_encode(encoder_t *enc, int buffer_fd, size_t size, int64_t timestamp_us); void encoder_encode(encoder_t *enc, int buffer_fd, size_t size, int64_t timestamp_us);
#endif #endif

110
internal/rpicamera/exe/main.c

@ -3,88 +3,53 @@
#include <stdarg.h> #include <stdarg.h>
#include <stdint.h> #include <stdint.h>
#include <stdlib.h> #include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <string.h> #include <string.h>
#include <pthread.h> #include <pthread.h>
#include "parameters.h" #include "parameters.h"
#include "pipe.h"
#include "camera.h" #include "camera.h"
#include "encoder.h" #include "encoder.h"
int pipe_fd; int pipe_video_fd;
pthread_mutex_t pipe_mutex; pthread_mutex_t pipe_video_mutex;
parameters_t params;
camera_t *cam;
encoder_t *enc; encoder_t *enc;
static void pipe_write_error(int fd, const char *format, ...) {
char buf[256];
buf[0] = 'e';
va_list args;
va_start(args, format);
vsnprintf(&buf[1], 255, format, args);
int n = strlen(buf);
write(fd, &n, 4);
write(fd, buf, n);
}
static void pipe_write_ready(int fd) {
char buf[] = {'r'};
int n = 1;
write(fd, &n, 4);
write(fd, buf, n);
}
static void pipe_write_buf(int fd, uint64_t ts, const uint8_t *buf, int n) {
char head[] = {'b'};
n += 1 + sizeof(uint64_t);
write(fd, &n, 4);
write(fd, head, 1);
write(fd, &ts, sizeof(uint64_t));
write(fd, buf, n - 1 - sizeof(uint64_t));
}
static void on_frame(int buffer_fd, uint64_t size, uint64_t timestamp) { static void on_frame(int buffer_fd, uint64_t size, uint64_t timestamp) {
encoder_encode(enc, buffer_fd, size, timestamp); encoder_encode(enc, buffer_fd, size, timestamp);
} }
static void on_encoder_output(uint64_t ts, const uint8_t *buf, uint64_t size) { static void on_encoder_output(uint64_t ts, const uint8_t *buf, uint64_t size) {
pthread_mutex_lock(&pipe_mutex); pthread_mutex_lock(&pipe_video_mutex);
pipe_write_buf(pipe_fd, ts, buf, size); pipe_write_buf(pipe_video_fd, ts, buf, size);
pthread_mutex_unlock(&pipe_mutex); pthread_mutex_unlock(&pipe_video_mutex);
}
static bool init_siglistener(sigset_t *set) {
sigemptyset(set);
int res = sigaddset(set, SIGKILL);
if (res == -1) {
return false;
}
return true;
} }
int main() { int main() {
pipe_fd = atoi(getenv("PIPE_FD")); int pipe_conf_fd = atoi(getenv("PIPE_CONF_FD"));
pipe_video_fd = atoi(getenv("PIPE_VIDEO_FD"));
pthread_mutex_init(&pipe_mutex, NULL); uint8_t *buf;
pthread_mutex_lock(&pipe_mutex); uint32_t n = pipe_read(pipe_conf_fd, &buf);
bool ok = parameters_load(&params); parameters_t params;
bool ok = parameters_unserialize(&params, &buf[1], n-1);
free(buf);
if (!ok) { if (!ok) {
pipe_write_error(pipe_fd, "parameters_load(): %s", parameters_get_error()); pipe_write_error(pipe_video_fd, "parameters_unserialize(): %s", parameters_get_error());
return 5; return 5;
} }
pthread_mutex_init(&pipe_video_mutex, NULL);
pthread_mutex_lock(&pipe_video_mutex);
camera_t *cam;
ok = camera_create( ok = camera_create(
&params, &params,
on_frame, on_frame,
&cam); &cam);
if (!ok) { if (!ok) {
pipe_write_error(pipe_fd, "camera_create(): %s", camera_get_error()); pipe_write_error(pipe_video_fd, "camera_create(): %s", camera_get_error());
return 5; return 5;
} }
@ -95,28 +60,41 @@ int main() {
on_encoder_output, on_encoder_output,
&enc); &enc);
if (!ok) { if (!ok) {
pipe_write_error(pipe_fd, "encoder_create(): %s", encoder_get_error()); pipe_write_error(pipe_video_fd, "encoder_create(): %s", encoder_get_error());
return 5; return 5;
} }
ok = camera_start(cam); ok = camera_start(cam);
if (!ok) { if (!ok) {
pipe_write_error(pipe_fd, "camera_start(): %s", camera_get_error()); pipe_write_error(pipe_video_fd, "camera_start(): %s", camera_get_error());
return 5; return 5;
} }
sigset_t set; pipe_write_ready(pipe_video_fd);
ok = init_siglistener(&set); pthread_mutex_unlock(&pipe_video_mutex);
if (!ok) {
pipe_write_error(pipe_fd, "init_siglistener() failed"); while (true) {
return 5; uint8_t *buf;
} uint32_t n = pipe_read(pipe_conf_fd, &buf);
pipe_write_ready(pipe_fd); switch (buf[0]) {
pthread_mutex_unlock(&pipe_mutex); case 'e':
return 0;
int sig; case 'c':
sigwait(&set, &sig); {
parameters_t params;
bool ok = parameters_unserialize(&params, &buf[1], n-1);
free(buf);
if (!ok) {
printf("skipping reloading parameters since they are invalid: %s\n", parameters_get_error());
continue;
}
camera_reload_params(cam, &params);
parameters_destroy(&params);
}
}
}
return 0; return 0;
} }

155
internal/rpicamera/exe/parameters.c

@ -20,26 +20,53 @@ const char *parameters_get_error() {
return errbuf; return errbuf;
} }
bool parameters_load(parameters_t *params) { bool parameters_unserialize(parameters_t *params, const uint8_t *buf, size_t buf_size) {
params->camera_id = atoi(getenv("CAMERA_ID")); char *tmp = malloc(buf_size + 1);
params->width = atoi(getenv("WIDTH")); memcpy(tmp, buf, buf_size);
params->height = atoi(getenv("HEIGHT")); tmp[buf_size] = 0x00;
params->h_flip = (strcmp(getenv("H_FLIP"), "1") == 0);
params->v_flip = (strcmp(getenv("V_FLIP"), "1") == 0); char *entry;
params->brightness = atof(getenv("BRIGHTNESS"));
params->contrast = atof(getenv("CONTRAST")); while ((entry = strsep(&tmp, " ")) != NULL) {
params->saturation = atof(getenv("SATURATION")); char *key = strsep(&entry, "=");
params->sharpness = atof(getenv("SHARPNESS")); char *val = strsep(&entry, "=");
params->exposure = getenv("EXPOSURE");
params->awb = getenv("AWB"); if (strcmp(key, "CameraID") == 0) {
params->denoise = getenv("DENOISE"); params->camera_id = atoi(val);
params->shutter = atoi(getenv("SHUTTER")); } else if (strcmp(key, "Width") == 0) {
params->metering = getenv("METERING"); params->width = atoi(val);
params->gain = atof(getenv("GAIN")); } else if (strcmp(key, "Height") == 0) {
params->ev = atof(getenv("EV")); params->height = atoi(val);
} else if (strcmp(key, "HFlip") == 0) {
if (strlen(getenv("ROI")) != 0) { params->h_flip = (strcmp(val, "1") == 0);
bool ok = window_load(getenv("ROI"), &params->roi); } else if (strcmp(key, "VFlip") == 0) {
params->v_flip = (strcmp(val, "1") == 0);
} else if (strcmp(key, "Brightness") == 0) {
params->brightness = atof(val);
} else if (strcmp(key, "Contrast") == 0) {
params->contrast = atof(val);
} else if (strcmp(key, "Saturation") == 0) {
params->saturation = atof(val);
} else if (strcmp(key, "Sharpness") == 0) {
params->sharpness = atof(val);
} else if (strcmp(key, "Exposure") == 0) {
params->exposure = strdup(val);
} else if (strcmp(key, "AWB") == 0) {
params->awb = strdup(val);
} else if (strcmp(key, "Denoise") == 0) {
params->denoise = strdup(val);
} else if (strcmp(key, "Shutter") == 0) {
params->shutter = atoi(val);
} else if (strcmp(key, "Metering") == 0) {
params->metering = strdup(val);
} else if (strcmp(key, "Gain") == 0) {
params->gain = atof(val);
} else if (strcmp(key, "EV") == 0) {
params->ev = atof(val);
} else if (strcmp(key, "ROI") == 0) {
if (strlen(val) != 0) {
params->roi = malloc(sizeof(window_t));
bool ok = window_load(val, params->roi);
if (!ok) { if (!ok) {
set_error("invalid ROI"); set_error("invalid ROI");
return false; return false;
@ -47,11 +74,12 @@ bool parameters_load(parameters_t *params) {
} else { } else {
params->roi = NULL; params->roi = NULL;
} }
} else if (strcmp(key, "TuningFile") == 0) {
params->tuning_file = getenv("TUNING_FILE"); params->tuning_file = strdup(val);
} else if (strcmp(key, "Mode") == 0) {
if (strlen(getenv("MODE")) != 0) { if (strlen(val) != 0) {
bool ok = sensor_mode_load(getenv("MODE"), &params->mode); params->mode = malloc(sizeof(sensor_mode_t));
bool ok = sensor_mode_load(val, params->mode);
if (!ok) { if (!ok) {
set_error("invalid sensor mode"); set_error("invalid sensor mode");
return false; return false;
@ -59,46 +87,75 @@ bool parameters_load(parameters_t *params) {
} else { } else {
params->mode = NULL; params->mode = NULL;
} }
} else if (strcmp(key, "FPS") == 0) {
params->fps = atoi(getenv("FPS")); params->fps = atoi(val);
params->idr_period = atoi(getenv("IDR_PERIOD")); } else if (strcmp(key, "IDRPeriod") == 0) {
params->bitrate = atoi(getenv("BITRATE")); params->idr_period = atoi(val);
} else if (strcmp(key, "Bitrate") == 0) {
const char *profile = getenv("PROFILE"); params->bitrate = atoi(val);
if (strcmp(profile, "baseline") == 0) { } else if (strcmp(key, "Profile") == 0) {
if (strcmp(val, "baseline") == 0) {
params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE; params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE;
} else if (strcmp(profile, "main") == 0) { } else if (strcmp(val, "main") == 0) {
params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_MAIN; params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_MAIN;
} else { } else {
params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH; params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH;
} }
} else if (strcmp(key, "Level") == 0) {
const char *level = getenv("LEVEL"); if (strcmp(val, "4.0") == 0) {
if (strcmp(level, "4.0") == 0) {
params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_0; params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_0;
} else if (strcmp(level, "4.1") == 0) { } else if (strcmp(val, "4.1") == 0) {
params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_1; params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_1;
} else { } else {
params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_2; params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_2;
} }
} else if (strcmp(key, "AfMode") == 0) {
params->buffer_count = 6; params->af_mode = strdup(val);
params->capture_buffer_count = params->buffer_count * 2; } else if (strcmp(key, "AfRange") == 0) {
params->af_range = strdup(val);
params->af_mode = getenv("AF_MODE"); } else if (strcmp(key, "AfSpeed") == 0) {
params->af_range = getenv("AF_RANGE"); params->af_speed = strdup(val);
params->af_speed = getenv("AF_SPEED"); } else if (strcmp(key, "LensPosition") == 0) {
params->lens_position = atof(getenv("LENS_POSITION")); params->lens_position = atof(val);
} else if (strcmp(key, "AfWindow") == 0) {
if (strlen(getenv("AF_WINDOW")) != 0) { if (strlen(val) != 0) {
bool ok = window_load(getenv("AF_WINDOW"), &params->af_window); params->af_window = malloc(sizeof(window_t));
bool ok = window_load(val, params->af_window);
if (!ok) { if (!ok) {
set_error("invalid AF_WINDOW"); set_error("invalid AfWindow");
return false; return false;
} }
} else { } else {
params->af_window = NULL; params->af_window = NULL;
} }
}
}
free(tmp);
params->buffer_count = 6;
params->capture_buffer_count = params->buffer_count * 2;
return true; return true;
} }
void parameters_destroy(parameters_t *params) {
free(params->exposure);
free(params->awb);
free(params->denoise);
free(params->metering);
free(params->tuning_file);
free(params->af_mode);
free(params->af_range);
free(params->af_speed);
if (params->roi != NULL) {
free(params->roi);
}
if (params->mode != NULL) {
free(params->mode);
}
if (params->af_window != NULL) {
free(params->af_window);
}
}

20
internal/rpicamera/exe/parameters.h

@ -1,6 +1,7 @@
#ifndef __PARAMETERS_H__ #ifndef __PARAMETERS_H__
#define __PARAMETERS_H__ #define __PARAMETERS_H__
#include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include "window.h" #include "window.h"
@ -16,24 +17,24 @@ typedef struct {
float contrast; float contrast;
float saturation; float saturation;
float sharpness; float sharpness;
const char *exposure; char *exposure;
const char *awb; char *awb;
const char *denoise; char *denoise;
unsigned int shutter; unsigned int shutter;
const char *metering; char *metering;
float gain; float gain;
float ev; float ev;
window_t *roi; window_t *roi;
const char *tuning_file; char *tuning_file;
sensor_mode_t *mode; sensor_mode_t *mode;
unsigned int fps; unsigned int fps;
unsigned int idr_period; unsigned int idr_period;
unsigned int bitrate; unsigned int bitrate;
unsigned int profile; unsigned int profile;
unsigned int level; unsigned int level;
const char *af_mode; char *af_mode;
const char *af_range; char *af_range;
const char *af_speed; char *af_speed;
float lens_position; float lens_position;
window_t *af_window; window_t *af_window;
@ -47,7 +48,8 @@ extern "C" {
#endif #endif
const char *parameters_get_error(); const char *parameters_get_error();
bool parameters_load(parameters_t *params); bool parameters_unserialize(parameters_t *params, const uint8_t *buf, size_t buf_size);
void parameters_destroy(parameters_t *params);
#ifdef __cplusplus #ifdef __cplusplus
} }

44
internal/rpicamera/exe/pipe.c

@ -0,0 +1,44 @@
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include "pipe.h"
void pipe_write_error(int fd, const char *format, ...) {
char buf[256];
buf[0] = 'e';
va_list args;
va_start(args, format);
vsnprintf(&buf[1], 255, format, args);
uint32_t n = strlen(buf);
write(fd, &n, 4);
write(fd, buf, n);
}
void pipe_write_ready(int fd) {
char buf[] = {'r'};
uint32_t n = 1;
write(fd, &n, 4);
write(fd, buf, n);
}
void pipe_write_buf(int fd, uint64_t ts, const uint8_t *buf, uint32_t n) {
char head[] = {'b'};
n += 1 + sizeof(uint64_t);
write(fd, &n, 4);
write(fd, head, 1);
write(fd, &ts, sizeof(uint64_t));
write(fd, buf, n - 1 - sizeof(uint64_t));
}
uint32_t pipe_read(int fd, uint8_t **pbuf) {
uint32_t n;
read(fd, &n, 4);
*pbuf = malloc(n);
read(fd, *pbuf, n);
return n;
}

12
internal/rpicamera/exe/pipe.h

@ -0,0 +1,12 @@
#ifndef __PIPE_H__
#define __PIPE_H__
#include <stdbool.h>
#include <stdint.h>
void pipe_write_error(int fd, const char *format, ...);
void pipe_write_ready(int fd);
void pipe_write_buf(int fd, uint64_t ts, const uint8_t *buf, uint32_t n);
uint32_t pipe_read(int fd, uint8_t **pbuf);
#endif

15
internal/rpicamera/exe/sensor_mode.c

@ -4,26 +4,23 @@
#include "sensor_mode.h" #include "sensor_mode.h"
bool sensor_mode_load(const char *encoded, sensor_mode_t **mode) { bool sensor_mode_load(const char *encoded, sensor_mode_t *mode) {
*mode = malloc(sizeof(sensor_mode_t));
char p; char p;
int n = sscanf(encoded, "%u:%u:%u:%c", &((*mode)->width), &((*mode)->height), &((*mode)->bit_depth), &p); int n = sscanf(encoded, "%u:%u:%u:%c", &(mode->width), &(mode->height), &(mode->bit_depth), &p);
if (n < 2) { if (n < 2) {
free(*mode);
return false; return false;
} }
if (n < 4) { if (n < 4) {
(*mode)->packed = true; mode->packed = true;
} else if (toupper(p) == 'P') { } else if (toupper(p) == 'P') {
(*mode)->packed = true; mode->packed = true;
} else if (toupper(p) == 'U') { } else if (toupper(p) == 'U') {
(*mode)->packed = false; mode->packed = false;
} }
if (n < 3) { if (n < 3) {
(*mode)->bit_depth = 12; mode->bit_depth = 12;
} }
return true; return true;

2
internal/rpicamera/exe/sensor_mode.h

@ -10,6 +10,6 @@ typedef struct {
bool packed; bool packed;
} sensor_mode_t; } sensor_mode_t;
bool sensor_mode_load(const char *encoded, sensor_mode_t **mode); bool sensor_mode_load(const char *encoded, sensor_mode_t *mode);
#endif #endif

12
internal/rpicamera/exe/window.c

@ -3,7 +3,7 @@
#include "window.h" #include "window.h"
bool window_load(const char *encoded, window_t **mode) { bool window_load(const char *encoded, window_t *window) {
float vals[4]; float vals[4];
int i = 0; int i = 0;
char *token = strtok((char *)encoded, ","); char *token = strtok((char *)encoded, ",");
@ -21,10 +21,10 @@ bool window_load(const char *encoded, window_t **mode) {
return false; return false;
} }
*mode = malloc(sizeof(window_t)); window->x = vals[0];
(*mode)->x = vals[0]; window->y = vals[1];
(*mode)->y = vals[1]; window->width = vals[2];
(*mode)->width = vals[2]; window->height = vals[3];
(*mode)->height = vals[3];
return true; return true;
} }

2
internal/rpicamera/exe/window.h

@ -10,6 +10,6 @@ typedef struct {
float height; float height;
} window_t; } window_t;
bool window_load(const char *encoded, window_t **mode); bool window_load(const char *encoded, window_t *window);
#endif #endif

20
internal/rpicamera/pipe.go

@ -4,7 +4,6 @@
package rpicamera package rpicamera
import ( import (
"encoding/binary"
"syscall" "syscall"
) )
@ -51,14 +50,14 @@ func (p *pipe) close() {
} }
func (p *pipe) read() ([]byte, error) { func (p *pipe) read() ([]byte, error) {
sizebuf := make([]byte, 4) buf := make([]byte, 4)
err := syscallReadAll(p.readFD, sizebuf) err := syscallReadAll(p.readFD, buf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
size := int(binary.LittleEndian.Uint32(sizebuf)) le := int(buf[3])<<24 | int(buf[2])<<16 | int(buf[1])<<8 | int(buf[0])
buf := make([]byte, size) buf = make([]byte, le)
err = syscallReadAll(p.readFD, buf) err = syscallReadAll(p.readFD, buf)
if err != nil { if err != nil {
@ -67,3 +66,14 @@ func (p *pipe) read() ([]byte, error) {
return buf, nil return buf, nil
} }
func (p *pipe) write(byts []byte) error {
le := len(byts)
_, err := syscall.Write(p.writeFD, []byte{byte(le), byte(le >> 8), byte(le >> 16), byte(le >> 24)})
if err != nil {
return err
}
_, err = syscall.Write(p.writeFD, byts)
return err
}

234
internal/rpicamera/rpicamera.go

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"reflect"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@ -16,16 +17,13 @@ import (
"github.com/aler9/gortsplib/v2/pkg/codecs/h264" "github.com/aler9/gortsplib/v2/pkg/codecs/h264"
) )
const (
tempPathPrefix = "/dev/shm/rtspss-embeddedexe-"
)
//go:embed exe/exe //go:embed exe/exe
var exeContent []byte var exeContent []byte
func bool2env(v bool) string {
if v {
return "1"
}
return "0"
}
func getKernelArch() (string, error) { func getKernelArch() (string, error) {
cmd := exec.Command("uname", "-m") cmd := exec.Command("uname", "-m")
@ -89,11 +87,77 @@ func setupSymlinks() error {
return setupSymlink("libcamera-base") return setupSymlink("libcamera-base")
} }
func removeSymlinks() {
os.Remove("/dev/shm/libcamera-base.so.x.x.x")
os.Remove("/dev/shm/libcamera.so.x.x.x")
}
func startEmbeddedExe(content []byte, env []string) (*exec.Cmd, error) {
tempPath := tempPathPrefix + strconv.FormatInt(time.Now().UnixNano(), 10)
err := os.WriteFile(tempPath, content, 0o755)
if err != nil {
return nil, err
}
cmd := exec.Command(tempPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = env
err = cmd.Start()
os.Remove(tempPath)
if err != nil {
return nil, err
}
return cmd, nil
}
func serializeParams(p Params) []byte {
rv := reflect.ValueOf(p)
rt := rv.Type()
nf := rv.NumField()
ret := make([]string, nf)
for i := 0; i < nf; i++ {
entry := rt.Field(i).Name + "="
f := rv.Field(i)
switch f.Kind() {
case reflect.Int:
entry += strconv.FormatInt(f.Int(), 10)
case reflect.Float64:
entry += strconv.FormatFloat(f.Float(), 'f', -1, 64)
case reflect.String:
entry += f.String()
case reflect.Bool:
if f.Bool() {
entry += "1"
} else {
entry += "0"
}
default:
panic("unhandled type")
}
ret[i] = entry
}
return []byte(strings.Join(ret, " "))
}
type RPICamera struct { type RPICamera struct {
onData func(time.Duration, [][]byte) onData func(time.Duration, [][]byte)
exe *embeddedExe cmd *exec.Cmd
pipe *pipe pipeConf *pipe
pipeVideo *pipe
waitDone chan error waitDone chan error
readerDone chan error readerDone chan error
@ -113,60 +177,88 @@ func New(
return nil, err return nil, err
} }
pipe, err := newPipe() c := &RPICamera{
onData: onData,
}
c.pipeConf, err = newPipe()
if err != nil { if err != nil {
return nil, err return nil, err
} }
c.pipeVideo, err = newPipe()
if err != nil {
c.pipeConf.close()
return nil, err
}
env := []string{ env := []string{
"LD_LIBRARY_PATH=/dev/shm", "LD_LIBRARY_PATH=/dev/shm",
"PIPE_FD=" + strconv.FormatInt(int64(pipe.writeFD), 10), "PIPE_CONF_FD=" + strconv.FormatInt(int64(c.pipeConf.readFD), 10),
"CAMERA_ID=" + strconv.FormatInt(int64(params.CameraID), 10), "PIPE_VIDEO_FD=" + strconv.FormatInt(int64(c.pipeVideo.writeFD), 10),
"WIDTH=" + strconv.FormatInt(int64(params.Width), 10), }
"HEIGHT=" + strconv.FormatInt(int64(params.Height), 10),
"H_FLIP=" + bool2env(params.HFlip), c.cmd, err = startEmbeddedExe(exeContent, env)
"V_FLIP=" + bool2env(params.VFlip),
"BRIGHTNESS=" + strconv.FormatFloat(params.Brightness, 'f', -1, 64),
"CONTRAST=" + strconv.FormatFloat(params.Contrast, 'f', -1, 64),
"SATURATION=" + strconv.FormatFloat(params.Saturation, 'f', -1, 64),
"SHARPNESS=" + strconv.FormatFloat(params.Sharpness, 'f', -1, 64),
"EXPOSURE=" + params.Exposure,
"AWB=" + params.AWB,
"DENOISE=" + params.Denoise,
"SHUTTER=" + strconv.FormatInt(int64(params.Shutter), 10),
"METERING=" + params.Metering,
"GAIN=" + strconv.FormatFloat(params.Gain, 'f', -1, 64),
"EV=" + strconv.FormatFloat(params.EV, 'f', -1, 64),
"ROI=" + params.ROI,
"TUNING_FILE=" + params.TuningFile,
"MODE=" + params.Mode,
"FPS=" + strconv.FormatInt(int64(params.FPS), 10),
"IDR_PERIOD=" + strconv.FormatInt(int64(params.IDRPeriod), 10),
"BITRATE=" + strconv.FormatInt(int64(params.Bitrate), 10),
"PROFILE=" + params.Profile,
"LEVEL=" + params.Level,
"AF_MODE=" + params.AfMode,
"AF_RANGE=" + params.AfRange,
"AF_SPEED=" + params.AfSpeed,
"LENS_POSITION=" + strconv.FormatFloat(params.LensPosition, 'f', -1, 64),
"AF_WINDOW=" + params.AfWindow,
}
exe, err := newEmbeddedExe(exeContent, env)
if err != nil { if err != nil {
pipe.close() removeSymlinks()
c.pipeConf.close()
c.pipeVideo.close()
return nil, err return nil, err
} }
waitDone := make(chan error) removeSymlinks()
c.pipeConf.write(append([]byte{'c'}, serializeParams(params)...))
c.waitDone = make(chan error)
go func() { go func() {
waitDone <- exe.cmd.Wait() c.waitDone <- c.cmd.Wait()
}() }()
readerDone := make(chan error) c.readerDone = make(chan error)
go func() { go func() {
readerDone <- func() error { c.readerDone <- c.readReady()
buf, err := pipe.read() }()
select {
case <-c.waitDone:
c.pipeConf.close()
c.pipeVideo.close()
<-c.readerDone
return nil, fmt.Errorf("process exited unexpectedly")
case err := <-c.readerDone:
if err != nil {
c.pipeConf.write([]byte{'e'})
<-c.waitDone
c.pipeConf.close()
c.pipeVideo.close()
return nil, err
}
}
c.readerDone = make(chan error)
go func() {
c.readerDone <- c.readData()
}()
return c, nil
}
func (c *RPICamera) Close() {
c.pipeConf.write([]byte{'e'})
<-c.waitDone
c.pipeConf.close()
c.pipeVideo.close()
<-c.readerDone
}
func (c *RPICamera) ReloadParams(params Params) {
c.pipeConf.write(append([]byte{'c'}, serializeParams(params)...))
}
func (c *RPICamera) readReady() error {
buf, err := c.pipeVideo.read()
if err != nil { if err != nil {
return err return err
} }
@ -179,32 +271,13 @@ func New(
return nil return nil
default: default:
return fmt.Errorf("unexpected output from pipe (%c)", buf[0]) return fmt.Errorf("unexpected output from video pipe: '0x%.2x'", buf[0])
}
}()
}()
select {
case <-waitDone:
exe.close()
pipe.close()
<-readerDone
return nil, fmt.Errorf("process exited unexpectedly")
case err := <-readerDone:
if err != nil {
exe.close()
<-waitDone
pipe.close()
return nil, err
} }
} }
readerDone = make(chan error) func (c *RPICamera) readData() error {
go func() {
readerDone <- func() error {
for { for {
buf, err := pipe.read() buf, err := c.pipeVideo.read()
if err != nil { if err != nil {
return err return err
} }
@ -222,23 +295,6 @@ func New(
return err return err
} }
onData(dts, nalus) c.onData(dts, nalus)
} }
}()
}()
return &RPICamera{
onData: onData,
exe: exe,
pipe: pipe,
waitDone: waitDone,
readerDone: readerDone,
}, nil
}
func (c *RPICamera) Close() {
c.exe.close()
<-c.waitDone
c.pipe.close()
<-c.readerDone
} }

4
internal/rpicamera/rpicamera_disabled.go

@ -23,3 +23,7 @@ func New(
// Close closes a RPICamera. // Close closes a RPICamera.
func (c *RPICamera) Close() { func (c *RPICamera) Close() {
} }
// ReloadParams reloads the camera parameters.
func (c *RPICamera) ReloadParams(params Params) {
}

4
rtsp-simple-server.yml

@ -328,8 +328,8 @@ paths:
# H264 level # H264 level
rpiCameraLevel: '4.1' rpiCameraLevel: '4.1'
# Autofocus mode # Autofocus mode
# values: manual, auto, continuous # values: auto, manual, continuous
rpiCameraAfMode: continuous rpiCameraAfMode: auto
# Autofocus range # Autofocus range
# values: normal, macro, full # values: normal, macro, full
rpiCameraAfRange: normal rpiCameraAfRange: normal

Loading…
Cancel
Save