619 Commits
4.1 ... v19.4

Author SHA1 Message Date
Steve Seguin
852f9d5fca Merge pull request #901 from steveseguin/v19.3
Version 19.4
2021-10-15 00:25:48 -04:00
Steve Seguin
90aab38b3e version 19.4
bug fixes
2021-10-15 00:23:44 -04:00
Steve Seguin
d098414037 Merge pull request #900 from steveseguin/v19.3
V19.3
2021-10-06 01:47:50 -04:00
Steve Seguin
deb66966c2 Add files via upload 2021-10-06 01:43:19 -04:00
Steve Seguin
8668a00601 Merge pull request #899 from GameErauntsia/master
Add Euskara/Basque (eu) translation
2021-10-03 12:15:51 -04:00
GameErauntsia
49e0ef40d6 Add Euskara/Basque (eu) translation
Translation by the basque gamer community Game Erauntsia
https://gamerauntsia.eus
2021-10-03 14:41:47 +02:00
Steve Seguin
a9ec33c97f Update README.md 2021-09-28 19:21:10 -04:00
Steve Seguin
8157cc3000 Add files via upload 2021-09-28 19:19:59 -04:00
Steve Seguin
c6de42650a Merge pull request #896 from steveseguin/base64css-param
add &base64css
2021-09-19 15:04:44 -04:00
Joel Calado
523e7a376f add &base64css 2021-09-19 19:48:04 +01:00
Steve Seguin
657c9eb232 v19.2 beta 2021-09-19 07:07:40 -04:00
STeve Seguin
842afb0465 repo cleanup; check examples dir for misc 2021-09-19 01:02:47 -04:00
Steve Seguin
2d61fd2ff0 Merge pull request #894 from steveseguin/19.1
Version 19.1
2021-09-19 00:32:59 -04:00
Steve Seguin
286bb5ef84 Add files via upload 2021-09-19 00:27:01 -04:00
Steve Seguin
491b41abca improving the iframe API / darkmode added
version v19.1
2021-09-07 01:22:01 -04:00
Steve Seguin
42fb75b756 Add files via upload 2021-09-06 00:24:02 -04:00
Steve Seguin
4a1f8ef063 Add files via upload 2021-09-06 00:19:55 -04:00
Steve Seguin
18acf94ba5 Add files via upload 2021-09-05 23:32:10 -04:00
Steve Seguin
33faff8e3a syncing translation files with v19
not translated; just updated
2021-09-05 23:04:58 -04:00
Steve Seguin
ec9e9eb632 Update blank.json 2021-09-05 22:59:22 -04:00
Steve Seguin
da61d0501f Update blank.json 2021-09-05 22:58:56 -04:00
Steve Seguin
52dddf28b8 Update blank.json 2021-09-05 22:58:37 -04:00
Steve Seguin
e9fc560e39 Update blank.json 2021-09-05 22:58:06 -04:00
Steve Seguin
ad90891163 Update readme.md 2021-09-05 22:44:11 -04:00
Steve Seguin
91b87707f7 Update readme.md 2021-09-05 22:43:48 -04:00
Steve Seguin
65f00b6ba4 Update translate.js 2021-09-05 22:43:21 -04:00
Steve Seguin
d1be5c455f Chinese (simplified) translation
50% roughly done ?
automated translations
2021-09-05 22:39:49 -04:00
Steve Seguin
ef6e86e257 Update translate.js 2021-09-05 21:31:55 -04:00
Joel Calado
af07f4f610 tweak share website info 2021-09-03 08:07:17 +01:00
Joel Calado
804a03a412 remove borders; restyle buttons 2021-09-03 07:19:23 +01:00
Steve Seguin
4573b34969 animation/fade fix 2021-09-02 15:48:05 -04:00
Steve Seguin
3217f459cf aec/gain/noise toggle fix 2021-09-02 15:47:05 -04:00
Steve Seguin
e4813d0ac1 Add files via upload 2021-09-01 02:21:27 -04:00
Steve Seguin
1428769910 Update update_translations.yml 2021-08-29 04:02:36 -04:00
Steve Seguin
4b174b11ff Add files via upload 2021-08-29 03:57:33 -04:00
Steve Seguin
a95affcf8a Add files via upload 2021-08-29 03:56:56 -04:00
Steve Seguin
a1b2cea991 Add files via upload 2021-08-29 03:55:35 -04:00
Steve Seguin
c066020b4f Update install.md 2021-08-18 11:47:14 -04:00
Steve Seguin
13c9a08976 Update turnserver3.conf 2021-08-12 01:19:23 -04:00
Steve Seguin
c6b58a33c0 Update turnserver.md 2021-08-12 01:09:12 -04:00
Steve Seguin
e9ae44602b Update turnserver3.conf 2021-08-12 01:08:46 -04:00
Steve Seguin
6e85d2dd8b removing minimum change requirement
changing based on feedback from lawyer
2021-07-26 05:28:15 -04:00
Steve Seguin
ad4d5585b6 Add files via upload 2021-07-21 22:25:26 -04:00
Steve Seguin
51fb3137a2 Update turnserver.md 2021-07-17 20:08:17 -04:00
Steve Seguin
8f44e744b9 Update turnserver.md 2021-07-17 20:01:04 -04:00
Steve Seguin
7d94bb8467 Update install.md 2021-07-15 06:36:17 -04:00
Steve Seguin
c0c6eb6ff1 Update install.md 2021-07-15 06:27:27 -04:00
Steve Seguin
c233d10d17 Update install.md 2021-07-15 06:23:53 -04:00
Steve Seguin
e5c1133d76 Update install.md 2021-07-15 06:20:46 -04:00
Steve Seguin
0addef063c Update install.md 2021-07-15 06:20:33 -04:00
Steve Seguin
f1fb66fd03 Merge pull request #887 from steveseguin/patch-18.3.1
Add files via upload
2021-07-10 07:17:38 -04:00
Steve Seguin
9ba29e76e3 Add files via upload 2021-07-10 07:16:49 -04:00
Steve Seguin
525f7d714c Update IFRAME.md 2021-07-09 19:53:20 -04:00
Steve Seguin
3a3de1d2f0 Update IFRAME.md 2021-07-09 19:53:07 -04:00
Steve Seguin
cc914d63b5 Update IFRAME.md 2021-07-09 19:52:44 -04:00
Steve Seguin
06d92409c1 Update IFRAME.md 2021-07-09 19:52:31 -04:00
Steve Seguin
4b661b2a57 Update IFRAME.md 2021-07-09 19:52:12 -04:00
Steve Seguin
922a7f7965 Merge pull request #876 from steveseguin/v18.3beta
v18.3 Release
2021-07-09 02:39:20 -04:00
Steve Seguin
295368aa4f Merge branch 'master' into v18.3beta 2021-07-09 02:38:55 -04:00
Steve Seguin
4c87d10134 Add files via upload 2021-07-09 02:25:20 -04:00
Steve Seguin
aa34df12b8 Update README.md 2021-07-07 16:08:02 -04:00
Steve Seguin
34b651209b Update README.md 2021-07-07 16:04:40 -04:00
Steve Seguin
73cabdf824 Update README.md 2021-07-07 16:04:00 -04:00
Steve Seguin
4cd4c1f171 Update README.md 2021-07-07 16:03:25 -04:00
Steve Seguin
3fd93ad6fb Update README.md 2021-07-07 16:03:00 -04:00
Steve Seguin
7bf131b83f Add files via upload 2021-07-03 16:30:31 -04:00
Steve Seguin
8e023705f1 language updates; bug fixes 2021-06-30 14:17:13 -04:00
Steve Seguin
cf41eb1ff1 updating CLA with new brand name; vdo.ninja 2021-06-30 03:41:47 -04:00
Steve Seguin
e1c2ad1748 added/tweaked translations + other fixes 2021-06-27 22:57:37 -04:00
Steve Seguin
bb5e19c70b Update install.md 2021-06-27 21:11:14 -04:00
Steve Seguin
1a00eaf2b7 minor auto-mixer-fix
fixed an issue with the self-preview not loading or loading slow; introduced by the new animation code

also added a flag &animate , which enables the animation code (by default, its off now)
2021-06-23 13:19:03 -04:00
Steve Seguin
78ac9f961c Add files via upload 2021-06-23 03:03:20 -04:00
Steve Seguin
f57df65c4b fixed bug with pie / socket server 2021-06-23 01:40:20 -04:00
Steve Seguin
3e30724e8c Update index.html 2021-06-20 12:33:23 -04:00
Steve Seguin
5e2249c8b9 Update install.md
branding fixes
2021-06-18 17:49:28 -04:00
Steve Seguin
b366469e59 Update install.md
updated to include context isolation, to allow for ffmpeg.js to work:
details:
https://marketplace.zoom.us/docs/sdk/native-sdks/web/advanced/web-isolation
2021-06-18 17:48:05 -04:00
Steve Seguin
a125421357 Merge pull request #877 from steveseguin/update-convert-ui
tweak the /convert UI
2021-06-17 13:20:47 -04:00
Joel Calado
9db09a6278 tweak the /convert UI 2021-06-17 18:03:57 +01:00
Steve Seguin
2bbda7fa19 adds custom user prompts/confirm boxes
introduces lib.js as well, which is a split of main.js
2021-06-16 18:54:49 -04:00
Steve Seguin
4c1151056a v18.3 beta
fixed fade-ins and added transitions, plus a group of bug fixes.
2021-06-15 04:55:51 -04:00
Steve Seguin
d87d04a4d4 Merge pull request #874 from steveseguin/18.2-beta
v18.2 beta
2021-06-15 04:54:39 -04:00
Steve Seguin
0154565589 Update IFRAME.md 2021-06-10 12:06:44 -04:00
Steve Seguin
6912a98867 Update IFRAME.md 2021-06-10 12:05:54 -04:00
Steve Seguin
dc26a23ff9 v18.2 beta 2021-06-07 12:51:24 -04:00
Steve Seguin
be28fbde1b Merge pull request #872 from steveseguin/improve-electron-page-GO-button
Add files via upload
2021-06-05 17:44:00 -04:00
Joel Calado
b32d040ea2 Add files via upload 2021-06-05 22:40:51 +01:00
Steve Seguin
4010bd220e Add files via upload 2021-06-05 17:26:11 -04:00
Steve Seguin
b37fcf855d Update README.md 2021-06-04 17:43:38 -04:00
Steve Seguin
d798c6bd14 Update README.md 2021-06-04 17:43:03 -04:00
Steve Seguin
717c3aa7cc Update README.md 2021-06-04 17:39:22 -04:00
Steve Seguin
e424cf3c5c Update README.md 2021-06-02 12:46:05 -04:00
Steve Seguin
36a63f6155 bug fixes + rebrand
- midiin/out issue with indexing fixed
- h264 broadcast support no longer default
- vdo.ninja branding
2021-05-31 15:46:08 -04:00
Steve Seguin
2eff3ef88b Add files via upload 2021-05-31 12:25:41 -04:00
Steve Seguin
5f77aace18 Update README.md 2021-05-31 02:19:48 -04:00
Steve Seguin
d10a08e84f Update README.md 2021-05-31 02:15:43 -04:00
Steve Seguin
c30e282c1b Update README.md 2021-05-31 02:10:42 -04:00
Steve Seguin
05d6f3d0ee Update README.md 2021-05-31 02:10:28 -04:00
Steve Seguin
d8f897b974 Update README.md 2021-05-31 02:09:13 -04:00
Steve Seguin
0a5e820801 Update README.md 2021-05-31 02:08:37 -04:00
Steve Seguin
0b8e809b2f Update README.md 2021-05-31 02:05:55 -04:00
Steve Seguin
ad79f8c452 Update README.md 2021-05-31 02:05:45 -04:00
Steve Seguin
3f7bb89b5d Update README.md 2021-05-31 02:05:19 -04:00
Steve Seguin
ef4c2e5d77 Update README.md 2021-05-31 02:05:13 -04:00
Steve Seguin
1d9da2b08e Merge pull request #871 from steveseguin/generated_translations
[OBSNinja Bot] Updated translations
2021-05-30 22:06:04 -04:00
steveseguin
b0a55bf225 Generated updated translations 2021-05-31 02:04:54 +00:00
Steve Seguin
db04376a68 Merge pull request #870 from steveseguin/v18.beta
version 18.1
2021-05-30 22:04:32 -04:00
Steve Seguin
c701bd4f8f Merge branch 'master' into v18.beta 2021-05-30 22:03:17 -04:00
Steve Seguin
77df3a2ff1 Add files via upload
Pre-release Version 18
2021-05-30 21:55:20 -04:00
Steve Seguin
ba56f40ba5 Add files via upload
more bug fixes, etc
2021-05-23 14:58:21 -04:00
Steve Seguin
d1c980f309 Add files via upload 2021-05-17 19:50:05 -04:00
Steve Seguin
27c7d5c43b Add files via upload 2021-05-17 19:49:52 -04:00
Steve Seguin
56806a2186 Add files via upload 2021-05-17 17:58:17 -04:00
Steve Seguin
e2e56e5f4b Add files via upload 2021-05-14 10:42:04 -04:00
Steve Seguin
321f366c6b Add files via upload 2021-05-14 10:24:27 -04:00
Steve Seguin
93a1813fda Update README.md 2021-05-13 05:21:52 -04:00
Steve Seguin
4adf5fbe2d Merge pull request #867 from steveseguin/v17.2.99999
V17.2.99999
2021-05-13 05:15:14 -04:00
Steve Seguin
cee772d031 Delete changepass.html 2021-05-13 05:12:17 -04:00
Steve Seguin
15030fe123 Add files via upload 2021-05-13 05:10:55 -04:00
Steve Seguin
0b064f7c69 I don't like spaces afterall 2021-05-13 05:06:15 -04:00
Steve Seguin
6f6c7c3d97 Add files via upload 2021-05-13 05:03:45 -04:00
Steve Seguin
06078ecd2b Update index.html 2021-05-13 05:01:06 -04:00
Steve Seguin
f1d60678c4 Update index.html 2021-05-13 04:30:33 -04:00
Steve Seguin
8624f5bd67 Add files via upload 2021-05-13 04:07:17 -04:00
Steve Seguin
3882583d6c Update electron.html 2021-05-13 03:15:17 -04:00
Steve Seguin
d66a44e57c Update electron.html 2021-05-13 02:50:43 -04:00
Steve Seguin
8cbd26e34b Update electron.html 2021-05-13 02:41:10 -04:00
Steve Seguin
98c293d436 Merge pull request #866 from jcalado/master
electron.html - remember last 5 used urls
2021-05-12 17:43:35 -04:00
Joel Calado
14cf419e31 last 5 urls instead of last url 2021-05-12 22:34:46 +01:00
Joel Calado
74670f5dd0 remember last used url 2021-05-12 21:11:58 +01:00
Steve Seguin
58e07f58d9 Merge pull request #865 from jcalado/master
overhaul of the electron capture UI
2021-05-12 14:25:12 -04:00
Joel Calado
bc5fd78196 overhaul of the electron capture UI 2021-05-12 19:19:43 +01:00
Steve Seguin
612949ba64 Merge pull request #864 from Jumper78/fix/link-to-advanced-settings
correct link to advanced documentation
2021-05-12 07:20:10 -04:00
Jumper78
2bf59f0c2a correct link to advanced documentation
the link to advanced documentation has pointed to a non-existing page; I pointed it to: https://docs.obs.ninja/advanced-settings
2021-05-12 13:13:08 +02:00
Steve Seguin
495f508919 Update README.md 2021-05-07 04:16:45 -04:00
Steve Seguin
e8354f45c0 Add files via upload 2021-05-06 08:34:43 -04:00
Steve Seguin
2de7c38d7f Merge pull request #855 from duncanbarnes/master
iFrame styling in CSS not JS
2021-05-05 07:24:53 -04:00
Duncan Barnes
2be4038885 iFrame styling in CSS not JS
Small change, moved iframe styling to CSS, allows &css param to override iframe styling without requiring !important on the end of values.
2021-05-05 12:19:48 +01:00
Steve Seguin
b92af980fb Update update-advanced-settings-toc.yml 2021-05-04 07:39:44 -04:00
Steve Seguin
f734d63118 Update update-advanced-settings-toc.yml 2021-05-04 07:36:30 -04:00
Steve Seguin
3859cb2762 Fires every Feb 31st now 2021-05-04 07:34:44 -04:00
Steve Seguin
9600693001 May the fourth be with you 2021-05-04 04:02:53 -04:00
Steve Seguin
8934838954 Merge pull request #853 from steveseguin/jcalado-patch-1
fix typo
2021-05-02 03:28:19 -04:00
Joel Calado
e5f203e178 fix typo 2021-05-02 08:25:36 +01:00
Steve Seguin
3890f28ede on-air tweaks, &tbr for director, +minor features 2021-05-01 12:34:02 -04:00
Steve Seguin
e4e9398853 Add files via upload 2021-04-29 03:04:48 -04:00
Steve Seguin
a296ba8417 Update main.js 2021-04-26 02:37:52 -04:00
Steve Seguin
a843d3d04d Version 17.2
More bug fixes; &optimize command logic made more visible

I may do a v17.3 within the next week and a few more iterations after that, before working on v18
2021-04-25 21:27:19 -04:00
Steve Seguin
bb1061f22f Merge pull request #844 from steveseguin/generated_translations
[OBSNinja Bot] Updated translations
2021-04-23 03:15:09 -04:00
steveseguin
29458e4a87 Generated updated translations 2021-04-23 07:13:23 +00:00
Steve Seguin
ce97e74bea Merge pull request #835 from steveseguin/steves-dev-branch
Version 17.1
2021-04-23 03:12:57 -04:00
Steve Seguin
98f058594e Delete tflite.js 2021-04-23 03:06:11 -04:00
Steve Seguin
8337a8548d Add files via upload 2021-04-23 03:05:54 -04:00
Steve Seguin
be3bb3e532 Add files via upload 2021-04-23 03:04:41 -04:00
Steve Seguin
82214cbb9d Update README.md 2021-04-22 20:58:35 -04:00
Steve Seguin
6ab5a338af Merge pull request #840 from steveseguin/auto-update-advanced-settings-toc
Create update-advanced-settings-toc.yml
2021-04-22 03:29:13 -04:00
Joel Calado
bd9d707434 Create update-advanced-settings-toc.yml 2021-04-22 08:25:26 +01:00
Steve Seguin
bc6f4f08fc v17.0.1 2021-04-20 18:52:27 -04:00
Steve Seguin
28847040f7 Update IFRAME.md 2021-04-20 04:11:20 -04:00
Steve Seguin
5ab89d61b3 Update IFRAME.md 2021-04-20 04:11:06 -04:00
Steve Seguin
d059757f35 Add files via upload 2021-04-19 05:33:55 -04:00
Steve Seguin
cae33a0b92 Delete bg_sample2.jpg 2021-04-19 05:33:33 -04:00
Steve Seguin
05d5cd1a00 Delete bg_sample.jpg 2021-04-19 05:33:28 -04:00
Steve Seguin
137112242f Add files via upload 2021-04-19 05:33:18 -04:00
Steve Seguin
2ac3ff7242 Merge branch 'master' into steves-dev-branch 2021-04-19 05:30:21 -04:00
Steve Seguin
56a007725d Add files via upload 2021-04-19 05:11:49 -04:00
Steve Seguin
6f3b869c6e Add files via upload 2021-04-19 02:44:02 -04:00
Steve Seguin
71dac482d4 adblocker issue fix
&ad to &audiodevice for iframe
2021-04-15 15:25:16 -04:00
Steve Seguin
ab4582cc46 Update blank.json 2021-04-15 12:23:38 -04:00
Steve Seguin
4a57a86b53 Add files via upload
&nvb and &nmb added
2021-04-14 17:12:42 -04:00
Steve Seguin
1086848f78 Add files via upload
Support for Electron Capture 1.5.0 screen-capture
2021-04-14 04:26:51 -04:00
Steve Seguin
e91aba28bd bug fixes + moved 2600 lines of code to main.js
electron app will also warn an error now if screen-sharing via this version
consolidated the statistics overlay UI code
mixing code moved, along with new track logic and more.
2021-04-13 03:00:07 -04:00
Steve Seguin
7736e3fb6f syncing with the beta pre-release of v17
language files need some work, but I think the files are all there otherwise.
2021-04-12 02:29:35 -04:00
Steve Seguin
8f292cf7d2 Merge pull request #823 from jcalado/digital-effects-translations
Translation keys for digital video effects options
2021-04-07 20:20:41 -04:00
Steve Seguin
4a1213781d Merge pull request #824 from jcalado/settings-popup-ui-tweaks
consistent titles; improved spacing
2021-04-07 20:20:25 -04:00
Joel Calado
56f46bcd77 consistent titles; improved spacing
- Video source menu title was different from all others
- Create some space around the title so things don't look so crowded
- Made the title font bold
2021-04-05 11:12:37 +01:00
Joel Calado
d7e2b1dc3d Update index.html 2021-04-05 11:06:45 +01:00
Steve Seguin
e926196bcc Merge pull request #820 from steveseguin/generated_translations
[OBSNinja Bot] Updated translations
2021-04-03 13:15:37 -04:00
steveseguin
a48986b618 Generated updated translations 2021-04-03 17:14:01 +00:00
Steve Seguin
35fdd27f42 Merge pull request #817 from filiptronicek/patch-1
More style for CONTRIBUTING.md
2021-04-03 13:13:57 -04:00
Steve Seguin
7692623d90 Merge pull request #818 from steveseguin/jcalado-patch-1
add Ukrainian to the language picker
2021-04-03 13:13:36 -04:00
Filip Troníček
f640a9a16f Important notice <h2> 2021-04-01 15:44:31 +02:00
Filip Troníček
1b69686288 Change heading level 2021-04-01 15:43:23 +02:00
Joel Calado
da63ec44fe add Ukrainian to the language picker 2021-04-01 13:44:54 +01:00
Joel Calado
38162971ed Merge pull request #816 from Arteboy/ukrainian_translation
Ukrainian translation
2021-04-01 13:40:54 +01:00
Filip Troníček
7bd12cc4a4 More style for CONTRIBUTING.md
Add some features like headings and bold text for better readability.
2021-04-01 11:32:27 +02:00
Arteboy
53ea3ac18c Update translate.js 2021-04-01 12:20:34 +03:00
Arteboy
c0bb8b5cad Rename ukr.json to uk.json 2021-04-01 12:18:58 +03:00
Arteboy
a8d2bd9c0c Add files via upload
Ukrainian translation
2021-04-01 11:59:19 +03:00
Steve Seguin
6b3f538da3 Merge pull request #814 from jcalado/share-website-tips
share website tips on the new .message-card
2021-03-31 10:52:54 -04:00
Joel Calado
dd13c46be9 share website tips on the new .message-card 2021-03-31 14:57:39 +01:00
Steve Seguin
47d69c1b40 Merge pull request #812 from steveseguin/master
merging master with my dev branch
2021-03-31 00:47:56 -04:00
Steve Seguin
c4280b6c1d Add files via upload 2021-03-31 00:46:20 -04:00
Steve Seguin
1511de0277 Merge pull request #810 from steveseguin/jcalado-patch-1
Update pt.json
2021-03-28 19:41:09 -04:00
Joel Calado
4fcdd805a2 Update pt.json 2021-03-29 00:39:13 +01:00
Joel Calado
bfb05c7581 Merge pull request #809 from jcalado/translation-updates
update translation files
2021-03-29 00:20:04 +01:00
Joel Calado
33b2340eac Merge branch 'master' into translation-updates 2021-03-29 00:19:39 +01:00
Joel Calado
a00a6af494 update translation files 2021-03-29 00:15:24 +01:00
Steve Seguin
cd6983d75a Merge pull request #807 from jcalado/panels
message cards for info, warnings; use them on &fs
2021-03-28 19:06:46 -04:00
Steve Seguin
5b70d31844 Merge pull request #808 from jcalado/pt-translation-update
Update pt.json
2021-03-28 19:06:37 -04:00
Joel Calado
36b25b7ab7 Update pt.json 2021-03-29 00:05:03 +01:00
Joel Calado
dcd3a35682 message cards for info, warnings; use them on &fs 2021-03-28 18:06:52 +01:00
Steve Seguin
8a13af5a1b Update README.md 2021-03-28 10:47:36 -04:00
Steve Seguin
7bf4fb7b8a Update README.md 2021-03-28 10:44:07 -04:00
Steve Seguin
c0f8ba75d0 Merge pull request #806 from filiptronicek/patch-1
Update README for markdown constistency
2021-03-28 10:40:43 -04:00
Filip Troníček
20d209e222 Update README for markdown constistency
- Updated some <h4> that were direct children of <h2>s to <h3>
- Removed some unnecessary question marks and other symbols (for it to be consistent everywhere)
2021-03-28 16:39:35 +02:00
Steve Seguin
bb84c95662 Update ci-generateTranslations.js 2021-03-28 10:14:59 -04:00
Steve Seguin
afad412936 Merge pull request #805 from jcalado/screenshare-audio-css
screenshare audio selector panel tweaks
2021-03-28 10:07:30 -04:00
Joel Calado
fa7fa50d26 screenshare audio selector panel tweaks
- extract inline styles to main.css
- The audio selector panel is now similar to the one from the video camera sharing section
2021-03-28 15:05:33 +01:00
Steve Seguin
48977fe737 Merge pull request #804 from steveseguin/arrows-patch-1
Update ci-generateTranslations.js
2021-03-27 17:44:33 -04:00
Steve Seguin
de99499e30 Merge pull request #803 from steveseguin/generated_translations
[OBSNinja Bot] Updated translations
2021-03-27 17:36:50 -04:00
Steve Seguin
77c401f9c9 Update ci-generateTranslations.js
based on filiptronicek's feedback
2021-03-27 17:35:22 -04:00
steveseguin
1d04fbc474 Generated updated translations 2021-03-27 21:17:23 +00:00
Steve Seguin
9190a4584e Merge pull request #801 from jcalado/master
move "gowebcam" enabled styles over to css
2021-03-27 17:16:57 -04:00
Joel Calado
22f34d6b1e less verbose functions, safer for loop 2021-03-27 21:10:55 +00:00
Joel Calado
b973e16396 missing catches; format code 2021-03-27 21:03:47 +00:00
Joel Calado
66559c72b7 what a mess... 2021-03-27 20:52:07 +00:00
Joel Calado
b25614bd5f Update update_translations.yml 2021-03-27 20:51:14 +00:00
Joel Calado
57ec0db1eb typo 2021-03-27 20:47:51 +00:00
Joel Calado
b113d53f15 Update ci-generateTranslations.js 2021-03-27 19:39:27 +00:00
Joel Calado
2cb96bb133 Update ci-generateTranslations.js
pull from the repo
2021-03-27 19:32:51 +00:00
Joel Calado
4992858414 Update ci-generateTranslations.js 2021-03-27 19:10:41 +00:00
Joel Calado
f43f79a6cd Update ci-generateTranslations.js 2021-03-27 19:07:46 +00:00
Joel Calado
6284b38694 Update and rename main.yml to update_translations.yml 2021-03-27 19:07:31 +00:00
Joel Calado
e69c5502b2 Update .gitignore 2021-03-27 18:58:04 +00:00
Joel Calado
bf3c1ee97c Update main.yml 2021-03-27 18:57:30 +00:00
Joel Calado
0dbecf021d Merge branch 'master' of https://github.com/jcalado/obsninja 2021-03-27 18:54:47 +00:00
Joel Calado
0573bb9191 Create .gitignore 2021-03-27 18:52:12 +00:00
Joel Calado
a09d86d79e Update main.yml 2021-03-27 18:50:08 +00:00
Joel Calado
d1a2cc89f5 Update main.yml 2021-03-27 18:48:34 +00:00
Joel Calado
fc48814111 Create requirements.txt 2021-03-27 18:48:24 +00:00
Joel Calado
c7bb3f8acf move script 2021-03-27 18:45:32 +00:00
Joel Calado
e91d972753 Create main.yml 2021-03-27 18:45:14 +00:00
Joel Calado
ab1b0f2aa0 Create ci-generateTranslations.js 2021-03-27 18:38:49 +00:00
Joel Calado
59d3b0f0fb move "gowebcam" enabled styles over to css 2021-03-27 09:06:06 +00:00
Steve Seguin
1eb2f50605 Merge pull request #795 from jcalado/master
automatic language priority
2021-03-24 17:32:36 -04:00
Joel Calado
a51d43777a automatic language priority 2021-03-24 21:27:49 +00:00
Steve Seguin
cf4959c6db Merge pull request #791 from jcalado/master
Hotkey: Alt-Shift-C to toggle control bar
2021-03-24 09:51:16 -04:00
Joel Calado
68923a2462 alt+shift+c to toggle the control bar 2021-03-23 22:27:31 +00:00
Steve Seguin
055c4652ae Update README.md 2021-03-19 16:55:29 -04:00
Steve Seguin
791aaf497e just syncing up a bit of mid-dev code
THIS IS NOT FOR RELEASE YET
2021-03-16 08:50:27 -04:00
Steve Seguin
f886e485db Add files via upload 2021-03-03 01:38:33 -05:00
Steve Seguin
6b56b5ea33 Merge pull request #775 from steveseguin/v16-beta
V16.4 Release
2021-03-03 01:34:20 -05:00
Steve Seguin
41e1b4dc02 v16.4 update 2021-03-03 01:32:41 -05:00
Steve Seguin
a22388a24b Add files via upload 2021-03-01 04:15:39 -05:00
Steve Seguin
10ef9193b0 Merge pull request #762 from steveseguin/master
sync jcalado changes with my own
2021-02-26 21:41:11 -05:00
Steve Seguin
554af144f2 Add files via upload 2021-02-26 21:38:58 -05:00
Steve Seguin
36ae6c0f1e Update install.md 2021-02-25 01:29:31 -05:00
Steve Seguin
b949986182 Merge pull request #745 from jcalado/alert-modal
alert modal
2021-02-24 05:37:12 -05:00
Steve Seguin
b36b52ad8b Merge pull request #746 from steveseguin/v16-beta
V16 Release
2021-02-24 05:34:55 -05:00
Steve Seguin
de4f981455 v16.3 2021-02-24 05:33:44 -05:00
Joel Calado
eae305913f alert modal 2021-02-24 08:45:46 +00:00
Steve Seguin
14c13b02de Merge pull request #742 from jcalado/offline-notification
"no network connection" alert
2021-02-23 17:54:12 -05:00
Joel Calado
5bb7bbdd5c respect cleanOutput
if cleanOutput is set don't alert. Log to console instead.
2021-02-23 22:52:55 +00:00
Joel Calado
ea1895c129 "no network connection" alert 2021-02-23 22:34:59 +00:00
Steve Seguin
80295a7a9a Merge pull request #741 from TychoWerner/patch-1
Updated a couple of translations for the Dutch language
2021-02-23 16:21:42 -05:00
TychoWerner
ec1ec458aa More changes :) 2021-02-23 22:18:03 +01:00
TychoWerner
6b6e2b5a3f Updated a couple of translations for the Dutch language 2021-02-23 22:05:30 +01:00
Steve Seguin
a67e585eb1 v16.3 preflight 2021-02-22 17:01:19 -05:00
Steve Seguin
c6690fcdac Merge pull request #723 from filiptronicek/master
Typo
2021-02-17 22:39:12 -05:00
Steve Seguin
1f2c193956 Merge pull request #724 from theprincy/patch-7
Update it.json
2021-02-17 22:38:29 -05:00
Notelseit.com
1ba4d61486 Update it.json 2021-02-17 21:24:45 +01:00
Filip Troníček
e1f7ccacfe Fix seconary -> secondary 2021-02-17 09:55:35 +00:00
Steve Seguin
e366bdfa4c Merge pull request #722 from steveseguin/master
beginnings of v16.3; merging master.
2021-02-16 19:07:30 -05:00
Steve Seguin
62292e71ea Add files via upload 2021-02-16 19:04:01 -05:00
Steve Seguin
06379c1f1f Add files via upload 2021-02-16 19:03:51 -05:00
Steve Seguin
b471096842 Update install.md 2021-02-10 12:16:42 -05:00
Steve Seguin
85b336bafd Update README.md 2021-02-10 12:11:43 -05:00
Steve Seguin
fae0b094ac Update README.md 2021-02-10 10:54:52 -05:00
Joel Calado
a028a753e8 Merge pull request #714 from filiptronicek/patch-1
Add Wiki links to the README
2021-02-10 09:50:53 +00:00
Filip Troníček
a24e5ae667 Add Wiki links to the README
Just update the README with more hyperlinks. It's more convenient
2021-02-10 10:43:57 +01:00
Joel Calado
0f7b09526d Merge pull request #711 from steveseguin/fix_devices
Fix /devices multiple video inputs bug
2021-02-08 21:23:23 +00:00
Joel Calado
f335813c5e Update devices.html
fixes a bug where you could select multiple video devices

changes from device-id strings to sanitized device names
2021-02-08 21:16:14 +00:00
Joel Calado
f62f31329d Update devices.html
sanitize device names function
2021-02-08 21:15:17 +00:00
Joel Calado
cbc7971fa5 Update devices.html 2021-02-08 21:14:47 +00:00
Joel Calado
838acf07ac Update devices.html
change text
2021-02-08 21:14:41 +00:00
Joel Calado
864969ebae Update devices.html
add some tips to the top notice
2021-02-08 21:14:21 +00:00
Joel Calado
474e142908 tweak /devices CSS 2021-02-08 21:13:51 +00:00
Joel Calado
e5a7b29a46 Merge pull request #707 from filiptronicek/devices-refactor
Devices refactor
2021-02-08 17:58:19 +00:00
Filip Troníček
9cc109c254 Update target because of the arrow function 2021-02-08 17:21:40 +01:00
Filip Troníček
8338573c10 Template string output 2021-02-08 17:19:28 +01:00
Filip Troníček
46b00b1ce1 First ES6 refactor
- Eqeqeq
- const and let
- arrow functions
2021-02-08 17:14:27 +01:00
Steve Seguin
d2dd662d10 Merge pull request #703 from filiptronicek/patch-1
Add hyperlink to turnserver
2021-02-08 11:07:25 -05:00
Steve Seguin
c89fcc0064 Merge pull request #704 from filiptronicek/patch-2
Add comments to the commands
2021-02-08 11:07:13 -05:00
Steve Seguin
8d24e23232 Merge pull request #705 from filiptronicek/patch-3
JS syntax highlighting
2021-02-08 11:07:00 -05:00
Steve Seguin
474155e0d9 Merge pull request #706 from filiptronicek/patch-4
Update link style in License
2021-02-08 11:06:16 -05:00
Steve Seguin
166f933953 Merge pull request #694 from duncanbarnes/master
Feature: &rooms
2021-02-08 11:05:33 -05:00
Filip Troníček
43aa080bce Update link style in License
The license contains a link, which links to the raw file with no markdown features at all, I think if it is markdown, it should be displayed as such.
2021-02-08 16:52:28 +01:00
Filip Troníček
4e273219f8 Format the JS and comment a plaintext comment 2021-02-08 15:57:02 +01:00
Filip Troníček
1c3d45ae6f Const -> let because of re-declaration issues 2021-02-08 15:55:26 +01:00
Filip Troníček
c7a801c0e0 Template strings 2021-02-08 15:52:27 +01:00
Filip Troníček
8c4c1cd040 Arrow functions as onclick handlers 2021-02-08 15:52:15 +01:00
Filip Troníček
c487bed906 Invalid assignment to const 2021-02-08 15:45:46 +01:00
Filip Troníček
e762910907 Eqeqeq 2021-02-08 15:44:40 +01:00
Filip Troníček
f0cfa1effd Format IFRAME.md 2021-02-08 15:44:04 +01:00
Filip Troníček
8ee5be2960 Var -> const 2021-02-08 15:43:13 +01:00
Filip Troníček
1934097875 JS syntax highlighting
- add syntax highlighting in IFRAME.md
- update syntax to ES6
2021-02-08 15:40:48 +01:00
Filip Troníček
4e922c13aa Fix the incorrect placement of UDP and TCP 2021-02-08 10:55:27 +01:00
Filip Troníček
17403d816b Add comments to the commands
Add some explanations for the commands so that the user is better aware of what he is currently running.
2021-02-08 10:38:08 +01:00
Filip Troníček
944e619f0a Add hyperlink to turnserver 2021-02-08 10:12:30 +01:00
Duncan Barnes
d6d4d93dab Feature: &rooms
Permits a list of rooms to be defined for room transfer operations. Buttons are added to the control bar, when selected and a transfer button is pushed the user will be transfered straight to that room rather than the room name having to be entered.
2021-02-06 17:32:31 +00:00
Steve Seguin
5c7648d86a Create turnserver3.conf 2021-02-04 11:11:46 -05:00
Steve Seguin
c28ecebc13 Merge pull request #686 from steveseguin/v16-beta
V16.3
2021-02-03 11:17:29 -05:00
Steve Seguin
28a79b20b7 v16.3 release? 2021-02-03 11:16:35 -05:00
Steve Seguin
9172ebade1 Add files via upload 2021-02-01 09:58:31 -05:00
Steve Seguin
c4aed40be6 v16.0 2021-01-30 09:40:34 -05:00
Steve Seguin
1927b84d25 Merge pull request #676 from steveseguin/steveseguin-patch-1
Update nl.json
2021-01-27 17:29:55 -05:00
Steve Seguin
b6b39b893f Update nl.json 2021-01-27 17:29:08 -05:00
Steve Seguin
1f408e645f Merge pull request #675 from SilverServerT/master
Update nl.json
2021-01-27 17:04:45 -05:00
SilverServerT
eee79d2ef9 Update nl.json 2021-01-27 22:57:28 +01:00
Steve Seguin
abe5028458 Update README.md 2021-01-27 10:42:11 -05:00
Steve Seguin
09e1111be3 Update README.md 2021-01-27 10:40:54 -05:00
Steve Seguin
c2b8b1769d Delete videojs-vr.js 2021-01-27 10:38:35 -05:00
Steve Seguin
7c9ba6fd12 Delete video.js 2021-01-27 10:38:27 -05:00
Steve Seguin
f50ca54b32 Delete adapter-latest.js
using a smaller version instead
2021-01-27 10:38:13 -05:00
Steve Seguin
a4edbf1a18 Delete jquery.min.js
begone vile beast
2021-01-27 10:37:56 -05:00
Steve Seguin
308664fdc2 Delete tone.ogg
moved to media
2021-01-27 10:37:18 -05:00
Steve Seguin
8c68737501 Delete tone.mp3
moved to media
2021-01-27 10:37:08 -05:00
Steve Seguin
50e50c4d1b Delete cap.webm
moved to media
2021-01-27 10:36:57 -05:00
Steve Seguin
f05efc5288 Delete animations.js
migrated all the code away from jquery
2021-01-27 10:36:40 -05:00
Steve Seguin
a6b500bfa8 Delete images directory
deleting the right directory for once.  not enough coffeeeeee
2021-01-27 10:36:10 -05:00
Steve Seguin
32f4527784 Add files via upload 2021-01-27 10:35:42 -05:00
Steve Seguin
d45584c444 Delete favicon-16x16.png 2021-01-27 10:34:50 -05:00
Steve Seguin
70d83b35a7 Delete cap.webm 2021-01-27 10:30:02 -05:00
Steve Seguin
5198817371 Delete tone.mp3 2021-01-27 10:29:48 -05:00
Steve Seguin
d483b8ca3a Delete tone.ogg 2021-01-27 10:29:42 -05:00
Steve Seguin
a6c903fe1a Add files via upload 2021-01-27 10:29:23 -05:00
Steve Seguin
4a203a1e60 Add files via upload 2021-01-27 10:27:45 -05:00
Steve Seguin
c9849849a5 Merge pull request #674 from steveseguin/version-v15.1
Version v15.1
2021-01-27 10:16:36 -05:00
Steve Seguin
705c8028a1 Add files via upload 2021-01-27 10:13:18 -05:00
Steve Seguin
4618426724 Update animations.js 2021-01-27 10:07:46 -05:00
Steve Seguin
5b86a4f906 version v15.1 2021-01-27 10:05:42 -05:00
Joel Calado
2d0908f326 Merge pull request #673 from digisomni/patch-1
Fix typo in index.html
2021-01-27 10:10:27 +00:00
Kalila
843c49fa20 Update index.html
Fix extra "
2021-01-27 01:01:03 -05:00
Steve Seguin
e4d6862d9f Update animations.js 2021-01-21 07:20:21 -05:00
Steve Seguin
5538afdd71 Update animations.js
updating code to align with css; 220px is old.
2021-01-21 07:19:54 -05:00
Steve Seguin
e34b68a573 Merge pull request #671 from filiptronicek/master
Refactor animation code
2021-01-21 07:18:15 -05:00
Filip Troníček
0af23bf6de Break 2021-01-20 17:54:16 +01:00
Filip Troníček
8819b2fe95 Eqeqeq
https://eslint.org/docs/rules/eqeqeq
2021-01-20 17:52:36 +01:00
Filip Troníček
0913cc204d Unnecesary spaces 2021-01-20 17:51:28 +01:00
Filip Troníček
e31571e508 Rewrite style injection as a template string 2021-01-20 17:50:43 +01:00
Filip Troníček
a26cd7a1c9 Unused events as _e 2021-01-20 17:46:33 +01:00
Filip Troníček
e412f486bd var =>const 2021-01-20 17:46:11 +01:00
Filip Troníček
547a914a59 Template strings and const 2021-01-20 17:44:55 +01:00
Filip Troníček
89b651bb89 Simple code changes in animations.js
- delete double semicolons
- Convert anonymous functions to arrow functions where possible
2021-01-20 17:42:42 +01:00
Steve Seguin
b0b992c3a9 Add files via upload 2021-01-20 11:27:21 -05:00
Steve Seguin
d7117cede1 Merge pull request #670 from steveseguin/14.0-alpha
Version 14 release
2021-01-20 11:22:38 -05:00
Steve Seguin
ec4940b139 Merge branch 'master' into 14.0-alpha 2021-01-20 11:19:31 -05:00
Steve Seguin
dda1c48b00 Add files via upload 2021-01-20 11:10:34 -05:00
Steve Seguin
94e6380081 v14 release version 2021-01-20 11:00:56 -05:00
Steve Seguin
7f46ea944d Update install.md 2021-01-20 03:15:22 -05:00
Steve Seguin
217b9e102b Update install.md 2021-01-18 05:18:40 -05:00
Steve Seguin
11e7f6162d Update install.md 2021-01-18 05:02:19 -05:00
Steve Seguin
986675a747 Update install.md 2021-01-18 05:01:29 -05:00
Steve Seguin
c75e641c3a Update install.md 2021-01-18 05:01:02 -05:00
Steve Seguin
bf3a576507 Update install.md 2021-01-18 04:56:26 -05:00
Steve Seguin
423081ba4f Merge pull request #662 from steveseguin/revert-661-jcalado-patch-1
Revert "Update pt.json"
2021-01-17 10:32:51 -05:00
Steve Seguin
e965aacedf Revert "Update pt.json" 2021-01-17 10:32:10 -05:00
Steve Seguin
980afc35cb Merge pull request #661 from steveseguin/jcalado-patch-1
Update pt.json
2021-01-17 10:28:11 -05:00
Joel Calado
a74a128227 Update pt.json 2021-01-17 15:27:30 +00:00
Steve Seguin
b9e556908c Merge pull request #658 from steveseguin/chrismarquardt-langauge-patch-1
Some minor updates for the German translation
2021-01-17 00:09:45 -05:00
chrismarquardt
614d8fcf51 Some minor updates for the German translation 2021-01-17 01:16:55 +01:00
Steve Seguin
5fd0facefb Update README.md 2021-01-13 01:03:08 -05:00
Steve Seguin
20d9339c14 Update main.js 2021-01-13 01:01:41 -05:00
Steve Seguin
7ef903d6b5 Update README.md 2021-01-13 00:59:24 -05:00
Steve Seguin
43c5e89c2d Merge pull request #645 from 0xflotus/patch-1
fix: small error
2020-12-30 15:08:50 -05:00
0xflotus
98270abc37 fix: small error 2020-12-30 18:49:18 +01:00
Steve Seguin
7e96ef72a3 Update README.md 2020-12-21 23:59:11 -05:00
Steve Seguin
a0e765fa4d Merge pull request #599 from filiptronicek/14.0-alpha
Convert animations.js to ES6
2020-12-11 12:02:14 -05:00
Filip Troníček
4f8d2c2d38 Remove extra semicolons 2020-12-11 17:55:33 +01:00
Filip Troníček
78b7b9cba9 Format animations.js 2020-12-11 17:54:38 +01:00
Filip Troníček
26288e685a Rename e to er due to IE8 overwriting it
Nobody will use OBS.ninja in IE8 but who knows...
2020-12-11 17:54:17 +01:00
Filip Troníček
b25632188a Template string 2020-12-11 17:53:42 +01:00
Filip Troníček
f7adb67e3e ES6 conversion 2020-12-11 17:53:33 +01:00
STeve Seguin
2f3570dc19 style update 2020-12-11 11:50:35 -05:00
Steve Seguin
c25ca2c5a2 Merge pull request #596 from jcalado/14.0-alpha
guest label styles
2020-12-11 10:33:35 -05:00
Joel Calado
91545d7a9a guest label styles
Label styles are added classes to .video-label

- zoom
- teams
- skype
- ninjablue
- toprounded
- fire
- floating3d
2020-12-11 15:07:24 +00:00
STeve Seguin
de68524b32 ugh. css fix. how did this not transfer? 2020-12-11 09:48:44 -05:00
STeve Seguin
99cdff796d merge error fix 2020-12-11 09:39:18 -05:00
Steve Seguin
e8d5fca0d0 Delete mic-animate.gif 2020-12-11 09:17:52 -05:00
Steve Seguin
b45be651fc Delete mic-slash.gif 2020-12-11 09:17:45 -05:00
Steve Seguin
5989cc8022 Delete mic.gif 2020-12-11 09:17:38 -05:00
STeve Seguin
b14ebeffb9 merge with master 2020-12-11 09:11:27 -05:00
STeve Seguin
ebc42a9665 merge fix; webaudio added 2020-12-11 09:07:28 -05:00
STeve Seguin
e745daf082 audio updates, webaudio etc 2020-12-11 08:52:41 -05:00
STeve Seguin
53600179d1 audio updates, webaudio etc 2020-12-11 08:51:10 -05:00
STeve Seguin
4ec34fdb8a audio updates, webaudio etc 2020-12-11 08:48:23 -05:00
Steve Seguin
bd5cb836c5 Merge pull request #582 from filiptronicek/14.0-alpha
ES6 conversion for translate.js
2020-12-07 10:25:07 -05:00
Steve Seguin
e5178e7c6d Merge pull request #581 from jcalado/patch-1
Update pt.json
2020-12-07 10:08:23 -05:00
Filip Troníček
dfc6fdfcf0 Semicolon 2020-12-07 16:08:08 +01:00
Steve Seguin
bbe5629e2c Merge pull request #580 from filiptronicek/alpha-translations-cs
Alpha translations CS
2020-12-07 10:08:05 -05:00
Filip Troníček
c6f442288e Convert callbacks to arrow functions 2020-12-07 16:07:32 +01:00
Filip Troníček
e9a4ffa3c8 Format code 2020-12-07 15:01:54 +00:00
Filip Troníček
b1b5d7f0ff Re-write stuff to ES6 2020-12-07 15:00:59 +00:00
Joel Calado
d8ddcb6d65 Update pt.json 2020-12-07 14:59:33 +00:00
Filip Troníček
785e531d47 Final and sixth bunch of translation strings! 2020-12-07 14:31:56 +00:00
Filip Troníček
e621e497f9 Fifth bunch of strings 2020-12-07 14:25:53 +00:00
Filip Troníček
60a0167ac3 Third bunch of strings 2020-12-07 14:22:46 +00:00
Filip Troníček
8f28cf50e4 Second bunch of strings 2020-12-07 14:14:11 +00:00
Filip Troníček
84b6a2c211 First bunch of strings 2020-12-07 14:09:49 +00:00
Steve Seguin
6f697a638f Update IFRAME.md 2020-12-07 08:18:42 -05:00
Steve Seguin
a0e545da4e Merge pull request #579 from jcalado/14.0-alpha
speedtest graph color, button status, log copy
2020-12-07 08:00:12 -05:00
Joel Calado
40834d04ba speedtest graph color, button status, log copy
graphs target value
graphs max value
graphs now have colors, green / yellow / red
log click to copy
bitrate buttons reflect incoming connection requested bitrates via background color
disconnect button hidden until first connection established
2020-12-07 12:51:07 +00:00
Steve Seguin
edb531d8a1 Merge pull request #577 from filiptronicek/master
Improve Czech translations
2020-12-06 16:43:17 -05:00
Filip Troníček
eb8d45daa3 Grammar typo (probably) 2020-12-06 21:33:22 +00:00
Filip Troníček
53eb8c41f6 Improve czech translation 2020-12-06 21:28:21 +00:00
Steve Seguin
c1e9eb6dae debug version of 14.0-alpha
first 14.0 alpha build

not done yet.  includes labels, broadcast flag fixes, updated translation file stuff, etc.

UNTESTED!!!!!
2020-12-06 13:38:55 -05:00
Steve Seguin
083ec819dd Update install.md 2020-12-04 05:45:33 -05:00
Steve Seguin
0f293e4922 Update install.md 2020-12-04 05:29:25 -05:00
Steve Seguin
9bcbebca14 Update README.md 2020-12-04 05:25:56 -05:00
Steve Seguin
33e79daf9d Update install.md 2020-12-04 05:25:28 -05:00
Steve Seguin
5bfdfe325a Merge pull request #566 from duncanbarnes/master
Create iframe.outbound-stats.html
2020-12-03 07:46:47 -05:00
Duncan Barnes
beb5881352 Create iframe.outbound-stats.html
Added an example of using the iFrame API to retrieve and display outgoing viewer stats.
2020-12-03 12:35:59 +00:00
STeve Seguin
7baa26c99d fixed bug; added scene=2 2020-12-02 06:39:29 -05:00
STeve Seguin
eb86f2f6e7 merge conflict 2020-12-02 04:28:45 -05:00
STeve Seguin
6b9242532c merging fix? 2020-12-02 04:24:29 -05:00
STeve Seguin
9a7446a083 merge fix? 2020-12-02 04:22:21 -05:00
Joel Calado
c87781dcbd add red button css 2020-12-02 04:20:27 -05:00
STeve Seguin
6bc6736cdf audio/video retry logic 2020-12-02 04:15:45 -05:00
Steve Seguin
d7c0e0cfcd Merge pull request #552 from jcalado/master
Speedtest graphs, data logging, UI revamp
2020-12-01 13:06:01 -05:00
Joel Calado
c361aa19c1 speedtest improvements
- Design update
- Graphs!
- Data logging
2020-11-30 23:47:14 +00:00
Joel Calado
bd4763a087 add red button css 2020-11-30 23:45:17 +00:00
STeve Seguin
1d93b28595 merge 2020-11-30 06:16:01 -05:00
STeve Seguin
aebd62fdcc Merge remote-tracking branch 'origin/master' into 13.5a 2020-11-30 06:15:51 -05:00
STeve Seguin
51438daec8 language changes 2020-11-30 06:14:36 -05:00
Steve Seguin
820b06d518 Merge pull request #547 from jcalado/master
Guest tips
2020-11-29 13:56:33 -05:00
Joel Calado
017d2b2de5 Merge remote-tracking branch 'upstream/master' 2020-11-29 17:20:29 +00:00
Joel Calado
bdb50bd8cb guest tips
tips for: network, power, audio
2020-11-29 17:19:42 +00:00
STeve Seguin
f502bbbec3 Merge remote-tracking branch 'origin/master' into 13.5a 2020-11-29 07:25:48 -05:00
STeve Seguin
a237d9b025 bug fixes; website supported; UI 2020-11-29 07:24:46 -05:00
Steve Seguin
dd47dee760 Merge pull request #542 from jcalado/master
Add easy url parameters sharing to /devices
2020-11-29 06:37:50 -05:00
Joel Calado
574f9c6652 Update devices.html 2020-11-28 22:05:44 +00:00
Joel Calado
1084c8efd7 share &ad and &vd strings easily
handles multiple selection of audio devices, just click the ones you want
string to share is copied on click
2020-11-28 22:05:22 +00:00
STeve Seguin
a2436704df sync rtc 2020-11-28 04:15:21 -05:00
STeve Seguin
82a709cf6f Merge remote-tracking branch 'origin/master' into 13.5a 2020-11-28 04:13:27 -05:00
STeve Seguin
e8abdb4e96 tinkering -- EXPERIMENTAL 13.5-alpha branch 2020-11-28 04:11:04 -05:00
Steve Seguin
a93556e662 Merge pull request #534 from jcalado/master
touch up /devices
2020-11-28 00:31:37 -05:00
Joel Calado
1332a62b5e touch up /devices 2020-11-27 18:56:37 +00:00
Steve Seguin
3e7d538156 Merge pull request #533 from jcalado/master
allow for more strings to be translated
2020-11-27 11:04:06 -05:00
Joel Calado
6b09ee3b45 allow for more strings to be translated
"data-translate-type" attribute with the value of the attribute that you wish translated.
allows for translation of virtually anything? placeholder attr, title attr, etc.
2020-11-27 15:55:14 +00:00
Steve Seguin
accf4ce228 Merge pull request #532 from jcalado/master
updates pt.json
2020-11-27 09:25:16 -05:00
Joel Calado
3c8e93e9a1 Merge remote-tracking branch 'upstream/master' 2020-11-27 14:23:03 +00:00
Joel Calado
0eee6d6c4f Update pt.json 2020-11-27 14:21:48 +00:00
Steve Seguin
b0bfebc451 Merge pull request #531 from jcalado/master
extract card styles to its own class
2020-11-27 09:17:24 -05:00
Joel Calado
9617ef3739 extract card styles to its own class 2020-11-27 14:13:32 +00:00
Steve Seguin
46d978d908 Merge pull request #526 from jcalado/master
tweaks to /supports
2020-11-27 08:23:41 -05:00
Joel Calado
df2e3c9d7f tweaks to /supports 2020-11-27 13:16:32 +00:00
Steve Seguin
f5f2114446 support tool for debugging device support; accessing sinkID, etc. 2020-11-26 04:45:53 -05:00
Steve Seguin
d48483f113 Version 13.4 Release
pushed to production on November 24th
2020-11-24 05:25:07 -05:00
Steve Seguin
595b233327 Update turnserver.md 2020-11-22 00:30:47 -05:00
Steve Seguin
e64fa5c598 Update turnserver.md 2020-11-22 00:28:04 -05:00
Steve Seguin
40ff06c77b Update main.js 2020-11-09 18:14:01 -05:00
Steve Seguin
9d1e1d7f53 version 13.2
-bug fixes
-camera settings added
2020-11-09 16:24:26 -05:00
Steve Seguin
a6332b347a minor fix for rtc.ninja 2020-11-02 02:59:34 -05:00
Steve Seguin
84e6565c01 v13.1 2020-11-02 02:40:44 -05:00
Steve Seguin
589d0e4575 Merge pull request #420 from steveseguin/iframe-api-docs
Create IFRAME.md
2020-10-20 00:13:26 -04:00
Steve Seguin
c08d707a7f Create IFRAME.md 2020-10-20 00:12:51 -04:00
Steve Seguin
115f24fe96 Merge pull request #384 from Sleavely/patch-1
Use pointer cursor on the columns
2020-10-17 22:05:19 -04:00
Joakim Hedlund
db522b4ba4 Removes typo 2020-10-17 12:19:19 +02:00
Joakim Hedlund
20b68aff06 Use a cursor pointer when columns not expanded 2020-10-17 12:17:12 +02:00
Steve Seguin
2f0a1ba99e Update README.md 2020-10-13 16:29:15 -04:00
Steve Seguin
61883e5e54 Merge pull request #359 from kindlyops/elliot/tiny-cleanup
minor cleanups
2020-10-02 13:48:52 -04:00
Elliot Murphy
706d19bfd4 Specify which macOS OBS versions need workaround
This should make the docs a bit more resilient against
confusion with future OBS releases.

Signed-off-by: Elliot Murphy <statik@users.noreply.github.com>
2020-10-01 23:51:21 -04:00
Elliot Murphy
dc63c510c0 Link to new podcast series
Signed-off-by: Elliot Murphy <statik@users.noreply.github.com>
2020-10-01 23:50:46 -04:00
Elliot Murphy
1d4b0dd02f Fix spelling typo
Signed-off-by: Elliot Murphy <statik@users.noreply.github.com>
2020-10-01 23:45:54 -04:00
Elliot Murphy
ac7a2d15cb Remove unreachable code in keyboard shortcuts
Signed-off-by: Elliot Murphy <statik@users.noreply.github.com>
2020-10-01 23:45:01 -04:00
Steve Seguin
12fc6e0f1f Update README.md 2020-09-25 16:27:11 -04:00
Steve Seguin
7af76cfa7b Update index.html 2020-09-24 14:16:35 -04:00
Steve Seguin
a3156bbfb8 Update main.js 2020-09-24 14:15:53 -04:00
Steve Seguin
bf24d6c02c Update README.md 2020-09-23 09:27:14 -04:00
Steve Seguin
ef513acdd7 Update README.md 2020-09-23 09:26:48 -04:00
Steve Seguin
5cca338c9e Update README.md 2020-09-23 09:26:37 -04:00
Steve Seguin
883d5f10ab Update README.md 2020-09-23 09:26:28 -04:00
Steve Seguin
6ab97abe5d Update README.md 2020-09-23 09:25:53 -04:00
Steve Seguin
1cf307a8b4 Update README.md 2020-09-23 09:25:04 -04:00
Steve Seguin
c619810dd5 Update README.md 2020-09-23 09:24:42 -04:00
Steve Seguin
d78a7ec29f Update README.md 2020-09-23 09:24:17 -04:00
Steve Seguin
c88b97f388 Update README.md 2020-09-23 09:22:10 -04:00
Steve Seguin
cf6db6c0d8 Merge pull request #335 from KovalevArtem/patch-4
Update ru.json
2020-09-18 16:24:42 -04:00
KovalevArtem
0759b62306 Update ru.json 2020-09-18 23:23:02 +03:00
Steve Seguin
fddc8bbf74 Update README.md 2020-09-16 08:09:29 -04:00
Steve Seguin
57e80baf24 Update turnserver.md 2020-09-16 03:01:32 -04:00
Steve Seguin
88798d7694 Update turnserver.md 2020-09-16 02:48:20 -04:00
Steve Seguin
16e5a7f4e6 Update README.md
my old TURN server that isn't be used by any official OBS.Ninja deployment currently is still being heavily used; just got a $30 bill for someone's abusive streaming on it over the last few hours alone. .. ouch. I'm going to stop it I think and likely secure the existing TURN server with temporary tokens. I've supplied enough info on how to deploy your own.
2020-09-16 02:45:16 -04:00
Steve Seguin
4bcc628126 Merge pull request #326 from jcalado/patch-1
Update pt.json
2020-09-15 09:47:50 -04:00
Joel Calado
1cec930afb Update pt.json 2020-09-15 11:18:50 +01:00
Steve Seguin
b270267ec9 Delete template.js 2020-09-14 09:58:42 -04:00
Steve Seguin
52ab49158e Add files via upload 2020-09-14 09:36:21 -04:00
Steve Seguin
de23614d69 Release v12
https://www.reddit.com/r/OBSNinja/comments/irz5be/obsninja_version_12_released_change_log_here/
2020-09-14 09:35:42 -04:00
Steve Seguin
308e81cf12 Merge pull request #315 from COOLIGUAY/patch-3
Update es.json
2020-09-12 06:38:41 -04:00
Monsteer
1b9723ebd6 Small fix 2020-09-12 12:33:10 +02:00
Monsteer
523863f29d Update es.json
Updated to the new additions
2020-09-12 12:29:00 +02:00
Steve Seguin
d6f35092f1 Update turnserver.md 2020-09-12 06:06:46 -04:00
Steve Seguin
031fab960a Update turnserver.md 2020-09-12 06:05:28 -04:00
Steve Seguin
34a6bb49d0 Create turnserver2.conf
this one uses a paid SSL cert
2020-09-12 01:41:40 -04:00
Steve Seguin
059a575b33 Merge pull request #314 from theprincy/patch-6
Update Italian
2020-09-11 17:45:48 -04:00
Notelseit.com
7a51c5a826 Update Italian 2020-09-11 23:44:23 +02:00
Steve Seguin
02c93549d6 Update README.md 2020-09-11 07:52:54 -04:00
Steve Seguin
fb10013ca2 Update README.md 2020-09-11 07:52:33 -04:00
Steve Seguin
5cf966b0e8 Update README.md 2020-09-11 07:52:00 -04:00
Steve Seguin
b40e743253 Update README.md 2020-09-11 07:51:30 -04:00
Steve Seguin
1cfadb29e2 Update README.md 2020-09-11 07:50:47 -04:00
Steve Seguin
a787a78892 Update install.md 2020-09-11 07:46:32 -04:00
Steve Seguin
9cd7b23436 Update install.md 2020-09-11 07:32:54 -04:00
Steve Seguin
afc6e4f059 Create install.md 2020-09-11 07:32:30 -04:00
Steve Seguin
df6c147311 Update turnserver.md 2020-09-10 09:49:01 -04:00
Steve Seguin
0d1606833d Update turnserver.md 2020-09-10 09:29:08 -04:00
Steve Seguin
b8cfecb108 Update turnserver.md 2020-09-10 09:17:20 -04:00
Steve Seguin
31b9df5a06 Update turnserver.md 2020-09-10 09:12:03 -04:00
Steve Seguin
905c39a58b Update turnserver.md 2020-09-10 09:09:27 -04:00
Steve Seguin
d52fb0905e Update turnserver.md 2020-09-10 08:51:22 -04:00
Steve Seguin
6d19c082f4 Update README.md 2020-09-10 08:46:40 -04:00
Steve Seguin
bae12cbf73 Update README.md 2020-09-10 08:44:15 -04:00
Steve Seguin
a0031fff0c Update turnserver.md 2020-09-10 08:42:53 -04:00
Steve Seguin
5115ae7735 Update turnserver.md 2020-09-10 08:42:31 -04:00
Steve Seguin
a1aab474fa Update turnserver.md 2020-09-10 08:41:58 -04:00
Steve Seguin
17c2ae5fc6 Update turnserver.md 2020-09-10 06:16:38 -04:00
Steve Seguin
268460dcd5 Update turnserver.md 2020-09-10 02:14:38 -04:00
Steve Seguin
e7ba4f9e8d Update turnserver.conf 2020-09-10 02:04:41 -04:00
Steve Seguin
214c8d2a76 Update turnserver.md 2020-09-10 02:03:21 -04:00
Steve Seguin
c2fea315a6 Update turnserver.md 2020-09-10 02:01:04 -04:00
Steve Seguin
da2b18f642 Update turnserver.md 2020-09-10 02:00:43 -04:00
Steve Seguin
c310afa1f7 Update turnserver.md 2020-09-10 02:00:09 -04:00
Steve Seguin
c801ceb19a Update turnserver.md 2020-09-10 01:25:36 -04:00
Steve Seguin
b0231f4e00 Add files via upload 2020-09-09 03:01:47 -04:00
Steve Seguin
aa24a4be94 did I fix it? 2020-09-09 02:58:34 -04:00
Steve Seguin
d20aa3cd43 Merge pull request #311 from jcalado/update-portuguese
update portuguese translation
2020-09-09 02:14:20 -04:00
Joel Calado
3b411091d4 update portuguese translation 2020-09-09 07:12:04 +01:00
Steve Seguin
bf9e520f1c try try again 2020-09-09 01:23:04 -04:00
Steve Seguin
fb8271142b Delete default.json 2020-09-09 01:06:13 -04:00
Steve Seguin
680535412e Add files via upload 2020-09-09 01:05:51 -04:00
Steve Seguin
c23aaced01 updated language files a small bit 2020-09-09 00:33:10 -04:00
Steve Seguin
d4f29c6491 Update README.md 2020-09-08 03:05:22 -04:00
Steve Seguin
05082022cd Rename turnserver.MD to turnserver.md 2020-09-08 02:31:56 -04:00
Steve Seguin
d2d9e4918d Update turnserver.MD 2020-09-08 02:30:57 -04:00
Steve Seguin
f1f146a8a5 Update turnserver.MD 2020-09-08 02:28:39 -04:00
Steve Seguin
0c60f1fdd6 Update turnserver.MD 2020-09-08 02:27:53 -04:00
Steve Seguin
daa39963e8 Update turnserver.MD 2020-09-08 02:27:24 -04:00
Steve Seguin
5e5c1592bf Update turnserver.MD 2020-09-08 02:26:41 -04:00
Steve Seguin
f3b7e86095 Update turnserver.MD 2020-09-08 02:25:46 -04:00
Steve Seguin
eeaace7788 Update turnserver.MD 2020-09-08 02:25:37 -04:00
Steve Seguin
5868223c35 Create turnserver.MD 2020-09-08 02:25:06 -04:00
Steve Seguin
6be0fcf79b Merge pull request #302 from COOLIGUAY/patch-2
Update es.json
2020-09-07 16:13:55 -04:00
Monsteer
a9ca78ee2a Update es.json
Things I forgot to change...
2020-09-07 20:28:13 +02:00
Monsteer
2a481a8dbe Update es.json
Small fixes/typing errors.
2020-09-07 20:16:47 +02:00
Steve Seguin
472a83870a Add files via upload
speedtest stats fix
title bar updates
stats menu UI updates
electron app tweaked
etc
2020-09-07 06:20:18 -04:00
Steve Seguin
7e839a0a5b Merge pull request #269 from jcalado/new-stats-ui
new stats ui
2020-09-05 23:21:48 -04:00
Joel Calado
61533e8c9e new stats ui 2020-09-05 22:03:19 +01:00
Steve Seguin
c618b91aa5 Merge pull request #268 from COOLIGUAY/patch-1
Update es.json
2020-09-04 18:03:14 -04:00
Monsteer
c15f33934b Update es.json 2020-09-04 23:27:54 +02:00
Steve Seguin
1df852448b reorganized stats page elements
stats menu for view/push videos has been refactored a bit; cleaned up for easier CSS/templating.
2020-09-04 15:56:03 -04:00
Steve Seguin
fb9bfcc12b push to talk, room fixes, minor tweaks
largely Untested -- not a proper release.
2020-09-04 07:33:53 -04:00
Steve Seguin
af516397d8 Merge pull request #266 from jcalado/update-pt-translation
Update pt.json
2020-09-02 17:20:15 -04:00
Joel Calado
b43a21e860 Update pt.json 2020-09-02 21:30:08 +01:00
Steve Seguin
4dbb6613ef Add files via upload
removing the extra tabs I introduced
2020-08-27 04:31:40 -04:00
Steve Seguin
d33f5bef4c Merge pull request #260 from steveseguin/contrib+patches
try #2.
2020-08-27 04:29:06 -04:00
Steve Seguin
5eefa9021d try #2.
Forgot to pull before I merged.
2020-08-27 04:27:45 -04:00
Steve Seguin
05b021e18c Merge pull request #259 from steveseguin/revert-258-pig-latin-and-novideo-fix
Revert "Add files via upload"
2020-08-27 04:18:21 -04:00
Steve Seguin
c7bbfc4b15 Revert "Add files via upload" 2020-08-27 04:18:09 -04:00
Steve Seguin
235908d0e6 Merge pull request #258 from steveseguin/pig-latin-and-novideo-fix
Add files via upload
2020-08-27 04:14:17 -04:00
Steve Seguin
9ad233cfa9 Add files via upload 2020-08-27 04:13:48 -04:00
Steve Seguin
7425fceeec Merge pull request #257 from jcalado/master
Tweak invite like generator UI
2020-08-27 04:06:22 -04:00
Joel Calado
2caf55089f Merge branch 'master' of https://github.com/jcalado/obsninja 2020-08-27 08:39:44 +01:00
Joel Calado
b31248c191 Tweak Reusable Invite generator UI
Separates settings in groups to improve further expansion of available toggles
Increases margins and padding
2020-08-27 08:37:41 +01:00
Joel Calado
3215094833 Tweak Reusable Invite generator UI
Separates settings in groups to improve further expansion of available toggles
Increases margins and padding
2020-08-27 08:30:11 +01:00
Steve Seguin
75ba4f4b4d Version 10.3 - beta
fixed more bugs; minor polish in features
2020-08-26 06:29:17 -04:00
Steve Seguin
8ff1a0f6b2 v10.2 - bug fixes
Fixed a bug with the group room, where videos froze on guest re-join

added &scene=0 (&scene) , which auto-adds videos to the scene without interaction

Updated the &mirror URL flag to have 0, 1, 2, and 3 as options.

-no mirror parameter in the URL is the default.  Local previews are mirrored
-mirror=0 turns off ALL mirroring, for everything.
-mirror=1 inverts the default.  Local previews are not mirrored, but guests are mirrored.
-mirror=2 mirrors ALL videos
-mirror=3 is the same as default, except the entire website is mirrored, including Text (useful for teleprompters?)

Experimental Code Change :  When the data-channel closes, I auto-close the video channels as well, and try to reload the video from scratch.  I do not wait for the video stream to time out.  This *might* fix a 'stacking video' problem, as well as reducing the "frozen frame" video problem when a guest reloads.

This 10.2 release is currently on https://obs.ninja/beta for testing
2020-08-24 03:14:27 -04:00
Steve Seguin
588e99d581 Merge pull request #239 from steveseguin/version-10.1
Release version 10.1
2020-08-23 02:59:00 -04:00
Steve Seguin
4c4bdeb026 Release version 10.1
release notes here
https://www.reddit.com/r/OBSNinja/comments/ib7vhk/version_10_released_text_chat_and_more_added_see/
2020-08-23 02:55:13 -04:00
Steve Seguin
d1d736549d Update FUNDING.yml 2020-08-18 22:15:15 -04:00
Steve Seguin
8af2a94edc Update FUNDING.yml 2020-08-18 21:49:36 -04:00
Steve Seguin
2a31e5508e fixed an issue with advanced audio settings; added meter 2020-07-27 04:08:41 -04:00
Steve Seguin
dd950b1c4e Add files via upload
added translations
2020-07-17 02:25:36 -04:00
Steve Seguin
079b1e8545 Added iFrame communication and an example 2020-07-17 02:25:09 -04:00
Steve Seguin
9d1c466025 Update turnserver.conf 2020-07-05 17:11:57 -04:00
Steve Seguin
be741878e8 tweaked the auto mixer; side by side instead of over/under as default 2020-07-01 22:17:54 -04:00
Steve Seguin
7dcaf7f6f3 Delete IMG_1148.jpg 2020-06-30 04:51:54 -04:00
Steve Seguin
3719f4a4da version 8.01
minor bug fixes
2020-06-28 21:05:33 -04:00
Steve Seguin
4ec298f77c Merge pull request #218 from steveseguin/steveseguin-patch-1
Version 8.0 release
2020-06-28 05:05:59 -04:00
Steve Seguin
0cb72c1a02 Add files via upload 2020-06-28 03:16:50 -04:00
Steve Seguin
a94e164bfe Add files via upload
being added for convenience;
2020-06-28 03:14:45 -04:00
Steve Seguin
e61d6f4830 Version 8 - release (part1) 2020-06-28 02:52:53 -04:00
Steve Seguin
832c584956 doc for OBS.ninja; lets you create invites from inside OBS 2020-06-22 17:29:28 -04:00
Steve Seguin
1aaae51db4 Add files via upload 2020-06-17 05:20:50 -04:00
Steve Seguin
d990bce2ea Add files via upload 2020-06-17 05:07:18 -04:00
Steve Seguin
6c80bf83e0 Add files via upload 2020-06-17 05:06:09 -04:00
Steve Seguin
49df175df6 Version 7 Release Candidate
Mainly just a 1-week sprint focusing on bug fixes and not so much on major features.

Biggest fix this sprint was related to VP9 + Custom Video Bitrates now working as intended.
2020-06-17 04:24:30 -04:00
Steve Seguin
815c619313 Update index.html
for private deployment, please see blank.json as a good starting point for unbranded customization text.  My goal is to move towards more of a library for the core code, so I am trying to separate out the UI/text/style more and more. This should make forking of new branches of the code easier in the long run.
2020-06-16 05:02:28 -04:00
Steve Seguin
aa50333dc9 BETA Version -- bug fixes + main.js editable again
&view should work in the previous version, but tested again in this version
&view=xx,yy,zz should work
&codec=vp9 issue fixed in codec handler
numerous other fixes.
2020-06-16 04:41:07 -04:00
Steve Seguin
7300421a19 Add files via upload 2020-06-16 01:34:59 -04:00
Steve Seguin
6dbc64138a translation tool and updated translation files.
will improve on this all more in the upcoming next release
2020-06-14 21:27:03 -04:00
Steve Seguin
fa820d812c Merge pull request #168 from jcalado/patch-1
Create pt.json
2020-06-14 13:49:54 -04:00
Joel Calado
2c955e62b1 Create pt.json 2020-06-14 11:16:47 +01:00
Steve Seguin
a6c9cfcf1b added more configuration options
TURN server can be specified, along with changing default values.

not heavily tested yet.
2020-06-10 13:21:11 -04:00
Steve Seguin
6a2e5d1a84 Merge pull request #163 from steveseguin/Version-6
release version 6 updates
2020-06-09 12:37:52 -04:00
Steve Seguin
2d60c0a441 release version 6 updates
I've fixed issues with innerHTML calls in this release that would normally totally break things if there were changes to index.html. Now, if there is an error, it is handled more gracefully and usually just a single UI element breaks. You should be able to customize the index.html file now without too many hassles.

You will also notice that the index.html has the following line: 
<script type="text/javascript" id="main-js" data-translation="blank" src="./main.js"></script>

you can specify different languages or different branding/wording translations by setting the data-translation value to that of the json filename located in the translations folder.  You can also create your own json files and specify them that way. By default I am specifying the "blank" translation.

I'm trying to steer the app code in the direction of being a bit like a library, for numerous reasons, but please reach out if you have issues with this new release. This release has numerous bug fixes and uses a new handshake server to improve the group-room experience, which I will continue to update. I will continue to work towards allowing greater accessibility in stylizing the application.
2020-06-09 12:35:40 -04:00
Steve Seguin
1db217d01c Merge pull request #152 from willl/patch-1
Fix a tiny typo
2020-05-23 08:26:59 -04:00
Steve Seguin
f416348596 Update README.md 2020-05-18 08:34:08 -04:00
Steve Seguin
86fee88382 testing webhooks 2020-05-18 08:28:57 -04:00
Steve Seguin
e39d886602 thank you 2020-05-18 04:18:38 -04:00
Steve Seguin
f9f5093238 Create FUNDING.yml 2020-05-18 04:13:46 -04:00
William Luu
609ac042a9 Fix a tiny typo
Change `unavoiably` to `unavoidably`.
2020-05-16 21:49:51 +10:00
Steve Seguin
f980555f18 Merge pull request #130 from KovalevArtem/patch-2
Update ru.json
2020-05-13 00:00:47 -04:00
Steve Seguin
d53528424e Updating the readme with 'what is obs ninja' tweaks 2020-05-12 22:59:00 -04:00
Steve Seguin
2651755647 Update README.md 2020-05-12 22:58:16 -04:00
Steve Seguin
3263cbd684 fixing my last commit. :/ 2020-05-12 05:02:21 -04:00
Steve Seguin
ac34971a50 audio devices + framerates on sceeenshare
fixing a bug with audio devices not all listing and screenshare not allowing for custom frame rates
2020-05-12 04:58:03 -04:00
KovalevArtem
e9de1b9a4c Update ru.json 2020-05-12 11:52:47 +03:00
Steve Seguin
d584a5cbdd Merge pull request #129 from steveseguin/language-russian-auto
Add files via upload
2020-05-11 21:37:44 -04:00
Steve Seguin
c5067c850b Add files via upload
Automatically applies a really bad (sorry) Russian translation to the site
2020-05-11 21:35:18 -04:00
Steve Seguin
af83c67def Delete en_ca.json 2020-05-11 21:33:33 -04:00
Steve Seguin
1631ab8d18 Delete en_uk.json 2020-05-11 21:33:26 -04:00
Steve Seguin
8da4b746be Merge pull request #128 from steveseguin/language-update
Delete en_us.json
2020-05-11 21:33:01 -04:00
Steve Seguin
e658beb41e Delete en_us.json 2020-05-11 21:31:50 -04:00
Steve Seguin
d746860c82 Delete fr_ca.json 2020-05-11 21:31:28 -04:00
Steve Seguin
735f77cec4 Update index.html
removing some debugging code
2020-05-10 22:34:32 -04:00
Steve Seguin
94cb69703b Add files via upload 2020-05-10 22:17:00 -04:00
Steve Seguin
7d81bc3b79 Add files via upload
thank you for the bug report, sky
2020-05-10 22:16:22 -04:00
Steve Seguin
5ed0d7f6dc Update README.md 2020-05-10 07:58:54 -04:00
Steve Seguin
252e73edac cleaning up readme 2020-05-10 07:51:19 -04:00
208 changed files with 168199 additions and 3967 deletions

6
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,6 @@
# These are supported funding model platforms
# Thank you <3
github: [steveseguin]
custom: ["https://www.paypal.me/steveseguin", "buymeacoffee.com/steveseguin"]

168
.github/ci-generateTranslations.js vendored Normal file
View File

@@ -0,0 +1,168 @@
var https = require("https");
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
const axios = require("axios").default;
const fs = require("fs");
const dom = new JSDOM(``, {
url: "https://obs.ninja",
contentType: "text/html",
includeNodeLocations: true,
storageQuota: 10000000,
pretendToBeVisual: true,
});
global.document = new JSDOM(``, {
url: "https://obs.ninja",
contentType: "text/html",
includeNodeLocations: true,
storageQuota: 10000000,
pretendToBeVisual: false,
}).window.document;
function downloadTranslation(filename, trans = {}) {
// downloads the current translation to a file
console.log("Downloading translation: " + filename);
const textDoc = JSON.stringify(trans, null, 2);
fs.writeFile(`translations/${filename}.json`, textDoc, (err) => {
if (err) {
return console.log(err);
}
});
return trans;
}
async function updateTranslation(filename) {
// updates the website with a specific translation
let data = await axios({
method: "get",
url: `https://raw.githubusercontent.com/steveseguin/obsninja/master/translations/${filename}.json?${(
Math.random() * 100
).toString()}`,
})
.then((response) => {
return response.data;
})
.catch((error) => {
console.log(error);
});
const oldTransItems = data["innerHTML"];
// const allItems1 = document.querySelectorAll('[data-translate]');
allItems.forEach((ele) => {
const key = ele.dataset.translate; //.replace(/[\W]+/g, "-").toLowerCase();
if (key in oldTransItems) {
ele.innerHTML = oldTransItems[key];
}
});
const oldTransTitles = data["titles"];
//const allTitles1 = document.querySelectorAll('[title]');
allTitles.forEach((ele) => {
const key = ele.dataset.key;
//const key = ele.title.replace(/[\W]+/g, "-").toLowerCase();
if (key in oldTransTitles) {
ele.title = oldTransTitles[key];
}
});
const oldTransPlaceholders = data.placeholders;
//const allPlaceholders1 = document.querySelectorAll('[placeholder]');
allPlaceholders.forEach((ele) => {
const key = ele.dataset.key;
//const key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase();
if (key in oldTransPlaceholders) {
ele.placeholder = oldTransPlaceholders[key];
}
});
return [true, data];
}
const updateList = [
"cs",
"de",
"en",
"es",
"fr",
"it",
"ja",
"nl",
"pig",
"pt",
"ru",
"tr",
"blank",
]; // list of languages to update. Update this if you add a new language.
const allItems = document.querySelectorAll("[data-translate]");
const defaultTrans = {};
allItems.forEach((ele) => {
const key = ele.dataset.translate; //.replace(/[\W]+/g, "-").toLowerCase();
defaultTrans[key] = ele.innerHTML;
});
const defaultTransTitles = {};
const allTitles = document.querySelectorAll("[title]");
allTitles.forEach((ele) => {
const key = ele.title.replace(/[\W]+/g, "-").toLowerCase();
ele.dataset.key = key;
defaultTransTitles[key] = ele.title;
});
const defaultTransPlaceholders = {};
const allPlaceholders = document.querySelectorAll("[placeholder]");
allPlaceholders.forEach((ele) => {
const key = ele.placeholder.replace(/[\W]+/g, "-").toLowerCase();
ele.dataset.key = key;
defaultTransPlaceholders[key] = ele.placeholder;
});
const combinedTrans = {};
combinedTrans.titles = defaultTransTitles;
combinedTrans.innerHTML = defaultTrans;
combinedTrans.placeholders = defaultTransPlaceholders;
var counter = 0;
for (var i = 0; i < updateList.length; i++) {
const lang = updateList[i];
var translation = updateTranslation(lang); // we don't need to worry about DATA.
updateTranslation(lang)
.then((translation) => {
const newTrans = translation[1]["innerHTML"];
//const allItems = document.querySelectorAll('[data-translate]');
allItems.forEach((ele) => {
const key = ele.dataset.translate; //.replace(/[\W]+/g, "-").toLowerCase();
newTrans[key] = ele.innerHTML;
});
const newTransTitles = translation[1]["titles"];
//const allTitles = document.querySelectorAll('[title]');
allTitles.forEach((ele) => {
const key = ele.dataset.key;
newTransTitles[key] = ele.title;
});
const newPlaceholders = translation[1]["placeholders"];
// const allPlaceholders = document.querySelectorAll('[placeholder]');
allPlaceholders.forEach((ele) => {
const key = ele.dataset.key;
newPlaceholders[key] = ele.placeholder;
});
// //// DOWNLOAD UPDATED TRANSLATION
const outputTrans = {};
outputTrans["titles"] = newTransTitles;
outputTrans["innerHTML"] = newTrans;
outputTrans["placeholders"] = newPlaceholders;
downloadTranslation(lang, outputTrans);
})
.catch((error) => {
console.log(error);
});
}

View File

@@ -0,0 +1,21 @@
name: Update Advanced Settings Markdown TOC
on:
workflow_dispatch:
schedule:
- cron: '0 0 29 2 1'
jobs:
update-toc:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
repository: ${{github.repository}}.wiki
- name: Run github-markdown-toc
run: |
curl https://raw.githubusercontent.com/ekalinin/github-markdown-toc/master/gh-md-toc -o gh-md-toc
chmod a+x gh-md-toc
./gh-md-toc --insert --no-backup Advanced-Settings.md
- uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "Auto update markdown TOC"

View File

@@ -0,0 +1,39 @@
# This is a basic workflow to help you get started with Actions
name: Update translations
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
paths:
- 'index.html'
- 'main.js'
branches:
- master
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: npm install jsdom && npm install axios
- run: node .github/ci-generateTranslations.js
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
commit-message: Generated updated translations
branch: generated_translations
title: "[VDONinja Bot] Updated translations"
labels: i18n

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
package-lock.json

View File

@@ -1,15 +1,19 @@
OBS.Ninja Contributor License Agreement (CLA)
# Contributor License Agreement (CLA)
To ensure the long-term viability of the open-source OBS.Ninja project, and for the protection of its creator and its users, we request that contributors to the project first agree to some basic terms. The terms when accepted applies to all of your past, present and future contributions.
To ensure the long-term viability of this project, and for the protection of its creator and its users, we request that contributors to the project first agree to some basic terms. The terms when accepted applies to all of your past, present and future contributions.
Contribution Policy
# Contribution Policy
Contributions that only take 20 lines of code or less will have its Intellectual Property implicitly transferred to Stephen Seguin, the creator of OBS.Ninja. You agree with this rule by pushing your code or works to github, by sending the code or works to one of OBS.Ninja's core developers, or by distributing your code or works in any other way.
For all code contributions that take more than 20 lines, you are invited to digitally sign the CLA with the provided CLA Assissant service. You may also print, sign, scan, and then email the CLA to steve@seguin.email.
You are invited to digitally sign the CLA with the provided CLA Assissant service. You may also print, sign, scan, and then email the CLA to steve@seguin.email.
It is not required that you sign the CLA for every contribution. Once you execute a CLA, it is valid until the CLA agreement is meaningfully changed and requires updating.
Important notice: if you are contributing on behalf of your company, an officer of your company (usually a VP or higher title) must sign the CLA on behalf of the company, indicating his or her title. The company can choose to list the specific individuals authorized to make contributions on the "Full Name" line, or may cover all employees with a blanket CLA by not limiting contributors to an authorized list. If necessary, the company may provide a list of authorized contributors in an attachment. The executive signing the CLA must be the first name on such an attached list, and this executive must sign the attachment as well. It may well be the case that your company already has signed a company-wide CLA with Stephen Seguin. Please check this first.
If you are contributing on behalf of your company, an officer of your company (usually a VP or higher title) must sign the CLA on behalf of the company, indicating his or her title. The company can choose to list the specific individuals authorized to make contributions on the "Full Name" line, or may cover all employees with a blanket CLA by not limiting contributors to an authorized list. If necessary, the company may provide a list of authorized contributors in an attachment. The executive signing the CLA must be the first name on such an attached list, and this executive must sign the attachment as well. It may well be the case that your company already has signed a company-wide CLA with Stephen Seguin. Please check this first.
You can stop your participation in a project at any time, but you cannot rescind your assignments or grants with respect to prior contributions. This protects the whole community, allowing Stephen Seguin and downstream users of the code base to rely on it.
You can stop your participation in a project at any time, but you cannot rescind your assignments or grants with respect to prior contributions.
You hereby grant Steve Seguin (Steve) a perpetual, worldwide, royalty-free, irrevocable, non-exclusive, and transferable license to use, reproduce, prepare derivative works of, publicly display, publicly perform, distribute the submissions, and to sublicense such rights to others. The rights granted may be exercised in any form or format, and Steve may distribute and sublicense to others on any licensing terms, including without limitation: (a) open source licenses like the GNU General Public License (GPL), or the Berkeley Software Distribution license (BSD); or (b) binary, proprietary, or commercial licenses.
You hereby represent that you are the sole and original author of all Submissions and that, to the best of your knowledge, the submissions do not infringe upon the rights of any third party.
You agree to these terms with your continued submission.

314
IFRAME.md Normal file
View File

@@ -0,0 +1,314 @@
***
This documentation has formally moved to: https://docs.vdo.ninja/guides/iframe-api-documentation
***
The documentation below is now considered depreciated and out-of-date.
## VDO.Ninja - iFrame API documentation (depreciated; see above)
VDO.Ninja (VDON) is offers here a simple and free solution to quickly enable real-time video streaming in their websites. VDON wishes to make live video streaming development accessible to any developer, even novices, yet still remain flexible and powerful.
While VDO.Ninja does offer source-code to customize the application and UI at a low level, this isn't for beginners and it is rather hard to maintain. As well, due to the complexity of video streaming in the web, typical approaches for offering API access isn't quite feasible either.
The solution decided on isn't an SDK framework, but rather the use of embeddable _IFrames_ and a corresponding bi-directional iframe API. An [iframe](https://www.w3schools.com/tags/tag_iframe.ASP) allows us to embed a webpage inside a webpage, including VDO.Ninja into your own website.
Modern web browsers allow the parent website to communicate with the child webpage, giving a high-level of control to a developer, while also abstracting the complex code and hosting requirements. Functionality, we can make an VDON video stream act much like an HTML video element tag, where you can issue commands like play, pause, or change video sources with ease.
Creating an VDON iframe can be done in HTML or programmatically with Javascript like so:
```
const iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone";
iframe.allowtransparency = "false";
iframe.src = "https://vdo.ninja/?webcam";
```
You can also make an VDO.Ninja without Javascript, using just HTML, like
`<iframe allow="autoplay;camera;microphone" src="https://vdo.ninja/?view=vhX5PYg&cleanoutput&transparent"></iframe>`
Adding that iframe to the DOM will reveal a simple page accessing for a user to select and share their webcam. For a developer wishing to access a remote guest's stream, this makes the ingestion of that stream into production software like OBS Studios very easy. The level of customization and control opens up opportunities, such as a pay-to-join audience option for a streaming interactive broadcast experience.
An example of how this API is used by VDO.Ninja is with its Internet Speedtest, which has two VDO.Ninja IFrames on a single page. One iframe feeds video to the other iframe, and the speed at which it does this is a measure of the system's performance. Detailed stats of the connection are made available to the parent window, which displays the results.
https://vdo.ninja/speedtest
More community-contributed IFRAME examples can be found here: https://github.com/steveseguin/vdoninja/tree/master/examples
A sandbox of options is available at this page, too: https://vdo.ninja/iframe You can enter an VDO.Ninja URL in the input box to start using it. For developers, viewing the source of that page will reveal examples of how all the available functions work, along with a way to test and play with each of them. You can also see here for the source-code on GitHub: https://github.com/steveseguin/vdoninja/blob/master/iframe.html
One thing to note about this iframe API is that it is a mix of URL parameters given to the iframe _src_ URL, but also the postMessage and addEventListener methods of the browser. The later is used to dynamically control VDO.Ninja, while the former is used to initiate the instance to a desired state.
For more information on the URL parameters thare are available, please see: https://github.com/steveseguin/vdoninja/wiki/Advanced-Settings
Some of the more interesting ones primarily for iframe users might include:
- &webcam
- &screenshare
- &videodevice=1 or 0
- &audiodevice=1 or 0
- &autostart
- &chroma
- &transparency
- As for API, allow for dynamic messaging, below are examples of the options available:
- Mute Speaker
- Mute Mic
- Disconnect
- Change Video Bitrate
- Reload the page
- Change the volume
- Request detailed connection stats
- Access the loudness level of the audio
- Send/Recieve a chat message to other connected guests
- Get notified when there is a video connection
As for the actually details for methods and options available to dynamically control child VDON iframe, they are primarily kept up to via the iframe.html file that is mentioned previously. see: _iframe.html_. Below is a snippet from that file:
```js
let button = document.createElement("button");
button.innerHTML = "Mute Speaker";
button.onclick = () => {
iframe.contentWindow.postMessage({
"mute": true
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Un-Mute Speaker";
button.onclick = () => {
iframe.contentWindow.postMessage({
"mute": false
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Toggle Speaker";
button.onclick = () => {
iframe.contentWindow.postMessage({
"mute": "toggle"
}, '*');
}
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Mute Mic";
button.onclick = () => {
iframe.contentWindow.postMessage({
"mic": false
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Un-Mute Mic";
button.onclick = () => {
iframe.contentWindow.postMessage({
"mic": true
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Toggle Mic";
button.onclick = () => {
iframe.contentWindow.postMessage({
"mic": "toggle"
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Disconnect";
button.onclick = () => {
iframe.contentWindow.postMessage({
"close": true
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Low Bitrate";
button.onclick = () => {
iframe.contentWindow.postMessage({
"bitrate": 30
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "High Bitrate";
button.onclick = () => {
iframe.contentWindow.postMessage({
"bitrate": 5000
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Default Bitrate";
button.onclick = () => {
iframe.contentWindow.postMessage({
"bitrate": -1
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Reload";
button.onclick = () => {
iframe.contentWindow.postMessage({
"reload": true
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "50% Volume";
button.onclick = () => {
iframe.contentWindow.postMessage({
"volume": 0.5
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "100% Volume";
button.onclick = () => {
iframe.contentWindow.postMessage({
"volume": 1.0
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Request Stats";
button.onclick = () => {
iframe.contentWindow.postMessage({
"getStats": true
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Request Loudness Levels";
button.onclick = () => {
iframe.contentWindow.postMessage({
"getLoudness": true
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Stop Sending Loudness Levels";
button.onclick = () => {
iframe.contentWindow.postMessage({
"getLoudness": false
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "Say Hello";
button.onclick = () => {
iframe.contentWindow.postMessage({
"sendChat": "Hello!"
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "previewWebcam()";
button.onclick = () => {
iframe.contentWindow.postMessage({
"function": "previewWebcam"
}, '*');
};
iframeContainer.appendChild(button);
button = document.createElement("button");
button.innerHTML = "CLOSE IFRAME";
button.onclick = () => {
iframeContainer.parentNode.removeChild(iframeContainer);
};
iframeContainer.appendChild(button);
// As for listening events, where the parent listens for responses or events from the VDON child frame:
// ////////// LISTEN FOR EVENTS
const eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
const eventer = window[eventMethod];
const messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
if (e.source !== iframe.contentWindow) {
return
} // reject messages send from other iframes
if ("stats" in e.data) {
const outputWindow = document.createElement("div");
let out = `<br />total_inbound_connections:${
e.data.stats.total_inbound_connections
}`;
out += `<br />total_outbound_connections:${
e.data.stats.total_outbound_connections
}`;
for (const streamID in e.data.stats.inbound_stats) {
out += `<br /><br /><b>streamID:</b> ${streamID}<br />`;
out += printValues(e.data.stats.inbound_stats[streamID]);
}
outputWindow.innerHTML = out;
iframeContainer.appendChild(outputWindow);
}
if ("gotChat" in e.data) {
const outputWindow = document.createElement("div");
outputWindow.innerHTML = e.data.gotChat.msg;
outputWindow.style.border = "1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("action" in e.data) {
const outputWindow = document.createElement("div");
outputWindow.innerHTML = `child-page-action: ${
e.data.action
}<br />`;
outputWindow.style.border = "1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("loudness" in e.data) {
console.log(e.data);
if (document.getElementById("loudness")) {
outputWindow = document.getElementById("loudness");
} else {
const outputWindow = document.createElement("div");
outputWindow.style.border = "1px dotted black";
iframeContainer.appendChild(outputWindow);
outputWindow.id = "loudness";
}
outputWindow.innerHTML = "child-page-action: loudness<br />";
for (const key in e.data.loudness) {
outputWindow.innerHTML += `${key} Loudness: ${
e.data.loudness[key]
}\n`;
}
outputWindow.style.border = "1px black";
}
});
```
This VDO.Ninja API is developed and expanded based on user feedback and requests. It is by no means complete.
Please feel free to follow me in the VDO.Ninja Discord channel (discord.vdo.ninja) where I post news about updates and listen to requests. The upcoming version of VDO.Ninja is also often hosted at https://vdo.ninja/beta, where you can explore new features and help crush any unexpected bugs.
-steve

View File

@@ -1,7 +1,7 @@
The OBS.Ninja source repository is governed by the GNU AFFERO GENERAL PUBLIC LICENSE. (AGPL-3.0)
That AGPL-3.0 licence can be found here: https://raw.githubusercontent.com/steveseguin/obsninja/master/AGPLv3.md
That AGPL-3.0 licence can be found here: [AGPLv3.md](https://github.com/steveseguin/obsninja/blob/master/AGPLv3.md)
In essence, OBS.Ninja is open-source and free to use, both for commericial and non-commericial use.
In essence, OBS.Ninja is open-source and free to use, both for commercial and non-commercial use.
Modifications of AGPL-3.0 licenced code must be made publicly accessible. Please refer to that licence.
Some individual source files may contain different licencing term and perhaps different copyright holders.

View File

@@ -1,60 +1,85 @@
![ILogo by brimace](images/obsNinja_logo_full.png)
## What is OBS NINJA
OBS.Ninja uses peer-to-peer technology to bring remote cameras into OBS. In most cases, all video data is transferred directly from peer to peer, without needing to go through any video server. This results in high-quality video with super low latency. In a small number of cases, video data may go through an encrypted TURN server, which is used to facilitate peer connections when otherwise not possible.
#### ⚠ Notice! We've rebranded from OBS.Ninja to VDO.Ninja - still all else the same though ✨
OBS Ninja is not affiliated with OBS, but OBS Ninja is dependent on OBS to function. OBS.Ninja is designed to allow content creators to produce real-time live shows with OBS Studio and to allow smartphones to be used as webcams on a Windows PC.
<img src="https://user-images.githubusercontent.com/2575698/124821455-bbfec580-df3c-11eb-9641-3d036cdd6c41.png" data-canonical-src="https://user-images.githubusercontent.com/2575698/124821455-bbfec580-df3c-11eb-9641-3d036cdd6c41.png" width="200" />
Please see the sub-reddit added info: https://reddit.com/r/obsninja
Also check out the FAQ for more info: https://github.com/steveseguin/obsninja/wiki
## What is **VDO NINJA**
VDO.Ninja uses peer-to-peer technology to bring remote cameras into OBS or other studio software.
## How to use:
I demo the basic usage of OBS.Ninja on YouTube: https://www.youtube.com/watch?v=6R_sQKxFAhg
In most cases, all video data is transferred directly from peer to peer, without needing to go through any video server. This results in high-quality video with super low latency. In a small number of cases, video data may go through an encrypted TURN server, which is used to facilitate peer connections when otherwise not possible.
VDO.Ninja is designed to allow content creators to produce real-time live shows using remote media streams. It can also turn smartphones into wireless webcams, with additional Virtualcam software.
VDO.Ninja is freely available to use as a managed service over at https://vdo.ninja.
For live support, please join our discord at https://discord.obs.ninja
Please see the sub-reddit added info: https://reddit.com/r/vdoninja
Also check out the FAQ for more info: https://github.com/steveseguin/vdoninja/wiki
<img src="https://user-images.githubusercontent.com/2575698/120865595-56de3b80-c55c-11eb-8b98-60c59ae0f904.png" height="300" />
## How to use
I demo the basic usage of VDO.Ninja on YouTube: https://www.youtube.com/watch?v=6R_sQKxFAhg
Here is a podcast series showing how to use different basic VDO.Ninja features, including macOS support: https://www.youtube.com/watch?v=XfSqufuoV74&list=PLWodc2tCfAH1l_LDvEyxEqFf42hOBKqQM
And Here is another video series touching on some more advanced settings: https://www.youtube.com/watch?v=mQ1Jdhf5aYg&list=PL8VJWj2-XLFpFu3G35Hdm1nKZ2xn9_0_8
Check the subreddit for added use cases, advanced features, and support. Advanced features includes high-quality audio modes, custom video resolutions, and more.
MacOS users will face some challenges in using OBS currently, but there are workarounds. Please see Reddit or the Wiki.
## What's in this repo
This repo contains software for VDO.Ninja, including the HTML landing page for its Electron Capture app offering. A sample config file and instructions for setting up a TURN server (video relay server), is also provided. You may also find [the Wiki](https://github.com/steveseguin/vdoninja/wiki) for the project in this repo, which contains added information on how to use the software.
## What's in this repo?
This repo contains the software for OBS.ninja and its TURN server deployment settings. Feel free to use it as you will; code contributions very welcomed. You may be best suited at getting ahold of me at steve@seguin.email instead of via Github if urgent though.
## How to Deploy this Repo
To use, just download and host the files on a HTTPS-enabled webserver. You may want to hide the .html extensions within your HTTP server as well, else the generated links will not work. See [here](https://github.com/steveseguin/vdoninja/blob/master/install.md) for added details and alternative install options.
## How to Deploy this Repo:
To use, just download and host on a HTTPS-enabled webserver. You may want to hide the .html extensions within your HTTP server as well, else the generated links will not work. Customize the code as desired.
Directions on how to deploy a TURN server are listed in the turnserver.md file. You may wish to do so, although not all use cases will not need one. Only about 10% of remote guests, those often connected via 4G LTE, will require a TURN server. While VDO.Ninja does host some TURN servers, they are quite expensive to operate and not really for private deployment use. If you are deploying your own version of VDO.Ninja, I'd ask you use your own TURN servers instead.
Directions on how to deploy a TURN server are listed in the turnserver.conf file. You may wish to do so if you are having problems with my existing TURN server, although most users will not need one. About 10% of users, those often connected via 4G LTE, will require a TURN server. My TURN server does cost me money to host, which I do for free, so please do not abuse it.
## Server side / API software
Since VDO.Ninja uses peer-2-peer technology, video connections are made directly between viewer and publisher in 90% of cases. Hosting a TURN server yourself may help improve performance, but less than 1% of users will see any benefit of this. Details on how to deploy a TURN server are provided. For those capable of hosting their own TURN server, that would be appreciated if possible, as TURN servers are the only real cost incurred by VDO.Ninja at present. (other than time, of course)
Why deploy OBS.Ninja? You may want to customize the UI for your brand/business, or control security/privacy beyond what I do already. I personally feel it is best to CONTRIBUTE desired changes to this repo instead, as technology moves fast and older forked versions will become obsolete.
Other than TURN servers, VDO.Ninja also uses public STUN servers and a hosted handshake server. These are used to facilitate the initial setup of peer connections and are generally not required after a peer connection is established. These servers are free to access and use, even for private deployments. As of Version 17.3 of VDO.Ninja, you can host your own handshake server or use a third-party managed one (such as piesocket.com); please see details here: https://github.com/steveseguin/websocket_server
## Server side / API software?
Other than the hosted TURN server, OBS.Ninja uses public STUN servers and a hosted handshake server. These are used to facilitate the initial setup of peer connections and are not required after a peer connection is established. Development builds of OBS.Ninja may include debugging software, but in-production releases have this removed.
Development builds of VDO.Ninja may include debugging software, but in-production releases have this removed. Double check to ensure "console.re" debugging is disabled before deployment, just to be safe.
A design goal of OBS.Ninja is to be serverless and we are 99% of the way there. This design objective ensures OBS.Ninja can be offered for free, along with providing increased levels of security and privacy.
A design goal of VDO.Ninja is to be serverless and we are 99% of the way there. This design objective ensures VDO.Ninja can be offered for free, along with providing increased levels of security and privacy.
## Issues? problems? Not working?
Please see the sub-reddit for more support: https://reddit.com/r/obsninja
Please see the sub-reddit for more support: https://reddit.com/r/vdoninja
Also check out the FAQ for common answers: https://github.com/steveseguin/obsninja/wiki
Also check out the FAQ for common answers: https://github.com/steveseguin/vdoninja/wiki
If urgent, join me on discord: https://discord.gg/EksyhGA or email me at steve@seguin.email
## Related Projects
#### OBS.Ninja's Electron Capture:
A better way to perform "Window Capturing" on desktop if OBS Browser Sources fails you. A downloadable tool designed to enhance OBS.Ninja.
### VDO.Ninja's Electron Capture:
A better way to perform "Window Capturing" on desktop if OBS Browser Sources fails you. A downloadable tool designed to enhance VDO.Ninja.
https://github.com/steveseguin/electroncapture
#### Steves.app:
A website designed to also work with OBS.Ninja as a Broadcasting tool. Share your webcam, window, desktop, or video file with friends and family. Peer-2-peer, so privacy can be maintained, but you can also list your broadcasts for others to watch.
### CAPTION.Ninja
A free AI-based closed-captioning tool to add speech-to-text overlays to OBS Studio. It's browser-based with an easy OBS or VMix integration. Developed by Steve as well! https://caption.ninja
### Chat.Overlay.Ninja
A free Chrome extension that lets you select Youtube Live Chat comments, with those comments then appearing directly in OBS or VMix as an overlay. No chroma-keying needed and the styling is pretty easy to customize without needing to modify the Chrome extension itself.
http://chat.overlay.ninja/
### Steves.app:
A website designed to also work with VDO.Ninja as a Broadcasting tool. Share your webcam, window, desktop, or video file with friends and family. Peer-2-peer, so privacy can be maintained, but you can also list your broadcasts for others to watch.
https://steves.app/
## Privacy
I try to avoid data collection whenever possible and video streams are generally designed to be private, but use at your own risk. It is best to not share links created with OBS.Ninja with those you do not trust. I've provided instructions on how to deploy a TURN server if IP-address privacy is an issue for you. See: turnserver.conf
I try to avoid data collection whenever possible and video streams are generally designed to be private, but use at your own risk. It is best to not share links created with VDO.Ninja with those you do not trust. I've provided instructions on how to deploy a TURN server if IP-address privacy is an issue for you. See: [turnserver.md](turnserver.md)
https://vdo.ninja may unavoidably use cookies that are exempt from EU laws of requiring notice of their use; they are exempt as they are required and necessary for the technical functioning of the web service. Our webserver is cached by Cloudflare and it provides denial of server protection for the users of VDO.Ninja.
Additional security features are being added weekly on request. Please ask about these options if added security and privacy are requirements for you.
## Feedback
Idea, feed back, bugs, etc -- all welcomed. I'm dumping many of my ideas as issues into Github. Feedback is typcially most welcomed via Email or Discord.
Ideas, feedback, bugs, etc -- all welcomed. I'm dumping many of my ideas as issues into Github. Feedback is typically most welcomed via Email or Discord.
## Licence
OBS.Ninja is available as open-source; please see the LICENCE.md file for details.
VDO.Ninja is available as 'mostly' open-source; please see the LICENCE.md file for details.
## Credit
Thank you to everyone who has helped support this project so far. From the moderators, volunteers helping with support, those contributing media assets, the project sponsors, those reporting issues, those offering feedback, and any code submissions.

View File

@@ -1,115 +0,0 @@
/* We need to create dynamic keyframes to show the animation from full-screen to normal. So we create a stylesheet in which we can insert CSS keyframe rules */
$("body").append('<style id="lightbox-animations" type="text/css"></style>');
/* Click on the container */
$(".column").on('click', function() {
/* The position of the container will be set to fixed, so set the top & left properties of the container */
var bounding_box = $(this).get(0).getBoundingClientRect();
$(this).css({ top: bounding_box.top + 'px', left: bounding_box.left -20+ 'px' });
/* Set container to fixed position. Add animation */
$(this).addClass('in-animation');
/* An empty container has to be added in place of the lightbox container so that the elements below don't come up
Dimensions of this empty container is the same as the original container */
$("#empty-container").remove();
$('<div id="empty-container" class="column"></div>').insertAfter(this);
/* To animate the container from full-screen to normal, we need dynamic keyframes */
var styles = '';
styles = '@keyframes outlightbox {';
styles += '0% {';
styles += 'height: 100%;';
styles += 'width: 100%;';
styles += 'top: 0px;';
styles += 'left: 0px;';
styles += '}';
styles += '50% {';
styles += 'height: 220px;';
styles += 'top: ' + bounding_box.y + 'px;';
styles += '}';
styles += '100% {';
styles += 'height: 220px;';
styles += 'width: '+bounding_box.width+'px;';
styles += 'top: ' + bounding_box.y + 'px;';
styles += 'left: ' + bounding_box.x + 'px;';
styles += '}';
styles += '}';
/* Add keyframe to CSS */
$("#lightbox-animations").get(0).sheet.insertRule(styles, 0);
/* Hide the window scrollbar */
$("body").css('overflow', 'hidden');
});
/* Click on close button when full-screen */
$(".close").on('click', function(e) {
$(this).hide();
$(".container-inner").hide();
/* Window scrollbar normal */
$("body").css('overflow', 'auto');
var bounding_box = $(this).parent().get(0).getBoundingClientRect();
$(this).parent().css({ top: bounding_box.top + 'px', left: bounding_box.left + 'px' });
/* Show animation */
$(this).parent().addClass('out-animation');
e.stopPropagation();
});
/* On animationend : from normal to full screen & full screen to normal */
$(".column").on('animationend', function(e) {
/* On animation end from normal to full-screen */
if(e.originalEvent.animationName == 'inlightbox') {
$(this).children(".close").show();
$(this).children(".container-inner").show();
}
/* On animation end from full-screen to normal */
else if(e.originalEvent.animationName == 'outlightbox') {
/* Remove fixed positioning, remove animation rules */
$(this).removeClass('in-animation').removeClass('out-animation').removeClass('columnfade');
/* Remove the empty container that was earlier added */
$("#empty-container").remove();
/* Delete the dynamic keyframe rule that was earlier created */
$("#lightbox-animations").get(0).sheet.deleteRule(0);
}
});
// multiselect dropdowns
$('.multiselect-trigger').on('mousedown touchend focusin focusout', function(e) {
var state = $(this).data('state') || 0;
if( state == 0 ) {
// open the dropdown
$(this).data('state', '1').addClass('open').removeClass('closed');
$(this).find('.fa').removeClass('fa-chevron-down').addClass('fa-chevron-up');
$(this).parent().find('.multiselect-contents').show();
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').parent().show();;
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').show();;
} else {
// close the dropdown
$(this).data('state', '0').addClass('closed').removeClass('open');
$(this).find('.fa').removeClass('fa-chevron-up').addClass('fa-chevron-down');
//$(this).parent().find('.multiselect-contents').hide();
//$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').hide();
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').not(":checked").parent().hide();;
$(this).parent().find('.multiselect-contents').find('input[type="checkbox"]').hide();;
}
e.preventDefault();
});
// when no preference is checked, uncheck the others
$('#multiselect1').on('change', function(e) {
if( $(this).is(':checked') ) {
$(this).parent().parent().find('input[type="checkbox"]').not('#multiselect1').prop('checked', false);
}
e.preventDefault();
});

235
convert.html Normal file
View File

@@ -0,0 +1,235 @@
<head>
<link rel="stylesheet" href="./main.css?ver=40" />
<style>
.container {
max-width: 80%;
width: fit-content;
margin: 0 auto;
}
h1 {
margin-top: 1em;
margin-bottom: 1em;
padding: 10px;
}
.card {
margin: 10px;
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 10%);
background-color: #ddd;
color: black;
margin-bottom: 1.5em;
}
.card>div {
padding: 10px;
}
.card h2 {
font-size: 1.5em;
padding: 10px;
background-color: #457b9d;
color: white;
border-bottom: 2px solid #3b6a87;
}
small {
font-style: italic;
display: block;
margin-left: 1em;
}
span.warning {
color: rgb(212, 191, 0);
}
input {
padding: 10px;
}
video {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
audio {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
div#processing {
display: none;
justify-content: center;
place-items: center;
position: absolute;
inset: 0;
font-size: 1.5em;
font-weight: bold;
background: #141926;
flex-direction: column;
}
</style>
</head>
<body style='color:white'>
<div id="header">
<a id="logoname" href="./" style="text-decoration: none; color: white; margin: 2px">
<span data-translate="logo-header">
<font id="qos">V</font>DO.Ninja
</span>
</a>
</div>
<div class="container">
<div id="info">
<h1>Web-based Media Conversion Tools</h1>
<div class="card">
<h2>WebM to MP4 (fixed 1280x720 resolution) <span class='warning'>(very slow!)</span></h2>
<div>
<small>The same as: fmpeg -i input.webm -vf scale=1280:720 output.mp4</small>
<input type="file" id="uploader" title="Convert WebM to MP4">
</div>
</div>
<div class="card">
<h2>WebM to MP4 files (no transcoding, attempts to force high resolutions)</h2>
<div>
<input type="file" id="uploader3" accept=".webm" title="Convert WebM to MP4">
</div>
</div>
<div class="card">
<h2>WebM to Audio-only files (opus or wav)</h2>
<div>
<input type="file" id="uploader4" accept=".webm" title="Convert WebM to OPUS">
</div>
</div>
<div class="card">
<h2>MKV to MP4 (no transcoding)</h2>
<div>
<small>The same as: fmpeg -i INPUTFILE -vcodec copy -acodec copy output.mp4</small>
<input type="file" id="uploader2" accept=".mkv" title="Convert MKV to MP4">
</div>
</div>
<div id="processing">
<span id="message"></span>
<video id="player" controls style="display:none"></video>
<audio id="player2" controls style="display:none"></audio>
</div>
</div>
<script>
if (window.location.hostname.indexOf("vdo.ninja") == 0) {
window.location = window.location.href.replace("vdo.ninja","isolated.vdo.ninja"); // FFMPEG requires an isolated domain.
}
</script>
<script src="./thirdparty/ffmpeg.min.js"></script>
<script>
function download(data, filename) {
const blob = new Blob([data.buffer]);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}, 100);
}
const { createFFmpeg, fetchFile } = FFmpeg;
const ffmpeg = createFFmpeg({ log: true });
const transcode = async ({ target: { files } }) => {
const { name } = files[0];
document.getElementById('uploader').style.display = "none";
document.getElementById('uploader2').style.display = "none";
document.getElementById('uploader3').style.display = "none";
document.getElementById('message').innerText = "Transcoding file... this will take a while";
document.getElementById('processing').style.display = 'flex';
await ffmpeg.load();
ffmpeg.FS('writeFile', name, await fetchFile(files[0]));
await ffmpeg.run('-i', name, '-vf', 'scale=1280:720', 'output.mp4');
const data = ffmpeg.FS('readFile', 'output.mp4');
const video = document.getElementById('player');
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
video.style.display = "block";
document.getElementById('message').innerText = "Operation Done. Play video or download it.";
}
const transmux = async ({ target: { files } }) => {
const { name } = files[0];
document.getElementById('uploader').style.display = "none";
document.getElementById('uploader2').style.display = "none";
document.getElementById('uploader3').style.display = "none";
document.getElementById('message').innerText = "Transcoding file... this will take a while";
document.getElementById('processing').style.display = 'flex';
await ffmpeg.load();
ffmpeg.FS('writeFile', name, await fetchFile(files[0]));
await ffmpeg.run('-i', name, '-vcodec', 'copy', '-acodec', 'copy', 'output.mp4');
const data = ffmpeg.FS('readFile', 'output.mp4');
const video = document.getElementById('player');
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
video.style.display = "block";
document.getElementById('message').innerText = "Operation Done. Play video or download it.";
}
const force1080 = async ({ target: { files } }) => {
const { name } = files[0];
const sourceBuffer = await fetch("./media/cap.webm").then(r => r.arrayBuffer());
document.getElementById('uploader').style.display = "none";
document.getElementById('uploader2').style.display = "none";
document.getElementById('uploader3').style.display = "none";
document.getElementById('message').innerText = "Tweaking file... this will take a moment";
document.getElementById('processing').style.display = 'flex';
await ffmpeg.load();
ffmpeg.FS('writeFile', name, await fetchFile(files[0]));
ffmpeg.FS("writeFile", "cap.webm", new Uint8Array(sourceBuffer, 0, sourceBuffer.byteLength));
await ffmpeg.run("-i", "concat:cap.webm|" + name, "-safe", "0", "-c", "copy", "-avoid_negative_ts", "1", "-strict", "experimental", "output.mp4");
const data = ffmpeg.FS('readFile', 'output.mp4');
const video = document.getElementById('player');
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
video.style.display = "block";
document.getElementById('message').innerText = "Operation Done. Play video or download it.";
document.getElementById('processing').style.display = 'flex';
}
const convertToAudioOnly = async ({ target: { files } }) => {
const { name } = files[0];
document.getElementById('message').innerText = "Transcoding file... this will take a while";
document.getElementById('processing').style.display = 'flex';
await ffmpeg.load();
ffmpeg.FS('writeFile', name, await fetchFile(files[0]));
const video = document.getElementById('player');
await ffmpeg.run('-i', name, '-vn', '-acodec', 'copy', 'output.opus');
const data = ffmpeg.FS('readFile', 'output.opus');
console.log(data.buffer.byteLength);
if (data.buffer.byteLength) {
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'audio/opus' }));
download(data, name.split(".")[0] + ".opus");
} else {
await ffmpeg.run('-i', name, '-vn', '-acodec', 'copy', 'output.wav');
const data2 = ffmpeg.FS('readFile', 'output.wav');
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'audio/pcm' }));
download(data2, name.split(".")[0] + ".wav");
}
video.style.display = "block";
document.getElementById('message').innerText = "Operation Done. Play audio or download it.";
document.getElementById('processing').style.display = 'flex';
}
document.getElementById('uploader').addEventListener('change', transcode);
document.getElementById('uploader2').addEventListener('change', transmux);
document.getElementById('uploader3').addEventListener('change', force1080);
document.getElementById('uploader4').addEventListener('change', convertToAudioOnly);
</script>
</body>

127
devices.css Normal file
View File

@@ -0,0 +1,127 @@
#devices {
max-width: 80%;
width: fit-content;
margin: 0 auto;
}
h1 {
font-size: 1.5em;
padding:10px;
background-color:#457b9d;
color:white;
border-bottom: 2px solid #3b6a87;
}
.device {
display: flex;
flex-direction: column;
margin: 10px 0px;
font-size: 1rem;
padding: 10px;
position: relative;
user-select: none;
background: #d0d0d0;
border-radius: 4px;
}
.device.selected {
background-color: #3ea03c;
}
.device.selected::before {
content: "\f00c";
font-family: "Line Awesome Free";
font-weight: 900;
position: absolute;
top: 10px;
right: 10px;
}
.device:hover {
cursor: pointer;
}
.device-name{
font-weight: bold;
margin-bottom: 5px;
}
.device-id {
}
.card {
margin: 10px;
}
.card > div {
padding: 10px;
}
.notice {
background-color: #fff18c;
margin: 10px;
padding: 20px 20px;
font-weight: bold;
font-size: 1.2em;
text-align: center;
line-height: 1.4em;
}
.notice a {
color: #457b9d;
}
@media only screen
and (min-device-width: 375px)
and (max-device-width: 812px)
and (orientation: portrait) {
#devices {
width: 100%;
max-width: 100%;
}
.device-id {
text-overflow: ellipsis;
overflow: hidden;
}
}
#sharedDevices {
position: fixed;
bottom: 20px;
width: 80%;
left: 10%;
color: white;
overflow-wrap: anywhere;
background: #2c3754;
padding: 20px;
box-shadow: 0px 0px 10px 5px #00000047;
border: 1px solid #333c52;
}
#sharedDevices span {
display: block;
margin-bottom: 10px;
}
#sharedDevices input {
width: 100%;
padding: 5px;
}
span#close {
position: absolute;
top: -10px;
right: -10px;
display: block;
width: 20px;
height: 20px;
background: #457b9d;
text-align: center;
border-radius: 20px;
line-height: 20px;
font-size: 20px;
cursor: pointer;
}

227
devices.html Normal file
View File

@@ -0,0 +1,227 @@
<html>
<head>
<link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" />
<link rel="stylesheet" href="./main.css?ver=11" />
<link rel="stylesheet" href="./devices.css?ver=1" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="utf8" />
</head>
<body>
<div id="header">
<a
id="logoname"
href="./"
style="text-decoration: none; color: white; margin: 2px"
>
<span data-translate="logo-header">
<font id="qos">O</font>BS.Ninja
</span>
</a>
</div>
<div id="devices">
<div class="notice">
Device IDs are bound to a combination of domain and browser. <br />If
you want to use electron-capture, open this URL on the electron-capture
app. <br/>
Click device names to preset them. Select multiple audio inputs by clicking multiple devices.
</div>
<div class="notice">
Check for browser and camera capabilities <a href="/supports">here</a>.
</div>
<div class="card">
<h1>🎤 Audio Inputs</h1>
<div id="audioInputs"></div>
</div>
<div class="card">
<h1>📹 Video Inputs</h1>
<div id="videoInputs"></div>
</div>
<div class="card">
<h1>🔉 Audio Outputs</h1>
<div id="audioOutputs"></div>
</div>
</div>
<div id="sharedDevices" style="display: none">
<span>Click to copy. Use this URL to preset audio/video devices.</span>
<span id="close" onclick="this.parentNode.style.display='none'">×</span>
<input id="devicesUrl" value="" />
</div>
<script>
const list = [];
const url = new URL(document.location.origin);
const audioInputDevices = [];
function isAudioInput(value) {
return value.kind === "audioinput";
}
function isAudioOutput(value) {
return value.kind === "audiooutput";
}
function isVideoInput(value) {
return value.kind === "videoinput";
}
function sanitizeDeviceName(deviceName) {
return String(deviceName).toLowerCase().replace(/[\W]+/g, "_");
}
function addDevice(element) {
const type = element.dataset.deviceType;
const device = sanitizeDeviceName(element.querySelector('span').innerText);
if (type === "audioinput") {
setAudioSearchParams(element);
}
if (type === "videoinput") {
setVideoSearchParams(element);
}
if (type === "audiooutput") {
setAudioOutputSearchParams(element);
}
/*
Allows for multiple audio devices to be selected
Will be output as a comma separated string to &ad
*/
function setAudioSearchParams(info) {
// Device was already selected
if (info.className === "device selected") {
// Remove device from list of selected devices
const index = audioInputDevices.indexOf(device);
if (index !== -1) {
audioInputDevices.splice(index, 1);
}
// Set the url param to the devices that are left
url.searchParams.set("ad", audioInputDevices.join(","));
element.className = "device";
// If no audio devices remained, just remove the param completely
if (audioInputDevices.length === 0) {
url.searchParams.delete("ad");
}
} else {
// Device is unselected
audioInputDevices.push(device);
url.searchParams.set("ad", audioInputDevices.join(","));
element.className = "device selected";
}
}
/*
Only allows for a single video device to be selected
*/
function setVideoSearchParams(info) {
// Device was already selected
if (info.className === "device selected") {
element.className = "device";
// Set the url param to the devices that are left
url.searchParams.set("vd", device);
element.className = "device";
// If no devices remained, just remove the param completely
if (audioInputDevices.length === 0) {
url.searchParams.delete("vd");
}
} else {
// Device is unselected
try {
element.parentElement.querySelector('.device.selected').className = "device";
} catch (error) {
console.log("There was no video device already selected.");
}
url.searchParams.set("vd", device);
element.className = "device selected";
}
}
/*
Only allows for a single audio output device to be selected
*/
function setAudioOutputSearchParams(info) {
// Device was already selected
if (info.className === "device selected") {
element.className = "device";
// Set the url param to the devices that are left
url.searchParams.set("od", device);
element.className = "device";
// If no devices remained, just remove the param completely
if (audioInputDevices.length === 0) {
url.searchParams.delete("od");
}
} else {
// Device is unselected
try {
element.parentElement.querySelector('.device.selected').className = "device";
} catch (error) {
console.log("There was no video device already selected.");
}
url.searchParams.set("od", device);
element.className = "device selected";
}
}
// Update UI
showDeviceIdsPopup();
}
function showDeviceIdsPopup() {
document.getElementById("devicesUrl").value = decodeURIComponent(url);
document.getElementById("sharedDevices").style.display = "block";
}
function prettyPrint(json, element) {
let output = "<div class='prettyJson two-col'>";
let nestedObjs;
Object.entries(json)
.sort()
.forEach(([key, value]) => {
output += `
<div class='device' onclick='addDevice(this)' data-device-type='${value.kind}'>
<span class='device-name'>${value.label}</span>
<span class='device-id'>
${value.deviceId}
</span>
</div>`;
});
output += "</div>";
document.getElementById(element).innerHTML = output;
}
document.getElementById("devicesUrl").onclick = (e) => {
e.target.select();
document.execCommand("copy");
};
navigator.mediaDevices
.enumerateDevices()
.then((devices) => {
devices.forEach((device) => {
console.log(
`${device.kind}: ${device.label} id = ${device.deviceId}`
);
list.push(device);
});
prettyPrint(devices.filter(isAudioInput), "audioInputs");
prettyPrint(devices.filter(isAudioOutput), "audioOutputs");
prettyPrint(devices.filter(isVideoInput), "videoInputs");
})
.catch((err) => {
console.log(`${err.name}: ${err.message}`);
});
</script>
</body>
</html>

24
devices.json.html Normal file
View File

@@ -0,0 +1,24 @@
<html>
<head><meta charset="UTF-8"></head>
<body>
<script>
var list = [];
navigator.mediaDevices.enumerateDevices()
.then(function(devices) {
devices.forEach(function(device) {
console.log(device.kind + ": " + device.label +
" id = " + device.deviceId);
list.push(device);
});
document.write(JSON.stringify(list, null, 2));
})
.catch(function(err) {
console.log(err.name + ": " + err.message);
});
</script>
</body>
</html>

298
dock.html Normal file
View File

@@ -0,0 +1,298 @@
<html>
<head>
<style>
body {
transform: scale(0.7);
transform-origin: 0 0;
margin:2px;
padding:0;
border:0;
color: #FFF;
background-color: #1F1E1F;
font-family: Arial, Helvetica, sans-serif;
width:300px;
overflow:hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width:1px;
}
#container-links {
z-index:10;
width:100%;
height:100%;
display:none;
}
#container-setup {
width:100%;
height:100%;
display:block;
}
.red {
background-color:#FCC;
}
.green {
background-color:#CFC;
}
.task {
cursor:grab;
width:100%;
padding:5px;
border:2px solid black;
margin:0;
}
button{
padding:5px;
transform: scale(1.4);
transform-origin: 0 0;
}
.gone {
position: absolute;
display:inline-block;
left: -9999px;
}
</style>
</head>
<body>
<script>
function getById(id) {
var el = document.getElementById(id);
if (!el) {
console.warn(id + " is not defined; skipping.");
el = document.createElement("span"); // create a fake element
}
return el;
}
function copyFunction(copyText) {
copyText.select();
copyText.setSelectionRange(0, 99999);
document.execCommand("copy");
}
function generateStreamID(){
var text = "";
var possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
for (var i = 0; i < 7; i++){
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
console.log(text);
return text;
};
function toHexString(byteArray){
return Array.prototype.map.call(byteArray, function(byte){
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('');
}
generateHash = function (str, length=false){
var buffer = new TextEncoder("utf-8").encode(str);
return crypto.subtle.digest("SHA-256", buffer).then(
function (hash) {
hash = new Uint8Array(hash);
if (length){
hash = hash.slice(0, parseInt(parseInt(length)/2));
}
hash = toHexString(hash);
return hash;
}
);
};
function generateInvite(){
var title = encodeURI(getById("videoname").value.replace(/[\W]+/g,"_"));
if (title.length){
title = "&label="+title;
}
var sid = generateStreamID();
var viewstr = "";
var sendstr = "";
if (getById("invite_bitrate").checked){
viewstr+="&bitrate=20000";
}
if (getById("invite_vp9").checked){
viewstr+="&codec=vp9";
}
if (getById("invite_h264").checked){
viewstr+="&codec=h264";
}
if (getById("invite_stereo").checked){
viewstr+="&stereo";
sendstr+="&stereo";
}
//if (getById("invite_secure").checked){
// sendstr+="&secure";
//}
if (getById("invite_hidescreen").checked){
sendstr+="&webcam";
}
//if (getById("invite_remotecontrol").checked){ //
// var remote_gen_id = generateStreamID();
// sendstr+="&remote="+remote_gen_id; // security
// viewstr+="&remote="+remote_gen_id;
//}
if (getById("invite_joinroom").value.trim().length){
sendstr+="&room="+getById("invite_joinroom").value.replace(/[\W]+/g,"_");
viewstr+="&scene&room="+getById("invite_joinroom").value.replace(/[\W]+/g,"_");
}
if (getById("invite_group_chat_type").value){ // 0 is default
if (getById("invite_group_chat_type").value==1){ // no video
sendstr+="&novideo";
} else if (getById("invite_group_chat_type").value==2){ // no view or audio
sendstr+="&view";
}
}
if (getById("invite_quality").value){
if (getById("invite_quality").value==0){
sendstr+="&quality=0";
} else if (getById("invite_quality").value==1){
sendstr+="&quality=1";
} else if (getById("invite_quality").value==2){
sendstr+="&quality=2";
}
}
var href = window.location.href;
var dir = href.substring(0, href.lastIndexOf('/')) + "/";
var salt = location.hostname; // "obs.ninja" is the expected default. You will want to change this if hosting dock.html locally.
if (getById("invite_password").value.trim().length){
generateHash(getById("invite_password").value.trim().replace(/[\W]+/g,"_")+salt,4).then(function(hash){
sendstr+="&hash="+hash;
viewstr+="&password="+getById("invite_password").value.trim();
sendstr = dir+'?push=' + sid + sendstr;
viewstr = dir+'?view=' + sid + viewstr + title;
getById("container-setup").style.display="none";
getById("container-links").style.display="block";
getById("guest-link").value = sendstr;
getById("obs-link").value = viewstr;
});
} else {
sendstr = dir+'?push=' + sid + sendstr;
viewstr = dir+'?view=' + sid + viewstr + title;
getById("container-setup").style.display="none";
getById("container-links").style.display="block";
getById("guest-link").value = sendstr;
getById("obs-link").value = viewstr;
}
}
function goBack(){
getById("container-setup").style.display="block";
getById("container-links").style.display="none";
}
document.addEventListener("dragstart", event => {
var url = event.target.href || event.target.value;
if (!url || !url.startsWith('https://')) return;
if (event.target.dataset.drag!="1"){
return;
}
//event.target.ondragend = function(){event.target.blur();}
var streamId = url.split('view=');
var label = url.split('label=');
url += '&layer-name=OBSN';
if (streamId.length>1) url += ': ' + streamId[1].split('&')[0];
if (label.length>1) url += ' - ' + decodeURI(label[1].split('&')[0]);
url += '&layer-width=1920'; // this isn't always 100% correct, as the resolution can fluxuate, but it is probably good enough
url += '&layer-height=1080';
event.dataTransfer.setDragImage(document.querySelector('#dragImage'), 24, 24);
event.dataTransfer.setData("text/uri-list", encodeURI(url));
//event.dataTransfer.setData("url", encodeURI(url));
//warnlog(event);
});
</script>
<div id="container-setup" >
<button style="padding:5px;" onclick="generateInvite()" >
<span data-translate="generate-invite-link">GENERATE THE INVITE LINK</span>
</button>
<br /><br />
<input type="checkbox" id="invite_bitrate" /><label for="invite_bitrate"> <span data-translate="unlock-video-bitrate">Unlock Video Bitrate (20mbps)</span></label>
<br />
<input type="checkbox" id="invite_vp9" onclick="getById('invite_h264').checked=false;" /><label for="invite_vp9"> <span data-translate="force-vp9-video-codec">VP9 Video Codec</span></label>
<br />
<input type="checkbox" id="invite_h264" onclick="getById('invite_vp9').checked=false;" /><label for="invite_h264"> <span data-translate="force-h264-video-codec">H264 Video Codec</span></label>
<br />
<input type="checkbox" id="invite_stereo" /><label for="invite_stereo"> <span data-translate="enable-stereo-and-pro">Stereo and Pro HD Audio</span></label>
<br />
<br />
<label for="invite_quality" data-translate="video-resolution">Video Resolution: </label>
<select id="invite_quality" name="invite_quality">
<option value="-1" selected>User Selectable</option>
<option value="0">Maximum Resolution</option>
<option value="1">Balanced</option>
<option value="2">Smooth and Cool</option>
</select>
<br />
<br />
<input type="checkbox" id="invite_hidescreen" />
<label for="invite_hidescreen"> <span data-translate="hide-screen-share">Hide Screenshare Option</span></label>
<br />
<br />
<label for="videoname">Stream Label:</label>
<input id="videoname" placeholder="Give stream a description" />
<br />
<br />
<span data-translate="add-a-password-to-stream"> Add password:</span>
<input id="invite_password" placeholder="Add an optional password" />
<br /><br />
<span data-translate="add-the-guest-to-a-room"> Add to a room:</span>
<input id="invite_joinroom" placeholder="Enter Room name here" oninput="document.getElementById('invitegroupchat').style.display='block';" />
<br />
<br />
<span id="invitegroupchat" style="display:none;">
<label for="invite_group_chat_type" data-translate="invite-group-chat-type">This room guest can:</label><br />
<select id="invite_group_chat_type" name="invite_group_chat_type">
<option value="0" selected data-translate="can-see-and-hear">Can see and hear the group chat</option>
<option value="1" data-translate="can-hear-only">Can only hear the group chat</option>
<option value="2" data-translate="cant-see-or-hear">Cannot hear or see the group chat</option>
</select>
</span>
</div>
<div id="container-links" >
<button onclick="goBack()" >
<span >Go Back</span>
</button>
<div id="container-links-inner" >
<br /><br />
<h3>Guest Invite Link:</h3>
<input id="guest-link" class="task green" onclick="copyFunction(this)" onmousedown="copyFunction(this)"/>
<br /><br />
<h3>OBS Browser Source Link:</h3>
<input id="obs-link" class="task red" data-drag="1" onmousedown="copyFunction(this)" onclick="copyFunction(this)" />
<br />
<br />
<i>(links are draggable)</i>
</div>
</div>
<div class="gone" >
<!-- This image is used when dragging elements -->
<img src="./media/favicon-32x32.png" id="dragImage" />
</div>
</body>
</html>

View File

@@ -1,299 +1,585 @@
<html>
<head><style>
html {
border:0;
margin:0;
}
video {
margin: 0;
padding: 0;
overflow: hidden;
cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=), none;
user-select: none;
}
body {
padding: 0 0px;
height: 100%;
width: 100%;
background-color: #141926;
background-color: -webkit-linear-gradient(to top, #181925, #141826, #0F2027); /* Chrome 10-25, Safari 5.1-6 */
background-color: linear-gradient(to top, #181825, #141926, #0F2027); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
font-size: 2em;
font-family: Helvetica, Arial, sans-serif;
display: flex;
flex-flow: column;
border:0;
margin:0;
}
button{
padding:10px;
font-size: 20px;
margin: auto auto;
}
#header{
height:80px;
width:100%;
background-color: #101520;
}
.inputfield{
font-size: 20px;
align-self:center;
height:30px;
width:780px;
margin: auto auto;
padding:20px
}
.formcss{
font-size: 20px;
align-self:center;
margin: auto auto;
}
label {
font: white;
font-size: 1em;
color: white;
}
input[type='checkbox'] {
-webkit-appearance:none;
width:30px;
height:30px;
background:white;
border-radius:5px;
border:2px solid #555;
cursor: pointer;
}
input[type='checkbox']:checked {
background: #1A1;
}
</style></head>
<body >
<div id="header" style="-webkit-app-region: drag;color:white;font-size:2em">OBS.Ninja</div>
<div class="formcss" >
<input type="checkbox" class="check" id="prefervp9" name="prefervp9" value="false" onclick="modURL(this);">
<label for="prefervp9">Force VP9 Codec</label>
<input type="checkbox" class="check" id="showcursor" name="showcursor" value="false" onclick="modURL(this);">
<label for="showcursor">Show Mouse Cursor</label>
<input type="checkbox" class="check" id="highbitrate" name="highbitrate" value="false" onclick="modURL(this);">
<label for="highbitrate">High Video Bitrate</label>
<input type="checkbox" class="check" id="stereo" name="stereo" value="false" onclick="modURL(this);">
<label for="stereo">Pro Audio Mode</label><br><br><br>
<div class="formcss">
<input type="text" id="changeText" class="inputfield" value="http://obs.ninja/?view=" onchange="modURL" onkeyup="enterPressed(event, gohere);" />
<button onclick="gohere();" id="gobutton">GO</button>
<br><br>
<label for="audioOutput">Audio output destination: </label><select id="audioOutput" style="max-width:400px"></select>
</div>
</div>
<script>
/*
* Copyright (c) 2020 Steve Seguin. All Rights Reserved.
*
* Use of this source code is governed by the APGLv3 open-source license
* that can be found in the LICENSE file in the root of the source
* tree. Alternative licencing options can be made available on request.
*
*/
var audioOutputSelect = document.querySelector('select#audioOutput');
audioOutputSelect.disabled = !('sinkId' in HTMLMediaElement.prototype);
audioOutputSelect.onclick = getPermssions;
audioOutputSelect.onchange = updateOutputTarget;
var listed = false;
function updateOutputTarget(e){
console.log("change audio: "+audioOutputSelect.value);
var url = document.getElementById('changeText').value;
url=updateURLParameter(url, "sink", audioOutputSelect.value);
document.getElementById('changeText').value = url;
}
function getPermssions(e){
if (listed==true){
return;
}
e.currentTarget.blur();
navigator.mediaDevices.getUserMedia({audio: true,video: false}).then(function(stream){
navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(console.error); // list all devices
stream.getTracks().forEach(track => {
track.stop();
});
listed=true;
audioOutputSelect.focus();
}).catch(function(){alert("Failed to list available output devices\n\nPlease ensure you allowed the microphone permissions.");});
}
function gotDevices(deviceInfos) {
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i];
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
if (deviceInfo.kind === 'audiooutput'){
option.text = deviceInfo.label || `speaker ${audioOutputSelect.length + 1}`;
audioOutputSelect.appendChild(option);
} else {
console.log('Some other kind of source/device: ', deviceInfo);
}
}
listed=true;
}
function enterPressed(event, callback){
if (event.keyCode === 13){ // Number 13 is the "Enter" key on the keyboard
event.preventDefault(); // Cancel the default action, if needed
callback();
}
}
(function (w) {
w.URLSearchParams = w.URLSearchParams || function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
}
else {
return decodeURI(results[1]) || 0;
}
};
}
})(window)
var urlParams = new URLSearchParams(window.location.search);
var isMobile = false;
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)){ // does not detect iPad Pros.
isMobile=true; // if iOS, default to H264? meh. let's not.
}
function updateURLParameter(url, param, paramVal){
var TheAnchor = null;
var newAdditionalURL = "";
var tempArray = url.split("?");
var baseURL = tempArray[0];
var additionalURL = tempArray[1];
var temp = "";
if (additionalURL){
var tmpAnchor = additionalURL.split("#");
var TheParams = tmpAnchor[0];
TheAnchor = tmpAnchor[1];
if (TheAnchor){additionalURL = TheParams;}
tempArray = additionalURL.split("&");
for (var i=0; i<tempArray.length; i++){
if(tempArray[i].split('=')[0] != param){
newAdditionalURL += temp + tempArray[i];
temp = "&";
}
}
} else {
var tmpAnchor = baseURL.split("#");
var TheParams = tmpAnchor[0];
TheAnchor = tmpAnchor[1];
if(TheParams){baseURL = TheParams;}
}
if (paramVal==false){
temp="";
if(TheAnchor){temp += "#" + TheAnchor;}
var rows_txt = temp
} else {
if(TheAnchor){paramVal += "#" + TheAnchor;}
var rows_txt = temp + "" + param + "=" + paramVal;
}
return baseURL + "?" + newAdditionalURL + rows_txt;
}
if (urlParams.has('name')){
var name = urlParams.get('name');
if (name!="OBSNinja"){
document.getElementById('changeText').value = "https://obs.ninja/?view="+name;
}
}
function modURL(ele=false){
var url = document.getElementById('changeText').value;
console.log(url);
if ((url.split("view").length>0) || (url.split("room").length>0)){
if (!document.getElementById("showcursor").checked){
url=updateURLParameter(url, "nocursor", "1");
} else {
url=updateURLParameter(url, "nocursor", false);
}
if (ele!=false){
if (ele.id =="prefervp9"){
if (document.getElementById("prefervp9").checked){
url=updateURLParameter(url, "codec", "vp9");
} else {
url=updateURLParameter(url, "codec", false);
}
}
if (ele.id =="highbitrate"){
if (document.getElementById("highbitrate").checked){
url=updateURLParameter(url, "bitrate", "10000");
} else {
url=updateURLParameter(url, "bitrate", false);
}
}
if (ele.id =="stereo"){
if (document.getElementById("stereo").checked){
url=updateURLParameter(url, "stereo", "1");
alert('Audio bitrate increased to 256kbps.\n\nPlease note: the Video Publisher must also have the stereo flag enabled for stereo to work.');
} else {
url=updateURLParameter(url, "stereo", false);
}
}
}
}
document.getElementById('changeText').value = url;
console.log(url);
return url;
}
function gohere(){
var url = modURL(true);
window.location = url;
};
</script>
</body>
</html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" />
<style>
html {
border:0;
margin:0;
outline:0;
overflow: hidden;
}
video {
margin: 0;
padding: 0;
overflow: hidden;
cursor: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=), none;
user-select: none;
}
body {
padding: 0 0px;
height: 100%;
width: 100%;
background-color: -webkit-linear-gradient(to top, #363644, 50%, #151b29); /* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(to top, #363644, 50%, #151b29); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
font-size: 2em;
font-family: Helvetica, Arial, sans-serif;
display: flex;
flex-flow: column;
border:0;
margin:0;
outline:0;
}
button.glyphicon-button:focus,
button.glyphicon-button:active:focus,
button.glyphicon-button.active:focus,
button.glyphicon-button.focus,
button.glyphicon-button:active.focus,
button.glyphicon-button.active.focus {
outline: none !important;
}
#gobutton {
font-size: 20px;
font-weight: bold;
border: none;
background: #6aab23;
display: flex;
border-radius: 0px;
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
box-shadow: 0 12px 15px -10px #5ca70b, 0 2px 0px #6aab23;
color: white;
cursor: pointer;
box-sizing: border-box;
align-items: center;
padding: 0 1em;
}
#header{
width:100%;
background-color: #101520;
}
input#changeText {
font-size: 1em;
align-self: center;
width: 100%;
padding: 1em;
font-weight: bold;
background: white;
border: 4px solid white;
box-shadow: 0px 30px 40px -32px #6aab23, 0 2px 0px #6aab23;
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
transition: all 0.2s linear;
box-sizing: border-box;
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
input#changeText:focus {
outline: none;
}
.container{
font-size: 20px;
align-self:center;
margin: auto auto;
}
label {
font: white;
font-size: 1em;
color: white;
}
input[type='checkbox'] {
-webkit-appearance:none;
width:30px;
height:30px;
background:white;
border-radius:5px;
border:2px solid #555;
cursor: pointer;
}
input[type='checkbox']:checked {
background: #1A1;
}
#audioOutput, #lastUrls {
font-size: calc(16px + 0.3vw);
width: 730px;
height: 100%;
flex: 20;
border-radius: 10px;
padding: 1em;
background: #eaeaea;
cursor:pointer;
}
label[for="audioOutput"] {
font-size: 3em;
color: #FE53BB;
text-shadow: 0px 0px 30px #fe53bb;
padding-right: 10px;
}
label[for="changeText"] {
font-size: 3em;
color: #00F6FF;
text-shadow: 0px 0px 30px #00f6ff;
padding-top: 5px;
padding-right: 10px;
}
label[for="lastUrls"] {
font-size: 3em;
color: #1a1;
text-shadow: 0px 0px 30px #1a1;
padding-right: 10px;
cursor: pointer;
}
div#audioOutputContainer, #history {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: center;
margin: 4em;
}
@media only screen and (max-width: 1030px) {
body{
zoom: 0.9;
-moz-transform: scale(0.9);
-moz-transform-origin: 0 0;
}
}
#messageDiv {
font-size: .7em;
color: #DDD;
transition: all 0.5s linear;
font-style: italic;
opacity: 0;
text-align: center;
margin: 10px 0;
}
div#urlInput {
margin: 4em;
display: flex;
flex-direction: row;
}
@media only screen and (max-width: 940px) {
body{
zoom: 0.74;
-moz-transform: scale(0.74);
-moz-transform-origin: 0 0;
}
.container{
max-width:99%;
}
div#urlInput {
margin: 2em;
}
div#audioOutputContainer, #history {
margin: 2em;
}
}
@media only screen and (max-width: 840px) {
body{
zoom: 0.64;
-moz-transform: scale(0.64);
-moz-transform-origin: 0 0;
}
}
@media only screen and (max-height: 639px) {
div#urlInput {
margin: 2em;
}
div#audioOutputContainer, #history {
margin: 2em;
}
}
@media only screen and (max-width: 767px) {
div#urlInput {
margin: 2em 1em;
}
div#audioOutputContainer, #history {
margin: 2em 1em;
}
}
@media only screen and (max-height: 380px) {
div#urlInput {
margin: 1em;
}
div#audioOutputContainer, #history {
margin: 1em;
}
}
label[for="audioOutput"], label[for="lastUrls"] {
font-size: 3em;
}
#warning4mac, #electronVersion {
background: #8500f7;
box-shadow: 0px 0px 50px 10px #8500f7ab, inset 0px 0px 10px 2px #8d08ffba;
border: 2px solid #8500f7;
border-radius: 10px;
width: 90%;
padding:1em;
margin:0 auto;
color:white;
font-size:1.3em;
margin-bottom: 20px;
}
#warning4mac a, #electronVersion a {
color:white;
}
ul#lastUrls {
list-style: none;
background: #101520;
color: white;
padding: 1em;
}
ul#lastUrls li {
padding: 5px 0px;
}
ul#lastUrls li:nth-child(even) {
background-color: #182031;
}
#inputCombo {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
flex-grow: 1;
}
</style>
</head>
<body >
<div id="header" style="-webkit-app-region: drag;color:#6f6f6f;font-size:40px; line-height: 40px; padding: 5px 10px; letter-spacing: 3; font-weight: bold;">Electron Capture</div>
<div class="container" >
<div id='warning4mac' style="display:none;"> ✨ Great News! OBS v26.1.2 <a href="https://github.com/obsproject/obs-browser/issues/209#issuecomment-748683083">now supports</a> VDO.Ninja without needing the Electron Capture app! 🥳</div>
<div id="electronVersion" style="display:none;">✨ Great News! <a href="https://github.com/steveseguin/electroncapture/releases/latest">Electron Capture <span id="currentElectronVersion"></span></a> is now available!<br>Update yours today to stay up-to-date with security patches.</div>
<div id="messageDiv" style='display:block'><br /></div>
<div class="container">
<div id="urlInput" title="Put the link you want to load here">
<label for="changeText">
<i class="las la-play"></i>
</label>
<div id="inputCombo">
<input type="text" id="changeText" class="inputfield" value="http://vdo.ninja/" onchange="modURL" onkeyup="enterPressed(event, gohere);" />
<button onclick="gohere();" id="gobutton">GO</button>
</div>
</div>
<div id="audioOutputContainer" title="This option will only work with the official vdo.ninja domain">
<label for="audioOutput"><i class="las la-headphones"></i></label><select id="audioOutput"></select>
</div>
<div id="history" title="History of past links used. You can clear this history using the button to the left">
<label for="lastUrls" onclick="resetHistory()">
<i class="las la-history"></i>
</label>
<select id="lastUrls" onchange="setUrl()"></select>
</div>
</div>
</div>
<script>
/*
* Copyright (c) 2020 Steve Seguin. All Rights Reserved.
*
* Use of this source code is governed by the APGLv3 open-source license
* that can be found in the LICENSE file in the root of the source
* tree. Alternative licencing options can be made available on request.
*
*/
var lastUrls = JSON.parse(localStorage.getItem('lastUrls'));
if (lastUrls != undefined) {
document.querySelector("#changeText").value = lastUrls[0];
if (lastUrls.length>0){
lastUrls.forEach((url)=>{
var o = document.createElement('option');
o.value = url;
o.text = url;
document.querySelector("#lastUrls").appendChild(o);
})
} else {
document.querySelector("#history").style.display="none";
}
} else {
document.querySelector("#history").style.display="none";
}
function setUrl(){
document.querySelector("#changeText").value = document.querySelector("#lastUrls").value;
gohere();
}
function resetHistory(){
localStorage.clear();
document.querySelector('#lastUrls').innerHTML = '';
lastUrls = [];
}
(function (w) {
w.URLSearchParams = w.URLSearchParams || function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
}
else {
return decodeURI(results[1]) || 0;
}
};
}
})(window)
var urlParams = new URLSearchParams(window.location.search);
if ((location.hostname.toLowerCase() == "vdo.ninja") || (location.hostname.toLowerCase() == "obs.ninja")){
try {
if (navigator.userAgent.toLowerCase().indexOf(' electron/') > -1) {
function compareVersions(version){
version = version.split(".");
fetch('https://api.github.com/repos/steveseguin/electroncapture/releases/latest')
.then(response => response.json())
.then(data => {
console.log("recentVersion: "+data.tag_name);
var recentVersion = data.tag_name.split(".");
var ood = false;
if (recentVersion[0]>version[0]){
ood = true;
} else if (recentVersion[0]==version[0]) {
if (recentVersion[1]>version[1]){
ood = true;
} else if (recentVersion[1]==version[1]) {
if (recentVersion[2]>version[2]){
ood = true;
}
}
}
if (ood){
document.getElementById("electronVersion").style.display = "block";
document.getElementById("currentElectronVersion").innerText = data.tag_name;
}
}).catch(console.error);
}
if (urlParams.has('version')){
var ver = urlParams.get('version');
console.log("version: "+ver);
compareVersions(ver);
} else{
var checkVersion = setTimeout(function(){ // pre 1.5.2
compareVersions("0.0.0");
},500);
try{
const ipcRenderer = require('electron').ipcRenderer;
console.log("ELECTRON DETECTED");
ipcRenderer.on('appVersion', function(event, version) {
clearTimeout(checkVersion);
console.log("version: "+version);
compareVersions(version);
})
ipcRenderer.send('getAppVersion');
} catch(e){}
}
}
} catch(e){console.error(e);}
}
var audioOutputSelect = document.querySelector('select#audioOutput');
audioOutputSelect.disabled = !('sinkId' in HTMLMediaElement.prototype);
audioOutputSelect.onclick = getPermssions;
audioOutputSelect.onchange = updateOutputTarget;
var listed = false;
function updateOutputTarget(e){
console.log("change audio: "+audioOutputSelect.value);
var url = document.getElementById('changeText').value;
url=updateURLParameter(url, "sink", audioOutputSelect.value);
document.getElementById('changeText').value = url;
}
function getPermssions(e=null){
if (listed==true){
return;
}
if (e!==null){
e.currentTarget.blur();
}
navigator.mediaDevices.getUserMedia({audio: true,video: false}).then((stream)=>{
navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(console.error); // list all devices
stream.getTracks().forEach(track => {
track.stop();
});
listed=true;
audioOutputSelect.focus();
}).catch(function(){
document.getElementById("messageDiv").innerHTML = "Failed to list available output devices\n\nPlease ensure you allowed the microphone permissions.";
document.getElementById("messageDiv").style.display="block";
setTimeout(function(){document.getElementById("messageDiv").style.opacity="1.0";},0);
});
}
function gotDevices(deviceInfos) {
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i];
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
if (deviceInfo.kind === 'audiooutput'){
option.text = deviceInfo.label || `speaker ${audioOutputSelect.length + 1}`;
audioOutputSelect.appendChild(option);
} else {
console.log('Some other kind of source/device: ', deviceInfo);
}
}
listed=true;
}
function enterPressed(event, callback){
if (event.keyCode === 13){ // Number 13 is the "Enter" key on the keyboard
event.preventDefault(); // Cancel the default action, if needed
callback();
}
}
var isMobile = false;
if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)){ // does not detect iPad Pros.
isMobile=true; // if iOS, default to H264? meh. let's not.
}
// Windows can show the cursor, since it captures in a different way.
//if (navigator.platform.indexOf("Win") != -1){
// document.getElementById("showcursor").checked=true;
//}
function updateURLParameter(url, param, paramVal){
var TheAnchor = null;
var newAdditionalURL = "";
var tempArray = url.split("?");
var baseURL = tempArray[0];
var additionalURL = tempArray[1];
var temp = "";
if (additionalURL){
var tmpAnchor = additionalURL.split("#");
var TheParams = tmpAnchor[0];
TheAnchor = tmpAnchor[1];
if (TheAnchor){additionalURL = TheParams;}
tempArray = additionalURL.split("&");
for (var i=0; i<tempArray.length; i++){
if(tempArray[i].split('=')[0] != param){
newAdditionalURL += temp + tempArray[i];
temp = "&";
}
}
} else {
var tmpAnchor = baseURL.split("#");
var TheParams = tmpAnchor[0];
TheAnchor = tmpAnchor[1];
if(TheParams){baseURL = TheParams;}
}
if (paramVal===false){
temp="";
if(TheAnchor){temp += "#" + TheAnchor;}
var rows_txt = temp
} else if (paramVal===""){
if(TheAnchor){paramVal += "#" + TheAnchor;}
var rows_txt = temp + "" + param;
} else {
if(TheAnchor){paramVal += "#" + TheAnchor;}
var rows_txt = temp + "" + param + "=" + paramVal;
}
return baseURL + "?" + newAdditionalURL + rows_txt;
}
if (urlParams.has('name')){
var name = urlParams.get('name');
if (name!="OBSNinja" && name!="VDONinja"){
document.getElementById('changeText').value = "https://vdo.ninja/?view="+name;
}
}
function addUrlToHistory(url){
if (lastUrls == undefined){
lastUrls = [];
}
if ( lastUrls[0] != url ) {
lastUrls.unshift(url);
if (lastUrls.length == 6) {
lastUrls.pop();
}
}
}
function modURL(){
var url = document.getElementById('changeText').value;
if (url.startsWith("obs.ninja")){
url = "https://"+url;
} else if (url.startsWith("youtube.com")){
url = "https://"+url;
} else if (url.startsWith("twitch.tv")){
url = "https://"+url;
} else if (url.startsWith("vdo.ninja")){
url = "https://"+url;
} else if (url.startsWith("http://")){
// pass
} else if (url.startsWith("https://")){
// pass
} else if (url.startsWith("file:")){
// pass
} else {
url = "https://"+url;
}
console.log(url);
return url;
}
function gohere(){
addUrlToHistory(document.getElementById('changeText').value);
localStorage.setItem('lastUrls', JSON.stringify(lastUrls));
var url = modURL();
if ((document.getElementById('changeText').value.includes("obs.ninja")) && (document.getElementById('changeText').value.includes("http")) && (document.getElementById('changeText').value.includes("&sink"))){
alert("Notice: OBS.Ninja has been renamed to VDO.Ninja.\n\nPlease update your links accordingly for audio output to work correctly.");
} else if (!(document.getElementById('changeText').value.includes(window.location.hostname)) && (document.getElementById('changeText').value.includes("http")) && (document.getElementById('changeText').value.includes("&sink"))){
alert("Notice: The &sink command is domain specific.\nVisit https://YOURDOMAIN.com/electron instead.");
}
window.location = url;
};
getPermssions();
</script>
</body>
</html>

131
esports.html Normal file
View File

@@ -0,0 +1,131 @@
<html>
<head>
<title>IFRAME Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<style>
body{
padding:0;
margin:0;
background-color: #0000;
}
iframe {
border:0;
margin:0;
padding:0;
display:block;
width:100%;
height:90%
}
#viewlink {
width:400px;
}
#container {
display:block;
padding:0px;
}
input{
padding:5px;
margin:5px;
}
button{
padding:5px;
margin:5px;
}
</style>
<script>
function loadIframe(){
document.getElementById("container").innerHTML = "";
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("div");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
var iframesrc = "https://vdo.ninja/?transparent&cleanoutput&bitrate=200&manual&noaudio&view=";
var listOfStreamIDs = [
"1234_pov",
"2345_pov",
"3456_pov",
"4567_pov",
"5678_pov"
];
var button = document.createElement("button");
button.innerHTML = "List connected StreamIDs";
button.onclick = function(){iframe.contentWindow.postMessage({"getStreamIDs":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "HIDE ALL";
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){iframe.contentWindow.postMessage({"target":"*", "remove":true}, '*');};
iframeContainer.appendChild(button);
for (var i=0;i<listOfStreamIDs.length;i++){
if (i!==0){
iframesrc+=",";
}
iframesrc+=listOfStreamIDs[i];
var button = document.createElement("button");
button.innerHTML = "SHOW "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.title = "Publish using: https://vdo.ninja/?push="+listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({"target":"*", "remove":true}, '*');
iframe.contentWindow.postMessage({"target":this.dataset.sid, "add":true, "settings":{"style":{"width":"100%", "height":"100%", "display":"block"}}}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
}
iframe.src = iframesrc;
iframeContainer.appendChild(iframe);
document.getElementById("container").appendChild(iframeContainer);
//////////// LISTEN FOR EVENTS
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
/// If you have a routing system setup, you could have just one global listener for all iframes instead.
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
if ("action" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "event: "+e.data.action+"<br />";
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("streamIDs" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "streamID list:<br />";
for (var key in e.data.streamIDs) {
outputWindow.innerHTML += "streamID: " + key + ", label:"+e.data.streamIDs[key] + "\n";
}
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
});
}
</script>
</head>
<body>
<div id="container">
<button onclick="loadIframe();">CONNECT</button>
</div>
</body>
</html>

121
examples/bigmutebutton.html Normal file
View File

@@ -0,0 +1,121 @@
<html>
<head><title>Twitch + Video</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<style>
body{
padding:0;
margin:0;
background-color:#003;
width:100%;
height:100%;
}
iframe {
width:100%;
height:100%;
border:0;
margin:0;
padding:0;
position:absolute;
display:block;
}
input{
padding:10px;
width:80%;
font-size:1.2em;
z-index: 1000;
margin:10%;
}
#startButton{
margin: 10px;
padding: 20px
display: block;
border-radius: 50px;
font-size:1.5em;
}
#toggleMute{
margin: 10px;
padding: 30px 0;
border-radius: 50px;
font-size:1.5em;
display: block;
position: fixed;
bottom: 0;
width:200px;
left: calc(50% - 100px);
}
.pressed {
background-color: red;
}
</style>
</head>
<body>
<div id="clean">
<center>
<input placeholder="Enter a VDON stream ID here" id="viewlink" type="text" />
<button id="startButton" onclick="loadIframes()" style="display:block;padding:10px;margin:10px;">START</button></center>
</div>
<script>
window.addEventListener("orientationchange", function() {
// Announce the new orientation number
// alert(window.orientation);
}, false);
function loadIframes(url=false){
var streamID = document.getElementById("viewlink").value;
https://vdo.ninja/?label&webcam&cleanoutput&ad=1&vd=1
var path = "vdo.ninja"; //window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
var streamSrc = "https://"+path+"/?push="+streamID+"&label&webcam&cleanoutput&ad=1&vd=1";
document.getElementById("clean").parentNode.removeChild(document.getElementById("clean"));
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = streamSrc;
document.body.appendChild(iframe);
function sendSelfCommand(action, value=null){
iframe.contentWindow.postMessage({"target":null, "action":action, "value":value}, '*');
}
var button = document.createElement("button");
button.id = "toggleMute";
button.innerHTML = "Mute";
button.dataset.value = "true";
document.body.appendChild(button);
button.onclick = function(){
if (this.dataset.value=="true"){
this.dataset.value = "false";
this.classList.add("pressed");
this.innerHTML = "Un-Mute";
sendSelfCommand("mic",false);
} else {
this.classList.remove("pressed");
this.innerHTML = "Mute";
this.dataset.value = "true";
sendSelfCommand("mic",true);
}
}
}
</script>
</body>
</html>

25
examples/changepass.html Normal file
View File

@@ -0,0 +1,25 @@
<html><body><script>
var generateHash = function (str, length=false){
var buffer = new TextEncoder("utf-8").encode(str);
return crypto.subtle.digest("SHA-256", buffer).then(
function (hash) {
hash = new Uint8Array(hash);
if (length){
hash = hash.slice(0, parseInt(parseInt(length)/2));
}
hash = toHexString(hash);
return hash;
}
);
};
function toHexString(byteArray){
return Array.prototype.map.call(byteArray, function(byte){
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('');
}
var password = prompt("Please enter the password");
generateHash(password + location.hostname, 4).then(function(hash) { // million to one error.
alert("hash value: "+hash)
});
</script></body></html>

159
examples/chat.html Normal file
View File

@@ -0,0 +1,159 @@
<html>
<head>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OBSN Chat Overlay</title>
<style>
@font-face {
font-family: 'Cousine';
src: url('fonts/Cousine-Bold.ttf') format('truetype');
}
body {
margin:0;
padding:0 10px;
height:100%;
border: 0;
display: flex;
flex-direction: column-reverse;
position:absolute;
bottom:0;
overflow:hidden;
max-width:100%;
}
div {
margin:0;
background-color: black;
padding: 8px 8px 0px 8px;
color: white;
font-family: Cousine, monospace;
font-size: 3.2em;
line-height: 1.1em;
letter-spacing: 0.0em;
text-transform: uppercase;
text-shadow: 0.05em 0.05em 0px rgba(0,0,0,1);
max-width:100%;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-all;
hyphens: auto;
display:inline-block;
}
a {
color:white;
font-size:1.2em;
text-transform: none;
word-wrap: break-word;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
hyphens: auto;
}
</style>
<script>
(function (w) {
w.URLSearchParams =
w.URLSearchParams ||
function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp("[\?&]" + name + "=([^&#]*)").exec(
self.searchString
);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlParams = new URLSearchParams(window.location.search);
function loadIframe() {
var iframe = document.createElement("iframe");
var view= "";
if (urlParams.has("view")) {
view = "&view="+(urlParams.get("view") || "");
}
var room="";
if (urlParams.has("room")) {
room = "&room="+urlParams.get("room");
}
var password="";
if (urlParams.has("password")) {
password = "&password="+urlParams.get("password");
}
iframe.allow = "autoplay";
var srcString = "./?novideo&noaudio&label=chatOverlay&scene"+room+view+password;
iframe.src = srcString;
iframe.style.width="0";
iframe.style.height="0";
iframe.style.border="0";
document.body.appendChild(iframe);
//////////// LISTEN FOR EVENTS
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
/// If you have a routing system setup, you could have just one global listener for all iframes instead.
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
console.log(e);
if ("gotChat" in e.data){
logData(e.data.gotChat.label,e.data.gotChat.msg);
}
});
}
function printValues(obj) {
var out = "";
for (var key in obj) {
if (typeof obj[key] === "object") {
out += "<br />";
out += printValues(obj[key]);
} else {
if (key.startsWith("_")) {
} else {
out += "<b>" + key + "</b>: " + obj[key] + "<br />";
}
}
}
return out;
}
function logData(type, data) {
var span = document.createElement('span');
var entry = document.createElement('div');
if (type){
type = "<i>"+type.replace(/_/g, ' ')+"</i>";
}
entry.innerHTML = type + data;
span.appendChild(entry);
document.body.prepend(span);
}
</script>
</head>
<body onload="loadIframe();">
</body>
</html>

331
examples/draggable.html Normal file
View File

@@ -0,0 +1,331 @@
<html>
<head><title>Dual Input</title>
<style>
body{
padding:0;
margin:0;
}
iframe {
border:0;
margin:0;
padding:0;
display:block;
margin:0px;
min-height: 100px;
min-width: 100px;
max-height: 95%;
max-width: 99%%;
float: left;
position: fixed;
}
#viewlink {
width:400px;
}
input{
padding:5px;
margin:5px;
}
button{
padding:5px;
margin:5px;
position:relative;
}
.menu {
z-index: 10;
float:right;
right: 20px;
color: #fff;
}
.close {
background-color: #d33;
color: #fff;
}
.reload {
background-color: #0a0;
color: #fff;
}
.popup {
z-index: 9;
background-color: #f1f1f1;
border: 1px solid #d3d3d3;
text-align: center;
min-height: 100px;
min-width: 100px;
max-height: 95%;
max-width: 99%;
scale: 0.5;
}
.popup {
position: absolute;
/*resize: both; !*enable this to css resize*! */
overflow: auto;
}
.popup-header {
cursor: move;
background-color: #2196f3;
}
.popup .resizer-right {
width: 5px;
height: 100%;
background: transparent;
position: absolute;
right: 0;
bottom: 0;
cursor: e-resize;
}
.popup .resizer-bottom {
width: 100%;
height: 5px;
background: transparent;
position: absolute;
right: 0;
bottom: 0;
cursor: n-resize;
}
.popup .resizer-both {
width: 5px;
height: 5px;
background: transparent;
z-index: 10;
position: absolute;
right: 0;
bottom: 0;
cursor: nw-resize;
}
/*NOSELECT*/
.popup * {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome and Opera */
}
</style>
</head>
<body>
<input placeholder="Enter an OBS.Ninja Room Link" id="viewlink" />
<button onclick="loadIframe();">Load URL</button>You can drag and resize the generated windows; multiple can be created.
<div id="container"></div>
<script>
var currentZIndex = 100;
function initDragElement(popup){
var pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
var elmnt = null;
var header = getHeader(popup);
var iframe = getIFrame(popup);
popup.onmousedown = function() {
this.style.zIndex = "" + ++currentZIndex;
};
if (header) {
header.parentPopup = popup;
header.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
elmnt = this.parentPopup;
elmnt.style.zIndex = "" + ++currentZIndex;
e = e || window.event;
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
if (!elmnt) {
return;
}
e = e || window.event;
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
elmnt.style.top = elmnt.offsetTop - pos2 + "px";
elmnt.style.left = elmnt.offsetLeft - pos1 + "px";
iframe.style.top = elmnt.offsetTop - pos2 + "px";
iframe.style.left = elmnt.offsetLeft - pos1 + "px";
}
function closeDragElement() {
/* stop moving when mouse button is released:*/
document.onmouseup = null;
document.onmousemove = null;
}
function getHeader(element) {
var headerItems = element.getElementsByClassName("popup-header");
if (headerItems.length === 1) {
return headerItems[0];
}
return null;
}
function getIFrame(element) {
var headerItems = element.getElementsByTagName("iframe");
if (headerItems.length === 1) {
return headerItems[0];
}
return null;
}
}
function initResizeElement(p) {
var iframe = getIFrame(p);
var element = null;
var startX, startY, startWidth, startHeight;
var right = document.createElement("div");
right.className = "resizer-right";
p.appendChild(right);
right.addEventListener("mousedown", initDrag, false);
right.parentPopup = p;
var bottom = document.createElement("div");
bottom.className = "resizer-bottom";
p.appendChild(bottom);
bottom.addEventListener("mousedown", initDrag, false);
bottom.parentPopup = p;
var both = document.createElement("div");
both.className = "resizer-both";
p.appendChild(both);
both.addEventListener("mousedown", initDrag, false);
both.parentPopup = p;
function initDrag(e) {
element = this.parentPopup;
startX = e.clientX;
startY = e.clientY;
startWidth = parseInt(
document.defaultView.getComputedStyle(element).width,
10
);
startHeight = parseInt(
document.defaultView.getComputedStyle(element).height,
10
);
document.documentElement.addEventListener("mousemove", doDrag, false);
document.documentElement.addEventListener("mouseup", stopDrag, false);
document.documentElement.addEventListener("click", stopDrag, false)
}
function doDrag(e) {
if (e.buttons==0){
stopDrag(e);
return false;
}
element.style.width = startWidth + e.clientX - startX + "px";
element.style.height = startHeight + e.clientY - startY + "px";
iframe.style.width = startWidth + e.clientX - startX + "px";
iframe.style.height = startHeight + e.clientY - startY + "px";
}
function stopDrag(e) {
document.documentElement.removeEventListener("mousemove", doDrag, false);
document.documentElement.removeEventListener("mouseup", stopDrag, false);
}
function getIFrame(element) {
var headerItems = element.getElementsByTagName("iframe");
if (headerItems.length === 1) {
return headerItems[0];
}
return null;
}
}
function loadIframe(){
var iframeContainer = document.createElement("div");
iframeContainer.className="popup";
iframeContainer.style.zIndex = "" + ++currentZIndex;
iframeContainer.style.width="325px";
iframeContainer.style.height="420px";
var button = document.createElement("button");
button.innerHTML = "Move";
button.className = "popup-header menu";
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Close";
button.className = "menu close";
button.onclick = function(){iframe.contentWindow.postMessage({"close":true}, '*');iframe.parentNode.parentNode.removeChild(iframeContainer);}
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Reload";
button.className = "menu reload";
button.onclick = function(){iframe.contentWindow.postMessage({"reload":true}, '*');}
iframeContainer.appendChild(button);
var iframe = document.createElement("iframe");
iframe.allow="autoplay";
iframe.src = document.getElementById("viewlink").value || "https://obs.ninja";
iframe.style.width="325px";
iframe.style.height="420px";
iframeContainer.appendChild(iframe);
document.getElementById("container").appendChild(iframeContainer);
initDragElement(iframeContainer);
initResizeElement(iframeContainer);
}
</script>
</body>
</html>

76
examples/dual.html Normal file
View File

@@ -0,0 +1,76 @@
<html>
<head><title>Dual Input</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<style>
body{
padding:0;
margin:0;
background-color:#003;
width:100%;
height:100%;
}
iframe {
width:100%;
height:100%;
border:0;
margin:0;
padding:0;
position:absolute;
display:block;
}
input{
padding:10px;
width:80%;
font-size:1.2em;
z-index: 1000;
}
</style>
</head>
<body>
<div id="container1" style="width:100%;height:100%;display:none;"></div>
<div id="container2" style="width: calc(25vh*1.777);height: calc(25vh); display:none; float:left; position: fixed; top: 2%; left: 0%;"></div>
<input placeholder="Enter a Room name" id="viewlink" type="text" onchange="loadIframes()"/>
<script>
function loadIframes(url=false){
var roomname = document.getElementById("viewlink").value;
document.getElementById("viewlink").parentNode.removeChild(document.getElementById("viewlink"));
document.getElementById("container1").style.display="inline-block";
document.getElementById("container2").style.display="inline-block";
var path = window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
var room1 = "https://"+path+"/?room="+roomname+"&push="+roomname+"_front&webcam&autostart&vd=front&ad=1&exclude="+roomname+"_rear";
var room2 = "https://"+path+"/?room="+roomname+"&push="+roomname+"_rear&webcam&autostart&vd=back&ad=0&view&cleanoutput&nosettings&transparent";
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = room1;
var iframeContainer = document.createElement("div");
iframeContainer.appendChild(iframe);
document.getElementById("container1").appendChild(iframeContainer);
setTimeout(function(){
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = room2;
var iframeContainer = document.createElement("div");
iframeContainer.appendChild(iframe);
document.getElementById("container2").appendChild(iframeContainer);
},3000);
}
</script>
</body>
</html>

131
examples/esports.html Normal file
View File

@@ -0,0 +1,131 @@
<html>
<head>
<title>IFRAME Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<style>
body{
padding:0;
margin:0;
background-color: #0000;
}
iframe {
border:0;
margin:0;
padding:0;
display:block;
width:100%;
height:90%
}
#viewlink {
width:400px;
}
#container {
display:block;
padding:0px;
}
input{
padding:5px;
margin:5px;
}
button{
padding:5px;
margin:5px;
}
</style>
<script>
function loadIframe(){
document.getElementById("container").innerHTML = "";
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("div");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
var iframesrc = "https://vdo.ninja/?transparent&cleanoutput&bitrate=200&manual&noaudio&view=";
var listOfStreamIDs = [
"1234_pov",
"2345_pov",
"3456_pov",
"4567_pov",
"5678_pov"
];
var button = document.createElement("button");
button.innerHTML = "List connected StreamIDs";
button.onclick = function(){iframe.contentWindow.postMessage({"getStreamIDs":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "HIDE ALL";
button.dataset.sid = listOfStreamIDs[i];
button.onclick = function(){iframe.contentWindow.postMessage({"target":"*", "remove":true}, '*');};
iframeContainer.appendChild(button);
for (var i=0;i<listOfStreamIDs.length;i++){
if (i!==0){
iframesrc+=",";
}
iframesrc+=listOfStreamIDs[i];
var button = document.createElement("button");
button.innerHTML = "SHOW "+listOfStreamIDs[i];
button.dataset.sid = listOfStreamIDs[i];
button.title = "Publish using: https://vdo.ninja/?push="+listOfStreamIDs[i];
button.onclick = function(){
iframe.contentWindow.postMessage({"target":"*", "remove":true}, '*');
iframe.contentWindow.postMessage({"target":this.dataset.sid, "add":true, "settings":{"style":{"width":"100%", "height":"100%", "display":"block"}}}, '*');
}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
}
iframe.src = iframesrc;
iframeContainer.appendChild(iframe);
document.getElementById("container").appendChild(iframeContainer);
//////////// LISTEN FOR EVENTS
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
/// If you have a routing system setup, you could have just one global listener for all iframes instead.
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
if ("action" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "event: "+e.data.action+"<br />";
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("streamIDs" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "streamID list:<br />";
for (var key in e.data.streamIDs) {
outputWindow.innerHTML += "streamID: " + key + ", label:"+e.data.streamIDs[key] + "\n";
}
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
});
}
</script>
</head>
<body>
<div id="container">
<button onclick="loadIframe();">CONNECT</button>
</div>
</body>
</html>

View File

@@ -0,0 +1,264 @@
<html>
<head>
<title>OBS.Ninja IFRAME Outgoing Stats Example</title>
<link rel="shortcut icon" href="data:image/x-icon;," type="image/x-icon" />
<link rel="icon" type="image/png" sizes="32x32" href="./images/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="./images/favicon-16x16.png" />
<link rel="icon" href="./images/favicon.ico" />
<link itemprop="thumbnailUrl" href="./images/obsNinja_logo_full.png" />
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<style>
body {
padding: 0;
margin: 0;
background-color: rgb(20, 25, 38);
}
iframe {
border: 0;
margin: 2px auto;
padding: 0;
display: block;
margin: 10px;
width: 640px;
height: 320px;
background-color: black;
}
input {
padding: 5px;
margin: 5px;
}
button {
padding: 5px;
margin: 5px;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row controls" style="margin-bottom:15px;border-bottom:1px solid black;">
<div class="col-8">
<input type="text" class="form-control" style="width:95%;margin:10px auto;" placeholder="Enter an OBS.Ninja View URL here" value="" id="viewlink" />
</div>
<div class="col-4">
<div class="row">
<div class="col-2"></div>
<div class="col-10">
<button type="button" class="btn btn-primary" style="margin:10px 0;width:calc(90% + 15px);margin-left:5px;" id="btnStart">Start</button>
</div>
</div>
</div>
</div>
<div class="row output">
<div class="col-7" id="source">
<iframe style="margin:0 auto;" allow="autoplay;camera;microphone" src=""></iframe>
</div>
<div class="col-5" id="sourcecontrols">
<div class="row text-light" style="margin-top:15px;">
<div class="col">
<p>This example will show all connections to the stream generated from this page using statistics gathered using the <a href="https://github.com/steveseguin/obsninja/blob/master/IFRAME.md">iFrame API</a>.</p>
<p>Click start to generate a stream using the OBS.Ninja URL shown. If you use the example URL shown, you can <a id="aView" href="" target="_blank">click here</a> to connect to this stream as a viewer in a new window/tab, this will then show in the table below. Expired connections will be removed after a short delay.</p>
</div>
</div>
<div class="row" style="margin-top:5px;">
<div style="padding-top:10px;" class="col-4 text-right font-weight-bold text-light">Audio:</div>
<div class="col-8">
<button type="button" class="btn btn-sm btn-secondary" style="width:45%;" id="btnMuteAudio">Disable</button>
<button type="button" class="btn btn-sm btn-success" style="width:45%;" id="btnUnMuteAudio">Enabled</button>
</div>
</div>
<div class="row">
<div style="padding-top:10px;" class="col-4 text-right font-weight-bold text-light">Video:</div>
<div class="col-8">
<button type="button" class="btn btn-sm btn-secondary" style="width:45%;" id="btnMuteVidio">Disable</button>
<button type="button" class="btn btn-sm btn-success" style="width:45%;" id="btnUnMuteVidio">Enabled</button>
</div>
</div>
<div class="row">
<div style="padding-top:10px;" class="col-4 text-right font-weight-bold text-light">Stats:</div>
<div class="col-8">
<button type="button" class="btn btn-sm btn-secondary" style="width:45%;" id="btnStatsAuto">Auto Refresh Off</button>
<button type="button" class="btn btn-sm btn-secondary" style="width:45%;" id="btnStatsRefresh">Refresh</button>
</div>
</div>
<div class="row">
<div style="padding-top:5px;" class="col-6 text-right font-weight-bold text-light">Outbound Connections:</div>
<div style="padding-top:5px;" class="col-6 font-weight-bold text-light" id="divTotalConnections">0</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<table id="viewers" style="margin-top:15px;" class="table table-hover text-center table-striped table-dark">
<thead>
<tr>
<th scope="col" class="align-middle">Label</th>
<th scope="col" class="align-middle">Added</th>
<th scope="col" class="align-middle">Quality Limit Reason</th>
<th scope="col" class="align-middle">Resolution</th>
<th scope="col" class="align-middle">Platform</th>
<th scope="col" class="align-middle">Encoder</th>
<th scope="col" class="align-middle">User Agent</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script>
var autorefresh = false;
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function(e) {
//Check message is coming from our iframe, otherwise we don't care
if (e.source != $('#source iframe')[0].contentWindow) return;
if ("stats" in e.data) {
var now = new Date(); //Used for "Added" column and to remove stale viewers
for (var viewer in e.data.stats.outbound_stats) {
//Check to see if a row exists for this viewier, if not then its a new viewer and we should create a row
if ($("#obsn_viewer_" + viewer).length == 0) {
var h = now.getHours();
var m = now.getMinutes();
var s = now.getSeconds();
$('#viewers tbody').append('<tr id="obsn_viewer_' + viewer + '"><th class="obsn_viewer_label" scope="row"></th><td class="obsn_viewer_added">' + ("0" + h).slice(-2) + ':' + ("0" + m).slice(-2) + ':' + ("0" + s).slice(-2) + '</td><td class="obsn_viewer_qlr"></td><td class="obsn_viewer_resolution"></td><td class="obsn_viewer_platform"></td><td class="obsn_viewer_encoder"></td><td class="obsn_viewer_useragent"></td></tr>');
}
//Insert/update stats
//Initially objects can be available but without any attributes, check they exist and ignore till the basics are available
if (e.data.stats.outbound_stats[viewer] == undefined) continue;
if (e.data.stats.outbound_stats[viewer].info == undefined) continue;
//Checking these exist as not all attributes are available straight away when stats are created
if (e.data.stats.outbound_stats[viewer].info.label != undefined) {
$("#obsn_viewer_" + viewer).find('.obsn_viewer_label').text(e.data.stats.outbound_stats[viewer].info.label);
}
if (e.data.stats.outbound_stats[viewer].quality_Limitation_Reason != undefined) {
$("#obsn_viewer_" + viewer).find('.obsn_viewer_qlr').text(e.data.stats.outbound_stats[viewer].quality_Limitation_Reason);
}
if (e.data.stats.outbound_stats[viewer].resolution != undefined) {
$("#obsn_viewer_" + viewer).find('.obsn_viewer_resolution').text(e.data.stats.outbound_stats[viewer].resolution);
}
if (e.data.stats.outbound_stats[viewer].info.platform != undefined) {
$("#obsn_viewer_" + viewer).find('.obsn_viewer_platform').text(e.data.stats.outbound_stats[viewer].info.platform);
}
if (e.data.stats.outbound_stats[viewer].encoder != undefined) {
$("#obsn_viewer_" + viewer).find('.obsn_viewer_encoder').text(e.data.stats.outbound_stats[viewer].encoder);
}
if (e.data.stats.outbound_stats[viewer].info.useragent != undefined) {
$("#obsn_viewer_" + viewer).find('.obsn_viewer_useragent').text(e.data.stats.outbound_stats[viewer].info.useragent);
}
$("#obsn_viewer_" + viewer).data('last', now.getTime()); //Used below to remove old viewers
}
//Mark and then remove viewers who have not been seen for a while
$('#viewers tbody tr').each(function(el) {
if (parseInt($(this).data('last')) < (now.getTime() - 10000)) { //10 seconds
$(this).remove();
} else if (parseInt($(this).data('last')) < (now.getTime())) { //Mark viewer in red to show they have disappeared, note that it takes a few seconds for this to happen
$(this).addClass('bg-danger');
} else { //Viewer is there, make sure they're not marked as missing
$(this).removeClass('bg-danger');
}
});
$('#divTotalConnections').text(e.data.stats.total_outbound_connections);
}
});
$(document).ready(function() {
$('#btnMuteAudio').on('click', function() {
$(this).addClass('btn-success').removeClass('btn-secondary').text('Disabled');
$('#btnUnMuteAudio').removeClass('btn-success').addClass('btn-secondary').text('Enable');
$('#source iframe')[0].contentWindow.postMessage({
"mic": false
}, '*');
});
$('#btnUnMuteAudio').on('click', function() {
$(this).addClass('btn-success').removeClass('btn-secondary').text('Enabled');
$('#btnMuteAudio').removeClass('btn-success').addClass('btn-secondary').text('Disable');
$('#source iframe')[0].contentWindow.postMessage({
"mic": true
}, '*');
});
$('#btnMuteVidio').on('click', function() {
$(this).addClass('btn-success').removeClass('btn-secondary').text('Disabled');
$('#btnUnMuteVidio').removeClass('btn-success').addClass('btn-secondary').text('Enable');
$('#source iframe')[0].contentWindow.postMessage({
"camera": false
}, '*');
});
$('#btnUnMuteVidio').on('click', function() {
$(this).addClass('btn-success').removeClass('btn-secondary').text('Enabled');
$('#btnMuteVidio').removeClass('btn-success').addClass('btn-secondary').text('Disable');
$('#source iframe')[0].contentWindow.postMessage({
"camera": true
}, '*');
});
$('#btnStatsAuto').on('click', function() {
if (autorefresh) {
autorefresh = false;
$('#btnStatsAuto').removeClass('btn-success').addClass('btn-secondary').text('Auto Refresh Off');
} else {
autorefresh = true;
$('#btnStatsAuto').addClass('btn-success').removeClass('btn-secondary').text('Auto Refresh On');
}
});
$('#btnStatsRefresh').on('click', function() {
$(this).addClass('btn-success').removeClass('btn-secondary').attr('disabled', true);
$('#source iframe')[0].contentWindow.postMessage({
"getStats": true
}, '*');
setTimeout(function() {
$('#btnStatsRefresh').addClass('btn-secondary').removeClass('btn-success').attr('disabled', false);
}, 700);
});
$('#btnStart').on('click', function() {
//Reset buttons as currently we can't check the state of these properties
$('#btnMuteAudio,#btnMuteVidio').removeClass('btn-success').addClass('btn-secondary').text('Disable');
$('#btnUnMuteAudio,#btnUnMuteVidio').addClass('btn-success').removeClass('btn-secondary').text('Enabled');
//Update the iframe source from the input field, yup, that simple
$('#source iframe').attr('src', $('#viewlink').val());
//Start autorefresh of stats
autorefresh = true;
$('#btnStatsAuto').addClass('btn-success').removeClass('btn-secondary').text('Auto Refresh On');
});
//Start checking for stats
setInterval(function() {
if (autorefresh == false) return;
$('#source iframe')[0].contentWindow.postMessage({
"getStats": true
}, '*');
}, 1000);
//Add in random ID and password strings to URL's, the below is purely for the purposes of this example
var pushid = makeid();
var password = makeid();
var baseUrl = "https://obs.ninja/";
$('#aView').attr('href', baseUrl + '?view=' + pushid + '&password=' + password + '&label=Test_Link');
$('#viewlink').val(baseUrl + '?push=' + pushid + '&password=' + password + '&autostart&turn=false&fps=25&maxbitrate=1000&cleanoutput&audiobitrate=32&aec=0&denoise=0&webcam');
});
//This function is purely used to generate random push id and password strings for the purposes of this example
function makeid() {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < 8; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
</script>
</body>
</html>

3086
examples/main.css Normal file

File diff suppressed because it is too large Load Diff

555
examples/midi.html Normal file
View File

@@ -0,0 +1,555 @@
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/webmidi"></script>
<link rel="stylesheet" href="./main.css" />
<style>
.container {
max-width: 80%;
width: fit-content;
margin: 0 auto;
}
h1 {
margin-top: 1em;
margin-bottom: 1em;
padding: 10px;
}
.card {
margin: 10px;
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 10%);
background-color: #ddd;
color: black;
margin-bottom: 1.5em;
}
.card>div {
padding: 10px;
}
.card h2 {
font-size: 1.5em;
padding: 10px;
background-color: #457b9d;
color: white;
border-bottom: 2px solid #3b6a87;
}
small {
font-style: italic;
display: block;
margin-left: 1em;
}
span.warning {
color: rgb(212, 191, 0);
}
input {
padding: 10px;
}
video {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
audio {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
div#processing {
display: none;
justify-content: center;
place-items: center;
position: absolute;
inset: 0;
font-size: 1.5em;
font-weight: bold;
background: #141926;
flex-direction: column;
}
button {
margin:5px;
border:solid black 2px;
}
body {
color:white;
}
a {
color: #225273!important;
}
</style>
<title>VDO.Ninja MIDI Controller</title>
</head>
<body>
<div id="header">
<a id="logoname" href="./" style="text-decoration: none; color: white; margin: 2px">
<span data-translate="logo-header">
<font id="qos">V</font>DO.Ninja
</span>
</a>
</div>
<div class="container">
<div id="info">
<h1>VDO.Ninja MIDI test app</h1>
<div class="card">
<h2>About</h2>
<div>
You can check the console debug logs for added details.
<br /><br />You can download a virtual MIDI I/O controller for windwos here:<br />
http://www.tobias-erichsen.de/software/loopmidi.html
<br /><br />This code uses the WebMIDI.js library, referenced here:<br />
https://github.com/djipco/webmidi
<br /><br />
Below you can test the <a href="https://docs.vdo.ninja/general-settings/midi#midi-pass-through-mode">MIDI hotkey commands</a> for VDO.Ninja below:<br />
</div>
</div>
<div class="card">
<h2>Select the MIDI Output device:</h2>
<div>
<label for="outputdevice">MIDI Output device:</label>
<select name="outputdevice" id="outputdevice">
</select>
</div>
</div>
<div class="card">
<h2>&midi=1</h2>
<div id="container1">
</div>
</div>
<div class="card">
<h2>&midi=3</h2>
<div id="container2">
</div>
</div>
<div class="card">
<h2>&midi=4 ; director</h2>
<div id="container3">
</div>
</div>
<div class="card">
<h2>&midi=4 ; guest 1</h2>
<div id="container4">
</div>
</div>
<div class="card">
<h2>&midi=4 ; guest 2</h2>
<div id="container5">
</div>
</div>
<div class="card">
<h2>Sample Remote Director Control links</h2>
<div id="container6">
</div>
</div>
</div>
</div>
<div id='commands'>
</div>
<script>
// Enable WebMidi.js
WebMidi.enable(function (err) {
if (err) {
console.log("WebMidi could not be enabled.", err);
}
// Viewing available inputs and outputs
console.log(WebMidi.inputs);
console.log(WebMidi.outputs);
var output = WebMidi.outputs[0];
var midiout = 0;
var outputdevice = document.getElementById("outputdevice");
for (var i=0;i<WebMidi.outputs.length;i++){
var opt = document.createElement('option');
opt.value = WebMidi.outputs[i].id;
opt.innerHTML = WebMidi.outputs[i].name + " (id:"+(1+i)+")";
if (i==0){
midiout = opt.value;
opt.selected = true;
}
outputdevice.appendChild(opt);
}
var path = window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
outputdevice.onchange = function(e){
midiout = outputdevice.value;
output = WebMidi.getOutputById(midiout);
console.log("MIDI DEVICE CHANGED: "+midiout);
var container = document.getElementById("container6");
container.innerHTML = "<br />https://"+path+"/?midiremote=4&director=ROOMNAMEHERE";
container.innerHTML += "<br /><br />";
container.innerHTML += "https://"+path+"/?room=ROOMNAMEHERE&midiout="+(outputdevice.selectedIndex+1)+"&vd=0&ad=0&push&autostart&label=MIDI_CONTROLLER";
}
var container = document.getElementById("container6");
container.innerHTML = "<br />https://"+path+"/?midiremote=4&director=ROOMNAMEHERE";
container.innerHTML += "<br /><br />";
container.innerHTML += "https://"+path+"/?room=ROOMNAMEHERE&midiout="+(outputdevice.selectedIndex+1)+"&vd=0&ad=0&push&autostart&label=MIDI_CONTROLLER";
// Reacting when a new device becomes available
WebMidi.addListener("connected", function(e) {
console.log(e);
});
// Reacting when a device becomes unavailable
WebMidi.addListener("disconnected", function(e) {
console.log(e);
});
// Display the current time
console.log(WebMidi.time);
// Retrieve an input by name, id or index
// var input = WebMidi.getInputByName("StreamDeck2Daw");
// input = WebMidi.getInputById("1809568182");
var input = WebMidi.inputs[1];
// Listen for a 'note on' message on all channels
input.addListener('noteon', "all",
function (e) {
console.log("Received 'noteon' message (" + e.note.name + e.note.octave + ").");
console.log(e);
}
);
// Listen to pitch bend message on channel 3
input.addListener('pitchbend', 3,
function (e) {
console.log("Received 'pitchbend' message.", e);
}
);
// Listen to control change message on all channels
input.addListener('controlchange', "all",
function (e) {
console.log("Received 'controlchange' message.", e);
}
);
// Listen to NRPN message on all channels
input.addListener('nrpn', "all",
function (e) {
if(e.controller.type === 'entry') {
console.log("Received 'nrpn' 'entry' message.", e);
}
if(e.controller.type === 'decrement') {
console.log("Received 'nrpn' 'decrement' message.", e);
}
if(e.controller.type === 'increment') {
console.log("Received 'nrpn' 'increment' message.", e);
}
console.log("message value: " + e.controller.value + ".", e);
}
);
var container = document.getElementById("container1");
///
var button = document.createElement("button");
button.innerHTML = "Note G3; Chat";
button.onclick = function(){output.playNote("G3");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note A3; Mute";
button.onclick = function(){output.playNote("A3");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note B3; Mute Video";
button.onclick = function(){output.playNote("B3");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C4; ScreenShare";
button.onclick = function(){output.playNote("C4");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note D4; Hangup";
button.onclick = function(){output.playNote("D4");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note E4; Hands";
button.onclick = function(){output.playNote("E4");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note F4; Record";
button.onclick = function(){output.playNote("F4");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note G4; Turn on Dir's Audio";
button.onclick = function(){output.playNote("G4");}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note A4; Stop Dir's Audio";
button.onclick = function(){output.playNote("A4");}; // "speaker" also works in the same way.
container.appendChild(button);
///
var container = document.getElementById("container2");
///
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 0";
button.onclick = function(){output.playNote("C1", 1, {velocity: 0});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 1";
button.onclick = function(){output.playNote("C1", 1, {velocity: 1});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 2";
button.onclick = function(){output.playNote("C1", 1, {velocity: 2});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 3";
button.onclick = function(){output.playNote("C1", 1, {velocity: 3});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 4";
button.onclick = function(){output.playNote("C1", 1, {velocity: 4});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 5";
button.onclick = function(){output.playNote("C1", 1, {velocity: 5});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 6";
button.onclick = function(){output.playNote("C1", 1, {velocity: 6});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 7";
button.onclick = function(){output.playNote("C1", 1, {velocity: 7});}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Note C1; velocity 8";
button.onclick = function(){output.playNote("C1", 1, {velocity: 8});}; // "speaker" also works in the same way.
container.appendChild(button);
///
var container = document.getElementById("container3");
///
var button = document.createElement("button");
button.innerHTML = "channel 110; value 0";
button.onclick = function(){output.sendControlChange(110, 0, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 1";
button.onclick = function(){output.sendControlChange(110, 1, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 2";
button.onclick = function(){output.sendControlChange(110, 2, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 3";
button.onclick = function(){output.sendControlChange(110, 3, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 4";
button.onclick = function(){output.sendControlChange(110, 4, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 5";
button.onclick = function(){output.sendControlChange(110, 5, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 6";
button.onclick = function(){output.sendControlChange(110, 6, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 7";
button.onclick = function(){output.sendControlChange(110, 7, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 110; value 8";
button.onclick = function(){output.sendControlChange(110, 8, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
///
var container = document.getElementById("container4");
///
var button = document.createElement("button");
button.innerHTML = "channel 111; value 0";
button.onclick = function(){output.sendControlChange(111, 0, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 111; value 1";
button.onclick = function(){output.sendControlChange(111, 1, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 111; value 2";
button.onclick = function(){output.sendControlChange(111, 2, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 111; value 3";
button.onclick = function(){output.sendControlChange(111, 3, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 111; value 4";
button.onclick = function(){output.sendControlChange(111, 4, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 111; value 5";
button.onclick = function(){output.sendControlChange(111, 5, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
///
var container = document.getElementById("container5");
///
var button = document.createElement("button");
button.innerHTML = "channel 112; transfer popup";
button.onclick = function(){output.sendControlChange(112, 0, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 112; scene 1";
button.onclick = function(){output.sendControlChange(112, 1, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 112; mute in scene";
button.onclick = function(){output.sendControlChange(112, 2, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 112; mute everywhere";
button.onclick = function(){output.sendControlChange(112, 3, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 112; hang up";
button.onclick = function(){output.sendControlChange(112, 4, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "channel 112; solo chat";
button.onclick = function(){output.sendControlChange(112, 5, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "remote speaker";
button.onclick = function(){output.sendControlChange(112, 6, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "remote display";
button.onclick = function(){output.sendControlChange(112, 7, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 2";
button.onclick = function(){output.sendControlChange(112, 12, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 3";
button.onclick = function(){output.sendControlChange(112, 13, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 4";
button.onclick = function(){output.sendControlChange(112, 14, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 5";
button.onclick = function(){output.sendControlChange(112, 15, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 6";
button.onclick = function(){output.sendControlChange(112, 16, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = " scene 7";
button.onclick = function(){output.sendControlChange(112, 17, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 8";
button.onclick = function(){output.sendControlChange(112, 18, 1);}; // "speaker" also works in the same way.
container.appendChild(button);
});
</script>
</body>
</html>

3
examples/mini.css Normal file
View File

@@ -0,0 +1,3 @@
.tile {
max-width:200px !important;
}

View File

@@ -0,0 +1,401 @@
<html>
<head>
<script type="text/javascript" src="./thirdparty/obs-websocket.min.js"></script>
<link rel="stylesheet" href="https://vdo.ninja/main.css" />
<title>OBS Controller Demo using VDO.Ninja</title>
<style>
.container {
max-width: 80%;
width: fit-content;
margin: 0 auto;
}
h1 {
margin-top: 1em;
margin-bottom: 1em;
padding: 10px;
}
.card {
margin: 10px;
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 10%);
background-color: #ddd;
color: black;
margin-bottom: 1.5em;
}
.card>div {
padding: 10px;
}
.card h2 {
font-size: 1.5em;
padding: 10px;
background-color: #457b9d;
color: white;
border-bottom: 2px solid #3b6a87;
}
small {
font-style: italic;
display: block;
margin-left: 1em;
}
span.warning {
color: rgb(212, 191, 0);
}
input {
padding: 10px;
display: inline-block;
flex-flow: unset;
margin: 10px;
}
video {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
audio {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
div#processing {
display: none;
justify-content: center;
place-items: center;
position: absolute;
inset: 0;
font-size: 1.5em;
font-weight: bold;
background: #141926;
flex-direction: column;
}
button {
margin:5px;
border:solid black 2px;
}
body {
color:white;
display: inline-block;
flex-flow: unset;
}
a {
color: #CEF!important;
}
#info{
margin: 20px;
max-height: 50%;
overflow: auto;
}
#client {
margin:10px;
display:block;
}
label {
color:white;
}
</style>
</head>
<body>
<div class="container">
<h1>OBS remote (server)</h1>
<span id='setup'>
<label for="address">Websocket Address</label>
<input name="address" id="address" placeholder="address (optional)" value="localhost:4444" />
<label for="address">Websocket Password</label>
<input name="password" id="password" placeholder="password here (optional)" />
<br />
<label for="vdoroomname">Room name to use</label>
<input name="vdoroomname" id="vdoroomname" placeholder="vdo room name to use (optional)" />
<label for="vdopassword">Room password to use</label>
<input name="vdopassword" id="vdopassword" placeholder="vdo password to use (optional)" />
<br />
<button id="address_button">Connect</button>
<button id="address_button_2">Connect and share OBS Output</button>
</span>
<a id="client" target="_blank"></a>
<div id="info"></div>
<small>Code available on GitHub: <a target="_blank" href='https://github.com/steveseguin/remote_ninja'>https://github.com/steveseguin/remote_ninja</a></small>
</div>
<script>
var hostname = "vdo.ninja"; // all that's supported as of this moment.
const obs = new OBSWebSocket();
var scenesData = {};
scenesData.scenes = [];
function sendToOBS(action, data={}){
document.getElementById("info").innerHTML = action + "<br />"+document.getElementById("info").innerHTML;
obs.sendCallback(action, data, sendCallback)
}
(function(w) {
w.URLSearchParams = w.URLSearchParams || function(searchString) {
var self = this;
searchString = searchString.replace("??", "?");
self.searchString = searchString;
self.get = function(name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlEdited = window.location.search.replace(/\?\?/g, "?");
urlEdited = urlEdited.replace(/\?/g, "&");
urlEdited = urlEdited.replace(/\&/, "?");
if (urlEdited !== window.location.search){
warnlog(window.location.search + " changed to " + urlEdited);
window.history.pushState({path: urlEdited.toString()}, '', urlEdited.toString());
}
var urlParams = new URLSearchParams(urlEdited);
var roomname = Math.floor(Math.random() * 1000000);
var pwurl = Math.floor(Math.random() * 1000000);
if (urlParams.get("password")){
pwurl = urlParams.get("password");
localStorage.setItem('password',pwurl)
} else if (localStorage.getItem('password')){
pwurl = localStorage.getItem('password');
} else {
localStorage.setItem('password',pwurl)
}
if (urlParams.get("room")){
roomname = urlParams.get("room")
localStorage.setItem('roomname',roomname)
} else if (localStorage.getItem('roomname')){
roomname = localStorage.getItem('roomname');
} else {
localStorage.setItem('roomname',roomname)
}
document.getElementById('vdoroomname').value = roomname;
document.getElementById('vdopassword').value = pwurl;
if (localStorage.getItem('address')){
document.getElementById('address').value = localStorage.getItem('address');
}
if (localStorage.getItem('wspass')){
document.getElementById('password').value = localStorage.getItem('wspass');
}
var iframe = null;
function createIFrame(visible=true){
iframe = document.createElement("iframe");
if (visible){
iframe.src = "https://"+hostname+"/?room="+roomname+"&push=mainOBSOutput&od=0&transparent&webcam&vd=obs&view&password="+pwurl+"&label=OBS_"+Math.floor(Math.random() * 1000000);
iframe.style.minWidth = "720px";
iframe.style.minHeight = "480px";
iframe.style.maxWidth = "50%";
iframe.style.maxHeight = "50%";
iframe.style.display = "block";
iframe.style.margin = "auto";
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
} else {
iframe.src = "https://"+hostname+"/?room="+roomname+"&push&autostart&vd=0&view&ad=0&transparent&cleanoutput&password="+pwurl+"&label=OBS_"+Math.floor(Math.random() * 1000000);
iframe.style.opacity = 0;
iframe.style.width = 0;
iframe.style.height = 0;
iframe.style.position = "absolulte";
iframe.style.top = "-100px";
iframe.style.left = "-100px";
}
document.getElementById("client").parentNode.insertBefore(iframe, document.getElementById("client").nextSibling);
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
console.log(e);
if ("dataReceived" in e.data){
if ("sendToOBS" in e.data.dataReceived){
if ("action" in e.data.dataReceived.sendToOBS){
if ("data" in e.data.dataReceived.sendToOBS){
sendToOBS(e.data.dataReceived.sendToOBS.action, e.data.dataReceived.sendToOBS.data);
}
}
}
} else if ("action" in e.data){
if (e.data.action === "new-push-connection"){
console.log(e.data);
updateSceneList();
}
}
});
}
function sendCallback(err, data){
console.log("CALLBACK TRIGGERED");
var msg = {};
msg.sentFromOBS = {};
msg.sentFromOBS.callbackData = data;
msg.sentFromOBS.callbackError = err;
try {
iframe.contentWindow.postMessage({"sendData":msg}, '*');
} catch(e){}
}
function sendRawData(data){
var msg = {};
msg.sentFromOBS = {};
msg.sentFromOBS.rawData = data;
try {
iframe.contentWindow.postMessage({"sendData":msg}, '*');
} catch(e){}
}
function heartBeat(){
obs.send("GetStats");
}
function updateSceneList(){
var msg = {};
msg.sentFromOBS = {};
msg.sentFromOBS.scenes = scenesData;
try {
iframe.contentWindow.postMessage({"sendData":msg}, '*');
} catch(e){}
console.log(msg);
obs.send("GetSourcesList");
obs.send('GetCurrentScene');
obs.send("GetVideoInfo");
obs.send("ListOutputs");
}
document.getElementById('address_button').addEventListener('click', e => {
connect(e, false);
});
document.getElementById('address_button_2').addEventListener('click', e => {
connect(e, true);
});
var heartBeatInterval = null;
function connect(e, camera){
const address = document.getElementById('address').value || "localhost:4444";
const password = document.getElementById('password').value;
roomname = document.getElementById('vdoroomname').value || Math.floor(Math.random() * 1000000);
pwurl = document.getElementById('vdopassword').value || Math.floor(Math.random() * 1000000);
localStorage.setItem('roomname',roomname)
localStorage.setItem('password',pwurl)
createIFrame(camera); // connects to VDO.Ninja's IFRAME API
localStorage.setItem('address',address);
if (password){
localStorage.setItem('wspass',password);
var ret = obs.connect({
address: address,
password: password
});
} else {
var ret = obs.connect({
address: address
});
}
ret.then(() => {
console.log(`Success!`);
return obs.send('GetSceneList');
}).then(data => {
document.getElementById("setup").style.display = "none";
scenesData = data;
updateSceneList();
var pathname = window.location.pathname.split("/");
pathname.pop();
pathname = pathname.join("/");
var clientLink = window.location.protocol + "//" + window.location.host + pathname + "/interface.html?room="+roomname+"&password="+pwurl;
document.getElementById("client").href = clientLink;
document.getElementById("client").innerHTML = "<b><font style='color:#70c4ff;'>client link:</font></b> "+clientLink;
document.getElementById("info").innerHTML = "<br /><p style='color:#bdffbd;'>Connection to OBS websockets opened.</p>" + document.getElementById("info").innerHTML;
try {
obs._socket.onmessage2 = obs._socket.onmessage; // hijacking the obs-websocket.js framework
obs._socket.onmessage = function(data){
console.log(data);
obs._socket.onmessage2(data);
if (data.type && data.data){
if (data.type == "message"){
sendRawData(data.data);
}
}
}
} catch(e){console.error(e);}
clearInterval(heartBeatInterval);
heartBeatInterval = setInterval(function(){heartBeat();},3000);
}).catch(err => { // Promise convention dicates you have a catch on every chain.
console.log(err);
document.getElementById("info").innerHTML = "<br />Error trying to connect. Is SSL enabled on your domain?" + document.getElementById("info").innerHTML;
if ("error" in err){
document.getElementById("info").innerHTML = "<br />"+err.error + document.getElementById("info").innerHTML;
}
});
};
// We use the source visibility one and filter visibility web socket commands quite often in shows.
obs.on('SwitchScenes', data => {
console.log(`New Active Scene: ${data.sceneName}`);
scenesData.currentScene = data.sceneName
updateSceneList();
});
obs.on('ConnectionOpened', (data) => function(){
document.getElementById("setup").style.display = "none";
document.getElementById("info").innerHTML = "<br /><p style='color:#bdffbd;'>Connection to OBS websockets opened.</p>" + document.getElementById("info").innerHTML;
});
obs.on('ConnectionClosed', (data) => function(){
document.getElementById("setup").style.display = "unset";
document.getElementById("info").innerHTML = "<br />Connection to OBS websockets closed" + document.getElementById("info").innerHTML;
});
obs.on('AuthenticationSuccess', (data) => function(){
document.getElementById("setup").style.display = "none";
document.getElementById("info").innerHTML = "<br />OBS websockets authenticated" + document.getElementById("info").innerHTML;
});
obs.on('AuthenticationFailure', (data) => function(){
document.getElementById("setup").style.display = "unset";
document.getElementById("info").innerHTML = "<br />Authentication to OBS websockets failed" + document.getElementById("info").innerHTML;
});
obs.on('ScenesChanged', (data) => function(){
scenesData = data;
updateSceneList()
});
// You must add this handler to avoid uncaught exceptions.
obs.on('error', err => {
document.getElementById("info").innerHTML = "<br />OBS websockets error" + document.getElementById("info").innerHTML;
console.error('socket error:', err);
if ("error" in err){
document.getElementById("info").innerHTML = "<br />"+err.error + document.getElementById("info").innerHTML;
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,474 @@
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="./thirdparty/obs-websocket.min.js"></script>
<link rel="stylesheet" href="https://vdo.ninja/main.css" />
<style>
.container {
max-width: 80%;
width: fit-content;
margin: 0 auto;
}
h1 {
margin-top: 1em;
margin-bottom: 1em;
padding: 10px;
}
.card {
margin: 10px;
box-shadow: 0 4px 8px 0 rgb(0 0 0 / 10%);
background-color: #ddd;
color: black;
margin-bottom: 1.5em;
}
.card>div {
padding: 10px;
}
.card h2 {
font-size: 1.5em;
padding: 10px;
background-color: #457b9d;
color: white;
border-bottom: 2px solid #3b6a87;
}
small {
font-style: italic;
display: block;
margin-left: 1em;
}
span.warning {
color: rgb(212, 191, 0);
}
input {
padding: 10px;
display: inline-block;
flex-flow: unset;
margin: 10px;
}
video {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
audio {
max-width: 640px;
max-height: 360px;
padding: 20px;
}
div#processing {
display: none;
justify-content: center;
place-items: center;
position: absolute;
inset: 0;
font-size: 1.5em;
font-weight: bold;
background: #141926;
flex-direction: column;
}
button {
margin:5px;
border:solid black 2px;
}
body {
color:white;
display: inline-block;
flex-flow: unset;
}
a {
color: #225273!important;
}
.hidden{
display:none !important;
}
.stat {
background-color: black;
margin: 7px;
padding: 5px;
}
.stat:nth-child(2n) {
background-color: #333;
}
.stat:empty {
display:none;
}
</style>
<title>OBS Controller Demo using VDO.NInja</title>
</head>
<div class="container">
<h1>OBS remote (client)</h1>
<div id="info">
<div class="card">
<h2>Scenes</h2>
<div id="scene_list">
</div>
</div>
<div class="card hidden">
<h2>General</h2>
<div>
<button onclick="basicCommand(this);" data-command="GetVersion">GetVersion</button>
<button onclick="basicCommand(this);" data-command="GetStats">GetStats</button>
<button onclick="basicCommand(this);" data-command="GetVideoInfo">GetVideoInfo</button>
</div>
</div>
<div class="card">
<h2>Output</h2>
<div id="outputs">
<button onclick="basicCommand(this);" data-command="ListOutputs">ListOutputs</button>
</div>
</div>
<div class="card">
<h2>Active Sources</h2>
<div id="active_source_list">
</div>
</div>
<div class="card">
<h2>All Sources</h2>
<div id="source_list">
</div>
</div>
<div class="card hidden">
<h2>Source debug</h2>
<div>
<button onclick="basicCommand(this);" data-command="GetMediaSourcesList">GetMediaSourcesList</button>
<button onclick="basicCommand(this);" data-command="GetSourcesList">GetSourcesList</button>
<button onclick="basicCommand(this);" data-command="GetSourceActive">GetSourceActive</button>
<button onclick="basicCommand(this);" data-command="GetAudioActive">GetAudioActive</button>
</div>
</div>
<div id="audio" class="card hidden">
<h2>Audio</h2>
<div>
<button class="hidden" onclick="basicCommand(this, {source:selectedSource});" data-command="GetVolume">GetVolume</button>
<input type='range' min="0" max="100" value="100" onchange="basicCommand(this, {source:selectedSource, volume:parseInt(this.value)/100});" data-command="SetVolume" />
<button class="hidden" onclick="basicCommand(this, {source:selectedSource});" data-command="ToggleMute">ToggleMute</button>
</div>
</div>
<div class="card" id='commands'>
<h2>Custom Commands</h2>
<input id="newCommand" placeholder="enter a custom command" type='text' /><button id="goCreate" onclick="createCommand()">Create</button>
<br />
A list of possible commands <a href="https://github.com/Palakis/obs-websocket/blob/4.x-current/docs/generated/protocol.md#requests">available here:</a><br />
</div>
</div>
<div style="width:100%;display:block;margin:0;padding:0;">
<div id='OBSstats' style="width:49%;display:inline-block;vertical-align: top;">
<div id="stat-current-profile" class='stat'></div>
<div id="stat-current-scene" class='stat'></div>
<div id="stat-streaming" class='stat'></div>
<div id="stat-memory-usage" class='stat'></div>
<div id="stat-recording" class='stat'></div>
<div id="stat-recording-paused" class='stat'></div>
<div id="stat-average-frame-time" class='stat'></div>
<div id="stat-cpu-usage" class='stat'></div>
<div id="stat-fps" class='stat'></div>
<div id="stat-free-disk-space" class='stat'></div>
</div>
<div id='OBSsettings' style="width:49%;display:inline-block;vertical-align: top;">
<div id="setting-baseHeight" class='stat'></div>
<div id="setting-baseWidth" class='stat'></div>
<div id="setting-outputHeight" class='stat'></div>
<div id="setting-outputWidth" class='stat'></div>
<div id="setting-scaleType" class='stat'></div>
<div id="setting-fps" class='stat'></div>
</div>
</div>
</div>
<script>
function basicCommand(ele, data={}){
sendToOBS(ele.dataset.command, data);
}
function createCommand(){
var command = document.getElementById("newCommand").value;
document.getElementById("newCommand").value = "";
var button = document.createElement("button");
button.innerText = command;
button.dataset.command = command;
button.onclick = function(){basicCommand(this);};
document.getElementById("commands").appendChild(button);
}
(function(w) {
w.URLSearchParams = w.URLSearchParams || function(searchString) {
var self = this;
searchString = searchString.replace("??", "?");
self.searchString = searchString;
self.get = function(name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlEdited = window.location.search.replace(/\?\?/g, "?");
urlEdited = urlEdited.replace(/\?/g, "&");
urlEdited = urlEdited.replace(/\&/, "?");
if (urlEdited !== window.location.search){
warnlog(window.location.search + " changed to " + urlEdited);
window.history.pushState({path: urlEdited.toString()}, '', urlEdited.toString());
}
var urlParams = new URLSearchParams(urlEdited);
if (urlParams.get("password")){
var password = urlParams.get("password");
localStorage.setItem('password',password)
} else if (localStorage.getItem('password')){
password = localStorage.getItem('password');
}
if (urlParams.get("room")){
var roomname = urlParams.get("room")
localStorage.setItem('roomname',roomname)
} else if (localStorage.getItem('roomname')){
roomname = localStorage.getItem('roomname');
}
const obs = new OBSWebSocket();
var scenesData = {};
var selectedSource = null;
scenesData.scenes = [];
function sendToOBS(action, data){
var msg = {};
msg.sendToOBS = {};
msg.sendToOBS.action = action;
msg.sendToOBS.data = data;
iframe.contentWindow.postMessage({"sendData":msg}, '*');
}
var iframe = document.createElement("iframe");
iframe.src = "https://vdo.ninja/?room="+roomname+"&password="+password+"&push&novideo=mainOBSOutput&noaudio=mainOBSOutput&autostart&vd=0&ad=0&transparent&cleanoutput&label=CLIENT_"+Math.floor(Math.random() * 1000000);
// iframe.style.opacity = 0;
iframe.style.width = "160px";
iframe.style.height = "90px";
iframe.style.position = "fixed";
iframe.style.top = "10px";
iframe.style.right = "170px";
document.body.appendChild(iframe)
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
if ("dataReceived" in e.data){
if ("sentFromOBS" in e.data.dataReceived){
processIncoming(e.data.dataReceived.sentFromOBS);
}
}
});
function changeScene(scene){
sendToOBS('SetCurrentScene', {
'scene-name': scene
});
}
function processIncoming(data){
if ("scenes" in data){
scenesData = data.scenes;
updateSceneList();
}
if ("callbackData" in data){
console.log(data.callbackData);
} else if ("callbackError" in data){
console.log(data.callbackError);
}
if ("rawData" in data){
var data = JSON.parse(data.rawData);
console.log(data);
if ("stats" in data){
var i = "stats";
for (var j in data[i]){
if (document.getElementById("stat-"+j)){
if (typeof data[i][j] == "number"){
data[i][j] = parseInt(data[i][j]*100)/100.0;
}
if (data[i][j]===true){
document.getElementById("stat-"+j).innerHTML = j+ " : <font color='#CFC'>" + data[i][j]+"</font>";
} else if (data[i][j] === false){
document.getElementById("stat-"+j).innerHTML = j+ " : <font color='#FCC'>" + data[i][j]+"</font>";
} else {
document.getElementById("stat-"+j).innerHTML = j+ " : <font color='#DDD'>" + data[i][j]+"</font>";
}
}
}
} else if ("baseHeight" in data){
for (var i in data){
if (document.getElementById("setting-"+i)){
if (data[i]===true){
document.getElementById("setting-"+i).innerHTML = i+ " : <font color='#CFC'>" + data[i]+"</font>";
} else if (data[i] === false){
document.getElementById("setting-"+i).innerHTML = i+ " : <font color='#FCC'>" + data[i]+"</font>";
} else {
document.getElementById("setting-"+i).innerHTML = i+ " : <font color='#DDD'>" + data[i]+"</font>";
}
}
}
} else if ("sources" in data){
if ("name" in data){
document.getElementById("active_source_list").innerHTML = "";
for (var i =0;i<data.sources.length;i++){
var button = document.createElement("button");
button.innerText = data.sources[i].name;
button.dataset.name = data.sources[i].name;
button.dataset.type = "source"
button.onclick = function(){
console.log("CLICKED: " + this.innerText);
selectedSource = this.dataset.name;
document.getElementById("audio").classList.add("hidden");
var sources = document.querySelectorAll("[data-name");
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.remove("pressed");
}
var sources = document.querySelectorAll("[data-name='"+selectedSource+"']");
console.log(sources);
for (var k = 0 ; k<sources.length; k++){
document.getElementById("audio").classList.remove("hidden");
sources[k].classList.add("pressed");
}
};
document.getElementById("active_source_list").appendChild(button);
}
if (selectedSource){
var sources = document.querySelectorAll("[data-name");
document.getElementById("audio").classList.add("hidden");
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.remove("pressed");
}
var sources = document.querySelectorAll("[data-name='"+selectedSource+"']");
console.log(sources);
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.add("pressed");
document.getElementById("audio").classList.remove("hidden");
}
} else {
document.getElementById("audio").classList.add("hidden");
}
} else {
document.getElementById("source_list").innerHTML = "";
for (var i =0;i<data.sources.length;i++){
var button = document.createElement("button");
button.innerText = data.sources[i].name;
button.dataset.name = data.sources[i].name;
button.dataset.type = "source"
button.onclick = function(){
console.log("CLICKED: " + this.innerText);
selectedSource = this.dataset.name;
document.getElementById("audio").classList.add("hidden");
var sources = document.querySelectorAll("[data-name");
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.remove("pressed");
}
var sources = document.querySelectorAll("[data-name='"+selectedSource+"']");
console.log(sources);
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.add("pressed");
document.getElementById("audio").classList.remove("hidden");
}
};
document.getElementById("source_list").appendChild(button);
}
if (selectedSource){
var sources = document.querySelectorAll("[data-name");
document.getElementById("audio").classList.add("hidden");
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.remove("pressed");
}
var sources = document.querySelectorAll("[data-name='"+selectedSource+"']");
console.log(sources);
for (var k = 0 ; k<sources.length; k++){
sources[k].classList.add("pressed");
document.getElementById("audio").classList.remove("hidden");
}
} else {
document.getElementById("audio").classList.add("hidden");
}
}
<!-- congestion: 0 -->
<!-- droppedFrames: 0 -->
<!-- flags: {audio: true, encoded: true, multiTrack: true, rawValue: 31, service: true, } -->
<!-- height: 1080 -->
<!-- name: "simple_stream" -->
<!-- reconnecting: false -->
<!-- settings: {bind_ip: 'default', dyn_bitrate: false, low_latency_mode_enabled: false, new_socket_loop_enabled: false} -->
<!-- totalBytes: 351121 -->
<!-- totalFrames: 30 -->
<!-- type: "rtmp_output" -->
<!-- width: 1920 -->
} else if ("outputs" in data){
document.getElementById("outputs").innerHTML = "";
for (var i =0;i<data.outputs.length;i++){
var button = document.createElement("button");
button.innerText = data.outputs[i].name;
button.dataset.output = data.outputs[i].name;
button.onclick = function(){
console.log("CLICKED: " + this.innerText);
var outputName = this.dataset.output;
if (this.classList.contains("pressed")){
this.classList.remove("pressed");
sendToOBS("StopOutput",{outputName:outputName});
} else {
this.classList.add("pressed");
sendToOBS("StartOutput",{outputName:outputName});
}
};
document.getElementById("outputs").appendChild(button);
}
}
}
}
function updateSceneList(){
var scenes = scenesData.scenes;
document.getElementById("scene_list").innerHTML = "";
scenes.forEach(scene => {
var button = document.createElement("button");
button.innerText = scene.name;
button.onclick = function(){
console.log("CLICKED");
changeScene(this.innerText);
}; // "speaker" also works in the same way.
document.getElementById("scene_list").appendChild(button);
if (scene.name === scenesData.currentScene) {
button.classList.add("pressed");
}
});
}
</script>
</body>
</html>

File diff suppressed because one or more lines are too long

69
examples/p2p.html Normal file
View File

@@ -0,0 +1,69 @@
<html>
<body>
<div id="results" style="overflow:scroll;max-height:300px;">
starting...
</div>
<script> // https://jsfiddle.net/steveseguin/0t3ayvk8/31/
var connectionID = Math.random()*100000001;
function RecvDataWindow(){
var iframe = document.createElement("iframe");
iframe.src = "https://vdo.ninja/beta/?view="+connectionID+"&cleanoutput";
iframe.style.width = "0px";
iframe.style.height = "0px";
iframe.style.position = "fixed";
iframe.style.left = "-100px";
iframe.style.top = "-100px";
iframe.id = "frame1"
document.body.appendChild(iframe);
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
if ("dataReceived" in e.data){ // raw data
document.getElementById("results").innerHTML += e.data.dataReceived+"<br />";
console.log(e.data);
try {
iframe.contentWindow.postMessage({"sendData":"pong!!", "UUID":e.data.UUID}, '*');
} catch(E){}
}
});
}
function SendDataWindow(){
var iframe = document.createElement("iframe");
iframe.src = "https://vdo.ninja/beta/?push="+connectionID+"&vd=0&ad=0&autostart&cleanoutput";
iframe.style.width = "0px";
iframe.style.height = "0px";
iframe.style.position = "fixed";
iframe.style.left = "-100px";
iframe.style.top = "-100px";
iframe.id = "frame2";
document.body.appendChild(iframe);
setInterval(function(){
try {
console.log(".");
document.getElementById("frame2").contentWindow.postMessage({"sendData":"ping!!"}, '*');
} catch(E){}
}, 1000);
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
if ("dataReceived" in e.data){ // raw data
console.log(e.data);
document.getElementById("results").innerHTML += e.data.dataReceived+"<br />";
}
});
}
SendDataWindow();
RecvDataWindow();
</script>
</body>
</html>

23
examples/readme.md Normal file
View File

@@ -0,0 +1,23 @@
p2p is a sample of how to use vdo.ninja as a data transport tunneling service
twitch is an example of how to have a twitch live chat side-by-side with VDO.NInja on the same screen
dual is an example of how to have two VDO.Ninja windows (or any windows really) open on the same page; Picture-in-Picture style
sensors is an example of how to transmit sensor and video data from a phone to a computer, drawing it to canvas: youtube video for this exists
midi demonstrates the MIDI API for VDO.Ninja
draggable demonstrates how to drag multiple windows around, if you wanted to create a custom layout of elements. (experimental)
chat.html is an example of a chat-only interface for VDO.NInja; maybe dockable into OBS even
iframe.outbound-stats.html demostrates how to get stats from VDO.Ninja using the IFRAME API
changepass lets you create passwords and related HASH values for VDO.NInja rooms
webhid demonstrates how to interface with a USB device, like a streamdeck (mouse/keyboard not supported)
zoom.html is a tool for letting you publish into VDO.Ninja, but then full-screen the window once setup, allowing for window-capturing into zoom.
obs_remote is also hosted on github elsewhere, but it's an example of how to remotely control OBS using VDO.Ninja's tunneling abilities

280
examples/sensors.html Normal file
View File

@@ -0,0 +1,280 @@
<html>
<head><title>Sensor and video stream access example</title>
<style>
body{
padding:0;
margin:0;
background-color: rgb(222,242,253);
}
iframe {
border:0;
margin:0;
padding:0;
display:block;
margin:10px;
width:640px;
height:320px;
}
#viewlink {
width:400px;
}
#container {
display:block;
padding:0px;
}
input{
padding:5px;
margin:5px;
}
button{
padding:5px;
margin:5px;
}
canvas{
width:100%;
display:block;
margin:0;
padding:0;
}
</style>
</head>
<body>
<canvas id="canvas" style="display:none;max-height:70vh;max-width:calc(70vh*1.777);width:100%;height:100%;" width="1920" height="1080" ></canvas>
<input placeholder="Enter a VDO.Ninja View URL here" id="viewlink" style="display:block;" onchange="loadIframe();"/>
<label for="hori">FOA-Horizontal</label>
<input type="range" id="hori" name="hori" value="63" title="63" min="40" max="80" title="67" onchange="updateHor(this);">
<label for="vert">FOA-Vertical</label>
<input type="range" id="vert" name="vert" value="50" title="50" min="30" max="70" onchange="updateVer(this);">
<label for="draw">Draw Delay</label>
<input type="range" id="draw" name="draw" value="110" title="110" min="0" max="500" style="width:500px" onchange="updateDelay(this);"><br /><br />
Add &sensor to the push link to send data; see: <a target="_blank" href="https://docs.vdo.ninja/source-settings/sensor">https://docs.vdo.ninja/source-settings/sensor</a>
<div id="container">
</div>
<script>
// https://www.camerafv5.com/devices/manufacturers/google/pixel_4a_sunfish_1/ ; pixel 4a specs
var horFOA = 49.6;
var verFOA = 63.3;
var drawDelay = 110;
function updateHor(hor){
horFOA = parseInt(hor.value);
hor.title = horFOA;
}
function updateVer(ver){
verFOA = parseInt(ver.value);
ver.title = verFOA;
}
function updateDelay(time){
drawDelay = parseInt(time.value);
time.title = drawDelay;
}
function loadIframe(url=false){ // this is pretty important if you want to avoid camera permission popup problems. You can also call it automatically via: <body onload=>loadIframe();"> , but don't call it before the page loads.
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled= false;
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("div");
if (url){
var iframesrc = url;
} else {
var iframesrc = document.getElementById("viewlink").value;
}
console.log(iframesrc);
document.getElementById("viewlink").parentNode.removeChild(document.getElementById("viewlink"));
document.getElementById("canvas").style.display="block";
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
if (iframesrc==""){
iframesrc="./";
}
iframe.src = iframesrc;
iframeContainer.appendChild(iframe);
document.getElementById("container").appendChild(iframeContainer);
var videos = iframe.contentWindow.document.querySelectorAll("video");
var sensors = {};
function drawFrame(vid){
try {
if (sensors.mag){ // androids may not support this.
var angle = 1.5 * Math.PI - Math.atan2(sensors.mag.y,sensors.mag.x);
var startPixel = (angle / ( 2 * Math.PI)) * 1920;
var endPixel = (verFOA/360) * 1920 + startPixel;
} else if (sensors.ori){
var angle = sensors.ori.a;
var frontToBack = sensors.ori.b;
var leftToRight = sensors.ori.g;
var startPixel = Math.floor((angle / 360) * 1920);
var width = Math.floor((verFOA/360) * 1920);
var height = vid.videoHeight*(width/vid.videoWidth);
var h_offset = Math.floor(((frontToBack+(verFOA/2))/180 * 1080)-540);
var w_offset = Math.floor((leftToRight+horFOA)/180 * 1920);
}
setTimeout(function(a1,a2,a3,a4,a5){
ctx.filter = 'blur(4px)';
ctx.drawImage(a1,a2,a3,a4,a5);
ctx.filter = "none";
ctx.drawImage(a1,a2,a3,a4,a5);
}, drawDelay, vid, startPixel-w_offset, h_offset, width, height);
} catch(e){console.error(e);}
};
setInterval(function(){
if (videos.length){
if ("UUID" in sensors){
if (videos[0].id !== "videosource_"+sensors.UUID){
videos = iframe.contentWindow.document.querySelectorAll("video#videosource_"+sensors.UUID);
}
if (videos.length){
drawFrame(videos[0]);
}
}
}
},100);
//////////// LISTEN FOR EVENTS
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
/// If you have a routing system setup, you could have just one global listener for all iframes instead.
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
if ("stats" in e.data){
var outputWindow = document.createElement("div");
//console.log(e.data.stats);
var out = "<br />total_inbound_connections:"+e.data.stats.total_inbound_connections;
out += "<br />total_outbound_connections:"+e.data.stats.total_outbound_connections;
for (var streamID in e.data.stats.inbound_stats){
out += "<br /><br /><b>streamID:</b> "+streamID+"<br />";
out += printValues(e.data.stats.inbound_stats[streamID]);
}
outputWindow.innerHTML = out;
iframeContainer.appendChild(outputWindow);
}
if ("gotChat" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = e.data.gotChat.msg;
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("action" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "child-page-action: "+e.data.action+"<br />";
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
console.log(e.data.action);
if (e.data.action == "new-view-connection"){
setTimeout(function(){
videos = iframe.contentWindow.document.querySelectorAll("video");
console.log(videos);
},500);
}
}
if ("streamIDs" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "child-page-action: streamIDs<br />";
for (var key in e.data.streamIDs) {
outputWindow.innerHTML += "streamID: " + key + ", label:"+e.data.streamIDs[key] + "\n";
}
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("loudness" in e.data){
//console.log(e.data);
if (document.getElementById("loudness")){
outputWindow = document.getElementById("loudness");
} else {
var outputWindow = document.createElement("div");
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
outputWindow.id = "loudness";
}
outputWindow.innerHTML = "child-page-action: loudness<br />";
for (var key in e.data.loudness) {
outputWindow.innerHTML += key + " Loudness: " + e.data.loudness[key] + "\n";
}
outputWindow.style.border="1px black";
}
if ("sensors" in e.data){
sensors = e.data.sensors;
if (document.getElementById("sensors")){
outputWindow = document.getElementById("sensors");
} else {
var outputWindow = document.createElement("div");
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
outputWindow.id = "sensors";
console.log(sensors);
}
outputWindow.innerHTML = "child-page-action: sensors<br /><br />";
for (var key in e.data.sensors.lin) {
outputWindow.innerHTML += key + " linear: " + e.data.sensors.lin[key] + "<br />";
}
for (var key in e.data.sensors.acc) {
outputWindow.innerHTML += key + " acceleration: " + e.data.sensors.acc[key] + "<br />";
}
for (var key in e.data.sensors.gyro) {
outputWindow.innerHTML += key + " gyro: " + e.data.sensors.gyro[key] + "<br />";
}
for (var key in e.data.sensors.mag) {
outputWindow.innerHTML += key + " magnet: " + e.data.sensors.mag[key] + "<br />";
}
for (var key in e.data.sensors.ori) {
outputWindow.innerHTML += key + " orientation: " + e.data.sensors.ori[key] + "<br />";
}
outputWindow.style.border="1px black";
}
});
}
function printValues( obj) {
var out = "";
for (var key in obj) {
if (typeof obj[key] === "object") {
out +="<br />";
out += printValues(obj[key]);
} else {
out +="<b>"+key+"</b>: "+obj[key]+"<br />";
}
}
return out;
}
</script>
</body>
</html>

172
examples/status.html Normal file
View File

@@ -0,0 +1,172 @@
<html>
<head>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OBSN Chat Overlay</title>
<style>
@font-face {
font-family: 'Cousine';
src: url('fonts/Cousine-Bold.ttf') format('truetype');
}
body {
margin:0;
padding:0 10px;
height:100%;
border: 0;
display: flex;
flex-direction: column-reverse;
position:absolute;
bottom:0;
overflow:hidden;
max-width:100%;
}
ul {
margin:0;
background-color: #0000;
color: white;
font-family: Cousine, monospace;
font-size: 3.2em;
line-height: 1.1em;
letter-spacing: 0.0em;
text-transform: uppercase;
padding: 0em;
text-shadow: 0.05em 0.05em 0px rgba(0,0,0,1);
max-width:100%;
}
ul li {
background-color: black;
padding: 8px 8px 0px 8px;
margin:0;
word-wrap: break-word;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
hyphens: auto;
max-width:100%;
}
a {
color:white;
font-size:1.2em;
text-transform: none;
word-wrap: break-word;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
hyphens: auto;
}
</style>
<script>
(function (w) {
w.URLSearchParams =
w.URLSearchParams ||
function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp("[\?&]" + name + "=([^&#]*)").exec(
self.searchString
);
if (results == null) {
return null;
} else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlParams = new URLSearchParams(window.location.search);
function loadIframe() {
var iframe = document.createElement("iframe");
var view= "";
if (urlParams.has("view")) {
view = "&view="+(urlParams.get("view") || "");
}
var room="";
if (urlParams.has("room")) {
room = "&room="+urlParams.get("room");
}
var password="";
if (urlParams.has("password")) {
password = "&password="+urlParams.get("password");
}
iframe.allow = "autoplay";
var srcString = "./?novideo&noaudio&label=chatOverlay&scene"+room+view+password;
iframe.src = srcString;
iframe.style.width="0";
iframe.style.height="0";
iframe.style.border="0";
document.body.appendChild(iframe);
//////////// LISTEN FOR EVENTS
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
/// If you have a routing system setup, you could have just one global listener for all iframes instead.
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
console.log(e);
if ("gotChat" in e.data){
logData(e.data.gotChat.label,e.data.gotChat.msg);
}
});
}
function printValues(obj) {
var out = "";
for (var key in obj) {
if (typeof obj[key] === "object") {
out += "<br />";
out += printValues(obj[key]);
} else {
if (key.startsWith("_")) {
} else {
out += "<b>" + key + "</b>: " + obj[key] + "<br />";
}
}
}
return out;
}
function logData(type, data) {
var log = document.body.getElementsByTagName("ul")[0];
var entry = document.createElement('li');
if (type){
type = "<i>"+type+"</i>";
}
entry.innerHTML = type + data;
//setTimeout(function(entry){ // hide message after 60 seconds
// entry.innerHTML="";
// entry.remove();
// },60000,entry);
log.appendChild(entry);
}
</script>
</head>
<body onload="loadIframe();">
<ul></ul>
</body>
</html>

111
examples/twitch.html Normal file
View File

@@ -0,0 +1,111 @@
<html>
<head><title>Twitch + Video</title>
<meta name="viewport" content="width=device-width, initial-scale=0.7, maximum-scale=1.0, user-scalable=yes" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<style>
body{
padding:0;
margin:0;
background-color:#003;
width:100%;
height:100%;
}
iframe {
width:100%;
height:100%;
border:0;
margin:0;
padding:0;
position:absolute;
display:block;
}
input{
padding:10px;
width:80%;
font-size:1.2em;
z-index: 1000;
}
@media screen and (orientation:portrait) {
#container2{
width:100%;height:100%;display:none;
}
#container1{
width: 50vw;height: 50vh; display:none; float:left; position: fixed; top: 0; right: 0%;
}
iframe{
width:100%;
}
}
@media screen and (orientation:landscape) {
#container2{
width:60vw;height:100%;display:none;
z-index:5;
}
#container1{
width: 50vw;height: 80vh; display:none; float:left; position: fixed; top: 0; right: -10vw;
}
iframe{
max-width:60vw;
}
}
</style>
</head>
<body>
<div id="container2"></div>
<div id="container1" ></div>
<div id="clean">
<input placeholder="Enter a VDON stream ID" id="viewlink" type="text" />
<input placeholder="Enter the Twitch channel name" id="twitch" type="text" />
<button onclick="loadIframes()" style="display:block;padding:10px;margin:10px;">START</button>
</div>
<script>
window.addEventListener("orientationchange", function() {
// Announce the new orientation number
// alert(window.orientation);
}, false);
function loadIframes(url=false){
var roomname = document.getElementById("viewlink").value;
var twitch = document.getElementById("twitch").value;
document.getElementById("clean").parentNode.removeChild(document.getElementById("clean"));
document.getElementById("container1").style.display="inline-block";
document.getElementById("container2").style.display="inline-block";
var path = window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
var room1 = "https://"+path+"/?push="+roomname+"&webcam&autostart&vd=front&ad=1&transparent&noheader";
var room2 = "https://www.twitch.tv/embed/"+twitch+"/chat?parent="+location.hostname;
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = room1;
var iframeContainer = document.createElement("div");
iframeContainer.appendChild(iframe);
document.getElementById("container1").appendChild(iframeContainer);
setTimeout(function(){
var iframe = document.createElement("iframe");
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
iframe.src = room2;
var iframeContainer = document.createElement("div");
iframeContainer.appendChild(iframe);
document.getElementById("container2").appendChild(iframeContainer);
},3000);
}
</script>
</body>
</html>

133
examples/webhid.html Normal file
View File

@@ -0,0 +1,133 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>WebHID Demo</title>
<style>
body {
text-align: center;
}
.button {
background-color: black;
border: none;
color: #00FF00;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
margin: 4px 2px;
cursor: pointer;
font-size: 24px;
}
#connected {
font-size: 24px;
max-height:700px;
overflow-y:scroll
}
#disconnectButton {
font-size: 24px;
}
</style>
</head>
<body>
<h1>STREAMDECK DEMO</h1>
<img src="./media/streamdeck.png" /><br />
<input class="button" type="button" id="connectButton" value="Connect" />
<input class="button" type="button" id="disconnectButton" style="display:none" value="Disconnect" />
<div id="connected" style>
</div>
<script>
const connectButton = document.getElementById("connectButton");
const disconnectButton = document.getElementById("disconnectButton");
const connect = document.getElementById("connect");
const deviceButtonPressed = document.getElementById("deviceButtonPressed");
var lastState = false;
//productId: 0x0060,
//class: models_1.StreamDeckOriginal,
//productId: 0x0063,
//class: models_1.StreamDeckMini,
//productId: 0x006c,
//class: models_1.StreamDeckXL,
//productId: 0x006d,
//class: models_1.StreamDeckOriginalV2,
document.addEventListener('DOMContentLoaded', async () => {
let devices = await navigator.hid.getDevices();
devices.forEach(device => {
console.log(`HID: ${device.productName}`);
});
});
function handleInputReport(e) {
console.log(e.device.productName + ": got input report " + e.reportId);
console.log(new Uint8Array(e.data.buffer));
var data = new Uint8Array(e.data.buffer);
if (lastState!==false){
for (var i=0;i<data.length;i++){
if (parseInt(data[i])!=data[i]){continue;}
if (lastState[i]!==data[i]){
if (data[i]){
document.getElementById("connected").innerHTML = "<br />Button "+(i+1)+" Pressed"+document.getElementById("connected").innerHTML;
} else {
document.getElementById("connected").innerHTML = "<br />Button "+(i+1)+" Released"+document.getElementById("connected").innerHTML;
}
} else {
if (data[i]){
document.getElementById("connected").innerHTML = "<br />Button "+(i+1)+" Pressed"+document.getElementById("connected").innerHTML;
}
}
}
}
lastState = data;
}
let device;
connectButton.onclick = async () => {
navigator.hid.requestDevice({
filters: [{ vendorId: 0x0fd9}] // elgato?
}).then((devices)=>{
console.log(devices);
device = devices[0];
console.log(`HID connected: ${device.productName}`);
document.getElementById("connected").innerHTML = "<br />Connected" +document.getElementById("connected").innerHTML;
document.getElementById("disconnectButton").style.display = "inline-block";
device.addEventListener("inputreport", handleInputReport);
//device.sendReport(outputReportId, outputReport).then(() => {
// console.log("Sent output report " + outputReportId);
//});
if (!device.opened){
device.open().then(()=>{
window.addEventListener("onbeforeunload", async () => {
await device.close();
});
}).catch(function(err){console.error(err);});
}
}).catch(function(err){console.error(err);});
};
disconnectButton.onclick = async () => {
await device.close();
//connected.style.display = "none";
//connectButton.style.display = "initial";
disconnectButton.style.display = "none";
};
</script>
</body>
</html>

182
examples/zoom.html Normal file
View File

@@ -0,0 +1,182 @@
<html>
<head><style>
span{margin:10px 0 0 0;display:block;}
body {
background-color:#cdf;
padding:0;
width;100%;height:100%
}
input{padding:5px;}
button {margin:10px 3px;}
#stream{
display:block;
}
</style></head>
<body id="main" style="margin:5%;"
<meta charset="utf-8"/>
<video id="video" autoplay="true" muted="true" playsinline style='height:420px;background-color:black;display:block;margin:0 0 10px 0;'></video>
<div id="devices">
<div class="select">
<label for="videoSource">Video source: </label><select id="videoSource"></select>
</div>
<div class="select">
<label for="audioSource">Audio source: </label><select id="audioSource"></select>
</div>
</div>
<button onclick="fullwindow()">FULL WINDOW</button>
<script>
window.onerror = function backupErr(errorMsg, url=false, lineNumber=false) {
console.error(errorMsg);
console.error(lineNumber);
console.error("Unhandeled Error occured"); //or any message
return false;
};
function fullwindow(){
videoElement.style.width="100%";
videoElement.style.padding= "0";
videoElement.style.margin="0";
videoElement.style.height="100%";
videoElement.style.zIndex="5";
videoElement.style.position = "absolute";
videoElement.style.top="0px";
videoElement.style.left="0px";
document.getElementById("main").style.overflow = "hidden";
videoElement.style.overflow = "hidden"
document.getElementById("main").style.backgroundColor="#000";
videoElement.style.cursor="none";
document.getElementById("main").style.cursor="none";
}
var videoElement = document.getElementById("video");
var gotDev = false;
async function gotDevices() {
if (gotDev){return;}
gotDev=true;
await navigator.mediaDevices.getUserMedia({audio:true, video:true}); // is needed to ask for permissinos.
navigator.mediaDevices.enumerateDevices().then((deviceInfos)=>{
for (let i = 0; i !== deviceInfos.length; ++i) {
var deviceInfo = deviceInfos[i];
var option = document.createElement("option");
option.value = deviceInfo.deviceId;
if (deviceInfo.kind === "audioinput") {
option.text = deviceInfo.label || "microphone " + (audioSelect.length + 1);
audioSelect.appendChild(option);
if (option.text.startsWith("CABLE")){
option.selected =true;
}
} else if (deviceInfo.kind === "videoinput") {
option.text = deviceInfo.label || "camera " + (videoSelect.length + 1);
if (option.text.startsWith("NewTek")){
continue;
}
videoSelect.appendChild(option);
if (option.text.startsWith("OBS")){
option.selected =true;
}
}
}
getStream();
});
}
function getStream() {
if (window.stream) {
window.stream.getTracks().forEach(function (track) {
track.stop();
log("TRack stopping");
});
}
const constraints = {
audio: {
deviceId: { exact: audioSelect.value },
echoCancellation : false,
autoGainControl : false,
noiseSuppression : false
},
video: {
deviceId: { exact: videoSelect.value },
width: { min: 1280, ideal: 1920, max: 1920 },
height: { min: 720, ideal: 1080, max: 1080 }
}
};
return navigator.mediaDevices.getUserMedia(constraints)
.then(gotStream)
.catch(console.error);
}
function gotStream(stream) {
if (window.stream) {
window.stream = stream; // make stream available to console
videoElement.srcObject = stream;
var senders = session.pc.getSenders();
videoElement.srcObject.getVideoTracks().forEach((track)=>{
var added = false;
senders.forEach((sender) => { // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
if (sender.track) {
if (sender.track && sender.track.kind == "video") {
sender.replaceTrack(track); // replace may not be supported by all browsers. eek.
track.enabled = notCensored;
added = true;
}
}
});
if (added==false){
session.pc.addTrack(track);
log("ADDED NOT REPLACED?");
}
});
videoElement.srcObject.getAudioTracks().forEach((track)=>{
var added = false;
senders.forEach((sender) => { // I suppose there could be a race condition between negotiating and updating this. if joining at the same time as changnig streams?
if (sender.track) {
if (sender.track && sender.track.kind == "audio") {
sender.replaceTrack(track); // replace may not be supported by all browsers. eek.
track.enabled = notCensored;
added = true;
}
}
});
if (added==false){
session.pc.addTrack(track);
log("ADDED NOT REPLACED?");
}
});
} else {
window.stream = stream; // make stream available to console
videoElement.srcObject = stream;
}
}
var audioSelect = document.querySelector("select#audioSource");
var videoSelect = document.querySelector("select#videoSource");
audioSelect.onchange = getStream;
videoSelect.onchange = getStream;
gotDevices();
</script>
</body>
</html>

143
filters/anon.js Normal file
View File

@@ -0,0 +1,143 @@
function effectsEngine(effectName){
function loadScript(url){
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = function(){
this.remove();
if (loadList.length){
loadScript(loadList.pop());
}
}
document.head.appendChild(script);
}
var loadList = [];
if (typeof JEELIZFACEFILTER == 'undefined' || JEELIZFACEFILTER==null){
loadList.push("./thirdparty/jeeliz/jeelizFaceFilter.js");
}
if (typeof THREE == 'undefined' || THREE == null){
loadList.push("./thirdparty/jeeliz/three/v112/three.min.js");
} else {
console.log("typeof THREE:"+typeof THREE);
}
if (typeof JeelizThreeHelper == 'undefined' || JeelizThreeHelper==null){
loadList.push("./thirdparty/jeeliz/JeelizThreeHelper.js");
}
if (typeof TWEEN == 'undefined' || TWEEN == null){
loadList.push("./thirdparty/jeeliz/Tween.min.js");
}
if (loadList.length){
loadList.reverse();
loadScript(loadList.pop());
}
// some globals:
let THREECAMERA = null; // should be prop of window
let ANONYMOUSMESH = null;
let ANONYMOUSOBJ3D = null;
let isTransformed = false;
var pathname = window.location.pathname.split("/");
pathname.pop();
pathname = window.location.protocol + "//" + window.location.host + pathname.join("/");
// callback: launched if a face is detected or lost.
function detect_callback(isDetected) {
// if (isDetected) {
// console.log('INFO in detect_callback(): DETECTED');
// } else {
// console.log('INFO in detect_callback(): LOST');
// }
}
// entry point:
function main(){
if (session.canvasSource && document.getElementById("effectsCanvasTarget") && JEELIZFACEFILTER){
try {
warnlog("LOADING JEELIZ");
THREECAMERA = null; // should be prop of window
ANONYMOUSMESH = null;
ANONYMOUSOBJ3D = null;
isTransformed = false;
init_faceFilter("effectsCanvasTarget", session.canvasSource);
} catch(e){
}
} else {
setTimeout(function(){main();},500);
warnlog("...retrying to load");
}
}
function init_faceFilter(canvasId, videoElement){
JEELIZFACEFILTER.init({
canvasId: canvasId,
NNCPath: pathname+'/thirdparty/jeeliz/neuralNets/',
videoSettings: {
videoElement: videoElement
},
callbackReady: function (errCode, spec) {
if (errCode) {
errorlog(errCode);
try{
JEELIZFACEFILTER.destroy();
} catch(e){}
THREECAMERA = null; // should be prop of window
ANONYMOUSMESH = null;
ANONYMOUSOBJ3D = null;
isTransformed = false;
setTimeout(function(){main();},500);
return;
}
const threeStuffs = JeelizThreeHelper.init(spec, detect_callback);
// CREATE OUR ANONYMOUS MASK:
const headLoader = new THREE.BufferGeometryLoader();
headLoader.load('./filters/anon/anonymous.json',(geometryHead) => {
const mat = new THREE.MeshLambertMaterial({
map: new THREE.TextureLoader().load('./filters/anon/anonymous.png'),
transparent: true
});
ANONYMOUSMESH = new THREE.Mesh(geometryHead, mat);
ANONYMOUSMESH.frustumCulled = false;
ANONYMOUSMESH.scale.multiplyScalar(0.065); // mask scale
ANONYMOUSMESH.position.fromArray([0, -0.75, 0.35]); // maskPositionOffset
ANONYMOUSMESH.renderOrder = 1000000;
ANONYMOUSMESH.material.opacity = 0;
ANONYMOUSOBJ3D = new THREE.Object3D();
ANONYMOUSOBJ3D.add(ANONYMOUSMESH);
threeStuffs.faceObject.add(ANONYMOUSOBJ3D);
});
THREECAMERA = JeelizThreeHelper.create_camera();
const ambient = new THREE.AmbientLight(0xffffff, 0.8);
threeStuffs.scene.add(ambient);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.position.set(100, 1000, 1000);
threeStuffs.scene.add(dirLight);
},
callbackTrack: function (detectState) {
if (effectName !== session.effects){
try{
JEELIZFACEFILTER.toggle_pause(true,false); // unload the filter when no longer active. Leaving the track active is required, else it breaks the app
} catch(e){errorlog(e);}
THREECAMERA = null; // should be prop of window
ANONYMOUSMESH = null;
ANONYMOUSOBJ3D = null;
isTransformed = false;
return;
}
const isDetected = JeelizThreeHelper.get_isDetected();
//if (isDetected && detectState.expressions[0] >= 0.8 && !isTransformed) { // If the person opens their mouth wide, then activate..
if (isDetected && !isTransformed){
isTransformed = true;
new TWEEN.Tween( ANONYMOUSMESH.material ).to({ opacity: 1}, 700).start(); // animation
}
TWEEN.update();
JeelizThreeHelper.render(detectState, THREECAMERA);
}
});
}
return main;
}

View File

@@ -0,0 +1,75 @@
function addVideoRecordingEffect(canvas) {
var viewWidth,
viewHeight,
canvas = document.getElementById("canvasVideoEffect"),
ctx;
// change these settings
var patternSize = 64,
patternScaleX = 3,
patternScaleY = 1,
patternRefreshInterval = 8,
patternAlpha = 25; // int between 0 and 255,
var patternPixelDataLength = patternSize * patternSize * 4,
patternCanvas,
patternCtx,
patternData,
frame = 0;
// create a canvas which will render the grain
function initCanvas() {
viewWidth = canvas.width = canvas.clientWidth;
viewHeight = canvas.height = canvas.clientHeight;
ctx = canvas.getContext('2d');
ctx.scale(patternScaleX, patternScaleY);
}
// create a canvas which will be used as a pattern
function initGrain() {
patternCanvas = document.createElement('canvas');
patternCanvas.width = patternSize;
patternCanvas.height = patternSize;
patternCtx = patternCanvas.getContext('2d');
patternData = patternCtx.createImageData(patternSize, patternSize);
}
// put a random shade of gray into every pixel of the pattern
function update() {
var value;
for (var i = 0; i < patternPixelDataLength; i += 1) {
value = (Math.random() * 155) | 0;
patternData.data[i ] = value;
patternData.data[i + 10] = value;
patternData.data[i + 15] = value;
patternData.data[i + 11] = patternAlpha;
}
patternCtx.putImageData(patternData, 0, 0);
}
// fill the canvas using the pattern
function draw() {
ctx.clearRect(0, 0, viewWidth, viewHeight);
ctx.fillStyle = ctx.createPattern(patternCanvas, 'repeat');
ctx.fillRect(0, 0, viewWidth, viewHeight);
}
function loop() {
if (++frame % patternRefreshInterval === 0) {
update();
draw();
}
requestAnimationFrame(loop);
}
initCanvas();
initGrain();
requestAnimationFrame(loop);
}

File diff suppressed because one or more lines are too long

BIN
filters/anon/anonymous.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

357
filters/dog.js Normal file
View File

@@ -0,0 +1,357 @@
function effectsEngine(effectName){
function loadScript(url){
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = function(){
this.remove();
if (loadList.length){
loadScript(loadList.pop());
}
}
document.head.appendChild(script);
}
var loadList = [];
if (typeof JEELIZFACEFILTER == 'undefined' || JEELIZFACEFILTER==null){
loadList.push("./thirdparty/jeeliz/jeelizFaceFilter.js");
}
if (typeof THREE == 'undefined' || THREE == null){
loadList.push("./thirdparty/jeeliz/three/v112/three.min.js");
} else {
console.log("typeof THREE:"+typeof THREE);
}
if (typeof JeelizThreeHelper == 'undefined' || JeelizThreeHelper==null){
loadList.push("./thirdparty/jeeliz/JeelizThreeHelper.js");
}
if (typeof TWEEN == 'undefined' || TWEEN == null){
loadList.push("./thirdparty/jeeliz/Tween.min.js");
}
loadList.push("./filters/dog/libs/glfx.js");
loadList.push("./thirdparty/jeeliz/three/customMaterials/FlexMaterial/ThreeFlexMaterial.js");
if (loadList.length){
loadList.reverse();
loadScript(loadList.pop());
}
var pathname = window.location.pathname.split("/");
pathname.pop();
pathname = window.location.protocol + "//" + window.location.host + pathname.join("/");
// some globals:
let THREECAMERA = null; // should be prop of window
let isTransformed = false;
let ISDETECTED = false;
let NOSEMESH = null, EARMESH = null;
let DOGOBJ3D = null, FRAMEOBJ3D = null;
let ISOVERTHRESHOLD = false, ISUNDERTRESHOLD = true;
let ISLOADED = false;
let MIXER = null;
let ACTION = null;
let ISANIMATING = false;
let ISOPAQUE = false;
let ISANIMATIONOVER = false;
let _flexParts = [];
let _videoGeometry = null;
// callback: launched if a face is detected or lost.
function detect_callback(isDetected) {
// if (isDetected) {
// console.log('INFO in detect_callback(): DETECTED');
// } else {
// console.log('INFO in detect_callback(): LOST');
// }
}
function create_mat2d(threeTexture, isTransparent){ // MT216: we put the creation of the video material in a func because we will also use it for the frame
return new THREE.RawShaderMaterial({
depthWrite: false,
depthTest: false,
transparent: isTransparent,
vertexShader: "attribute vec2 position;\n\
varying vec2 vUV;\n\
void main(void){\n\
gl_Position = vec4(position, 0., 1.);\n\
vUV = 0.5 + 0.5 * position;\n\
}",
fragmentShader: "precision lowp float;\n\
uniform sampler2D samplerVideo;\n\
varying vec2 vUV;\n\
void main(void){\n\
gl_FragColor = texture2D(samplerVideo, vUV);\n\
}",
uniforms:{
samplerVideo: { value: threeTexture }
}
});
}
function applyFilter() {
let canvas;
try {
canvas = fx.canvas();
} catch (e) {
alert('Ow no! WebGL isn\'t supported...')
return
}
const tempImage = new Image(512, 512);
tempImage.src = './filters/dog/images/texture_pink.jpg';
tempImage.onload = () => {
const texture = canvas.texture(tempImage);
// Create the effet
canvas.draw(texture).vignette(0.5, 0.6).update();
const canvasOpacity = document.createElement('canvas');
canvasOpacity.width = 512;
canvasOpacity.height = 512;
const ctx = canvasOpacity.getContext('2d');
ctx.globalAlpha = 0.2
ctx.drawImage(canvas, 0, 0, 512, 512);
// Add the effect
const calqueMesh = new THREE.Mesh(_videoGeometry, create_mat2d(new THREE.TextureLoader().load(canvasOpacity.toDataURL('image/png')), true))
calqueMesh.material.opacity = 0.2;
calqueMesh.material.transparent = true;
calqueMesh.renderOrder = 999; // render last
calqueMesh.frustumCulled = false;
FRAMEOBJ3D.add(calqueMesh);
}
}
// build the 3D. called once when Jeeliz Face Filter is OK
function init_threeScene(spec) {
// INIT THE THREE.JS context
const threeStuffs = JeelizThreeHelper.init(spec, detect_callback);
_videoGeometry = threeStuffs.videoMesh.geometry;
// CREATE OUR DOG EARS:
// let's begin by creating a loading manager that will allow us to
// have more control over the three parts of our dog model
const loadingManager = new THREE.LoadingManager();
const loaderEars = new THREE.BufferGeometryLoader(loadingManager);
loaderEars.load(
'./filters/dog/models/dog/dog_ears.json',
function (geometry) {
const mat = new THREE.FlexMaterial({
map: new THREE.TextureLoader().load('./filters/dog/models/dog/texture_ears.jpg'),
flexMap: new THREE.TextureLoader().load('./filters/dog/models/dog/flex_ears_256.jpg'),
alphaMap: new THREE.TextureLoader().load('./filters/dog/models/dog/alpha_ears_256.jpg'),
transparent: true,
opacity: 1,
bumpMap: new THREE.TextureLoader().load('./filters/dog/models/dog/normal_ears.jpg'),
bumpScale: 0.0075,
shininess: 1.5,
specular: 0xffffff,
});
EARMESH = new THREE.Mesh(geometry, mat);
EARMESH.scale.multiplyScalar(0.025);
EARMESH.position.setY(-0.3);
EARMESH.frustumCulled = false;
EARMESH.renderOrder = 10000;
EARMESH.material.opacity.value = 1;
}
);
// CREATE OUR DOG NOSE
const loaderNose = new THREE.BufferGeometryLoader(loadingManager);
loaderNose.load(
'./filters/dog/models/dog/dog_nose.json',
function (geometry) {
const mat = new THREE.MeshPhongMaterial({
map: new THREE.TextureLoader().load('./filters/dog/models/dog/texture_nose.jpg'),
shininess: 1.5,
specular: 0xffffff,
bumpMap: new THREE.TextureLoader().load('./filters/dog/models/dog/normal_nose.jpg'),
bumpScale: 0.005
});
NOSEMESH = new THREE.Mesh(geometry, mat);
NOSEMESH.scale.multiplyScalar(0.018);
NOSEMESH.position.setY(-0.05);
NOSEMESH.position.setZ(0.15);
NOSEMESH.frustumCulled = false;
NOSEMESH.renderOrder = 10000;
}
);
loadingManager.onLoad = () => {
DOGOBJ3D.add(EARMESH);
DOGOBJ3D.add(NOSEMESH);
threeStuffs.faceObject.add(DOGOBJ3D);
ISLOADED = true;
}
// CREATE AN AMBIENT LIGHT
const ambient = new THREE.AmbientLight(0xffffff, 0.8);
threeStuffs.scene.add(ambient);
// CREAT A DIRECTIONALLIGHT
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.position.set(100, 1000, 1000);
threeStuffs.scene.add(dirLight);
// CREATE THE CAMERA
THREECAMERA = JeelizThreeHelper.create_camera();
threeStuffs.scene.add(FRAMEOBJ3D);
// Add filter
applyFilter();
} // end init_threeScene()
function animateTongue (mesh, isReverse) {
mesh.visible = true;
if (isReverse) {
ACTION.timescale = -1;
ACTION.paused = false;
setTimeout(() => {
ACTION.paused = true;
ISOPAQUE = false;
ISANIMATING = false;
ISANIMATIONOVER = true;
new TWEEN.Tween(mesh.material.opacity)
.to({ value: 0 }, 150)
.start();
}, 150);
} else {
ACTION.timescale = 1;
ACTION.reset();
ACTION.paused = false;
new TWEEN.Tween(mesh.material.opacity)
.to({ value: 1 }, 100)
.onComplete(() => {
ISOPAQUE = true;
setTimeout(() => {
ACTION.paused = true;
ISANIMATING = false;
ISANIMATIONOVER = true;
}, 150);
})
.start();
}
}
// entry point:
function main(){
if (session.canvasSource && document.getElementById("effectsCanvasTarget") && JEELIZFACEFILTER){
try {
warnlog("LOADING JEELIZ");
THREECAMERA = null; // should be prop of window
isTransformed = false;
DOGOBJ3D = new THREE.Object3D();
FRAMEOBJ3D = new THREE.Object3D();
init_faceFilter("effectsCanvasTarget", session.canvasSource);
} catch(e){
}
} else {
setTimeout(function(){main();},500);
warnlog("...retrying to load");
}
}
function init_faceFilter(canvasId, videoElement){
JEELIZFACEFILTER.init({
canvasId: canvasId,
NNCPath: pathname+'/thirdparty/jeeliz/neuralNets/',
videoSettings: {
videoElement: videoElement
},
callbackReady: function (errCode, spec) {
if (errCode) {
errorlog(errCode);
try{
JEELIZFACEFILTER.destroy();
} catch(e){}
THREECAMERA = null; // should be prop of window
DOGOBJ3D=null;
FRAMEOBJ3D=null;
isTransformed = false;
setTimeout(function(){main();},500);
return;
}
init_threeScene(spec);
},
callbackTrack: function (detectState) {
if (effectName !== session.effects){
try{
JEELIZFACEFILTER.toggle_pause(true,false); // unload the filter when no longer active. Leaving the track active is required, else it breaks the app
} catch(e){errorlog(e);}
THREECAMERA = null; // should be prop of window
isTransformed = false;
DOGOBJ3D=null;
FRAMEOBJ3D=null;
return;
}
const ISDETECTED = JeelizThreeHelper.get_isDetected();
//if (isDetected && detectState.expressions[0] >= 0.8 && !isTransformed) { // If the person opens their mouth wide, then activate..
if (ISDETECTED){
const _quat = new THREE.Quaternion();
const _eul = new THREE.Euler();
_eul.setFromQuaternion(_quat);
// flex ears material:
if (EARMESH && EARMESH.material.set_amortized){
EARMESH.material.set_amortized(
EARMESH.getWorldPosition(new THREE.Vector3(0,0,0)),
EARMESH.getWorldScale(new THREE.Vector3(0,0,0)),
EARMESH.getWorldQuaternion(_eul),
false,
0.1
);
}
if (detectState.expressions[0] >= 0.85 && !ISOVERTHRESHOLD) {
ISOVERTHRESHOLD = true;
ISUNDERTRESHOLD = false;
ISANIMATIONOVER = false;
}
if (detectState.expressions[0] <= 0.1 && !ISUNDERTRESHOLD) {
ISOVERTHRESHOLD = false;
ISUNDERTRESHOLD = true;
ISANIMATIONOVER = false;
}
}
TWEEN.update();
if (ISOPAQUE) {
MIXER.update(0.16);
}
JeelizThreeHelper.render(detectState, THREECAMERA);
}
});
}
return main;
}

View File

@@ -0,0 +1,132 @@
"use strict";
THREE.FlexMaterial = function(spec){
const _worldMatrixDelayed = new THREE['Matrix4']();
function mix(a,b,t){
a.set(
b.x*t+a.x*(1-t),
b.y*t+a.y*(1-t),
b.z*t+a.z*(1-t)
);
}
// tweak shaders helpers:
function tweak_shaderAdd(code, chunk, glslCode){
return code.replace(chunk, chunk+"\n"+glslCode);
}
function tweak_shaderDel(code, chunk){
return code.replace(chunk, '');
}
function tweak_shaderRepl(code, chunk, glslCode){
return code.replace(chunk, glslCode);
}
// get PHONG shader and tweak it :
const phongShader = THREE.ShaderLib.phong;
let vertexShaderSource = phongShader.vertexShader;
vertexShaderSource = tweak_shaderAdd(vertexShaderSource, '#include <common>',
'uniform mat4 modelMatrixDelayed;\n'
+'uniform sampler2D flexMap;\n'
);
vertexShaderSource = tweak_shaderDel(vertexShaderSource, '#include <worldpos_vertex>');
vertexShaderSource = tweak_shaderRepl(vertexShaderSource, '#include <project_vertex>',
"vec4 worldPosition = modelMatrix * vec4( transformed, 1.0 );\n\
vec4 worldPositionDelayed = modelMatrixDelayed * vec4( transformed, 1.0 );\n\
worldPosition = mix(worldPosition, worldPositionDelayed, texture2D(flexMap, uv).r);\n\
vec4 mvPosition = viewMatrix* worldPosition;\n\
gl_Position = projectionMatrix * mvPosition;");
const uniforms0 = {
'modelMatrixDelayed': {
'value': _worldMatrixDelayed
},
'flexMap': {
value: spec.flexMap
},
'opacity': {
value: (typeof(spec.opacity)!=='undefined')?spec.opacity:1
}
};
const uniforms = Object.assign({}, phongShader.uniforms, uniforms0);
const isMorphs = (spec.morphTargets) ? true : false;
const mat = new THREE.ShaderMaterial({
vertexShader: vertexShaderSource,
fragmentShader: phongShader.fragmentShader,
uniforms: uniforms,
transparent: (spec.transparent)?true:false,
lights: true,
morphTargets: isMorphs,
morphNormals: isMorphs
});
mat.flexMap = spec.flexMap;
mat.opacity = mat.uniforms.opacity; // shortcut
if (typeof(spec.map)!=='undefined') {
uniforms.map = {value: spec.map};
mat.map = spec.map;
}
if (typeof(spec.alphaMap)!=='undefined') {
uniforms.alphaMap = {value: spec.alphaMap};
mat.transparent = true;
mat.alphaMap = spec.alphaMap;
}
if (typeof(spec.bumpMap)!=='undefined') {
uniforms.bumpMap = {value: spec.bumpMap};
mat.bumpMap = spec.bumpMap;
}
if (typeof(spec.bumpScale)!=='undefined') {
uniforms.bumpScale = {value: spec.bumpScale};
mat.bumpScale = spec.bumpScale;
}
if (typeof(spec.shininess)!=='undefined') {
uniforms.shininess = {value: spec.shininess};
mat.shininess = spec.shininess;
}
const _positionDelayed = new THREE.Vector3();
const _scaleDelayed = new THREE.Vector3();
const _eulerDelayed = new THREE['Euler']();
let _initialized = false;
mat.set_amortized = function(positionTarget, scaleTarget, eulerTarget, parentMatrix, amortization){
if (!_initialized){
if (positionTarget){
_positionDelayed.copy(positionTarget);
}
if (scaleTarget){
_scaleDelayed.copy(scaleTarget);
}
if (eulerTarget){
_eulerDelayed.copy(eulerTarget);
}
_initialized = true;
}
if (eulerTarget){
mix( _eulerDelayed, eulerTarget, amortization );
_worldMatrixDelayed['makeRotationFromEuler'](_eulerDelayed);
}
if (positionTarget){
mix( _positionDelayed, positionTarget, amortization );
_worldMatrixDelayed['setPosition'](_positionDelayed);
}
if (scaleTarget){
mix(_scaleDelayed, scaleTarget, amortization );
_worldMatrixDelayed['scale'](_scaleDelayed);
}
if (parentMatrix){
_worldMatrixDelayed.multiplyMatrices(parentMatrix, _worldMatrixDelayed);
}
}
return mat;
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

87
filters/dog/index.html Normal file
View File

@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="content-language" content="en-EN" />
<title>JEELIZ FACEFILTER: DOG</title>
<!-- INCLUDE JEELIZ FACEFILTER SCRIPT -->
<script src="../../../dist/jeelizFaceFilter.js"></script>
<!-- INCLUDE THREE.JS -->
<script src="../../../libs/three/v97/three.js"></script>
<!-- INCLUDE JEELIZRESIZER -->
<script src="../../../helpers/JeelizResizer.js"></script>
<!-- INCLUDE JEELIZTHREEJSHELPER -->
<script src="../../../helpers/JeelizThreeHelper.js"></script>
<!-- INCLUDE FLEXMATERIAL (CUSTOM DEV) -->
<script src="../../../libs/three/customMaterials/FlexMaterial/ThreeFlexMaterial.js"></script>
<!-- INCLUDE TWEEN.JS -->
<script src='../../../libs/tween/v16_3_5/Tween.min.js'></script>
<!-- INCLUDE JQUERY -->
<script src='../../../libs/jquery/jquery-3.3.1.min.js'></script>
<!-- INCLUDE GLFX -->
<script src='libs/glfx.js'></script>
<!-- INCLUDE DEMO SCRIPT -->
<script src="./main.js"></script>
<!-- INCLUDE ADDDRAGEVENTLISTENER.JS -->
<script src='../../../helpers/addDragEventListener.js'></script>
<!-- INCLUDE FORK ME ON GITHUB BANNER -->
<script src="../../appearance/widget.js"></script>
<link rel="stylesheet" href="../../appearance/style.css" type="text/css" />
<style>
.canvasContainer {
position: relative;
margin: 0 auto;
text-align: center;
}
#jeeFaceFilterCanvas {
z-index: 0;
max-height: 100%;
left: auto;
top: auto;
width: 100vmin;
transform: translate(0,0) rotateY(180deg);
position: static;
}
img {
max-width: 100%;
}
#filter {
position: absolute;
z-index: 0;
max-height: 100%;
width: 100vmin;
top: 0;
left: 50%;
transform: translate(-50%);
opacity: 0.15;
}
#filter canvas {
width: 100%;
height: 100%;
}
</style>
</head>
<body onload="main()">
<div class="canvasContainer">
<canvas width="600" height="600" id='jeeFaceFilterCanvas'></canvas>
<div id='filter'></div>
</div>
</body>
</html>

59
filters/dog/libs/glfx.js Normal file
View File

@@ -0,0 +1,59 @@
/*
* glfx.js
* http://evanw.github.com/glfx.js/
*
* Copyright 2011 Evan Wallace
* Released under the MIT license
*/
var fx=function(){function q(a,d,c){return Math.max(a,Math.min(d,c))}function w(b){return{_:b,loadContentsOf:function(b){a=this._.gl;this._.loadContentsOf(b)},destroy:function(){a=this._.gl;this._.destroy()}}}function A(a){return w(r.fromElement(a))}function B(b,d){var c=a.UNSIGNED_BYTE;if(a.getExtension("OES_texture_float")&&a.getExtension("OES_texture_float_linear")){var e=new r(100,100,a.RGBA,a.FLOAT);try{e.drawTo(function(){c=a.FLOAT})}catch(g){}e.destroy()}this._.texture&&this._.texture.destroy();
this._.spareTexture&&this._.spareTexture.destroy();this.width=b;this.height=d;this._.texture=new r(b,d,a.RGBA,c);this._.spareTexture=new r(b,d,a.RGBA,c);this._.extraTexture=this._.extraTexture||new r(0,0,a.RGBA,c);this._.flippedShader=this._.flippedShader||new h(null,"uniform sampler2D texture;varying vec2 texCoord;void main(){gl_FragColor=texture2D(texture,vec2(texCoord.x,1.0-texCoord.y));}");this._.isInitialized=!0}function C(a,d,c){this._.isInitialized&&
a._.width==this.width&&a._.height==this.height||B.call(this,d?d:a._.width,c?c:a._.height);a._.use();this._.texture.drawTo(function(){h.getDefaultShader().drawRect()});return this}function D(){this._.texture.use();this._.flippedShader.drawRect();return this}function f(a,d,c,e){(c||this._.texture).use();this._.spareTexture.drawTo(function(){a.uniforms(d).drawRect()});this._.spareTexture.swapWith(e||this._.texture)}function E(a){a.parentNode.insertBefore(this,a);a.parentNode.removeChild(a);return this}
function F(){var b=new r(this._.texture.width,this._.texture.height,a.RGBA,a.UNSIGNED_BYTE);this._.texture.use();b.drawTo(function(){h.getDefaultShader().drawRect()});return w(b)}function G(){var b=this._.texture.width,d=this._.texture.height,c=new Uint8Array(4*b*d);this._.texture.drawTo(function(){a.readPixels(0,0,b,d,a.RGBA,a.UNSIGNED_BYTE,c)});return c}function k(b){return function(){a=this._.gl;return b.apply(this,arguments)}}function x(a,d,c,e,g,l,n,p){var m=c-g,h=e-l,f=n-g,k=p-l;g=a-c+g-n;l=
d-e+l-p;var q=m*k-f*h,f=(g*k-f*l)/q,m=(m*l-g*h)/q;return[c-a+f*c,e-d+f*e,f,n-a+m*n,p-d+m*p,m,a,d,1]}function y(a){var d=a[0],c=a[1],e=a[2],g=a[3],l=a[4],n=a[5],p=a[6],m=a[7];a=a[8];var f=d*l*a-d*n*m-c*g*a+c*n*p+e*g*m-e*l*p;return[(l*a-n*m)/f,(e*m-c*a)/f,(c*n-e*l)/f,(n*p-g*a)/f,(d*a-e*p)/f,(e*g-d*n)/f,(g*m-l*p)/f,(c*p-d*m)/f,(d*l-c*g)/f]}function z(a){var d=a.length;this.xa=[];this.ya=[];this.u=[];this.y2=[];a.sort(function(a,b){return a[0]-b[0]});for(var c=0;c<d;c++)this.xa.push(a[c][0]),this.ya.push(a[c][1]);
this.u[0]=0;this.y2[0]=0;for(c=1;c<d-1;++c){a=this.xa[c+1]-this.xa[c-1];var e=(this.xa[c]-this.xa[c-1])/a,g=e*this.y2[c-1]+2;this.y2[c]=(e-1)/g;this.u[c]=(6*((this.ya[c+1]-this.ya[c])/(this.xa[c+1]-this.xa[c])-(this.ya[c]-this.ya[c-1])/(this.xa[c]-this.xa[c-1]))/a-e*this.u[c-1])/g}this.y2[d-1]=0;for(c=d-2;0<=c;--c)this.y2[c]=this.y2[c]*this.y2[c+1]+this.u[c]}function u(a,d){return new h(null,a+"uniform sampler2D texture;uniform vec2 texSize;varying vec2 texCoord;void main(){vec2 coord=texCoord*texSize;"+
d+"gl_FragColor=texture2D(texture,coord/texSize);vec2 clampedCoord=clamp(coord,vec2(0.0),texSize);if(coord!=clampedCoord){gl_FragColor.a*=max(0.0,1.0-length(coord-clampedCoord));}}")}function H(b,d){a.brightnessContrast=a.brightnessContrast||new h(null,"uniform sampler2D texture;uniform float brightness;uniform float contrast;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);color.rgb+=brightness;if(contrast>0.0){color.rgb=(color.rgb-0.5)/(1.0-contrast)+0.5;}else{color.rgb=(color.rgb-0.5)*(1.0+contrast)+0.5;}gl_FragColor=color;}");
f.call(this,a.brightnessContrast,{brightness:q(-1,b,1),contrast:q(-1,d,1)});return this}function t(a){a=new z(a);for(var d=[],c=0;256>c;c++)d.push(q(0,Math.floor(256*a.interpolate(c/255)),255));return d}function I(b,d,c){b=t(b);1==arguments.length?d=c=b:(d=t(d),c=t(c));for(var e=[],g=0;256>g;g++)e.splice(e.length,0,b[g],d[g],c[g],255);this._.extraTexture.initFromBytes(256,1,e);this._.extraTexture.use(1);a.curves=a.curves||new h(null,"uniform sampler2D texture;uniform sampler2D map;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);color.r=texture2D(map,vec2(color.r)).r;color.g=texture2D(map,vec2(color.g)).g;color.b=texture2D(map,vec2(color.b)).b;gl_FragColor=color;}");
a.curves.textures({map:1});f.call(this,a.curves,{});return this}function J(b){a.denoise=a.denoise||new h(null,"uniform sampler2D texture;uniform float exponent;uniform float strength;uniform vec2 texSize;varying vec2 texCoord;void main(){vec4 center=texture2D(texture,texCoord);vec4 color=vec4(0.0);float total=0.0;for(float x=-4.0;x<=4.0;x+=1.0){for(float y=-4.0;y<=4.0;y+=1.0){vec4 sample=texture2D(texture,texCoord+vec2(x,y)/texSize);float weight=1.0-abs(dot(sample.rgb-center.rgb,vec3(0.25)));weight=pow(weight,exponent);color+=sample*weight;total+=weight;}}gl_FragColor=color/total;}");
for(var d=0;2>d;d++)f.call(this,a.denoise,{exponent:Math.max(0,b),texSize:[this.width,this.height]});return this}function K(b,d){a.hueSaturation=a.hueSaturation||new h(null,"uniform sampler2D texture;uniform float hue;uniform float saturation;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);float angle=hue*3.14159265;float s=sin(angle),c=cos(angle);vec3 weights=(vec3(2.0*c,-sqrt(3.0)*s-c,sqrt(3.0)*s-c)+1.0)/3.0;float len=length(color.rgb);color.rgb=vec3(dot(color.rgb,weights.xyz),dot(color.rgb,weights.zxy),dot(color.rgb,weights.yzx));float average=(color.r+color.g+color.b)/3.0;if(saturation>0.0){color.rgb+=(average-color.rgb)*(1.0-1.0/(1.001-saturation));}else{color.rgb+=(average-color.rgb)*(-saturation);}gl_FragColor=color;}");
f.call(this,a.hueSaturation,{hue:q(-1,b,1),saturation:q(-1,d,1)});return this}function L(b){a.noise=a.noise||new h(null,"uniform sampler2D texture;uniform float amount;varying vec2 texCoord;float rand(vec2 co){return fract(sin(dot(co.xy,vec2(12.9898,78.233)))*43758.5453);}void main(){vec4 color=texture2D(texture,texCoord);float diff=(rand(texCoord)-0.5)*amount;color.r+=diff;color.g+=diff;color.b+=diff;gl_FragColor=color;}");
f.call(this,a.noise,{amount:q(0,b,1)});return this}function M(b){a.sepia=a.sepia||new h(null,"uniform sampler2D texture;uniform float amount;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);float r=color.r;float g=color.g;float b=color.b;color.r=min(1.0,(r*(1.0-(0.607*amount)))+(g*(0.769*amount))+(b*(0.189*amount)));color.g=min(1.0,(r*0.349*amount)+(g*(1.0-(0.314*amount)))+(b*0.168*amount));color.b=min(1.0,(r*0.272*amount)+(g*0.534*amount)+(b*(1.0-(0.869*amount))));gl_FragColor=color;}");
f.call(this,a.sepia,{amount:q(0,b,1)});return this}function N(b,d){a.unsharpMask=a.unsharpMask||new h(null,"uniform sampler2D blurredTexture;uniform sampler2D originalTexture;uniform float strength;uniform float threshold;varying vec2 texCoord;void main(){vec4 blurred=texture2D(blurredTexture,texCoord);vec4 original=texture2D(originalTexture,texCoord);gl_FragColor=mix(blurred,original,1.0+strength);}");
this._.extraTexture.ensureFormat(this._.texture);this._.texture.use();this._.extraTexture.drawTo(function(){h.getDefaultShader().drawRect()});this._.extraTexture.use(1);this.triangleBlur(b);a.unsharpMask.textures({originalTexture:1});f.call(this,a.unsharpMask,{strength:d});this._.extraTexture.unuse(1);return this}function O(b){a.vibrance=a.vibrance||new h(null,"uniform sampler2D texture;uniform float amount;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);float average=(color.r+color.g+color.b)/3.0;float mx=max(color.r,max(color.g,color.b));float amt=(mx-average)*(-amount*3.0);color.rgb=mix(color.rgb,vec3(mx),amt);gl_FragColor=color;}");
f.call(this,a.vibrance,{amount:q(-1,b,1)});return this}function P(b,d){a.vignette=a.vignette||new h(null,"uniform sampler2D texture;uniform float size;uniform float amount;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);float dist=distance(texCoord,vec2(0.5,0.5));color.rgb*=smoothstep(0.8,size*0.799,dist*(amount+size));gl_FragColor=color;}");
f.call(this,a.vignette,{size:q(0,b,1),amount:q(0,d,1)});return this}function Q(b,d,c){a.lensBlurPrePass=a.lensBlurPrePass||new h(null,"uniform sampler2D texture;uniform float power;varying vec2 texCoord;void main(){vec4 color=texture2D(texture,texCoord);color=pow(color,vec4(power));gl_FragColor=vec4(color);}");var e="uniform sampler2D texture0;uniform sampler2D texture1;uniform vec2 delta0;uniform vec2 delta1;uniform float power;varying vec2 texCoord;"+
s+"vec4 sample(vec2 delta){float offset=random(vec3(delta,151.7182),0.0);vec4 color=vec4(0.0);float total=0.0;for(float t=0.0;t<=30.0;t++){float percent=(t+offset)/30.0;color+=texture2D(texture0,texCoord+delta*percent);total+=1.0;}return color/total;}";
a.lensBlur0=a.lensBlur0||new h(null,e+"void main(){gl_FragColor=sample(delta0);}");a.lensBlur1=a.lensBlur1||new h(null,e+"void main(){gl_FragColor=(sample(delta0)+sample(delta1))*0.5;}");a.lensBlur2=a.lensBlur2||(new h(null,e+"void main(){vec4 color=(sample(delta0)+2.0*texture2D(texture1,texCoord))/3.0;gl_FragColor=pow(color,vec4(power));}")).textures({texture1:1});for(var e=
[],g=0;3>g;g++){var l=c+2*g*Math.PI/3;e.push([b*Math.sin(l)/this.width,b*Math.cos(l)/this.height])}b=Math.pow(10,q(-1,d,1));f.call(this,a.lensBlurPrePass,{power:b});this._.extraTexture.ensureFormat(this._.texture);f.call(this,a.lensBlur0,{delta0:e[0]},this._.texture,this._.extraTexture);f.call(this,a.lensBlur1,{delta0:e[1],delta1:e[2]},this._.extraTexture,this._.extraTexture);f.call(this,a.lensBlur0,{delta0:e[1]});this._.extraTexture.use(1);f.call(this,a.lensBlur2,{power:1/b,delta0:e[2]});return this}
function R(b,d,c,e,g,l){a.tiltShift=a.tiltShift||new h(null,"uniform sampler2D texture;uniform float blurRadius;uniform float gradientRadius;uniform vec2 start;uniform vec2 end;uniform vec2 delta;uniform vec2 texSize;varying vec2 texCoord;"+s+"void main(){vec4 color=vec4(0.0);float total=0.0;float offset=random(vec3(12.9898,78.233,151.7182),0.0);vec2 normal=normalize(vec2(start.y-end.y,end.x-start.x));float radius=smoothstep(0.0,1.0,abs(dot(texCoord*texSize-start,normal))/gradientRadius)*blurRadius;for(float t=-30.0;t<=30.0;t++){float percent=(t+offset-0.5)/30.0;float weight=1.0-abs(percent);vec4 sample=texture2D(texture,texCoord+delta/texSize*percent*radius);sample.rgb*=sample.a;color+=sample*weight;total+=weight;}gl_FragColor=color/total;gl_FragColor.rgb/=gl_FragColor.a+0.00001;}");
var n=c-b,p=e-d,m=Math.sqrt(n*n+p*p);f.call(this,a.tiltShift,{blurRadius:g,gradientRadius:l,start:[b,d],end:[c,e],delta:[n/m,p/m],texSize:[this.width,this.height]});f.call(this,a.tiltShift,{blurRadius:g,gradientRadius:l,start:[b,d],end:[c,e],delta:[-p/m,n/m],texSize:[this.width,this.height]});return this}function S(b){a.triangleBlur=a.triangleBlur||new h(null,"uniform sampler2D texture;uniform vec2 delta;varying vec2 texCoord;"+s+"void main(){vec4 color=vec4(0.0);float total=0.0;float offset=random(vec3(12.9898,78.233,151.7182),0.0);for(float t=-30.0;t<=30.0;t++){float percent=(t+offset-0.5)/30.0;float weight=1.0-abs(percent);vec4 sample=texture2D(texture,texCoord+delta*percent);sample.rgb*=sample.a;color+=sample*weight;total+=weight;}gl_FragColor=color/total;gl_FragColor.rgb/=gl_FragColor.a+0.00001;}");
f.call(this,a.triangleBlur,{delta:[b/this.width,0]});f.call(this,a.triangleBlur,{delta:[0,b/this.height]});return this}function T(b,d,c){a.zoomBlur=a.zoomBlur||new h(null,"uniform sampler2D texture;uniform vec2 center;uniform float strength;uniform vec2 texSize;varying vec2 texCoord;"+s+"void main(){vec4 color=vec4(0.0);float total=0.0;vec2 toCenter=center-texCoord*texSize;float offset=random(vec3(12.9898,78.233,151.7182),0.0);for(float t=0.0;t<=40.0;t++){float percent=(t+offset)/40.0;float weight=4.0*(percent-percent*percent);vec4 sample=texture2D(texture,texCoord+toCenter*percent*strength/texSize);sample.rgb*=sample.a;color+=sample*weight;total+=weight;}gl_FragColor=color/total;gl_FragColor.rgb/=gl_FragColor.a+0.00001;}");
f.call(this,a.zoomBlur,{center:[b,d],strength:c,texSize:[this.width,this.height]});return this}function U(b,d,c,e){a.colorHalftone=a.colorHalftone||new h(null,"uniform sampler2D texture;uniform vec2 center;uniform float angle;uniform float scale;uniform vec2 texSize;varying vec2 texCoord;float pattern(float angle){float s=sin(angle),c=cos(angle);vec2 tex=texCoord*texSize-center;vec2 point=vec2(c*tex.x-s*tex.y,s*tex.x+c*tex.y)*scale;return(sin(point.x)*sin(point.y))*4.0;}void main(){vec4 color=texture2D(texture,texCoord);vec3 cmy=1.0-color.rgb;float k=min(cmy.x,min(cmy.y,cmy.z));cmy=(cmy-k)/(1.0-k);cmy=clamp(cmy*10.0-3.0+vec3(pattern(angle+0.26179),pattern(angle+1.30899),pattern(angle)),0.0,1.0);k=clamp(k*10.0-5.0+pattern(angle+0.78539),0.0,1.0);gl_FragColor=vec4(1.0-cmy-k,color.a);}");
f.call(this,a.colorHalftone,{center:[b,d],angle:c,scale:Math.PI/e,texSize:[this.width,this.height]});return this}function V(b,d,c,e){a.dotScreen=a.dotScreen||new h(null,"uniform sampler2D texture;uniform vec2 center;uniform float angle;uniform float scale;uniform vec2 texSize;varying vec2 texCoord;float pattern(){float s=sin(angle),c=cos(angle);vec2 tex=texCoord*texSize-center;vec2 point=vec2(c*tex.x-s*tex.y,s*tex.x+c*tex.y)*scale;return(sin(point.x)*sin(point.y))*4.0;}void main(){vec4 color=texture2D(texture,texCoord);float average=(color.r+color.g+color.b)/3.0;gl_FragColor=vec4(vec3(average*10.0-5.0+pattern()),color.a);}");
f.call(this,a.dotScreen,{center:[b,d],angle:c,scale:Math.PI/e,texSize:[this.width,this.height]});return this}function W(b){a.edgeWork1=a.edgeWork1||new h(null,"uniform sampler2D texture;uniform vec2 delta;varying vec2 texCoord;"+s+"void main(){vec2 color=vec2(0.0);vec2 total=vec2(0.0);float offset=random(vec3(12.9898,78.233,151.7182),0.0);for(float t=-30.0;t<=30.0;t++){float percent=(t+offset-0.5)/30.0;float weight=1.0-abs(percent);vec3 sample=texture2D(texture,texCoord+delta*percent).rgb;float average=(sample.r+sample.g+sample.b)/3.0;color.x+=average*weight;total.x+=weight;if(abs(t)<15.0){weight=weight*2.0-1.0;color.y+=average*weight;total.y+=weight;}}gl_FragColor=vec4(color/total,0.0,1.0);}");
a.edgeWork2=a.edgeWork2||new h(null,"uniform sampler2D texture;uniform vec2 delta;varying vec2 texCoord;"+s+"void main(){vec2 color=vec2(0.0);vec2 total=vec2(0.0);float offset=random(vec3(12.9898,78.233,151.7182),0.0);for(float t=-30.0;t<=30.0;t++){float percent=(t+offset-0.5)/30.0;float weight=1.0-abs(percent);vec2 sample=texture2D(texture,texCoord+delta*percent).xy;color.x+=sample.x*weight;total.x+=weight;if(abs(t)<15.0){weight=weight*2.0-1.0;color.y+=sample.y*weight;total.y+=weight;}}float c=clamp(10000.0*(color.y/total.y-color.x/total.x)+0.5,0.0,1.0);gl_FragColor=vec4(c,c,c,1.0);}");
f.call(this,a.edgeWork1,{delta:[b/this.width,0]});f.call(this,a.edgeWork2,{delta:[0,b/this.height]});return this}function X(b,d,c){a.hexagonalPixelate=a.hexagonalPixelate||new h(null,"uniform sampler2D texture;uniform vec2 center;uniform float scale;uniform vec2 texSize;varying vec2 texCoord;void main(){vec2 tex=(texCoord*texSize-center)/scale;tex.y/=0.866025404;tex.x-=tex.y*0.5;vec2 a;if(tex.x+tex.y-floor(tex.x)-floor(tex.y)<1.0)a=vec2(floor(tex.x),floor(tex.y));else a=vec2(ceil(tex.x),ceil(tex.y));vec2 b=vec2(ceil(tex.x),floor(tex.y));vec2 c=vec2(floor(tex.x),ceil(tex.y));vec3 TEX=vec3(tex.x,tex.y,1.0-tex.x-tex.y);vec3 A=vec3(a.x,a.y,1.0-a.x-a.y);vec3 B=vec3(b.x,b.y,1.0-b.x-b.y);vec3 C=vec3(c.x,c.y,1.0-c.x-c.y);float alen=length(TEX-A);float blen=length(TEX-B);float clen=length(TEX-C);vec2 choice;if(alen<blen){if(alen<clen)choice=a;else choice=c;}else{if(blen<clen)choice=b;else choice=c;}choice.x+=choice.y*0.5;choice.y*=0.866025404;choice*=scale/texSize;gl_FragColor=texture2D(texture,choice+center/texSize);}");
f.call(this,a.hexagonalPixelate,{center:[b,d],scale:c,texSize:[this.width,this.height]});return this}function Y(b){a.ink=a.ink||new h(null,"uniform sampler2D texture;uniform float strength;uniform vec2 texSize;varying vec2 texCoord;void main(){vec2 dx=vec2(1.0/texSize.x,0.0);vec2 dy=vec2(0.0,1.0/texSize.y);vec4 color=texture2D(texture,texCoord);float bigTotal=0.0;float smallTotal=0.0;vec3 bigAverage=vec3(0.0);vec3 smallAverage=vec3(0.0);for(float x=-2.0;x<=2.0;x+=1.0){for(float y=-2.0;y<=2.0;y+=1.0){vec3 sample=texture2D(texture,texCoord+dx*x+dy*y).rgb;bigAverage+=sample;bigTotal+=1.0;if(abs(x)+abs(y)<2.0){smallAverage+=sample;smallTotal+=1.0;}}}vec3 edge=max(vec3(0.0),bigAverage/bigTotal-smallAverage/smallTotal);gl_FragColor=vec4(color.rgb-dot(edge,edge)*strength*100000.0,color.a);}");
f.call(this,a.ink,{strength:b*b*b*b*b,texSize:[this.width,this.height]});return this}function Z(b,d,c,e){a.bulgePinch=a.bulgePinch||u("uniform float radius;uniform float strength;uniform vec2 center;","coord-=center;float distance=length(coord);if(distance<radius){float percent=distance/radius;if(strength>0.0){coord*=mix(1.0,smoothstep(0.0,radius/distance,percent),strength*0.75);}else{coord*=mix(1.0,pow(percent,1.0+strength*0.75)*radius/distance,1.0-percent);}}coord+=center;");
f.call(this,a.bulgePinch,{radius:c,strength:q(-1,e,1),center:[b,d],texSize:[this.width,this.height]});return this}function $(b,d,c){a.matrixWarp=a.matrixWarp||u("uniform mat3 matrix;uniform bool useTextureSpace;","if(useTextureSpace)coord=coord/texSize*2.0-1.0;vec3 warp=matrix*vec3(coord,1.0);coord=warp.xy/warp.z;if(useTextureSpace)coord=(coord*0.5+0.5)*texSize;");b=Array.prototype.concat.apply([],b);if(4==b.length)b=
[b[0],b[1],0,b[2],b[3],0,0,0,1];else if(9!=b.length)throw"can only warp with 2x2 or 3x3 matrix";f.call(this,a.matrixWarp,{matrix:d?y(b):b,texSize:[this.width,this.height],useTextureSpace:c|0});return this}function aa(a,d){var c=x.apply(null,d),e=x.apply(null,a),c=y(c);return this.matrixWarp([c[0]*e[0]+c[1]*e[3]+c[2]*e[6],c[0]*e[1]+c[1]*e[4]+c[2]*e[7],c[0]*e[2]+c[1]*e[5]+c[2]*e[8],c[3]*e[0]+c[4]*e[3]+c[5]*e[6],c[3]*e[1]+c[4]*e[4]+c[5]*e[7],c[3]*e[2]+c[4]*e[5]+c[5]*e[8],c[6]*e[0]+c[7]*e[3]+c[8]*e[6],
c[6]*e[1]+c[7]*e[4]+c[8]*e[7],c[6]*e[2]+c[7]*e[5]+c[8]*e[8]])}function ba(b,d,c,e){a.swirl=a.swirl||u("uniform float radius;uniform float angle;uniform vec2 center;","coord-=center;float distance=length(coord);if(distance<radius){float percent=(radius-distance)/radius;float theta=percent*percent*angle;float s=sin(theta);float c=cos(theta);coord=vec2(coord.x*c-coord.y*s,coord.x*s+coord.y*c);}coord+=center;");
f.call(this,a.swirl,{radius:c,center:[b,d],angle:e,texSize:[this.width,this.height]});return this}var v={};(function(){function a(b){if(!b.getExtension("OES_texture_float"))return!1;var c=b.createFramebuffer(),e=b.createTexture();b.bindTexture(b.TEXTURE_2D,e);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MAG_FILTER,b.NEAREST);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MIN_FILTER,b.NEAREST);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_S,b.CLAMP_TO_EDGE);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_T,b.CLAMP_TO_EDGE);
b.texImage2D(b.TEXTURE_2D,0,b.RGBA,1,1,0,b.RGBA,b.UNSIGNED_BYTE,null);b.bindFramebuffer(b.FRAMEBUFFER,c);b.framebufferTexture2D(b.FRAMEBUFFER,b.COLOR_ATTACHMENT0,b.TEXTURE_2D,e,0);c=b.createTexture();b.bindTexture(b.TEXTURE_2D,c);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MAG_FILTER,b.LINEAR);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_MIN_FILTER,b.LINEAR);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_S,b.CLAMP_TO_EDGE);b.texParameteri(b.TEXTURE_2D,b.TEXTURE_WRAP_T,b.CLAMP_TO_EDGE);b.texImage2D(b.TEXTURE_2D,
0,b.RGBA,2,2,0,b.RGBA,b.FLOAT,new Float32Array([2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]));var e=b.createProgram(),d=b.createShader(b.VERTEX_SHADER),g=b.createShader(b.FRAGMENT_SHADER);b.shaderSource(d,"attribute vec2 vertex;void main(){gl_Position=vec4(vertex,0.0,1.0);}");b.shaderSource(g,"uniform sampler2D texture;void main(){gl_FragColor=texture2D(texture,vec2(0.5));}");b.compileShader(d);b.compileShader(g);b.attachShader(e,d);b.attachShader(e,
g);b.linkProgram(e);d=b.createBuffer();b.bindBuffer(b.ARRAY_BUFFER,d);b.bufferData(b.ARRAY_BUFFER,new Float32Array([0,0]),b.STREAM_DRAW);b.enableVertexAttribArray(0);b.vertexAttribPointer(0,2,b.FLOAT,!1,0,0);d=new Uint8Array(4);b.useProgram(e);b.viewport(0,0,1,1);b.bindTexture(b.TEXTURE_2D,c);b.drawArrays(b.POINTS,0,1);b.readPixels(0,0,1,1,b.RGBA,b.UNSIGNED_BYTE,d);return 127===d[0]||128===d[0]}function d(){}function c(a){"OES_texture_float_linear"===a?(void 0===this.$OES_texture_float_linear$&&Object.defineProperty(this,
"$OES_texture_float_linear$",{enumerable:!1,configurable:!1,writable:!1,value:new d}),a=this.$OES_texture_float_linear$):a=n.call(this,a);return a}function e(){var a=f.call(this);-1===a.indexOf("OES_texture_float_linear")&&a.push("OES_texture_float_linear");return a}try{var g=document.createElement("canvas").getContext("experimental-webgl")}catch(l){}if(g&&-1===g.getSupportedExtensions().indexOf("OES_texture_float_linear")&&a(g)){var n=WebGLRenderingContext.prototype.getExtension,f=WebGLRenderingContext.prototype.getSupportedExtensions;
WebGLRenderingContext.prototype.getExtension=c;WebGLRenderingContext.prototype.getSupportedExtensions=e}})();var a;v.canvas=function(){var b=document.createElement("canvas");try{a=b.getContext("experimental-webgl",{premultipliedAlpha:!1})}catch(d){a=null}if(!a)throw"This browser does not support WebGL";b._={gl:a,isInitialized:!1,texture:null,spareTexture:null,flippedShader:null};b.texture=k(A);b.draw=k(C);b.update=k(D);b.replace=k(E);b.contents=k(F);b.getPixelArray=k(G);b.brightnessContrast=k(H);
b.hexagonalPixelate=k(X);b.hueSaturation=k(K);b.colorHalftone=k(U);b.triangleBlur=k(S);b.unsharpMask=k(N);b.perspective=k(aa);b.matrixWarp=k($);b.bulgePinch=k(Z);b.tiltShift=k(R);b.dotScreen=k(V);b.edgeWork=k(W);b.lensBlur=k(Q);b.zoomBlur=k(T);b.noise=k(L);b.denoise=k(J);b.curves=k(I);b.swirl=k(ba);b.ink=k(Y);b.vignette=k(P);b.vibrance=k(O);b.sepia=k(M);return b};v.splineInterpolate=t;var h=function(){function b(b,c){var e=a.createShader(b);a.shaderSource(e,c);a.compileShader(e);if(!a.getShaderParameter(e,
a.COMPILE_STATUS))throw"compile error: "+a.getShaderInfoLog(e);return e}function d(d,l){this.texCoordAttribute=this.vertexAttribute=null;this.program=a.createProgram();d=d||c;l=l||e;l="precision highp float;"+l;a.attachShader(this.program,b(a.VERTEX_SHADER,d));a.attachShader(this.program,b(a.FRAGMENT_SHADER,l));a.linkProgram(this.program);if(!a.getProgramParameter(this.program,a.LINK_STATUS))throw"link error: "+a.getProgramInfoLog(this.program);}var c="attribute vec2 vertex;attribute vec2 _texCoord;varying vec2 texCoord;void main(){texCoord=_texCoord;gl_Position=vec4(vertex*2.0-1.0,0.0,1.0);}",
e="uniform sampler2D texture;varying vec2 texCoord;void main(){gl_FragColor=texture2D(texture,texCoord);}";d.prototype.destroy=function(){a.deleteProgram(this.program);this.program=null};d.prototype.uniforms=function(b){a.useProgram(this.program);for(var e in b)if(b.hasOwnProperty(e)){var c=a.getUniformLocation(this.program,e);if(null!==c){var d=b[e];if("[object Array]"==Object.prototype.toString.call(d))switch(d.length){case 1:a.uniform1fv(c,new Float32Array(d));break;
case 2:a.uniform2fv(c,new Float32Array(d));break;case 3:a.uniform3fv(c,new Float32Array(d));break;case 4:a.uniform4fv(c,new Float32Array(d));break;case 9:a.uniformMatrix3fv(c,!1,new Float32Array(d));break;case 16:a.uniformMatrix4fv(c,!1,new Float32Array(d));break;default:throw"dont't know how to load uniform \""+e+'" of length '+d.length;}else if("[object Number]"==Object.prototype.toString.call(d))a.uniform1f(c,d);else throw'attempted to set uniform "'+e+'" to invalid value '+(d||"undefined").toString();
}}return this};d.prototype.textures=function(b){a.useProgram(this.program);for(var c in b)b.hasOwnProperty(c)&&a.uniform1i(a.getUniformLocation(this.program,c),b[c]);return this};d.prototype.drawRect=function(b,c,e,d){var f=a.getParameter(a.VIEWPORT);c=void 0!==c?(c-f[1])/f[3]:0;b=void 0!==b?(b-f[0])/f[2]:0;e=void 0!==e?(e-f[0])/f[2]:1;d=void 0!==d?(d-f[1])/f[3]:1;null==a.vertexBuffer&&(a.vertexBuffer=a.createBuffer());a.bindBuffer(a.ARRAY_BUFFER,a.vertexBuffer);a.bufferData(a.ARRAY_BUFFER,new Float32Array([b,
c,b,d,e,c,e,d]),a.STATIC_DRAW);null==a.texCoordBuffer&&(a.texCoordBuffer=a.createBuffer(),a.bindBuffer(a.ARRAY_BUFFER,a.texCoordBuffer),a.bufferData(a.ARRAY_BUFFER,new Float32Array([0,0,0,1,1,0,1,1]),a.STATIC_DRAW));null==this.vertexAttribute&&(this.vertexAttribute=a.getAttribLocation(this.program,"vertex"),a.enableVertexAttribArray(this.vertexAttribute));null==this.texCoordAttribute&&(this.texCoordAttribute=a.getAttribLocation(this.program,"_texCoord"),a.enableVertexAttribArray(this.texCoordAttribute));
a.useProgram(this.program);a.bindBuffer(a.ARRAY_BUFFER,a.vertexBuffer);a.vertexAttribPointer(this.vertexAttribute,2,a.FLOAT,!1,0,0);a.bindBuffer(a.ARRAY_BUFFER,a.texCoordBuffer);a.vertexAttribPointer(this.texCoordAttribute,2,a.FLOAT,!1,0,0);a.drawArrays(a.TRIANGLE_STRIP,0,4)};d.getDefaultShader=function(){a.defaultShader=a.defaultShader||new d;return a.defaultShader};return d}();z.prototype.interpolate=function(a){for(var d=0,c=this.ya.length-1;1<c-d;){var e=c+d>>1;this.xa[e]>a?c=e:d=e}var e=this.xa[c]-
this.xa[d],g=(this.xa[c]-a)/e;a=(a-this.xa[d])/e;return g*this.ya[d]+a*this.ya[c]+((g*g*g-g)*this.y2[d]+(a*a*a-a)*this.y2[c])*e*e/6};var r=function(){function b(b,c,d,f){this.gl=a;this.id=a.createTexture();this.width=b;this.height=c;this.format=d;this.type=f;a.bindTexture(a.TEXTURE_2D,this.id);a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MAG_FILTER,a.LINEAR);a.texParameteri(a.TEXTURE_2D,a.TEXTURE_MIN_FILTER,a.LINEAR);a.texParameteri(a.TEXTURE_2D,a.TEXTURE_WRAP_S,a.CLAMP_TO_EDGE);a.texParameteri(a.TEXTURE_2D,
a.TEXTURE_WRAP_T,a.CLAMP_TO_EDGE);b&&c&&a.texImage2D(a.TEXTURE_2D,0,this.format,b,c,0,this.format,this.type,null)}function d(a){null==c&&(c=document.createElement("canvas"));c.width=a.width;c.height=a.height;a=c.getContext("2d");a.clearRect(0,0,c.width,c.height);return a}b.fromElement=function(c){var d=new b(0,0,a.RGBA,a.UNSIGNED_BYTE);d.loadContentsOf(c);return d};b.prototype.loadContentsOf=function(b){this.width=b.width||b.videoWidth;this.height=b.height||b.videoHeight;a.bindTexture(a.TEXTURE_2D,
this.id);a.texImage2D(a.TEXTURE_2D,0,this.format,this.format,this.type,b)};b.prototype.initFromBytes=function(b,c,d){this.width=b;this.height=c;this.format=a.RGBA;this.type=a.UNSIGNED_BYTE;a.bindTexture(a.TEXTURE_2D,this.id);a.texImage2D(a.TEXTURE_2D,0,a.RGBA,b,c,0,a.RGBA,this.type,new Uint8Array(d))};b.prototype.destroy=function(){a.deleteTexture(this.id);this.id=null};b.prototype.use=function(b){a.activeTexture(a.TEXTURE0+(b||0));a.bindTexture(a.TEXTURE_2D,this.id)};b.prototype.unuse=function(b){a.activeTexture(a.TEXTURE0+
(b||0));a.bindTexture(a.TEXTURE_2D,null)};b.prototype.ensureFormat=function(b,c,d,f){if(1==arguments.length){var h=arguments[0];b=h.width;c=h.height;d=h.format;f=h.type}if(b!=this.width||c!=this.height||d!=this.format||f!=this.type)this.width=b,this.height=c,this.format=d,this.type=f,a.bindTexture(a.TEXTURE_2D,this.id),a.texImage2D(a.TEXTURE_2D,0,this.format,b,c,0,this.format,this.type,null)};b.prototype.drawTo=function(b){a.framebuffer=a.framebuffer||a.createFramebuffer();a.bindFramebuffer(a.FRAMEBUFFER,
a.framebuffer);a.framebufferTexture2D(a.FRAMEBUFFER,a.COLOR_ATTACHMENT0,a.TEXTURE_2D,this.id,0);if(a.checkFramebufferStatus(a.FRAMEBUFFER)!==a.FRAMEBUFFER_COMPLETE)throw Error("incomplete framebuffer");a.viewport(0,0,this.width,this.height);b();a.bindFramebuffer(a.FRAMEBUFFER,null)};var c=null;b.prototype.fillUsingCanvas=function(b){b(d(this));this.format=a.RGBA;this.type=a.UNSIGNED_BYTE;a.bindTexture(a.TEXTURE_2D,this.id);a.texImage2D(a.TEXTURE_2D,0,a.RGBA,a.RGBA,a.UNSIGNED_BYTE,c);return this};
b.prototype.toImage=function(b){this.use();h.getDefaultShader().drawRect();var f=4*this.width*this.height,k=new Uint8Array(f),n=d(this),p=n.createImageData(this.width,this.height);a.readPixels(0,0,this.width,this.height,a.RGBA,a.UNSIGNED_BYTE,k);for(var m=0;m<f;m++)p.data[m]=k[m];n.putImageData(p,0,0);b.src=c.toDataURL()};b.prototype.swapWith=function(a){var b;b=a.id;a.id=this.id;this.id=b;b=a.width;a.width=this.width;this.width=b;b=a.height;a.height=this.height;this.height=b;b=a.format;a.format=
this.format;this.format=b};return b}(),s="float random(vec3 scale,float seed){return fract(sin(dot(gl_FragCoord.xyz+seed,scale))*43758.5453+seed);}";return v}();

354
filters/dog/main.js Normal file
View File

@@ -0,0 +1,354 @@
"use strict";
// some globalz:
let THREECAMERA = null;
let ISDETECTED = false;
let TONGUEMESH = null, NOSEMESH = null, EARMESH = null;
let DOGOBJ3D = null, FRAMEOBJ3D = null;
let ISOVERTHRESHOLD = false, ISUNDERTRESHOLD = true;
let ISLOADED = false;
let MIXER = null;
let ACTION = null;
let ISANIMATING = false;
let ISOPAQUE = false;
let ISTONGUEOUT = false;
let ISANIMATIONOVER = false;
let _flexParts = [];
let _videoGeometry = null;
// callback: launched if a face is detected or lost
function detect_callback(isDetected) {
if (isDetected) {
console.log('INFO in detect_callback(): DETECTED');
} else {
console.log('INFO in detect_callback(): LOST');
}
}
function create_mat2d(threeTexture, isTransparent){ // MT216: we put the creation of the video material in a func because we will also use it for the frame
return new THREE.RawShaderMaterial({
depthWrite: false,
depthTest: false,
transparent: isTransparent,
vertexShader: "attribute vec2 position;\n\
varying vec2 vUV;\n\
void main(void){\n\
gl_Position = vec4(position, 0., 1.);\n\
vUV = 0.5 + 0.5 * position;\n\
}",
fragmentShader: "precision lowp float;\n\
uniform sampler2D samplerVideo;\n\
varying vec2 vUV;\n\
void main(void){\n\
gl_FragColor = texture2D(samplerVideo, vUV);\n\
}",
uniforms:{
samplerVideo: { value: threeTexture }
}
});
}
function applyFilter() {
let canvas;
try {
canvas = fx.canvas();
} catch (e) {
alert('Ow no! WebGL isn\'t supported...')
return
}
const tempImage = new Image(512, 512);
tempImage.src = './images/texture_pink.jpg';
tempImage.onload = () => {
const texture = canvas.texture(tempImage);
// Create the effet
canvas.draw(texture).vignette(0.5, 0.6).update();
const canvasOpacity = document.createElement('canvas');
canvasOpacity.width = 512;
canvasOpacity.height = 512;
const ctx = canvasOpacity.getContext('2d');
ctx.globalAlpha = 0.2
ctx.drawImage(canvas, 0, 0, 512, 512);
// Add the effect
const calqueMesh = new THREE.Mesh(_videoGeometry, create_mat2d(new THREE.TextureLoader().load(canvasOpacity.toDataURL('image/png')), true))
calqueMesh.material.opacity = 0.2;
calqueMesh.material.transparent = true;
calqueMesh.renderOrder = 999; // render last
calqueMesh.frustumCulled = false;
FRAMEOBJ3D.add(calqueMesh);
}
}
// build the 3D. called once when Jeeliz Face Filter is OK
function init_threeScene(spec) {
// INIT THE THREE.JS context
const threeStuffs = JeelizThreeHelper.init(spec, detect_callback);
_videoGeometry = threeStuffs.videoMesh.geometry;
// CREATE OUR DOG EARS:
// let's begin by creating a loading manager that will allow us to
// have more control over the three parts of our dog model
const loadingManager = new THREE.LoadingManager();
const loaderEars = new THREE.BufferGeometryLoader(loadingManager);
loaderEars.load(
'./models/dog/dog_ears.json',
function (geometry) {
const mat = new THREE.FlexMaterial({
map: new THREE.TextureLoader().load('./models/dog/texture_ears.jpg'),
flexMap: new THREE.TextureLoader().load('./models/dog/flex_ears_256.jpg'),
alphaMap: new THREE.TextureLoader().load('./models/dog/alpha_ears_256.jpg'),
transparent: true,
opacity: 1,
bumpMap: new THREE.TextureLoader().load('./models/dog/normal_ears.jpg'),
bumpScale: 0.0075,
shininess: 1.5,
specular: 0xffffff,
});
EARMESH = new THREE.Mesh(geometry, mat);
EARMESH.scale.multiplyScalar(0.025);
EARMESH.position.setY(-0.3);
EARMESH.frustumCulled = false;
EARMESH.renderOrder = 10000;
EARMESH.material.opacity.value = 1;
}
);
// CREATE OUR DOG NOSE
const loaderNose = new THREE.BufferGeometryLoader(loadingManager);
loaderNose.load(
'./models/dog/dog_nose.json',
function (geometry) {
const mat = new THREE.MeshPhongMaterial({
map: new THREE.TextureLoader().load('./models/dog/texture_nose.jpg'),
shininess: 1.5,
specular: 0xffffff,
bumpMap: new THREE.TextureLoader().load('./models/dog/normal_nose.jpg'),
bumpScale: 0.005
});
NOSEMESH = new THREE.Mesh(geometry, mat);
NOSEMESH.scale.multiplyScalar(0.018);
NOSEMESH.position.setY(-0.05);
NOSEMESH.position.setZ(0.15);
NOSEMESH.frustumCulled = false;
NOSEMESH.renderOrder = 10000;
}
);
// CREATE OUR DOG TONGUE
const loaderTongue = new THREE.JSONLoader(loadingManager);
loaderTongue.load(
'models/dog/dog_tongue.json',
function (geometry) {
geometry.computeMorphNormals();
const mat = new THREE.FlexMaterial({
map: new THREE.TextureLoader().load('./models/dog/dog_tongue.jpg'),
flexMap: new THREE.TextureLoader().load('./models/dog/flex_tongue_256.png'),
alphaMap: new THREE.TextureLoader().load('./models/dog/tongue_alpha_256.jpg'),
transparent: true,
morphTargets: true,
opacity: 1
});
TONGUEMESH = new THREE.Mesh(geometry, mat);
TONGUEMESH.material.opacity.value = 0;
TONGUEMESH.scale.multiplyScalar(2);
TONGUEMESH.position.setY(-0.28);
TONGUEMESH.frustumCulled = false;
TONGUEMESH.visible = false;
if (!MIXER) {
// the mixer is declared globally so we can use it in the THREE renderer
MIXER = new THREE.AnimationMixer(TONGUEMESH);
const clips = TONGUEMESH.geometry.animations;
const clip = clips[0];
ACTION = MIXER.clipAction(clip);
ACTION.noLoop = true;
ACTION.play();
}
}
);
loadingManager.onLoad = () => {
DOGOBJ3D.add(EARMESH);
DOGOBJ3D.add(NOSEMESH);
DOGOBJ3D.add(TONGUEMESH);
addDragEventListener(DOGOBJ3D);
threeStuffs.faceObject.add(DOGOBJ3D);
ISLOADED = true;
}
// CREATE AN AMBIENT LIGHT
const ambient = new THREE.AmbientLight(0xffffff, 0.8);
threeStuffs.scene.add(ambient);
// CREAT A DIRECTIONALLIGHT
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5);
dirLight.position.set(100, 1000, 1000);
threeStuffs.scene.add(dirLight);
// CREATE THE CAMERA
THREECAMERA = JeelizThreeHelper.create_camera();
threeStuffs.scene.add(FRAMEOBJ3D);
// Add filter
applyFilter();
} // end init_threeScene()
function animateTongue (mesh, isReverse) {
mesh.visible = true;
if (isReverse) {
ACTION.timescale = -1;
ACTION.paused = false;
setTimeout(() => {
ACTION.paused = true;
ISOPAQUE = false;
ISTONGUEOUT = false;
ISANIMATING = false;
ISANIMATIONOVER = true;
new TWEEN.Tween(mesh.material.opacity)
.to({ value: 0 }, 150)
.start();
}, 150);
} else {
ACTION.timescale = 1;
ACTION.reset();
ACTION.paused = false;
new TWEEN.Tween(mesh.material.opacity)
.to({ value: 1 }, 100)
.onComplete(() => {
ISOPAQUE = true;
setTimeout(() => {
ACTION.paused = true;
ISANIMATING = false;
ISTONGUEOUT = true;
ISANIMATIONOVER = true;
}, 150);
})
.start();
}
}
// Entry point: launched by body.onload()
function main(){
DOGOBJ3D = new THREE.Object3D();
FRAMEOBJ3D = new THREE.Object3D();
JeelizResizer.size_canvas({
canvasId: 'jeeFaceFilterCanvas',
callback: function(isError, bestVideoSettings){
init_faceFilter(bestVideoSettings);
}
});
}
function init_faceFilter(videoSettings){
JEELIZFACEFILTER.init({
canvasId: 'jeeFaceFilterCanvas',
NNCPath: '../../../neuralNets/', // root of NN_DEFAULT.json file
videoSettings: videoSettings,
callbackReady: function (errCode, spec) {
if (errCode) {
console.log('AN ERROR HAPPENS. SORRY BRO :( . ERR =', errCode);
return;
}
console.log('INFO: JEELIZFACEFILTER IS READY');
init_threeScene(spec);
}, // end callbackReady()
// called at each render iteration (drawing loop)
callbackTrack: function (detectState) {
ISDETECTED = JeelizThreeHelper.get_isDetected();
if (ISDETECTED) {
const _quat = new THREE.Quaternion();
const _eul = new THREE.Euler();
_eul.setFromQuaternion(_quat);
// flex ears material:
if (EARMESH && EARMESH.material.set_amortized){
EARMESH.material.set_amortized(
EARMESH.getWorldPosition(new THREE.Vector3(0,0,0)),
EARMESH.getWorldScale(new THREE.Vector3(0,0,0)),
EARMESH.getWorldQuaternion(_eul),
false,
0.1
);
}
if (TONGUEMESH && TONGUEMESH.material.set_amortized){
TONGUEMESH.material.set_amortized(
TONGUEMESH.getWorldPosition(new THREE.Vector3(0,0,0)),
TONGUEMESH.getWorldScale(new THREE.Vector3(0,0,0)),
TONGUEMESH.getWorldQuaternion(_eul),
false,
0.3
);
}
if (detectState.expressions[0] >= 0.85 && !ISOVERTHRESHOLD) {
ISOVERTHRESHOLD = true;
ISUNDERTRESHOLD = false;
ISANIMATIONOVER = false;
}
if (detectState.expressions[0] <= 0.1 && !ISUNDERTRESHOLD) {
ISOVERTHRESHOLD = false;
ISUNDERTRESHOLD = true;
ISANIMATIONOVER = false;
}
if (ISLOADED && ISOVERTHRESHOLD && !ISANIMATING && !ISANIMATIONOVER) {
if (!ISTONGUEOUT) {
ISANIMATING = true;
animateTongue(TONGUEMESH);
} else {
ISANIMATING = true;
animateTongue(TONGUEMESH, true);
}
}
}
TWEEN.update();
// Update the mixer on each frame:
if (ISOPAQUE) {
MIXER.update(0.16);
}
JeelizThreeHelper.render(detectState, THREECAMERA);
} // end callbackTrack()
}); // end JEELIZFACEFILTER.init call
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

3
filters/readme.md Normal file
View File

@@ -0,0 +1,3 @@
Some of the code in this folder is based on the sample code made available from Jeeliz;
Apache-2.0 License
https://github.com/jeeliz/jeelizFaceFilter/blob/master/LICENSE

65
filters/sample.js Normal file
View File

@@ -0,0 +1,65 @@
function effectsEngine(){
console.log('LOADED SAMPLE');
function loadScript(url, callback=false){
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.onload = function(){this.remove();if(callback){callback();}}
document.head.appendChild(script);
}
loadScript("./thirdparty/jeeliz/jeelizFaceFilter.js",
function(){
loadScript("./thirdparty/jeeliz/helpers/JeelizCanvas2DHelper.js");
}
);
function main(){ // entry point
console.log(".");
if (session.canvasSource && session.canvasSource.videoWidth && session.canvasSource.videoHeight && session.canvasWebGL){
if (session.canvasWebGL && !(document.getElementById("effectsCanvasTarget"))){
session.canvasWebGL.id = "effectsCanvasTarget";
session.canvasWebGL.style.position="fixed";
session.canvasWebGL.style.top= "-9999px";
session.canvasWebGL.style.left= "-9999px";
document.body.appendChild(session.canvasWebGL);
}
start(JEELIZFACEFILTER, "effectsCanvasTarget", session.canvasSource, 'yellow');
} else {
setTimeout(main, 500);
}
}
function start(jeeFaceFilterAPIInstance, canvasId, videoElement, borderColor){
let cvd = null; // return of Canvas2DDisplay
jeeFaceFilterAPIInstance.init({
canvasId: canvasId,
videoSettings: {
videoElement: videoElement
},
NNCPath: './thirdparty/jeeliz/neuralNets/', // root of NN_DEFAULT.json file
callbackReady: function(errCode, spec){
if (errCode){
console.log('AN ERROR HAPPENS. SORRY BRO :( . ERR =', errCode);
return;
}
console.log('INFO: JEELIZFACEFILTER IS READY');
cvd = JeelizCanvas2DHelper(spec);
cvd.ctx.strokeStyle = borderColor;
},
callbackTrack: function(detectState){ // drawing loop
if (detectState.detected>0.6){
// draw a border around the face:
const faceCoo = cvd.getCoordinates(detectState);
cvd.ctx.clearRect(0,0,cvd.canvas.width, cvd.canvas.height);
cvd.ctx.strokeRect(faceCoo.x, faceCoo.y, faceCoo.w, faceCoo.h);
cvd.update_canvasTexture();
}
cvd.draw();
}
});
}
main();
};

646
iframe.html Normal file
View File

@@ -0,0 +1,646 @@
<html>
<head>
<title>IFRAME Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<style>
body{
padding:0;
margin:0;
background-color: rgb(222,242,253);
}
iframe {
border:0;
margin:0;
padding:0;
display:block;
margin:10px;
width:100%;
height:520px;
}
#viewlink {
width:400px;
}
#container {
display:block;
padding:0px;
}
input{
padding:5px;
margin:5px;
}
button{
padding:5px;
margin:5px;
}
</style>
<script>
function loadIframe(){ // this is pretty important if you want to avoid camera permission popup problems. You can also call it automatically via: <body onload=>loadIframe();"> , but don't call it before the page loads.
var iframe = document.createElement("iframe");
var iframeContainer = document.createElement("div");
var iframesrc = document.getElementById("viewlink").value;
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;";
if (iframesrc==""){
iframesrc="./";
}
if (document.getElementById("clean").checked){
if (iframesrc.includes("?")){
iframesrc+='&';
} else {
iframesrc+='?';
}
iframesrc+="cleanoutput";
}
if (document.getElementById("transparent").checked){
if (iframesrc.includes("?")){
iframesrc+='&';
} else {
iframesrc+='?';
}
iframesrc+="transparent";
}
if (document.getElementById("hidemenu").checked){
if (iframesrc.includes("?")){
iframesrc+='&';
} else {
iframesrc+='?';
}
iframesrc+="hidemenu";
}
iframe.src = iframesrc;
iframeContainer.appendChild(iframe);
document.getElementById("container").appendChild(iframeContainer);
var h3 = document.createElement("h3");
h3.innerText = "The following commands are exclusive to the IFRAME API.";
iframeContainer.appendChild(h3);
var button = document.createElement("button");
button.innerHTML = "Mute Speaker";
button.onclick = function(){iframe.contentWindow.postMessage({"mute":true}, '*');}; // "speaker" also works in the same way.
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Un-Mute Speaker";
button.onclick = function(){iframe.contentWindow.postMessage({"mute":false}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Toggle Speaker";
button.onclick = function(){iframe.contentWindow.postMessage({"mute":"toggle"}, '*');}; // open to a better suggestion here.
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Mute Mic";
button.onclick = function(){iframe.contentWindow.postMessage({"mic":false}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Un-Mute Mic";
button.onclick = function(){iframe.contentWindow.postMessage({"mic":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Toggle Mic";
button.onclick = function(){iframe.contentWindow.postMessage({"mic":"toggle"}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Mute Camera";
button.onclick = function(){iframe.contentWindow.postMessage({"camera":false}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Unmute Camera";
button.onclick = function(){iframe.contentWindow.postMessage({"camera":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Toggle Camera";
button.onclick = function(){iframe.contentWindow.postMessage({"camera":"toggle"}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Disconnect";
button.onclick = function(){iframe.contentWindow.postMessage({"close":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Low Bitrate";
button.onclick = function(){iframe.contentWindow.postMessage({"bitrate":30}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "High Bitrate";
button.onclick = function(){iframe.contentWindow.postMessage({"bitrate":5000}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Default Bitrate";
button.onclick = function(){iframe.contentWindow.postMessage({"bitrate":-1}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Reload";
button.onclick = function(){iframe.contentWindow.postMessage({"reload":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "50% Volume";
button.onclick = function(){iframe.contentWindow.postMessage({"volume":0.5}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "100% Volume";
button.onclick = function(){iframe.contentWindow.postMessage({"volume":1.0}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Pan Left";
button.title = "Requires &panning to be added to the view link";
button.onclick = function(){iframe.contentWindow.postMessage({"panning":0}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Pan right";
button.title = "Requires &panning to be added to the view link";
button.onclick = function(){iframe.contentWindow.postMessage({"panning":180}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Pan Center";
button.title = "Requires &panning to be added to the view link";
button.onclick = function(){iframe.contentWindow.postMessage({"panning":90}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Request Loudness Levels";
button.onclick = function(){iframe.contentWindow.postMessage({"getLoudness":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Stop Sending Loudness Levels";
button.onclick = function(){iframe.contentWindow.postMessage({"getLoudness":false}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Get detailed state (if Director)";
button.onclick = function(){iframe.contentWindow.postMessage({"getDetailedState":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Start Recording";
button.onclick = function(){iframe.contentWindow.postMessage({"record":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Stop Recording";
button.onclick = function(){iframe.contentWindow.postMessage({"record":false}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Say Hello";
button.onclick = function(){iframe.contentWindow.postMessage({"sendChat":"Hello!"}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Send Keyframe";
button.onclick = function(){iframe.contentWindow.postMessage({"keyframe":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Insert Style Sheet";
var stylesheet = "#main { zoom: 0.5;} video {float: left; margin: 0; padding: 0; } #info {display:none;}";
button.onclick = function(){iframe.contentWindow.postMessage({"style":stylesheet}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "get StreamIDs and labels";
button.onclick = function(){iframe.contentWindow.postMessage({"getStreamIDs":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Start AutoMixer";
button.onclick = function(){iframe.contentWindow.postMessage({"automixer":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Stop AutoMixer";
button.onclick = function(){iframe.contentWindow.postMessage({"automixer":false}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "ENABLE TALLY LIGHT";
button.onclick = function(){iframe.contentWindow.postMessage({"sceneState":true}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "STOP TALLY LIGHT";
button.onclick = function(){iframe.contentWindow.postMessage({"sceneState":false}, '*');};
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Add Target Video";
button.onclick = function(){iframe.contentWindow.postMessage({"target":"*", "add":true, "settings":{"style":{"width":"640px", "height":"360px", "float":"left", "border":"10px solid red", "display":"block"}}}, '*');}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Remove Target Video";
button.onclick = function(){iframe.contentWindow.postMessage({"target":"*", "remove": true}, '*');}; // target can be a stream ID or * for all.
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "previewWebcam()";
button.onclick = function(){iframe.contentWindow.postMessage({"function":"previewWebcam"}, '*');}; // publishScreen
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Change to Camera #2";
button.onclick = function(){iframe.contentWindow.postMessage({"changeVideoDevice":2}, '*');}; // change text of add camera button
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Change to Microphone #4";
button.onclick = function(){iframe.contentWindow.postMessage({"changeAudioDevice":4}, '*');}; // change text of add camera button
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "eval('alert(\"DANGERUS\")'";
button.onclick = function(){iframe.contentWindow.postMessage({"function":"eval", "value":'alert(\"DANGERUS\")'}, '*');}; // publishScreen
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "Change Add Camera text";
button.onclick = function(){iframe.contentWindow.postMessage({"function":"changeHTML", "target":"add_camera", "value":"NEW CAMERA TEXT"}, '*');}; // change text of add camera button
iframeContainer.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "CLOSE IFRAME";
button.onclick = function(){iframeContainer.parentNode.removeChild(iframeContainer);};
iframeContainer.appendChild(button);
//////////// LISTEN FOR EVENTS
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
var eventer = window[eventMethod];
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
/// If you have a routing system setup, you could have just one global listener for all iframes instead.
eventer(messageEvent, function (e) {
if (e.source != iframe.contentWindow){return} // reject messages send from other iframes
if ("stats" in e.data){
var outputWindow = document.createElement("div");
console.log(e.data.stats);
var out = "<br />total_inbound_connections:"+e.data.stats.total_inbound_connections;
out += "<br />total_outbound_connections:"+e.data.stats.total_outbound_connections;
for (var streamID in e.data.stats.inbound_stats){
out += "<br /><br /><b>streamID:</b> "+streamID+"<br />";
out += printValues(e.data.stats.inbound_stats[streamID]);
}
outputWindow.innerHTML = out;
iframeContainer.appendChild(outputWindow);
}
if ("gotChat" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = e.data.gotChat.msg;
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("action" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "child-page-action: "+e.data.action+"<br />";
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("streamIDs" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "child-page-action: streamIDs<br />";
for (var key in e.data.streamIDs) {
outputWindow.innerHTML += "streamID: " + key + ", label:"+e.data.streamIDs[key] + "\n";
}
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("loudness" in e.data){
console.log(e.data);
if (document.getElementById("loudness")){
outputWindow = document.getElementById("loudness");
} else {
var outputWindow = document.createElement("div");
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
outputWindow.id = "loudness";
}
outputWindow.innerHTML = "child-page-action: loudness<br />";
for (var key in e.data.loudness) {
outputWindow.innerHTML += key + " Loudness: " + e.data.loudness[key] + "\n";
}
outputWindow.style.border="1px black";
}
if ("detailedState" in e.data){
var outputWindow = document.createElement("div");
outputWindow.innerHTML = "child-page-action: detailedState<br />"+JSON.stringify(e.data.detailedState)+"<br />";
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
}
if ("sensors" in e.data){
console.log(e.data);
if (document.getElementById("sensors")){
outputWindow = document.getElementById("sensors");
} else {
var outputWindow = document.createElement("div");
outputWindow.style.border="1px dotted black";
iframeContainer.appendChild(outputWindow);
outputWindow.id = "sensors";
}
outputWindow.innerHTML = "child-page-action: sensors<br /><br />";
for (var key in e.data.sensors.lin) {
outputWindow.innerHTML += key + " linear: " + e.data.sensors.lin[key] + "<br />";
}
for (var key in e.data.sensors.acc) {
outputWindow.innerHTML += key + " acceleration: " + e.data.sensors.acc[key] + "<br />";
}
for (var key in e.data.sensors.gyro) {
outputWindow.innerHTML += key + " gyro: " + e.data.sensors.gyro[key] + "<br />";
}
for (var key in e.data.sensors.mag) {
outputWindow.innerHTML += key + " magnet: " + e.data.sensors.mag[key] + "<br />";
}
outputWindow.style.border="1px black";
}
});
function loadSelfCommands(){
var commands = {}
commands.speaker = function(value){sendSelfCommand("speaker",value)}; // "speaker" also works in the same way
commands.mic = function(value){sendSelfCommand("mic",value)};
commands.camera = function(value){sendSelfCommand("camera",value)};
commands.bitrate = function(value){sendSelfCommand("bitrate",value)};
commands.volume = function(value){sendSelfCommand("volume",value)};
commands.record = function(value){sendSelfCommand("record",value)};
commands.sayHello = function(value){sendSelfCommand("sendChat","Hello")};
var target_self = document.body;
var hr = document.createElement("hr");
target_self.appendChild(hr);
var h3 = document.createElement("h3");
h3.innerText = "The following commands re-use the Companion.Ninja HTTP/WSS API, except you can send them via this Iframe interface.";
target_self.appendChild(h3);
var a = document.createElement("a");
a.innerText = "More details of the below IFRAME API details are here: https://github.com/steveseguin/Companion-Ninja#api-commands";
a.href = "https://github.com/steveseguin/Companion-Ninja#api-commands";
a.target = "_blank";
target_self.appendChild(a);
var hr = document.createElement("hr");
target_self.appendChild(hr);
for (var k in commands) {
var button = document.createElement("button");
button.innerHTML = k + ":<br />TRUE";
button.dataset.command = k;
button.onclick = function(){commands[this.dataset.command](true);}
target_self.appendChild(button);
var button = document.createElement("button");
button.innerHTML = k + ":<br />FALSE";
button.dataset.command = k;
button.onclick = function(){commands[this.dataset.command](false);}
target_self.appendChild(button);
if (k=="mic" || k=="camera" || k=="record" || k=="speaker"){
var button = document.createElement("button");
button.innerHTML = k + ":<br />TOGGLE";
button.dataset.command = k;
button.onclick = function(){commands[this.dataset.command]("toggle");}
target_self.appendChild(button);
}
} // list available commands to console
commands.reload = function(){sendSelfCommand("reload",true);};
commands.hangup = function(){sendSelfCommand("hangup",true);};
var button = document.createElement("button");
button.innerHTML = k + ":<br />TRUE";
button.onclick = function(){commands["reload"](true);}
target_self.appendChild(button);
var button = document.createElement("button");
button.innerHTML = k + ":<br />TRUE";
button.onclick = function(){commands["hangup"](true);}
target_self.appendChild(button);
return commands;
}
function loadGuestCommands(guestid, streamID=false){ // this is the same as the Companion API, but can be issued to the IFRAME API
var container = document.createElement("div");
container.id = "guest_"+guestid+"_container";
document.body.appendChild(container);
var hr = document.createElement("hr");
container.appendChild(hr);
var h3 = document.createElement("h3");
if (streamID){
h3.innerHTML = "These target guest with the streamID: <i>"+guestid+ "</i> (if a director)";
} else {
h3.innerText = "These target the guest in slot "+guestid+ " (if you are director)";
}
container.appendChild(h3);
var button = document.createElement("button");
button.innerHTML = "transfer popup";
button.onclick = function(){sendGuestCommand(guestid, "forward");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "transfer to 'room321'";
button.onclick = function(){sendGuestCommand(guestid, "forward", 'room321');};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 1";
button.onclick = function(){sendGuestCommand(guestid, "addScene");}; /// SCENE 1 or specify a custom scene name as a value
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "mute in scene";
button.onclick = function(){sendGuestCommand(guestid, "muteScene");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "mute everywhere";
button.onclick = function(){sendGuestCommand(guestid, "mic");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "hang up";
button.onclick = function(){sendGuestCommand(guestid, "hangup");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "solo chat";
button.onclick = function(){sendGuestCommand(guestid, "soloChat");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "remote speaker";
button.onclick = function(){sendGuestCommand(guestid, "speaker");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "remote display";
button.onclick = function(){sendGuestCommand(guestid, "display");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "rainbow puke fix";
button.onclick = function(){sendGuestCommand(guestid, "forceKeyframe");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "highlight";
button.onclick = function(){sendGuestCommand(guestid, "soloVideo");};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 2";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 2);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 3";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 3);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 4";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 4);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 5";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 5);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 6";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 6);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = " scene 7";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 7);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 8";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 8);};
container.appendChild(button);
var button = document.createElement("button");
button.innerHTML = "scene 'test'";
button.onclick = function(){sendGuestCommand(guestid, "addScene", 'test');}; // specifying a custom scene; it needs to be active for this to work..
container.appendChild(button);
var input = document.createElement("label");
input.innerHTML = "mic volume:";
container.appendChild(input);
var input = document.createElement("input");
input.type = "range";
input.title = "volume";
input.min = 0;
input.max = 200;
input.value = 100;
input.onchange = function(){sendGuestCommand(guestid, "volume", this.value);};
container.appendChild(input);
}
function sendGuestCommand(target, action, value=null){ //
iframe.contentWindow.postMessage({"target":target, "action":action, "value":value}, '*'); //
//sendMessage(JSON.stringify({"target":target, "action":action, "value":value})); // if using the Companion API..
}
function sendSelfCommand(action, value=null){
iframe.contentWindow.postMessage({"target":null, "action":action, "value":value}, '*');
//sendMessage(JSON.stringify({"target":target, "action":action, "value":value})); // if using the Companion API..
}
loadSelfCommands();
loadGuestCommands(1);
loadGuestCommands(2);
loadGuestCommands(3);
var randomGuest = Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10);
loadGuestCommands(randomGuest, true);
}
function printValues( obj) {
var out = "";
for (var key in obj) {
if (typeof obj[key] === "object") {
out +="<br />";
out += printValues(obj[key]);
} else {
out +="<b>"+key+"</b>: "+obj[key]+"<br />";
}
}
return out;
}
</script>
</head>
<body>
<input placeholder="Enter an VDO.Ninja View URL here" id="viewlink" />
<button onclick="loadIframe();">ADD</button>
<input type="checkbox" id="clean" >Clean Output
<input type="checkbox" id="transparent">Transparent
<input type="checkbox" id="hidemenu">Hide Menu
<div id="container">
</div>
</body>
</html>

2062
index.html

File diff suppressed because it is too large Load Diff

106
install.md Normal file
View File

@@ -0,0 +1,106 @@
Deploying this code. A guide.
### PREFACE
The code is obvious enough already that someone experienced with NGINX webservers or with basic site deployments should have no problem getting things running.
I'm concerned at times that less experienced users will be deploying the code without really understanding why or properly how to. There are few cases a person needs to deploy any code. Those reasons are: wanting custom branding; contributing as a developer to the codebase; or deploying a private TURN server.
There may be misconceptions that deploying the website code will make the service faster; it will not. The service is video peer-to-peer based, so deploying webservers servers will not make it faster. Deploying a TURN server may help in some cases, although using a VPN, a cloud VM (for OBS), or disabling any symmetrical firewall will often provide a better end result than a private TURN server.
For those looking for a brand-free experience already with a different domain name, I offer https://rtc.ninja, and as well as other alternate domain names, such as:
- https://invite.cam (via URL obfuscation option)
- https://ltt.ninja
- https://rtc.ninja
- https://vmix.ninja
- https://auxiliary.live (backup hosted)
- https://backup.vdo.ninja (fully backup hosted)
There is also an isolated version specificly designed for use in mainland China, hosted at https://insecure.cam in Hong Kong AWS.
You can also point your domain to the VDO.Ninja IP address (provided on request), which will also rebrand the site automatically to match your domain name. (Requires Cloudflare as DNS server and proxy, Flexible SSL cert on, and HTTPs always on - all free.)
For those wanting a private TURN server setup, you can load the settings for those via the URL parameters. If infrequently needing a private TURN, this is a great solution. You can also use URL forwarding services to load up a customized link to VDO.Ninja, with URL parameters already included, such as https://invite.mypersonaldomain.com , which might secretly resolve to https://vdo.ninja/?room=myRoom&hash=3423&label or such.
VDO.Ninja also supports IFRAMES, so you can embed VDO.Ninja into your website and customize it via both URL parameters, but also via the IFRAME API. You can insert custom CSS styles with this method, giving VDO.Ninja quite a bit of flare.
See more on IFRAMES here: https://github.com/steveseguin/vdo.ninja/blob/master/IFRAME.md
Understanding clearly why you need to deploy any code or server is important. Maintaining updated deployed code can be quite hard, as VDO.Ninja updates frequently, so there are good reasons to consider an IFRAME approach instead. Feature requests there are welcomed.
That all aside, please continue:
### SETUP
There's a community-created video tutorial on setting up here; https://youtu.be/8sDMwBIlgwE Otherwise, read on.
I use Cloudflare with Flexible SSL enabled and HTTP Rewrites. If you do not use Cloudflare, you will need to deploy SSL certificates onto your website. You will also have to have Cloudflare or whatever DNS provider you have, point your domain name to the IP address of your webserver. VDO.Ninja REQUIRES a domain name and SSL.
For webservers, I use NGINX on a Ubuntu server; smaller the better. I rely on Cloudflare to provide caching and SSL, so my installation of NGINX is pretty simple.
```
sudo apt-get update
apt-get install nginx -y
sudo vi /etc/nginx/sites-available/default
sudo systemctl restart nginx
```
An example NGINX config file that "hides" the file extensions is as follows. Update the file as needed and
```server {
listen 80;
listen [::]:80;
server_name vdo.ninja;
root /var/www/html/vdo.ninja;
index index.html;
location ~ ^/([^/]+)/([^/?]+)$ {
root /var/www/html/vdo.ninja;
try_files /$1/$2 /$1/$2.html /$1/$2/ /$2 /$2/ /$1/index.html;
add_header Access-Control-Allow-Origin *;
}
location / {
if ($request_uri ~ ^/(.*)\.html$) {
return 302 /$1;
}
try_files $uri $uri.html $uri/ /index.html;
add_header Access-Control-Allow-Origin *;
}
}
```
You'll want to deploy (copy/clone) the GitHub VDO.Ninja files into your NGINX web folder, that is specified in your NGINX config file. Update the NGINX config file to match your domain and and folder, etc. Restart NGINX after.
As for the TURN server, it can run on a single or dual-core computer. It doesn't take much to host many users -- it mainly just needs a good internet connection. Most users will not need a TURN server, but since VDO.Ninja handles many different types of users, the TURN server is there as a failsafe for those occasional problem users. I'm assuming you know why you need and want a TURN server -- if not, you may not actually need one.
A guide and sample config file for the turn server is here:
https://github.com/steveseguin/vdo.ninja/blob/master/turnserver.md
If deploying to GCP or AWS, you might need to make some tweaks to the IP address values to include the internet local IP as well as the external. Please see online guides no setting up a TURN server for your particular setup. Setups will vary.
Once you have your TURN server setup, you can update the index.html of the VDO.Ninja code. Nightly or official releases should be fine to pull. You probably will want to uncomment the lines linked below once deployed, adjusting the default values to your liking and updating the server location address and credentials of your TURN server (if you deployed one that is). Unless your TURN server also provides STUN capabilities, you may want to also use the Google STUN servers, so uncomment that stuff too.
https://github.com/steveseguin/vdo.ninja/blob/df6c147311b9e7d19659ddbb1799d6598f59aa0d/index.html#L644
A newly deployed code deployment should work without any changes to the index.html file. The code needs to be constantly kept up to date though, as after a few months it may become deprecated and stop working. This is the reality of deploying VDO.Ninja -- you will need to update it every few months for it to continue to function well. Keep this in mind when making changes to the VDO.Ninja source code, as heavy custom changes will make updating harder to do. The fewer the changes the better.
My suggestion? Limit changes to images and perhaps the translation files (maybe add a new one); these are good starting points. If making changes to the main.css style sheet or index.html file, you should be mostly okay too, since these files are designed to be changed; I try to keep that in mind when updating the code at least. Making changes to other files though is strongly not recommend and in some cases discouraged. If you find a bug or need to make a change to other files, it might be best to make a Pull Request with the desired changes and hope it gets adopted into the main codebase.
### Internet-free deployments
For those looking to deploy a completely Internet-free or fully-isolated option, you'll need to deploy your own STUN services and a handshake server as well.
Details on how to deploy your own private STUN server can be Googled online; it's rather out of the scope of this guide's goals though. As of this writing, VDO.Ninja uses Google's public STUN servers, but most TURN-servers offers STUN server functionality as well; just takes some added configuration.
Internet-free deployments will also need to deal with private SSL certificates and any DNS secure context issues that may arise. VDO.Ninja relies on SSL for security, but if you can't figure out how to do private SSL issuance, these SSL restrictions can be disabled via the Chrome browser's command-line. VDO.Ninja may complain about the lack of security if you take this approach though.
Finally, there is an experimental handshake server option that lets advanced users use a basic/generic websocket service as a personal handshake server; useful for air-gapped private deployments of the service. A simple socket server has been developed that can be used as a personal handshake server. Documentation with installation instructions are included here: https://github.com/steveseguin/websocket_server
Just on a side note, support for piesocket.com has also been added as a third-party handshake-server service option, which demonstrates and offers a cheap alternative to a managed hosted alternative of the official handshake service. If using piesocket, you can just do &pie=APKKEY to use that service, without deploying any code or servers yourself. The free tier is quite generous and I have no affiliation with them, but you'll need the Internet to make use of their service.
Regards,
Steve

18962
lib.js Normal file

File diff suppressed because one or more lines are too long

1
lineawesome/LICENSE.txt Normal file
View File

@@ -0,0 +1 @@
If you use the icons publicly, please link to https://icons8.com/line-awesome somewhere on your page or artwork, so that more creators could know about it and use it for free.

1
lineawesome/Readme.md Normal file
View File

@@ -0,0 +1 @@
Please see: https://icons8.com/line-awesome

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

4340
main.css

File diff suppressed because it is too large Load Diff

5371
main.js

File diff suppressed because it is too large Load Diff

BIN
media/bg_sample.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
media/bg_sample2.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

73
media/camera_inkscape.svg Normal file
View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="91.859253mm"
height="49.479435mm"
viewBox="0 0 91.859252 49.479436"
version="1.1"
id="svg4530"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="camera_inkscape.svg">
<defs
id="defs4524" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.3235466"
inkscape:cx="338.91574"
inkscape:cy="131.01428"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:snap-text-baseline="false"
inkscape:snap-bbox="false"
inkscape:snap-global="true"
inkscape:window-width="2560"
inkscape:window-height="1361"
inkscape:window-x="2391"
inkscape:window-y="-9"
inkscape:window-maximized="1" />
<metadata
id="metadata4527">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-4.0990803,-66.079714)">
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.29999971;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:22.7149601;stroke-opacity:1;paint-order:markers fill stroke"
id="rect5164"
width="65.568779"
height="43.179436"
x="7.2490802"
y="69.229713" />
<path
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.30000019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 73.017762,94.91748 19.790574,15.89243 V 73.227808 L 73.117714,88.420574 Z"
id="path5172"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
media/cap.webm Normal file

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 757 B

After

Width:  |  Height:  |  Size: 757 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

88
media/grid_inkscape.svg Normal file
View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="90.680443mm"
height="86.222237mm"
viewBox="0 0 90.680443 86.222237"
version="1.1"
id="svg4530"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="grid_inkscape.svg">
<defs
id="defs4524" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.9358888"
inkscape:cx="-118.37523"
inkscape:cy="171.46325"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:snap-text-baseline="false"
inkscape:snap-bbox="false"
inkscape:snap-global="true"
inkscape:window-width="1558"
inkscape:window-height="815"
inkscape:window-x="3042"
inkscape:window-y="342"
inkscape:window-maximized="0" />
<metadata
id="metadata4527">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-7.8547072,-50.964769)">
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.30000019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:22.7149601;stroke-opacity:1"
id="rect5084"
width="37.378147"
height="35.23999"
x="11.004707"
y="54.115173" />
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.30000019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:22.7149601;stroke-opacity:1"
id="rect5084-7"
width="37.378147"
height="35.23999"
x="58.006535"
y="54.114769" />
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.30000019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:22.7149601;stroke-opacity:1"
id="rect5084-5"
width="37.378147"
height="35.23999"
x="11.005169"
y="98.797012" />
<rect
style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:6.30000019;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:22.7149601;stroke-opacity:1"
id="rect5084-7-8"
width="37.378147"
height="35.23999"
x="58.007"
y="98.797005" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

1
media/hd.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="white" x="0px" y="0px" viewBox="0 0 122.88 122.87" style="enable-background:new 0 0 122.88 122.87" xml:space="preserve"><g><path d="M122.88,77.63v41.12c0,2.28-1.85,4.12-4.12,4.12H77.33v-9.62h35.95c0-12.34,0-23.27,0-35.62H122.88L122.88,77.63z M77.39,9.53V0h41.37c2.28,0,4.12,1.85,4.12,4.12v41.18h-9.63V9.53H77.39L77.39,9.53z M9.63,45.24H0V4.12C0,1.85,1.85,0,4.12,0h41 v9.64H9.63V45.24L9.63,45.24z M45.07,113.27v9.6H4.12c-2.28,0-4.12-1.85-4.12-4.13V77.57h9.63v35.71H45.07L45.07,113.27z"/></g></svg>

After

Width:  |  Height:  |  Size: 650 B

BIN
media/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

BIN
media/join.mp3 Normal file

Binary file not shown.

BIN
media/join.ogg Normal file

Binary file not shown.

BIN
media/join.wav Normal file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More