/* * 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 */ #include "config.h" #include #include #include #include #include #include #include #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_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); }