/*
* Copyright (C) 2023 Mikhail Burakov. This file is part of streamer.
*
* streamer is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* streamer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with streamer. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "capture.h"
#include "colorspace.h"
#include "encode.h"
#include "gpu.h"
#include "input.h"
#include "toolbox/io_muxer.h"
#include "toolbox/utils.h"
// TODO(mburakov): Currently zwp_linux_dmabuf_v1 has no way to provide
// colorspace and range information to the compositor. Maybe this would change
// in the future, i.e keep an eye on color-representation Wayland protocol:
// https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/183
static const enum YuvColorspace colorspace = kItuRec601;
static const enum YuvRange range = kNarrowRange;
static const int capture_period = 1000000000 / 60;
static volatile sig_atomic_t g_signal;
static void OnSignal(int status) { g_signal = status; }
struct Contexts {
struct IoMuxer io_muxer;
int timer_fd;
int server_fd;
struct GpuContext* gpu_context;
struct CaptureContext* capture_context;
int client_fd;
struct InputHandler* input_handler;
struct EncodeContext* encode_context;
};
static int CreateServerSocket(const char* arg) {
int port = atoi(arg);
if (0 > port || port > UINT16_MAX) {
LOG("Invalid port number argument");
return -1;
}
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
LOG("Failed to create socket (%s)", strerror(errno));
return -1;
}
if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &(int){1}, sizeof(int))) {
LOG("Failed to reuse socket address (%s)", strerror(errno));
goto rollback_sock;
}
const struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons((uint16_t)port),
};
if (bind(sock, (const struct sockaddr*)&addr, sizeof(addr))) {
LOG("Failed to bind socket (%s)", strerror(errno));
goto rollback_sock;
}
if (listen(sock, SOMAXCONN)) {
LOG("Failed to listen socket (%s)", strerror(errno));
goto rollback_sock;
}
return sock;
rollback_sock:
close(sock);
return -1;
}
static void MaybeDropClient(struct Contexts* contexts) {
static const struct itimerspec spec = {0};
if (contexts->encode_context) {
EncodeContextDestroy(contexts->encode_context);
contexts->encode_context = NULL;
}
if (contexts->input_handler) {
IoMuxerForget(&contexts->io_muxer,
InputHandlerGetEventsFd(contexts->input_handler));
InputHandlerDestroy(contexts->input_handler);
contexts->input_handler = NULL;
}
if (contexts->client_fd != -1) {
IoMuxerForget(&contexts->io_muxer, contexts->client_fd);
close(contexts->client_fd);
contexts->client_fd = -1;
}
}
static void OnTimerExpire(void* user) {
struct Contexts* contexts = user;
if (!IoMuxerOnRead(&contexts->io_muxer, contexts->timer_fd, &OnTimerExpire,
user)) {
LOG("Failed to reschedule timer (%s)", strerror(errno));
g_signal = SIGABRT;
return;
}
uint64_t expirations;
if (read(contexts->timer_fd, &expirations, sizeof(expirations)) !=
sizeof(expirations)) {
LOG("Failed to read timer expirations (%s)", strerror(errno));
g_signal = SIGABRT;
return;
}
if (contexts->client_fd == -1) {
// mburakov: Timer must disarm itself AFTER reading.
static const struct itimerspec spec = {0};
if (timerfd_settime(contexts->timer_fd, 0, &spec, NULL)) {
LOG("Failed to disarm timer (%s)", strerror(errno));
g_signal = SIGABRT;
}
return;
}
const struct GpuFrame* captured_frame =
CaptureContextGetFrame(contexts->capture_context);
if (!captured_frame) {
LOG("Failed to capture frame");
goto drop_client;
}
if (!contexts->encode_context) {
contexts->encode_context =
EncodeContextCreate(contexts->gpu_context, captured_frame->width,
captured_frame->height, colorspace, range);
if (!contexts->encode_context) {
LOG("Failed to create encode context");
goto drop_client;
}
}
const struct GpuFrame* encoded_frame =
EncodeContextGetFrame(contexts->encode_context);
if (!encoded_frame) {
LOG("Failed to get encoded frame");
goto drop_client;
}
if (!GpuContextConvertFrame(contexts->gpu_context, captured_frame,
encoded_frame)) {
LOG("Failed to convert frame");
goto drop_client;
}
if (!EncodeContextEncodeFrame(contexts->encode_context,
contexts->client_fd)) {
LOG("Failed to encode frame");
goto drop_client;
}
return;
drop_client:
MaybeDropClient(contexts);
}
static void OnClientWriting(void* user) {
struct Contexts* contexts = user;
if (!IoMuxerOnRead(&contexts->io_muxer, contexts->client_fd, &OnClientWriting,
user)) {
LOG("Failed to reschedule client reading (%s)", strerror(errno));
goto drop_client;
}
if (!InputHandlerHandle(contexts->input_handler, contexts->client_fd)) {
LOG("Failed to handle client input");
goto drop_client;
}
return;
drop_client:
MaybeDropClient(contexts);
}
static void OnInputEvents(void* user) {
struct Contexts* contexts = user;
if (!IoMuxerOnRead(&contexts->io_muxer,
InputHandlerGetEventsFd(contexts->input_handler),
&OnInputEvents, user)) {
LOG("Failed to reschedule events reading (%s)", strerror(errno));
goto drop_client;
}
if (!InputHandlerProcessEvents(contexts->input_handler)) {
LOG("Failed to process events");
goto drop_client;
}
return;
drop_client:
MaybeDropClient(contexts);
}
static void OnClientConnecting(void* user) {
struct Contexts* contexts = user;
if (!IoMuxerOnRead(&contexts->io_muxer, contexts->server_fd,
&OnClientConnecting, user)) {
LOG("Failed to reschedule accept (%s)", strerror(errno));
g_signal = SIGABRT;
return;
}
int client_fd = accept(contexts->server_fd, NULL, NULL);
if (client_fd < 0) {
LOG("Failed to accept client (%s)", strerror(errno));
g_signal = SIGABRT;
return;
}
if (contexts->client_fd != -1) {
LOG("One client is already connected");
close(client_fd);
return;
}
contexts->client_fd = client_fd;
if (!IoMuxerOnRead(&contexts->io_muxer, contexts->client_fd, &OnClientWriting,
user)) {
LOG("Failed to schedule client reading (%s)", strerror(errno));
goto drop_client;
}
contexts->input_handler = InputHandlerCreate();
if (!contexts->input_handler) {
LOG("Failed to create input handler");
goto drop_client;
}
if (!IoMuxerOnRead(&contexts->io_muxer,
InputHandlerGetEventsFd(contexts->input_handler),
&OnInputEvents, user)) {
LOG("Failed to schedule events reading (%s)", strerror(errno));
goto drop_client;
}
static const struct itimerspec spec = {
.it_interval.tv_nsec = capture_period,
.it_value.tv_nsec = capture_period,
};
if (timerfd_settime(contexts->timer_fd, 0, &spec, NULL)) {
LOG("Failed to arm timer (%s)", strerror(errno));
goto drop_client;
}
return;
drop_client:
MaybeDropClient(contexts);
}
int main(int argc, char* argv[]) {
if (argc < 2) {
LOG("Usage: %s ", argv[0]);
return EXIT_FAILURE;
}
if (signal(SIGINT, OnSignal) == SIG_ERR ||
signal(SIGPIPE, SIG_IGN) == SIG_ERR ||
signal(SIGTERM, OnSignal) == SIG_ERR) {
LOG("Failed to set signal handlers (%s)", strerror(errno));
return EXIT_FAILURE;
}
struct Contexts contexts = {
.timer_fd = -1,
.server_fd = -1,
.client_fd = -1,
};
IoMuxerCreate(&contexts.io_muxer);
contexts.server_fd = CreateServerSocket(argv[1]);
if (contexts.server_fd == -1) {
LOG("Failed to create server socket");
goto rollback_io_muxer;
}
contexts.timer_fd = timerfd_create(CLOCK_MONOTONIC, 0);
if (contexts.timer_fd == -1) {
LOG("Failed to create timer (%s)", strerror(errno));
goto rollback_server_fd;
}
contexts.gpu_context = GpuContextCreate(colorspace, range);
if (!contexts.gpu_context) {
LOG("Failed to create gpu context");
goto rollback_timer_fd;
}
contexts.capture_context = CaptureContextCreate(contexts.gpu_context);
if (!contexts.capture_context) {
LOG("Failed to create capture context");
goto rollback_gpu_context;
}
if (!IoMuxerOnRead(&contexts.io_muxer, contexts.timer_fd, &OnTimerExpire,
&contexts)) {
LOG("Failed to schedule timer (%s)", strerror(errno));
goto rollback_capture_context;
}
if (!IoMuxerOnRead(&contexts.io_muxer, contexts.server_fd,
&OnClientConnecting, &contexts)) {
LOG("Failed to schedule accept (%s)", strerror(errno));
goto rollback_capture_context;
}
while (!g_signal) {
if (IoMuxerIterate(&contexts.io_muxer, -1) && errno != EINTR) {
LOG("Failed to iterate io muxer (%s)", strerror(errno));
g_signal = SIGABRT;
}
}
MaybeDropClient(&contexts);
rollback_capture_context:
CaptureContextDestroy(contexts.capture_context);
rollback_gpu_context:
GpuContextDestroy(contexts.gpu_context);
rollback_timer_fd:
close(contexts.timer_fd);
rollback_server_fd:
close(contexts.server_fd);
rollback_io_muxer:
IoMuxerDestroy(&contexts.io_muxer);
bool result = g_signal == SIGINT || g_signal == SIGTERM;
return result ? EXIT_SUCCESS : EXIT_FAILURE;
}