/** * 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 #include #include #include #include #include #include #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 #include #include #ifdef GDK_WINDOWING_X11 #include // for GDK_WINDOW_XID #endif #ifdef GDK_WINDOWING_WIN32 #include // 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 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 &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 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 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; }