Initial commit
This commit is contained in:
41
node_modules/node-red-contrib-browser-utils/CONTRIBUTING.md
generated
vendored
Normal file
41
node_modules/node-red-contrib-browser-utils/CONTRIBUTING.md
generated
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
# CONTRIBUTING
|
||||
|
||||
## Code
|
||||
|
||||
* The code should follow Watson Developer Cloud [coding guidances](https://github.com/watson-developer-cloud/api-guidelines)
|
||||
* The code should follow: https://github.com/airbnb/javascript
|
||||
* 2 spaces identation
|
||||
* `snake_case`
|
||||
|
||||
## Issues
|
||||
|
||||
If you encounter an issue with using the labs here, you are welcome to submit
|
||||
a [bug report](https://github.com/node-red-contrib-utils/node-red-contrib-media-utils/issues).
|
||||
Before that, please search for similar issues. It's possible somebody has already encountered this issue.
|
||||
|
||||
## Pull Requests
|
||||
|
||||
If you want to contribute to the repository, follow these steps:
|
||||
|
||||
1. Fork the repo.
|
||||
2. Develop code changes.
|
||||
5. Commit your changes.
|
||||
6. Push to your fork and submit a pull request.
|
||||
|
||||
### Getting started
|
||||
|
||||
* Install [Node-RED](http://nodered.org/)
|
||||
* Fork [this repo](https://github.com/node-red-contrib-utils/node-red-contrib-media-utils)
|
||||
* Clone the project
|
||||
* Create an npm link to your forked project
|
||||
* Within the cloned directory, run `npm link`
|
||||
* Within the Node-RED directory, run `npm link node-red-contrib-media-utils`
|
||||
* Run Node-RED
|
||||
|
||||
Now you should have the media nodes installed in your local palette, allowing you to work on your project locally.
|
||||
|
||||
### Modify or Create new nodes
|
||||
|
||||
If you want to add a node, create a folder using the same naming convention.
|
||||
|
||||
Please refer to the [Node-RED](http://nodered.org/docs/creating-nodes/) documentation.
|
||||
37
node_modules/node-red-contrib-browser-utils/README.md
generated
vendored
Normal file
37
node_modules/node-red-contrib-browser-utils/README.md
generated
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# Node-RED Contrib Browser Utils
|
||||
|
||||
[Node-RED](http://nodered.org) nodes for browser functionality such as file upload, camera & microphone.
|
||||
|
||||
## Install
|
||||
|
||||
|
||||
Run the following command in the root directory of your Node-RED install:
|
||||
|
||||
npm install node-red-contrib-browser-utils
|
||||
|
||||
## Usage
|
||||
|
||||
### Camera
|
||||
|
||||
The browser takes a picture with the default camera when the button next to the node is clicked. The node outputs it as a PNG buffer.
|
||||
|
||||
The `camera` node has a 2000ms delay to prevent slow camera driver startup causing an issue in some browsers.
|
||||
|
||||
|
||||
### Microphone
|
||||
|
||||
The browser starts recording after the button next to the node is clicked and stops it when the button is clicked again. The node outputs it as a WAV buffer.
|
||||
|
||||
|
||||
### File upload
|
||||
|
||||
The node accepts a file to be uploaded and outputs it as a buffer.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
For simple typos and fixes please just raise an issue pointing out our mistakes. If you need to raise a pull request please read our [contribution guidelines](https://github.com/node-red-contrib-utils/node-red-contrib-browser-utils/blob/master/CONTRIBUTING.md) before doing so.
|
||||
|
||||
## Copyright and license
|
||||
|
||||
Copyright 2014, 2016, 2019 IBM Corp. under the Apache 2.0 license.
|
||||
152
node_modules/node-red-contrib-browser-utils/camera/camera.html
generated
vendored
Normal file
152
node_modules/node-red-contrib-browser-utils/camera/camera.html
generated
vendored
Normal file
@@ -0,0 +1,152 @@
|
||||
<!--
|
||||
Copyright 2013, 2016, 2019 IBM Corp.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script type="text/x-red" data-template-name="camera">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Camera">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="camera">
|
||||
<p>A simple camera node to capture the current image from the device webcam</p>
|
||||
<p>Usage:</p>
|
||||
<ol>
|
||||
<li>Add the camera node to your flow</li>
|
||||
<li>Click the <code>button</code> capture an image from the webcam</li>
|
||||
</ol>
|
||||
<p>The <code>png</code> image is sent as the <code>msg.payload</code> object</p>
|
||||
<p>Supported browsers</p>
|
||||
<ul>
|
||||
<li>Chrome</li>
|
||||
<li>Firefox</li>
|
||||
</ul>
|
||||
</script>
|
||||
|
||||
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
|
||||
window.URL = window.URL || window.webkitURL || window.mozURL || window.msURL;
|
||||
|
||||
window.video = document.createElement('video')
|
||||
window.canvas = document.createElement('canvas')
|
||||
canvas.style.display = 'none'
|
||||
canvas.style.display = 'none'
|
||||
var id
|
||||
|
||||
function onCanPlay(evt) {
|
||||
video.removeEventListener('canplay', onCanPlay, false)
|
||||
var width = 320
|
||||
var height = video.videoHeight / (video.videoWidth / width)
|
||||
|
||||
if (isNaN(height)) {
|
||||
height = width / (4 / 3)
|
||||
}
|
||||
|
||||
setTimeout(function(evt) {
|
||||
|
||||
video.setAttribute('width', width)
|
||||
video.setAttribute('height', height)
|
||||
canvas.setAttribute('width', width)
|
||||
canvas.setAttribute('height', height)
|
||||
|
||||
var ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(video, 0, 0, width, height)
|
||||
|
||||
canvas.toBlob(function(blob) {
|
||||
uploadRecord(id, blob)
|
||||
})
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
function onStreamReady(id, stream) {
|
||||
// As of Chrome 71 createObjectURL is dropped so try direct
|
||||
// access first
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/srcObject
|
||||
try {
|
||||
video.srcObject = stream;
|
||||
} catch (error) {
|
||||
video.src = window.URL.createObjectURL(stream)
|
||||
}
|
||||
|
||||
video.addEventListener('canplay', onCanPlay, false)
|
||||
|
||||
video.play()
|
||||
}
|
||||
|
||||
function onVideoPlaying(id, evt) {
|
||||
canvas.style.height = video.clientHeight + 'px'
|
||||
canvas.style.width = video.clientWidth + 'px'
|
||||
|
||||
var ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(video, 0, 0)
|
||||
|
||||
canvas.toBlob(function(blob) {
|
||||
uploadRecord(id, blob)
|
||||
})
|
||||
}
|
||||
|
||||
function setCameraStatus(record, id) {
|
||||
$.getJSON('node-red-camera/status', {status: record, id: id})
|
||||
.done(function () {})
|
||||
.fail(function (err) {
|
||||
console.log(err);
|
||||
})
|
||||
.always(function () {});
|
||||
}
|
||||
|
||||
function takeSnapshot(id) {
|
||||
const constraints = {video : true, audio : false};
|
||||
navigator.mediaDevices.getUserMedia(constraints)
|
||||
.then(function(stream) {
|
||||
setCameraStatus(true, id)
|
||||
onStreamReady(id, stream)
|
||||
})
|
||||
.catch(function(err) {
|
||||
window.alert('Your browser does not support the camera node')
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
function uploadRecord(id, blob) {
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', 'node-red-camera/' + id, true)
|
||||
xhr.send(blob)
|
||||
}
|
||||
|
||||
RED.nodes.registerType('camera', {
|
||||
category: 'input',
|
||||
defaults: {
|
||||
name: {value: ''}
|
||||
},
|
||||
color: 'rgb(215, 201, 194)',
|
||||
inputs: 0,
|
||||
outputs: 1,
|
||||
icon: 'camera.png',
|
||||
paletteLabel: 'camera',
|
||||
label: function() {
|
||||
return this.name || 'camera';
|
||||
},
|
||||
labelStyle: function() {
|
||||
return this.name ? 'node_label_italic' : '';
|
||||
},
|
||||
button: {
|
||||
onclick: function(){
|
||||
id = this.id
|
||||
takeSnapshot(id)
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
66
node_modules/node-red-contrib-browser-utils/camera/camera.js
generated
vendored
Normal file
66
node_modules/node-red-contrib-browser-utils/camera/camera.js
generated
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright 2013, 2016 IBM Corp.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
var bodyParser = require('body-parser')
|
||||
|
||||
module.exports = function (RED) {
|
||||
|
||||
function Node (config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
var node = this;
|
||||
var requestSize = '50mb';
|
||||
|
||||
|
||||
RED.httpAdmin.get('/node-red-camera/status', function (req, res) {
|
||||
var n = RED.nodes.getNode(req.query.id)
|
||||
var status = {};
|
||||
if ('true' == req.query.status) {
|
||||
status = {fill:'red', shape:'dot', text:'taking picture...'}
|
||||
}
|
||||
if (n) {
|
||||
n.status(status);
|
||||
}
|
||||
res.json({});
|
||||
});
|
||||
|
||||
|
||||
RED.httpAdmin.post('/node-red-camera/:id', bodyParser.raw({ type: '*/*', limit: requestSize }), function(req,res) {
|
||||
|
||||
var node = RED.nodes.getNode(req.params.id)
|
||||
|
||||
if (node != null) {
|
||||
try {
|
||||
node.receive({payload: req.body})
|
||||
node.status({})
|
||||
res.sendStatus(200)
|
||||
} catch(err) {
|
||||
node.status({fill:'red', shape:'dot', text:'upload failed'});
|
||||
res.sendStatus(500)
|
||||
node.error(RED._("upload-camera.failed", { error: err.toString() }))
|
||||
}
|
||||
} else {
|
||||
res.status(404).send("no node found")
|
||||
}
|
||||
})
|
||||
|
||||
this.on('input', function (msg) {
|
||||
if(msg.payload !== '') {
|
||||
node.send(msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
RED.nodes.registerType('camera', Node)
|
||||
};
|
||||
BIN
node_modules/node-red-contrib-browser-utils/camera/icons/camera.png
generated
vendored
Executable file
BIN
node_modules/node-red-contrib-browser-utils/camera/icons/camera.png
generated
vendored
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
158
node_modules/node-red-contrib-browser-utils/fileinject/fileinject.html
generated
vendored
Normal file
158
node_modules/node-red-contrib-browser-utils/fileinject/fileinject.html
generated
vendored
Normal file
@@ -0,0 +1,158 @@
|
||||
<!--
|
||||
Copyright 2013, 2016, 2018, 2021 IBM Corp.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script type="text/x-red" data-template-name="fileinject">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="File Inject">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="fileinject">
|
||||
<p>Simple file inject node</p>
|
||||
<p>Press the button to inject a file directly to the flow.</p>
|
||||
<p>File will be sent as part of the <code>msg.payload</code> object.</p>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
|
||||
var id
|
||||
var inputElement = document.createElement('input')
|
||||
inputElement.setAttribute('type', 'file')
|
||||
inputElement.addEventListener('change', injectFile)
|
||||
document.body.appendChild(inputElement)
|
||||
|
||||
function simulate(element, eventName){
|
||||
var options = extend(defaultOptions, arguments[2] || {});
|
||||
var oEvent, eventType = null;
|
||||
|
||||
for (var name in eventMatchers){
|
||||
if (eventMatchers[name].test(eventName)) { eventType = name; break; }
|
||||
}
|
||||
|
||||
if (!eventType)
|
||||
throw new SyntaxError('Only HTMLEvents and MouseEvents interfaces are supported');
|
||||
|
||||
if (document.createEvent){
|
||||
oEvent = document.createEvent(eventType);
|
||||
if (eventType == 'HTMLEvents'){
|
||||
oEvent.initEvent(eventName, options.bubbles, options.cancelable);
|
||||
}
|
||||
else{
|
||||
oEvent.initMouseEvent(eventName, options.bubbles, options.cancelable, document.defaultView,
|
||||
options.button, options.pointerX, options.pointerY, options.pointerX, options.pointerY,
|
||||
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, element);
|
||||
}
|
||||
element.dispatchEvent(oEvent);
|
||||
}
|
||||
else{
|
||||
options.clientX = options.pointerX;
|
||||
options.clientY = options.pointerY;
|
||||
var evt = document.createEventObject();
|
||||
oEvent = extend(evt, options);
|
||||
element.fireEvent('on' + eventName, oEvent);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function extend(destination, source) {
|
||||
for (var property in source)
|
||||
destination[property] = source[property];
|
||||
return destination;
|
||||
}
|
||||
|
||||
var eventMatchers = {
|
||||
'HTMLEvents': /^(?:load|unload|abort|error|select|change|submit|reset|focus|blur|resize|scroll)$/,
|
||||
'MouseEvents': /^(?:click|dblclick|mouse(?:down|up|over|move|out))$/
|
||||
}
|
||||
var defaultOptions = {
|
||||
pointerX: 0,
|
||||
pointerY: 0,
|
||||
button: 0,
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
metaKey: false,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
}
|
||||
|
||||
function triggerClick(event) {
|
||||
simulate(inputElement, 'click')
|
||||
}
|
||||
|
||||
function setNodeStatus(uploading, id) {
|
||||
$.getJSON('node-red-fileinject/status', {status: uploading, id: id})
|
||||
.done(function () {})
|
||||
.fail(function (err) {
|
||||
console.log(err);
|
||||
})
|
||||
.always(function () {});
|
||||
}
|
||||
|
||||
function injectFile() {
|
||||
var blob;
|
||||
var currentInput = this
|
||||
|
||||
if(currentInput.files.length !== 0) {
|
||||
var file = currentInput.files[0]
|
||||
var reader = new FileReader()
|
||||
|
||||
setNodeStatus(true, id)
|
||||
|
||||
reader.addEventListener('load', function(onLoadEvent) {
|
||||
var filename = file.name
|
||||
blob = new Blob([onLoadEvent.target.result], {
|
||||
type: (file.type=='')?'application/octet-stream':file.type
|
||||
});
|
||||
|
||||
currentInput.value = ''
|
||||
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', 'node-red-fileinject/' + id, true)
|
||||
xhr.setRequestHeader('x-filename', filename);
|
||||
xhr.send(blob)
|
||||
})
|
||||
|
||||
reader.readAsArrayBuffer(file)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
RED.nodes.registerType('fileinject', {
|
||||
category: 'input',
|
||||
defaults: {
|
||||
name: {value: ''}
|
||||
},
|
||||
color: 'rgb(254, 245, 136)',
|
||||
inputs: 0,
|
||||
outputs: 1,
|
||||
icon: 'fileinject.png',
|
||||
paletteLabel: 'file inject',
|
||||
label: function() {
|
||||
return this.name || 'file inject';
|
||||
},
|
||||
labelStyle: function() {
|
||||
return this.name ? 'node_label_italic' : '';
|
||||
},
|
||||
button: {
|
||||
onclick: function(evt){
|
||||
id = this.id
|
||||
triggerClick(evt)
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
64
node_modules/node-red-contrib-browser-utils/fileinject/fileinject.js
generated
vendored
Normal file
64
node_modules/node-red-contrib-browser-utils/fileinject/fileinject.js
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright 2013, 2016, 2018, 2021 IBM Corp.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
var bodyParser = require('body-parser')
|
||||
|
||||
module.exports = function (RED) {
|
||||
var requestSize = '50mb'
|
||||
|
||||
function Node (config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
var node = this;
|
||||
|
||||
RED.httpAdmin.get('/node-red-fileinject/status', function (req, res) {
|
||||
var n = RED.nodes.getNode(req.query.id)
|
||||
var status = {};
|
||||
if ('true' == req.query.status) {
|
||||
status = {fill:'red', shape:'dot', text:'sending file...'}
|
||||
}
|
||||
if (n) {
|
||||
n.status(status);
|
||||
}
|
||||
res.json({});
|
||||
});
|
||||
|
||||
|
||||
RED.httpAdmin.post('/node-red-fileinject/:id', bodyParser.raw({ type: '*/*', limit: requestSize }), function(req,res) {
|
||||
|
||||
var node = RED.nodes.getNode(req.params.id)
|
||||
|
||||
if (node != null) {
|
||||
try {
|
||||
node.receive({payload: req.body, filename: req.headers["x-filename"], mimetype: req.headers["content-type"]})
|
||||
node.status({})
|
||||
res.sendStatus(200)
|
||||
} catch(err) {
|
||||
res.sendStatus(500)
|
||||
node.error(RED._("inject-file.failed", { error: err.toString() }))
|
||||
}
|
||||
} else {
|
||||
res.status(404).send("no node found")
|
||||
}
|
||||
})
|
||||
|
||||
this.on('input', function (msg) {
|
||||
if(msg.payload !== '') {
|
||||
node.send(msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
RED.nodes.registerType('fileinject', Node)
|
||||
};
|
||||
BIN
node_modules/node-red-contrib-browser-utils/fileinject/icons/fileinject.png
generated
vendored
Executable file
BIN
node_modules/node-red-contrib-browser-utils/fileinject/icons/fileinject.png
generated
vendored
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
node_modules/node-red-contrib-browser-utils/microphone/icons/microphone.png
generated
vendored
Executable file
BIN
node_modules/node-red-contrib-browser-utils/microphone/icons/microphone.png
generated
vendored
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
176
node_modules/node-red-contrib-browser-utils/microphone/microphone.html
generated
vendored
Normal file
176
node_modules/node-red-contrib-browser-utils/microphone/microphone.html
generated
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
<!--
|
||||
Copyright 2013, 2016, 2018, 2019 IBM Corp.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script type="text/x-red" data-template-name="microphone">
|
||||
<div class="form-row">
|
||||
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||
<input type="text" id="node-input-name" placeholder="Microphone">
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="microphone">
|
||||
<p>A simple microphone node to record audio directly from the web browser</p>
|
||||
<p>Usage:</p>
|
||||
<ol>
|
||||
<li>Note - you must be using <code>https</code> to record</li>
|
||||
<li>Click the <code>button</code> to start recording</li>
|
||||
<li>Click the <code>button</code> again to stop recording</li>
|
||||
</ol>
|
||||
<p>The recorded audio is stored in the nodes <code>msg.payload</code> object</p>
|
||||
<p>To see if the microphone node is supported in your browser check <a href="http://caniuse.com/#feat=stream" target="_blank">this</a> page</p>
|
||||
<p>Supported output formats:</p>
|
||||
<ul>
|
||||
<li>WAV</li>
|
||||
</ul>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
|
||||
window.AudioContext = window.AudioContext || window.webkitAudioContext || window.mozAudioContext || window.oAudioContext || window.msAudioContext;;
|
||||
|
||||
window.URL = window.URL || window.webkitURL || window.mozURL || window.msURL;
|
||||
|
||||
const MICSTATUS = {
|
||||
OFF : '1',
|
||||
ON : '2',
|
||||
CONTEXTERROR : '4'
|
||||
}
|
||||
|
||||
$.getScript( 'microphone/js/recorder.js' )
|
||||
.done(function( script, textStatus ) {
|
||||
// script loaded, do nothing
|
||||
})
|
||||
.fail(function( jqxhr, settings, exception ) {
|
||||
console.log('FAILED to load recorder script: '+exception)
|
||||
});
|
||||
|
||||
var instances = {}
|
||||
var audio_context = new AudioContext
|
||||
|
||||
function checkAudioContextState() {
|
||||
var p = new Promise(function resolver(resolve, reject) {
|
||||
if (audio_context.state && 'running' !== audio_context.state && audio_context.resume) {
|
||||
audio_context.resume().then(() => {
|
||||
resolve()
|
||||
})
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
});
|
||||
return p
|
||||
}
|
||||
|
||||
function startRecording(id, stream) {
|
||||
checkAudioContextState()
|
||||
.then(() => {
|
||||
var input = audio_context.createMediaStreamSource(stream)
|
||||
instances[id] = new Recorder(input)
|
||||
instances[id] && instances[id].record()
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('Audio Context Error')
|
||||
setMicStatus(MICSTATUS.CONTEXTERROR, id)
|
||||
})
|
||||
}
|
||||
|
||||
function stopRecording(id, callback) {
|
||||
if(!instances[id]) {
|
||||
return
|
||||
}
|
||||
|
||||
instances[id].stop()
|
||||
|
||||
instances[id].exportWAV(function(blob) {
|
||||
instances[id].clear()
|
||||
instances[id] = null
|
||||
|
||||
callback & callback(id, blob)
|
||||
})
|
||||
|
||||
setMicStatus(MICSTATUS.OFF, id);
|
||||
}
|
||||
|
||||
function getRecorder(id) {
|
||||
const constraints = {video : false, audio : true};
|
||||
navigator.mediaDevices.getUserMedia(constraints)
|
||||
.then(function(stream) {
|
||||
setMicStatus(MICSTATUS.ON, id)
|
||||
startRecording(id, stream)
|
||||
})
|
||||
.catch(function(err) {
|
||||
window.alert('Your browser does not support audio recording')
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function getRecorderXX(id) {
|
||||
if(!navigator.getUserMedia) {
|
||||
window.alert('Your browser does not support audio recording')
|
||||
return
|
||||
}
|
||||
|
||||
navigator.getUserMedia({audio: true},
|
||||
function(stream){
|
||||
setMicStatus(MICSTATUS.ON, id)
|
||||
startRecording(id, stream)
|
||||
},
|
||||
function(e) {
|
||||
window.alert(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
function uploadRecord(id, blob) {
|
||||
var xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', 'node-red-microphone/' + id, true)
|
||||
xhr.send(blob)
|
||||
}
|
||||
|
||||
function setMicStatus(record, id) {
|
||||
$.getJSON('node-red-microphone/status', {status: record, id: id})
|
||||
.done(function () {})
|
||||
.fail(function (err) {
|
||||
console.log(err);
|
||||
})
|
||||
.always(function () {});
|
||||
}
|
||||
|
||||
RED.nodes.registerType('microphone', {
|
||||
category: 'input',
|
||||
defaults: {
|
||||
name: {value: ''}
|
||||
},
|
||||
color: 'rgb(204, 230, 171)',
|
||||
inputs: 0,
|
||||
outputs: 1,
|
||||
icon: 'microphone.png',
|
||||
paletteLabel: 'microphone',
|
||||
label: function() {
|
||||
return this.name || 'microphone';
|
||||
},
|
||||
labelStyle: function() {
|
||||
return this.name ? 'node_label_italic' : '';
|
||||
},
|
||||
button: {
|
||||
onclick: function(){
|
||||
if(instances[this.id]) {
|
||||
stopRecording(this.id, uploadRecord)
|
||||
} else {
|
||||
getRecorder(this.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
84
node_modules/node-red-contrib-browser-utils/microphone/microphone.js
generated
vendored
Normal file
84
node_modules/node-red-contrib-browser-utils/microphone/microphone.js
generated
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright 2013, 2016 IBM Corp.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
var bodyParser = require('body-parser')
|
||||
|
||||
module.exports = function (RED) {
|
||||
var requestSize = '50mb'
|
||||
var requestType = 'audio/wav'
|
||||
|
||||
const MICSTATUS = {
|
||||
OFF : '1',
|
||||
ON : '2',
|
||||
CONTEXTERROR : '4'
|
||||
}
|
||||
|
||||
function Node (config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
var node = this;
|
||||
|
||||
RED.httpAdmin.get('/node-red-microphone/status', function (req, res) {
|
||||
var n = RED.nodes.getNode(req.query.id)
|
||||
var status = {};
|
||||
switch(req.query.status) {
|
||||
case MICSTATUS.ON :
|
||||
status = {fill:'red', shape:'dot', text:'recording...'}
|
||||
break;
|
||||
case MICSTATUS.CONTEXTERROR :
|
||||
status = {fill:'red', shape:'dot', text:'error resuming audio context'}
|
||||
break;
|
||||
}
|
||||
if (n) {
|
||||
n.status(status);
|
||||
}
|
||||
res.json({});
|
||||
});
|
||||
|
||||
RED.httpAdmin.post('/node-red-microphone/:id', bodyParser.raw({ type: requestType, limit: requestSize }), function(req,res) {
|
||||
|
||||
var node = RED.nodes.getNode(req.params.id)
|
||||
|
||||
if (node != null) {
|
||||
try {
|
||||
node.receive({payload: req.body})
|
||||
res.sendStatus(200)
|
||||
} catch(err) {
|
||||
node.status({fill:'red', shape:'dot', text:'upload failed'});
|
||||
res.sendStatus(500)
|
||||
node.error(RED._("upload-microphone.failed", { error: err.toString() }))
|
||||
}
|
||||
} else {
|
||||
res.status(404).send("no node found")
|
||||
}
|
||||
})
|
||||
|
||||
this.on('input', function (msg) {
|
||||
if(msg.payload !== '') {
|
||||
node.send(msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
RED.nodes.registerType('microphone', Node)
|
||||
|
||||
RED.httpAdmin.get('/microphone/js/*', function(req, res){
|
||||
var options = {
|
||||
root: __dirname + '/static/',
|
||||
dotfiles: 'deny'
|
||||
};
|
||||
|
||||
res.sendFile(req.params[0], options);
|
||||
});
|
||||
};
|
||||
357
node_modules/node-red-contrib-browser-utils/microphone/static/recorder.js
generated
vendored
Normal file
357
node_modules/node-red-contrib-browser-utils/microphone/static/recorder.js
generated
vendored
Normal file
@@ -0,0 +1,357 @@
|
||||
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Recorder = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||
"use strict";
|
||||
|
||||
module.exports = require("./recorder").Recorder;
|
||||
|
||||
},{"./recorder":2}],2:[function(require,module,exports){
|
||||
'use strict';
|
||||
|
||||
var _createClass = (function () {
|
||||
function defineProperties(target, props) {
|
||||
for (var i = 0; i < props.length; i++) {
|
||||
var descriptor = props[i];descriptor.enumerable = descriptor.enumerable || false;descriptor.configurable = true;if ("value" in descriptor) descriptor.writable = true;Object.defineProperty(target, descriptor.key, descriptor);
|
||||
}
|
||||
}return function (Constructor, protoProps, staticProps) {
|
||||
if (protoProps) defineProperties(Constructor.prototype, protoProps);if (staticProps) defineProperties(Constructor, staticProps);return Constructor;
|
||||
};
|
||||
})();
|
||||
|
||||
Object.defineProperty(exports, "__esModule", {
|
||||
value: true
|
||||
});
|
||||
exports.Recorder = undefined;
|
||||
|
||||
var _inlineWorker = require('inline-worker');
|
||||
|
||||
var _inlineWorker2 = _interopRequireDefault(_inlineWorker);
|
||||
|
||||
function _interopRequireDefault(obj) {
|
||||
return obj && obj.__esModule ? obj : { default: obj };
|
||||
}
|
||||
|
||||
function _classCallCheck(instance, Constructor) {
|
||||
if (!(instance instanceof Constructor)) {
|
||||
throw new TypeError("Cannot call a class as a function");
|
||||
}
|
||||
}
|
||||
|
||||
var Recorder = exports.Recorder = (function () {
|
||||
function Recorder(source, cfg) {
|
||||
var _this = this;
|
||||
|
||||
_classCallCheck(this, Recorder);
|
||||
|
||||
this.config = {
|
||||
bufferLen: 4096,
|
||||
numChannels: 2,
|
||||
mimeType: 'audio/wav'
|
||||
};
|
||||
this.recording = false;
|
||||
this.callbacks = {
|
||||
getBuffer: [],
|
||||
exportWAV: []
|
||||
};
|
||||
|
||||
Object.assign(this.config, cfg);
|
||||
this.context = source.context;
|
||||
this.node = (this.context.createScriptProcessor || this.context.createJavaScriptNode).call(this.context, this.config.bufferLen, this.config.numChannels, this.config.numChannels);
|
||||
|
||||
this.node.onaudioprocess = function (e) {
|
||||
if (!_this.recording) return;
|
||||
|
||||
var buffer = [];
|
||||
for (var channel = 0; channel < _this.config.numChannels; channel++) {
|
||||
buffer.push(e.inputBuffer.getChannelData(channel));
|
||||
}
|
||||
_this.worker.postMessage({
|
||||
command: 'record',
|
||||
buffer: buffer
|
||||
});
|
||||
};
|
||||
|
||||
source.connect(this.node);
|
||||
this.node.connect(this.context.destination); //this should not be necessary
|
||||
|
||||
var self = {};
|
||||
this.worker = new _inlineWorker2.default(function () {
|
||||
var recLength = 0,
|
||||
recBuffers = [],
|
||||
sampleRate = undefined,
|
||||
numChannels = undefined;
|
||||
|
||||
self.onmessage = function (e) {
|
||||
switch (e.data.command) {
|
||||
case 'init':
|
||||
init(e.data.config);
|
||||
break;
|
||||
case 'record':
|
||||
record(e.data.buffer);
|
||||
break;
|
||||
case 'exportWAV':
|
||||
exportWAV(e.data.type);
|
||||
break;
|
||||
case 'getBuffer':
|
||||
getBuffer();
|
||||
break;
|
||||
case 'clear':
|
||||
clear();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
function init(config) {
|
||||
sampleRate = config.sampleRate;
|
||||
numChannels = config.numChannels;
|
||||
initBuffers();
|
||||
}
|
||||
|
||||
function record(inputBuffer) {
|
||||
for (var channel = 0; channel < numChannels; channel++) {
|
||||
recBuffers[channel].push(inputBuffer[channel]);
|
||||
}
|
||||
recLength += inputBuffer[0].length;
|
||||
}
|
||||
|
||||
function exportWAV(type) {
|
||||
var buffers = [];
|
||||
for (var channel = 0; channel < numChannels; channel++) {
|
||||
buffers.push(mergeBuffers(recBuffers[channel], recLength));
|
||||
}
|
||||
var interleaved = undefined;
|
||||
if (numChannels === 2) {
|
||||
interleaved = interleave(buffers[0], buffers[1]);
|
||||
} else {
|
||||
interleaved = buffers[0];
|
||||
}
|
||||
var dataview = encodeWAV(interleaved);
|
||||
var audioBlob = new Blob([dataview], { type: type });
|
||||
|
||||
self.postMessage({ command: 'exportWAV', data: audioBlob });
|
||||
}
|
||||
|
||||
function getBuffer() {
|
||||
var buffers = [];
|
||||
for (var channel = 0; channel < numChannels; channel++) {
|
||||
buffers.push(mergeBuffers(recBuffers[channel], recLength));
|
||||
}
|
||||
self.postMessage({ command: 'getBuffer', data: buffers });
|
||||
}
|
||||
|
||||
function clear() {
|
||||
recLength = 0;
|
||||
recBuffers = [];
|
||||
initBuffers();
|
||||
}
|
||||
|
||||
function initBuffers() {
|
||||
for (var channel = 0; channel < numChannels; channel++) {
|
||||
recBuffers[channel] = [];
|
||||
}
|
||||
}
|
||||
|
||||
function mergeBuffers(recBuffers, recLength) {
|
||||
var result = new Float32Array(recLength);
|
||||
var offset = 0;
|
||||
for (var i = 0; i < recBuffers.length; i++) {
|
||||
result.set(recBuffers[i], offset);
|
||||
offset += recBuffers[i].length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function interleave(inputL, inputR) {
|
||||
var length = inputL.length + inputR.length;
|
||||
var result = new Float32Array(length);
|
||||
|
||||
var index = 0,
|
||||
inputIndex = 0;
|
||||
|
||||
while (index < length) {
|
||||
result[index++] = inputL[inputIndex];
|
||||
result[index++] = inputR[inputIndex];
|
||||
inputIndex++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function floatTo16BitPCM(output, offset, input) {
|
||||
for (var i = 0; i < input.length; i++, offset += 2) {
|
||||
var s = Math.max(-1, Math.min(1, input[i]));
|
||||
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
||||
}
|
||||
}
|
||||
|
||||
function writeString(view, offset, string) {
|
||||
for (var i = 0; i < string.length; i++) {
|
||||
view.setUint8(offset + i, string.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
function encodeWAV(samples) {
|
||||
var buffer = new ArrayBuffer(44 + samples.length * 2);
|
||||
var view = new DataView(buffer);
|
||||
|
||||
/* RIFF identifier */
|
||||
writeString(view, 0, 'RIFF');
|
||||
/* RIFF chunk length */
|
||||
view.setUint32(4, 36 + samples.length * 2, true);
|
||||
/* RIFF type */
|
||||
writeString(view, 8, 'WAVE');
|
||||
/* format chunk identifier */
|
||||
writeString(view, 12, 'fmt ');
|
||||
/* format chunk length */
|
||||
view.setUint32(16, 16, true);
|
||||
/* sample format (raw) */
|
||||
view.setUint16(20, 1, true);
|
||||
/* channel count */
|
||||
view.setUint16(22, numChannels, true);
|
||||
/* sample rate */
|
||||
view.setUint32(24, sampleRate, true);
|
||||
/* byte rate (sample rate * block align) */
|
||||
view.setUint32(28, sampleRate * 4, true);
|
||||
/* block align (channel count * bytes per sample) */
|
||||
view.setUint16(32, numChannels * 2, true);
|
||||
/* bits per sample */
|
||||
view.setUint16(34, 16, true);
|
||||
/* data chunk identifier */
|
||||
writeString(view, 36, 'data');
|
||||
/* data chunk length */
|
||||
view.setUint32(40, samples.length * 2, true);
|
||||
|
||||
floatTo16BitPCM(view, 44, samples);
|
||||
|
||||
return view;
|
||||
}
|
||||
}, self);
|
||||
|
||||
this.worker.postMessage({
|
||||
command: 'init',
|
||||
config: {
|
||||
sampleRate: this.context.sampleRate,
|
||||
numChannels: this.config.numChannels
|
||||
}
|
||||
});
|
||||
|
||||
this.worker.onmessage = function (e) {
|
||||
var cb = _this.callbacks[e.data.command].pop();
|
||||
if (typeof cb == 'function') {
|
||||
cb(e.data.data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_createClass(Recorder, [{
|
||||
key: 'record',
|
||||
value: function record() {
|
||||
this.recording = true;
|
||||
}
|
||||
}, {
|
||||
key: 'stop',
|
||||
value: function stop() {
|
||||
this.recording = false;
|
||||
}
|
||||
}, {
|
||||
key: 'clear',
|
||||
value: function clear() {
|
||||
this.worker.postMessage({ command: 'clear' });
|
||||
}
|
||||
}, {
|
||||
key: 'getBuffer',
|
||||
value: function getBuffer(cb) {
|
||||
cb = cb || this.config.callback;
|
||||
if (!cb) throw new Error('Callback not set');
|
||||
|
||||
this.callbacks.getBuffer.push(cb);
|
||||
|
||||
this.worker.postMessage({ command: 'getBuffer' });
|
||||
}
|
||||
}, {
|
||||
key: 'exportWAV',
|
||||
value: function exportWAV(cb, mimeType) {
|
||||
mimeType = mimeType || this.config.mimeType;
|
||||
cb = cb || this.config.callback;
|
||||
if (!cb) throw new Error('Callback not set');
|
||||
|
||||
this.callbacks.exportWAV.push(cb);
|
||||
|
||||
this.worker.postMessage({
|
||||
command: 'exportWAV',
|
||||
type: mimeType
|
||||
});
|
||||
}
|
||||
}], [{
|
||||
key: 'forceDownload',
|
||||
value: function forceDownload(blob, filename) {
|
||||
var url = (window.URL || window.webkitURL).createObjectURL(blob);
|
||||
var link = window.document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename || 'output.wav';
|
||||
var click = document.createEvent("Event");
|
||||
click.initEvent("click", true, true);
|
||||
link.dispatchEvent(click);
|
||||
}
|
||||
}]);
|
||||
|
||||
return Recorder;
|
||||
})();
|
||||
|
||||
exports.default = Recorder;
|
||||
|
||||
},{"inline-worker":3}],3:[function(require,module,exports){
|
||||
"use strict";
|
||||
|
||||
module.exports = require("./inline-worker");
|
||||
},{"./inline-worker":4}],4:[function(require,module,exports){
|
||||
(function (global){
|
||||
"use strict";
|
||||
|
||||
var _createClass = (function () { function defineProperties(target, props) { for (var key in props) { var prop = props[key]; prop.configurable = true; if (prop.value) prop.writable = true; } Object.defineProperties(target, props); } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
|
||||
|
||||
var _classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } };
|
||||
|
||||
var WORKER_ENABLED = !!(global === global.window && global.URL && global.Blob && global.Worker);
|
||||
|
||||
var InlineWorker = (function () {
|
||||
function InlineWorker(func, self) {
|
||||
var _this = this;
|
||||
|
||||
_classCallCheck(this, InlineWorker);
|
||||
|
||||
if (WORKER_ENABLED) {
|
||||
var functionBody = func.toString().trim().match(/^function\s*\w*\s*\([\w\s,]*\)\s*{([\w\W]*?)}$/)[1];
|
||||
var url = global.URL.createObjectURL(new global.Blob([functionBody], { type: "text/javascript" }));
|
||||
|
||||
return new global.Worker(url);
|
||||
}
|
||||
|
||||
this.self = self;
|
||||
this.self.postMessage = function (data) {
|
||||
setTimeout(function () {
|
||||
_this.onmessage({ data: data });
|
||||
}, 0);
|
||||
};
|
||||
|
||||
setTimeout(function () {
|
||||
func.call(self);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
_createClass(InlineWorker, {
|
||||
postMessage: {
|
||||
value: function postMessage(data) {
|
||||
var _this = this;
|
||||
|
||||
setTimeout(function () {
|
||||
_this.self.onmessage({ data: data });
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return InlineWorker;
|
||||
})();
|
||||
|
||||
module.exports = InlineWorker;
|
||||
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
||||
},{}]},{},[1])(1)
|
||||
});
|
||||
51
node_modules/node-red-contrib-browser-utils/package.json
generated
vendored
Normal file
51
node_modules/node-red-contrib-browser-utils/package.json
generated
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "node-red-contrib-browser-utils",
|
||||
"version": "0.0.11",
|
||||
"description": "A collection of Node-RED nodes for browser interaction",
|
||||
"dependencies": {
|
||||
"node-red-contrib-play-audio": "^2.5.0",
|
||||
"body-parser": "^1.20.0"
|
||||
},
|
||||
"license": "Apache-2.0",
|
||||
"keywords": [
|
||||
"node-red",
|
||||
"microphone",
|
||||
"camera",
|
||||
"upload",
|
||||
"bluemix",
|
||||
"watson"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ibm-early-programs/node-red-contrib-browser-utils"
|
||||
},
|
||||
"contributors": [
|
||||
{
|
||||
"name": "Chris Parsons",
|
||||
"email": "christopherkparsons@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Yacine Rezgui",
|
||||
"email": "rezgui.y@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Gibson Fahnestock",
|
||||
"email": "gibfahn@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Anna Thomas",
|
||||
"email": "annat21@live.co.uk"
|
||||
},
|
||||
{
|
||||
"name": "Soheel Chughtai",
|
||||
"email": "soheel_chughtai@uk.ibm.com"
|
||||
}
|
||||
],
|
||||
"node-red": {
|
||||
"nodes": {
|
||||
"fileinject": "./fileinject/fileinject.js",
|
||||
"microphone": "./microphone/microphone.js",
|
||||
"camera": "./camera/camera.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user