├── .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 |
2 |
3 |
Component Extension
4 |
5 |
6 |
7 |
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 |
2 |
3 |
Broadcaster Configuration
4 |
5 |
6 |
7 |
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 |
2 |
3 |
Broadcaster Live Dashboard
4 |
5 |
6 |
7 |
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 |
2 |
3 |
Overlay Extension
4 |
5 |
6 |
7 |
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 |
2 |
3 |
Panel Extension
4 |
5 |
6 |
7 |
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 |
2 | COMING SOON
3 |
4 | MEDKit User Info
5 | {{ medkitInfo }}
6 |
7 | Current Channel State
8 | {{ channelState }}
9 |
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 |
2 |
3 |
Component Extension
4 |
5 |
6 |
7 |
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 |
2 |
3 |
Broadcaster Configuration
4 |
5 |
6 |
7 |
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 |
2 |
3 |
Broadcaster Live Dashboard
4 |
5 |
6 |
7 |
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 |
2 |
3 |
Overlay Extension
4 |
5 |
6 |
7 |
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 |
2 |
3 |
Panel Extension
4 |
5 |
6 |
7 |
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 |
2 | COMING SOON
3 |
4 | MEDKit User Info
5 | {{ medkitInfo }}
6 |
7 | Current Channel State
8 | {{ channelState }}
9 |
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 |
--------------------------------------------------------------------------------