diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index 87a419461f2f84f9f788d35f0a4a026f20ed1e15..9cc5cabfd72e85cd29bf24d1c491ac4137929d5a 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -16,4 +16,6 @@ add_subdirectory(rtsp-describe) add_subdirectory(rav-lookup) add_subdirectory(rav-publish) +add_subdirectory(rtp-send) + diff --git a/src/utils/rtp-send/CMakeLists.txt b/src/utils/rtp-send/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..ee1df254d3660fe8cf53dc2b8bb12083c4df3cae --- /dev/null +++ b/src/utils/rtp-send/CMakeLists.txt @@ -0,0 +1,22 @@ +cmake_minimum_required(VERSION 3.11) + +set (CMAKE_CONFIGURATION_TYPES "Debug;Release") + +project(rtp-send) + +add_executable(rtp-send + rtp-send.c + aes67opts.h + ${AES67_INCLUDES} + ${AES67_SOURCE_FILES} + ) +target_include_directories(rtp-send + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${AES67_INCLUDE_DIRS} + ${AES67_PORT_INCLUDE_DIRS} + ) +target_link_libraries(rtp-send "${AES67_PORT_LIB}") + + + diff --git a/src/utils/rtp-send/aes67opts.h b/src/utils/rtp-send/aes67opts.h new file mode 100644 index 0000000000000000000000000000000000000000..b4e0f505b2bd6ac42e27ea1ed9bf87659e326c88 --- /dev/null +++ b/src/utils/rtp-send/aes67opts.h @@ -0,0 +1,22 @@ +/** + * AES67 Framework + * Copyright (C) 2021 Philip Tschiemer, https://github.com/tschiemer/aes67 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef AES67_AES67OPTS_H +#define AES67_AES67OPTS_H + +#endif //AES67_AES67OPTS_H_H diff --git a/src/utils/rtp-send/rtp-send.c b/src/utils/rtp-send/rtp-send.c new file mode 100644 index 0000000000000000000000000000000000000000..0b13173569234cfa97ea4dca938befd3fa063678 --- /dev/null +++ b/src/utils/rtp-send/rtp-send.c @@ -0,0 +1,487 @@ +/** + * AES67 Framework + * Copyright (C) 2021 Philip Tschiemer, https://github.com/tschiemer/aes67 + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "aes67/sap.h" +#include "aes67/sdp.h" +#include "aes67/rtp.h" +#include "aes67/rtp-avp.h" + +#include <stdbool.h> +#include <stdio.h> +#include <unistd.h> +#include <string.h> +#include <getopt.h> +#include <fcntl.h> +#include <sys/socket.h> +#include <netinet/in.h> + +static struct { + struct aes67_net_addr ip; + uint16_t port; + struct aes67_sdp_attr_encoding encoding; + ptime_t ptime; +} opts = { + .ip = { + .ipver = aes67_net_ipver_undefined + }, + .port = AES67_RTP_AVP_PORT_DEFAULT, + .encoding = { + .payloadtype = AES67_RTP_AVP_PAYLOADTYPE_DYNAMIC_START, + .encoding = aes67_audio_encoding_undefined, + .nchannels = 0, + .samplerate = 0, + }, + .ptime = 1000 +}; + +static char * argv0; + +static volatile bool keep_running; + +static struct { + int fd; + struct sockaddr_in addr_in; +} sock = { + .fd = -1 +}; + +static void help(FILE * fd) +{ + fprintf( fd, + "Usage:\n" + "%s -h|-?\n" + "%s --sdp <sdp-file>\n" + "%s --i <ipv4> -p <port> -r <samplerate> -c <channels> -b <bits> --ptime <ptime> [--payloadtype <type>]\n" + "Options:\n" + "\t --sdp <sdp-file>\t Load all parameters from given SDP\n" + "\t --ip, -i <ipv4>\t Send to given sink address (uni- or multicast)\n" + "\t --port, -p <port>\t Send to given sink port (default %d)\n" + "\t --channels, -c <channels>\n" + "\t\t\t\t\t\t Channel count\n" + "\t --bits, -b <bits>\t Sample bits (8,16,24,32)\n" + "\t --ptime, -p <ptime> ptime value as millisec float (default 1.0)" + + , argv0, argv0, argv0, AES67_RTP_AVP_PORT_DEFAULT); +} + +static void sig_stop(int sig) +{ + keep_running = false; +} + +static void block_until_event(){ +// +// int nfds = 0; +// fd_set rfds, xfds; +//// sigset_t sigmask; +// +// FD_ZERO(&rfds); +// FD_ZERO(&xfds); +// +// if (opts.rtsp){ +// nfds = rtsp_srv.listen_sockfd; +// FD_SET(rtsp_srv.listen_sockfd, &rfds); +// FD_SET(rtsp_srv.listen_sockfd, &xfds); +// if (rtsp_srv.client_sockfd != -1){ +// FD_SET(rtsp_srv.client_sockfd, &rfds); +// FD_SET(rtsp_srv.client_sockfd, &xfds); +// if (rtsp_srv.client_sockfd > rtsp_srv.listen_sockfd){ +// nfds = rtsp_srv.client_sockfd; +// } +// } +// } +// +// int * sockfds; +// size_t count = 0; +// aes67_mdns_getsockfds(mdns, &sockfds, &count); +// for (size_t i = 0; i < count; i++) { +// FD_SET(sockfds[i], &rfds); +// FD_SET(sockfds[i], &xfds); +// if (sockfds[i] > nfds) { +// nfds = sockfds[i]; +// } +// } +// +// nfds++; +// +// // just wait until something interesting happens +// select(nfds, &rfds, NULL, &xfds, NULL); +} + +static size_t readfile(char * fname, u8_t * buf, size_t maxlen) +{ + FILE * fd = fopen(fname, "rb"); + if (fd == NULL){ + fprintf(stderr, "ERROR failed to open file %s\n", fname); + exit(EXIT_FAILURE); + } + + int c; + ssize_t len = 0; + while( (c = fgetc(fd)) != EOF ){ + if (len >= maxlen){ + fprintf(stderr, "ERROR overflow\n"); + exit(EXIT_FAILURE); + } + buf[len++] = c; + } + + fclose(fd); + + return len; +} + +static int loadsdp(char * fname) +{ + u8_t fbuf[1500]; + + int flen = readfile(fname, fbuf, sizeof(fbuf)); + if (flen == 0){ + return EXIT_FAILURE; + } + + struct aes67_sdp sdp; + + int r = aes67_sdp_fromstr(&sdp, (u8_t*)fbuf, flen, NULL); + if (r != AES67_SDP_OK){ + fprintf(stderr, "failed to parse SDP %s\n", fname); + return EXIT_FAILURE; + } + + if (sdp.streams.count != 1){ + fprintf(stderr, "invalid stream/media count in SDP\n"); + } + + if (sdp.streams.data[0].nencodings != 1){ + fprintf(stderr, "please only provide one encoding for stream/media in SDP\n"); + return EXIT_FAILURE; + } + + if (sdp.encodings.count != 1){ + fprintf(stderr, "there should really only be one encoding for one SDP\n"); + return EXIT_FAILURE; + } + + if (sdp.connections.count != 1){ + fprintf(stderr, "there should only be one connection in SDP\n"); + return EXIT_FAILURE; + } + + if (aes67_net_str2addr(&opts.ip, sdp.connections.data[0].address.data, sdp.connections.data[0].address.length) == false){ + fprintf(stderr, "connection must be an ip (not doing lookups)!\n"); + return EXIT_FAILURE; + } + + opts.port = sdp.streams.data[0].port; + opts.ptime = sdp.streams.data[0].ptime; + memcpy(&opts.encoding, sdp.encodings.data, sizeof(struct aes67_sdp_attr_encoding)); + + + return EXIT_SUCCESS; +} + +static int socket_setup() +{ + sock.fd = socket(AF_INET, SOCK_RAW, IPPROTO_UDP); + + if (sock.fd == -1){ + perror("socket()"); + return EXIT_FAILURE; + } + + memset(&sock.addr_in, 0, sizeof(struct sockaddr_in)); + + sock.addr_in.sin_family = AF_INET; + sock.addr_in.sin_addr.s_addr = *(in_addr_t*)opts.ip.ip; + sock.addr_in.sin_port = htons(opts.port); + + int on = 1; + // inform the kernel do not fill up the packet structure, we will build our own + if(setsockopt(sock.fd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on)) < 0) { + perror("setsockopt() error"); + return EXIT_FAILURE; + } + + // if multicast address join group + if (aes67_net_ismcastip_addr(&opts.ip)){ + + struct ip_mreq mreq; + mreq.imr_interface.s_addr = INADDR_ANY; + mreq.imr_multiaddr.s_addr = *(in_addr_t*)opts.ip.ip; + + if (setsockopt(sock.fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(struct ip_mreq)) < 0){ + perror("setsockopt(.. IP_ADD_MEMBERSHIP..)"); + return EXIT_FAILURE; + } + + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + +static void socket_teardown() +{ + if (sock.fd == -1){ + return; + } + // if multicast address leave group + if (aes67_net_ismcastip_addr(&opts.ip)){ + + struct ip_mreq mreq; + mreq.imr_interface.s_addr = INADDR_ANY; + mreq.imr_multiaddr.s_addr = *(in_addr_t*)opts.ip.ip; + + if (setsockopt(sock.fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(struct ip_mreq)) < 0){ + perror("setsockopt(.. IP_DROP_MEMBERSHIP..)"); + return; + } + + return; + } + + close(sock.fd); + + sock.fd = -1; +} + +int main(int argc, char * argv[]) +{ + argv0 = argv[0]; + + if (argc == 1){ + help(stdout); + return EXIT_SUCCESS; + } + + + while (1) { + int c; + + int option_index = 0; + static struct option long_options[] = { + {"sdp", required_argument, 0, 1 }, + {"bits", required_argument, 0, 'b' }, + {"channels", required_argument, 0, 'c' }, + {"rate", required_argument, 0, 'r' }, + {"payloadtype", required_argument, 0, 2 }, + {"port", required_argument, 0, 'p'}, + {"rtcp", no_argument, 0, 3}, + {"ip", required_argument, 0, 'i'}, +// {"v1", no_argument, 0, 2}, +// {"xf", no_argument, 0, 3}, + {0, 0, 0, 0 } + }; + + c = getopt_long(argc, argv, "?hb:c:r:p:i:", + long_options, &option_index); + if (c == -1) + break; + + switch (c) { + case 1: // --sdp + if (loadsdp(optarg)){ + fprintf(stderr, "failed to load sdp: %s\n", optarg); + return EXIT_FAILURE; + } + break; + + case 'b': { // --bits + int t = atoi(optarg); + if (t == 8) { + opts.encoding.encoding = aes67_audio_encoding_L8; + } else if (t == 16) { + opts.encoding.encoding = aes67_audio_encoding_L16; + } else if (t == 24) { + opts.encoding.encoding = aes67_audio_encoding_L24; + } else if (t == 32) { + opts.encoding.encoding = aes67_audio_encoding_L32; +// } else if (strcmp(AES67_AUDIO_ENC_AM824_STR, optarg) == 0){ +// opts.encoding.encoding = aes67_audio_encoding_AM824; + } else { + fprintf(stderr, "invalid --bitrate\n"); + return EXIT_FAILURE; + } + break; + } + + case 'r':{ // --rate + int t = atoi(optarg); + if (t == 0){ // wellllly well + fprintf(stderr, "invalid --rate\n"); + return EXIT_FAILURE; + } + opts.encoding.samplerate = t; + break; + } + + case 'c':{ // --rate + int t = atoi(optarg); + if (t == 0){ // well... + fprintf(stderr, "invalid --nchannel\n"); + return EXIT_FAILURE; + } + opts.encoding.nchannels = t; + break; + } + + case 2: { // --payloadtype + int t = atoi(optarg); + if ((t & ~0x7f) != t) { + fprintf(stderr, "invalid payloadtype, must be in 0 - 127\n"); + return EXIT_FAILURE; + } + opts.encoding.payloadtype = t; + break; + } + + case 'p': {// --port <port> + int t = atoi(optarg); + if ( t <= 0 || 0xffff < t){ + fprintf(stderr, "invalid port!\n"); + return EXIT_FAILURE; + } + opts.port = t; + break; + } + + case 'i': // --ip + if (0 == aes67_net_str2addr(&opts.ip, (uint8_t*)optarg, strlen(optarg))){ + fprintf(stderr, "ERROR invalid originating source %s (must be ipv4)\n", optarg); + return EXIT_FAILURE; + } + if (opts.ip.port != 0){ + opts.port = opts.ip.port; + } + break; + + + case '?': + case 'h': + help(stdout); + return EXIT_SUCCESS; + + default: + fprintf(stderr, "Unrecognized option %c\n", c); + return EXIT_FAILURE; + } + } + + + if (optind != argc){ + fprintf(stderr, "too many arguments\n"); + return EXIT_FAILURE; + } + + // parameter checking + + if (opts.ip.ipver != aes67_net_ipver_4){ + fprintf(stderr, "sink must be ipv4\n"); + return EXIT_FAILURE; + } + + if (!AES67_AUDIO_ENCODING_ISVALID(opts.encoding.encoding) && opts.encoding.encoding != aes67_audio_encoding_AM824){ + fprintf(stderr, "invalid audio encoding\n"); + return EXIT_FAILURE; + } + if (opts.encoding.samplerate == 0){ /// ... TODO + fprintf(stderr, "invalid samplerate\n"); + return EXIT_FAILURE; + } + if (opts.encoding.nchannels == 0){ + fprintf(stderr, "invalid channel count\n"); + return EXIT_FAILURE; + } + if (opts.ptime == 0){ + fprintf(stderr, "invalid ptime\n"); + return EXIT_FAILURE; + } + + + if (socket_setup()){ + goto shutdown; + } + + + + + signal(SIGINT, sig_stop); + signal(SIGTERM, sig_stop); + keep_running = true; +// while(keep_running){ +// block_until_event(); +// } + +shutdown: + + socket_teardown(); + +// +// // set non-blocking stdin +// int flags = fcntl(STDIN_FILENO, F_GETFL, 0); +// if (fcntl(STDIN_FILENO, F_SETFL, flags | O_NONBLOCK) == -1){ +// fprintf(stderr, "Couldn't nonblock stdin\n"); +// return EXIT_FAILURE; +// } +// +// +// +// u8_t * payload = &inbuf[typelen]; +// +// size_t maxlen = sizeof(inbuf) - typelen; +// +// ssize_t r; +// +// u8_t ch; +// size_t len = 0; +// +// // get char by char, try to locate "v=0" +// while( (r = read(STDIN_FILENO, &ch, 1)) ){ +// +// // timeout +// if (r == -1){ +// if (len > 0){ +// generate_packet(inbuf, len, typelen); +// } +// } +// if (r > 0) { +// +// if (len >= maxlen){ +// fprintf(stderr, "SAP-PACK inbuffer overflow, discarding data\n"); +// +// len = 0; +// } +// +// payload[len++] = ch; +// +// if (len > 8 && memcmp(&payload[len-4], "\nv=0", 4) == 0){ +// payload[len-3] = '\0'; // not needed, but makes it nicer in debug +// generate_packet(inbuf, len - 3, typelen); +// memcpy(payload, "v=0", 3); +// len = 3; +// } +// } +// } +// +// // in case STDIN was closed see if there might be something to be processed still +// if (len > 0){ +// generate_packet(inbuf, len, typelen); +// } + + return 0; +} \ No newline at end of file