Merge branch 'develop' into feature/turn-server_with_static-auth-secret
2
.github/workflows/update_translations.yml
vendored
@ -35,5 +35,5 @@ jobs:
|
||||
with:
|
||||
commit-message: Generated updated translations
|
||||
branch: generated_translations
|
||||
title: "[OBSNinja Bot] Updated translations"
|
||||
title: "[VDONinja Bot] Updated translations"
|
||||
labels: i18n
|
||||
|
||||
@ -1,16 +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
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
49
IFRAME.md
@ -1,38 +1,47 @@
|
||||
## OBS.Ninja - iFrame API documentation
|
||||
***
|
||||
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.
|
||||
|
||||
OBS.Ninja (OBSN) is offers here a simple and free solution to quickly enable real-time video streaming in their websites. OBSN wishes to make live video streaming development accessible to any developer, even novices, yet still remain flexible and powerful.
|
||||
|
||||
While OBS.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 OBS.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 OBSN 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 OBSN iframe can be done in HTML or programmatically with Javascript like so:
|
||||
## 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://obs.ninja/?webcam";
|
||||
iframe.src = "https://vdo.ninja/?webcam";
|
||||
```
|
||||
|
||||
You can also make an OBS.Ninja without Javascript, using just HTML, like
|
||||
You can also make an VDO.Ninja without Javascript, using just HTML, like
|
||||
|
||||
`<iframe allow="autoplay;camera;microphone" src="https://obs.ninja/?view=vhX5PYg&cleanoutput&transparent"></iframe>`
|
||||
`<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 OBS.Ninja is with its Internet Speedtest, which has two OBS.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://obs.ninja/speedtest
|
||||
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/obsninja/tree/master/examples
|
||||
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://obs.ninja/iframe You can enter an OBS.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/obsninja/blob/master/iframe.html
|
||||
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 OBS.Ninja, while the former is used to initiate the instance to a desired state.
|
||||
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/obsninja/wiki/Advanced-Settings
|
||||
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:
|
||||
|
||||
@ -56,7 +65,7 @@ Some of the more interesting ones primarily for iframe users might include:
|
||||
- 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 OBSN 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:
|
||||
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");
|
||||
@ -228,7 +237,7 @@ button.onclick = () => {
|
||||
};
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
// As for listening events, where the parent listens for responses or events from the OBSN child frame:
|
||||
// As for listening events, where the parent listens for responses or events from the VDON child frame:
|
||||
|
||||
// ////////// LISTEN FOR EVENTS
|
||||
|
||||
@ -298,10 +307,8 @@ eventer(messageEvent, function (e) {
|
||||
});
|
||||
```
|
||||
|
||||
This OBS.Ninja API is developed and expanded based on user feedback and requests. It is by no means complete.
|
||||
This VDO.Ninja API is developed and expanded based on user feedback and requests. It is by no means complete.
|
||||
|
||||
Regarding versioning, I currently host past versions of OBS.Ninja, so using those past versions can ensure some level of consistency and expectation. https://obs.ninja/v12/ for example is the version 12 hosted version. The active and main production version of OBSN of course undergoes constant updates, and while I try to maintain backwards compatibility with changes to the API, it is still early days and changes might happen.
|
||||
|
||||
Please feel free to follow me in the OBS.Ninja Discord channel, where I post news about updates and listen to requests. The upcoming version of OBS.Ninja is also often hosted at https://obs.ninja/beta, where you can explore new features and help crush any unexpected bugs.
|
||||
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
|
||||
|
||||
11
LICENCE.md
@ -1,15 +1,14 @@
|
||||
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: [AGPLv3.md](https://github.com/steveseguin/obsninja/blob/master/AGPLv3.md)
|
||||
The VDO.Ninja source repository is governed by the GNU AFFERO GENERAL PUBLIC LICENSE. (AGPL-3.0)
|
||||
That AGPL-3.0 licence can be found here: [AGPLv3.md](https://github.com/steveseguin/vdo.ninja/blob/master/AGPLv3.md)
|
||||
|
||||
In essence, OBS.Ninja is open-source and free to use, both for commercial and non-commercial use.
|
||||
In essence, VDO.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.
|
||||
Such licencing and copyright information will be contained in the file's header and be limited to those files.
|
||||
If no such header is present in a file, the default AGPL-3.0 licence applies.
|
||||
Alternative licencing options can be made available on request, if AGPL-3.0 is not appropriate.
|
||||
|
||||
Unless stated otherwise, all code is copyright 2020 Stephen Seguin. All rights reserved.
|
||||
Contributors to the OBS.Ninja project must first agree to the Contributor License Agreement (CLA).
|
||||
Unless stated otherwise, all code is copyright 2021 Stephen Seguin. All rights reserved.
|
||||
Contributors to the VDO.Ninja project must first agree to the Contributor License Agreement (CLA).
|
||||
|
||||
Thank you for your understanding.
|
||||
|
||||
91
README.md
@ -1,72 +1,87 @@
|
||||
|
||||
<img src="media/obsNinja_logo_full.png" alt="Logo by brimace" height="150" />
|
||||
### ⚠ Notice! We've rebranded from OBS.Ninja to VDO.Ninja - all else is staying the same ✨
|
||||
|
||||
## 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.
|
||||
<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" />
|
||||
|
||||
OBS Ninja is not affiliated with OBS. OBS.Ninja is designed to allow content creators to produce real-time live shows with OBS Studio (or other compatible software) using remote media streams. It can also turn smartphones into wireless webcams, with additional Virtualcam software.
|
||||
## What is **VDO NINJA**
|
||||
VDO.Ninja uses peer-to-peer technology to bring remote cameras into OBS or other studio software.
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2575698/94018108-34b1de00-fd7e-11ea-8c7d-df001253b60d.png" height="300" />
|
||||
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.vdo.ninja
|
||||
Please see the sub-reddit added info: https://reddit.com/r/vdoninja
|
||||
Also check out the user documentation at: https://docs.vdo.ninja
|
||||
|
||||
<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 OBS.Ninja on YouTube: https://www.youtube.com/watch?v=6R_sQKxFAhg
|
||||
|
||||
Here is a podcast series showing how to use different basic OBS.Ninja features, including macOS support: https://www.youtube.com/watch?v=XfSqufuoV74&list=PLWodc2tCfAH1l_LDvEyxEqFf42hOBKqQM
|
||||
A video demo and playlist of the basic usage of VDO.Ninja on YouTube can be found here: https://www.youtube.com/watch?v=QaA_6aOP9z8&list=PLWodc2tCfAH1l_LDvEyxEqFf42hOBKqQM&index=1.
|
||||
|
||||
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 25/26, but there are workarounds. Please see the subreddit or [the Wiki](https://github.com/steveseguin/obsninja/wiki).
|
||||
|
||||
## What's in this repo
|
||||
This repo contains software for OBS.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/obsninja/wiki) for the project in this repo, which contains added information on how to use the software.
|
||||
This repo contains the web client software for VDO.Ninja, along with many sample apps that leverage its IFRAME API. A sample config file and instructions for setting up an optional TURN video relay server is also provided here. The user documentation for VDO.Ninja itself is found at docs.vdo.ninja.
|
||||
|
||||
## 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/obsninja/blob/master/install.md) for added details and alternative install options.
|
||||
VDO.Ninja is available as a free-to-use hosted service at https://vdo.ninja, so deploying is optional. If you do wish to self-deploy the service however, details are provided below.
|
||||
|
||||
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 OBS.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 OBS.Ninja, I'd ask you use your own TURN servers instead.
|
||||
Hosting a private deployment can be as simple as hosting the files in this repository on a HTTPS-enabled webserver. For a very simple method on how to do this, there's a video guide here: https://www.youtube.com/watch?v=uYLKkX2_flY
|
||||
|
||||
For more advanced users, you can see the [install.md](https://github.com/steveseguin/vdoninja/blob/master/install.md) file for alternative hosting options and more details on deploying additional system components. Limited technical support is provided for self-deployments, mainly due to how time-consuming such requests are, but the details to fully-deploy all system components are provided in the install.md file.
|
||||
|
||||
If self-hosting, you might also wish to host your own video relay TURN server. Directions on how to deploy a TURN server are listed in the turnserver.md file. Only about ~ 5% of remote guests usually will need a TURN server, often those connected via 4G LTE or those behind a strict firewall, but most other users don't need one. While VDO.Ninja does host some pubiic TURN servers, they are quite expensive to operate, so please try to avoid abusing if possible. If you are deploying your own version of VDO.Ninja, I'd ask you to use your own TURN servers if you are capable of doing so; it's understandable if you aren't able to though.
|
||||
|
||||
### Develop vs Release versions
|
||||
|
||||
The develop branch is a bit like the preview or nightly version of VDO.Ninja. It's intended to be functional, but it may not be that well tested or there could be incomplete features. This version aligns closely with what is normally on vdo.ninja/beta/ or vdo.ninja/alpha/, which is well suited for those wishing to submit code changes or to gain access to experimental new features. You can access the GitHub develop on Github pages here as well: https://steveseguin.github.io/vdo.ninja/
|
||||
|
||||
Release versions of VDO.Ninja have their own branches though, with the newest release being hosted at https://vdo.ninja/. These latest release branch will be updated to fix bugs or critical issues as needed, but are otherwise unchanged. https://github.com/steveseguin/vdo.ninja/branches
|
||||
|
||||
Due to the nature of live video production, where unexpected changes to the app are not welcomed usually, I don't update https://vdo.ninja/ all that often. As well, constant updates to the primary hosted app makes supporting users challenging, as its hard to tell if an issue is with the code or with the user. For this reason, VDO.Ninja does infrequent updates to the primary hosted production version. Users wanting newer features, or who have greater risk tolerance, should use the beta version (vdo.ninja/beta/). The alpha version is usually updated daily, whereas beta is usually updated weekly.
|
||||
|
||||
## Server side / API software
|
||||
Since OBS.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 OBS.Ninja at present. (other than time, of course)
|
||||
Since VDO.Ninja uses peer-2-peer technology, video connections are made directly between viewer and publisher in 95% of cases. Hosting a TURN server yourself may help improve performance, but less than 1% of users will see an improvement to video quality by using one. They also will not help lower bandwidth usage or CPU usage, so generally you wish to avoid using them if possible.
|
||||
|
||||
Other than TURN servers, OBS.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. The handshake server's code is currently not available, so basic access to the Internet is still required to use OBS.Ninja even with a private deployment.
|
||||
Details on how to deploy a TURN server are provided; see: turnserver.md. For those capable of hosting their own TURN server, that would be appreciated if possible, as TURN servers are the largest cost incurred by VDO.Ninja at present. (other than time, of course)
|
||||
|
||||
Development builds of OBS.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.
|
||||
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
|
||||
|
||||
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 near 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
|
||||
If urgent, join me on discord: https://discord.vdo.ninja or email me at steve@seguin.email (Steve may not respond to emails if deemed unimportant)
|
||||
|
||||
## 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, but has been expanded to have additional functionality for content creators in general
|
||||
https://github.com/steveseguin/electroncapture
|
||||
|
||||
### 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 integration. Developed by Steve as well! https://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/
|
||||
### Social Stream Ninja
|
||||
A free Chrome extension that lets you stream and feature chat comments from Youtube, Twitch, Facebook, and more. Featured comments will appear directly in OBS or VMix as an overlay, or as a stream list of comments. It also includes a dock for more advanced function, such as text-to-speech, sentiment analysis, and saving to disk. No chroma-keying needed and the styling is pretty easy to customize without needing to modify the Chrome extension itself.
|
||||
http://socialstream.ninja
|
||||
|
||||
### 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.
|
||||
https://steves.app/
|
||||
### Rasbperry Ninja
|
||||
Use a Raspberry Pi, NVidia Jetson, or Linux box as a dedicated camera for VDO.Ninja. This project can use the hardware encoder of the RPi or Jetson to enable 1080p30 or even 4K video capture and webRTC-based broadcasting. Support for USB, CSI, and HDMI video sources is available. Python-based.
|
||||
[http://socialstream.ninja](https://github.com/steveseguin/raspberry_ninja)
|
||||
|
||||
## 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.md](turnserver.md)
|
||||
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://obs.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 OBS.Ninja.
|
||||
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.
|
||||
|
||||
@ -74,4 +89,12 @@ Additional security features are being added weekly on request. Please ask about
|
||||
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 'mostly' 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.
|
||||
|
||||
## Contributors of this repo
|
||||
<a href="https://github.com/steveseguin/vdoninja/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=steveseguin/vdoninja" />
|
||||
</a>
|
||||
|
||||
752
check.html
Normal file
@ -0,0 +1,752 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="./lineawesome/css/line-awesome.min.css" />
|
||||
<link rel="stylesheet" href="./speedtest.css?ver=1" />
|
||||
<meta charset="utf8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>VDON Speed Test</title>
|
||||
<style>
|
||||
.fullscreen {
|
||||
width:100%;
|
||||
height: calc(100% - 35px);
|
||||
position:absolute;
|
||||
left:0;
|
||||
display:block;
|
||||
background-color: #444;
|
||||
color:white;
|
||||
margin: auto;
|
||||
padding-top: 35px;
|
||||
transition: all ease-in 1s;
|
||||
animation-name: fadein;
|
||||
animation-duration: .3s;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
0% {opacity: 0.5;}
|
||||
100% {opacity: 1;}
|
||||
}
|
||||
a {
|
||||
color: white;
|
||||
}
|
||||
#controls button {
|
||||
cursor: pointer;
|
||||
display: inline;
|
||||
padding: 20px;
|
||||
}
|
||||
.hidden {
|
||||
display:none!important;
|
||||
}
|
||||
body {
|
||||
text-align: center;
|
||||
height:unset;
|
||||
background: #444;
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
color:white;
|
||||
}
|
||||
h2 {
|
||||
width: 760px;
|
||||
margin: auto;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
li {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
button{
|
||||
margin: 50px auto;
|
||||
font-size: 120%;
|
||||
padding: 20px 30px;
|
||||
cursor:pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="fullscreen" id="page1">
|
||||
<h1>
|
||||
Welcome
|
||||
</h1>
|
||||
<h2>
|
||||
This application will access your camera and complete a video test stream.
|
||||
<br />
|
||||
|
||||
<br />
|
||||
The test will take a few minutes to complete.<br />
|
||||
<button onclick="next1();">Continue</button>⭐⭐⭐
|
||||
|
||||
</h2>
|
||||
</div>
|
||||
<div class="fullscreen hidden" id="page2">
|
||||
|
||||
<h2>
|
||||
Please note, for best results:<br /><br />
|
||||
<li>Connect your computer to a wired connection, instead of Wi-Fi</li><br />
|
||||
<li>Have no other applications open while running this test</li><br />
|
||||
<li>If using a laptop, connect your laptop to a power outlet</li><br />
|
||||
🌠<button onclick="next2();">Continue</button>⭐⭐
|
||||
|
||||
</h2>
|
||||
</div>
|
||||
<div class="fullscreen hidden" id="page3">
|
||||
|
||||
<h2>
|
||||
The next step will access your camera and microphone.<br /><br />
|
||||
<br />
|
||||
Accept the camera and microphone permissions if prompted.
|
||||
<br /><br />
|
||||
<img src='./media/accept.png'/><br />
|
||||
🌠🌠<button onclick="next3();">Continue</button>⭐
|
||||
|
||||
</h2>
|
||||
</div>
|
||||
<div id="mainapp" class="hidden">
|
||||
<h1>
|
||||
Video and stream quality check
|
||||
</h1>
|
||||
<div id="container">
|
||||
</div>
|
||||
<div class="hidden" id="graphs">
|
||||
<div class="graph">
|
||||
<h3>Bitrate (kbps)</h3>
|
||||
<span>0</span>
|
||||
<canvas id="bitrate-graph"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="graph">
|
||||
<h3>Buffer delay (ms)</h3>
|
||||
<span>0</span>
|
||||
<canvas id="buffer-graph"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="graph">
|
||||
<h3>Packet Loss (%)</h3>
|
||||
<span>0</span>
|
||||
<canvas id="packetloss-graph"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:none;" id="explanation">
|
||||
<div id="remote"></div>
|
||||
<br />
|
||||
Testing location: <select name="turnlist" id="turnlist" onchange="reloadTurn();" title="Select an exact location to test against">
|
||||
<option selected value="">Automatic</option>
|
||||
<option value="de1">Saarbruecken, Germany</option>
|
||||
<option value="de2">Frankfurt, Germany</option>
|
||||
<option value="fr1">Strasbourg, France</option>
|
||||
<option value="bra1">São Paulo, Brazil</option>
|
||||
<option value="pol1">Warsaw, Poland</option>
|
||||
<option value="cae1">Montreal, Canada</option>
|
||||
<option value="use1">Virgina, USA</option>
|
||||
<option disabled value="usc1">Chicago, USA</option>
|
||||
<option disabled value="usw1">Los Angeles, USA</option>
|
||||
<option value="usw2">Oregon, USA</option>
|
||||
<option value="aus1">Sydney, Australia</option>
|
||||
<option value="jap1">Tokyo, Japan</option>
|
||||
<option value="sing1">Singapore</option>
|
||||
<option value="ind1">Mumbai, India</option>
|
||||
<option value="pol1">Warsaw, Poland</option>
|
||||
</select>
|
||||
<br /><br /><br />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
function getChromeVersion() {
|
||||
var raw = navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./);
|
||||
return raw ? parseInt(raw[2], 10) : false;
|
||||
}
|
||||
|
||||
function next1(){
|
||||
document.getElementById("page1").classList.add("hidden");
|
||||
document.getElementById("page2").classList.remove("hidden");
|
||||
}
|
||||
|
||||
function next2(){
|
||||
document.getElementById("page2").classList.add("hidden");
|
||||
document.getElementById("page3").classList.remove("hidden");
|
||||
loadIframe(region);
|
||||
}
|
||||
|
||||
function next3(){
|
||||
document.getElementById("page3").classList.add("hidden");
|
||||
document.getElementById("mainapp").classList.remove("hidden");
|
||||
loadIframe(region);
|
||||
}
|
||||
|
||||
(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 quality_reason = "";
|
||||
var encoder = "";
|
||||
var Round_Trip_Time_ms = "";
|
||||
var recordResults = false;
|
||||
|
||||
function copyFunction(copyText) {
|
||||
alert("Log copied to the clipboard.");
|
||||
try {
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(0, 99999);
|
||||
document.execCommand("copy");
|
||||
} catch (e) {
|
||||
var dummy = document.createElement("input");
|
||||
document.body.appendChild(dummy);
|
||||
dummy.value = copyText;
|
||||
dummy.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(dummy);
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
var logged = [];
|
||||
function logData(data) {
|
||||
logged.push(data);
|
||||
}
|
||||
|
||||
function reloadTurn(){
|
||||
console.log("Reloading to change TURN servers");
|
||||
loadIframe(document.getElementById("turnlist").value);
|
||||
}
|
||||
|
||||
function updateTurnlist(value){
|
||||
var select = document.getElementById("turnlist");
|
||||
var selected = select.value;
|
||||
|
||||
select.innerHTML = "";
|
||||
|
||||
var opt = document.createElement("option");
|
||||
opt.value = ""
|
||||
opt.title = "Choose the closest location automatically";
|
||||
opt.innerHTML = "Automatic";
|
||||
select.appendChild(opt);
|
||||
if (selected == ""){
|
||||
opt.selected = true;
|
||||
}
|
||||
|
||||
for (var i =0;i<value.length;i++){
|
||||
var opt = document.createElement("option");
|
||||
opt.value = value[i].locale;
|
||||
opt.title = value[i].name;
|
||||
opt.innerHTML = value[i].name;
|
||||
select.appendChild(opt);
|
||||
if (selected == opt.value){
|
||||
opt.selected = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var eventMethod = window.addEventListener
|
||||
? "addEventListener"
|
||||
: "attachEvent";
|
||||
var eventer = window[eventMethod];
|
||||
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
|
||||
var previousResolution;
|
||||
var timer= null;
|
||||
|
||||
var statsSent = false;
|
||||
|
||||
eventer(messageEvent, function (e) {
|
||||
if ("action" in e.data) {
|
||||
|
||||
if (e.data.action == "available-speedtest-servers"){
|
||||
console.warn("Speedtest server list loaded");
|
||||
updateTurnlist(e.data.value);
|
||||
}
|
||||
|
||||
if (e.data.action == "started-camera"){
|
||||
loadIframe2();
|
||||
document.getElementById("localVideoText").innerText = "Local video before transmission";
|
||||
}
|
||||
|
||||
if (e.data.action == "new-view-connection") {
|
||||
if (timer===null){
|
||||
timer = 0;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
buttonContainer.querySelectorAll(
|
||||
"#controls button:last-child"
|
||||
)[0].style.display = "inline";
|
||||
|
||||
|
||||
var showdetails = document.createElement("button");
|
||||
showdetails.onclick = function(){
|
||||
document.getElementById("graphs").classList.toggle('hidden');
|
||||
}
|
||||
showdetails.innerText = "Show testing details";
|
||||
buttonContainer.appendChild(showdetails);
|
||||
|
||||
setTimeout(function(button){
|
||||
button.click();
|
||||
}, 90000,button);
|
||||
}
|
||||
|
||||
if (e.data.action == "setVideoBitrate") {
|
||||
buttonContainer.querySelectorAll("button").forEach((button) => {
|
||||
button.classList.remove("active");
|
||||
});
|
||||
if (e.data.value == 30) {
|
||||
document
|
||||
.querySelectorAll("#controls button")[0]
|
||||
.classList.add("active");
|
||||
}
|
||||
if (e.data.value == 6000) {
|
||||
document
|
||||
.querySelectorAll("#controls button")[1]
|
||||
.classList.add("active");
|
||||
}
|
||||
if (e.data.value == -1) {
|
||||
document
|
||||
.querySelectorAll("#controls button")[2]
|
||||
.classList.add("active");
|
||||
}
|
||||
}
|
||||
}
|
||||
if ("stats" in e.data) {
|
||||
var out = "";
|
||||
|
||||
for (var someValue in e.data.stats.inbound_stats) {
|
||||
out += printValues(e.data.stats.inbound_stats[someValue]);
|
||||
if (e.data.stats.inbound_stats[someValue]){
|
||||
if (!statsSent){
|
||||
statsSent = e.data.stats.inbound_stats[someValue];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var someValue in e.data.stats.outbound_stats) {
|
||||
if (e.data.stats.outbound_stats[someValue].quality_limitation_reason){
|
||||
if (quality_reason != e.data.stats.outbound_stats[someValue].quality_limitation_reason) {
|
||||
quality_reason = e.data.stats.outbound_stats[someValue].quality_limitation_reason;
|
||||
logData({"QLR": quality_reason});
|
||||
}
|
||||
}
|
||||
|
||||
if (e.data.stats.outbound_stats[someValue].encoder){
|
||||
if (encoder != e.data.stats.outbound_stats[someValue].encoder) {
|
||||
encoder = e.data.stats.outbound_stats[someValue].encoder;
|
||||
logData({"encoder":encoder});
|
||||
}
|
||||
} else if (e.data.stats.outbound_stats[someValue].video_codec){
|
||||
if (encoder != e.data.stats.outbound_stats[someValue].video_codec) {
|
||||
encoder = e.data.stats.outbound_stats[someValue].video_codec;
|
||||
logData({"encoder":encoder});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var key in e.data.stats.inbound_stats[streamID]){
|
||||
if (typeof e.data.stats.inbound_stats[streamID][key] == "object"){
|
||||
if ("Bitrate_in_kbps" in e.data.stats.inbound_stats[streamID][key]){
|
||||
var bitrate = e.data.stats.inbound_stats[streamID][key]["Bitrate_in_kbps"];
|
||||
updateData("bitrate", bitrate);
|
||||
}
|
||||
if ("Buffer_Delay_in_ms" in e.data.stats.inbound_stats[streamID][key]){
|
||||
var buffer = e.data.stats.inbound_stats[streamID][key]["Buffer_Delay_in_ms"];
|
||||
updateData("buffer", buffer);
|
||||
}
|
||||
if ("packetLoss_in_percentage" in e.data.stats.inbound_stats[streamID][key]){
|
||||
var packetloss = e.data.stats.inbound_stats[streamID][key]["packetLoss_in_percentage"];
|
||||
if (packetloss != undefined) {
|
||||
packetloss = packetloss.toFixed(2);
|
||||
updateData("packetloss", packetloss);
|
||||
}
|
||||
}
|
||||
|
||||
if ("Resolution" in e.data.stats.inbound_stats[streamID][key]){
|
||||
var resolution = e.data.stats.inbound_stats[streamID][key]["Resolution"];
|
||||
|
||||
if (previousResolution != resolution) {
|
||||
previousResolution = resolution;
|
||||
logData({"resolution": resolution});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var streamID = "";
|
||||
var possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
|
||||
for (var i = 0; i < 7; i++) {
|
||||
streamID += possible.charAt(
|
||||
Math.floor(Math.random() * possible.length)
|
||||
);
|
||||
}
|
||||
|
||||
if (urlParams.has("sid")) {
|
||||
streamID = urlParams.get("sid");
|
||||
} else if (urlParams.has("push")) {
|
||||
streamID = urlParams.get("push");
|
||||
}
|
||||
|
||||
var region = "";
|
||||
if (urlParams.has("location")) {
|
||||
region = urlParams.get("location");
|
||||
} else if (urlParams.has("region")) {
|
||||
region = urlParams.get("region");
|
||||
}
|
||||
|
||||
<!-- <option selected value="">Automatic</option> -->
|
||||
<!-- <option value="de1">Saarbruecken, Germany</option> -->
|
||||
<!-- <option value="de2">Frankfurt, Germany</option> -->
|
||||
<!-- <option value="fr1">Strasbourg, France</option> -->
|
||||
<!-- <option value="bra1">São Paulo, Brazil</option> -->
|
||||
<!-- <option value="pol1">Warsaw, Poland</option> -->
|
||||
<!-- <option value="cae1">Montreal, Canada</option> -->
|
||||
<!-- <option value="use1">Virgina, USA</option> -->
|
||||
<!-- <option disabled value="usc1">Chicago, USA</option> -->
|
||||
<!-- <option disabled value="usw1">Los Angeles, USA</option> -->
|
||||
<!-- <option value="usw2">Oregon, USA</option> -->
|
||||
<!-- <option value="aus1">Sydney, Australia</option> -->
|
||||
<!-- <option value="jap1">Tokyo, Japan</option> -->
|
||||
<!-- <option value="sing1">Singapore</option> -->
|
||||
<!-- <option value="ind1">Mumbai, India</option> -->
|
||||
<!-- <option value="pol1">Warsaw, Poland</option> -->
|
||||
|
||||
var iframe1 = document.createElement("iframe");
|
||||
|
||||
function loadIframe(zone="") {
|
||||
// this is pretty important if you want to avoid camera permission popup problems. YOu need to load the iFRAME after you load the parent body. A quick solution is like: <body onload=>loadIframe();"> !!!
|
||||
|
||||
document.getElementById("container").innerHTML = "";
|
||||
|
||||
|
||||
var iframeContainer = document.createElement("span");
|
||||
|
||||
iframe1.allow="autoplay;camera;microphone;display-capture;";
|
||||
iframe1.allowtransparency="true";
|
||||
iframe1.allowfullscreen ="true";
|
||||
|
||||
//iframe.allow = "autoplay";
|
||||
var srcString = "./?push=" + streamID + "&cleanoutput&privacy&"+testType+"&audiodevice=1&fullscreen&transparent&remote&maxbandwidth&speedtest="+zone;
|
||||
|
||||
if (urlParams.has("turn")) {
|
||||
srcString = srcString + "&turn=" + urlParams.get("turn");
|
||||
}
|
||||
|
||||
// we are changing some text on page load, just to demonstrate what's possible.
|
||||
iframe1.onload = function (e) {
|
||||
e.target.contentWindow.postMessage(
|
||||
{
|
||||
function: "changeHTML",
|
||||
target: "add_camera",
|
||||
value: "Select your Camera",
|
||||
},
|
||||
"*"
|
||||
);
|
||||
};
|
||||
iframe1.src = srcString;
|
||||
|
||||
iframeContainer.appendChild(iframe1);
|
||||
|
||||
var title = document.createElement("h3");
|
||||
title.innerText = "Select the camera you intend to use";
|
||||
title.id = "localVideoText";
|
||||
iframeContainer.appendChild(title);
|
||||
|
||||
var feeds = document.createElement("div");
|
||||
feeds.id = "feeds";
|
||||
|
||||
document.getElementById("container").appendChild(feeds);
|
||||
document.getElementById("feeds").appendChild(iframeContainer);
|
||||
|
||||
setInterval(function (iframe1) {
|
||||
try {
|
||||
iframe1.contentWindow.postMessage({ getStats: true }, "*");
|
||||
} catch(e){
|
||||
clearInterval(this);
|
||||
}
|
||||
}, 1000, iframe1);
|
||||
|
||||
}
|
||||
|
||||
var buttonContainer = document.createElement("div");
|
||||
buttonContainer.id = "controls";
|
||||
var button = document.createElement("button");
|
||||
|
||||
function loadIframe2(zone="") {
|
||||
|
||||
//document.getElementById("graphs").classList.remove('hidden');
|
||||
var iframe = document.createElement("iframe");
|
||||
var iframeContainer = document.createElement("span");
|
||||
|
||||
iframe.allow = "autoplay";
|
||||
var srcString = "./?view=" + streamID + "&cleanoutput&privacy&noaudio&transparent&bitrate=6000&scale=100&speedtest="+zone; // No TURN servers set on the reciever. Don't want to query for TURN servers needlessly.
|
||||
|
||||
if (urlParams.has("turn")) {
|
||||
srcString = srcString + "&turn=" + urlParams.get("turn");
|
||||
}
|
||||
|
||||
if (urlParams.has("buffer")) {
|
||||
srcString = srcString + "&buffer=" + urlParams.get("buffer");
|
||||
}
|
||||
|
||||
if (urlParams.has("id")) {
|
||||
recordResults = urlParams.get("id") || false;
|
||||
} else if (urlParams.has("session")) {
|
||||
recordResults = urlParams.get("session") || false;
|
||||
}
|
||||
|
||||
iframe.src = srcString;
|
||||
|
||||
iframeContainer.appendChild(iframe);
|
||||
|
||||
var title = document.createElement("h3");
|
||||
title.innerText = "Video after traversing the Internet";
|
||||
iframeContainer.appendChild(title);
|
||||
|
||||
document.getElementById("feeds").appendChild(iframeContainer);
|
||||
|
||||
var br = document.createElement("br");
|
||||
document.getElementById("container").appendChild(br);
|
||||
|
||||
|
||||
|
||||
var div = document.createElement("h1");
|
||||
buttonContainer.appendChild(div);
|
||||
|
||||
|
||||
button.className = "red";
|
||||
//button.disabled = true;
|
||||
button.innerHTML = "Abort the test";
|
||||
|
||||
button.style.display = "none";
|
||||
button.onclick = function (){
|
||||
|
||||
logData(statsSent);
|
||||
|
||||
if (!recordResults){
|
||||
recordResults = "results_"+streamID;
|
||||
document.getElementById("container").innerHTML = "<br />Link to results: <a target='_blank' href='https://vdo.ninja/alpha/results?id="+recordResults+"'>https://vdo.ninja/alpha/results?id="+recordResults+"</a><br /><br /><small><i>Results are anonymous and deleted after 7-days</i></small><br /><br /><br />Final Test Results:<br />";
|
||||
document.getElementById("graphs").classList.remove('hidden');
|
||||
}
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('POST', "https://record.vdo.workers.dev/?name="+recordResults);
|
||||
try {
|
||||
logged = JSON.stringify(logged);
|
||||
} catch(e){
|
||||
console.error(e);
|
||||
}
|
||||
request.send(logged);
|
||||
|
||||
timer = 91;
|
||||
div.innerHTML = "Test ended";
|
||||
clearInterval(interval);
|
||||
|
||||
try{
|
||||
if (iframe){
|
||||
iframe.contentWindow.postMessage({ close: true }, "*");
|
||||
}
|
||||
button.remove();
|
||||
iframe1.remove();
|
||||
iframe.remove();
|
||||
feeds.style.display = "none";
|
||||
} catch(e){};
|
||||
};
|
||||
buttonContainer.appendChild(button);
|
||||
document.getElementById("container").appendChild(buttonContainer);
|
||||
|
||||
var interval = setInterval(function (iframe1) {
|
||||
if (timer==90){
|
||||
document.body.innerHTML = "<h1>Test complete. Thank you</h1>";
|
||||
return;
|
||||
}
|
||||
if (timer>90){
|
||||
clearInterval(interval);
|
||||
return;
|
||||
}
|
||||
if (timer == 30){
|
||||
iframe.contentWindow.postMessage({ bitrate: 4000 }, "*");
|
||||
bitrate.target = 4000;
|
||||
updateData("target", bitrate.target);
|
||||
}
|
||||
if (timer == 60){
|
||||
iframe.contentWindow.postMessage({ bitrate: 6000 }, "*");
|
||||
bitrate.target = 6000;
|
||||
updateData("target", bitrate.target);
|
||||
}
|
||||
|
||||
if (timer!==null){
|
||||
timer+=1
|
||||
div.innerHTML = "Test completes in "+(90-timer)+" seconds";
|
||||
}
|
||||
try {
|
||||
if (iframe1){
|
||||
iframe1.contentWindow.postMessage({ getStats: true }, "*");
|
||||
}
|
||||
} catch(e){
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000, iframe);
|
||||
}
|
||||
|
||||
|
||||
var testType= "webcam&quality=-1&css=speedtest.css";
|
||||
if (urlParams.has("screen") || urlParams.has("ss") || urlParams.has("screenshare") || urlParams.has("screentest")) {
|
||||
document.getElementById("screen").innerHTML = '<a href="./speedtest" style="color: #CCC;">Test webcam-streaming performance here</a>';
|
||||
testType = "quality=0&screenshare&css=speedtest.css"
|
||||
}
|
||||
|
||||
var bitrate = {
|
||||
element: "bitrate-graph",
|
||||
data: 0,
|
||||
max: 6500,
|
||||
target: 2500,
|
||||
};
|
||||
var frames;
|
||||
var buffer = {
|
||||
element: "buffer-graph",
|
||||
data: 0,
|
||||
max: 200,
|
||||
target: 100,
|
||||
};
|
||||
var packetloss = {
|
||||
element: "packetloss-graph",
|
||||
data: 0,
|
||||
max: 3,
|
||||
target: 2,
|
||||
};
|
||||
|
||||
function updateData(type, data) {
|
||||
if (type == "bitrate") {
|
||||
bitrate.data = data;
|
||||
plotData("bitrate", bitrate);
|
||||
plotData("bitrate", bitrate);
|
||||
plotData("bitrate", bitrate);
|
||||
}
|
||||
|
||||
if (type == "buffer") {
|
||||
buffer.data = data;
|
||||
plotData("buffer", buffer);
|
||||
plotData("buffer", buffer);
|
||||
plotData("buffer", buffer);
|
||||
}
|
||||
|
||||
if (type == "packetloss") {
|
||||
packetloss.data = data;
|
||||
plotData("packetloss", packetloss);
|
||||
plotData("packetloss", packetloss);
|
||||
plotData("packetloss", packetloss);
|
||||
}
|
||||
|
||||
logData({[type]: data});
|
||||
}
|
||||
|
||||
function plotData(type, stat) {
|
||||
var canvas;
|
||||
var context;
|
||||
var yScale;
|
||||
|
||||
canvas = document.getElementById(stat.element);
|
||||
context = canvas.getContext("2d");
|
||||
|
||||
if (isNaN(stat.data)) {
|
||||
stat.data = 0;
|
||||
}
|
||||
|
||||
var text = (canvas.previousElementSibling.innerHTML = stat.data);
|
||||
|
||||
var height = context.canvas.height;
|
||||
var width = context.canvas.width;
|
||||
|
||||
var borderWidth = 5;
|
||||
var offset = borderWidth * 2;
|
||||
|
||||
// Create gradient
|
||||
|
||||
var grd = context.createLinearGradient(0, 0, 0, height);
|
||||
|
||||
if (type == "bitrate") {
|
||||
|
||||
if (stat.target == 2500){
|
||||
grd.addColorStop(0, "#33C433");
|
||||
grd.addColorStop(0.7, "#33C433");
|
||||
grd.addColorStop(0.8, "#F3F304");
|
||||
grd.addColorStop(0.92, "#F30404");
|
||||
} else if (stat.target == 4000){
|
||||
grd.addColorStop(0, "#33C433");
|
||||
grd.addColorStop(0.5, "#33C433");
|
||||
grd.addColorStop(0.8, "#F3F304");
|
||||
grd.addColorStop(0.92, "#F30404");
|
||||
} else if (stat.target == 6000){
|
||||
grd.addColorStop(0, "#33C433");
|
||||
grd.addColorStop(0.3, "#33C433");
|
||||
grd.addColorStop(0.8, "#F3F304");
|
||||
grd.addColorStop(0.92, "#F30404");
|
||||
}
|
||||
|
||||
} else {
|
||||
// Higher values are red
|
||||
grd.addColorStop(0, "#F30404");
|
||||
grd.addColorStop(0.3, "#F3F304");
|
||||
grd.addColorStop(0.7, "#33C433");
|
||||
}
|
||||
|
||||
context.strokeStyle = "white";
|
||||
context.fillStyle = grd;
|
||||
//context.fillStyle = "#009933";
|
||||
//context.imageSmoothingEnabled = true;
|
||||
|
||||
yScale = height / stat.max;
|
||||
|
||||
if (stat.data > stat.max) {
|
||||
stat.data = stat.max;
|
||||
}
|
||||
|
||||
if (type == "packetloss" && stat.data == 0.0) {
|
||||
stat.data = 0.1;
|
||||
}
|
||||
|
||||
var x = width - 1;
|
||||
var y = height - stat.data * yScale;
|
||||
var w = 1;
|
||||
|
||||
context.fillStyle = grd;
|
||||
context.fillRect(x, y, w, height);
|
||||
|
||||
// shift everything to the left:
|
||||
var imageData = context.getImageData(1, 0, width - 1, height);
|
||||
context.putImageData(imageData, 0, 0);
|
||||
// now clear the right-most pixels:
|
||||
context.clearRect(width - 1, 0, 1, height);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
84
codecs.html
Normal file
@ -0,0 +1,84 @@
|
||||
<html>
|
||||
<body>
|
||||
<div id="output"></div>
|
||||
<script>
|
||||
|
||||
function getSupportedMimeTypes(media, types, codecs) {
|
||||
const isSupported = MediaRecorder.isTypeSupported;
|
||||
const supported = [];
|
||||
types.forEach((type) => {
|
||||
const mimeType = `${media}/${type}`;
|
||||
acodecs.forEach((codec2) => {
|
||||
codecs.forEach((codec) => [
|
||||
mimeType+';codecs="'+codec+', '+codec2+'"',
|
||||
mimeType+';codecs:"'+codec+', '+codec2+'"',
|
||||
mimeType+';codecs="'+codec.toUpperCase()+', '+codec2.toUpperCase()+'"',
|
||||
mimeType+';codecs:"'+codec.toUpperCase()+', '+codec2.toUpperCase()+'"'
|
||||
].forEach(variation => {
|
||||
if (isSupported(variation)){
|
||||
try {
|
||||
options.mimeType = variation;
|
||||
new MediaRecorder(stream, options);
|
||||
|
||||
mediaSource.addSourceBuffer(variation);
|
||||
|
||||
supported.push(variation);
|
||||
} catch(e){
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
if (isSupported(mimeType))
|
||||
supported.push(mimeType);
|
||||
});
|
||||
return supported;
|
||||
};
|
||||
|
||||
|
||||
var options = {};
|
||||
options.videoBitsPerSecond = parseInt(2500 * 1024);
|
||||
|
||||
const videoTypes = ["webm", "ogg", "mp4", "x-matroska"];
|
||||
const audioTypes = ["webm", "ogg", "mp3", "x-matroska"];
|
||||
const codecs = ["vp9", "vp9.0", "vp8", "vp8.0", "avc1", "av1", "h265", "h.265", "h264", "h.264"]
|
||||
const acodecs = ["opus", "pcm", "aac", "mpeg", "mp4a", "mp3", "vorbis"];
|
||||
|
||||
document.getElementById("output").innerHTML= "";
|
||||
var supportedVideos = null;
|
||||
var supportedAudios = null;
|
||||
// Usage ------------------
|
||||
var stream = null;
|
||||
var mediaSource = new MediaSource();
|
||||
|
||||
var video = document.createElement("video");
|
||||
video.autoplay = true;
|
||||
video.muted = false;
|
||||
video.setAttribute("playsinline","");
|
||||
video.src = URL.createObjectURL(mediaSource);
|
||||
|
||||
mediaSource.addEventListener('sourceopen', sourceOpen);
|
||||
console.log("1");
|
||||
function sourceOpen(e) {
|
||||
console.log("2");
|
||||
navigator.mediaDevices.getUserMedia({audio:true,video:true}).then(function(s) {
|
||||
stream = s;
|
||||
|
||||
supportedVideos = getSupportedMimeTypes("video", videoTypes, codecs);
|
||||
supportedAudios = getSupportedMimeTypes("audio", audioTypes, codecs);
|
||||
|
||||
for (var i=0;i<supportedVideos.length;i++){
|
||||
document.getElementById("output").innerHTML += supportedVideos[i]+"<br/>";
|
||||
}
|
||||
}).catch(function(err) {
|
||||
/* handle the error */
|
||||
});
|
||||
}
|
||||
|
||||
//for (var i=0;i<supportedAudios.length;i++){
|
||||
//document.getElementById("output").innerHTML += supportedAudios[i]+"<br/>";
|
||||
//}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
2163
comms.html
Normal file
121
control.html
Normal file
@ -0,0 +1,121 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
margin:0;
|
||||
padding:0;
|
||||
height:100%;
|
||||
width:100%;
|
||||
border:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body id="body">
|
||||
<button onclick='send({"abc1231":[0,0,50,50],abc1232:[50,0,50,50],abc1233:[0,50,50,50],abc1234:[50,50,50,50]});'>2x2</button>
|
||||
<button onclick='send({"abc1231":[0,0,100,100],abc1232:[0, 0, 0, 0 ],abc1233:[0,0,0,0],abc1234:[0,0,0,0]});'>1</button>
|
||||
<button onclick='send({"abc1231":[0,0,50 ,100],abc1232:[50,0,100,100],abc1233:[0,0,0,0],abc1234:[0,0,0,0]});'>2x1</button>
|
||||
<button onclick='send({"abc1231":[0,0,100,100],abc1232:[70,70,20,20],abc1233:[0,0,0,0],abc1234:[0,0,0,0]});'>Pip</button>
|
||||
<script>
|
||||
|
||||
function updateURL(param, force=false) {
|
||||
var para = param.split('=')[0];
|
||||
if (!(urlParams.has(para)) || (force)){
|
||||
if (history.pushState){
|
||||
|
||||
var arr = window.location.href.split('?');
|
||||
var newurl;
|
||||
if (arr.length > 1 && arr[1] !== '') {
|
||||
newurl = window.location.href + '&' +param;
|
||||
} else {
|
||||
newurl = window.location.href + '?' +param;
|
||||
}
|
||||
|
||||
window.history.pushState({path:newurl},'',newurl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
(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 generateStreamID(){
|
||||
var text = "";
|
||||
var possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
|
||||
for (var i = 0; i < 7; i++){
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
var roomID = "undefined";
|
||||
|
||||
if (urlParams.has("room")){
|
||||
roomID = urlParams.get("room");
|
||||
} else {
|
||||
roomID = generateStreamID();
|
||||
updateURL("room="+roomID);
|
||||
}
|
||||
|
||||
var url = document.URL.substr(0,document.URL.lastIndexOf('/'));
|
||||
|
||||
|
||||
navigator.clipboard.writeText(url+"/mixer?room="+roomID).then(() => {
|
||||
/* clipboard successfully set */
|
||||
}, () => {
|
||||
/* clipboard write failed */
|
||||
});
|
||||
|
||||
document.getElementById("body").innerHTML+=url+"/mixer?room="+roomID;
|
||||
|
||||
|
||||
var socket = new WebSocket("wss://api.action.wtf:666");
|
||||
|
||||
socket.onclose = function (){
|
||||
setTimeout(function(){window.location.reload(true);},100);
|
||||
};
|
||||
|
||||
socket.onopen = function (){
|
||||
socket.send(JSON.stringify({"join":roomID}));
|
||||
}
|
||||
|
||||
|
||||
socket.addEventListener('message', function (event) {
|
||||
if (event.data){
|
||||
var data = JSON.parse(event.data);
|
||||
log(data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.onclose = function (){
|
||||
setTimeout(function(){window.location.reload(true);},100);
|
||||
};
|
||||
|
||||
var counter=0;
|
||||
function send(scene){
|
||||
counter+=1;
|
||||
socket.send(JSON.stringify({"msg":true, "scene":scene, "id":counter}));
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
436
convert.html
@ -1,157 +1,285 @@
|
||||
<head>
|
||||
<link rel="stylesheet" href="./main.css?ver=39" />
|
||||
<style>
|
||||
|
||||
body {
|
||||
|
||||
}
|
||||
|
||||
input {padding:10px;}
|
||||
|
||||
h3 { margin-top:10px; padding:5px;}
|
||||
|
||||
hr {
|
||||
margin: 20px 0;
|
||||
}
|
||||
video{
|
||||
max-width:640px;
|
||||
max-height:360px;
|
||||
padding:20px;
|
||||
}
|
||||
audio{
|
||||
max-width:640px;
|
||||
max-height:360px;
|
||||
padding:20px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body style='color:white'>
|
||||
|
||||
<video id="player" controls style="display:none"></video>
|
||||
<audio id="player2" controls style="display:none"></audio>
|
||||
<div id="info">
|
||||
<h1>Web-based Media Conversion Tools</h1>
|
||||
<hr>
|
||||
<h3>Transcodes WebM files to MP4 files with a fixed 1280x720 resolution. (very slow!)</h3><br />
|
||||
<small><p>This tool performs the following action in your browser: <i> fmpeg -i input.webm -vf scale=1280:720 output.mp4</i></p></small>
|
||||
<input type="file" id="uploader" title="Convert WebM to MP4">
|
||||
|
||||
<hr>
|
||||
<h3>Remuxes MKV files to MP4 files without transcoding.</h3> </p><br /><small><i> fmpeg -i INPUTFILE -vcodec copy -acodec copy output.mp4</i></small>
|
||||
<br /><input type="file" id="uploader2" accept=".mkv" title="Convert MKV to MP4">
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Remuxes WebM files to MP4 files without transcoding (attempts to force high resolutions, also)</h3>
|
||||
<input type="file" id="uploader3" accept=".webm" title="Convert WebM to MP4">
|
||||
|
||||
<hr>
|
||||
<h3>Remuxes WebM to Audio-only files (opus or wav)</h3>
|
||||
<input type="file" id="uploader4" accept=".webm" title="Convert WebM to OPUS">
|
||||
|
||||
<hr>
|
||||
|
||||
</div>
|
||||
<script src="https://unpkg.com/@ffmpeg/ffmpeg@0.9.6/dist/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('info').innerText = "Transcoding file... this will take a while";
|
||||
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('info').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('info').innerText = "Transcoding file... this will take a while";
|
||||
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('info').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('info').innerText = "Tweaking file... this will take a moment";
|
||||
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('info').innerText = "Operation Done. Play video or download it.";
|
||||
}
|
||||
|
||||
const convertToAudioOnly = async ({ target: { files } }) => {
|
||||
const { name } = files[0];
|
||||
document.getElementById('info').innerText = "Transcoding file... this will take a while";
|
||||
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");
|
||||
<link rel="stylesheet" href="./main.css?ver=40" />
|
||||
<style>
|
||||
.container {
|
||||
max-width: min(80%,875px);
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
video.style.display="block";
|
||||
document.getElementById('info').innerText = "Operation Done. Play audio or download it.";
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
h1 {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00538c;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #00538c;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #00538c;
|
||||
}
|
||||
|
||||
.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 (or MKV/FLV) to MP4 (fixed 1280x720 resolution) <span class='warning'>(very slow!)</span></h2>
|
||||
<div>
|
||||
<small>The same as: <i>fmpeg -i input.webm -vf scale="1280:720" output.mp4</i></small>
|
||||
<input type="file" accept=".mkv, .flv, .webm" 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>
|
||||
<small>The same as: <i>ffmpeg.exe -i concat:"<a href='cap.webm' target="_blank">cap.webm</a>|input.webm" -safe 0 -c copy -avoid_negative_ts 1 -strict experimental output.mp4</i></small>
|
||||
<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>
|
||||
<small>The same as: <i>fmpeg -i input.webm -vn -acodec copy output.wav</i></small>
|
||||
<input type="file" id="uploader4" accept=".webm" title="Convert WebM to OPUS (or WAV)">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>MKV (or FLV/WebM) to MP4 (no transcoding)</h2>
|
||||
<div>
|
||||
<small>The same as: <i>fmpeg -i INPUTFILE -vcodec copy -acodec copy output.mp4</i></small>
|
||||
<input type="file" id="uploader2" accept=".mkv, .flv, .webm" title="Convert MKV (or FLV) to MP4">
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Having problems?</h2>
|
||||
<div>
|
||||
For larger files, over 2-gigabytes in size, the browser may not be able to properly process the video in memory.
|
||||
</div>
|
||||
<div>
|
||||
Please consider using FFmpeg <a href='https://ffmpeg.org/download.html' target="_blank">[get it free here]</a> to run these processes from the command-line instead. The corresponding commands are provided above, where you need to replace input.webm with your own file.
|
||||
</div>
|
||||
<div>
|
||||
Other users who find FFmpeg too challenging have had luck using the graphical <a href="https://handbrake.fr/" target="_blank">Handbrake</a> application instead.
|
||||
</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>
|
||||
|
||||
window.onerror = function backupErr(errorMsg, url=false, lineNumber=false) {
|
||||
console.error(errorMsg);
|
||||
alert("An error occured.\n\nIf your file is larger than 2-GB, you may need to run the FFmpeg commands locally or use Handbrake instead.");
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
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];
|
||||
console.log(files[0]);
|
||||
if (files[0].size>2147483648){
|
||||
alert("Warning: The largest file size currently supported is 2-GB.\n\nFor larger files, please instead consider using the FFmpeg commands locally or use Handbrake. ");
|
||||
return;
|
||||
}
|
||||
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];
|
||||
if (files[0].size>2147483648){
|
||||
alert("Warning: The largest file size currently supported is 2-GB.\n\nFor larger files, please instead consider using the FFmpeg commands locally or use Handbrake. ");
|
||||
return;
|
||||
}
|
||||
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];
|
||||
if (files[0].size>2147483648){
|
||||
alert("Warning: The largest file size currently supported is 2-GB.\n\nFor larger files, please instead consider using the FFmpeg commands locally or use Handbrake. ");
|
||||
return;
|
||||
}
|
||||
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];
|
||||
if (files[0].size>2147483648){
|
||||
alert("Warning: The largest file size currently supported is 2-GB.\n\nFor larger files, please instead consider using the FFmpeg commands locally or use Handbrake. ");
|
||||
return;
|
||||
}
|
||||
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>
|
||||
@ -19,7 +19,6 @@ h1 {
|
||||
font-size: 1rem;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
background: #d0d0d0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
style="text-decoration: none; color: white; margin: 2px"
|
||||
>
|
||||
<span data-translate="logo-header">
|
||||
<font id="qos">O</font>BS.Ninja
|
||||
<font id="qos">V</font>DO.Ninja
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -163,7 +163,7 @@ function generateInvite(){
|
||||
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.
|
||||
var salt = location.hostname; // "vdo.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){
|
||||
|
||||
980
electron.html
@ -1,393 +1,587 @@
|
||||
<html>
|
||||
<meta charset="UTF-8">
|
||||
<head><style>
|
||||
html {
|
||||
border:0;
|
||||
margin:0;
|
||||
outline: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;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
#audioOutput{
|
||||
font-size: calc(16px + 0.3vw);
|
||||
max-width:590px
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1030px) {
|
||||
body{
|
||||
zoom: 0.9;
|
||||
-moz-transform: scale(0.9);
|
||||
-moz-transform-origin: 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 940px) {
|
||||
body{
|
||||
zoom: 0.64;
|
||||
-moz-transform: scale(0.64);
|
||||
-moz-transform-origin: 0 0;
|
||||
|
||||
}
|
||||
#audioOutput{
|
||||
font-size: calc(14px + 1.4vw);
|
||||
max-width:486px
|
||||
}
|
||||
}
|
||||
|
||||
#messageDiv {
|
||||
font-size: .7em;
|
||||
color: #DDD;
|
||||
transition: all 0.5s linear;
|
||||
font-style: italic;
|
||||
opacity: 0;
|
||||
text-align: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
</style></head>
|
||||
<body >
|
||||
|
||||
<div id="header" style="-webkit-app-region: drag;color:white;font-size:2em">OBS.Ninja</div>
|
||||
<div class="formcss" >
|
||||
|
||||
<div id='warning4mac' style="border:2px dotted; display:none;max-width:800px; padding:10px; margin:0 90px 20px 90px;color:white;font-size:1.3em"> ✨ Great News! OBS v26.1.2 <a href="https://github.com/obsproject/obs-browser/issues/209#issuecomment-748683083">now supports</a> OBS.Ninja without needing the Electron Capture app! 🥳</div>
|
||||
|
||||
<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>
|
||||
|
||||
<input type="checkbox" class="check" id="buffer" name="buffer" value="false" onclick="modURL(this);">
|
||||
<label for="buffer">Lip-sync Fix</label>
|
||||
|
||||
<br>
|
||||
<div id="messageDiv" style='display:block'><br /></div>
|
||||
<div class="formcss"><center>
|
||||
<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"></select>
|
||||
|
||||
</center></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.
|
||||
*
|
||||
*/
|
||||
|
||||
if (navigator.userAgent.indexOf('Mac OS X') != -1){
|
||||
document.getElementById("warning4mac").style.display="block";
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
(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.
|
||||
}
|
||||
// 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"){
|
||||
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", "");
|
||||
} 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, "proaudio", "");
|
||||
document.getElementById("messageDiv").innerHTML = "Audio bitrate increased to 256-kbps.\n\nPlease note that the Sender must also have the <b>&proaudio</b> flag added for full-effect";
|
||||
document.getElementById("messageDiv").style.display="block";
|
||||
setTimeout(function(){document.getElementById("messageDiv").style.opacity="1.0";},0);
|
||||
} else {
|
||||
url=updateURLParameter(url, "proaudio", false);
|
||||
setTimeout(function(){document.getElementById("messageDiv").style.opacity="0";},0);
|
||||
}
|
||||
}
|
||||
|
||||
if (ele.id =="buffer"){
|
||||
if (document.getElementById("buffer").checked){
|
||||
url=updateURLParameter(url, "buffer", "");
|
||||
} else {
|
||||
url=updateURLParameter(url, "buffer", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
document.getElementById('changeText').value = url;
|
||||
console.log(url);
|
||||
return url;
|
||||
}
|
||||
function gohere(){
|
||||
var url = modURL(true);
|
||||
if (!(document.getElementById('changeText').value.includes("obs.ninja")) && (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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<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;
|
||||
}
|
||||
#version{
|
||||
margin: 0 auto;
|
||||
font-size: 30%;
|
||||
display: inline-block;
|
||||
color: #000A;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header" style="-webkit-app-region: drag; color:#6f6f6f;font-size:20px; line-height: 20px; 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>
|
||||
<div id="version"></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"){
|
||||
try {
|
||||
if (navigator.userAgent.toLowerCase().indexOf(' electron/') > -1) {
|
||||
function compareVersions(version){
|
||||
document.getElementById("version").innerHTML = "Current version: "+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 (parseInt(recentVersion[0])>parseInt(version[0])){
|
||||
ood = true;
|
||||
} else if (parseInt(recentVersion[0])==parseInt(version[0])) {
|
||||
if (parseInt(recentVersion[1])>parseInt(version[1])){
|
||||
ood = true;
|
||||
} else if (parseInt(recentVersion[1])==parseInt(version[1])) {
|
||||
if (parseInt(recentVersion[2])>parseInt(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') || urlParams.has('ver')){
|
||||
var ver = urlParams.get('version') || urlParams.get('ver') || false;
|
||||
console.log("version: "+ver);
|
||||
if (ver){
|
||||
compareVersions(ver);
|
||||
}
|
||||
} else{
|
||||
document.getElementById("version").innerHTML = "Elevate app privilleges to see current version";
|
||||
try{
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
console.log("ELECTRON DETECTED");
|
||||
ipcRenderer.on('appVersion', function(event, version) {
|
||||
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;
|
||||
url = url.trim();
|
||||
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:")){
|
||||
alert("Warning:\n\nFor security purposes, local files need to be loaded via the command-line or via the right-click context menu -> Edit URL.\n\nThis is supported in Electron Capture 2.15.2 and newer.");
|
||||
} 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
@ -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>
|
||||
145
examples/addtoscene.html
Normal file
@ -0,0 +1,145 @@
|
||||
<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;
|
||||
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;";
|
||||
|
||||
iframe.src = "../?dir=teststeve123&password=1234";
|
||||
iframeContainer.appendChild(iframe);
|
||||
document.getElementById("container").appendChild(iframeContainer);
|
||||
|
||||
var listOfStreamIDs = [
|
||||
"1234_pov",
|
||||
"2345_pov",
|
||||
"3456_pov",
|
||||
"4567_pov",
|
||||
"5678_pov"
|
||||
];
|
||||
|
||||
|
||||
for (var i=0;i<listOfStreamIDs.length;i++){
|
||||
|
||||
var button = document.createElement("a");
|
||||
button.innerHTML = "Invite "+listOfStreamIDs[i];
|
||||
button.target = "_blank";
|
||||
button.href = "../?room=teststeve123&password=1234&broadcast&transparent&autostart&nmb&nvb&gain=0&webcam&l=stevetest&push="+listOfStreamIDs[i];
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "TOGGLE "+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "addScene",
|
||||
value: "1",
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "mic",
|
||||
value: true,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
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 ("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();">Go to Directors Room</button>
|
||||
<br />
|
||||
The password for guests is 1234<br />
|
||||
<br />
|
||||
<br />
|
||||
Custom guest invites and toggles for add/removing from scene=1 are on the bottom.
|
||||
<br />
|
||||
<br />
|
||||
Scene=1 link: <a target="_blank" href="https://vdo.ninja/?scene=1&room=teststeve123&password=1234">https://vdo.ninja/?scene=1&room=teststeve123&password=1234</a>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
121
examples/bigmutebutton.html
Normal 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>
|
||||
27
examples/changepass.html
Normal file
@ -0,0 +1,27 @@
|
||||
<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");
|
||||
password = password.trim();
|
||||
password = encodeURIComponent(password);
|
||||
|
||||
generateHash(password + location.hostname, 4).then(function(hash) { // million to one error.
|
||||
alert("hash value: "+hash)
|
||||
});
|
||||
</script></body></html>
|
||||
162
examples/chatoverlay.html
Normal file
@ -0,0 +1,162 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>VDON 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= "";
|
||||
var room="";
|
||||
var password="";
|
||||
if (urlParams.has("view")) {
|
||||
view = "&view="+(urlParams.get("view") || "");
|
||||
} else if (urlParams.has("room")) {
|
||||
room = "&room="+urlParams.get("room");
|
||||
} else {
|
||||
var help = document.createElement("h2");
|
||||
help.innerHTML = "This app supports <i>&room, &view, </i>and<i> &password</i> URL parameters.";
|
||||
document.body.appendChild(help);
|
||||
return;
|
||||
}
|
||||
if (urlParams.has("password")) {
|
||||
password = "&password="+urlParams.get("password");
|
||||
}
|
||||
|
||||
iframe.allow = "autoplay";
|
||||
var srcString = "../?datamode&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>
|
||||
123
examples/control.html
Normal file
@ -0,0 +1,123 @@
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
margin:0;
|
||||
padding:0;
|
||||
height:100%;
|
||||
width:100%;
|
||||
border:0;
|
||||
overflow:hidden;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body id="body">
|
||||
<button onclick='send({"abc1231":[0,0,50,50],abc1232:[50,0,50,50],abc1233:[0,50,50,50],abc1234:[50,50,50,50]});'>2x2</button>
|
||||
<button onclick='send({"abc1231":[0,0,100,100],abc1232:[0, 0, 0, 0 ],abc1233:[0,0,0,0],abc1234:[0,0,0,0]});'>1</button>
|
||||
<button onclick='send({"abc1231":[0,0,50 ,100],abc1232:[50,0,100,100],abc1233:[0,0,0,0],abc1234:[0,0,0,0]});'>2x1</button>
|
||||
<button onclick='send({"abc1231":[0,0,100,100],abc1232:[70,70,20,20],abc1233:[0,0,0,0],abc1234:[0,0,0,0]});'>Pip</button>
|
||||
<script>
|
||||
|
||||
// this app is mainly for demo purposes at this time. It has been depreciated.
|
||||
|
||||
function updateURL(param, force=false) {
|
||||
var para = param.split('=')[0];
|
||||
if (!(urlParams.has(para)) || (force)){
|
||||
if (history.pushState){
|
||||
|
||||
var arr = window.location.href.split('?');
|
||||
var newurl;
|
||||
if (arr.length > 1 && arr[1] !== '') {
|
||||
newurl = window.location.href + '&' +param;
|
||||
} else {
|
||||
newurl = window.location.href + '?' +param;
|
||||
}
|
||||
|
||||
window.history.pushState({path:newurl},'',newurl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
(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 generateStreamID(){
|
||||
var text = "";
|
||||
var possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
|
||||
for (var i = 0; i < 7; i++){
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
var roomID = "undefined";
|
||||
|
||||
if (urlParams.has("room")){
|
||||
roomID = urlParams.get("room");
|
||||
} else {
|
||||
roomID = generateStreamID();
|
||||
updateURL("room="+roomID);
|
||||
}
|
||||
|
||||
var url = document.URL.substr(0,document.URL.lastIndexOf('/'));
|
||||
|
||||
|
||||
navigator.clipboard.writeText(url+"/mixer?room="+roomID).then(() => {
|
||||
/* clipboard successfully set */
|
||||
}, () => {
|
||||
/* clipboard write failed */
|
||||
});
|
||||
|
||||
document.getElementById("body").innerHTML+=url+"/mixer?room="+roomID;
|
||||
|
||||
|
||||
var socket = new WebSocket("wss://api.action.wtf:666"); // api.action.wtf has been deprecated.
|
||||
|
||||
socket.onclose = function (){
|
||||
setTimeout(function(){window.location.reload(true);},100);
|
||||
};
|
||||
|
||||
socket.onopen = function (){
|
||||
socket.send(JSON.stringify({"join":roomID}));
|
||||
}
|
||||
|
||||
|
||||
socket.addEventListener('message', function (event) {
|
||||
if (event.data){
|
||||
var data = JSON.parse(event.data);
|
||||
log(data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.onclose = function (){
|
||||
setTimeout(function(){window.location.reload(true);},100);
|
||||
};
|
||||
|
||||
var counter=0;
|
||||
function send(scene){
|
||||
counter+=1;
|
||||
socket.send(JSON.stringify({"msg":true, "scene":scene, "id":counter}));
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
54
examples/custom_video_switcher.html
Normal file
@ -0,0 +1,54 @@
|
||||
<html>
|
||||
<body>
|
||||
<button onclick="togglePlayer('STREAM123a')" >toggle video 1</button>
|
||||
<button onclick="togglePlayer('STREAM123b')" >toggle video 2</button>
|
||||
<iframe id="scene"
|
||||
style="position: relative; display:block; width:100%; height: calc(100vh - 50px);"
|
||||
allow="document-domain;encrypted-media;sync-xhr;cross-origin-isolated;accelerometer;midi;autoplay;fullscreen;picture-in-picture;display-capture;"
|
||||
src="https://vdo.ninja/alpha/?room=ROOMHERE123&cleanoutput&transparent&noaudio&controls=0&noap&optimize=0&scale=100&scene&manual&b64css=dmlkZW97CiAgICBwb3NpdGlvbjogYWJzb2x1dGU7CiAgICBsZWZ0OiAwOwogICAgdG9wOiAwOwp9"
|
||||
></iframe>
|
||||
<script>
|
||||
|
||||
// you can remotely switch between video streams A and B, instead of using the toggle buttons. You'll need your own API service to switch, but that's up to you.
|
||||
|
||||
// https://vdo.ninja/alpha/?room=ROOMHERE123&push=STREAM123a&view <= invite guest-a
|
||||
// https://vdo.ninja/alpha/?room=ROOMHERE123&push=STREAM123b&view <= invite guest-b
|
||||
|
||||
// &b64css=dmlkZW97CiAgICBwb3NpdGlvbjogYWJzb2x1dGU7CiAgICBsZWZ0OiAwOwogICAgdG9wOiAwOwp9" -- makes sure all videos added align to the top-left corner, overlapping other videos if needed
|
||||
// we do not use &fadein=0, as that can cause flicker
|
||||
// &manual disables the auto mixer for scene=0, so we can manually add/remove elements to the scene via the IFRAME API instead
|
||||
var scene = document.getElementById("scene");
|
||||
|
||||
//////////// LISTEN FOR EVENTS
|
||||
var eventMethod = window.addEventListener ? "addEventListener" : "attachEvent";
|
||||
var eventer = window[eventMethod];
|
||||
var messageEvent = eventMethod === "attachEvent" ? "onmessage" : "message";
|
||||
eventer(messageEvent, function (e) {
|
||||
if (scene && e.source == scene.contentWindow){
|
||||
if (e.data.action === 'view-connection') {
|
||||
console.log(e.data);
|
||||
} else if (e.data.action === 'end-view-connection') {
|
||||
console.log(e.data);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
var activeVideo = null;
|
||||
async function togglePlayer(video){
|
||||
activeVideo = video;
|
||||
scene.contentWindow.postMessage({
|
||||
add: true,
|
||||
target: activeVideo
|
||||
}, '*');
|
||||
|
||||
setTimeout(function(){
|
||||
scene.contentWindow.postMessage({
|
||||
replace: true, // this replaces all videos in the current scene with the target stream ID. We coudl use `remove` instead, but that requires specifying the streamID to remove
|
||||
target: activeVideo
|
||||
}, '*');
|
||||
},500);
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -125,7 +125,7 @@ button{
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<input placeholder="Enter an OBS.Ninja Room Link" id="viewlink" />
|
||||
<input placeholder="Enter an VDO.Ninja Room Link" id="viewlink" />
|
||||
|
||||
<button onclick="loadIframe();">Load URL</button>You can drag and resize the generated windows; multiple can be created.
|
||||
|
||||
@ -313,7 +313,7 @@ function loadIframe(){
|
||||
|
||||
var iframe = document.createElement("iframe");
|
||||
iframe.allow="autoplay";
|
||||
iframe.src = document.getElementById("viewlink").value || "https://obs.ninja";
|
||||
iframe.src = document.getElementById("viewlink").value || "https://vdo.ninja";
|
||||
iframe.style.width="325px";
|
||||
iframe.style.height="420px";
|
||||
|
||||
76
examples/dual.html
Normal 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
@ -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>
|
||||
1
examples/github.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
|
||||
|
After Width: | Height: | Size: 814 B |
@ -1,12 +1,12 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>OBS.Ninja IFRAME Outgoing Stats Example</title>
|
||||
<title>VDO.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="icon" type="image/png" sizes="32x32" href="../media//favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="../media/favicon-16x16.png" />
|
||||
<link rel="icon" href=".../media/favicon.ico" />
|
||||
<link itemprop="thumbnailUrl" href="../media/vdoNinja_logo_full.png" />
|
||||
<link rel="stylesheet" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|
||||
<style>
|
||||
body {
|
||||
@ -43,7 +43,7 @@
|
||||
<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" />
|
||||
<input type="text" class="form-control" style="width:95%;margin:10px auto;" placeholder="Enter an VDO.Ninja View URL here" value="" id="viewlink" />
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="row">
|
||||
@ -61,8 +61,8 @@
|
||||
<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>
|
||||
<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/vdoninja/blob/master/IFRAME.md">iFrame API</a>.</p>
|
||||
<p>Click start to generate a stream using the VDO.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;">
|
||||
@ -129,11 +129,11 @@
|
||||
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) {
|
||||
if ($("#vdon_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>');
|
||||
$('#viewers tbody').append('<tr id="vdon_viewer_' + viewer + '"><th class="vdon_viewer_label" scope="row"></th><td class="vdon_viewer_added">' + ("0" + h).slice(-2) + ':' + ("0" + m).slice(-2) + ':' + ("0" + s).slice(-2) + '</td><td class="vdon_viewer_qlr"></td><td class="vdon_viewer_resolution"></td><td class="vdon_viewer_platform"></td><td class="vdon_viewer_encoder"></td><td class="vdon_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
|
||||
@ -141,24 +141,24 @@
|
||||
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);
|
||||
$("#vdon_viewer_" + viewer).find('.vdon_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);
|
||||
$("#vdon_viewer_" + viewer).find('.vdon_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);
|
||||
$("#vdon_viewer_" + viewer).find('.vdon_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);
|
||||
$("#vdon_viewer_" + viewer).find('.vdon_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);
|
||||
$("#vdon_viewer_" + viewer).find('.vdon_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);
|
||||
$("#vdon_viewer_" + viewer).find('.vdon_viewer_useragent').text(e.data.stats.outbound_stats[viewer].info.useragent);
|
||||
}
|
||||
$("#obsn_viewer_" + viewer).data('last', now.getTime()); //Used below to remove old viewers
|
||||
$("#vdon_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) {
|
||||
@ -243,7 +243,7 @@
|
||||
//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/";
|
||||
var baseUrl = "https://vdo.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');
|
||||
});
|
||||
|
||||
181
examples/index.html
Normal file
@ -0,0 +1,181 @@
|
||||
<head>
|
||||
<link rel="stylesheet" href="../main.css?ver=40" />
|
||||
<style>
|
||||
.container {
|
||||
max-width: 900px;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2em;
|
||||
padding: 10px;
|
||||
background-color: #457b9d;
|
||||
color: white;
|
||||
border-bottom: 2px solid #3b6a87;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 a {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
#examples {
|
||||
margin-top: 3em;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-gap: 1em;
|
||||
}
|
||||
|
||||
|
||||
div#examples>div {
|
||||
background: #dddddd;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 1em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.youtube {
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.media {
|
||||
background: hsl(203deg 26% 73%);
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.2em;
|
||||
}
|
||||
</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>VDO.Ninja tech demonstrations</h1>
|
||||
<div id="examples">
|
||||
<div>
|
||||
<h2><a href='p2p.html'>p2p</a></h2>
|
||||
<div class="description">How to use vdo.ninja as a data transport tunneling service</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='twitch.html'>twitch</a></h2>
|
||||
<div class="description">How to have a twitch live chat side-by-side with VDO.NInja on the same
|
||||
screen</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='youtube.html'>youtube</a></h2>
|
||||
<div class="description">How to have a youtube live chat side-by-side with VDO.NInja on the same
|
||||
screen</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='dual.html'>dual</a></h2>
|
||||
<div class="description">how to have two VDO.Ninja windows (or any windows really) open on the same
|
||||
page;
|
||||
Picture-in-Picture style</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='multi.html?rooms=room1xx,room2xx,room3xx'>Multiple rooms</a></h2>
|
||||
<div class="description">how to have multiple director rooms open in a single tab; note the URL's ?rooms=xx,yy command</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='https://versus.cam'>versus.cam</a></h2>
|
||||
<div class="description">How to use the IFRAME API to transport audio and video to the parent frame in Chrome</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='addtoscene.html'>add to scene</a></h2>
|
||||
<div class="description">How to use the IFRAME API to add/remove guests to a scene remotely</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='bigmutebutton.html'>big mute button</a></h2>
|
||||
<div class="description">Mobile-friendly big-button for muting yourself easily</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='sensors.html'>sensors</a></h2>
|
||||
<div class="media">
|
||||
<a href='https://www.youtube.com/watch?v=SqbufszHKi4' class="youtube">
|
||||
<img src="youtube.svg" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="description">how to transmit sensor and video data from a phone to a computer, drawing
|
||||
it to canvas.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='sensoroverlay.html'>sensor overlay</a></h2>
|
||||
|
||||
<div class="description">Overlay the incoming speed from remote mobile sensor data onto your video
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='../midi.html'>midi</a></h2>
|
||||
<div class="media">
|
||||
<a href='https://www.youtube.com/watch?v=rnZ8HM9FL4I' class="youtube">
|
||||
<img src="youtube.svg" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="description">Demonstrates the MIDI API for VDO.Ninja
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='draggable.html'>draggable</a></h2>
|
||||
<div class="description">demonstrates how to drag multiple
|
||||
windows around, if you wanted to create a custom
|
||||
layout of elements. (experimental)</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='chatoverlay.html'>chat</a></h2>
|
||||
<div class="description">Example of a chat-only interface for VDO.Ninja; maybe
|
||||
dockable into OBS even.</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='iframe.outbound-stats.html'>iframe.outbound-stats</a></h2>
|
||||
<div class="description">iframe.outbound-stats.html demostrates how to get stats from VDO.Ninja
|
||||
using the
|
||||
IFRAME API</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='changepass.html'>changepass</a></h2>
|
||||
<div class="description">lets you create passwords and related HASH values for VDO.NInja
|
||||
rooms</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='webhid.html'>webhid</a></h2>
|
||||
<div class="description">webhid demonstrates how to interface with a USB device, like a streamdeck
|
||||
(mouse/keyboard not supported)</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='zoom.html'>zoom</a></h2>
|
||||
<div class="description">A tool for letting you publish into VDO.Ninja, but then
|
||||
full-screen the window once setup, allowing for
|
||||
window-capturing into zoom.</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2><a href='obs_remote/index.html'>obs_remote</a></h2>
|
||||
<div class="media">
|
||||
<a href='https://github.com/steveseguin/remote_ninja' class="youtube">
|
||||
<img src="github.svg" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="description">Also hosted on github elsewhere, but it's an example of how to remotely
|
||||
control OBS using VDO.Ninja's tunneling abilities</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
3086
examples/main.css
Normal file
555
examples/midi.html
Normal 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
@ -0,0 +1,3 @@
|
||||
.tile {
|
||||
max-width:200px !important;
|
||||
}
|
||||
451
examples/mixer.html
Normal file
@ -0,0 +1,451 @@
|
||||
<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;
|
||||
padding:0;
|
||||
display:block;
|
||||
width:1280px;
|
||||
height:720px;
|
||||
background-color: #111;
|
||||
}
|
||||
#viewlink {
|
||||
width:400px;
|
||||
}
|
||||
#container {
|
||||
display:block;
|
||||
padding:0px;
|
||||
}
|
||||
input{
|
||||
padding:5px;
|
||||
margin:5px;
|
||||
}
|
||||
button{
|
||||
padding:5px;
|
||||
margin:5px;
|
||||
}
|
||||
canvas{
|
||||
padding:10px;
|
||||
cursor:pointer;
|
||||
}
|
||||
.thing {
|
||||
width: 100px;
|
||||
height: 2em;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em;
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
cursor: grab;
|
||||
}
|
||||
.empty {
|
||||
width: 100px;
|
||||
height: 2em;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em;
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
font-family: sans-serif;
|
||||
user-select: none;
|
||||
}
|
||||
.col {
|
||||
width: 130px;
|
||||
height: 450px;
|
||||
padding: 1em;
|
||||
border: 1px solid;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
float: left;
|
||||
}
|
||||
|
||||
</style>
|
||||
<script>
|
||||
|
||||
function allowDrop(ev) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
function swapNodes(n1, n2) {
|
||||
var p1 = n1.parentNode;
|
||||
var p2 = n2.parentNode;
|
||||
var i1, i2;
|
||||
|
||||
if ( !p1 || !p2 || p1.isEqualNode(n2) || p2.isEqualNode(n1) ) return;
|
||||
|
||||
for (var i = 0; i < p1.children.length; i++) {
|
||||
if (p1.children[i].isEqualNode(n1)) {
|
||||
i1 = i;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < p2.children.length; i++) {
|
||||
if (p2.children[i].isEqualNode(n2)) {
|
||||
i2 = i;
|
||||
}
|
||||
}
|
||||
|
||||
if ( p1.isEqualNode(p2) && i1 < i2 ) {
|
||||
i2++;
|
||||
}
|
||||
p1.insertBefore(n2, p1.children[i1]);
|
||||
p2.insertBefore(n1, p2.children[i2]);
|
||||
}
|
||||
|
||||
function drag(ev) {
|
||||
ev.dataTransfer.setData("text", ev.target.id);
|
||||
}
|
||||
|
||||
function drop(ev) {
|
||||
ev.preventDefault();
|
||||
var data = ev.dataTransfer.getData("text");
|
||||
var origThing = document.getElementById(data);
|
||||
console.log(origThing);
|
||||
console.log(data);
|
||||
console.log(ev);
|
||||
//var newThing = origThing.cloneNode(true);
|
||||
if (ev.target.classList.contains("thing")){
|
||||
//ev.target.parentNode.insertBefore(origThing, ev.target.nextSibling);
|
||||
//elem.parentNode.insertBefore(elem, elem.parentNode.firstChild);
|
||||
swapNodes( ev.target, origThing);
|
||||
var slot = origThing.dataset.slot;
|
||||
origThing.dataset.slot = ev.target.dataset.slot;
|
||||
ev.target.dataset.slot = slot;
|
||||
|
||||
} else if (ev.target.classList.contains("empty")){
|
||||
ev.target.parentNode.insertBefore(origThing, ev.target.nextSibling);
|
||||
origThing.dataset.slot = ev.target.dataset.slot;
|
||||
ev.target.style.display = "none";
|
||||
}
|
||||
origThing.style.backgroundColor = ev.target.style.backgroundColor;
|
||||
|
||||
|
||||
}
|
||||
|
||||
function dropRemove(ev) {
|
||||
ev.preventDefault();
|
||||
var data = ev.dataTransfer.getData("text");
|
||||
var origThing = document.getElementById(data);
|
||||
if (origThing.dataset.slot){
|
||||
document.querySelector(".empty[data-slot='"+origThing.dataset.slot+"']").style.display = "block";
|
||||
delete origThing.dataset.slot;
|
||||
}
|
||||
origThing.style.backgroundColor = "#000";
|
||||
if (ev.target.classList.contains("thing")){
|
||||
ev.target.parentNode.insertBefore(origThing, ev.target.nextSibling);
|
||||
} else {
|
||||
ev.target.appendChild(origThing);
|
||||
}
|
||||
document.getElementById("col2").appendChild(document.getElementById("delete"));
|
||||
}
|
||||
|
||||
var streamIDs = [];
|
||||
|
||||
function updateList(){
|
||||
//<div id="col2" ondrop="dropRemove(event)" ondragover="allowDrop(event)">
|
||||
// <div class="thing" draggable="true" ondragstart="drag(event)" id="thing4">THING 4</div>
|
||||
// <div class="thing" draggable="true" ondragstart="drag(event)" id="thing1">THING 1</div>
|
||||
//</div>
|
||||
for (var i=0;i<streamIDs.length;i++){
|
||||
if (!document.getElementById("sid_"+streamIDs[i])){
|
||||
var thing = document.createElement("div");
|
||||
thing.draggable = true;
|
||||
thing.classList.add("thing");
|
||||
thing.addEventListener("dragstart", drag);
|
||||
thing.dataset.sid = streamIDs[i];
|
||||
thing.id = "sid_"+streamIDs[i];
|
||||
thing.innerText = streamIDs[i];
|
||||
document.getElementById("col2").appendChild(thing);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("col2").appendChild(document.getElementById("delete"));
|
||||
}
|
||||
|
||||
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 promptRoom = prompt("Enter a room name to use");
|
||||
if (promptRoom){
|
||||
var iframesrc = "https://vdo.ninja/?transparent&cleanoutput&manual&scene=manualtestscene&room="+promptRoom;
|
||||
} else {
|
||||
promptRoom = "testroom123312";
|
||||
var iframesrc = "https://vdo.ninja/?transparent&cleanoutput&manual&scene=manualtestscene&room="+promptRoom;
|
||||
}
|
||||
|
||||
function activate(){
|
||||
console.log(this.dataset.layout);
|
||||
var layout = JSON.parse(this.dataset.layout);
|
||||
|
||||
iframe.contentWindow.postMessage({"target":"*", "remove":true}, '*');
|
||||
|
||||
|
||||
|
||||
for (var i=0;i<layout.length;i++){
|
||||
|
||||
var stream = document.querySelector(".thing[data-slot='"+(i+1)+"'");
|
||||
if (!stream){continue;}
|
||||
|
||||
var x = layout[i].x|| 0;
|
||||
var y = layout[i].y || 0;
|
||||
var w = layout[i].w || 0;
|
||||
var h = layout[i].h || 0;
|
||||
var cover = layout[i].cover || false;
|
||||
|
||||
if (!(w && h)){continue;}
|
||||
|
||||
x = x + "%";
|
||||
y = y + "%";
|
||||
w = w + "%";
|
||||
h = h + "%";
|
||||
|
||||
if (cover){
|
||||
cover = "object-fit:cover;";
|
||||
} else {
|
||||
cover = "";
|
||||
}
|
||||
|
||||
iframe.contentWindow.postMessage({"target":stream.dataset.sid, "add":true, "settings":{"style": "width:"+w+";height:"+h+";position:absolute;left:"+x+";top:"+y+";display:block;"+cover}}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Refresh list";
|
||||
button.onclick = function(){iframe.contentWindow.postMessage({"getStreamIDs":true}, '*');};
|
||||
button.style.display = "block";
|
||||
document.getElementById("sources").appendChild(button);
|
||||
|
||||
var a = document.createElement("a");
|
||||
a.innerHTML = "Invite Guest Link";
|
||||
a.href = "https://vdo.ninja/?room="+promptRoom+"&broadcast";
|
||||
a.target = "_blank";
|
||||
document.getElementById("sources").appendChild(a);
|
||||
|
||||
var colors = [
|
||||
"#00AAAA",
|
||||
"#FF0000",
|
||||
"#0000FF",
|
||||
"#AA00AA",
|
||||
"#00FF00",
|
||||
"#AAAA00"
|
||||
];
|
||||
|
||||
|
||||
var slots = document.getElementById("col1").children;
|
||||
for (var i=0;i<slots.length;i++){
|
||||
slots[i].style.backgroundColor = colors[i];
|
||||
}
|
||||
|
||||
|
||||
function drawLayout(layout){
|
||||
for (var i=0;i<layout.length;i++){
|
||||
layout[i].i = i;
|
||||
}
|
||||
|
||||
function compare( a, b ) { // sorts layout based on z-index.
|
||||
var aa = a.z || 0;
|
||||
var bb = b.z || 0;
|
||||
if ( aa > bb ){
|
||||
return 1;
|
||||
}
|
||||
if ( aa < bb ){
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
layout.sort(compare);
|
||||
|
||||
|
||||
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width="80";
|
||||
canvas.height="45";
|
||||
var ctx = canvas.getContext('2d');
|
||||
document.getElementById("container").appendChild(canvas);
|
||||
ctx.beginPath();
|
||||
ctx.rect(0, 0, 80, 45);
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.fill();
|
||||
|
||||
for (var i=0;i<layout.length;i++){
|
||||
|
||||
ctx.fillStyle = colors[layout[i].i];
|
||||
ctx.lineWidth = 3;
|
||||
var x = layout[i].x*0.8 || 0;
|
||||
var y = layout[i].y*0.45 || 0;
|
||||
var w = layout[i].w*0.8 || 0;
|
||||
var h = layout[i].h*0.45 || 0;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.rect(x, y, w, h);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
canvas.dataset.layout = JSON.stringify(layout);
|
||||
canvas.onclick = activate;
|
||||
}
|
||||
|
||||
var data = [
|
||||
{x:0, y:0, w:100, h:100}
|
||||
];
|
||||
drawLayout(data);
|
||||
|
||||
var data = [
|
||||
{x:0, y:0, w:0, h:0},
|
||||
{x:0, y:0, w:100, h:100, cover:true}
|
||||
];
|
||||
drawLayout(data);
|
||||
|
||||
var data = [
|
||||
{x:0, y:25, w:50, h:50, cover:true},
|
||||
{x:50, y:25, w:50, h:50, cover:true}
|
||||
];
|
||||
drawLayout(data);
|
||||
|
||||
|
||||
var data = [
|
||||
{x:70, y:70, w:30, h:30, z:1, cover:false},
|
||||
{x:0, y:0, w:100, h:100,z:0, cover:true}
|
||||
];
|
||||
drawLayout(data);
|
||||
|
||||
var data = [
|
||||
{x:0, y:0, w:100, h:100,z:0, cover:true},
|
||||
{x:70, y:70, w:30, h:30, z:1, cover:false}
|
||||
];
|
||||
drawLayout(data);
|
||||
|
||||
var data = [
|
||||
{x:0, y:0, w:20, h:20, z:1, cover:false},
|
||||
{x:0, y:0, w:100, h:100,z:0, cover:true}
|
||||
];
|
||||
drawLayout(data);
|
||||
|
||||
var data = [
|
||||
{x:0, y:0, w:50, h:50},
|
||||
{x:50, y:0, w:50, h:50},
|
||||
{x:0, y:50, w:50, h:50},
|
||||
{x:50, y:50, w:50, h:50}
|
||||
];
|
||||
drawLayout(data);
|
||||
|
||||
var data = [
|
||||
{x:0, y:16.667, w:66.667, h:66.667},
|
||||
{x:66.667, y:0, w:33.333, h:33.333},
|
||||
{x:66.667, y:33.333, w:33.333, h:33.333},
|
||||
{x:66.667, y:66.667, w:33.333, h:33.333}
|
||||
];
|
||||
drawLayout(data);
|
||||
|
||||
var data = [
|
||||
{x:66.667, y:0, w:33.333, h:33.333},
|
||||
{x:0, y:16.667, w:66.667, h:66.667},
|
||||
{x:66.667, y:33.333, w:33.333, h:33.333},
|
||||
{x:66.667, y:66.667, w:33.333, h:33.333}
|
||||
];
|
||||
drawLayout(data);
|
||||
|
||||
var data = [
|
||||
{x:0, y:0, w:0, h:0},
|
||||
{},
|
||||
{x:0, y:0, w:100, h:100}
|
||||
];
|
||||
drawLayout(data);
|
||||
|
||||
var data = [
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{x:0, y:0, w:100, h:100}
|
||||
];
|
||||
drawLayout(data);
|
||||
|
||||
var data = [
|
||||
{},
|
||||
{},
|
||||
{x:70, y:70, w:30, h:30, z:1, cover:false},
|
||||
{x:0, y:0, w:100, h:100,z:0, cover:true}
|
||||
];
|
||||
drawLayout(data);
|
||||
|
||||
var data = [
|
||||
{},
|
||||
{},
|
||||
{x:0, y:25, w:50, h:50, cover:true},
|
||||
{x:50, y:25, w:50, h:50, cover:true}
|
||||
];
|
||||
drawLayout(data);
|
||||
|
||||
|
||||
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 (e.data.action === "new-view-connection"){
|
||||
iframe.contentWindow.postMessage({"getStreamIDs":true}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ("streamIDs" in e.data){
|
||||
streamIDs = [];
|
||||
for (var key in e.data.streamIDs){
|
||||
streamIDs.push(key);
|
||||
}
|
||||
updateList();
|
||||
console.log(streamIDs);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="loadIframe();">
|
||||
<div class="col" id="sources">
|
||||
<div id="col2" ondrop="dropRemove(event)" ondragover="allowDrop(event)">
|
||||
<div class="thing" draggable="false" id="delete" style="background-color:rgb(96 9 9);">REMOVE</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col" id="col1" ondrop="drop(event)" ondragover="allowDrop(event)">
|
||||
<div class="empty" data-slot="1">SLOT 1</div>
|
||||
<div class="empty" data-slot="2">SLOT 2</div>
|
||||
<div class="empty" data-slot="3">SLOT 3</div>
|
||||
<div class="empty" data-slot="4">SLOT 4</div>
|
||||
</div>
|
||||
<div id="container">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
147
examples/mobiledirector.css
Normal file
@ -0,0 +1,147 @@
|
||||
body{
|
||||
zoom: 75%;
|
||||
}
|
||||
button[data-action-type='solo-chat'] {
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
button[data-action-type] {
|
||||
padding: 20px 10px;
|
||||
}
|
||||
|
||||
#controlButtons{
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
button[data-cluster='2'] {
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
button[data-action-type='solo-video'] {
|
||||
display:unset!important;
|
||||
visibility: visible;
|
||||
width:unset;
|
||||
height:unset;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div > a.soloLink{
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
div.shift{
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
div.streamID{
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
button[data-action-type='forward'] {
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
button[data-action-type='direct-chat'] {
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
button[data-action-type='hangup'] {
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
button[data-action-type='solo-chat'] {
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
button[data-action-type='solo-chat'] {
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
button[data-cluster='1'] {
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
|
||||
button[data-action-type='recorder-local'] {
|
||||
display:none! important;
|
||||
}
|
||||
span[data-action-type='ordering'] {
|
||||
display:none! important;
|
||||
}
|
||||
button[data-action-type='open-file-share'] {
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
button[data-action-type='add-channel']{
|
||||
display:none! important;
|
||||
}
|
||||
button[data-action-type='toggle-remote-speaker']{
|
||||
display:none! important;
|
||||
}
|
||||
button[data-action-type='toggle-remote-display']{
|
||||
display:none! important;
|
||||
}
|
||||
button[data-action-type='hide-guest']{
|
||||
display:none! important;
|
||||
}
|
||||
button[data-action-type='create-timer']{
|
||||
display:none! important;
|
||||
}
|
||||
button[data-action-type='change-url']{
|
||||
display:none! important;
|
||||
}
|
||||
button[data-action-type='change-params']{
|
||||
display:none! important;
|
||||
}
|
||||
span[data-action-type='change-quality']{
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
span[data-action-type='sceneCluster2']{
|
||||
display:none! important;
|
||||
}
|
||||
span[data-action-type='sceneCluster1']{
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
.orderspan{
|
||||
display:none! important;
|
||||
}
|
||||
#roomHeader{
|
||||
display:none! important;
|
||||
}
|
||||
.directorContainer {
|
||||
display:none!important;
|
||||
}
|
||||
|
||||
.hideDropMenu{
|
||||
display:none!important;
|
||||
}
|
||||
|
||||
#header{
|
||||
display:none!important;
|
||||
}
|
||||
body {
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
button[class="pull-right"]{
|
||||
display:none! important;
|
||||
}
|
||||
|
||||
div#guestFeeds {
|
||||
padding: 1px!important;
|
||||
margin: 1px!important;
|
||||
}
|
||||
div[class="vidcon directorMargins"] {
|
||||
padding: 1px!important;
|
||||
margin: 1px!important;
|
||||
width: 260px;
|
||||
}
|
||||
button[data-action-type]{
|
||||
margin: 1px!important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
88
examples/multi.html
Normal file
@ -0,0 +1,88 @@
|
||||
<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:470px;
|
||||
border:0;
|
||||
margin:0;
|
||||
padding:0;
|
||||
display:block;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
|
||||
(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 path = window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
|
||||
var rooms = "";
|
||||
if (urlParams.has("rooms")){
|
||||
rooms = urlParams.get("rooms");
|
||||
rooms = rooms.split(",");
|
||||
|
||||
var password = prompt("Enter the password for the rooms; leave blank for none");
|
||||
if (password){
|
||||
password = "&password="+password;
|
||||
} else {
|
||||
password = "";
|
||||
}
|
||||
rooms.forEach(room=>{
|
||||
loadIframes("https://"+path+"/../?clean&hidecodirectors&director="+room+password);
|
||||
});
|
||||
} else {
|
||||
document.write("To use, comma separate the room names. ie: https://vdo.ninja/examples/multi?rooms=xxxx,yyy,ccc");
|
||||
}
|
||||
|
||||
function loadIframes(url){
|
||||
var iframe = document.createElement("iframe");
|
||||
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
|
||||
iframe.src = url;
|
||||
document.body.appendChild(iframe);
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
319
examples/muteguestiframe.html
Normal file
@ -0,0 +1,319 @@
|
||||
<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;
|
||||
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;";
|
||||
|
||||
iframe.src = "../?dir=teststeve123&password=1234";
|
||||
iframeContainer.appendChild(iframe);
|
||||
document.getElementById("container").appendChild(iframeContainer);
|
||||
|
||||
var listOfStreamIDs = [
|
||||
"1234_pov"
|
||||
];
|
||||
|
||||
|
||||
for (var i=0;i<listOfStreamIDs.length;i++){
|
||||
|
||||
var button = document.createElement("a");
|
||||
button.innerHTML = "Invite "+listOfStreamIDs[i];
|
||||
button.target = "_blank";
|
||||
button.href = "../?room=teststeve123&password=1234&broadcast&transparent&autostart&nmb&nvb&gain=0&webcam&l=stevetest&push="+listOfStreamIDs[i];
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
|
||||
|
||||
///////////////////
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "speaker true "+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "speaker",
|
||||
value: true,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "speaker false "+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "speaker",
|
||||
value: false,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
///////////////////
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "display true "+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "display",
|
||||
value: true,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "display false "+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "display",
|
||||
value: false,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
///////////////////
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "MUTE true "+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "mic",
|
||||
value: true,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "UN-MUTE "+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "mic",
|
||||
value: false,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
///////////////////
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "addScene 1 toggle "+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "addScene",
|
||||
value: 1,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Scene 1 toggle "+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "addScene",
|
||||
value: "toggle",
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "add Scene 1"+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "addScene",
|
||||
value: true,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "remove Scene 1"+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "addScene",
|
||||
value: false,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
///////////////////
|
||||
///////////////////
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "MUTE SCENE "+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "muteScene",
|
||||
value: true,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "un-mute Scene "+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "muteScene",
|
||||
value: false,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
///////////////////
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "soloChat "+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "soloChat",
|
||||
value: true,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
iframeContainer.appendChild(button);
|
||||
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "soloChat off"+listOfStreamIDs[i];
|
||||
button.dataset.sid = listOfStreamIDs[i];
|
||||
button.onclick = function(){
|
||||
iframe.contentWindow.postMessage({
|
||||
action: "soloChat",
|
||||
value: false,
|
||||
target: this.dataset.sid
|
||||
}, '*');
|
||||
|
||||
}; // target can be a stream ID or * for all.
|
||||
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 (typeof e.data !== "object"){return;}
|
||||
|
||||
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();">Go to Directors Room</button>
|
||||
<br />
|
||||
The password for guests is 1234<br />
|
||||
<br />
|
||||
<br />
|
||||
Custom guest invites and toggles for add/removing from scene=1 are on the bottom.
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
11
examples/nes.min.css
vendored
Normal file
401
examples/obs_remote/index.html
Normal 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>
|
||||
474
examples/obs_remote/interface.html
Normal 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>
|
||||
10
examples/obs_remote/thirdparty/obs-websocket.min.js
vendored
Normal file
69
examples/p2p.html
Normal 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
@ -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
|
||||
477
examples/remoteapi.html
Normal file
@ -0,0 +1,477 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link href="https://fonts.googleapis.com/css?family=Press+Start+2P" rel="stylesheet">
|
||||
<link href="./nes.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
html, body, pre, code, kbd, samp {
|
||||
font-family:"Press Start 2P";
|
||||
}
|
||||
|
||||
body{
|
||||
margin:1%;
|
||||
border:0;
|
||||
background-image: linear-gradient(to left, #e1c5d5, #ddc5da, #d8c6e0, #d1c7e5, #c8c9e9, #c1cded, #bad2f0, #b4d6f2, #b1ddf3, #b1e4f3, #b4eaf0, #bbf0ed);
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
|
||||
button {
|
||||
margin:10px 3px;
|
||||
}
|
||||
|
||||
button, input, optgroup, select, textarea {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
button:active{
|
||||
background-color:#BBB;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header"></div>
|
||||
<div id="target_self"></div>
|
||||
<div id="guest_1_container"></div>
|
||||
<script>
|
||||
function generateStreamID(){
|
||||
var text = "";
|
||||
var possible = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789";
|
||||
for (var i = 0; i < 10; i++){
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
(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);
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
window.onbeforeunload = function() {
|
||||
return "Dude, are you sure you want to leave? Think of the kittens!"; // prevents accidental page reloads.
|
||||
}
|
||||
|
||||
var WID = "testVDON";
|
||||
if (urlParams.has("api")){
|
||||
WID = urlParams.get("api");
|
||||
} else if (urlParams.has("osc")){
|
||||
WID = urlParams.get("osc");
|
||||
} else if (urlParams.has("id")){
|
||||
WID = urlParams.get("id");
|
||||
} else if (urlParams.has("ID")){
|
||||
WID = urlParams.get("ID");
|
||||
} else if (urlParams.has("wid")){
|
||||
WID = urlParams.get("wid");
|
||||
} else {
|
||||
WID = generateStreamID(10);
|
||||
|
||||
var href = window.location.href;
|
||||
var arr = href.split('?');
|
||||
var newurl;
|
||||
if (arr.length > 1 && arr[1] !== '') {
|
||||
newurl = href + '&api=' + WID;
|
||||
} else {
|
||||
newurl = href + '?api=' + WID;
|
||||
}
|
||||
|
||||
window.history.pushState({path: newurl.toString()}, '', newurl.toString());
|
||||
|
||||
}
|
||||
|
||||
var path = "vdo.ninja"; //window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
|
||||
var header = document.getElementById("header");
|
||||
header.innerHTML += "Your Ninja Link: <a href='https://"+path+"/?api="+WID+"' target='_blank'>https://"+path+"/?api="+WID+"</a><br /><br />";
|
||||
header.innerHTML += "<small>You can append your own VDO.Ninja parameters to this link, treating it like a normal VDO.Ninja link.</small>";
|
||||
header.innerHTML += "<br /><br /><small>Code and documentation hosted at <a href='https://github.com/steveseguin/Companion-Ninja'>https://github.com/steveseguin/Companion-Ninja</a></small> <svg width='32' height='32' viewBox='0 0 1024 1024' fill='none' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z' transform='scale(64)' fill='#1B1F23'/></svg>";
|
||||
|
||||
var socket = null;
|
||||
var connecting = false;
|
||||
var failedCount = 0;
|
||||
|
||||
|
||||
|
||||
function connect(){
|
||||
clearTimeout(connecting);
|
||||
if (socket){
|
||||
if (socket.readyState === socket.OPEN){return;}
|
||||
try{
|
||||
socket.close();
|
||||
} catch(e){}
|
||||
}
|
||||
socket = new WebSocket("wss://api.vdo.ninja:443");
|
||||
|
||||
socket.onclose = function (){
|
||||
failedCount+=1;
|
||||
clearTimeout(connecting);
|
||||
connecting = setTimeout(function(){connect();},100*(failedCount-1));
|
||||
};
|
||||
|
||||
socket.onerror = function (){
|
||||
failedCount+=1;
|
||||
clearTimeout(connecting);
|
||||
connecting = setTimeout(function(){connect();},100*failedCount);
|
||||
};
|
||||
|
||||
socket.onopen = function (){
|
||||
failedCount = 0;
|
||||
try{
|
||||
socket.send(JSON.stringify({"join":WID}));
|
||||
} catch(e){
|
||||
connecting = setTimeout(function(){connect();},1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
connect();
|
||||
|
||||
function sendGuestCommand(target, action, value=null){
|
||||
sendMessage(JSON.stringify({"target":target, "action":action, "value":value}));
|
||||
}
|
||||
|
||||
function sendMessage(msg){
|
||||
if (socket.readyState !== socket.OPEN){
|
||||
console.log("not connected; msg didn't send");
|
||||
connect();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
socket.send(msg);
|
||||
} catch(e){
|
||||
connecting = setTimeout(function(){connect();},100);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function log(msg){
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
function ajax(data) {
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
log("AJAX MESSAGE SENT SUCCESSFULL");
|
||||
}
|
||||
};
|
||||
var action = false
|
||||
if ("action" in data){
|
||||
action=data['action'];
|
||||
}
|
||||
var value = "null"
|
||||
if ("value" in data){
|
||||
value=data['value'];
|
||||
}
|
||||
var apiid = false
|
||||
if ("apiid" in data){
|
||||
apiid=data['apiid'];
|
||||
}
|
||||
var target = "null";
|
||||
if ("target" in data){
|
||||
target=data['target'];
|
||||
}
|
||||
|
||||
if (!action || !apiid){
|
||||
alert("no action or api ID provided; request won't work");
|
||||
} else {
|
||||
var URL = "https://api.vdo.ninja/"+apiid+"/"+action+"/"+target+"/"+value;
|
||||
xhttp.open("GET", URL, true);
|
||||
xhttp.send();
|
||||
}
|
||||
}
|
||||
|
||||
function loadSelfCommands(){
|
||||
var commands = {}
|
||||
commands.speaker = function(value){sendMessage(JSON.stringify({"action":"speaker","value":value}))}; // "speaker" also works in the same way
|
||||
|
||||
commands.mic = function(value){sendMessage(JSON.stringify({"action":"mic","value":value}))};
|
||||
|
||||
commands.camera = function(value){sendMessage(JSON.stringify({"action":"camera","value":value}))};
|
||||
|
||||
commands.bitrate = function(value){sendMessage(JSON.stringify({"action":"bitrate","value":value}))};
|
||||
|
||||
commands.volume = function(value){sendMessage(JSON.stringify({"action":"volume","value":value}))};
|
||||
|
||||
commands.record = function(value){sendMessage(JSON.stringify({"action":"record","value":value}))};
|
||||
|
||||
commands.sayHello = function(value){sendMessage(JSON.stringify({"action":"sendChat","value":"Hello"}))};
|
||||
|
||||
var target_self = document.getElementById("target_self");
|
||||
|
||||
setTimeout(function(){
|
||||
var hr = document.createElement("hr");
|
||||
target_self.appendChild(hr);
|
||||
var h3 = document.createElement("h3");
|
||||
h3.innerText = "These are Websocket-based requests";
|
||||
target_self.appendChild(h3);
|
||||
},0);
|
||||
|
||||
for (var k in commands) {
|
||||
setTimeout(function(k){
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = k + ":<br />TRUE";
|
||||
button.onclick = function(){commands[k](true);}
|
||||
target_self.appendChild(button);
|
||||
},0,k);
|
||||
|
||||
setTimeout(function(k){
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = k + ":<br />FALSE";
|
||||
button.onclick = function(){commands[k](false);}
|
||||
target_self.appendChild(button);
|
||||
},0,k);
|
||||
|
||||
if (k=="mic" || k=="camera" || k=="record" || k=="speaker"){
|
||||
setTimeout(function(k){
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = k + ":<br />TOGGLE";
|
||||
button.onclick = function(){commands[k]("toggle");}
|
||||
target_self.appendChild(button);
|
||||
},0,k);
|
||||
}
|
||||
|
||||
log(k);
|
||||
} // list available commands to console
|
||||
|
||||
commands.reload = function(){sendMessage(JSON.stringify({"action":"reload","value":true}))};
|
||||
|
||||
commands.hangup = function(){sendMessage(JSON.stringify({"action":"hangup","value":true}))};
|
||||
|
||||
k = "reload";
|
||||
setTimeout(function(k){
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = k + ":<br />TRUE";
|
||||
button.onclick = function(){commands[k](true);}
|
||||
target_self.appendChild(button);
|
||||
},0,k);
|
||||
|
||||
k = "hangup";
|
||||
setTimeout(function(k){
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = k + ":<br />TRUE";
|
||||
button.onclick = function(){commands[k](true);}
|
||||
target_self.appendChild(button);
|
||||
},0,k);
|
||||
|
||||
setTimeout(function(){
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Rainbow<br />Puke";
|
||||
button.onclick = function(){sendMessage(JSON.stringify({"action":"forceKeyframe"}))}
|
||||
target_self.appendChild(button);
|
||||
},0);
|
||||
|
||||
|
||||
|
||||
var commands2 = {}
|
||||
commands2.speaker = function(value){ajax({"action":"speaker","value":value,"apiid":WID})}; // "speaker" also works in the same way
|
||||
commands2.mic = function(value){ajax({"action":"mic","value":value,"apiid":WID})}; // "speaker" also works in the same way
|
||||
commands2.camera = function(value){ajax({"action":"camera","value":value,"apiid":WID})}; // "speaker" also works in the same way
|
||||
commands2.bitrate = function(value){ajax({"action":"bitrate","value":value,"apiid":WID})}; // "speaker" also works in the same way
|
||||
commands2.volume = function(value){ajax({"action":"volume","value":value,"apiid":WID})}; // "speaker" also works in the same way
|
||||
commands2.record = function(value){ajax({"action":"record","value":value,"apiid":WID})}; // "speaker" also works in the same way
|
||||
|
||||
setTimeout(function(){
|
||||
var hr = document.createElement("hr");
|
||||
target_self.appendChild(hr);
|
||||
var h3 = document.createElement("h3");
|
||||
h3.innerText = "These are HTTP-based GET requests";
|
||||
target_self.appendChild(h3);
|
||||
},0);
|
||||
|
||||
for (var k in commands2) {
|
||||
|
||||
setTimeout(function(k){
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = k + ":<br />TRUE";
|
||||
button.onclick = function(){commands2[k](true);}
|
||||
target_self.appendChild(button);
|
||||
},0,k);
|
||||
|
||||
setTimeout(function(k){
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = k + ":<br />FALSE";
|
||||
button.onclick = function(){commands2[k](false);}
|
||||
target_self.appendChild(button);
|
||||
},0,k);
|
||||
|
||||
if (k=="mic" || k=="camera" || k=="record" || k=="speaker"){
|
||||
setTimeout(function(k){
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = k + ":<br />TOGGLE";
|
||||
button.onclick = function(){commands2[k]("toggle");}
|
||||
target_self.appendChild(button);
|
||||
},0,k);
|
||||
}
|
||||
log(k);
|
||||
}
|
||||
|
||||
setTimeout(function(WID){
|
||||
var button = document.createElement("button");
|
||||
button.innerHTML = "Rainbow<br />Puke"
|
||||
button.onclick = function(){ajax({"action":"forceKeyframe","apiid":WID})}
|
||||
target_self.appendChild(button);
|
||||
},0,WID);
|
||||
|
||||
|
||||
return commands;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function loadGuestCommands(guestid){
|
||||
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");
|
||||
h3.innerText = "These target guest "+guestid+ " (if a 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);
|
||||
|
||||
}
|
||||
|
||||
loadSelfCommands();
|
||||
loadGuestCommands(1);
|
||||
loadGuestCommands(2);
|
||||
loadGuestCommands(3);
|
||||
loadGuestCommands(4);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
266
examples/sensoroverlay.html
Normal file
@ -0,0 +1,266 @@
|
||||
<html>
|
||||
<head><title>Video with sensor overlayed data</title>
|
||||
<style>
|
||||
body{
|
||||
padding:0;
|
||||
margin:0;
|
||||
background-color: #0000;
|
||||
}
|
||||
iframe {
|
||||
border:0;
|
||||
margin:0;
|
||||
padding:0;
|
||||
display:block;
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
|
||||
#container {
|
||||
border:0;
|
||||
margin:0;
|
||||
padding:0;
|
||||
display:block;
|
||||
width:100%;
|
||||
height:100%;
|
||||
position:absolute;
|
||||
top:0;
|
||||
left:0;
|
||||
|
||||
}
|
||||
|
||||
#overlay{
|
||||
border:0;
|
||||
margin:0;
|
||||
padding:0;
|
||||
display:block;
|
||||
text-align:right;
|
||||
position:absolute;
|
||||
top:100px;
|
||||
right:0;
|
||||
z-index: 10;
|
||||
color: white;
|
||||
font-size:300%;
|
||||
}
|
||||
|
||||
#canvas{
|
||||
border:0;
|
||||
margin:0;
|
||||
padding:0;
|
||||
display:block;
|
||||
width:20%;
|
||||
text-align:right;
|
||||
height:100px;
|
||||
position:absolute;
|
||||
top:0;
|
||||
right:0;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body id="main">
|
||||
<div id="overlay"></div>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div id="container"></div>
|
||||
<script>
|
||||
function getColor(value) {
|
||||
var hue = (Math.abs(value*100+50)).toString(10);
|
||||
return ["hsl(", hue, ",100%,50%)"].join("");
|
||||
}
|
||||
var canvas = document.getElementById("canvas");
|
||||
var context = canvas.getContext("2d");
|
||||
var height = context.canvas.height;
|
||||
var width = context.canvas.width;
|
||||
canvas.history_accel = [];
|
||||
canvas.history_speed = [];
|
||||
var canvasNew = true
|
||||
|
||||
function plotData(speed, accel) {
|
||||
|
||||
|
||||
if (isNaN(speed)) {
|
||||
speed = 0;
|
||||
}
|
||||
if (isNaN(accel)) {
|
||||
accel = 0;
|
||||
}
|
||||
|
||||
canvas.history_accel.push(accel);
|
||||
canvas.history_speed.push(speed);
|
||||
|
||||
canvas.history_accel = canvas.history_accel.slice(-1 * canvas.width);
|
||||
canvas.history_speed = canvas.history_speed.slice(-1 * canvas.width);
|
||||
|
||||
var maxSpeed = Math.max(...canvas.history_speed);
|
||||
|
||||
var interval = 10;
|
||||
var target = canvas.target || (interval*1.5);
|
||||
if (target && (maxSpeed > target)){
|
||||
|
||||
canvas.target = maxSpeed*1.5; // set it higher than it needs to be, so it doens't jump around a lot
|
||||
var yScale = height / canvas.target;
|
||||
context.clearRect(0, 0, width, height);
|
||||
var w = 1;
|
||||
var x = width - w;
|
||||
|
||||
|
||||
for (var i = 0; i<canvas.history_speed.length;i++){
|
||||
|
||||
var accel = canvas.history_accel[i];
|
||||
var speed = canvas.history_speed[i];
|
||||
|
||||
var val = (10-accel)/10;
|
||||
if (val>1){val=1;}
|
||||
else if (val<0){val=0;}
|
||||
var color = getColor(val);
|
||||
var y = height - speed * yScale;
|
||||
context.fillStyle = color;
|
||||
context.fillRect(x, y, w, height);
|
||||
context.fillStyle = "#DDD5";
|
||||
context.fillRect(x, y-2, w, 4);
|
||||
|
||||
if (y-5>0){
|
||||
context.fillStyle = "#FFF3";
|
||||
context.fillRect(x, y+2, w, 1);
|
||||
}
|
||||
|
||||
var imageData = context.getImageData(w, 0, x, height);
|
||||
context.putImageData(imageData, 0, 0);
|
||||
context.clearRect(x, 0, w, height);
|
||||
}
|
||||
|
||||
for (var tt = interval; tt<canvas.target;tt+=interval){
|
||||
var y = parseInt(height - tt * yScale);
|
||||
context.fillStyle = "#0555";
|
||||
context.fillRect(0, y, width, 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var val = (10-accel)/10;
|
||||
if (val>1){val=1;}
|
||||
else if (val<0){val=0;}
|
||||
var color = getColor(val);
|
||||
|
||||
var yScale = height / target;
|
||||
|
||||
var w = 1;
|
||||
var x = width - w;
|
||||
var y = height - speed * yScale;
|
||||
|
||||
context.fillStyle = color;
|
||||
context.fillRect(x, y, w, height);
|
||||
context.fillStyle = "#DDD5";
|
||||
context.fillRect(x, y-2, w, 4);
|
||||
|
||||
if (y-5>0){
|
||||
context.fillStyle = "#FFF3";
|
||||
context.fillRect(x, y+2, w, 1);
|
||||
}
|
||||
|
||||
context.fillStyle = "#0555";
|
||||
if (canvasNew){
|
||||
canvasNew = false;
|
||||
for (var tt = interval; tt<target;tt+=interval){
|
||||
var y = parseInt(height - tt * yScale);
|
||||
context.fillRect(0, y, width, 1);
|
||||
}
|
||||
} else {
|
||||
for (var tt = interval; tt<target;tt+=interval){
|
||||
var y = parseInt(height - tt * yScale);
|
||||
context.fillRect(x, y, w, 1);
|
||||
}
|
||||
}
|
||||
|
||||
var imageData = context.getImageData(w, 0, x, height);
|
||||
context.putImageData(imageData, 0, 0);
|
||||
context.clearRect(x, 0, w, height);
|
||||
|
||||
}
|
||||
|
||||
function loadIframe(url=false){
|
||||
var iframe = document.createElement("iframe");
|
||||
|
||||
if (url){
|
||||
var iframesrc = url;
|
||||
} else {
|
||||
var iframesrc = document.getElementById("viewlink").value;
|
||||
}
|
||||
|
||||
iframe.allow = "autoplay;camera;microphone;fullscreen;picture-in-picture;";
|
||||
|
||||
if (iframesrc==""){
|
||||
iframesrc="./";
|
||||
}
|
||||
|
||||
iframe.src = iframesrc;
|
||||
|
||||
document.getElementById("container").appendChild(iframe);
|
||||
var outputWindow = document.getElementById("overlay");
|
||||
|
||||
var sensors = {};
|
||||
|
||||
//////////// LISTEN FOR EVENTS
|
||||
|
||||
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 ("sensors" in e.data){
|
||||
//console.log(e.data.sensors);
|
||||
|
||||
var speed = 0;
|
||||
if (e.data.sensors.pos){
|
||||
speed = e.data.sensors.pos.speed;
|
||||
// e.data.sensors.pos.alt
|
||||
// e.data.sensors.pos.t
|
||||
}
|
||||
|
||||
var accel = 0;
|
||||
if (e.data.sensors.lin){
|
||||
accel += Math.pow(e.data.sensors.lin.x, 2);
|
||||
accel += Math.pow(e.data.sensors.lin.y, 2);
|
||||
accel += Math.pow(e.data.sensors.lin.z, 2);
|
||||
}
|
||||
if (accel){
|
||||
accel = Math.pow(accel,0.5);
|
||||
}
|
||||
|
||||
if (isNaN(accel)){
|
||||
accel = 0;
|
||||
}
|
||||
|
||||
plotData(speed, accel);
|
||||
|
||||
outputWindow.innerHTML = "";
|
||||
|
||||
speed = parseInt(speed*100)/100;
|
||||
outputWindow.innerHTML += "speed: "+speed+"m/s<br />";
|
||||
|
||||
accel = parseInt(accel*100)/100;
|
||||
outputWindow.innerHTML += "acceleration: " + accel + "m/s^2<br />";
|
||||
|
||||
//for (var key in e.data.sensors.lin) {
|
||||
// outputWindow.innerHTML += key + " lin: " + e.data.sensors.lin[key] + "<br />";
|
||||
//}
|
||||
//for (var key in e.data.sensors.acc) {
|
||||
// outputWindow.innerHTML += key + " acc: " + e.data.sensors.acc[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 />";
|
||||
//}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadIframe("../"+window.location.search);
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
280
examples/sensors.html
Normal 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>
|
||||
185
examples/socal.html
Normal file
@ -0,0 +1,185 @@
|
||||
<html>
|
||||
<head><title>SocialStream + 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;
|
||||
}
|
||||
|
||||
h1{
|
||||
color: white;
|
||||
font-family: verdana;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#container2{
|
||||
width:100vw;
|
||||
height:100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left:0;
|
||||
display:none;
|
||||
z-index:2;
|
||||
}
|
||||
#container1{
|
||||
width:100vw;
|
||||
height:100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left:0;
|
||||
display:none;
|
||||
}
|
||||
iframe{
|
||||
width:100vw;
|
||||
height:100vh;
|
||||
}
|
||||
|
||||
@media screen and (orientation:portrait) {
|
||||
#container2{
|
||||
}
|
||||
#container1{
|
||||
}
|
||||
iframe{
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (orientation:landscape) {
|
||||
#container2{
|
||||
}
|
||||
#container1{
|
||||
}
|
||||
iframe{
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container2"></div>
|
||||
<div id="container1" ></div>
|
||||
<div id="selectChatSource">
|
||||
<h1>Which social integration are you adding?</h1>
|
||||
|
||||
|
||||
</div>
|
||||
<div id="clean">
|
||||
<h1>Use VDO.Ninja and SocialStream chat at the same time</h1>
|
||||
<input placeholder="Enter a VDON stream ID or VDON URL" id="viewlink" type="text" />
|
||||
<input placeholder="Enter the SocialStream URL" id="social" 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 removeStorage(cname){
|
||||
localStorage.removeItem(cname);
|
||||
}
|
||||
|
||||
function setStorage(cname, cvalue, hours=9999){ // not actually a cookie
|
||||
var now = new Date();
|
||||
var item = {
|
||||
value: cvalue,
|
||||
expiry: now.getTime() + (hours * 60 * 60 * 1000),
|
||||
};
|
||||
try{
|
||||
localStorage.setItem(cname, JSON.stringify(item));
|
||||
}catch(e){errorlog(e);}
|
||||
}
|
||||
|
||||
function getStorage(cname) {
|
||||
try {
|
||||
var itemStr = localStorage.getItem(cname);
|
||||
} catch(e){
|
||||
errorlog(e);
|
||||
return;
|
||||
}
|
||||
if (!itemStr) {
|
||||
return "";
|
||||
}
|
||||
var item = JSON.parse(itemStr);
|
||||
var now = new Date();
|
||||
if (now.getTime() > item.expiry) {
|
||||
localStorage.removeItem(cname);
|
||||
return "";
|
||||
}
|
||||
return item.value;
|
||||
}
|
||||
if (getStorage("SocialStreamChatLink")){
|
||||
document.getElementById("social").value = getStorage("SocialStreamChatLink");
|
||||
}
|
||||
if (getStorage("vdoNinjaSocialStreamURL")){
|
||||
document.getElementById("viewlink").value = getStorage("vdoNinjaSocialStreamURL");
|
||||
}
|
||||
|
||||
function loadIframes(url=false){
|
||||
|
||||
var roomname = document.getElementById("viewlink").value;
|
||||
var room2 = document.getElementById("social").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("/");
|
||||
path = path.replace("/examples","");
|
||||
|
||||
if (roomname.startsWith("https://")){
|
||||
var room1 = roomname;
|
||||
} else {
|
||||
var room1 = "https://"+path+"/?push="+roomname+"&webcam&autostart&vd=front&ad=1&transparent&noheader";
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
|
||||
setStorage("SocialStreamChatLink", room2);
|
||||
|
||||
setStorage("vdoNinjaSocialStreamURL", room1);
|
||||
|
||||
|
||||
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>
|
||||
@ -2,7 +2,7 @@
|
||||
<head>
|
||||
<meta charset="utf8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>OBSN Chat Overlay</title>
|
||||
<title>VDON Chat Overlay</title>
|
||||
<style>
|
||||
|
||||
@font-face {
|
||||
167
examples/twitch.html
Normal file
@ -0,0 +1,167 @@
|
||||
<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;
|
||||
}
|
||||
|
||||
h1{
|
||||
color: white;
|
||||
font-family: verdana;
|
||||
margin: 10px;
|
||||
}
|
||||
@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">
|
||||
<h1>Use VDO.Ninja and Twitch chat at the same time</h1>
|
||||
<input placeholder="Enter a VDON stream ID or VDON URL" 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 removeStorage(cname){
|
||||
localStorage.removeItem(cname);
|
||||
}
|
||||
|
||||
function setStorage(cname, cvalue, hours=9999){ // not actually a cookie
|
||||
var now = new Date();
|
||||
var item = {
|
||||
value: cvalue,
|
||||
expiry: now.getTime() + (hours * 60 * 60 * 1000),
|
||||
};
|
||||
try{
|
||||
localStorage.setItem(cname, JSON.stringify(item));
|
||||
}catch(e){errorlog(e);}
|
||||
}
|
||||
|
||||
function getStorage(cname) {
|
||||
try {
|
||||
var itemStr = localStorage.getItem(cname);
|
||||
} catch(e){
|
||||
errorlog(e);
|
||||
return;
|
||||
}
|
||||
if (!itemStr) {
|
||||
return "";
|
||||
}
|
||||
var item = JSON.parse(itemStr);
|
||||
var now = new Date();
|
||||
if (now.getTime() > item.expiry) {
|
||||
localStorage.removeItem(cname);
|
||||
return "";
|
||||
}
|
||||
return item.value;
|
||||
}
|
||||
if (getStorage("twitchChatLink")){
|
||||
document.getElementById("twitch").value = getStorage("twitchChatLink");
|
||||
}
|
||||
if (getStorage("vdoNinjaTwitchURL")){
|
||||
document.getElementById("viewlink").value = getStorage("vdoNinjaTwitchURL");
|
||||
}
|
||||
|
||||
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("/");
|
||||
path = path.replace("/examples","");
|
||||
|
||||
if (roomname.startsWith("https://")){
|
||||
var room1 = roomname;
|
||||
} else {
|
||||
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);
|
||||
|
||||
|
||||
setStorage("twitchChatLink", room2);
|
||||
|
||||
setStorage("vdoNinjaTwitchURL", room1);
|
||||
|
||||
|
||||
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
@ -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>
|
||||
|
||||
129
examples/youtube.html
Normal file
@ -0,0 +1,129 @@
|
||||
<html>
|
||||
<head><title>YouTube Chat + VDON</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%;
|
||||
color:white;
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
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;
|
||||
color:black;
|
||||
}
|
||||
|
||||
|
||||
@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="vdonlink" onchange="updateLink(event);" type="text" />
|
||||
<input placeholder="Enter the Youtube Video ID" id="youtube" type="text" />
|
||||
<button onclick="loadIframes()" style="display:block;padding:10px;margin:10px;">START</button>
|
||||
<div id="viewLink">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
window.addEventListener("orientationchange", function() {
|
||||
// Announce the new orientation number
|
||||
// alert(window.orientation);
|
||||
}, false);
|
||||
|
||||
function updateLink(event){
|
||||
var streamid = document.getElementById("vdonlink").value;
|
||||
|
||||
|
||||
var path = window.location.host+window.location.pathname.split("/").slice(0,-1).join("/");
|
||||
path = path.replace("/examples","");
|
||||
|
||||
var viewLink = "https://"+path+"/?view="+streamid;
|
||||
document.getElementById("viewLink").innerHTML = "View link is: "+viewLink;
|
||||
}
|
||||
|
||||
function loadIframes(url=false){
|
||||
|
||||
var streamid = document.getElementById("vdonlink").value;
|
||||
var youtube = document.getElementById("youtube").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("/");
|
||||
path = path.replace("/examples","");
|
||||
|
||||
var room1 = "https://"+path+"/?push="+streamid+"&webcam&autostart&vd=front&ad=1&transparent&noheader";
|
||||
var room2 = "https://www.youtube.com/live_chat?is_popout=1&v="+youtube+"&embed_domain="+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>
|
||||
1
examples/youtube.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19.615 3.184c-3.604-.246-11.631-.245-15.23 0-3.897.266-4.356 2.62-4.385 8.816.029 6.185.484 8.549 4.385 8.816 3.6.245 11.626.246 15.23 0 3.897-.266 4.356-2.62 4.385-8.816-.029-6.185-.484-8.549-4.385-8.816zm-10.615 12.816v-8l8 3.993-8 4.007z"/></svg>
|
||||
|
After Width: | Height: | Size: 342 B |
182
examples/zoom.html
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
137
filters/anon.js
Normal file
@ -0,0 +1,137 @@
|
||||
async function effectsEngine(effectName){
|
||||
|
||||
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();
|
||||
while (loadList.length){
|
||||
await loadScript(loadList.pop());
|
||||
}
|
||||
}
|
||||
|
||||
log("finished loading anon effect");
|
||||
|
||||
// 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){
|
||||
errorlog(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.effect){
|
||||
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;
|
||||
}
|
||||
75
filters/anon/addVideoRecordingEffect.js
Normal 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);
|
||||
}
|
||||
1
filters/anon/anonymous.json
Normal file
BIN
filters/anon/anonymous.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
filters/anon/anonymous_mask.blend
Normal file
350
filters/dog.js
Normal file
@ -0,0 +1,350 @@
|
||||
async function effectsEngine(effectName){
|
||||
|
||||
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();
|
||||
while (loadList.length){
|
||||
await 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){
|
||||
errorlog(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.effect){
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
log("returning main");
|
||||
return main;
|
||||
}
|
||||
132
filters/dog/ThreeFlexMaterial.js
Normal 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;
|
||||
};
|
||||
BIN
filters/dog/images/texture_pink.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
filters/dog/images/texture_white.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
87
filters/dog/index.html
Normal 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
@ -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
@ -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
|
||||
}
|
||||
|
||||
BIN
filters/dog/models/dog/alpha_ears.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
filters/dog/models/dog/alpha_ears_1024.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
filters/dog/models/dog/alpha_ears_256.jpg
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
filters/dog/models/dog/displace_nez.jpg
Normal file
|
After Width: | Height: | Size: 182 KiB |
BIN
filters/dog/models/dog/displace_oreilles.jpg
Normal file
|
After Width: | Height: | Size: 378 KiB |
1
filters/dog/models/dog/dog_ears.json
Normal file
1
filters/dog/models/dog/dog_nose.json
Normal file
BIN
filters/dog/models/dog/dog_tongue.jpg
Normal file
|
After Width: | Height: | Size: 209 KiB |
1
filters/dog/models/dog/dog_tongue.json
Normal file
BIN
filters/dog/models/dog/flex_ears.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
filters/dog/models/dog/flex_ears_1024.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
filters/dog/models/dog/flex_ears_256.jpg
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
filters/dog/models/dog/flex_nose.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
filters/dog/models/dog/flex_tongue.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
filters/dog/models/dog/flex_tongue_1024.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
filters/dog/models/dog/flex_tongue_256.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
filters/dog/models/dog/normal_ears.jpg
Normal file
|
After Width: | Height: | Size: 367 KiB |
BIN
filters/dog/models/dog/normal_nose.jpg
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
filters/dog/models/dog/texture_ears.jpg
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
filters/dog/models/dog/texture_nose.jpg
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
filters/dog/models/dog/tongue_alpha.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
filters/dog/models/dog/tongue_alpha_1024.jpg
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
filters/dog/models/dog/tongue_alpha_256.jpg
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
3
filters/readme.md
Normal 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
|
||||
61
filters/sample.js
Normal file
@ -0,0 +1,61 @@
|
||||
async function effectsEngine(){
|
||||
console.log('LOADED SAMPLE');
|
||||
var loadList = [];
|
||||
loadList.push("./thirdparty/jeeliz/jeelizFaceFilter.js");
|
||||
loadList.push("./thirdparty/jeeliz/helpers/JeelizCanvas2DHelper.js");
|
||||
|
||||
if (loadList.length){
|
||||
loadList.reverse();
|
||||
while (loadList.length){
|
||||
await loadScript(loadList.pop());
|
||||
}
|
||||
}
|
||||
|
||||
function main(){ // entry point
|
||||
console.log("LOADED MAIN OF SAMPLE");
|
||||
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();
|
||||
};
|
||||
326
iframe-examples.js
Normal file
@ -0,0 +1,326 @@
|
||||
/**
|
||||
* SANDBOX EXAMPLE CONFIG
|
||||
*
|
||||
* options: (number | boolean | string | null)[]
|
||||
* - OPTIONAL if using a user input; see "input" below
|
||||
* - List of values to test (passed into "result" function)
|
||||
* - An option button will be generated for each value in the list
|
||||
*
|
||||
* input: object
|
||||
* - OPTIONAL
|
||||
* - User input for testing (eg a range slider).
|
||||
* - Output of element onchange will be passed into RESULT function.
|
||||
*
|
||||
* labels: string[]
|
||||
* - OPTIONAL
|
||||
* - List of labels for option buttons (should be same length as options list).
|
||||
* - If no labels are provided, option buttons are labeled with their value
|
||||
*
|
||||
* result: (value: any) => object
|
||||
* - Function that returns a postMessage object
|
||||
* - Based on the value produced by an option button, or a user input
|
||||
*/
|
||||
|
||||
const IFRAME_API = {
|
||||
add: {
|
||||
options: [true],
|
||||
labels: ["Video with styles"],
|
||||
result: value => ({
|
||||
"target": "*",
|
||||
"add": value,
|
||||
"settings": { "style": "width:640px;height:360px;float:left;border:10px solid red;display:block;"}
|
||||
})
|
||||
},
|
||||
automixer: {
|
||||
options: [true, false],
|
||||
result: value => ({ "automixer": value })
|
||||
},
|
||||
bitrate: {
|
||||
options: [-1],
|
||||
labels: ["default (-1)"],
|
||||
input: {
|
||||
title: "bitrate",
|
||||
type: "range",
|
||||
min: 0,
|
||||
max: 6000,
|
||||
value: 3000
|
||||
},
|
||||
result: value => ({ "bitrate": value, "target": "*" })
|
||||
},
|
||||
camera: {
|
||||
options: [false, true, "toggle"],
|
||||
result: value => ({ "camera": value })
|
||||
},
|
||||
changeAudioDevice: { // change text of add camera button
|
||||
options: [1,2,3,4],
|
||||
result: value => ({ "changeAudioDevice": value })
|
||||
},
|
||||
changeVideoDevice: { // change text of add camera button
|
||||
options: [1,2,3,4],
|
||||
result: value => ({ "changeVideoDevice": value })
|
||||
},
|
||||
close: {
|
||||
options: [true],
|
||||
result: value => ({ "close": value })
|
||||
},
|
||||
getDetailedState: {
|
||||
options: [true],
|
||||
result: value => ({ "getDetailedState": value }),
|
||||
},
|
||||
getDeviceList: {
|
||||
options: [true],
|
||||
result: value => ({ "getDeviceList": value }),
|
||||
},
|
||||
getLoudness: {
|
||||
options: [false, true],
|
||||
result: value => ({ "getLoudness": value }),
|
||||
},
|
||||
getStreamIDs: {
|
||||
options: [true],
|
||||
result: value => ({ "getStreamIDs": value }),
|
||||
},
|
||||
keyframe: {
|
||||
options: [true],
|
||||
result: value => ({ "keyframe": value }),
|
||||
},
|
||||
mute: {
|
||||
options: [
|
||||
true,
|
||||
false,
|
||||
"toggle" // open to a better suggestion here.
|
||||
],
|
||||
result: value => ({ "mute": value }),
|
||||
},
|
||||
mic: {
|
||||
options: [true, false, "toggle"],
|
||||
result: value => ({ "mic": value }),
|
||||
},
|
||||
panning: {
|
||||
options: [0, 180, 90],
|
||||
input: {
|
||||
title: "panning",
|
||||
type: "range",
|
||||
min: 0,
|
||||
max: 180,
|
||||
value: 90
|
||||
},
|
||||
labels: ["Left (0)", "Right (180)", "Center (90)"],
|
||||
result: value => ({ "panning": value }),
|
||||
},
|
||||
previewWebcam: {
|
||||
options: ["previewWebcam"], // publishScreen
|
||||
result: value => ({ "function": value }),
|
||||
},
|
||||
record: {
|
||||
options: [true, false],
|
||||
result: value => ({ "record": value }),
|
||||
},
|
||||
reload: {
|
||||
options: [true],
|
||||
result: value => ({ "reload": value }),
|
||||
},
|
||||
remove: { // target can be a stream ID or * for all.
|
||||
options: [true],
|
||||
labels: ["Target video"],
|
||||
result: value => ({ "target": "*", "remove": value })
|
||||
},
|
||||
sendChat: {
|
||||
input: {
|
||||
type: "text",
|
||||
value: "Hello"
|
||||
},
|
||||
result: value => ({ "sendChat": value }),
|
||||
},
|
||||
sceneState: {
|
||||
options: [true, false],
|
||||
labels: ["ENABLE TALLY LIGHT (true)", "STOP TALLY LIGHT (false)"],
|
||||
result: value => ({ "sceneState": value })
|
||||
},
|
||||
style: {
|
||||
options: ["#main { zoom: 0.5;} video {float: left; margin: 0; padding: 0; } #info {display:none;}"],
|
||||
labels: ["Insert Style Sheet"],
|
||||
result: value => ({ "style": value }),
|
||||
},
|
||||
volume: {
|
||||
input: {
|
||||
title: "volume",
|
||||
type: "range",
|
||||
min: 0,
|
||||
max: 200,
|
||||
value: 100
|
||||
},
|
||||
result: value => ({ "volume": value }),
|
||||
},
|
||||
["function: Eval"]: {
|
||||
options: ["eval"], // publishScreen
|
||||
result: value => ({ "function": value, "value": 'alert(\"DANGERUS\")' })
|
||||
},
|
||||
["function: Change html"]: {
|
||||
options: ["changeHTML"], // change text of add camera button
|
||||
result: value => ({ "function": value, "target": "add_camera", "value": "NEW CAMERA TEXT" })
|
||||
}
|
||||
}
|
||||
|
||||
const COMPANION_API = { // list available commands to console
|
||||
bitrate: {
|
||||
input: {
|
||||
title: "bitrate",
|
||||
type: "range",
|
||||
min: 0,
|
||||
max: 6000,
|
||||
value: 3000
|
||||
},
|
||||
result: value => ({ target: null, action: "bitrate", value })
|
||||
},
|
||||
camera: {
|
||||
options: [false, true, "toggle"],
|
||||
result: value => ({ target: null, action: "camera", value })
|
||||
},
|
||||
forceKeyframe: {
|
||||
options: [null],
|
||||
labels: ["Rainbow puke fix"],
|
||||
result: value => ({ target: null, action: "forceKeyframe", value }),
|
||||
},
|
||||
getDetails: {
|
||||
options: [null],
|
||||
result: value => ({ target: null, action: "getDetails", value })
|
||||
},
|
||||
group: {
|
||||
options: [1,2,3,4,5,6,7,8],
|
||||
result: value => ({ target: null, action: "group", value })
|
||||
},
|
||||
hangup: {
|
||||
options: [null],
|
||||
result: value => ({ target: null, action: "hangup", value })
|
||||
},
|
||||
mic: {
|
||||
options: [false, true, "toggle"],
|
||||
result: value => ({ target: null, action: "mic", value })
|
||||
},
|
||||
panning: {
|
||||
options: [0, 180, 90],
|
||||
input: {
|
||||
title: "panning",
|
||||
type: "range",
|
||||
min: 0,
|
||||
max: 180,
|
||||
value: 90
|
||||
},
|
||||
labels: ["Left (0)", "Right (180)", "Center (90)"],
|
||||
result: value => ({ target: null, action: "panning", value })
|
||||
},
|
||||
record: {
|
||||
options: [false, true, "toggle"],
|
||||
result: value => ({ target: null, action: "record", value })
|
||||
},
|
||||
reload: {
|
||||
options: [null],
|
||||
result: value => ({ target: null, action: "reload", value })
|
||||
},
|
||||
sendChat: {
|
||||
input: {
|
||||
type: "text",
|
||||
value: "Hello"
|
||||
},
|
||||
result: value => ({ target: null, action: "sendChat", value }),
|
||||
},
|
||||
speaker: { // "speaker" also works in the same way
|
||||
options: [false, true, "toggle"],
|
||||
result: value => ({ target: null, action: "speaker", value }),
|
||||
},
|
||||
togglehand: {
|
||||
options: [null],
|
||||
result: value => ({ target: null, action: "togglehand", value }),
|
||||
},
|
||||
togglescreenshare: {
|
||||
options: [null],
|
||||
result: value => ({ target: null, action: "togglescreenshare", value }),
|
||||
},
|
||||
volume: {
|
||||
input: {
|
||||
title: "volume",
|
||||
type: "range",
|
||||
min: 0,
|
||||
max: 200,
|
||||
value: 100
|
||||
},
|
||||
result: value => ({ target: null, "action": "volume", value }),
|
||||
},
|
||||
}
|
||||
|
||||
function guestTargetedAPI(target) {
|
||||
return {
|
||||
addScene: {
|
||||
options: [null,1,2,3,4,5,6,7,8],
|
||||
input: {
|
||||
type: "text",
|
||||
value: "scene321"
|
||||
},
|
||||
result: value => ({ "action": "addScene", target, value })
|
||||
},
|
||||
display: {
|
||||
options: [null],
|
||||
result: value => ({ "action": "display", target, value })
|
||||
},
|
||||
forward: {
|
||||
options: [null],
|
||||
input: {
|
||||
type: "text",
|
||||
value: "room321"
|
||||
},
|
||||
result: value => ({ "action": "forward", target, value })
|
||||
},
|
||||
forceKeyframe: {
|
||||
options: [null],
|
||||
labels: ["Rainbow puke fix"],
|
||||
result: value => ({ "action": "forceKeyframe", target, value })
|
||||
},
|
||||
hangup: {
|
||||
options: [null],
|
||||
result: value => ({ "action": "hangup", target, value })
|
||||
},
|
||||
group: {
|
||||
options: [0,1,2,3,4,5,6,7,8],
|
||||
input: {
|
||||
type: "text",
|
||||
value: "group321"
|
||||
},
|
||||
result: value => ({ "action": "group", target, value })
|
||||
},
|
||||
soloChat: {
|
||||
options: [null],
|
||||
result: value => ({ "action": "soloChat", target, value })
|
||||
},
|
||||
soloVideo: {
|
||||
options: [null],
|
||||
result: value => ({ "action": "soloVideo", target, value })
|
||||
},
|
||||
speaker: {
|
||||
options: [null],
|
||||
labels: ["Remote speaker"],
|
||||
result: value =>({ "action": "speaker", target, value })
|
||||
},
|
||||
mic: {
|
||||
options: [null],
|
||||
result: value =>({ "action": "mic", target, value })
|
||||
},
|
||||
muteScene: {
|
||||
options: [null,1,2,3,4,5,6,7,8],
|
||||
input: {
|
||||
type: "text",
|
||||
value: "scene321"
|
||||
},
|
||||
result: value => ({ "action": "muteScene", target, value })
|
||||
},
|
||||
volume: {
|
||||
input: {
|
||||
title: "volume",
|
||||
type: "range",
|
||||
min: 0,
|
||||
max: 200,
|
||||
value: 100
|
||||
},
|
||||
result: value => ({ "action": "volume", target, value }),
|
||||
},
|
||||
}
|
||||
}
|
||||
200
iframe.css
Normal file
@ -0,0 +1,200 @@
|
||||
body {
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
background-color: #e1e8fc;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 5px;
|
||||
margin-bottom: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 5px;
|
||||
background: white;
|
||||
border: solid;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
button:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
video {
|
||||
max-width: 300px;
|
||||
max-height: 100px;
|
||||
}
|
||||
|
||||
#viewlink {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
#container {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.api-section {
|
||||
padding: 10px;
|
||||
border-bottom: solid 1px;
|
||||
}
|
||||
|
||||
.api-section > h4:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.api-section-header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-bottom: solid 1px;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.custom-post {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.custom-post > * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.custom-post > button {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
padding: 0 20px;
|
||||
font-size: 16px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.custom-post-input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: solid 5px #4d66a8;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.controls {
|
||||
overflow: auto;
|
||||
background: white;
|
||||
width: 350px;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.example-body {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 700px;
|
||||
}
|
||||
|
||||
.example-header {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
background: #4d66a8;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.example-header > button {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.iframe-example {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 20px;
|
||||
border: solid #4d66a8 2px;
|
||||
}
|
||||
|
||||
.main-log {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
flex-shrink: 0;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
background: #0b0e15;
|
||||
color: white;
|
||||
border-top: solid 1px #6e6e6e;
|
||||
font-family: monospace;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.output-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.post-log {
|
||||
background: #273047;
|
||||
padding: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.sensors-log {
|
||||
padding: 10px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.stream-data-logs {
|
||||
height: 100%;
|
||||
background: black;
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.target-guest {
|
||||
padding: 10px;
|
||||
border-bottom: solid 1px;
|
||||
}
|
||||
|
||||
.target-guest-inputs {
|
||||
margin-top: 5px;
|
||||
}
|
||||
862
iframe.html
@ -1,391 +1,473 @@
|
||||
<html>
|
||||
<head><title>IFRAME 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;
|
||||
}
|
||||
</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";
|
||||
iframe.allowtransparency="true";
|
||||
iframe.allowfullscreen ="true";
|
||||
|
||||
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 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 = "Request Stats";
|
||||
button.onclick = function(){iframe.contentWindow.postMessage({"getStats":true}, '*');};
|
||||
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 = "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 = "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 = "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 ("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 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 OBS.Ninja View URL here" id="viewlink" />
|
||||
<button onclick="loadIframe();">ADD</button>
|
||||
<input type="checkbox" id="clean" checked>Clean Output
|
||||
<input type="checkbox" id="transparent" checked>Transparent
|
||||
<input type="checkbox" id="hidemenu">Hide Menu
|
||||
<div id="container">
|
||||
|
||||
</div>
|
||||
</body>
|
||||
<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" />
|
||||
<link rel="stylesheet" href="./iframe.css" />
|
||||
<script src="iframe-examples.js"></script>
|
||||
<script>
|
||||
const ExpandableCards = {};
|
||||
|
||||
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 exampleId = generateId();
|
||||
|
||||
// Keep track of which API cards are opened vs closed
|
||||
ExpandableCards[exampleId] = {
|
||||
iframe: false,
|
||||
companion: false,
|
||||
}
|
||||
|
||||
var iframesrc = document.getElementById("viewlink").value;
|
||||
var container = document.getElementById("container");
|
||||
var iframe = newElement("iframe", {
|
||||
allow: "document-domain;encrypted-media;sync-xhr;usb;web-share;cross-origin-isolated;accelerometer;midi;geolocation;autoplay;camera;microphone;fullscreen;picture-in-picture;display-capture;",
|
||||
src: addUrlParams(iframesrc, ["cleanoutput", "transparent", "hidemenu"])
|
||||
});
|
||||
var iframeExample = newElement("div", { id: exampleId, classList: "iframe-example" });
|
||||
var exampleHeader = newElement("div", { classList: "example-header", innerHTML: iframesrc });
|
||||
var exampleBody = newElement("div", { classList: "example-body" });
|
||||
var outputContainer = newElement("div", { classList: "output-container" });
|
||||
var customPostContainer = newElement("div", { classList: "custom-post" });
|
||||
var customPostInput = newElement("input", {
|
||||
classList: "custom-post-input", type: "text", placeholder: "Post Message (format as a JSON object}"
|
||||
});
|
||||
var mainLog = newElement("div", { id: exampleId + "-logs", classList: "main-log" });
|
||||
var streamDataLogs = newElement("div", { classList: "stream-data-logs" } );
|
||||
var controlsContainer = getExampleControls();
|
||||
var targetGuest = getGuestAPIGenerator();
|
||||
var customPostSend = newElement("button", {
|
||||
innerText: "POST",
|
||||
onclick: function() {
|
||||
try {
|
||||
const message = JSON.parse(customPostInput.value);
|
||||
postMessage(message, '*');
|
||||
} catch (error) {
|
||||
alert(error)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
customPostContainer.append(customPostInput, customPostSend);
|
||||
controlsContainer.appendChild(targetGuest);
|
||||
outputContainer.append(iframe, mainLog, customPostContainer);
|
||||
exampleBody.append(controlsContainer, outputContainer, streamDataLogs);
|
||||
iframeExample.append(exampleHeader, exampleBody);
|
||||
container.prepend(iframeExample);
|
||||
|
||||
|
||||
|
||||
var media = {};
|
||||
media.tracks = {};
|
||||
media.streams = {};
|
||||
|
||||
window.addEventListener('messageerror', e => {
|
||||
console.error(e);
|
||||
});
|
||||
|
||||
//////////// LISTEN FOR EVENTS
|
||||
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 (typeof e.data !== "object"){
|
||||
console.log(e.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.data.frame){ // add `&sendframes` to the view link to trigger this event; it lets you capture video/audio from the parent window
|
||||
if (!media.tracks[e.data.trackID]){
|
||||
media.tracks[e.data.trackID] = {};
|
||||
media.tracks[e.data.trackID].generator = new MediaStreamTrackGenerator({kind:e.data.kind});
|
||||
media.tracks[e.data.trackID].stream = new MediaStream([media.tracks[e.data.trackID].generator]);
|
||||
media.tracks[e.data.trackID].frameWriter = media.tracks[e.data.trackID].generator.writable.getWriter();
|
||||
|
||||
media.tracks[e.data.trackID].frameWriter.write(e.data.frame);
|
||||
|
||||
if (!media.streams[e.data.streamID]){
|
||||
media.streams[e.data.streamID] = document.createElement("video");
|
||||
media.streams[e.data.streamID].id = "video_"+e.data.streamID;
|
||||
media.streams[e.data.streamID].autoplay = true;
|
||||
media.streams[e.data.streamID].muted = true;
|
||||
setTimeout(function(ele){ele.controls=true;},3000, media.streams[e.data.streamID]);
|
||||
media.streams[e.data.streamID].srcObject = media.tracks[e.data.trackID].stream;
|
||||
mainLog.appendChild(media.streams[e.data.streamID]);
|
||||
} else {
|
||||
if (e.data.kind=="video"){
|
||||
media.streams[e.data.streamID].srcObject.getVideoTracks().forEach(trk=>{
|
||||
media.streams[e.data.streamID].srcObject.removeTrack(trk);
|
||||
});
|
||||
} else if (e.data.kind=="audio"){
|
||||
media.streams[e.data.streamID].srcObject.getAudioTracks().forEach(trk=>{
|
||||
media.streams[e.data.streamID].srcObject.removeTrack(trk);
|
||||
});
|
||||
}
|
||||
media.tracks[e.data.trackID].stream.getTracks().forEach(trk=>{
|
||||
media.streams[e.data.streamID].srcObject.addTrack(trk);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
media.tracks[e.data.trackID].frameWriter.write(e.data.frame);
|
||||
}
|
||||
return;
|
||||
} // end of video/audio capture
|
||||
|
||||
var consolelog = true;
|
||||
if (e.data.stats){
|
||||
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]);
|
||||
}
|
||||
logOutput(out)
|
||||
consolelog = false;
|
||||
}
|
||||
|
||||
if (e.data.gotChat){
|
||||
logOutput(e.data.gotChat)
|
||||
}
|
||||
|
||||
if ("action" in e.data && e.data.action !== "loudness") {
|
||||
logOutput("child-page-action: "+e.data.action+"<br />")
|
||||
}
|
||||
|
||||
if ("streamIDs" in e.data){
|
||||
var out = "child-page-action: streamIDs<br />"
|
||||
for (var key in e.data.streamIDs) {
|
||||
out += "streamID: " + key + ", label:"+e.data.streamIDs[key] + "\n";
|
||||
}
|
||||
logOutput(out)
|
||||
}
|
||||
|
||||
if ("deviceList" in e.data){
|
||||
var out = "child-page-action: deviceList<br />";
|
||||
for (var i = 0;i<e.data.deviceList.length;i++){
|
||||
out += e.data.deviceList[i].label + "<br />";
|
||||
}
|
||||
logOutput(out);
|
||||
}
|
||||
|
||||
if ("loudness" in e.data) {
|
||||
var loudnessOutput;
|
||||
var id = exampleId + "-loudness";
|
||||
var out = "child-page-action: loudness<br />";
|
||||
|
||||
if (document.getElementById(id)){
|
||||
loudnessOutput = document.getElementById(id);
|
||||
} else {
|
||||
loudnessOutput = newElement("div", { id, classList: "sensors-log" });
|
||||
streamDataLogs.prepend(loudnessOutput);
|
||||
}
|
||||
for (var key in e.data.loudness) {
|
||||
out += key + " Loudness: " + e.data.loudness[key] + "\n";
|
||||
}
|
||||
logOutput(out, { window: loudnessOutput, replace: true, style: { paddingBottom: '20px', borderBottom: 'dotted 1px #4d66a8' } });
|
||||
consolelog = false;
|
||||
}
|
||||
|
||||
if ("detailedState" in e.data){
|
||||
logOutput("child-page-action: detailedState<br />"+JSON.stringify(e.data.detailedState)+"<br />")
|
||||
}
|
||||
|
||||
if ("callback" in e.data) {
|
||||
if (e.data.callback.action === 'getDetails') {
|
||||
logOutput("child-page-action: getDetails<br />"+JSON.stringify(e.data.callback.result, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
if ("sensors" in e.data){
|
||||
var sensorsOutput;
|
||||
var id = exampleId + "-sensors";
|
||||
var out = "child-page-action: sensors<br /><br />";
|
||||
|
||||
if (document.getElementById(id)){
|
||||
sensorsOutput = document.getElementById(id);
|
||||
} else {
|
||||
sensorsOutput = newElement("div", { id, classList: "sensors-log" });
|
||||
streamDataLogs.appendChild(sensorsOutput);
|
||||
}
|
||||
for (var key in e.data.sensors.lin) {
|
||||
out += key + " linear: " + roundSensorData(e.data.sensors.lin[key]) + "<br />";
|
||||
}
|
||||
for (var key in e.data.sensors.acc) {
|
||||
out += key + " acceleration: " + roundSensorData(e.data.sensors.acc[key]) + "<br />";
|
||||
}
|
||||
for (var key in e.data.sensors.gyro) {
|
||||
out += key + " gyro: " + roundSensorData(e.data.sensors.gyro[key]) + "<br />";
|
||||
}
|
||||
for (var key in e.data.sensors.mag) {
|
||||
out += key + " magnet: " + roundSensorData(e.data.sensors.mag[key]) + "<br />";
|
||||
}
|
||||
logOutput(out, { window: sensorsOutput, replace: true })
|
||||
|
||||
function roundSensorData(data) {
|
||||
return Math.round(data * 100000) / 100000
|
||||
}
|
||||
consolelog = false;
|
||||
}
|
||||
|
||||
if (("action" in e.data) && (e.data.action === "view-stats-updated")){
|
||||
consolelog = false;
|
||||
}
|
||||
|
||||
if (consolelog){
|
||||
console.log(e.data);
|
||||
}
|
||||
});
|
||||
|
||||
function getExampleControls() {
|
||||
var controlsContainer = newElement("div", { classList: "controls" });
|
||||
|
||||
var iframeExamples = getExamplesSection(
|
||||
IFRAME_API,
|
||||
{
|
||||
guestId: 'iframe',
|
||||
headers: '<h3>IFRAME</h3><p><em>These commands are exclusive to the IFRAME API.</em></p>',
|
||||
}
|
||||
);
|
||||
var companionExamples = getExamplesSection(
|
||||
COMPANION_API,
|
||||
{
|
||||
guestId: 'companion',
|
||||
headers: '<h3>HTTP/WSS</h3><p><em>These commands re-use the Companion.Ninja HTTP/WSS API, except you can send them via this Iframe interface.</em></p>',
|
||||
info: '\
|
||||
<p><a target="_blank" style="word-break: break-word" href="https://github.com/steveseguin/Companion-Ninja#api-commands">\
|
||||
More details of the below IFRAME API details are here: https://github.com/steveseguin/Companion-Ninja#api-commands\
|
||||
</a></p>'
|
||||
}
|
||||
)
|
||||
|
||||
var removeIframeButton = document.createElement("button");
|
||||
removeIframeButton.innerHTML = "REMOVE IFRAME";
|
||||
removeIframeButton.onclick = function(){iframeExample.parentNode.removeChild(iframeExample);};
|
||||
exampleHeader.appendChild(removeIframeButton);
|
||||
|
||||
controlsContainer.append(
|
||||
iframeExamples.header,
|
||||
iframeExamples.examples,
|
||||
companionExamples.header,
|
||||
companionExamples.examples
|
||||
);
|
||||
|
||||
return controlsContainer
|
||||
}
|
||||
|
||||
function getGuestAPIGenerator() {
|
||||
var targetGuest = newElement('div', {
|
||||
classList: 'target-guest',
|
||||
innerHTML: '<h3>HTTP/WSS Director</h3><p><em>Directors can target commands to individual guests</em></p><h4>Target guest by: </h4>'
|
||||
});
|
||||
var targetGuestBySlot = newElement("input", {
|
||||
type: "radio",
|
||||
checked: true,
|
||||
id: exampleId+"-guest-slot-radio",
|
||||
value: "slot",
|
||||
name: exampleId+"-guest-target",
|
||||
onclick: function() {
|
||||
document.getElementById(exampleId+"-guest-slot").classList.remove("hidden");
|
||||
document.getElementById(exampleId+"-guest-id").classList="hidden";
|
||||
}
|
||||
});
|
||||
var targetGuestBySlotLabel = newElement("label", { for: exampleId+"-guest-slot-radio", innerText: "Slot (number)" })
|
||||
var targetGuestById = newElement("input", {
|
||||
type: "radio",
|
||||
id: exampleId+"-guest-id-radio",
|
||||
value: "id",
|
||||
min: 0,
|
||||
name: exampleId+"-guest-target",
|
||||
onclick: function() {
|
||||
document.getElementById(exampleId+"-guest-slot").classList="hidden";
|
||||
document.getElementById(exampleId+"-guest-id").classList.remove("hidden")
|
||||
}
|
||||
});
|
||||
var targetGuestByIdLabel = newElement("label", { for: exampleId+"-guest-id-radio", innerText: "streamID (string)" })
|
||||
var targetGuestInputs = newElement("div", { classList: "target-guest-inputs"});
|
||||
var guestIdInput = newElement("input", { id: exampleId+"-guest-id", type: "text", placeholder: "Guest ID", value: "abc123", classList: "hidden" });
|
||||
var guestSlotInput = newElement("input", { value: 1, id: exampleId+"-guest-slot", type: "number", placeholder: "Guest Slot" });
|
||||
var generateGuestAPI = newElement("button", {
|
||||
innerText: "Generate commands",
|
||||
onclick: function() {
|
||||
var targetType = (guestSlotInput.classList.value === "hidden") ? 'id' : 'slot';
|
||||
var value = (targetType === 'id') ? guestIdInput.value : guestSlotInput.value;
|
||||
var guestId = 'guest' + value;
|
||||
var headers = '<h4>Target GUEST ' + value + '</h4><p><em>These commands target a guest';
|
||||
headers += (targetType === 'id') ? ' with id ' : ' in slot ';
|
||||
headers += value + '</em></p>'
|
||||
|
||||
if (!document.getElementById(exampleId + '-' + guestId)) {
|
||||
var guestTargetedExamples = getExamplesSection(
|
||||
guestTargetedAPI(value),
|
||||
{ guestId: 'guest' + value, headers },
|
||||
)
|
||||
controlsContainer.appendChild(guestTargetedExamples.header);
|
||||
controlsContainer.appendChild(guestTargetedExamples.examples);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
targetGuestInputs.append(guestIdInput, guestSlotInput, generateGuestAPI);
|
||||
targetGuest.append(targetGuestBySlot, targetGuestBySlotLabel, targetGuestById, targetGuestByIdLabel, targetGuestInputs);
|
||||
|
||||
return targetGuest;
|
||||
}
|
||||
|
||||
function getExamplesSection(api, section) {
|
||||
var sectionId = exampleId + "-" + section.guestId;
|
||||
var header = document.createElement('div');
|
||||
header.classList = "api-section-header";
|
||||
|
||||
var toggleBtn = document.createElement('button');
|
||||
toggleBtn.innerText = "Show";
|
||||
toggleBtn.onclick = function() {
|
||||
ExpandableCards[exampleId][section.guestId] = !ExpandableCards[exampleId][section.guestId];
|
||||
if (ExpandableCards[exampleId][section.guestId] === true) {
|
||||
document.getElementById(sectionId).classList = "api-section";
|
||||
toggleBtn.innerText = "Hide"
|
||||
} else {
|
||||
document.getElementById(sectionId).classList = "hidden";
|
||||
toggleBtn.innerText = "Show"
|
||||
}
|
||||
};
|
||||
|
||||
var headerText = document.createElement('div');
|
||||
headerText.innerHTML = section.headers;
|
||||
|
||||
header.append(headerText, toggleBtn);
|
||||
|
||||
var examples = document.createElement('div');
|
||||
examples.classList = "hidden";
|
||||
examples.id = sectionId;
|
||||
|
||||
if (section.info) {
|
||||
var info = document.createElement("div");
|
||||
info.innerHTML = section?.info;
|
||||
examples.appendChild(info);
|
||||
}
|
||||
|
||||
Object.keys(api).sort().forEach((param) => {
|
||||
var header = document.createElement('h4');
|
||||
header.innerHTML = param;
|
||||
examples.appendChild(header);
|
||||
addExamples(examples, param, api[param]);
|
||||
})
|
||||
|
||||
return { header, examples }
|
||||
}
|
||||
|
||||
function newElement(type, attributes) {
|
||||
var el = document.createElement(type);
|
||||
for (var attr in attributes) {
|
||||
el[attr] = attributes[attr]
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
function addExamples(parentContainer, name, data) {
|
||||
if (data.options) {
|
||||
for (var i = 0; i < data.options.length; i++) {
|
||||
var button = document.createElement("button");
|
||||
|
||||
button.innerHTML = (data.labels !== undefined)
|
||||
? data.labels[i]
|
||||
: JSON.stringify(data.options[i]);
|
||||
|
||||
const result = data.result(data.options[i]); // result needs to be a const, not a var
|
||||
button.onclick = function() { postMessage(result)};
|
||||
parentContainer.appendChild(button)
|
||||
}
|
||||
}
|
||||
|
||||
if (data.input) {
|
||||
var inputWrapper = document.createElement("div");
|
||||
var input = document.createElement("input");
|
||||
|
||||
for (var attr in data.input) {
|
||||
input[attr] = data.input[attr];
|
||||
}
|
||||
if (input.type === 'range') {
|
||||
input.onchange = function() { postMessage(data.result(this.value)) };
|
||||
} else {
|
||||
var button = newElement("button", { innerText: "Send" });
|
||||
button.onclick = function() { postMessage(data.result(input.value)) };
|
||||
inputWrapper.appendChild(button);
|
||||
}
|
||||
|
||||
inputWrapper.prepend(input);
|
||||
parentContainer.appendChild(inputWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
function logOutput(output, config) {
|
||||
var log = document.createElement("div");
|
||||
var outputWindow = config?.window ?? mainLog;
|
||||
|
||||
log.innerHTML = output;
|
||||
if (config?.classList) {
|
||||
log.classList = config?.classList;
|
||||
}
|
||||
|
||||
// Clears the log when a new entry comes in
|
||||
if (config?.replace === true) {
|
||||
outputWindow.innerHTML = ""
|
||||
}
|
||||
|
||||
outputWindow.appendChild(log);
|
||||
outputWindow.scrollTo(0, outputWindow.scrollHeight);
|
||||
}
|
||||
|
||||
function postMessage(message) {
|
||||
iframe.contentWindow.postMessage(message, '*');
|
||||
var out = "Post: " + JSON.stringify(message);
|
||||
logOutput(out, { classList: "post-log" });
|
||||
}
|
||||
}
|
||||
|
||||
function generateId() {
|
||||
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function addUrlParams(_url, paramCheckboxes) {
|
||||
var url = _url !== "" ? _url : "./";
|
||||
|
||||
for(var i = 0; i < paramCheckboxes.length; i++) {
|
||||
var param = paramCheckboxes[i];
|
||||
if (document.getElementById(param).checked){
|
||||
url += url.includes("?") ? '&' : '?';
|
||||
url += param;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<input placeholder="Enter an VDO.Ninja View URL here" id="viewlink" />
|
||||
<button onclick="loadIframe();">ADD</button>
|
||||
<input type="checkbox" id="cleanoutput" >Clean Output
|
||||
<input type="checkbox" id="transparent">Transparent
|
||||
<input type="checkbox" id="hidemenu">Hide Menu
|
||||
<div id="container"></div>
|
||||
</body>
|
||||
</html>
|
||||
1895
index.html
136
install.md
@ -1,47 +1,45 @@
|
||||
Deploying this code. A guide. (INITIAL DRAFT VERSION)
|
||||
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.
|
||||
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; deploying a private TURN server; or running VDO.Ninja without internet on a private LAN.
|
||||
|
||||
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://chromicam.com
|
||||
- https://invite.cam (via URL obfuscation option)
|
||||
- https://ltt.ninja
|
||||
- https://obsn.me
|
||||
- https://rtc.ninja
|
||||
- https://vmix.ninja
|
||||
- https://webrtc.party
|
||||
- https://callin.ninja
|
||||
- https://auxiliary.live (backup hosted)
|
||||
- https://backup.obs.ninja (fully 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.
|
||||
There's also the Github-hosted version, which mirrors the master branch of the code repo. https://steveseguin.github.io/vdo.ninja/ You can use this hosted version or fork VDO.Ninja and host it yourself in the same way via Github Pages; it's free and can be done within minutes. I provide a video guide on how to do this here, https://www.youtube.com/watch?v=uYLKkX2_flY, and it's what I recommend for most users looking to deploy their own version of VDO.Ninja.
|
||||
|
||||
You can also point your domain to the OBS.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.
|
||||
|
||||
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 OBS.Ninja, with URL parameters already included, such as https://invite.mypersonaldomain.com , which might secretly resolve to https://obs.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.
|
||||
|
||||
OBS.Ninja also supports IFRAMES, so you can embed OBS.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 OBS.Ninja quite a bit of flare.
|
||||
See more on IFRAMES here: https://docs.vdo.ninja/guides/iframe-api-documentation
|
||||
|
||||
See more on IFRAMES here: https://github.com/steveseguin/obsninja/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.
|
||||
|
||||
Understanding clearly why you need to deploy any code or server is important. Maintaining updated deployed code can be quite hard, as OBS.Ninja updates frequently, so there are good reasons to consider an IFRAME approach instead. Feature requests there are welcomed.
|
||||
That all aside, please continue for instructions for hosting on your own webserver, turn relay server, and more.
|
||||
|
||||
That all aside, please continue:
|
||||
### Deploy to GitHub pages: the quick and simple method
|
||||
|
||||
### SETUP
|
||||
For a very simple method on how to deploy VDO.Ninja, there's a detailed video guide here: https://www.youtube.com/watch?v=uYLKkX2_flY
|
||||
|
||||
There's a community-created video tutorial on setting up here; https://youtu.be/8sDMwBIlgwE Otherwise, read on.
|
||||
Most users might find the Github Pages deployment option easiest and quickest.
|
||||
|
||||
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. OBS.Ninja REQUIRES a domain name and SSL.
|
||||
### Deplying to a NGINX web server
|
||||
|
||||
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.
|
||||
For advanced users, NGINX might be more appropriate than using Github Pages, so find written directions below. There's also a community-created video tutorial on setting up on AWS + Nginx here; https://youtu.be/8sDMwBIlgwE, but it's not an official install guide.
|
||||
|
||||
Please consider the below directions just loose guidelines; you may need to change things up depending on factors like firewalls, operating system versions, and other factors. This NGINX install guide makes some assumptions that you know the basics of NGINX, running Linux servers, domain name setup, and code deployments. Most users getting stuck do because of the SSL requirement, or because of overly complicated firewall/VPS setups.
|
||||
|
||||
Please note, VDO.Ninja REQUIRES a domain name and SSL, unless you modify all browsers being used to support otherwise. (More on this in the [Internet-free section](#internet-free-deployments) below) As a result, getting VDO.Ninja working can be quite challenging, as setting up domain names and SSL can be tricky for some.
|
||||
|
||||
The following commands will setup NGINX, assuming you are running on a standard Ubuntu server.
|
||||
```
|
||||
sudo apt-get update
|
||||
apt-get install nginx -y
|
||||
@ -49,52 +47,110 @@ 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
|
||||
If you need to download the code for VDO.Ninja, the basic idea is something like this:
|
||||
|
||||
```
|
||||
sudo apt-get install git -y
|
||||
cd /var/www/html
|
||||
git clone https://github.com/steveseguin/vdo.ninja
|
||||
```
|
||||
|
||||
To keep things easy, Steve generally recommends using Cloudflare to provide caching and SSL, but you can google `Certbot` for another free SSL option. The below NGINX config assumes you are using Cloudflare's flexible SSL option, which is the simpliest way to get started. You'll need to also add the VDO.Ninja code to the /var/www/html/vdo.ninja folder (or whatever you set it to) and modify the port/SSL/domain-name settings as needed.
|
||||
|
||||
```server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name obs.ninja;
|
||||
server_name vdo.ninja;
|
||||
|
||||
root /var/www/html/obs.ninja;
|
||||
root /var/www/html/vdo.ninja;
|
||||
index index.html;
|
||||
|
||||
|
||||
location ~ ^/([^/]+)/([^/?]+)$ {
|
||||
root /var/www/html/obs.ninja;
|
||||
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 *;
|
||||
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 OBS.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.
|
||||
|
||||
`sudo systemctl reload nginx` will reload the settings after making changes.
|
||||
|
||||
At this point, if you've managed to make it this far, you should have VDO.Ninja's web code hosted and accessible via yuor domain name.
|
||||
|
||||
You can find many settings for VDO.Ninja at the bottom of the `index.html` file, including settings for specifiying TURN servers and default values.
|
||||
|
||||
|
||||
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 OBS.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.
|
||||
### Deploying your own media relay TURN Server
|
||||
|
||||
As for the TURN relay server, a basic one can run on a single or dual-core computer; 2GB of RAM or more recommended though. It doesn't take much of a server to host a few 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/obsninja/blob/master/turnserver.md
|
||||
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.
|
||||
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, especially if you need a TURN server that has TLS, IPv6 support, or token-based auth support.
|
||||
|
||||
Once you have your TURN server setup, you can update the index.html of the OBS.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.
|
||||
Once you have your TURN server setup, you can update the index.html of the VDO.Ninja code (near the bottom) with your TURN/STUN settings.
|
||||
|
||||
https://github.com/steveseguin/obsninja/blob/df6c147311b9e7d19659ddbb1799d6598f59aa0d/index.html#L644
|
||||
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 OBS.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 OBS.Ninja source code, as heavy custom changes will make updating harder to do. The fewer the changes the better.
|
||||
Also note: There are third-party providers offering TURN services, if you would like a managed third party provider, although they are often quite expensive. Some example code on using Twillio as a TURN provider, with auth logic, can be found in the main.js file.
|
||||
|
||||
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.
|
||||
### Further customization of the website code
|
||||
|
||||
Final note: I haven't provided here instructions on deploying STUN services or a private handshake server; the OBS.Ninja handshake server code is currently not provided, but access to it as a service is freely accessible for private deployments.
|
||||
A new deployment of VDO.Ninja should work without any changes to the index.html file, although you'll want to change it to support your own TURN server and perhaps branding. The VDO.Ninja code needs to be constantly kept up to date though; this is the reality of deploying VDO.Ninja -- you should update it every few months at the very least. As a result, 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 to extensive editing? 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 hosting option, you'll need to deploy your own handshake server. You may not need to deploy a TURN / STUN server if using things just on a LAN.
|
||||
|
||||
I've created an install script, and also am providing a Raspberry Pi image, for those looking for a simple working example of how to do it all.
|
||||
See it here: https://github.com/steveseguin/offline_deployment
|
||||
|
||||
Note, if doing things yourself, since you will be using a private handshake server, don't forget that you'll need to specify that in the VDO.Ninja index.html file.
|
||||
|
||||
Lastly, SSL self-signed certificates will be a haunting issue for those not experienced. See below for a bit of direction on options there.
|
||||
|
||||
#### Dealing with no SSL scenarios
|
||||
|
||||
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 without Internet, you'll need to create and use a private ceriticate that get added to your system's trusted certificate key chain.
|
||||
|
||||
On Mac, you open the Keychain Access and add the cert to the Certificates section, allowing it always. It's a bit more work on PC, but Google is your friend there. There's also plenty of guides on using openssl to create a self-signed certiificates als.
|
||||
|
||||
If you can't figure out how to do private SSL issuance, these SSL restrictions can be somewhat disabled at the browser's command-line or for localhost via the Chrome://flags settings. VDO.Ninja may complain about the lack of security if you take this approach though, but you can edit out those lines of code which trigger those warnings as needed. You may still need to issue an SSL certificate, self-signed or what not, but with these flags enabled it doesn't at least need to be valid.
|
||||
|
||||
Setting it via command line on Windows,
|
||||
```
|
||||
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --ignore-certificate-errors
|
||||
```
|
||||
and on macOS
|
||||
```
|
||||
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --ignore-certificate-errors --ignore-urlfetcher-cert-requests &> /dev/null
|
||||
```
|
||||
and if you intend to only access it as a localhost,
|
||||
```
|
||||
chrome://flags/#allow-insecure-localhost
|
||||
```
|
||||
|
||||
|
||||
### Hand-shake server deployment
|
||||
|
||||
Finally, there is a handshake server hosting option available; advanced users can host their own personal handshake server, which is useful for air-gapped private deployments of the service. Some basic documentation with instructions on setting it up are included here: https://github.com/steveseguin/websocket_server. Just be sure that your SSL-certifcates are valid or that you modify your browser to support invalid SSL certificates, else it will not work. From there, you just need to modify a couple ilnes in the index.html file of VDO.Ninja to configure things.
|
||||
|
||||
Please note that despite how simple the provided handshake server appears, it does work quite well. VDO.Ninja was designed to be as serverless and agnostic as possible, so it will work with 3rd-party signaling services as well, such as piesocket.com, and even certain blockchain networks, IRC, and more (with a bit of added tweaking at course).
|
||||
|
||||
WebRTC is very prickly about security, so if you run into issues with things not working, double check your SSL settings first.
|
||||
|
||||
Regards,
|
||||
Steve
|
||||
|
||||
1
lineawesome/css/line-awesome.min.css
vendored
|
Before Width: | Height: | Size: 906 KiB |