diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 66f45fb6..8a7248b6 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -79,6 +79,25 @@ if(ENABLE_AUDIO AND NOT (ENABLE_PIPEWIRE OR ENABLE_PULSEAUDIO)) endif() add_feature_info(ENABLE_AUDIO ENABLE_AUDIO "Audio support.") +cmake_dependent_option( + ENABLE_USB_AUDIO + "Build with USB audio support" + ON + "ENABLE_AUDIO" + OFF +) + +if(ENABLE_USB_AUDIO) + pkg_check_modules(USBREDIRPARSER QUIET IMPORTED_TARGET + libusbredirparser-0.5>=0.7.1) + if(NOT USBREDIRPARSER_FOUND) + message(STATUS + "libusbredirparser was not found, disabling USB audio support") + set(ENABLE_USB_AUDIO OFF) + endif() +endif() +add_feature_info(ENABLE_USB_AUDIO ENABLE_USB_AUDIO "USB audio support.") + add_compile_options( "-Wall" "-Wextra" @@ -195,6 +214,12 @@ if(ENABLE_AUDIO) ) endif() +if(ENABLE_USB_AUDIO) + list(APPEND SOURCES + src/usbredir.c + ) +endif() + add_subdirectory("${PROJECT_TOP}/resources" "${CMAKE_BINARY_DIR}/resources") add_subdirectory("${PROJECT_TOP}/common" "${CMAKE_BINARY_DIR}/common" ) add_subdirectory("${PROJECT_TOP}/repos/LGMP/lgmp" "${CMAKE_BINARY_DIR}/LGMP" ) @@ -259,6 +284,13 @@ if(ENABLE_AUDIO) ) endif() +if(ENABLE_USB_AUDIO) + target_compile_definitions(looking-glass-client PRIVATE ENABLE_USB_AUDIO) + target_link_libraries(looking-glass-client + PkgConfig::USBREDIRPARSER + ) +endif() + install(TARGETS looking-glass-client RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT binary) diff --git a/client/src/usbredir.c b/client/src/usbredir.c new file mode 100644 index 00000000..c1ab3b45 --- /dev/null +++ b/client/src/usbredir.c @@ -0,0 +1,348 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * 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 2 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., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "usbredir.h" + +#include "common/debug.h" + +#include + +#include +#include +#include + +#define USB_REDIR_CHANNEL_COUNT 256 + +struct LG_USBRedir +{ + const LG_USBRedirDeviceOps * deviceOps; + void * deviceOpaque; + + PSUSBRedirChannel * channels[USB_REDIR_CHANNEL_COUNT]; + PSUSBRedirChannel * channel; + struct usbredirparser * parser; + + const uint8_t * readData; + size_t readSize; + size_t readOffset; + + atomic_bool desiredPlugged; + atomic_bool available; + bool plugged; +}; + +static int readUSBRedir(void * opaque, uint8_t * data, int count) +{ + LG_USBRedir * usbredir = opaque; + const size_t remaining = usbredir->readSize - usbredir->readOffset; + if (remaining < (size_t)count) + count = (int)remaining; + + if (!count) + return 0; + + memcpy(data, usbredir->readData + usbredir->readOffset, count); + usbredir->readOffset += count; + return count; +} + +static int writeUSBRedir(void * opaque, uint8_t * data, int count) +{ + LG_USBRedir * usbredir = opaque; + if (!usbredir->channel || + !purespice_usbRedirWrite(usbredir->channel, data, count)) + return -1; + + return count; +} + +static void logUSBRedir(void * opaque, int level, const char * message) +{ + (void)opaque; + switch (level) + { + case usbredirparser_error: + DEBUG_ERROR("USB redirection: %s", message); + break; + + case usbredirparser_warning: + DEBUG_WARN("USB redirection: %s", message); + break; + + case usbredirparser_info: + DEBUG_INFO("USB redirection: %s", message); + break; + + case usbredirparser_debug: + case usbredirparser_debug_data: + DEBUG_TRACE("USB redirection: %s", message); + break; + + case usbredirparser_none: + break; + } +} + +static void helloUSBRedir(void * opaque, + struct usb_redir_hello_header * hello) +{ + (void)hello; + LG_USBRedir * usbredir = opaque; + atomic_store_explicit(&usbredir->available, true, memory_order_release); +} + +static void unplugDevice(LG_USBRedir * usbredir) +{ + if (!usbredir->plugged) + return; + + usbredir->deviceOps->unplug(usbredir->deviceOpaque); + usbredir->plugged = false; +} + +static void destroyParser(LG_USBRedir * usbredir) +{ + atomic_store_explicit(&usbredir->available, false, memory_order_release); + unplugDevice(usbredir); + + if (!usbredir->parser) + return; + + usbredirparser_destroy(usbredir->parser); + usbredir->parser = NULL; +} + +static bool flushUSBRedir(LG_USBRedir * usbredir) +{ + return !usbredir->parser || + usbredirparser_do_write(usbredir->parser) == 0; +} + +static bool connectChannel(LG_USBRedir * usbredir, + PSUSBRedirChannel * channel) +{ + usbredir->channel = channel; + if (purespice_usbRedirConnect(channel)) + return true; + + DEBUG_WARN("Failed to connect USB redirection channel %u", + purespice_usbRedirChannelId(channel)); + usbredir->channel = NULL; + return false; +} + +static void selectChannel(LG_USBRedir * usbredir) +{ + if (usbredir->channel) + return; + + for (unsigned int i = 0; i < USB_REDIR_CHANNEL_COUNT; ++i) + { + PSUSBRedirChannel * channel = usbredir->channels[i]; + if (!channel || !purespice_usbRedirAvailable(channel)) + continue; + + if (connectChannel(usbredir, channel)) + return; + } +} + +static bool createParser(LG_USBRedir * usbredir) +{ + if (usbredir->parser) + return true; + + usbredir->parser = usbredirparser_create(); + if (!usbredir->parser) + { + DEBUG_ERROR("Failed to create the USB redirection parser"); + return false; + } + + usbredir->deviceOps->setup(usbredir->deviceOpaque, usbredir->parser); + usbredir->parser->priv = usbredir; + usbredir->parser->log_func = logUSBRedir; + usbredir->parser->read_func = readUSBRedir; + usbredir->parser->write_func = writeUSBRedir; + usbredir->parser->hello_func = helloUSBRedir; + + uint32_t caps[USB_REDIR_CAPS_SIZE] = { 0 }; + usbredirparser_caps_set_cap(caps, + usb_redir_cap_connect_device_version); + usbredirparser_caps_set_cap(caps, + usb_redir_cap_ep_info_max_packet_size); + usbredirparser_caps_set_cap(caps, usb_redir_cap_64bits_ids); + usbredirparser_caps_set_cap(caps, usb_redir_cap_32bits_bulk_length); + usbredirparser_init(usbredir->parser, "Looking Glass", caps, + USB_REDIR_CAPS_SIZE, usbredirparser_fl_usb_host); + + if (flushUSBRedir(usbredir)) + return true; + + DEBUG_ERROR("Failed to send the USB redirection hello packet"); + destroyParser(usbredir); + return false; +} + +LG_USBRedir * lgUsbRedir_create( + const LG_USBRedirDeviceOps * deviceOps, void * deviceOpaque) +{ + if (!deviceOps || !deviceOps->setup || !deviceOps->plug || + !deviceOps->unplug) + return NULL; + + LG_USBRedir * usbredir = calloc(1, sizeof(*usbredir)); + if (!usbredir) + return NULL; + + usbredir->deviceOps = deviceOps; + usbredir->deviceOpaque = deviceOpaque; + atomic_init(&usbredir->desiredPlugged, false); + atomic_init(&usbredir->available, false); + return usbredir; +} + +void lgUsbRedir_destroy(LG_USBRedir * usbredir) +{ + if (!usbredir) + return; + + destroyParser(usbredir); + free(usbredir); +} + +void lgUsbRedir_setPlugged(LG_USBRedir * usbredir, bool plugged) +{ + atomic_store_explicit(&usbredir->desiredPlugged, plugged, + memory_order_release); +} + +bool lgUsbRedir_available(const LG_USBRedir * usbredir) +{ + return atomic_load_explicit(&usbredir->available, memory_order_acquire); +} + +bool lgUsbRedir_process(LG_USBRedir * usbredir) +{ + if (!usbredir->parser || + !atomic_load_explicit(&usbredir->available, memory_order_acquire)) + return flushUSBRedir(usbredir); + + const bool desired = atomic_load_explicit(&usbredir->desiredPlugged, + memory_order_acquire); + if (desired != usbredir->plugged) + { + if (desired) + { + usbredir->deviceOps->plug( + usbredir->deviceOpaque, usbredir->parser); + usbredir->plugged = true; + } + else + { + unplugDevice(usbredir); + usbredirparser_send_device_disconnect(usbredir->parser); + } + } + + return flushUSBRedir(usbredir); +} + +void lgUsbRedir_state(PSUSBRedirChannel * channel, + PSUSBRedirState state, void * opaque) +{ + LG_USBRedir * usbredir = opaque; + const uint8_t id = purespice_usbRedirChannelId(channel); + + switch (state) + { + case PS_USB_REDIR_AVAILABLE: + usbredir->channels[id] = channel; + selectChannel(usbredir); + break; + + case PS_USB_REDIR_CONNECTED: + if (channel != usbredir->channel) + { + purespice_usbRedirDisconnect(channel); + break; + } + + if (!createParser(usbredir)) + purespice_usbRedirDisconnect(channel); + break; + + case PS_USB_REDIR_DISCONNECTED: + if (channel == usbredir->channel) + destroyParser(usbredir); + break; + + case PS_USB_REDIR_UNAVAILABLE: + if (usbredir->channels[id] == channel) + usbredir->channels[id] = NULL; + + if (channel == usbredir->channel) + { + destroyParser(usbredir); + usbredir->channel = NULL; + selectChannel(usbredir); + } + break; + } +} + +bool lgUsbRedir_data(PSUSBRedirChannel * channel, + const uint8_t * data, size_t size, void * opaque) +{ + LG_USBRedir * usbredir = opaque; + if (channel != usbredir->channel || !usbredir->parser || + usbredir->readData) + return false; + + usbredir->readData = data; + usbredir->readSize = size; + usbredir->readOffset = 0; + + const int result = usbredirparser_do_read(usbredir->parser); + const bool consumed = usbredir->readOffset == usbredir->readSize; + usbredir->readData = NULL; + usbredir->readSize = 0; + usbredir->readOffset = 0; + + if (result == usbredirparser_read_parse_error) + { + DEBUG_ERROR("Invalid USB redirection packet"); + return false; + } + + if (!consumed) + { + DEBUG_ERROR("USB redirection parser did not consume its input"); + return false; + } + + return lgUsbRedir_process(usbredir); +} + +void * lgUsbRedir_device(void * parserOpaque) +{ + LG_USBRedir * usbredir = parserOpaque; + return usbredir->deviceOpaque; +} diff --git a/client/src/usbredir.h b/client/src/usbredir.h new file mode 100644 index 00000000..b9f1ad4c --- /dev/null +++ b/client/src/usbredir.h @@ -0,0 +1,69 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * 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 2 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., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _H_LG_CLIENT_USBREDIR_ +#define _H_LG_CLIENT_USBREDIR_ + +#include + +#include +#include +#include + +struct usbredirparser; + +typedef struct LG_USBRedir LG_USBRedir; + +typedef struct LG_USBRedirDeviceOps +{ + /* Install the device packet callbacks on a newly-created parser. */ + void (*setup)(void * opaque, struct usbredirparser * parser); + + /* Queue the device descriptors and connection announcement. */ + void (*plug)(void * opaque, struct usbredirparser * parser); + + /* Stop all device activity. The bridge sends the disconnect packet. */ + void (*unplug)(void * opaque); +} +LG_USBRedirDeviceOps; + +LG_USBRedir * lgUsbRedir_create( + const LG_USBRedirDeviceOps * deviceOps, void * deviceOpaque); +/* The PureSpice session must be stopped before destroying the bridge. */ +void lgUsbRedir_destroy(LG_USBRedir * usbredir); + +/* This may be called from another thread. The request is applied by + * lgUsbRedir_process on the PureSpice processing thread. */ +void lgUsbRedir_setPlugged(LG_USBRedir * usbredir, bool plugged); +bool lgUsbRedir_available(const LG_USBRedir * usbredir); + +/* Apply pending device state and flush parser output. This must be called on + * the PureSpice processing thread. */ +bool lgUsbRedir_process(LG_USBRedir * usbredir); + +void lgUsbRedir_state(PSUSBRedirChannel * channel, + PSUSBRedirState state, void * opaque); +bool lgUsbRedir_data(PSUSBRedirChannel * channel, + const uint8_t * data, size_t size, void * opaque); + +/* Parser callbacks receive the bridge as their opaque value. */ +void * lgUsbRedir_device(void * parserOpaque); + +#endif