Files
AirplayServer/airplayServer.cpp
2021-01-19 12:31:40 +01:00

433 lines
13 KiB
C++
Executable File

/**
* RPiPlay - An open-source AirPlay mirroring server for Raspberry Pi
* Copyright (C) 2019 Florian Draschbacher
*
* This program 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.
*
* This program 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 this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <stddef.h>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <string>
#include <vector>
#include <fstream>
#include "log.h"
#include "lib/raop.h"
#include "lib/stream.h"
#include "lib/logger.h"
#include "lib/dnssd.h"
#include "renderers/video_renderer.h"
#include "renderers/audio_renderer.h"
#include <gst/video/videooverlay.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#ifdef GDK_WINDOWING_X11
#include <gdk/gdkx.h> // for GDK_WINDOW_XID
#endif
#ifdef GDK_WINDOWING_WIN32
#include <gdk/gdkwin32.h> // for GDK_WINDOW_HWND
#endif
#define VERSION "1.2"
#define DEFAULT_NAME "AirPlayServer for Linux"
#define DEFAULT_BACKGROUND_MODE BACKGROUND_MODE_ON
#define DEFAULT_AUDIO_DEVICE AUDIO_DEVICE_HDMI
#define DEFAULT_LOW_LATENCY false
#define DEFAULT_DEBUG_LOG false
#define DEFAULT_HW_ADDRESS { (char) 0x48, (char) 0x5d, (char) 0x60, (char) 0x7c, (char) 0xee, (char) 0x22 }
int start_server(std::vector<char> hw_addr, std::string name, background_mode_t background_mode,
audio_device_t audio_device, bool low_latency, bool debug_log, gulong embed_xid);
int stop_server();
static bool running = false;
static dnssd_t *dnssd = NULL;
static raop_t *raop = NULL;
static video_renderer_t *video_renderer = NULL;
static audio_renderer_t *audio_renderer = NULL;
struct GTK_STRUCT {
GtkWidget *app_window, *video_window;
GdkWindow *video_window_xwindow;
gulong embed_xid;
cairo_surface_t *surface = NULL;
bool is_fullscreen = false;
bool is_active = false;
};
static GTK_STRUCT * GTK_data = NULL;
static void signal_handler(int sig) {
switch (sig) {
case SIGINT:
case SIGTERM:
running = 0;
break;
}
}
static void init_signals(void) {
struct sigaction sigact;
sigact.sa_handler = signal_handler;
sigemptyset(&sigact.sa_mask);
sigact.sa_flags = 0;
sigaction(SIGINT, &sigact, NULL);
sigaction(SIGTERM, &sigact, NULL);
}
static int parse_hw_addr(std::string str, std::vector<char> &hw_addr) {
for (int i = 0; i < str.length(); i += 3) {
hw_addr.push_back((char) stol(str.substr(i), NULL, 16));
}
return 0;
}
std::string find_mac() {
std::ifstream iface_stream("/sys/class/net/eth0/address");
if (!iface_stream) {
printf("No eth0 \n");
iface_stream.open("/sys/class/net/wlan0/address");
}
if (!iface_stream) {
printf("No wlan0 \n");
iface_stream.open("/sys/class/net/enp0s25/address");
}
if (!iface_stream) {
printf("No enp0s25 \n");
iface_stream.open("/sys/class/net/wlp2s0/address");
}
if (!iface_stream) {
printf("No wlp2s0 \n");
iface_stream.open("/sys/class/net/enp8s0/address");
}
if (!iface_stream) {
printf("No enp8s0 \n");
iface_stream.open("/sys/class/net/enp4s0/address");
}
if (!iface_stream) {
printf("No enp4s0 \n");
printf("No Device found! \n");
return "";
}
std::string mac_address;
iface_stream >> mac_address;
iface_stream.close();
return mac_address;
}
void print_info(char *name) {
printf("RPiPlay %s: An open-source AirPlay mirroring server for Raspberry Pi\n", VERSION);
printf("Usage: %s [-n name]\n", name);
printf("Options:\n");
printf("-n name Specify the network name of the AirPlay server\n");
printf("-a Turn audio off. Only video output\n");
printf("-l Enable low-latency mode (disables render clock)\n");
printf("-d Enable debug logging\n");
printf("-v/-h Displays this help and version information\n");
}
static void gtk_destroy (GtkWidget * widget, gpointer data) {
gtk_main_quit();
}
gboolean draw_callback (GtkWidget *widget, cairo_t *cr, gpointer data) {
if(!GTK_data->is_active) {
guint width, height;
GdkRGBA color;
GtkStyleContext *context;
context = gtk_widget_get_style_context (widget);
width = gtk_widget_get_allocated_width (widget);
height = gtk_widget_get_allocated_height (widget);
gtk_render_background (context, cr, 0, 0, width, height);
cairo_rectangle (cr, 0, 0, width, height);
gtk_style_context_get_color (context,
gtk_style_context_get_state (context),
&color);
gdk_cairo_set_source_rgba (cr, &color);
cairo_fill (cr);
}
return FALSE;
}
void configure_callback(GtkWindow *window, GdkEvent *event, gpointer data) {
if(!GTK_data->is_active) {
gtk_widget_queue_draw(GTK_data->video_window);
}
}
static gboolean click_callback(GtkWindow *window, GdkEventButton *event, gpointer data) {
if (event->button == 1 && event->type == GDK_2BUTTON_PRESS) {
if(GTK_data->is_fullscreen) {
gtk_window_unfullscreen((GtkWindow*)GTK_data->app_window);
GTK_data->is_fullscreen = false;
} else {
gtk_window_fullscreen((GtkWindow*)GTK_data->app_window);
GTK_data->is_fullscreen = true;
}
}
return true;
}
static gboolean on_key_press (GtkWidget *widget, GdkEventKey *event, gpointer user_data) {
if(event->keyval == GDK_KEY_Escape && GTK_data->is_fullscreen) {
gtk_window_unfullscreen((GtkWindow*)GTK_data->app_window);
GTK_data->is_fullscreen = false;
}
}
int main(int argc, char *argv[]) {
init_signals();
background_mode_t background = DEFAULT_BACKGROUND_MODE;
std::string server_name = DEFAULT_NAME;
std::vector<char> server_hw_addr = DEFAULT_HW_ADDRESS;
audio_device_t audio_device = DEFAULT_AUDIO_DEVICE;
bool low_latency = DEFAULT_LOW_LATENCY;
bool debug_log = DEFAULT_DEBUG_LOG;
// Parse arguments
for (int i = 1; i < argc; i++) {
std::string arg(argv[i]);
if (arg == "-n") {
if (i == argc - 1) continue;
server_name = std::string(argv[++i]);
} else if (arg == "-a") {
audio_device = AUDIO_DEVICE_NONE;
} else if (arg == "-l") {
low_latency = !low_latency;
} else if (arg == "-d") {
debug_log = !debug_log;
} else if (arg == "-h" || arg == "-v") {
print_info(argv[0]);
exit(0);
}
}
std::string mac_address = find_mac();
if (!mac_address.empty()) {
server_hw_addr.clear();
parse_hw_addr(mac_address, server_hw_addr);
}
//GTK
gtk_init (&argc, &argv);
GTK_data = new GTK_STRUCT();
GTK_data->app_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
gtk_window_set_title (GTK_WINDOW (GTK_data->app_window), DEFAULT_NAME);
gtk_widget_set_size_request (GTK_data->app_window, 200, 200);
GTK_data->video_window = gtk_drawing_area_new ();
gtk_widget_set_size_request(GTK_data->video_window, 800, 600);
g_signal_connect (GTK_data->video_window, "destroy",G_CALLBACK (gtk_destroy), NULL);
//gtk_widget_set_double_buffered (GTK_data->video_window, FALSE);
gtk_container_add (GTK_CONTAINER (GTK_data->app_window), GTK_data->video_window);
gtk_widget_show_all (GTK_data->app_window);
gtk_widget_add_events(GTK_data->app_window, GDK_BUTTON_PRESS_MASK);
gtk_widget_realize (GTK_data->video_window);
gtk_widget_realize (GTK_data->app_window);
g_signal_connect (G_OBJECT (GTK_data->video_window), "draw",G_CALLBACK (draw_callback), NULL);
g_signal_connect (G_OBJECT (GTK_data->app_window), "button-press-event" ,G_CALLBACK (click_callback), NULL);
g_signal_connect (G_OBJECT (GTK_data->app_window), "key_press_event", G_CALLBACK (on_key_press), NULL);
g_signal_connect(G_OBJECT(GTK_data->video_window), "configure_event", G_CALLBACK(configure_callback), NULL);
GTK_data->video_window_xwindow = gtk_widget_get_window (GTK_data->video_window);
GTK_data->embed_xid = GDK_WINDOW_XID (GTK_data->video_window_xwindow);
if (start_server(server_hw_addr, server_name, background, audio_device, low_latency, debug_log, GTK_data->embed_xid) != 0) {
return 1;
}
gtk_main();
/*
running = true;
while (running) {
sleep(1);
}
*/
LOGI("Stopping...");
stop_server();
}
// Server callbacks
extern "C" void conn_init(void *cls) {
//video_renderer_update_background(video_renderer, 1);
GTK_data->is_active = true;
}
extern "C" void conn_destroy(void *cls) {
GTK_data->is_active = false;
gtk_widget_queue_draw(GTK_data->video_window);
//int width = gtk_widget_get_allocated_width (GTK_data->video_window);
//int height = gtk_widget_get_allocated_height (GTK_data->video_window);
//video_renderer_update_background(video_renderer, -1, width, height);
}
extern "C" void audio_process(void *cls, raop_ntp_t *ntp, aac_decode_struct *data) {
if (audio_renderer != NULL) {
audio_renderer_render_buffer(audio_renderer, ntp, data->data, data->data_len, data->pts);
}
}
extern "C" void video_process(void *cls, raop_ntp_t *ntp, h264_decode_struct *data) {
video_renderer_render_buffer(video_renderer, ntp, data->data, data->data_len, data->pts, data->frame_type);
}
extern "C" void audio_flush(void *cls) {
audio_renderer_flush(audio_renderer);
}
extern "C" void video_flush(void *cls) {
video_renderer_flush(video_renderer);
}
extern "C" void audio_set_volume(void *cls, float volume) {
if (audio_renderer != NULL) {
audio_renderer_set_volume(audio_renderer, volume);
}
}
extern "C" void log_callback(void *cls, int level, const char *msg) {
switch (level) {
case LOGGER_DEBUG: {
LOGD("%s", msg);
break;
}
case LOGGER_WARNING: {
LOGW("%s", msg);
break;
}
case LOGGER_INFO: {
LOGI("%s", msg);
break;
}
case LOGGER_ERR: {
LOGE("%s", msg);
break;
}
default:
break;
}
}
int start_server(std::vector<char> hw_addr, std::string name, background_mode_t background_mode, audio_device_t audio_device, bool low_latency, bool debug_log, gulong embed_xid) {
raop_callbacks_t raop_cbs;
memset(&raop_cbs, 0, sizeof(raop_cbs));
raop_cbs.conn_init = conn_init;
raop_cbs.conn_destroy = conn_destroy;
raop_cbs.audio_process = audio_process;
raop_cbs.video_process = video_process;
raop_cbs.audio_flush = audio_flush;
raop_cbs.video_flush = video_flush;
raop_cbs.audio_set_volume = audio_set_volume;
raop = raop_init(10, &raop_cbs);
if (raop == NULL) {
LOGE("Error initializing raop!");
return -1;
}
raop_set_log_callback(raop, log_callback, NULL);
raop_set_log_level(raop, debug_log ? RAOP_LOG_DEBUG : LOGGER_INFO);
logger_t *render_logger = logger_init();
logger_set_callback(render_logger, log_callback, NULL);
logger_set_level(render_logger, debug_log ? LOGGER_DEBUG : LOGGER_INFO);
if (low_latency) logger_log(render_logger, LOGGER_INFO, "Using low-latency mode");
if ((video_renderer = video_renderer_init(render_logger, background_mode, low_latency)) == NULL) {
LOGE("Could not init video renderer");
return -1;
}
if (audio_device == AUDIO_DEVICE_NONE) {
LOGI("Audio disabled");
} else if ((audio_renderer = audio_renderer_init(render_logger, video_renderer, audio_device, low_latency)) ==
NULL) {
LOGE("Could not init audio renderer");
return -1;
}
if (video_renderer) {
set_video_overlay2(video_renderer, embed_xid);
video_renderer_start(video_renderer);
}
if (audio_renderer) audio_renderer_start(audio_renderer);
unsigned short port = 0;
raop_start(raop, &port);
raop_set_port(raop, port);
int error;
dnssd = dnssd_init(name.c_str(), strlen(name.c_str()), hw_addr.data(), hw_addr.size(), &error);
if (error) {
LOGE("Could not initialize dnssd library!");
return -2;
}
raop_set_dnssd(raop, dnssd);
dnssd_register_raop(dnssd, port);
dnssd_register_airplay(dnssd, port + 1);
return 0;
}
int stop_server() {
raop_destroy(raop);
dnssd_unregister_raop(dnssd);
dnssd_unregister_airplay(dnssd);
if (audio_renderer) audio_renderer_destroy(audio_renderer);
video_renderer_destroy(video_renderer);
return 0;
}