Repository: pupil-labs/pyuvc (language: python)

Issue:
Semantic Versioning - `v0.9.1` and not `v0.91` (?)
Shouldn't the version be `v0.9.1` and not `v0.91`? 

I have a Windows 10 wheel ready for release - but want to make sure that version number remain consistent and compliant with SemVer before making the release. 

The following would need to be changed if this is the case:
- https://github.com/pupil-labs/pyuvc/blob/master/setup.py#L65 
- https://github.com/pupil-labs/pyuvc/blob/master/uvc.pyx#L79

And assertion of version would need to be changed in Pupil source code if updated:
- https://github.com/pupil-labs/pupil/blob/950d8ba005cdda92689b2fd1ccd25696c5c6ccb6/pupil_src/shared_modules/video_capture/uvc_backend.py#L19
windows: 'libusb/libusb.h': No such file or directory
I am triing to install the pupil source code on windows according to.:
https://github.com/pupil-labs/pupil/wiki/Dependencies-Installation-Windows

while installing pyuvc using

$ git clone https://github.com/pupil-labs/pyuvc
$ python setup.py install

libusb seems to be missing (see below). libusb0.dll does exist in /windows/system32 though.

what am i missing?

thanks a lot for your time!

```
C:\Program Files\pyuvc>python setup.py install
running install
running build
running build_ext
building 'uvc' extension
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\x86_amd64\cl.exe /c /nologo /Ox /W3 /GL /DNDEBUG /MD -IC:\Users\admin\AppData\Local\Programs\Python\Python35\lib\site-packages\numpy\core\include -IC:\work\libjpeg-turbo-VC64\include -IC:\work\libusb -IC:\work\libusb\libusb -IC:\work\libuvc\include -IC:\work\libuvc\bin\include -IC:\Users\admin\AppData\Local\Programs\Python\Python35\include -IC:\Users\admin\AppData\Local\Programs\Python\Python35\include "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE" "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\ATLMFC\INCLUDE" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\NETFXSDK\4.6.1\include\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\\shared" "-IC:\Program Files (x86)\Windows Kits\8.1\include\\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\\winrt" "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\INCLUDE" "-IC:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\ATLMFC\INCLUDE" "-IC:\Program Files (x86)\Windows Kits\10\include\10.0.10240.0\ucrt" "-IC:\Program Files (x86)\Windows Kits\NETFXSDK\4.6.1\include\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\\shared" "-IC:\Program Files (x86)\Windows Kits\8.1\include\\um" "-IC:\Program Files (x86)\Windows Kits\8.1\include\\winrt" /Tcuvc.c /Fobuild\temp.win-amd64-3.5\Release\uvc.obj
uvc.c
uvc.c(462): fatal error C1083: Cannot open include file: 'libusb/libusb.h': No such file or directory
error: command 'C:\\Program Files (x86)\\Microsoft Visual Studio 14.0\\VC\\BIN\\x86_amd64\\cl.exe' failed with exit status 2
```
feedback ELP-USBFHD01M-L21
**ELP-USBFHD01M-L21**

This model is running with no bugs (until now), as an eye camera. I am having performance issues with my Intel® Core™ i3 CPU M 370 @ 2.40GHz × 4, 64 bits, and integrated GPU:

```
#00:02.0 VGA compatible controller: Intel Corporation Core Processor Integrated Graphics Controller
rafael@B14HM21:~$ lspci -v -s 00:02.0
00:02.0 VGA compatible controller: Intel Corporation Core Processor Integrated Graphics Controller (rev 18) (prog-if 00 [VGA controller])
    Subsystem: Holco Enterprise Co, Ltd/Shuttle Computer Device 2013
    Flags: bus master, fast devsel, latency 0, IRQ 44
    Memory at fe000000 (64-bit, non-prefetchable) [size=4M]
    Memory at d0000000 (64-bit, prefetchable) [size=256M]
    I/O ports at f080 [size=8]
    Expansion ROM at <unassigned> [disabled]
    Capabilities: [90] MSI: Enable+ Count=1/1 Maskable- 64bit-
    Capabilities: [d0] Power Management version 2
    Capabilities: [a4] PCI Advanced Features
    Kernel driver in use: i915
```

![screenshot from 2015-12-07 13 36 45](https://cloud.githubusercontent.com/assets/6511849/11639553/3fe3b3fc-9d0c-11e5-92b4-1d4163c5401f.png)
![screenshot from 2015-12-07 18 36 53](https://cloud.githubusercontent.com/assets/6511849/11640553/e000fff2-9d11-11e5-8be8-b10446d3292a.png)



Discussion / hints:
thanks fixed.
The required information was found here.:
https://github.com/pupil-labs/pyuvc/blob/master/WINDOWS_DEVELOPER.md

The
`There are some further python extensions that are specific to pupil:

    PyAV
    pyglui
    pyndsi
    pyuvc
`
section of 
https://github.com/pupil-labs/pupil/wiki/Dependencies-Installation-Windows
should be elaborated. Information is not available in a step by step manner.
We have updated windows development instructions for Pupil with Python36 and VS 2017 preview. I have verified that these instructions work on a clean machine (VM). Please see: https://docs.pupil-labs.com/master/#windows-dependencies

Please take a look at the updated instructions. 
```
Brand Name:    ELP
IR Distance(m):    0
Power Supply(V):    5V
Power Consumption(W):    DC 5V/ current: 150mA
Sensor:    CMOS
Ships From:    Shenzhen
Dimensions (L x W x D)(mm):    32*32/38*38mm
Viewing Angle (Degree):    150degree
Connectivity:    Closed System/CCTV Wired
Supported Mobile Systems:    Android
Supported Operating Systems:    Windows 98,Windows XP,Windows Vista,Windows 7,Mac os,Windows 8
Video Compression Format:    MJPEG
High Definition:    1080P(Full-HD)
Style:    Mini Camera
Model Number:    ELP-USBFHD01M-L21
Technology:    usb camera with uvc
Special Features:    High frame rate 120fps at 480P, 60fps at 720P,30fps at 1080P
Type:    Usb camera module
Lens (mm):    2.1mm lens
Sensor:    1/2.7" CMOS OV2710
Resolution:    1920X1080P, 2megapixel
Picture format::    MJPEG&YUY2
USB Protocal:    USB2.0 HS/FS, USB1.1 FS
Lens:    2.1mm lens(3.6/2.8/6/8/12mm lens optional)
Night vision::    optional, can choose IR board
Voltage:    DC 5V/ current: 150mA
Work temperature:    DEGREES(-20~70)
Size:    32x32mm/38*38mm
is_customized:    Yes
```

I want to run this camera as an eye camera in Pupil Capture. So, do I need to install IR Led and exposed film on this camera?
@sonny07 yes, you need to install the IR Led on it. For that matter, it has some advantages for low cost DIY. The board has a ready to use 5v output (circular positive and negative holes). You don't need SMD tools for the job.

Fix patch:
diff --git a/.bumpversion.cfg b/.bumpversion.cfg
deleted file mode 100644
index 1fdd581..0000000
--- a/.bumpversion.cfg
+++ /dev/null
@@ -1,8 +0,0 @@
-[bumpversion]
-current_version = 0.15.0
-commit = True
-tag = True
-
-[bumpversion:file:setup.py]
-
-[bumpversion:file:uvc.pyx]
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..01164f6
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,8 @@
+[run]
+omit =
+	# leading `*/` for pytest-dev/pytest-cov#456
+	*/.tox/*
+	*/pep517-build-env-*
+
+[report]
+show_missing = True
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..304196f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,19 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = tab
+indent_size = 4
+insert_final_newline = true
+end_of_line = lf
+
+[*.py]
+indent_style = space
+max_line_length = 88
+
+[*.{yml,yaml}]
+indent_style = space
+indent_size = 2
+
+[*.rst]
+indent_style = space
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..52378bc
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,16 @@
+[flake8]
+max-line-length = 88
+
+max-complexity = 18
+
+extend-ignore =
+	# Black creates whitespace before colon
+	E203
+	W503
+	F541
+
+extend-select = B,C,E,F,W,T4,B9
+exclude =
+    .venv
+    .pyenv
+    __pycache__
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..89ff339
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
+version: 2
+updates:
+  - package-ecosystem: "pip"
+    directory: "/"
+    schedule:
+      interval: "daily"
+    allow:
+      - dependency-type: "all"
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..a6515d1
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,63 @@
+name: tests
+
+on:
+  pull_request:
+  push:
+    tags:
+      - "**"
+  workflow_dispatch:
+
+jobs:
+  build_wheels:
+    name: "Build ${{ matrix.CIBW_BUILD }} wheels on ${{ matrix.os }}"
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest, windows-latest]
+        CIBW_BUILD: ["*"]
+        exclude:
+          - os: ubuntu-latest
+            CIBW_BUILD: "*"
+        include:
+          - os: ubuntu-latest
+            CIBW_BUILD: "*manylinux*"
+          # Disable musllinux
+          # - os: ubuntu-latest
+          #   CIBW_BUILD: "*musllinux*"
+    continue-on-error: true
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v3
+        with:
+          submodules: true
+      - name: Build ${{ matrix.CIBW_BUILD }} wheels on ${{ matrix.os }}
+        uses: pypa/cibuildwheel@v2.9.0
+        env:
+          CIBW_BUILD: ${{ matrix.CIBW_BUILD }}
+
+      - uses: actions/upload-artifact@v3
+        if: always()
+        with:
+          name: distribution
+          path: ./wheelhouse/*.whl
+
+  release:
+    needs: [build_wheels]
+    if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+      - uses: actions/download-artifact@v3
+        with:
+          name: distribution
+          path: dist/
+      - name: Setup Python
+        uses: actions/setup-python@v4
+      - name: Install tox
+        run: |
+          python -m pip install tox
+      - name: Release
+        run: tox -e release
+        env:
+          TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
index dab2dd5..fd03188 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,30 +1,193 @@
+version.py
+
+# Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,macos,windows,linux,pycharm
+# Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,macos,windows,linux,pycharm
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### PyCharm ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn.  Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### PyCharm Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+# Sonarlint plugin
+# https://plugins.jetbrains.com/plugin/7973-sonarlint
+.idea/**/sonarlint/
+
+# SonarQube Plugin
+# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
+.idea/**/sonarIssues.xml
+
+# Markdown Navigator plugin
+# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
+.idea/**/markdown-navigator.xml
+.idea/**/markdown-navigator-enh.xml
+.idea/**/markdown-navigator/
+
+# Cache file creation bug
+# See https://youtrack.jetbrains.com/issue/JBR-2257
+.idea/$CACHE_FILE$
+
+# CodeStream plugin
+# https://plugins.jetbrains.com/plugin/12206-codestream
+.idea/codestream.xml
+
+### Python ###
 # Byte-compiled / optimized / DLL files
 __pycache__/
 *.py[cod]
-
-# C source
-*.c
+*$py.class
 
 # C extensions
 *.so
 
-
 # Distribution / packaging
 .Python
-env/
-bin/
 build/
 develop-eggs/
 dist/
+downloads/
 eggs/
+.eggs/
 lib/
 lib64/
 parts/
 sdist/
 var/
+wheels/
+share/python-wheels/
 *.egg-info/
 .installed.cfg
 *.egg
+MANIFEST
+MANIFEST.in
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
 
 # Installer logs
 pip-log.txt
@@ -33,26 +196,149 @@ pip-delete-this-directory.txt
 # Unit test / coverage reports
 htmlcov/
 .tox/
+.nox/
 .coverage
+.coverage.*
 .cache
 nosetests.xml
 coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
 
 # Translations
 *.mo
-
-# Mr Developer
-.mr.developer.cfg
-.project
-.pydevproject
-
-# Rope
-.ropeproject
+*.pot
 
 # Django stuff:
 *.log
-*.pot
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
 
 # Sphinx documentation
 docs/_build/
 
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv*
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+### VisualStudioCode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
+.ionide
+
+# Support for Project snippet scope
+!.vscode/*.code-snippets
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,macos,windows,linux,pycharm
+
+_skbuild/
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..0eab007
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,7 @@
+[submodule "libuvc-source"]
+	path = libuvc-source
+	url = https://github.com/pupil-labs/libuvc.git
+[submodule "libusb-source"]
+	path = libusb-source
+	url = https://github.com/pupil-labs/libusb.git
+	branch = pl-fixes
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..fe500dc
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,41 @@
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v4.3.0
+    hooks:
+      - id: check-case-conflict
+      - id: check-merge-conflict
+      - id: check-yaml
+      - id: check-toml
+      - id: debug-statements
+      - id: end-of-file-fixer
+      - id: mixed-line-ending
+      - id: trailing-whitespace
+
+  - repo: https://github.com/asottile/pyupgrade
+    rev: v2.37.3
+    hooks:
+      - id: pyupgrade
+        name: PyUpgrade 3.6+
+        args: ["--py36-plus"]
+        exclude: ^bin/
+
+  - repo: https://github.com/pycqa/flake8
+    rev: 5.0.4
+    hooks:
+      - id: flake8
+
+  - repo: https://github.com/PyCQA/isort
+    rev: 5.10.1
+    hooks:
+      - id: isort
+        args: [--profile, black]
+
+  - repo: https://github.com/psf/black
+    rev: 22.6.0
+    hooks:
+      - id: black
+
+  - repo: https://github.com/asottile/setup-cfg-fmt
+    rev: v2.0.0
+    hooks:
+      - id: setup-cfg-fmt
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 0000000..cc69854
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,6 @@
+version: 2
+python:
+  install:
+  - path: .
+    extra_requirements:
+      - docs
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..55ab0d5
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+    "cmake.sourceDirectory": "${workspaceFolder}/pyuvc-source",
+}
\ No newline at end of file
diff --git a/CHANGES.rst b/CHANGES.rst
new file mode 100644
index 0000000..e69de29
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 9f122b8..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1,1 +0,0 @@
-recursive-include . *.pyx *.pxd *.pxi *.h *.c *.cpp
diff --git a/README.md b/README.md
deleted file mode 100644
index 6bbcf6a..0000000
--- a/README.md
+++ /dev/null
@@ -1,92 +0,0 @@
-pyuvc
-=======
-
-Python bindings for the Pupil Labs fork of [libuvc](https://github.com/pupil-labs/libuvc) with super fast jpeg decompression using [libjpegturbo](http://libjpeg-turbo.virtualgl.org/) (utilizing the tubojpeg api).
-
-* cross platform access to UVC capture devices.
-* Full access to all uvc settings (Zoom,Focus,Brightness,etc.)
-* Full access to all stream and format parameters (rates,sizes,etc.)
-* Enumerate all capture devices with device_list()
-* Capture instance will always grab mjpeg conpressed frames from cameras.
-
-Image data is returned as `Frame` object. This object will decompress and convert on the fly when image data is requested.
-This gives the user the full flexiblity: Grab just the jpeg buffers or have them converted to YUV or Gray or RGB and only when you need.
-
-The `Frame` class has caching build in to avoid double decompression or conversion.
-
-
-# Example
-See `example.py` for an code example.
-
-# Dependencies Linux
-
-### libuvc
-```
-git clone https://github.com/pupil-labs/libuvc
-cd libuvc
-mkdir build
-cd build
-cmake ..
-make && sudo make install
-sudo ldconfig
-```
-
-### libjpeg-turbo
-```
-wget -O libjpeg-turbo.tar.gz https://sourceforge.net/projects/libjpeg-turbo/files/1.5.1/libjpeg-turbo-1.5.1.tar.gz/download
-tar xvzf libjpeg-turbo.tar.gz
-cd libjpeg-turbo-1.5.1
-./configure --with-pic --prefix=/usr/local
-sudo make install
-sudo ldconfig
-```
-
-### cython
-```
-sudo pip install cython
-```
-
-### udev rules for running as normal user:
-```
-echo 'SUBSYSTEM=="usb",  ENV{DEVTYPE}=="usb_device", GROUP="plugdev", MODE="0664"' | sudo tee /etc/udev/rules.d/10-libuvc.rules > /dev/null
-sudo udevadm trigger
-```
-
-# Dependencies Mac
-
-### libuvc
-```
-git clone https://github.com/pupil-labs/libuvc
-cd libuvc
-mkdir build
-cd build
-cmake ..
-make && sudo make install
-```
-
-### libjpeg-turbo
-
-```
-brew install libjpeg-turbo
-```
-
-### cython,numpy
-```
-pip install cython
-pip install numpy
-```
-
-## just build locally
-```
-python setup.py build_ext -i
-```
-
-## or install system wide
-```
-python setup.py install
-```
-
-# WINDOWS
-
-Please have a look at WINDOWS_USER.md for install instructions if you want to use PYUVC.
-Please have a look at WINDOWS_DEVELOER.md for install instructions if you want to modify PYUVC.
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..f959171
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,132 @@
+.. image:: https://img.shields.io/pypi/v/pupil-labs-uvc.svg
+   :target: `PyPI link`_
+
+.. image:: https://img.shields.io/pypi/pyversions/pupil-labs-uvc.svg
+   :target: `PyPI link`_
+
+.. _PyPI link: https://pypi.org/project/pupil-labs-uvc
+
+.. image:: https://github.com/pupil-labs/pyuvc/actions/workflows/main.yml/badge.svg
+   :target: https://github.com/pupil-labs/pyuvc/actions?query=workflow%3A%22tests%22
+   :alt: tests
+
+.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
+   :target: https://github.com/psf/black
+   :alt: Code style: Black
+
+.. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest
+..    :target: https://skeleton.readthedocs.io/en/latest/?badge=latest
+
+.. image:: https://img.shields.io/badge/skeleton-2022-informational
+   :target: https://blog.jaraco.com/skeleton
+
+*****
+pyuvc
+*****
+
+Python bindings for the Pupil Labs fork of `libuvc <https://github.com/pupil-labs/libuvc>`__
+with super fast jpeg decompression using `libjpegturbo <http://libjpeg-turbo.virtualgl.org/>`__
+(utilizing the tubojpeg api).
+
+* cross platform access to UVC capture devices.
+* Full access to all uvc settings (Zoom,Focus,Brightness,etc.)
+* Full access to all stream and format parameters (rates,sizes,etc.)
+* Enumerate all capture devices with device_list()
+* Capture instance will always grab mjpeg conpressed frames from cameras.
+
+Image data is returned as `Frame` object. This object will decompress and convert on the
+fly when image data is requested. This gives the user the full flexiblity: Grab just the
+jpeg buffers or have them converted to YUV or Gray or RGB and only when you need.
+
+The `Frame` class has caching build in to avoid double decompression or conversion.
+
+Install via PyPI
+################
+
+.. code-block::
+
+   pip install pupil-labs-uvc
+
+Example
+#######
+
+See `examples/` for code examples.
+
+Install from source
+###################
+
+pyuvc requires the following dependencies:
+
+- `libusb <https://github.com/libusb/libusb/>`__
+- `libturbo-jpeg <https://libjpeg-turbo.org/>`__
+- `POSIX Threads for Windows <https://sourceforge.net/projects/pthreads4w/>`__ (Windows
+  only, supplied via the `pupil-pthreads-win <https://pypi.org/project/pupil-pthreads-win/>`__
+  Python module)
+
+Once the dependencies are installed, you can pip install the source tree::
+
+   git clone https://github.com/pupil-labs/pyuvc --recursive
+   export FORCE_LOCAL_LIBUVC_BUILD=ON
+   pip install ./pyuvc
+
+Linux
+*****
+
+Ubuntu/Debian::
+
+   apt-get update -y
+   apt-get install -y libusb-1.0-0-dev libturbojpeg-dev
+
+Running as a non-root user
+==========================
+
+One needs to setup udev rules and add the target user to the ``plugdev`` group to avoid
+the privileged access requirement.
+
+.. code-block:: bash
+
+   echo 'SUBSYSTEM=="usb",  ENV{DEVTYPE}=="usb_device", GROUP="plugdev", MODE="0664"' | sudo tee /etc/udev/rules.d/10-libuvc.rules > /dev/null
+   sudo udevadm trigger
+   sudo usermod -a -G plugdev $USER
+   # logout and back in
+
+macOS
+*****
+
+Homebrew::
+
+   brew update
+   brew install libusb jpeg-turbo
+
+Running as a non-root user
+==========================
+
+Unfortunately, this is currently not possible. See
+`this libusb issue thread <https://github.com/libusb/libusb/issues/1014>`__ for details.
+
+WINDOWS
+*******
+
+Run the following code in a powershell to install the dependencies (requires
+`7z <https://www.7-zip.org/>`__ to be installed)
+
+.. code-block:: powershell
+
+   pip install build delvewheel
+   git clone https://github.com/pupil-labs/pyuvc --recursive
+   cd pyuvc
+   scripts/download-deps-win.ps1 -DEPS_TMP_PATH tmp
+   $Env:DEPS_PATHS_LOC = "tmp/dep_paths.json"
+   python -m build -w   # will create a wheel in dist/ folder; insert the wheel path below
+   python scripts/repair-wheels-win.py $Env:DEPS_PATHS_LOC <wheel location> wheelhouse
+   pip install wheelhouse/<wheel name>
+
+
+Manual driver installation
+==========================
+
+pyuvc requires the libUSBk driver to be installed for your corresponding camera.
+Otherwise, metadata like the product name will be set to ``"unknown"``.
+
+Please see `these instructions <https://github.com/pupil-labs/pyuvc/blob/master/WINDOWS_USER.md>`__
+on how to manually install libUSBk drivers for your specific camera.
diff --git a/WINDOWS_DEVELOPER.md b/WINDOWS_DEVELOPER.md
deleted file mode 100644
index 0ac1a42..0000000
--- a/WINDOWS_DEVELOPER.md
+++ /dev/null
@@ -1,24 +0,0 @@
-# Windows Installation Instructions for developers
-
-## Prerequisites
-
-1. [Libusb with isochronous support] (http://github.com/pupil-labs/libusb)
-2. [Libuvc] (http://github.com/pupil-labs/libuvc)
-3. python 3 64-bit
-4. MSVS 2015
-5. numpy 
-6. cython
-7. wheel (make sure pip version is latest)
-
-
-## Installation
-
-1. Install turbojpeg VC 64 version: http://netassist.dl.sourceforge.net/project/libjpeg-turbo/1.5.1/libjpeg-turbo-1.5.1-vc64.exe
-Make sure <jpeg_install_dir>\bin directory is added to the system path
-2. Open setup.py and locate the code block branching from "elif platform.system() == 'Windows':". Edit the uvc_dir , tj_dir, and usb_dir 
-top level directory locations, corresponding to the installation locations  of libuvc, libturbojpeg and libusb. If you built luibuvc with
-binaries directory name different from "bin", and build type different from "Release" , you need to update  "uvc_lib" and "include_dirs" as
-well. 
-3. Open a new command prompt (to make sure PATH var is loaded)
-4. If you wish to install directly run "python setup.py install"
-5. If you'd like to create a wheel, run "pip wheel ."
diff --git a/WINDOWS_USER.md b/WINDOWS_USER.md
index 1e9ab7e..20e8257 100644
--- a/WINDOWS_USER.md
+++ b/WINDOWS_USER.md
@@ -1,8 +1,7 @@
-## How to prepare your system for uvc on Windows (8 and later)
+# Manual lisbUSBk driver installation
 
 PYUVC will only work with Python3.5+ 64bit!
 
-
 1. Download and install [libusbk 3.0.7.0] (https://sourceforge.net/projects/libusbk/files/libusbK-release/3.0.7.0/libusbK-3.0.7.0-setup.exe/download)
 
 2. Download and install [Zadig] (https://github.com/pbatard/libwdi/releases/download/v1.2.5/zadig-2.2.exe)
@@ -16,7 +15,3 @@ PYUVC will only work with Python3.5+ 64bit!
 6. Set the driver replacement to libusbK (v3.0.7.0) and click "Replace driver"
 
 7. Verify in device manager it is correctly replaced. Should be listed under a new category libusbK USB Devices
-
-8. download the latest wheel file from: https://github.com/pupil-labs/pyuvc/releases and do `pip install --upgrade <name of latest wheel file here>`
-
-At this point the uvc extension should be able to locate and stream from your web camera.
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..d0a8c59
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+
+extensions = ["sphinx.ext.autodoc", "jaraco.packaging.sphinx", "rst.linker"]
+
+master_doc = "index"
+
+link_files = {
+    "../CHANGES.rst": dict(
+        using=dict(GH="https://github.com"),
+        replace=[
+            dict(
+                pattern=r"(Issue #|\B#)(?P<issue>\d+)",
+                url="{package_url}/issues/{issue}",
+            ),
+            dict(
+                pattern=r"(?m:^((?P<scm_version>v?\d+(\.\d+){1,2}))\n[-=]+\n)",
+                with_scm="{text}\n{rev[timestamp]:%d %b %Y}\n",
+            ),
+            dict(
+                pattern=r"PEP[- ](?P<pep_number>\d+)",
+                url="https://peps.python.org/pep-{pep_number:0>4}/",
+            ),
+        ],
+    )
+}
+
+# Be strict about any broken references:
+nitpicky = True
+
+# Include Python intersphinx mapping to prevent failures
+# jaraco/skeleton#51
+extensions += ["sphinx.ext.intersphinx"]
+intersphinx_mapping = {
+    "python": ("https://docs.python.org/3", None),
+}
+
+html_theme = "furo"
diff --git a/docs/history.rst b/docs/history.rst
new file mode 100644
index 0000000..8e21750
--- /dev/null
+++ b/docs/history.rst
@@ -0,0 +1,8 @@
+:tocdepth: 2
+
+.. _changes:
+
+History
+*******
+
+.. include:: ../CHANGES (links).rst
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..3976af9
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,24 @@
+Welcome to |project| documentation!
+===================================
+
+.. include:: ../README.rst
+   :start-line: 26
+
+.. toctree::
+   :maxdepth: 1
+
+   history
+
+
+.. automodule:: uvc.uvc_bindings
+    :members:
+    :undoc-members:
+    :show-inheritance:
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/example.py b/example.py
deleted file mode 100644
index 053e937..0000000
--- a/example.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from __future__ import print_function
-import uvc
-import logging
-
-logging.basicConfig(level=logging.INFO)
-
-dev_list = uvc.device_list()
-print(dev_list)
-cap = uvc.Capture(dev_list[0]["uid"])
-
-# Uncomment the following lines to configure the Pupil 200Hz IR cameras:
-# controls_dict = dict([(c.display_name, c) for c in cap.controls])
-# controls_dict['Auto Exposure Mode'].value = 1
-# controls_dict['Gamma'].value = 200
-
-print(cap.avaible_modes)
-for x in range(10):
-    print(x)
-    cap.frame_mode = (640, 480, 30)
-    for x in range(100):
-        frame = cap.get_frame_robust()
-        print(frame.img.shape)
-        # cv2.imshow("img",frame.gray)
-        # cv2.waitKey(1)
-cap = None
diff --git a/examples/access_100_frames.py b/examples/access_100_frames.py
new file mode 100644
index 0000000..a160205
--- /dev/null
+++ b/examples/access_100_frames.py
@@ -0,0 +1,51 @@
+#!python3
+
+import logging
+
+from rich import print
+from rich.logging import RichHandler
+
+
+def main():
+    import uvc
+
+    for device in uvc.device_list():
+
+        cap = uvc.Capture(device["uid"])
+
+        for mode in cap.available_modes:
+            print(f"{cap.name} running at {mode}")
+            try:
+                cap.frame_mode = mode
+            except uvc.InitError as err:
+                print(f"{cap.name} mode selection - {err}")
+                continue
+            try:
+                for x in range(10):
+                    frame = cap.get_frame_robust()
+                    print("frame gray mean", frame.gray.mean())
+                # print(frame.img.mean())
+            except uvc.InitError as err:
+                print(f"{cap.name} getting frames - {err}")
+
+        cap.close()
+
+    # Uncomment the following lines to configure the Pupil 200Hz IR cameras:
+    # controls_dict = dict([(c.display_name, c) for c in cap.controls])
+    # controls_dict['Auto Exposure Mode'].value = 1
+    # controls_dict['Gamma'].value = 200
+
+    # cap.frame_mode = cap.avaible_modes[0]
+    # for x in range(100):
+    #     frame = cap.get_frame_robust()
+    #     print(frame.img.mean())
+    # cap = None
+
+
+if __name__ == "__main__":
+    logging.basicConfig(
+        level=logging.NOTSET,
+        handlers=[RichHandler(level="DEBUG")],
+        format="%(message)s",
+        datefmt="[%X]",
+    )
diff --git a/examples/print_camera_controls.py b/examples/print_camera_controls.py
new file mode 100644
index 0000000..5b9a99b
--- /dev/null
+++ b/examples/print_camera_controls.py
@@ -0,0 +1,48 @@
+import logging
+
+from rich import print
+from rich.logging import RichHandler
+
+
+def main():
+    import uvc
+
+    devices = uvc.device_list()
+    print("Available devices", devices)
+
+    for device in devices:
+
+        try:
+            cap = uvc.Capture(device["uid"])
+        except uvc.DeviceNotFoundError:
+            continue
+
+        print(f"{cap.name}")
+
+        print("Available modes:")
+        for mode in cap.available_modes:
+            print(
+                f"MODE: {mode.width} x {mode.height} @ {mode.fps} ({mode.format_name})"
+            )
+
+        print("Iterating over frame sizes and rates")
+        for res in cap.frame_sizes:
+            cap.frame_size = res
+            for rate in cap.frame_rates:
+                cap.frame_rate = rate
+                print(f"RES/RATE: {res[0]} x {res[1]} @ {rate} Hz")
+
+        cap.close()
+
+
+if __name__ == "__main__":
+    # import os
+    # os.environ["LIBUSB_DEBUG"] = "0"
+
+    logging.basicConfig(
+        level=logging.NOTSET,
+        handlers=[RichHandler(level="WARNING")],
+        format="%(message)s",
+        datefmt="[%X]",
+    )
+    main()
diff --git a/examples/vis_two_cameras.py b/examples/vis_two_cameras.py
new file mode 100644
index 0000000..bb0ad8a
--- /dev/null
+++ b/examples/vis_two_cameras.py
@@ -0,0 +1,111 @@
+import logging
+import os
+import time
+from typing import Iterable, NamedTuple, Optional
+
+import cv2
+import uvc.uvc_bindings as uvc
+from rich.logging import RichHandler
+from rich.traceback import install as install_rich_traceback
+
+
+class CameraSpec(NamedTuple):
+    name: str
+    width: int
+    height: int
+    fps: int
+    bandwidth_factor: float = 2.0
+
+
+def main(camera_specs: Iterable[CameraSpec]):
+    devices = uvc.device_list()
+    cameras = {spec: init_camera_from_list(devices, spec) for spec in camera_specs}
+    if not all(cameras.values()):
+        raise RuntimeError(
+            "Could not initialize all specified cameras. Available: "
+            f"{[dev['name'] for dev in devices]}"
+        )
+
+    try:
+        keep_running = True
+        last_update = time.perf_counter()
+
+        while keep_running:
+            for spec, cam in cameras.items():
+                try:
+                    frame = cam.get_frame(timeout=0.001)
+                except TimeoutError:
+                    pass
+                    # keep_running = False
+                    # break
+                except uvc.InitError as err:
+                    logging.debug(f"Failed to init {spec}: {err}")
+                    keep_running = False
+                    break
+                except uvc.StreamError as err:
+                    logging.debug(f"Failed to get a frame for {spec}: {err}")
+                else:
+                    data = frame.bgr if hasattr(frame, "bgr") else frame.gray
+                    if frame.data_fully_received:
+                        cv2.imshow(spec.name, data)
+
+            if (time.perf_counter() - last_update) > 1 / 60:
+                if cv2.waitKey(1) & 0xFF == 27:
+                    break
+                last_update = time.perf_counter()
+
+    except KeyboardInterrupt:
+        pass
+
+    for cam in cameras.values():
+        cam.close()
+
+
+def init_camera_from_list(devices, camera: CameraSpec) -> Optional[uvc.Capture]:
+    logging.debug(f"Searching {camera}...")
+    for device in devices:
+        if device["name"] == camera.name:
+            logging.debug(f"Found match by name")
+            capture = uvc.Capture(device["uid"])
+            capture.bandwidth_factor = camera.bandwidth_factor
+            for mode in capture.available_modes:
+                if mode[:3] == camera[1:4]:  # compare width, height, fps
+                    capture.frame_mode = mode
+                    return capture
+            else:
+                logging.warning(
+                    f"None of the available modes matched: {capture.available_modes}"
+                )
+            capture.close()
+    else:
+        logging.warning(f"No matching camera with name {camera.name!r} found")
+
+
+if __name__ == "__main__":
+    os.environ["LIBUSB_DEBUG"] = "3"
+    install_rich_traceback()
+    logging.basicConfig(
+        level=logging.NOTSET,
+        handlers=[RichHandler(level="DEBUG")],
+        format="%(message)s",
+        datefmt="[%X]",
+    )
+    # logging.getLogger("uvc").setLevel("INFO")
+    main(
+        [
+            CameraSpec(
+                name=os.environ["CAM1"],
+                width=1600,
+                height=1200,
+                fps=30,
+                bandwidth_factor=1.6,
+            ),
+            CameraSpec(
+                name=os.environ["CAM2"],
+                width=384,
+                height=192,
+                fps=200,
+                bandwidth_factor=0,
+            ),
+        ]
+    )
diff --git a/libusb-source b/libusb-source
new file mode 160000
index 0000000..ebfec61
--- /dev/null
+++ b/libusb-source
@@ -0,0 +1,1 @@
+Subproject commit ebfec61f0a283a7b2b4844b59b9029c9b9e1563d
diff --git a/libuvc-source b/libuvc-source
new file mode 160000
index 0000000..b7181e9
--- /dev/null
+++ b/libuvc-source
@@ -0,0 +1,1 @@
+Subproject commit b7181e9625fb19eae80b54818537d2fd0a9090d3
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 0000000..976ba02
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,2 @@
+[mypy]
+ignore_missing_imports = True
diff --git a/pyproject.toml b/pyproject.toml
index 8f9f7ea..6bf0433 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,3 +1,50 @@
 [build-system]
-requires = ["setuptools", "wheel", "Cython", "numpy", "pkgconfig"]
+requires = [
+    "setuptools",
+    "setuptools_scm[toml]>=3.4.1",
+    "wheel",
+    "Cython",
+    "numpy",
+    "scikit-build",
+    "cmake",
+    "ninja;platform_system != \"Windows\"",
+    "pupil_pthreads_win; platform_system == \"Windows\"",
+]
 build-backend = "setuptools.build_meta"
+
+[tool.setuptools_scm]
+
+[tool.cibuildwheel]
+archs = ["native"]
+skip = "{cp,pp}27-* {cp,pp}35-* pp*"
+
+build-frontend = "build"
+
+test-command = "pytest {package}/tests -v"
+before-test = ""
+test-requires = ["pytest"]
+test-extras = []
+
+manylinux-x86_64-image = "manylinux2014"
+
+[tool.cibuildwheel.windows]
+environment = { DEPS_PATHS_LOC = "tmp/dep_paths.json" }
+before-all = "powershell scripts/download-deps-win.ps1 -DEPS_TMP_PATH tmp"
+before-build = "pip install delvewheel pupil-pthreads-win"
+repair-wheel-command = "python scripts/repair-wheels-win.py tmp/dep_paths.json {wheel} {dest_dir}"
+
+[tool.cibuildwheel.macos]
+before-all = ["brew install automake", "{project}/scripts/build-deps-unix.sh"]
+
+[tool.cibuildwheel.linux]
+before-all = [
+    "yum install -y libudev-devel",
+    "{project}/scripts/build-deps-manylinux.sh",
+]
+
+[[tool.cibuildwheel.overrides]]
+select = "*-musllinux*"
+before-all = [
+    "apk add eudev-dev nasm",
+    "{project}/scripts/build-deps-manylinux.sh",
+]
diff --git a/pyuvc-source/CMakeLists.txt b/pyuvc-source/CMakeLists.txt
new file mode 100644
index 0000000..45a7117
--- /dev/null
+++ b/pyuvc-source/CMakeLists.txt
@@ -0,0 +1,113 @@
+cmake_minimum_required(VERSION 3.24)
+project(pyuvc
+    LANGUAGES C
+    DESCRIPTION "Python bindings for the Pupil Labs libuvc library"
+    HOMEPAGE_URL "https://github.com/pupil-labs/pyuvc"
+)
+cmake_policy(SET CMP0054 NEW)
+
+option(FORCE_LOCAL_LIBUVC_BUILD "Build local libuvc, even if an installed version was found" OFF)
+message(STATUS "FORCE_LOCAL_LIBUVC_BUILD=${FORCE_LOCAL_LIBUVC_BUILD}")
+
+if(NOT FORCE_LOCAL_LIBUVC_BUILD)
+    message(STATUS "Looking for libuvc...")
+    find_package(libuvc)
+endif()
+
+if(NOT libuvc_FOUND)
+    option(UVC_DEBUGGING "Enable UVC debug logs" OFF)
+    message(STATUS "UVC_DEBUGGING=${UVC_DEBUGGING}")
+
+    if(${UVC_DEBUGGING} AND UNIX)
+        SET(CMAKE_C_FLAGS "-g  -DUVC_DEBUGGING")
+    endif()
+
+    add_subdirectory(../libuvc-source skbuild-libuvc)
+
+    if(APPLE)
+        set(CMAKE_INSTALL_RPATH @loader_path/${CMAKE_INSTALL_LIBDIR})
+    elseif(UNIX)
+        set(CMAKE_INSTALL_RPATH $ORIGIN/${CMAKE_INSTALL_LIBDIR})
+    endif()
+elseif(UNIX)
+    message(STATUS "Setting RPATH to ${libuvc_LIBRARY_DIRS}")
+    set(CMAKE_INSTALL_RPATH ${libuvc_LIBRARY_DIRS})
+endif()
+
+# find skbuild cmake packages
+find_package(PythonExtensions REQUIRED)
+find_package(Cython REQUIRED)
+
+# find external cmake packages
+find_package(NumPy REQUIRED)
+
+if(UNIX)
+    find_package(PkgConfig)
+    pkg_check_modules(libturbojpeg REQUIRED libturbojpeg)
+    pkg_check_modules(LIBUSB REQUIRED libusb-1.0)
+else()
+    set(TURBOJPEG_WIN_INCLUDE_DIR "NOTSET" CACHE PATH "Path to the libturbojpeg include dir")
+    set(TURBOJPEG_WIN_IMPORT_LIB_PATH "NOTSET" CACHE FILEPATH "Path to the libturbojpeg import library")
+
+    list(APPEND required_variables
+        TURBOJPEG_WIN_INCLUDE_DIR
+        TURBOJPEG_WIN_IMPORT_LIB_PATH
+    )
+    message(STATUS "Validating config:")
+
+    foreach(VAR IN LISTS required_variables)
+        if(${VAR} STREQUAL "NOTSET")
+            message(FATAL_ERROR "Required variable ${VAR} is not set!")
+        endif()
+    endforeach()
+
+    foreach(VAR IN LISTS required_variables)
+        message(STATUS "  ${VAR}: ${${VAR}}")
+    endforeach()
+
+    add_definitions("-D_TIMESPEC_DEFINED")
+endif()
+
+# Note: add_cython_target does not actually add a target, but fills a variable with the
+# corresponding compiled source file, e.g. here 'uvc_bindings.c'. If only the
+# name is specified, it will look for a cython file with the same base name:
+# 'uvc_bindings.pyx' in this case.
+set(CYTHON_ANNOTATE TRUE)
+add_cython_target(uvc_bindings C PY3) # CXX does not generate a corretc module
+
+# Create a module library from the source file and wrap it with settings for
+# creating a python extension.
+add_library(uvc_bindings MODULE ${uvc_bindings})
+python_extension_module(uvc_bindings)
+
+# include cpp folder for finding headers
+target_include_directories(uvc_bindings PRIVATE ".")
+
+# include numpy headers
+target_include_directories(
+    uvc_bindings
+    PRIVATE
+    ${NumPy_INCLUDE_DIRS}
+
+    # Unix
+    ${LIBUSB_INCLUDE_DIRS}
+    ${libturbojpeg_INCLUDE_DIRS}
+
+    # Windows vars
+    ${LIBUSB_WIN_INCLUDE_DIR}
+    ${TURBOJPEG_WIN_INCLUDE_DIR}
+)
+
+target_link_directories(uvc_bindings PRIVATE ${libturbojpeg_LIBRARY_DIRS})
+
+target_link_libraries(
+    uvc_bindings
+    PLLibUVC::uvc
+    ${libturbojpeg_LIBRARIES}
+
+    # Windows
+    ${TURBOJPEG_WIN_IMPORT_LIB_PATH}
+)
+
+# install here
+install(TARGETS uvc_bindings LIBRARY DESTINATION ".")
diff --git a/controls.pxi b/pyuvc-source/controls.pxi
similarity index 100%
rename from controls.pxi
rename to pyuvc-source/controls.pxi
diff --git a/cturbojpeg.pxd b/pyuvc-source/cturbojpeg.pxd
similarity index 99%
rename from cturbojpeg.pxd
rename to pyuvc-source/cturbojpeg.pxd
index b10241c..f30cc70 100644
--- a/cturbojpeg.pxd
+++ b/pyuvc-source/cturbojpeg.pxd
@@ -267,5 +267,3 @@ cdef extern from "turbojpeg.h":
     int tjDecompress(tjhandle handle, unsigned char *jpegBuf, long unsigned int jpegSize, unsigned char *dstBuf, int width, int pitch, int height, int pixelSize, int flags)
 
     int tjDecompressToYUV(tjhandle handle, unsigned char *jpegBuf, long unsigned int jpegSize, unsigned char *dstBuf, int flags)
-
-
diff --git a/cuvc.pxd b/pyuvc-source/cuvc.pxd
similarity index 90%
rename from cuvc.pxd
rename to pyuvc-source/cuvc.pxd
index 62b13b3..87dc82e 100644
--- a/cuvc.pxd
+++ b/pyuvc-source/cuvc.pxd
@@ -21,15 +21,11 @@ IF UNAME_SYSNAME == "Windows":
         cdef struct timespec:
             time_t tv_sec
             long tv_nsec
-    cdef extern from "libusb/libusb.h":
-        pass
-ELIF UNAME_SYSNAME == "Darwin":
-    from posix.time cimport timeval,timespec
     cdef extern from "libusb.h":
         pass
 ELSE:
     from posix.time cimport timeval,timespec
-    cdef extern from "libusb-1.0/libusb.h":
+    cdef extern from "libusb.h":
         pass
 
 cdef extern from "Python.h":
@@ -117,8 +113,17 @@ cdef extern from  "libuvc/libuvc.h":
         UVC_FRAME_FORMAT_RGB
         UVC_FRAME_FORMAT_BGR
         UVC_FRAME_FORMAT_MJPEG
+        UVC_FRAME_FORMAT_H264
         UVC_FRAME_FORMAT_GRAY8
+        UVC_FRAME_FORMAT_GRAY16
         UVC_FRAME_FORMAT_BY8
+        UVC_FRAME_FORMAT_BA81
+        UVC_FRAME_FORMAT_SGRBG8
+        UVC_FRAME_FORMAT_SGBRG8
+        UVC_FRAME_FORMAT_SRGGB8
+        UVC_FRAME_FORMAT_SBGGR8
+        UVC_FRAME_FORMAT_NV12
+        UVC_FRAME_FORMAT_P010
         UVC_FRAME_FORMAT_COUNT
 
     enum:
@@ -177,18 +182,14 @@ cdef extern from  "libuvc/libuvc.h":
         uvc_vs_desc_subtype bDescriptorSubtype
         uint8_t bFormatIndex
         uint8_t bNumFrameDescriptors
-        #union {
-        #uint8_t guidFormat[16]
-        #uint8_t fourccFormat[4]
-        #}
-        #/** Format-specific data */
-        #union {
-        #/** BPP for uncompressed stream */
-        #uint8_t bBitsPerPixel
-        #/** Flags for JPEG stream */
-        #uint8_t bmFlags
-        #}
-        #/** Default {uvc_frame_desc} to choose given this format */
+        uint8_t guidFormat[16]
+        uint8_t fourccFormat[4]
+        # /** Format-specific data */
+        # /** BPP for uncompressed stream */
+        uint8_t bBitsPerPixel
+        # /** Flags for JPEG stream */
+        uint8_t bmFlags
+        # /** Default {uvc_frame_desc} to choose given this format */
         uint8_t bDefaultFrameIndex
         uint8_t bAspectRatioX
         uint8_t bAspectRatioY
@@ -356,7 +357,23 @@ cdef extern from  "libuvc/libuvc.h":
 
 
     cdef struct uvc_stream_ctrl:
-        pass
+        uint16_t bmHint
+        uint8_t bFormatIndex
+        uint8_t bFrameIndex
+        uint32_t dwFrameInterval
+        uint16_t wKeyFrameRate
+        uint16_t wPFrameRate
+        uint16_t wCompQuality
+        uint16_t wCompWindowSize
+        uint16_t wDelay
+        uint32_t dwMaxVideoFrameSize
+        uint32_t dwMaxPayloadTransferSize
+        uint32_t dwClockFrequency
+        uint8_t bmFramingInfo
+        uint8_t bPreferredVersion
+        uint8_t bMinVersion
+        uint8_t bMaxVersion
+        uint8_t bInterfaceNumber
     ctypedef uvc_stream_ctrl uvc_stream_ctrl_t
 
 
@@ -381,7 +398,7 @@ cdef extern from  "libuvc/libuvc.h":
     void uvc_unref_device(uvc_device_t *dev)
 
 
-    uvc_error_t uvc_open(uvc_device_t *dev,uvc_device_handle_t **devh)
+    uvc_error_t uvc_open(uvc_device_t *dev,uvc_device_handle_t **devh, int should_detach_kernel_driver)
     void uvc_close(uvc_device_handle_t *devh)
 
     uvc_device_t *uvc_get_device(uvc_device_handle_t *devh)
@@ -396,17 +413,18 @@ cdef extern from  "libuvc/libuvc.h":
     const uvc_extension_unit_t *uvc_get_extension_units(uvc_device_handle_t *devh)
 
 
-    uvc_error_t uvc_get_stream_ctrl_format_size( uvc_device_handle_t *devh, uvc_stream_ctrl_t *ctrl, uvc_frame_format format, int width, int height, int fps)
+    uvc_frame_format uvc_frame_format_for_guid(uint8_t guid[16])
+    uvc_error_t uvc_get_stream_ctrl_format_size( uvc_device_handle_t *devh, uvc_stream_ctrl_t *ctrl, uvc_frame_format format, int width, int height, int fps, int should_detach_kernel_driver)
 
     uvc_format_desc_t *uvc_get_format_descs(uvc_device_handle_t* )
 
-    uvc_error_t uvc_probe_stream_ctrl( uvc_device_handle_t *devh, uvc_stream_ctrl_t *ctrl)
+    uvc_error_t uvc_probe_stream_ctrl( uvc_device_handle_t *devh, uvc_stream_ctrl_t *ctrl, int should_detach_kernel_driver)
 
-    uvc_error_t uvc_start_streaming( uvc_device_handle_t *devh, uvc_stream_ctrl_t *ctrl, uvc_frame_callback_t *cb, void *user_ptr, uint8_t flags)
+    uvc_error_t uvc_start_streaming( uvc_device_handle_t *devh, uvc_stream_ctrl_t *ctrl, uvc_frame_callback_t *cb, void *user_ptr, uint8_t flags, int should_detach_kernel_driver)
 
     void uvc_stop_streaming(uvc_device_handle_t *devh)
 
-    uvc_error_t uvc_stream_open_ctrl(uvc_device_handle_t *devh, uvc_stream_handle_t **strmh, uvc_stream_ctrl_t *ctrl)
+    uvc_error_t uvc_stream_open_ctrl(uvc_device_handle_t *devh, uvc_stream_handle_t **strmh, uvc_stream_ctrl_t *ctrl, int should_detach_kernel_driver)
     uvc_error_t set_uvc_stream_ctrl"uvc_stream_ctrl"(uvc_stream_handle_t *strmh, uvc_stream_ctrl_t *ctrl)
     uvc_error_t uvc_stream_start(uvc_stream_handle_t *strmh,uvc_frame_callback_t *cb,void *user_ptr,float bandwidth_factor, uint8_t flags)
     #uvc_error_t uvc_stream_start_iso(uvc_stream_handle_t *strmh, uvc_frame_callback_t *cb, void *user_ptr)
@@ -441,5 +459,3 @@ cdef inline void INT_TO_DW(int32_t i, uint8_t *p):
     p[1] = i >> 8
     p[2] = i >> 16
     p[3] = i >> 24
-
-
diff --git a/darwin_time.pxi b/pyuvc-source/darwin_time.pxi
similarity index 99%
rename from darwin_time.pxi
rename to pyuvc-source/darwin_time.pxi
index 53c28d1..0748b47 100644
--- a/darwin_time.pxi
+++ b/pyuvc-source/darwin_time.pxi
@@ -21,6 +21,3 @@ cdef double get_sys_time_monotonic():
         get_mach_timebase_info(&timeBase)
         timeConvert = <double>timeBase.numer /<double>timeBase.denom / 1000000000.0
     return mach_absolute_time( ) * timeConvert
-
-
-
diff --git a/linux_time.pxi b/pyuvc-source/linux_time.pxi
similarity index 66%
rename from linux_time.pxi
rename to pyuvc-source/linux_time.pxi
index abd55fa..51c1eb1 100644
--- a/linux_time.pxi
+++ b/pyuvc-source/linux_time.pxi
@@ -1,4 +1,4 @@
-from posix.time cimport timeval,timespec,clock_gettime,CLOCK_MONOTONIC
+from posix.time cimport CLOCK_MONOTONIC, clock_gettime, timespec, timeval
 
 
 cdef double get_sys_time_monotonic():
diff --git a/pyuvc-source/uvc/__init__.py b/pyuvc-source/uvc/__init__.py
new file mode 100644
index 0000000..050a5cd
--- /dev/null
+++ b/pyuvc-source/uvc/__init__.py
@@ -0,0 +1,38 @@
+import logging
+
+try:
+    from importlib.metadata import PackageNotFoundError, version
+except ImportError:
+    from importlib_metadata import PackageNotFoundError, version
+
+# .uvc_bindings expects `logger` to be present
+logger = logging.getLogger(__name__)
+
+from .uvc_bindings import (  # noqa: E402
+    Capture,
+    Device_List,
+    InitError,
+    OpenError,
+    StreamError,
+    device_list,
+    get_time_monotonic,
+    is_accessible,
+)
+
+try:
+    __version__ = version("pupil-labs-uvc")
+except PackageNotFoundError:
+    # package is not installed
+    pass
+
+__all__ = [
+    "__version__",
+    "Capture",
+    "device_list",
+    "Device_List",
+    "get_time_monotonic",
+    "InitError",
+    "is_accessible",
+    "OpenError",
+    "StreamError",
+]
diff --git a/uvc.pyx b/pyuvc-source/uvc_bindings.pyx
similarity index 61%
rename from uvc.pyx
rename to pyuvc-source/uvc_bindings.pyx
index 7d3269a..007cdfd 100644
--- a/uvc.pyx
+++ b/pyuvc-source/uvc_bindings.pyx
@@ -1,21 +1,21 @@
-'''
-(*)~----------------------------------------------------------------------------------
- Pupil - eye tracking platform
- Copyright (C) 2012-2015  Pupil Labs
-
- Distributed under the terms of the CC BY-NC-SA License.
- License details are in the file LICENSE, distributed as part of this software.
-----------------------------------------------------------------------------------~(*)
-'''
+import logging
+import platform
+import warnings
+from itertools import chain
 
 import cython
+
 from libc.string cimport memset
-cimport cuvc as uvc
-cimport cturbojpeg as turbojpeg
+
+from typing import NamedTuple, Optional, Tuple
+
 cimport numpy as np
+
 import numpy as np
-from cuvc cimport uvc_frame_t, timeval
-import warnings
+
+cimport cturbojpeg as turbojpeg
+cimport cuvc as uvc
+from cuvc cimport timeval, uvc_frame_t
 
 __version__ = "0.15.0"
 
@@ -26,23 +26,60 @@ ELIF UNAME_SYSNAME == "Darwin":
 ELIF UNAME_SYSNAME == "Linux":
     include "linux_time.pxi"
 
-uvc_error_codes = {  0:"Success (no error)",
-                    -1:"Input/output error.",
-                    -2:"Invalid parameter.",
-                    -3:"Access denied.",
-                    -4:"No such device.",
-                    -5:"Entity not found.",
-                    -6:"Resource busy.",
-                    -7:"Operation timed out.",
-                    -8:"Overflow.",
-                    -9:"Pipe error.",
-                    -10:"System call interrupted.",
-                    -11:"Insufficient memory.     ",
-                    -12:"Operation not supported.",
-                    -50:"Device is not UVC-compliant.",
-                    -51:"Mode not supported.",
-                    -52:"Resource has a callback (can't use polling and async)",
-                    -99:"Undefined error."}
+from . import logger
+
+# Until version 1.0.24, libusb did not support detaching kernel drivers on macOS. As a
+# result, the cameras could only be accessed if no other driver was attached. In macOS
+# Monterey, the OS attaches a driver by default. As a result, libusb needs to explicitly
+# detach the driver before claiming the device. This is only implemented in newer
+# versions of libusb and requires either root privileges or the
+# "com.apple.vm.device-access" entitlement. The latter requires a special provisioning
+# profile that is not publicly available. https://github.com/libusb/libusb/issues/1014
+
+# To avoid requiring root privileges on every macOS version, the code below checks if
+# it is running on macOS Big Sur or older and if so, disables the driver detachment
+# which corresponds to libusb 1.0.24 behavior, even if libusb 1.0.25 or newer is being
+# used.
+_system_info = platform.uname()
+if _system_info.system == "Darwin":
+    logger.debug(f"Running on macOS (Kernel release {_system_info.release})")
+    # https://en.wikipedia.org/wiki/Darwin_(operating_system)#Release_history
+    # macOS Monterey uses Kernel version 21.*
+    IS_MACOS_BIG_SUR_OR_OLDER = int(_system_info.release.split(".")[0]) < 21
+    if IS_MACOS_BIG_SUR_OR_OLDER:
+        logger.debug(
+            "Running on macOS Big Sur or older. "
+            "Will not attempt to detach kernel drivers."
+        )
+    else:
+        logger.debug(
+            "Running on macOS Monterey or newer. "
+            "Requires root privileges to detach kernel drivers."
+        )
+else:
+    logger.debug(f"Not running on macOS. Will attempt to detach kernel drivers.")
+    IS_MACOS_BIG_SUR_OR_OLDER = False
+cdef int SHOULD_DETACH_KERNEL_DRIVER = int(not IS_MACOS_BIG_SUR_OR_OLDER)
+
+uvc_error_codes = {
+    0:"Success (no error)",
+    -1:"Input/output error",
+    -2:"Invalid parameter",
+    -3:"Access denied",
+    -4:"No such device",
+    -5:"Entity not found",
+    -6:"Resource busy",
+    -7:"Operation timed out",
+    -8:"Overflow",
+    -9:"Pipe error",
+    -10:"System call interrupted",
+    -11:"Insufficient memory",
+    -12:"Operation not supported",
+    -50:"Device is not UVC-compliant",
+    -51:"Mode not supported",
+    -52:"Resource has a callback (can't use polling and async)",
+    -99:"Undefined error",
+}
 
 
 cdef str _to_str(object s):
@@ -52,70 +89,46 @@ cdef str _to_str(object s):
         return (<bytes>s).decode('utf-8')
 
 class CaptureError(Exception):
-    def __init__(self, message):
-        super(CaptureError, self).__init__()
-        self.message = message
+    pass
 
 class StreamError(CaptureError):
-    def __init__(self, message):
-        super(StreamError, self).__init__(message)
-        self.message = message
+    pass
 
 class InitError(CaptureError):
-    def __init__(self, message):
-        super(InitError, self).__init__(message)
-        self.message = message
+    pass
 
 class OpenError(InitError):
-    def __init__(self, message):
-        super(InitError, self).__init__(message)
-        self.message = message
+    pass
 
 class DeviceNotFoundError(InitError):
     def __init__(self, message):
-        super(InitError, self).__init__(message)
-        self.message = message
-#logging
-import logging
-logger = logging.getLogger(__name__)
+        super().__init__(message)
 
-__version__ = '0.14' #make sure this is the same in setup.py
+class InsufficientBandwidthError(InitError):
+    def __init__(self, message):
+        super().__init__(message)
 
 
-cdef class Frame:
-    '''
-    The Frame Object holds image data and image metadata.
-
-    The Frame object is returned from Capture.get_frame()
+CameraMode = NamedTuple(
+    "CameraMode",
+    [
+        ("width", int),
+        ("height", int),
+        ("fps", int),
+        ("format_native", int),
+        ("format_name", str),
+        ("supported", bool)
+    ],
+)
 
-    It will hold the data in the transport format the Capture is configured to grab.
-    Usually this is mjpeg or yuyv
+_supported_formats = (uvc.UVC_FRAME_FORMAT_GRAY8, uvc.UVC_FRAME_FORMAT_MJPEG)
 
-    Other formats can be requested and will be converted/decoded on the fly.
-    Frame will use caching to avoid redunant work.
-    Usually RGB8,YUYV or GRAY are requested formats.
 
-    WARNING:
-    When capture.get_frame() is called again previos instances of Frame will point to invalid memory.
-    Specifically all image data in the capture transport format.
-    Previously converted formats are still valid.
-    '''
-
-    cdef turbojpeg.tjhandle tj_context
+cdef class Frame:
     cdef uvc.uvc_frame * _uvc_frame
-    cdef unsigned char[:] _bgr_buffer, _gray_buffer,_yuv_buffer #we use numpy for memory management.
-    cdef bint _yuv_converted, _bgr_converted
-    cdef public double timestamp
-    cdef public yuv_subsampling
     cdef bint owns_uvc_frame
 
-    def __cinit__(self):
-        self._yuv_converted = False
-        self._bgr_converted = False
-        self.tj_context = NULL
-
-    def __init__(self):
-        pass
+    cdef public double timestamp
 
     cdef attach_uvcframe(self,uvc.uvc_frame *uvc_frame,copy=True):
         if copy:
@@ -130,6 +143,10 @@ cdef class Frame:
         if self.owns_uvc_frame:
             uvc.uvc_free_frame(self._uvc_frame)
 
+    @property
+    def data_fully_received(self):
+        return self._uvc_frame.width * self._uvc_frame.height == self._uvc_frame.data_bytes
+
     property width:
         def __get__(self):
             return self._uvc_frame.width
@@ -142,6 +159,71 @@ cdef class Frame:
         def __get__(self):
             return self._uvc_frame.sequence
 
+    property gray:
+        def __get__(self):
+            cdef np.uint8_t[::1] view
+            if self._uvc_frame.width * self._uvc_frame.height > self._uvc_frame.data_bytes:
+                logger.debug(
+                    f"Received less data than expected ({self._uvc_frame.data_bytes} / "
+                    f"{self._uvc_frame.width * self._uvc_frame.height})"
+                )
+                # less data transferred than expected, filling with zeros
+                view = <np.uint8_t[:self._uvc_frame.data_bytes]>self._uvc_frame.data
+                frame = 255 + np.zeros((self._uvc_frame.height * self._uvc_frame.width), dtype=np.uint8)
+                frame[:self._uvc_frame.data_bytes] = view
+            else:
+                view = <np.uint8_t[:self._uvc_frame.height*self._uvc_frame.width]>self._uvc_frame.data
+                frame = np.array(view, copy=True)
+            return frame.reshape((self._uvc_frame.height, self._uvc_frame.width))
+
+    @property
+    def bgr(self):
+        return np.stack([self.gray] * 3, axis=2)
+
+    @property
+    def yuv_buffer(self):
+        return None
+
+    #for legacy reasons.
+    @property
+    def img(self):
+        return self.bgr
+
+
+cdef class MJPEGFrame(Frame):
+    '''
+    The Frame Object holds image data and image metadata.
+
+    The Frame object is returned from Capture.get_frame()
+
+    It will hold the data in the transport format the Capture is configured to grab.
+    Usually this is mjpeg or yuyv
+
+    Other formats can be requested and will be converted/decoded on the fly.
+    Frame will use caching to avoid redunant work.
+    Usually RGB8,YUYV or GRAY are requested formats.
+
+    WARNING:
+    When capture.get_frame() is called again previos instances of Frame will point to invalid memory.
+    Specifically all image data in the capture transport format.
+    Previously converted formats are still valid.
+    '''
+
+    cdef turbojpeg.tjhandle tj_context
+    cdef unsigned char[:] _bgr_buffer, _gray_buffer,_yuv_buffer #we use numpy for memory management.
+    cdef bint _yuv_converted, _bgr_converted
+    cdef bint _data_fully_received
+    cdef public yuv_subsampling
+
+    def __cinit__(self):
+        self._data_fully_received = True
+        self._yuv_converted = False
+        self._bgr_converted = False
+        self.tj_context = NULL
+
+    def __init__(self):
+        pass
+
     property jpeg_buffer:
         def __get__(self):
             cdef np.uint8_t[::1] view = <np.uint8_t[:self._uvc_frame.data_bytes]>self._uvc_frame.data
@@ -250,11 +332,6 @@ cdef class Frame:
             return BGR
 
 
-    #for legacy reasons.
-    property img:
-        def __get__(self):
-            return self.bgr
-
     cdef yuv2bgr(self):
         #2.75 ms at 1080p
         cdef int channels = 3
@@ -290,10 +367,15 @@ cdef class Frame:
                                              &self._yuv_buffer[0],
                                               0)
         if result == -1:
+            self._data_fully_received = False
             logger.warning('Turbojpeg jpeg2yuv: %s'%turbojpeg.tjGetErrorStr() )
         self.yuv_subsampling = jpegSubsamp
         self._yuv_converted = True
 
+    @property
+    def data_fully_received(self):
+        return self._data_fully_received
+
     def clear_caches(self):
         self._bgr_converted = False
         self._yuv_converted = False
@@ -429,8 +511,8 @@ cdef class Capture:
     cdef uvc.uvc_stream_handle_t *strmh
     cdef float _bandwidth_factor
 
-    cdef tuple _active_mode
-    cdef list _available_modes
+    cdef object _active_mode
+    cdef list _camera_modes
     cdef dict _info
     cdef public list controls
 
@@ -441,8 +523,8 @@ cdef class Capture:
         self._stream_on = 0
         self._configured = 0
         self.strmh = NULL
-        self._available_modes = []
-        self._active_mode = None,None,None
+        self._camera_modes = []
+        self._active_mode = None
         self._info = {}
         self.controls = []
         self._bandwidth_factor = 2.0
@@ -476,38 +558,42 @@ cdef class Capture:
                 break
             device_address = uvc.uvc_get_device_address(dev)
             bus_number = uvc.uvc_get_bus_number(dev)
-            if dev_uid == '%s:%s'%(bus_number,device_address):
-                logger.debug("Found device that mached uid:'%s'"%dev_uid)
+            dev_uid_found = f'{bus_number}:{device_address}'
+            if dev_uid == dev_uid_found:
+                logger.debug(f"Found device that mached uid: {dev_uid}")
                 uvc.uvc_ref_device(dev)
                 if (uvc.uvc_get_device_descriptor(dev, &desc) == uvc.UVC_SUCCESS):
-                            product = desc.product or "unknown"
-                            manufacturer = desc.manufacturer or "unknown"
-                            serialNumber = desc.serialNumber or "unknown"
-                            idProduct,idVendor = desc.idProduct,desc.idVendor
-                            device_address = uvc.uvc_get_device_address(dev)
-                            bus_number = uvc.uvc_get_bus_number(dev)
-                            self._info = {'name':_to_str(product),
-                                            'manufacturer':_to_str(manufacturer),
-                                            'serialNumber':_to_str(serialNumber),
-                                            'idProduct':idProduct,
-                                            'idVendor':idVendor,
-                                            'device_address':device_address,
-                                            'bus_number':bus_number,
-                                            'uid':'%s:%s'%(bus_number,device_address)}
+                    product = desc.product or "unknown"
+                    manufacturer = desc.manufacturer or "unknown"
+                    serialNumber = desc.serialNumber or "unknown"
+                    idProduct,idVendor = desc.idProduct,desc.idVendor
+                    device_address = uvc.uvc_get_device_address(dev)
+                    bus_number = uvc.uvc_get_bus_number(dev)
+                    self._info = {
+                        'name': _to_str(product),
+                        'manufacturer': _to_str(manufacturer),
+                        'serialNumber': _to_str(serialNumber),
+                        'idProduct': idProduct,
+                        'idVendor': idVendor,
+                        'device_address': device_address,
+                        'bus_number': bus_number,
+                        'uid': dev_uid_found,
+                    }
+                    logger.debug(f"Device info: {self._info}")
                 uvc.uvc_free_device_descriptor(desc)
                 break
             idx +=1
 
         uvc.uvc_free_device_list(dev_list, 1)
         if dev == NULL:
-            raise DeviceNotFoundError("Device with uid: '%s' not found"%dev_uid)
+            raise DeviceNotFoundError(f"Device with uid: `{dev_uid}` not found")
 
 
         #once found we open the device
         self.dev = dev
-        error = uvc.uvc_open(self.dev,&self.devh)
+        error = uvc.uvc_open(self.dev, &self.devh, SHOULD_DETACH_KERNEL_DRIVER)
         if error != uvc.UVC_SUCCESS:
-            raise OpenError("could not open device. Error:%s"%uvc_error_codes[error])
+            raise OpenError(f"Could not open device. Error: {uvc_error_codes[error]}")
         logger.debug("Device '%s' opended."%dev_uid)
 
     cdef _de_init_device(self):
@@ -523,35 +609,59 @@ cdef class Capture:
         self._re_init_device()
         self._start()
 
-    cdef _configure_stream(self,mode=(640,480,30)):
-        cdef int status
+    cdef _configure_stream(self, mode: Optional[CameraMode] = None):
+        if mode is None:
+            if self.available_modes:
+                mode = self.available_modes[0]
+            else:
+                raise InitError(
+                    f"Error: No supported mode found. Unsupported modes: "
+                    f"{self.all_modes}"
+                )
 
         if self._stream_on:
             self._stop()
 
-        status = uvc.uvc_get_stream_ctrl_format_size( self.devh, &self.ctrl,
-                                                      uvc.UVC_FRAME_FORMAT_COMPRESSED,
-                                                      mode[0],mode[1],mode[2] )
+        cdef int status
+        status = uvc.uvc_get_stream_ctrl_format_size(
+            self.devh,
+            &self.ctrl,
+            mode.format_native,
+            mode.width,
+            mode.height,
+            mode.fps,
+            SHOULD_DETACH_KERNEL_DRIVER,
+        )
         if status != uvc.UVC_SUCCESS:
-            raise InitError("Can't get stream control: Error:'%s'."%uvc_error_codes[status])
+            raise InitError(
+                f"Error: Can't get stream control: {uvc_error_codes[status]} "
+                f"(requested {mode.format_native})"
+            )
+        else:
+            logger.debug(f"Negotiated frame format: {self.ctrl}")
+
         self._configured = 1
         self._active_mode = mode
 
-
     cdef _start(self):
         cdef int status
         if not self._configured:
             self._configure_stream()
-        status = uvc.uvc_stream_open_ctrl(self.devh, &self.strmh, &self.ctrl)
+        status = uvc.uvc_stream_open_ctrl(
+            self.devh, &self.strmh, &self.ctrl, SHOULD_DETACH_KERNEL_DRIVER
+        )
         if status != uvc.UVC_SUCCESS:
-            raise InitError("Can't open stream control: Error:'%s'."%uvc_error_codes[status])
-        status = uvc.uvc_stream_start(self.strmh, NULL, NULL,self._bandwidth_factor,0)
+            raise InitError(
+                f"Can't open stream control - error: {uvc_error_codes[status]!r}"
+            )
+        status = uvc.uvc_stream_start(self.strmh, NULL, NULL, self._bandwidth_factor, 0)
         if status != uvc.UVC_SUCCESS:
-            raise InitError("Can't start isochronous stream: Error:'%s'."%uvc_error_codes[status])
+            raise InsufficientBandwidthError(
+                f"Can't start isochronous stream - error: {uvc_error_codes[status]!r}."
+            )
         self._stream_on = 1
         logger.debug("Stream start.")
 
-
     def stop_stream(self):
         self._stop()
 
@@ -574,47 +684,78 @@ cdef class Capture:
             try:
                 frame = self.get_frame()
             except StreamError as e:
-                if a:
-                    logger.info('Could not get Frame: "%s". Attempt:%s/%s '%(e.message,a+1,attempts))
-                else:
-                    logger.debug('Could not get Frame of first try: "%s". Attempt:%s/%s '%(e.message,a+1,attempts))
+                logger.debug(
+                    f"Could not get Frame of first try: '{e}'. Attempt: "
+                    f"{a+1}/{attempts}"
+                )
             else:
                 return frame
         raise StreamError("Could not grab frame after 3 attempts. Giving up.")
 
-
-
     def get_frame(self,timeout=0):
-        cdef int status, j_width,j_height,jpegSubsamp,header_ok
+        cdef int status, j_width, j_height, jpegSubsamp, header_ok
         cdef int  timeout_usec = int(timeout*1e6) #sec to usec
         if not self._stream_on:
             self._start()
         cdef uvc.uvc_frame *uvc_frame = NULL
         #when this is called we will overwrite the last jpeg buffer! This can be dangerous!
         with nogil:
-            status = uvc.uvc_stream_get_frame(self.strmh,&uvc_frame,timeout_usec)
-        if status !=uvc.UVC_SUCCESS:
+            status = uvc.uvc_stream_get_frame(self.strmh, &uvc_frame, timeout_usec)
+        if status == uvc.UVC_ERROR_TIMEOUT:
+            raise TimeoutError
+        elif status != uvc.UVC_SUCCESS:
             raise StreamError(uvc_error_codes[status])
         if uvc_frame is NULL:
             raise StreamError("Frame pointer is NULL")
-        ##check jpeg header
-        header_ok = turbojpeg.tjDecompressHeader2(self.tj_context,  <unsigned char *>uvc_frame.data, uvc_frame.data_bytes, &j_width, &j_height, &jpegSubsamp)
-        if not (header_ok >=0 and uvc_frame.width == j_width and uvc_frame.height == j_height):
-            raise StreamError("JPEG header corrupt.")
 
-        cdef Frame out_frame = Frame()
-        out_frame.tj_context = self.tj_context
-        out_frame.attach_uvcframe(uvc_frame = uvc_frame,copy=True)
-        out_frame.timestamp = uvc_frame.capture_time.tv_sec + <double>uvc_frame.capture_time.tv_usec * 1e-6
-        return out_frame
+        cdef Frame out_frame_gray
+        cdef MJPEGFrame out_frame_mjpeg
+        cdef object frame
+
+        if uvc_frame.frame_format == uvc.UVC_COLOR_FORMAT_MJPEG:
+            ##check jpeg header
+            header_ok = turbojpeg.tjDecompressHeader2(
+                self.tj_context,
+                <unsigned char *>uvc_frame.data,
+                uvc_frame.data_bytes,
+                &j_width,
+                &j_height,
+                &jpegSubsamp
+            )
+            if not (
+                header_ok >= 0 and
+                uvc_frame.width == j_width and
+                uvc_frame.height == j_height
+            ):
+                raise StreamError("JPEG header corrupt.")
+            out_frame_mjpeg = MJPEGFrame()
+            out_frame_mjpeg.tj_context = self.tj_context
+            out_frame_mjpeg.attach_uvcframe(uvc_frame=uvc_frame, copy=True)
+            frame = out_frame_mjpeg
+
+        elif uvc_frame.frame_format == uvc.UVC_COLOR_FORMAT_GRAY8:
+            # if uvc_frame.width * uvc_frame.height > uvc_frame.data_bytes:
+            #     raise StreamError(
+            #         f"Received too few bytes to fill {uvc_frame.width}x"
+            #         f"{uvc_frame.height} GRAY8 image"
+            #     )
+            out_frame_gray = Frame()
+            out_frame_gray.attach_uvcframe(uvc_frame=uvc_frame, copy=True)
+            frame = out_frame_gray
+
+        else:
+            raise NotImplementedError(f"Unsupported frame format {uvc_frame.frame_format}")
+
+        frame.timestamp = uvc_frame.capture_time.tv_sec + <double>uvc_frame.capture_time.tv_usec * 1e-6
+        return frame
 
 
     cdef _enumerate_controls(self):
 
-        cdef uvc.uvc_input_terminal_t  *input_terminal = uvc.uvc_get_input_terminals(self.devh)
-        cdef uvc.uvc_output_terminal_t  *output_terminal = uvc.uvc_get_output_terminals(self.devh)
-        cdef uvc.uvc_processing_unit_t  *processing_unit = uvc.uvc_get_processing_units(self.devh)
-        cdef uvc.uvc_extension_unit_t  *extension_unit = uvc.uvc_get_extension_units(self.devh)
+        cdef uvc.uvc_input_terminal_t *input_terminal = <uvc.uvc_input_terminal_t *>uvc.uvc_get_input_terminals(self.devh)
+        cdef uvc.uvc_output_terminal_t *output_terminal = <uvc.uvc_output_terminal_t *>uvc.uvc_get_output_terminals(self.devh)
+        cdef uvc.uvc_processing_unit_t *processing_unit = <uvc.uvc_processing_unit_t *>uvc.uvc_get_processing_units(self.devh)
+        cdef uvc.uvc_extension_unit_t *extension_unit = <uvc.uvc_extension_unit_t *>uvc.uvc_get_extension_units(self.devh)
 
         cdef int x = 0
         avaible_controls_per_unit = {}
@@ -647,7 +788,11 @@ cdef class Capture:
                 try:
                     control= Control(cap = self,**std_ctl)
                 except Exception as e:
-                    logger.error("Could not init '%s'! Error: %s" %(std_ctl['display_name'],e))
+                    import traceback
+
+                    logger.debug(f"Could not init {std_ctl['display_name']} control!")
+                    logger.debug(f"Control info: {std_ctl}")
+                    logger.debug(traceback.format_exc())
                 else:
                     self.controls.append(control)
 
@@ -657,22 +802,34 @@ cdef class Capture:
 
 
     cdef _enumerate_formats(self):
-        cdef uvc.uvc_format_desc_t *format_desc = uvc.uvc_get_format_descs(self.devh)
+        cdef uvc.uvc_format_desc_t *format_desc = <uvc.uvc_format_desc_t *>uvc.uvc_get_format_descs(self.devh)
         cdef uvc.uvc_frame_desc *frame_desc
         cdef int i
-        self._available_modes = []
+        self._camera_modes = []
+        cdef uvc.uvc_frame_format format_native
+        cdef str format_name
+
         while format_desc is not NULL:
             frame_desc = format_desc.frame_descs
+
+            format_native = uvc.uvc_frame_format_for_guid(format_desc.guidFormat)
+            format_name = (<char*>format_desc.fourccFormat).decode('UTF-8')
+            is_format_supported = (format_native in _supported_formats)
             while frame_desc is not NULL:
-                if frame_desc.bDescriptorSubtype == uvc.UVC_VS_FRAME_MJPEG:
-                    frame_index = frame_desc.bFrameIndex
-                    width,height = frame_desc.wWidth,frame_desc.wHeight
-                    mode = {'size':(width,height),'rates':[]}
-                    i = 0
-                    while frame_desc.intervals[i]:
-                        mode['rates'].append(interval_to_fps(frame_desc.intervals[i]) )
-                        i+=1
-                    self._available_modes.append(mode)
+                i = 0
+                while frame_desc.intervals[i]:
+                    fps = interval_to_fps(frame_desc.intervals[i])
+                    self._camera_modes.append(
+                        CameraMode(
+                            width=frame_desc.wWidth,
+                            height=frame_desc.wHeight,
+                            fps=fps,
+                            format_native=format_native,
+                            format_name=format_name,
+                            supported=is_format_supported,
+                        )
+                    )
+                    i+=1
 
                 #go to next frame_desc
                 frame_desc = frame_desc.next
@@ -680,10 +837,11 @@ cdef class Capture:
             #go to next format_desc
             format_desc = format_desc.next
 
-        logger.debug('avaible video modes: %s'%self._available_modes)
+        logger.debug(f'{self} - all camera modes: {self._camera_modes}')
 
     def __str__(self):
-        return "Capture device \n\t" + "\n\t".join(('%s: %s'%(k,v) for k,v in self._info.iteritems()))
+        meta = " ".join((f'{k}={v!r}' for k,v in self._info.iteritems()))
+        return f"Capture({meta})"
 
 
     def close(self):
@@ -701,66 +859,94 @@ cdef class Capture:
         self.close()
 
 
-    property frame_size:
-        def __get__(self):
-            return self._active_mode[:2]
-        def __set__(self,size):
-            for m in self._available_modes:
-                if size == m['size']:
-                    if self.frame_rate is not None:
-                        #closest match for rate
-                        rates = [ abs(r-self.frame_rate) for r in m['rates'] ]
-                        best_rate_idx = rates.index(min(rates))
-                        rate = m['rates'][best_rate_idx]
-                    else:
-                        #fist one
-                        rate = m['rates'][0]
-                    mode = size + (rate,)
-                    self._configure_stream(mode)
-                    return
-            raise ValueError("Frame size not suported.")
+    @property
+    def frame_size(self) -> Tuple[int, int]:
+        if self._active_mode:
+            return (self._active_mode.width, self._active_mode.height)
+
+    @frame_size.setter
+    def frame_size(self, size: Tuple[int, int]):
+        logger.debug(f"Changing resolution to target: {size}")
+        modes_with_target_res = [
+            mode for mode in self.available_modes if size == (mode.width, mode.height)
+        ]
+        if not modes_with_target_res:
+            raise ValueError(
+                f"Frame size {size} not suported. "
+                f"Available modes: {self.available_modes}"
+            )
+        if self.frame_rate is not None:
+            # closest match for previously selected rate
+            rates = [abs(m.fps-self.frame_rate) for m in modes_with_target_res]
+            best_rate_idx = rates.index(min(rates))
+            mode = modes_with_target_res[best_rate_idx]
+        else:
+            mode = modes_with_target_res[0]
+        self.frame_mode = mode
 
-    property frame_rate:
-        def __get__(self):
-            return self._active_mode[2]
-        def __set__(self,val):
-            if  self._configured:
-                self.frame_mode = self._active_mode[:2]+(val,)
-            else:
-                raise ValueError('set frame size first.')
+    @property
+    def frame_rate(self):
+        if self._active_mode:
+            return self._active_mode.fps
+
+    @frame_rate.setter
+    def frame_rate(self, val):
+        if self._active_mode and self._active_mode.fps == val:
+            return
+        elif self._active_mode:
+            logger.debug(f"Changing frame rate to target: {val}")
+            for mode in self.available_modes:
+                if (
+                    mode.width == self._active_mode.width and
+                    mode.height == self._active_mode.height and
+                    mode.fps == val
+                ):
+                    self.frame_mode = mode
+                    return
+            raise ValueError(
+                f"No available mode with target frame rate {val} Hz found: "
+                f"{self.available_modes}"
+            )
+        else:
+            logger.warning("Could not set frame rate. Set frame size first.")
 
     property frame_sizes:
         def __get__(self):
-            return [m['size'] for m in self._available_modes]
+            return tuple(sorted(set((m.width, m.height) for m in self.available_modes)))
 
     property frame_rates:
         def __get__(self):
-            for m in self._available_modes:
-                if m['size'] == self.frame_size:
-                    return m['rates']
-            raise ValueError("Please set frame_size before asking for rates.")
-
-
-    property frame_mode:
-        def __get__(self):
-            return self._active_mode
-        def __set__(self,mode):
-            logger.debug('Setting mode: %s,%s,%s'%mode)
-            self._configure_stream(mode)
-
-    property avaible_modes:
-        def __get__(self):
-            warnings.warn("Please use 'available_modes' property", DeprecationWarning)
-            return self.available_modes
-
-    property available_modes:
-        def __get__(self):
-            modes = []
-            for idx,m in enumerate(self._available_modes):
-                for r in m['rates']:
-                    modes.append(m['size']+(r,))
-            return modes
-
+            frame_size = self.frame_size
+            frame_rates = [
+                m.fps for m in self.available_modes
+                if (m.width, m.height) == frame_size
+            ]
+            if not frame_rates:
+                raise ValueError("Please set frame_size before asking for rates.")
+            frame_rates = tuple(sorted(set(frame_rates)))
+            logger.debug(
+                f"Available frame rates at {frame_size[0]}x{frame_size[1]}: "
+                f"{frame_rates}"
+            )
+            return frame_rates
+
+
+    @property
+    def frame_mode(self) -> CameraMode:
+        return self._active_mode
+
+    @frame_mode.setter
+    def frame_mode(self, mode: CameraMode):
+        logger.debug(f"Setting mode: {mode}")
+        self._configure_stream(mode)
+
+    @property
+    def available_modes(self) -> Tuple[CameraMode,...]:
+        return tuple(sorted(mode for mode in self._camera_modes if mode.supported))
+
+    @property
+    def all_modes(self) -> Tuple[CameraMode,...]:
+        return tuple(self._camera_modes)
     property name:
         def __get__(self):
             return self._info['name']
@@ -831,7 +1017,7 @@ def is_accessible(dev_uid):
         raise ValueError("Device with uid: '%s' not found"%dev_uid)
 
     #once found we open the device
-    error = uvc.uvc_open(dev,&devh)
+    error = uvc.uvc_open(dev, &devh, SHOULD_DETACH_KERNEL_DRIVER)
     if error != uvc.UVC_SUCCESS:
         uvc.uvc_unref_device(dev)
         uvc.uvc_exit(ctx)
@@ -841,5 +1027,3 @@ def is_accessible(dev_uid):
         uvc.uvc_unref_device(dev)
         uvc.uvc_exit(ctx)
         return True
-
-
diff --git a/windows_time.pxi b/pyuvc-source/windows_time.pxi
similarity index 60%
rename from windows_time.pxi
rename to pyuvc-source/windows_time.pxi
index 633832d..bce1e30 100644
--- a/windows_time.pxi
+++ b/pyuvc-source/windows_time.pxi
@@ -1,11 +1,15 @@
 
 import time
+
 #on windows
-from libc.stdint cimport uint64_t, int64_t
+
+from libc.stdint cimport int64_t, uint64_t
+
 
 cdef extern from "Windows.h":
     int QueryPerformanceCounter(int64_t *)
     int QueryPerformanceFrequency(int64_t *)
+
 cdef double get_sys_time_monotonic():
     cdef int64_t ts
     cdef int64_t freq
@@ -14,10 +18,10 @@ cdef double get_sys_time_monotonic():
     cdef int64_t us_left
     QueryPerformanceCounter(&ts)
     QueryPerformanceFrequency(&freq)
-    t_us = (ts * 1000000 )  / freq
-    
-    fullsecs = t_us / 1000000
-    us_left = t_us - fullsecs*1000000
+    t_us = <int64_t>((ts * 1_000_000 ) / freq)
+
+    fullsecs = <int64_t>(t_us / 1_000_000)
+    us_left = t_us - fullsecs * 1_000_000
 
-    cdef double secs = <double> fullsecs + <double>us_left/1000000.0
+    cdef double secs = <double> fullsecs + <double>us_left / 1_000_000
     return secs
diff --git a/scripts/build-deps-manylinux.sh b/scripts/build-deps-manylinux.sh
new file mode 100755
index 0000000..3a22eb1
--- /dev/null
+++ b/scripts/build-deps-manylinux.sh
@@ -0,0 +1,14 @@
+#!/bin/bash -xe
+
+# Build libturbojpeg
+pushd /tmp
+curl -L -o libjpeg-turbo.tar.gz https://sourceforge.net/projects/libjpeg-turbo/files/1.5.1/libjpeg-turbo-1.5.1.tar.gz/download
+tar xvzf libjpeg-turbo.tar.gz
+pushd libjpeg-turbo-1.5.1
+./configure --prefix=/usr/local
+make -j
+make install
+popd
+popd
+
+$(dirname "$0")/build-deps-unix.sh
\ No newline at end of file
diff --git a/scripts/build-deps-unix.sh b/scripts/build-deps-unix.sh
new file mode 100755
index 0000000..709b62a
--- /dev/null
+++ b/scripts/build-deps-unix.sh
@@ -0,0 +1,19 @@
+#!/bin/bash -xe
+
+# Build libusb master
+pushd libusb-source
+./bootstrap.sh
+./configure
+make -j
+make install
+popd
+
+BUILD_DIR=libuvc-source/build
+
+rm -rf $BUILD_DIR
+mkdir $BUILD_DIR
+pushd $BUILD_DIR
+cmake ..
+cmake --build .
+cmake --install .
+pushd
diff --git a/scripts/download-deps-win.ps1 b/scripts/download-deps-win.ps1
new file mode 100644
index 0000000..ff1ef97
--- /dev/null
+++ b/scripts/download-deps-win.ps1
@@ -0,0 +1,53 @@
+param ($DEPS_TMP_PATH)
+if ($DEPS_TMP_PATH -eq $null) {
+    Write-Error "DEPS_TMP_PATH param not set"
+    Exit -1
+}
+
+rm _skbuild -Recurse -Confirm:$false
+
+choco install vcredist-all
+
+# libturbojpeg
+Set-Variable -Name "LTJ_VERSION" -Value "2.1.0"
+Set-Variable -Name "LTJ_URL" -Value "https://github.com/pupil-labs/pyndsi/wiki/libjpeg-turbo-${LTJ_VERSION}-vc64.exe"
+Set-Variable -Name "LTJ_INSTALLER" -Value "${DEPS_TMP_PATH}\libjpeg-turbo.exe"
+Set-Variable -Name "LTJ_INSTALL_PATH" -Value "${DEPS_TMP_PATH}\libjpeg-turbo-install"
+
+[System.IO.Directory]::CreateDirectory(${LTJ_INSTALL_PATH})
+Write-Output "Downloading libturbojpeg..."
+Invoke-WebRequest -Uri ${LTJ_URL} -OutFile ${LTJ_INSTALLER}
+Write-Output "Installing libturbojpeg..."
+& 'C:\Program Files\7-Zip\7z.exe' x -aoa ${LTJ_INSTALLER} -o"${LTJ_INSTALL_PATH}"
+
+Set-Variable -Name "LUSB_VERSION" -Value "1.0.26"
+Set-Variable -Name "LUSB_BIN_URL" -Value "https://github.com/libusb/libusb/releases/download/v${LUSB_VERSION}/libusb-${LUSB_VERSION}-binaries.7z"
+Set-Variable -Name "LUSB_SRC_URL" -Value "https://github.com/libusb/libusb/releases/download/v${LUSB_VERSION}/libusb-${LUSB_VERSION}.tar.bz2"
+
+Set-Variable -Name "LUSB_BIN_ARCHIVE" -Value "${DEPS_TMP_PATH}\libusb-bin-archive.7z"
+Set-Variable -Name "LUSB_SRC_ARCHIVE_TAR_BZ2" -Value "${DEPS_TMP_PATH}\libusb-src-archive.tar.bz2"
+Set-Variable -Name "LUSB_SRC_ARCHIVE_TAR" -Value "${DEPS_TMP_PATH}\libusb-src-archive.tar"
+Set-Variable -Name "LUSB_INSTALL_PATH" -Value "${DEPS_TMP_PATH}\libusb-install"
+
+Write-Output "Downloading libusb... ${LUSB_BIN_URL}"
+Invoke-WebRequest -Uri ${LUSB_BIN_URL} -OutFile ${LUSB_BIN_ARCHIVE}
+Write-Output "Downloading libusb... ${LUSB_SRC_URL}"
+Invoke-WebRequest -Uri ${LUSB_SRC_URL} -OutFile ${LUSB_SRC_ARCHIVE_TAR_BZ2}
+
+Write-Output "Extracting libusb..."
+& 'C:\Program Files\7-Zip\7z.exe' x -aoa ${LUSB_BIN_ARCHIVE} -o"${LUSB_INSTALL_PATH}"
+& 'C:\Program Files\7-Zip\7z.exe' x -aoa ${LUSB_SRC_ARCHIVE_TAR_BZ2} -o"${DEPS_TMP_PATH}"
+& 'C:\Program Files\7-Zip\7z.exe' x -aoa ${LUSB_SRC_ARCHIVE_TAR} -o"${LUSB_INSTALL_PATH}"
+
+$dep_paths = @{
+    LIBUSB_WIN_IMPORT_LIB_PATH    = "${LUSB_INSTALL_PATH}/libusb-${LUSB_VERSION}-binaries/VS2015-x64/dll/libusb-1.0.lib"
+    LIBUSB_WIN_DLL_SEARCH_PATH    = "${LUSB_INSTALL_PATH}/libusb-${LUSB_VERSION}-binaries/VS2015-x64/dll"
+    LIBUSB_WIN_INCLUDE_DIR        = "${LUSB_INSTALL_PATH}/libusb-${LUSB_VERSION}/libusb"
+    TURBOJPEG_WIN_IMPORT_LIB_PATH = "${LTJ_INSTALL_PATH}/lib/turbojpeg.lib"
+    TURBOJPEG_WIN_DLL_SEARCH_PATH = "${LTJ_INSTALL_PATH}/bin"
+    TURBOJPEG_WIN_INCLUDE_DIR     = "${LTJ_INSTALL_PATH}/include"
+}
+
+$dep_paths_json = $dep_paths | ConvertTo-Json
+$Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
+[System.IO.File]::WriteAllLines("${DEPS_TMP_PATH}\dep_paths.json", $dep_paths_json, $Utf8NoBomEncoding)
diff --git a/scripts/repair-wheels-win.py b/scripts/repair-wheels-win.py
new file mode 100644
index 0000000..93c5592
--- /dev/null
+++ b/scripts/repair-wheels-win.py
@@ -0,0 +1,55 @@
+import argparse
+import json
+import pathlib
+import shutil
+import subprocess
+
+
+def repair(deps_paths_loc, wheel, dest_dir):
+    import pupil_pthreads_win
+
+    deps_paths = json.loads(pathlib.Path(deps_paths_loc).read_text())
+    dll_search_paths = ";".join(
+        map(
+            str,
+            [
+                pupil_pthreads_win.dll_path.parent,
+                *(
+                    p
+                    for key, p in deps_paths.items()
+                    if key.endswith("DLL_SEARCH_PATH")
+                ),
+            ],
+        )
+    )
+    cmd = [
+        "delvewheel.exe",
+        "repair",
+        "-w",
+        dest_dir,
+        wheel,
+        "--add-path",
+        dll_search_paths,
+    ]
+    out = subprocess.check_output(cmd).decode()
+    print(f"+ {cmd}")
+    print(f"+ delvewheel.exe output:\n{out}")
+    last_line = out.splitlines()[-1]
+
+    # cibuildwheels expects the wheel to be in dest_dir but delvewheel does not copy the
+    # wheel to dest_dir if there is nothing to repair
+    if last_line.startswith("no external dependencies are needed"):
+        print(f"+ Manually copying {wheel} to {dest_dir}")
+        pathlib.Path(dest_dir).mkdir(exist_ok=True)
+        shutil.copy2(wheel, dest_dir)
+    else:
+        print(f"+ No need for a manual copy")
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument("deps_paths_loc")
+    parser.add_argument("wheel")
+    parser.add_argument("dest_dir")
+    args = parser.parse_args()
+    repair(args.deps_paths_loc, args.wheel, args.dest_dir)
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..db5e3e0
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,38 @@
+[metadata]
+name = pupil_labs_uvc
+description = Usb Video Class Device bindings with format conversion tool
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+url = https://github.com/pupil-labs/pyuvc
+author = Pupil Labs GmbH
+author_email = info@pupil-labs.com
+license = MIT
+license_file = LICENSE
+classifiers =
+    Development Status :: 5 - Production/Stable
+    Intended Audience :: Developers
+    License :: OSI Approved :: MIT License
+    Programming Language :: Python :: 3
+    Programming Language :: Python :: 3 :: Only
+
+[options]
+install_requires =
+    numpy
+    importlib-metadata;python_version<"3.8"
+python_requires = >=3.6
+
+[options.extras_require]
+docs =
+    furo
+    jaraco.packaging>=9
+    rst.linker>=1.9
+    sphinx
+example =
+    opencv-python
+    rich
+testing =
+    pytest>=6
+    pytest-checkdocs>=2.4
+    pytest-cov
+    pytest-enabler>=1.3
+    pytest-mypy>=0.9.1;python_implementation != "PyPy"
diff --git a/setup.py b/setup.py
index 32b8d8e..3a7113b 100644
--- a/setup.py
+++ b/setup.py
@@ -1,83 +1,44 @@
-"""
-(*)~----------------------------------------------------------------------------------
- Pupil - eye tracking platform
- Copyright (C) 2012-2015  Pupil Labs
-
- Distributed under the terms of the CC BY-NC-SA License.
- License details are in the file license.txt, distributed as part of this software.
-----------------------------------------------------------------------------------~(*)
-"""
-import glob
+import json
 import os
+import pathlib
 import platform
 
-import numpy
-import pkgconfig
-from Cython.Build import cythonize
-from setuptools import Extension, setup
+from setuptools import find_packages
+from skbuild import setup
 
-extra_link_args = []
-plat_data_files = []
-extra_objects = []
-library_dirs = []
-include_dirs = [numpy.get_include()]
-if platform.system() == "Darwin":
-    configs = pkgconfig.parse("libturbojpeg libuvc")
-    libs = configs["libraries"]
-    include_dirs += configs["include_dirs"]
-    library_dirs += configs["library_dirs"]
-elif platform.system() == "Linux":
-    libs = ["rt", "uvc", "turbojpeg"]
-elif platform.system() == "Windows":
-    pack_dir = ""
-    uvc_dir = "C:\\work\\libuvc"
-    tj_dir = "C:\\work\\libjpeg-turbo-VC64"
-    usb_dir = "C:\\work\\libusb"
-    pthread_dir = "C:\\work\\pthreads-w32-2-9-1-release\\Pre-built.2\\dll\\x64"
+cmake_args = []
+cmake_args.append(f"-DUVC_DEBUGGING={os.environ.get('UVC_DEBUGGING', 'OFF')}")
+cmake_args.append(
+    f"-DFORCE_LOCAL_LIBUVC_BUILD={os.environ.get('FORCE_LOCAL_LIBUVC_BUILD', 'OFF')}"
+)
 
-    tj_lib = tj_dir + "\\lib\\turbojpeg.lib"
-    uvc_lib = uvc_dir + "\\bin\\Release\\uvc.lib"
+if platform.system() == "Windows":
+    import os
 
-    uvc_dll = uvc_dir + "\\bin\\Release\\uvc.dll"
-    usb_dll = usb_dir + "\\x64\Release\\dll\\libusb-1.0.dll"
-    tj_dll = tj_dir + "\\bin\\turbojpeg.dll"
-    jpg_dll = tj_dir + "\\bin\\jpeg62.dll"
-    pthr_dll = pthread_dir + "\\pthreadVC2.dll"
+    import pupil_pthreads_win as ptw
 
-    extra_objects = [tj_lib, uvc_lib]
-    libs = ["winmm"]
-    extra_link_args = []
-    include_dirs += [tj_dir + "\\include"]
-    include_dirs += [usb_dir] + [usb_dir + "\\libusb"]
-    include_dirs += [uvc_dir + "\\include"] + [uvc_dir + "\\bin\\include"]
+    cmake_args.append(f"-DPTHREADS_WIN_INCLUDE_DIR='{ptw.include_path}'")
+    cmake_args.append(f"-DPTHREADS_WIN_IMPORT_LIB_PATH='{ptw.import_lib_path}'")
 
-    plat_data_files = [
-        (pack_dir, [uvc_dll]),
-        (pack_dir, [usb_dll]),
-        (pack_dir, [tj_dll]),
-        (pack_dir, [jpg_dll]),
-        (pack_dir, [pthr_dll]),
-    ]
+    paths_loc = os.environ.get("DEPS_PATHS_LOC")
+    paths = json.loads(pathlib.Path(paths_loc).read_text())
+    for path_name, path_value in paths.items():
+        path_value = pathlib.Path(path_value).resolve()
+        cmake_args.append(f"-D{path_name}='{path_value}'")
 
+    # The Ninja cmake generator will use mingw (gcc) on windows travis instances, but we
+    # need to use msvc for compatibility. The easiest solution I found was to just use
+    # the vs cmake generator as it defaults to msvc.
+    cmake_args.append("-GVisual Studio 17 2022")
+    cmake_args.append("-DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS=True")
 
-extensions = [
-    Extension(
-        name="uvc",
-        sources=["uvc.pyx"],
-        include_dirs=include_dirs,
-        library_dirs=library_dirs,
-        libraries=libs,
-        extra_link_args=extra_link_args,
-        extra_objects=extra_objects,
-        extra_compile_args=[],
-    )
-]
+pyuvc_source_folder = "pyuvc-source"
 
 setup(
-    name="uvc",
-    version="0.15.0",
-    description="Usb Video Class Device bindings with format conversion tool.",
-    install_requires=["numpy"],
-    ext_modules=cythonize(extensions),
-    data_files=plat_data_files,
+    packages=find_packages(where=pyuvc_source_folder),
+    package_dir={"": pyuvc_source_folder},
+    include_package_data=False,
+    cmake_source_dir=pyuvc_source_folder,
+    cmake_install_dir=pyuvc_source_folder + "/uvc",
+    cmake_args=cmake_args,
 )
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..b58c837
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,37 @@
+[tox]
+envlist = python
+minversion = 3.2
+# https://github.com/jaraco/skeleton/issues/6
+tox_pip_extensions_ext_venv_update = true
+toxworkdir={env:TOX_WORK_DIR:.tox}
+isolated_build=true
+
+[testenv]
+deps = pytest
+commands =
+	pytest tests {posargs}
+usedevelop = False
+extras = testing
+
+[testenv:docs]
+extras =
+	docs
+	testing
+changedir = docs
+commands =
+	python -m sphinx -W --keep-going . {toxinidir}/build/html
+
+[testenv:release]
+skip_install = True
+deps =
+	build
+	twine>=3
+	jaraco.develop>=7.1
+passenv =
+	TWINE_PASSWORD
+	GITHUB_TOKEN
+setenv =
+	TWINE_USERNAME = {env:TWINE_USERNAME:__token__}
+commands =
+	python -m twine upload dist/*
+	python -m jaraco.develop.create-github-release


Test patch:
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 0000000..845aec0
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,5 @@
+[pytest]
+norecursedirs=dist build .tox .eggs
+addopts=--doctest-modules
+doctest_optionflags=ALLOW_UNICODE ELLIPSIS
+filterwarnings=
diff --git a/scripts/build-and-test.sh b/scripts/build-and-test.sh
new file mode 100755
index 0000000..e7a46d4
--- /dev/null
+++ b/scripts/build-and-test.sh
@@ -0,0 +1,29 @@
+#!/bin/bash -xe
+
+if [ "$1" == "clean" ]
+then
+    git clean -dxf -e .venv/ -e .venv-dbg/ -e .tox/ -e uvc_bindings.pyx
+fi
+
+export PKG_CONFIG_PATH=/opt/homebrew/opt/jpeg-turbo/lib/pkgconfig:$PKG_CONFIG_PATH
+export PKG_CONFIG_PATH=/Users/ppr/work/pyuvc/libusb-source/libusb-install/lib/pkgconfig:$PKG_CONFIG_PATH
+
+export FORCE_LOCAL_LIBUVC_BUILD=ON
+export UVC_DEBUGGING=OFF
+
+DIRECT_INSTALL=true
+BUILD_WHEEL=true
+
+if [ "$DIRECT_INSTALL" = true ]
+then
+    python -m pip install -v ".[example]" --no-build-isolation
+elif [ "$BUILD_WHEEL" = true ]
+then
+    python -m build . --wheel -n
+    delocate-wheel -v dist/*.whl
+    pip install -v "dist/*.whl[example]" --force-reinstall
+else
+    python -m build . --sdist -n
+    pip install -vv "dist/*.tar.gz[example]" --force-reinstall
+fi
+# sudo python examples/access_100_frames.py
diff --git a/tests/test_api.py b/tests/test_api.py
new file mode 100644
index 0000000..65437ca
--- /dev/null
+++ b/tests/test_api.py
@@ -0,0 +1,5 @@
+import uvc as this_project
+
+
+def test_package_metadata() -> None:
+    assert hasattr(this_project, "__version__")