tvoe-0.1/0000755000175000017500000000000012422146366012203 5ustar dominikdominiktvoe-0.1/tvoe.man0000644000175000017500000000454312422146352013656 0ustar dominikdominik.TH tvoe 1 tvoe .SH NAME tvoe \- TV over ethernet streaming .SH SYNOPSIS .B tvoe [\fB\-h\fP] [\fB\-c config\fP] [\fB\-f\fP] .SH DESCRIPTION tvoe (TV over Ethernet streaming server) is a lightweight DVB-S/S2 network streaming software. It can serve many transponders on multiple tuners simultaneously and provides dynamic tuner allocation to the clients, giving you the possibility to add more stations to the channel list than the tuner configuration would be able to stream simultaneously. tvoe is designed for a homogenous tuner configuration, i.e., if you are including DVB-S2-channels in your channel list, all configured tuners should be able to be S2-capable. Also, tvoe only supports S2API for tuner configuration. Some legacy systems and DVB-S1-only-drivers might not support S2API and thus cannot be used with tvoe. tvoe uses the same format for channel lists as the zap(1) utility from dvb-tools. They can be easily generated using scan, wscan or other utilities supporting the zap file format. For example, to generate a channel list of all channels on Astra 19.2E (mostly relevant for central Europe), you can do scan /usr/share/dvb/dvb-s/Astra-19.2E > channels.conf All channels listed in the channels.conf will be served by tvoe. If necessary, remove unused channels from the list before using it. Next, adjust the example config file to match your tuner configuration. Make sure to set the correct path to the channel list. You can then start tvoe using tvoe -f CONFIGFILE The streams can then be accessed via http://IP:CONFIGURED_PORT/by-sid/SID, where SID is the DVB service ID of the requested station (the second-last column in the channel specification in channels.conf). Additional URLs might be added in the future. .SH OPTIONS .TP \fB\-h\fR Display a short usage summary .TP \fB\-c config\fR Use the specified config .TP \fB\--p pidfile\fR Write process PID to given pidfile .TP \fB\--q\fR (quiet) - don't write debug output to stdout when starting. .TP \fB\-f\fR Start tvoe as a foreground process and disable forking to the background. .SH AUTHORS tvoe is written by Dominik Paulus, inspired by the classic getstream utility but designed for greater flexibility by not using a fixed transponder configuration. .SH COPYRIGHT Copyright (c) 2013-2014 Dominik Paulus. License: ISC license. .SH BUGS At the moment, tvoe only supports S2API, even for normal DVB-S-streams. tvoe-0.1/tvoe.conf.example0000644000175000017500000000236112422146352015456 0ustar dominikdominik# Specify HTTP listener port # At the moment, only one port per tvoe instance is supported. http-listen 8080; # List of channels to serve, in "zap" file format. # (NAME:FREQUENCY:POLARIZATION:UNUSED:SYMBOLRATE:UNUSED:UNUSED:SID:DELIVERY_SYSTEM) channels "/etc/tvoe/channels.conf"; # Set the HTTP output buffer size (optional). Clients that # exceed this bufsize will be dropped. client_bufsize 10485760; # 10MiB, this is the default # Kernel demuxer buffer size. Increase this if you get # frontend reads failed with errno "Value too large for defined data # type". Allocated once per card, setting it too large just # causes increased memory usage. Kernel default is currently # 2 * 4096. demux_bufsize 16384; # Frontends to use # Clients will be dynamically assigned to these # adapters in a round-robin fashion frontend { adapter 0; }; # Corresponds to /dev/dvb/adapter0/... frontend { adapter 1; }; # Corresponds to /dev/dvb/adapter1/... frontend { adapter 1; frontend 1; # Optional, by default, 0 will be used # These are the default LNB params for a Ku band # universal LNB. They can be changed, if necessary lof1 9750000; lof2 10600000; slof 11700000; }; # Set logfile (optional) #logfile "tvoe.log"; # Log to syslog? (Optional) use_syslog yes; tvoe-0.1/tvoe.c0000644000175000017500000000625212422146352013324 0ustar dominikdominik#include #include #include #include #include #include #include #include #include #include #include #include #include "http.h" #include "log.h" const char *conffile = "./tvoe.conf"; extern int loglevel; static bool daemonize = true; bool daemonized = false; extern void yylex_destroy(); extern void init_lexer(); extern void init_parser(); extern int yyparse(void); int main(int argc, char **argv) { int c; char *pidfile = NULL; bool quiet = false; while((c = getopt(argc, argv, "qhfd:c:p:")) != -1) { switch(c) { case 'c': // Config filename conffile = optarg; break; case 'f': // Foreground daemonize = false; break; case 'p': // Pidfile pidfile = optarg; break; case 'q': // Quiet quiet = true; break; case 'h': default: fprintf(stderr, "Usage: %s [-c config] [-f] [-h] [-p pidfile]\n" "\t-c: Sets configuration file path. Default: ./tvoe.conf\n" "\t-f: Disable daemon fork\n" "\t-p: Write PID to given pidfile\n" "\t-q: Quiet startup\n" "\t-h: Show this help\n", argv[0]); exit(EXIT_FAILURE); } } if(!quiet) printf("tvoe version %s compiled on %s %s\n", "0.1", __DATE__, __TIME__); /* Initialize libevent */ evthread_use_pthreads(); event_init(); httpd = evhttp_new(NULL); /* Parse config file */ init_lexer(); init_parser(); yyparse(); yylex_destroy(); /* Initialize logging subsystem */ init_log(); if(!quiet) logger(LOG_INFO, "tvoe starting"); // Daemonize if necessary if(daemonize && getppid() != 1) { if(!quiet) printf("Daemonizing... "); fflush(stdout); pid_t pid = fork(); if(pid < 0) { perror("fork()"); return EXIT_FAILURE; } if(pid > 0) {// Exit parent if(!quiet) printf("success. (pid: %u)\n", pid); return EXIT_SUCCESS; } if(pidfile) { int pidfd = open(pidfile, O_RDWR | O_CREAT, 0600); char pid[8]; // PID is not greater than 65536 if(pidfd < 0) { logger(LOG_ERR, "Unable to open PID file %s: %s, exiting", pidfile, strerror(errno)); return EXIT_FAILURE; } if(lockf(pidfd, F_TLOCK, 0) < 0) { logger(LOG_ERR, "Unable to lock PID file %s. tvoe is probably already running.", pidfile); return EXIT_FAILURE; } snprintf(pid, 8, "%d\n", getpid()); if(write(pidfd, pid, strlen(pid)) != strlen(pid)) { logger(LOG_ERR, "Unable to write to PID file %s: %s", pidfile, pidfile); return EXIT_FAILURE; } } daemonized = true; // prevents logger from logging to stderr umask(0); if(setsid() < 0) { perror("setsid()"); exit(EXIT_FAILURE); } if(chdir("/") < 0) { perror("chdir()"); exit(EXIT_FAILURE); } freopen("/dev/null", "r", stdin); freopen("/dev/null", "w", stdout); freopen("/dev/null", "w", stderr); } /* Initialize frontend handler */ frontend_init(); /* Ignore SIGPIPE */ { struct sigaction action; sigemptyset(&action.sa_mask); action.sa_flags = SA_RESTART; action.sa_handler = SIG_IGN; sigaction(SIGPIPE, &action, NULL); } event_dispatch(); logger(LOG_ERR, "Event loop exited"); return EXIT_SUCCESS; } tvoe-0.1/mpeg.h0000644000175000017500000000302312422146352013275 0ustar dominikdominik#ifndef __INCLUDED_GETSH_MPEG #define __INCLUDED_GETSH_MPEG #include #include #include "frontend.h" #define MAX_PID 0x2000 /** * Callback for new MPEG-TS input data. Called by the frontend module * when reading from frontend succeeded and data is ready for parsing. * @param handle Handler for this transport stream * @param data Pointer to data to be parsed * @param len Length of data at "data" */ void mpeg_input(void *handle, unsigned char *data, size_t len); /** * Register new client requesting program "sid". This module will take care of * extracting the requested service from the input data stream and generating a * new MPEG-TS stream supplied to the client. The specified callback will be * invoked every time new data is ready to be sent to the client, ptr is a * pointer to an arbitrary data structure that will be provided unchanged to * the callback. * @param s Requested program * @param cb Callback to invoke when new data is ready * @param timeout_cb Callback to invoke on frontend tune timeout * @param ptr Pointer to be passed to the callback when invoked * @return Pointer to client handle, to be passed to mpeg_unregister() */ void *mpeg_register(struct tune s, void (*cb) (void *, struct evbuffer *), void (*timeout_cb) (void *), void *ptr); /** * Deregister a specific client * @param ptr Pointer to handle returned by mpeg_register() */ void mpeg_unregister(void *ptr); /** * Called by the frontend module when tuning times out. */ void mpeg_notify_timeout(void *handle); #endif tvoe-0.1/mpeg.c0000644000175000017500000003116112422146352013274 0ustar dominikdominik#include #include #include #include #include #include #include #include #include "mpeg.h" #include "log.h" /* * This module handles remultiplexing the incoming DVB stream for different * clients, including only the SID (service ID) and associated PIDs requested * by the client. * * It parses the PAT (containing SID to PMT mapping) and PMTs (containing * associated PIDs for the SID this PMT corresponds to) and maintains a list of * clients for each PID. * * Each incoming packet is forwarded to all clients requesting a SID containing * this PID. Also, it regularly sends a PAT containing only the requested SID * to all clients. */ /* * Struct describing one specific client and the associated callbacks */ struct client { /** sid requested by this client */ int sid; /** As we send different PATs to different clients, we have a per-client * PAT continuity counter */ uint8_t pid0_cc; /** Associated transponder */ struct transponder *t; /** Callback for MPEG-TS input */ void (*cb) (void *, struct evbuffer *); /** Callback to call on timeout */ void (*timeout_cb) (void *); /** Argument to supply to the callback functions */ void *ptr; }; /* * Struct containing information about a given PID (list of callbacks * and buffers used for decoding PAT or PMTs sent on this PID, if * applicable */ struct pid_info { /** true if we are interested in this PID, i.e., we should parse * tables transmitted over this PID */ bool parse; int8_t last_cc; /* Buffers used by bitstream for decoding psi tables */ uint8_t *psi_buffer; uint16_t psi_buffer_used; GSList *callback; }; struct transponder { /** Transport stream ID. Taken over as part of the PAT */ uint16_t tsid; /** User refcount */ int users; /** Handle for the associated frontend */ void *frontend_handle; /** Current frequency */ struct tune in; struct pid_info pids[MAX_PID]; /** Used as temporary data storage for data sent to the client */ struct evbuffer *out; /** List of clients subscribed to this transponder */ GSList *clients; }; static GSList *transponders; /* Helper function for send_pat(). Send a PSI section to the client. */ /* This code is copied from bitstream examples. See LICENSE. */ static void output_psi_section(struct transponder *a, struct client *c, uint8_t *section, uint16_t pid, uint8_t *cc) { uint16_t section_length = psi_get_length(section) + PSI_HEADER_SIZE; uint16_t section_offset = 0; do { uint8_t ts[TS_SIZE]; uint8_t ts_offset = 0; memset(ts, 0xff, TS_SIZE); psi_split_section(ts, &ts_offset, section, §ion_offset); ts_set_pid(ts, pid); ts_set_cc(ts, *cc); (*cc)++; *cc &= 0xf; if (section_offset == section_length) psi_split_end(ts, &ts_offset); evbuffer_add(a->out, ts, TS_SIZE); c->cb(c->ptr, a->out); } while (section_offset < section_length); } /* * Assemble new PAT containing only the SID requested by the client and * sent it to him. */ static void send_pat(struct transponder *a, struct client *c, uint16_t sid, uint16_t pid) { uint8_t *pat = psi_allocate(); uint8_t *pat_n, j = 0; pat_init(pat); pat_set_tsid(pat, 0); psi_set_section(pat, 0); psi_set_lastsection(pat, 0); psi_set_version(pat, 0); psi_set_current(pat); psi_set_length(pat, PSI_MAX_SIZE); pat_n = pat_get_program(pat, j++); patn_init(pat_n); patn_set_program(pat_n, sid); patn_set_pid(pat_n, pid); // Set correct PAT length pat_n = pat_get_program(pat, j); // Get offset of the end of last program pat_set_length(pat, pat_n - pat - PAT_HEADER_SIZE); psi_set_crc(pat); output_psi_section(a, c, pat, PAT_PID, &c->pid0_cc); free(pat); } /* * Helper function to register all clients in it as callbacks for PID pid * on transponder a */ static void register_callback(GSList *it, struct transponder *a, uint16_t pid) { // Loop over all supplied clients and add them if requested for(; it; it = g_slist_next(it)) { GSList *it2 = a->pids[pid].callback; for(; it2 != NULL; it2 = g_slist_next(it2)) { if(it2->data == it->data) break; } if(it2) // Client already registered continue; a->pids[pid].callback = g_slist_prepend(a->pids[pid].callback, it->data); } } /* * Process a new parsed PMT. Map PIDs to corresponding SIDs. * @param p Pointer to struct pmt_handle */ static void pmt_handler(struct transponder *a, uint16_t pid, uint8_t *section) { int j; if(!pmt_validate(section)) { //logger(LOG_NOTICE, "Invalid PMT received on PID %u", pid); free(section); return; } uint8_t *es; // Register callback for all elementary streams for this SID for(j = 0; (es = pmt_get_es(section, j)); j++) register_callback(a->pids[pid].callback, a, pmtn_get_pid(es)); // ... and for the PCR register_callback(a->pids[pid].callback, a, pmt_get_pcrpid(section)); free(section); } /* * Process a new parsed PAT on input stream. Add PMT parsers for all referenced * channels, if necessary. * @param p Pointer to struct mpeg_handle */ static void pat_handler(struct transponder *a, uint16_t pid, uint8_t *section) { PSI_TABLE_DECLARE(new_pat); uint8_t last_section; int i; if(!pat_validate(section)) { free(section); return; } psi_table_init(new_pat); if(!psi_table_section(new_pat, section) || !psi_table_validate(new_pat) || !pat_table_validate(new_pat)) { psi_table_free(new_pat); return; } last_section = psi_table_get_lastsection(new_pat); for(i = 0; i <= last_section; i++) { uint8_t *cur = psi_table_get_section(new_pat, i); const uint8_t *program; int j; a->tsid = pat_get_tsid(cur); /* * For every proram in this PAT, check whether we have clients * that request it. Add callbacks for them, if necessary. */ for(j = 0; (program = pat_get_program(cur, j)); j++) { uint16_t cur_sid = patn_get_program(program); a->pids[patn_get_pid(program)].parse = true; // We always parse all PMTs /* * Loop over all registered clients for this transponder. * This might be expensive, however, PATs are only sent about * once a second, so this should not hurt. */ for(GSList *it = a->clients; it != NULL; it = g_slist_next(it)) { struct client *c = it->data; if(c->sid != cur_sid) continue; /* * Receiving a new PAT from the uplink triggers sending * a new, reduced PAT on the remuxed transport * streams */ send_pat(a, c, cur_sid, patn_get_pid(program)); /* * If necessary, add this client as callback for the * referenced PMT. */ GSList *it2; for(it2 = a->pids[patn_get_pid(program)].callback; it2 != NULL; it2 = g_slist_next(it2)) { if(it2->data == c) break; } if(it2) // Callback already registered continue; a->pids[patn_get_pid(program)].callback = g_slist_prepend(a->pids[patn_get_pid(program)].callback, c); } //logger(LOG_DEBUG, "%d -> %d", patn_get_program(program), // patn_get_pid(program)); } } /* Additionally to the PIDs defined in the PMT, we also forward the EPG * informations to all clients. They always have PID 18. */ register_callback(a->clients, a, 18); psi_table_free(new_pat); return; } static void handle_section(struct transponder *a, uint16_t pid, uint8_t *section) { uint8_t table_pid = psi_get_tableid(section); if(!psi_validate(section)) { free(section); return; } switch(table_pid) { case PAT_TABLE_ID: pat_handler(a, pid, section); break; case PMT_TABLE_ID: pmt_handler(a, pid, section); break; default: free(section); } } void mpeg_input(void *ptr, unsigned char *data, size_t len) { struct transponder *a = ptr; if(len % TS_SIZE) { logger(LOG_NOTICE, "Unaligned MPEG-TS packets received, dropping."); return; } /* * Loop over all packets, parse PSI tables, if necessary and forward them * to all requesting clients */ for(int i=0; i < len; i+=TS_SIZE) { uint8_t *cur = data + i; uint16_t pid = ts_get_pid(cur); GSList *it; if(pid >= MAX_PID - 1) continue; // Send packet to clients for(it = a->pids[pid].callback; it != NULL; it = g_slist_next(it)) { struct client *c = it->data; evbuffer_add(a->out, cur, TS_SIZE); c->cb(c->ptr, a->out); } if(!a->pids[pid].parse) continue; /* The following code is based on bitstream examples */ if(ts_check_duplicate(ts_get_cc(cur), a->pids[pid].last_cc) || !ts_has_payload(cur)) continue; if(ts_check_discontinuity(ts_get_cc(cur), a->pids[pid].last_cc)) psi_assemble_reset(&a->pids[pid].psi_buffer, &a->pids[pid].psi_buffer_used); a->pids[pid].last_cc = ts_get_cc(cur); const uint8_t *payload = ts_section(cur); uint8_t length = data + TS_SIZE - payload; if(!psi_assemble_empty(&a->pids[pid].psi_buffer, &a->pids[pid].psi_buffer_used)) { uint8_t *section = psi_assemble_payload(&a->pids[pid].psi_buffer, &a->pids[pid].psi_buffer_used, &payload, &length); if(section) handle_section(a, pid, section); } payload = ts_next_section(cur); length = cur + TS_SIZE - payload; while(length) { uint8_t *section = psi_assemble_payload(&a->pids[pid].psi_buffer, &a->pids[pid].psi_buffer_used, &payload, &length); if(section) handle_section(a, pid, section); } } } /* * Called if transponder times out waiting for data */ void mpeg_notify_timeout(void *handle) { struct transponder *t = handle; frontend_release(t->frontend_handle); /* If possible, acquire new frontend as a replacement */ t->frontend_handle = frontend_acquire(t->in, t); if(!t->frontend_handle) { /* No replacement found. Disconnect all clients on this * transponder */ logger(LOG_ERR, "Unable to acquire transponder while looking for replacement after timeout"); GSList *copy = g_slist_copy(t->clients); for(GSList *it = copy; it; it = g_slist_next(it)) { struct client *scb = it->data; scb->timeout_cb(scb->ptr); } g_slist_free(copy); } logger(LOG_NOTICE, "Switched frontend after timeout"); } void *mpeg_register(struct tune s, void (*cb) (void *, struct evbuffer *), void (*timeout_cb) (void *), void *ptr) { struct client *scb = g_slice_alloc(sizeof(struct client)); scb->cb = cb; scb->timeout_cb = timeout_cb; scb->ptr = ptr; scb->sid = s.sid; scb->pid0_cc = 0; /* Check whether we are already receiving a multiplex containing * the requested program */ GSList *it = transponders; for(; it != NULL; it = g_slist_next(it)) { struct transponder *t = it->data; struct tune in = t->in; if(in.dvbs.delivery_system == s.dvbs.delivery_system && in.dvbs.symbol_rate == s.dvbs.symbol_rate && in.dvbs.frequency == s.dvbs.frequency && in.dvbs.polarization == s.dvbs.polarization) { t->users++; t->clients = g_slist_prepend(t->clients, scb); scb->t = t; logger(LOG_DEBUG, "New client on known transponder. New client count: %d", t->users); return scb; } } /* We aren't, acquire new frontend */ struct transponder *t = g_slice_alloc(sizeof(struct transponder)); t->frontend_handle = frontend_acquire(s, t); if(!t->frontend_handle) { // Unable to acquire frontend g_slice_free1(sizeof(struct transponder), t); g_slice_free1(sizeof(struct client), scb); return NULL; } t->in = s; t->out = evbuffer_new(); t->users = 1; t->clients = NULL; t->clients = g_slist_prepend(t->clients, scb); scb->t = t; for(int i = 0; i < MAX_PID; i++) { t->pids[i].last_cc = 0; t->pids[i].callback = NULL; psi_assemble_init(&t->pids[i].psi_buffer, &t->pids[i].psi_buffer_used); } t->pids[0].parse = true; // Always parse the PAT transponders = g_slist_prepend(transponders, t); return scb; } void mpeg_unregister(void *ptr) { struct client *scb = ptr; struct transponder *t = scb->t; t->users--; if(!t->users) { // Completely remove transponder if(t->frontend_handle) frontend_release(t->frontend_handle); for(int i = 0; i < MAX_PID; i++) { psi_assemble_reset(&t->pids[i].psi_buffer, &t->pids[i].psi_buffer_used); g_slist_free(t->pids[i].callback); } evbuffer_free(t->out); g_slice_free1(sizeof(struct client), scb); transponders = g_slist_remove(transponders, t); g_slice_free1(sizeof(struct transponder), t); } else { // Only unregister this client /* * Iterate over all callbacks and remove this client from them. * This is extremely expensive, however, disconnects should be * rather rare. This code should be optimized in the future. */ for(int i=0; i < MAX_PID; i++) t->pids[i].callback = g_slist_remove(t->pids[i].callback, scb); t->clients = g_slist_remove(t->clients, scb); g_slice_free1(sizeof(struct client), scb); logger(LOG_INFO, "Client quitted, new transponder user count: %d", t->users); } } tvoe-0.1/log.h0000644000175000017500000000024212422146352013126 0ustar dominikdominik#ifndef __INCLUDED_GETSH_LOGGER #define __INCLUDED_GETSH_LOGGER #include void logger(int loglevel, const char *fmt, ...); int init_log(void); #endif tvoe-0.1/log.c0000644000175000017500000000174212422146352013127 0ustar dominikdominik#include #include #include #include #include #include #include #include #include char *logfile = NULL; int use_syslog = 0; int loglevel = 1; static FILE * log_fd; extern bool daemonized; void logger(int level, char *fmt, ...) { char text[2048], tv[256]; time_t t; struct tm * ti; // TODO: Filter loglevel time(&t); ti = localtime(&t); strftime(tv, sizeof(tv), "[%X %x]", ti); va_list args; va_start(args, fmt); vsnprintf(text, sizeof(text), fmt, args); va_end(args); if(log_fd) { fprintf(log_fd, "%s %s\n", tv, text); fflush(log_fd); } if(use_syslog) syslog(level, "%s", text); if(!daemonized) fprintf(stderr, "%s %s\n", tv, text); } int init_log(void) { if(logfile) { log_fd = fopen(logfile, "a"); if(!log_fd) fprintf(stderr, "Unable to open logfile %s: %s\n", logfile, strerror(errno)); } if(use_syslog) openlog("tvoe", 0, LOG_DAEMON); return 0; } tvoe-0.1/http.h0000644000175000017500000000100612422146352013323 0ustar dominikdominik#ifndef __INCLUDED_GETSH_HTTP #define __INCLUDED_GETSH_HTTP #include #include #include "frontend.h" extern struct evhttp *httpd; /** * Adds URL handlers for the specified channel. On client request, the HTTP * module will tune to the specified transponder and send the service "sid" to * the client * @param name Human-readable channel name * @param sid Service ID * @param t Transponder to tune to */ extern void http_add_channel(const char *name, int sid, struct tune t); #endif tvoe-0.1/http.c0000644000175000017500000000464412422146352013331 0ustar dominikdominik#include #include #include "frontend.h" #include "log.h" #include "mpeg.h" #include "http.h" /* Client buffer size: Set by config parser */ int clientbuf = 10485760; /* Global handle for the HTTP base used by tvoe */ struct evhttp *httpd; struct http_output { struct tune *t; void *handle; struct event *timer; }; // Called by libevent on connection close static void http_closecb(struct evhttp_connection *req, void *ptr) { struct http_output *c = (struct http_output *) ptr; mpeg_unregister(c->handle); event_del(c->timer); event_free(c->timer); g_slice_free1(sizeof(struct http_output), ptr); } /* * Invoked every second to make sure our buffers don't overflow. * We don't do this on every packet sent to save CPU time, thus, * we need a timer. */ static void http_check_bufsize(evutil_socket_t fd, short what, void *arg) { struct evhttp_connection *conn = evhttp_request_get_connection(arg); size_t len = evbuffer_get_length(bufferevent_get_output(evhttp_connection_get_bufferevent(conn))); size_t len2 = evbuffer_get_length(evhttp_request_get_output_buffer(arg)); if(clientbuf >= 0 && (len > clientbuf || len2 > clientbuf)) { logger(LOG_ERR, "HTTP client buffer overflowed, dropping client"); evhttp_send_reply_end(arg); evhttp_connection_free(conn); return; } } void http_timeout(void *arg) { evhttp_send_reply_end(arg); } /* * Invoked by libevent when new HTTP request is received */ static void http_callback(struct evhttp_request *req, void *ptr) { struct tune *t = ptr; void *handle; if(!(handle = mpeg_register(*t, (void (*) (void *, struct evbuffer *)) evhttp_send_reply_chunk, http_timeout, req))) { logger(LOG_NOTICE, "HTTP: Unable to fulfill request: mpeg_register() failed"); evhttp_send_reply(req, HTTP_SERVUNAVAIL, "No available tuner", NULL); return; } struct http_output *c = g_slice_new(struct http_output); c->handle = handle; c->t = t; evhttp_send_reply_start(req, 200, "OK"); evhttp_connection_set_closecb(evhttp_request_get_connection(req), http_closecb, c); /* Add bufsize timer */ c->timer = event_new(NULL, -1, EV_PERSIST, http_check_bufsize, req); struct timeval tv = { 1, 0 }; event_add(c->timer, &tv); } void http_add_channel(const char *name, int sid, struct tune t) { char text[128]; snprintf(text, sizeof(text), "/by-sid/%d", sid); struct tune *ptr = g_slice_alloc(sizeof(struct tune)); *ptr = t; evhttp_set_cb(httpd, text, http_callback, ptr); } tvoe-0.1/frontend.h0000644000175000017500000000323312422146352014167 0ustar dominikdominik#ifndef __INCLUDED_GETSH_FRONTEND #define __INCLUDED_GETSH_FRONTEND #include struct tune { /** Delivery system type, reserved for future use */ unsigned int type; // union { struct { /** Delivery system (SYS_DVBS vs SYS_DVBS2) */ unsigned int delivery_system; unsigned int frequency; unsigned int symbol_rate; /** Polarization. True: horizontal, false: Vertical */ bool polarization; } dvbs; /** Service ID requested */ unsigned int sid; // }; }; struct lnb { int lof1, lof2, slof; size_t dmxbuf; }; /** * Tune to a specific transponder. This function selects a new, current unused * frontend and tunes to the specified frequency. dvr_callback will be called * with the argument "ptr" passed unmodified to the callback * @param s Struct describing the transponder to tune to * @param ptr Pointer to be passed to the callback function * @return Frontend handle to be passed to frontend_release(), NULL * on error. */ void *frontend_acquire(struct tune s, void *ptr); /** * Release a specific frontend * @param ptr Pointer returned by frontend_acquire() */ void frontend_release(void *ptr); /** * Add a new DVB-S frontend on /dev/dvb/adapterX/frontendY, X and Y are * specified by the caller, and sets the parameters of the attached LNB. * No error handling is performed if the frontend does not exists or is in use, * subscribe_to_frontend() will fail at some point then. (This behaviour might * change in the future) * @param adapter Adapter number * @param frontend Frontend number */ void frontend_add(int adapter, int frontend, struct lnb l); /** * Initialize the frontend management subsystem */ void frontend_init(void); #endif tvoe-0.1/frontend.c0000644000175000017500000002021712422146352014163 0ustar dominikdominik#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "frontend.h" #include "log.h" #include "mpeg.h" #include "frontend.h" /* Size of demux buffer. Set by config parser, 0 means default */ size_t dmxbuf = 0; static GList *idle_fe, *used_fe; static GMutex queue_lock; static void dvr_callback(evutil_socket_t fd, short int flags, void *arg); struct frontend { struct tune in; /**< Associated transponder, if applicable */ struct lnb lnb; /**< Attached LNB */ int adapter; /**< Adapter number */ int frontend; /**< Frontend number */ int fe_fd; /**< File descriptor for /dev/dvb/adapterX/frontendY (O_RDONLY) */ int dmx_fd; /**< File descriptor for /dev/dvb/adapterX/demuxY (O_WRONLY) */ int dvr_fd; /**< File descriptor for /dev/dvb/adapterX/dvrY (O_RDONLY) */ struct event *event;/**< Handle for the event callbacks on the dvr file handle */ void *mpeg_handle; /**< Handle for associated MPEG-TS decoder (see mpeg.c) */ }; /** Compute program frequency based on transponder frequency * and LNB parameters. Ripped from getstream-poempel */ static int get_frequency(int freq, struct lnb l) { if(freq > 2200000) { /* Frequency contains l.osc.f. */ if(freq < l.slof) return freq - l.lof1; else return freq - l.lof2; } else return freq; } /* * Most ioctl() operations on the DVB frontends are asynchronous (they usually * return before the request is completed), but can't be expected to be * non-blocking. Thus, we do all the frontend parameter settings in a seperate * thread. Work is provided to the tuning thread using the GASyncQueue * work_queue. * * As the frontend is inserted into the idle_fe list by the tuner thread after * release, we have to synchronize access to the idle_fe queue using the * tune_thread lock. */ #define FE_WORK_TUNE 1 #define FE_WORK_RELEASE 2 struct work { int action; struct frontend *fe; }; static GAsyncQueue *work_queue; /* * Open frontend descriptors */ static bool open_fe(struct frontend *fe) { char path_fe[512], path_dmx[512], path_dvr[512]; /* Open frontend, demuxer and DVR output */ snprintf(path_fe, sizeof(path_fe), "/dev/dvb/adapter%d/frontend%d", fe->adapter, fe->frontend); snprintf(path_dmx, sizeof(path_dmx), "/dev/dvb/adapter%d/demux%d", fe->adapter, fe->frontend); snprintf(path_dvr, sizeof(path_dvr), "/dev/dvb/adapter%d/dvr%d", fe->adapter, fe->frontend); if((fe->fe_fd = open(path_fe, O_RDWR | O_NONBLOCK)) < 0 || (fe->dmx_fd = open(path_dmx, O_RDWR)) < 0 || (fe->dvr_fd = open(path_dvr, O_RDONLY | O_NONBLOCK)) < 0) { logger(LOG_ERR, "Failed to open frontend (%d/%d): %s", fe->adapter, fe->frontend, strerror(errno)); return false; } /* Add libevent callback for TS input */ struct event *ev = event_new(NULL, fe->dvr_fd, EV_READ | EV_PERSIST, dvr_callback, fe); struct timeval tv = { 10, 0 }; // 10s timeout if(event_add(ev, &tv)) { logger(LOG_ERR, "Adding frontend to libevent failed."); event_free(fe->event); return false; } fe->event = ev; return true; } /* * Tune previously unkown frontend */ static void tune_to_fe(struct frontend *fe) { struct tune s = fe->in; // TODO: Proper error handling for this function. At the moment, we just return // leaving this frontend idle - eventually, all clients will drop and we will // release this frontend... /* Tune to transponder */ { struct dtv_property p[9]; struct dtv_properties cmds; bool tone = s.dvbs.frequency > 2200000 && s.dvbs.frequency >= fe->lnb.slof; p[0].cmd = DTV_CLEAR; p[1].cmd = DTV_DELIVERY_SYSTEM; p[1].u.data = s.dvbs.delivery_system; p[2].cmd = DTV_SYMBOL_RATE; p[2].u.data = s.dvbs.symbol_rate; p[3].cmd = DTV_INNER_FEC; p[3].u.data = FEC_AUTO; p[4].cmd = DTV_INVERSION; p[4].u.data = INVERSION_AUTO; p[5].cmd = DTV_FREQUENCY; p[5].u.data = get_frequency(s.dvbs.frequency, fe->lnb); p[6].cmd = DTV_VOLTAGE; p[6].u.data = s.dvbs.polarization ? SEC_VOLTAGE_18 : SEC_VOLTAGE_13; p[7].cmd = DTV_TONE; p[7].u.data = tone ? SEC_TONE_ON : SEC_TONE_OFF; p[8].cmd = DTV_TUNE; p[8].u.data = 0; cmds.num = 9; cmds.props = p; if(ioctl(fe->fe_fd, FE_SET_PROPERTY, &cmds) < 0) { // This should only fail if we get an event overflow, thus, // we can safely continue after this error. logger(LOG_ERR, "Failed to tune frontend %d/%d to freq %d, sym %d", fe->adapter, fe->frontend, get_frequency(p[5].u.data, fe->lnb), s.dvbs.symbol_rate); return; } } /* Now wait for the tuning to be successful */ /* struct dvb_frontend_event ev; do { if(ioctl(fe->fe_fd, FE_GET_EVENT, &ev) < 0) { logger(LOG_ERR, "Failed to get event from frontend %d/%d: %s", fe->adapter, fe->frontend, strerror(errno)); } } while(!(ev.status & FE_HAS_LOCK) && !(ev.status & FE_TIMEDOUT)); if(ev.status & FE_TIMEDOUT) { logger(LOG_ERR, "Timed out waiting for lock on frontend %d/%d", fe->adapter, fe->frontend); return; } */ logger(LOG_INFO, "Tuning on adapter %d/%d succeeded", fe->adapter, fe->frontend); { struct dmx_pes_filter_params par; par.pid = 0x2000; par.input = DMX_IN_FRONTEND; par.output = DMX_OUT_TS_TAP; par.pes_type = DMX_PES_OTHER; par.flags = DMX_IMMEDIATE_START; if(ioctl(fe->dmx_fd, DMX_SET_PES_FILTER, &par) < 0) { logger(LOG_ERR, "Failed to configure tmuxer on frontend %d/%d", fe->adapter, fe->frontend); return; } } /* Set demux buffer size, if requested */ if(dmxbuf) ioctl(fe->dmx_fd, DMX_SET_BUFFER_SIZE, dmxbuf); } static void release_fe(struct frontend *fe) { close(fe->fe_fd); close(fe->dmx_fd); close(fe->dvr_fd); g_mutex_lock(&queue_lock); idle_fe = g_list_append(idle_fe, fe); g_mutex_unlock(&queue_lock); } /* * Frontend worker thread main routine */ static void *tune_worker(void *ptr) { for(;;) { struct work *w = g_async_queue_pop(work_queue); struct frontend *fe = w->fe; if(w->action == FE_WORK_TUNE) { if(open_fe(fe)) tune_to_fe(fe); } else release_fe(fe); g_slice_free(struct work, w); } return NULL; } void frontend_init(void) { work_queue = g_async_queue_new(); g_mutex_init(&queue_lock); /* Start tuning thread */ g_thread_new("tune_worker", tune_worker, NULL); } /* libevent callback for data on dvr fd */ static void dvr_callback(evutil_socket_t fd, short int flags, void *arg) { struct frontend *fe = (struct frontend *) arg; unsigned char buf[1024 * 188]; if(flags & EV_TIMEOUT) { logger(LOG_ERR, "Timeout reading data from frontend %d/%d", fe->adapter, fe->frontend); mpeg_notify_timeout(fe->mpeg_handle); return; } int n = read(fd, buf, sizeof(buf)); if(n < 0) { logger(LOG_ERR, "Invalid read on frontend %d/%d: %s", fe->adapter, fe->frontend, strerror(errno)); return; } mpeg_input(fe->mpeg_handle, buf, n); } /* Tune to a new, previously unknown transponder */ void *frontend_acquire(struct tune s, void *ptr) { // Get new idle frontend from queue g_mutex_lock(&queue_lock); GList *f = g_list_first(idle_fe); if(!f) { g_mutex_unlock(&queue_lock); return NULL; } idle_fe = g_list_remove_link(idle_fe, f); g_mutex_unlock(&queue_lock); struct frontend *fe = (struct frontend *) (f->data); fe->in = s; fe->mpeg_handle = ptr; logger(LOG_DEBUG, "Acquiring frontend %d/%d", fe->adapter, fe->frontend); used_fe = g_list_append(used_fe, fe); // Tell tuning thread to tune struct work *w = g_slice_new(struct work); w->action = FE_WORK_TUNE; w->fe = fe; g_async_queue_push(work_queue, w); return fe; } void frontend_release(void *ptr) { struct frontend *fe = ptr; logger(LOG_INFO, "Releasing frontend %d/%d", fe->adapter, fe->frontend); event_del(fe->event); event_free(fe->event); used_fe = g_list_remove(used_fe, fe); struct work *w = g_slice_new(struct work); w->action = FE_WORK_RELEASE; w->fe = fe; g_async_queue_push(work_queue, w); } void frontend_add(int adapter, int frontend, struct lnb l) { struct frontend *fe = g_slice_alloc0(sizeof(struct frontend)); fe->lnb = l; fe->adapter = adapter; fe->frontend = frontend; idle_fe = g_list_append(idle_fe, fe); } tvoe-0.1/config_parser.y0000644000175000017500000000530212422146352015211 0ustar dominikdominik%error-verbose %{ #include #include #include #include #include #include #include "http.h" #include "frontend.h" #include "channels.h" extern FILE *yyin; extern int yylineno; extern int yylex(void); extern char *logfile; extern int use_syslog; extern int loglevel; extern int clientbuf; extern size_t dmxbuf; /* Temporary variables needed while parsing */ static struct lnb l; static int adapter = -1, frontend = -1; void yyerror(const char *str) { fprintf(stderr, "Parse error on line %d: %s\n", yylineno, str); exit(EXIT_FAILURE); } static void parse_error(char *text, ...) { static char error[1024]; va_list args; va_start(args, text); vsnprintf(error, sizeof(error), text, args); va_end(args); yyerror(error); } int yywrap() { fclose(yyin); return 1; } void init_parser() { } %} %union { char *text; int num; }; %token STRING %token NUMBER %token YESNO %token SEMICOLON HTTPLISTEN FRONTEND ADAPTER LOF1 LOF2 SLOF CHANNELSCONF %token LOGFILE USESYSLOG LOGLEVEL CLIENTBUF DMXBUF %% statements: | statements statement SEMICOLON; statement: http | frontend | channels | logfile | syslog | loglevel | clientbuf | dmxbuf; clientbuf: CLIENTBUF NUMBER { if($2 <= 0) parse_error("Client buffer size must be greater than 0 bytes"); clientbuf = $2; } dmxbuf: DMXBUF NUMBER { if($2 <= 0) parse_error("Demuxer buffer size must be greater than 0 bytes"); dmxbuf = $2; } loglevel: LOGLEVEL NUMBER { loglevel = $2; if(loglevel < 0 || loglevel > 5) parse_error("Loglevel must be between 0 and 5."); } logfile: LOGFILE STRING { logfile = strdup($2); } syslog: USESYSLOG YESNO { use_syslog = $2; } http: HTTPLISTEN NUMBER { struct evhttp_bound_socket *handle = evhttp_bind_socket_with_handle(httpd, "::", $2); if(handle == NULL) { fprintf(stderr, "Unable to bind to port %d. Exiting\n", $2); exit(EXIT_FAILURE); } } channels: CHANNELSCONF STRING { if(parse_channels($2)) { parse_error("parse_channels() failed"); exit(EXIT_FAILURE); } } frontend: FRONTEND '{' frontendoptions '}' { if(adapter == -1) parse_error("frontend block needs an adapter number"); /* Default Universal LNB */ if(!l.lof1) l.lof1 = 9750000; if(!l.lof2) l.lof2 = 10600000; if(!l.slof) l.slof = 11700000; frontend_add(adapter, frontend, l); adapter = -1; frontend = 0; } frontendoptions: | frontendoptions frontendoption; frontendoption: adapter | frontend | lof1 | lof2 | slof; adapter: ADAPTER NUMBER SEMICOLON { adapter = $2; } frontend: FRONTEND NUMBER SEMICOLON { frontend = $2; } lof1: LOF1 NUMBER SEMICOLON { l.lof1 = $2; } lof2: LOF2 NUMBER SEMICOLON { l.lof2 = $2; } slof: SLOF NUMBER SEMICOLON { l.slof = $2; } tvoe-0.1/config_lexer.l0000644000175000017500000000164112422146352015021 0ustar dominikdominik%option never-interactive case-insensitive yylineno warn nodefault nounput %{ #define YY_NO_INPUT #include "config_parser.h" extern const char *conffile; int init_lexer(void) { yyin = fopen(conffile, "r"); if (yyin == NULL) { fprintf(stderr, "Unable to open config file %s: %s\n", conffile, strerror(errno)); exit(EXIT_FAILURE); } return 0; } %} %% \"[^"\n]+[\"\n] yytext[yyleng-1] = 0; yylval.text = yytext+1; return STRING; yes|no yylval.num=!strcmp(yytext,"yes"); return YESNO; [0-9]+ yylval.num=atoi(yytext); return NUMBER; http-listen return HTTPLISTEN; frontend return FRONTEND; adapter return ADAPTER; lof1 return LOF1; lof2 return LOF2; slof return SLOF; channels return CHANNELSCONF; logfile return LOGFILE; use_syslog return USESYSLOG; loglevel return LOGLEVEL; client_bufsize return CLIENTBUF; demux_bufsize return DMXBUF; ; return SEMICOLON; [ \t\r\n]+ ; #.* ; . return yytext[0]; tvoe-0.1/channels.h0000644000175000017500000000034112422146352014140 0ustar dominikdominik#ifndef __INCLUDED_GETSH_CHANNELS #define __INCLUDED_GETSH_CHANNELS /** * Parse the channels.conf in "channelsconf" and add the appropriate * HTTP callbacks */ extern int parse_channels(const char *channelsconf); #endif tvoe-0.1/channels.c0000644000175000017500000000303612422146352014137 0ustar dominikdominik#include #include #include #include #include #include "channels.h" #include "log.h" #include "http.h" #include "mpeg.h" #define MAXTOKS 2048 struct tokens { char *t[MAXTOKS]; int count; }; static struct tokens tokenize(char *str) { int i = 0; struct tokens ret; ret.count = 0; ret.t[0] = strtok(str, ":"); if(ret.t[0] == NULL) return ret; for(i=1; i < MAXTOKS && (ret.t[i] = strtok(NULL, ":")); i++) ; ret.count = i; return ret; } int parse_channels(const char *file) { int i; FILE * fd = fopen(file, "r"); if(!fd) { logger(LOG_ERR, "channels.conf open failed: %s", strerror(errno)); return -1; } char buf[2048]; for(i=1; fgets(buf, sizeof(buf), fd); i++) { if(feof(fd) || ferror(fd)) break; struct tokens tok = tokenize(buf); if(tok.count != 9) { logger(LOG_ERR, "Parsing channel config failed: Line %d: " "Invalid number of tokens (was: %d, expected: %d)", i, tok.count, 9); continue; } /*if(atoi(tok.t[7]) >= MAX_PID) { logger(LOG_ERR, "Parsing channels config failed: Line %d: " "Invalid SID: %s", i, tok.t[7]); continue; }*/ bool pol = tok.t[2][0] == 'h'; struct tune l = { 0, { // unused atoi(tok.t[8]), // delivery system atoi(tok.t[1]) * 1000, // frequency atoi(tok.t[4]) * 1000, // symbol rate pol }, atoi(tok.t[7]) }; // polarization and SID http_add_channel(tok.t[0], atoi(tok.t[7]), l); } if(ferror(fd)) { logger(LOG_ERR, "channels.conf read failed: %s", strerror(errno)); return -1; } return 0; } tvoe-0.1/README0000644000175000017500000000431712422146352013063 0ustar dominikdominikOverview ======== tvoe (TV over Ethernet streaming server) is a lightweight DVB-S/S2 network streaming software. It can serve many transponders on multiple tuners simultaneously and provides dynamic tuner allocation to the clients, giving you the possibility to add more stations to the channel list than the tuner configuration would be able to stream simultaneously. tvoe is designed for a homogenous tuner configuration, i.e., if you are including DVB-S2-channels in your channel list, all configured tuners should be able to be S2-capable. Also, tvoe only supports S2API for tuner configuration. Some legacy systems and DVB-S1-only-drivers might not support S2API and thus cannot be used with tvoe. Dependencies ============ * libbitstream (http://www.videolan.org/developers/bitstream.html) version >= 1.0, used for MPEG structure parsing * libevent 2.0 (http://libevent.org/) * a sufficiently recent glib (https://developer.gnome.org/glib/2.40/) Additional build time dependencies: yacc, lex for the config file parser and linux headers. Building ======== ... is easy: $ cmake . $ make Quickstart ========== First, you have to generate a list of channels to serve. tvoe uses the "zap" file format that is generated by the dvb-tools frequency scanning utilities. To generate a channel list of all channels on Astra-19.2, use: scan /usr/share/dvb/dvb-s/Astra-19.2E > channels.conf All channels listed in the channels.conf will be served by tvoe. If necessary, remove unused channels from the list before using it. Next, adjust the example config file to match your tuner configuration. Make sure to set the correct path to the channel list. You can then start tvoe using tvoe -f CONFIGFILE The streams can then be accessed via http://IP:CONFIGURED_PORT/by-sid/SID, where SID is the DVB service ID of the requested station (the second-last column in the channel specification in channels.conf). Additional URLs might be added in the future. tvoe only does minor modifications to the original satellite transport stream (e.g. remuxing to include only the requested service from a given transponder). Special data like teletext and EPG (for the whole transponder) is passed through untouched and can be interpreted by some clients. tvoe-0.1/LICENSE0000644000175000017500000000363412422146352013211 0ustar dominikdominikThis program uses and some example code from libbitstream, Copyright (c) 2010-2011 VideoLAN. bitstream is distributed under the following license: -- Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -- tvoe is copyright (c) 2013-2014, Dominik Paulus Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. tvoe-0.1/CMakeLists.txt0000644000175000017500000000202112422146352014731 0ustar dominikdominikcmake_minimum_required(VERSION 2.8) project(tvoe C) add_definitions("-std=c99 -Wall -D_XOPEN_SOURCE=700 -flto") set(CMAKE_BUILD_TYPE Release) include(FindBISON) include(FindFLEX) include(FindPkgConfig) find_package(BISON) find_package(FLEX) find_path(BITSTREAM_INCLUDE_DIR bitstream/common.h) pkg_check_modules(GLIB REQUIRED glib-2.0) pkg_check_modules(EVENT REQUIRED libevent) pkg_check_modules(EVENT-THREAD REQUIRED libevent_pthreads) BISON_TARGET(ConfigParser config_parser.y ${CMAKE_CURRENT_BINARY_DIR}/config_parser.c) FLEX_TARGET(ConfigLexer config_lexer.l ${CMAKE_CURRENT_BINARY_DIR}/config_lexer.c) ADD_FLEX_BISON_DEPENDENCY(ConfigLexer ConfigParser) INCLUDE_DIRECTORIES($(CMAKE_CURRENT_SOURCE_DIR) ${CMAKE_CURRENT_BINARY_DIR} ${GLIB_INCLUDE_DIRS} ${BITSTREAM_INCLUDE_DIR}) ADD_EXECUTABLE(tvoe ${BISON_ConfigParser_OUTPUTS} ${FLEX_ConfigLexer_OUTPUTS} tvoe.c http.c frontend.c log.c mpeg.c channels.c) TARGET_LINK_LIBRARIES(tvoe ${EVENT_LIBRARIES} ${EVENT-THREAD_LIBRARIES} ${GLIB_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})