Ready-to-use SRT / WebRTC / RTSP / RTMP / LL-HLS media server and media proxy that allows to read, publish, proxy, record and playback video and audio streams.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

318 lines
5.5 KiB

//go:build rpicamera
// +build rpicamera
package rpicamera
import (
_ "embed"
"encoding/base64"
"fmt"
"os"
"os/exec"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/bluenviron/mediacommon/pkg/codecs/h264"
)
const (
tempPathPrefix = "/dev/shm/rtspss-embeddedexe-"
)
//go:embed exe/exe
var exeContent []byte
func getKernelArch() (string, error) {
cmd := exec.Command("uname", "-m")
byts, err := cmd.Output()
if err != nil {
return "", err
}
return string(byts[:len(byts)-1]), nil
}
func startEmbeddedExe(content []byte, env []string) (*exec.Cmd, error) {
tempPath := tempPathPrefix + strconv.FormatInt(time.Now().UnixNano(), 10)
err := os.WriteFile(tempPath, content, 0o755)
if err != nil {
return nil, err
}
cmd := exec.Command(tempPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = env
err = cmd.Start()
os.Remove(tempPath)
if err != nil {
return nil, err
}
return cmd, nil
}
func serializeParams(p Params) []byte {
rv := reflect.ValueOf(p)
rt := rv.Type()
nf := rv.NumField()
ret := make([]string, nf)
for i := 0; i < nf; i++ {
entry := rt.Field(i).Name + ":"
f := rv.Field(i)
switch f.Kind() {
case reflect.Int:
entry += strconv.FormatInt(f.Int(), 10)
case reflect.Float64:
entry += strconv.FormatFloat(f.Float(), 'f', -1, 64)
case reflect.String:
entry += base64.StdEncoding.EncodeToString([]byte(f.String()))
case reflect.Bool:
if f.Bool() {
entry += "1"
} else {
entry += "0"
}
default:
panic("unhandled type")
}
ret[i] = entry
}
return []byte(strings.Join(ret, " "))
}
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 setupSymlink(name string) error {
lib, err := findLibrary(name)
if err != nil {
return err
}
os.Remove("/dev/shm/" + name + ".so.x.x.x")
return os.Symlink(lib, "/dev/shm/"+name+".so.x.x.x")
}
// 32-bit embedded executables can't run on 64-bit.
func checkArch() error {
if runtime.GOARCH != "arm" {
return nil
}
arch, err := getKernelArch()
if err != nil {
return err
}
if arch == "aarch64" {
return fmt.Errorf("OS is 64-bit, you need the arm64 server version")
}
return nil
}
var (
mutex sync.Mutex
setupped bool
)
func setupLibcameraOnce() error {
mutex.Lock()
defer mutex.Unlock()
if !setupped {
err := checkArch()
if err != nil {
return err
}
err = setupSymlink("libcamera")
if err != nil {
return err
}
err = setupSymlink("libcamera-base")
if err != nil {
return err
}
setupped = true
}
return nil
}
// Cleanup cleanups files created by the camera implementation.
func Cleanup() {
if setupped {
os.Remove("/dev/shm/libcamera-base.so.x.x.x")
os.Remove("/dev/shm/libcamera.so.x.x.x")
}
}
type RPICamera struct {
onData func(time.Duration, [][]byte)
cmd *exec.Cmd
pipeConf *pipe
pipeVideo *pipe
waitDone chan error
readerDone chan error
}
func New(
params Params,
onData func(time.Duration, [][]byte),
) (*RPICamera, error) {
err := setupLibcameraOnce()
if err != nil {
return nil, err
}
c := &RPICamera{
onData: onData,
}
c.pipeConf, err = newPipe()
if err != nil {
return nil, err
}
c.pipeVideo, err = newPipe()
if err != nil {
c.pipeConf.close()
return nil, err
}
env := []string{
"LD_LIBRARY_PATH=/dev/shm",
"PIPE_CONF_FD=" + strconv.FormatInt(int64(c.pipeConf.readFD), 10),
"PIPE_VIDEO_FD=" + strconv.FormatInt(int64(c.pipeVideo.writeFD), 10),
}
c.cmd, err = startEmbeddedExe(exeContent, env)
if err != nil {
c.pipeConf.close()
c.pipeVideo.close()
return nil, err
}
c.pipeConf.write(append([]byte{'c'}, serializeParams(params)...))
c.waitDone = make(chan error)
go func() {
c.waitDone <- c.cmd.Wait()
}()
c.readerDone = make(chan error)
go func() {
c.readerDone <- c.readReady()
}()
select {
case <-c.waitDone:
c.pipeConf.close()
c.pipeVideo.close()
<-c.readerDone
return nil, fmt.Errorf("process exited unexpectedly")
case err := <-c.readerDone:
if err != nil {
c.pipeConf.write([]byte{'e'})
<-c.waitDone
c.pipeConf.close()
c.pipeVideo.close()
return nil, err
}
}
c.readerDone = make(chan error)
go func() {
c.readerDone <- c.readData()
}()
return c, nil
}
func (c *RPICamera) Close() {
c.pipeConf.write([]byte{'e'})
<-c.waitDone
c.pipeConf.close()
c.pipeVideo.close()
<-c.readerDone
}
func (c *RPICamera) ReloadParams(params Params) {
c.pipeConf.write(append([]byte{'c'}, serializeParams(params)...))
}
func (c *RPICamera) readReady() error {
buf, err := c.pipeVideo.read()
if err != nil {
return err
}
switch buf[0] {
case 'e':
return fmt.Errorf(string(buf[1:]))
case 'r':
return nil
default:
return fmt.Errorf("unexpected output from video pipe: '0x%.2x'", buf[0])
}
}
func (c *RPICamera) readData() error {
for {
buf, err := c.pipeVideo.read()
if err != nil {
return err
}
if buf[0] != 'b' {
return fmt.Errorf("unexpected output from pipe (%c)", buf[0])
}
tmp := uint64(buf[8])<<56 | uint64(buf[7])<<48 | uint64(buf[6])<<40 | uint64(buf[5])<<32 |
uint64(buf[4])<<24 | uint64(buf[3])<<16 | uint64(buf[2])<<8 | uint64(buf[1])
dts := time.Duration(tmp) * time.Microsecond
nalus, err := h264.AnnexBUnmarshal(buf[9:])
if err != nil {
return err
}
c.onData(dts, nalus)
}
}