├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .prettierrc ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── src ├── components │ └── Player.js ├── index.js └── util │ └── getPosterSrc.js └── test-app ├── .eslintignore ├── config-overrides.js ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── App.css ├── App.js ├── App.test.js ├── Video.js ├── index.css ├── index.js └── player ├── components └── Player.js ├── index.js └── util └── getPosterSrc.js /.eslintignore: -------------------------------------------------------------------------------- 1 | /build 2 | /test-app/public 3 | 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": false, 4 | "browser": true 5 | }, 6 | "parser": "babel-eslint", 7 | "extends": [ 8 | "eslint-config-sanity", 9 | "prettier", 10 | "prettier/react" 11 | ], 12 | "rules": { 13 | "react/jsx-uses-vars": [2], 14 | "newline-per-chained-call": 0, 15 | "sort-imports": 0, 16 | "import/no-unresolved": [ 17 | "error", 18 | { 19 | "ignore": [ 20 | ".*:.*" 21 | ] 22 | } 23 | ], 24 | "prettier/prettier": "error", 25 | "import/no-extraneous-dependencies": "off", 26 | "import/unambiguous": "off" 27 | }, 28 | "globals": { 29 | "__DEV__": true 30 | }, 31 | "settings": { 32 | "import/ignore": [ 33 | "\\.css$", 34 | ".*node_modules.*", 35 | ".*:.*" 36 | ] 37 | }, 38 | "plugins": [ 39 | "import", 40 | "prettier", 41 | "react" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | # Logs 24 | logs 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | *.pid.lock 35 | 36 | # Directory for instrumented libs generated by jscoverage/JSCover 37 | lib-cov 38 | 39 | # Coverage directory used by tools like istanbul 40 | coverage 41 | 42 | # nyc test coverage 43 | .nyc_output 44 | 45 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 46 | .grunt 47 | 48 | # Bower dependency directory (https://bower.io/) 49 | bower_components 50 | 51 | # node-waf configuration 52 | .lock-wscript 53 | 54 | # Compiled binary addons (https://nodejs.org/api/addons.html) 55 | build/Release 56 | 57 | # Dependency directories 58 | node_modules/ 59 | jspm_packages/ 60 | 61 | # TypeScript v1 declaration files 62 | typings/ 63 | 64 | # Optional npm cache directory 65 | .npm 66 | 67 | # Optional eslint cache 68 | .eslintcache 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variables file 80 | .env 81 | 82 | # next.js build output 83 | .next 84 | # Logs 85 | logs 86 | *.log 87 | npm-debug.log* 88 | yarn-debug.log* 89 | yarn-error.log* 90 | 91 | # Runtime data 92 | pids 93 | *.pid 94 | *.seed 95 | *.pid.lock 96 | 97 | # Directory for instrumented libs generated by jscoverage/JSCover 98 | lib-cov 99 | 100 | # Coverage directory used by tools like istanbul 101 | coverage 102 | 103 | # nyc test coverage 104 | .nyc_output 105 | 106 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 107 | .grunt 108 | 109 | # Bower dependency directory (https://bower.io/) 110 | bower_components 111 | 112 | # node-waf configuration 113 | .lock-wscript 114 | 115 | # Compiled binary addons (https://nodejs.org/api/addons.html) 116 | build/Release 117 | 118 | # Dependency directories 119 | node_modules/ 120 | jspm_packages/ 121 | 122 | # TypeScript v1 declaration files 123 | typings/ 124 | 125 | # Optional npm cache directory 126 | .npm 127 | 128 | # Optional eslint cache 129 | .eslintcache 130 | 131 | # Optional REPL history 132 | .node_repl_history 133 | 134 | # Output of 'npm pack' 135 | *.tgz 136 | 137 | # Yarn Integrity file 138 | .yarn-integrity 139 | 140 | # dotenv environment variables file 141 | .env 142 | 143 | # next.js build output 144 | .next 145 | 146 | .serverless 147 | .webpack 148 | storybook-static 149 | 150 | sanity/dist 151 | /test-app/node_modules 152 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /src 3 | /node_modules 4 | /test-app 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sanity Mux Video Player 2 | 3 | This library is deprecated in favor of [Mux Player](https://www.mux.com/player), try the [Codesandbox example](https://codesandbox.io/s/github/sanity-io/sanity-plugin-mux-input/tree/main/example). 4 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | module.exports = function(api) { 3 | api.cache(true) 4 | const presets = ['@babel/env', '@babel/preset-react'] 5 | const plugins = ['@babel/plugin-proposal-class-properties'] 6 | return { 7 | presets, 8 | plugins 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-mux-player", 3 | "version": "0.0.27", 4 | "description": "A video player to play videos in the front end uploaded with the Sanity MUX plugin", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "build": "babel src -d build --copy-files", 8 | "prepublishOnly": "npm run build", 9 | "test-app": "babel src -d test-app/src/player --copy-files --watch & cd ./test-app && react-app-rewired start --scripts-version react-scripts-rewired" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/sanity-io/sanity-plugin-mux-input.git" 14 | }, 15 | "keywords": [ 16 | "sanity", 17 | "video", 18 | "mux", 19 | "player" 20 | ], 21 | "publishConfig": { 22 | "access": "public" 23 | }, 24 | "author": "Sanity.io ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/sanity-io/sanity-mux-player/issues" 28 | }, 29 | "homepage": "https://github.com/sanity-io/sanity-mux-player#readme", 30 | "devDependencies": { 31 | "@babel/cli": "^7.0.0", 32 | "@babel/core": "^7.0.0", 33 | "@babel/plugin-proposal-class-properties": "^7.1.0", 34 | "@babel/preset-env": "^7.0.0", 35 | "@babel/preset-react": "^7.0.0", 36 | "babel-eslint": "^10.0.2", 37 | "eslint": "^6.1.0", 38 | "eslint-config-prettier": "^3.0.1", 39 | "eslint-config-sanity": "^4.0.2", 40 | "eslint-plugin-import": "^2.9.0", 41 | "eslint-plugin-prettier": "^2.6.0", 42 | "eslint-plugin-react": "^7.7.0", 43 | "prettier": "^1.14.2", 44 | "react": "16.5.2", 45 | "react-app-rewire-eslint": "^0.2.3", 46 | "react-app-rewired": "^2.1.3", 47 | "react-dom": "16.5.2", 48 | "react-hot-loader": "4.3.8", 49 | "react-scripts-rewired": "^3.1.1" 50 | }, 51 | "dependencies": { 52 | "hls.js": "^0.11.0", 53 | "lodash": "^4.17.20", 54 | "prop-types": "^15.6.0" 55 | }, 56 | "peerDependencies": { 57 | "react": ">=16.3.x" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Player.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable complexity */ 2 | import React, {Component} from 'react' 3 | import PropTypes from 'prop-types' 4 | import Hls from 'hls.js' 5 | 6 | import omit from 'lodash/omit' 7 | import getPosterSrc from '../util/getPosterSrc' 8 | 9 | const propTypes = { 10 | assetDocument: PropTypes.object.isRequired, 11 | autoload: PropTypes.bool, 12 | autoplay: PropTypes.bool, 13 | loop: PropTypes.bool, 14 | showControls: PropTypes.bool, 15 | width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 16 | height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 17 | style: PropTypes.object, 18 | className: PropTypes.string, 19 | poster: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), 20 | onClick: PropTypes.func 21 | } 22 | 23 | const handledPropNames = [ 24 | 'assetDocument', 25 | 'autoload', 26 | 'autoplay', 27 | 'muted', 28 | 'showControls', 29 | 'style', 30 | 'className', 31 | 'poster', 32 | 'onClick', 33 | 'children' 34 | ] 35 | 36 | class SanityMuxPlayer extends Component { 37 | state = { 38 | posterUrl: null, 39 | source: null, 40 | isLoading: true, 41 | error: null 42 | } 43 | 44 | static defaultProps = { 45 | autoload: true, 46 | autoplay: false, 47 | className: '', 48 | height: '', 49 | loop: false, 50 | muted: false, 51 | showControls: true, 52 | style: {width: '100%', height: 'auto'}, 53 | width: '100%', 54 | poster: true 55 | } 56 | 57 | videoContainer = React.createRef() 58 | hls = null 59 | 60 | // eslint-disable-next-line complexity 61 | static getDerivedStateFromProps(nextProps) { 62 | let source = null 63 | let posterUrl = null 64 | let isLoading = true 65 | const {assetDocument, poster} = nextProps 66 | if (assetDocument && assetDocument.status === 'preparing') { 67 | isLoading = 'MUX is processing the video' 68 | } 69 | if (assetDocument && assetDocument.status === 'ready') { 70 | isLoading = false 71 | } 72 | if (assetDocument && assetDocument.playbackId) { 73 | source = `https://stream.mux.com/${assetDocument.playbackId}.m3u8` 74 | // Load video poster only if explictly requested. 75 | if (poster === true) { 76 | posterUrl = getPosterSrc(assetDocument.playbackId, { 77 | time: assetDocument.thumbTime || 1, 78 | fitMode: 'preserve' 79 | }) 80 | } 81 | } 82 | if (assetDocument && typeof assetDocument.status === 'undefined') { 83 | isLoading = false 84 | } 85 | if (typeof poster === 'string') { 86 | posterUrl = poster 87 | } 88 | return {isLoading, source, posterUrl} 89 | } 90 | 91 | componentDidMount() { 92 | this.video = React.createRef() 93 | this.setState(SanityMuxPlayer.getDerivedStateFromProps(this.props)) 94 | } 95 | 96 | componentDidUpdate(prevProps, prevState) { 97 | if (this.state.source !== null && this.video.current && !this.video.current.src) { 98 | if (this.state.error) { 99 | this.setState({error: null}) 100 | } 101 | 102 | this.attachVideo() 103 | } 104 | 105 | if (this.state.source !== null && this.state.source !== prevState.source) { 106 | if (this.state.error || this.state.showControls) { 107 | this.setState({error: null, showControls: false}) 108 | } 109 | 110 | if (this.hls) { 111 | this.hls.destroy() 112 | } 113 | 114 | this.attachVideo() 115 | } 116 | } 117 | 118 | getVideoElement() { 119 | return this.video && this.video.current 120 | } 121 | 122 | attachVideo() { 123 | const {autoload} = this.props 124 | if (Hls.isSupported()) { 125 | this.hls = new Hls({autoStartLoad: autoload}) 126 | this.hls.loadSource(this.state.source) 127 | this.hls.attachMedia(this.video.current) 128 | this.hls.on(Hls.Events.MANIFEST_PARSED, () => { 129 | if (this.videoContainer.current) { 130 | this.videoContainer.current.style.display = 'block' 131 | } 132 | }) 133 | this.hls.on(Hls.Events.ERROR, (event, data) => { 134 | switch (data.type) { 135 | case Hls.ErrorTypes.NETWORK_ERROR: 136 | this.videoContainer.current.style.display = 'none' 137 | this.setState({error: data}) 138 | break 139 | case Hls.ErrorTypes.MEDIA_ERROR: 140 | // Don't output anything visible as these mostly are non-fatal 141 | break 142 | default: 143 | this.videoContainer.current.style.display = 'none' 144 | this.setState({error: data}) 145 | } 146 | console.error(data) // eslint-disable-line no-console 147 | }) 148 | } else if (this.video.current.canPlayType('application/vnd.apple.mpegurl')) { 149 | this.video.current.src = this.state.source 150 | this.video.current.addEventListener('loadedmetadata', () => { 151 | this.videoContainer.current.style.display = 'block' 152 | }) 153 | this.video.current.addEventListener('error', () => { 154 | this.videoContainer.current.style.display = 'none' 155 | this.setState({ 156 | error: { 157 | type: `${this.video.current.error.constructor.name} code ${this.video.current.error.code}` 158 | } 159 | }) 160 | console.error(this.video.current.error) // eslint-disable-line no-console 161 | }) 162 | } 163 | } 164 | 165 | handleVideoClick = event => { 166 | const {autoload} = this.props 167 | if (!autoload) { 168 | this.setState({showControls: true}) 169 | if (this.hls) { 170 | this.hls.startLoad(0) 171 | } 172 | } 173 | if (this.props.onClick) { 174 | this.props.onClick(event) 175 | } 176 | } 177 | 178 | render() { 179 | const {posterUrl, isLoading, error} = this.state 180 | const {assetDocument, autoload, children} = this.props 181 | 182 | if (!assetDocument || !assetDocument.status) { 183 | return null 184 | } 185 | 186 | if (isLoading) { 187 | return ( 188 |
189 |
190 | Waiting for MUX to complete the file... 191 |
192 |
193 | ) 194 | } 195 | 196 | let showControls = autoload || this.state.showControls 197 | if (this.props.showControls === false) { 198 | showControls = false 199 | } 200 | 201 | const videoProps = omit(this.props, handledPropNames) 202 | 203 | return ( 204 |
205 |
206 |
217 | {error && ( 218 |
219 | There was an error loading this video ({error.type}). 220 |
221 | )} 222 | {children} 223 |
224 | ) 225 | } 226 | } 227 | 228 | SanityMuxPlayer.propTypes = propTypes 229 | 230 | export default SanityMuxPlayer 231 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Player from './components/Player' 2 | export default Player 3 | -------------------------------------------------------------------------------- /src/util/getPosterSrc.js: -------------------------------------------------------------------------------- 1 | export default function getPosterSrc(playbackId, options = {}) { 2 | const width = options.width || 640 3 | const height = options.height || '' 4 | const time = options.time || 1 5 | const fitMode = typeof options.fitMode === 'undefined' ? 'smartcrop' : options.fitMode 6 | let url = `https://image.mux.com/${playbackId}/thumbnail.png?width=${width}&fit_mode=${fitMode}&time=${time}` 7 | if (options.height) { 8 | url += `&height=${height}` 9 | } 10 | return url 11 | } 12 | -------------------------------------------------------------------------------- /test-app/.eslintignore: -------------------------------------------------------------------------------- 1 | src/player/ 2 | -------------------------------------------------------------------------------- /test-app/config-overrides.js: -------------------------------------------------------------------------------- 1 | /* config-overrides.js */ 2 | 3 | const rewireEslint = require('react-app-rewire-eslint') 4 | 5 | module.exports = function override(config, env) { 6 | config = rewireEslint(config, env) 7 | return config 8 | } 9 | -------------------------------------------------------------------------------- /test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sanity-mux-player-test-app", 3 | "version": "0.0.0", 4 | "description": "Just for testing the sanity-mux-player locally", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "start": "react-app-rewired start" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/sanity-io/sanity-plugin-mux-input.git" 12 | }, 13 | "keywords": [ 14 | "sanity", 15 | "video", 16 | "mux", 17 | "player" 18 | ], 19 | "author": "Sanity.io ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/sanity-io/sanity-mux-player/issues" 23 | }, 24 | "homepage": "https://github.com/sanity-io/sanity-mux-player#readme", 25 | "browserslist": [ 26 | ">0.2%", 27 | "not dead", 28 | "not ie <= 11", 29 | "not op_mini all" 30 | ], 31 | "dependencies": { 32 | "hls.js": "^0.11.0", 33 | "lodash": "^4.17.11", 34 | "prop-types": "^15.6.0", 35 | "react": "16.5.2", 36 | "react-app-rewire-eslint": "^0.2.3", 37 | "react-app-rewired": "1.6.2", 38 | "react-dom": "16.5.2", 39 | "react-hot-loader": "4.3.8", 40 | "react-scripts": "^2.1.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/sanity-mux-player/63a7d3cd05255099bccf39c9e8b78b4378032321/test-app/public/favicon.ico -------------------------------------------------------------------------------- /test-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | sanity-mux-player test-app 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /test-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /test-app/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-header { 6 | background-color: #282c34; 7 | min-height: 100vh; 8 | display: flex; 9 | flex-direction: column; 10 | align-items: center; 11 | justify-content: center; 12 | font-size: calc(10px + 2vmin); 13 | color: white; 14 | } 15 | 16 | .videoContainer { 17 | max-width: 640px; 18 | } 19 | -------------------------------------------------------------------------------- /test-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import Video from './Video' 3 | import './App.css' 4 | 5 | class App extends Component { 6 | // eslint-disable-next-line class-methods-use-this 7 | render() { 8 | return ( 9 |
10 |
11 |
12 |
14 |
15 |
16 | ) 17 | } 18 | } 19 | 20 | export default App 21 | -------------------------------------------------------------------------------- /test-app/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div') 7 | ReactDOM.render(, div) 8 | ReactDOM.unmountComponentAtNode(div) 9 | }) 10 | -------------------------------------------------------------------------------- /test-app/src/Video.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | 3 | import React, {Component} from 'react' 4 | import SanityMuxPlayer from './player' 5 | 6 | // Just for test. You would get the asset document from the client in real life. 7 | const assetDocument = { 8 | _id: 'a6e70715-ec6f-4fe1-8b5b-7d6aeb74759c', 9 | _rev: '0xLJdqcI4pgly0b1Ixu67q', 10 | _type: 'mux.videoAsset', 11 | data: { 12 | aspect_ratio: '16:9', 13 | created_at: '1543532219', 14 | duration: 170.859, 15 | id: 'KNlhusaO201gm3vrD00LLHaRO02DW9RBPjF', 16 | max_stored_frame_rate: 25, 17 | max_stored_resolution: 'HD', 18 | mp4_support: 'none', 19 | passthrough: 'a6e70715-ec6f-4fe1-8b5b-7d6aeb74759c', 20 | playback_ids: [{id: 'oxWh34cgT802eHzHIhPXWoHsZb9htpkZL', policy: 'public'}], 21 | status: 'ready', 22 | tracks: [ 23 | { 24 | duration: 170.84, 25 | id: 'ZYxBhbZy8hnmcNaXWDDeRC302zO01LbLv3', 26 | max_frame_rate: 25, 27 | max_height: 720, 28 | max_width: 1280, 29 | type: 'video' 30 | }, 31 | { 32 | duration: 170.858667, 33 | id: 'ZCavEVHaoxjI02RWMBRBuviQLnTxIu2NGk2M4mDGn9Mo', 34 | max_channel_layout: '5.1', 35 | max_channels: 6, 36 | type: 'audio' 37 | } 38 | ] 39 | }, 40 | filename: 'SampleVideo_1280x720_30mb.mp4', 41 | playbackId: 'sRXwrKHnIO2WJ8GZNRlHY00oxAbI2f2W6', 42 | status: 'ready', 43 | thumbTime: 13.736837 44 | } 45 | 46 | class Video extends Component { 47 | // eslint-disable-next-line class-methods-use-this 48 | render() { 49 | return ( 50 | 58 | ) 59 | } 60 | } 61 | 62 | export default Video 63 | -------------------------------------------------------------------------------- /test-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /test-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import './index.css' 4 | import App from './App' 5 | 6 | ReactDOM.render(, document.getElementById('root')) 7 | -------------------------------------------------------------------------------- /test-app/src/player/components/Player.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = void 0; 7 | 8 | var _react = _interopRequireWildcard(require("react")); 9 | 10 | var _propTypes = _interopRequireDefault(require("prop-types")); 11 | 12 | var _hls = _interopRequireDefault(require("hls.js")); 13 | 14 | var _omit = _interopRequireDefault(require("lodash/omit")); 15 | 16 | var _getPosterSrc = _interopRequireDefault(require("../util/getPosterSrc")); 17 | 18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 19 | 20 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } 21 | 22 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 23 | 24 | function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } 25 | 26 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 27 | 28 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 29 | 30 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 31 | 32 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 33 | 34 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 35 | 36 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 37 | 38 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 39 | 40 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 41 | 42 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 43 | 44 | var propTypes = { 45 | assetDocument: _propTypes.default.object.isRequired, 46 | autoload: _propTypes.default.bool, 47 | autoplay: _propTypes.default.bool, 48 | loop: _propTypes.default.bool, 49 | showControls: _propTypes.default.bool, 50 | width: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number]), 51 | height: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.number]), 52 | style: _propTypes.default.object, 53 | className: _propTypes.default.string, 54 | poster: _propTypes.default.oneOfType([_propTypes.default.string, _propTypes.default.bool]), 55 | onClick: _propTypes.default.func 56 | }; 57 | var handledPropNames = ['assetDocument', 'autoload', 'autoplay', 'muted', 'showControls', 'style', 'className', 'poster', 'onClick', 'children']; 58 | 59 | var SanityMuxPlayer = 60 | /*#__PURE__*/ 61 | function (_Component) { 62 | _inherits(SanityMuxPlayer, _Component); 63 | 64 | function SanityMuxPlayer() { 65 | var _getPrototypeOf2; 66 | 67 | var _this; 68 | 69 | _classCallCheck(this, SanityMuxPlayer); 70 | 71 | for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 72 | args[_key] = arguments[_key]; 73 | } 74 | 75 | _this = _possibleConstructorReturn(this, (_getPrototypeOf2 = _getPrototypeOf(SanityMuxPlayer)).call.apply(_getPrototypeOf2, [this].concat(args))); 76 | 77 | _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "state", { 78 | posterUrl: null, 79 | source: null, 80 | isLoading: true, 81 | error: null 82 | }); 83 | 84 | _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "videoContainer", _react.default.createRef()); 85 | 86 | _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "hls", null); 87 | 88 | _defineProperty(_assertThisInitialized(_assertThisInitialized(_this)), "handleVideoClick", function (event) { 89 | var autoload = _this.props.autoload; 90 | 91 | if (!autoload) { 92 | _this.setState({ 93 | showControls: true 94 | }); 95 | 96 | if (_this.hls) { 97 | _this.hls.startLoad(0); 98 | } 99 | } 100 | 101 | if (_this.props.onClick) { 102 | _this.props.onClick(event); 103 | } 104 | }); 105 | 106 | return _this; 107 | } 108 | 109 | _createClass(SanityMuxPlayer, [{ 110 | key: "componentDidMount", 111 | value: function componentDidMount() { 112 | this.video = _react.default.createRef(); 113 | this.setState(SanityMuxPlayer.getDerivedStateFromProps(this.props)); 114 | } 115 | }, { 116 | key: "componentDidUpdate", 117 | value: function componentDidUpdate(prevProps, prevState) { 118 | if (this.state.source !== null && this.video.current && !this.video.current.src) { 119 | this.setState({ 120 | error: null 121 | }); 122 | this.attachVideo(); 123 | } 124 | 125 | if (this.state.source !== null && this.state.source !== prevState.source) { 126 | this.setState({ 127 | error: null, 128 | showControls: false 129 | }); 130 | 131 | if (this.hls) { 132 | this.hls.destroy(); 133 | } 134 | 135 | this.attachVideo(); 136 | } 137 | } 138 | }, { 139 | key: "getVideoElement", 140 | value: function getVideoElement() { 141 | return this.video && this.video.current; 142 | } 143 | }, { 144 | key: "attachVideo", 145 | value: function attachVideo() { 146 | var _this2 = this; 147 | 148 | var autoload = this.props.autoload; 149 | 150 | if (_hls.default.isSupported()) { 151 | this.hls = new _hls.default({ 152 | autoStartLoad: autoload 153 | }); 154 | this.hls.loadSource(this.state.source); 155 | this.hls.attachMedia(this.video.current); 156 | this.hls.on(_hls.default.Events.MANIFEST_PARSED, function () { 157 | if (_this2.videoContainer.current) { 158 | _this2.videoContainer.current.style.display = 'block'; 159 | } 160 | }); 161 | this.hls.on(_hls.default.Events.ERROR, function (event, data) { 162 | switch (data.type) { 163 | case _hls.default.ErrorTypes.NETWORK_ERROR: 164 | _this2.videoContainer.current.style.display = 'none'; 165 | 166 | _this2.setState({ 167 | error: data 168 | }); 169 | 170 | break; 171 | 172 | case _hls.default.ErrorTypes.MEDIA_ERROR: 173 | // Don't output anything visible as these mostly are non-fatal 174 | break; 175 | 176 | default: 177 | _this2.videoContainer.current.style.display = 'none'; 178 | 179 | _this2.setState({ 180 | error: data 181 | }); 182 | 183 | } 184 | 185 | console.error(data); // eslint-disable-line no-console 186 | }); 187 | } else if (this.video.current.canPlayType('application/vnd.apple.mpegurl')) { 188 | this.video.current.src = this.state.source; 189 | this.video.current.addEventListener('loadedmetadata', function () { 190 | _this2.videoContainer.current.style.display = 'block'; 191 | }); 192 | this.video.current.addEventListener('error', function () { 193 | _this2.videoContainer.current.style.display = 'none'; 194 | 195 | _this2.setState({ 196 | error: { 197 | type: "".concat(_this2.video.current.error.constructor.name, " code ").concat(_this2.video.current.error.code) 198 | } 199 | }); 200 | 201 | console.error(_this2.video.current.error); // eslint-disable-line no-console 202 | }); 203 | } 204 | } 205 | }, { 206 | key: "render", 207 | // eslint-disable-next-line complexity 208 | value: function render() { 209 | var _this$state = this.state, 210 | posterUrl = _this$state.posterUrl, 211 | isLoading = _this$state.isLoading, 212 | error = _this$state.error; 213 | var _this$props = this.props, 214 | assetDocument = _this$props.assetDocument, 215 | autoload = _this$props.autoload, 216 | children = _this$props.children; 217 | 218 | if (!assetDocument || !assetDocument.status) { 219 | return null; 220 | } 221 | 222 | if (isLoading) { 223 | return _react.default.createElement("div", { 224 | className: this.props.className, 225 | style: this.props.style 226 | }, _react.default.createElement("div", { 227 | className: "SanityMuxPlayerInfoContainer" 228 | }, "Waiting for MUX to complete the file...")); 229 | } 230 | 231 | var showControls = autoload || this.state.showControls; 232 | 233 | if (this.props.showControls === false) { 234 | showControls = false; 235 | } 236 | 237 | var videoProps = (0, _omit.default)(this.props, handledPropNames); 238 | return _react.default.createElement("div", { 239 | className: this.props.className, 240 | style: this.props.style 241 | }, _react.default.createElement("div", { 242 | ref: this.videoContainer 243 | }, _react.default.createElement("video", _extends({ 244 | style: { 245 | display: 'block' 246 | } // Needs to be here to avoid 1px gap in the bottom of controls 247 | , 248 | onClick: this.handleVideoClick, 249 | controls: showControls, 250 | muted: this.props.autoplay || this.props.muted // Force mute if autoplay (or it might not even work at all) 251 | , 252 | autoPlay: this.props.autoplay, 253 | ref: this.video, 254 | poster: posterUrl 255 | }, videoProps))), error && _react.default.createElement("div", { 256 | className: "SanityMuxPlayerInfoContainer SanityMuxPlayerError" 257 | }, "There was an error loading this video (", error.type, ")."), children); 258 | } 259 | }], [{ 260 | key: "getDerivedStateFromProps", 261 | // eslint-disable-next-line complexity 262 | value: function getDerivedStateFromProps(nextProps) { 263 | var source = null; 264 | var posterUrl = null; 265 | var isLoading = true; 266 | var assetDocument = nextProps.assetDocument, 267 | poster = nextProps.poster; 268 | 269 | if (assetDocument && assetDocument.status === 'preparing') { 270 | isLoading = 'MUX is processing the video'; 271 | } 272 | 273 | if (assetDocument && assetDocument.status === 'ready') { 274 | isLoading = false; 275 | } 276 | 277 | if (assetDocument && assetDocument.playbackId) { 278 | source = "https://stream.mux.com/".concat(assetDocument.playbackId, ".m3u8"); // Load video poster only if explictly requested. 279 | 280 | if (poster === true) { 281 | posterUrl = (0, _getPosterSrc.default)(assetDocument.playbackId, { 282 | time: assetDocument.thumbTime || 1, 283 | fitMode: 'preserve' 284 | }); 285 | } 286 | } 287 | 288 | if (assetDocument && typeof assetDocument.status === 'undefined') { 289 | isLoading = false; 290 | } 291 | 292 | if (typeof poster === "string") { 293 | posterUrl = poster; 294 | } 295 | 296 | return { 297 | isLoading: isLoading, 298 | source: source, 299 | posterUrl: posterUrl 300 | }; 301 | } 302 | }]); 303 | 304 | return SanityMuxPlayer; 305 | }(_react.Component); 306 | 307 | _defineProperty(SanityMuxPlayer, "defaultProps", { 308 | autoload: true, 309 | autoplay: false, 310 | className: '', 311 | height: '', 312 | loop: false, 313 | muted: false, 314 | showControls: true, 315 | style: { 316 | width: '100%', 317 | height: 'auto' 318 | }, 319 | width: '100%', 320 | poster: true 321 | }); 322 | 323 | SanityMuxPlayer.propTypes = propTypes; 324 | var _default = SanityMuxPlayer; 325 | exports.default = _default; -------------------------------------------------------------------------------- /test-app/src/player/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = void 0; 7 | 8 | var _Player = _interopRequireDefault(require("./components/Player")); 9 | 10 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 11 | 12 | var _default = _Player.default; 13 | exports.default = _default; -------------------------------------------------------------------------------- /test-app/src/player/util/getPosterSrc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = getPosterSrc; 7 | 8 | function getPosterSrc(playbackId) { 9 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 10 | var width = options.width || 640; 11 | var height = options.height || ''; 12 | var time = options.time || 1; 13 | var fitMode = typeof options.fitMode === 'undefined' ? 'smartcrop' : options.fitMode; 14 | var url = "https://image.mux.com/".concat(playbackId, "/thumbnail.png?width=").concat(width, "&fit_mode=").concat(fitMode, "&time=").concat(time); 15 | 16 | if (options.height) { 17 | url += "&height=".concat(height); 18 | } 19 | 20 | return url; 21 | } --------------------------------------------------------------------------------