├── .babelrc.js ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── deploy.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── example ├── .babelrc.js ├── app.js ├── index.html └── package.json ├── index.d.ts ├── index.test-d.tsx ├── package.json ├── rollup.config.mjs ├── src ├── eventNames.js └── index.js └── test ├── .eslintrc.js ├── test.js └── util ├── createVimeo.js └── render.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | const TEST = process.env.BABEL_ENV === 'test'; 2 | const CJS = process.env.BABEL_ENV === 'cjs'; 3 | 4 | module.exports = { 5 | presets: [ 6 | ['@babel/env', { 7 | modules: TEST || CJS ? 'commonjs' : false, 8 | loose: true, 9 | targets: TEST ? { node: 'current' } : {}, 10 | }], 11 | '@babel/react', 12 | ], 13 | plugins: TEST ? [ 14 | 'dynamic-import-node', 15 | ] : [], 16 | }; 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | 17 | [*.js] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | example/bundle.js 3 | dist 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'airbnb', 3 | parserOptions: { 4 | ecmaVersion: 2022, 5 | sourceType: 'module', 6 | }, 7 | rules: { 8 | // I disagree 9 | 'react/jsx-filename-extension': 'off', 10 | // I disagree 11 | 'react/require-default-props': 'off', 12 | // Our babel config doesn't support class properties 13 | 'react/state-in-constructor': 'off', 14 | 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 15 | 'jsx-a11y/label-has-for': ['error', { 16 | components: [], 17 | required: { 18 | some: ['nesting', 'id'], 19 | }, 20 | allowChildren: false, 21 | }], 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: github-actions 9 | directory: "/" 10 | schedule: 11 | interval: weekly 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | types: 7 | name: Types 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v4 12 | - name: Install Node.js 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version: lts/* 16 | - name: Install dependencies 17 | run: npm install 18 | - name: Check types 19 | run: npm run tsd 20 | 21 | lint: 22 | name: Code style 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout sources 26 | uses: actions/checkout@v4 27 | - name: Install Node.js 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: lts/* 31 | - name: Install dependencies 32 | run: npm install 33 | - name: Check code style 34 | run: npm run lint 35 | 36 | test: 37 | name: Tests 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | node-version: [16.x, 18.x, 20.x] 42 | react-version: [17.x, 18.x, 19.x] 43 | include: 44 | - node-version: 14.x 45 | react-version: 16.0.0 46 | runs-on: ubuntu-latest 47 | steps: 48 | - name: Checkout sources 49 | uses: actions/checkout@v4 50 | - name: Install Node.js ${{matrix.node-version}} 51 | uses: actions/setup-node@v4 52 | with: 53 | node-version: ${{matrix.node-version}} 54 | - name: Install dependencies 55 | run: npm install 56 | - name: Install React ${{matrix.react-version}} 57 | if: matrix.react-version != '18.x' 58 | run: | 59 | npm install --save-dev \ 60 | react@${{matrix.react-version}} \ 61 | react-dom@${{matrix.react-version}} 62 | - name: Run tests 63 | run: npm run tests-only 64 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - default 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: lts/* 16 | - name: Install 17 | run: npm install 18 | - name: Run tests 19 | run: npm test 20 | - name: Build example 21 | run: | 22 | npm run example 23 | mkdir _deploy 24 | cp example/bundle.js example/index.html _deploy 25 | - name: Publish site 26 | if: success() 27 | uses: crazy-max/ghaction-github-pages@v4 28 | with: 29 | target_branch: gh-pages 30 | build_dir: _deploy 31 | keep_history: true 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist 3 | /example/bundle.js 4 | /generated-docs.md 5 | .eslintcache 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintcache 2 | node_modules 3 | /tools 4 | /example 5 | /generated-docs.md 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @u-wave/react-vimeo change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## 0.9.12 8 | * Add React 19 to supported `peerDependencies` range. (#282) 9 | 10 | ## 0.9.11 11 | * Fix type bug. (d0d38a84d4b5f3f94d6aaad681d0f90492c8ef4c) 12 | 13 | ## 0.9.10 14 | * Fix npm package keywords. (bb944df) 15 | 16 | ## 0.9.9 17 | * Bump minimum `@vimeo/player` version. (@k-p-jones in #191) 18 | * Document `controls` as pro-account-only. (fd61877) 19 | * Add `onPlaying` event, fix documentation for `onPlay`. (@Aliath in #212) 20 | * Add `playbackRate` prop. (@chidimi in #220) 21 | * Support changing the `quality` prop during playback. (941b68b) 22 | * Add support for passing a URL to the `video` prop, eg. for embedding private videos. (e7128b6) 23 | 24 | ## 0.9.8 25 | * Fix a warning about workspaces when installing with yarn. (#190) 26 | 27 | ## 0.9.7 28 | * Add React 18 to supported `peerDependencies` range. (@dockwarder in #189) 29 | * Accept typings from React 17 and 18 … hopefully npm picks the correct version for you :) 30 | 31 | ## 0.9.6 32 | * Add `textTrack`, `playsInline`, `pip`, `keyboard`, `quality` props. (@thanhsonng in #178) 33 | 34 | ## 0.9.5 35 | * Add the `dnt` prop to the typescript definitions. (@k-p-jones in #140) 36 | 37 | ## 0.9.4 38 | * Add prop for the `speed` player option. (@warrenmcquinn in #128) 39 | 40 | This option enables speed controls so the user can select speeds in the Vimeo frame UI, 41 | it does not itself control the playback rate. 42 | 43 | ## 0.9.3 44 | * Add prop for the `dnt` player option. (@k-p-jones in #125) 45 | 46 | ## 0.9.2 47 | * Add prop for the `onPlaybackRateChange` event. (@houmark in #120) 48 | 49 | ## 0.9.1 50 | * Add React 17 to allowed peerDependency range. (#114) 51 | 52 | ## 0.9.0 53 | * Add typescript typings. (#103) 54 | 55 | ## 0.8.3 56 | * Set `sideEffects: false` in package.json. 57 | * Add `style` pass-through property to set CSS properties on the container element. (@Authchirion in #100) 58 | 59 | ## 0.8.2 60 | * Call `onError()` prop if initial load fails (#96). 61 | * Call `setCurrentTime()` after a new video has loaded. (#95) 62 | 63 | ## 0.8.1 64 | * Add color string example to docs. (@ivoilic in #82) 65 | * Fix documentation for union prop types. 66 | 67 | ## 0.8.0 68 | * Add `controls` prop, set to `controls={false}` to disable UI on videos uploaded by pro accounts. (@ljmsouza in #81) 69 | 70 | ## 0.7.0 71 | * Add `responsive` prop that automatically fills the parent element. (@deJong in #80) 72 | 73 | ## 0.6.0 74 | * Add working `onReady` callback. You can use it to get access to the raw [@vimeo/player](https://github.com/vimeo/player.js) instance. 75 | 76 | ## 0.5.0 77 | * Clean up the `@vimeo/player` instance when unmounting. 78 | 79 | ## 0.4.0 80 | * Add `muted` and `background` props from new Vimeo player. (@pgib in #5) 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Renée Kooi 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 | # @u-wave/react-vimeo 2 | 3 | Vimeo player component for React. 4 | 5 | [Install][] - [Usage][] - [Demo][] - [Props][] 6 | 7 | ## Install 8 | 9 | ``` 10 | npm install --save @u-wave/react-vimeo 11 | ``` 12 | 13 | ## Usage 14 | 15 | [Demo][] - [Demo source code][] 16 | 17 | ```js 18 | import Vimeo from '@u-wave/react-vimeo'; 19 | 20 | 24 | ``` 25 | 26 | ## Props 27 | | Name | Type | Default | Description | 28 | |:-----|:-----|:-----|:-----| 29 | | video | number, string | | A Vimeo video ID or URL. | 30 | | id | string | | DOM ID for the player element. | 31 | | className | string | | CSS className for the player element. | 32 | | style | object | | Inline style for container element. | 33 | | width | number, string | | Width of the player element. | 34 | | height | number, string | | Height of the player element. | 35 | | paused | bool | | Pause the video. | 36 | | volume | number | | The playback volume as a number between 0 and 1. | 37 | | start | number | | The time in seconds at which to start playing the video. | 38 | | autopause | bool | true | Pause this video automatically when another one plays. | 39 | | autoplay | bool | false | Automatically start playback of the video. Note that this won’t work on some devices. | 40 | | showByline | bool | true | Show the byline on the video. | 41 | | color | string | | Specify the color of the video controls. Colors may be overridden by the embed settings of the video. _(Ex: "ef2f9f")_ | 42 | | dnt | bool | false | Blocks the player from tracking any session data, including all cookies and analytics. | 43 | | controls | bool | true | Hide all elements in the player, such as the progress bar, sharing buttons, etc. (requires Vimeo PRO / Business account) | 44 | | loop | bool | false | Play the video again when it reaches the end. | 45 | | showPortrait | bool | true | Show the portrait on the video. | 46 | | showTitle | bool | true | Show the title on the video. | 47 | | muted | bool | false | Starts in a muted state to help with autoplay | 48 | | background | bool | false | Starts in a background state with no controls to help with autoplay | 49 | | responsive | bool | false | Enable responsive mode and resize according to parent element (experimental) | 50 | | playbackRate | number | | Specify playback rate (requires Vimeo PRO / Business account) 51 | | speed | bool | false | Enable playback rate controls (requires Vimeo PRO / Business account) | 52 | | keyboard | bool | true | Allows for keyboard input to trigger player events. | 53 | | pip | bool | false | Show the picture-in-picture button in the controlbar and enable the picture-in-picture API. | 54 | | playsInline | bool | true | Play video inline on mobile devices, to automatically go fullscreen on playback set this parameter to false. | 55 | | quality | string | | Vimeo Plus, PRO, and Business members can default an embedded video to a specific quality on desktop. | 56 | | textTrack | string | | Turn captions/subtitles on for a specific language by default. | 57 | | transparent | bool | true | The responsive player and transparent background are enabled by default, to disable set this parameter to false. | 58 | | onReady | function | | Sent when the Vimeo player API has loaded. Receives the Vimeo player object in the first parameter. | 59 | | onError | function | | Sent when the player triggers an error. | 60 | | onPlay | function | | Triggered when video playback is initiated. | 61 | | onPlaying | function | | Triggered when the video starts playing. | 62 | | onPause | function | | Triggered when the video pauses. | 63 | | onEnd | function | | Triggered any time the video playback reaches the end. Note: when `loop` is turned on, the ended event will not fire. | 64 | | onTimeUpdate | function | | Triggered as the `currentTime` of the video updates. It generally fires every 250ms, but it may vary depending on the browser. | 65 | | onProgress | function | | Triggered as the video is loaded. Reports back the amount of the video that has been buffered. | 66 | | onSeeked | function | | Triggered when the player seeks to a specific time. An `onTimeUpdate` event will also be fired at the same time. | 67 | | onTextTrackChange | function | | Triggered when the active text track (captions/subtitles) changes. The values will be `null` if text tracks are turned off. | 68 | | onCueChange | function | | Triggered when the active cue for the current text track changes. It also fires when the active text track changes. There may be multiple cues active. | 69 | | onCuePoint | function | | Triggered when the current time hits a registered cue point. | 70 | | onVolumeChange | function | | Triggered when the volume in the player changes. Some devices do not support setting the volume of the video independently from the system volume, so this event will never fire on those devices. | 71 | | onPlaybackRateChange | function | | Triggered when the playback rate changes. | 72 | | onLoaded | function | | Triggered when a new video is loaded in the player. | 73 | 74 | ## Related 75 | 76 | - [@u-wave/react-youtube][] - A YouTube component with a similar declarative API. 77 | - [react-dailymotion][] - A Dailymotion component with a similar declarative API. 78 | 79 | ## License 80 | 81 | [MIT] 82 | 83 | [Install]: #install 84 | [Usage]: #usage 85 | [Props]: #props 86 | [Demo]: https://u-wave.github.io/react-vimeo 87 | [Demo source code]: ./example 88 | [MIT]: ./LICENSE 89 | [@u-wave/react-youtube]: https://github.com/u-wave/react-youtube 90 | [react-dailymotion]: https://github.com/u-wave/react-dailymotion 91 | -------------------------------------------------------------------------------- /example/.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../.babelrc'); 2 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | /* global document */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import Vimeo from '@u-wave/react-vimeo'; // eslint-disable-line import/no-unresolved 5 | 6 | const videos = [ 7 | { id: 115783408, name: 'Jambinai - Connection' }, 8 | { id: 162959050, name: 'Jambinai - They Keep Silence' }, 9 | { id: 169408731, name: 'Hoody - Like You' }, 10 | ]; 11 | 12 | class App extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | 16 | this.state = { 17 | videoIndex: 0, 18 | volume: 1, 19 | paused: false, 20 | }; 21 | 22 | this.handlePause = this.handlePause.bind(this); 23 | this.handlePlayerPause = this.handlePlayerPause.bind(this); 24 | this.handlePlayerPlay = this.handlePlayerPlay.bind(this); 25 | this.handleVolume = this.handleVolume.bind(this); 26 | } 27 | 28 | handlePause(event) { 29 | this.setState({ 30 | paused: event.target.checked, 31 | }); 32 | } 33 | 34 | handlePlayerPause() { 35 | this.setState({ paused: true }); 36 | } 37 | 38 | handlePlayerPlay() { 39 | this.setState({ paused: false }); 40 | } 41 | 42 | handleVolume(event) { 43 | this.setState({ 44 | volume: parseFloat(event.target.value), 45 | }); 46 | } 47 | 48 | selectVideo(index) { 49 | this.setState({ videoIndex: index }); 50 | } 51 | 52 | render() { 53 | const { videoIndex, paused, volume } = this.state; 54 | 55 | const video = videos[videoIndex]; 56 | return ( 57 |
58 |
59 |
60 | Video 61 |
62 |
63 | {videos.map((choice, index) => ( 64 | this.selectVideo(index)} 68 | > 69 | {choice.name} 70 | 71 | ))} 72 |
73 |
74 | Paused 75 |
76 |

77 | 86 |

87 |
88 | Volume 89 |
90 | 98 |
99 |
100 | 110 |
111 |
112 | ); 113 | } 114 | } 115 | 116 | // eslint-disable-next-line react/no-deprecated 117 | ReactDOM.render(, document.getElementById('example')); 118 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | @u-wave/react-vimeo example 6 | 8 | 18 | 19 | 20 | 32 |
33 |
34 |
35 |

@u-wave/react-vimeo example

36 |

37 | An example Vimeo player using React 38 | and @u-wave/react-vimeo. 39 | view source 40 |

41 |
42 |
43 |
44 |
45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@u-wave/react-vimeo-example", 4 | "description": "@u-wave/react-vimeo example.", 5 | "version": "0.0.0-example", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "esbuild --bundle app.js --loader:.js=jsx --sourcemap=inline --minify > bundle.js", 9 | "start": "serve ." 10 | }, 11 | "dependencies": { 12 | "@u-wave/react-vimeo": "file:..", 13 | "esbuild": "^0.14.0", 14 | "react": "^18.0.0", 15 | "react-dom": "^18.0.0", 16 | "serve": "^13.0.2" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import Player, { Error } from '@vimeo/player' 3 | 4 | export type PlayEvent = { 5 | /** 6 | * The length of the video in seconds. 7 | */ 8 | duration: number 9 | /** 10 | * The amount of the video, in seconds, that has played. 11 | */ 12 | seconds: number 13 | /** 14 | * The amount of the video that has played in comparison to the length of the video; 15 | * multiply by 100 to obtain the percentage. 16 | */ 17 | percent: number 18 | } 19 | 20 | export type PlayingEvent = PlayEvent; 21 | 22 | export type PauseEvent = { 23 | /** 24 | * The length of the video in seconds. 25 | */ 26 | duration: number 27 | /** 28 | * The amount of the video, in seconds, that has played to the pause position. 29 | */ 30 | seconds: number 31 | /** 32 | * The amount of the video that has played to the pause position in comparison to the length of the video; multiply by 100 to obtain the percentage. 33 | */ 34 | percent: number 35 | } 36 | 37 | export type EndEvent = PauseEvent 38 | 39 | export type TimeUpdateEvent = { 40 | /** 41 | * The length of the video in seconds. 42 | */ 43 | duration: number 44 | /** 45 | * The amount of the video, in seconds, that has played from the current playback position. 46 | */ 47 | seconds: number 48 | /** 49 | * The amount of the video that has played from the current playback position in comparison to the length of the video; multiply by 100 to obtain the percentage. 50 | */ 51 | percent: number 52 | } 53 | 54 | export type ProgressEvent = { 55 | /** 56 | * The length of the video in seconds. 57 | */ 58 | duration: number 59 | /** 60 | * The amount of the video, in seconds, that has buffered. 61 | */ 62 | seconds: number 63 | /** 64 | * The amount of the video that has buffered in comparison to the length of the video; 65 | * multiply by 100 to obtain the percentage. 66 | */ 67 | percent: number 68 | } 69 | 70 | export type SeekedEvent = { 71 | /** 72 | * The length of the video in seconds. 73 | */ 74 | duration: number 75 | /** 76 | * The amount of the video, in seconds, that has played from the new seek position. 77 | */ 78 | seconds: number 79 | /** 80 | * The amount of the video that has played from the new seek position in comparison to the length of the video; multiply by 100 to obtain the percentage. 81 | */ 82 | percent: number 83 | } 84 | 85 | export type TextTrackEvent = { 86 | kind: 'captions' | 'subtitles' 87 | label: string 88 | language: string 89 | } 90 | 91 | export type Cue = { 92 | html: string 93 | text: string 94 | } 95 | 96 | export type CueChangeEvent = { 97 | cues: Cue[] 98 | kind: 'captions' | 'subtitles' 99 | label: string 100 | language: string 101 | } 102 | 103 | export type CuePointEvent = { 104 | /** 105 | * The location of the cue point in seconds. 106 | */ 107 | time: number 108 | /** 109 | * The ID of the cue point. 110 | */ 111 | id: string 112 | /** 113 | * The custom data from the `addCuePoint()` call, or an empty object. 114 | */ 115 | data: object 116 | } 117 | 118 | export type VolumeEvent = { 119 | /** 120 | * The new volume level. 121 | */ 122 | volume: number 123 | } 124 | 125 | export type PlaybackRateEvent = { 126 | /** 127 | * The new playback rate. 128 | */ 129 | playbackRate: number 130 | } 131 | 132 | export type LoadEvent = { 133 | /** 134 | * The ID of the new video. 135 | */ 136 | id: number 137 | } 138 | 139 | export interface VimeoProps { 140 | /** 141 | * A Vimeo video ID or URL. 142 | */ 143 | video: number | string 144 | /** 145 | * DOM ID for the player element. 146 | */ 147 | id?: string 148 | /** 149 | * CSS className for the player element. 150 | */ 151 | className?: string 152 | /** 153 | * Inline style for container element. 154 | */ 155 | style?: React.CSSProperties 156 | /** 157 | * Width of the player element. 158 | */ 159 | width?: number | string 160 | /** 161 | * Height of the player element. 162 | */ 163 | height?: number | string 164 | 165 | /** 166 | * Pause the video. 167 | */ 168 | paused?: boolean 169 | 170 | /** 171 | * The playback volume as a number between 0 and 1. 172 | */ 173 | volume?: number 174 | 175 | /** 176 | * The time in seconds at which to start playing the video. 177 | */ 178 | start?: number 179 | 180 | /** 181 | * Pause this video automatically when another one plays. 182 | */ 183 | autopause?: boolean 184 | 185 | /** 186 | * Automatically start playback of the video. Note that this won’t work on 187 | * some devices. 188 | */ 189 | autoplay?: boolean 190 | 191 | /** 192 | * Show the byline on the video. 193 | */ 194 | showByline?: boolean 195 | 196 | /** 197 | * Specify the color of the video controls. Colors may be overridden by the 198 | * embed settings of the video. _(Ex: "ef2f9f")_ 199 | */ 200 | color?: string 201 | 202 | /** 203 | * Hide all elements in the player, such as the progress bar, sharing buttons, etc. 204 | * (requires Vimeo PRO / Business account) 205 | */ 206 | controls?: boolean 207 | 208 | /** 209 | * Play the video again when it reaches the end. 210 | */ 211 | loop?: boolean 212 | 213 | /** 214 | * Show the portrait on the video. 215 | */ 216 | showPortrait?: boolean 217 | 218 | /** 219 | * Show the title on the video. 220 | */ 221 | showTitle?: boolean 222 | 223 | /** 224 | * Starts in a muted state to help with autoplay 225 | */ 226 | muted?: boolean 227 | 228 | /** 229 | * Starts in a background state with no controls to help with autoplay 230 | */ 231 | background?: boolean 232 | 233 | /** 234 | * Enable responsive mode and resize according to parent element (experimental) 235 | */ 236 | responsive?: boolean 237 | 238 | /** 239 | * Specify playback rate (requires Vimeo PRO / Business account) 240 | */ 241 | playbackRate?: number 242 | 243 | /** 244 | * Enable playback rate controls (requires Vimeo PRO / Business account) 245 | */ 246 | speed?: boolean 247 | 248 | /** 249 | * Blocks the player from tracking any session data, including all cookies and analytics 250 | */ 251 | dnt?: boolean 252 | 253 | /** 254 | * Allows for keyboard input to trigger player events. 255 | */ 256 | keyboard?: boolean 257 | 258 | /** 259 | * Show the picture-in-picture button in the controlbar 260 | * and enable the picture-in-picture API. 261 | */ 262 | pip?: boolean 263 | 264 | /** 265 | * Play video inline on mobile devices, to automatically 266 | * go fullscreen on playback set this parameter to false. 267 | */ 268 | playsInline?: boolean 269 | 270 | /** 271 | * Vimeo Plus, PRO, and Business members can default 272 | * an embedded video to a specific quality on desktop. 273 | */ 274 | quality?: string 275 | 276 | /** 277 | * Turn captions/subtitles on for a specific language by default. 278 | */ 279 | textTrack?: string 280 | 281 | /** 282 | * The responsive player and transparent background are enabled 283 | * by default, to disable set this parameter to false. 284 | */ 285 | transparent?: boolean 286 | 287 | /** 288 | * Sent when the Vimeo player API has loaded. 289 | * Receives the Vimeo player object in the first parameter. 290 | */ 291 | onReady?: (player: Player) => void 292 | /** 293 | * Sent when the player triggers an error. 294 | */ 295 | onError?: (error: Error) => void 296 | /** 297 | * Triggered when video playback is initiated. 298 | */ 299 | onPlay?: (event: PlayEvent) => void 300 | /** 301 | * Triggered when the video starts playing. 302 | */ 303 | onPlaying?: (event: PlayingEvent) => void 304 | /** 305 | * Triggered when the video pauses. 306 | */ 307 | onPause?: (event: PauseEvent) => void 308 | /** 309 | * Triggered any time the video playback reaches the end. 310 | * Note: when `loop` is turned on, the ended event will not fire. 311 | */ 312 | onEnd?: (event: EndEvent) => void 313 | /** 314 | * Triggered as the `currentTime` of the video updates. It generally fires 315 | * every 250ms, but it may vary depending on the browser. 316 | */ 317 | onTimeUpdate?: (event: TimeUpdateEvent) => void 318 | /** 319 | * Triggered as the video is loaded. Reports back the amount of the video 320 | * that has been buffered. 321 | */ 322 | onProgress?: (event: ProgressEvent) => void 323 | /** 324 | * Triggered when the player seeks to a specific time. An `onTimeUpdate` 325 | * event will also be fired at the same time. 326 | */ 327 | onSeeked?: (event: SeekedEvent) => void 328 | /** 329 | * Triggered when the active text track (captions/subtitles) changes. The 330 | * values will be `null` if text tracks are turned off. 331 | */ 332 | onTextTrackChange?: (event: TextTrackEvent) => void 333 | /** 334 | * Triggered when the active cue for the current text track changes. It also 335 | * fires when the active text track changes. There may be multiple cues 336 | * active. 337 | */ 338 | onCueChange?: (event: CueChangeEvent) => void 339 | /** 340 | * Triggered when the current time hits a registered cue point. 341 | */ 342 | onCuePoint?: (event: CuePointEvent) => void 343 | /** 344 | * Triggered when the volume in the player changes. Some devices do not 345 | * support setting the volume of the video independently from the system 346 | * volume, so this event will never fire on those devices. 347 | */ 348 | onVolumeChange?: (event: VolumeEvent) => void 349 | /** 350 | * Triggered when the playback rate in the player changes. 351 | */ 352 | onPlaybackRateChange?: (event: PlaybackRateEvent) => void 353 | /** 354 | * Triggered when a new video is loaded in the player. 355 | */ 356 | onLoaded?: (event: LoadEvent) => void 357 | } 358 | 359 | /** 360 | * Vimeo player component for React. 361 | */ 362 | export default class Vimeo extends React.Component {} 363 | -------------------------------------------------------------------------------- /index.test-d.tsx: -------------------------------------------------------------------------------- 1 | import { expectError, expectType } from 'tsd' 2 | import * as React from 'react' 3 | import Vimeo from '.' 4 | 5 | // Missing required prop `video`. 6 | expectError() 7 | 8 | { 9 | const element = ( 10 | { 13 | expectType(event.seconds) 14 | expectType(event.duration) 15 | expectType(event.percent) 16 | }} 17 | /> 18 | ) 19 | } 20 | 21 | { 22 | const element = 23 | } 24 | 25 | { 26 | const element = ( 27 | { 30 | player.getCurrentTime() 31 | }} 32 | /> 33 | ) 34 | } 35 | 36 | { 37 | const element = ( 38 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@u-wave/react-vimeo", 3 | "version": "0.9.12", 4 | "description": "Vimeo player component for React.", 5 | "main": "dist/react-vimeo.js", 6 | "module": "dist/react-vimeo.es.js", 7 | "types": "index.d.ts", 8 | "scripts": { 9 | "prepare": "npm run build", 10 | "build": "rollup -c", 11 | "lint": "eslint --cache .", 12 | "test": "npm run lint && npm run tests-only && npm run tsd", 13 | "tests-only": "cross-env BABEL_ENV=test mocha --require @babel/register test/*.js", 14 | "tsd": "tsd", 15 | "docs": "prop-types-table src/index.js | md-insert README.md --header Props -i", 16 | "example": "npm run --prefix example build" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/u-wave/react-vimeo.git" 21 | }, 22 | "keywords": [ 23 | "vimeo", 24 | "react", 25 | "player", 26 | "react-component", 27 | "video" 28 | ], 29 | "author": "Renée Kooi ", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/u-wave/react-vimeo/issues" 33 | }, 34 | "homepage": "https://github.com/u-wave/react-vimeo#readme", 35 | "dependencies": { 36 | "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", 37 | "@types/vimeo__player": "^2.10.0", 38 | "@vimeo/player": "^2.16.4", 39 | "prop-types": "^15.7.2" 40 | }, 41 | "peerDependencies": { 42 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.12.10", 46 | "@babel/preset-env": "^7.12.10", 47 | "@babel/preset-react": "^7.12.10", 48 | "@babel/register": "^7.12.10", 49 | "@rollup/plugin-babel": "^6.0.0", 50 | "@u-wave/react-vimeo-example": "file:example", 51 | "babel-plugin-dynamic-import-node": "^2.3.3", 52 | "cross-env": "^7.0.3", 53 | "eslint": "^8.2.0", 54 | "eslint-config-airbnb": "^19.0.0", 55 | "eslint-plugin-import": "^2.25.3", 56 | "eslint-plugin-jsx-a11y": "^6.5.1", 57 | "eslint-plugin-react": "^7.27.0", 58 | "eslint-plugin-react-hooks": "^4.3.0", 59 | "expect": "^1.20.2", 60 | "md-insert": "^2.0.0", 61 | "min-react-env": "^2.0.0", 62 | "mocha": "^10.0.0", 63 | "prop-types-table": "^1.0.0", 64 | "proxyquire": "^2.1.3", 65 | "react": "^18.0.0", 66 | "react-dom": "^18.0.0", 67 | "rollup": "^3.1.0", 68 | "tsd": "^0.31.0" 69 | }, 70 | "sideEffects": false 71 | } 72 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import babel from '@rollup/plugin-babel'; 3 | 4 | const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8')); 5 | 6 | process.env.BABEL_ENV = 'rollup'; 7 | 8 | export default { 9 | input: './src/index.js', 10 | output: [ 11 | { format: 'cjs', file: pkg.main, exports: 'named' }, 12 | { format: 'es', file: pkg.module }, 13 | ], 14 | 15 | external: Object.keys(pkg.dependencies) 16 | .concat(Object.keys(pkg.peerDependencies)), 17 | plugins: [ 18 | babel({ 19 | babelHelpers: 'bundled', 20 | }), 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /src/eventNames.js: -------------------------------------------------------------------------------- 1 | export default { 2 | play: 'onPlay', 3 | playing: 'onPlaying', 4 | pause: 'onPause', 5 | ended: 'onEnd', 6 | timeupdate: 'onTimeUpdate', 7 | progress: 'onProgress', 8 | seeked: 'onSeeked', 9 | texttrackchange: 'onTextTrackChange', 10 | cuechange: 'onCueChange', 11 | cuepoint: 'onCuePoint', 12 | volumechange: 'onVolumeChange', 13 | playbackratechange: 'onPlaybackRateChange', 14 | error: 'onError', 15 | loaded: 'onLoaded', 16 | }; 17 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Player from '@vimeo/player'; 4 | import eventNames from './eventNames'; 5 | 6 | class Vimeo extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.refContainer = this.refContainer.bind(this); 11 | } 12 | 13 | componentDidMount() { 14 | this.createPlayer(); 15 | } 16 | 17 | componentDidUpdate(prevProps) { 18 | // eslint-disable-next-line react/destructuring-assignment 19 | const changes = Object.keys(this.props).filter((name) => this.props[name] !== prevProps[name]); 20 | 21 | this.updateProps(changes); 22 | } 23 | 24 | componentWillUnmount() { 25 | this.player.destroy(); 26 | } 27 | 28 | /** 29 | * @private 30 | */ 31 | getInitialOptions() { 32 | const { video } = this.props; 33 | const videoType = /^https?:/i.test(video) ? 'url' : 'id'; 34 | /* eslint-disable react/destructuring-assignment */ 35 | return { 36 | [videoType]: video, 37 | width: this.props.width, 38 | height: this.props.height, 39 | autopause: this.props.autopause, 40 | autoplay: this.props.autoplay, 41 | byline: this.props.showByline, 42 | color: this.props.color, 43 | controls: this.props.controls, 44 | loop: this.props.loop, 45 | portrait: this.props.showPortrait, 46 | title: this.props.showTitle, 47 | muted: this.props.muted, 48 | background: this.props.background, 49 | responsive: this.props.responsive, 50 | dnt: this.props.dnt, 51 | speed: this.props.speed, 52 | keyboard: this.props.keyboard, 53 | pip: this.props.pip, 54 | playsinline: this.props.playsInline, 55 | quality: this.props.quality, 56 | texttrack: this.props.textTrack, 57 | transparent: this.props.transparent, 58 | }; 59 | /* eslint-enable react/destructuring-assignment */ 60 | } 61 | 62 | /** 63 | * @private 64 | */ 65 | updateProps(propNames) { 66 | const { player } = this; 67 | propNames.forEach((name) => { 68 | // eslint-disable-next-line react/destructuring-assignment 69 | const value = this.props[name]; 70 | switch (name) { 71 | case 'autopause': 72 | player.setAutopause(value); 73 | break; 74 | case 'color': 75 | player.setColor(value); 76 | break; 77 | case 'loop': 78 | player.setLoop(value); 79 | break; 80 | case 'volume': 81 | player.setVolume(value); 82 | break; 83 | case 'paused': 84 | player.getPaused().then((paused) => { 85 | if (value && !paused) { 86 | return player.pause(); 87 | } 88 | if (!value && paused) { 89 | return player.play(); 90 | } 91 | return null; 92 | }); 93 | break; 94 | case 'width': 95 | case 'height': 96 | player.element[name] = value; 97 | break; 98 | case 'video': 99 | if (value) { 100 | const { start } = this.props; 101 | const loaded = player.loadVideo(value); 102 | // Set the start time only when loading a new video. 103 | // It seems like this has to be done after the video has loaded, else it just starts at 104 | // the beginning! 105 | if (typeof start === 'number') { 106 | loaded.then(() => { 107 | player.setCurrentTime(start); 108 | }); 109 | } 110 | } else { 111 | player.unload(); 112 | } 113 | break; 114 | case 'playbackRate': 115 | player.setPlaybackRate(value); 116 | break; 117 | case 'quality': 118 | player.setQuality(value); 119 | break; 120 | default: 121 | // Nothing 122 | } 123 | }); 124 | } 125 | 126 | /** 127 | * @private 128 | */ 129 | createPlayer() { 130 | const { start, volume, playbackRate } = this.props; 131 | 132 | this.player = new Player(this.container, this.getInitialOptions()); 133 | 134 | Object.keys(eventNames).forEach((dmName) => { 135 | const reactName = eventNames[dmName]; 136 | this.player.on(dmName, (event) => { 137 | // eslint-disable-next-line react/destructuring-assignment 138 | const handler = this.props[reactName]; 139 | if (handler) { 140 | handler(event); 141 | } 142 | }); 143 | }); 144 | 145 | const { onError, onReady } = this.props; 146 | this.player.ready().then(() => { 147 | if (onReady) { 148 | onReady(this.player); 149 | } 150 | }, (err) => { 151 | if (onError) { 152 | onError(err); 153 | } else { 154 | throw err; 155 | } 156 | }); 157 | 158 | if (typeof start === 'number') { 159 | this.player.setCurrentTime(start); 160 | } 161 | 162 | if (typeof volume === 'number') { 163 | this.updateProps(['volume']); 164 | } 165 | 166 | if (typeof playbackRate === 'number') { 167 | this.updateProps(['playbackRate']); 168 | } 169 | } 170 | 171 | /** 172 | * @private 173 | */ 174 | refContainer(container) { 175 | this.container = container; 176 | } 177 | 178 | render() { 179 | const { id, className, style } = this.props; 180 | 181 | return ( 182 |
188 | ); 189 | } 190 | } 191 | 192 | if (process.env.NODE_ENV !== 'production') { 193 | Vimeo.propTypes = { 194 | /** 195 | * A Vimeo video ID or URL. 196 | */ 197 | video: PropTypes.oneOfType([ 198 | PropTypes.number, 199 | PropTypes.string, 200 | ]), 201 | /** 202 | * DOM ID for the player element. 203 | */ 204 | id: PropTypes.string, 205 | /** 206 | * CSS className for the player element. 207 | */ 208 | className: PropTypes.string, 209 | /** 210 | * Inline style for container element. 211 | */ 212 | style: PropTypes.object, // eslint-disable-line react/forbid-prop-types 213 | /** 214 | * Width of the player element. 215 | */ 216 | width: PropTypes.oneOfType([ 217 | PropTypes.number, 218 | PropTypes.string, 219 | ]), 220 | /** 221 | * Height of the player element. 222 | */ 223 | height: PropTypes.oneOfType([ 224 | PropTypes.number, 225 | PropTypes.string, 226 | ]), 227 | 228 | /** 229 | * Pause the video. 230 | */ 231 | paused: PropTypes.bool, // eslint-disable-line react/no-unused-prop-types 232 | 233 | /** 234 | * The playback volume as a number between 0 and 1. 235 | */ 236 | volume: PropTypes.number, 237 | 238 | /** 239 | * The time in seconds at which to start playing the video. 240 | */ 241 | start: PropTypes.number, 242 | 243 | // Player parameters 244 | /** 245 | * Pause this video automatically when another one plays. 246 | */ 247 | autopause: PropTypes.bool, 248 | 249 | /** 250 | * Automatically start playback of the video. Note that this won’t work on 251 | * some devices. 252 | */ 253 | autoplay: PropTypes.bool, 254 | 255 | /** 256 | * Show the byline on the video. 257 | */ 258 | showByline: PropTypes.bool, 259 | 260 | /** 261 | * Specify the color of the video controls. Colors may be overridden by the 262 | * embed settings of the video. _(Ex: "ef2f9f")_ 263 | */ 264 | color: PropTypes.string, 265 | 266 | /** 267 | * Blocks the player from tracking any session data, including all cookies and analytics. 268 | */ 269 | dnt: PropTypes.bool, 270 | 271 | // Player controls 272 | /** 273 | * Hide all elements in the player, such as the progress bar, sharing buttons, etc. 274 | * (requires Vimeo PRO / Business account) 275 | */ 276 | controls: PropTypes.bool, 277 | 278 | /** 279 | * Play the video again when it reaches the end. 280 | */ 281 | loop: PropTypes.bool, 282 | 283 | /** 284 | * Show the portrait on the video. 285 | */ 286 | showPortrait: PropTypes.bool, 287 | 288 | /** 289 | * Show the title on the video. 290 | */ 291 | showTitle: PropTypes.bool, 292 | 293 | /** 294 | * Starts in a muted state to help with autoplay 295 | */ 296 | muted: PropTypes.bool, 297 | 298 | /** 299 | * Starts in a background state with no controls to help with autoplay 300 | */ 301 | background: PropTypes.bool, 302 | 303 | /** 304 | * Enable responsive mode and resize according to parent element (experimental) 305 | */ 306 | responsive: PropTypes.bool, 307 | 308 | /** 309 | * Specify playback rate (requires Vimeo PRO / Business account) 310 | */ 311 | playbackRate: PropTypes.number, 312 | 313 | /** 314 | * Enable playback rate controls (requires Vimeo PRO / Business account) 315 | */ 316 | speed: PropTypes.bool, 317 | 318 | /** 319 | * Allows for keyboard input to trigger player events. 320 | */ 321 | keyboard: PropTypes.bool, 322 | 323 | /** 324 | * Show the picture-in-picture button in the controlbar 325 | * and enable the picture-in-picture API. 326 | */ 327 | pip: PropTypes.bool, 328 | 329 | /** 330 | * Play video inline on mobile devices, to automatically 331 | * go fullscreen on playback set this parameter to false. 332 | */ 333 | playsInline: PropTypes.bool, 334 | 335 | /** 336 | * Vimeo Plus, PRO, and Business members can default 337 | * an embedded video to a specific quality on desktop. 338 | */ 339 | quality: PropTypes.string, 340 | 341 | /** 342 | * Turn captions/subtitles on for a specific language by default. 343 | */ 344 | textTrack: PropTypes.string, 345 | 346 | /** 347 | * The responsive player and transparent background are enabled 348 | * by default, to disable set this parameter to false. 349 | */ 350 | transparent: PropTypes.bool, 351 | 352 | // Events 353 | /* eslint-disable react/no-unused-prop-types */ 354 | 355 | /** 356 | * Sent when the Vimeo player API has loaded. 357 | * Receives the Vimeo player object in the first parameter. 358 | */ 359 | onReady: PropTypes.func, 360 | /** 361 | * Sent when the player triggers an error. 362 | */ 363 | onError: PropTypes.func, 364 | /** 365 | * Triggered when video playback is initiated. 366 | */ 367 | onPlay: PropTypes.func, 368 | /** 369 | * Triggered when the video starts playing. 370 | */ 371 | onPlaying: PropTypes.func, 372 | /** 373 | * Triggered when the video pauses. 374 | */ 375 | onPause: PropTypes.func, 376 | /** 377 | * Triggered any time the video playback reaches the end. 378 | * Note: when `loop` is turned on, the ended event will not fire. 379 | */ 380 | onEnd: PropTypes.func, 381 | /** 382 | * Triggered as the `currentTime` of the video updates. It generally fires 383 | * every 250ms, but it may vary depending on the browser. 384 | */ 385 | onTimeUpdate: PropTypes.func, 386 | /** 387 | * Triggered as the video is loaded. Reports back the amount of the video 388 | * that has been buffered. 389 | */ 390 | onProgress: PropTypes.func, 391 | /** 392 | * Triggered when the player seeks to a specific time. An `onTimeUpdate` 393 | * event will also be fired at the same time. 394 | */ 395 | onSeeked: PropTypes.func, 396 | /** 397 | * Triggered when the active text track (captions/subtitles) changes. The 398 | * values will be `null` if text tracks are turned off. 399 | */ 400 | onTextTrackChange: PropTypes.func, 401 | /** 402 | * Triggered when the active cue for the current text track changes. It also 403 | * fires when the active text track changes. There may be multiple cues 404 | * active. 405 | */ 406 | onCueChange: PropTypes.func, 407 | /** 408 | * Triggered when the current time hits a registered cue point. 409 | */ 410 | onCuePoint: PropTypes.func, 411 | /** 412 | * Triggered when the volume in the player changes. Some devices do not 413 | * support setting the volume of the video independently from the system 414 | * volume, so this event will never fire on those devices. 415 | */ 416 | onVolumeChange: PropTypes.func, 417 | /** 418 | * Triggered when the playback rate changes. 419 | */ 420 | onPlaybackRateChange: PropTypes.func, 421 | /** 422 | * Triggered when a new video is loaded in the player. 423 | */ 424 | onLoaded: PropTypes.func, 425 | 426 | /* eslint-enable react/no-unused-prop-types */ 427 | }; 428 | } 429 | 430 | Vimeo.defaultProps = { 431 | autopause: true, 432 | autoplay: false, 433 | showByline: true, 434 | controls: true, 435 | loop: false, 436 | showPortrait: true, 437 | showTitle: true, 438 | muted: false, 439 | background: false, 440 | responsive: false, 441 | dnt: false, 442 | speed: false, 443 | keyboard: true, 444 | pip: false, 445 | playsInline: true, 446 | transparent: true, 447 | }; 448 | 449 | export default Vimeo; 450 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../.eslintrc.js', 3 | env: { 4 | mocha: true, 5 | }, 6 | rules: { 7 | // We have good reasons 8 | 'react/jsx-props-no-spreading': 'off', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import expect, { createSpy } from 'expect'; 2 | import render from './util/render'; 3 | 4 | describe('Vimeo', () => { 5 | it('should create a Vimeo player when mounted', async () => { 6 | const onReady = createSpy(); 7 | const { sdkMock, playerMock } = await render({ 8 | video: 169408731, 9 | onReady, 10 | }); 11 | expect(sdkMock).toHaveBeenCalled(); 12 | expect(sdkMock.calls[0].arguments[1]).toMatch({ id: 169408731 }); 13 | await playerMock.ready(); 14 | expect(onReady).toHaveBeenCalled(); 15 | expect(onReady.calls[0].arguments[0]).toBe(playerMock); 16 | }); 17 | 18 | it('should use `url` prop for full vimeo URLs', async () => { 19 | const { sdkMock } = await render({ video: 'https://vimeo.com/179290396' }); 20 | expect(sdkMock).toHaveBeenCalled(); 21 | expect(sdkMock.calls[0].arguments[1]).toMatch({ url: 'https://vimeo.com/179290396' }); 22 | }); 23 | 24 | it('should all onError when `ready()` fails', async () => { 25 | const onError = createSpy(); 26 | const { sdkMock } = await render({ 27 | video: 404, 28 | shouldFail: true, 29 | onError, 30 | }); 31 | await Promise.resolve(); 32 | expect(sdkMock).toHaveBeenCalled(); 33 | expect(sdkMock.calls[0].arguments[1]).toMatch({ id: 404 }); 34 | expect(onError).toHaveBeenCalled(); 35 | expect(onError.calls[0].arguments[0]).toEqual(new Error('artificial failure')); 36 | }); 37 | 38 | it('should load a different video when "video" prop changes', async () => { 39 | const { sdkMock, playerMock, rerender } = await render({ 40 | video: 169408731, 41 | }); 42 | expect(sdkMock).toHaveBeenCalled(); 43 | expect(sdkMock.calls[0].arguments[1]).toMatch({ id: 169408731 }); 44 | 45 | await rerender({ video: 162959050 }); 46 | 47 | expect(playerMock.loadVideo).toHaveBeenCalled(); 48 | expect(playerMock.loadVideo.calls[0].arguments[0]).toEqual(162959050); 49 | }); 50 | 51 | it('should pause the video using the "paused" prop', async () => { 52 | const { playerMock, rerender } = await render({ 53 | video: 169408731, 54 | autoplay: true, 55 | }); 56 | 57 | // Don't call `play` again when we were already playing 58 | await rerender({ paused: false }); 59 | expect(playerMock.play).toNotHaveBeenCalled(); 60 | 61 | await rerender({ paused: true }); 62 | expect(playerMock.pause).toHaveBeenCalled(); 63 | 64 | await rerender({ paused: false }); 65 | expect(playerMock.play).toHaveBeenCalled(); 66 | }); 67 | 68 | it('should set the volume using the "volume" prop', async () => { 69 | const { playerMock, rerender } = await render({ 70 | video: 169408731, 71 | volume: 0.5, 72 | }); 73 | expect(playerMock.setVolume).toHaveBeenCalledWith(0.5); 74 | 75 | await rerender({ volume: 1 }); 76 | 77 | expect(playerMock.setVolume).toHaveBeenCalledWith(1); 78 | }); 79 | 80 | it('should set the start time using the "start" prop', async () => { 81 | const { playerMock, rerender } = await render({ 82 | video: 169408731, 83 | start: 60, 84 | }); 85 | expect(playerMock.setCurrentTime).toHaveBeenCalledWith(60); 86 | 87 | playerMock.setCurrentTime.reset(); 88 | await rerender({ start: 90 }); 89 | expect(playerMock.setCurrentTime).toNotHaveBeenCalled(); 90 | 91 | await rerender({ video: 169408732, start: 120 }); 92 | expect(playerMock.setCurrentTime).toHaveBeenCalledWith(120); 93 | }); 94 | 95 | it('should set the player color using the "color" prop', async () => { 96 | const { playerMock, sdkMock, rerender } = await render({ 97 | video: 169408731, 98 | color: '#0000ff', 99 | }); 100 | expect(sdkMock).toHaveBeenCalled(); 101 | expect(sdkMock.calls[0].arguments[1]).toMatch({ color: '#0000ff' }); 102 | 103 | await rerender({ color: '#ff0000' }); 104 | expect(playerMock.setColor).toHaveBeenCalledWith('#ff0000'); 105 | await rerender({ color: '#00ff00' }); 106 | expect(playerMock.setColor).toHaveBeenCalledWith('#00ff00'); 107 | }); 108 | 109 | it('should set the looping flag using the "loop" prop', async () => { 110 | const { playerMock, sdkMock, rerender } = await render({ 111 | video: 169408731, 112 | loop: false, 113 | }); 114 | expect(sdkMock).toHaveBeenCalled(); 115 | expect(sdkMock.calls[0].arguments[1]).toMatch({ loop: false }); 116 | 117 | await rerender({ loop: true }); 118 | expect(playerMock.setLoop).toHaveBeenCalledWith(true); 119 | await rerender({ loop: false }); 120 | expect(playerMock.setLoop).toHaveBeenCalledWith(false); 121 | }); 122 | 123 | it('should set the iframe width/height using the width/height props', async () => { 124 | const { sdkMock, playerMock, rerender } = await render({ 125 | video: 169408731, 126 | width: 640, 127 | height: 320, 128 | }); 129 | expect(sdkMock.calls[0].arguments[1]).toMatch({ 130 | width: 640, 131 | height: 320, 132 | }); 133 | 134 | await rerender({ 135 | width: '100%', 136 | height: 800, 137 | }); 138 | 139 | expect(playerMock.setWidth).toHaveBeenCalledWith('100%'); 140 | expect(playerMock.setHeight).toHaveBeenCalledWith(800); 141 | }); 142 | 143 | it('should set the playback rate using the "playbackRate" props', async () => { 144 | const { playerMock, rerender } = await render({ 145 | video: 169408731, 146 | playbackRate: 0.5, 147 | }); 148 | 149 | expect(playerMock.setPlaybackRate).toHaveBeenCalledWith(0.5); 150 | 151 | await rerender({ playbackRate: 2 }); 152 | 153 | expect(playerMock.setPlaybackRate).toHaveBeenCalledWith(2); 154 | }); 155 | 156 | it('should destroy player when unmounting', async () => { 157 | const { playerMock, unmount } = await render({ 158 | video: 169408731, 159 | width: 640, 160 | height: 320, 161 | }); 162 | 163 | unmount(); 164 | 165 | expect(playerMock.destroy).toHaveBeenCalled(); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /test/util/createVimeo.js: -------------------------------------------------------------------------------- 1 | import { createSpy } from 'expect'; 2 | import proxyquire from 'proxyquire'; 3 | 4 | export default function createVimeo({ shouldFail = false } = {}) { 5 | let isPaused = true; 6 | 7 | const createPromiseSpy = () => createSpy().andCall(() => Promise.resolve()); 8 | 9 | const playerMock = { 10 | on: createSpy(), 11 | ready() { 12 | return shouldFail 13 | ? Promise.reject(new Error('artificial failure')) 14 | : Promise.resolve(); 15 | }, 16 | setVolume: createPromiseSpy(), 17 | setPlaybackRate: createPromiseSpy(), 18 | setCurrentTime: createPromiseSpy(), 19 | setAutopause: createPromiseSpy(), 20 | setColor: createPromiseSpy(), 21 | setLoop: createPromiseSpy(), 22 | loadVideo: createPromiseSpy(), 23 | playing: createPromiseSpy(), 24 | unload: createPromiseSpy(), 25 | play: createSpy().andCall(() => { 26 | isPaused = false; 27 | }), 28 | pause: createSpy().andCall(() => { 29 | isPaused = true; 30 | }), 31 | getPaused() { 32 | return Promise.resolve(isPaused); 33 | }, 34 | destroy: createSpy(), 35 | setWidth: createSpy(), 36 | setHeight: createSpy(), 37 | element: { 38 | set width(value) { 39 | playerMock.setWidth(value); 40 | }, 41 | set height(value) { 42 | playerMock.setHeight(value); 43 | }, 44 | }, 45 | }; 46 | 47 | const sdkMock = createSpy().andCall((container, options) => { 48 | isPaused = !options.autoplay; 49 | return playerMock; 50 | }); 51 | 52 | const Vimeo = proxyquire.noCallThru().load('../../src/index.js', { 53 | '@vimeo/player': function Player(...args) { 54 | return sdkMock(...args); 55 | }, 56 | }).default; 57 | 58 | return { Vimeo, sdkMock, playerMock }; 59 | } 60 | -------------------------------------------------------------------------------- /test/util/render.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Taken from react-youtube's tests at 3 | * https://github.com/troybetz/react-youtube 4 | */ 5 | 6 | import React from 'react'; 7 | import ReactDOM from 'react-dom'; 8 | import { act } from 'react-dom/test-utils'; 9 | // Doing this after React is loaded makes React do a bit less DOM work 10 | import 'min-react-env/install'; 11 | import env from 'min-react-env'; 12 | import createVimeo from './createVimeo'; 13 | 14 | const reactMajor = parseInt((ReactDOM.version || '16').split('.')[0], 10); 15 | 16 | function noAct(fn) { 17 | return fn(); 18 | } 19 | 20 | async function render(initialProps) { 21 | const { Vimeo, sdkMock, playerMock } = createVimeo({ 22 | shouldFail: initialProps.shouldFail, 23 | }); 24 | 25 | let component; 26 | // Emulate changes to component.props using a container component's state 27 | class Container extends React.Component { 28 | constructor(ytProps) { 29 | super(ytProps); 30 | 31 | this.state = { props: ytProps }; 32 | } 33 | 34 | render() { 35 | const { props } = this.state; 36 | 37 | return ( 38 | { component = vimeo; }} 40 | {...props} 41 | /> 42 | ); 43 | } 44 | } 45 | 46 | const div = env.document.createElement('div'); 47 | let root; 48 | if (reactMajor >= 18) { 49 | const { createRoot } = await import('react-dom/client'); 50 | root = createRoot(div); 51 | } else { 52 | root = { 53 | render(element) { 54 | // eslint-disable-next-line react/no-deprecated 55 | ReactDOM.render(element, div); 56 | }, 57 | unmount() { 58 | // eslint-disable-next-line react/no-deprecated 59 | ReactDOM.unmountComponentAtNode(div); 60 | }, 61 | }; 62 | } 63 | const container = await new Promise((resolve) => { 64 | root.render(); 65 | }); 66 | 67 | function rerender(newProps) { 68 | return (act || noAct)(async () => { 69 | container.setState({ props: newProps }); 70 | }); 71 | } 72 | 73 | function unmount() { 74 | root.unmount(); 75 | } 76 | 77 | return { 78 | sdkMock, 79 | playerMock, 80 | component, 81 | rerender, 82 | unmount, 83 | }; 84 | } 85 | 86 | export default render; 87 | --------------------------------------------------------------------------------