├── .gitattributes
├── .github
├── FUNDING.yml
├── copilot-instructions.md
├── dependabot.yml
└── workflows
│ ├── build-and-lint.yml
│ ├── build-and-release.yml
│ ├── codeql-analysis.yml
│ └── manual-storybook-release.yml
├── .gitignore
├── .npmignore
├── .prettierignore
├── .prettierrc
├── .storybook
├── .config.js
├── main.mjs
└── preview.js
├── .vscode
└── settings.json
├── CHANGELOG.md
├── LICENSE
├── README.md
├── dist
├── index.d.ts
├── react-twitch-embed-video.js
└── react-twitch-embed-video.umd.cjs
├── eslint.config.js
├── lefthook.yml
├── package.json
├── pnpm-lock.yaml
├── src
├── @types
│ └── types.ts
├── global.d.ts
├── index.tsx
├── loadEmbedApi.test.tsx
├── loadEmbedApi.tsx
├── stories
│ ├── ChannelExample.stories.tsx
│ ├── Introduction.mdx
│ ├── MultiplePlayers.stories.tsx
│ └── VODExample.stories.tsx
├── test
│ └── setup.ts
├── useEventListener.test.tsx
├── useEventListener.tsx
├── usePlayerPlay.test.tsx
├── usePlayerPlay.tsx
├── usePlayerReady.test.tsx
├── usePlayerReady.tsx
├── useTwitchEmbed.test.tsx
├── useTwitchEmbed.tsx
└── utils
│ ├── constants.ts
│ ├── enforceAutoPlay.test.tsx
│ ├── enforceAutoPlay.tsx
│ ├── enforceVolume.test.tsx
│ ├── enforceVolume.tsx
│ ├── index.ts
│ └── tuplify.ts
├── test-code-style.md
├── tsconfig.json
└── vite.config.mjs
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | # Custom for Visual Studio
5 | *.cs diff=csharp
6 |
7 | # Standard to msysgit
8 | *.doc diff=astextplain
9 | *.DOC diff=astextplain
10 | *.docx diff=astextplain
11 | *.DOCX diff=astextplain
12 | *.dot diff=astextplain
13 | *.DOT diff=astextplain
14 | *.pdf diff=astextplain
15 | *.PDF diff=astextplain
16 | *.rtf diff=astextplain
17 | *.RTF diff=astextplain
18 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | patreon: talk2megooseman
4 | github: talk2megooseman
5 |
--------------------------------------------------------------------------------
/.github/copilot-instructions.md:
--------------------------------------------------------------------------------
1 | Always use React functional components.
2 |
3 | Declare functions before they are used.
4 |
5 | Define new types in types.ts file.
6 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: npm
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 10
8 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint Code and Verify
2 |
3 | on:
4 | push:
5 | branches:
6 | - development
7 |
8 | jobs:
9 | build:
10 |
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: pnpm/action-setup@v4
16 | with:
17 | version: 9
18 | run_install: false
19 | - uses: actions/setup-node@v4
20 | with:
21 | node-version: '20'
22 | registry-url: 'https://registry.npmjs.org'
23 | cache: 'pnpm'
24 | - name: install
25 | run: |
26 | pnpm install
27 |
28 | - name: lint
29 | run: |
30 | pnpm lint
31 |
32 | - name: Check Build Storybook
33 | run: |
34 | pnpm release-storybook
35 |
36 | - name: Check Build Package Release
37 | run: |
38 | pnpm release
39 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-release.yml:
--------------------------------------------------------------------------------
1 | name: Build and Release Tagged Version
2 |
3 | on:
4 | release:
5 | types: [published]
6 |
7 | jobs:
8 | build_and_publish:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v4
13 | - uses: pnpm/action-setup@v4
14 | with:
15 | version: 9
16 | run_install: false
17 | - uses: actions/setup-node@v4
18 | with:
19 | node-version: '20'
20 | registry-url: 'https://registry.npmjs.org'
21 | cache: 'pnpm'
22 | - name: pnpm install
23 | run: |
24 | pnpm install
25 |
26 | - name: pnpm lint
27 | run: |
28 | pnpm lint
29 |
30 | - name: Build Package Release
31 | run: |
32 | pnpm release
33 |
34 | - run: npm publish
35 | env:
36 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN}}
37 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "Code scanning - action"
2 |
3 | on:
4 | push:
5 | branches: [development, master]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [development]
9 | schedule:
10 | - cron: '0 11 * * 6'
11 |
12 | jobs:
13 | CodeQL-Build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | steps:
18 | - name: Checkout repository
19 | uses: actions/checkout@v2
20 | with:
21 | # We must fetch at least the immediate parents so that if this is
22 | # a pull request then we can checkout the head.
23 | fetch-depth: 2
24 |
25 | # If this run was triggered by a pull request event, then checkout
26 | # the head of the pull request instead of the merge commit.
27 | - run: git checkout HEAD^2
28 | if: ${{ github.event_name == 'pull_request' }}
29 |
30 | # Initializes the CodeQL tools for scanning.
31 | - name: Initialize CodeQL
32 | uses: github/codeql-action/init@v1
33 | # Override language selection by uncommenting this and choosing your languages
34 | # with:
35 | # languages: go, javascript, csharp, python, cpp, java
36 |
37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
38 | # If this step fails, then you should remove it and run the build manually (see below)
39 | - name: Autobuild
40 | uses: github/codeql-action/autobuild@v1
41 |
42 | # ℹ️ Command-line programs to run using the OS shell.
43 | # 📚 https://git.io/JvXDl
44 |
45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
46 | # and modify them (or add more) to build your code if your project
47 | # uses a compiled language
48 |
49 | #- run: |
50 | # make bootstrap
51 | # make release
52 |
53 | - name: Perform CodeQL Analysis
54 | uses: github/codeql-action/analyze@v1
55 |
--------------------------------------------------------------------------------
/.github/workflows/manual-storybook-release.yml:
--------------------------------------------------------------------------------
1 | name: Publish Storybook GitHub Page
2 |
3 | on: workflow_dispatch
4 |
5 | jobs:
6 | build:
7 | # Specify runner + deployment step
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: pnpm/action-setup@v4
13 | with:
14 | version: 9
15 | run_install: false
16 | - uses: actions/setup-node@v4
17 | with:
18 | node-version: '20'
19 | registry-url: 'https://registry.npmjs.org'
20 | cache: 'pnpm'
21 | - name: pnpm install
22 | run: |
23 | pnpm install
24 |
25 | - name: Build Storybook
26 | run: |
27 | pnpm release-storybook
28 |
29 | - name: Upload artifact
30 | uses: actions/upload-pages-artifact@v3
31 | with:
32 | path: '.out/'
33 |
34 | storybook-to-pages:
35 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment
36 | permissions:
37 | contents: read
38 | pages: write
39 | id-token: write
40 |
41 | # Deploy to the github-pages environment
42 | environment:
43 | name: github-pages
44 | url: ${{ steps.deployment.outputs.page_url }}
45 |
46 | needs: build
47 | runs-on: ubuntu-latest
48 | name: Deploy
49 | steps:
50 | - name: Deploy to GitHub Pages
51 | id: deployment
52 | uses: actions/deploy-pages@v4
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore github pages builds
2 | out*
3 |
4 | .env
5 |
6 | # Logs
7 | logs
8 | *.log
9 | npm-debug.log*
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 |
16 | # Directory for instrumented libs generated by jscoverage/JSCover
17 | lib-cov
18 |
19 | # Coverage directory used by tools like istanbul
20 | coverage
21 |
22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
23 | .grunt
24 |
25 | # node-waf configuration
26 | .lock-wscript
27 |
28 | # Compiled binary addons (http://nodejs.org/api/addons.html)
29 | build/Release
30 |
31 | # Dependency directories
32 | node_modules
33 | jspm_packages
34 |
35 | # Optional npm cache directory
36 | .npm
37 |
38 | # Optional REPL history
39 | .node_repl_history
40 |
41 | # =========================
42 | # Operating System Files
43 | # =========================
44 |
45 | # OSX
46 | # =========================
47 |
48 | .DS_Store
49 | .AppleDouble
50 | .LSOverride
51 |
52 | # Thumbnails
53 | ._*
54 |
55 | # Files that might appear in the root of a volume
56 | .DocumentRevisions-V100
57 | .fseventsd
58 | .Spotlight-V100
59 | .TemporaryItems
60 | .Trashes
61 | .VolumeIcon.icns
62 |
63 | # Directories potentially created on remote AFP share
64 | .AppleDB
65 | .AppleDesktop
66 | Network Trash Folder
67 | Temporary Items
68 | .apdisk
69 |
70 | # Windows
71 | # =========================
72 |
73 | # Windows image file caches
74 | Thumbs.db
75 | ehthumbs.db
76 |
77 | # Folder config file
78 | Desktop.ini
79 |
80 | # Recycle Bin used on file shares
81 | $RECYCLE.BIN/
82 |
83 | # Windows Installer files
84 | *.cab
85 | *.msi
86 | *.msm
87 | *.msp
88 |
89 | # Windows shortcuts
90 | *.lnk
91 |
92 | \.out/
93 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | *.log
2 | npm-debug.log*
3 | yarn-error.log
4 |
5 | # Coverage directory used by tools like istanbul
6 | coverage
7 | .nyc_output
8 |
9 | # Dependency directories
10 | node_modules
11 |
12 | # npm package lock
13 | package-lock.json
14 | yarn.lock
15 |
16 | # project files
17 | src
18 | test
19 | examples
20 | CHANGELOG.md
21 | .travis.yml
22 | .editorconfig
23 | .eslintignore
24 | .eslintrc
25 | .babelrc
26 | .gitignore
27 |
28 |
29 | # lock files
30 | package-lock.json
31 | yarn.lock
32 |
33 | # others
34 | .DS_Store
35 | .idea
36 |
37 | .out
38 | .github
39 | .storybook
40 | .config
41 | stories
42 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | /dist/
3 | /build/
4 | /artifacts/
5 | /coverage/
6 | /pnpm-lock.yaml
7 | .git/
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "trailingComma": "all",
5 | "semi": false,
6 | "singleQuote": true
7 | }
8 |
--------------------------------------------------------------------------------
/.storybook/.config.js:
--------------------------------------------------------------------------------
1 | import { configure, addParameters } from '@storybook/react';
2 | import { create } from '@storybook/theming';
3 |
4 | function loadStories() {
5 | require('../stories/index.js');
6 | // You can require as many stories as you need.
7 | }
8 |
9 | addParameters({
10 | options: {
11 | theme: create({
12 | base: 'dark',
13 | brandTitle: 'React Twitch Embed Video',
14 | brandUrl: 'https://github.com/talk2MeGooseman/react-twitch-embed-video',
15 | }),
16 | /**
17 | * name to display in the top left corner
18 | * @type {String}
19 | */
20 | name: 'React Twitch Embed Video',
21 | /**
22 | * URL for name in top left corner to link to
23 | * @type {String}
24 | */
25 | url: 'https://github.com/talk2MeGooseman/react-twitch-embed-video',
26 | /**
27 | * show story component as full screen
28 | * @type {Boolean}
29 | */
30 | isFullscreen: false,
31 | /**
32 | * display panel that shows a list of stories
33 | * @type {Boolean}
34 | */
35 | showNav: true,
36 | /**
37 | * display panel that shows addon configurations
38 | * @type {Boolean}
39 | */
40 | showPanel: true,
41 | /**
42 | * where to show the addon panel
43 | * @type {('bottom'|'right')}
44 | */
45 | panelPosition: 'bottom',
46 | /**
47 | * regex for finding the hierarchy separator
48 | * @example:
49 | * null - turn off hierarchy
50 | * /\// - split by `/`
51 | * /\./ - split by `.`
52 | * /\/|\./ - split by `/` or `.`
53 | * @type {Regex}
54 | */
55 | hierarchySeparator: /\/|\./,
56 | /**
57 | * regex for finding the hierarchy root separator
58 | * @example:
59 | * null - turn off multiple hierarchy roots
60 | * /\|/ - split by `|`
61 | * @type {Regex}
62 | */
63 | hierarchyRootSeparator: /\|/,
64 | /**
65 | * sidebar tree animations
66 | * @type {Boolean}
67 | */
68 | sidebarAnimations: true,
69 | /**
70 | * enable/disable shortcuts
71 | * @type {Boolean}
72 | */
73 | enableShortcuts: true,
74 | /**
75 | * show/hide tool bar
76 | * @type {Boolean}
77 | */
78 | isToolshown: true,
79 | /**
80 | * function to sort stories in the tree view
81 | * common use is alphabetical `(a, b) => a[1].id.localeCompare(b[1].id)`
82 | * if left undefined, then the order in which the stories are imported will
83 | * be the order they display
84 | * @type {Function}
85 | */
86 | storySort: undefined,
87 | },
88 | });
89 |
90 | configure(loadStories, module);
91 |
--------------------------------------------------------------------------------
/.storybook/main.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | typescript: {
3 | check: false,
4 | checkOptions: {},
5 | reactDocgen: 'react-docgen-typescript',
6 | reactDocgenTypescriptOptions: {
7 | shouldExtractLiteralValuesFromEnum: true,
8 | propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
9 | },
10 | },
11 |
12 | "stories": ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
13 |
14 | "addons": [
15 | "@storybook/addon-essentials",
16 | "@chromatic-com/storybook"
17 | ],
18 |
19 | framework: {
20 | name: "@storybook/react-vite",
21 | options: {}
22 | },
23 |
24 | docs: {
25 | autodocs: true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/.storybook/preview.js:
--------------------------------------------------------------------------------
1 |
2 | export const parameters = {
3 | controls: { expanded: true },
4 | backgrounds: { disable: true },
5 | options: {
6 | storySort: {
7 | order: [
8 | 'Guide',
9 | 'Channel Example',
10 | 'VOD Example',
11 | 'Multiple Players',
12 | ],
13 | },
14 | },
15 | };
16 | export const tags = ['autodocs'];
17 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "github.copilot.chat.testGeneration.instructions": [
3 | {
4 | "file": "test-code-style.md"
5 | }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ### Changelog
2 |
3 | All notable changes to this project will be documented in this file. Dates are displayed in UTC.
4 |
5 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6 |
7 | #### [v3.0.0](https://github.com/talk2MeGooseman/react-twitch-embed-video/compare/2.0.4...v3.0.0)
8 |
9 | Version v3.0.0 is fully backward compatible with v2. All internals of the project have been updated to be written and TypeScript and new functionality was added.
10 |
11 | - chore: Use new eslint config and update code [`#257`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/257)
12 | - feat: TypesScript Migration!!!!! [`#254`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/254)
13 | - Upgrade to the latest storybook and with examples [`ec8a495`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/ec8a495d8087d0be42b20e9410a3ad493aeb68cd)
14 | - chore: Fix lint issue with readme and push dep updates [`f1cea54`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/f1cea5448d7538fe888eef7b0d20677e1c54d67d)
15 | - Add size-limit [`82a1b66`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/82a1b669f6e6ce30198e6ec2ca3b68dbd15b2eda)
16 |
17 | #### [2.0.4](https://github.com/talk2MeGooseman/react-twitch-embed-video/compare/2.0.3...2.0.4)
18 |
19 | > 19 July 2020
20 |
21 | - Merge funding yamlk [`#156`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/156)
22 | - Add player object as onReady argument [`#148`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/148)
23 | - Add documentation for new parent prop [`bb84514`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/bb84514e36aad4cab5924aa0e35646645b8a84e1)
24 | - Upgrade minor dependencies [`fb22492`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/fb224923ac7ec7cfda509677ee7a31fdf86f6876)
25 | - Add new storybook just for testing and debugging [`06d92f2`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/06d92f242ed42fa8a2668261667456f2bfd94876)
26 |
27 | #### [2.0.3](https://github.com/talk2MeGooseman/react-twitch-embed-video/compare/2.0.2...2.0.3)
28 |
29 | > 17 May 2020
30 |
31 | - Add player object as onReady argument [`#148`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/148)
32 | - Add documentation for new parent prop [`70b3b31`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/70b3b317dbe68b67984443b713d2342c394e1b72)
33 | - Bump @storybook/addon-notes from 5.3.14 to 5.3.17 [`1c03a2c`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/1c03a2c06f769a278241f9a650404ad5cce6fea6)
34 | - Bump @storybook/addon-knobs from 5.3.14 to 5.3.17 [`ccbd4eb`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/ccbd4eb6d0fe5ec87faa3e44b191d9f85a0306f6)
35 |
36 | #### [2.0.2](https://github.com/talk2MeGooseman/react-twitch-embed-video/compare/2.0.0...2.0.2)
37 |
38 | > 9 March 2020
39 |
40 | - Bump to versoin 2.0.2 for npm publish [`8d0b0b6`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/8d0b0b6c8cbd0bddbf128c3a6342d0363b27aaad)
41 |
42 | ### [2.0.0](https://github.com/talk2MeGooseman/react-twitch-embed-video/compare/1.1.4...2.0.0)
43 |
44 | > 9 March 2020
45 |
46 | - Convert class to hooks [`#125`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/125)
47 | - Fix issue with onReady callback and tweak webpack [`6e11952`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/6e11952da35327a92ed1e976b7b494264102a96b)
48 | - Webpack updates [`9645fc9`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/9645fc94eb53956c6b890e8942826f5f9c547183)
49 | - ♻️ Pairing session refactors [`3496247`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/3496247106ca23f652ee8887f94d96f198584339)
50 |
51 | #### [1.1.4](https://github.com/talk2MeGooseman/react-twitch-embed-video/compare/1.1.3...1.1.4)
52 |
53 | > 15 January 2020
54 |
55 | - Revert "Update deps" [`a4f14f7`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/a4f14f73f61316353fd5acf3467b448632fb08d2)
56 | - I broke 1.1.3 so now were on 1.1.4 [`d06e58d`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/d06e58d2c6434e675e027690ba9abeec3ac8d06e)
57 |
58 | #### [1.1.3](https://github.com/talk2MeGooseman/react-twitch-embed-video/compare/1.1.2...1.1.3)
59 |
60 | > 15 January 2020
61 |
62 | - Add lefthook to the project [`5fb3e6e`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/5fb3e6ec059ad0c454c501a89b1889a91a5752fe)
63 | - Update deps [`147b30a`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/147b30a4c1af3fa3e40fd7dadaab6b044307efe9)
64 | - Version bump [`829735c`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/829735c6bddea6a6aff32d2cfa991b25265989ef)
65 |
66 | #### [1.1.2](https://github.com/talk2MeGooseman/react-twitch-embed-video/compare/1.1.1...1.1.2)
67 |
68 | > 26 October 2019
69 |
70 | - Fix sizing issues 20 [`#37`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/37)
71 | - Use new Github Action syntax (#21) [`#22`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/22)
72 | - Use not Github Action syntax [`#21`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/21)
73 | - Bump webpack-dev-server from 2.11.5 to 3.1.11 [`#18`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/18)
74 | - Update build-and-release.yml [`56a6c6e`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/56a6c6e5f7b26263fa7665e1c729a18305acac83)
75 | - Update build-and-release.yml [`872231e`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/872231edb7eaf995cb66b97db0def82f1f3830b6)
76 | - Update build-and-release.yml [`45632a3`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/45632a3148a7f259cd544e1be8f5ae4b4470f4de)
77 |
78 | #### [1.1.1](https://github.com/talk2MeGooseman/react-twitch-embed-video/compare/1.1.0...1.1.1)
79 |
80 | > 10 September 2019
81 |
82 | - Update js-yaml for security [`#17`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/17)
83 | - Update main.workflow [`1d7c743`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/1d7c7431444759cc3c99c78c68535e6804838536)
84 |
85 | #### [1.1.0](https://github.com/talk2MeGooseman/react-twitch-embed-video/compare/v1.0.1...1.1.0)
86 |
87 | > 31 July 2019
88 |
89 | - Update main.workflow [`#15`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/15)
90 | - Feature: 2 or more streams at the same time [`#14`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/14)
91 | - Bump lodash-es from 4.17.4 to 4.17.14 [`#13`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/13)
92 | - Added in server-side rendering capabilities by switching window to ro… [`#3`](https://github.com/talk2MeGooseman/react-twitch-embed-video/pull/3)
93 | - Minor upgrades to resolve security audit [`b8ebc77`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/b8ebc77556ec16ee967dbfe6f4d1cf5498635527)
94 | - Added in server-side rendering capabilities by switching window to root or root to global [`4291f08`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/4291f088e7e5a27f0b34340ad49f304fe9890408)
95 | - Update story book and dependencies [`b7a2c0e`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/b7a2c0e8b1dbf9dc69a048d5dbb838d3d3ec0215)
96 |
97 | #### [v1.0.1](https://github.com/talk2MeGooseman/react-twitch-embed-video/compare/v1.0.0...v1.0.1)
98 |
99 | > 6 December 2017
100 |
101 | - Opps the dist folder......... :-1: [`26c5bb0`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/26c5bb04956073137972c2482e9647c91e1b1305)
102 |
103 | #### v1.0.0
104 |
105 | > 6 December 2017
106 |
107 | - React Twitch Embed Video Component [`53d2021`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/53d20211fca4416ba1e63a1b35d6e5e966bc2dbc)
108 | - Babel updates and Github pages updates [`ff1bc9b`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/ff1bc9b3f71ff066cdda85fe2b71e0b2b2813e52)
109 | - Add eslint and publish-please [`0e90aea`](https://github.com/talk2MeGooseman/react-twitch-embed-video/commit/0e90aea469619e5c73732d46fb49fb501b56af5a)
110 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Erik Guzman
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Twitch Embed Video
2 |
3 | [](https://badge.fury.io/js/react-twitch-embed-video)
4 |
5 | Your solution to embedding the Twitch video player in your ReactJS application
6 |
7 | https://talk2megooseman.github.io/react-twitch-embed-video/
8 |
9 | ## Installation
10 |
11 | yarn | npm
12 | ---- | ---
13 | `yarn add react-twitch-embed-video` | `npm install react-twitch-embed-video`
14 |
15 | ## Usage
16 |
17 | Visit the github page for lives examples and documentation: https://talk2megooseman.github.io/react-twitch-embed-video/
18 |
19 |
20 | ```
21 | import ReactTwitchEmbedVideo from "react-twitch-embed-video"
22 | .
23 | .
24 | .
25 |
26 |
27 | ```
28 |
29 | For full documentation on how to use React Twitch Embed Video visit: https://talk2megooseman.github.io/react-twitch-embed-video/
30 |
31 | ## Troubleshooting
32 |
33 | - Video embed not working in Brave browser
34 | * By default Brave block all third party cookies which causes issues using the Twitch Embed Player. In order to get the player to work you either have to allow third party cookies `Setting->Additional Settings->Privacy and Security->Site Settings->Cookies` or you can add Twitch to the whitelist so you can still block other third party cookies.
35 |
36 | ## Development
37 |
38 | ### Usage
39 |
40 | 1. Install modules
41 | > pnpm
42 |
43 | 1. Start storybook and start coding!
44 | > pnpm dev
45 |
46 | 1. Make project available locally by using `npm link`
47 | 1. To test if it works correctly in another project you can use npm `npm link react-twitch-embed-video`
48 | 1. Verify all tests are passing
49 | > pnpm test
50 |
51 | #### Extra
52 |
53 | * If you want to automatically fix lint problems run :
54 | > pnpm lint:fix
55 |
56 | Commands
57 | ----
58 | - `pnpm`
59 | - `pnpm build`
60 | - `pnpm test`
61 | - `pnpm coverage`
62 | - `pnpm start`
63 | - `pnpm release`
64 | - `pnpm lint`
65 | - `pnpm lint:fix`
66 |
--------------------------------------------------------------------------------
/dist/index.d.ts:
--------------------------------------------------------------------------------
1 | import { default as default_2 } from 'react';
2 |
3 | declare const _default: default_2.MemoExoticComponent<{
4 | (props: IChannelEmbedParameters | IVodCollectionEmbedParameters | IVodEmbedParameters): default_2.JSX.Element;
5 | defaultProps: {
6 | targetId: string;
7 | width: string;
8 | height: string;
9 | autoplay: boolean;
10 | muted: boolean;
11 | };
12 | }>;
13 | export default _default;
14 |
15 | declare interface IBaseEmbedParameters {
16 | /** If true, the player can go full screen. Default: true. */
17 | allowfullscreen?: boolean;
18 | /** If true, the video starts playing automatically, without the user clicking play.
19 | * The exception is mobile platforms, on which video cannot be played without user
20 | * interaction. Default: true. */
21 | autoplay?: boolean;
22 | /** Specifies the type of chat to use. Valid values:
23 | *
24 | * _default: Default value, uses full-featured chat._.
25 | *
26 | * _mobile: Uses a read-only version of chat, optimized for mobile devices._.
27 | *
28 | * To omit chat, specify a value of video for the layout option. */
29 | chat?: 'default' | 'mobile';
30 | /** Maximum width of the rendered element, in pixels. This can be expressed as a
31 | * percentage, by passing a string like 100%. */
32 | height?: string | number;
33 | /** Determines the screen layout. Valid values:
34 | *
35 | * _video-with-chat: Default if channel is provided. Shows both video and chat side-by-side.
36 | * At narrow sizes, chat renders under the video player.
37 | *
38 | * _video: Default if channel is not provided. Shows only the video player (omits chat). */
39 | layout?: 'video' | 'video-with-chat';
40 | /** Specifies whether the initial state of the video is muted. _Default: false. */
41 | muted?: boolean;
42 | /** The video started playing. This callback receives an object with a sessionId property. */
43 | onPlay?: IVideoPlayEventCallback;
44 | /** The video player is ready for API commands. This callback receives the player object. */
45 | onReady?: IVideoReadyEventCallback;
46 | /** Required if your site is embedded on any domain(s) other than the one that instantiates
47 | * the Twitch embed.
48 | *
49 | * Example parent parameter: ["streamernews.example.com", "embed.example.com"]. */
50 | parent?: string[];
51 | /** Custom class name for div wrapper. */
52 | targetClass?: string;
53 | /** Custom id to target, used if you're going to have multiple players on the page. */
54 | targetId?: string;
55 | /** The Twitch embed color theme to use.
56 | *
57 | * Valid values: light or dark.
58 | *
59 | * _Default: light or the users chosen theme on Twitch. */
60 | theme?: 'light' | 'dark';
61 | /** Time in the video where playback starts. Specifies hours, minutes, and seconds.
62 | *
63 | * Default: 0h0m0s (the start of the video). */
64 | time?: string;
65 | /** Width of video embed including chat. */
66 | width?: string | number;
67 | }
68 |
69 | declare interface IChannelEmbedParameters extends IBaseEmbedParameters {
70 | /** Optional for VOD embeds; otherwise, required. Name of the chat room and channel to stream. */
71 | channel: string;
72 | }
73 |
74 | declare interface IPlaybackStatsInterface {
75 | backendVersion: string;
76 | bufferSize: number;
77 | codecs: string;
78 | displayResolution: string;
79 | fps: number;
80 | hlsLatencyBroadcaster: number;
81 | playbackRate: number;
82 | skippedFrames: number;
83 | videoResolution: string;
84 | }
85 |
86 | declare interface IPlayerInterface {
87 | /** Disables display of Closed Captions. */
88 | disableCaptions: () => void;
89 | enableCaptions: () => void;
90 | pause: () => void;
91 | play: () => void;
92 | seek: (timestamp: number) => void;
93 | setChannel: (channel: string) => void;
94 | setCollection: (collection_id: string, video_id: string) => void;
95 | setQuality: (quality: string) => void;
96 | setVideo: (video_id: string, timestamp: number) => void;
97 | getMuted: () => boolean;
98 | setMuted: (muted: boolean) => void;
99 | getVolume: () => number;
100 | setVolume: (volumelevel: number) => void;
101 | getPlaybackStats: () => IPlaybackStatsInterface;
102 | getChannel: () => string;
103 | getCurrentTime: () => number;
104 | getDuration: () => number;
105 | getEnded: () => boolean;
106 | getQualities: () => string[];
107 | getQuality: () => string;
108 | getVideo: () => string;
109 | isPaused: () => boolean;
110 | }
111 |
112 | declare type IVideoPlayEventCallback = () => void;
113 |
114 | declare type IVideoReadyEventCallback = (player: IPlayerInterface) => void;
115 |
116 | declare interface IVodCollectionEmbedParameters extends IBaseEmbedParameters {
117 | /** The VOD collection to play. If you use this, you may also specify an initial video
118 | * in the VOD collection, otherwise playback will begin with the first video in the collection.
119 | * All VODs are auto-played. Chat replay is not supported.
120 | *
121 | * @example
122 | * `{ video: "124085610", collection: "GMEgKwTQpRQwyA" }` */
123 | collection: {
124 | video: string;
125 | collection: string;
126 | };
127 | }
128 |
129 | declare interface IVodEmbedParameters extends IBaseEmbedParameters {
130 | /** ID of a VOD to play. Chat replay is not supported. */
131 | video: string;
132 | }
133 |
134 | export { }
135 |
--------------------------------------------------------------------------------
/dist/react-twitch-embed-video.js:
--------------------------------------------------------------------------------
1 | import ae, { useCallback as G, useState as Le, useRef as br, useEffect as xe } from "react";
2 | var q = typeof globalThis < "u" ? globalThis : typeof window < "u" ? window : typeof global < "u" ? global : typeof self < "u" ? self : {};
3 | function gr(a) {
4 | return a && a.__esModule && Object.prototype.hasOwnProperty.call(a, "default") ? a.default : a;
5 | }
6 | var B = { exports: {} }, L = {};
7 | /**
8 | * @license React
9 | * react-jsx-runtime.production.min.js
10 | *
11 | * Copyright (c) Facebook, Inc. and its affiliates.
12 | *
13 | * This source code is licensed under the MIT license found in the
14 | * LICENSE file in the root directory of this source tree.
15 | */
16 | var De;
17 | function _r() {
18 | if (De) return L;
19 | De = 1;
20 | var a = ae, l = Symbol.for("react.element"), p = Symbol.for("react.fragment"), E = Object.prototype.hasOwnProperty, v = a.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner, w = { key: !0, ref: !0, __self: !0, __source: !0 };
21 | function _(P, d, S) {
22 | var y, b = {}, R = null, x = null;
23 | S !== void 0 && (R = "" + S), d.key !== void 0 && (R = "" + d.key), d.ref !== void 0 && (x = d.ref);
24 | for (y in d) E.call(d, y) && !w.hasOwnProperty(y) && (b[y] = d[y]);
25 | if (P && P.defaultProps) for (y in d = P.defaultProps, d) b[y] === void 0 && (b[y] = d[y]);
26 | return { $$typeof: l, type: P, key: R, ref: x, props: b, _owner: v.current };
27 | }
28 | return L.Fragment = p, L.jsx = _, L.jsxs = _, L;
29 | }
30 | var W = {};
31 | /**
32 | * @license React
33 | * react-jsx-runtime.development.js
34 | *
35 | * Copyright (c) Facebook, Inc. and its affiliates.
36 | *
37 | * This source code is licensed under the MIT license found in the
38 | * LICENSE file in the root directory of this source tree.
39 | */
40 | var ke;
41 | function Rr() {
42 | return ke || (ke = 1, process.env.NODE_ENV !== "production" && function() {
43 | var a = ae, l = Symbol.for("react.element"), p = Symbol.for("react.portal"), E = Symbol.for("react.fragment"), v = Symbol.for("react.strict_mode"), w = Symbol.for("react.profiler"), _ = Symbol.for("react.provider"), P = Symbol.for("react.context"), d = Symbol.for("react.forward_ref"), S = Symbol.for("react.suspense"), y = Symbol.for("react.suspense_list"), b = Symbol.for("react.memo"), R = Symbol.for("react.lazy"), x = Symbol.for("react.offscreen"), Y = Symbol.iterator, K = "@@iterator";
44 | function $(e) {
45 | if (e === null || typeof e != "object")
46 | return null;
47 | var r = Y && e[Y] || e[K];
48 | return typeof r == "function" ? r : null;
49 | }
50 | var D = a.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
51 | function h(e) {
52 | {
53 | for (var r = arguments.length, t = new Array(r > 1 ? r - 1 : 0), n = 1; n < r; n++)
54 | t[n - 1] = arguments[n];
55 | Ye("error", e, t);
56 | }
57 | }
58 | function Ye(e, r, t) {
59 | {
60 | var n = D.ReactDebugCurrentFrame, u = n.getStackAddendum();
61 | u !== "" && (r += "%s", t = t.concat([u]));
62 | var s = t.map(function(i) {
63 | return String(i);
64 | });
65 | s.unshift("Warning: " + r), Function.prototype.apply.call(console[e], console, s);
66 | }
67 | }
68 | var $e = !1, Ue = !1, Ve = !1, Me = !1, Ne = !1, oe;
69 | oe = Symbol.for("react.module.reference");
70 | function qe(e) {
71 | return !!(typeof e == "string" || typeof e == "function" || e === E || e === w || Ne || e === v || e === S || e === y || Me || e === x || $e || Ue || Ve || typeof e == "object" && e !== null && (e.$$typeof === R || e.$$typeof === b || e.$$typeof === _ || e.$$typeof === P || e.$$typeof === d || // This needs to include all possible module reference object
72 | // types supported by any Flight configuration anywhere since
73 | // we don't know which Flight build this will end up being used
74 | // with.
75 | e.$$typeof === oe || e.getModuleId !== void 0));
76 | }
77 | function Be(e, r, t) {
78 | var n = e.displayName;
79 | if (n)
80 | return n;
81 | var u = r.displayName || r.name || "";
82 | return u !== "" ? t + "(" + u + ")" : t;
83 | }
84 | function ie(e) {
85 | return e.displayName || "Context";
86 | }
87 | function C(e) {
88 | if (e == null)
89 | return null;
90 | if (typeof e.tag == "number" && h("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."), typeof e == "function")
91 | return e.displayName || e.name || null;
92 | if (typeof e == "string")
93 | return e;
94 | switch (e) {
95 | case E:
96 | return "Fragment";
97 | case p:
98 | return "Portal";
99 | case w:
100 | return "Profiler";
101 | case v:
102 | return "StrictMode";
103 | case S:
104 | return "Suspense";
105 | case y:
106 | return "SuspenseList";
107 | }
108 | if (typeof e == "object")
109 | switch (e.$$typeof) {
110 | case P:
111 | var r = e;
112 | return ie(r) + ".Consumer";
113 | case _:
114 | var t = e;
115 | return ie(t._context) + ".Provider";
116 | case d:
117 | return Be(e, e.render, "ForwardRef");
118 | case b:
119 | var n = e.displayName || null;
120 | return n !== null ? n : C(e.type) || "Memo";
121 | case R: {
122 | var u = e, s = u._payload, i = u._init;
123 | try {
124 | return C(i(s));
125 | } catch {
126 | return null;
127 | }
128 | }
129 | }
130 | return null;
131 | }
132 | var O = Object.assign, F = 0, ue, se, le, ce, fe, de, ve;
133 | function pe() {
134 | }
135 | pe.__reactDisabledLog = !0;
136 | function Je() {
137 | {
138 | if (F === 0) {
139 | ue = console.log, se = console.info, le = console.warn, ce = console.error, fe = console.group, de = console.groupCollapsed, ve = console.groupEnd;
140 | var e = {
141 | configurable: !0,
142 | enumerable: !0,
143 | value: pe,
144 | writable: !0
145 | };
146 | Object.defineProperties(console, {
147 | info: e,
148 | log: e,
149 | warn: e,
150 | error: e,
151 | group: e,
152 | groupCollapsed: e,
153 | groupEnd: e
154 | });
155 | }
156 | F++;
157 | }
158 | }
159 | function Ge() {
160 | {
161 | if (F--, F === 0) {
162 | var e = {
163 | configurable: !0,
164 | enumerable: !0,
165 | writable: !0
166 | };
167 | Object.defineProperties(console, {
168 | log: O({}, e, {
169 | value: ue
170 | }),
171 | info: O({}, e, {
172 | value: se
173 | }),
174 | warn: O({}, e, {
175 | value: le
176 | }),
177 | error: O({}, e, {
178 | value: ce
179 | }),
180 | group: O({}, e, {
181 | value: fe
182 | }),
183 | groupCollapsed: O({}, e, {
184 | value: de
185 | }),
186 | groupEnd: O({}, e, {
187 | value: ve
188 | })
189 | });
190 | }
191 | F < 0 && h("disabledDepth fell below zero. This is a bug in React. Please file an issue.");
192 | }
193 | }
194 | var z = D.ReactCurrentDispatcher, H;
195 | function U(e, r, t) {
196 | {
197 | if (H === void 0)
198 | try {
199 | throw Error();
200 | } catch (u) {
201 | var n = u.stack.trim().match(/\n( *(at )?)/);
202 | H = n && n[1] || "";
203 | }
204 | return `
205 | ` + H + e;
206 | }
207 | }
208 | var X = !1, V;
209 | {
210 | var Ke = typeof WeakMap == "function" ? WeakMap : Map;
211 | V = new Ke();
212 | }
213 | function Ee(e, r) {
214 | if (!e || X)
215 | return "";
216 | {
217 | var t = V.get(e);
218 | if (t !== void 0)
219 | return t;
220 | }
221 | var n;
222 | X = !0;
223 | var u = Error.prepareStackTrace;
224 | Error.prepareStackTrace = void 0;
225 | var s;
226 | s = z.current, z.current = null, Je();
227 | try {
228 | if (r) {
229 | var i = function() {
230 | throw Error();
231 | };
232 | if (Object.defineProperty(i.prototype, "props", {
233 | set: function() {
234 | throw Error();
235 | }
236 | }), typeof Reflect == "object" && Reflect.construct) {
237 | try {
238 | Reflect.construct(i, []);
239 | } catch (g) {
240 | n = g;
241 | }
242 | Reflect.construct(e, [], i);
243 | } else {
244 | try {
245 | i.call();
246 | } catch (g) {
247 | n = g;
248 | }
249 | e.call(i.prototype);
250 | }
251 | } else {
252 | try {
253 | throw Error();
254 | } catch (g) {
255 | n = g;
256 | }
257 | e();
258 | }
259 | } catch (g) {
260 | if (g && n && typeof g.stack == "string") {
261 | for (var o = g.stack.split(`
262 | `), m = n.stack.split(`
263 | `), c = o.length - 1, f = m.length - 1; c >= 1 && f >= 0 && o[c] !== m[f]; )
264 | f--;
265 | for (; c >= 1 && f >= 0; c--, f--)
266 | if (o[c] !== m[f]) {
267 | if (c !== 1 || f !== 1)
268 | do
269 | if (c--, f--, f < 0 || o[c] !== m[f]) {
270 | var T = `
271 | ` + o[c].replace(" at new ", " at ");
272 | return e.displayName && T.includes("") && (T = T.replace("", e.displayName)), typeof e == "function" && V.set(e, T), T;
273 | }
274 | while (c >= 1 && f >= 0);
275 | break;
276 | }
277 | }
278 | } finally {
279 | X = !1, z.current = s, Ge(), Error.prepareStackTrace = u;
280 | }
281 | var A = e ? e.displayName || e.name : "", j = A ? U(A) : "";
282 | return typeof e == "function" && V.set(e, j), j;
283 | }
284 | function ze(e, r, t) {
285 | return Ee(e, !1);
286 | }
287 | function He(e) {
288 | var r = e.prototype;
289 | return !!(r && r.isReactComponent);
290 | }
291 | function M(e, r, t) {
292 | if (e == null)
293 | return "";
294 | if (typeof e == "function")
295 | return Ee(e, He(e));
296 | if (typeof e == "string")
297 | return U(e);
298 | switch (e) {
299 | case S:
300 | return U("Suspense");
301 | case y:
302 | return U("SuspenseList");
303 | }
304 | if (typeof e == "object")
305 | switch (e.$$typeof) {
306 | case d:
307 | return ze(e.render);
308 | case b:
309 | return M(e.type, r, t);
310 | case R: {
311 | var n = e, u = n._payload, s = n._init;
312 | try {
313 | return M(s(u), r, t);
314 | } catch {
315 | }
316 | }
317 | }
318 | return "";
319 | }
320 | var I = Object.prototype.hasOwnProperty, ye = {}, he = D.ReactDebugCurrentFrame;
321 | function N(e) {
322 | if (e) {
323 | var r = e._owner, t = M(e.type, e._source, r ? r.type : null);
324 | he.setExtraStackFrame(t);
325 | } else
326 | he.setExtraStackFrame(null);
327 | }
328 | function Xe(e, r, t, n, u) {
329 | {
330 | var s = Function.call.bind(I);
331 | for (var i in e)
332 | if (s(e, i)) {
333 | var o = void 0;
334 | try {
335 | if (typeof e[i] != "function") {
336 | var m = Error((n || "React class") + ": " + t + " type `" + i + "` is invalid; it must be a function, usually from the `prop-types` package, but received `" + typeof e[i] + "`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
337 | throw m.name = "Invariant Violation", m;
338 | }
339 | o = e[i](r, i, n, t, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
340 | } catch (c) {
341 | o = c;
342 | }
343 | o && !(o instanceof Error) && (N(u), h("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).", n || "React class", t, i, typeof o), N(null)), o instanceof Error && !(o.message in ye) && (ye[o.message] = !0, N(u), h("Failed %s type: %s", t, o.message), N(null));
344 | }
345 | }
346 | }
347 | var Ze = Array.isArray;
348 | function Z(e) {
349 | return Ze(e);
350 | }
351 | function Qe(e) {
352 | {
353 | var r = typeof Symbol == "function" && Symbol.toStringTag, t = r && e[Symbol.toStringTag] || e.constructor.name || "Object";
354 | return t;
355 | }
356 | }
357 | function er(e) {
358 | try {
359 | return me(e), !1;
360 | } catch {
361 | return !0;
362 | }
363 | }
364 | function me(e) {
365 | return "" + e;
366 | }
367 | function be(e) {
368 | if (er(e))
369 | return h("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.", Qe(e)), me(e);
370 | }
371 | var ge = D.ReactCurrentOwner, rr = {
372 | key: !0,
373 | ref: !0,
374 | __self: !0,
375 | __source: !0
376 | }, _e, Re;
377 | function tr(e) {
378 | if (I.call(e, "ref")) {
379 | var r = Object.getOwnPropertyDescriptor(e, "ref").get;
380 | if (r && r.isReactWarning)
381 | return !1;
382 | }
383 | return e.ref !== void 0;
384 | }
385 | function nr(e) {
386 | if (I.call(e, "key")) {
387 | var r = Object.getOwnPropertyDescriptor(e, "key").get;
388 | if (r && r.isReactWarning)
389 | return !1;
390 | }
391 | return e.key !== void 0;
392 | }
393 | function ar(e, r) {
394 | typeof e.ref == "string" && ge.current;
395 | }
396 | function or(e, r) {
397 | {
398 | var t = function() {
399 | _e || (_e = !0, h("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", r));
400 | };
401 | t.isReactWarning = !0, Object.defineProperty(e, "key", {
402 | get: t,
403 | configurable: !0
404 | });
405 | }
406 | }
407 | function ir(e, r) {
408 | {
409 | var t = function() {
410 | Re || (Re = !0, h("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)", r));
411 | };
412 | t.isReactWarning = !0, Object.defineProperty(e, "ref", {
413 | get: t,
414 | configurable: !0
415 | });
416 | }
417 | }
418 | var ur = function(e, r, t, n, u, s, i) {
419 | var o = {
420 | // This tag allows us to uniquely identify this as a React Element
421 | $$typeof: l,
422 | // Built-in properties that belong on the element
423 | type: e,
424 | key: r,
425 | ref: t,
426 | props: i,
427 | // Record the component responsible for creating this element.
428 | _owner: s
429 | };
430 | return o._store = {}, Object.defineProperty(o._store, "validated", {
431 | configurable: !1,
432 | enumerable: !1,
433 | writable: !0,
434 | value: !1
435 | }), Object.defineProperty(o, "_self", {
436 | configurable: !1,
437 | enumerable: !1,
438 | writable: !1,
439 | value: n
440 | }), Object.defineProperty(o, "_source", {
441 | configurable: !1,
442 | enumerable: !1,
443 | writable: !1,
444 | value: u
445 | }), Object.freeze && (Object.freeze(o.props), Object.freeze(o)), o;
446 | };
447 | function sr(e, r, t, n, u) {
448 | {
449 | var s, i = {}, o = null, m = null;
450 | t !== void 0 && (be(t), o = "" + t), nr(r) && (be(r.key), o = "" + r.key), tr(r) && (m = r.ref, ar(r, u));
451 | for (s in r)
452 | I.call(r, s) && !rr.hasOwnProperty(s) && (i[s] = r[s]);
453 | if (e && e.defaultProps) {
454 | var c = e.defaultProps;
455 | for (s in c)
456 | i[s] === void 0 && (i[s] = c[s]);
457 | }
458 | if (o || m) {
459 | var f = typeof e == "function" ? e.displayName || e.name || "Unknown" : e;
460 | o && or(i, f), m && ir(i, f);
461 | }
462 | return ur(e, o, m, u, n, ge.current, i);
463 | }
464 | }
465 | var Q = D.ReactCurrentOwner, Te = D.ReactDebugCurrentFrame;
466 | function k(e) {
467 | if (e) {
468 | var r = e._owner, t = M(e.type, e._source, r ? r.type : null);
469 | Te.setExtraStackFrame(t);
470 | } else
471 | Te.setExtraStackFrame(null);
472 | }
473 | var ee;
474 | ee = !1;
475 | function re(e) {
476 | return typeof e == "object" && e !== null && e.$$typeof === l;
477 | }
478 | function we() {
479 | {
480 | if (Q.current) {
481 | var e = C(Q.current.type);
482 | if (e)
483 | return `
484 |
485 | Check the render method of \`` + e + "`.";
486 | }
487 | return "";
488 | }
489 | }
490 | function lr(e) {
491 | return "";
492 | }
493 | var Pe = {};
494 | function cr(e) {
495 | {
496 | var r = we();
497 | if (!r) {
498 | var t = typeof e == "string" ? e : e.displayName || e.name;
499 | t && (r = `
500 |
501 | Check the top-level render call using <` + t + ">.");
502 | }
503 | return r;
504 | }
505 | }
506 | function Se(e, r) {
507 | {
508 | if (!e._store || e._store.validated || e.key != null)
509 | return;
510 | e._store.validated = !0;
511 | var t = cr(r);
512 | if (Pe[t])
513 | return;
514 | Pe[t] = !0;
515 | var n = "";
516 | e && e._owner && e._owner !== Q.current && (n = " It was passed a child from " + C(e._owner.type) + "."), k(e), h('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.', t, n), k(null);
517 | }
518 | }
519 | function Ce(e, r) {
520 | {
521 | if (typeof e != "object")
522 | return;
523 | if (Z(e))
524 | for (var t = 0; t < e.length; t++) {
525 | var n = e[t];
526 | re(n) && Se(n, r);
527 | }
528 | else if (re(e))
529 | e._store && (e._store.validated = !0);
530 | else if (e) {
531 | var u = $(e);
532 | if (typeof u == "function" && u !== e.entries)
533 | for (var s = u.call(e), i; !(i = s.next()).done; )
534 | re(i.value) && Se(i.value, r);
535 | }
536 | }
537 | }
538 | function fr(e) {
539 | {
540 | var r = e.type;
541 | if (r == null || typeof r == "string")
542 | return;
543 | var t;
544 | if (typeof r == "function")
545 | t = r.propTypes;
546 | else if (typeof r == "object" && (r.$$typeof === d || // Note: Memo only checks outer props here.
547 | // Inner props are checked in the reconciler.
548 | r.$$typeof === b))
549 | t = r.propTypes;
550 | else
551 | return;
552 | if (t) {
553 | var n = C(r);
554 | Xe(t, e.props, "prop", n, e);
555 | } else if (r.PropTypes !== void 0 && !ee) {
556 | ee = !0;
557 | var u = C(r);
558 | h("Component %s declared `PropTypes` instead of `propTypes`. Did you misspell the property assignment?", u || "Unknown");
559 | }
560 | typeof r.getDefaultProps == "function" && !r.getDefaultProps.isReactClassApproved && h("getDefaultProps is only used on classic React.createClass definitions. Use a static property named `defaultProps` instead.");
561 | }
562 | }
563 | function dr(e) {
564 | {
565 | for (var r = Object.keys(e.props), t = 0; t < r.length; t++) {
566 | var n = r[t];
567 | if (n !== "children" && n !== "key") {
568 | k(e), h("Invalid prop `%s` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.", n), k(null);
569 | break;
570 | }
571 | }
572 | e.ref !== null && (k(e), h("Invalid attribute `ref` supplied to `React.Fragment`."), k(null));
573 | }
574 | }
575 | var Oe = {};
576 | function je(e, r, t, n, u, s) {
577 | {
578 | var i = qe(e);
579 | if (!i) {
580 | var o = "";
581 | (e === void 0 || typeof e == "object" && e !== null && Object.keys(e).length === 0) && (o += " You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.");
582 | var m = lr();
583 | m ? o += m : o += we();
584 | var c;
585 | e === null ? c = "null" : Z(e) ? c = "array" : e !== void 0 && e.$$typeof === l ? (c = "<" + (C(e.type) || "Unknown") + " />", o = " Did you accidentally export a JSX literal instead of a component?") : c = typeof e, h("React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s", c, o);
586 | }
587 | var f = sr(e, r, t, u, s);
588 | if (f == null)
589 | return f;
590 | if (i) {
591 | var T = r.children;
592 | if (T !== void 0)
593 | if (n)
594 | if (Z(T)) {
595 | for (var A = 0; A < T.length; A++)
596 | Ce(T[A], e);
597 | Object.freeze && Object.freeze(T);
598 | } else
599 | h("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");
600 | else
601 | Ce(T, e);
602 | }
603 | if (I.call(r, "key")) {
604 | var j = C(e), g = Object.keys(r).filter(function(mr) {
605 | return mr !== "key";
606 | }), te = g.length > 0 ? "{key: someKey, " + g.join(": ..., ") + ": ...}" : "{key: someKey}";
607 | if (!Oe[j + te]) {
608 | var hr = g.length > 0 ? "{" + g.join(": ..., ") + ": ...}" : "{}";
609 | h(`A props object containing a "key" prop is being spread into JSX:
610 | let props = %s;
611 | <%s {...props} />
612 | React keys must be passed directly to JSX without using spread:
613 | let props = %s;
614 | <%s key={someKey} {...props} />`, te, j, hr, j), Oe[j + te] = !0;
615 | }
616 | }
617 | return e === E ? dr(f) : fr(f), f;
618 | }
619 | }
620 | function vr(e, r, t) {
621 | return je(e, r, t, !0);
622 | }
623 | function pr(e, r, t) {
624 | return je(e, r, t, !1);
625 | }
626 | var Er = pr, yr = vr;
627 | W.Fragment = E, W.jsx = Er, W.jsxs = yr;
628 | }()), W;
629 | }
630 | var Ae;
631 | function Tr() {
632 | return Ae || (Ae = 1, process.env.NODE_ENV === "production" ? B.exports = _r() : B.exports = Rr()), B.exports;
633 | }
634 | var wr = Tr(), J, Fe;
635 | function Pr() {
636 | return Fe || (Fe = 1, J = typeof self == "object" && self.self === self && self || typeof q == "object" && q.global === q && q || J), J;
637 | }
638 | var Sr = Pr();
639 | const ne = /* @__PURE__ */ gr(Sr), Cr = "https://embed.twitch.tv/embed/v1.js", Or = () => {
640 | }, jr = (a = Or) => {
641 | if (document.querySelector("script[src='https://embed.twitch.tv/embed/v1.js']"))
642 | return;
643 | const l = document.createElement("script");
644 | l.setAttribute("src", Cr), l.addEventListener("load", a), document.body.append(l);
645 | }, xr = () => {
646 | }, Dr = (a) => G(
647 | (l, p) => a ? (a.addEventListener(l, p), () => {
648 | a.removeEventListener(l, p);
649 | }) : xr,
650 | [a]
651 | ), kr = (a, {
652 | autoplay: l,
653 | onPlay: p
654 | }) => {
655 | const [E, v] = Le(
656 | l
657 | );
658 | return G(() => {
659 | if (!a)
660 | return;
661 | if (E) {
662 | p && p();
663 | return;
664 | }
665 | a.getPlayer().pause(), v(!0);
666 | }, [p, a, v, E]);
667 | }, Ar = "twitch-embed", Fr = "940", Ir = "480", Ie = {
668 | MUTED: 0,
669 | AUDIBLE: 1
670 | }, Lr = (a, l) => {
671 | !l && a.pause();
672 | }, Wr = (a, l) => {
673 | a.setVolume(l ? Ie.MUTED : Ie.AUDIBLE);
674 | }, Yr = (...a) => a, $r = (a, {
675 | autoplay: l,
676 | muted: p,
677 | onReady: E
678 | }) => G(() => {
679 | if (!a)
680 | return;
681 | const v = a.getPlayer();
682 | Wr(v, p), Lr(v, l), E && E(v);
683 | }, [a, p, l, E]), Ur = (a) => {
684 | const [l, p] = Le(), E = G(() => {
685 | var _;
686 | const v = ne;
687 | if (((_ = v == null ? void 0 : v.Twitch) == null ? void 0 : _.Embed) === void 0)
688 | return;
689 | const w = new v.Twitch.Embed(
690 | a.targetId ?? "",
691 | {
692 | ...a
693 | }
694 | );
695 | p(w);
696 | }, [a]);
697 | return Yr(l, E);
698 | }, We = (a) => {
699 | const { width: l, height: p, targetId: E, targetClass: v } = a, w = br(null), [_, P] = Ur(a), d = Dr(_), S = $r(_, a), y = kr(_, a);
700 | return xe(() => {
701 | var $;
702 | const b = ne;
703 | if ((($ = b.Twitch) == null ? void 0 : $.Embed) === void 0)
704 | return;
705 | const { VIDEO_PLAY: R, VIDEO_READY: x } = b.Twitch.Embed, Y = d(
706 | R,
707 | y
708 | ), K = d(
709 | x,
710 | S
711 | );
712 | return () => {
713 | K(), Y();
714 | };
715 | }, [S, d, y]), xe(() => {
716 | var R;
717 | const b = ne;
718 | if (w.current && (w.current.innerHTML = ""), (R = b.Twitch) != null && R.Embed) {
719 | P();
720 | return;
721 | }
722 | jr(P);
723 | }, [P]), /* @__PURE__ */ wr.jsx(
724 | "div",
725 | {
726 | ref: w,
727 | style: { width: l, height: p },
728 | className: v,
729 | id: E
730 | }
731 | );
732 | };
733 | We.defaultProps = {
734 | targetId: Ar,
735 | width: Ir,
736 | height: Fr,
737 | autoplay: !0,
738 | muted: !1
739 | };
740 | const Mr = ae.memo(We);
741 | export {
742 | Mr as default
743 | };
744 |
--------------------------------------------------------------------------------
/dist/react-twitch-embed-video.umd.cjs:
--------------------------------------------------------------------------------
1 | (function(p,O){typeof exports=="object"&&typeof module<"u"?module.exports=O(require("react")):typeof define=="function"&&define.amd?define(["react"],O):(p=typeof globalThis<"u"?globalThis:p||self,p["react-twitch-embed-video"]=O(p.react))})(this,function(p){"use strict";var O=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function Ie(a){return a&&a.__esModule&&Object.prototype.hasOwnProperty.call(a,"default")?a.default:a}var V={exports:{}},L={};/**
2 | * @license React
3 | * react-jsx-runtime.production.min.js
4 | *
5 | * Copyright (c) Facebook, Inc. and its affiliates.
6 | *
7 | * This source code is licensed under the MIT license found in the
8 | * LICENSE file in the root directory of this source tree.
9 | */var ae;function Le(){if(ae)return L;ae=1;var a=p,l=Symbol.for("react.element"),E=Symbol.for("react.fragment"),y=Object.prototype.hasOwnProperty,v=a.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,P={key:!0,ref:!0,__self:!0,__source:!0};function R(C,d,S){var h,g={},T=null,k=null;S!==void 0&&(T=""+S),d.key!==void 0&&(T=""+d.key),d.ref!==void 0&&(k=d.ref);for(h in d)y.call(d,h)&&!P.hasOwnProperty(h)&&(g[h]=d[h]);if(C&&C.defaultProps)for(h in d=C.defaultProps,d)g[h]===void 0&&(g[h]=d[h]);return{$$typeof:l,type:C,key:T,ref:k,props:g,_owner:v.current}}return L.Fragment=E,L.jsx=R,L.jsxs=R,L}var W={};/**
10 | * @license React
11 | * react-jsx-runtime.development.js
12 | *
13 | * Copyright (c) Facebook, Inc. and its affiliates.
14 | *
15 | * This source code is licensed under the MIT license found in the
16 | * LICENSE file in the root directory of this source tree.
17 | */var oe;function We(){return oe||(oe=1,process.env.NODE_ENV!=="production"&&function(){var a=p,l=Symbol.for("react.element"),E=Symbol.for("react.portal"),y=Symbol.for("react.fragment"),v=Symbol.for("react.strict_mode"),P=Symbol.for("react.profiler"),R=Symbol.for("react.provider"),C=Symbol.for("react.context"),d=Symbol.for("react.forward_ref"),S=Symbol.for("react.suspense"),h=Symbol.for("react.suspense_list"),g=Symbol.for("react.memo"),T=Symbol.for("react.lazy"),k=Symbol.for("react.offscreen"),N=Symbol.iterator,X="@@iterator";function B(e){if(e===null||typeof e!="object")return null;var r=N&&e[N]||e[X];return typeof r=="function"?r:null}var A=a.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;function b(e){{for(var r=arguments.length,t=new Array(r>1?r-1:0),n=1;n=1&&f>=0&&o[c]!==m[f];)f--;for(;c>=1&&f>=0;c--,f--)if(o[c]!==m[f]){if(c!==1||f!==1)do if(c--,f--,f<0||o[c]!==m[f]){var w=`
21 | `+o[c].replace(" at new "," at ");return e.displayName&&w.includes("")&&(w=w.replace("",e.displayName)),typeof e=="function"&&G.set(e,w),w}while(c>=1&&f>=0);break}}}finally{$=!1,q.current=s,fr(),Error.prepareStackTrace=u}var I=e?e.displayName||e.name:"",D=I?J(I):"";return typeof e=="function"&&G.set(e,D),D}function vr(e,r,t){return ge(e,!1)}function pr(e){var r=e.prototype;return!!(r&&r.isReactComponent)}function K(e,r,t){if(e==null)return"";if(typeof e=="function")return ge(e,pr(e));if(typeof e=="string")return J(e);switch(e){case S:return J("Suspense");case h:return J("SuspenseList")}if(typeof e=="object")switch(e.$$typeof){case d:return vr(e.render);case g:return K(e.type,r,t);case T:{var n=e,u=n._payload,s=n._init;try{return K(s(u),r,t)}catch{}}}return""}var U=Object.prototype.hasOwnProperty,_e={},Re=A.ReactDebugCurrentFrame;function z(e){if(e){var r=e._owner,t=K(e.type,e._source,r?r.type:null);Re.setExtraStackFrame(t)}else Re.setExtraStackFrame(null)}function Er(e,r,t,n,u){{var s=Function.call.bind(U);for(var i in e)if(s(e,i)){var o=void 0;try{if(typeof e[i]!="function"){var m=Error((n||"React class")+": "+t+" type `"+i+"` is invalid; it must be a function, usually from the `prop-types` package, but received `"+typeof e[i]+"`.This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");throw m.name="Invariant Violation",m}o=e[i](r,i,n,t,null,"SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED")}catch(c){o=c}o&&!(o instanceof Error)&&(z(u),b("%s: type specification of %s `%s` is invalid; the type checker function must return `null` or an `Error` but returned a %s. You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument).",n||"React class",t,i,typeof o),z(null)),o instanceof Error&&!(o.message in _e)&&(_e[o.message]=!0,z(u),b("Failed %s type: %s",t,o.message),z(null))}}}var yr=Array.isArray;function Q(e){return yr(e)}function hr(e){{var r=typeof Symbol=="function"&&Symbol.toStringTag,t=r&&e[Symbol.toStringTag]||e.constructor.name||"Object";return t}}function br(e){try{return Te(e),!1}catch{return!0}}function Te(e){return""+e}function we(e){if(br(e))return b("The provided key is an unsupported type %s. This value must be coerced to a string before before using it here.",hr(e)),Te(e)}var Pe=A.ReactCurrentOwner,mr={key:!0,ref:!0,__self:!0,__source:!0},Ce,Se;function gr(e){if(U.call(e,"ref")){var r=Object.getOwnPropertyDescriptor(e,"ref").get;if(r&&r.isReactWarning)return!1}return e.ref!==void 0}function _r(e){if(U.call(e,"key")){var r=Object.getOwnPropertyDescriptor(e,"key").get;if(r&&r.isReactWarning)return!1}return e.key!==void 0}function Rr(e,r){typeof e.ref=="string"&&Pe.current}function Tr(e,r){{var t=function(){Ce||(Ce=!0,b("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",r))};t.isReactWarning=!0,Object.defineProperty(e,"key",{get:t,configurable:!0})}}function wr(e,r){{var t=function(){Se||(Se=!0,b("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://reactjs.org/link/special-props)",r))};t.isReactWarning=!0,Object.defineProperty(e,"ref",{get:t,configurable:!0})}}var Pr=function(e,r,t,n,u,s,i){var o={$$typeof:l,type:e,key:r,ref:t,props:i,_owner:s};return o._store={},Object.defineProperty(o._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:!1}),Object.defineProperty(o,"_self",{configurable:!1,enumerable:!1,writable:!1,value:n}),Object.defineProperty(o,"_source",{configurable:!1,enumerable:!1,writable:!1,value:u}),Object.freeze&&(Object.freeze(o.props),Object.freeze(o)),o};function Cr(e,r,t,n,u){{var s,i={},o=null,m=null;t!==void 0&&(we(t),o=""+t),_r(r)&&(we(r.key),o=""+r.key),gr(r)&&(m=r.ref,Rr(r,u));for(s in r)U.call(r,s)&&!mr.hasOwnProperty(s)&&(i[s]=r[s]);if(e&&e.defaultProps){var c=e.defaultProps;for(s in c)i[s]===void 0&&(i[s]=c[s])}if(o||m){var f=typeof e=="function"?e.displayName||e.name||"Unknown":e;o&&Tr(i,f),m&&wr(i,f)}return Pr(e,o,m,u,n,Pe.current,i)}}var ee=A.ReactCurrentOwner,Oe=A.ReactDebugCurrentFrame;function F(e){if(e){var r=e._owner,t=K(e.type,e._source,r?r.type:null);Oe.setExtraStackFrame(t)}else Oe.setExtraStackFrame(null)}var re;re=!1;function te(e){return typeof e=="object"&&e!==null&&e.$$typeof===l}function je(){{if(ee.current){var e=j(ee.current.type);if(e)return`
22 |
23 | Check the render method of \``+e+"`."}return""}}function Sr(e){return""}var xe={};function Or(e){{var r=je();if(!r){var t=typeof e=="string"?e:e.displayName||e.name;t&&(r=`
24 |
25 | Check the top-level render call using <`+t+">.")}return r}}function De(e,r){{if(!e._store||e._store.validated||e.key!=null)return;e._store.validated=!0;var t=Or(r);if(xe[t])return;xe[t]=!0;var n="";e&&e._owner&&e._owner!==ee.current&&(n=" It was passed a child from "+j(e._owner.type)+"."),F(e),b('Each child in a list should have a unique "key" prop.%s%s See https://reactjs.org/link/warning-keys for more information.',t,n),F(null)}}function ke(e,r){{if(typeof e!="object")return;if(Q(e))for(var t=0;t",o=" Did you accidentally export a JSX literal instead of a component?"):c=typeof e,b("React.jsx: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s",c,o)}var f=Cr(e,r,t,u,s);if(f==null)return f;if(i){var w=r.children;if(w!==void 0)if(n)if(Q(w)){for(var I=0;I0?"{key: someKey, "+_.join(": ..., ")+": ...}":"{key: someKey}";if(!Ae[D+ne]){var Ir=_.length>0?"{"+_.join(": ..., ")+": ...}":"{}";b(`A props object containing a "key" prop is being spread into JSX:
26 | let props = %s;
27 | <%s {...props} />
28 | React keys must be passed directly to JSX without using spread:
29 | let props = %s;
30 | <%s key={someKey} {...props} />`,ne,D,Ir,D),Ae[D+ne]=!0}}return e===y?xr(f):jr(f),f}}function Dr(e,r,t){return Fe(e,r,t,!0)}function kr(e,r,t){return Fe(e,r,t,!1)}var Ar=kr,Fr=Dr;W.Fragment=y,W.jsx=Ar,W.jsxs=Fr}()),W}var ie;function Ye(){return ie||(ie=1,process.env.NODE_ENV==="production"?V.exports=Le():V.exports=We()),V.exports}var Ue=Ye(),M,ue;function Ve(){return ue||(ue=1,M=typeof self=="object"&&self.self===self&&self||typeof O=="object"&&O.global===O&&O||M),M}var Me=Ve();const H=Ie(Me),Ne="https://embed.twitch.tv/embed/v1.js",Be=()=>{},Je=(a=Be)=>{if(document.querySelector("script[src='https://embed.twitch.tv/embed/v1.js']"))return;const l=document.createElement("script");l.setAttribute("src",Ne),l.addEventListener("load",a),document.body.append(l)},Ge=()=>{},Ke=a=>p.useCallback((l,E)=>a?(a.addEventListener(l,E),()=>{a.removeEventListener(l,E)}):Ge,[a]),ze=(a,{autoplay:l,onPlay:E})=>{const[y,v]=p.useState(l);return p.useCallback(()=>{if(!a)return;if(y){E&&E();return}a.getPlayer().pause(),v(!0)},[E,a,v,y])},He="twitch-embed",Xe="940",qe="480",se={MUTED:0,AUDIBLE:1},Ze=(a,l)=>{!l&&a.pause()},$e=(a,l)=>{a.setVolume(l?se.MUTED:se.AUDIBLE)},Qe=(...a)=>a,er=(a,{autoplay:l,muted:E,onReady:y})=>p.useCallback(()=>{if(!a)return;const v=a.getPlayer();$e(v,E),Ze(v,l),y&&y(v)},[a,E,l,y]),rr=a=>{const[l,E]=p.useState(),y=p.useCallback(()=>{var R;const v=H;if(((R=v==null?void 0:v.Twitch)==null?void 0:R.Embed)===void 0)return;const P=new v.Twitch.Embed(a.targetId??"",{...a});E(P)},[a]);return Qe(l,y)},le=a=>{const{width:l,height:E,targetId:y,targetClass:v}=a,P=p.useRef(null),[R,C]=rr(a),d=Ke(R),S=er(R,a),h=ze(R,a);return p.useEffect(()=>{var B;const g=H;if(((B=g.Twitch)==null?void 0:B.Embed)===void 0)return;const{VIDEO_PLAY:T,VIDEO_READY:k}=g.Twitch.Embed,N=d(T,h),X=d(k,S);return()=>{X(),N()}},[S,d,h]),p.useEffect(()=>{var T;const g=H;if(P.current&&(P.current.innerHTML=""),(T=g.Twitch)!=null&&T.Embed){C();return}Je(C)},[C]),Ue.jsx("div",{ref:P,style:{width:l,height:E},className:v,id:y})};return le.defaultProps={targetId:He,width:qe,height:Xe,autoplay:!0,muted:!1},p.memo(le)});
31 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { sheriff, tseslint } from 'eslint-config-sheriff';
2 |
3 | const sheriffOptions = {
4 | "react": true,
5 | "lodash": false,
6 | "remeda": false,
7 | "next": false,
8 | "astro": false,
9 | "playwright": false,
10 | "jest": false,
11 | "vitest": true
12 | };
13 |
14 | export default tseslint.config(sheriff(sheriffOptions),
15 | {
16 | rules: {
17 | "@typescript-eslint/explicit-module-boundary-types": [
18 | 0,
19 | ],
20 | "@typescript-eslint/naming-convention": [
21 | 0,
22 | ],
23 | "fsecond/prefer-destructured-optionals": [
24 | 0,
25 | ],
26 | "@typescript-eslint/no-extraneous-class": [
27 | 0,
28 | ],
29 | "no-restricted-syntax": [
30 | 0,
31 | ],
32 | "sonarjs/no-duplicate-string": [0],
33 | "arrow-return-style/arrow-return-style": [0],
34 | },
35 | }
36 | );
37 |
--------------------------------------------------------------------------------
/lefthook.yml:
--------------------------------------------------------------------------------
1 | # EXAMPLE USAGE
2 | # Refer for explanation to following link:
3 | # https://github.com/Arkweid/lefthook/blob/master/docs/full_guide.md
4 | #
5 | # pre-push:
6 | # commands:
7 | # packages-audit:
8 | # tags: frontend security
9 | # run: yarn audit
10 | # gems-audit:
11 | # tags: backend security
12 | # run: bundle audit
13 | #
14 | pre-commit:
15 | parallel: true
16 | commands:
17 | eslint:
18 | glob: "src/*.{js,ts}"
19 | run: yarn lint {staged_files}
20 |
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-twitch-embed-video",
3 | "version": "3.1.0",
4 | "author": "Erik Guzman ",
5 | "description": "Allows you to easily embed a twitch video in your ReactJS application",
6 | "bugs": {
7 | "url": "https://github.com/talk2MeGooseman/react-twitch-embed-video/issues"
8 | },
9 | "homepage": "https://talk2megooseman.github.io/react-twitch-embed-video/",
10 | "license": "MIT",
11 | "type": "module",
12 | "files": [
13 | "dist",
14 | "src",
15 | "tsconfig.json"
16 | ],
17 | "main": "dist/react-twitch-embed-video.umd.cjs",
18 | "module": "dist/react-twitch-embed-video.js",
19 | "types": "dist/index.d.ts",
20 | "exports": {
21 | ".": {
22 | "import": "./dist/react-twitch-embed-video.js",
23 | "require": "./dist/react-twitch-embed-video.umd.cjs"
24 | }
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "https://github.com/talk2MeGooseman/react-twitch-embed-video.git"
29 | },
30 | "scripts": {
31 | "build": "vite build",
32 | "dev": "storybook dev -p 9001",
33 | "lint": "eslint --color src",
34 | "lint:fix": "eslint --fix --color src",
35 | "release-storybook": "storybook build -o .out",
36 | "release": "vite build",
37 | "test": "vitest",
38 | "coverage": "vitest run --coverage"
39 | },
40 | "peerDependencies": {
41 | "react": ">= 17.0.0",
42 | "react-dom": ">= 17.0.0"
43 | },
44 | "devDependencies": {
45 | "@chromatic-com/storybook": "^3",
46 | "@storybook/addon-essentials": "^8.4.7",
47 | "@storybook/preset-create-react-app": "^8.4.7",
48 | "@storybook/react": "^8.4.7",
49 | "@storybook/react-vite": "^8.4.7",
50 | "@storybook/storybook-deployer": "^2.8.16",
51 | "@storybook/theming": "^8.4.7",
52 | "@testing-library/jest-dom": "^6.6.3",
53 | "@testing-library/react": "^16.2.0",
54 | "@types/react": "^19.0.8",
55 | "@types/react-dom": "^19.0.3",
56 | "@types/window-or-global": "^1.0.6",
57 | "@vitejs/plugin-react": "^4.3.0",
58 | "@vitest/coverage-v8": "^3.0.5",
59 | "eslint": "^9.19.0",
60 | "eslint-config-sheriff": "^25.8.0",
61 | "jsdom": "^26.0.0",
62 | "lefthook": "^1.10.10",
63 | "prettier": "^3.4.2",
64 | "react": "^19.0.0",
65 | "react-dom": "^19.0.0",
66 | "storybook": "^8.4.7",
67 | "typescript": "^5.7.3",
68 | "vite": "^6.0.11",
69 | "vite-plugin-dts": "^4.5.0",
70 | "vitest": "^3.0.5"
71 | },
72 | "dependencies": {
73 | "window-or-global": "^1.0.1"
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/@types/types.ts:
--------------------------------------------------------------------------------
1 | export type IAddEventListener = (event: string, callback: () => void) => () => void
2 |
3 | export type IPlayAction = () => void
4 |
5 | export type IReadyAction = () => void
6 |
7 | export type IVideoPlayEventCallback = (player: IPlayerInterface) => void
8 |
9 | export interface IPlaybackStatsInterface {
10 | backendVersion: string
11 | bufferSize: number
12 | codecs: string
13 | displayResolution: string
14 | fps: number
15 | hlsLatencyBroadcaster: number
16 | playbackRate: number
17 | skippedFrames: number
18 | videoResolution: string
19 | }
20 |
21 | export interface IPlayerInterface {
22 | /** Disables display of Closed Captions. */
23 | disableCaptions: () => void
24 | enableCaptions: () => void
25 | pause: () => void
26 | play: () => void
27 | seek: (timestamp: number) => void
28 | setChannel: (channel: string) => void
29 | setCollection: (collection_id: string, video_id: string) => void
30 | setQuality: (quality: string) => void
31 | setVideo: (video_id: string, timestamp: number) => void
32 | getMuted: () => boolean
33 | setMuted: (muted: boolean) => void
34 | getVolume: () => number
35 | setVolume: (volumelevel: number) => void
36 | getPlaybackStats: () => IPlaybackStatsInterface
37 | getChannel: () => string
38 | getCurrentTime: () => number
39 | getDuration: () => number
40 | getEnded: () => boolean
41 | getQualities: () => string[]
42 | getQuality: () => string
43 | getVideo: () => string
44 | isPaused: () => boolean
45 | }
46 |
47 | export type IVideoReadyEventCallback = (player: IPlayerInterface) => void
48 |
49 | export interface IBaseEmbedParameters {
50 | /** If true, the player can go full screen. Default: true. */
51 | allowfullscreen?: boolean
52 | /** If true, the video starts playing automatically, without the user clicking play.
53 | * The exception is mobile platforms, on which video cannot be played without user
54 | * interaction. Default: true. */
55 | autoplay?: boolean
56 | /** Specifies the type of chat to use. Valid values:
57 | *
58 | * _default: Default value, uses full-featured chat._.
59 | *
60 | * _mobile: Uses a read-only version of chat, optimized for mobile devices._.
61 | *
62 | * To omit chat, specify a value of video for the layout option. */
63 | chat?: 'default' | 'mobile'
64 | /** Maximum width of the rendered element, in pixels. This can be expressed as a
65 | * percentage, by passing a string like 100%. */
66 | height?: string | number
67 | /** Determines the screen layout. Valid values:
68 | *
69 | * _video-with-chat: Default if channel is provided. Shows both video and chat side-by-side.
70 | * At narrow sizes, chat renders under the video player.
71 | *
72 | * _video: Default if channel is not provided. Shows only the video player (omits chat). */
73 | layout?: 'video' | 'video-with-chat'
74 | /** Specifies whether the initial state of the video is muted. _Default: false. */
75 | muted?: boolean
76 | /** The video started playing. This callback receives an object with a sessionId property. */
77 | onPlay?: IVideoPlayEventCallback
78 | /** The video player is ready for API commands. This callback receives the player object. */
79 | onReady?: IVideoReadyEventCallback
80 | /** Required if your site is embedded on any domain(s) other than the one that instantiates
81 | * the Twitch embed.
82 | *
83 | * Example parent parameter: ["streamernews.example.com", "embed.example.com"]. */
84 | parent?: string[]
85 | /** Custom class name for div wrapper. */
86 | targetClass?: string
87 | /** Custom id to target, used if you're going to have multiple players on the page. */
88 | targetId?: string
89 | /** The Twitch embed color theme to use.
90 | *
91 | * Valid values: light or dark.
92 | *
93 | * _Default: light or the users chosen theme on Twitch. */
94 | theme?: 'light' | 'dark'
95 | /** Time in the video where playback starts. Specifies hours, minutes, and seconds.
96 | *
97 | * Default: 0h0m0s (the start of the video). */
98 | time?: string
99 | /** Width of video embed including chat. */
100 | width?: string | number
101 | }
102 |
103 | export interface IChannelEmbedParameters extends IBaseEmbedParameters {
104 | /** Optional for VOD embeds; otherwise, required. Name of the chat room and channel to stream. */
105 | channel: string
106 | }
107 |
108 | export interface IVodCollectionEmbedParameters extends IBaseEmbedParameters {
109 | /** The VOD collection to play. If you use this, you may also specify an initial video
110 | * in the VOD collection, otherwise playback will begin with the first video in the collection.
111 | * All VODs are auto-played. Chat replay is not supported.
112 | *
113 | * @example
114 | * `{ video: "124085610", collection: "GMEgKwTQpRQwyA" }` */
115 | collection: { video: string; collection: string }
116 | }
117 |
118 | export interface IVodEmbedParameters extends IBaseEmbedParameters {
119 | /** ID of a VOD to play. Chat replay is not supported. */
120 | video: string
121 | }
122 |
123 | export type IEmbedParameters = IChannelEmbedParameters | IVodCollectionEmbedParameters | IVodEmbedParameters
124 |
125 | export interface ITwitchEmbed {
126 | addEventListener: (
127 | event: string,
128 | callback: IVideoPlayEventCallback | IVideoReadyEventCallback,
129 | ) => void
130 | removeEventListener: (
131 | event: string,
132 | callback: IVideoPlayEventCallback | IVideoReadyEventCallback,
133 | ) => void
134 | getPlayer: () => IPlayerInterface
135 | }
136 |
137 | export interface ITwitchEmbedConstructor {
138 | VIDEO_PLAY: string
139 | VIDEO_READY: string
140 | new(
141 | id: string,
142 | parameters:
143 | | IChannelEmbedParameters
144 | | IVodCollectionEmbedParameters
145 | | IVodEmbedParameters,
146 | ): ITwitchEmbed
147 | }
148 |
149 | export interface ITwitch {
150 | Embed: ITwitchEmbedConstructor
151 | }
152 |
153 | export interface ITwitchWindow {
154 | Twitch?: ITwitch
155 | }
156 |
157 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | // All types, classes and interfaces will be moved here
2 |
3 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from 'react'
2 | import root from 'window-or-global'
3 | import type { IEmbedParameters, ITwitchWindow } from './@types/types'
4 | import { loadEmbedApi } from './loadEmbedApi'
5 | import { useEventListener } from './useEventListener'
6 | import { usePlayerPlay } from './usePlayerPlay'
7 | import { usePlayerReady } from './usePlayerReady'
8 | import {
9 | useTwitchEmbed,
10 | } from './useTwitchEmbed'
11 | import { DEFAULT_HEIGHT, DEFAULT_TARGET_ID, DEFAULT_WIDTH } from './utils'
12 |
13 | const TwitchEmbedVideo = (
14 | props: IEmbedParameters,
15 | ) => {
16 | const {
17 | width = DEFAULT_WIDTH,
18 | height = DEFAULT_HEIGHT,
19 | targetId = DEFAULT_TARGET_ID,
20 | targetClass
21 | } = props
22 |
23 | const containerRef = useRef(null)
24 | const [embed, initializeEmbed] = useTwitchEmbed(props)
25 |
26 | const eventListenerFactory = useEventListener(embed)
27 | const onPlayerReady = usePlayerReady(embed, props)
28 | const onPlayerPlay = usePlayerPlay(embed, props)
29 |
30 | useEffect(() => {
31 | const rootWindow = root as unknown as ITwitchWindow
32 |
33 | if (rootWindow.Twitch?.Embed === undefined) { return }
34 |
35 | const { VIDEO_PLAY, VIDEO_READY } = rootWindow.Twitch.Embed
36 |
37 | const removeVideoPlayListener = eventListenerFactory(
38 | VIDEO_PLAY,
39 | onPlayerPlay,
40 | )
41 |
42 | const removePlayerReadyEventListener = eventListenerFactory(
43 | VIDEO_READY,
44 | onPlayerReady,
45 | )
46 |
47 | return () => {
48 | removePlayerReadyEventListener()
49 | removeVideoPlayListener()
50 | }
51 | }, [onPlayerReady, eventListenerFactory, onPlayerPlay])
52 |
53 | // Builds the Twitch Embed
54 | useEffect(() => {
55 | const rootWindow = root as unknown as ITwitchWindow
56 |
57 | if (containerRef.current) { containerRef.current.innerHTML = '' }
58 |
59 | // Check if we have Twitch in the global space and Embed is available
60 | if (rootWindow.Twitch?.Embed) {
61 | initializeEmbed()
62 |
63 | return
64 | }
65 |
66 | // Initialize the Twitch embed lib if not present
67 | loadEmbedApi(initializeEmbed)
68 | }, [initializeEmbed])
69 |
70 | return (
71 |
77 | )
78 | }
79 |
80 | // eslint-disable-next-line import/no-default-export
81 | export default React.memo(TwitchEmbedVideo)
82 |
--------------------------------------------------------------------------------
/src/loadEmbedApi.test.tsx:
--------------------------------------------------------------------------------
1 | // Generate vitest tests for the loadEmbedApi function
2 | import { loadEmbedApi } from './loadEmbedApi'
3 |
4 | describe('loadEmbedApi', () => {
5 | const scriptMock = {
6 | addEventListener: vi.fn(),
7 | setAttribute: vi.fn(),
8 | }
9 |
10 | const querySelectorMock = vi.fn()
11 | const createElementMock = vi.fn(() => scriptMock)
12 | const appendMock = vi.fn()
13 |
14 | const documentMock = {
15 | querySelector: querySelectorMock,
16 | createElement: createElementMock,
17 | body: { append: appendMock },
18 | }
19 |
20 | vi.stubGlobal('document', documentMock)
21 |
22 | describe('when the script tag already exists', () => {
23 | it('does not inject the Twitch embed script into the dom', () => {
24 | querySelectorMock.mockReturnValue(true)
25 | loadEmbedApi()
26 | expect(querySelectorMock).toHaveBeenCalledWith(
27 | "script[src='https://embed.twitch.tv/embed/v1.js']",
28 | )
29 | expect(createElementMock).not.toHaveBeenCalled()
30 | expect(appendMock).not.toHaveBeenCalled()
31 | })
32 | })
33 |
34 | describe('when the script tag does not exist', () => {
35 | it('injects the Twitch embed script into the dom and adds an event listener', () => {
36 | querySelectorMock.mockReturnValue(false)
37 |
38 | loadEmbedApi()
39 | expect(querySelectorMock).toHaveBeenCalledWith(
40 | "script[src='https://embed.twitch.tv/embed/v1.js']",
41 | )
42 | expect(createElementMock).toHaveBeenCalledWith('script')
43 | expect(scriptMock.setAttribute).toHaveBeenCalledWith(
44 | 'src',
45 | 'https://embed.twitch.tv/embed/v1.js',
46 | )
47 | expect(scriptMock.addEventListener).toHaveBeenCalledWith(
48 | 'load',
49 | expect.any(Function),
50 | )
51 | expect(appendMock).toHaveBeenCalledWith(scriptMock)
52 | })
53 | })
54 | })
55 |
--------------------------------------------------------------------------------
/src/loadEmbedApi.tsx:
--------------------------------------------------------------------------------
1 | export const EMBED_URL = 'https://embed.twitch.tv/embed/v1.js'
2 |
3 | // eslint-disable-next-line @typescript-eslint/no-empty-function
4 | const func: () => unknown = () => {}
5 |
6 | const loadEmbedApi = (callback = func): void => {
7 | // Check if the script tag already exists
8 | if (document.querySelector("script[src='https://embed.twitch.tv/embed/v1.js']")) {return}
9 |
10 | const script = document.createElement('script')
11 |
12 | script.setAttribute('src', EMBED_URL)
13 | // Wait for DOM to finishing loading before we try loading embed
14 | script.addEventListener('load', callback)
15 | document.body.append(script)
16 | }
17 |
18 | export { loadEmbedApi }
19 |
--------------------------------------------------------------------------------
/src/stories/ChannelExample.stories.tsx:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
2 | import React from 'react'
3 | import TwitchEmbedVideo from ".."
4 | import type { IChannelEmbedParameters, IVodCollectionEmbedParameters, IVodEmbedParameters } from '../@types/types'
5 | import { DEFAULT_HEIGHT, DEFAULT_WIDTH } from '../utils'
6 |
7 | export default {
8 | title: 'Channel Example',
9 | component: TwitchEmbedVideo,
10 | args: {
11 | channel: 'talk2megooseman',
12 | width: DEFAULT_WIDTH,
13 | height: DEFAULT_HEIGHT,
14 | autoplay: true,
15 | },
16 | argTypes: {
17 | onPlay: { action: 'Video Playback Started.' },
18 | onReady: { action: 'Player is ready.' },
19 | },
20 | parameters: {
21 | docs: {
22 | description: {
23 | component:
24 | 'Take control of the Twitch Player component in this example and control all aspects of the player. Choose a channel you want to display or change some of the default configurations.',
25 | },
26 | },
27 | },
28 | }
29 |
30 | const Template = (
31 | args:
32 | | IVodCollectionEmbedParameters
33 | | IVodEmbedParameters
34 | | IChannelEmbedParameters,
35 | ) => { return }
36 |
37 | export const ChannelExample = Template.bind({})
38 |
--------------------------------------------------------------------------------
/src/stories/Introduction.mdx:
--------------------------------------------------------------------------------
1 | import { Meta } from '@storybook/blocks';
2 |
3 |
4 |
5 | # React Twitch Embed Video
6 |
7 | [](https://badge.fury.io/js/react-twitch-embed-video)
8 |
9 | Your solution to embedding the Twitch video player in your ReactJS application
10 |
11 | ## Installation
12 |
13 | `yarn add react-twitch-embed-video` or `npm install react-twitch-embed-video`
14 |
15 | ## Usage
16 |
17 | ```
18 | import ReactTwitchEmbedVideo from "react-twitch-embed-video"
19 | .
20 | .
21 | .
22 |
23 |
24 | ```
25 |
26 | Visit the examples on the left margin to play with the component and view the generated example code.
27 |
28 | ## Troubleshooting
29 |
30 | * Video embed not working in Brave browser
31 | * By default Brave block all third party cookies which causes issues using the Twitch Embed Player. In order to get the player to work you either have to allow third party cookies `Setting->Additional Settings->Privacy and Security->Site Settings->Cookies` or you can add Twitch to the whitelist so you can still block other third party cookies.
32 |
--------------------------------------------------------------------------------
/src/stories/MultiplePlayers.stories.tsx:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
2 | import React from 'react'
3 | import TwitchEmbedVideo from ".."
4 |
5 | export default {
6 | title: 'Multiple Players',
7 | component: TwitchEmbedVideo,
8 | parameters: {
9 | docs: {
10 | description: {
11 | component:
12 | 'If you are attempting to have multiple Twitch Video Players on the page at the same time. It is required that you give each additional player a unique **_targetId_** or else each player will attempt use the same iFrame container.',
13 | },
14 | },
15 | },
16 | argTypes: {
17 | player1Channel: {
18 | description:
19 | 'Property to change the channel for the first player, this is only for demo purposes.',
20 | },
21 | player2Channel: {
22 | description:
23 | 'Property to change the channel for the second player, this is only for demo purposes.',
24 | },
25 | },
26 | }
27 |
28 | export const MultiplePlayers = ({
29 | player1Channel,
30 | player2Channel,
31 | }: {
32 | player1Channel: string
33 | player2Channel: string
34 | }) =>
35 | { return <>
36 |
37 |
43 | > }
44 |
45 |
46 | MultiplePlayers.args = {
47 | player1Channel: 'talk2megooseman',
48 | player2Channel: 'lana_lux',
49 | }
50 |
--------------------------------------------------------------------------------
/src/stories/VODExample.stories.tsx:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars
2 | import React from 'react'
3 | import TwitchEmbedVideo from ".."
4 | import type { IEmbedParameters } from '../@types/types'
5 | import { DEFAULT_HEIGHT, DEFAULT_WIDTH } from '../utils'
6 |
7 | export default {
8 | title: 'VOD Example',
9 | component: TwitchEmbedVideo,
10 | args: {
11 | video: '462014255',
12 | width: DEFAULT_WIDTH,
13 | height: DEFAULT_HEIGHT,
14 | autoplay: true,
15 | },
16 | argTypes: {
17 | onPlay: { action: 'Video Playback Started.' },
18 | onReady: { action: 'Player is ready.' },
19 | },
20 | parameters: {
21 | docs: {
22 | description: {
23 | component:
24 | 'To play a VOD all you have to do is pass the Video ID to the `video` prop.',
25 | },
26 | },
27 | },
28 | }
29 |
30 | const Template = (
31 | args:
32 | IEmbedParameters,
33 | ) => { return }
34 |
35 | export const VODExample = Template.bind({})
36 |
--------------------------------------------------------------------------------
/src/test/setup.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom'
2 |
--------------------------------------------------------------------------------
/src/useEventListener.test.tsx:
--------------------------------------------------------------------------------
1 | import { renderHook } from '@testing-library/react'
2 | import type { ITwitchEmbed } from './@types/types'
3 | import { useEventListener } from './useEventListener'
4 |
5 | describe('useEventListener', () => {
6 | const addEventListenerMock = vi.fn()
7 | const removeEventListenerMock = vi.fn()
8 | const embedObj: Partial = {
9 | addEventListener: addEventListenerMock,
10 | removeEventListener: removeEventListenerMock,
11 | }
12 |
13 | const EVENT = 'load'
14 | const callback = vi.fn()
15 |
16 | afterEach(() => {
17 | vi.clearAllMocks()
18 | })
19 |
20 | it('adds an event listener to the embed object', () => {
21 | const { result } = renderHook(() =>
22 | useEventListener(embedObj as ITwitchEmbed),
23 | )
24 |
25 | result.current(EVENT, callback)
26 |
27 | expect(addEventListenerMock).toHaveBeenCalledWith(EVENT, callback)
28 | })
29 |
30 | it('removes the event listener from the embed object', () => {
31 | const { result } = renderHook(() =>
32 | useEventListener(embedObj as ITwitchEmbed),
33 | )
34 |
35 | const cleanUpFunc = result.current(EVENT, callback)
36 |
37 | cleanUpFunc()
38 |
39 | expect(addEventListenerMock).toHaveBeenCalledWith(EVENT, callback)
40 | expect(removeEventListenerMock).toHaveBeenCalledWith(EVENT, callback)
41 | })
42 |
43 | it('returns a noop function when the embed object is undefined', () => {
44 |
45 | const { result } = renderHook(() => useEventListener(undefined))
46 |
47 | const cleanUpFunc = result.current(EVENT, callback)
48 |
49 | cleanUpFunc()
50 |
51 | expect(addEventListenerMock).not.toHaveBeenCalled()
52 | expect(removeEventListenerMock).not.toHaveBeenCalled()
53 | })
54 | })
55 |
--------------------------------------------------------------------------------
/src/useEventListener.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import type { IAddEventListener, ITwitchEmbed } from './@types/types'
3 |
4 | // eslint-disable-next-line @typescript-eslint/no-empty-function
5 | const noop = (): void => {}
6 |
7 | const useEventListener = (
8 | embedObj?: ITwitchEmbed,
9 | ): IAddEventListener =>
10 | { return useCallback(
11 | (event, callback) => {
12 | if (!embedObj) {return noop}
13 |
14 | embedObj.addEventListener(event, callback)
15 |
16 | return () => { embedObj.removeEventListener(event, callback); }
17 | },
18 | [embedObj],
19 | ) }
20 |
21 | export { useEventListener }
22 |
--------------------------------------------------------------------------------
/src/usePlayerPlay.test.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | /* eslint-disable @typescript-eslint/no-unsafe-argument */
3 | import { act, renderHook } from '@testing-library/react'
4 | import { usePlayerPlay } from './usePlayerPlay'
5 |
6 | describe('usePlayerPlay', () => {
7 | const onPlayMock = vi.fn()
8 | const pauseMock = vi.fn()
9 | const playerMock = { pause: pauseMock }
10 | const getPlayerMock = vi.fn(() => playerMock)
11 |
12 | afterEach(() => {
13 | vi.clearAllMocks()
14 | })
15 |
16 | describe('when embedObj is defined', () => {
17 | describe('when shouldForcePlay is true', () => {
18 | it('calls onPlay', () => {
19 | const { result } = renderHook(() =>
20 | usePlayerPlay({ getPlayer: getPlayerMock } as any, {
21 | autoplay: true,
22 | onPlay: onPlayMock,
23 | }),
24 | )
25 |
26 | act(() => {
27 | result.current()
28 | })
29 |
30 | expect(onPlayMock).toHaveBeenCalled()
31 | })
32 |
33 | describe('when onPlay is not defined', () => {
34 | it('returns without error', () => {
35 | const { result } = renderHook(() =>
36 | usePlayerPlay({ getPlayer: getPlayerMock } as any, {
37 | autoplay: true,
38 | }),
39 | )
40 |
41 | act(() => {
42 | expect(() => { result.current(); }).not.toThrow()
43 | })
44 | })
45 | })
46 | })
47 |
48 | describe('when shouldForcePlay is false', () => {
49 | it('calls onPlay with the player', () => {
50 | const { result } = renderHook(() =>
51 | usePlayerPlay({ getPlayer: getPlayerMock } as any, {
52 | autoplay: false,
53 | onPlay: onPlayMock,
54 | }),
55 | )
56 |
57 | act(() => {
58 | result.current()
59 | })
60 |
61 | expect(onPlayMock).toHaveBeenCalledWith(playerMock)
62 | expect(getPlayerMock).toHaveBeenCalled()
63 | })
64 | })
65 | })
66 |
67 | describe('when embedObj is undefined', () => {
68 | it('returns without error', () => {
69 | const { result } = renderHook(() =>
70 | usePlayerPlay(undefined, { autoplay: false }),
71 | )
72 |
73 | expect(() => { result.current(); }).not.toThrow()
74 | })
75 | })
76 | })
77 |
--------------------------------------------------------------------------------
/src/usePlayerPlay.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import type { IBaseEmbedParameters, IPlayAction, ITwitchEmbed } from './@types/types'
3 |
4 | const usePlayerPlay = (
5 | embedObj: ITwitchEmbed | undefined,
6 | {
7 | onPlay,
8 | }: IBaseEmbedParameters,
9 | ): IPlayAction => {
10 | return useCallback(() => {
11 | if (!embedObj) {
12 | return
13 | }
14 | const player = embedObj.getPlayer()
15 |
16 | onPlay?.(player)
17 | }, [onPlay, embedObj])
18 | }
19 |
20 | export { usePlayerPlay }
21 |
--------------------------------------------------------------------------------
/src/usePlayerReady.test.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | /* eslint-disable @typescript-eslint/no-unsafe-argument */
3 | import { act, renderHook } from '@testing-library/react'
4 | import { usePlayerReady } from './usePlayerReady'
5 |
6 | describe('usePlayerReady', () => {
7 | const setVolumeMock = vi.fn()
8 | const setMutedMock = vi.fn()
9 | const getMutedMock = vi.fn()
10 | const pauseMock = vi.fn()
11 | const playMock = vi.fn()
12 | const getPlayerMock = vi.fn(() => {
13 | return {
14 | setVolume: setVolumeMock,
15 | setMuted: setMutedMock,
16 | getMuted: getMutedMock,
17 | pause: pauseMock,
18 | play: playMock,
19 | }
20 | })
21 | const onReadyMock = vi.fn()
22 |
23 | afterEach(() => {
24 | vi.clearAllMocks()
25 | })
26 |
27 | describe('when embedObj is defined', () => {
28 | it('calls the onReady callback with the player', () => {
29 | const { result } = renderHook(() => {
30 | return usePlayerReady({ getPlayer: getPlayerMock } as any, {
31 | onReady: onReadyMock,
32 | })
33 | },
34 | )
35 |
36 | act(() => {
37 | result.current()
38 | })
39 |
40 | expect(onReadyMock).toHaveBeenCalledWith(getPlayerMock())
41 | })
42 |
43 | describe('when autoplay is not set', () => {
44 | it('calls player.play', () => {
45 | const { result } = renderHook(() => {
46 | return usePlayerReady({ getPlayer: getPlayerMock } as any, {
47 | })
48 | },
49 | )
50 |
51 | act(() => {
52 | result.current()
53 | })
54 |
55 | expect(playMock).not.toHaveBeenCalled()
56 | })
57 | })
58 |
59 | describe('when autoplay is true', () => {
60 | it('does not call player.pause', () => {
61 | const { result } = renderHook(() => {
62 | return usePlayerReady({ getPlayer: getPlayerMock } as any, {
63 | autoplay: true,
64 | })
65 | },
66 | )
67 |
68 | act(() => {
69 | result.current()
70 | })
71 |
72 | expect(pauseMock).not.toHaveBeenCalled()
73 | })
74 | })
75 |
76 | describe('when autoplay is false', () => {
77 | it('does call player.pause', () => {
78 | const { result } = renderHook(() => {
79 | return usePlayerReady({ getPlayer: getPlayerMock } as any, {
80 | autoplay: false,
81 | })
82 | },
83 | )
84 |
85 | act(() => {
86 | result.current()
87 | })
88 |
89 | expect(pauseMock).toHaveBeenCalled()
90 | })
91 | })
92 |
93 | describe('when muted is true', () => {
94 | it('calls player.setVolume with 0', () => {
95 | const { result } = renderHook(() => {
96 | return usePlayerReady({ getPlayer: getPlayerMock } as any, {
97 | muted: true,
98 | })
99 | },
100 | )
101 |
102 | act(() => {
103 | result.current()
104 | })
105 |
106 | expect(setVolumeMock).toHaveBeenCalledWith(0)
107 | })
108 | })
109 |
110 | describe('when muted is false', () => {
111 | it('calls player.setVolume with 1', () => {
112 | const { result } = renderHook(() => {
113 | return usePlayerReady({ getPlayer: getPlayerMock } as any, {
114 | muted: false,
115 | })
116 | },
117 | )
118 |
119 | act(() => {
120 | result.current()
121 | })
122 |
123 | expect(setVolumeMock).toHaveBeenCalledWith(1)
124 | })
125 | })
126 |
127 | describe('when onReady is not defined', () => {
128 | it('returns with out error', () => {
129 | const { result } = renderHook(() =>
130 | usePlayerReady({ getPlayer: getPlayerMock } as any, {}),
131 | )
132 |
133 | expect(() => { result.current(); }).not.toThrow()
134 | })
135 | })
136 | })
137 |
138 | describe('when embedObj is undefined', () => {
139 | it('returns with out error', () => {
140 |
141 | const { result } = renderHook(() => usePlayerReady(undefined, {}))
142 |
143 | expect(() => { result.current(); }).not.toThrow()
144 | })
145 | })
146 | })
147 |
--------------------------------------------------------------------------------
/src/usePlayerReady.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import type { IBaseEmbedParameters, IReadyAction, ITwitchEmbed } from './@types/types'
3 | import { enforceAutoPlay, enforceVolume } from './utils'
4 |
5 | const usePlayerReady = (
6 | embedObj: ITwitchEmbed | undefined,
7 | {
8 | autoplay: isAutoPlay,
9 | muted: isMuted = true,
10 | onReady,
11 | }: IBaseEmbedParameters,
12 | ): IReadyAction => {
13 | return useCallback(() => {
14 | if (!embedObj) { return }
15 |
16 | const player = embedObj.getPlayer()
17 |
18 | enforceVolume(player, isMuted)
19 | enforceAutoPlay(player, isAutoPlay)
20 |
21 | onReady && onReady(player)
22 |
23 | }, [embedObj, isMuted, isAutoPlay, onReady])
24 | }
25 |
26 | export { usePlayerReady }
27 |
--------------------------------------------------------------------------------
/src/useTwitchEmbed.test.tsx:
--------------------------------------------------------------------------------
1 | import { act, renderHook } from '@testing-library/react'
2 | import { useTwitchEmbed } from './useTwitchEmbed'
3 |
4 | const mocks = vi.hoisted(() => {
5 | const mockCallback = vi.fn()
6 |
7 | class MockEmbed {
8 | constructor(targetId: string, props: unknown) {
9 | mockCallback(targetId, props)
10 | }
11 | }
12 |
13 | return {
14 | embedMock: MockEmbed,
15 | mockCallback,
16 | }
17 | })
18 |
19 | vi.mock('window-or-global', () => { return {
20 | default: {
21 | Twitch: {
22 | Embed: mocks.embedMock,
23 | },
24 | },
25 | } })
26 |
27 | describe('useTwitchEmbed', () => {
28 | describe('if targetId is provided', () => {
29 | it('returns the embed object', () => {
30 | const { result } = renderHook(() =>
31 |
32 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
33 | { return useTwitchEmbed({
34 | targetId: 'some-id',
35 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
36 | } as any) },
37 | )
38 |
39 | const [embed, initialize] = result.current
40 |
41 | act(() => {
42 | initialize()
43 | })
44 |
45 | expect(embed).toBeUndefined()
46 | expect(mocks.mockCallback).toHaveBeenCalled()
47 |
48 | const [updatedEmbed] = result.current
49 |
50 | expect(updatedEmbed).toBeInstanceOf(mocks.embedMock)
51 | expect(mocks.mockCallback).toHaveBeenCalledWith('some-id', {})
52 | })
53 | })
54 |
55 | describe('if targetId is not provided', () => {
56 | it('returns the embed object, with default props', () => {
57 | const { result } = renderHook(() =>
58 |
59 | // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
60 | { return useTwitchEmbed({
61 | anotherKey: 'value',
62 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
63 | } as any) },
64 | )
65 |
66 | const [embed, initialize] = result.current
67 |
68 | act(() => {
69 | initialize()
70 | })
71 |
72 | expect(embed).toBeUndefined()
73 | expect(mocks.mockCallback).toHaveBeenCalled()
74 |
75 | const [updatedEmbed] = result.current
76 |
77 | expect(updatedEmbed).toBeInstanceOf(mocks.embedMock)
78 | expect(mocks.mockCallback).toHaveBeenCalledWith('twitch-embed', {
79 | anotherKey: 'value',
80 | })
81 | })
82 | })
83 | })
84 |
--------------------------------------------------------------------------------
/src/useTwitchEmbed.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react'
2 | import root from 'window-or-global'
3 | import type { IEmbedParameters, ITwitchEmbed, ITwitchWindow } from './@types/types'
4 | import { DEFAULT_TARGET_ID, tuplify } from './utils'
5 |
6 | const useTwitchEmbed = (
7 | props: IEmbedParameters,
8 | ) => {
9 | const [embed, setEmbed] = useState()
10 |
11 | const initialize = useCallback(() => {
12 | const { targetId = DEFAULT_TARGET_ID, ...otherProps } = props
13 | const rootWindow = root as unknown as ITwitchWindow | null
14 |
15 | if (rootWindow?.Twitch?.Embed === undefined) {
16 | return
17 | }
18 |
19 | const twitchEmbed = new rootWindow.Twitch.Embed(
20 | targetId,
21 | {
22 | ...otherProps,
23 | },
24 | )
25 |
26 | setEmbed(twitchEmbed)
27 | }, [props])
28 |
29 | return tuplify(embed, initialize)
30 | }
31 |
32 | export { useTwitchEmbed }
33 |
--------------------------------------------------------------------------------
/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_TARGET_ID = 'twitch-embed'
2 | export const DEFAULT_WIDTH = '940'
3 | export const DEFAULT_HEIGHT = '480'
4 |
5 | export const Volume = {
6 | MUTED: 0,
7 | AUDIBLE: 1,
8 | } as const
9 |
--------------------------------------------------------------------------------
/src/utils/enforceAutoPlay.test.tsx:
--------------------------------------------------------------------------------
1 | import type { IPlayerInterface } from '../@types/types'
2 | import { enforceAutoPlay } from './enforceAutoPlay'
3 |
4 | describe('enforceAutoPlay', () => {
5 | const playerMock: Partial = {
6 | pause: vi.fn(),
7 | }
8 |
9 | afterEach(() => {
10 | vi.clearAllMocks()
11 | })
12 |
13 | it('should call player.pause() when isAutoPlay is false', () => {
14 | enforceAutoPlay(playerMock as IPlayerInterface, false)
15 |
16 | expect(playerMock.pause).toHaveBeenCalled()
17 | })
18 |
19 | it('should not call player.pause() when isAutoPlay is true', () => {
20 | enforceAutoPlay(playerMock as IPlayerInterface, true)
21 |
22 | expect(playerMock.pause).not.toHaveBeenCalled()
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/src/utils/enforceAutoPlay.tsx:
--------------------------------------------------------------------------------
1 | import type { IPlayerInterface } from "../@types/types"
2 |
3 | const enforceAutoPlay = (
4 | player: IPlayerInterface,
5 | isAutoPlay?: boolean,
6 | ): void => { !isAutoPlay && player.pause() }
7 |
8 | export { enforceAutoPlay }
9 |
--------------------------------------------------------------------------------
/src/utils/enforceVolume.test.tsx:
--------------------------------------------------------------------------------
1 | import type { IPlayerInterface } from '../@types/types'
2 | import { Volume } from './constants'
3 | import { enforceVolume } from './enforceVolume'
4 |
5 | describe('enforceVolume', () => {
6 | const playerMock: Partial = {
7 | setVolume: vi.fn(),
8 | setMuted: vi.fn()
9 | }
10 |
11 | afterEach(() => {
12 | vi.clearAllMocks()
13 | })
14 |
15 | it('should set volume to MUTED when isMuted is true', () => {
16 | enforceVolume(playerMock as IPlayerInterface, true)
17 |
18 | expect(playerMock.setVolume).toHaveBeenCalledWith(Volume.MUTED)
19 | })
20 |
21 | it('should set volume to AUDIBLE when isMuted is false', () => {
22 | enforceVolume(playerMock as IPlayerInterface, false)
23 |
24 | expect(playerMock.setVolume).toHaveBeenCalledWith(Volume.AUDIBLE)
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/src/utils/enforceVolume.tsx:
--------------------------------------------------------------------------------
1 | import type { IPlayerInterface } from '../@types/types'
2 | import { Volume } from './constants'
3 |
4 | export const enforceVolume = (
5 | player: IPlayerInterface,
6 | isMuted?: boolean,
7 | ): void => {
8 | player.setMuted(Boolean(isMuted))
9 | player.setVolume(isMuted ? Volume.MUTED : Volume.AUDIBLE)
10 | }
11 |
--------------------------------------------------------------------------------
/src/utils/index.ts:
--------------------------------------------------------------------------------
1 | export * from './constants'
2 | export * from './enforceAutoPlay'
3 | export * from './enforceVolume'
4 | export * from './tuplify'
5 |
--------------------------------------------------------------------------------
/src/utils/tuplify.ts:
--------------------------------------------------------------------------------
1 | export const tuplify = (...elements: T) => elements
2 |
--------------------------------------------------------------------------------
/test-code-style.md:
--------------------------------------------------------------------------------
1 | Use Vitest for testing TypeScript code
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
4 | "outDir": "./dist/", // path to output directory
5 | "sourceMap": true, // allow sourcemap support
6 | "strict": true,
7 | "strictNullChecks": true, // enable strict null checks as a best practice
8 | "module": "CommonJS", // specify module code generation
9 | "jsx": "react", // use typescript to transpile jsx to js
10 | "target": "es5", // specify ECMAScript target version
11 | "allowJs": true, // allow a partial TypeScript and JavaScript codebase
12 | "skipLibCheck": true, /* Skip type checking of declaration files. */
13 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
14 | "declaration": true,
15 | "types": ["vitest/globals"],
16 | },
17 | "include": [
18 | "./src/",
19 | ".src/test/setup.ts"
20 | ],
21 | "files": ["./src/global.d.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/vite.config.mjs:
--------------------------------------------------------------------------------
1 | ///
2 | /* eslint-disable eslint-comments/disable-enable-pair */
3 | /* eslint-disable import/no-extraneous-dependencies */
4 | import path, { resolve } from 'node:path'
5 | import { fileURLToPath } from 'node:url'
6 |
7 | import react from '@vitejs/plugin-react'
8 | import { defineConfig } from 'vite'
9 | import dts from 'vite-plugin-dts'
10 | // eslint-disable-next-line import/no-unresolved
11 | import { configDefaults } from 'vitest/config'
12 |
13 | export default defineConfig({
14 | plugins: [react(), dts({ rollupTypes: true })],
15 | build: {
16 | lib: {
17 | // Could also be a dictionary or array of multiple entry points
18 | entry: resolve(
19 | path.dirname(fileURLToPath(import.meta.url)),
20 | 'src/index.tsx',
21 | ),
22 | name: 'react-twitch-embed-video',
23 | // the proper extensions will be added
24 | fileName: 'react-twitch-embed-video',
25 | },
26 | rollupOptions: {
27 | external: ['react', 'reactDOM'],
28 | output: {
29 | globals: {
30 | react: 'react',
31 | 'react-dom': 'reactDOM',
32 | },
33 | },
34 | },
35 | },
36 | test: {
37 | globals: true,
38 | environment: 'jsdom',
39 | setupFiles: './src/test/setup.ts',
40 | coverage: {
41 | exclude: [
42 | ...configDefaults.coverage.exclude,
43 | '**/stories/**',
44 | 'src/index.tsx',
45 | ],
46 | },
47 | },
48 | })
49 |
--------------------------------------------------------------------------------