commit 972b16b54cab0b7c6423bde78c545d5747171c3c Author: David Benqué Date: Sun Apr 25 20:59:54 2021 +0100 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 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 0000000..4103791 Binary files /dev/null and b/py/assets/icon_reload.png differ 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 0000000..15149a7 Binary files /dev/null and b/py/dist/inc/Steps-Mono.ttf differ diff --git a/py/dist/inc/logo.ico b/py/dist/inc/logo.ico new file mode 100644 index 0000000..fe5bac4 Binary files /dev/null and b/py/dist/inc/logo.ico differ 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"