27 changed files with 1366 additions and 95 deletions
@ -1,3 +1,6 @@
@@ -1,3 +1,6 @@
|
||||
# do not add .git, since it is needed to extract the tag |
||||
|
||||
/tmp |
||||
/release |
||||
/coverage*.txt |
||||
/apidocs/*.html |
||||
|
@ -0,0 +1,94 @@
@@ -0,0 +1,94 @@
|
||||
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 { |
||||
params rpicamera.Params |
||||
parent rpiCameraSourceParent |
||||
} |
||||
|
||||
func newRPICameraSource( |
||||
params rpicamera.Params, |
||||
parent rpiCameraSourceParent, |
||||
) *rpiCameraSource { |
||||
return &rpiCameraSource{ |
||||
params: params, |
||||
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} |
||||
tracks := gortsplib.Tracks{track} |
||||
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: tracks, |
||||
}) |
||||
if res.err != nil { |
||||
return |
||||
} |
||||
|
||||
s.Log(logger.Info, "ready: %s", sourceTrackInfo(tracks)) |
||||
stream = res.stream |
||||
start = time.Now() |
||||
} |
||||
|
||||
pts := time.Since(start) |
||||
|
||||
stream.writeData(&data{ |
||||
trackID: 0, |
||||
ptsEqualsDTS: h264.IDRPresent(nalus), |
||||
pts: pts, |
||||
h264NALUs: nalus, |
||||
}) |
||||
} |
||||
|
||||
cam, err := rpicamera.New(s.params, 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"} |
||||
} |
@ -0,0 +1,85 @@
@@ -0,0 +1,85 @@
|
||||
//go:build rpicamera
|
||||
// +build rpicamera
|
||||
|
||||
package rpicamera |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
"os/exec" |
||||
"runtime" |
||||
"strconv" |
||||
"time" |
||||
) |
||||
|
||||
const ( |
||||
tempPathPrefix = "/dev/shm/rtspss-embeddedexe-" |
||||
) |
||||
|
||||
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 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 |
||||
} |
||||
|
||||
type embeddedExe struct { |
||||
cmd *exec.Cmd |
||||
} |
||||
|
||||
func newEmbeddedExe(content []byte, env []string) (*embeddedExe, error) { |
||||
err := checkArch() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
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 &embeddedExe{ |
||||
cmd: cmd, |
||||
}, nil |
||||
} |
||||
|
||||
func (e *embeddedExe) close() { |
||||
e.cmd.Process.Kill() |
||||
e.cmd.Wait() |
||||
} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
CC = gcc |
||||
CXX = g++ |
||||
|
||||
CFLAGS = \
|
||||
-Ofast \
|
||||
-Werror \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter |
||||
|
||||
CXXFLAGS = \
|
||||
-Ofast \
|
||||
-Werror \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter \
|
||||
-std=c++17 \
|
||||
$$(pkg-config --cflags libcamera) |
||||
|
||||
LDFLAGS = \
|
||||
-s \
|
||||
-pthread \
|
||||
$$(pkg-config --libs libcamera) |
||||
|
||||
OBJS = \
|
||||
camera.o \
|
||||
encoder.o \
|
||||
main.o \
|
||||
parameters.o |
||||
|
||||
all: exe |
||||
|
||||
%.o: %.c |
||||
$(CC) $(CFLAGS) -c $< -o $@ |
||||
|
||||
%.o: %.cpp |
||||
$(CXX) $(CXXFLAGS) -c $< -o $@ |
||||
|
||||
exe: $(OBJS) |
||||
$(CXX) $(LDFLAGS) -o $@ $^ |
@ -0,0 +1,209 @@
@@ -0,0 +1,209 @@
|
||||
#include <stdio.h> |
||||
#include <stdarg.h> |
||||
#include <cstring> |
||||
#include <sys/mman.h> |
||||
#include <iostream> |
||||
|
||||
#include <libcamera/camera_manager.h> |
||||
#include <libcamera/camera.h> |
||||
#include <libcamera/formats.h> |
||||
#include <libcamera/control_ids.h> |
||||
#include <libcamera/controls.h> |
||||
#include <libcamera/framebuffer_allocator.h> |
||||
#include <linux/videodev2.h> |
||||
|
||||
#include "parameters.h" |
||||
#include "camera.h" |
||||
|
||||
using libcamera::CameraManager; |
||||
using libcamera::CameraConfiguration; |
||||
using libcamera::Camera; |
||||
using libcamera::StreamRoles; |
||||
using libcamera::StreamRole; |
||||
using libcamera::StreamConfiguration; |
||||
using libcamera::Stream; |
||||
using libcamera::ControlList; |
||||
using libcamera::FrameBufferAllocator; |
||||
using libcamera::FrameBuffer; |
||||
using libcamera::Request; |
||||
using libcamera::Span; |
||||
|
||||
namespace controls = libcamera::controls; |
||||
namespace formats = libcamera::formats; |
||||
|
||||
char errbuf[256]; |
||||
|
||||
static void set_error(const char *format, ...) { |
||||
va_list args; |
||||
va_start(args, format); |
||||
vsnprintf(errbuf, 256, format, args); |
||||
} |
||||
|
||||
const char *camera_get_error() { |
||||
return errbuf; |
||||
} |
||||
|
||||
struct CameraPriv { |
||||
parameters_t *params; |
||||
camera_frame_cb frame_cb; |
||||
std::unique_ptr<CameraManager> camera_manager; |
||||
std::shared_ptr<Camera> camera; |
||||
std::unique_ptr<FrameBufferAllocator> allocator; |
||||
std::vector<std::unique_ptr<Request>> requests; |
||||
}; |
||||
|
||||
static int get_v4l2_colorspace(std::optional<libcamera::ColorSpace> const &cs) { |
||||
if (cs == libcamera::ColorSpace::Rec709) { |
||||
return V4L2_COLORSPACE_REC709; |
||||
} |
||||
return V4L2_COLORSPACE_SMPTE170M; |
||||
} |
||||
|
||||
bool camera_create(parameters_t *params, camera_frame_cb frame_cb, camera_t **cam) { |
||||
std::unique_ptr<CameraPriv> camp = std::make_unique<CameraPriv>(); |
||||
|
||||
camp->camera_manager = std::make_unique<CameraManager>(); |
||||
int ret = camp->camera_manager->start(); |
||||
if (ret != 0) { |
||||
set_error("CameraManager.start() failed"); |
||||
return false; |
||||
} |
||||
|
||||
std::vector<std::shared_ptr<libcamera::Camera>> cameras = camp->camera_manager->cameras(); |
||||
auto rem = std::remove_if(cameras.begin(), cameras.end(), |
||||
[](auto &cam) { return cam->id().find("/usb") != std::string::npos; }); |
||||
cameras.erase(rem, cameras.end()); |
||||
if (params->camera_id >= cameras.size()){ |
||||
set_error("selected camera is not available"); |
||||
return false; |
||||
} |
||||
|
||||
camp->camera = camp->camera_manager->get(cameras[params->camera_id]->id()); |
||||
if (camp->camera == NULL) { |
||||
set_error("CameraManager.get() failed"); |
||||
return false; |
||||
} |
||||
|
||||
ret = camp->camera->acquire(); |
||||
if (ret != 0) { |
||||
set_error("Camera.acquire() failed"); |
||||
return false; |
||||
} |
||||
|
||||
StreamRoles stream_roles = { StreamRole::VideoRecording }; |
||||
std::unique_ptr<CameraConfiguration> conf = camp->camera->generateConfiguration(stream_roles); |
||||
if (conf == NULL) { |
||||
set_error("Camera.generateConfiguration() failed"); |
||||
return false; |
||||
} |
||||
|
||||
StreamConfiguration &stream_conf = conf->at(0); |
||||
stream_conf.pixelFormat = formats::YUV420; |
||||
stream_conf.bufferCount = params->buffer_count; |
||||
stream_conf.size.width = params->width; |
||||
stream_conf.size.height = params->height; |
||||
if (params->width >= 1280 || params->height >= 720) { |
||||
stream_conf.colorSpace = libcamera::ColorSpace::Rec709; |
||||
} else { |
||||
stream_conf.colorSpace = libcamera::ColorSpace::Smpte170m; |
||||
} |
||||
|
||||
CameraConfiguration::Status vstatus = conf->validate(); |
||||
if (vstatus == CameraConfiguration::Invalid) { |
||||
set_error("StreamConfiguration.validate() failed"); |
||||
return false; |
||||
} |
||||
|
||||
int res = camp->camera->configure(conf.get()); |
||||
if (res != 0) { |
||||
set_error("Camera.configure() failed"); |
||||
return false; |
||||
} |
||||
|
||||
Stream *stream = stream_conf.stream(); |
||||
|
||||
camp->allocator = std::make_unique<FrameBufferAllocator>(camp->camera); |
||||
res = camp->allocator->allocate(stream); |
||||
if (res < 0) { |
||||
set_error("allocate() failed"); |
||||
return false; |
||||
} |
||||
|
||||
for (const std::unique_ptr<FrameBuffer> &buffer : camp->allocator->buffers(stream)) { |
||||
std::unique_ptr<Request> request = camp->camera->createRequest((uint64_t)camp.get()); |
||||
if (request == NULL) { |
||||
set_error("createRequest() failed"); |
||||
return false; |
||||
} |
||||
|
||||
int res = request->addBuffer(stream, buffer.get()); |
||||
if (res != 0) { |
||||
set_error("addBuffer() failed"); |
||||
return false; |
||||
} |
||||
|
||||
camp->requests.push_back(std::move(request)); |
||||
} |
||||
|
||||
camp->params = params; |
||||
camp->frame_cb = frame_cb; |
||||
*cam = camp.release(); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
static void on_request_complete(Request *request) { |
||||
if (request->status() == Request::RequestCancelled) { |
||||
return; |
||||
} |
||||
|
||||
CameraPriv *camp = (CameraPriv *)request->cookie(); |
||||
|
||||
FrameBuffer *buffer = request->buffers().begin()->second; |
||||
|
||||
int size = 0; |
||||
for (const FrameBuffer::Plane &plane : buffer->planes()) { |
||||
size += plane.length; |
||||
} |
||||
|
||||
camp->frame_cb(buffer->planes()[0].fd.get(), size, buffer->metadata().timestamp / 1000); |
||||
|
||||
request->reuse(Request::ReuseFlag::ReuseBuffers); |
||||
camp->camera->queueRequest(request); |
||||
} |
||||
|
||||
int camera_get_stride(camera_t *cam) { |
||||
CameraPriv *camp = (CameraPriv *)cam; |
||||
return (*camp->camera->streams().begin())->configuration().stride; |
||||
} |
||||
|
||||
int camera_get_colorspace(camera_t *cam) { |
||||
CameraPriv *camp = (CameraPriv *)cam; |
||||
return get_v4l2_colorspace((*camp->camera->streams().begin())->configuration().colorSpace); |
||||
} |
||||
|
||||
bool camera_start(camera_t *cam) { |
||||
CameraPriv *camp = (CameraPriv *)cam; |
||||
|
||||
ControlList ctrls = ControlList(controls::controls); |
||||
int64_t frame_time = 1000000 / camp->params->fps; |
||||
ctrls.set(controls::FrameDurationLimits, Span<const int64_t, 2>({ frame_time, frame_time })); |
||||
|
||||
int res = camp->camera->start(&ctrls); |
||||
if (res != 0) { |
||||
set_error("Camera.start() failed"); |
||||
return false; |
||||
} |
||||
|
||||
camp->camera->requestCompleted.connect(on_request_complete); |
||||
|
||||
for (std::unique_ptr<Request> &request : camp->requests) { |
||||
int res = camp->camera->queueRequest(request.get()); |
||||
if (res != 0) { |
||||
set_error("Camera.queueRequest() failed"); |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
return true; |
||||
} |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
typedef void camera_t; |
||||
|
||||
typedef void (*camera_frame_cb)(int buffer_fd, uint64_t size, uint64_t timestamp); |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
const char *camera_get_error(); |
||||
bool camera_create(parameters_t *params, camera_frame_cb frame_cb, camera_t **cam); |
||||
int camera_get_stride(camera_t *cam); |
||||
int camera_get_colorspace(camera_t *cam); |
||||
bool camera_start(camera_t *cam); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
@ -0,0 +1,315 @@
@@ -0,0 +1,315 @@
|
||||
#include <stdbool.h> |
||||
#include <stdio.h> |
||||
#include <stdarg.h> |
||||
#include <stdlib.h> |
||||
#include <stdint.h> |
||||
#include <fcntl.h> |
||||
#include <unistd.h> |
||||
#include <string.h> |
||||
#include <sys/mman.h> |
||||
#include <sys/ioctl.h> |
||||
#include <errno.h> |
||||
#include <poll.h> |
||||
#include <pthread.h> |
||||
|
||||
#include <linux/videodev2.h> |
||||
|
||||
#include "parameters.h" |
||||
#include "encoder.h" |
||||
|
||||
char errbuf[256]; |
||||
|
||||
static void set_error(const char *format, ...) { |
||||
va_list args; |
||||
va_start(args, format); |
||||
vsnprintf(errbuf, 256, format, args); |
||||
} |
||||
|
||||
const char *encoder_get_error() { |
||||
return errbuf; |
||||
} |
||||
|
||||
typedef struct { |
||||
parameters_t *params; |
||||
int fd; |
||||
void **capture_buffers; |
||||
int cur_buffer; |
||||
encoder_output_cb output_cb; |
||||
pthread_t output_thread; |
||||
} encoder_priv_t; |
||||
|
||||
static void *output_thread(void *userdata) { |
||||
encoder_priv_t *encp = (encoder_priv_t *)userdata; |
||||
|
||||
while (true) { |
||||
struct pollfd p = { encp->fd, POLLIN, 0 }; |
||||
int res = poll(&p, 1, 200); |
||||
if (res == -1) { |
||||
fprintf(stderr, "output_thread(): poll() failed\n"); |
||||
exit(1); |
||||
} |
||||
|
||||
if (p.revents & POLLIN) { |
||||
struct v4l2_buffer buf = {0}; |
||||
struct v4l2_plane planes[VIDEO_MAX_PLANES] = {0}; |
||||
buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; |
||||
buf.memory = V4L2_MEMORY_DMABUF; |
||||
buf.length = 1; |
||||
buf.m.planes = planes; |
||||
int res = ioctl(encp->fd, VIDIOC_DQBUF, &buf); |
||||
if (res != 0) { |
||||
fprintf(stderr, "output_thread(): ioctl() failed\n"); |
||||
exit(1); |
||||
} |
||||
|
||||
memset(&buf, 0, sizeof(buf)); |
||||
memset(planes, 0, sizeof(planes)); |
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; |
||||
buf.memory = V4L2_MEMORY_MMAP; |
||||
buf.length = 1; |
||||
buf.m.planes = planes; |
||||
res = ioctl(encp->fd, VIDIOC_DQBUF, &buf); |
||||
if (res == 0) { |
||||
const uint8_t *bufmem = (const uint8_t *)encp->capture_buffers[buf.index]; |
||||
int bufsize = buf.m.planes[0].bytesused; |
||||
encp->output_cb(bufmem, bufsize); |
||||
|
||||
int index = buf.index; |
||||
int length = buf.m.planes[0].length; |
||||
|
||||
struct v4l2_buffer buf = {0}; |
||||
struct v4l2_plane planes[VIDEO_MAX_PLANES] = {0}; |
||||
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; |
||||
buf.memory = V4L2_MEMORY_MMAP; |
||||
buf.index = index; |
||||
buf.length = 1; |
||||
buf.m.planes = planes; |
||||
buf.m.planes[0].bytesused = 0; |
||||
buf.m.planes[0].length = length; |
||||
int res = ioctl(encp->fd, VIDIOC_QBUF, &buf); |
||||
if (res < 0) { |
||||
fprintf(stderr, "output_thread(): ioctl() failed\n"); |
||||
exit(1); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return NULL; |
||||
} |
||||
|
||||
bool encoder_create(parameters_t *params, int stride, int colorspace, encoder_output_cb output_cb, encoder_t **enc) { |
||||
*enc = malloc(sizeof(encoder_priv_t)); |
||||
encoder_priv_t *encp = (encoder_priv_t *)(*enc); |
||||
|
||||
encp->fd = open("/dev/video11", O_RDWR, 0); |
||||
if (encp->fd < 0) { |
||||
set_error("unable to open device"); |
||||
return false; |
||||
} |
||||
|
||||
struct v4l2_control ctrl = {0}; |
||||
ctrl.id = V4L2_CID_MPEG_VIDEO_BITRATE; |
||||
ctrl.value = params->bitrate; |
||||
int res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl); |
||||
if (res != 0) { |
||||
set_error("unable to set bitrate"); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
ctrl.id = V4L2_CID_MPEG_VIDEO_H264_PROFILE; |
||||
ctrl.value = params->profile; |
||||
res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl); |
||||
if (res != 0) { |
||||
set_error("unable to set profile"); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
ctrl.id = V4L2_CID_MPEG_VIDEO_H264_LEVEL; |
||||
ctrl.value = params->level; |
||||
res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl); |
||||
if (res != 0) { |
||||
set_error("unable to set level"); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
ctrl.id = V4L2_CID_MPEG_VIDEO_H264_I_PERIOD; |
||||
ctrl.value = params->idr_period; |
||||
res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl); |
||||
if (res != 0) { |
||||
set_error("unable to set IDR period"); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
ctrl.id = V4L2_CID_MPEG_VIDEO_REPEAT_SEQ_HEADER; |
||||
ctrl.value = 0; |
||||
res = ioctl(encp->fd, VIDIOC_S_CTRL, &ctrl); |
||||
if (res != 0) { |
||||
set_error("unable to set REPEAT_SEQ_HEADER"); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
struct v4l2_format fmt = {0}; |
||||
fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; |
||||
fmt.fmt.pix_mp.width = params->width; |
||||
fmt.fmt.pix_mp.height = params->height; |
||||
fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_YUV420; |
||||
fmt.fmt.pix_mp.plane_fmt[0].bytesperline = stride; |
||||
fmt.fmt.pix_mp.field = V4L2_FIELD_ANY; |
||||
fmt.fmt.pix_mp.colorspace = colorspace; |
||||
fmt.fmt.pix_mp.num_planes = 1; |
||||
res = ioctl(encp->fd, VIDIOC_S_FMT, &fmt); |
||||
if (res != 0) { |
||||
set_error("unable to set output format"); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
memset(&fmt, 0, sizeof(fmt)); |
||||
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; |
||||
fmt.fmt.pix_mp.width = params->width; |
||||
fmt.fmt.pix_mp.height = params->height; |
||||
fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_H264; |
||||
fmt.fmt.pix_mp.field = V4L2_FIELD_ANY; |
||||
fmt.fmt.pix_mp.colorspace = V4L2_COLORSPACE_DEFAULT; |
||||
fmt.fmt.pix_mp.num_planes = 1; |
||||
fmt.fmt.pix_mp.plane_fmt[0].bytesperline = 0; |
||||
fmt.fmt.pix_mp.plane_fmt[0].sizeimage = 512 << 10; |
||||
res = ioctl(encp->fd, VIDIOC_S_FMT, &fmt); |
||||
if (res != 0) { |
||||
set_error("unable to set capture format"); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
struct v4l2_streamparm parm = {0}; |
||||
parm.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; |
||||
parm.parm.output.timeperframe.numerator = 1; |
||||
parm.parm.output.timeperframe.denominator = params->fps; |
||||
res = ioctl(encp->fd, VIDIOC_S_PARM, &parm); |
||||
if (res != 0) { |
||||
set_error("unable to set fps"); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
struct v4l2_requestbuffers reqbufs = {0}; |
||||
reqbufs.count = params->buffer_count; |
||||
reqbufs.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; |
||||
reqbufs.memory = V4L2_MEMORY_DMABUF; |
||||
res = ioctl(encp->fd, VIDIOC_REQBUFS, &reqbufs); |
||||
if (res != 0) { |
||||
set_error("unable to set output buffers"); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
memset(&reqbufs, 0, sizeof(reqbufs)); |
||||
reqbufs.count = params->capture_buffer_count; |
||||
reqbufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; |
||||
reqbufs.memory = V4L2_MEMORY_MMAP; |
||||
res = ioctl(encp->fd, VIDIOC_REQBUFS, &reqbufs); |
||||
if (res != 0) { |
||||
set_error("unable to set capture buffers"); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
encp->capture_buffers = malloc(sizeof(void *) * reqbufs.count); |
||||
|
||||
for (unsigned int i = 0; i < reqbufs.count; i++) { |
||||
struct v4l2_plane planes[VIDEO_MAX_PLANES]; |
||||
|
||||
struct v4l2_buffer buffer = {0}; |
||||
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; |
||||
buffer.memory = V4L2_MEMORY_MMAP; |
||||
buffer.index = i; |
||||
buffer.length = 1; |
||||
buffer.m.planes = planes; |
||||
int res = ioctl(encp->fd, VIDIOC_QUERYBUF, &buffer); |
||||
if (res != 0) { |
||||
set_error("unable to query buffer"); |
||||
free(encp->capture_buffers); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
encp->capture_buffers[i] = mmap( |
||||
0, |
||||
buffer.m.planes[0].length, |
||||
PROT_READ | PROT_WRITE, MAP_SHARED, |
||||
encp->fd, |
||||
buffer.m.planes[0].m.mem_offset); |
||||
if (encp->capture_buffers[i] == MAP_FAILED) { |
||||
set_error("mmap() failed"); |
||||
free(encp->capture_buffers); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
res = ioctl(encp->fd, VIDIOC_QBUF, &buffer); |
||||
if (res != 0) { |
||||
set_error("ioctl() failed"); |
||||
free(encp->capture_buffers); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; |
||||
res = ioctl(encp->fd, VIDIOC_STREAMON, &type); |
||||
if (res != 0) { |
||||
set_error("unable to activate output stream"); |
||||
free(encp->capture_buffers); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; |
||||
res = ioctl(encp->fd, VIDIOC_STREAMON, &type); |
||||
if (res != 0) { |
||||
set_error("unable to activate capture stream"); |
||||
free(encp->capture_buffers); |
||||
close(encp->fd); |
||||
return false; |
||||
} |
||||
|
||||
encp->params = params; |
||||
encp->cur_buffer = 0; |
||||
encp->output_cb = output_cb; |
||||
|
||||
pthread_create(&encp->output_thread, NULL, output_thread, encp); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
void encoder_encode(encoder_t *enc, int buffer_fd, size_t size, int64_t timestamp_us) { |
||||
encoder_priv_t *encp = (encoder_priv_t *)enc; |
||||
|
||||
int index = encp->cur_buffer++; |
||||
encp->cur_buffer %= encp->params->buffer_count; |
||||
|
||||
struct v4l2_buffer buf = {0}; |
||||
struct v4l2_plane planes[VIDEO_MAX_PLANES] = {0}; |
||||
buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; |
||||
buf.index = index; |
||||
buf.field = V4L2_FIELD_NONE; |
||||
buf.memory = V4L2_MEMORY_DMABUF; |
||||
buf.length = 1; |
||||
buf.timestamp.tv_sec = timestamp_us / 1000000; |
||||
buf.timestamp.tv_usec = timestamp_us % 1000000; |
||||
buf.m.planes = planes; |
||||
buf.m.planes[0].m.fd = buffer_fd; |
||||
buf.m.planes[0].bytesused = size; |
||||
buf.m.planes[0].length = size; |
||||
int res = ioctl(encp->fd, VIDIOC_QBUF, &buf); |
||||
if (res != 0) { |
||||
fprintf(stderr, "encoder_encode(): ioctl() failed\n"); |
||||
exit(1); |
||||
} |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
typedef void encoder_t; |
||||
|
||||
typedef void (*encoder_output_cb)(const uint8_t *buf, uint64_t size); |
||||
|
||||
const char *encoder_get_error(); |
||||
bool encoder_create(parameters_t *params, int stride, int colorspace, encoder_output_cb output_cb, encoder_t **enc); |
||||
void encoder_encode(encoder_t *enc, int buffer_fd, size_t size, int64_t timestamp_us); |
@ -0,0 +1,117 @@
@@ -0,0 +1,117 @@
|
||||
#include <stdio.h> |
||||
#include <stdbool.h> |
||||
#include <stdarg.h> |
||||
#include <stdint.h> |
||||
#include <stdlib.h> |
||||
#include <fcntl.h> |
||||
#include <unistd.h> |
||||
#include <signal.h> |
||||
#include <string.h> |
||||
#include <pthread.h> |
||||
|
||||
#include "parameters.h" |
||||
#include "camera.h" |
||||
#include "encoder.h" |
||||
|
||||
int pipe_fd; |
||||
pthread_mutex_t pipe_mutex; |
||||
parameters_t params; |
||||
camera_t *cam; |
||||
encoder_t *enc; |
||||
|
||||
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, const uint8_t *buf, int n) { |
||||
char head[] = {'b'}; |
||||
n++; |
||||
write(fd, &n, 4); |
||||
write(fd, head, 1); |
||||
write(fd, buf, n-1); |
||||
} |
||||
|
||||
static void on_frame(int buffer_fd, uint64_t size, uint64_t timestamp) { |
||||
encoder_encode(enc, buffer_fd, size, timestamp); |
||||
} |
||||
|
||||
static void on_encoder_output(const uint8_t *buf, uint64_t size) { |
||||
pthread_mutex_lock(&pipe_mutex); |
||||
pipe_write_buf(pipe_fd, buf, size); |
||||
pthread_mutex_unlock(&pipe_mutex); |
||||
} |
||||
|
||||
static bool init_siglistener(sigset_t *set) { |
||||
sigemptyset(set); |
||||
|
||||
int res = sigaddset(set, SIGKILL); |
||||
if (res == -1) { |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
int main() { |
||||
pipe_fd = atoi(getenv("PIPE_FD")); |
||||
|
||||
pthread_mutex_init(&pipe_mutex, NULL); |
||||
pthread_mutex_lock(&pipe_mutex); |
||||
|
||||
parameters_load(¶ms); |
||||
|
||||
bool ok = camera_create( |
||||
¶ms, |
||||
on_frame, |
||||
&cam); |
||||
if (!ok) { |
||||
pipe_write_error(pipe_fd, "camera_create(): %s", camera_get_error()); |
||||
return 5; |
||||
} |
||||
|
||||
ok = encoder_create( |
||||
¶ms, |
||||
camera_get_stride(cam), |
||||
camera_get_colorspace(cam), |
||||
on_encoder_output, |
||||
&enc); |
||||
if (!ok) { |
||||
pipe_write_error(pipe_fd, "encoder_create(): %s", encoder_get_error()); |
||||
return 5; |
||||
} |
||||
|
||||
ok = camera_start(cam); |
||||
if (!ok) { |
||||
pipe_write_error(pipe_fd, "camera_start(): %s", camera_get_error()); |
||||
return 5; |
||||
} |
||||
|
||||
sigset_t set; |
||||
ok = init_siglistener(&set); |
||||
if (!ok) { |
||||
pipe_write_error(pipe_fd, "init_siglistener() failed"); |
||||
return 5; |
||||
} |
||||
|
||||
pipe_write_ready(pipe_fd); |
||||
pthread_mutex_unlock(&pipe_mutex); |
||||
|
||||
int sig; |
||||
sigwait(&set, &sig); |
||||
|
||||
return 0; |
||||
} |
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
#include <stdlib.h> |
||||
#include <string.h> |
||||
|
||||
#include <linux/videodev2.h> |
||||
|
||||
#include "parameters.h" |
||||
|
||||
void parameters_load(parameters_t *params) { |
||||
params->camera_id = atoi(getenv("CAMERA_ID")); |
||||
params->width = atoi(getenv("WIDTH")); |
||||
params->height = atoi(getenv("HEIGHT")); |
||||
params->fps = atoi(getenv("FPS")); |
||||
params->idr_period = atoi(getenv("IDR_PERIOD")); |
||||
params->bitrate = atoi(getenv("BITRATE")); |
||||
|
||||
const char *profile = getenv("PROFILE"); |
||||
if (strcmp(profile, "baseline") == 0) { |
||||
params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_BASELINE; |
||||
} else if (strcmp(profile, "main") == 0) { |
||||
params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_MAIN; |
||||
} else { |
||||
params->profile = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH; |
||||
} |
||||
|
||||
const char *level = getenv("LEVEL"); |
||||
if (strcmp(level, "4.0") == 0) { |
||||
params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_0; |
||||
} else if (strcmp(level, "4.1") == 0) { |
||||
params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_1; |
||||
} else { |
||||
params->level = V4L2_MPEG_VIDEO_H264_LEVEL_4_2; |
||||
} |
||||
|
||||
params->buffer_count = 3; |
||||
params->capture_buffer_count = params->buffer_count * 2; |
||||
} |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
typedef struct { |
||||
unsigned int camera_id; |
||||
unsigned int width; |
||||
unsigned int height; |
||||
unsigned int fps; |
||||
unsigned int idr_period; |
||||
unsigned int bitrate; |
||||
unsigned int profile; |
||||
unsigned int level; |
||||
unsigned int buffer_count; |
||||
unsigned int capture_buffer_count; |
||||
} parameters_t; |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
void parameters_load(parameters_t *params); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
package rpicamera |
||||
|
||||
// Params is a set of camera parameters.
|
||||
type Params struct { |
||||
CameraID int |
||||
Width int |
||||
Height int |
||||
FPS int |
||||
IDRPeriod int |
||||
Bitrate int |
||||
Profile string |
||||
Level string |
||||
} |
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
//go:build rpicamera
|
||||
// +build rpicamera
|
||||
|
||||
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 |
||||
} |
@ -0,0 +1,134 @@
@@ -0,0 +1,134 @@
|
||||
//go:build rpicamera
|
||||
// +build rpicamera
|
||||
|
||||
package rpicamera |
||||
|
||||
import ( |
||||
_ "embed" |
||||
"fmt" |
||||
"strconv" |
||||
|
||||
"github.com/aler9/gortsplib/pkg/h264" |
||||
) |
||||
|
||||
//go:embed exe/exe
|
||||
var exeContent []byte |
||||
|
||||
type RPICamera struct { |
||||
onData func([][]byte) |
||||
|
||||
exe *embeddedExe |
||||
pipe *pipe |
||||
|
||||
waitDone chan error |
||||
readerDone chan error |
||||
} |
||||
|
||||
func New( |
||||
params Params, |
||||
onData func([][]byte), |
||||
) (*RPICamera, error) { |
||||
pipe, err := newPipe() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
env := []string{ |
||||
"PIPE_FD=" + strconv.FormatInt(int64(pipe.writeFD), 10), |
||||
"CAMERA_ID=" + strconv.FormatInt(int64(params.CameraID), 10), |
||||
"WIDTH=" + strconv.FormatInt(int64(params.Width), 10), |
||||
"HEIGHT=" + strconv.FormatInt(int64(params.Height), 10), |
||||
"FPS=" + strconv.FormatInt(int64(params.FPS), 10), |
||||
"IDR_PERIOD=" + strconv.FormatInt(int64(params.IDRPeriod), 10), |
||||
"BITRATE=" + strconv.FormatInt(int64(params.Bitrate), 10), |
||||
"PROFILE=" + params.Profile, |
||||
"LEVEL=" + params.Level, |
||||
} |
||||
|
||||
exe, err := newEmbeddedExe(exeContent, env) |
||||
if err != nil { |
||||
pipe.close() |
||||
return nil, err |
||||
} |
||||
|
||||
waitDone := make(chan error) |
||||
go func() { |
||||
waitDone <- exe.cmd.Wait() |
||||
}() |
||||
|
||||
readerDone := make(chan error) |
||||
go func() { |
||||
readerDone <- func() error { |
||||
buf, err := pipe.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 pipe (%c)", buf[0]) |
||||
} |
||||
}() |
||||
}() |
||||
|
||||
select { |
||||
case <-waitDone: |
||||
exe.close() |
||||
pipe.close() |
||||
<-readerDone |
||||
return nil, fmt.Errorf("process exited unexpectedly") |
||||
|
||||
case err := <-readerDone: |
||||
if err != nil { |
||||
exe.close() |
||||
<-waitDone |
||||
pipe.close() |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
readerDone = make(chan error) |
||||
go func() { |
||||
readerDone <- func() error { |
||||
for { |
||||
buf, err := pipe.read() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if buf[0] != 'b' { |
||||
return fmt.Errorf("unexpected output from pipe (%c)", buf[0]) |
||||
} |
||||
buf = buf[1:] |
||||
|
||||
nalus, err := h264.AnnexBUnmarshal(buf) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
onData(nalus) |
||||
} |
||||
}() |
||||
}() |
||||
|
||||
return &RPICamera{ |
||||
onData: onData, |
||||
exe: exe, |
||||
pipe: pipe, |
||||
waitDone: waitDone, |
||||
readerDone: readerDone, |
||||
}, nil |
||||
} |
||||
|
||||
func (c *RPICamera) Close() { |
||||
c.exe.close() |
||||
<-c.waitDone |
||||
c.pipe.close() |
||||
<-c.readerDone |
||||
} |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
//go:build !rpicamera
|
||||
// +build !rpicamera
|
||||
|
||||
package rpicamera |
||||
|
||||
import ( |
||||
"fmt" |
||||
) |
||||
|
||||
// RPICamera is a RPI Camera reader.
|
||||
type RPICamera struct{} |
||||
|
||||
// New allocates a RPICamera.
|
||||
func New( |
||||
params Params, |
||||
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() { |
||||
} |
Loading…
Reference in new issue