From 6256d0b893813a0953f95a8f2e05b2b52a8a79c2 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 18 Aug 2024 19:12:26 +0200 Subject: [PATCH] rpi: embed libcamera and libfreetype into the server (#2581) (#3665) --- README.md | 22 ++-- internal/staticsources/rpicamera/camera.go | 123 +++--------------- internal/staticsources/rpicamera/component.go | 104 +++++++++++++++ .../staticsources/rpicamera/component_32.go | 6 +- .../staticsources/rpicamera/component_64.go | 6 +- .../staticsources/rpicamera/component_dl.go | 3 + .../rpicamera/mtxrpicamdownloader/VERSION | 2 +- .../rpicamera/mtxrpicamdownloader/main.go | 57 +++++++- scripts/dockerhub.mk | 2 - 9 files changed, 196 insertions(+), 129 deletions(-) create mode 100644 internal/staticsources/rpicamera/component_dl.go diff --git a/README.md b/README.md index c5b86d61..bc3d6f6b 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/internal/staticsources/rpicamera/camera.go b/internal/staticsources/rpicamera/camera.go index 46bddb86..b4f31479 100644 --- a/internal/staticsources/rpicamera/camera.go +++ b/internal/staticsources/rpicamera/camera.go @@ -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) { diff --git a/internal/staticsources/rpicamera/component.go b/internal/staticsources/rpicamera/component.go index 918e554e..35d2ec65 100644 --- a/internal/staticsources/rpicamera/component.go +++ b/internal/staticsources/rpicamera/component.go @@ -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) + } +} diff --git a/internal/staticsources/rpicamera/component_32.go b/internal/staticsources/rpicamera/component_32.go index 0cdeffd9..d91cce1c 100644 --- a/internal/staticsources/rpicamera/component_32.go +++ b/internal/staticsources/rpicamera/component_32.go @@ -4,8 +4,8 @@ package rpicamera import ( - _ "embed" + "embed" ) -//go:embed mtxrpicam_32 -var component []byte +//go:embed mtxrpicam_32/* +var component embed.FS diff --git a/internal/staticsources/rpicamera/component_64.go b/internal/staticsources/rpicamera/component_64.go index d1c562a0..cdc924dd 100644 --- a/internal/staticsources/rpicamera/component_64.go +++ b/internal/staticsources/rpicamera/component_64.go @@ -4,8 +4,8 @@ package rpicamera import ( - _ "embed" + "embed" ) -//go:embed mtxrpicam_64 -var component []byte +//go:embed mtxrpicam_64/* +var component embed.FS diff --git a/internal/staticsources/rpicamera/component_dl.go b/internal/staticsources/rpicamera/component_dl.go new file mode 100644 index 00000000..918e554e --- /dev/null +++ b/internal/staticsources/rpicamera/component_dl.go @@ -0,0 +1,3 @@ +package rpicamera + +//go:generate go run ./mtxrpicamdownloader diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION index 0ec25f75..46b105a3 100644 --- a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION @@ -1 +1 @@ -v1.0.0 +v2.0.0 diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go b/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go index 6f8ee9bc..30d38292 100644 --- a/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/main.go @@ -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") diff --git a/scripts/dockerhub.mk b/scripts/dockerhub.mk index 4d26a79e..d7905245 100644 --- a/scripts/dockerhub.mk +++ b/scripts/dockerhub.mk @@ -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