mirror of
https://github.com/bluenviron/mediamtx.git
synced 2025-12-20 02:00:05 -08:00
rpi: support exposing a secondary stream from the same camera (#4426)
This commit is contained in:
parent
9579989eff
commit
8ce49727d6
18 changed files with 619 additions and 282 deletions
|
|
@ -5,4 +5,4 @@
|
||||||
/apidocs/*.html
|
/apidocs/*.html
|
||||||
/internal/core/VERSION
|
/internal/core/VERSION
|
||||||
/internal/servers/hls/hls.min.js
|
/internal/servers/hls/hls.min.js
|
||||||
/internal/staticsources/rpicamera/mtxrpicam_*
|
/internal/staticsources/rpicamera/mtxrpicam_*/
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -4,4 +4,4 @@
|
||||||
/apidocs/*.html
|
/apidocs/*.html
|
||||||
/internal/core/VERSION
|
/internal/core/VERSION
|
||||||
/internal/servers/hls/hls.min.js
|
/internal/servers/hls/hls.min.js
|
||||||
/internal/staticsources/rpicamera/mtxrpicam_*
|
/internal/staticsources/rpicamera/mtxrpicam_*/
|
||||||
|
|
|
||||||
73
README.md
73
README.md
|
|
@ -88,6 +88,8 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi
|
||||||
* [By device](#by-device)
|
* [By device](#by-device)
|
||||||
* [Generic webcam](#generic-webcam)
|
* [Generic webcam](#generic-webcam)
|
||||||
* [Raspberry Pi Cameras](#raspberry-pi-cameras)
|
* [Raspberry Pi Cameras](#raspberry-pi-cameras)
|
||||||
|
* [Adding audio](#adding-audio)
|
||||||
|
* [Secondary stream](#secondary-stream)
|
||||||
* [By protocol](#by-protocol)
|
* [By protocol](#by-protocol)
|
||||||
* [SRT clients](#srt-clients)
|
* [SRT clients](#srt-clients)
|
||||||
* [SRT cameras and servers](#srt-cameras-and-servers)
|
* [SRT cameras and servers](#srt-cameras-and-servers)
|
||||||
|
|
@ -274,7 +276,7 @@ The RTSP protocol supports multiple underlying transport protocols, each with it
|
||||||
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp -rtsp_transport tcp rtsp://localhost:8554/mystream
|
ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp -rtsp_transport tcp rtsp://localhost:8554/mystream
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/mystream`.
|
The resulting stream is available in path `/mystream`.
|
||||||
|
|
||||||
#### GStreamer
|
#### GStreamer
|
||||||
|
|
||||||
|
|
@ -301,7 +303,7 @@ gst-launch-1.0 filesrc location=file.mp4 ! qtdemux name=d \
|
||||||
d.video_0 ! rtspclientsink protocols=tcp name=s location=rtsp://localhost:8554/mystream
|
d.video_0 ! rtspclientsink protocols=tcp name=s location=rtsp://localhost:8554/mystream
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/mystream`.
|
The resulting stream is available in path `/mystream`.
|
||||||
|
|
||||||
GStreamer can also publish a stream by using the [WebRTC / WHIP protocol](#webrtc). Make sure that GStreamer version is at least 1.22, and that if the codec is H264, the profile is baseline. Use the `whipclientsink` element:
|
GStreamer can also publish a stream by using the [WebRTC / WHIP protocol](#webrtc). Make sure that GStreamer version is at least 1.22, and that if the codec is H264, the profile is baseline. Use the `whipclientsink` element:
|
||||||
|
|
||||||
|
|
@ -350,7 +352,7 @@ Latest versions of OBS Studio can publish to the server with the [WebRTC / WHIP
|
||||||
|
|
||||||
Save the configuration and click `Start streaming`.
|
Save the configuration and click `Start streaming`.
|
||||||
|
|
||||||
The resulting stream will be available in path `/mystream`.
|
The resulting stream is available in path `/mystream`.
|
||||||
|
|
||||||
#### OpenCV
|
#### OpenCV
|
||||||
|
|
||||||
|
|
@ -425,7 +427,7 @@ while True:
|
||||||
start = now
|
start = now
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/mystream`.
|
The resulting stream is available in path `/mystream`.
|
||||||
|
|
||||||
#### Unity
|
#### Unity
|
||||||
|
|
||||||
|
|
@ -536,7 +538,7 @@ public class WebRTCPublisher : MonoBehaviour
|
||||||
|
|
||||||
In the _Hierarchy_ window, find or create a scene and a camera, then add the `WebRTCPublisher.cs` script as component of the camera, by dragging it inside the _Inspector_ window. then Press the _Play_ button at the top of the page.
|
In the _Hierarchy_ window, find or create a scene and a camera, then add the `WebRTCPublisher.cs` script as component of the camera, by dragging it inside the _Inspector_ window. then Press the _Play_ button at the top of the page.
|
||||||
|
|
||||||
The resulting stream will be available in path `/unity`.
|
The resulting stream is available in path `/unity`.
|
||||||
|
|
||||||
#### Web browsers
|
#### Web browsers
|
||||||
|
|
||||||
|
|
@ -546,7 +548,7 @@ Web browsers can publish a stream to the server by using the [WebRTC protocol](#
|
||||||
http://localhost:8889/mystream/publish
|
http://localhost:8889/mystream/publish
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/mystream`.
|
The resulting stream is available in path `/mystream`.
|
||||||
|
|
||||||
This web page can be embedded into another web page by using an iframe:
|
This web page can be embedded into another web page by using an iframe:
|
||||||
|
|
||||||
|
|
@ -584,7 +586,7 @@ Where `USB2.0 HD UVC WebCam` is the name of a webcam, that can be obtained with:
|
||||||
ffmpeg -list_devices true -f dshow -i dummy
|
ffmpeg -list_devices true -f dshow -i dummy
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/cam`.
|
The resulting stream is available in path `/cam`.
|
||||||
|
|
||||||
#### Raspberry Pi Cameras
|
#### Raspberry Pi Cameras
|
||||||
|
|
||||||
|
|
@ -611,7 +613,7 @@ If you want to run the standard (non-Docker) version of the server:
|
||||||
source: rpiCamera
|
source: rpiCamera
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/cam`.
|
The resulting stream is available in path `/cam`.
|
||||||
|
|
||||||
If you want to run the server inside Docker, you need to use the `latest-rpi` image and launch the container with some additional flags:
|
If you want to run the server inside Docker, you need to use the `latest-rpi` image and launch the container with some additional flags:
|
||||||
|
|
||||||
|
|
@ -639,6 +641,8 @@ paths:
|
||||||
|
|
||||||
All available parameters are listed in the [sample configuration file](/mediamtx.yml).
|
All available parameters are listed in the [sample configuration file](/mediamtx.yml).
|
||||||
|
|
||||||
|
##### Adding audio
|
||||||
|
|
||||||
In order to add audio from a USB microfone, install GStreamer and alsa-utils:
|
In order to add audio from a USB microfone, install GStreamer and alsa-utils:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
@ -678,7 +682,42 @@ paths:
|
||||||
runOnInitRestart: yes
|
runOnInitRestart: yes
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/cam_with_audio`.
|
The resulting stream is available in path `/cam_with_audio`.
|
||||||
|
|
||||||
|
##### Secondary stream
|
||||||
|
|
||||||
|
It is possible to enable a secondary stream from the same camera, with a different resolution, FPS and codec. Configuration is the same of a primary stream, with `rpiCameraSecondary` set to `true` and parameters adjusted accordingly:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
paths:
|
||||||
|
# primary stream
|
||||||
|
rpi:
|
||||||
|
source: rpiCamera
|
||||||
|
# Width of frames.
|
||||||
|
rpiCameraWidth: 1920
|
||||||
|
# Height of frames.
|
||||||
|
rpiCameraHeight: 1080
|
||||||
|
# FPS.
|
||||||
|
rpiCameraFPS: 30
|
||||||
|
|
||||||
|
# secondary stream
|
||||||
|
secondary:
|
||||||
|
source: rpiCamera
|
||||||
|
# This is a secondary stream.
|
||||||
|
rpiCameraSecondary: true
|
||||||
|
# Width of frames.
|
||||||
|
rpiCameraWidth: 640
|
||||||
|
# Height of frames.
|
||||||
|
rpiCameraHeight: 480
|
||||||
|
# FPS.
|
||||||
|
rpiCameraFPS: 10
|
||||||
|
# Codec. in case of secondary streams, it defaults to M-JPEG.
|
||||||
|
rpiCameraCodec: auto
|
||||||
|
# JPEG quality.
|
||||||
|
rpiCameraJPEGQuality: 60
|
||||||
|
```
|
||||||
|
|
||||||
|
The secondary stream is available in path `/secondary`.
|
||||||
|
|
||||||
### By protocol
|
### By protocol
|
||||||
|
|
||||||
|
|
@ -690,7 +729,7 @@ SRT is a protocol that allows to publish and read live data stream, providing en
|
||||||
srt://localhost:8890?streamid=publish:mystream&pkt_size=1316
|
srt://localhost:8890?streamid=publish:mystream&pkt_size=1316
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace `mystream` with any name you want. The resulting stream will be available in path `/mystream`.
|
Replace `mystream` with any name you want. The resulting stream is available in path `/mystream`.
|
||||||
|
|
||||||
If credentials are enabled, append username and password to `streamid`:
|
If credentials are enabled, append username and password to `streamid`:
|
||||||
|
|
||||||
|
|
@ -723,7 +762,7 @@ WebRTC is an API that makes use of a set of protocols and methods to connect two
|
||||||
http://localhost:8889/mystream/publish
|
http://localhost:8889/mystream/publish
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/mystream`.
|
The resulting stream is available in path `/mystream`.
|
||||||
|
|
||||||
WHIP is a WebRTC extensions that allows to publish streams by using a URL, without passing through a web page. This allows to use WebRTC as a general purpose streaming protocol. If you are using a software that supports WHIP (for instance, latest versions of OBS Studio), you can publish a stream to the server by using this URL:
|
WHIP is a WebRTC extensions that allows to publish streams by using a URL, without passing through a web page. This allows to use WebRTC as a general purpose streaming protocol. If you are using a software that supports WHIP (for instance, latest versions of OBS Studio), you can publish a stream to the server by using this URL:
|
||||||
|
|
||||||
|
|
@ -756,7 +795,7 @@ RTSP is a protocol that allows to publish and read streams. It supports differen
|
||||||
rtsp://localhost:8554/mystream
|
rtsp://localhost:8554/mystream
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/mystream`.
|
The resulting stream is available in path `/mystream`.
|
||||||
|
|
||||||
Known clients that can publish with RTSP are [FFmpeg](#ffmpeg), [GStreamer](#gstreamer), [OBS Studio](#obs-studio).
|
Known clients that can publish with RTSP are [FFmpeg](#ffmpeg), [GStreamer](#gstreamer), [OBS Studio](#obs-studio).
|
||||||
|
|
||||||
|
|
@ -771,7 +810,7 @@ paths:
|
||||||
source: rtsp://original-url
|
source: rtsp://original-url
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/proxied`.
|
The resulting stream is available in path `/proxied`.
|
||||||
|
|
||||||
The server supports any number of source streams (count is just limited by available hardware resources) it's enough to add additional entries to the paths section:
|
The server supports any number of source streams (count is just limited by available hardware resources) it's enough to add additional entries to the paths section:
|
||||||
|
|
||||||
|
|
@ -792,7 +831,7 @@ RTMP is a protocol that allows to read and publish streams, but is less versatil
|
||||||
rtmp://localhost/mystream
|
rtmp://localhost/mystream
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/mystream`.
|
The resulting stream is available in path `/mystream`.
|
||||||
|
|
||||||
In case authentication is enabled, credentials can be passed to the server by using the `user` and `pass` query parameters:
|
In case authentication is enabled, credentials can be passed to the server by using the `user` and `pass` query parameters:
|
||||||
|
|
||||||
|
|
@ -813,7 +852,7 @@ paths:
|
||||||
source: rtmp://original-url
|
source: rtmp://original-url
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/proxied`.
|
The resulting stream is available in path `/proxied`.
|
||||||
|
|
||||||
#### HLS cameras and servers
|
#### HLS cameras and servers
|
||||||
|
|
||||||
|
|
@ -826,7 +865,7 @@ paths:
|
||||||
source: http://original-url/stream/index.m3u8
|
source: http://original-url/stream/index.m3u8
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/proxied`.
|
The resulting stream is available in path `/proxied`.
|
||||||
|
|
||||||
#### UDP/MPEG-TS
|
#### UDP/MPEG-TS
|
||||||
|
|
||||||
|
|
@ -854,7 +893,7 @@ paths:
|
||||||
source: udp://238.0.0.1:1234
|
source: udp://238.0.0.1:1234
|
||||||
```
|
```
|
||||||
|
|
||||||
The resulting stream will be available in path `/mypath`.
|
The resulting stream is available in path `/mypath`.
|
||||||
|
|
||||||
Known clients that can publish with WebRTC and WHIP are [FFmpeg](#ffmpeg) and [GStreamer](#gstreamer).
|
Known clients that can publish with WebRTC and WHIP are [FFmpeg](#ffmpeg) and [GStreamer](#gstreamer).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -364,6 +364,8 @@ components:
|
||||||
# Raspberry Pi Camera source
|
# Raspberry Pi Camera source
|
||||||
rpiCameraCamID:
|
rpiCameraCamID:
|
||||||
type: integer
|
type: integer
|
||||||
|
rpiCameraSecondary:
|
||||||
|
type: boolean
|
||||||
rpiCameraWidth:
|
rpiCameraWidth:
|
||||||
type: integer
|
type: integer
|
||||||
rpiCameraHeight:
|
rpiCameraHeight:
|
||||||
|
|
@ -436,6 +438,8 @@ components:
|
||||||
type: string
|
type: string
|
||||||
rpiCameraLevel:
|
rpiCameraLevel:
|
||||||
type: string
|
type: string
|
||||||
|
rpiCameraJPEGQuality:
|
||||||
|
type: integer
|
||||||
|
|
||||||
# Hooks
|
# Hooks
|
||||||
runOnInit:
|
runOnInit:
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,7 @@ func TestConfFromFile(t *testing.T) {
|
||||||
RPICameraBitrate: 5000000,
|
RPICameraBitrate: 5000000,
|
||||||
RPICameraProfile: "main",
|
RPICameraProfile: "main",
|
||||||
RPICameraLevel: "4.1",
|
RPICameraLevel: "4.1",
|
||||||
|
RPICameraJPEGQuality: 60,
|
||||||
RunOnDemandStartTimeout: 5 * Duration(time.Second),
|
RunOnDemandStartTimeout: 5 * Duration(time.Second),
|
||||||
RunOnDemandCloseAfter: 10 * Duration(time.Second),
|
RunOnDemandCloseAfter: 10 * Duration(time.Second),
|
||||||
}, pa)
|
}, pa)
|
||||||
|
|
|
||||||
|
|
@ -154,41 +154,48 @@ type Path struct {
|
||||||
SourceRedirect string `json:"sourceRedirect"`
|
SourceRedirect string `json:"sourceRedirect"`
|
||||||
|
|
||||||
// Raspberry Pi Camera source
|
// Raspberry Pi Camera source
|
||||||
RPICameraCamID uint `json:"rpiCameraCamID"`
|
RPICameraCamID uint `json:"rpiCameraCamID"`
|
||||||
RPICameraWidth uint `json:"rpiCameraWidth"`
|
RPICameraSecondary bool `json:"rpiCameraSecondary"`
|
||||||
RPICameraHeight uint `json:"rpiCameraHeight"`
|
RPICameraWidth uint `json:"rpiCameraWidth"`
|
||||||
RPICameraHFlip bool `json:"rpiCameraHFlip"`
|
RPICameraHeight uint `json:"rpiCameraHeight"`
|
||||||
RPICameraVFlip bool `json:"rpiCameraVFlip"`
|
RPICameraHFlip bool `json:"rpiCameraHFlip"`
|
||||||
RPICameraBrightness float64 `json:"rpiCameraBrightness"`
|
RPICameraVFlip bool `json:"rpiCameraVFlip"`
|
||||||
RPICameraContrast float64 `json:"rpiCameraContrast"`
|
RPICameraBrightness float64 `json:"rpiCameraBrightness"`
|
||||||
RPICameraSaturation float64 `json:"rpiCameraSaturation"`
|
RPICameraContrast float64 `json:"rpiCameraContrast"`
|
||||||
RPICameraSharpness float64 `json:"rpiCameraSharpness"`
|
RPICameraSaturation float64 `json:"rpiCameraSaturation"`
|
||||||
RPICameraExposure string `json:"rpiCameraExposure"`
|
RPICameraSharpness float64 `json:"rpiCameraSharpness"`
|
||||||
RPICameraAWB string `json:"rpiCameraAWB"`
|
RPICameraExposure string `json:"rpiCameraExposure"`
|
||||||
RPICameraAWBGains []float64 `json:"rpiCameraAWBGains"`
|
RPICameraAWB string `json:"rpiCameraAWB"`
|
||||||
RPICameraDenoise string `json:"rpiCameraDenoise"`
|
RPICameraAWBGains []float64 `json:"rpiCameraAWBGains"`
|
||||||
RPICameraShutter uint `json:"rpiCameraShutter"`
|
RPICameraDenoise string `json:"rpiCameraDenoise"`
|
||||||
RPICameraMetering string `json:"rpiCameraMetering"`
|
RPICameraShutter uint `json:"rpiCameraShutter"`
|
||||||
RPICameraGain float64 `json:"rpiCameraGain"`
|
RPICameraMetering string `json:"rpiCameraMetering"`
|
||||||
RPICameraEV float64 `json:"rpiCameraEV"`
|
RPICameraGain float64 `json:"rpiCameraGain"`
|
||||||
RPICameraROI string `json:"rpiCameraROI"`
|
RPICameraEV float64 `json:"rpiCameraEV"`
|
||||||
RPICameraHDR bool `json:"rpiCameraHDR"`
|
RPICameraROI string `json:"rpiCameraROI"`
|
||||||
RPICameraTuningFile string `json:"rpiCameraTuningFile"`
|
RPICameraHDR bool `json:"rpiCameraHDR"`
|
||||||
RPICameraMode string `json:"rpiCameraMode"`
|
RPICameraTuningFile string `json:"rpiCameraTuningFile"`
|
||||||
RPICameraFPS float64 `json:"rpiCameraFPS"`
|
RPICameraMode string `json:"rpiCameraMode"`
|
||||||
RPICameraAfMode string `json:"rpiCameraAfMode"`
|
RPICameraFPS float64 `json:"rpiCameraFPS"`
|
||||||
RPICameraAfRange string `json:"rpiCameraAfRange"`
|
RPICameraAfMode string `json:"rpiCameraAfMode"`
|
||||||
RPICameraAfSpeed string `json:"rpiCameraAfSpeed"`
|
RPICameraAfRange string `json:"rpiCameraAfRange"`
|
||||||
RPICameraLensPosition float64 `json:"rpiCameraLensPosition"`
|
RPICameraAfSpeed string `json:"rpiCameraAfSpeed"`
|
||||||
RPICameraAfWindow string `json:"rpiCameraAfWindow"`
|
RPICameraLensPosition float64 `json:"rpiCameraLensPosition"`
|
||||||
RPICameraFlickerPeriod uint `json:"rpiCameraFlickerPeriod"`
|
RPICameraAfWindow string `json:"rpiCameraAfWindow"`
|
||||||
RPICameraTextOverlayEnable bool `json:"rpiCameraTextOverlayEnable"`
|
RPICameraFlickerPeriod uint `json:"rpiCameraFlickerPeriod"`
|
||||||
RPICameraTextOverlay string `json:"rpiCameraTextOverlay"`
|
RPICameraTextOverlayEnable bool `json:"rpiCameraTextOverlayEnable"`
|
||||||
RPICameraCodec string `json:"rpiCameraCodec"`
|
RPICameraTextOverlay string `json:"rpiCameraTextOverlay"`
|
||||||
RPICameraIDRPeriod uint `json:"rpiCameraIDRPeriod"`
|
RPICameraCodec string `json:"rpiCameraCodec"`
|
||||||
RPICameraBitrate uint `json:"rpiCameraBitrate"`
|
RPICameraIDRPeriod uint `json:"rpiCameraIDRPeriod"`
|
||||||
RPICameraProfile string `json:"rpiCameraProfile"`
|
RPICameraBitrate uint `json:"rpiCameraBitrate"`
|
||||||
RPICameraLevel string `json:"rpiCameraLevel"`
|
RPICameraProfile string `json:"rpiCameraProfile"`
|
||||||
|
RPICameraLevel string `json:"rpiCameraLevel"`
|
||||||
|
RPICameraJPEGQuality uint `json:"rpiCameraJPEGQuality"`
|
||||||
|
RPICameraPrimaryName string `json:"-"` // filled by Check()
|
||||||
|
RPICameraSecondaryWidth uint `json:"-"` // filled by Check()
|
||||||
|
RPICameraSecondaryHeight uint `json:"-"` // filled by Check()
|
||||||
|
RPICameraSecondaryFPS float64 `json:"-"` // filled by Check()
|
||||||
|
RPICameraSecondaryJPEGQuality uint `json:"-"` // filled by Check()
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
RunOnInit string `json:"runOnInit"`
|
RunOnInit string `json:"runOnInit"`
|
||||||
|
|
@ -245,6 +252,7 @@ func (pconf *Path) setDefaults() {
|
||||||
pconf.RPICameraBitrate = 5000000
|
pconf.RPICameraBitrate = 5000000
|
||||||
pconf.RPICameraProfile = "main"
|
pconf.RPICameraProfile = "main"
|
||||||
pconf.RPICameraLevel = "4.1"
|
pconf.RPICameraLevel = "4.1"
|
||||||
|
pconf.RPICameraJPEGQuality = 60
|
||||||
|
|
||||||
// Hooks
|
// Hooks
|
||||||
pconf.RunOnDemandStartTimeout = 10 * Duration(time.Second)
|
pconf.RunOnDemandStartTimeout = 10 * Duration(time.Second)
|
||||||
|
|
@ -272,6 +280,11 @@ func (pconf Path) Clone() *Path {
|
||||||
}
|
}
|
||||||
|
|
||||||
dest.Regexp = pconf.Regexp
|
dest.Regexp = pconf.Regexp
|
||||||
|
dest.RPICameraPrimaryName = pconf.RPICameraPrimaryName
|
||||||
|
dest.RPICameraSecondaryWidth = pconf.RPICameraSecondaryWidth
|
||||||
|
dest.RPICameraSecondaryHeight = pconf.RPICameraSecondaryHeight
|
||||||
|
dest.RPICameraSecondaryFPS = pconf.RPICameraSecondaryFPS
|
||||||
|
dest.RPICameraSecondaryJPEGQuality = pconf.RPICameraSecondaryJPEGQuality
|
||||||
|
|
||||||
return &dest
|
return &dest
|
||||||
}
|
}
|
||||||
|
|
@ -350,6 +363,7 @@ func (pconf *Path) validate(
|
||||||
l.Log(logger.Warn, "parameter 'sourceProtocol' is deprecated and has been replaced with 'rtspTransport'")
|
l.Log(logger.Warn, "parameter 'sourceProtocol' is deprecated and has been replaced with 'rtspTransport'")
|
||||||
pconf.RTSPTransport = *pconf.SourceProtocol
|
pconf.RTSPTransport = *pconf.SourceProtocol
|
||||||
}
|
}
|
||||||
|
|
||||||
if pconf.SourceAnyPortEnable != nil {
|
if pconf.SourceAnyPortEnable != nil {
|
||||||
l.Log(logger.Warn, "parameter 'sourceAnyPortEnable' is deprecated and has been replaced with 'rtspAnyPort'")
|
l.Log(logger.Warn, "parameter 'sourceAnyPortEnable' is deprecated and has been replaced with 'rtspAnyPort'")
|
||||||
pconf.RTSPAnyPort = *pconf.SourceAnyPortEnable
|
pconf.RTSPAnyPort = *pconf.SourceAnyPortEnable
|
||||||
|
|
@ -377,6 +391,7 @@ func (pconf *Path) validate(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
|
return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.Scheme != "http" && u.Scheme != "https" {
|
if u.Scheme != "http" && u.Scheme != "https" {
|
||||||
return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
|
return fmt.Errorf("'%s' is not a valid URL", pconf.Source)
|
||||||
}
|
}
|
||||||
|
|
@ -420,12 +435,13 @@ func (pconf *Path) validate(
|
||||||
}
|
}
|
||||||
|
|
||||||
case pconf.Source == "rpiCamera":
|
case pconf.Source == "rpiCamera":
|
||||||
for otherName, otherPath := range conf.Paths {
|
|
||||||
if otherPath != pconf && otherPath != nil &&
|
if pconf.RPICameraWidth == 0 {
|
||||||
otherPath.Source == "rpiCamera" && otherPath.RPICameraCamID == pconf.RPICameraCamID {
|
return fmt.Errorf("invalid 'rpiCameraWidth' value")
|
||||||
return fmt.Errorf("'rpiCamera' with same camera ID %d is used as source in two paths, '%s' and '%s'",
|
}
|
||||||
pconf.RPICameraCamID, name, otherName)
|
|
||||||
}
|
if pconf.RPICameraHeight == 0 {
|
||||||
|
return fmt.Errorf("invalid 'rpiCameraHeight' value")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch pconf.RPICameraExposure {
|
switch pconf.RPICameraExposure {
|
||||||
|
|
@ -433,43 +449,99 @@ func (pconf *Path) validate(
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid 'rpiCameraExposure' value")
|
return fmt.Errorf("invalid 'rpiCameraExposure' value")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch pconf.RPICameraAWB {
|
switch pconf.RPICameraAWB {
|
||||||
case "auto", "incandescent", "tungsten", "fluorescent", "indoor", "daylight", "cloudy", "custom":
|
case "auto", "incandescent", "tungsten", "fluorescent", "indoor", "daylight", "cloudy", "custom":
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid 'rpiCameraAWB' value")
|
return fmt.Errorf("invalid 'rpiCameraAWB' value")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(pconf.RPICameraAWBGains) != 2 {
|
if len(pconf.RPICameraAWBGains) != 2 {
|
||||||
return fmt.Errorf("invalid 'rpiCameraAWBGains' value")
|
return fmt.Errorf("invalid 'rpiCameraAWBGains' value")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch pconf.RPICameraDenoise {
|
switch pconf.RPICameraDenoise {
|
||||||
case "off", "cdn_off", "cdn_fast", "cdn_hq":
|
case "off", "cdn_off", "cdn_fast", "cdn_hq":
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid 'rpiCameraDenoise' value")
|
return fmt.Errorf("invalid 'rpiCameraDenoise' value")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch pconf.RPICameraMetering {
|
switch pconf.RPICameraMetering {
|
||||||
case "centre", "spot", "matrix", "custom":
|
case "centre", "spot", "matrix", "custom":
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid 'rpiCameraMetering' value")
|
return fmt.Errorf("invalid 'rpiCameraMetering' value")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch pconf.RPICameraAfMode {
|
switch pconf.RPICameraAfMode {
|
||||||
case "auto", "manual", "continuous":
|
case "auto", "manual", "continuous":
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid 'rpiCameraAfMode' value")
|
return fmt.Errorf("invalid 'rpiCameraAfMode' value")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch pconf.RPICameraAfRange {
|
switch pconf.RPICameraAfRange {
|
||||||
case "normal", "macro", "full":
|
case "normal", "macro", "full":
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid 'rpiCameraAfRange' value")
|
return fmt.Errorf("invalid 'rpiCameraAfRange' value")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch pconf.RPICameraAfSpeed {
|
switch pconf.RPICameraAfSpeed {
|
||||||
case "normal", "fast":
|
case "normal", "fast":
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid 'rpiCameraAfSpeed' value")
|
return fmt.Errorf("invalid 'rpiCameraAfSpeed' value")
|
||||||
}
|
}
|
||||||
switch pconf.RPICameraCodec {
|
|
||||||
case "auto", "hardwareH264", "softwareH264":
|
if !pconf.RPICameraSecondary {
|
||||||
default:
|
switch pconf.RPICameraCodec {
|
||||||
return fmt.Errorf("invalid 'rpiCameraCodec' value")
|
case "auto", "hardwareH264", "softwareH264":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("supported codecs for a primary RPI Camera stream are auto, hardwareH264, softwareH264")
|
||||||
|
}
|
||||||
|
|
||||||
|
for otherName, otherPath := range conf.Paths {
|
||||||
|
if otherPath != pconf &&
|
||||||
|
otherPath != nil &&
|
||||||
|
otherPath.Source == "rpiCamera" &&
|
||||||
|
otherPath.RPICameraCamID == pconf.RPICameraCamID &&
|
||||||
|
!otherPath.RPICameraSecondary {
|
||||||
|
return fmt.Errorf("'rpiCamera' with same camera ID %d is used as source in two paths, '%s' and '%s'",
|
||||||
|
pconf.RPICameraCamID, name, otherName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch pconf.RPICameraCodec {
|
||||||
|
case "auto", "mjpeg":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("supported codecs for a secondary RPI Camera stream are auto, mjpeg")
|
||||||
|
}
|
||||||
|
|
||||||
|
var primaryName string
|
||||||
|
var primary *Path
|
||||||
|
|
||||||
|
for otherPathName, otherPath := range conf.Paths {
|
||||||
|
if otherPath != pconf &&
|
||||||
|
otherPath != nil &&
|
||||||
|
otherPath.Source == "rpiCamera" &&
|
||||||
|
otherPath.RPICameraCamID == pconf.RPICameraCamID &&
|
||||||
|
!otherPath.RPICameraSecondary {
|
||||||
|
primaryName = otherPathName
|
||||||
|
primary = otherPath
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if primary == nil {
|
||||||
|
return fmt.Errorf("cannot find a primary RPI Camera stream to associate with the secondary stream")
|
||||||
|
}
|
||||||
|
|
||||||
|
if primary.RPICameraSecondaryWidth != 0 {
|
||||||
|
return fmt.Errorf("a primary RPI Camera stream is associated with multiple secondary streams")
|
||||||
|
}
|
||||||
|
|
||||||
|
pconf.RPICameraPrimaryName = primaryName
|
||||||
|
primary.RPICameraSecondaryWidth = pconf.RPICameraWidth
|
||||||
|
primary.RPICameraSecondaryHeight = pconf.RPICameraHeight
|
||||||
|
primary.RPICameraSecondaryFPS = pconf.RPICameraFPS
|
||||||
|
primary.RPICameraSecondaryJPEGQuality = pconf.RPICameraJPEGQuality
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ type pathParent interface {
|
||||||
pathReady(*path)
|
pathReady(*path)
|
||||||
pathNotReady(*path)
|
pathNotReady(*path)
|
||||||
closePath(*path)
|
closePath(*path)
|
||||||
|
AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type pathOnDemandState int
|
type pathOnDemandState int
|
||||||
|
|
@ -174,6 +175,7 @@ func (pa *path) run() {
|
||||||
WriteTimeout: pa.writeTimeout,
|
WriteTimeout: pa.writeTimeout,
|
||||||
WriteQueueSize: pa.writeQueueSize,
|
WriteQueueSize: pa.writeQueueSize,
|
||||||
Matches: pa.matches,
|
Matches: pa.matches,
|
||||||
|
PathManager: pa.parent,
|
||||||
Parent: pa,
|
Parent: pa,
|
||||||
}
|
}
|
||||||
pa.source.(*staticsources.Handler).Initialize()
|
pa.source.(*staticsources.Handler).Initialize()
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import (
|
||||||
sssrt "github.com/bluenviron/mediamtx/internal/staticsources/srt"
|
sssrt "github.com/bluenviron/mediamtx/internal/staticsources/srt"
|
||||||
ssudp "github.com/bluenviron/mediamtx/internal/staticsources/udp"
|
ssudp "github.com/bluenviron/mediamtx/internal/staticsources/udp"
|
||||||
sswebrtc "github.com/bluenviron/mediamtx/internal/staticsources/webrtc"
|
sswebrtc "github.com/bluenviron/mediamtx/internal/staticsources/webrtc"
|
||||||
|
"github.com/bluenviron/mediamtx/internal/stream"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -42,6 +43,10 @@ func resolveSource(s string, matches []string, query string) string {
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type handlerPathManager interface {
|
||||||
|
AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
|
||||||
|
}
|
||||||
|
|
||||||
type handlerParent interface {
|
type handlerParent interface {
|
||||||
logger.Writer
|
logger.Writer
|
||||||
StaticSourceHandlerSetReady(context.Context, defs.PathSourceStaticSetReadyReq)
|
StaticSourceHandlerSetReady(context.Context, defs.PathSourceStaticSetReadyReq)
|
||||||
|
|
@ -56,6 +61,7 @@ type Handler struct {
|
||||||
WriteTimeout conf.Duration
|
WriteTimeout conf.Duration
|
||||||
WriteQueueSize int
|
WriteQueueSize int
|
||||||
Matches []string
|
Matches []string
|
||||||
|
PathManager handlerPathManager
|
||||||
Parent handlerParent
|
Parent handlerParent
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
@ -298,3 +304,8 @@ func (s *Handler) SetNotReady(req defs.PathSourceStaticSetNotReadyReq) {
|
||||||
case <-s.ctx.Done():
|
case <-s.ctx.Done():
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddReader is called by a staticSource.
|
||||||
|
func (s *Handler) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) {
|
||||||
|
return s.PathManager.AddReader(req)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,15 @@
|
||||||
package rpicamera
|
package rpicamera
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"debug/elf"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
@ -15,6 +19,18 @@ import (
|
||||||
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
|
"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
libraryToCheckArchitecture = "libc.so.6"
|
||||||
|
dumpPrefix = "/dev/shm/mediamtx-rpicamera-"
|
||||||
|
executableName = "mtxrpicam"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dumpMutex sync.Mutex
|
||||||
|
dumpCount = 0
|
||||||
|
dumpPath = ""
|
||||||
|
)
|
||||||
|
|
||||||
func ntpTime() syscall.Timespec {
|
func ntpTime() syscall.Timespec {
|
||||||
var t syscall.Timespec
|
var t syscall.Timespec
|
||||||
syscall.Syscall(syscall.SYS_CLOCK_GETTIME, 0, uintptr(unsafe.Pointer(&t)), 0)
|
syscall.Syscall(syscall.SYS_CLOCK_GETTIME, 0, uintptr(unsafe.Pointer(&t)), 0)
|
||||||
|
|
@ -33,9 +49,147 @@ func multiplyAndDivide(v, m, d int64) int64 {
|
||||||
return (secs*m + dec*m/d)
|
return (secs*m + dec*m/d)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getArchitecture(libPath string) (bool, error) {
|
||||||
|
f, err := os.Open(libPath)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
ef, err := elf.NewFile(f)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer ef.Close()
|
||||||
|
|
||||||
|
return (ef.FileHeader.Class == elf.ELFCLASS64), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkArchitecture() error {
|
||||||
|
byts, err := exec.Command("ldconfig", "-p").Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("ldconfig failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(string(byts), "\n") {
|
||||||
|
f := strings.Split(line, " => ")
|
||||||
|
if len(f) == 2 && strings.Contains(f[1], libraryToCheckArchitecture) {
|
||||||
|
is64, err := getArchitecture(f[1])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOARCH == "arm" {
|
||||||
|
if !is64 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if is64 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOARCH == "arm" {
|
||||||
|
return fmt.Errorf("the operating system is 64-bit, you need the 64-bit server version")
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("the operating system is 32-bit, you need the 32-bit server version")
|
||||||
|
}
|
||||||
|
|
||||||
|
func dumpEmbedFSRecursive(src string, dest string) error {
|
||||||
|
files, err := mtxrpicam.ReadDir(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
if f.IsDir() {
|
||||||
|
err = os.Mkdir(filepath.Join(dest, f.Name()), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dumpEmbedFSRecursive(filepath.Join(src, f.Name()), filepath.Join(dest, f.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buf, err := mtxrpicam.ReadFile(filepath.Join(src, f.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(filepath.Join(dest, f.Name()), buf, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dumpComponent() error {
|
||||||
|
dumpMutex.Lock()
|
||||||
|
defer dumpMutex.Unlock()
|
||||||
|
|
||||||
|
if dumpCount > 0 {
|
||||||
|
dumpCount++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := checkArchitecture()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dumpPath = dumpPrefix + strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||||
|
|
||||||
|
err = os.Mkdir(dumpPath, 0o755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := mtxrpicam.ReadDir(".")
|
||||||
|
if err != nil {
|
||||||
|
os.RemoveAll(dumpPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = dumpEmbedFSRecursive(files[0].Name(), dumpPath)
|
||||||
|
if err != nil {
|
||||||
|
os.RemoveAll(dumpPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Chmod(filepath.Join(dumpPath, executableName), 0o755)
|
||||||
|
if err != nil {
|
||||||
|
os.RemoveAll(dumpPath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dumpCount++
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func freeComponent() {
|
||||||
|
dumpMutex.Lock()
|
||||||
|
defer dumpMutex.Unlock()
|
||||||
|
|
||||||
|
dumpCount--
|
||||||
|
|
||||||
|
if dumpCount == 0 {
|
||||||
|
os.RemoveAll(dumpPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type camera struct {
|
type camera struct {
|
||||||
params params
|
params params
|
||||||
onData func(int64, time.Time, [][]byte)
|
onData func(int64, time.Time, [][]byte)
|
||||||
|
onDataSecondary func(int64, time.Time, []byte)
|
||||||
|
|
||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
pipeOut *pipe
|
pipeOut *pipe
|
||||||
|
|
@ -181,7 +335,7 @@ outer:
|
||||||
case 'e':
|
case 'e':
|
||||||
return fmt.Errorf(string(buf[1:]))
|
return fmt.Errorf(string(buf[1:]))
|
||||||
|
|
||||||
case 'b':
|
case 'd':
|
||||||
dts := int64(buf[8])<<56 | int64(buf[7])<<48 | int64(buf[6])<<40 | int64(buf[5])<<32 |
|
dts := int64(buf[8])<<56 | int64(buf[7])<<48 | int64(buf[6])<<40 | int64(buf[5])<<32 |
|
||||||
int64(buf[4])<<24 | int64(buf[3])<<16 | int64(buf[2])<<8 | int64(buf[1])
|
int64(buf[4])<<24 | int64(buf[3])<<16 | int64(buf[2])<<8 | int64(buf[1])
|
||||||
|
|
||||||
|
|
@ -204,6 +358,23 @@ outer:
|
||||||
ntp,
|
ntp,
|
||||||
nalus)
|
nalus)
|
||||||
|
|
||||||
|
case 's':
|
||||||
|
dts := int64(buf[8])<<56 | int64(buf[7])<<48 | int64(buf[6])<<40 | int64(buf[5])<<32 |
|
||||||
|
int64(buf[4])<<24 | int64(buf[3])<<16 | int64(buf[2])<<8 | int64(buf[1])
|
||||||
|
|
||||||
|
unixNTP := ntpTime()
|
||||||
|
unixMono := monotonicTime()
|
||||||
|
|
||||||
|
// subtract from NTP the delay from now to the moment the frame was taken
|
||||||
|
ntp := time.Unix(int64(unixNTP.Sec), int64(unixNTP.Nsec))
|
||||||
|
deltaT := time.Duration(unixMono.Nano()-dts*1e3) * time.Nanosecond
|
||||||
|
ntp = ntp.Add(-deltaT)
|
||||||
|
|
||||||
|
c.onDataSecondary(
|
||||||
|
multiplyAndDivide(dts, 90000, 1e6),
|
||||||
|
ntp,
|
||||||
|
buf[9:])
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unexpected data from pipe: '0x%.2x'", buf[0])
|
return fmt.Errorf("unexpected data from pipe: '0x%.2x'", buf[0])
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type camera struct {
|
type camera struct {
|
||||||
params params
|
params params
|
||||||
onData func(int64, time.Time, [][]byte)
|
onData func(int64, time.Time, [][]byte)
|
||||||
|
onDataSecondary func(int64, time.Time, []byte)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *camera) initialize() error {
|
func (c *camera) initialize() error {
|
||||||
|
|
|
||||||
|
|
@ -1,165 +0,0 @@
|
||||||
//go:build (linux && arm) || (linux && arm64)
|
|
||||||
|
|
||||||
package rpicamera
|
|
||||||
|
|
||||||
import (
|
|
||||||
"debug/elf"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
libraryToCheckArchitecture = "libc.so.6"
|
|
||||||
dumpPrefix = "/dev/shm/mediamtx-rpicamera-"
|
|
||||||
executableName = "mtxrpicam"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
dumpMutex sync.Mutex
|
|
||||||
dumpCount = 0
|
|
||||||
dumpPath = ""
|
|
||||||
)
|
|
||||||
|
|
||||||
func getArchitecture(libPath string) (bool, error) {
|
|
||||||
f, err := os.Open(libPath)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
ef, err := elf.NewFile(f)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
defer ef.Close()
|
|
||||||
|
|
||||||
return (ef.FileHeader.Class == elf.ELFCLASS64), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkArchitecture() error {
|
|
||||||
byts, err := exec.Command("ldconfig", "-p").Output()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ldconfig failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, line := range strings.Split(string(byts), "\n") {
|
|
||||||
f := strings.Split(line, " => ")
|
|
||||||
if len(f) == 2 && strings.Contains(f[1], libraryToCheckArchitecture) {
|
|
||||||
is64, err := getArchitecture(f[1])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOARCH == "arm" {
|
|
||||||
if !is64 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if is64 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if runtime.GOARCH == "arm" {
|
|
||||||
return fmt.Errorf("the operating system is 64-bit, you need the 64-bit server version")
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("the operating system is 32-bit, you need the 32-bit server version")
|
|
||||||
}
|
|
||||||
|
|
||||||
func dumpEmbedFSRecursive(src string, dest string) error {
|
|
||||||
files, err := component.ReadDir(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range files {
|
|
||||||
if f.IsDir() {
|
|
||||||
err = os.Mkdir(filepath.Join(dest, f.Name()), 0o755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = dumpEmbedFSRecursive(filepath.Join(src, f.Name()), filepath.Join(dest, f.Name()))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buf, err := component.ReadFile(filepath.Join(src, f.Name()))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(filepath.Join(dest, f.Name()), buf, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dumpComponent() error {
|
|
||||||
dumpMutex.Lock()
|
|
||||||
defer dumpMutex.Unlock()
|
|
||||||
|
|
||||||
if dumpCount > 0 {
|
|
||||||
dumpCount++
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := checkArchitecture()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dumpPath = dumpPrefix + strconv.FormatInt(time.Now().UnixNano(), 10)
|
|
||||||
|
|
||||||
err = os.Mkdir(dumpPath, 0o755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
files, err := component.ReadDir(".")
|
|
||||||
if err != nil {
|
|
||||||
os.RemoveAll(dumpPath)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = dumpEmbedFSRecursive(files[0].Name(), dumpPath)
|
|
||||||
if err != nil {
|
|
||||||
os.RemoveAll(dumpPath)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Chmod(filepath.Join(dumpPath, executableName), 0o755)
|
|
||||||
if err != nil {
|
|
||||||
os.RemoveAll(dumpPath)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dumpCount++
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func freeComponent() {
|
|
||||||
dumpMutex.Lock()
|
|
||||||
defer dumpMutex.Unlock()
|
|
||||||
|
|
||||||
dumpCount--
|
|
||||||
|
|
||||||
if dumpCount == 0 {
|
|
||||||
os.RemoveAll(dumpPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,4 +7,4 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed mtxrpicam_32/*
|
//go:embed mtxrpicam_32/*
|
||||||
var component embed.FS
|
var mtxrpicam embed.FS
|
||||||
|
|
@ -7,4 +7,4 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed mtxrpicam_64/*
|
//go:embed mtxrpicam_64/*
|
||||||
var component embed.FS
|
var mtxrpicam embed.FS
|
||||||
|
|
@ -1 +1 @@
|
||||||
v2.3.7
|
v2.4.0
|
||||||
|
|
|
||||||
|
|
@ -38,4 +38,8 @@ type params struct {
|
||||||
Bitrate uint32
|
Bitrate uint32
|
||||||
Profile string
|
Profile string
|
||||||
Level string
|
Level string
|
||||||
|
SecondaryWidth uint32
|
||||||
|
SecondaryHeight uint32
|
||||||
|
SecondaryFPS float32
|
||||||
|
SecondaryQuality uint32
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,16 @@
|
||||||
package rpicamera
|
package rpicamera
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
"github.com/bluenviron/gortsplib/v4/pkg/description"
|
||||||
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
"github.com/bluenviron/gortsplib/v4/pkg/format"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtph264"
|
||||||
|
"github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg"
|
||||||
|
"github.com/pion/rtp"
|
||||||
|
|
||||||
"github.com/bluenviron/mediamtx/internal/conf"
|
"github.com/bluenviron/mediamtx/internal/conf"
|
||||||
"github.com/bluenviron/mediamtx/internal/defs"
|
"github.com/bluenviron/mediamtx/internal/defs"
|
||||||
|
|
@ -14,6 +20,10 @@ import (
|
||||||
"github.com/bluenviron/mediamtx/internal/unit"
|
"github.com/bluenviron/mediamtx/internal/unit"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
pauseBetweenErrors = 1 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
func paramsFromConf(logLevel conf.LogLevel, cnf *conf.Path) params {
|
func paramsFromConf(logLevel conf.LogLevel, cnf *conf.Path) params {
|
||||||
return params{
|
return params{
|
||||||
LogLevel: func() string {
|
LogLevel: func() string {
|
||||||
|
|
@ -63,13 +73,40 @@ func paramsFromConf(logLevel conf.LogLevel, cnf *conf.Path) params {
|
||||||
Bitrate: uint32(cnf.RPICameraBitrate),
|
Bitrate: uint32(cnf.RPICameraBitrate),
|
||||||
Profile: cnf.RPICameraProfile,
|
Profile: cnf.RPICameraProfile,
|
||||||
Level: cnf.RPICameraLevel,
|
Level: cnf.RPICameraLevel,
|
||||||
|
SecondaryWidth: uint32(cnf.RPICameraSecondaryWidth),
|
||||||
|
SecondaryHeight: uint32(cnf.RPICameraSecondaryHeight),
|
||||||
|
SecondaryFPS: float32(cnf.RPICameraSecondaryFPS),
|
||||||
|
SecondaryQuality: uint32(cnf.RPICameraSecondaryJPEGQuality),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type secondaryReader struct {
|
||||||
|
ctx context.Context
|
||||||
|
ctxCancel func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements reader.
|
||||||
|
func (r *secondaryReader) Close() {
|
||||||
|
r.ctxCancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIReaderDescribe implements reader.
|
||||||
|
func (*secondaryReader) APIReaderDescribe() defs.APIPathSourceOrReader {
|
||||||
|
return defs.APIPathSourceOrReader{
|
||||||
|
Type: "rpiCameraSecondary",
|
||||||
|
ID: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type parent interface {
|
||||||
|
defs.StaticSourceParent
|
||||||
|
AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error)
|
||||||
|
}
|
||||||
|
|
||||||
// Source is a Raspberry Pi Camera static source.
|
// Source is a Raspberry Pi Camera static source.
|
||||||
type Source struct {
|
type Source struct {
|
||||||
LogLevel conf.LogLevel
|
LogLevel conf.LogLevel
|
||||||
Parent defs.StaticSourceParent
|
Parent parent
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log implements logger.Writer.
|
// Log implements logger.Writer.
|
||||||
|
|
@ -79,6 +116,15 @@ func (s *Source) Log(level logger.Level, format string, args ...interface{}) {
|
||||||
|
|
||||||
// Run implements StaticSource.
|
// Run implements StaticSource.
|
||||||
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
|
if !params.Conf.RPICameraSecondary {
|
||||||
|
return s.runPrimary(params)
|
||||||
|
}
|
||||||
|
return s.runSecondary(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Source) runPrimary(params defs.StaticSourceRunParams) error {
|
||||||
|
var medias []*description.Media
|
||||||
|
|
||||||
medi := &description.Media{
|
medi := &description.Media{
|
||||||
Type: description.MediaTypeVideo,
|
Type: description.MediaTypeVideo,
|
||||||
Formats: []format.Format{&format.H264{
|
Formats: []format.Format{&format.H264{
|
||||||
|
|
@ -86,29 +132,88 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
PacketizationMode: 1,
|
PacketizationMode: 1,
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
medias := []*description.Media{medi}
|
medias = append(medias, medi)
|
||||||
|
|
||||||
|
var mediaSecondary *description.Media
|
||||||
|
|
||||||
|
if params.Conf.RPICameraSecondaryWidth != 0 {
|
||||||
|
mediaSecondary = &description.Media{
|
||||||
|
Type: description.MediaTypeApplication,
|
||||||
|
Formats: []format.Format{&format.Generic{
|
||||||
|
PayloadTyp: 96,
|
||||||
|
RTPMa: "rpicamera_secondary",
|
||||||
|
ClockRat: 90000,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
medias = append(medias, mediaSecondary)
|
||||||
|
}
|
||||||
|
|
||||||
var stream *stream.Stream
|
var stream *stream.Stream
|
||||||
|
|
||||||
onData := func(pts int64, ntp time.Time, au [][]byte) {
|
initializeStream := func() {
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
||||||
Desc: &description.Session{Medias: medias},
|
Desc: &description.Session{Medias: medias},
|
||||||
GenerateRTPPackets: true,
|
GenerateRTPPackets: false,
|
||||||
})
|
})
|
||||||
if res.Err != nil {
|
if res.Err != nil {
|
||||||
return
|
panic("should not happen")
|
||||||
}
|
}
|
||||||
|
|
||||||
stream = res.Stream
|
stream = res.Stream
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stream.WriteUnit(medi, medi.Formats[0], &unit.H264{
|
encH264 := &rtph264.Encoder{
|
||||||
Base: unit.Base{
|
PayloadType: 96,
|
||||||
PTS: pts,
|
PayloadMaxSize: 1460,
|
||||||
NTP: ntp,
|
}
|
||||||
},
|
err := encH264.Init()
|
||||||
AU: au,
|
if err != nil {
|
||||||
})
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
onData := func(pts int64, ntp time.Time, au [][]byte) {
|
||||||
|
initializeStream()
|
||||||
|
|
||||||
|
pkts, err2 := encH264.Encode(au)
|
||||||
|
if err2 != nil {
|
||||||
|
s.Log(logger.Error, err2.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkt := range pkts {
|
||||||
|
pkt.Timestamp = uint32(pts)
|
||||||
|
stream.WriteRTPPacket(medi, medi.Formats[0], pkt, ntp, pts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var onDataSecondary func(pts int64, ntp time.Time, au []byte)
|
||||||
|
|
||||||
|
if params.Conf.RPICameraSecondaryWidth != 0 {
|
||||||
|
encJpeg := &rtpmjpeg.Encoder{
|
||||||
|
PayloadMaxSize: 1460,
|
||||||
|
}
|
||||||
|
err = encJpeg.Init()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
onDataSecondary = func(pts int64, ntp time.Time, au []byte) {
|
||||||
|
initializeStream()
|
||||||
|
|
||||||
|
pkts, err2 := encJpeg.Encode(au)
|
||||||
|
if err2 != nil {
|
||||||
|
s.Log(logger.Error, err2.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkt := range pkts {
|
||||||
|
pkt.Timestamp = uint32(pts)
|
||||||
|
pkt.PayloadType = 96
|
||||||
|
stream.WriteRTPPacket(mediaSecondary, mediaSecondary.Formats[0], pkt, ntp, pts)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|
@ -118,10 +223,11 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
cam := &camera{
|
cam := &camera{
|
||||||
params: paramsFromConf(s.LogLevel, params.Conf),
|
params: paramsFromConf(s.LogLevel, params.Conf),
|
||||||
onData: onData,
|
onData: onData,
|
||||||
|
onDataSecondary: onDataSecondary,
|
||||||
}
|
}
|
||||||
err := cam.initialize()
|
err = cam.initialize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -146,6 +252,93 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Source) runSecondary(params defs.StaticSourceRunParams) error {
|
||||||
|
r := &secondaryReader{}
|
||||||
|
r.ctx, r.ctxCancel = context.WithCancel(context.Background())
|
||||||
|
defer r.ctxCancel()
|
||||||
|
|
||||||
|
path, origStream, err := s.waitForPrimary(r, params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer path.RemoveReader(defs.PathRemoveReaderReq{Author: r})
|
||||||
|
|
||||||
|
media := &description.Media{
|
||||||
|
Type: description.MediaTypeVideo,
|
||||||
|
Formats: []format.Format{&format.MJPEG{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
res := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{
|
||||||
|
Desc: &description.Session{Medias: []*description.Media{media}},
|
||||||
|
GenerateRTPPackets: false,
|
||||||
|
})
|
||||||
|
if res.Err != nil {
|
||||||
|
return res.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
origStream.AddReader(
|
||||||
|
s,
|
||||||
|
origStream.Desc.Medias[1],
|
||||||
|
origStream.Desc.Medias[1].Formats[0],
|
||||||
|
func(u unit.Unit) error {
|
||||||
|
pkt := u.GetRTPPackets()[0]
|
||||||
|
|
||||||
|
newPkt := &rtp.Packet{
|
||||||
|
Header: pkt.Header,
|
||||||
|
Payload: pkt.Payload,
|
||||||
|
}
|
||||||
|
newPkt.PayloadType = 26
|
||||||
|
|
||||||
|
res.Stream.WriteRTPPacket(media, media.Formats[0], newPkt, u.GetNTP(), u.GetPTS())
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
origStream.StartReader(s)
|
||||||
|
defer origStream.RemoveReader(s)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-origStream.ReaderError(s):
|
||||||
|
return err
|
||||||
|
|
||||||
|
case <-r.ctx.Done():
|
||||||
|
return fmt.Errorf("primary stream closed")
|
||||||
|
|
||||||
|
case <-params.Context.Done():
|
||||||
|
return fmt.Errorf("terminated")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Source) waitForPrimary(
|
||||||
|
r *secondaryReader,
|
||||||
|
params defs.StaticSourceRunParams,
|
||||||
|
) (defs.Path, *stream.Stream, error) {
|
||||||
|
for {
|
||||||
|
path, origStream, err := s.Parent.AddReader(defs.PathAddReaderReq{
|
||||||
|
Author: r,
|
||||||
|
AccessRequest: defs.PathAccessRequest{
|
||||||
|
Name: params.Conf.RPICameraPrimaryName,
|
||||||
|
SkipAuth: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
var err2 defs.PathNoStreamAvailableError
|
||||||
|
if errors.As(err, &err2) {
|
||||||
|
select {
|
||||||
|
case <-time.After(pauseBetweenErrors):
|
||||||
|
case <-params.Context.Done():
|
||||||
|
return nil, nil, fmt.Errorf("terminated")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, origStream, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// APISourceDescribe implements StaticSource.
|
// APISourceDescribe implements StaticSource.
|
||||||
func (*Source) APISourceDescribe() defs.APIPathSourceOrReader {
|
func (*Source) APISourceDescribe() defs.APIPathSourceOrReader {
|
||||||
return defs.APIPathSourceOrReader{
|
return defs.APIPathSourceOrReader{
|
||||||
|
|
|
||||||
64
mediamtx.yml
64
mediamtx.yml
|
|
@ -517,64 +517,66 @@ pathDefaults:
|
||||||
###############################################
|
###############################################
|
||||||
# Default path settings -> Raspberry Pi Camera source (when source is "rpiCamera")
|
# Default path settings -> Raspberry Pi Camera source (when source is "rpiCamera")
|
||||||
|
|
||||||
# ID of the camera
|
# ID of the camera.
|
||||||
rpiCameraCamID: 0
|
rpiCameraCamID: 0
|
||||||
# Width of frames
|
# Whether this is a secondary stream.
|
||||||
|
rpiCameraSecondary: false
|
||||||
|
# Width of frames.
|
||||||
rpiCameraWidth: 1920
|
rpiCameraWidth: 1920
|
||||||
# Height of frames
|
# Height of frames.
|
||||||
rpiCameraHeight: 1080
|
rpiCameraHeight: 1080
|
||||||
# Flip horizontally
|
# Flip horizontally.
|
||||||
rpiCameraHFlip: false
|
rpiCameraHFlip: false
|
||||||
# Flip vertically
|
# Flip vertically.
|
||||||
rpiCameraVFlip: false
|
rpiCameraVFlip: false
|
||||||
# Brightness [-1, 1]
|
# Brightness [-1, 1].
|
||||||
rpiCameraBrightness: 0
|
rpiCameraBrightness: 0
|
||||||
# Contrast [0, 16]
|
# Contrast [0, 16].
|
||||||
rpiCameraContrast: 1
|
rpiCameraContrast: 1
|
||||||
# Saturation [0, 16]
|
# Saturation [0, 16].
|
||||||
rpiCameraSaturation: 1
|
rpiCameraSaturation: 1
|
||||||
# Sharpness [0, 16]
|
# Sharpness [0, 16].
|
||||||
rpiCameraSharpness: 1
|
rpiCameraSharpness: 1
|
||||||
# Exposure mode.
|
# Exposure mode.
|
||||||
# values: normal, short, long, custom
|
# values: normal, short, long, custom.
|
||||||
rpiCameraExposure: normal
|
rpiCameraExposure: normal
|
||||||
# Auto-white-balance mode.
|
# Auto-white-balance mode.
|
||||||
# values: auto, incandescent, tungsten, fluorescent, indoor, daylight, cloudy, custom
|
# values: auto, incandescent, tungsten, fluorescent, indoor, daylight, cloudy, custom.
|
||||||
rpiCameraAWB: auto
|
rpiCameraAWB: auto
|
||||||
# Auto-white-balance fixed gains. This can be used in place of rpiCameraAWB.
|
# Auto-white-balance fixed gains. This can be used in place of rpiCameraAWB.
|
||||||
# format: [red,blue]
|
# format: [red,blue].
|
||||||
rpiCameraAWBGains: [0, 0]
|
rpiCameraAWBGains: [0, 0]
|
||||||
# Denoise operating mode.
|
# Denoise operating mode.
|
||||||
# values: off, cdn_off, cdn_fast, cdn_hq
|
# values: off, cdn_off, cdn_fast, cdn_hq.
|
||||||
rpiCameraDenoise: "off"
|
rpiCameraDenoise: "off"
|
||||||
# Fixed shutter speed, in microseconds.
|
# Fixed shutter speed, in microseconds.
|
||||||
rpiCameraShutter: 0
|
rpiCameraShutter: 0
|
||||||
# Metering mode of the AEC/AGC algorithm.
|
# Metering mode of the AEC/AGC algorithm.
|
||||||
# values: centre, spot, matrix, custom
|
# values: centre, spot, matrix, custom.
|
||||||
rpiCameraMetering: centre
|
rpiCameraMetering: centre
|
||||||
# Fixed gain
|
# Fixed gain.
|
||||||
rpiCameraGain: 0
|
rpiCameraGain: 0
|
||||||
# EV compensation of the image [-10, 10]
|
# EV compensation of the image [-10, 10].
|
||||||
rpiCameraEV: 0
|
rpiCameraEV: 0
|
||||||
# Region of interest, in format x,y,width,height (all normalized between 0 and 1)
|
# Region of interest, in format x,y,width,height (all normalized between 0 and 1).
|
||||||
rpiCameraROI:
|
rpiCameraROI:
|
||||||
# Whether to enable HDR on Raspberry Camera 3.
|
# Whether to enable HDR on Raspberry Camera 3.
|
||||||
rpiCameraHDR: false
|
rpiCameraHDR: false
|
||||||
# Tuning file
|
# Tuning file.
|
||||||
rpiCameraTuningFile:
|
rpiCameraTuningFile:
|
||||||
# Sensor mode, in format [width]:[height]:[bit-depth]:[packing]
|
# Sensor mode, in format [width]:[height]:[bit-depth]:[packing]
|
||||||
# bit-depth and packing are optional.
|
# bit-depth and packing are optional.
|
||||||
rpiCameraMode:
|
rpiCameraMode:
|
||||||
# frames per second
|
# frames per second.
|
||||||
rpiCameraFPS: 30
|
rpiCameraFPS: 30
|
||||||
# Autofocus mode
|
# Autofocus mode.
|
||||||
# values: auto, manual, continuous
|
# values: auto, manual, continuous.
|
||||||
rpiCameraAfMode: continuous
|
rpiCameraAfMode: continuous
|
||||||
# Autofocus range
|
# Autofocus range.
|
||||||
# values: normal, macro, full
|
# values: normal, macro, full.
|
||||||
rpiCameraAfRange: normal
|
rpiCameraAfRange: normal
|
||||||
# Autofocus speed
|
# Autofocus speed.
|
||||||
# values: normal, fast
|
# values: normal, fast.
|
||||||
rpiCameraAfSpeed: normal
|
rpiCameraAfSpeed: normal
|
||||||
# Lens position (for manual autofocus only), will be set to focus to a specific distance
|
# Lens position (for manual autofocus only), will be set to focus to a specific distance
|
||||||
# calculated by the following formula: d = 1 / value
|
# calculated by the following formula: d = 1 / value
|
||||||
|
|
@ -592,16 +594,18 @@ pathDefaults:
|
||||||
# Text that is printed on each frame.
|
# Text that is printed on each frame.
|
||||||
# format is the one of the strftime() function.
|
# format is the one of the strftime() function.
|
||||||
rpiCameraTextOverlay: '%Y-%m-%d %H:%M:%S - MediaMTX'
|
rpiCameraTextOverlay: '%Y-%m-%d %H:%M:%S - MediaMTX'
|
||||||
# Codec. Available values: auto, hardwareH264, softwareH264
|
# Codec. Available values: auto, hardwareH264, softwareH264, mjpeg.
|
||||||
rpiCameraCodec: auto
|
rpiCameraCodec: auto
|
||||||
# Period between IDR frames
|
# Period between H264 IDR frames.
|
||||||
rpiCameraIDRPeriod: 60
|
rpiCameraIDRPeriod: 60
|
||||||
# Bitrate
|
# H264 Bitrate.
|
||||||
rpiCameraBitrate: 5000000
|
rpiCameraBitrate: 5000000
|
||||||
# H264 profile
|
# H264 profile.
|
||||||
rpiCameraProfile: main
|
rpiCameraProfile: main
|
||||||
# H264 level
|
# H264 level.
|
||||||
rpiCameraLevel: '4.1'
|
rpiCameraLevel: '4.1'
|
||||||
|
# JPEG quality.
|
||||||
|
rpiCameraJPEGQuality: 60
|
||||||
|
|
||||||
###############################################
|
###############################################
|
||||||
# Default path settings -> Hooks
|
# Default path settings -> Hooks
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue