diff --git a/.gitignore b/.gitignore index fb2b06ea3b6d..c447f49e1c80 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ /release /coverage*.txt /apidocs/*.html +/rtsp-simple-server-rpi +/rtsp-simple-server-rpi64 diff --git a/Makefile b/Makefile index 6c5d76cedcc8..af7d149844c3 100644 --- a/Makefile +++ b/Makefile @@ -278,3 +278,95 @@ dockerhub: docker buildx rm builder rm -rf $$HOME/.docker/manifests/* + +define DOCKERFILE_RPI +FROM balenalib/raspberrypi3:buster-run AS camera + +RUN ["cross-build-start"] + +RUN apt update && apt install -y g++ libraspberrypi-dev libcamera-dev + +WORKDIR /s/internal/rpicamera + +COPY internal/rpicamera/*.cpp internal/rpicamera/*.c ./ + +RUN gcc \ + rpicamera_legacy.c \ + -o rpicamera_legacy \ + -Ofast \ + -Werror \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + -I/opt/vc/include \ + -L/opt/vc/lib -lmmal_core -lmmal_util -lmmal_vc_client -lvcsm -lvcos -lvchiq_arm + +RUN g++ \ + rpicamera_libcamera.cpp \ + -o rpicamera_libcamera \ + -Ofast \ + -Werror \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + $(pkg-config --cxxflags --libs libcamera) + +FROM $(BASE_IMAGE) + +WORKDIR /s + +COPY go.mod go.sum ./ +RUN go mod download + +COPY --from=camera /s/internal/rpicamera/rpicamera_legacy /s/internal/rpicamera/ +COPY --from=camera /s/internal/rpicamera/rpicamera_libcamera /s/internal/rpicamera/ + +COPY . . + +RUN GOOS=linux GOARCH=arm GOARM=6 go build -tags rpilibcamera -o /out +endef +export DOCKERFILE_RPI + +rpi: + echo "$$DOCKERFILE_RPI" | docker build . -f - -t temp + docker run --rm -it -v $(PWD):/o temp sh -c "mv /out /o/rtsp-simple-server-rpi" + +define DOCKERFILE_RPI64 +FROM balenalib/raspberrypi3-64:buster-run AS camera + +RUN ["cross-build-start"] + +RUN apt update && apt install -y g++ libcamera-dev + +WORKDIR /s/internal/rpicamera + +COPY internal/rpicamera/*.cpp ./ + +RUN g++ \ + rpicamera_libcamera.cpp \ + -o rpicamera_libcamera \ + -Ofast \ + -Werror \ + -Wall \ + -Wextra \ + -Wno-unused-parameter \ + $(pkg-config --cxxflags --libs libcamera) + +FROM $(BASE_IMAGE) + +WORKDIR /s + +COPY go.mod go.sum ./ +RUN go mod download + +COPY --from=camera /s/internal/rpicamera/rpicamera_libcamera /s/internal/rpicamera/ + +COPY . . + +RUN GOOS=linux GOARCH=arm64 go build -tags rpilibcamera -o /out +endef +export DOCKERFILE_RPI64 + +rpi64: + echo "$$DOCKERFILE_RPI64" | docker build . -f - -t temp + docker run --rm -it -v $(PWD):/o temp sh -c "mv /out /o/rtsp-simple-server-rpi64" diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index 96e498b4d225..b1919e56ae1f 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -199,6 +199,7 @@ components: - $ref: '#/components/schemas/PathSourceRTSPSource' - $ref: '#/components/schemas/PathSourceRTMPSource' - $ref: '#/components/schemas/PathSourceHLSSource' + - $ref: '#/components/schemas/PathSourceRPICameraSource' sourceReady: type: boolean readers: @@ -258,6 +259,13 @@ components: type: string enum: [hlsSource] + PathSourceRPICameraSource: + type: object + properties: + type: + type: string + enum: [rpiCameraSource] + PathReaderRTSPSession: type: object properties: diff --git a/internal/conf/path.go b/internal/conf/path.go index 6e9905f58480..57226d3bcc78 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -165,6 +165,8 @@ func (pconf *PathConf) checkAndFillMissing(conf *Conf, name string) error { return fmt.Errorf("'%s' is not a valid RTSP URL", pconf.SourceRedirect) } + case pconf.Source == "rpicamera": + default: return fmt.Errorf("invalid source: '%s'", pconf.Source) } diff --git a/internal/core/path.go b/internal/core/path.go index 9f29aa9323cb..f07cab5ee676 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -333,7 +333,8 @@ func (pa *path) hasStaticSource() bool { strings.HasPrefix(pa.conf.Source, "rtsps://") || strings.HasPrefix(pa.conf.Source, "rtmp://") || strings.HasPrefix(pa.conf.Source, "http://") || - strings.HasPrefix(pa.conf.Source, "https://") + strings.HasPrefix(pa.conf.Source, "https://") || + pa.conf.Source == "rpicamera" } func (pa *path) hasOnDemandStaticSource() bool { diff --git a/internal/core/rpicamera_source.go b/internal/core/rpicamera_source.go new file mode 100644 index 000000000000..74ec8aca35df --- /dev/null +++ b/internal/core/rpicamera_source.go @@ -0,0 +1,107 @@ +package core + +import ( + "context" + "time" + + "github.com/aler9/gortsplib" + "github.com/aler9/gortsplib/pkg/h264" + "github.com/aler9/gortsplib/pkg/rtph264" + + "github.com/aler9/rtsp-simple-server/internal/logger" + "github.com/aler9/rtsp-simple-server/internal/rpicamera" +) + +type rpiCameraSourceParent interface { + log(logger.Level, string, ...interface{}) + sourceStaticImplSetReady(req pathSourceStaticSetReadyReq) pathSourceStaticSetReadyRes + sourceStaticImplSetNotReady(req pathSourceStaticSetNotReadyReq) +} + +type rpiCameraSource struct { + parent rpiCameraSourceParent +} + +func newRPICameraSource( + parent rpiCameraSourceParent, +) *rpiCameraSource { + return &rpiCameraSource{ + parent: parent, + } +} + +func (s *rpiCameraSource) Log(level logger.Level, format string, args ...interface{}) { + s.parent.log(level, "[rpicamera source] "+format, args...) +} + +// run implements sourceStaticImpl. +func (s *rpiCameraSource) run(ctx context.Context) error { + track := &gortsplib.TrackH264{PayloadType: 96} + enc := &rtph264.Encoder{PayloadType: 96} + enc.Init() + var stream *stream + var start time.Time + + onData := func(nalus [][]byte) { + if stream == nil { + res := s.parent.sourceStaticImplSetReady(pathSourceStaticSetReadyReq{ + tracks: gortsplib.Tracks{track}, + }) + if res.err != nil { + return + } + + s.Log(logger.Info, "ready") + stream = res.stream + start = time.Now() + } + + pts := time.Since(start) + + pkts, err := enc.Encode(nalus, pts) + if err != nil { + return + } + + lastPkt := len(pkts) - 1 + for i, pkt := range pkts { + if i != lastPkt { + stream.writeData(&data{ + trackID: 0, + rtp: pkt, + ptsEqualsDTS: false, + }) + } else { + stream.writeData(&data{ + trackID: 0, + rtp: pkt, + ptsEqualsDTS: h264.IDRPresent(nalus), + h264NALUs: nalus, + h264PTS: pts, + }) + } + } + } + + cam, err := rpicamera.New(onData) + if err != nil { + return err + } + defer cam.Close() + + defer func() { + if stream != nil { + s.parent.sourceStaticImplSetNotReady(pathSourceStaticSetNotReadyReq{}) + } + }() + + <-ctx.Done() + return nil +} + +// apiSourceDescribe implements sourceStaticImpl. +func (*rpiCameraSource) apiSourceDescribe() interface{} { + return struct { + Type string `json:"type"` + }{"rpiCameraSource"} +} diff --git a/internal/core/source_static.go b/internal/core/source_static.go index 277fd44d90fc..27a7e7fc230c 100644 --- a/internal/core/source_static.go +++ b/internal/core/source_static.go @@ -96,6 +96,10 @@ func newSourceStatic( s.ur, s.fingerprint, s) + + case s.ur == "rpicamera": + s.impl = newRPICameraSource( + s) } return s diff --git a/internal/rpicamera/embeddedexecutable.go b/internal/rpicamera/embeddedexecutable.go new file mode 100644 index 000000000000..0184d5b9e8d7 --- /dev/null +++ b/internal/rpicamera/embeddedexecutable.go @@ -0,0 +1,80 @@ +//go:build rpilibcamera || rpilegacy +// +build rpilibcamera rpilegacy + +package rpicamera + +import ( + "fmt" + "os" + "os/exec" + "runtime" +) + +const ( + embeddedExecutableTempPath = "/dev/shm/rtspss-embeddedbin" +) + +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 +} + +// 32-bit embedded binaries 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("you need the arm64 version") + } + + return nil +} + +type embeddedExecutable struct { + cmd *exec.Cmd +} + +func newEmbeddedExecutable(content []byte, arg string) (*embeddedExecutable, error) { + err := checkArch() + if err != nil { + return nil, err + } + + err = os.WriteFile(embeddedExecutableTempPath, content, 0o755) + if err != nil { + return nil, err + } + + cmd := exec.Command(embeddedExecutableTempPath, arg) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err = cmd.Start() + if err != nil { + os.Remove(embeddedExecutableTempPath) + return nil, err + } + + return &embeddedExecutable{ + cmd: cmd, + }, nil +} + +func (e *embeddedExecutable) close() { + e.cmd.Process.Kill() + e.cmd.Wait() + os.Remove(embeddedExecutableTempPath) +} diff --git a/internal/rpicamera/pipe.go b/internal/rpicamera/pipe.go new file mode 100644 index 000000000000..466445890ed5 --- /dev/null +++ b/internal/rpicamera/pipe.go @@ -0,0 +1,69 @@ +//go:build rpilibcamera || rpilegacy +// +build rpilibcamera rpilegacy + +package rpicamera + +import ( + "encoding/binary" + "syscall" +) + +func syscallReadAll(fd int, buf []byte) error { + size := len(buf) + read := 0 + + for { + n, err := syscall.Read(fd, buf[read:size]) + if err != nil { + return err + } + + read += n + if read >= size { + break + } + } + + return nil +} + +type pipe struct { + readFD int + writeFD int +} + +func newPipe() (*pipe, error) { + fds := make([]int, 2) + err := syscall.Pipe(fds) + if err != nil { + return nil, err + } + + return &pipe{ + readFD: fds[0], + writeFD: fds[1], + }, nil +} + +func (p *pipe) close() { + syscall.Close(p.readFD) + syscall.Close(p.writeFD) +} + +func (p *pipe) read() ([]byte, error) { + sizebuf := make([]byte, 4) + err := syscallReadAll(p.readFD, sizebuf) + if err != nil { + return nil, err + } + + size := int(binary.LittleEndian.Uint32(sizebuf)) + buf := make([]byte, size) + + err = syscallReadAll(p.readFD, buf) + if err != nil { + return nil, err + } + + return buf, nil +} diff --git a/internal/rpicamera/rpicamera_legacy.c b/internal/rpicamera/rpicamera_legacy.c new file mode 100644 index 000000000000..36c5895f0a11 --- /dev/null +++ b/internal/rpicamera/rpicamera_legacy.c @@ -0,0 +1,368 @@ +//go:build never +// +build never + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#define WIDTH 1920 +#define HEIGHT 1080 +#define FPS 25 +#define IDR_PERIOD 50 +#define BITRATE 1000000 // maximum is 25MBps +#define PROFILE MMAL_VIDEO_PROFILE_H264_MAIN +#define LEVEL MMAL_VIDEO_LEVEL_H264_41 +#define CAMERA_ID 0 +#define CAMERA_OUTPUT_ID 1 +#define BUFFER_COUNT 1 +#define BUFFER_SIZE 200000 // must be less than maximum NALU size + +static void pipe_write_error(int fd, const char *format, ...) { + char buf[256]; + buf[0] = 'e'; + va_list args; + va_start(args, format); + vsnprintf(&buf[1], 255, format, args); + int n = strlen(buf); + write(fd, &n, 4); + write(fd, buf, n); +} + +static void pipe_write_ready(int fd) { + char buf[] = {'r'}; + int n = 1; + write(fd, &n, 4); + write(fd, buf, n); +} + +static void pipe_write_buf(int fd, uint8_t *buf, int n) { + char head[] = {'b'}; + n++; + write(fd, &n, 4); + write(fd, head, 1); + write(fd, buf, n-1); +} + +static MMAL_STATUS_T connect_ports(MMAL_PORT_T *output_port, MMAL_PORT_T *input_port, MMAL_CONNECTION_T **connection) { + MMAL_STATUS_T status = mmal_connection_create(connection, output_port, input_port, + MMAL_CONNECTION_FLAG_TUNNELLING | MMAL_CONNECTION_FLAG_ALLOCATION_ON_INPUT); + if (status != MMAL_SUCCESS) { + return status; + } + + status = mmal_connection_enable(*connection); + if (status != MMAL_SUCCESS) { + mmal_connection_destroy(*connection); + return status; + } + + return MMAL_SUCCESS; +} + +static bool check_camera(int pipe_fd) { + MMAL_COMPONENT_T *camera_info; + MMAL_STATUS_T status = mmal_component_create(MMAL_COMPONENT_DEFAULT_CAMERA_INFO, &camera_info); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "check_camera(): mmal_component_create() failed"); + return false; + } + + MMAL_PARAMETER_CAMERA_INFO_T param; + param.hdr.id = MMAL_PARAMETER_CAMERA_INFO; + param.hdr.size = sizeof(param); + status = mmal_port_parameter_get(camera_info->control, ¶m.hdr); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "check_camera(): mmal_port_parameter_get() failed"); + mmal_component_destroy(camera_info); + return false; + } + + if (param.num_cameras != 1) { + pipe_write_error(pipe_fd, "check_camera(): num_cameras is not 1 (is %d)", param.num_cameras); + mmal_component_destroy(camera_info); + return false; + } + + mmal_component_destroy(camera_info); + return true; +} + +static bool create_camera(int pipe_fd, MMAL_COMPONENT_T **cam) { + MMAL_STATUS_T status = mmal_component_create(MMAL_COMPONENT_DEFAULT_CAMERA, cam); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "create_camera(): mmal_component_create() failed"); + return false; + } + + MMAL_PARAMETER_INT32_T camera_num; + camera_num.hdr.id = MMAL_PARAMETER_CAMERA_NUM; + camera_num.hdr.size = sizeof(camera_num); + camera_num.value = CAMERA_ID; + status = mmal_port_parameter_set((*cam)->control, &camera_num.hdr); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "create_camera(): mmal_port_parameter_set() failed"); + mmal_component_destroy(*cam); + return false; + } + + MMAL_PORT_T *video_port = (*cam)->output[CAMERA_OUTPUT_ID]; + + MMAL_ES_FORMAT_T *format = video_port->format; + format->encoding_variant = MMAL_ENCODING_I420; + format->encoding = MMAL_ENCODING_OPAQUE; + MMAL_VIDEO_FORMAT_T *video = &format->es->video; + video->width = VCOS_ALIGN_UP(WIDTH, 32); + video->height = VCOS_ALIGN_UP(HEIGHT, 16); + video->crop.width = WIDTH; + video->crop.height = HEIGHT; + video->frame_rate.num = FPS; + video->frame_rate.den = 1; + status = mmal_port_format_commit(video_port); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "create_camera(): mmal_port_format_commit() failed"); + mmal_component_destroy(*cam); + return false; + } + + if (video_port->buffer_num < BUFFER_COUNT) { + video_port->buffer_num = BUFFER_COUNT; + } + + status = mmal_component_enable(*cam); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "create_camera(): mmal_component_enable() failed"); + mmal_component_destroy(*cam); + return false; + } + + return true; +} + +static bool create_encoder(int pipe_fd, MMAL_COMPONENT_T **enc, MMAL_POOL_T **enc_pool) { + MMAL_STATUS_T status = mmal_component_create(MMAL_COMPONENT_DEFAULT_VIDEO_ENCODER, enc); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "create_encoder(): mmal_component_create() failed"); + return false; + } + + MMAL_PORT_T *enc_input_port = (*enc)->input[0]; + MMAL_PORT_T *enc_output_port = (*enc)->output[0]; + mmal_format_copy(enc_output_port->format, enc_input_port->format); + + MMAL_ES_FORMAT_T *format = enc_output_port->format; + format->encoding = MMAL_ENCODING_H264; + format->bitrate = BITRATE; + enc_output_port->buffer_size = BUFFER_SIZE; + enc_output_port->buffer_num = enc_output_port->buffer_num_recommended; + MMAL_VIDEO_FORMAT_T *video = &format->es->video; + video->frame_rate.num = 0; + video->frame_rate.den = 1; + status = mmal_port_format_commit(enc_output_port); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "create_encoder(): mmal_port_format_commit() failed"); + mmal_component_destroy(*enc); + return false; + } + + MMAL_PARAMETER_UINT32_T param_idr; + param_idr.hdr.id = MMAL_PARAMETER_INTRAPERIOD; + param_idr.hdr.size = sizeof(param_idr); + param_idr.value = IDR_PERIOD; + status = mmal_port_parameter_set(enc_output_port, ¶m_idr.hdr); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "create_encoder(): mmal_port_parameter_set() failed"); + mmal_component_destroy(*enc); + return false; + } + + MMAL_PARAMETER_VIDEO_PROFILE_T param_prof; + param_prof.hdr.id = MMAL_PARAMETER_PROFILE; + param_prof.hdr.size = sizeof(param_prof); + param_prof.profile[0].profile = PROFILE; + param_prof.profile[0].level = LEVEL; + status = mmal_port_parameter_set(enc_output_port, ¶m_prof.hdr); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "create_encoder(): mmal_port_parameter_set() failed"); + mmal_component_destroy(*enc); + return false; + } + + status = mmal_port_parameter_set_boolean(enc_input_port, MMAL_PARAMETER_VIDEO_IMMUTABLE_INPUT, 1); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "create_encoder(): mmal_port_parameter_set_boolean() failed"); + mmal_component_destroy(*enc); + return false; + } + + status = mmal_port_parameter_set_boolean(enc_output_port, MMAL_PARAMETER_VIDEO_ENCODE_SPS_TIMING, 1); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "create_encoder(): mmal_port_parameter_set_boolean() failed"); + mmal_component_destroy(*enc); + return false; + } + + status = mmal_port_parameter_set_boolean(enc_output_port, MMAL_PARAMETER_VIDEO_ENCODE_H264_LOW_LATENCY, 1); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "create_encoder(): mmal_port_parameter_set_boolean() failed"); + mmal_component_destroy(*enc); + return false; + } + + status = mmal_component_enable(*enc); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "create_encoder(): mmal_component_enable() failed"); + mmal_component_destroy(*enc); + return false; + } + + *enc_pool = mmal_port_pool_create(enc_output_port, enc_output_port->buffer_num, enc_output_port->buffer_size); + if (*enc_pool == NULL) { + pipe_write_error(pipe_fd, "create_encoder(): mmal_port_pool_create() failed"); + mmal_component_destroy(*enc); + return false; + } + + return true; +} + +typedef struct { + int pipe_fd; + MMAL_POOL_T *enc_pool; +} userdata; + +static void encoder_buffer_callback(MMAL_PORT_T *port, MMAL_BUFFER_HEADER_T *buffer) { + userdata *ud = (userdata *)port->userdata; + + mmal_buffer_header_mem_lock(buffer); + + if (buffer->length > 0) { + pipe_write_buf(ud->pipe_fd, buffer->data, buffer->length); + } + + mmal_buffer_header_mem_unlock(buffer); + mmal_buffer_header_release(buffer); + + if (port->is_enabled != 0) { + MMAL_BUFFER_HEADER_T *new_buffer = mmal_queue_get(ud->enc_pool->queue); + mmal_port_send_buffer(port, new_buffer); + } +} + +static bool link_camera_to_encoder(int pipe_fd, MMAL_COMPONENT_T *cam, MMAL_COMPONENT_T *enc, + MMAL_POOL_T *enc_pool, MMAL_CONNECTION_T **conn, userdata *ud) { + MMAL_PORT_T *cam_video_port = cam->output[CAMERA_OUTPUT_ID]; + MMAL_PORT_T *enc_input_port = enc->input[0]; + MMAL_PORT_T *enc_output_port = enc->output[0]; + + MMAL_STATUS_T status = connect_ports(cam_video_port, enc_input_port, conn); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "link_camera_to_encoder(): connect_ports() failed"); + return false; + } + + ud->pipe_fd = pipe_fd; + ud->enc_pool = enc_pool; + enc_output_port->userdata = (struct MMAL_PORT_USERDATA_T *)ud; + status = mmal_port_enable(enc_output_port, encoder_buffer_callback); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "link_camera_to_encoder(): mmal_port_enable() failed"); + mmal_connection_destroy(*conn); + return false; + } + + int num = mmal_queue_length(enc_pool->queue); + for (int q = 0; q < num; q++) { + MMAL_BUFFER_HEADER_T *buf = mmal_queue_get(enc_pool->queue); + + MMAL_STATUS_T status = mmal_port_send_buffer(enc_output_port, buf); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "link_camera_to_encoder(): mmal_port_send_buffer() failed"); + mmal_port_disable(enc_output_port); + mmal_connection_destroy(*conn); + return false; + } + } + + mmal_port_parameter_set_boolean(cam_video_port, MMAL_PARAMETER_CAPTURE, 1); + if (status != MMAL_SUCCESS) { + pipe_write_error(pipe_fd, "link_camera_to_encoder(): mmal_port_parameter_set_boolean() failed"); + mmal_port_disable(enc_output_port); + mmal_connection_destroy(*conn); + return false; + } + + return true; +} + +static bool init_siglistener(int pipe_fd, sigset_t *set) { + sigemptyset(set); + + int res = sigaddset(set, SIGKILL); + if (res == -1) { + pipe_write_error(pipe_fd, "sigaddset() failed"); + return false; + } + + return true; +} + +int main(int argc,char *argv[]) { + int pipe_fd = atoi(argv[1]); + + bool ok = check_camera(pipe_fd); + if (!ok) { + return 5; + } + + MMAL_COMPONENT_T *cam; + ok = create_camera(pipe_fd, &cam); + if (!ok) { + return 5; + } + + MMAL_COMPONENT_T *enc; + MMAL_POOL_T *enc_pool; + ok = create_encoder(pipe_fd, &enc, &enc_pool); + if (!ok) { + mmal_component_destroy(cam); + return 5; + } + + MMAL_CONNECTION_T *conn; + userdata ud; + ok = link_camera_to_encoder(pipe_fd, cam, enc, enc_pool, &conn, &ud); + if (!ok) { + mmal_port_pool_destroy(enc->output[0], enc_pool); + mmal_component_destroy(enc); + mmal_component_destroy(cam); + return 5; + } + + sigset_t set; + ok = init_siglistener(pipe_fd, &set); + if (!ok) { + mmal_port_pool_destroy(enc->output[0], enc_pool); + mmal_component_destroy(enc); + mmal_component_destroy(cam); + return 5; + } + + pipe_write_ready(pipe_fd); + + int sig; + sigwait(&set, &sig); + + mmal_port_pool_destroy(enc->output[0], enc_pool); + mmal_component_destroy(enc); + mmal_component_destroy(cam); + + return 0; +} diff --git a/internal/rpicamera/rpicamera_legacy.go b/internal/rpicamera/rpicamera_legacy.go new file mode 100644 index 000000000000..9da7f98140f3 --- /dev/null +++ b/internal/rpicamera/rpicamera_legacy.go @@ -0,0 +1,94 @@ +//go:build rpilegacy +// +build rpilegacy + +package rpicamera + +import ( + _ "embed" + "fmt" + "strconv" + + "github.com/aler9/gortsplib/pkg/h264" +) + +//go:embed rpicamera_legacy +var exeLegacy []byte + +type RPICamera struct { + onData func([][]byte) + + bin *embeddedExecutable + pipe *pipe + + readerClosed chan struct{} +} + +func New( + onData func([][]byte), +) (*RPICamera, error) { + pipe, err := newPipe() + if err != nil { + return nil, err + } + + bin, err := newEmbeddedExecutable(exeLegacy, strconv.FormatInt(int64(pipe.writeFD), 10)) + if err != nil { + return nil, err + } + + buf, err := pipe.read() + if err != nil { + bin.close() + pipe.close() + return nil, err + } + + switch buf[0] { + case 'e': + bin.close() + pipe.close() + return nil, fmt.Errorf(string(buf[1:])) + + case 'r': + + default: + return nil, fmt.Errorf("unexpected output from pipe") + } + + readerClosed := make(chan struct{}) + go func() { + defer close(readerClosed) + + for { + buf, err := pipe.read() + if err != nil { + return + } + + if buf[0] != 'b' { + return + } + buf = buf[1:] + + nalus, err := h264.AnnexBUnmarshal(buf) + if err != nil { + return + } + + onData(nalus) + } + }() + + return &RPICamera{ + onData: onData, + bin: bin, + pipe: pipe, + readerClosed: readerClosed, + }, nil +} + +func (c *RPICamera) Close() { + c.bin.close() + c.pipe.close() + <-c.readerClosed +} diff --git a/internal/rpicamera/rpicamera_libcamera.cpp b/internal/rpicamera/rpicamera_libcamera.cpp new file mode 100644 index 000000000000..19e68b0680dc --- /dev/null +++ b/internal/rpicamera/rpicamera_libcamera.cpp @@ -0,0 +1,10 @@ +//go:build never +// +build never + +#include + +int main() { + printf("TODO\n"); + + return 0; +} diff --git a/internal/rpicamera/rpicamera_libcamera.go b/internal/rpicamera/rpicamera_libcamera.go new file mode 100644 index 000000000000..9907699a38fa --- /dev/null +++ b/internal/rpicamera/rpicamera_libcamera.go @@ -0,0 +1,87 @@ +//go:build rpilibcamera +// +build rpilibcamera + +package rpicamera + +import ( + _ "embed" + "fmt" + "strconv" +) + +//go:embed rpicamera_libcamera +var exeLibcamera []byte + +type RPICamera struct { + onData func([][]byte) + + bin *embeddedExecutable + pipe *pipe + + readerClosed chan struct{} +} + +func New( + onData func([][]byte), +) (*RPICamera, error) { + pipe, err := newPipe() + if err != nil { + return nil, err + } + + bin, err := newEmbeddedExecutable(exeLibcamera, strconv.FormatInt(int64(pipe.writeFD), 10)) + if err != nil { + return nil, err + } + + buf, err := pipe.read() + if err != nil { + bin.close() + pipe.close() + return nil, err + } + + switch buf[0] { + case 'e': + bin.close() + pipe.close() + return nil, fmt.Errorf(string(buf[1:])) + + case 'r': + + default: + return nil, fmt.Errorf("unexpected output from pipe") + } + + readerClosed := make(chan struct{}) + go func() { + defer close(readerClosed) + + for { + buf, err := pipe.read() + if err != nil { + return + } + + if buf[0] != 'b' { + return + } + buf = buf[1:] + + fmt.Println("TODO", len(buf)) + } + }() + + return &RPICamera{ + onData: onData, + bin: bin, + pipe: pipe, + readerClosed: readerClosed, + }, nil +} + +func (c *RPICamera) Close() { + c.bin.close() + c.pipe.close() + <-c.readerClosed +} diff --git a/internal/rpicamera/rpicamera_unsupported.go b/internal/rpicamera/rpicamera_unsupported.go new file mode 100644 index 000000000000..84aff532583b --- /dev/null +++ b/internal/rpicamera/rpicamera_unsupported.go @@ -0,0 +1,22 @@ +//go:build !rpilibcamera && !rpilegacy +// +build !rpilibcamera,!rpilegacy + +package rpicamera + +import ( + "fmt" +) + +// RPICamera is a RPI Camera reader. +type RPICamera struct{} + +// New allocates a RPICamera. +func New( + onData func([][]byte), +) (*RPICamera, error) { + return nil, fmt.Errorf("server was compiled without support for the Raspberry Pi Camera") +} + +// Close closes a RPICamera. +func (c *RPICamera) Close() { +} diff --git a/rtsp-simple-server.yml b/rtsp-simple-server.yml index 0053ef409185..7daea8ba0c7a 100644 --- a/rtsp-simple-server.yml +++ b/rtsp-simple-server.yml @@ -173,6 +173,7 @@ paths: # * http://existing-url/stream.m3u8 -> the stream is pulled from another HLS server # * https://existing-url/stream.m3u8 -> the stream is pulled from another HLS server with HTTPS # * redirect -> the stream is provided by another path or server + # * rpicamera -> the stream is provided by a Raspberry Pi Camera source: publisher # If the source is an RTSP or RTSPS URL, this is the protocol that will be used to