Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rpi: embed libcamera and libfreetype into the server (#2581) #3665

Merged
merged 1 commit into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

If you want to run the standard (non-Docker) version of the server:
Both 32 bit and 64 bit architectures are supported.

1. Make sure that the following packages are installed:
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.

* `libcamera0` (≥ 0.0.5)
* `libfreetype6`
If you want to run the standard (non-Docker) version of the server:

2. download the server executable. If you're using 64-bit version of the operative system, make sure to pick the `arm64` variant.
1. 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:
Expand All @@ -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 \
Expand All @@ -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:

Expand Down
123 changes: 20 additions & 103 deletions internal/staticsources/rpicamera/camera.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}

Expand All @@ -164,18 +78,20 @@ 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 {
c.pipeConf.write([]byte{'e'})
<-c.waitDone
c.pipeConf.close()
c.pipeVideo.close()
freeComponent()
return err
}
}
Expand All @@ -194,6 +110,7 @@ func (c *camera) close() {
c.pipeConf.close()
c.pipeVideo.close()
<-c.readerDone
freeComponent()
}

func (c *camera) reloadParams(params params) {
Expand Down
104 changes: 104 additions & 0 deletions internal/staticsources/rpicamera/component.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
6 changes: 3 additions & 3 deletions internal/staticsources/rpicamera/component_32.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
package rpicamera

import (
_ "embed"
"embed"
)

//go:embed mtxrpicam_32
var component []byte
//go:embed mtxrpicam_32/*
var component embed.FS
6 changes: 3 additions & 3 deletions internal/staticsources/rpicamera/component_64.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
package rpicamera

import (
_ "embed"
"embed"
)

//go:embed mtxrpicam_64
var component []byte
//go:embed mtxrpicam_64/*
var component embed.FS
3 changes: 3 additions & 0 deletions internal/staticsources/rpicamera/component_dl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package rpicamera

//go:generate go run ./mtxrpicamdownloader
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v1.0.0
v2.0.0
Loading
Loading