commit edf0f5e06a3f5c1b260ed5086c3efa89c3555678 Author: Rich Date: Wed Dec 6 14:24:46 2023 +0000 Initial commit diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..09c7338 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,61 @@ +{ + "version": "0.2.0", + "configurations": [{ + "name": "Python", + "type": "python", + "request": "launch", + "stopOnEntry": false, + "python": "${command:python.interpreterPath}", + "program": "${workspaceRoot}/mfe.py", + "cwd": "${workspaceRoot}", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "console": "internalConsole", + "debugOptions": [ + "RedirectOutput" + ] + }, + { + "name": "Integrated Terminal/Console", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "python": "${command:python.interpreterPath}", + "program": "${file}", + "cwd": "", + "console": "integratedTerminal", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit" + ] + }, + { + "name": "External Terminal/Console", + "type": "python", + "request": "launch", + "stopOnEntry": true, + "python": "${command:python.interpreterPath}", + "program": "${file}", + "cwd": "", + "console": "externalTerminal", + "env": {}, + "envFile": "${workspaceRoot}/.env", + "debugOptions": [ + "WaitOnAbnormalExit", + "WaitOnNormalExit" + ] + }, + { + "name": "Attach (Remote Debug)", + "type": "python", + "request": "attach", + "localRoot": "${workspaceRoot}", + "remoteRoot": "${workspaceRoot}", + "port": 3000, + "secret": "my_secret", + "host": "localhost" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5241205 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "editor.formatOnPaste": true, + "editor.formatOnSave": true, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "black-formatter.args": [ + "--line-length=120" + ] +} diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..c0930e7 --- /dev/null +++ b/Pipfile @@ -0,0 +1,25 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +configobj = ">=5.0.6" +appdirs = ">=1.4.3" +lxml = ">=3.8.0" +titlecase = ">=0.11.0" +pygame = "*" +#pyvidplayer2 = {editable = true,git = "https://github.com/ree1261/pyvidplayer2.git"} + +[dev-packages] +pylint = "*" +flake8 = "*" +#cx-freeze = {editable = true,git = "https://github.com/anthony-tuininga/cx_Freeze.git"} +yapf = "*" +black = "*" + +[requires] +python_version = "3.11" + +[pipenv] +allow_prereleases = true diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..4347176 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,391 @@ +{ + "_meta": { + "hash": { + "sha256": "312e9824c037c11e65b606532a2a8e3b0ea7e3eadcae2249c2f6336e001caec9" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.11" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "appdirs": { + "hashes": [ + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + ], + "index": "pypi", + "version": "==1.4.4" + }, + "configobj": { + "hashes": [ + "sha256:6f704434a07dc4f4dc7c9a745172c1cad449feb548febd9f7fe362629c627a97", + "sha256:a7a8c6ab7daade85c3f329931a807c8aee750a2494363934f8ea84d8a54c87ea", + "sha256:d808d7e04e6f81fbb23d5ac2cd50e69ccbee58eaf9360eb89ede22d93216a314" + ], + "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==5.0.8" + }, + "lxml": { + "hashes": [ + "sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3", + "sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d", + "sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a", + "sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120", + "sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305", + "sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287", + "sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23", + "sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52", + "sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f", + "sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4", + "sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584", + "sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f", + "sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693", + "sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef", + "sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5", + "sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02", + "sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc", + "sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7", + "sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da", + "sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a", + "sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40", + "sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8", + "sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd", + "sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601", + "sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c", + "sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be", + "sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2", + "sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c", + "sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129", + "sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc", + "sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2", + "sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1", + "sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7", + "sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d", + "sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477", + "sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d", + "sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e", + "sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7", + "sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2", + "sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574", + "sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf", + "sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b", + "sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98", + "sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12", + "sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42", + "sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35", + "sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d", + "sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce", + "sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d", + "sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f", + "sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db", + "sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4", + "sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694", + "sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac", + "sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2", + "sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7", + "sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96", + "sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d", + "sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b", + "sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a", + "sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13", + "sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340", + "sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6", + "sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458", + "sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c", + "sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c", + "sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9", + "sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432", + "sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991", + "sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69", + "sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf", + "sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb", + "sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b", + "sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833", + "sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76", + "sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85", + "sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e", + "sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50", + "sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8", + "sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4", + "sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b", + "sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5", + "sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190", + "sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7", + "sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa", + "sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0", + "sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9", + "sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0", + "sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b", + "sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5", + "sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7", + "sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4" + ], + "index": "pypi", + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.9.3" + }, + "pygame": { + "hashes": [ + "sha256:03879ec299c9f4ba23901b2649a96b2143f0a5d787f0b6c39469989e2320caf1", + "sha256:074aa6c6e110c925f7f27f00c7733c6303407edc61d738882985091d1eb2ef17", + "sha256:0e24d05184e4195fe5ebcdce8b18ecb086f00182b9ae460a86682d312ce8d31f", + "sha256:1822d534bb7fe756804647b6da2c9ea5d7a62d8796b2e15d172d3be085de28c6", + "sha256:1a2a43802bb5e89ce2b3b775744e78db4f9a201bf8d059b946c61722840ceea8", + "sha256:1c289f2613c44fe70a1e40769de4a49c5ab5a29b9376f1692bb1a15c9c1c9bfa", + "sha256:1f3849f97372a3381c66955f99a0d58485ccd513c3d00c030b869094ce6997a6", + "sha256:224c308856334bc792f696e9278e50d099a87c116f7fc314cd6aa3ff99d21592", + "sha256:263b4a7cbfc9fe2055abc21b0251cc17dea6dff750f0e1c598919ff350cdbffe", + "sha256:2b34c73cb328024f8db3cb6487a37e54000148988275d8d6e5adf99d9323c937", + "sha256:30a8d7cf12363b4140bf2f93b5eec4028376ca1d0fe4b550588f836279485308", + "sha256:31648d38ecdc2335ffc0e38fb18a84b3339730521505dac68514f83a1092e3f4", + "sha256:34646ca20e163dc6f6cf8170f1e12a2e41726780112594ac061fa448cf7ccd75", + "sha256:35632035fd81261f2d797fa810ea8c46111bd78ceb6089d52b61ed7dc3c5d05f", + "sha256:35cf093a51cb294ede56c29d4acf41538c00f297fcf78a9b186fb7d23c0577b6", + "sha256:39690e9be9baf58b7359d1f3b2336e1fd6f92fedbbce42987be5df27f8d30718", + "sha256:3b3e619e33d11c297d7a57a82db40681f9c2c3ae1d5bf06003520b4fe30c435d", + "sha256:3b8a6e351665ed26ea791f0e1fd649d3f483e8681892caef9d471f488f9ea5ee", + "sha256:41f8779f52e0f6e6e6ccb8f0b5536e432bf386ee29c721a1c22cada7767b0cef", + "sha256:47a8415d2bd60e6909823b5643a1d4ef5cc29417d817f2a214b255f6fa3a1e4c", + "sha256:485239c7d32265fd35b76ae8f64f34b0637ae11e69d76de15710c4b9edcc7c8d", + "sha256:4f1559e7efe4efb9dc19d2d811d702f325d9605f9f6f9ececa39ee6890c798f5", + "sha256:4ff21201df6278b8ca2e948fb148ffe88f5481fd03760f381dd61e45954c7dff", + "sha256:5697528266b4716d9cdd44a5a1d210f4d86ef801d0f64ca5da5d0816704009d9", + "sha256:677e37bc0ea7afd89dde5a88ced4458aa8656159c70a576eea68b5622ee1997b", + "sha256:68c4e8e60b725ffc7a6c6ecd9bb5fcc5ed2d6e0e2a2c4a29a8454856ef16ad63", + "sha256:6cf2257447ce7f2d6de37e5fb019d2bbe32ed05a5721ace8bc78c2d9beaf3aee", + "sha256:6d58c8cf937815d3b7cdc0fa9590c5129cb2c9658b72d00e8a4568dea2ff1d42", + "sha256:6fe323acbf53a0195c8c98b1b941eba7ac24e3e2b28ae48e8cda566f15fc4945", + "sha256:74e1d6284100e294f445832e6f6343be4fe4748decc4f8a51131ae197dae8584", + "sha256:78fcd7643358b886a44127ff7dec9041c056c212b3a98977674f83f99e9b12d3", + "sha256:7d0a2794649defa57ef50b096a99f7113d3d0c2e32d1426cafa7d618eadce4c7", + "sha256:88d1cdacc2d3471eceab98bf0c93c14d3a8461f93e58e3d926f20d4de3a75554", + "sha256:9b30bc1220c457169571aac998e54b013aaeb732d2fd8744966cb1cfab1f61d1", + "sha256:9bd738fd4ecc224769d0b4a719f96900a86578e26e0105193658a32966df2aae", + "sha256:9dcff6cbba1584cf7732ce1dbdd044406cd4f6e296d13bcb7fba963fb4aeefc9", + "sha256:a0769eb628c818761755eb0a0ca8216b95270ea8cbcbc82227e39ac9644643da", + "sha256:a0bd67426c02ffe6c9827fc4bcbda9442fbc451d29b17c83a3c088c56fef2c90", + "sha256:bc12e4dea3e88ea8a553de6d56a37b704dbe2aed95105889f6afeb4b96e62097", + "sha256:c13edebc43c240fb0532969e914f0ccefff5ae7e50b0b788d08ad2c15ef793e4", + "sha256:c1b89eb5d539e7ac5cf75513125fb5f2f0a2d918b1fd6e981f23bf0ac1b1c24a", + "sha256:ce4b6c0bfe44d00bb0998a6517bd0cf9455f642f30f91bc671ad41c05bf6f6ae", + "sha256:cf2191b756ceb0e8458a761d0c665b0c70b538570449e0d39b75a5ba94ac5cf0", + "sha256:d29a84b2e02814b9ba925357fd2e1df78efe5e1aa64dc3051eaed95d2b96eafd", + "sha256:d75cbbfaba2b81434d62631d0b08b85fab16cf4a36e40b80298d3868927e1299", + "sha256:d78485c4d21133d6b2fbb504cd544ca655e50b6eb551d2995b3aa6035928adda", + "sha256:d851247239548aa357c4a6840fb67adc2d570ce7cb56988d036a723d26b48bff", + "sha256:daca456d5b9f52e088e06a127dec182b3638a775684fb2260f25d664351cf1ae", + "sha256:dc346965847aef00013fa2364f41a64f068cd096dcc7778fc306ca3735f0eedf", + "sha256:dd2d2650faf54f9a0f5bd0db8409f79609319725f8f08af6507a0609deadcad4", + "sha256:e58e2b0c791041e4bccafa5bd7650623ba1592b8fe62ae0a276b7d0ecb314b6c", + "sha256:e708fc8f709a0fe1d1876489345f2e443d47f3976d33455e2e1e937f972f8677", + "sha256:ed9a3d98adafa0805ccbaaff5d2996a2b5795381285d8437a4a5d248dbd12b4a", + "sha256:edda1f7cff4806a4fa39e0e8ccd75f38d1d340fa5fc52d8582ade87aca247d92", + "sha256:f02c1c7505af18d426d355ac9872bd5c916b27f7b0fe224749930662bea47a50", + "sha256:f30d1618672a55e8c6669281ba264464b3ab563158e40d89e8c8b3faa0febebd", + "sha256:fe0228501ec616779a0b9c4299e837877783e18df294dd690b9ab0eed3d8aaab" + ], + "index": "pypi", + "markers": "python_version >= '3.6'", + "version": "==2.5.2" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'", + "version": "==1.16.0" + }, + "titlecase": { + "hashes": [ + "sha256:7d83a277ccbbda11a2944e78a63e5ccaf3d32f828c594312e4862f9a07f635f5" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.4.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca", + "sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.1" + }, + "black": { + "hashes": [ + "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4", + "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b", + "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f", + "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07", + "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187", + "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6", + "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05", + "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06", + "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e", + "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5", + "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244", + "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f", + "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221", + "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055", + "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479", + "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394", + "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911", + "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==23.11.0" + }, + "click": { + "hashes": [ + "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.7" + }, + "dill": { + "hashes": [ + "sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e", + "sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03" + ], + "markers": "python_version >= '3.11'", + "version": "==0.3.7" + }, + "flake8": { + "hashes": [ + "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23", + "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==6.1.0" + }, + "importlib-metadata": { + "hashes": [ + "sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7", + "sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67" + ], + "markers": "python_version >= '3.8'", + "version": "==7.0.0" + }, + "isort": { + "hashes": [ + "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504", + "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6" + ], + "markers": "python_full_version >= '3.8.0'", + "version": "==5.12.0" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "pathspec": { + "hashes": [ + "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20", + "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3" + ], + "markers": "python_version >= '3.7'", + "version": "==0.11.2" + }, + "platformdirs": { + "hashes": [ + "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380", + "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420" + ], + "markers": "python_version >= '3.8'", + "version": "==4.1.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", + "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" + ], + "markers": "python_version >= '3.8'", + "version": "==2.11.1" + }, + "pyflakes": { + "hashes": [ + "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774", + "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc" + ], + "markers": "python_version >= '3.8'", + "version": "==3.1.0" + }, + "pylint": { + "hashes": [ + "sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496", + "sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.0'", + "version": "==3.0.2" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.1" + }, + "tomlkit": { + "hashes": [ + "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4", + "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba" + ], + "markers": "python_version >= '3.7'", + "version": "==0.12.3" + }, + "yapf": { + "hashes": [ + "sha256:4dab8a5ed7134e26d57c1647c7483afb3f136878b579062b786c9ba16b94637b", + "sha256:adc8b5dd02c0143108878c499284205adb258aad6db6634e5b869e7ee2bd548b" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==0.40.2" + }, + "zipp": { + "hashes": [ + "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31", + "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0" + ], + "markers": "python_version >= '3.8'", + "version": "==3.17.0" + } + } +} diff --git a/arcade.png b/arcade.png new file mode 100644 index 0000000..322bbd6 Binary files /dev/null and b/arcade.png differ diff --git a/config.py b/config.py new file mode 100644 index 0000000..9ba88b7 --- /dev/null +++ b/config.py @@ -0,0 +1,156 @@ +import os + +from appdirs import user_data_dir +from configobj import ConfigObj, flatten_errors, Section +from validate import Validator + +# from utils import get_pygame_keydict, get_keys + +CFGSPEC = """ResolutionX = integer(default=1024) +ResolutionY = integer(default=768) +FullScreen = boolean(default=False) +Font = string(default='microsoftsansserif') +FontSize = integer(default=13) +QuitKeyPresses = integer(min=1,default=3) +ScanLines = boolean(default=True) +Key_Up = list(default=list('K_UP')) +Key_Down = list(default=list('K_DOWN')) +Key_PgUp = list(default=list('K_LEFT','K_PAGEUP')) +Key_PgDn = list(default=list('K_RIGHT','K_PAGEDOWN')) +Key_Home = list(default=list('K_HOME')) +Key_End = list(default=list('K_END')) +Key_Select = list(default=list('K_RETURN','K_1')) +Key_GameInfo = list(default=list('K_5')) +Key_GameHistoryxP = list(default=list('K_6')) +Key_Popup = list(default=list('K_2')) +Key_ShowArtwork = list(default=list()) +AlwaysChangeSnap = boolean(default=True) +Emulator = string() +FPS = integer(default=60) +[__many__] +EXE = string() +Version = string(default=None) +ShowClones = boolean(default=True) +Category = string(default='All Games') +GameAtTop = integer(min=0,default=0) +CurrentGame = integer(min=0,default=0) +EmulatorType = option('MAME','MESS','CSV',default='MAME') +StatusFilter = list(default=list()) +Sort = option('Name Asc','Name Dec','Year Asc','Year Dec',default='Name Asc') +SnapDir = string(default=None) +CSVFile = string(default=None) +ArtworkDirs = list(default=list()) +ShowVideoSnaps = boolean(default=True) +HideMature = boolean(default=True) +""" + +appname = "MFE6" +appauthor = "RMJ" + +debug = True + +datadir = user_data_dir(appname, appauthor) + +if debug: + datadir = os.getcwd() + +if not os.path.isdir(datadir): + os.makedirs(datadir) + +spec = CFGSPEC.split("\n") +cfg = ConfigObj(os.path.join(datadir, "mfe.ini"), configspec=spec, encoding="UTF8") +cfg.write() + +validator = Validator() +result = cfg.validate(validator, copy=True, preserve_errors=True) + +errored = False + +for entry in flatten_errors(cfg, result): + section_list, key, error = entry + if key is not None: + section_list.append(key) + else: + section_list.append("[missing section]") + section_string = ", ".join(section_list) + if error is False: + error = "Missing value or section." + print(section_string, " = ", error) + errored = True + +if errored: + print("Error in INI file") + exit() + +cfg.write() + +cfg["datadir"] = datadir + +if not os.path.isfile(os.path.join(datadir, "mame_exclude.txt")): + from exclude import MAME_EXCLUDE + + with open(os.path.join(datadir, "mame_exclude.txt"), "wt") as f: + f.write(MAME_EXCLUDE) + + # import gfx_pygame + + # keys = gfx_pygame.get_keydict() + # get_keys = gfx_pygame.get_keys + +# cfg["KeyUp"] = get_keys(keys, cfg["Key_Up"]) +# cfg["KeyDown"] = get_keys(keys, cfg["Key_Down"]) +# cfg["KeyPgUp"] = get_keys(keys, cfg["Key_PgUp"]) +# cfg["KeyPgDn"] = get_keys(keys, cfg["Key_PgDn"]) +# cfg["KeyHome"] = get_keys(keys, cfg["Key_Home"]) +# cfg["KeyEnd"] = get_keys(keys, cfg["Key_End"]) +# cfg["KeySelect"] = get_keys(keys, cfg["Key_Select"]) +# cfg["KeyGameInfo"] = get_keys(keys, cfg["Key_GameInfo"]) +# cfg["KeyGameHistory"] = get_keys(keys, cfg["Key_GameHistory"]) +# cfg["KeyPopup"] = get_keys(keys, cfg["Key_Popup"]) +# cfg["KeyShowArtwork"] = get_keys(keys, cfg["Key_ShowArtwork"]) + + +def config_write(): + Exclude = [ + "KeyUp", + "KeyDown", + "KeyPgUp", + "KeyPgDn", + "KeySelect", + "KeyGameInfo", + "KeyGameHistory", + "KeyPopup", + "KeyHome", + "KeyEnd", + "KeyShowArtwork", + "datadir", + ] + + t = {} + for item in Exclude: + t[item] = cfg[item] + del cfg[item] + + cfg.write() + + for item in t: + cfg[item] = t[item] + + +# def get_emu(option=None): +# if option: +# r = cfg[cfg['Emulator']][option] +# else: +# r = cfg[cfg['Emulator']] +# return r + +# def set_emu(option, data): +# cfg[cfg['Emulator']][option] = data + + +def get_emulators(): + r = [] + for k in cfg.keys(): + if isinstance(cfg[k], Section): + r.append(k) + return r diff --git a/emu.py b/emu.py new file mode 100644 index 0000000..9ca553f --- /dev/null +++ b/emu.py @@ -0,0 +1,10 @@ +from config import cfg +import mame + + +class emu: + def __init__(self): + self.config = cfg[cfg["Emulator"]] + self.name = cfg["Emulator"] + if self.config["EmulatorType"].upper() == "MAME": + self.roms = mame.mameROMs(self.config, cfg["datadir"]) diff --git a/exclude.py b/exclude.py new file mode 100644 index 0000000..7e85b47 --- /dev/null +++ b/exclude.py @@ -0,0 +1,99 @@ +MAME_EXCLUDE = """3D Printer +Astrological Computer +Audio Sequencer +Bank-teller Terminal +Barcode Printer +Bridge Machine +Business Computer / Terminal +Calculator / Pocket Computer +Cash Counter +Chess Machine +Clock +Credit Card Terminal +DVD Player +DVD Reader/Writer +Dame Machine +Development Computer +Devices +Document Processors +Dot-Matrix Display +Drum Machine +EPROM Programmer +Educational Game +Electromechanical / Change Money +Electromechanical / Coin Pusher +Electromechanical / Misc. +Electromechanical / Pinball +Electromechanical / Redemption +Electromechanical / Reels +Electromechanical / Utilities +Electronic Board Game +Electronic Typewriter +Engine Control Unit +Gambling Board +Game Console +Game Console Expansion +Graphic Tablet +Graphics Display Controller +Handheld Child Computers +Handheld Game +Handheld Game Console +Home Computer +In Circuit Emulator +JukeBox +Kit Computer +Laptop / Notebook / Portable +Laser Printer +Matrix Printer +Microcomputer +Misc. +Misc. * Mature * +Mobile Phone +Modem +Multi-cart Board +Network Processor +Not Classified +Pinball +Pinball * Mature * +Pinball / Pachinko +Pinball / Pachinko * Mature * +Player +Pocket Device / Pad / PDA +Portable Media Player +Print Club +Printer Handbook +Programming Machine +Punched Card Computer +Quiz / Chinese +Quiz / French +Quiz / German +Quiz / Italian +Quiz / Japanese +Quiz / Japanese * Mature * +Quiz / Japanese - Music +Quiz / Korean +Quiz / Spanish +Rhythm / Dance +Rhythm / Instruments +Rhythm / Misc. +Robot Control +Satellite Receiver +Single Board Computer +Speech Synthesizer +Synthesizer +System / BIOS +System / Device +Talking Calculator +Telephone / ComputerPhone +Test ROM +Thermal Printer +Toy cars +Training Board +Utilities / Test +Utilities / Update +VTR Control +Virtual Environment +Wavetables Generator +Word-processing Machine +Workstation / Server +""" diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..f554177 --- /dev/null +++ b/gui.py @@ -0,0 +1,413 @@ +import pygame +# pylint: disable=E0611 +from pygame.locals import KEYDOWN +# pylint: enable=E0611 + +from configfile import cfg +from utils import wrap_multi_line, image_from_data + +# pylint: disable=E1121,R0902,R0903,R0912,R0913 + +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) + + +class baseobject(object): + def __init__(self, + surface, + x, + y, + width, + height, + data, + font, + border=False, + fg=WHITE, + bg=BLACK, + name=None, + title=None): + self.surface = surface + self.x = x + self.y = y + self.width = width + self.height = height + self.font = font + self.border = border + self.fontheight = self.font.get_height() + self.fg = fg + self.bg = bg + self.data = data + self.name = name + self.title = title + + if self.width < 1: + for item in self.data: + size = self.font.size(item)[0] + if size > self.width: + self.width = size + if self.width > self.surface.get_width(): + self.surface.get_width() + if self.border: + self.width += 8 + + if self.height < 1: + self.height = self.fontheight * (len(self.data)) + if self.border: + self.height += 8 + if self.height > self.surface.get_height(): + if self.border: + self.height = self.surface.get_height() - 8 - self.y + else: + self.height = self.surface.get_height - self.y + + if self.x == -1: + self.x = int((self.surface.get_width() / 2) - (self.width / 2)) + + if self.y == -1: + self.y = int((self.surface.get_height() / 2) - (self.height / 2)) + + if self.border: + if not self.title: + self.textx = self.x + 4 + self.texty = self.y + 4 + self.textwidth = self.width - 8 + self.textheight = self.height - 8 + else: + self.textx = self.x + 4 + self.texty = self.y + 4 + self.fontheight + self.textwidth = self.width - 8 + self.textheight = self.height - 8 - self.fontheight + else: + if not self.title: + self.textx = self.x + self.texty = self.y + self.textwidth = self.width + self.textheight = self.height + else: + self.textx = self.x + self.texty = self.y + self.fontheight + self.textwidth = self.width + self.textheight = self.height - self.fontheight + + +class menu(baseobject): + def __init__(self, + surface, + x, + y, + width, + height, + data, + font, + border=False, + fg=WHITE, + bg=BLACK, + name=None, + startitem=0, + itemattop=0): + baseobject.__init__(self, surface, x, y, width, height, data, font, + border, fg, bg, name) + self.currentitem = startitem + self.itemattop = itemattop + self.itemsperpage = int(self.textheight / self.fontheight) + self.isrom = True + if str(type(self.data[0])) == "": + self.isrom = False + + def processevent(self, event): # pylint: disable=R0912 + if event.type == KEYDOWN: + if event.key in cfg["KeyUp"] and self.currentitem > 0: + if self.currentitem == self.itemattop: + self.itemattop -= 1 + self.currentitem -= 1 + if event.key in cfg["KeyDown"] and self.currentitem < len( + self.data) - 1: + self.currentitem += 1 + if self.itemattop + self.itemsperpage <= self.currentitem: + self.itemattop += 1 + if event.key in cfg["KeyPgUp"] and self.currentitem > 0: + self.currentitem -= self.itemsperpage - 1 + while self.itemattop > self.currentitem: + self.itemattop -= 1 + if self.itemattop < 0: + self.itemattop = 0 + self.currentitem = 0 + if event.key in cfg["KeyPgDn"] and self.currentitem < len( + self.data) - 1: + self.currentitem += self.itemsperpage - 1 + while self.currentitem - self.itemattop >= self.itemsperpage: + self.itemattop += 1 + if self.itemattop + self.itemsperpage > len(self.data): + self.itemattop = len(self.data) - self.itemsperpage + self.currentitem = len(self.data) - 1 + if event.key in cfg["KeyHome"] and self.currentitem > 0: + self.currentitem = 0 + self.itemattop = 0 + if event.key in cfg["KeyEnd"] and self.currentitem < len( + self.data) - 1: + self.currentitem = len(self.data) - 1 + self.itemattop = self.currentitem - self.itemsperpage + 1 + + def render(self): + pygame.Surface.fill( + self.surface, + self.bg, + rect=pygame.Rect(self.x, self.y, self.width, self.height)) + + if self.border: + pygame.draw.rect(self.surface, self.fg, + pygame.Rect(self.x + 1, self.y + 1, + self.width - 2, self.height - 2), 1) + + for i in range(0, self.itemsperpage): + if i > len(self.data) - 1: + t = pygame.Surface((self.textwidth, self.fontheight)) + t.fill(self.bg) + self.surface.blit(t, (self.textx, + self.texty + i * self.fontheight)) + continue + + if self.isrom: + item = self.data[self.itemattop + i].description + else: + item = self.data[self.itemattop + i] + + if self.itemattop + i == self.currentitem: + s = self.font.render(item, True, self.bg, self.fg) + t = pygame.Surface((self.textwidth, self.fontheight)) + t.fill(self.fg) + t.blit(s, (0, 0)) + self.surface.blit(t, (self.textx, + self.texty + i * self.fontheight)) + else: + s = self.font.render(item, True, self.fg, self.bg) + t = pygame.Surface((self.textwidth, self.fontheight)) + t.fill(self.bg) + t.blit(s, (0, 0)) + self.surface.blit(t, (self.textx, + self.texty + i * self.fontheight)) + + +class notepad(baseobject): + def __init__(self, + surface, + x, + y, + width, + height, + data, + font, + border=False, + fg=WHITE, + bg=BLACK, + name="None", + title=None): + baseobject.__init__(self, surface, x, y, width, height, data, font, + border, fg, bg, name, title) + self.data = wrap_multi_line(data, self.font, self.textwidth) + self.lineattop = 0 + self.linesperpage = int(self.textheight / self.fontheight) + self.lasttopline = int(len(self.data) - self.linesperpage) + + def processevent(self, event): + if event.type == KEYDOWN: + if event.key in cfg["KeyDown"] and self.lineattop < self.lasttopline: + self.lineattop += 1 + elif event.key in cfg["KeyUp"] and self.lineattop > 0: + self.lineattop -= 1 + elif event.key in cfg["KeyPgDn"] and self.lineattop < self.lasttopline: + self.lineattop += self.linesperpage + if self.lineattop > self.lasttopline: + self.lineattop = self.lasttopline + elif event.key in cfg["KeyPgUp"] and self.lineattop > 0: + self.lineattop -= self.linesperpage + if self.lineattop < 0: + self.lineattop = 0 + elif event.key in cfg["KeyHome"] and self.lineattop > 0: + self.lineattop = 0 + elif event.key in cfg["KeyEnd"] and self.lineattop < self.lasttopline: + self.lineattop = self.lasttopline + + def render(self): + pygame.Surface.fill( + self.surface, + self.bg, + rect=pygame.Rect(self.x, self.y, self.width, self.height)) + if self.border: + pygame.draw.rect(self.surface, self.fg, + pygame.Rect(self.x + 1, self.y + 1, + self.width - 2, self.height - 2), 1) + if self.title: + x = (self.textwidth / 2) - ( + self.font.size('%s' % self.title)[0] / 2) + title_surface = pygame.Surface((self.textwidth, + self.fontheight)) + title_surface.fill(self.fg) + title_surface.blit( + self.font.render('%s' % self.title, True, BLACK, self.fg), + (x, 0)) + self.surface.blit(title_surface, + (self.textx, self.texty - self.fontheight)) + for c in range(0, self.linesperpage): + try: + itemsurface = self.font.render(self.data[c + self.lineattop], + True, self.fg, self.bg) + self.surface.blit(itemsurface, + (self.textx, + self.texty + c * self.fontheight)) + except: + pass + + +class image_notepad(baseobject): + def __init__(self, + surface, + x, + y, + width, + height, + data, + font, + border=False, + fg=WHITE, + bg=BLACK, + name="None", + title=None): + baseobject.__init__(self, surface, x, y, width, height, data, font, + border, fg, bg, name, title) + self.currentitem = 0 + self.data = data + + def processevent(self, event): + if event.type == KEYDOWN: + if event.key in cfg["KeyPgDn"]: + if self.currentitem < len(self.data) - 1: + self.currentitem += 1 + elif event.key in cfg["KeyPgUp"]: + if self.currentitem > 0: + self.currentitem -= 1 + + def render(self): + pygame.Surface.fill( + self.surface, + self.bg, + rect=pygame.Rect(self.x, self.y, self.width, self.height)) + if self.border: + pygame.draw.rect(self.surface, self.fg, + pygame.Rect(self.x + 1, self.y + 1, + self.width - 2, self.height - 2), 1) + x = (self.textwidth / 2) - (self.font.size('%s' % self.title)[0] / 2) + title_surface = pygame.Surface((self.textwidth, self.fontheight)) + title_surface.fill(self.fg) + title_surface.blit( + self.font.render('%s' % self.data[self.currentitem][0], True, + BLACK, self.fg), (x, 0)) + self.surface.blit(title_surface, (self.textx, + self.texty - self.fontheight)) + + image_surface = image_from_data(self.data[self.currentitem][1], + (self.textwidth, self.textheight)) + + w, h = image_surface.get_width(), image_surface.get_height() + + x = (self.textwidth / 2) - (w / 2) + self.textx + y = (self.textheight / 2) - (h / 2) + self.texty + + self.surface.blit(image_surface, (x, y)) + + +class gui(): + def __init__(self, surface): + self.surface = surface + self.objects = [] + self.font = pygame.font.Font( + pygame.font.match_font(cfg["Font"], bold=True), cfg["FontSize"]) + # self.fontheight = self.font.get_height() + self.currentobject = 0 + + def waitevent(self): + event = pygame.event.wait() + + if self.objects: + self.objects[-1].processevent(event) + + return event + + def add_menu(self, + x, + y, + width, + height, + data, + border=False, + fg=WHITE, + bg=BLACK, + name="None", + startitem=0, + itemattop=0): + self.currentobject += 1 + self.objects.append( + menu(self.surface, x, y, width, height, data, self.font, border, + fg, bg, name, startitem, itemattop)) + + def add_notepad(self, + x, + y, + width, + height, + data, + border=False, + fg=WHITE, + bg=BLACK, + name="None", + title=None): + self.currentobject += 1 + self.objects.append( + notepad(self.surface, x, y, width, height, data, self.font, border, + fg, bg, name, title)) + + def add_image_notepad(self, + x, + y, + width, + height, + data, + border=False, + fg=WHITE, + bg=BLACK, + name="None", + title=None): + self.currentobject += 1 + self.objects.append( + image_notepad(self.surface, x, y, width, height, data, self.font, + border, fg, bg, name, title)) + + def deletelastobject(self): + del self.objects[-1] + self.currentobject -= 1 + + def getmenuname(self): + return self.objects[-1].name + + def deleteallmenus(self): + while self.currentobject > 1: + self.deletelastobject() + + def getcurrentitem(self, objnum=None): + if objnum is None: + return self.objects[-1].currentitem + return self.objects[objnum].currentitem + + def getitemattop(self): + return self.objects[-1].itemattop + + # TODO: Fix me for itemattop on normal menus + def setcurrentitem(self, selected_item): + self.objects[-1].itemattop = selected_item + self.objects[-1].currentitem = selected_item + + def render(self): + for t in self.objects: + t.render() + + pygame.display.update() diff --git a/mame.dat b/mame.dat new file mode 100644 index 0000000..dd4cda5 Binary files /dev/null and b/mame.dat differ diff --git a/mame.py b/mame.py new file mode 100644 index 0000000..3c223ac --- /dev/null +++ b/mame.py @@ -0,0 +1,213 @@ +import os +import zipfile +import subprocess +import pickle +from lxml import etree + +from config import debug +from utils import getMameResource + + +class mameROM(object): + def __init__(self): + self.name = "" + self.cloneof = None + self.description = "" + self.year = "" + self.manufacturer = "" + self.status = "" + self.category = "" + + def __str__(self): + return self.description + + def __repr__(self): + return '%s("%s", "%s", "%s", "%s", "%s", "%s", "%s")' % ( + self.__class__.__name__, + self.name, + self.cloneof, + self.description, + self.year, + self.manufacturer, + self.status, + self.category, + ) + + +class mameROMs(object): + def __init__(self, config, datadir): + self.all_roms = [] + self.roms = [] + self.all_categories = [] + self.categories = [] + self.catdict = {} + self.len = 0 + self.cfg = config + self.data_dir = datadir + self.hide_mature = self.cfg["HideMature"] + self.emulator_dir = os.path.split(self.cfg["EXE"])[0] + + self.parse() + self.filter() + + self.snapdir = getMameResource(self.emulator_dir, "snap", is_dir=True) + self.marqueesdir = getMameResource(self.emulator_dir, "marquees", is_dir=True) + self.titlesdir = getMameResource(self.emulator_dir, "titles", is_dir=True) + self.videosnapsdir = getMameResource(self.emulator_dir, "videosnaps", is_dir=True) + self.info = getMameResource(self.emulator_dir, "mameinfo.dat", is_dir=False) + self.hostory = getMameResource(self.emulator_dir, "history.dat", is_dir=False) + + def get_mame_version(self): + mamerun = subprocess.run([self.cfg["EXE"], "-?"], stdout=subprocess.PIPE) + output = mamerun.stdout.decode("utf-8") + output = output[output.find("v") :] + output = output[: output.find(" ")] + + return output + + def parse(self): + if not debug: + mame_version = self.get_mame_version() + print(mame_version) + + xmlfile = os.path.join(self.emulator_dir, "mame.xml") + datfile = os.path.join(self.data_dir, "mame.dat") + + if not debug: + if self.cfg["Version"] != mame_version: + self.cfg["Version"] = mame_version + + if os.path.isfile(xmlfile): + os.unlink(xmlfile) + + if os.path.isfile(datfile): + with open(datfile, "rb") as i: + temp_mame_version = pickle.load(i) + + if debug or temp_mame_version == mame_version: + self.all_categories = pickle.load(i) + self.all_roms = pickle.load(i) + return True + + os.unlink(datfile, "mame.dat") + + tempcat = {} + catfile = open(os.path.join(self.emulator_dir, "catver.ini")) + + found = False + for line in catfile: + line = line.strip() + if line == "[VerAdded]": + break + if line == "[Category]": + found = True + if found and line != "" and line[0] != ";" and line[0] != "[": + # zwackery=Platform / Run Jump + game, category = line.split("=") + tempcat[game] = category + if category not in self.all_categories: + self.all_categories.append(category) + self.catdict[category] = 0 + + self.all_categories.sort() + self.all_categories.insert(0, "All Games") + self.all_categories.append("Unknown") + self.catdict["All Games"] = 1 + self.catdict["Unknown"] = 0 + + if not os.path.isfile(xmlfile): + try: + with open(xmlfile, "w") as out: + retcode = subprocess.call([self.cfg["EXE"], "-listxml"], stdout=out) + if retcode != 0: + try: + os.unlink(xmlfile) + except: + pass + return False + except OSError: + try: + os.unlink(xmlfile) + except: + pass + return False + + with open(os.path.join(self.data_dir, "mame_exclude.txt"), "rt") as f: + exclude = f.readlines() + exclude = [x.strip() for x in exclude] + + tree = etree.parse(xmlfile) + + rom = None + for child in tree.getiterator(): + if child.tag == "machine": + if rom: + if rom.category not in exclude: + self.all_roms.append(rom) + self.catdict[rom.category] += 1 + rom = None + if "runnable" in child.attrib and child.attrib["runnable"] == "yes": + rom = mameROM() + rom.name = child.attrib["name"] + else: + rom = None + if rom and "cloneof" in child.attrib: + rom.cloneof = child.attrib["cloneof"] + if rom and rom.name in tempcat: + rom.category = tempcat[rom.name] + elif rom and rom.cloneof in tempcat: + rom.category = tempcat[rom.cloneof] + elif rom: + rom.category = "Unknown" + elif rom and child.tag == "description": + rom.description = child.text + elif rom and child.tag == "year": + rom.year = child.text + elif rom and child.tag == "manufacturer": + rom.manufacturer = child.text + elif rom and child.tag == "driver": + rom.status = child.attrib["status"] + + if rom: + if rom.category not in exclude: + self.all_roms.append(rom) + + for cat in self.catdict: + if self.catdict[cat] == 0: + del self.all_categories[self.all_categories.index(cat)] + + with open(datfile, "wb") as output: + pickle.dump(mame_version, output) + pickle.dump(self.all_categories, output) + pickle.dump(self.all_roms, output) + + return True + + def filter(self): + self.roms = [] + + for rom in self.all_roms: + if rom.status in self.cfg["StatusFilter"]: + if self.cfg["Category"] == "All Games" or self.cfg["Category"] == rom.category: + if self.cfg["ShowClones"] or rom.cloneof is None: + if not self.hide_mature or "* Mature *" not in rom.category: + self.roms.append(rom) + + if self.cfg["Sort"] == "Name Asc": + self.roms.sort(key=lambda x: x.description.lower(), reverse=False) + elif self.cfg["Sort"] == "Name Dec": + self.roms.sort(key=lambda x: x.description.lower(), reverse=True) + elif self.cfg["Sort"] == "Year Asc": + self.roms.sort(key=lambda x: x.year.lower(), reverse=False) + elif self.cfg["Sort"] == "Year Dec": + self.roms.sort(key=lambda x: x.year.lower(), reverse=True) + + self.len = len(self.roms) + + if self.hide_mature: + self.categories = [] + for category in self.all_categories: + if "* Mature *" not in category: + self.categories.append(category) + else: + self.categories = self.all_categories diff --git a/mame_exclude.txt b/mame_exclude.txt new file mode 100644 index 0000000..6bf780a --- /dev/null +++ b/mame_exclude.txt @@ -0,0 +1,98 @@ +3D Printer +Astrological Computer +Audio Sequencer +Bank-teller Terminal +Barcode Printer +Bridge Machine +Business Computer / Terminal +Calculator / Pocket Computer +Cash Counter +Chess Machine +Clock +Credit Card Terminal +DVD Player +DVD Reader/Writer +Dame Machine +Development Computer +Devices +Document Processors +Dot-Matrix Display +Drum Machine +EPROM Programmer +Educational Game +Electromechanical / Change Money +Electromechanical / Coin Pusher +Electromechanical / Misc. +Electromechanical / Pinball +Electromechanical / Redemption +Electromechanical / Reels +Electromechanical / Utilities +Electronic Board Game +Electronic Typewriter +Engine Control Unit +Gambling Board +Game Console +Game Console Expansion +Graphic Tablet +Graphics Display Controller +Handheld Child Computers +Handheld Game +Handheld Game Console +Home Computer +In Circuit Emulator +JukeBox +Kit Computer +Laptop / Notebook / Portable +Laser Printer +Matrix Printer +Microcomputer +Misc. +Misc. * Mature * +Mobile Phone +Modem +Multi-cart Board +Network Processor +Not Classified +Pinball +Pinball * Mature * +Pinball / Pachinko +Pinball / Pachinko * Mature * +Player +Pocket Device / Pad / PDA +Portable Media Player +Print Club +Printer Handbook +Programming Machine +Punched Card Computer +Quiz / Chinese +Quiz / French +Quiz / German +Quiz / Italian +Quiz / Japanese +Quiz / Japanese * Mature * +Quiz / Japanese - Music +Quiz / Korean +Quiz / Spanish +Rhythm / Dance +Rhythm / Instruments +Rhythm / Misc. +Robot Control +Satellite Receiver +Single Board Computer +Speech Synthesizer +Synthesizer +System / BIOS +System / Device +Talking Calculator +Telephone / ComputerPhone +Test ROM +Thermal Printer +Toy cars +Training Board +Utilities / Test +Utilities / Update +VTR Control +Virtual Environment +Wavetables Generator +Word-processing Machine +Workstation / Server diff --git a/mfe.ini b/mfe.ini new file mode 100644 index 0000000..8383501 --- /dev/null +++ b/mfe.ini @@ -0,0 +1,37 @@ +Emulator = MAME +ResolutionX = 500 +ResolutionY = 500 +FullScreen = False +Font = microsoftsansserif +FontSize = 13 +QuitKeyPresses = 3 +ScanLines = True +Key_Up = K_UP, +Key_Down = K_DOWN, +Key_PgUp = K_LEFT, K_PAGEUP +Key_PgDn = K_RIGHT, K_PAGEDOWN +Key_Home = K_HOME, +Key_End = K_END, +Key_Select = K_RETURN, K_1 +Key_GameInfo = K_5, +Key_GameHistory = K_6, +Key_Popup = K_2, +Key_ShowArtwork = , +AlwaysChangeSnap = True +FPS = 60 +Key_GameHistoryxP = K_6, +[MAME] +EXE = /Users/rich/git/mame/mame +Version = /Users/rich/git/mame/mame +ShowClones = False +Category = All Games +GameAtTop = 0 +CurrentGame = 0 +EmulatorType = MAME +StatusFilter = good, imperfect +Sort = Name Asc +SnapDir = None +CSVFile = None +ArtworkDirs = cabinets, cpanel +ShowVideoSnaps = True +HideMature = True diff --git a/mfe.py b/mfe.py new file mode 100644 index 0000000..2349f36 --- /dev/null +++ b/mfe.py @@ -0,0 +1,65 @@ +import pyglet +from config import cfg + +from emu import emu + +from pygletgui import pygletgui + +icon = pyglet.resource.image("arcade.png") + +window = pyglet.window.Window( + cfg["ResolutionX"], + cfg["ResolutionY"], + fullscreen=cfg["FullScreen"], + style=pyglet.window.Window.WINDOW_STYLE_BORDERLESS, +) +pyglet.gl.glClearColor(255, 0, 0, 1.0) # red, green, blue, and alpha(transparency) + +window.set_icon(icon) +# window.set_exclusive_mouse() + +font = pyglet.font.load(cfg["Font"], cfg["FontSize"]) +font_height = font.ascent - font.descent + 1 +print(font_height) + +rom_list_width = int(cfg["ResolutionX"] / 2) + +emu = emu() + +gui = pygletgui(font_height) + +gui.add_menu( + 0, + cfg["ResolutionY"] - 1, + rom_list_width, + cfg["ResolutionY"] - 1, + border=True, + # data=self.roms, + data=["1", "2", "AAAAAaaaaAAAAAAAAaaaaaaaaaaaaasasadsfdsafdsfdsfdsfdsfdsfdsfsadfdsfsdfsdfdsfds"], + fg=(255, 255, 255), + bg=(0, 0, 0), + name="MainList", + # startitem=get_emu('CurrentGame'), + # itemattop=get_emu('GameAtTop')) + startitem=0, + itemattop=0, + font_height=font_height, +) + +# # self.gui.render() + + +@window.event +def on_draw(): + window.clear() + gui.render() + + +@window.event +def on_key_press(symbol, modifiers): + if symbol == pyglet.window.key.ESCAPE: + return + return pyglet.event.EVENT_HANDLED + + +pyglet.app.run() diff --git a/pyvidplayer2/.gitignore b/pyvidplayer2/.gitignore new file mode 100644 index 0000000..2f57176 --- /dev/null +++ b/pyvidplayer2/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +build/ +dist/ +*.egg-info/ +build.txt +test.py +test.mp4 +resources/ +.VSCodeCounter/ +video/ \ No newline at end of file diff --git a/pyvidplayer2/LICENSE b/pyvidplayer2/LICENSE new file mode 100644 index 0000000..2490cba --- /dev/null +++ b/pyvidplayer2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 ree1261 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pyvidplayer2/README.md b/pyvidplayer2/README.md new file mode 100644 index 0000000..55a365e --- /dev/null +++ b/pyvidplayer2/README.md @@ -0,0 +1,85 @@ +# pyvidplayer2 (please report all bugs!) + +Introducing pyvidplayer2, the successor to pyvidplayer. It's better in +pretty much every way, and finally allows an easy and reliable way to play videos in Python. + +All the features from the original library have been ported over, with the exception of ```alt_resize()```. Since pyvidplayer2 has a completely revamped foundation, the unreliability of ```set_size()``` has been quashed, and a fallback function is now redundant. + +# Features (tested on Windows) +- Easy to implement (4 lines of code) +- Fast and reliable +- Adjust playback speed +- No audio/video sync issues +- Subtitle support (.srt, .ass, etc) +- Play multiple videos in parallel +- Built in GUI +- Support for Pygame, Pyglet, Tkinter, and PyQT6 +- Can play all ffmpeg supported video formats +- Post process effects +- Webcam feed + +# Installation +``` +pip install pyvidplayer2 +``` +Note: FFMPEG (just the essentials is fine) must be installed and accessible via the system PATH. Here's an online article on how to do this (windows): +https://phoenixnap.com/kb/ffmpeg-windows. + +# Quickstart + +Refer to the examples folder for more basic guides, and documentation.md contains more detailed information. + +``` +import pygame +from pyvidplayer2 import Video + + +# create video object + +vid = Video("video.mp4") + +win = pygame.display.set_mode(vid.current_size) +pygame.display.set_caption(vid.name) + + +while vid.active: + key = None + for event in pygame.event.get(): + if event.type == pygame.QUIT: + vid.stop() + elif event.type == pygame.KEYDOWN: + key = pygame.key.name(event.key) + + if key == "r": + vid.restart() #rewind video to beginning + elif key == "p": + vid.toggle_pause() #pause/plays video + elif key == "m": + vid.toggle_mute() #mutes/unmutes video + elif key == "right": + vid.seek(15) #skip 15 seconds in video + elif key == "left": + vid.seek(-15) #rewind 15 seconds in video + elif key == "up": + vid.set_volume(1.0) #max volume + elif key == "down": + vid.set_volume(0.0) #min volume + elif key == "1": + vid.set_speed(1.0) #regular playback speed + elif key == "2": + vid.set_speed(2.0) #doubles video speed + + # only draw new frames, and only update the screen if something is drawn + + if vid.draw(win, (0, 0), force_draw=False): + pygame.display.update() + + pygame.time.wait(16) # around 60 fps + + +# close video when done + +vid.close() +pygame.quit() + +``` \ No newline at end of file diff --git a/pyvidplayer2/documentation.md b/pyvidplayer2/documentation.md new file mode 100644 index 0000000..772a63c --- /dev/null +++ b/pyvidplayer2/documentation.md @@ -0,0 +1,180 @@ +# Video(path, chunk_size=300, max_threads=1, max_chunks=1, subs=None, post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, use_pygame_audio=False) + +Main object used to play videos. It uses FFMPEG to extract chunks of audio from videos and then feeds it into a Pyaudio stream. Finally, it uses OpenCV to display the appropriate video frames. Videos can only be played simultaneously if they're using Pyaudio (see use_pygame_audio below). This object uses Pygame for graphics. See bottom for other supported libraries. + +## Arguments + - ```path``` - Path to video file. I tested a few popular video types, such as mkv, mp4, mov, avi, and 3gp, but theoretically anything FFMPEG can extract data from should work. + - ```chunk_size``` - How much audio is extracted at a time, in seconds. Increasing this value can mean less total extracts, but slower extracts. + - ```max_threads``` - Maximum number of chunks that can be extracted at any given time. Increasing this value can speed up extract at the expense of cpu usage. + - ```max_chunks``` - Maximum number of chunks allowed to be extracted and reserved. Increasing this value can help with buffering, but will use more memory. + - ```subs``` - Pass a Subtitle class here for the video to display subtitles. + - ```post_process``` - Post processing function that is applied whenever a frame is rendered. This is PostProcessing.none by default, which means no alterations are taking place. + - ```interp``` - Interpolation technique used when resizing frames. In general, the three main ones are cv2.INTER_LINEAR, which is balanced, cv2.INTER_CUBIC, which is slower but produces better results, and cv2.INTER_AREA, which is better for downscaling. + - ```use_pygame_audio``` - Specifies whether to use Pyaudio or Pygame to play audio. + +## Attributes + - ```path``` - Same as given argument. + - ```name``` - Name of file without the directory and extension. + - ```ext``` - Type of video (mp4, mkv, mov, etc). + - ```frame``` - Current frame index. Starts from 0. + - ```frame_rate``` - How many frames are in one second. + - ```frame_count``` - How many total frames there are. + - ```frame_delay``` - Time between frames in order to maintain frame rate (in fractions of a second). + - ```duration``` - Length of video in seconds. + - ```original_size``` + - ```current_size``` + - ```aspect_ratio``` - Width divided by height. + - ```chunk_size``` - Same as given argument. + - ```max_chunks``` - Same as given argument. + - ```max_threads``` - Same as given argument. + - ```frame_data``` - Current video frame as a NumPy ndarray. + - ```frame_surf``` - Current video frame as a Pygame Surface. + - ```active``` - Whether the video is currently playing. This is unaffected by pausing and resuming. + - ```buffering``` - Whether the video is waiting for audio to extract. + - ```paused``` + - ```muted``` + - ```speed``` - Float from 0.5 to 10.0 that multiplies the playback speed. + - ```subs``` - Same as given argument. + - ```post_func``` - Same as given argument. + - ```interp``` - Same as given argument. + - ```use_pygame_audio``` - Same as given argument. + +## Methods + - ```play()``` + - ```stop()``` + - ```resize(size)``` + - ```change_resolution(height)``` - Given a height, the video will scale its width while maintaining aspect ratio. + - ```close()``` - Releases resources. Always recommended to call when done. + - ```restart() ``` + - ```set_speed(speed)``` - Accepts a float from 0.5 (half speed) to 10.0 (ten times speed) + - ```get_speed()``` + - ```set_volume(volume)``` - Adjusts the volume of the video, from 0.0 (min) to 1.0 (max). + - ```get_volume()``` + - ```get_paused()``` + - ```toggle_pause()``` - Pauses if the video is playing, and resumes if the video is paused. + - ```pause()``` + - ```resume()``` + - ```toggle_mute()``` + - ```mute()``` + - ```unmute()``` + - ```get_pos()``` - Returns the current position in seconds. + - ```seek(time, relative=True)``` - Changes the current position in the video. If relative is true, the given time will be added or subtracted to the current time. Otherwise, the current position will be set to the given time exactly. Time must be given in seconds, and seeking will be accurate to one tenth of a second. + - ```draw(surf, pos, force_draw=True)``` - Draws the current video frame onto the given surface, at the given position. If force_draw is true, a surface will be drawn every time this is called. Otherwise, only new frames will be drawn. This reduces cpu usage, but will cause flickering if anything is drawn under or above the video. This method also returns whether a frame was drawn. + - ```preview()``` - Opens a window and plays the video. This method will hang until the video closes. Videos are played at 60 fps with force_draw disabled. + +# VideoPlayer(video, rect, interactable=True, loop=False, preview_thumbnails=0) + +VideoPlayers are GUI containers for videos. This seeks to mimic standard video players, so clicking it will play/pause playback, and the GUI will only show when the mouse is hovering over it. Only supported for Pygame. + +## Arguments + - ```video``` - Video object to play. + - ```rect``` - An x, y, width, and height of the VideoPlayer. The topleft corner will be the x, y coordinate. + - ```interactable``` - Enables the GUI. + - ```loop``` - Whether the contained video will restart after it finishes. If the queue is not empty, the entire queue will loop, not just the current video. + - ```preview_thumbnails``` - Number of preview thumbnails loaded and saved in memory. When seeking, a preview window will show the closest loaded frame. The higher this number is, the more frames are loaded, increasing the preview accuracy, but also increasing initial load time and memory usage. Because of this, this value is defaulted to 0, which turns seek previewing off. + +## Attributes + - ```video``` - Same as given argument. + - ```frame_rect``` - Same as given argument. + - ```vid_rect``` - This is the video fitted into the frame_rect while maintaining aspect ratio. Black bars will appear in any unused space. + - ```interactable``` - Same as given argument. + - ```loop``` - Same as given argument. + - ```queue_``` - Videos to play after the current one finishes. + - ```preview_thumbnails``` - Same as given argument. + +## Methods + - ```zoom_to_fill()``` - Zooms in the video so that the entire frame_rect is filled in, while maintaining aspect ratio. + - ```zoom_out()``` - Reverts zoom_to_fill() + - ```queue(input)``` - Accepts a path to a video or a Video object and adds it to the queue. Passing a path will not load the video until it becomes the active video. Passing a Video object will cause it to silently load its first audio chunk, so changing videos will be as seamless as possible. + - ```get_queue()``` + - ```resize(size)``` + - ```move(pos, relative)``` - Moves the VideoPlayer. If relative is true, the given coordinates will be added onto the current coordinates. Otherwise, the current coordinates will be set to the given coordinates. + - ```update(events, show_ui=None)``` - Allows the VideoPlayer to make calculations. It must be given the returns of pygame.event.get(). The GUI automatically shows up when your mouse hovers over the video player, so show_ui can be used to override that. This method also returns show_ui. + - ```draw(surface)``` - Draws the VideoPlayer onto the given Surface. + - ```close()``` - Releases resources. Always recommended to call when done. + - ```skip()``` - Moves onto the next video in the queue. + - ```get_video()``` - Returns currently playing video. + +# Subtitles(path, colour="white", highlight=(0, 0, 0, 128), font=pygame.font.SysFont("arial", 30), encoding="utf-8-sig") + +Object used for handling subtitles. Only supported for Pygame. + +## Arguments + - ```path``` - Path to subtitle file. This can be any file pysubs2 can read, including .srt, .ass, .vtt, and others. + - ```colour``` - Colour of text. + - ```highlight``` - Background colour of text. Accepts RGBA, so it can be made completely transparent. + - ```font``` - Pygame Font or SysFont object used to render Surfaces. This includes the size of the text. + - ```encoding``` - Encoding used to open the srt file. + - ```offset``` - The higher this number is, the close the subtitle is to the top of the screen. + +## Attributes + - ```path``` - Same as given argument. + - ```encoding``` - Same as given argument. + - ```start``` - Starting timestamp of current subtitle. + - ```end``` - Ending timestamp of current subtitle. + - ```text``` - Current subtitle text. + - ```surf``` - Current text in a Pygame Surface. + - ```colour``` - Same as given argument. + - ```highlight``` - Same as given argument. + - ```font``` - Same as given argument. + - ```offset``` - Same as given argument. + +## Methods + - ```set_font(font)``` + - ```get_font()``` + +# Webcam(post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, fps=30) + +Object used for displaying a webcam feed. Only supported for Pygame. + +## Arguments + - ```post_process``` - Post processing function that is applied whenever a frame is rendered. This is PostProcessing.none by default, which means no alterations are taking place. + - ```interp``` - Interpolation technique used when resizing frames. In general, the three main ones are cv2.INTER_LINEAR, which is balanced, cv2.INTER_CUBIC, which is slower but produces better results, and cv2.INTER_AREA, which is better for downscaling. + - ```fps``` - Maximum number of frames captured from the webcam per second. + +## Attributes + - ```post_process``` - Same as given argument. + - ```interp``` - Same as given argument. + - ```fps``` - Same as given argument. + - ```original_size``` + - ```current_size``` + - ```aspect_ratio``` - Width divided by height. + - ```active``` - Whether the webcam is currently playing. + - ```frame_data``` - Current video frame as a NumPy ndarray. + - ```frame_surf``` - Current video frame as a Pygame Surface. + +## Methods + - ```play()``` + - ```stop()``` + - ```resize(size)``` + - ```change_resolution(height)``` - Given a height, the video will scale its width while maintaining aspect ratio. + - ```close()``` - Releases resources. Always recommended to call when done. + - ```get_pos()``` - Returns how long the webcam has been active. Is not reset if webcam is stopped. + - ```draw(surf, pos, force_draw=True)``` - Draws the current video frame onto the given surface, at the given position. If force_draw is true, a surface will be drawn every time this is called. Otherwise, only new frames will be drawn. This reduces cpu usage, but will cause flickering if anything is drawn under or above the video. This method also returns whether a frame was drawn. + - ```preview()``` - Opens a window and plays the webcam. This method will hang until the window is closed. Videos are played at whatever fps the webcam object is set to. + +# PostProcessing +Used to apply various filters to video playback. Mostly for fun. Works across all graphics libraries. + - ```none``` - Default. Nothing happens. + - ```blur``` - Slightly blurs frames. + - ```sharpen``` - An okay-looking sharpen. Looks pretty bad for small resolutions. + - ```greyscale``` - Removes colour from frame. + - ```noise``` - Adds a static-like filter. Very intensive. + - ```letterbox``` - Adds black bars above and below the frame to look more cinematic. + - ```cel_shading``` - Thickens borders for a comic book style filter. + +# Supported Graphics Libraries + - Pygame (```Video```) <- default and best supported + - Tkinter (```VideoTkinter```) + - Pyglet (```VideoPyglet```) + - PyQT6 (```VideoPyQT```) + +To use other libraries instead of Pygame, use their respective video object. Each preview method will use their respective graphics API to create a window and draw frames. See the examples folder for details. Note that Subtitles, Webcam, and VideoPlayer only work with Pygame installed. + +# Get Version + +``` +print(pyvidplayer2.get_version_info()) +``` + +Returns a dictionary with the version of pyvidplayer2, FFMPEG, and Pygame. \ No newline at end of file diff --git a/pyvidplayer2/examples/all_previews_demo.py b/pyvidplayer2/examples/all_previews_demo.py new file mode 100644 index 0000000..c3e5b56 --- /dev/null +++ b/pyvidplayer2/examples/all_previews_demo.py @@ -0,0 +1,13 @@ +''' +This shows off each graphics api and their respective preview methods +''' + + +from pyvidplayer2 import Video, VideoTkinter, VideoPyglet, VideoPyQT + +PATH = r"resources\trailer1.mp4" + +Video(PATH).preview() +VideoTkinter(PATH).preview() +VideoPyglet(PATH).preview() +VideoPyQT(PATH).preview() \ No newline at end of file diff --git a/pyvidplayer2/examples/cel_shading_demo.py b/pyvidplayer2/examples/cel_shading_demo.py new file mode 100644 index 0000000..68fd123 --- /dev/null +++ b/pyvidplayer2/examples/cel_shading_demo.py @@ -0,0 +1,7 @@ +''' +This is an example of the cel shading post process you can apply to your videos +''' + +from pyvidplayer2 import Video, PostProcessing + +Video(r"resources\medic.mov", post_process=PostProcessing.cel_shading).preview() \ No newline at end of file diff --git a/pyvidplayer2/examples/custom_post_processing_demo.py b/pyvidplayer2/examples/custom_post_processing_demo.py new file mode 100644 index 0000000..44a63ba --- /dev/null +++ b/pyvidplayer2/examples/custom_post_processing_demo.py @@ -0,0 +1,13 @@ +''' +This example shows how you can merge and create new post processing functions +''' + + +from pyvidplayer2 import Video, PostProcessing + +# applies a letterbox, cel shading, and greyscale to video + +def custom_process(data): + return PostProcessing.letterbox(PostProcessing.cel_shading(PostProcessing.greyscale(data))) + +Video(r"resources\birds.avi", post_process=custom_process).preview() \ No newline at end of file diff --git a/pyvidplayer2/examples/custom_subtitles_demo.py b/pyvidplayer2/examples/custom_subtitles_demo.py new file mode 100644 index 0000000..1cd0ea0 --- /dev/null +++ b/pyvidplayer2/examples/custom_subtitles_demo.py @@ -0,0 +1,11 @@ +''' +This is an example of custom subtitle fonts +''' + + +from pyvidplayer2 import Subtitles, Video +from pygame.font import Font + +subtitles = Subtitles(r"resources\subs2.srt", font=Font(r"resources\font.ttf", 60), highlight=(255, 0, 0, 128), offset=500) + +Video(r"resources\trailer2.mp4", subs=subtitles).preview() \ No newline at end of file diff --git a/pyvidplayer2/examples/many_videos_demo.py b/pyvidplayer2/examples/many_videos_demo.py new file mode 100644 index 0000000..42f0f8c --- /dev/null +++ b/pyvidplayer2/examples/many_videos_demo.py @@ -0,0 +1,39 @@ +''' +This is an example of a VideoCollection, which allows you to treat a large +amount of ParallelVideos as one +''' + + +import pygame +from pyvidplayer2 import Video, VideoPlayer + +win = pygame.display.set_mode((1066, 744)) +pygame.display.set_caption("video collection demo") + + +videos = [VideoPlayer(Video(r"resources\billiejean.mp4"), (0, 0, 426, 240), interactable=False), + VideoPlayer(Video(r"resources\trailer1.mp4"), (426, 0, 256, 144), interactable=False), + VideoPlayer(Video(r"resources\medic.mov"), (682, 0, 256, 144), interactable=False), + VideoPlayer(Video(r"resources\trailer2.mp4"), (426, 144, 640, 360), interactable=False), + VideoPlayer(Video(r"resources\clip.mp4"), (0, 240, 256, 144), interactable=False), + VideoPlayer(Video(r"resources\birds.avi"), (0, 384, 426, 240), interactable=False), + VideoPlayer(Video(r"resources\ocean.mkv"), (426, 504, 426, 240), interactable=False)] + +while True: + key = None + for event in pygame.event.get(): + if event.type == pygame.QUIT: + [video.close() for video in videos] + pygame.quit() + exit() + elif event.type == pygame.KEYDOWN: + key = pygame.key.name(event.key) + + pygame.time.wait(16) + + win.fill("white") + + [video.update() for video in videos] + [video.draw(win) for video in videos] + + pygame.display.update() \ No newline at end of file diff --git a/pyvidplayer2/examples/parallel_playing_demo.py b/pyvidplayer2/examples/parallel_playing_demo.py new file mode 100644 index 0000000..7f3e0d2 --- /dev/null +++ b/pyvidplayer2/examples/parallel_playing_demo.py @@ -0,0 +1,32 @@ +''' +This is an example of two videos playing simultaneously +''' + +import pygame +from pyvidplayer2 import Video + + +win = pygame.display.set_mode((960, 360)) +pygame.display.set_caption("parallel playing demo") + +vid1 = Video(r"resources\trailer1.mp4") +vid1.resize((480, 360)) + +vid2 = Video(r"resources\trailer2.mp4") +vid2.resize((480, 360)) + + +while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + vid1.close() + vid2.close() + pygame.quit() + exit() + + pygame.time.wait(16) + + vid1.draw(win, (0, 0)) + vid2.draw(win, (480, 0)) + + pygame.display.update() \ No newline at end of file diff --git a/pyvidplayer2/examples/parallel_subtitles_demo.py b/pyvidplayer2/examples/parallel_subtitles_demo.py new file mode 100644 index 0000000..fdde079 --- /dev/null +++ b/pyvidplayer2/examples/parallel_subtitles_demo.py @@ -0,0 +1,32 @@ +''' +This example shows how every video can play subtitles +''' + +import pygame +from pyvidplayer2 import Video, Subtitles + + +win = pygame.display.set_mode((960, 360)) +pygame.display.set_caption("parallel subtitles demo") + +vid1 = Video(r"resources\trailer1.mp4", subs=Subtitles(r"resources\subs1.srt")) +vid1.resize((480, 360)) + +vid2 = Video(r"resources\trailer2.mp4", subs=Subtitles(r"resources\subs2.srt")) +vid2.resize((480, 360)) + + +while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + vid1.close() + vid2.close() + pygame.quit() + exit() + + pygame.time.wait(16) + + vid1.draw(win, (0, 0)) + vid2.draw(win, (480, 0)) + + pygame.display.update() \ No newline at end of file diff --git a/pyvidplayer2/examples/pip_demo.py b/pyvidplayer2/examples/pip_demo.py new file mode 100644 index 0000000..e104bfd --- /dev/null +++ b/pyvidplayer2/examples/pip_demo.py @@ -0,0 +1,59 @@ +''' +A quick example showing how pyvidplayer2 can be used in more complicated applications +This is a Picture-in-Picture app +''' + + +import pygame +from win32gui import SetWindowPos, GetCursorPos, GetWindowRect, GetForegroundWindow, SetForegroundWindow +from win32api import GetSystemMetrics +from win32con import SWP_NOSIZE, HWND_TOPMOST +from win32com.client import Dispatch +from pyvidplayer2 import VideoPlayer, Video +from cv2 import INTER_AREA + + +SIZE = (426, 240) +FILE = r"resources\billiejean.mp4" + +win = pygame.display.set_mode(SIZE, pygame.NOFRAME) + +# creates the video player + +vid = VideoPlayer(Video(FILE, interp=INTER_AREA), (0, 0, *SIZE)) + +# moves the window to the bottom right corner and pins it above other windows + +hwnd = pygame.display.get_wm_info()["window"] +SetWindowPos(hwnd, HWND_TOPMOST, GetSystemMetrics(0) - SIZE[0], GetSystemMetrics(1) - SIZE[1] - 48, 0, 0, SWP_NOSIZE) + +clock = pygame.time.Clock() + +shell = Dispatch("WScript.Shell") + +while True: + events = pygame.event.get() + for event in events: + if event.type == pygame.QUIT: + vid.close() + pygame.quit() + quit() + + clock.tick(60) + + # allows the ui to be seamlessly interacted with + + touching = pygame.Rect(GetWindowRect(hwnd)).collidepoint(GetCursorPos()) + if touching and GetForegroundWindow() != hwnd: + + # weird behaviour with SetForegroundWindow that requires the alt key to be pressed before it's called + + shell.SendKeys("%") + SetForegroundWindow(hwnd) + + # handles video playback + + vid.update(events, show_ui=touching) + vid.draw(win) + + pygame.display.update() \ No newline at end of file diff --git a/pyvidplayer2/examples/playback_speed_demo.py b/pyvidplayer2/examples/playback_speed_demo.py new file mode 100644 index 0000000..7a7cbcd --- /dev/null +++ b/pyvidplayer2/examples/playback_speed_demo.py @@ -0,0 +1,10 @@ +''' +This example shows how you can control the playback speed of videos +''' + + +from pyvidplayer2 import Video + +v = Video(r"resources\trailer1.mp4") +v.set_speed(2) # twice as fast +v.preview() diff --git a/pyvidplayer2/examples/post_processing_demo.py b/pyvidplayer2/examples/post_processing_demo.py new file mode 100644 index 0000000..afef78e --- /dev/null +++ b/pyvidplayer2/examples/post_processing_demo.py @@ -0,0 +1,42 @@ +''' +This example gives a side by side comparison between a few available post process effects +''' + + +import pygame +from pyvidplayer2 import Video, PostProcessing + +PATH = r"resources\ocean.mkv" + +win = pygame.display.set_mode((960, 240)) +pygame.display.set_caption("post processing demo") + +# using a video collection to play videos in parallel for a side to side comparison + +videos = [Video(PATH, post_process=PostProcessing.sharpen), + Video(PATH), + Video(PATH, post_process=PostProcessing.blur)] + +font = pygame.font.SysFont("arial", 30) +surfs = [font.render("Sharpen", True, "white"), font.render("Normal", True, "white"), font.render("Blur", True, "white")] + + +while True: + key = None + for event in pygame.event.get(): + if event.type == pygame.QUIT: + [video.close() for video in videos] + pygame.quit() + exit() + elif event.type == pygame.KEYDOWN: + key = pygame.key.name(event.key) + + pygame.time.wait(16) + + for i, surf in enumerate(surfs): + x = 320 * i + videos[i].draw(win, (x, 0)) + pygame.draw.rect(win, "black", (x, 0, *surf.get_size())) + win.blit(surf, (x, 0)) + + pygame.display.update() \ No newline at end of file diff --git a/pyvidplayer2/examples/preview_demo.py b/pyvidplayer2/examples/preview_demo.py new file mode 100644 index 0000000..0070b24 --- /dev/null +++ b/pyvidplayer2/examples/preview_demo.py @@ -0,0 +1,8 @@ +''' +This is the quickest and simplest way to play videos +''' + + +from pyvidplayer2 import Video + +Video(r"resources\trailer1.mp4").preview() diff --git a/pyvidplayer2/examples/pyglet_demo.py b/pyvidplayer2/examples/pyglet_demo.py new file mode 100644 index 0000000..8c45896 --- /dev/null +++ b/pyvidplayer2/examples/pyglet_demo.py @@ -0,0 +1,22 @@ +''' +This is a quick example of integrating a video into a pyglet project +Double buffering is turned off to benefit from turning off force draw on the video +''' + + +import pyglet +from pyvidplayer2 import VideoPyglet + +video = VideoPyglet(r"resources\trailer1.mp4") + +def update(dt): + video.draw((0, 0), force_draw=False) + if not video.active: + win.close() + +win = pyglet.window.Window(width=video.current_size[0], height=video.current_size[1], config=pyglet.gl.Config(double_buffer=False), caption=f"pyglet support demo") + +pyglet.clock.schedule_interval(update, 1/60.0) + +pyglet.app.run() +video.close() \ No newline at end of file diff --git a/pyvidplayer2/examples/pyqt6_demo.py b/pyvidplayer2/examples/pyqt6_demo.py new file mode 100644 index 0000000..9ac2ff8 --- /dev/null +++ b/pyvidplayer2/examples/pyqt6_demo.py @@ -0,0 +1,33 @@ +''' +This is a quick example of integrating a video into a pyqt6 project +''' + + +from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget +from PyQt6.QtCore import QTimer +from pyvidplayer2 import VideoPyQT + + +class Window(QMainWindow): + def __init__(self): + super().__init__() + self.canvas = QWidget(self) + self.setCentralWidget(self.canvas) + + self.timer = QTimer(self) + self.timer.timeout.connect(self.update) + self.timer.start(16) + + def paintEvent(self, _): + video.draw(self, (0, 0)) + + +video = VideoPyQT(r"resources\trailer1.mp4") + +app = QApplication([]) +win = Window() +win.setWindowTitle(f"pyqt6 support demo") +win.setFixedSize(*video.current_size) +win.show() +app.exec() +video.close() \ No newline at end of file diff --git a/pyvidplayer2/examples/queue_demo.py b/pyvidplayer2/examples/queue_demo.py new file mode 100644 index 0000000..75c8ffc --- /dev/null +++ b/pyvidplayer2/examples/queue_demo.py @@ -0,0 +1,32 @@ +''' +This example shows how videos can be queued and skipped through with the VideoPlayer object +''' + +import pygame +from pyvidplayer2 import VideoPlayer, Video + +win = pygame.display.set_mode((1280, 720)) + + +vid = VideoPlayer(Video(r"resources\clip.mp4"), (0, 0, 1280, 720), loop=True) + +vid.queue(Video(r"resources\ocean.mkv")) +vid.queue(Video(r"resources\birds.avi")) + + +while True: + events = pygame.event.get() + for event in events: + if event.type == pygame.QUIT: + vid.close() + pygame.quit() + exit() + elif event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE: + vid.skip() + + pygame.time.wait(16) + + vid.update(events) + vid.draw(win) + + pygame.display.update() \ No newline at end of file diff --git a/pyvidplayer2/examples/subtitles_demo.py b/pyvidplayer2/examples/subtitles_demo.py new file mode 100644 index 0000000..2d55043 --- /dev/null +++ b/pyvidplayer2/examples/subtitles_demo.py @@ -0,0 +1,8 @@ +''' +This is an example showing how to add subtitles to a video +''' + + +from pyvidplayer2 import Subtitles, Video + +Video(r"resources\trailer2.mp4", subs=Subtitles(r"resources\subs2.srt")).preview() \ No newline at end of file diff --git a/pyvidplayer2/examples/tkinter_demo.py b/pyvidplayer2/examples/tkinter_demo.py new file mode 100644 index 0000000..0a9c62f --- /dev/null +++ b/pyvidplayer2/examples/tkinter_demo.py @@ -0,0 +1,27 @@ +''' +This is a quick example of integrating a video into a tkinter project +''' + + +import tkinter +from pyvidplayer2 import VideoTkinter + +video = VideoTkinter(r"resources\trailer1.mp4") + +def update(): + video.draw(canvas, (video.current_size[0] / 2, video.current_size[1] / 2), force_draw=False) + if video.active: + root.after(16, update) # for around 60 fps + else: + root.destroy() + +root = tkinter.Tk() +root.title(f"tkinter support demo") + +canvas = tkinter.Canvas(root, width=video.current_size[0], height=video.current_size[1], highlightthickness=0) +canvas.pack() + +update() +root.mainloop() + +video.close() \ No newline at end of file diff --git a/pyvidplayer2/examples/video_demo.py b/pyvidplayer2/examples/video_demo.py new file mode 100644 index 0000000..1e2a140 --- /dev/null +++ b/pyvidplayer2/examples/video_demo.py @@ -0,0 +1,46 @@ +''' +This is the same example from the original pyvidplayer +The video class still does everything it did, but with many more features +''' + + +import pygame +from pyvidplayer2 import Video + +pygame.init() +win = pygame.display.set_mode((1280, 720)) +clock = pygame.time.Clock() + +#provide video class with the path to your video +vid = Video(r"resources\medic.mov") + +while True: + key = None + for event in pygame.event.get(): + if event.type == pygame.QUIT: + vid.close() + pygame.quit() + exit() + elif event.type == pygame.KEYDOWN: + key = pygame.key.name(event.key) + + #your program frame rate does not affect video playback + clock.tick(60) + + if key == "r": + vid.restart() #rewind video to beginning + elif key == "p": + vid.toggle_pause() #pause/plays video + elif key == "right": + vid.seek(15) #skip 15 seconds in video + elif key == "left": + vid.seek(-15) #rewind 15 seconds in video + elif key == "up": + vid.set_volume(1.0) #max volume + elif key == "down": + vid.set_volume(0.0) #min volume + + #draws the video to the given surface, at the given position + vid.draw(win, (0, 0), force_draw=False) + + pygame.display.update() \ No newline at end of file diff --git a/pyvidplayer2/examples/videoplayer_demo.py b/pyvidplayer2/examples/videoplayer_demo.py new file mode 100644 index 0000000..343eb01 --- /dev/null +++ b/pyvidplayer2/examples/videoplayer_demo.py @@ -0,0 +1,30 @@ +''' +This is an example of the built in GUI for videos +''' + + +import pygame +from pyvidplayer2 import VideoPlayer, Video + +win = pygame.display.set_mode((1124, 868)) +pygame.display.set_caption("video player demo") + +vid = VideoPlayer(Video(r"resources\ocean.mkv"), (50, 50, 1024, 768), preview_thumbnails=11) + + +while True: + events = pygame.event.get() + for event in events: + if event.type == pygame.QUIT: + vid.close() + pygame.quit() + exit() + + pygame.time.wait(16) + + win.fill("white") + + vid.update(events) + vid.draw(win) + + pygame.display.update() diff --git a/pyvidplayer2/examples/webcam_demo.py b/pyvidplayer2/examples/webcam_demo.py new file mode 100644 index 0000000..964426d --- /dev/null +++ b/pyvidplayer2/examples/webcam_demo.py @@ -0,0 +1,24 @@ +''' +Webcam example +''' + +import pygame +from pyvidplayer2 import Webcam + +webcam = Webcam() + +win = pygame.display.set_mode(webcam.current_size) +clock = pygame.time.Clock() + +while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + webcam.close() + pygame.quit() + exit() + + clock.tick(60) + + webcam.draw(win, (0, 0), force_draw=False) + + pygame.display.update() \ No newline at end of file diff --git a/pyvidplayer2/pyvidplayer2/__init__.py b/pyvidplayer2/pyvidplayer2/__init__.py new file mode 100644 index 0000000..a2fe8a0 --- /dev/null +++ b/pyvidplayer2/pyvidplayer2/__init__.py @@ -0,0 +1,49 @@ +import subprocess + +from .post_processing import PostProcessing +from .video_tkinter import VideoTkinter + +try: + import PyQt6 +except ImportError: + pass +else: + from .video_pyqt import VideoPyQT + +try: + import pygame +except ImportError: + pass +else: + pygame.init() + + from .video_pygame import VideoPygame as Video + from .subtitles import Subtitles + from .video_player import VideoPlayer + from .webcam import Webcam + +try: + import pyglet +except ImportError: + pass +else: + from .video_pyglet import VideoPyglet + + +_VERSION = "0.9.11" + + +def get_version_info() -> dict: + try: + pygame_ver = pygame.version.ver + except NameError: + pygame_ver = "not installed" + + try: + ffmpeg_ver = subprocess.run(["ffmpeg", "-version"], capture_output=True, universal_newlines=True).stdout.split(" ")[2] + except FileNotFoundError: + ffmpeg_ver = "not installed" + + return {"pyvidplayer2": _VERSION, + "ffmpeg": ffmpeg_ver, + "pygame": pygame_ver} \ No newline at end of file diff --git a/pyvidplayer2/pyvidplayer2/error.py b/pyvidplayer2/pyvidplayer2/error.py new file mode 100644 index 0000000..72d1ccb --- /dev/null +++ b/pyvidplayer2/pyvidplayer2/error.py @@ -0,0 +1,2 @@ +class Pyvidplayer2Error(Exception): + pass diff --git a/pyvidplayer2/pyvidplayer2/mixer_handler.py b/pyvidplayer2/pyvidplayer2/mixer_handler.py new file mode 100644 index 0000000..7ecd87b --- /dev/null +++ b/pyvidplayer2/pyvidplayer2/mixer_handler.py @@ -0,0 +1,50 @@ +import pygame +from io import BytesIO + + +class MixerHandler: + def __init__(self) -> None: + self.muted = False + self.volume = 1 + + def get_busy(self): + return pygame.mixer.music.get_busy() + + def load(self, bytes): + pygame.mixer.music.load(BytesIO(bytes)) + + def unload(self): + self.stop() + pygame.mixer.music.unload() + + def play(self): + pygame.mixer.music.play() + + def set_volume(self, vol): + self.volume = vol + pygame.mixer.music.set_volume(min(1.0, max(0.0, vol))) + + def get_volume(self): + return self.volume + + def get_pos(self): + return max(0, pygame.mixer.music.get_pos()) / 1000 + + def stop(self): + pygame.mixer.music.stop() + + def pause(self): + pygame.mixer.music.pause() + + def unpause(self): + # unpausing the mixer when nothing has been loaded causes weird behaviour + if pygame.mixer.music.get_pos() != -1: + pygame.mixer.music.unpause() + + def mute(self): + self.muted = True + pygame.mixer.music.set_volume(0) + + def unmute(self): + self.muted = False + self.set_volume(self.volume) \ No newline at end of file diff --git a/pyvidplayer2/pyvidplayer2/post_processing.py b/pyvidplayer2/pyvidplayer2/post_processing.py new file mode 100644 index 0000000..2d2289f --- /dev/null +++ b/pyvidplayer2/pyvidplayer2/post_processing.py @@ -0,0 +1,33 @@ +import cv2 +import numpy + + +class PostProcessing: + def none(data: numpy.ndarray) -> numpy.ndarray: + return data + + def blur(data: numpy.ndarray) -> numpy.ndarray: + return cv2.blur(data, (5, 5)) + + def sharpen(data: numpy.ndarray) -> numpy.ndarray: + return cv2.filter2D(data, -1, numpy.array([[-1,-1,-1], [-1,9,-1], [-1,-1,-1]])) + + def greyscale(data: numpy.ndarray) -> numpy.ndarray: + return numpy.stack((cv2.cvtColor(data, cv2.COLOR_BGR2GRAY),) * 3, axis=-1) + + def noise(data: numpy.ndarray) -> numpy.ndarray: + noise = numpy.zeros(data.shape, dtype=numpy.uint8) + cv2.randn(noise, (0,) * 3, (20,) * 3) + return data + noise + + def letterbox(data: numpy.ndarray) -> numpy.ndarray: + background = numpy.zeros((*data.shape[:2], 3), dtype=numpy.uint8) + + x1, y1 = 0, int(data.shape[0] * 0.1) #topleft crop + x2, y2 = data.shape[1], int(data.shape[0] * 0.9) #bottomright crop + data = data[y1:y2, x1:x2] # crops image + background[y1:y1 + data.shape[0], x1:x1 + data.shape[1]] = data # draws image onto background + return background + + def cel_shading(data: numpy.ndarray) -> numpy.ndarray: + return cv2.subtract(data, cv2.blur(cv2.merge((cv2.Canny(data, 150, 200),) * 3), (2, 2))) \ No newline at end of file diff --git a/pyvidplayer2/pyvidplayer2/pyaudio_handler.py b/pyvidplayer2/pyvidplayer2/pyaudio_handler.py new file mode 100644 index 0000000..79db897 --- /dev/null +++ b/pyvidplayer2/pyvidplayer2/pyaudio_handler.py @@ -0,0 +1,125 @@ +import pyaudio +import wave +import math +import time +import numpy +from threading import Thread +from io import BytesIO + + +class PyaudioHandler: + def __init__(self) -> None: + self.stream = None + self.wave = None + + self.thread = None + self.stop_thread = False + + self.position = 0 + + self.loaded = False + self.paused = False + self.active = False + + self.volume = 1.0 + self.muted = False + + self.p = pyaudio.PyAudio() + self.stream = None + + def get_busy(self): + return self.active + + def load(self, bytes): + self.unload() + + try: + self.wave = wave.open(BytesIO(bytes), "rb") + except EOFError: + raise EOFError("Audio is empty. This may mean the file is corrupted.") + + if self.stream is None: + self.stream = self.p.open( + format=self.p.get_format_from_width(self.wave.getsampwidth()), + channels=self.wave.getnchannels(), + rate=self.wave.getframerate(), + output=True) + + self.loaded = True + + def close(self): + self.stream.stop_stream() + self.stream.close() + self.p.terminate() + + def unload(self): + if self.loaded: + self.stop() + + self.wave.close() + + self.wave = None + self.thread = None + + self.loaded = False + + def play(self): + self.stop_thread = False + self.position = 0 + self.active = True + + self.wave.rewind() + self.thread = Thread(target=self._threaded_play) + + self.thread.start() + + def _threaded_play(self): + chunk = 2048 + data = self.wave.readframes(chunk) + + while data != b'' and not self.stop_thread: + + if self.paused: + time.sleep(0.01) + else: + audio = numpy.frombuffer(data, dtype=numpy.int16) + + if self.volume == 0.0 or self.muted: + audio = numpy.zeros_like(audio) + else: + db = 20 * math.log10(self.volume) + audio = (audio * 10**(db/20)).astype(numpy.int16) + + self.stream.write(audio.tobytes()) + data = self.wave.readframes(chunk) + + self.position += chunk / self.wave.getframerate() + + self.active = False + + def set_volume(self, vol): + self.volume = min(1.0, max(0.0, vol)) + + def get_volume(self): + return self.volume + + def get_pos(self): + return self.position + + def stop(self): + if self.loaded: + self.stop_thread = True + self.thread.join() + self.position = 0 + + def pause(self): + self.paused = True + + def unpause(self): + self.paused = False + + def mute(self): + self.muted = True + + def unmute(self): + self.muted = False \ No newline at end of file diff --git a/pyvidplayer2/pyvidplayer2/subtitles.py b/pyvidplayer2/pyvidplayer2/subtitles.py new file mode 100644 index 0000000..14fca49 --- /dev/null +++ b/pyvidplayer2/pyvidplayer2/subtitles.py @@ -0,0 +1,68 @@ +import pygame +import pysubs2 + + +class Subtitles: + def __init__(self, path: str, colour="white", highlight=(0, 0, 0, 128), font=pygame.font.SysFont("arial", 30), encoding="utf-8", offset=50) -> None: + self.path = path + self.encoding = encoding + + self._subs = iter(pysubs2.load(path, encoding=encoding)) + + self.start = 0 + self.end = 0 + self.text = "" + self.surf = pygame.Surface((0, 0)) + self.offset = offset + + self.colour = colour + self.highlight = highlight + self.font = font + + def __str__(self) -> str: + return f"" + + def _to_surf(self, text: str) -> pygame.Surface: + h = self.font.render(" ", True, "black").get_height() + + lines = text.strip().split("\n") + surfs = [self.font.render(line, True, self.colour) for line in lines] + + surface = pygame.Surface((max([s.get_width() for s in surfs]), len(surfs) * h), pygame.SRCALPHA) + surface.fill(self.highlight) + for i, surf in enumerate(surfs): + surface.blit(surf, (surface.get_width() / 2 - surf.get_width() / 2, i * h)) + + return surface + + def _get_next(self) -> bool: + try: + s = next(self._subs) + except StopIteration: + self.start = 0 + self.end = 0 + self.text = "" + self.surf = pygame.Surface((0, 0)) + return False + else: + self.start = s.start / 1000 + self.end = s.end / 1000 + self.text = s.plaintext + self.surf = self._to_surf(self.text) + return True + + def _seek(self, time: float) -> None: + self._subs = iter(pysubs2.load(self.path, encoding=self.encoding)) + + while not (self.start <= time <= self.end): + if not self._get_next(): + break + + def _write_subs(self, surf: pygame.Surface) -> None: + surf.blit(self.surf, (surf.get_width() / 2 - self.surf.get_width() / 2, surf.get_height() - self.surf.get_height() - self.offset)) + + def set_font(self, font: pygame.font.SysFont) -> None: + self.font = font + + def get_font(self) -> pygame.font.SysFont: + return self.font diff --git a/pyvidplayer2/pyvidplayer2/video.py b/pyvidplayer2/pyvidplayer2/video.py new file mode 100644 index 0000000..23444f0 --- /dev/null +++ b/pyvidplayer2/pyvidplayer2/video.py @@ -0,0 +1,299 @@ +import cv2 +import subprocess +import os +from typing import Tuple +from threading import Thread +from .pyaudio_handler import PyaudioHandler + +try: + import pygame +except ImportError: + pass +else: + from .mixer_handler import MixerHandler + + +class Video: + def __init__(self, path: str, chunk_size, max_threads, max_chunks, subs, post_process, interp, use_pygame_audio) -> None: + + self.path = path + self.name, self.ext = os.path.splitext(os.path.basename(self.path)) + + self._vid = cv2.VideoCapture(self.path) + + if not self._vid.isOpened(): + raise FileNotFoundError(f'Could not find "{self.path}"') + + # file information + + self.frame_count = int(self._vid.get(cv2.CAP_PROP_FRAME_COUNT)) + self.frame_rate = self._vid.get(cv2.CAP_PROP_FPS) + self.frame_delay = 1 / self.frame_rate + self.duration = self.frame_count / self.frame_rate + self.original_size = (int(self._vid.get(cv2.CAP_PROP_FRAME_WIDTH)), int(self._vid.get(cv2.CAP_PROP_FRAME_HEIGHT))) + self.current_size = self.original_size + self.aspect_ratio = self.original_size[0] / self.original_size[1] + + self.chunk_size = chunk_size + self.max_chunks = max_chunks + self.max_threads = max_threads + + self._chunks = [] + self._threads = [] + self._starting_time = 0 + self._chunks_claimed = 0 + self._chunks_played = 0 + self._stop_loading = False + self.frame = 0 + + self.frame_data = None + self.frame_surf = None + + self.active = False + self.buffering = False + self.paused = False + self.muted = False + + self.subs = subs + self.post_func = post_process + self.interp = interp + self.use_pygame_audio = use_pygame_audio + + if use_pygame_audio: + try: + self._audio = MixerHandler() + except NameError: + raise ModuleNotFoundError("Unable to use Pygame audio because Pygame is not installed.") + else: + self._audio = PyaudioHandler() + + self.speed = 1 + + self._missing_ffmpeg = False # for throwing errors + + self.play() + + def _chunks_len(self) -> int: + i = 0 + for c in self._chunks: + if c is not None: + i += 1 + return i + + def _convert_seconds(self, seconds: float) -> str: + h = int(seconds // 3600) + seconds = seconds % 3600 + m = int(seconds // 60) + s = int(seconds % 60) + d = round(seconds % 1, 1) + return f"{h}:{m}:{s}.{int(d * 10)}" + + def _threaded_load(self, index) -> None: + i = index # assigned to variable so another thread does not change it + + self._chunks.append(None) + + s = self._convert_seconds((self._starting_time + (self._chunks_claimed - 1) * self.chunk_size) * (1 / self.speed)) + + command = [ + "ffmpeg", + "-i", + self.path, + "-ss", + str(s), + "-t", + str(self._convert_seconds(self.chunk_size)), + "-vn", + "-f", + "wav", + "-loglevel", + "quiet", + "-" + ] + + filters = [] + if self.speed != 1: + filters += ["-filter:a", f"atempo={self.speed}"] + + command = command[:7] + filters + command[7:] + + try: + p = subprocess.run(command, capture_output=True) + except FileNotFoundError: + self._missing_ffmpeg = True + + self._chunks[i - self._chunks_played - 1] = p.stdout + + def _update_threads(self) -> None: + for t in self._threads: + if not t.is_alive(): + self._threads.remove(t) + + self._stop_loading = self._starting_time + self._chunks_claimed * self.chunk_size >= self.duration + if not self._stop_loading and len(self._threads) < self.max_threads and self._chunks_len() + len(self._threads) < self.max_chunks: + self._chunks_claimed += 1 + self._threads.append(Thread(target=self._threaded_load, args=(self._chunks_claimed,))) + self._threads[-1].start() + + def _write_subs(self) -> None: + p = self.get_pos() + + if p >= self.subs.start: + if p > self.subs.end: + if self.subs._get_next(): + self._write_subs() + else: + self.subs._write_subs(self.frame_surf) + + def _update(self) -> bool: + if self._missing_ffmpeg: + raise FileNotFoundError("Could not find FFMPEG. Make sure it's downloaded and accessible via $PATH.") + + self._update_threads() + + n = False + self.buffering = False + + if self._audio.get_busy() or self.paused: + + while self.get_pos() > self.frame * self.frame_delay: + + has_frame, data = self._vid.read() + self.frame += 1 + + if has_frame: + if self.original_size != self.current_size: + data = cv2.resize(data, dsize=self.current_size, interpolation=self.interp) + data = self.post_func(data) + + self.frame_data = data + self.frame_surf = self._create_frame(data) + + if self.subs is not None: + self._write_subs() + + n = True + else: + break + + elif self.active: + if self._chunks and self._chunks[0] is not None: + self._chunks_played += 1 + self._audio.load(self._chunks.pop(0)) + self._audio.play() + elif self._stop_loading and self._chunks_played == self._chunks_claimed: + self.stop() + else: + self.buffering = True + + return n + + def mute(self) -> None: + self.muted = True + self._audio.mute() + + def unmute(self) -> None: + self.muted = False + self._audio.unmute() + + def set_speed(self, speed: float) -> None: + speed = max(0.5, min(10, speed)) + if speed != self.speed: + self.speed = speed + self.seek(0) # must reload audio chunks + + def get_speed(self) -> float: + return self.speed + + def play(self) -> None: + self.active = True + + def stop(self) -> None: + self.restart() + self.active = False + self.frame_data = None + self.frame_surf = None + self.paused = False + + def resize(self, size: Tuple[int, int]) -> None: + self.current_size = size + + def change_resolution(self, height: int) -> None: + self.current_size = (int(height * self.aspect_ratio), height) + + def close(self) -> None: + self.stop() + self._vid.release() + self._audio.unload() + for t in self._threads: + t.join() + if not self.use_pygame_audio: + self._audio.close() + + def restart(self) -> None: + self.seek(0, relative=False) + self.play() + + def set_volume(self, vol: float) -> None: + self._audio.set_volume(vol) + + def get_volume(self) -> float: + return self._audio.get_volume() + + def get_paused(self) -> bool: + # here because the original pyvidplayer had get_paused + return self.paused + + def toggle_pause(self) -> None: + self.resume() if self.paused else self.pause() + + def toggle_mute(self) -> None: + self.unmute() if self.muted else self.mute() + + def pause(self) -> None: + if self.active: + self.paused = True + self._audio.pause() + + def resume(self) -> None: + if self.active: + self.paused = False + self._audio.unpause() + + def get_pos(self) -> float: + return self._starting_time + max(0, self._chunks_played - 1) * self.chunk_size + self._audio.get_pos() * self.speed + + def seek(self, time: float, relative=True) -> None: + # seeking accurate to 1 tenth of a second + + self._starting_time = (self.get_pos() + time) if relative else time + self._starting_time = round(min(max(0, self._starting_time), self.duration), 1) + + for t in self._threads: + t.join() + self._chunks = [] + self._threads = [] + self._chunks_claimed = 0 + self._chunks_played = 0 + + self._audio.unload() + + self._vid.set(cv2.CAP_PROP_POS_FRAMES, self._starting_time * self.frame_rate) + self.frame = int(self._vid.get(cv2.CAP_PROP_POS_FRAMES)) + if self.subs is not None: + self.subs._seek(self._starting_time) + + def draw(self, surf, pos: Tuple[int, int], force_draw=True) -> bool: + if (self._update() or force_draw) and self.frame_surf is not None: + self._render_frame(surf, pos) + return True + return False + + def _create_frame(self): + pass + + def _render_frame(self): + pass + + def preview(self): + pass \ No newline at end of file diff --git a/pyvidplayer2/pyvidplayer2/video_player.py b/pyvidplayer2/pyvidplayer2/video_player.py new file mode 100644 index 0000000..d6075e8 --- /dev/null +++ b/pyvidplayer2/pyvidplayer2/video_player.py @@ -0,0 +1,259 @@ +import pygame +import cv2 +import math +from typing import Tuple, List +from . import Video + + +class VideoPlayer: + def __init__(self, video: Video, rect: Tuple[int, int, int, int], interactable=True, loop=False, preview_thumbnails=0) -> None: + + self.video = video + self.frame_rect = pygame.Rect(rect) + self.interactable = interactable + self.loop = loop + self.preview_thumbnails = min(max(preview_thumbnails, 0), self.video.frame_count) + self._show_intervals = self.preview_thumbnails != 0 + + self.vid_rect = pygame.Rect(0, 0, 0, 0) + self._progress_back = pygame.Rect(0, 0, 0, 0) + self._progress_bar = pygame.Rect(0, 0, 0, 0) + self._smooth_bar = 0 # used for making the progress bar look smooth when seeking + self._font = pygame.font.SysFont("arial", 0) + + self._buffer_rect = pygame.Rect(0, 0, 0, 0) + self._buffer_angle = 0 + + self._transform(self.frame_rect) + + self._show_ui = False + self.queue_ = [] + + self._clock = pygame.time.Clock() + + self._seek_pos = 0 + self._seek_time = 0 + self._show_seek = False + + self._fade_timer = 0 + + if self._show_intervals: + self._interval = self.video.duration / self.preview_thumbnails + self._interval_frames = [] + self._get_interval_frames() + + def __str__(self) -> str: + return f"" + + def _close_queue(self): + for video in self.queue_: + try: + video.close() + except AttributeError: + pass + + def _get_interval_frames(self): + size = (int(70 * self.video.aspect_ratio), 70) + for i in range(self.preview_thumbnails): + self.video._vid.set(cv2.CAP_PROP_POS_FRAMES, int(i * self.video.frame_rate * self._interval)) + + self._interval_frames.append(pygame.image.frombuffer(cv2.resize(self.video._vid.read()[1], dsize=size, interpolation=cv2.INTER_AREA).tobytes(), size, "BGR")) + + # add last readable frame + + i = 1 + while True: + self.video._vid.set(cv2.CAP_PROP_POS_FRAMES, self.video.frame_count - i) + try: + self._interval_frames.append(pygame.image.frombuffer(cv2.resize(self.video._vid.read()[1], dsize=size, interpolation=cv2.INTER_AREA).tobytes(), size, "BGR")) + except: + i += 1 + else: + break + + self.video._vid.set(cv2.CAP_PROP_POS_FRAMES, 0) + + def _get_closest_frame(self, time): + i = math.floor(time // self._interval) + if (i + 1) * self._interval - time >= self._interval // 2: + return self._interval_frames[i] + else: + return self._interval_frames[i + 1] + + def _best_fit(self, rect: pygame.Rect, r: float) -> pygame.Rect: + s = rect.size + r = self.video.aspect_ratio + + w = s[0] + h = int(w / r) + y = int(s[1] /2 - h / 2) + x = 0 + if h > s[1]: + h = s[1] + w = int(h * r) + x = int(s[0] / 2 - w / 2) + y = 0 + + return pygame.Rect(rect.x + x, rect.y + y, w, h) + + def _transform(self, rect: pygame.Rect) -> None: + self.frame_rect = rect + self.vid_rect = self._best_fit(self.frame_rect, self.video.aspect_ratio) + self.video.resize(self.vid_rect.size) + + self._progress_back = pygame.Rect(self.frame_rect.x + 10, self.frame_rect.bottom - 25, self.frame_rect.w - 20, 15) + self._progress_bar = self._progress_back.copy() + + self._font = pygame.font.SysFont("arial", 10) + + if self.video.frame_data is not None: + self.video.frame_surf = pygame.transform.smoothscale(self.video.frame_surf, self.vid_rect.size) + + self._buffer_rect = pygame.Rect(0, 0, 200, 200) + self._buffer_rect.center = self.frame_rect.center + + def _move_angle(self, pos: Tuple[int, int], angle: float, distance: int) -> Tuple[float, float]: + return pos[0] + math.cos(angle) * distance, pos[1] + math.sin(angle) * distance + + def _convert_seconds(self, time: float) -> str: + return self.video._convert_seconds(time).split(".")[0] + + def zoom_to_fill(self): + s = max(abs(self.frame_rect.w - self.vid_rect.w), abs(self.frame_rect.h - self.vid_rect.h)) + self.vid_rect.inflate_ip(s, s) + self.video.resize(self.vid_rect.size) + self.vid_rect.center = self.frame_rect.center + + def zoom_out(self): + self.vid_rect = self._best_fit(self.frame_rect, self.video.aspect_ratio) + self.video.resize(self.vid_rect.size) + + def queue(self, input_: str | Video) -> None: + self.queue_.append(input_) + + # update once to trigger audio loading + try: + input_.stop() + input_._update() + except AttributeError: + pass + + def resize(self, size: Tuple[int, int]) -> None: + self.frame_rect.size = size + self._transform(self.frame_rect) + + def move(self, pos: Tuple[int, int], relative=False) -> None: + if relative: + self.frame_rect.move_ip(*pos) + else: + self.frame_rect.topleft = pos + self._transform(self.frame_rect) + + def update(self, events: List[pygame.event.Event] = None, show_ui=None) -> bool: + dt = self._clock.tick() + + if not self.video.active: + if self.queue_: + if self.loop: + self.queue(self.video) + input_ = self.queue_.pop(0) + try: + self.video = Video(input_) + except TypeError: + self.video = input_ + self.video.play() + self._transform(self.frame_rect) + elif self.loop: + self.video.restart() + + if self.video._update() and self.video.current_size > self.frame_rect.size: + self.video.frame_surf = self.video.frame_surf.subsurface(self.frame_rect.x - self.vid_rect.x, self.frame_rect.y - self.vid_rect.y, *self.frame_rect.size) + + if self.interactable: + + mouse = pygame.mouse.get_pos() + click = False + for event in events: + if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: + click = True + + self._show_ui = self.frame_rect.collidepoint(mouse) if show_ui is None else show_ui + + if self._show_ui: + self._progress_bar.w = self._progress_back.w * (self.video.get_pos() / self.video.duration) + self._smooth_bar += (self._progress_bar.w - self._smooth_bar) / (dt * 0.25) + self._show_seek = self._progress_back.collidepoint(mouse) + + if self._show_seek: + t = (self._progress_back.w - (self._progress_back.right - mouse[0])) * (self.video.duration / self._progress_back.w) + + self._seek_pos = self._progress_back.w * (round(t, 1) / self.video.duration) + self._progress_back.x + self._seek_time = t + + if click: + self.video.seek(t, relative=False) + self.video.play() + + elif click: + self.video.toggle_pause() + + self._buffer_angle += dt / 10 + + return self._show_ui + + def draw(self, win: pygame.Surface) -> None: + pygame.draw.rect(win, "black", self.frame_rect) + if self.video.frame_surf is not None: + win.blit(self.video.frame_surf, self.frame_rect.topleft if self.video.current_size > self.frame_rect.size else self.vid_rect.topleft) + + if self._show_ui: + pygame.draw.line(win, (50, 50, 50), (self._progress_back.x, self._progress_back.centery), (self._progress_back.right, self._progress_back.centery), 5) + if self._smooth_bar > 1: + pygame.draw.line(win, "white", (self._progress_bar.x, self._progress_bar.centery), (self._progress_bar.x + self._smooth_bar, self._progress_bar.centery), 5) + + f = self._font.render(self.video.name, True, "white") + win.blit(f, (self.frame_rect.x + 10, self.frame_rect.y + 10)) + + f = self._font.render(self._convert_seconds(self.video.get_pos()), True, "white") + win.blit(f, (self.frame_rect.x + 10, self._progress_bar.top - f.get_height() - 10)) + + if self._show_seek: + pygame.draw.line(win, "white", (self._seek_pos, self._progress_back.top), (self._seek_pos, self._progress_back.bottom), 2) + + f = self._font.render(self._convert_seconds(self._seek_time), True, "white") + win.blit(f, (self._seek_pos - f.get_width() // 2, self._progress_back.y - 10 - f.get_height())) + + if self._show_intervals: + surf = self._get_closest_frame(self._seek_time) + x = self._seek_pos - surf.get_width() // 2 + x = min(max(x, self.frame_rect.x), self.frame_rect.right - surf.get_width()) + win.blit(surf, (x, self._progress_back.y - 80 - f.get_height())) + + if self.interactable: + if self.video.buffering: + for i in range(6): + a = math.radians(self._buffer_angle + i * 60) + pygame.draw.line(win, "white", self._move_angle(self.frame_rect.center, a, 10), self._move_angle(self.frame_rect.center, a, 30)) + elif self.video.paused: + pygame.draw.rect(win, "white", (self.frame_rect.centerx - 15, self.frame_rect.centery - 20, 10, 40)) + pygame.draw.rect(win, "white", (self.frame_rect.centerx + 5, self.frame_rect.centery - 20, 10, 40)) + + def close(self) -> None: + self.video.close() + self._close_queue() + + def skip(self) -> None: + self.video.stop() if self.loop else self.video.close() + + def get_next(self) -> None | Video | str: + return self.queue_[0] if self.queue_ else None + + def clear_queue(self) -> None: + self._close_queue() + self.queue_ = [] + + def get_video(self) -> Video: + return self.video + + def get_queue(self) -> List: + return self.queue_ diff --git a/pyvidplayer2/pyvidplayer2/video_pygame.py b/pyvidplayer2/pyvidplayer2/video_pygame.py new file mode 100644 index 0000000..f358010 --- /dev/null +++ b/pyvidplayer2/pyvidplayer2/video_pygame.py @@ -0,0 +1,34 @@ +import cv2 +import pygame +import numpy +from .video import Video +from typing import Tuple +from .post_processing import PostProcessing + + +class VideoPygame(Video): + def __init__(self, path: str, chunk_size=300, max_threads=1, max_chunks=1, subs=None, post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, use_pygame_audio=False) -> None: + Video.__init__(self, path, chunk_size, max_threads, max_chunks, subs, post_process, interp, use_pygame_audio) + + def __str__(self) -> str: + return f"" + + def _create_frame(self, data: numpy.ndarray) -> pygame.Surface: + return pygame.image.frombuffer(data.tobytes(), self.current_size, "BGR") + + def _render_frame(self, surf: pygame.Surface, pos: Tuple[int, int]) -> None: + surf.blit(self.frame_surf, pos) + + def preview(self) -> None: + win = pygame.display.set_mode(self.current_size) + pygame.display.set_caption(f"pygame - {self.name}") + self.play() + while self.active: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.stop() + pygame.time.wait(16) + self.draw(win, (0, 0), force_draw=False) + pygame.display.update() + pygame.display.quit() + self.close() \ No newline at end of file diff --git a/pyvidplayer2/pyvidplayer2/video_pyglet.py b/pyvidplayer2/pyvidplayer2/video_pyglet.py new file mode 100644 index 0000000..0fbd28d --- /dev/null +++ b/pyvidplayer2/pyvidplayer2/video_pyglet.py @@ -0,0 +1,36 @@ +import cv2 +import pyglet +import numpy +from .video import Video +from typing import Tuple +from .post_processing import PostProcessing + + +class VideoPyglet(Video): + def __init__(self, path: str, chunk_size=300, max_threads=1, max_chunks=1, post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, use_pygame_audio=False) -> None: + Video.__init__(self, path, chunk_size, max_threads, max_chunks, None, post_process, interp, use_pygame_audio) + + def __str__(self) -> str: + return f"" + + def _create_frame(self, data: numpy.ndarray) -> pyglet.image.ImageData: + return pyglet.image.ImageData(*self.current_size, "BGR", cv2.flip(data, 0).tobytes()) + + def _render_frame(self, pos: Tuple[int, int]) -> None: + self.frame_surf.blit(*pos) + + def draw(self, pos: Tuple[int, int], force_draw=True) -> bool: + if (self._update() or force_draw) and self.frame_surf is not None: + self._render_frame(pos) # (0, 0) pos draws the video bottomleft + return True + return False + + def preview(self) -> None: + def update(dt): + self.draw((0, 0), force_draw=False) + if not self.active: + win.close() + win = pyglet.window.Window(width=self.current_size[0], height=self.current_size[1], config=pyglet.gl.Config(double_buffer=False), caption=f"pyglet - {self.name}") + pyglet.clock.schedule_interval(update, 1/60.0) + pyglet.app.run() + self.close() \ No newline at end of file diff --git a/pyvidplayer2/pyvidplayer2/video_pyqt.py b/pyvidplayer2/pyvidplayer2/video_pyqt.py new file mode 100644 index 0000000..c299d0b --- /dev/null +++ b/pyvidplayer2/pyvidplayer2/video_pyqt.py @@ -0,0 +1,41 @@ +import cv2 +import numpy +from .video import Video +from typing import Tuple +from PyQt6.QtGui import QImage, QPixmap, QPainter +from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget +from PyQt6.QtCore import QTimer +from .post_processing import PostProcessing + + +class VideoPyQT(Video): + def __init__(self, path: str, chunk_size=300, max_threads=1, max_chunks=1, post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, use_pygame_audio=False) -> None: + Video.__init__(self, path, chunk_size, max_threads, max_chunks, None, post_process, interp, use_pygame_audio) + + def __str__(self) -> str: + return f"" + + def _create_frame(self, data: numpy.ndarray) -> QImage: + return QImage(data, data.shape[1], data.shape[0], data.strides[0], QImage.Format.Format_BGR888) + + def _render_frame(self, win: QMainWindow, pos: Tuple[int, int]) -> None: #must be called in paintEvent + QPainter(win).drawPixmap(*pos, QPixmap.fromImage(self.frame_surf)) + + def preview(self) -> None: + class Window(QMainWindow): + def __init__(self): + super().__init__() + self.canvas = QWidget(self) + self.setCentralWidget(self.canvas) + self.timer = QTimer(self) + self.timer.timeout.connect(self.update) + self.timer.start(16) + def paintEvent(self_, _): + self.draw(self_, (0, 0)) + app = QApplication([]) + win = Window() + win.setWindowTitle(f"pyqt6 - {self.name}") + win.setFixedSize(*self.current_size) + win.show() + app.exec() + self.close() diff --git a/pyvidplayer2/pyvidplayer2/video_tkinter.py b/pyvidplayer2/pyvidplayer2/video_tkinter.py new file mode 100644 index 0000000..767d0ff --- /dev/null +++ b/pyvidplayer2/pyvidplayer2/video_tkinter.py @@ -0,0 +1,36 @@ +import cv2 +import numpy +import tkinter +from .video import Video +from typing import Tuple +from .post_processing import PostProcessing + + +class VideoTkinter(Video): + def __init__(self, path: str, chunk_size=300, max_threads=1, max_chunks=1, post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, use_pygame_audio=False) -> None: + Video.__init__(self, path, chunk_size, max_threads, max_chunks, None, post_process, interp, use_pygame_audio) + + def __str__(self) -> str: + return f"" + + def _create_frame(self, data: numpy.ndarray) -> tkinter.PhotoImage: + h, w = data.shape[:2] + return tkinter.PhotoImage(width=w, height=h, data=f"P6 {w} {h} 255 ".encode() + cv2.cvtColor(data, cv2.COLOR_BGR2RGB).tobytes(), format='PPM') + + def _render_frame(self, canvas: tkinter.Canvas, pos: Tuple[int, int]) -> None: + canvas.create_image(*pos, image=self.frame_surf) + + def preview(self) -> None: + def update(): + self.draw(canvas, (self.current_size[0] / 2, self.current_size[1] / 2), force_draw=False) + if self.active: + root.after(16, update) # for around 60 fps + else: + root.destroy() + root = tkinter.Tk() + root.title(f"tkinter - {self.name}") + canvas = tkinter.Canvas(root, width=self.current_size[0], height=self.current_size[1], highlightthickness=0) + canvas.pack() + update() + root.mainloop() + self.close() \ No newline at end of file diff --git a/pyvidplayer2/pyvidplayer2/webcam.py b/pyvidplayer2/pyvidplayer2/webcam.py new file mode 100644 index 0000000..a6b7813 --- /dev/null +++ b/pyvidplayer2/pyvidplayer2/webcam.py @@ -0,0 +1,107 @@ +import cv2 +import pygame +import time +import numpy +from .post_processing import PostProcessing +from .error import Pyvidplayer2Error +from typing import Tuple + + +class Webcam: + def __init__(self, post_process=PostProcessing.none, interp=cv2.INTER_LINEAR, fps=30) -> None: + self._vid = cv2.VideoCapture(0) + + if not self._vid.isOpened(): + raise Pyvidplayer2Error("Failed to find webcam.") + + self.original_size = (int(self._vid.get(cv2.CAP_PROP_FRAME_WIDTH)), int(self._vid.get(cv2.CAP_PROP_FRAME_HEIGHT))) + self.current_size = self.original_size + self.aspect_ratio = self.original_size[0] / self.original_size[1] + + self.frame_data = None + self.frame_surf = None + + self.active = False + + self.post_func = post_process + self.interp = interp + self.fps = fps + + self._frame_delay = 1 / self.fps + self._frames = 0 + self._last_tick = 0 + + self.play() + + def __str__(self) -> str: + return f"" + + def _update(self) -> bool: + if self.active: + + if time.time() - self._last_tick > self._frame_delay: + + has_frame, data = self._vid.read() + + if has_frame: + if self.original_size != self.current_size: + data = cv2.resize(data, dsize=self.current_size, interpolation=self.interp) + data = self.post_func(data) + + self.frame_data = data + self.frame_surf = self._create_frame(data) + + self._frames += 1 + self._last_tick = time.time() + + return True + + return False + + def play(self) -> None: + self.active = True + + def stop(self) -> None: + self.active = False + self.frame_data = None + self.frame_surf = None + + def resize(self, size: Tuple[int, int]) -> None: + self.current_size = size + + def change_resolution(self, height: int) -> None: + self.current_size = (int(height * self.aspect_ratio), height) + + def close(self) -> None: + self.stop() + self._vid.release() + + def get_pos(self) -> float: + return self._frames / self.fps + + def draw(self, surf, pos: Tuple[int, int], force_draw=True) -> bool: + if (self._update() or force_draw) and self.frame_surf is not None: + self._render_frame(surf, pos) + return True + return False + + def _create_frame(self, data: numpy.ndarray) -> pygame.Surface: + return pygame.image.frombuffer(data.tobytes(), self.current_size, "BGR") + + def _render_frame(self, surf: pygame.Surface, pos: Tuple[int, int]): + surf.blit(self.frame_surf, pos) + + def preview(self) -> None: + win = pygame.display.set_mode(self.current_size) + pygame.display.set_caption(f"webcam") + self.play() + while self.active: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + self.stop() + pygame.time.wait(int(self._frame_delay * 1000)) + self.draw(win, (0, 0), force_draw=False) + pygame.display.update() + pygame.display.quit() + self.close() + diff --git a/pyvidplayer2/requirements.txt b/pyvidplayer2/requirements.txt new file mode 100644 index 0000000..1bdd1a4 --- /dev/null +++ b/pyvidplayer2/requirements.txt @@ -0,0 +1,5 @@ +numpy<1.25,>=1.21 +opencv_python +pygame +pysubs2 +PyAudio \ No newline at end of file diff --git a/pyvidplayer2/setup.py b/pyvidplayer2/setup.py new file mode 100644 index 0000000..f8e824c --- /dev/null +++ b/pyvidplayer2/setup.py @@ -0,0 +1,27 @@ +from setuptools import setup +from pyvidplayer2 import _VERSION + + +with open("README.md", 'r') as f: + long_desc = f.read() + + +setup( + name="pyvidplayer2", + version=_VERSION, + description="Video playback in Python", + long_description=long_desc, + long_description_content_type = "text/markdown", + author="Anray Liu", + author_email="anrayliu@gmail.com", + license="MIT", + packages=["pyvidplayer2"], + install_requires=["numpy<1.25,>=1.21", + "opencv_python", + "pygame", + "pysubs2", + "PyAudio"], + url="https://github.com/ree1261/pyvidplayer2", + platforms=["windows"], + keywords=["pygame", "video", "playback"], +) \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..f2f7c84 --- /dev/null +++ b/utils.py @@ -0,0 +1,132 @@ +from itertools import chain +import io +import os +import os.path +import zipfile + +# import os +# import subprocess + + +def getMameResource(emulatorDir, typeofResource, is_dir): + resource = os.path.join(emulatorDir, typeofResource) + if is_dir: + if not os.path.isdir(resource): + return None + if os.path.isfile(os.path.join(resource, typeofResource + ".zip")): + resource = zipfile.ZipFile(os.path.join(resource, typeofResource + ".zip")) + else: + resource = os.path.join(resource, typeofResource) + else: + if not os.path.isfile(resource): + return None + with open(resource, "rt", encoding="latin1") as f: + resource = f.read() + + return resource + + +# def aspect_scale(img, bx, by): +# """Scales 'img' to fit into box bx/by. +# This method will retain the original image's aspect ratio""" +# ix, iy = img.get_size() +# if ix > iy: +# # fit to width +# scale_factor = bx / float(ix) +# sy = scale_factor * iy +# if sy > by: +# scale_factor = by / float(iy) +# sx = scale_factor * ix +# sy = by +# else: +# sx = bx +# else: +# # fit to height +# scale_factor = by / float(iy) +# sx = scale_factor * ix +# if sx > bx: +# scale_factor = bx / float(ix) +# sx = bx +# sy = scale_factor * iy +# else: +# sy = by + +# return pygame.transform.scale(img, (int(sx), int(sy))) + + +# def image_from_data(data, image_size): +# if data: +# with io.BytesIO(data) as f: +# surface = pygame.image.load(f) +# surface = aspect_scale(surface, image_size[0], image_size[1]) +# else: +# surface = pygame.Surface(image_size) # pylint: disable=E1121 + +# return surface + + +# def addscanlines(surface): +# width = surface.get_width() +# for y in range(surface.get_height()): +# if y % 2: +# pygame.draw.line(surface, (0, 0, 0), (0, y), (width, y)) + +# return surface + + +def truncline(text, font, maxwidth): + real = len(text) + stext = text + size = font.size(text)[0] + cut = 0 + a = 0 + done = 1 + while size > maxwidth: + a = a + 1 + n = text.rsplit(None, a)[0] + if stext == n: + cut += 1 + stext = n[:-cut] + else: + stext = n + size = font.size(stext)[0] + real = len(stext) + done = 0 + return real, done, stext + + +def wrapline(text, font, maxwidth): + done = 0 + wrapped = [] + + while not done: + nl, done, stext = truncline(text, font, maxwidth) + wrapped.append(stext.strip()) + text = text[nl:] + return wrapped + + +def wrap_multi_line(text, font, maxwidth): + """returns text taking new lines into account.""" + if type(text) is str: + lines = chain(*(wrapline(line, font, maxwidth) for line in text.splitlines())) + else: + lines = chain(*(wrapline(line, font, maxwidth) for line in text)) + + return list(lines) + + +# def run_emulator(emu_type, exe, rom): +# old_path = os.getcwd() +# os.chdir(os.path.split(exe)[0]) + +# if emu_type == 'MAME': +# subprocess.run([exe, rom]) +# elif emu_type == 'MESS': +# subprocess.run([exe, rom]) +# elif emu_type == 'CSV': +# subprocess.run([exe.replace('', rom)]) + +# os.chdir(old_path) + +# return True