rpi: embed libcamera and libfreetype into the server (#2581) (#3665)
Some checks are pending
code_lint / golangci_lint (push) Waiting to run
code_lint / mod_tidy (push) Waiting to run
code_lint / api_docs (push) Waiting to run
code_test / test_64 (push) Waiting to run
code_test / test_32 (push) Waiting to run
code_test / test_highlevel (push) Waiting to run

This commit is contained in:
Alessandro Ros 2024-08-18 19:12:26 +02:00 committed by GitHub
parent c5059fa7a0
commit 6256d0b893
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 196 additions and 129 deletions

View file

@ -471,20 +471,20 @@ The resulting stream will be available in path `/cam`.
_MediaMTX_ natively supports the Raspberry Pi Camera, enabling high-quality and low-latency video streaming from the camera to any user, for any purpose. There are a couple of requirements:
1. The server must run on a Raspberry Pi, with Raspberry Pi OS Bullseye as operative system. Both 32 bit and 64 bit architectures are supported.
1. The server must run on a Raspberry Pi, with one of the following operating systems:
2. Make sure that the legacy camera stack is disabled. Type `sudo raspi-config`, then go to `Interfacing options`, `enable/disable legacy camera support`, choose `no`. Reboot the system.
* Raspberry Pi OS Bookworm
* Raspberry Pi OS Bullseye
Both 32 bit and 64 bit architectures are supported.
2. If you are using Raspberry Pi OS Bullseye, make sure that the legacy camera stack is disabled. Type `sudo raspi-config`, then go to `Interfacing options`, `enable/disable legacy camera support`, choose `no`. Reboot the system.
If you want to run the standard (non-Docker) version of the server:
1. Make sure that the following packages are installed:
1. Download the server executable. If you're using 64-bit version of the operative system, make sure to pick the `arm64` variant.
* `libcamera0` (≥ 0.0.5)
* `libfreetype6`
2. download the server executable. If you're using 64-bit version of the operative system, make sure to pick the `arm64` variant.
3. edit `mediamtx.yml` and replace everything inside section `paths` with the following content:
2. Edit `mediamtx.yml` and replace everything inside section `paths` with the following content:
```yml
paths:
@ -494,7 +494,7 @@ If you want to run the standard (non-Docker) version of the server:
The resulting stream will be available in path `/cam`.
If you want to run the server inside Docker, you need to use the `latest-rpi` image (that already contains required libraries) 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:
```sh
docker run --rm -it \
@ -506,7 +506,7 @@ docker run --rm -it \
bluenviron/mediamtx:latest-rpi
```
Be aware that the Docker image is not compatible with cameras that requires a custom `libcamera` (like some ArduCam products), since it comes with a standard `libcamera` included.
Be aware that the server is not compatible with cameras that requires a custom `libcamera` (like some ArduCam products), since it comes with a bundled `libcamera`. If you want to use a custom one, you can [compile from source](#compile-from-source).
Camera settings can be changed by using the `rpiCamera*` parameters:

View file

@ -4,109 +4,16 @@
package rpicamera
import (
"debug/elf"
"fmt"
"os"
"os/exec"
"runtime"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
)
const (
tempPathPrefix = "/dev/shm/mediamtx-rpicamera-"
)
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 findLibrary(name string) (string, error) {
byts, err := exec.Command("ldconfig", "-p").Output()
if err == nil {
for _, line := range strings.Split(string(byts), "\n") {
f := strings.Split(line, " => ")
if len(f) == 2 && strings.Contains(f[1], name+".so") {
return f[1], nil
}
}
}
return "", fmt.Errorf("library '%s' not found", name)
}
func check64bit(fpath string) error {
f, err := os.Open(fpath)
if err != nil {
return err
}
defer f.Close()
ef, err := elf.NewFile(f)
if err != nil {
return err
}
defer ef.Close()
if ef.FileHeader.Class == elf.ELFCLASS64 {
return fmt.Errorf("libcamera is 64-bit, you need the 64-bit server version")
}
return nil
}
var (
mutex sync.Mutex
checked bool
)
func checkLibraries64Bit() error {
mutex.Lock()
defer mutex.Unlock()
if checked {
return nil
}
for _, name := range []string{"libcamera", "libcamera-base"} {
lib, err := findLibrary(name)
if err != nil {
return err
}
err = check64bit(lib)
if err != nil {
return err
}
}
checked = true
return nil
}
type camera struct {
Params params
OnData func(time.Duration, [][]byte)
@ -120,34 +27,41 @@ type camera struct {
}
func (c *camera) initialize() error {
if runtime.GOARCH == "arm" {
err := checkLibraries64Bit()
if err != nil {
return err
}
err := dumpComponent()
if err != nil {
return err
}
var err error
c.pipeConf, err = newPipe()
if err != nil {
freeComponent()
return err
}
c.pipeVideo, err = newPipe()
if err != nil {
c.pipeConf.close()
freeComponent()
return err
}
env := []string{
"PIPE_CONF_FD=" + strconv.FormatInt(int64(c.pipeConf.readFD), 10),
"PIPE_VIDEO_FD=" + strconv.FormatInt(int64(c.pipeVideo.writeFD), 10),
"LD_LIBRARY_PATH=" + dumpPath,
}
c.cmd, err = startEmbeddedExe(component, env)
c.cmd = exec.Command(filepath.Join(dumpPath, "exe"))
c.cmd.Stdout = os.Stdout
c.cmd.Stderr = os.Stderr
c.cmd.Env = env
c.cmd.Dir = dumpPath
err = c.cmd.Start()
if err != nil {
c.pipeConf.close()
c.pipeVideo.close()
freeComponent()
return err
}
@ -164,11 +78,12 @@ func (c *camera) initialize() error {
}()
select {
case <-c.waitDone:
case err := <-c.waitDone:
c.pipeConf.close()
c.pipeVideo.close()
<-c.readerDone
return fmt.Errorf("process exited unexpectedly")
freeComponent()
return fmt.Errorf("process exited unexpectedly: %v", err)
case err := <-c.readerDone:
if err != nil {
@ -176,6 +91,7 @@ func (c *camera) initialize() error {
<-c.waitDone
c.pipeConf.close()
c.pipeVideo.close()
freeComponent()
return err
}
}
@ -194,6 +110,7 @@ func (c *camera) close() {
c.pipeConf.close()
c.pipeVideo.close()
<-c.readerDone
freeComponent()
}
func (c *camera) reloadParams(params params) {

View file

@ -1,3 +1,107 @@
//go:build (linux && arm) || (linux && arm64)
// +build linux,arm linux,arm64
package rpicamera
import (
"os"
"path/filepath"
"strconv"
"sync"
"time"
)
//go:generate go run ./mtxrpicamdownloader
const (
dumpPrefix = "/dev/shm/mediamtx-rpicamera-"
)
var (
dumpMutex sync.Mutex
dumpCount = 0
dumpPath = ""
)
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
}
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, "exe"), 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)
}
}

View file

@ -4,8 +4,8 @@
package rpicamera
import (
_ "embed"
"embed"
)
//go:embed mtxrpicam_32
var component []byte
//go:embed mtxrpicam_32/*
var component embed.FS

View file

@ -4,8 +4,8 @@
package rpicamera
import (
_ "embed"
"embed"
)
//go:embed mtxrpicam_64
var component []byte
//go:embed mtxrpicam_64/*
var component embed.FS

View file

@ -0,0 +1,3 @@
package rpicamera
//go:generate go run ./mtxrpicamdownloader

View file

@ -1 +1 @@
v1.0.0
v2.0.0

View file

@ -2,6 +2,9 @@
package main
import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"log"
@ -10,6 +13,47 @@ import (
"strings"
)
func dumpTar(src io.Reader) error {
uncompressed, err := gzip.NewReader(src)
if err != nil {
return err
}
tr := tar.NewReader(uncompressed)
for {
header, err := tr.Next()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return err
}
switch header.Typeflag {
case tar.TypeDir:
err = os.Mkdir(header.Name, header.FileInfo().Mode())
if err != nil {
return err
}
case tar.TypeReg:
f, err := os.OpenFile(header.Name, os.O_WRONLY|os.O_CREATE, header.FileInfo().Mode())
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, tr)
if err != nil {
return err
}
}
}
return nil
}
func do() error {
buf, err := os.ReadFile("./mtxrpicamdownloader/VERSION")
if err != nil {
@ -19,7 +63,12 @@ func do() error {
log.Printf("downloading mediamtx-rpicamera version %s...", version)
for _, f := range []string{"mtxrpicam_32", "mtxrpicam_64"} {
for _, f := range []string{"mtxrpicam_32.tar.gz", "mtxrpicam_64.tar.gz"} {
err = os.RemoveAll(strings.TrimSuffix(f, ".tar.gz"))
if err != nil {
return err
}
res, err := http.Get("https://github.com/bluenviron/mediamtx-rpicamera/releases/download/" + version + "/" + f)
if err != nil {
return err
@ -30,14 +79,10 @@ func do() error {
return fmt.Errorf("bad status code: %v", res.StatusCode)
}
buf, err := io.ReadAll(res.Body)
err = dumpTar(res.Body)
if err != nil {
return err
}
if err = os.WriteFile(f, buf, 0o644); err != nil {
return err
}
}
log.Println("ok")

View file

@ -19,13 +19,11 @@ export DOCKERFILE_DOCKERHUB_FFMPEG
define DOCKERFILE_DOCKERHUB_RPI_BASE_32
FROM $(RPI32_IMAGE)
RUN apt update && apt install -y --no-install-recommends libcamera0 libfreetype6 && rm -rf /var/lib/apt/lists/*
endef
export DOCKERFILE_DOCKERHUB_RPI_BASE_32
define DOCKERFILE_DOCKERHUB_RPI_BASE_64
FROM $(RPI64_IMAGE)
RUN apt update && apt install -y --no-install-recommends libcamera0 libfreetype6 && rm -rf /var/lib/apt/lists/*
endef
export DOCKERFILE_DOCKERHUB_RPI_BASE_64