golanggohlsrtmpwebrtcmedia-serverobs-studiortcprtmp-proxyrtmp-serverrtprtsprtsp-proxyrtsp-relayrtsp-serversrtstreamingwebrtc-proxy
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
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) |
|
} |
|
}
|
|
|