├── .gitignore ├── .prettierrc ├── README.md ├── js ├── .env ├── .env.sample ├── .eslintrc.js ├── Dockerfile ├── README.md ├── babel.config.js ├── package.json ├── public │ └── index.html ├── src │ ├── component │ │ ├── App.vue │ │ └── main.js │ ├── config │ │ ├── App.vue │ │ └── main.js │ ├── live │ │ ├── App.vue │ │ └── main.js │ ├── overlay │ │ ├── App.vue │ │ └── main.js │ ├── panel │ │ ├── App.vue │ │ └── main.js │ └── shared │ │ ├── README.md │ │ ├── analytics.js │ │ ├── assets │ │ └── images │ │ │ └── README.md │ │ ├── globals.js │ │ ├── hooks │ │ └── use-medkit.js │ │ ├── scss │ │ └── base.scss │ │ └── views │ │ └── ComingSoon.vue └── vue.config.js └── ts ├── .browserslistrc ├── .dockerignore ├── .env ├── .env.sample ├── .eslintrc.js ├── Dockerfile ├── README.md ├── babel.config.js ├── package.json ├── public └── index.html ├── src ├── component │ ├── App.vue │ └── main.ts ├── config │ ├── App.vue │ └── main.ts ├── live │ ├── App.vue │ └── main.ts ├── overlay │ ├── App.vue │ └── main.ts ├── panel │ ├── App.vue │ └── main.ts └── shared │ ├── README.md │ ├── analytics.ts │ ├── assets │ └── images │ │ └── README.md │ ├── globals.ts │ ├── hooks │ └── use-medkit.ts │ ├── scss │ └── base.scss │ ├── types │ └── channel-state.ts │ └── views │ └── ComingSoon.vue ├── tsconfig.json └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | js/dist/ 3 | ts/dist/ 4 | node_modules/ 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false, 4 | "singleQuote": false, 5 | "trailingComma": "es5" 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Muxy Extension Skeleton 2 | 3 | The Muxy Extension Skeleton is a baseline project for twitch extensions using Muxy MEDKit. 4 | It comes in two flavors, the TypeScript version, in the `ts/` directory, and the JavaScript 5 | version, in the `js/` directory. 6 | 7 | ## Using the Skeleton 8 | 9 | 1. Install nodejs from https://nodejs.org/en/ . This should install node and npm. Make sure 10 | that the node and npm binaries are in your PATH. 11 | 12 | 2. Choose what language you're using, either TypeScript or JavaScript, and copy the corresponding 13 | folder to a new directory, naming it something relevant to your project. 14 | 15 | 3. Open the README.md file in that directory to continue. 16 | -------------------------------------------------------------------------------- /js/.env: -------------------------------------------------------------------------------- 1 | VUE_APP_CLIENT_ID=1234 2 | VUE_APP_CLIENT_ID=1234 3 | -------------------------------------------------------------------------------- /js/.env.sample: -------------------------------------------------------------------------------- 1 | VUE_APP_CLIENT_ID=notaclientid -------------------------------------------------------------------------------- /js/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/prettier", 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020 13 | }, 14 | rules: { 15 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 16 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /js/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.8.0 2 | 3 | ARG VUE_APP_CLIENT_ID 4 | ENV VUE_APP_CLIENT_ID $VUE_APP_CLIENT_ID 5 | RUN test -n "$VUE_APP_CLIENT_ID" || (echo "Docker arg VUE_APP_CLIENT_ID unset" && false) 6 | 7 | ARG VUE_APP_UA_STRING 8 | ENV VUE_APP_UA_STRING $VUE_APP_UA_STRING 9 | 10 | COPY package.json . 11 | 12 | RUN npm install 13 | 14 | COPY . . 15 | 16 | RUN npm run build 17 | -------------------------------------------------------------------------------- /js/README.md: -------------------------------------------------------------------------------- 1 | # MEDKit Vue Skeleton Extension - JavaScript 2 | 3 | ## Finish Setup 4 | 5 | 1. Copy `.env.sample` to `.env` 6 | 7 | 2. Edit the variable `VUE_APP_CLIENT_ID` to match the Twitch Extension Client ID you created on the [Twitch Dev Console](https://dev.twitch.tv). 8 | 9 | 3. Install dependencies: `npm install` 10 | 11 | 4. Run the local server to develop your extension: `npm run serve`. This will start a local server with hot reloading. Your various extension pages can be accessed at: 12 | - http://localhost:4000/config.html - The broadcaster configuration 13 | - http://localhost:4000/live.html - The broadcaster live dashboard 14 | - http://localhost:4000/panel.html - The viewer panel extension 15 | - http://localhost:4000/component.html - The viewer component extension 16 | - http://localhost:4000/overlay.html - The viewer overlay extension 17 | 18 | More information about the different types of extensions and broadcaster pages can be found [here](https://docs.muxy.io/docs/getting-started-with-medkit). 19 | 20 | 5. When you are ready to release your extension, run `npm run build`. This will create a `dist/` folder with compiled versions of the pages as well as a ZIP file containing all HTML, JS and other assets bundled together. You may upload this ZIP directly to Twitch's upload page. 21 | -------------------------------------------------------------------------------- /js/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | }; 4 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PROJECT_NAME", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@muxy/extensions-js": "^2.4.3", 12 | "core-js": "^3.18.3", 13 | "vue": "^3.0.0" 14 | }, 15 | "devDependencies": { 16 | "@vue/cli-plugin-babel": "~4.5.0", 17 | "@vue/cli-plugin-eslint": "~4.5.0", 18 | "@vue/cli-service": "~4.5.0", 19 | "@vue/compiler-sfc": "^3.0.0", 20 | "@vue/eslint-config-prettier": "^6.0.0", 21 | "eslint": "^6.7.2", 22 | "eslint-plugin-prettier": "^3.1.3", 23 | "eslint-plugin-vue": "^7.0.0-0", 24 | "lint-staged": "^9.5.0", 25 | "prettier": "^1.19.1", 26 | "sass": "^1.26.5", 27 | "sass-loader": "^8.0.2", 28 | "vue-svg-inline-loader": "^1.5.0", 29 | "zip-webpack-plugin": "^3.0.0" 30 | }, 31 | "gitHooks": { 32 | "pre-commit": "lint-staged" 33 | }, 34 | "lint-staged": { 35 | "*.{js,jsx,vue,ts,tsx}": [ 36 | "vue-cli-service lint", 37 | "git add" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /js/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | MEDKit-Powered Extension 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /js/src/component/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 57 | 58 | 70 | -------------------------------------------------------------------------------- /js/src/component/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | 3 | import App from "./App.vue"; 4 | 5 | createApp(App).mount("#app"); 6 | -------------------------------------------------------------------------------- /js/src/config/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 57 | 58 | 70 | -------------------------------------------------------------------------------- /js/src/config/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | 3 | import App from "./App.vue"; 4 | 5 | createApp(App).mount("#app"); 6 | -------------------------------------------------------------------------------- /js/src/live/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 57 | 58 | 70 | -------------------------------------------------------------------------------- /js/src/live/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | 3 | import App from "./App.vue"; 4 | 5 | createApp(App).mount("#app"); 6 | -------------------------------------------------------------------------------- /js/src/overlay/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 57 | 58 | 70 | -------------------------------------------------------------------------------- /js/src/overlay/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | 3 | import App from "./App.vue"; 4 | 5 | createApp(App).mount("#app"); 6 | -------------------------------------------------------------------------------- /js/src/panel/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 57 | 58 | 70 | -------------------------------------------------------------------------------- /js/src/panel/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | 3 | import App from "./App.vue"; 4 | 5 | createApp(App).mount("#app"); 6 | -------------------------------------------------------------------------------- /js/src/shared/README.md: -------------------------------------------------------------------------------- 1 | Files that are shared between extension types (overlay, config, etc.) should live in this folder. 2 | As well, assets like images, fonts or stylesheets. 3 | -------------------------------------------------------------------------------- /js/src/shared/analytics.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef { import("@muxy/extensions-js").MEDKit } MEDKit 3 | */ 4 | 5 | /** 6 | * @typedef {Object} AnalyticsEvent 7 | * @property {string} action - The action of the analytics event, like 'heartbeat' or 'page-view' 8 | * @property {number} value - The value associated with the analytics event 9 | * @property {string} label - The label associated with the analytics event 10 | **/ 11 | 12 | /** 13 | * @type {MEDKit} 14 | **/ 15 | let medkit = null; 16 | let category = "SET_APP_CATEGORY"; 17 | 18 | // Send a keep alive heartbeat to the analytics system every minute. 19 | const HEARTBEAT_TIMEOUT_MS = 60 * 1000; 20 | 21 | export default { 22 | /** 23 | * Sets the cached medkit instance 24 | * 25 | * @param {MEDKit} appMedkit - MEDKit instance 26 | **/ 27 | setMEDKit(appMedkit) { 28 | medkit = appMedkit; 29 | }, 30 | 31 | /** 32 | * Sets the cached category 33 | * 34 | * @param {string} appCategory - category 35 | **/ 36 | setCategory(appCategory) { 37 | category = appCategory; 38 | }, 39 | 40 | /** 41 | * Sends a single event namespaced to the provided category. 42 | * 43 | * @param {AnalyticsEvent} event - The event itself 44 | * @returns {Promise} - Promise that is resolved when the analytics event is sent. 45 | * Resolves instantly if analytics isn't enabled. 46 | */ 47 | async sendEvent(event) { 48 | if (!medkit?.analytics) { 49 | return Promise.resolve(); 50 | } 51 | 52 | await medkit?.loaded(); 53 | return medkit?.analytics.sendEvent( 54 | category, 55 | event.action, 56 | event.value, 57 | event.label 58 | ); 59 | }, 60 | 61 | /** 62 | * Starts a cycle of sending live heartbeat events. 63 | */ 64 | startKeepAliveHeartbeat() { 65 | this.sendEvent({ 66 | action: "heartbeat", 67 | value: 1, 68 | label: "Viewer heartbeat", 69 | }); 70 | 71 | window.setTimeout(() => { 72 | this.startKeepAliveHeartbeat(); 73 | }, HEARTBEAT_TIMEOUT_MS); 74 | }, 75 | 76 | /** 77 | * Send a single "pageview" event. 78 | * 79 | * @returns {Promise} - Promise that is resolved when the analytics event is sent. 80 | * Resolves instantly if analytics isn't enabled. 81 | */ 82 | async sendPageView() { 83 | if (!medkit?.analytics) { 84 | return Promise.resolve(); 85 | } 86 | 87 | await medkit?.loaded(); 88 | return medkit?.analytics.pageView(); 89 | }, 90 | }; 91 | -------------------------------------------------------------------------------- /js/src/shared/assets/images/README.md: -------------------------------------------------------------------------------- 1 | Extension images go here. 2 | -------------------------------------------------------------------------------- /js/src/shared/globals.js: -------------------------------------------------------------------------------- 1 | // This module maps globals injected by Webpack (or from other sources) 2 | // to an accessible module usable by our code. 3 | 4 | /** 5 | * @typedef {Object} Globals 6 | * 7 | * @property {boolean} ANALYTICS - Boolean if analytics is enabled or not 8 | * @property {string} CLIENT_ID - Client ID, as obtained from twitch. 9 | * @property {boolean} PRODUCTION - true if this is being built in production. 10 | * @property {string} UA_STRING - UA string, as obtained from google analytics 11 | * @property {string | undefined} TESTING_CHANNEL_ID - string 12 | * @property {string | undefined} TESTING_USER_ID - Boolean if analytics is enabled or not 13 | **/ 14 | 15 | /** 16 | * @type Globals 17 | **/ 18 | const g = { 19 | ANALYTICS: process.env.VUE_APP_ANALYTICS, 20 | CLIENT_ID: process.env.VUE_APP_CLIENT_ID, 21 | PRODUCTION: process.env.NODE_ENV === "production", 22 | UA_STRING: process.env.VUE_APP_UA_STRING, 23 | 24 | TESTING_CHANNEL_ID: process.env.VUE_APP_TESTING_CHANNEL_ID, 25 | TESTING_USER_ID: process.env.VUE_APP_TESTING_USER_ID, 26 | }; 27 | 28 | export default g; 29 | -------------------------------------------------------------------------------- /js/src/shared/hooks/use-medkit.js: -------------------------------------------------------------------------------- 1 | import { inject, provide } from "vue"; 2 | import Muxy from "@muxy/extensions-js"; 3 | 4 | // MEDKit Options 5 | /** 6 | * @typedef {Object} VueMEDKitOptions 7 | * @property {string} clientId - Client ID, as obtained from twitch. 8 | * @property {string?} channelId - Channel ID, used for testing. Optional. 9 | * @property {string?} environment - Override environment instead of one derived by MEDKit 10 | * @property {string?} jwt - Override JWT, isntead of using the provided one. 11 | * @property {string?} role - JWT role, used in sandbox only, to override the provided one. 12 | * @property {string?} uaString - UA string for analytics. 13 | * @property {string?} url - URL to use instead of the default muxy API endpoints. 14 | * @property {string?} userId - User ID, used for testing. 15 | **/ 16 | 17 | /** 18 | * Symbol to be used in vue injection 19 | **/ 20 | const MEDKitInjectionKey = Symbol("medkit"); 21 | 22 | /** 23 | * Sets up MEDKit so that the useMEDKit function will return a valid MEDKit instance 24 | * @param {VueMEDKitOptions} options - Options 25 | */ 26 | export function provideMEDKit(options) { 27 | if (!options.clientId) { 28 | throw new Error("Must specify client id when using the MEDKit Vue plugin"); 29 | } 30 | 31 | const opts = new Muxy.DebuggingOptions(); 32 | opts.role(options.role || "viewer"); 33 | 34 | if (options.environment) { 35 | opts.environment(options.environment); 36 | } 37 | 38 | if (options.jwt) { 39 | opts.jwt(options.jwt); 40 | } 41 | 42 | if (options.channelId) { 43 | opts.channelID(options.channelId); 44 | } 45 | 46 | if (options.userId) { 47 | opts.userID(options.userId); 48 | } 49 | 50 | if (options.url) { 51 | opts.url(options.url); 52 | } 53 | 54 | const setup = { 55 | clientID: options.clientId, 56 | }; 57 | 58 | if (options.uaString) { 59 | setup.uaString = options.uaString; 60 | } 61 | 62 | Muxy.debug(opts); 63 | Muxy.setup(setup); 64 | 65 | const medkit = new Muxy.SDK(); 66 | provide(MEDKitInjectionKey, medkit); 67 | 68 | return medkit; 69 | } 70 | 71 | export function useMEDKit() { 72 | const medkit = inject(MEDKitInjectionKey); 73 | if (!medkit) { 74 | throw new Error("MEDKit could not be created"); 75 | } 76 | 77 | return { medkit }; 78 | } 79 | -------------------------------------------------------------------------------- /js/src/shared/scss/base.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: Roboto, sans-serif; 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | #app { 10 | height: 100%; 11 | width: 100%; 12 | } 13 | } -------------------------------------------------------------------------------- /js/src/shared/views/ComingSoon.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 57 | 58 | 72 | -------------------------------------------------------------------------------- /js/vue.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const path = require("path"); 4 | const ZipPlugin = require("zip-webpack-plugin"); 5 | 6 | const pkg = require("./package.json"); 7 | 8 | const config = { 9 | publicPath: "./", 10 | pages: { 11 | component: "./src/component/main.js", 12 | config: "./src/config/main.js", 13 | live: "./src/live/main.js", 14 | overlay: "./src/overlay/main.js", 15 | panel: "./src/panel/main.js", 16 | }, 17 | 18 | chainWebpack: config => { 19 | config.resolve.alias.set("@", path.resolve(__dirname, "src")); 20 | 21 | // Zip Plugin 22 | config 23 | .plugin("zip-plugin") 24 | .use(ZipPlugin, [{ filename: `${pkg.name}.zip` }]); 25 | 26 | // ESLint autofix 27 | config.module 28 | .rule("eslint") 29 | .use("eslint-loader") 30 | .options({ 31 | fix: true, 32 | }); 33 | }, 34 | 35 | devServer: { 36 | host: "0.0.0.0", 37 | port: 4000, 38 | }, 39 | }; 40 | 41 | module.exports = config; 42 | -------------------------------------------------------------------------------- /ts/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /ts/.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | Dockerfile 3 | -------------------------------------------------------------------------------- /ts/.env: -------------------------------------------------------------------------------- 1 | VUE_APP_CLIENT_ID=1234 2 | VUE_APP_CLIENT_ID=1234 3 | -------------------------------------------------------------------------------- /ts/.env.sample: -------------------------------------------------------------------------------- 1 | VUE_APP_CLIENT_ID=notaclientid 2 | -------------------------------------------------------------------------------- /ts/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/typescript/recommended", 10 | "@vue/prettier", 11 | "@vue/prettier/@typescript-eslint" 12 | ], 13 | parserOptions: { 14 | ecmaVersion: 2020 15 | }, 16 | rules: { 17 | "no-console": process.env.NODE_ENV === "production" ? "warn" : "off", 18 | "no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off" 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /ts/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.8.0 2 | 3 | ARG VUE_APP_CLIENT_ID 4 | ENV VUE_APP_CLIENT_ID $VUE_APP_CLIENT_ID 5 | RUN test -n "$VUE_APP_CLIENT_ID" || (echo "Docker arg VUE_APP_CLIENT_ID unset" && false) 6 | 7 | ARG VUE_APP_UA_STRING 8 | ENV VUE_APP_UA_STRING $VUE_APP_UA_STRING 9 | 10 | COPY package.json . 11 | 12 | RUN npm install 13 | 14 | COPY . . 15 | 16 | RUN npm run build 17 | -------------------------------------------------------------------------------- /ts/README.md: -------------------------------------------------------------------------------- 1 | # MEDKit Vue Skeleton Extension - TypeScript 2 | 3 | ## Finish Setup 4 | 5 | 1. Copy `.env.sample` to `.env` 6 | 7 | 2. Edit the variable `VUE_APP_CLIENT_ID` to match the Twitch Extension Client ID you created on the [Twitch Dev Console](https://dev.twitch.tv). 8 | 9 | 3. Install dependencies: `npm install` 10 | 11 | 4. Run the local server to develop your extension: `npm run serve`. This will start a local server with hot reloading. Your various extension pages can be accessed at: 12 | - http://localhost:4000/config.html - The broadcaster configuration 13 | - http://localhost:4000/live.html - The broadcaster live dashboard 14 | - http://localhost:4000/panel.html - The viewer panel extension 15 | - http://localhost:4000/component.html - The viewer component extension 16 | - http://localhost:4000/overlay.html - The viewer overlay extension 17 | 18 | More information about the different types of extensions and broadcaster pages can be found [here](https://docs.muxy.io/docs/getting-started-with-medkit). 19 | 20 | 5. When you are ready to release your extension, run `npm run build`. This will create a `dist/` folder with compiled versions of the pages as well as a ZIP file containing all HTML, JS and other assets bundled together. You may upload this ZIP directly to Twitch's upload page. 21 | -------------------------------------------------------------------------------- /ts/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@vue/cli-plugin-babel/preset"] 3 | }; 4 | -------------------------------------------------------------------------------- /ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PROJECT_NAME", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@muxy/extensions-js": "^2.4.3", 12 | "vue": "^3.0.0" 13 | }, 14 | "devDependencies": { 15 | "@typescript-eslint/eslint-plugin": "^4.32.0", 16 | "@typescript-eslint/parser": "^4.32.0", 17 | "@vue/cli-plugin-babel": "~4.5.13", 18 | "@vue/cli-plugin-eslint": "~4.5.13", 19 | "@vue/cli-plugin-typescript": "~4.5.13", 20 | "@vue/cli-service": "~4.5.13", 21 | "@vue/compiler-sfc": "^3.2.19", 22 | "@vue/eslint-config-prettier": "^6.0.0", 23 | "@vue/eslint-config-typescript": "^7.0.0", 24 | "core-js": "^3.18.1", 25 | "eslint": "^6.0.0", 26 | "eslint-plugin-prettier": "^3.1.0", 27 | "eslint-plugin-vue": "^7.18.0", 28 | "lint-staged": "^11.1.2", 29 | "prettier": "^2.4.1", 30 | "sass": "^1.42.1", 31 | "sass-loader": "^10.2.0", 32 | "typescript": "^4.4.3", 33 | "webpack": "^4.46.0", 34 | "zip-webpack-plugin": "^4.0.1" 35 | }, 36 | "gitHooks": { 37 | "pre-commit": "lint-staged" 38 | }, 39 | "lint-staged": { 40 | "*.{js,jsx,vue,ts,tsx}": [ 41 | "vue-cli-service lint", 42 | "git add" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ts/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | MEDKit-Powered Extension 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /ts/src/component/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 59 | 60 | 72 | -------------------------------------------------------------------------------- /ts/src/component/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | 3 | import App from "./App.vue"; 4 | 5 | createApp(App).mount("#app"); 6 | -------------------------------------------------------------------------------- /ts/src/config/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 59 | 60 | 72 | -------------------------------------------------------------------------------- /ts/src/config/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | 3 | import App from "./App.vue"; 4 | 5 | createApp(App).mount("#app"); 6 | -------------------------------------------------------------------------------- /ts/src/live/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 59 | 60 | 72 | -------------------------------------------------------------------------------- /ts/src/live/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | 3 | import App from "./App.vue"; 4 | 5 | createApp(App).mount("#app"); 6 | -------------------------------------------------------------------------------- /ts/src/overlay/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 59 | 60 | 76 | -------------------------------------------------------------------------------- /ts/src/overlay/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | 3 | import App from "./App.vue"; 4 | 5 | createApp(App).mount("#app"); 6 | -------------------------------------------------------------------------------- /ts/src/panel/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 59 | 60 | 72 | -------------------------------------------------------------------------------- /ts/src/panel/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | 3 | import App from "./App.vue"; 4 | 5 | createApp(App).mount("#app"); 6 | -------------------------------------------------------------------------------- /ts/src/shared/README.md: -------------------------------------------------------------------------------- 1 | Files that are shared between extension types (overlay, config, etc.) should live in this folder. 2 | As well, assets like images, fonts or stylesheets. 3 | -------------------------------------------------------------------------------- /ts/src/shared/analytics.ts: -------------------------------------------------------------------------------- 1 | import { MEDKit } from "@muxy/extensions-js"; 2 | 3 | interface AnalyticsEvent { 4 | action: string; 5 | value: number; 6 | label: string; 7 | } 8 | 9 | let medkit: MEDKit | null = null; 10 | let category = "SET_APP_CATEGORY"; 11 | 12 | // Send a keep alive heartbeat to the analytics system every minute. 13 | const HEARTBEAT_TIMEOUT_MS = 60 * 1000; 14 | 15 | export default { 16 | setMEDKit(appMedkit: MEDKit): void { 17 | medkit = appMedkit; 18 | }, 19 | 20 | setCategory(appCategory: string): void { 21 | category = appCategory; 22 | }, 23 | 24 | // Sends a single event namespaced to the provided category. 25 | async sendEvent(event: AnalyticsEvent): Promise { 26 | if (!medkit?.analytics) { 27 | console.error( 28 | "MEDKit analytics has not been initialized. Make sure to set the `UA_STRING` environment variable." 29 | ); 30 | return Promise.resolve(); 31 | } 32 | 33 | await medkit?.loaded(); 34 | return medkit?.analytics.sendEvent( 35 | category, 36 | event.action, 37 | event.value, 38 | event.label 39 | ); 40 | }, 41 | 42 | // Starts a cycle of sending live heartbeat events. 43 | startKeepAliveHeartbeat(): void { 44 | this.sendEvent({ 45 | action: "heartbeat", 46 | value: 1, 47 | label: "Viewer heartbeat", 48 | }); 49 | 50 | window.setTimeout(() => { 51 | this.startKeepAliveHeartbeat(); 52 | }, HEARTBEAT_TIMEOUT_MS); 53 | }, 54 | 55 | // Send a single "pageview" event. 56 | async sendPageView(): Promise { 57 | if (!medkit?.analytics) { 58 | console.error( 59 | "MEDKit analytics has not been initialized. Make sure to set the `UA_STRING` environment variable." 60 | ); 61 | return Promise.resolve(); 62 | } 63 | 64 | await medkit?.loaded(); 65 | return medkit?.analytics.pageView(); 66 | }, 67 | }; 68 | -------------------------------------------------------------------------------- /ts/src/shared/assets/images/README.md: -------------------------------------------------------------------------------- 1 | Extension images/fonts/other files go here. 2 | -------------------------------------------------------------------------------- /ts/src/shared/globals.ts: -------------------------------------------------------------------------------- 1 | // This module maps globals injected by Webpack (or from other sources) 2 | // to an accessible module usable by our code. 3 | export class Globals { 4 | public ANALYTICS!: boolean; 5 | public CLIENT_ID!: string; 6 | public PRODUCTION!: boolean; 7 | public UA_STRING!: string; 8 | 9 | public TESTING_CHANNEL_ID!: string | undefined; 10 | public TESTING_USER_ID!: string | undefined; 11 | } 12 | 13 | declare let process: { 14 | env: { 15 | NODE_ENV: string; 16 | VUE_APP_ANALYTICS: boolean; 17 | VUE_APP_CLIENT_ID: string; 18 | VUE_APP_TESTING_CHANNEL_ID: string; 19 | VUE_APP_TESTING_USER_ID: string; 20 | VUE_APP_UA_STRING: string; 21 | }; 22 | }; 23 | 24 | const g: Globals = { 25 | ANALYTICS: process.env.VUE_APP_ANALYTICS, 26 | CLIENT_ID: process.env.VUE_APP_CLIENT_ID, 27 | PRODUCTION: process.env.NODE_ENV === "production", 28 | UA_STRING: process.env.VUE_APP_UA_STRING, 29 | 30 | TESTING_CHANNEL_ID: process.env.VUE_APP_TESTING_CHANNEL_ID, 31 | TESTING_USER_ID: process.env.VUE_APP_TESTING_USER_ID, 32 | }; 33 | 34 | export default g; 35 | -------------------------------------------------------------------------------- /ts/src/shared/hooks/use-medkit.ts: -------------------------------------------------------------------------------- 1 | import { inject, provide, InjectionKey } from "vue"; 2 | 3 | import Muxy, { MEDKit, SetupOptions } from "@muxy/extensions-js"; 4 | 5 | // MEDKit Options 6 | interface VueMEDKitOptions { 7 | clientId: string; 8 | 9 | channelId?: string; 10 | environment?: string; 11 | jwt?: string; 12 | role?: string; 13 | uaString?: string; 14 | url?: string; 15 | userId?: string; 16 | } 17 | 18 | const MEDKitInjectionKey: InjectionKey = Symbol("medkit"); 19 | 20 | // Initialized and returns an instance of the MEDKit extension 21 | // SDK. The instance is also provided to the Vue injection system 22 | // for easier retrieval using the `useMEDKit` helper. 23 | export function provideMEDKit(options: VueMEDKitOptions): MEDKit { 24 | if (!options.clientId) { 25 | throw new Error("Must specify client id when using the MEDKit Vue plugin"); 26 | } 27 | 28 | const opts = new Muxy.DebuggingOptions(); 29 | opts.role(options.role || "viewer"); 30 | 31 | if (options.environment) { 32 | opts.environment(options.environment); 33 | } 34 | 35 | if (options.jwt) { 36 | opts.jwt(options.jwt); 37 | } 38 | 39 | if (options.channelId) { 40 | opts.channelID(options.channelId); 41 | } 42 | 43 | if (options.userId) { 44 | opts.userID(options.userId); 45 | } 46 | 47 | if (options.url) { 48 | opts.url(options.url); 49 | } 50 | 51 | const setup: SetupOptions = { 52 | clientID: options.clientId, 53 | }; 54 | 55 | if (options.uaString) { 56 | setup.uaString = options.uaString; 57 | } 58 | 59 | Muxy.debug(opts); 60 | Muxy.setup(setup); 61 | 62 | const medkit = new Muxy.SDK(); 63 | provide(MEDKitInjectionKey, medkit); 64 | 65 | return medkit; 66 | } 67 | 68 | type UseMEDKitContext = { 69 | medkit: MEDKit; 70 | }; 71 | 72 | // Returns the provided MEDKit instance created by an earlier call 73 | // to `provideMEDKit` 74 | export function useMEDKit(): UseMEDKitContext { 75 | const medkit = inject(MEDKitInjectionKey); 76 | if (!medkit) { 77 | throw new Error("MEDKit could not be initialized"); 78 | } 79 | 80 | return { medkit }; 81 | } 82 | -------------------------------------------------------------------------------- /ts/src/shared/scss/base.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | #app { 10 | height: 100%; 11 | width: 100%; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ts/src/shared/types/channel-state.ts: -------------------------------------------------------------------------------- 1 | // The expected shape of channel state passed over the network. 2 | export interface ChannelState { 3 | show_coming_soon: boolean; 4 | } 5 | -------------------------------------------------------------------------------- /ts/src/shared/views/ComingSoon.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 58 | 59 | 73 | -------------------------------------------------------------------------------- /ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": ["webpack-env"], 15 | "paths": { 16 | "@/*": ["src/*"] 17 | }, 18 | "lib": ["esnext", "dom", "dom.iterable", "scripthost"] 19 | }, 20 | "include": [ 21 | "src/**/*.ts", 22 | "src/**/*.tsx", 23 | "src/**/*.vue", 24 | "tests/**/*.ts", 25 | "tests/**/*.tsx" 26 | ], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /ts/vue.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const path = require("path"); 4 | const ZipPlugin = require("zip-webpack-plugin"); 5 | 6 | const pkg = require("./package.json"); 7 | 8 | const config = { 9 | publicPath: "./", 10 | pages: { 11 | component: "./src/component/main.ts", 12 | config: "./src/config/main.ts", 13 | live: "./src/live/main.ts", 14 | overlay: "./src/overlay/main.ts", 15 | panel: "./src/panel/main.ts", 16 | }, 17 | 18 | chainWebpack: config => { 19 | config.resolve.alias.set("@", path.resolve(__dirname, "src")); 20 | 21 | // Zip Plugin 22 | config 23 | .plugin("zip-plugin") 24 | .use(ZipPlugin, [{ filename: `${pkg.name}.zip` }]); 25 | 26 | // ESLint autofix 27 | config.module 28 | .rule("eslint") 29 | .use("eslint-loader") 30 | .options({ 31 | fix: true, 32 | }); 33 | }, 34 | 35 | devServer: { 36 | host: "0.0.0.0", 37 | port: 4000, 38 | }, 39 | }; 40 | 41 | module.exports = config; 42 | --------------------------------------------------------------------------------