From 972b16b54cab0b7c6423bde78c545d5747171c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Benqu=C3=A9?= Date: Sun, 25 Apr 2021 20:59:54 +0100 Subject: [PATCH] initial commit - re-starting version control fresh - old repo had video files committed + node modules at one point that made it much larger than needed --- .gitignore | 3 + README.md | 51 +++++ TODO.md | 56 ++++++ poetry.lock | 169 +++++++++++++++++ py/.idea/.gitignore | 3 + .../inspectionProfiles/profiles_settings.xml | 6 + py/.idea/misc.xml | 7 + py/.idea/modules.xml | 8 + py/.idea/py.iml | 13 ++ py/.idea/vcs.xml | 6 + py/WM_utils.py | 86 +++++++++ py/assets/icon_reload.png | Bin 0 -> 1605 bytes py/dist/WM.nsi | 123 ++++++++++++ py/dist/inc/License.txt | 10 + py/dist/inc/Steps-Mono.ttf | Bin 0 -> 17260 bytes py/dist/inc/logo.ico | Bin 0 -> 107709 bytes py/examples_demos/font_demo.py | 37 ++++ py/examples_demos/font_list.py | 7 + py/examples_demos/pyvirtualcam_example.py | 10 + py/webcam_manager.py | 177 ++++++++++++++++++ pyproject.toml | 19 ++ 21 files changed, 791 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 TODO.md create mode 100644 poetry.lock create mode 100644 py/.idea/.gitignore create mode 100644 py/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 py/.idea/misc.xml create mode 100644 py/.idea/modules.xml create mode 100644 py/.idea/py.iml create mode 100644 py/.idea/vcs.xml create mode 100644 py/WM_utils.py create mode 100644 py/assets/icon_reload.png create mode 100644 py/dist/WM.nsi create mode 100644 py/dist/inc/License.txt create mode 100644 py/dist/inc/Steps-Mono.ttf create mode 100644 py/dist/inc/logo.ico create mode 100644 py/examples_demos/font_demo.py create mode 100644 py/examples_demos/font_list.py create mode 100644 py/examples_demos/pyvirtualcam_example.py create mode 100644 py/webcam_manager.py create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b439c37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +static/ +__pycache__ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a90a1c7 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# webcam_manager + +en avant les commits! + + +## Installation + +We are looking to package this as a bundled application, in the meantime to install and run the Python code: + +### Python dependencies + +See `pyproject.toml` for requirements to install. + +### Linux + +On linux [v4l2loopback](https://github.com/umlaeute/v4l2loopback) is required to create a virtual webcam that can be used in call software. + +``` +sudo apt install v4l2loopback-dkms +``` + +The program starts the service automatically (this is why it asks for the sudo password at startup) to start manually: + +``` +sudo modprobe v4l2loopback devices=1 +``` + +### Windows + +On Windows OBS is required + +add details here + +### MacOS + +Coming soon... + +## Usage + +### Connect to call software + +### Recording and looping + +- buttons +- keybaord shortcuts + + + + + + diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..1836850 --- /dev/null +++ b/TODO.md @@ -0,0 +1,56 @@ + +# TODO + +- [ ] V4L2 warnings on Linux + - 9 warnings so I guess one for each camera that is not found (we test for 10 devices in `camera_indexes()`) + +- [ ] Stealth button names/colors + - icons only? see [this](https://pysimplegui.readthedocs.io/en/latest/cookbook/#use-with-buttons) + +- [ ] Start modprobe service automatically from Python? + - probably can call a `subprocess` or something. Probably check first if the `v4l2loopback` package exist, and if not display the command to execute? + +- [ ] Package Font? + - Title using [Steps Mono](https://www.velvetyne.fr/fonts/steps-mono/download/) libre font from Velvetyne. + - Can we embed it/package it? + - let's see how it goes when we use `pyinstaller`. As far as I understand `pysimplegui` uses `tkinter` to manage fonts, so checking how tkinter does it might give us a clue. + - https://stackoverflow.com/questions/63585632/how-to-add-a-truetype-font-file-to-a-pyinstaller-executable-for-use-with-pygame + +- [ ] How do we package the rest anyway? + - Cookiecutter: https://cookiecutter.readthedocs.io/en/1.7.2/index.html + + - package FFMPEG builds and include in pyinstaller +https://stackoverflow.com/a/57081121 +https://stackoverflow.com/a/60822647 + +## DONE + +- [x] Keyboard shortcuts + - ~~to easily start/stop recording: space bar~~ + - ~~start/stop loop: enter?~~ + - Done and tested on Windows!! + +- [x] Added device selector for the camera, to be tested elsewhere + +- [x] Added a flush function + - Empties the video recorded in static at the start of the application + - Added a `Flush` button next the `Exit` + +- [x] Pick normcore theme + +- [x] create `static/` if it doesn't exist. + +- [x] Output of virtualcam (e.g. previewing in Jitsi) has weird smurf colors. + - ~~preview in window is fine~~ + - ~~recorded output file was fine~~ + - preview in window fine, recorded out fine, preview in Zoom fine. + +- [x] Are we downsizing the video resolution? why not use best res available? (line 57 see # XXX) + - we was, now we're webcam native size + +- [x] Click on LOOP after launch before recording crashes + - ~~`variable referenced before assignment`~~ + - fixed. If you click LOOP first and there's no static videos, the button gets disabled. Also added colours and disabled buttons + +- [x] Stop saving video files locally? + - or at least add to `.gitignore`? diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..b2e39ce --- /dev/null +++ b/poetry.lock @@ -0,0 +1,169 @@ +[[package]] +name = "imutils" +version = "0.5.4" +description = "A series of convenience functions to make basic image processing functions such as translation, rotation, resizing, skeletonization, displaying Matplotlib images, sorting contours, detecting edges, and much more easier with OpenCV and both Python 2.7 and Python 3." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "numpy" +version = "1.20.1" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "opencv-contrib-python" +version = "4.5.1.48" +description = "Wrapper package for OpenCV python bindings." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +numpy = ">=1.17.3" + +[[package]] +name = "pillow" +version = "8.1.2" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pysimplegui" +version = "4.37.0" +description = "Python GUIs for Humans. Launched in 2018. It's 2021 & PySimpleGUI is an ACTIVE project. Super-simple to create custom GUI's. 300 Demo programs & Cookbook for rapid start. Extensive documentation. Main docs at www.PySimpleGUI.org. Your success is the focus. Examples using Machine Learning (GUI, OpenCV Integration, Chatterbot), Rainmeter Style Desktop Widgets, Matplotlib + Pyplot, PIL support, add GUI to command line scripts, PDF & Image Viewers. Great for beginners & advanced GUI programmers" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pyvirtualcam" +version = "0.5.0" +description = "Send frames to a virtual camera" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +numpy = "*" + +[metadata] +lock-version = "1.1" +python-versions = "^3.8" +content-hash = "00f019a9f36d6b628f19b4128196043431e52383c1a40a8af2bd3b22e6a95073" + +[metadata.files] +imutils = [ + {file = "imutils-0.5.4.tar.gz", hash = "sha256:03827a9fca8b5c540305c0844a62591cf35a0caec199cb0f2f0a4a0fb15d8f24"}, +] +numpy = [ + {file = "numpy-1.20.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ae61f02b84a0211abb56462a3b6cd1e7ec39d466d3160eb4e1da8bf6717cdbeb"}, + {file = "numpy-1.20.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:65410c7f4398a0047eea5cca9b74009ea61178efd78d1be9847fac1d6716ec1e"}, + {file = "numpy-1.20.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:2d7e27442599104ee08f4faed56bb87c55f8b10a5494ac2ead5c98a4b289e61f"}, + {file = "numpy-1.20.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4ed8e96dc146e12c1c5cdd6fb9fd0757f2ba66048bf94c5126b7efebd12d0090"}, + {file = "numpy-1.20.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ecb5b74c702358cdc21268ff4c37f7466357871f53a30e6f84c686952bef16a9"}, + {file = "numpy-1.20.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b9410c0b6fed4a22554f072a86c361e417f0258838957b78bd063bde2c7f841f"}, + {file = "numpy-1.20.1-cp37-cp37m-win32.whl", hash = "sha256:3d3087e24e354c18fb35c454026af3ed8997cfd4997765266897c68d724e4845"}, + {file = "numpy-1.20.1-cp37-cp37m-win_amd64.whl", hash = "sha256:89f937b13b8dd17b0099c7c2e22066883c86ca1575a975f754babc8fbf8d69a9"}, + {file = "numpy-1.20.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a1d7995d1023335e67fb070b2fae6f5968f5be3802b15ad6d79d81ecaa014fe0"}, + {file = "numpy-1.20.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:60759ab15c94dd0e1ed88241fd4fa3312db4e91d2c8f5a2d4cf3863fad83d65b"}, + {file = "numpy-1.20.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:125a0e10ddd99a874fd357bfa1b636cd58deb78ba4a30b5ddb09f645c3512e04"}, + {file = "numpy-1.20.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c26287dfc888cf1e65181f39ea75e11f42ffc4f4529e5bd19add57ad458996e2"}, + {file = "numpy-1.20.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:7199109fa46277be503393be9250b983f325880766f847885607d9b13848f257"}, + {file = "numpy-1.20.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:72251e43ac426ff98ea802a931922c79b8d7596480300eb9f1b1e45e0543571e"}, + {file = "numpy-1.20.1-cp38-cp38-win32.whl", hash = "sha256:c91ec9569facd4757ade0888371eced2ecf49e7982ce5634cc2cf4e7331a4b14"}, + {file = "numpy-1.20.1-cp38-cp38-win_amd64.whl", hash = "sha256:13adf545732bb23a796914fe5f891a12bd74cf3d2986eed7b7eba2941eea1590"}, + {file = "numpy-1.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:104f5e90b143dbf298361a99ac1af4cf59131218a045ebf4ee5990b83cff5fab"}, + {file = "numpy-1.20.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:89e5336f2bec0c726ac7e7cdae181b325a9c0ee24e604704ed830d241c5e47ff"}, + {file = "numpy-1.20.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:032be656d89bbf786d743fee11d01ef318b0781281241997558fa7950028dd29"}, + {file = "numpy-1.20.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:66b467adfcf628f66ea4ac6430ded0614f5cc06ba530d09571ea404789064adc"}, + {file = "numpy-1.20.1-cp39-cp39-win32.whl", hash = "sha256:12e4ba5c6420917571f1a5becc9338abbde71dd811ce40b37ba62dec7b39af6d"}, + {file = "numpy-1.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:9c94cab5054bad82a70b2e77741271790304651d584e2cdfe2041488e753863b"}, + {file = "numpy-1.20.1-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:9eb551d122fadca7774b97db8a112b77231dcccda8e91a5bc99e79890797175e"}, + {file = "numpy-1.20.1.zip", hash = "sha256:3bc63486a870294683980d76ec1e3efc786295ae00128f9ea38e2c6e74d5a60a"}, +] +opencv-contrib-python = [ + {file = "opencv-contrib-python-4.5.1.48.tar.gz", hash = "sha256:74ebf353d7e1666066265922153a0f60fff9e1dd603f5929b13a99415363f078"}, + {file = "opencv_contrib_python-4.5.1.48-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:74a866acca3fddb4666afa38ecf2cadfa6df319c62e5dc511760acb13caade5b"}, + {file = "opencv_contrib_python-4.5.1.48-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:6afc58493e085956e548e3cb748df2b420b5b4d8a37d57f7945d7e54262d7526"}, + {file = "opencv_contrib_python-4.5.1.48-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:ff62b4f0f4d3431fd13fc1eda0ec6be493ffa5e208f054d5933851987f3fabb6"}, + {file = "opencv_contrib_python-4.5.1.48-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:2871c8c243c1a5217b8727c21fd347ec2ae755e39d52dd268bf8f01515b6e8d1"}, + {file = "opencv_contrib_python-4.5.1.48-cp36-cp36m-win32.whl", hash = "sha256:11167768a6c643cd6a03158152ee73b62c8d4906e3bde2ed1383099b93ce14df"}, + {file = "opencv_contrib_python-4.5.1.48-cp36-cp36m-win_amd64.whl", hash = "sha256:529a770bad6d01f59eab9065e73af3416492ebeac2d86f69ce603a6e922f021e"}, + {file = "opencv_contrib_python-4.5.1.48-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:24e4a770908daf1718830c6059bbfe7fb4d5ff0dcb1253416cbe17d365889afe"}, + {file = "opencv_contrib_python-4.5.1.48-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:8c9f350ebc8261c7b4a7d61d2cb22b94fb5cad5f1a0a7bf69bcea55886d98c90"}, + {file = "opencv_contrib_python-4.5.1.48-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:9453fbad079d65b1987a963b3986bd283ff31d2ee04d349a538ba4e383ba0399"}, + {file = "opencv_contrib_python-4.5.1.48-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:4f38147203216182622844ce9af539af0e3d28ba285904319fb479b0f71943a5"}, + {file = "opencv_contrib_python-4.5.1.48-cp37-cp37m-win32.whl", hash = "sha256:703dc1a1f65aed065d4b5ef31756718f709a5d8fde69c110c0d637121ac3aee9"}, + {file = "opencv_contrib_python-4.5.1.48-cp37-cp37m-win_amd64.whl", hash = "sha256:83681ee941e4b8b13c7023589b3354f07343b051df9cd668aae78d97bac9fa2e"}, + {file = "opencv_contrib_python-4.5.1.48-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:1034b252b3d7ec5d07bf85f3a5f91fe6d3172dedaf7c4c3474848c6204d0f0f1"}, + {file = "opencv_contrib_python-4.5.1.48-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:28e3562d8421a47d08e388cb19c05df9f055a604009d515ffd118cf1b459560a"}, + {file = "opencv_contrib_python-4.5.1.48-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:b16aa1a5fc2fae8faf360667016592b5862489d106052a4aa01458bea775e6d2"}, + {file = "opencv_contrib_python-4.5.1.48-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:aef0e18485250500059d9a08db1f3b8f1a0dda53069a011a5e9c24c01dcb5627"}, + {file = "opencv_contrib_python-4.5.1.48-cp38-cp38-win32.whl", hash = "sha256:9ae70b05d9fdb61647d0fca7f23c0132eb9e245c5671637bcfbcc85a3efa131b"}, + {file = "opencv_contrib_python-4.5.1.48-cp38-cp38-win_amd64.whl", hash = "sha256:6f577039e42e6255cd2f2a495e0d55b6cd283d677905c974824d575e9dc57b42"}, + {file = "opencv_contrib_python-4.5.1.48-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:8b02e54f591ee0eabfae1f3fe14852821020e04a0c39af8e05a78a426be1a353"}, + {file = "opencv_contrib_python-4.5.1.48-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:e2f058b19a0cdaaee5c09aefa32068d3609f55ea86a37ade48525f8b8d63fad9"}, + {file = "opencv_contrib_python-4.5.1.48-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:12aa410897d47cf9bb63fbfd0513a645487982e77b3542ee84133d00d7851691"}, + {file = "opencv_contrib_python-4.5.1.48-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:9ba6bdb54af0963adb92d7d20ec10b5ec9d319d10ecd3796787b058fccad789b"}, + {file = "opencv_contrib_python-4.5.1.48-cp39-cp39-win32.whl", hash = "sha256:11ebe4d5ca0af69744c73d6e28560c48d6cbc65b256574d5d47f24b99923878f"}, + {file = "opencv_contrib_python-4.5.1.48-cp39-cp39-win_amd64.whl", hash = "sha256:c5a523ce046f4405da8c177c11ee614e815efd8751a8ace545098b5768246628"}, +] +pillow = [ + {file = "Pillow-8.1.2-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:5cf03b9534aca63b192856aa601c68d0764810857786ea5da652581f3a44c2b0"}, + {file = "Pillow-8.1.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f91b50ad88048d795c0ad004abbe1390aa1882073b1dca10bfd55d0b8cf18ec5"}, + {file = "Pillow-8.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5762ebb4436f46b566fc6351d67a9b5386b5e5de4e58fdaa18a1c83e0e20f1a8"}, + {file = "Pillow-8.1.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:e2cd8ac157c1e5ae88b6dd790648ee5d2777e76f1e5c7d184eaddb2938594f34"}, + {file = "Pillow-8.1.2-cp36-cp36m-win32.whl", hash = "sha256:72027ebf682abc9bafd93b43edc44279f641e8996fb2945104471419113cfc71"}, + {file = "Pillow-8.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d1d6bca39bb6dd94fba23cdb3eeaea5e30c7717c5343004d900e2a63b132c341"}, + {file = "Pillow-8.1.2-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:90882c6f084ef68b71bba190209a734bf90abb82ab5e8f64444c71d5974008c6"}, + {file = "Pillow-8.1.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:89e4c757a91b8c55d97c91fa09c69b3677c227b942fa749e9a66eef602f59c28"}, + {file = "Pillow-8.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:8c4e32218c764bc27fe49b7328195579581aa419920edcc321c4cb877c65258d"}, + {file = "Pillow-8.1.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a01da2c266d9868c4f91a9c6faf47a251f23b9a862dce81d2ff583135206f5be"}, + {file = "Pillow-8.1.2-cp37-cp37m-win32.whl", hash = "sha256:30d33a1a6400132e6f521640dd3f64578ac9bfb79a619416d7e8802b4ce1dd55"}, + {file = "Pillow-8.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:71b01ee69e7df527439d7752a2ce8fb89e19a32df484a308eca3e81f673d3a03"}, + {file = "Pillow-8.1.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:5a2d957eb4aba9d48170b8fe6538ec1fbc2119ffe6373782c03d8acad3323f2e"}, + {file = "Pillow-8.1.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:87f42c976f91ca2fc21a3293e25bd3cd895918597db1b95b93cbd949f7d019ce"}, + {file = "Pillow-8.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:15306d71a1e96d7e271fd2a0737038b5a92ca2978d2e38b6ced7966583e3d5af"}, + {file = "Pillow-8.1.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:71f31ee4df3d5e0b366dd362007740106d3210fb6a56ec4b581a5324ba254f06"}, + {file = "Pillow-8.1.2-cp38-cp38-win32.whl", hash = "sha256:98afcac3205d31ab6a10c5006b0cf040d0026a68ec051edd3517b776c1d78b09"}, + {file = "Pillow-8.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:328240f7dddf77783e72d5ed79899a6b48bc6681f8d1f6001f55933cb4905060"}, + {file = "Pillow-8.1.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bead24c0ae3f1f6afcb915a057943ccf65fc755d11a1410a909c1fefb6c06ad1"}, + {file = "Pillow-8.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:81b3716cc9744ffdf76b39afb6247eae754186838cedad0b0ac63b2571253fe6"}, + {file = "Pillow-8.1.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:63cd413ac52ee3f67057223d363f4f82ce966e64906aea046daf46695e3c8238"}, + {file = "Pillow-8.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:8565355a29655b28fdc2c666fd9a3890fe5edc6639d128814fafecfae2d70910"}, + {file = "Pillow-8.1.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:1940fc4d361f9cc7e558d6f56ff38d7351b53052fd7911f4b60cd7bc091ea3b1"}, + {file = "Pillow-8.1.2-cp39-cp39-win32.whl", hash = "sha256:46c2bcf8e1e75d154e78417b3e3c64e96def738c2a25435e74909e127a8cba5e"}, + {file = "Pillow-8.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:aeab4cd016e11e7aa5cfc49dcff8e51561fa64818a0be86efa82c7038e9369d0"}, + {file = "Pillow-8.1.2-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:74cd9aa648ed6dd25e572453eb09b08817a1e3d9f8d1bd4d8403d99e42ea790b"}, + {file = "Pillow-8.1.2-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:e5739ae63636a52b706a0facec77b2b58e485637e1638202556156e424a02dc2"}, + {file = "Pillow-8.1.2-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:903293320efe2466c1ab3509a33d6b866dc850cfd0c5d9cc92632014cec185fb"}, + {file = "Pillow-8.1.2-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:5daba2b40782c1c5157a788ec4454067c6616f5a0c1b70e26ac326a880c2d328"}, + {file = "Pillow-8.1.2-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:1f93f2fe211f1ef75e6f589327f4d4f8545d5c8e826231b042b483d8383e8a7c"}, + {file = "Pillow-8.1.2-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:6efac40344d8f668b6c4533ae02a48d52fd852ef0654cc6f19f6ac146399c733"}, + {file = "Pillow-8.1.2-pp37-pypy37_pp73-win32.whl", hash = "sha256:f36c3ff63d6fc509ce599a2f5b0d0732189eed653420e7294c039d342c6e204a"}, + {file = "Pillow-8.1.2.tar.gz", hash = "sha256:b07c660e014852d98a00a91adfbe25033898a9d90a8f39beb2437d22a203fc44"}, +] +pysimplegui = [ + {file = "PySimpleGUI-4.37.0-py3-none-any.whl", hash = "sha256:ff1ffa7fb73a5665b299426facc2522197977732fb69d27becf9fca3c4d62ed3"}, + {file = "PySimpleGUI-4.37.0.tar.gz", hash = "sha256:c5764fa58f4c9a9b25b36fb0133fc8893a2e2c8a9da1f96a268378eb54e42beb"}, +] +pyvirtualcam = [ + {file = "pyvirtualcam-0.5.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bfc5d65657c710f34f1bdc76618532500da39bead0573c686449469024f6a352"}, + {file = "pyvirtualcam-0.5.0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:5a3a5fee93ae4f289513a5de2ab45b799d46d1d10397d6f0cb24424368129694"}, + {file = "pyvirtualcam-0.5.0-cp36-cp36m-win_amd64.whl", hash = "sha256:b243a3290df84dc4f92280660f89413fd4078356f6b11c284e0c3612ac81b93f"}, + {file = "pyvirtualcam-0.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b36b1548ad1c540c27dc781315d9b702f7e88800c38a1997e062ece25eb814d0"}, + {file = "pyvirtualcam-0.5.0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:a51a4caf48803c8a3684e665796c8b4ced73046457da3183c008076c65c52967"}, + {file = "pyvirtualcam-0.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e049fa89407a928827b4fbe9e15ad2c83c8b718adc68c66ed2a1fa408e2c8599"}, + {file = "pyvirtualcam-0.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1e2bc1ad0a60cf32a361c5e87f46abae2236678254da49e7e6b0da04c12ad419"}, + {file = "pyvirtualcam-0.5.0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:e40d54dd38a28f12796a11c1f2c1770859b908f0bbeabb81b93c79a4e83eb33c"}, + {file = "pyvirtualcam-0.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:de577dfb7d84f31c9526adf911fb09ebc2efebbf4e47c7781c7fa85dcef6b8b7"}, + {file = "pyvirtualcam-0.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:21484ccc29ec433f6c8c8e4336287d41d0cd41791901dcbada482d874efac769"}, + {file = "pyvirtualcam-0.5.0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:f66236c2adf6b17c35b11f1290869477fe85d5b55526eceb8d54d4b95f3c888c"}, + {file = "pyvirtualcam-0.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:e665f108b8d4ebb899f8b24c263c33b225c120df863e63c4bfc911c68d5705dd"}, +] diff --git a/py/.idea/.gitignore b/py/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/py/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/py/.idea/inspectionProfiles/profiles_settings.xml b/py/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/py/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/py/.idea/misc.xml b/py/.idea/misc.xml new file mode 100644 index 0000000..032f8f0 --- /dev/null +++ b/py/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/py/.idea/modules.xml b/py/.idea/modules.xml new file mode 100644 index 0000000..3a65488 --- /dev/null +++ b/py/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/py/.idea/py.iml b/py/.idea/py.iml new file mode 100644 index 0000000..dee7e42 --- /dev/null +++ b/py/.idea/py.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/py/.idea/vcs.xml b/py/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/py/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/py/WM_utils.py b/py/WM_utils.py new file mode 100644 index 0000000..e623d4a --- /dev/null +++ b/py/WM_utils.py @@ -0,0 +1,86 @@ +import PIL.Image +import io +import cv2 +import shutil +import os +import glob + +def camera_indexes(): + ''' + grab the available cam indexes + checks the first 10 indexes. + ''' + index = 0 + arr = [] + i = 10 + while i > 0: + cap = cv2.VideoCapture(index) + if cap.read()[0]: + arr.append(index) + cap.release() + index += 1 + i -= 1 + return arr + +def flushvideofiles(): + ''' Delete all video files accumulated from previous seshs''' + dir_path = 'static' + try: + shutil.rmtree(dir_path) + os.mkdir(dir_path) + except OSError as e: + print("Error: %s : %s" % (dir_path, e.strerror)) + +def isstaticfolderempty(): + '''check if the static/ folder already contains files''' + if len(os.listdir('static/')) == 0: + print("Directory is empty") + filename = True + else: + print("Directory is not empty") + list_of_files = glob.glob('static/*') # * means all if need specific format then *.csv + latest_file = max(list_of_files, key=os.path.getctime) + filename = latest_file + return filename + +def resize(image,window_width = 500): + ''' + Resize an image to the supplied width while preserving aspect ratio + Adjusted (flipped) from here: https://stackoverflow.com/a/53631990 + ''' + aspect_ratio = float(image.shape[0])/float(image.shape[1]) + window_height = window_width*aspect_ratio + image = cv2.resize(image, (int(window_width),int(window_height))) + return image + +def convert_to_bytes(file_or_bytes, resize=None): + ''' + Copied from https://pysimplegui.readthedocs.io/en/latest/cookbook/ + + Will convert into bytes and optionally resize an image that is a file or a base64 bytes object. + Turns into PNG format in the process so that can be displayed by tkinter + :param file_or_bytes: either a string filename or a bytes base64 image object + :type file_or_bytes: (Union[str, bytes]) + :param resize: optional new size + :type resize: (Tuple[int, int] or None) + :return: (bytes) a byte-string object + :rtype: (bytes) + ''' + if isinstance(file_or_bytes, str): + img = PIL.Image.open(file_or_bytes) + else: + try: + img = PIL.Image.open(io.BytesIO(base64.b64decode(file_or_bytes))) + except Exception as e: + dataBytesIO = io.BytesIO(file_or_bytes) + img = PIL.Image.open(dataBytesIO) + + cur_width, cur_height = img.size + if resize: + new_width, new_height = resize + scale = min(new_height/cur_height, new_width/cur_width) + img = img.resize((int(cur_width*scale), int(cur_height*scale)), PIL.Image.ANTIALIAS) + bio = io.BytesIO() + img.save(bio, format="PNG") + del img + return bio.getvalue() \ No newline at end of file diff --git a/py/assets/icon_reload.png b/py/assets/icon_reload.png new file mode 100644 index 0000000000000000000000000000000000000000..4103791f9d168b895371d70766a3877c73b1c28b GIT binary patch literal 1605 zcma)-Yg7^j9L1T+LP=*nhAD}dW<**>Ix+M8rgUOvCi96IRwKlBT8d^1nGZBDg;)id zNoZz;=~7H5nPt*LbhOC8bm#y(M@^ff-zTF==AKUMBasYxGdcMr5o#%pyW6$nh;3^$&*LG_*X4!L8O8f% zZH&Cp{Kr|ywG9RPTA#*sQRU=H;-L52Ml{tmU@Zl`oO0hSjUO7m=ArYs>H;aIDS#)J zDeshxf0Zi1?XmT2Ibx4D!)He~yo=f{ZgGNQ_=TN_C$Y+B8>w7t}q zd4$qSb`!k!IZf=8YOIzk0?NH2t-@%I+DR`J7W`SjYRP5RM320mjA@pu`+=!eN6B*H zImXCcBP~XL=lH8Uewm%w`)FFLw|AF>p-J2E+Jv)V3Y2)(NAG_xl_06-!Sl|Z2KT~c zI|<|Q)Izr;4f=WCVULPGlNY@YMi#mr)R-=9{*H{t+zwq=6Oo*=ZK5KyHWymgZiK*+ z?_n!rzrj-Fm?dYIb$2U1<4mB(s?%V_2ugB&X8w%D#z$FKqj{jyKK^x<7IeFYND_hq zKl6xVS#x<;+Z`uSZJ*-z3ia6cvu$qscPfyDm?95k5*1^$UKjZXK)VhL z_{CF_8(HI7_wk|*Cd-nfN#CyK?lMqt^N{5Fg3&L@N;f4vf;!FBi` zB}MJQhF$kl5)Mlu)vg22OUahd%>$X17JAS$!w7MG`;~f=3dRVXV^~3`{Pf%*7vlM7 zk~s+sw43Tkch~gb66){QDghl~6| zZb}`GP$1&htUUt{FjIqqu(?$$52m!qm{Hwm-?o6tSvvmAe5JIwP3IohAitawvh`(p z#=Y0eeTW%S)!&yY_lRPNZ6A!iLezIM$9clU5%S8Gp;h|yppZehq(W{n+vA>LT+M%m zgri$p^L$b28$QFH^K@W~O|W6-d{vSGZ2g#@^OkJUX`yEC+v#IU`+}-e=#FC|bGN^v z2cBF%pBUIafwzk>t1C|d-5mM0IlRyoXCWB&2~TvRr1rH<`ZXjrsf}w`&KKvk4fS#{ zIv|I@)6q3a>^Rvd6c}`UYNkIKbTcPj!KfWO9(jU;1BOlembEBcJ~cVyh5mL6O@=_b z9c1ErT%!MlkxYsdy|HQ`eLU{s2(5(km(>q6yFhE!V;ViEacs7h3zvNc2V!u6v-|UV zfy(fObk8QPh~aScO%eMx%#}LVttNu7#I4+^5`+iH{>rupM1w?}9XkEL+H`f?Np;~v oHIz7g&hmcDnU<(ts(_^jWaH1r%;3>C*LDFA5fT+F2uz{;1Bq%62mk;8 literal 0 HcmV?d00001 diff --git a/py/dist/WM.nsi b/py/dist/WM.nsi new file mode 100644 index 0000000..4cc886a --- /dev/null +++ b/py/dist/WM.nsi @@ -0,0 +1,123 @@ +!include "FontRegAdv.nsh" +!include "FontName.nsh" + +;NSIS Macros + + + + +!define PRODUCT_NAME "Webcam Manager" +!define EXE_NAME "webcam_manager.exe" +!define PRODUCT_VERSION "1.0" +!define PRODUCT_PUBLISHER "Webcam Managers" +!define PRODUCT_LEGAL "Webcam Managers 2021-3021" +!define TEMP_DIR "" + +;-------------------------------- +;Include Modern UI + + !include "MUI2.nsh" + + +;-------------------------------- +;General + + ;Name and file + Name "Webcam Manager" + OutFile "WebcamManager_installer.exe" + ;Unicode True + + ;Default installation folder + InstallDir "$PROGRAMFILES\Webcam_Manager" + + ;Get installation folder from registry if available + ;InstallDirRegKey HKCU "Software\Modern UI Test" "" + + ;Request application privileges for Windows Vista + ;RequestExecutionLevel user + + !define MUI_ICON "inc\logo.ico" + !define MUI_FINISHPAGE_RUN "$INSTDIR\webcam_manager.exe" + +;-------------------------------- +;Interface Settings + + !define MUI_ABORTWARNING + +;-------------------------------- +;Pages + + !insertmacro MUI_PAGE_LICENSE "inc\License.txt" + !insertmacro MUI_PAGE_DIRECTORY + !insertmacro MUI_PAGE_COMPONENTS + !insertmacro MUI_PAGE_INSTFILES + !insertmacro MUI_PAGE_FINISH + + + !insertmacro MUI_UNPAGE_CONFIRM + !insertmacro MUI_UNPAGE_INSTFILES + +;-------------------------------- +;Languages + + !insertmacro MUI_LANGUAGE "English" + +;-------------------------------- +;Installer Sections + +Section "Fonts" fonts + StrCpy $FONT_DIR $FONTS + !insertmacro InstallTTF "inc\Steps-Mono.ttf" + SendMessage ${HWND_BROADCAST} ${WM_FONTCHANGE} 0 0 /TIMEOUT=5000 +SectionEnd + +Section "OBS Studio" Prerequisites + + SetOutPath "$INSTDIR" + + MessageBox MB_YESNO "Install OBS?" /SD IDYES IDNO endOBS + File "inc\OBS-Studio-26.1.1-Full-Installer-x64.exe" + ExecWait "$INSTDIR\OBS-Studio-26.1.1-Full-Installer-x64.exe" + Goto endOBS + endOBS: +SectionEnd + +Section "Webcam Manager" WebMan + + ;ADD YOUR OWN FILES HERE... + File "webcam_manager.exe" + + ;Create uninstaller + WriteUninstaller "$INSTDIR\Uninstall.exe" + +SectionEnd + +;-------------------------------- +;Descriptions + + ;Language strings + LangString DESC_fonts ${LANG_ENGLISH} "Necessary GUI Fonts" + LangString DESC_Prerequisites ${LANG_ENGLISH} "Relies on OBS virtual camera." + LangString DESC_WebMan ${LANG_ENGLISH} "Manage your webcam" + + ;Assign language strings to sections + !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN + !insertmacro MUI_DESCRIPTION_TEXT ${fonts} $(DESC_fonts) + !insertmacro MUI_DESCRIPTION_TEXT ${Prerequisites} $(DESC_Prerequisites) + !insertmacro MUI_DESCRIPTION_TEXT ${WebMan} $(DESC_WebMan) + !insertmacro MUI_FUNCTION_DESCRIPTION_END + +;-------------------------------- +;Uninstaller Section + +Section "Uninstall" + + ;ADD YOUR OWN FILES HERE... + + Delete "$INSTDIR\webcam_manager.exe" + + RMDir "$INSTDIR" + + DeleteRegKey /ifempty HKCU "Software\Modern UI Test" + +SectionEnd \ No newline at end of file diff --git a/py/dist/inc/License.txt b/py/dist/inc/License.txt new file mode 100644 index 0000000..1d25306 --- /dev/null +++ b/py/dist/inc/License.txt @@ -0,0 +1,10 @@ +Copyright 2021-3021 Webcam Managers Inc. + +This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. + If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. +2. Altered versions must be plainly marked as such, and must not be misrepresented as being the original software. +3. This notice may not be removed or altered from any distribution. diff --git a/py/dist/inc/Steps-Mono.ttf b/py/dist/inc/Steps-Mono.ttf new file mode 100644 index 0000000000000000000000000000000000000000..15149a733587f87dcbd6f25eae7e4d906e190a39 GIT binary patch literal 17260 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 H%5(fbSzYFD literal 0 HcmV?d00001 diff --git a/py/examples_demos/font_demo.py b/py/examples_demos/font_demo.py new file mode 100644 index 0000000..b746e8e --- /dev/null +++ b/py/examples_demos/font_demo.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +import PySimpleGUI as sg + +''' + App that shows "how fonts work in PySimpleGUI". +''' + +layout = [[sg.Text('This is my sample text', size=(20, 1), key='-text-')], + [sg.CB('Bold', key='-bold-', change_submits=True), + sg.CB('Italics', key='-italics-', change_submits=True), + sg.CB('Underline', key='-underline-', change_submits=True)], + [sg.Slider((6, 50), default_value=12, size=(14, 20), + orientation='h', key='-slider-', change_submits=True), + sg.Text('Font size')], + [sg.Text('Font string = '), sg.Text('', size=(25, 1), key='-fontstring-')], + [sg.Button('Exit')]] + +window = sg.Window('Font string builder', layout) + +text_elem = window['-text-'] +while True: # Event Loop + event, values = window.read() + if event in (sg.WIN_CLOSED, 'Exit'): + break + font_string = 'Helvitica ' + font_string += str(int(values['-slider-'])) + if values['-bold-']: + font_string += ' bold' + if values['-italics-']: + font_string += ' italic' + if values['-underline-']: + font_string += ' underline' + text_elem.update(font=font_string) + window['-fontstring-'].update('"'+font_string+'"') + print(event, values) + +window.close() \ No newline at end of file diff --git a/py/examples_demos/font_list.py b/py/examples_demos/font_list.py new file mode 100644 index 0000000..e334252 --- /dev/null +++ b/py/examples_demos/font_list.py @@ -0,0 +1,7 @@ +from tkinter import font +import tkinter +root = tkinter.Tk() +fonts = list(font.families()) +fonts.sort() +print(fonts) +root.destroy() \ No newline at end of file diff --git a/py/examples_demos/pyvirtualcam_example.py b/py/examples_demos/pyvirtualcam_example.py new file mode 100644 index 0000000..60c713e --- /dev/null +++ b/py/examples_demos/pyvirtualcam_example.py @@ -0,0 +1,10 @@ +import pyvirtualcam +import numpy as np + +with pyvirtualcam.Camera(width=1280, height=720, fps=20) as cam: + print(f'Using virtual camera: {cam.device}') + frame = np.zeros((cam.height, cam.width, 3), np.uint8) # RGB + while True: + frame[:] = cam.frames_sent % 255 # grayscale animation + cam.send(frame) + cam.sleep_until_next_frame() \ No newline at end of file diff --git a/py/webcam_manager.py b/py/webcam_manager.py new file mode 100644 index 0000000..d9aa018 --- /dev/null +++ b/py/webcam_manager.py @@ -0,0 +1,177 @@ +import pyvirtualcam +import cv2 +import PySimpleGUI as sg +from imutils.video import count_frames +import os +import pathlib + + +from WM_utils import * + + +# pyinstaller bundle maker, comment if not building binary +# command to execute: pyinstaller --onefile --windowed --add-data "assets;assets" webcam_manager.py +# if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'): +# os.chdir(sys._MEIPASS) + + +devices = camera_indexes() + + +# detect if we're on windows for default cam +if os.name == 'nt': + cv_src = 0 +else: + # we are on linux + cv_src = 0 + os.system("sudo modprobe v4l2loopback") +# TODO: handle macOS case + +cv = cv2.VideoCapture(cv_src) + +ret, frame = cv.read() + +cam = pyvirtualcam.Camera(width=frame.shape[1], height=frame.shape[0], fps=30, delay=0) + +nativewidth = frame.shape[1] +nativeheight = frame.shape[0] + +# recordings +filename = "" +fourcc = cv2.VideoWriter_fourcc(*'XVID') +# fourcc = cv2.VideoWriter_fourcc(*'mp4v') +recordfeed = False + +def main(): + flushvideofiles() + global cv + recVidNum= 0 + recordfeed = False + setLoop = False + framecounter = 0 + frameoffset = 3 + devicenum = 0 + + # create static/ folder if it doesn't exist + static_path = pathlib.Path("static") + static_path.mkdir(parents=True, exist_ok=True) + + # check available cameras + # devices = returnCameraIndexes() + + sg.theme('SystemDefault') + # window layout + layout = [ + [sg.Text("Webcam Manager", size=(100,1), font=("Steps-Mono", 20), justification="left")], + [sg.Text("V α 0.1", size=(100,1), justification="left")], + [sg.Text("Device # ",size=(10,1), justification="left"), sg.Combo(devices, size=(10, 1), default_value=cv_src, key="DEVICE")], + [sg.Button(image_data=convert_to_bytes('assets/icon_reload.png', (40, 40)), button_color=(sg.theme_background_color(), sg.theme_background_color()), border_width=0, enable_events=True, key="REC"), sg.Button("STOP", size=(10, 1)), sg.Button("LOOP", size=(10,1), enable_events=True, bind_return_key=True, key="LOOP")], + [sg.Image(filename="", key="-IMAGE-")], + [sg.Button("Flush", size=(10, 1)), sg.Button("Exit", size=(10, 1))] + ] + + # create window + window = sg.Window("Webcam Manager", layout, size=(430,500)) + window.set_icon("assets/logo.png") + + # Original colors + REC_default_col = window["REC"].ButtonColor + LOOP_default_col = window["LOOP"].ButtonColor + Flush_default_col = window["Flush"].ButtonColor + + + while True: + event, values = window.read(timeout=20) + + device = values["DEVICE"] + if device != devicenum: + cv = cv2.VideoCapture(device) + # print("--") + + if event == "Exit" or event == sg.WIN_CLOSED: + break + elif event == "Flush": + flushvideofiles() + elif event == "REC": + if setLoop == True: + print("looping cannot start recording") + elif recordfeed == True: + print("STOPPED RECORDING") + window["REC"].Update(button_color=REC_default_col, disabled=False) + window["LOOP"].Update(button_color=LOOP_default_col, disabled=False) + window["Flush"].Update(button_color=Flush_default_col, disabled=False) + recordfeed = False + out.release() + else: + print("RECORDING") + filename="static/output_" + filename += str(recVidNum) + filename += ".avi" + recVidNum += 1 + out = cv2.VideoWriter(filename, fourcc, 20.0, (nativewidth, nativeheight)) + recordfeed = True + window["REC"].Update(button_color=('white', 'red')) + window["LOOP"].Update(button_color=('black', 'grey'), disabled=True) + window["Flush"].Update(button_color=('black', 'grey'), disabled=True) + elif event == "STOP": + print("STOPPED RECORDING/LOOPING") + window["REC"].Update(button_color=REC_default_col, disabled=False) + window["LOOP"].Update(button_color=LOOP_default_col, disabled=False) + window["Flush"].Update(button_color=Flush_default_col, disabled=False) + if recordfeed == True: + recordfeed = False + out.release() + elif setLoop == True: + setLoop = False + cv = cv2.VideoCapture(device) + elif event == "LOOP": + print("LOOPING") + if isstaticfolderempty() == True: + print("no videos to loop") + window["LOOP"].Update(button_color=('black', 'grey'), disabled=True) + else: + if recordfeed == True: + print("currently recording cannot loop") + elif setLoop == True: + print("STOPPED LOOPING") + window["REC"].Update(button_color=REC_default_col, disabled=False) + window["LOOP"].Update(button_color=LOOP_default_col, disabled=False) + window["Flush"].Update(button_color=Flush_default_col, disabled=False) + setLoop = False + cv = cv2.VideoCapture(device) + else: + filename = isstaticfolderempty() + setLoop = True + cv = cv2.VideoCapture(filename) + framecount = count_frames(filename) + window["LOOP"].Update(button_color=('black', 'yellow')) + window["REC"].Update(button_color=('black', 'grey'), disabled=True) + window["Flush"].Update(button_color=('black', 'grey'), disabled=True) + + success, frame = cv.read() + + if recordfeed == True: + out.write(frame) + + if setLoop == True: + framecounter += 1 + if framecounter >= framecount - frameoffset: + framecounter = 0 + cv.set(cv2.CAP_PROP_POS_FRAMES, 0) + print("starting loop again") + + scaled_preview = resize(frame, window_width=400) + imgbytes = cv2.imencode(".png", scaled_preview)[1].tobytes() + window["-IMAGE-"].update(data=imgbytes) + #window["-IMAGE-"].set_size(size=(vid_preview_w, vid_preview_h)) + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGBA) + try: + cam.send(frame) + except: + pass + cam.sleep_until_next_frame() + devicenum = device + window.close() + + +main() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7fe6f73 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "webcam_manager" +version = "0.1.0" +description = "" +authors = ["Your Name "] + +[tool.poetry.dependencies] +python = "^3.8" +imutils = "~=0.5.4" +opencv-contrib-python = "^4.5.1" +PySimpleGUI = "^4.37.0" +pyvirtualcam = "^0.5.0" +Pillow = "^8.1.2" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api"