├── .babelrc
├── .gitignore
├── README.md
├── examples
└── animated-toast.js
├── index.js
├── lib
├── client
│ ├── overlay-client.jsx
│ ├── overlay.css
│ ├── overlay.jsx
│ ├── overlaylist.jsx
│ └── types
│ │ ├── audiooverlay.jsx
│ │ ├── htmloverlay.jsx
│ │ ├── textoverlay.jsx
│ │ └── videooverlay.jsx
├── static
│ ├── css
│ │ ├── HarryP.woff
│ │ ├── HarryP.woff2
│ │ └── min.css
│ └── js
│ │ ├── bundle.js
│ │ ├── bundle.js.map
│ │ ├── overlays.js
│ │ └── overlays.test.js
├── twitch-overlay.js
├── twitch-overlay.spec.js
├── types
│ ├── audio.js
│ ├── audio.spec.js
│ ├── html.js
│ ├── html.spec.js
│ ├── html_test.html
│ ├── text.js
│ ├── text.spec.js
│ ├── video.js
│ └── video.spec.js
└── views
│ └── index.pug
├── package-lock.json
├── package.json
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env", "react"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 |
6 | # Runtime data
7 | pids
8 | *.pid
9 | *.seed
10 |
11 | # Directory for instrumented libs generated by jscoverage/JSCover
12 | lib-cov
13 |
14 | # Coverage directory used by tools like istanbul
15 | coverage
16 |
17 | # nyc test coverage
18 | .nyc_output
19 |
20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
21 | .grunt
22 |
23 | # node-waf configuration
24 | .lock-wscript
25 |
26 | # Compiled binary addons (http://nodejs.org/api/addons.html)
27 | build/Release
28 |
29 | # Dependency directories
30 | node_modules
31 | jspm_packages
32 |
33 | # Optional npm cache directory
34 | .npm
35 |
36 | # Optional REPL history
37 | .node_repl_history
38 |
39 | # dotenv config file
40 | .env
41 |
42 | # Redis db
43 | dump.rdb
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # twitch-overlay
2 | Animated overlays for Twitch streamers that can be loaded into OBS or XSplit
3 |
4 | This library will add an express-based server that listens for events (via a simple EventEmitter) and plays overlays when it receives an event. You can load the default URL '/' into your OBS/Xsplit client which will render overlays with a transparent background.
5 |
6 | You can pair this with lots of other modules such as [hpc-bot](https://github.com/bdickason/hpc-bot) to integrate with a chat bot or [twitch-soundboard]() to play overlays manually via your browser.
7 |
8 | # Installation
9 |
10 | 1. Include the module in your codebase: `var Overlays = require('twitch-overlay');`
11 | 1. Define any optional settings `var options = {};` (see **Config** below)
12 | 1. Start the overlay server: `overlays = new Overlays(options);`
13 |
14 | ## Config
15 |
16 | The `constructor()` function accepts the following optional parameters via a single json object:
17 | ````
18 | var options = {
19 | hostname: 'localhost', // Binds server to this host (optional)
20 | port: 3000, // Binds server to this port
21 | directory: '/', // URL you want to point OBS/Xsplit at (optional, default: '/')
22 | events: new Events.EventEmitter() // Listens to events to trigger overlays
23 | };
24 | ````
25 |
26 | # Usage
27 |
28 | ## Types of Overlays
29 |
30 | First, you should decide what type of overlay you want to display on your stream. The basic types are `text`, `video`, or `html` (see 'Modules' below for more details). The easier overlay to get started with is video which just takes a single mp4 file and a name as a parameter.
31 |
32 | ## Adding Overlays `add(overlay)` or event: `overlays:add`
33 |
34 | To add an overlay, you need to pass a json object (or array of objects) with the following parameters:
35 |
36 | **Example video overlay:**
37 | ````
38 | var overlay = {
39 | name: 'butterbeer',
40 | type: 'video',
41 | file: 'events/beer.mp4''
42 | }
43 | overlays.add(overlay);
44 |
45 | events.emit('overlay:butterbeer:show');
46 | ````
47 |
48 | **Example text overlay**
49 | ````
50 | var overlay = {
51 | name: 'text',
52 | type: 'text'
53 | }
54 | overlays.add(overlay);
55 |
56 | events.emit('overlay:text:show', 'text to say goes here!');
57 | ````
58 |
59 | **Example html overlay:**
60 | ````
61 | var overlay = {
62 | name: 'quidditch',
63 | type: 'html',
64 | view: './views/quidditch.html',
65 | static: './js/'
66 | }
67 | overlays.add(overlay);
68 |
69 | events.emit('overlay:quidditch:show');
70 | ````
71 |
72 | Overlays will be added to an array and stored with a set of event listeners (See below: Firing Overlays). Each type has an associated React client-side template that is rendered when you want to display the overlay. The overlay server maintains a persistent state variable that is passed to react which keeps track of what is on screen and off screen at any given time.
73 |
74 | See a fully functional example here: https://github.com/bdickason/dumbledore
75 |
76 | ## Firing Overlays `show()` or `overlays:name:show`
77 |
78 | When an overlay is added to the server, it automatically has an event listener created in the format: `overlay:(name):show` where (name) is the name of the overlay that you passed in.
79 |
80 | To trigger an overlay, just emit this event to the EventEmitter you passed in and the overlay will play on the server. Most overlays end automatically when they complete (i.e. video) but some such as html events do not have a fixed endpoint and listen for custom events from the client. If you have a custom event, use `io.socket.emit('endOverlay', null, (name), (payload))` where (name) is the name of your overlay and (payload) is any additional data you want to pass along. When the server retrieves this, it will relay the event to your EventEmitter in the format: `overlay:(name):end(payload)` so you can listen for this and act accordinly.
81 |
82 | ## Removing Overlays `remove(overlay)` or `overlays:name:remove`
83 |
84 | If you want to remove an existing overlay entirely, you can issue the remove() command.
85 |
86 | To add an overlay, you need to pass a json object (or array of objects) with the `name` parameter. This allows you to pass in an existing overlay object (like you would with `add()`) or create a new object that specifies the name.
87 |
88 | *Note: This does not remove the overlay from state in case one is currently playing*
89 |
90 | ## Hiding Overlays `hide()` or `overlays:name:hide`
91 |
92 | If you want to manually hide an overlay rather than waiting for it to complete, you can issue the hide() command.
93 |
94 |
95 | ## Displaying Overlays on your Stream (Adding to OBS/Xsplit)
96 |
97 | In order for overlays to display in your OBS or Xsplit client, you need to do the following:
98 |
99 | **OBS**
100 | 1. Click the '+' under Sources
101 | 2. Add a BrowserSource
102 | 3. Point it at http://(yourhostname):(yourport)/(yourdirectory). The default is http://localhost:3000
103 | 4. Adjust the width/height to be 16:9
104 | 5. Click 'Ok' to Save
105 |
106 | After you've added the source (and made sure it's visible and on top of your stream), you should start seeing overlays show up. They show up with a transparent background so you won't see anything visible until you start triggering overlays.
107 |
108 | ## Running Tests
109 |
110 | You can run tests to verify that everything is working with the command `npm test`. This requires **mocha** to be installed with `npm install -g mocha`.
111 |
112 | If you plan to submit pull requests, please ensure that the request includes proper test coverage of your feature.
113 |
--------------------------------------------------------------------------------
/examples/animated-toast.js:
--------------------------------------------------------------------------------
1 | animated-toast.js
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /* Index.js - Main startup file for twitch-overlay */
2 |
3 | module.exports = require('./lib/twitch-overlay');
4 |
--------------------------------------------------------------------------------
/lib/client/overlay-client.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from 'react-dom';
3 |
4 | // Detect hostname for socket.io to connect
5 | const hostname = window && window.location && window.location.hostname;
6 |
7 | // Libraries
8 | import openSocket from 'socket.io-client';
9 | const socket = openSocket(hostname + ':3000'); // Connect to the server to get client updates
10 |
11 | // Components
12 | import OverlayList from './overlaylist.jsx'
13 |
14 | class OverlayPlayer extends React.Component {
15 | constructor(props) {
16 | super(props);
17 | this.state = {
18 | overlays: {
19 | fullscreen: [],
20 | center: [],
21 | left: [],
22 | right: []
23 | }
24 | };
25 |
26 | this.updateState = this.updateState.bind(this);
27 | this.end = this.end.bind(this);
28 |
29 | /* Handle updates from server */
30 | socket.on('overlays:state', this.updateState); // Receive state updates from server
31 | }
32 |
33 | render () {
34 | return(
35 |
45 | );
46 | }
47 |
48 | updateState(state) {
49 | this.setState(state);
50 | }
51 |
52 | end(id) {
53 | socket.emit('endOverlay', id);
54 | }
55 | }
56 |
57 |
58 | render( , document.getElementById('overlays'));
59 |
--------------------------------------------------------------------------------
/lib/client/overlay.css:
--------------------------------------------------------------------------------
1 | /* overlay.css styling */
2 | .responsive {
3 | max-width: 100%;
4 | height: auto;
5 | }
6 |
7 | /* Custom harryp1 font */
8 | .text {
9 | font-family: 'Harry P';
10 | font-weight: normal;
11 | font-style: normal;
12 | font-size: -webkit-xxx-large;
13 | color: yellow;
14 | }
15 |
16 | @font-face {
17 | font-family: 'Harry P';
18 | src: url('/css/HarryP.woff2') format('woff2'),
19 | url('/css/HarryP.woff') format('woff');
20 | font-weight: normal;
21 | font-style: normal;
22 | }
23 |
24 | video {
25 | width: 100% !important;
26 | height: auto !important;
27 | }
28 |
--------------------------------------------------------------------------------
/lib/client/overlay.jsx:
--------------------------------------------------------------------------------
1 | /* Overlay - Render a single overlay */
2 | import React from 'react';
3 | import {render} from 'react-dom';
4 |
5 | import css from './overlay.css'
6 |
7 | import VideoOverlay from './types/videooverlay.jsx'
8 | import AudioOverlay from './types/audiooverlay.jsx'
9 | import HtmlOverlay from './types/htmloverlay.jsx'
10 | import TextOverlay from './types/textoverlay.jsx'
11 |
12 | class Overlay extends React.Component {
13 | constructor(props) {
14 | super(props);
15 |
16 | this.end = this.end.bind(this);
17 | }
18 |
19 | render () {
20 | switch(this.props.type) {
21 | case 'video':
22 | return();
23 | break;
24 | case 'audio':
25 | return();
26 | break;
27 | case 'html':
28 | return( )
29 | break;
30 | case 'text':
31 | return( );
32 | break;
33 | }
34 | }
35 |
36 | end() {
37 | this.props.end(this.props.id);
38 | }
39 | }
40 |
41 | export default Overlay
42 |
--------------------------------------------------------------------------------
/lib/client/overlaylist.jsx:
--------------------------------------------------------------------------------
1 | /* OverlayList - Display a list of overlays */
2 |
3 | import React from 'react';
4 | import {render} from 'react-dom';
5 |
6 | // Components
7 | import Overlay from './overlay.jsx'
8 |
9 | class OverlayList extends React.Component {
10 | constructor(props) {
11 | super(props);
12 | }
13 |
14 | render () {
15 | let overlayList = this.props.list.map((overlay, index) =>
16 |
28 | );
29 |
30 | return(
31 | {overlayList}
32 | );
33 | }
34 | }
35 |
36 | export default OverlayList
37 |
--------------------------------------------------------------------------------
/lib/client/types/audiooverlay.jsx:
--------------------------------------------------------------------------------
1 | /* AudioOverlay - Template for playing audio clips */
2 | import React from 'react';
3 | import {render} from 'react-dom';
4 |
5 | class AudioOverlay extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | render () {
11 | return(
12 |
13 |
14 |
15 |
16 |
17 | {this.props.text}
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | export default AudioOverlay
29 |
--------------------------------------------------------------------------------
/lib/client/types/htmloverlay.jsx:
--------------------------------------------------------------------------------
1 | /* HtmlOverlay - Template for rendering arbitrary html */
2 | import React from 'react';
3 | import {render} from 'react-dom';
4 |
5 | import ReactHtmlParser, { processNodes, convertNodeToElement, htmlparser2 } from 'react-html-parser';
6 |
7 | class HtmlOverlay extends React.Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.transform = this.transform.bind(this);
12 | }
13 |
14 | componentDidMount() {
15 | ReactHtmlParser(this.props.html, {transform: this.transform});
16 | }
17 | render() {
18 | return
;
19 | }
20 |
21 | transform(node) {
22 | // HACK - Manually load/run script tags
23 | if (node.type === 'script') {
24 | const script = document.createElement("script");
25 | script.id = this.props.id;
26 | script.src = node.attribs.src;
27 | script.onEnd = this.end;
28 | document.body.appendChild(script);
29 | return null;
30 | }
31 | }
32 |
33 | end() {
34 | console.log('ended!');
35 | }
36 | }
37 |
38 | export default HtmlOverlay
39 |
--------------------------------------------------------------------------------
/lib/client/types/textoverlay.jsx:
--------------------------------------------------------------------------------
1 | /* TextOverlay - Template for speaking to stream */
2 | import React from 'react';
3 | import {render} from 'react-dom';
4 |
5 | class TextOverlay extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | render () {
11 | window.responsiveVoice.speak(this.props.text, 'US English Female', {onend: this.props.onEnd()});
12 |
13 | return(
14 |
15 |
16 | {this.props.text}
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | export default TextOverlay
24 |
--------------------------------------------------------------------------------
/lib/client/types/videooverlay.jsx:
--------------------------------------------------------------------------------
1 | /* VideoOverlay - Template for displaying videos */
2 | import React from 'react';
3 | import {render} from 'react-dom';
4 |
5 | class VideoOverlay extends React.Component {
6 | constructor(props) {
7 | super(props);
8 | }
9 |
10 | componentDidMount() {
11 | // Grab the video object once it's available so we can adjust the volume
12 | this.refs.video.volume = this.props.volume
13 | }
14 | render () {
15 | return(
16 |
17 |
18 |
19 |
20 |
21 | {this.props.text}
22 |
23 |
24 |
25 | );
26 | }
27 | }
28 |
29 | export default VideoOverlay
30 |
--------------------------------------------------------------------------------
/lib/static/css/HarryP.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hpcbot/twitch-overlay/600d81ab8f3f1b6b22782ef6bc010bc249a561f8/lib/static/css/HarryP.woff
--------------------------------------------------------------------------------
/lib/static/css/HarryP.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hpcbot/twitch-overlay/600d81ab8f3f1b6b22782ef6bc010bc249a561f8/lib/static/css/HarryP.woff2
--------------------------------------------------------------------------------
/lib/static/css/min.css:
--------------------------------------------------------------------------------
1 | /* Copyright 2014 Owen Versteeg; MIT licensed */body,textarea,input,select{background:0;border-radius:0;font:16px sans-serif;margin:0}.smooth{transition:all .2s}.btn,.nav a{text-decoration:none}.container{margin:0 20px;width:auto}label>*{display:inline}form>*{display:block;margin-bottom:10px}.btn{background:#999;border-radius:6px;border:0;color:#fff;cursor:pointer;display:inline-block;margin:2px 0;padding:12px 30px 14px}.btn:hover{background:#888}.btn:active,.btn:focus{background:#777}.btn-a{background:#0ae}.btn-a:hover{background:#09d}.btn-a:active,.btn-a:focus{background:#08b}.btn-b{background:#3c5}.btn-b:hover{background:#2b4}.btn-b:active,.btn-b:focus{background:#2a4}.btn-c{background:#d33}.btn-c:hover{background:#c22}.btn-c:active,.btn-c:focus{background:#b22}.btn-sm{border-radius:4px;padding:10px 14px 11px}.row{margin:1% 0;overflow:auto}.col{float:left}.table,.c12{width:100%}.c11{width:91.66%}.c10{width:83.33%}.c9{width:75%}.c8{width:66.66%}.c7{width:58.33%}.c6{width:50%}.c5{width:41.66%}.c4{width:33.33%}.c3{width:25%}.c2{width:16.66%}.c1{width:8.33%}h1{font-size:3em}.btn,h2{font-size:2em}.ico{font:33px Arial Unicode MS,Lucida Sans Unicode}.addon,.btn-sm,.nav,textarea,input,select{outline:0;font-size:14px}textarea,input,select{padding:8px;border:1px solid #ccc}textarea:focus,input:focus,select:focus{border-color:#5ab}textarea,input[type=text]{-webkit-appearance:none;width:13em}.addon{padding:8px 12px;box-shadow:0 0 0 1px #ccc}.nav,.nav .current,.nav a:hover{background:#000;color:#fff}.nav{height:24px;padding:11px 0 15px}.nav a{color:#aaa;padding-right:1em;position:relative;top:-1px}.nav .pagename{font-size:22px;top:1px}.btn.btn-close{background:#000;float:right;font-size:25px;margin:-54px 7px;display:none}@media(min-width:1310px){.container{margin:auto;width:1270px}}@media(max-width:870px){.row .col{width:100%}}@media(max-width:500px){.btn.btn-close{display:block}.nav{overflow:hidden}.pagename{margin-top:-11px}.nav:active,.nav:focus{height:auto}.nav div:before{background:#000;border-bottom:10px double;border-top:3px solid;content:'';float:right;height:4px;position:relative;right:3px;top:14px;width:20px}.nav a{padding:.5em 0;display:block;width:50%}}.table th,.table td{padding:.5em;text-align:left}.msg{padding:1.5em;background:#def;border-left:5px solid #59d}
2 |
--------------------------------------------------------------------------------
/lib/static/js/bundle.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["webpack:///webpack/bootstrap 4a74d8f99d3c918b7b15"],"names":[],"mappings":";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA","file":"bundle.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 4a74d8f99d3c918b7b15"],"sourceRoot":""}
--------------------------------------------------------------------------------
/lib/static/js/overlays.js:
--------------------------------------------------------------------------------
1 | /* overlays.js - Contains basic logic for overlays */
2 |
3 | $(document).ready(function() {
4 | var socket = io();
5 |
6 | // Loads an overlay when a new browser session comes online
7 | socket.on('load:template', function(overlay) {
8 | // Allows text to fade in after the voice command is executed
9 | var delayFadeOut = function () {
10 | // Animation delay passed, fade back in
11 | $(overlay.selector).fadeOut();
12 | };
13 |
14 | $.get('/overlays/' + overlay.name, function(template) {
15 | $('.overlays').append(template);
16 |
17 | // Hide overlay when video finishes
18 | var videoPlayer = $(overlay.selector).find('video');
19 | if(videoPlayer) {
20 | videoPlayer.on('ended', delayFadeOut);
21 | }
22 |
23 | // Hide overlay when audio/mp4 finishes
24 | var audioPlayer = $(overlay.selector).find('audio');
25 | if(audioPlayer) {
26 | audioPlayer.on('ended', delayFadeOut);
27 | }
28 | })
29 | });
30 |
31 | // Fired when a specific overlay should be shown
32 | socket.on('show:overlay', function(template, payload) {
33 | addData(template.name, template.selector, payload);
34 | showOverlay(template.selector);
35 | });
36 |
37 | // Fades the overlay in/out
38 | var showOverlay = function showOverlay(selector) {
39 | $(selector).fadeIn(650);
40 | };
41 |
42 | // Loads the specific template data passed in for this event
43 | var addData = function addData(name, selector, data) {
44 | var count = 0;
45 |
46 | var callbackFadeOut = function () {
47 | // responsiveVoice places the audio callback on the 'speak' event so we need a separate fadeout function for them
48 | // Animation delay passed, fade back in
49 | $(selector).fadeOut();
50 | };
51 |
52 | for (var key in data) {
53 | var value = data[key];
54 |
55 | switch(getType(value)) {
56 | case 'audio':
57 | // Load and play sound
58 | $(selector + ' #' + key).attr('src', value);
59 | $(selector + ' #' + key + '_player')[0].load();
60 | $(selector + ' #' + key + '_player')[0].play();
61 | break;
62 | case 'image':
63 | $(selector + ' #' + key).attr('src', value);
64 | break;
65 | case 'speech':
66 | // Handle text to speech
67 | value = value.substring(0, value.length-4); // Strip the .speech from the end
68 | $(selector + ' #' + key).text(value); // Display the text on screen
69 |
70 | responsiveVoice.speak(value, 'US English Female', {onend: callbackFadeOut}); // Say it out loud
71 | break;
72 | case 'text':
73 | $(selector + ' #' + key).text(value);
74 | break;
75 | case 'video':
76 | var source = $(selector).find('source')[0];
77 | source.src = name + '/' + value;
78 |
79 | var player = $(selector).find('video')[0];
80 | player.load();
81 | player.play();
82 | break;
83 | }
84 | }
85 | };
86 |
87 | var getType = function getType(filename) {
88 | if(typeof(filename) == 'string') {
89 | var extension=(filename.substring(filename.length-4, filename.length));
90 |
91 | if(extension == '.png') {
92 | return('image');
93 | }
94 | else if(extension == '.m4a') {
95 | return('audio');
96 | }
97 | else if(extension == '.mp4') {
98 | return('video');
99 | }
100 | else if(extension == '.tts') {
101 | return('speech');
102 | }
103 | else {
104 | return('text');
105 | }
106 | }
107 | }
108 | });
109 |
--------------------------------------------------------------------------------
/lib/static/js/overlays.test.js:
--------------------------------------------------------------------------------
1 | /* overlays.spec.js - client-side Tests for Twitch Overlays */
2 | casper.test.begin('Twitch Chat Overlay', 7, function(test) {
3 | // Connect to server
4 | casper.start('http://localhost:3000', function() {
5 | // Sortinghat Overlay
6 | casper.waitForSelector('.sortinghat', function() {
7 | // casper.log('test');
8 | // Is .sortinghat rendered on page properly?
9 | test.assertExists('.sortinghat');
10 |
11 | // overlay should be hidden by default
12 | var overlay = casper.getElementInfo('.sortinghat');
13 | test.assertEqual(overlay.visible, false);
14 | test.assertExists('#house_logo');
15 | test.assertExists('#username');
16 | test.assertExists('#house_text');
17 | test.assertExists('#house_audio');
18 | test.assertExists('#house_audio_player');
19 | });
20 | });
21 |
22 | casper.run(function() {
23 | test.done();
24 | });
25 |
26 | // Debugging
27 | casper.on("remote.message", function(msg) {
28 | this.echo("Console: " + msg);
29 | });
30 |
31 | // http://docs.casperjs.org/en/latest/events-filters.html#page-error
32 | casper.on("page.error", function(msg, trace) {
33 | this.echo("Error: " + msg);
34 | // maybe make it a little fancier with the code from the PhantomJS equivalent
35 | });
36 |
37 | // http://docs.casperjs.org/en/latest/events-filters.html#resource-error
38 | casper.on("resource.error", function(resourceError) {
39 | this.echo("ResourceError: " + JSON.stringify(resourceError, undefined, 4));
40 | });
41 |
42 | // http://docs.casperjs.org/en/latest/events-filters.html#page-initialized
43 | casper.on("page.initialized", function(page) {
44 | // CasperJS doesn't provide `onResourceTimeout`, so it must be set through
45 | // the PhantomJS means. This is only possible when the page is initialized
46 | page.onResourceTimeout = function(request) {
47 | console.log('Response Timeout (#' + request.id + '): ' + JSON.stringify(request));
48 | };
49 | });
50 |
51 | });
52 |
--------------------------------------------------------------------------------
/lib/twitch-overlay.js:
--------------------------------------------------------------------------------
1 | /* twitch-overlay.js - Simple http server to serve overlays */
2 |
3 | /* To add overlay:
4 | let video = {
5 | name: 'hpcwins',
6 | type: 'video',
7 | file: 'videos/events/hpcwins.mp4'
8 | }
9 | bot.overlays.add(video)
10 | // -OR-
11 | events.emit('overlays:add', video)*/
12 |
13 | const express = require('express')
14 | const Events = require('events')
15 | const path = require('path')
16 | const extend = require('extend')
17 | const shortid = require('shortid') // Generate short unique identifiers
18 |
19 | const types = {
20 | text: require('./types/text.js'),
21 | video: require('./types/video.js'),
22 | html: require('./types/html.js'),
23 | audio: require('./types/audio.js')
24 | }
25 |
26 | let app
27 | let io
28 | let self
29 |
30 | let overlays = [] // List of overlays that have been loaded
31 | let clients = {} // List of clients that are connected (for cleaning events)
32 |
33 | let state = {
34 | overlays: []
35 | } // Persistent state for the app
36 |
37 |
38 | module.exports = class TwitchOverlay {
39 | constructor(_options) {
40 |
41 | // config letiables w/ default values
42 | this.options = {
43 | hostname: 'localhost', // Binds server to this host (optional)
44 | port: 3000, // Binds server to this port
45 | directory: '/', // URL you want to point OBS/Xsplit at (optional, default: '/')
46 | viewEngine: 'pug', // Templating system you'd like to use (Express-compatible) (optional: defaults to pug) */
47 | events: new Events.EventEmitter() // Listens to events to trigger overlays
48 | }
49 |
50 | const self = this // HACK for eventemitter listener scope
51 |
52 | // this.options for starting the overlay server:
53 | this.options = extend(this.options, _options) // Copy _this.options into this.options, overwriting defaults
54 |
55 | app = express() // Express should be accessible from all functions
56 | /* Start webapp */
57 | app.use(express.static(path.join(__dirname, 'static')))
58 | app.set('views', path.join(__dirname, 'views'))
59 | app.locals.basedir = app.get('views')
60 | app.set('view engine', this.options.viewEngine)
61 |
62 | app.get(this.options.directory, function (req, res, next) {
63 | try {
64 | res.render('index')
65 | } catch (e) {
66 | next(e)
67 | }
68 | })
69 |
70 | this.options.events.on('overlays:add', (overlays) => {
71 | this.add(overlays)
72 | })
73 |
74 | this.options.events.on('overlays:remove', (overlays) => {
75 | this.remove(overlays)
76 | })
77 |
78 | // Start server
79 | const server = require('http').Server(app)
80 | const port = this.options.port
81 | server.listen(port)
82 | console.log('[Overlay Server] listening on port ' + port)
83 |
84 | // Start Socket IO (event handler)
85 | io = require('socket.io')(server)
86 |
87 | io.on('connection', (socket) => {
88 | clients[socket.id] = []
89 |
90 | socket.on('disconnect', () => {
91 | // Clean up Overlay listener events on disconnect or reload to prevent memory leak
92 | this.unload(socket.id)
93 | })
94 |
95 | socket.on('endOverlay', (id, name, payload) => {
96 | this.end(id, name, payload)
97 | })
98 |
99 | this.update() // send latest state down to the client
100 | })
101 |
102 | /* Create initial state when server starts */
103 | this.update()
104 | }
105 |
106 | add(_overlays) {
107 | // Add overlay(s) to display them on stream.
108 | // Expects an Object or array of Objects w/ the following structure:
109 | // name: 'powermove', // String that will activate this in chat/events i.e. !powermove and powermove:show
110 | // type: 'text', // (text, video, html)
111 | // file: '../blah.mp4' // (optional) Filename for video w/ path
112 | // text: 'Blah has subscribed!' // (optional) Text to display/read
113 | // static: '../static/blah' // (optional) directory containing images, etc to serve via webserver
114 | // view: '../test.pug' // (optional) pug template to inject instead of the default
115 |
116 | let queue = [] // Overlays to be processed
117 |
118 | if(Array.isArray(_overlays)) {
119 | // Process multiple overlays
120 | queue = _overlays
121 | } else {
122 | queue.push(_overlays)
123 | }
124 |
125 | // Loop through each overlay and load it into our overlays array
126 | queue.forEach((item) => {
127 | let overlay
128 | let template
129 |
130 | switch(item.type) {
131 | case 'text':
132 | overlay = new types.text()
133 | break
134 | case 'video':
135 | overlay = new types.video(item.name, item.file, item.volume)
136 | break
137 | case 'html':
138 | overlay = new types.html(item.name, item.view, item.static)
139 | break
140 | case 'audio':
141 | overlay = new types.audio(item.name, item.directory)
142 | break
143 | }
144 |
145 | overlay.type = item.type // Pass template down to client
146 | if(item.layout) {
147 | overlay.layout = item.layout
148 | } else {
149 | overlay.layout = 'center'
150 | }
151 |
152 |
153 | overlays.push(overlay) // Keep track of all of the overlays
154 |
155 | if(overlay.directory) {
156 | // Add static assets (images, audio, etc) to server
157 | app.use('/' + overlay.name, express.static(overlay.directory))
158 | }
159 |
160 | this.options.events.on('overlay:' + overlay.name + ':show', (payload) => {
161 | this.show(overlay, payload)
162 | })
163 |
164 | this.options.events.on('overlay:' + overlay.name + ':hide', () => {
165 | this.hide(overlay.name)
166 | })
167 | })
168 | }
169 |
170 | remove(_overlays) {
171 | // Remove overlay(s) from the system
172 | // Expects an Object or array of Objects w/ the following structure:
173 | // name: 'powermove',
174 | // (Ignores all other parameters)
175 |
176 | let queue = [] // Overlays to be processed
177 |
178 | if(Array.isArray(_overlays)) {
179 | // Process multiple overlays
180 | queue = _overlays
181 | } else {
182 | queue.push(_overlays)
183 | }
184 |
185 | // Loop through each overlay and load it into our overlays array
186 | queue.forEach((item) => {
187 | if(item.name) {
188 | // Search overlays to see if item exists
189 | for(let i = 0; i < overlays.length; i++) {
190 | if(overlays[i].name == item.name) {
191 | // Remove item from the list of overlays
192 | overlays.splice(i, 1)
193 |
194 | // Remove dangling event listeners to prevent memory leaks or unintended calls
195 | this.options.events.removeAllListeners('overlay:' + item.name + ':show')
196 | this.options.events.removeAllListeners('overlay:' + item.name + ':hide')
197 | }
198 | }
199 | }
200 | })
201 |
202 | }
203 |
204 | update() {
205 | // Called any time we update the state on the server
206 |
207 | // Transform state to filter by layout
208 | let _state = {
209 | overlays: {
210 | fullscreen: state.overlays.filter(overlay => overlay.layout == 'fullscreen'),
211 | center: state.overlays.filter(overlay => overlay.layout == 'center'),
212 | right: state.overlays.filter(overlay => overlay.layout == 'right'),
213 | left: state.overlays.filter(overlay => overlay.layout == 'left')
214 | }
215 | }
216 | io.sockets.emit('overlays:state', _state) // Send an update to all connected clients
217 | }
218 |
219 | show(overlay, payload) {
220 | let _overlay = {
221 | id: shortid.generate(),
222 | name: overlay.name,
223 | type: overlay.type,
224 | payload: overlay.payload,
225 | layout: 'center'
226 | }
227 |
228 | if(payload) {
229 | _overlay.payload = payload
230 | }
231 |
232 | _overlay = extend(_overlay, overlay)
233 |
234 | // Set overlay to 'showing'
235 | state.overlays.push(_overlay)
236 |
237 | this.update()
238 | }
239 |
240 | hide(name) {
241 | /* hide - Hides an overlay by name
242 | input: 'overlayName'
243 | output: n/a
244 | */
245 | if(name) {
246 | // Ignore empty requests
247 | for(let i = 0; i < state.overlays.length; i++) {
248 | if(state.overlays[i].name == name) {
249 | state.overlays.splice(i, 1)
250 | }
251 | }
252 |
253 | this.update()
254 | }
255 | }
256 |
257 | end(id, name, payload) {
258 | // Overlay ended, remove it from visible state
259 | state.overlays.forEach((overlay, index) => {
260 | if(overlay.id == id) {
261 | state.overlays.splice(index, 1)
262 | this.update()
263 | } else if (overlay.name == name) {
264 | state.overlays.splice(index, 1)
265 |
266 | // Trigger custom event
267 | this.options.events.emit('overlays:' + name + ':end', payload)
268 | this.update()
269 | }
270 | })
271 | }
272 |
273 | unload(socketId) {
274 | // remove listeners for each overlay event to prevent them sticking around
275 | for (let key in clients[socketId]) {
276 | this.options.events.removeListener(key, clients[socketId][key])
277 | }
278 |
279 | // Delete item from client object so it doesn't show up any more
280 | delete clients[socketId]
281 |
282 | if(Object.keys(clients).length === 0 && clients.constructor === Object) {
283 | overlays = [] // Empty the overlays queue if no clients are connected
284 | }
285 | }
286 |
287 | getState() {
288 | return(state.overlays)
289 | }
290 |
291 | clearState() {
292 | state.overlays = []
293 | }
294 |
295 | list() {
296 | // Returns the currently loaded overlays
297 | return(overlays)
298 | }
299 |
300 | clear() {
301 | overlays = []
302 | }
303 | }
304 |
--------------------------------------------------------------------------------
/lib/twitch-overlay.spec.js:
--------------------------------------------------------------------------------
1 | /* Test for lib/twitch-overlay.js */
2 |
3 | const assert = require('chai').assert
4 | const sinon = require('sinon')
5 |
6 | const EventEmitter = require('events')
7 | const eventbus = new EventEmitter // Temporary event bus to prevent events firing across files
8 |
9 | // let overlays
10 |
11 | describe('Server', function() {
12 | beforeEach(function() {
13 | this.sinon = sandbox = sinon.sandbox.create()
14 | Overlays = require('./twitch-overlay.js')
15 | })
16 | describe('constructor', function() {
17 | it('Starts with no options (all defaults)', function() {
18 | let overlays = new Overlays()
19 |
20 | let _hostname = 'localhost'
21 | let _port = 3000
22 | let _directory = '/'
23 | let _viewEngine = 'pug'
24 |
25 | // Verify defaults
26 | assert.notEqual(overlays.options, null)
27 | assert.equal(overlays.options.hostname, _hostname)
28 | assert.equal(overlays.options.port, _port)
29 | assert.equal(overlays.options.directory, _directory)
30 | assert.equal(overlays.options.viewEngine, _viewEngine)
31 | assert.notEqual(overlays.options.events, null)
32 | })
33 |
34 | it('Starts with all options (no defaults)', function() {
35 | let options = {
36 | hostname: 'test.com',
37 | port: 3500,
38 | directory: '/test',
39 | viewEngine: 'jade',
40 | events: eventbus
41 | }
42 |
43 | let overlays = new Overlays(options)
44 |
45 | let _hostname = 'test.com'
46 | let _port = 3500
47 | let _directory = '/test'
48 | let _viewEngine = 'jade'
49 | let _events = eventbus
50 |
51 | // Verify defaults
52 | assert.notEqual(overlays.options, null)
53 | assert.equal(overlays.options.hostname, _hostname)
54 | assert.equal(overlays.options.port, _port)
55 | assert.equal(overlays.options.directory, _directory)
56 | assert.equal(overlays.options.viewEngine, _viewEngine)
57 | assert.equal(typeof(overlays.options.events), typeof(_events))
58 | })
59 | })
60 | describe('add() - Load Overlays', function() {
61 | it('Loads a single overlay', function() {
62 | overlays = new Overlays({events: eventbus})
63 |
64 | let overlay = {
65 | name: 'hpcwins',
66 | type: 'video',
67 | file: 'overlays/events/hpcwins.mp4'
68 | }
69 |
70 | let _overlay = {
71 | name: 'hpcwins',
72 | type: 'video',
73 | directory: 'overlays/events',
74 | video: '/hpcwins/hpcwins.mp4',
75 | volume: 1
76 | }
77 |
78 | let _payload = {
79 | video: 'hpcwins.mp4'
80 | }
81 |
82 | overlays.add(overlay)
83 | let list = overlays.list()
84 |
85 | assert.equal(list.length, 1)
86 | assert.equal(list[0].name, _overlay.name)
87 | assert.include(list[0].type, _overlay.type)
88 | assert.equal(list[0].selector, _overlay.selector)
89 | assert.equal(list[0].directory, _overlay.directory)
90 | })
91 | it('Loads an array of overlays', function() {
92 | overlays = new Overlays({events: eventbus})
93 |
94 | let overlay = [{
95 | name: 'hpcwins',
96 | type: 'video',
97 | file: 'overlays/events/hpcwins.mp4'
98 | }, {
99 | name: 'hpctest',
100 | type: 'audio',
101 | directory: '/test'
102 | }]
103 |
104 | let _overlays = [{
105 | name: 'hpcwins',
106 | type: 'video',
107 | directory: 'overlays/events',
108 | video: '/hpcwins/hpcwins.mp4',
109 | layout: 'center',
110 | volume: 1
111 | }, {
112 | name: 'hpctest',
113 | layout: 'center',
114 | type: 'audio',
115 | directory: '/test'
116 | }]
117 |
118 | overlays.add(overlay)
119 |
120 | let list = overlays.list()
121 |
122 | assert.equal(list.length, 2)
123 | assert.deepEqual(list, _overlays)
124 | })
125 | it('No payload: Calls show with no payload', function() {
126 | overlays = new Overlays({events: eventbus})
127 |
128 | let overlay = {
129 | name: 'hpcwins',
130 | type: 'video',
131 | file: 'overlays/events/hpcwins.mp4'
132 | }
133 |
134 | // Add Overlay
135 | overlays.add(overlay)
136 |
137 | // Setup spy to monitor function
138 | let spy = sinon.spy(overlays, 'show')
139 |
140 | // Trigger event
141 | eventbus.emit('overlay:hpcwins:show')
142 |
143 | assert.isOk(spy.calledOnce)
144 | })
145 | it('Payload: Calls show with a payload', function() {
146 | overlays = new Overlays({events: eventbus})
147 |
148 | let overlay = {
149 | name: 'cupadd',
150 | type: 'video',
151 | file: 'overlays/events/hpcwins.mp4'
152 | }
153 |
154 | let _overlay = {
155 | name: 'cupadd',
156 | type: 'video',
157 | directory: 'overlays/events',
158 | video: '/cupadd/hpcwins.mp4',
159 | layout: 'center',
160 | volume: 1
161 | }
162 |
163 | let payload = "s 2"
164 |
165 | let _payload = "s 2"
166 |
167 | // Add Overlay
168 | overlays.add(overlay)
169 |
170 | // Setup spy to monitor function
171 | let spy = sinon.spy(overlays, 'show')
172 |
173 | // Trigger event
174 | eventbus.emit('overlay:cupadd:show', payload)
175 |
176 | assert.isOk(spy.calledOnce)
177 | assert.deepEqual(spy.getCall(0).args[0], _overlay)
178 | assert.equal(spy.getCall(0).args[1], _payload)
179 | })
180 | afterEach(function() {
181 | overlays.clear()
182 | })
183 | })
184 | describe('remove() - Unload Overlays', function() {
185 | it('Overlay does not exist: Does nothing', function() {
186 | overlays = new Overlays({events: eventbus})
187 |
188 | let overlay = {
189 | name: 'hpcwins',
190 | type: 'video',
191 | file: 'overlays/events/hpcwins.mp4'
192 | }
193 |
194 | let _overlay = {
195 | name: 'hpcwins',
196 | type: 'video',
197 | directory: 'overlays/events',
198 | video: '/hpcwins/hpcwins.mp4',
199 | volume: 1
200 | }
201 |
202 | let _payload = {
203 | video: 'hpcwins.mp4'
204 | }
205 |
206 | overlays.add(overlay)
207 | let list = overlays.list()
208 |
209 | assert.equal(list.length, 1)
210 | assert.equal(list[0].name, _overlay.name)
211 |
212 | let fakeOverlay = {
213 | name: 'doesnotexist'
214 | }
215 | overlays.remove(fakeOverlay)
216 | let updatedList = overlays.list()
217 | assert.equal(list.length, 1)
218 | })
219 | it('Unloads a single overlay', function() {
220 | overlays = new Overlays({events: eventbus})
221 |
222 | let overlay = {
223 | name: 'hpcwins',
224 | type: 'video',
225 | file: 'overlays/events/hpcwins.mp4'
226 | }
227 |
228 | let _overlay = {
229 | name: 'hpcwins',
230 | type: 'video',
231 | directory: 'overlays/events',
232 | video: '/hpcwins/hpcwins.mp4',
233 | volume: 1
234 | }
235 |
236 | let _payload = {
237 | video: 'hpcwins.mp4'
238 | }
239 |
240 | overlays.add(overlay)
241 | let list = overlays.list()
242 |
243 | assert.equal(list.length, 1)
244 | assert.equal(list[0].name, _overlay.name)
245 |
246 | overlays.remove(overlay)
247 | let updatedList = overlays.list()
248 | assert.equal(list.length, 0)
249 | })
250 | it('Loads an array of overlays', function() {
251 | overlays = new Overlays({events: eventbus})
252 |
253 | let overlay = [{
254 | name: 'hpcwins',
255 | type: 'video',
256 | file: 'overlays/events/hpcwins.mp4'
257 | }, {
258 | name: 'hpctest',
259 | type: 'audio',
260 | directory: '/test'
261 | }]
262 |
263 | let _overlays = [{
264 | name: 'hpcwins',
265 | type: 'video',
266 | directory: 'overlays/events',
267 | video: '/hpcwins/hpcwins.mp4',
268 | layout: 'center',
269 | volume: 1
270 | }, {
271 | name: 'hpctest',
272 | layout: 'center',
273 | type: 'audio',
274 | directory: '/test'
275 | }]
276 |
277 | overlays.add(overlay)
278 |
279 | let list = overlays.list()
280 |
281 | assert.equal(list.length, 2)
282 | assert.deepEqual(list, _overlays)
283 |
284 | overlays.remove(overlay)
285 | let updatedList = overlays.list()
286 | assert.equal(list.length, 0)
287 | })
288 | afterEach(function() {
289 | overlays.clear()
290 | })
291 | })
292 | describe('end() - Stops an overlay', function() {
293 | it('No payload: Ends a single overlay by id', function(done) {
294 | const overlay = {
295 | name: 'cupadd',
296 | type: 'video',
297 | file: 'overlays/events/hpcwins.mp4'
298 | }
299 |
300 | overlays.add(overlay)
301 |
302 | let list = overlays.list()
303 |
304 | let id = list[0].id
305 |
306 | eventbus.on('overlays:cupadd:end', function(payload) {
307 | assert.equal(payload, null)
308 | done()
309 | })
310 |
311 | overlays.end(id, overlay.name, null)
312 | })
313 | afterEach(function() {
314 | overlays.clear()
315 | })
316 | })
317 | describe('hide() - Hides an overlay by name', function() {
318 | before(function() {
319 | overlays.clearState()
320 | })
321 | it('No name: doesn\t hide an overlay', function() {
322 | const overlay = {
323 | name: 'cupadd',
324 | type: 'video',
325 | file: 'overlays/events/hpcwins.mp4'
326 | }
327 |
328 | overlays.add(overlay)
329 | let list = overlays.list()
330 |
331 | overlays.show(list[0], null)
332 |
333 | overlays.hide(null)
334 |
335 | let state = overlays.getState()
336 |
337 | assert.equal(state.length, 1)
338 | assert.equal(state[0].name, 'cupadd')
339 | })
340 | it('Name but no overlays: does nothing', function() {
341 | overlays.hide('cupadd')
342 |
343 | let state = overlays.getState()
344 |
345 | assert.equal(state.length, 0)
346 | })
347 | it('Name but no match: does nothing', function() {
348 | const overlay = {
349 | name: 'cupadd',
350 | type: 'video',
351 | file: 'overlays/events/hpcwins.mp4'
352 | }
353 |
354 | overlays.add(overlay)
355 | let list = overlays.list()
356 |
357 | overlays.show(list[0], null)
358 |
359 | overlays.hide('test')
360 |
361 | let state = overlays.getState()
362 |
363 | assert.equal(state.length, 1)
364 | assert.equal(state[0].name, 'cupadd')
365 | })
366 | it('Name matches: removes overlay', function() {
367 | const overlay = {
368 | name: 'cupadd',
369 | type: 'video',
370 | file: 'overlays/events/hpcwins.mp4'
371 | }
372 |
373 | overlays.add(overlay)
374 | let list = overlays.list()
375 |
376 | overlays.show(list[0], null)
377 |
378 | overlays.hide('cupadd')
379 |
380 | let state = overlays.getState()
381 |
382 | assert.equal(state.length, 0)
383 | })
384 | afterEach(function() {
385 | overlays.clearState()
386 | })
387 | })
388 | afterEach(function() {
389 | sandbox.restore()
390 | })
391 | })
392 |
--------------------------------------------------------------------------------
/lib/types/audio.js:
--------------------------------------------------------------------------------
1 | /* audio.js - Re-usable module to create simple audio overlays */
2 |
3 | var path = require('path');
4 |
5 | var eventbus; // Global event bus attached to AudioOverlay object - used for passing events
6 |
7 | module.exports = AudioOverlay;
8 |
9 | function AudioOverlay(name, directory) {
10 | // this.name;
11 | // this.file;
12 |
13 | var self; // Used for scope when passing around events
14 | self = this;
15 |
16 | if(!name) {
17 | throw new Error("You must specify a name for this overlay")
18 | }
19 |
20 | // Required options
21 | this.name = name; // Unique identifier for event listeners
22 |
23 | this.directory = directory;
24 | this.type = 'audio';
25 | };
26 |
--------------------------------------------------------------------------------
/lib/types/audio.spec.js:
--------------------------------------------------------------------------------
1 | /* Test for audio.js */
2 |
3 | var assert = require('chai').assert;
4 | var sinon = require('sinon');
5 |
6 | let audio
7 |
8 | describe('audio.js - Audio Overlay Module', function() {
9 | beforeEach(function() {
10 | this.sinon = sandbox = sinon.sandbox.create();
11 | audio = require('./audio.js')
12 | });
13 | it('Returns an overlay when given parameters', () => {
14 | const name = 'testOverlay'
15 | const directory = './test'
16 |
17 | const _name = 'testOverlay'
18 | const _directory = './test'
19 | const _type = 'audio'
20 |
21 | let overlay = new audio(name, directory)
22 |
23 | assert.equal(overlay.name, _name)
24 | assert.equal(overlay.directory, _directory)
25 | assert.equal(overlay.type, _type)
26 | });
27 | it('Errors if no name is provided', () => {
28 | const directory = './test'
29 |
30 | assert.throws(() => {
31 | let overlay = new audio(name, directory)
32 | }, 'name is not defined')
33 | })
34 | afterEach(function() {
35 | sandbox.restore();
36 | });
37 | });
38 |
--------------------------------------------------------------------------------
/lib/types/html.js:
--------------------------------------------------------------------------------
1 | /* html.js - Re-usable module to display html on your stream */
2 |
3 | var path = require('path');
4 | var fs = require('fs');
5 |
6 | var eventbus; // Global event bus attached to HtmlOverlay object - used for passing events
7 |
8 | module.exports = HtmlOverlay;
9 |
10 | function HtmlOverlay(name, view, static) {
11 | // name - name to reference the overlay
12 | // view - the .html file to render when requested
13 | // static - custom static directory to host videos, images, etc
14 |
15 | var self; // Used for scope when passing around events
16 | self = this;
17 |
18 | if(!name) {
19 | throw new Error("You must specify a name for this overlay")
20 | }
21 | if(!view) {
22 | throw new Error("You must specify an .html view");
23 | }
24 |
25 | // Required parameters
26 | this.name = name; // Unique identifier for event listeners
27 | this.type = 'html';
28 | this.html = fs.readFileSync(view).toString(); // Grab file from disk and pass it to the template
29 | this.directory = path.join(path.dirname(view));
30 |
31 | // Optional parameters
32 | if(static) {
33 | // Custom directory to serve static assets from
34 | this.directory = static;
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/lib/types/html.spec.js:
--------------------------------------------------------------------------------
1 | /* Test for html.js */
2 |
3 | var assert = require('chai').assert;
4 | var sinon = require('sinon');
5 |
6 | let html
7 |
8 | describe('html.js - HTML Overlay Module', function() {
9 | beforeEach(function() {
10 | this.sinon = sandbox = sinon.sandbox.create();
11 | html = require('./html.js')
12 | });
13 | it('Returns an overlay when given parameters', () => {
14 | const name = 'testOverlay'
15 | const view = 'lib/types/html_test.html'
16 |
17 | const _name = 'testOverlay'
18 | const _html = '\n\n'
19 | const _type = 'html'
20 | const _directory = 'lib/types'
21 |
22 | let overlay = new html(name, view)
23 |
24 | assert.equal(overlay.name, _name)
25 | assert.equal(overlay.html, _html)
26 | assert.equal(overlay.type, _type)
27 | assert.equal(overlay.directory, _directory)
28 | });
29 | it('Returns an static directory when provided', () => {
30 | const name = 'testOverlay'
31 | const view = 'lib/types/html_test.html'
32 | const static = '..'
33 |
34 | const _name = 'testOverlay'
35 | const _html = '\n\n'
36 | const _type = 'html'
37 | const _directory = '..'
38 |
39 | let overlay = new html(name, view, static)
40 |
41 | assert.equal(overlay.name, _name)
42 | assert.equal(overlay.html, _html)
43 | assert.equal(overlay.type, _type)
44 | assert.equal(overlay.directory, _directory)
45 | });
46 | it('Errors if no name is provided', () => {
47 | const view = './html_test.html'
48 |
49 | const _err = 'You must specify a name for this overlay'
50 |
51 | assert.throws(() => {
52 | let overlay = new html(null, view)
53 | }, _err)
54 |
55 | })
56 | it('Errors if no view is provided', () => {
57 | const name = 'testOverlay'
58 |
59 | const _err = "You must specify an .html view"
60 |
61 | assert.throws(() => {
62 | let overlay = new html(name, null)
63 | }, _err)
64 | })
65 |
66 | afterEach(function() {
67 | sandbox.restore();
68 | });
69 | });
70 |
--------------------------------------------------------------------------------
/lib/types/html_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/lib/types/text.js:
--------------------------------------------------------------------------------
1 | /* text.js - Re-usable module to read text on your stream */
2 |
3 | var path = require('path');
4 |
5 | var eventbus; // Global event bus attached to TextOverlay object - used for passing events
6 |
7 | module.exports = TextOverlay;
8 |
9 | function TextOverlay(text) {
10 | // text - text to display and read aloud
11 |
12 | // Required parameters
13 | this.name = 'text'; // Unique identifier for event listeners
14 | this.type = 'text';
15 | this.directory = path.join(path.dirname('./static'));
16 | this.text = text;
17 | };
18 |
--------------------------------------------------------------------------------
/lib/types/text.spec.js:
--------------------------------------------------------------------------------
1 | /* Test for text.js */
2 |
3 | var assert = require('chai').assert;
4 | var sinon = require('sinon');
5 |
6 | let text
7 |
8 | describe('text.js - HTML Overlay Module', function() {
9 | beforeEach(function() {
10 | this.sinon = sandbox = sinon.sandbox.create();
11 | text = require('./text.js')
12 | });
13 | it('Returns an overlay when given parameters', () => {
14 | const input = 'whatever'
15 |
16 | const _name = 'text'
17 | const _type = 'text'
18 | const _text = 'whatever'
19 | const _directory = '.'
20 |
21 | let overlay = new text(text)
22 |
23 | assert.equal(overlay.name, _name)
24 | assert.equal(overlay.text, text)
25 | assert.equal(overlay.type, _type)
26 | assert.equal(overlay.directory, _directory)
27 | });
28 | afterEach(function() {
29 | sandbox.restore();
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/lib/types/video.js:
--------------------------------------------------------------------------------
1 | /* video.js - Re-usable module to create simple video overlays */
2 |
3 | var path = require('path');
4 |
5 | var eventbus; // Global event bus attached to AudioOverlay object - used for passing events
6 |
7 | module.exports = VideoOverlay;
8 |
9 | function VideoOverlay(name, file, volume) {
10 |
11 | if(!name) {
12 | throw new Error("You must specify a name for this overlay")
13 | }
14 | if(!file) {
15 | throw new Error("You must specify a Video URL");
16 | }
17 |
18 | // Required options
19 | this.name = name; // Unique identifier for event listeners
20 | this.type = 'video';
21 |
22 | // Assemble filename
23 | var filename = path.basename(file);
24 | var directory = path.join(path.dirname(file));
25 |
26 | this.directory = directory; // Path to file will be added as a static endpoint
27 |
28 | this.video = '/' + this.name + '/' + filename;
29 |
30 | // Set volume (optional)
31 | if(volume) {
32 | this.volume = volume;
33 | } else {
34 | this.volume = 1.0;
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/lib/types/video.spec.js:
--------------------------------------------------------------------------------
1 | /* Test for video.js */
2 |
3 | var assert = require('chai').assert;
4 | var sinon = require('sinon');
5 |
6 | let video
7 |
8 | describe('video.js - video Overlay Module', function() {
9 | beforeEach(function() {
10 | this.sinon = sandbox = sinon.sandbox.create();
11 | video = require('./video.js')
12 | });
13 | it('Returns an overlay when given parameters', () => {
14 | const name = 'testOverlay'
15 | const file = './video_test.mp4'
16 |
17 | const _name = 'testOverlay'
18 | const _video = '/testOverlay/video_test.mp4'
19 | const _type = 'video'
20 | const _directory = '.'
21 | const _volume = 1.0
22 |
23 | let overlay = new video(name, file)
24 |
25 | assert.equal(overlay.name, _name)
26 | assert.equal(overlay.video, _video)
27 | assert.equal(overlay.type, _type)
28 | assert.equal(overlay.directory, _directory)
29 | assert.equal(overlay.volume, _volume)
30 | });
31 | it('Returns a volume when provided', () => {
32 | const name = 'testOverlay'
33 | const file = './video_test.mp4'
34 | const volume = 0.3
35 |
36 | const _name = 'testOverlay'
37 | const _video = '/testOverlay/video_test.mp4'
38 | const _type = 'video'
39 | const _directory = '.'
40 | const _volume = 0.3
41 |
42 | let overlay = new video(name, file, volume)
43 |
44 | assert.equal(overlay.name, _name)
45 | assert.equal(overlay.video, _video)
46 | assert.equal(overlay.type, _type)
47 | assert.equal(overlay.directory, _directory)
48 | assert.equal(overlay.volume, _volume)
49 | });
50 | it('Errors if no name is provided', () => {
51 | const videoInput = './video_test.video'
52 |
53 | const _err = 'You must specify a name for this overlay'
54 |
55 | assert.throws(() => {
56 | let overlay = new video(null, videoInput)
57 | }, _err)
58 |
59 | })
60 | it('Errors if no view is provided', () => {
61 | const name = 'testOverlay'
62 |
63 | const _err = "You must specify a Video URL"
64 |
65 | assert.throws(() => {
66 | let overlay = new video(name, null)
67 | }, _err)
68 | })
69 |
70 | afterEach(function() {
71 | sandbox.restore();
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/lib/views/index.pug:
--------------------------------------------------------------------------------
1 | doctype html
2 | html(lang="en")
3 | head
4 | script(src='/socket.io/socket.io.js')
5 | script(src='http://code.responsivevoice.org/responsivevoice.js')
6 | link(href='/css/min.css' rel='stylesheet' type='text/css')
7 | body(bgcolor=transparent)
8 | #overlays
9 |
10 | // Load react client-side app
11 | script(src='/js/bundle.js')
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twitch-overlay",
3 | "version": "1.5.0",
4 | "description": "Overlay for Twitch streamers that can be loaded into OBS or XSplit",
5 | "keywords": [
6 | "twitch",
7 | "obs",
8 | "xsplit"
9 | ],
10 | "main": "index.js",
11 | "scripts": {
12 | "start": "node index.js",
13 | "build": "./node_modules/.bin/webpack",
14 | "dev": "./node_modules/.bin/webpack -d -w",
15 | "test": "mocha lib/*.spec.js lib/**/*.spec.js --exit",
16 | "watch": "mocha -w lib/*.spec.js lib/**/*.spec.js"
17 | },
18 | "engines": {
19 | "node": ">=8.0.0"
20 | },
21 | "author": "bdickason",
22 | "contributors": [
23 | {
24 | "name": "bdickason",
25 | "email": "dickason@gmail.com"
26 | }
27 | ],
28 | "repository": {
29 | "type": "git",
30 | "url": "git://github.com/bdickason/twitch-overlay.git"
31 | },
32 | "dependencies": {
33 | "babel-core": "^6.26.0",
34 | "babel-loader": "^7.1.2",
35 | "babel-preset-env": "^1.6.1",
36 | "babel-preset-react": "^6.24.1",
37 | "express": "4.15.2",
38 | "extend": "3.0.0",
39 | "pug": "2.0.0-beta11",
40 | "react": "^16.2.0",
41 | "react-dom": "^16.2.0",
42 | "react-html-parser": "^2.0.2",
43 | "shortid": "^2.2.8",
44 | "socket.io": "1.7.3",
45 | "webpack": "^3.8.1"
46 | },
47 | "license": "GPL-3.0",
48 | "devDependencies": {
49 | "chai": "^4.1.2",
50 | "css-loader": "^0.28.7",
51 | "mocha": "^4.0.1",
52 | "sinon": "^4.1.3",
53 | "style-loader": "^0.19.0"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 |
4 | var BUILD_DIR = path.resolve(__dirname, 'lib/static/js');
5 | var APP_DIR = path.resolve(__dirname, 'lib/client');
6 |
7 | var config = {
8 | devtool: 'source-map',
9 | entry: APP_DIR + '/overlay-client.jsx',
10 | output: {
11 | path: BUILD_DIR,
12 | filename: 'bundle.js'
13 | },
14 | module: {
15 | loaders: [
16 | {
17 | test: /\.js?/,
18 | include: APP_DIR,
19 | loader: 'babel-loader'
20 | },
21 | {
22 | test: /\.css$/,
23 | loader: 'style-loader!css-loader'
24 | }
25 | ]
26 | }
27 | };
28 |
29 | module.exports = config;
30 |
--------------------------------------------------------------------------------