diff options
Diffstat (limited to 'lib/igt_chamelium_stream.c')
-rw-r--r-- | lib/igt_chamelium_stream.c | 589 |
1 files changed, 589 insertions, 0 deletions
diff --git a/lib/igt_chamelium_stream.c b/lib/igt_chamelium_stream.c new file mode 100644 index 00000000..68ddb217 --- /dev/null +++ b/lib/igt_chamelium_stream.c @@ -0,0 +1,589 @@ +/* + * Copyright © 2019 Intel Corporation + * + * 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 (including the next + * paragraph) 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. + * + * Authors: Simon Ser <simon.ser@intel.com> + */ + +#include "config.h" + +#include <arpa/inet.h> +#include <errno.h> +#include <netdb.h> +#include <stdbool.h> +#include <stdlib.h> +#include <sys/types.h> +#include <sys/socket.h> + +#include "igt_chamelium_stream.h" +#include "igt_core.h" +#include "igt_rc.h" + +#define STREAM_PORT 9994 +#define STREAM_VERSION_MAJOR 1 +#define STREAM_VERSION_MINOR 0 + +enum stream_error { + STREAM_ERROR_NONE = 0, + STREAM_ERROR_COMMAND = 1, + STREAM_ERROR_ARGUMENT = 2, + STREAM_ERROR_EXISTS = 3, + STREAM_ERROR_VIDEO_MEM_OVERFLOW_STOP = 4, + STREAM_ERROR_VIDEO_MEM_OVERFLOW_DROP = 5, + STREAM_ERROR_AUDIO_MEM_OVERFLOW_STOP = 6, + STREAM_ERROR_AUDIO_MEM_OVERFLOW_DROP = 7, + STREAM_ERROR_NO_MEM = 8, +}; + +enum stream_message_kind { + STREAM_MESSAGE_REQUEST = 0, + STREAM_MESSAGE_RESPONSE = 1, + STREAM_MESSAGE_DATA = 2, +}; + +enum stream_message_type { + STREAM_MESSAGE_RESET = 0, + STREAM_MESSAGE_GET_VERSION = 1, + STREAM_MESSAGE_VIDEO_STREAM = 2, + STREAM_MESSAGE_SHRINK_VIDEO = 3, + STREAM_MESSAGE_VIDEO_FRAME = 4, + STREAM_MESSAGE_DUMP_REALTIME_VIDEO = 5, + STREAM_MESSAGE_STOP_DUMP_VIDEO = 6, + STREAM_MESSAGE_DUMP_REALTIME_AUDIO = 7, + STREAM_MESSAGE_STOP_DUMP_AUDIO = 8, +}; + +struct chamelium_stream { + char *host; + unsigned int port; + + int fd; +}; + +static const char *stream_error_str(enum stream_error err) +{ + switch (err) { + case STREAM_ERROR_NONE: + return "no error"; + case STREAM_ERROR_COMMAND: + return "invalid command"; + case STREAM_ERROR_ARGUMENT: + return "invalid arguments"; + case STREAM_ERROR_EXISTS: + return "dump already started"; + case STREAM_ERROR_VIDEO_MEM_OVERFLOW_STOP: + return "video dump stopped after overflow"; + case STREAM_ERROR_VIDEO_MEM_OVERFLOW_DROP: + return "video frame dropped after overflow"; + case STREAM_ERROR_AUDIO_MEM_OVERFLOW_STOP: + return "audio dump stoppred after overflow"; + case STREAM_ERROR_AUDIO_MEM_OVERFLOW_DROP: + return "audio page dropped after overflow"; + case STREAM_ERROR_NO_MEM: + return "out of memory"; + } + return "unknown error"; +} + +/** + * The Chamelium URL is specified in the configuration file. We need to extract + * the host to connect to the stream server. + */ +static char *parse_url_host(const char *url) +{ + static const char prefix[] = "http://"; + char *colon; + + if (strstr(url, prefix) != url) + return NULL; + url += strlen(prefix); + + colon = strchr(url, ':'); + if (!colon) + return NULL; + + return strndup(url, colon - url); +} + +static bool chamelium_stream_read_config(struct chamelium_stream *client) +{ + GError *error = NULL; + gchar *chamelium_url; + + if (!igt_key_file) { + igt_warn("No configuration file available for chamelium\n"); + return false; + } + + chamelium_url = g_key_file_get_string(igt_key_file, "Chamelium", "URL", + &error); + if (!chamelium_url) { + igt_warn("Couldn't read Chamelium URL from config file: %s\n", + error->message); + return false; + } + + client->host = parse_url_host(chamelium_url); + if (!client->host) { + igt_warn("Invalid Chamelium URL in config file: %s\n", + chamelium_url); + return false; + } + client->port = STREAM_PORT; + + return true; +} + +static bool chamelium_stream_connect(struct chamelium_stream *client) +{ + int ret; + char port_str[16]; + struct addrinfo hints = {}; + struct addrinfo *results, *ai; + struct timeval tv = {}; + + igt_debug("Connecting to Chamelium stream server: tcp://%s:%u\n", + client->host, client->port); + + snprintf(port_str, sizeof(port_str), "%u", client->port); + + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + ret = getaddrinfo(client->host, port_str, &hints, &results); + if (ret != 0) { + igt_warn("getaddrinfo failed: %s\n", gai_strerror(ret)); + return false; + } + + client->fd = -1; + for (ai = results; ai != NULL; ai = ai->ai_next) { + client->fd = socket(ai->ai_family, ai->ai_socktype, + ai->ai_protocol); + if (client->fd == -1) + continue; + + if (connect(client->fd, ai->ai_addr, ai->ai_addrlen) == -1) { + close(client->fd); + client->fd = -1; + continue; + } + + break; + } + + freeaddrinfo(results); + + if (client->fd < 0) { + igt_warn("Failed to connect to Chamelium stream server\n"); + return false; + } + + /* Set a read and write timeout of 5 seconds. */ + tv.tv_sec = 5; + setsockopt(client->fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + setsockopt(client->fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); + + return true; +} + +static bool read_whole(int fd, void *buf, size_t buf_len) +{ + ssize_t ret; + size_t n = 0; + char *ptr; + + while (n < buf_len) { + ptr = (char *) buf + n; + ret = read(fd, ptr, buf_len - n); + if (ret < 0) { + igt_warn("read failed: %s\n", strerror(errno)); + return false; + } else if (ret == 0) { + igt_warn("short read\n"); + return false; + } + n += ret; + } + + return true; +} + +static bool write_whole(int fd, void *buf, size_t buf_len) +{ + ssize_t ret; + size_t n = 0; + char *ptr; + + while (n < buf_len) { + ptr = (char *) buf + n; + ret = write(fd, ptr, buf_len - n); + if (ret < 0) { + igt_warn("write failed: %s\n", strerror(errno)); + return false; + } else if (ret == 0) { + igt_warn("short write\n"); + return false; + } + n += ret; + } + + return true; +} + +static bool read_and_discard(int fd, size_t len) +{ + char buf[1024]; + size_t n; + + while (len > 0) { + n = len; + if (n > sizeof(buf)) + n = sizeof(buf); + + if (!read_whole(fd, buf, n)) + return false; + + len -= n; + } + + return true; +} + +/** Read a message header from the socket. + * + * The header is laid out as follows: + * - u16: message type + * - u16: error code + * - u32: message length + */ +static bool chamelium_stream_read_header(struct chamelium_stream *client, + enum stream_message_kind *kind, + enum stream_message_type *type, + enum stream_error *err, + size_t *len) +{ + uint16_t _type; + char buf[8]; + + if (!read_whole(client->fd, buf, sizeof(buf))) + return false; + + _type = ntohs(*(uint16_t *) &buf[0]); + *type = _type & 0xFF; + *kind = _type >> 8; + *err = ntohs(*(uint16_t *) &buf[2]); + *len = ntohl(*(uint32_t *) &buf[4]); + + return true; +} + +static bool chamelium_stream_write_header(struct chamelium_stream *client, + enum stream_message_type type, + enum stream_error err, + size_t len) +{ + char buf[8]; + uint16_t _type; + + _type = type | (STREAM_MESSAGE_REQUEST << 8); + + *(uint16_t *) &buf[0] = htons(_type); + *(uint16_t *) &buf[2] = htons(err); + *(uint32_t *) &buf[4] = htonl(len); + + return write_whole(client->fd, buf, sizeof(buf)); +} + +static bool chamelium_stream_read_response(struct chamelium_stream *client, + enum stream_message_type type, + void *buf, size_t buf_len) +{ + enum stream_message_kind read_kind; + enum stream_message_type read_type; + enum stream_error read_err; + size_t read_len; + + if (!chamelium_stream_read_header(client, &read_kind, &read_type, + &read_err, &read_len)) + return false; + + if (read_kind != STREAM_MESSAGE_RESPONSE) { + igt_warn("Expected a response, got kind %d\n", read_kind); + return false; + } + if (read_type != type) { + igt_warn("Expected message type %d, got %d\n", + type, read_type); + return false; + } + if (read_err != STREAM_ERROR_NONE) { + igt_warn("Received error: %s (%d)\n", + stream_error_str(read_err), read_err); + return false; + } + if (buf_len != read_len) { + igt_warn("Received invalid message body size " + "(got %zu bytes, want %zu bytes)\n", + read_len, buf_len); + return false; + } + + return read_whole(client->fd, buf, buf_len); +} + +static bool chamelium_stream_write_request(struct chamelium_stream *client, + enum stream_message_type type, + void *buf, size_t buf_len) +{ + if (!chamelium_stream_write_header(client, type, STREAM_ERROR_NONE, + buf_len)) + return false; + + if (buf_len == 0) + return true; + + return write_whole(client->fd, buf, buf_len); +} + +static bool chamelium_stream_call(struct chamelium_stream *client, + enum stream_message_type type, + void *req_buf, size_t req_len, + void *resp_buf, size_t resp_len) +{ + if (!chamelium_stream_write_request(client, type, req_buf, req_len)) + return false; + + return chamelium_stream_read_response(client, type, resp_buf, resp_len); +} + +static bool chamelium_stream_check_version(struct chamelium_stream *client) +{ + char resp[2]; + uint8_t major, minor; + + if (!chamelium_stream_call(client, STREAM_MESSAGE_GET_VERSION, + NULL, 0, resp, sizeof(resp))) + return false; + + major = resp[0]; + minor = resp[1]; + if (major != STREAM_VERSION_MAJOR || minor < STREAM_VERSION_MINOR) { + igt_warn("Version mismatch (want %d.%d, got %d.%d)\n", + STREAM_VERSION_MAJOR, STREAM_VERSION_MINOR, + major, minor); + return false; + } + + return true; +} + +/** + * chamelium_stream_dump_realtime_audio: + * + * Starts audio capture. The caller can then call + * #chamelium_stream_receive_realtime_audio to receive audio pages. + */ +bool chamelium_stream_dump_realtime_audio(struct chamelium_stream *client, + enum chamelium_stream_realtime_mode mode) +{ + char req[1]; + + igt_debug("Starting real-time audio capture\n"); + + req[0] = mode; + return chamelium_stream_call(client, STREAM_MESSAGE_DUMP_REALTIME_AUDIO, + req, sizeof(req), NULL, 0); +} + +/** + * chamelium_stream_receive_realtime_audio: + * @page_count: if non-NULL, will be set to the dumped page number + * @buf: must either point to a dynamically allocated memory region or NULL + * @buf_len: number of elements of *@buf, for zero if @buf is NULL + * + * Receives one audio page from the streaming server. + * + * In "best effort" mode, some pages can be dropped. This can be detected via + * the page count. + * + * buf_len will be set to the size of the page. The caller is responsible for + * calling free(3) on *buf. + */ +bool chamelium_stream_receive_realtime_audio(struct chamelium_stream *client, + size_t *page_count, + int32_t **buf, size_t *buf_len) +{ + enum stream_message_kind kind; + enum stream_message_type type; + enum stream_error err; + size_t body_len; + char page_count_buf[4]; + int32_t *ptr; + + while (true) { + if (!chamelium_stream_read_header(client, &kind, &type, + &err, &body_len)) + return false; + + if (kind != STREAM_MESSAGE_DATA) { + igt_warn("Expected a data message, got kind %d\n", kind); + return false; + } + if (type != STREAM_MESSAGE_DUMP_REALTIME_AUDIO) { + igt_warn("Expected real-time audio dump message, " + "got type %d\n", type); + return false; + } + + if (err == STREAM_ERROR_NONE) + break; + else if (err != STREAM_ERROR_AUDIO_MEM_OVERFLOW_DROP) { + igt_warn("Received error: %s (%d)\n", + stream_error_str(err), err); + return false; + } + + igt_debug("Dropped an audio page because of an overflow\n"); + igt_assert(body_len == 0); + } + + igt_assert(body_len >= sizeof(page_count_buf)); + + if (!read_whole(client->fd, page_count_buf, sizeof(page_count_buf))) + return false; + if (page_count) + *page_count = ntohl(*(uint32_t *) &page_count_buf[0]); + body_len -= sizeof(page_count_buf); + + igt_assert(body_len % sizeof(int32_t) == 0); + if (*buf_len * sizeof(int32_t) != body_len) { + ptr = realloc(*buf, body_len); + if (!ptr) { + igt_warn("realloc failed: %s\n", strerror(errno)); + return false; + } + *buf = ptr; + *buf_len = body_len / sizeof(int32_t); + } + + return read_whole(client->fd, *buf, body_len); +} + +/** + * chamelium_stream_stop_realtime_audio: + * + * Stops real-time audio capture. This also drops any buffered audio pages. + * The caller shouldn't call #chamelium_stream_receive_realtime_audio after + * stopping audio capture. + */ +bool chamelium_stream_stop_realtime_audio(struct chamelium_stream *client) +{ + enum stream_message_kind kind; + enum stream_message_type type; + enum stream_error err; + size_t len; + + igt_debug("Stopping real-time audio capture\n"); + + if (!chamelium_stream_write_request(client, + STREAM_MESSAGE_STOP_DUMP_AUDIO, + NULL, 0)) + return false; + + while (true) { + if (!chamelium_stream_read_header(client, &kind, &type, + &err, &len)) + return false; + + if (kind == STREAM_MESSAGE_RESPONSE) + break; + + if (!read_and_discard(client->fd, len)) + return false; + } + + if (type != STREAM_MESSAGE_STOP_DUMP_AUDIO) { + igt_warn("Unexpected response type %d\n", type); + return false; + } + if (err != STREAM_ERROR_NONE) { + igt_warn("Received error: %s (%d)\n", + stream_error_str(err), err); + return false; + } + if (len != 0) { + igt_warn("Expected an empty response, got %zu bytes\n", len); + return false; + } + + return true; +} + +/** + * chamelium_stream_audio_format: + * + * Gets the format used for audio pages. + * + * Data will always be captured in raw pages of S32_LE elements. This function + * exposes the sampling rate and the number of channels. + */ +void chamelium_stream_audio_format(struct chamelium_stream *stream, + int *rate, int *channels) +{ + /* TODO: the Chamelium streaming server doesn't expose those yet. + * Just hardcode the values for now. */ + *rate = 48000; + *channels = 8; +} + +/** + * chamelium_stream_init: + * + * Connects to the Chamelium streaming server. + */ +struct chamelium_stream *chamelium_stream_init(void) +{ + struct chamelium_stream *client; + + client = calloc(1, sizeof(*client)); + + if (!chamelium_stream_read_config(client)) + goto error_client; + if (!chamelium_stream_connect(client)) + goto error_client; + if (!chamelium_stream_check_version(client)) + goto error_fd; + + return client; + +error_fd: + close(client->fd); +error_client: + free(client); + return NULL; +} + +void chamelium_stream_deinit(struct chamelium_stream *client) +{ + if (close(client->fd) != 0) + igt_warn("close failed: %s\n", strerror(errno)); + free(client); +} |