]> git.sesse.net Git - bmusb/commitdiff
Support card hotplugging (both add and remove).
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Fri, 22 Jul 2016 15:33:34 +0000 (17:33 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Fri, 22 Jul 2016 15:33:34 +0000 (17:33 +0200)
bmusb.cpp
bmusb.h

index ed7a1f5caa42874909bf13804210322ee34942b5..e89610e7a5f2d7cc28a34ed1a22f326a5221bca5 100644 (file)
--- a/bmusb.cpp
+++ b/bmusb.cpp
@@ -7,6 +7,7 @@
 #include <assert.h>
 #include <errno.h>
 #include <libusb.h>
+#include <unistd.h>
 #include <netinet/in.h>
 #include <sched.h>
 #include <stdint.h>
@@ -34,6 +35,7 @@
 using namespace std;
 using namespace std::placeholders;
 
+#define USB_VENDOR_BLACKMAGIC 0x1edb
 #define MIN_WIDTH 640
 #define HEADER_SIZE 44
 //#define HEADER_SIZE 0
@@ -42,6 +44,8 @@ using namespace std::placeholders;
 #define FRAME_SIZE (8 << 20)  // 8 MB.
 #define USB_VIDEO_TRANSFER_SIZE (128 << 10)  // 128 kB.
 
+card_connected_callback_t BMUSBCapture::card_connected_callback = nullptr;
+
 namespace {
 
 FILE *audiofp;
@@ -616,8 +620,9 @@ void decode_packs(const libusb_transfer *xfr,
 
 void BMUSBCapture::cb_xfr(struct libusb_transfer *xfr)
 {
-       if (xfr->status != LIBUSB_TRANSFER_COMPLETED) {
-               fprintf(stderr, "transfer status %d\n", xfr->status);
+       if (xfr->status != LIBUSB_TRANSFER_COMPLETED &&
+           xfr->status != LIBUSB_TRANSFER_NO_DEVICE) {
+               fprintf(stderr, "error: transfer status %d\n", xfr->status);
                libusb_free_transfer(xfr);
                exit(3);
        }
@@ -625,6 +630,18 @@ void BMUSBCapture::cb_xfr(struct libusb_transfer *xfr)
        assert(xfr->user_data != nullptr);
        BMUSBCapture *usb = static_cast<BMUSBCapture *>(xfr->user_data);
 
+       if (xfr->status == LIBUSB_TRANSFER_NO_DEVICE) {
+               if (!usb->disconnected) {
+                       fprintf(stderr, "Device went away, stopping transfers.\n");
+                       usb->disconnected = true;
+                       if (usb->card_disconnected_callback) {
+                               usb->card_disconnected_callback();
+                       }
+               }
+               // Don't reschedule the transfer; the loop will stop by itself.
+               return;
+       }
+
        if (xfr->type == LIBUSB_TRANSFER_TYPE_ISOCHRONOUS) {
                if (xfr->endpoint == 0x84) {
                        decode_packs(xfr, "DeckLinkAudioResyncT", 20, &usb->current_audio_frame, "audio", bind(&BMUSBCapture::start_new_audio_block, usb, _1));
@@ -682,6 +699,26 @@ void BMUSBCapture::cb_xfr(struct libusb_transfer *xfr)
        }
 }
 
+int BMUSBCapture::cb_hotplug(libusb_context *ctx, libusb_device *dev, libusb_hotplug_event event, void *user_data)
+{
+       if (card_connected_callback != nullptr) {
+               libusb_device_descriptor desc;
+                if (libusb_get_device_descriptor(dev, &desc) < 0) {
+                       fprintf(stderr, "Error getting device descriptor for hotplugged device %p, killing hotplug\n", dev);
+                       libusb_unref_device(dev);
+                       return 1;
+               }
+
+               if ((desc.idVendor == USB_VENDOR_BLACKMAGIC && desc.idProduct == 0xbd3b) ||
+                   (desc.idVendor == USB_VENDOR_BLACKMAGIC && desc.idProduct == 0xbd4f)) {
+                       card_connected_callback(dev);  // Callback takes ownership.
+                       return 0;
+               }
+       }
+       libusb_unref_device(dev);
+       return 0;
+}
+
 void BMUSBCapture::usb_thread_func()
 {
        sched_param param;
@@ -703,8 +740,30 @@ struct USBCardDevice {
        libusb_device *device;
 };
 
+const char *get_product_name(uint16_t product)
+{
+       if (product == 0xbd3b) {
+               return "Intensity Shuttle";
+       } else if (product == 0xbd4f) {
+               return "UltraStudio SDI";
+       } else {
+               assert(false);
+               return nullptr;
+       }
+}
+
+string get_card_description(int id, uint8_t bus, uint8_t port, uint16_t product)
+{
+       const char *product_name = get_product_name(product);
+
+       char buf[256];
+       snprintf(buf, sizeof(buf), "USB card %d: Bus %03u Device %03u  %s",
+               id, bus, port, product_name);
+       return buf;
+}
+
 libusb_device_handle *open_card(int card_index, string *description)
-{      
+{
        libusb_device **devices;
        ssize_t num_devices = libusb_get_device_list(nullptr, &devices);
        if (num_devices == -1) {
@@ -722,8 +781,8 @@ libusb_device_handle *open_card(int card_index, string *description)
                uint8_t bus = libusb_get_bus_number(devices[i]);
                uint8_t port = libusb_get_port_number(devices[i]);
 
-               if (!(desc.idVendor == 0x1edb && desc.idProduct == 0xbd3b) &&
-                   !(desc.idVendor == 0x1edb && desc.idProduct == 0xbd4f)) {
+               if (!(desc.idVendor == USB_VENDOR_BLACKMAGIC && desc.idProduct == 0xbd3b) &&
+                   !(desc.idVendor == USB_VENDOR_BLACKMAGIC && desc.idProduct == 0xbd4f)) {
                        libusb_unref_device(devices[i]);
                        continue;
                }
@@ -742,22 +801,11 @@ libusb_device_handle *open_card(int card_index, string *description)
        });
 
        for (size_t i = 0; i < found_cards.size(); ++i) {
-               const char *product_name = nullptr;
-               if (found_cards[i].product == 0xbd3b) {
-                       product_name = "Intensity Shuttle";
-               } else if (found_cards[i].product == 0xbd4f) {
-                       product_name = "UltraStudio SDI";
-               } else {
-                       assert(false);
-               }
-
-               char buf[256];
-               snprintf(buf, sizeof(buf), "USB card %d: Bus %03u Device %03u  %s",
-                       int(i), found_cards[i].bus, found_cards[i].port, product_name);
+               string tmp_description = get_card_description(i, found_cards[i].bus, found_cards[i].port, found_cards[i].product);
+               fprintf(stderr, "%s\n", tmp_description.c_str());
                if (i == size_t(card_index)) {
-                       *description = buf;
+                       *description = tmp_description;
                }
-               fprintf(stderr, "%s\n", buf);
        }
 
        if (size_t(card_index) >= found_cards.size()) {
@@ -779,6 +827,29 @@ libusb_device_handle *open_card(int card_index, string *description)
        return devh;
 }
 
+libusb_device_handle *open_card(unsigned card_index, libusb_device *dev, string *description)
+{
+       uint8_t bus = libusb_get_bus_number(dev);
+       uint8_t port = libusb_get_port_number(dev);
+
+       libusb_device_descriptor desc;
+       if (libusb_get_device_descriptor(dev, &desc) < 0) {
+               fprintf(stderr, "Error getting device descriptor for device %p\n", dev);
+               exit(1);
+       }
+
+       *description = get_card_description(card_index, bus, port, desc.idProduct);
+
+       libusb_device_handle *devh;
+       int rc = libusb_open(dev, &devh);
+       if (rc < 0) {
+               fprintf(stderr, "Error opening card %p: %s\n", dev, libusb_error_name(rc));
+               exit(1);
+       }
+
+       return devh;
+}
+
 void BMUSBCapture::configure_card()
 {
        if (video_frame_allocator == nullptr) {
@@ -801,7 +872,12 @@ void BMUSBCapture::configure_card()
                exit(1);
        }
 
-       devh = open_card(card_index, &description);
+       if (dev == nullptr) {
+               devh = open_card(card_index, &description);
+       } else {
+               devh = open_card(card_index, dev, &description);
+               libusb_unref_device(dev);
+       }
        if (!devh) {
                fprintf(stderr, "Error finding USB device\n");
                exit(1);
@@ -1117,6 +1193,18 @@ void BMUSBCapture::stop_dequeue_thread()
 
 void BMUSBCapture::start_bm_thread()
 {
+       // Devices leaving are discovered by seeing the isochronous packets
+       // coming back with errors, so only care about devices joining.
+       if (card_connected_callback != nullptr) {
+               if (libusb_hotplug_register_callback(
+                       nullptr, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED, LIBUSB_HOTPLUG_NO_FLAGS,
+                       USB_VENDOR_BLACKMAGIC, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY,
+                       &BMUSBCapture::cb_hotplug, nullptr, nullptr) < 0) {
+                       fprintf(stderr, "libusb_hotplug_register_callback() failed\n");
+                       exit(1);
+               }
+       }
+
        should_quit = false;
        usb_thread = thread(&BMUSBCapture::usb_thread_func);
 }
diff --git a/bmusb.h b/bmusb.h
index bf1f51fb60eb65b77a40d26da003787b88414ded..941c57c27d358ba22f28cb97268f71c475178f9c 100644 (file)
--- a/bmusb.h
+++ b/bmusb.h
@@ -1,6 +1,7 @@
 #ifndef _BMUSB_H
 #define _BMUSB_H
 
+#include <libusb.h>
 #include <stdint.h>
 #include <atomic>
 #include <condition_variable>
@@ -13,8 +14,7 @@
 #include <thread>
 #include <vector>
 
-struct libusb_device_handle;
-struct libusb_transfer;
+class BMUSBCapture;
 
 // An interface for frame allocators; if you do not specify one
 // (using set_video_frame_allocator), a default one that pre-allocates
@@ -111,6 +111,9 @@ typedef std::function<void(uint16_t timecode,
                            FrameAllocator::Frame audio_frame, size_t audio_offset, AudioFormat audio_format)>
        frame_callback_t;
 
+typedef std::function<void(libusb_device *dev)> card_connected_callback_t;
+typedef std::function<void()> card_disconnected_callback_t;
+
 class CaptureInterface {
  public:
        virtual ~CaptureInterface() {}
@@ -150,13 +153,17 @@ class CaptureInterface {
        virtual void start_bm_capture() = 0;
 
        virtual void stop_dequeue_thread() = 0;
+
+       // If a card is disconnected, it cannot come back; you should call stop_dequeue_thread()
+       // and delete it.
+       virtual bool get_disconnected() const = 0;
 };
 
 // The actual capturing class, representing capture from a single card.
 class BMUSBCapture : public CaptureInterface {
  public:
-       BMUSBCapture(int card_index)
-               : card_index(card_index)
+       BMUSBCapture(int card_index, libusb_device *dev = nullptr)
+               : card_index(card_index), dev(dev)
        {
        }
 
@@ -223,11 +230,34 @@ class BMUSBCapture : public CaptureInterface {
        void configure_card() override;
        void start_bm_capture() override;
        void stop_dequeue_thread() override;
+       bool get_disconnected() const override { return disconnected; }
 
        // TODO: It's rather messy to have these outside the interface.
        static void start_bm_thread();
        static void stop_bm_thread();
 
+       // Hotplug event (for devices being inserted between start_bm_thread()
+       // and stop_bm_thread()); entirely optional, but must be set before
+       // start_bm_capture(). Note that your callback should do as little work
+       // as possible, since the callback comes from the main USB handling
+       // thread, which is very time-sensitive.
+       //
+       // The callback function transfers ownership. If you don't want to hold
+       // on to the device given to you in the callback, you need to call
+       // libusb_unref_device().
+       static void set_card_connected_callback(card_connected_callback_t callback)
+       {
+               card_connected_callback = callback;
+       }
+
+       // Similar to set_card_connected_callback(), with the same caveats.
+       // (Note that this is set per-card and not global, as it is logically
+       // connected to an existing BMUSBCapture object.)
+       void set_card_disconnected_callback(card_disconnected_callback_t callback)
+       {
+               card_disconnected_callback = callback;
+       }
+
  private:
        struct QueuedFrame {
                uint16_t timecode;
@@ -243,6 +273,7 @@ class BMUSBCapture : public CaptureInterface {
 
        static void usb_thread_func();
        static void cb_xfr(struct libusb_transfer *xfr);
+       static int cb_hotplug(libusb_context *ctx, libusb_device *dev, libusb_hotplug_event event, void *user_data);
 
        void update_capture_mode();
 
@@ -261,6 +292,8 @@ class BMUSBCapture : public CaptureInterface {
        std::unique_ptr<FrameAllocator> owned_video_frame_allocator;
        std::unique_ptr<FrameAllocator> owned_audio_frame_allocator;
        frame_callback_t frame_callback = nullptr;
+       static card_connected_callback_t card_connected_callback;
+       card_disconnected_callback_t card_disconnected_callback = nullptr;
 
        std::thread dequeue_thread;
        std::atomic<bool> dequeue_thread_should_quit;
@@ -273,13 +306,20 @@ class BMUSBCapture : public CaptureInterface {
        static constexpr int NUM_BMUSB_REGISTERS = 60;
        uint8_t register_file[NUM_BMUSB_REGISTERS];
 
-       int card_index;
+       // If <dev> is nullptr, will choose device number <card_index> from the list
+       // of available devices on the system. <dev> is not used after configure_card()
+       // (it will be unref-ed).
+       int card_index = -1;
+       libusb_device *dev = nullptr;
+
        std::vector<libusb_transfer *> iso_xfrs;
        int assumed_frame_width = 1280;
 
        libusb_device_handle *devh = nullptr;
        uint32_t current_video_input = 0x00000000;  // HDMI/SDI.
        uint32_t current_audio_input = 0x00000000;  // Embedded.
+
+       bool disconnected = false;
 };
 
 // Get details for the given video format; returns false if detection was incomplete.