├── .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 | [![npm version](https://badge.fury.io/js/react-twitch-embed-video.svg)](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 | [![npm version](https://badge.fury.io/js/react-twitch-embed-video.svg)](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 | --------------------------------------------------------------------------------