├── .babelrc ├── .gitignore ├── README.md ├── dist └── index.js ├── index.js ├── package.json ├── src ├── css │ ├── playerComponent.css │ └── videojs-resolution-switcher.css ├── index.js └── js │ ├── videojs-contrib-hls.js │ └── videojs-resolution-switcher.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env"], 3 | "plugins": ["transform-object-rest-spread","transform-react-jsx"] 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /package-lock.json 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VIDEOJS FRO REACT 2 | 3 | 该项目是一个react组件,组件封装了video.js。并集成了部分拓展功能,如:多码流切换,对HLS流的支持。 4 | 5 | ## 使用方法 6 | 7 | 在项目中使用npm或者yarn安装依赖: 8 | 9 | `npm install --save videojs-fro-react` 或者 `yarn add videojf-fro-react` 10 | 11 | 该组件是针对video.js的封装,支持video.js的所有设置。具体设置请参考[ video.js文档](http://docs.videojs.com/)及[video.js github](https://github.com/videojs/video.js)。 12 | 以下仅仅展现部分设置,以及该组件所添加的API。 13 | 14 | 示例代码: 15 | 16 | ``` 17 | 18 | import React, { Component } from 'react'; 19 | import VideoJsForReact from 'videojs-for-react'; 20 | 21 | class App extends Component { 22 | constructor() { 23 | super() 24 | this.state = { 25 | videoJsOptions: { 26 | preload: 'auto', // 预加载 27 | bigPlayButton: {}, // 大按钮 28 | autoplay: true, // 自动播放 29 | controls: true, // 是否开启控制栏 30 | width: 800, // 播放器宽度 31 | height: 600, // 播放器高度 32 | playbackRates: [1, 1.5, 2], // 播放倍速 33 | sources: [ // 视频源 34 | { 35 | src: 'http://yunxianchang.live.ujne7.com/vod-system-bj/44_176_20170224113626af3a75cd-3508-4bc3-b51f-366fca3c7e39.m3u8', 36 | type: 'application/x-mpegURL', 37 | label: 'HLS1', 38 | withCredentials: false, 39 | res: 960 40 | }, { 41 | src: 'http://192.168.199.197:5000/nodeJS%E8%A7%86%E9%A2%914.mp4', 42 | type: 'video/mp4', 43 | label: 'MP4', 44 | res: 1080 45 | } 46 | ] 47 | } 48 | } 49 | } 50 | 51 | render() { 52 | return ( 53 |
54 | console.log(player)} 56 | onReady={(player) => console.log('准备完毕', player)} 57 | {...this.state.videoJsOptions} 58 | > 59 | 60 |
61 | ) 62 | } 63 | } 64 | 65 | export default App; 66 | 67 | ``` 68 | 69 | 与video.js不同的是,videojs-fro-react对接收的资源做了一些拓展: 70 | 71 | ``` 72 | 73 | // 字段sources为array类型,当sources.length为1时,代表只播放一路码流,不开启视频源切换。 74 | // 当字段sources.length>1时,播放器将会开启多码流切换功能。 75 | sources: [ 76 | { 77 | src: 'http://yunxianchang.live.ujne7.com/vod-system-bj/44_176_20170224113626af3a75cd-3508-4bc3-b51f-366fca3c7e39.m3u8', 78 | type: 'application/x-mpegURL', // MIME类型 79 | label: 'HLS1', // 代表该路码流名称 80 | res: 960 // 清晰度 81 | }, { 82 | src: 'http://192.168.199.197:5000/nodeJS%E8%A7%86%E9%A2%914.mp4', 83 | type: 'video/mp4', 84 | label: 'MP4', 85 | res: 1080 86 | } 87 | ] 88 | ``` 89 | 90 | ## API 91 | 92 | | API | 用途| 93 | | - | -: | 94 | | sourceChanged| 码流切换成功的回调 | 95 | | onReady| 播放器准备就绪的回调 | 96 | 97 | 现阶段该组件并没有实现更多功能的封装,接下来将会考虑支持rtmp、http-flv等。希望能够提出宝贵的建议,大家一起完善组件的功能。 -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/index'); 2 | // "build": "cross-env NODE_ENV=production webpack --config webpack.config.js --mode=production" 3 | // "build": "webpack --config webpack.config.js" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videojs-for-react", 3 | "version": "0.0.3", 4 | "description": "videojs-for-react,This is react component", 5 | "main": "index.js", 6 | "files": [ 7 | "dist", 8 | "index.js", 9 | "package.json" 10 | ], 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1", 13 | "build": "cross-env NODE_ENV=production webpack --config webpack.config.js --mode=production" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/sohoorc/videojs-for-react.git" 18 | }, 19 | "keywords": [ 20 | "videojs", 21 | "videojsForReact", 22 | "videojs-react", 23 | "videojs-fro-react", 24 | "videojsReact" 25 | ], 26 | "author": "WeiTian Gan", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/sohoorc/videojs-for-react/issues" 30 | }, 31 | "homepage": "https://github.com/sohoorc/videojs-for-react#readme", 32 | "dependencies": { 33 | "prop-types": "^15.6.1", 34 | "react": "^16.3.2", 35 | "react-dom": "^16.3.2", 36 | "video.js": "^6.9.0", 37 | "videojs-contrib-hls": "^5.14.1" 38 | }, 39 | "devDependencies": { 40 | "babel-core": "^6.26.3", 41 | "babel-loader": "^7.1.4", 42 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 43 | "babel-plugin-transform-react-jsx": "^6.24.1", 44 | "babel-preset-env": "^1.7.0", 45 | "cross-env": "^5.1.5", 46 | "css-loader": "^0.28.11", 47 | "extract-text-webpack-plugin": "^3.0.2", 48 | "prop-types": "^15.6.1", 49 | "style-loader": "^0.21.0", 50 | "url-loader": "^1.0.1", 51 | "webpack": "^3.x", 52 | "webpack-cli": "^2.1.3" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/css/playerComponent.css: -------------------------------------------------------------------------------- 1 | @import './videojs-resolution-switcher.css'; 2 | 3 | .vjs-menu-button-popup div.vjs-menu{ 4 | left: 0; 5 | } -------------------------------------------------------------------------------- /src/css/videojs-resolution-switcher.css: -------------------------------------------------------------------------------- 1 | .vjs-resolution-button { 2 | color: #ccc; 3 | font-family: VideoJS; 4 | } 5 | 6 | .vjs-resolution-button .vjs-resolution-button-staticlabel:before { 7 | content: '\f110'; 8 | font-size: 1.8em; 9 | line-height: 1.67; 10 | } 11 | 12 | .vjs-resolution-button .vjs-resolution-button-label { 13 | font-size: 1em; 14 | line-height: 3em; 15 | position: absolute; 16 | top: 0; 17 | left: 0; 18 | width: 100%; 19 | height: 100%; 20 | text-align: center; 21 | box-sizing: inherit; 22 | font-family: Arial, Helvetica, sans-serif; 23 | } 24 | 25 | .vjs-resolution-button ul.vjs-menu-content { 26 | width: 4em !important; 27 | } 28 | 29 | .vjs-resolution-button .vjs-menu { 30 | left: 0; 31 | } 32 | 33 | .vjs-resolution-button .vjs-menu li { 34 | text-transform: none; 35 | font-size: 1em; 36 | font-family: Arial, Helvetica, sans-serif; 37 | } 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import videojs from 'video.js'; 4 | import { HlsSourceHandler } from 'videojs-contrib-hls'; 5 | import './js/videojs-resolution-switcher'; 6 | import 'video.js/dist/video-js.min.css'; 7 | import './css/playerComponent.css'; 8 | // import Swf from 'videojs-swf/dist/video-js.swf'; 9 | // import 'videojs-flash'; 10 | // import 'videojs-resolution-switcher'; 11 | 12 | export default class VideoJsForReact extends Component { 13 | constructor(props) { 14 | super(props); 15 | this.options = {} 16 | this.sources = []; 17 | // 判断是否是多码流,来修改播放器的播放方式 18 | if (props.sources.length > 1) { 19 | // 若存在多个流地址,则开启videoJsResolutionSwitcher 20 | this.sources = props.sources 21 | } else { 22 | this.options.source = props.sources 23 | } 24 | } 25 | 26 | componentDidMount() { 27 | videojs.getTech('html5').registerSourceHandler(HlsSourceHandler('html5'), 0); 28 | let _this = this 29 | this.player = videojs(this.videoContainer, { 30 | ...this.props, 31 | ...this.options, 32 | plugins: { 33 | videoJsResolutionSwitcher: { 34 | default: 'low', // Default resolution [{Number}, 'low', 'high'], 35 | dynamicLabel: true // Display dynamic labels or gear symbol 36 | } 37 | } 38 | // flash: { 39 | // swf: Swf, 40 | // }, 41 | }, function () { 42 | let player = this 43 | let props = _this.props 44 | let sources = _this.sources 45 | 46 | // 播放器加载成功的回调 47 | if (!!props.onReady) { 48 | props.onReady(props) 49 | } 50 | 51 | // 修正使用多码流播放时自动播放失效的BUG 52 | // if (!!props.autoplay) { 53 | // setInterval(() => { 54 | // player.play(); 55 | // }, 100) 56 | // } 57 | 58 | // 判断是否是多码流,单码流调用video.js播放器播放,多码流使用插件播放 59 | if (sources.length > 1) { 60 | player.updateSrc([...sources]) 61 | player.on('resolutionchange', function () { 62 | // 切换成功的回调 63 | if (!!props.sourceChanged) { 64 | props.sourceChanged(player) 65 | } 66 | }) 67 | } 68 | }) 69 | } 70 | 71 | componentWillUnmount() { 72 | // 销毁播放器 73 | if (this.player) { 74 | this.player.dispose() 75 | } 76 | } 77 | 78 | render() { 79 | return ( 80 |
81 | 85 |
86 | ) 87 | } 88 | } 89 | 90 | VideoJsForReact.propTypes = { 91 | sources: PropTypes.array.isRequired, // 视频流地址列表 数组,必填 92 | sourceChanged: PropTypes.func, // 多码流时,对码流切换的回调 93 | onReady: PropTypes.func // 播放器加载成功时的回调 94 | } 95 | 96 | -------------------------------------------------------------------------------- /src/js/videojs-resolution-switcher.js: -------------------------------------------------------------------------------- 1 | /*! videojs-resolution-switcher - 2015-7-26 2 | * Copyright (c) 2016 Kasper Moskwiak 3 | * Modified by Pierre Kraft 4 | * Licensed under the Apache-2.0 license. */ 5 | 6 | (function() { 7 | /* jshint eqnull: true*/ 8 | /* global require */ 9 | 'use strict'; 10 | var videojs = null; 11 | if(typeof window.videojs === 'undefined' && typeof require === 'function') { 12 | videojs = require('video.js'); 13 | } else { 14 | videojs = window.videojs; 15 | } 16 | 17 | (function(window, videojs) { 18 | 19 | 20 | var defaults = {}, 21 | videoJsResolutionSwitcher, 22 | currentResolution = {}, // stores current resolution 23 | menuItemsHolder = {}; // stores menuItems 24 | 25 | function setSourcesSanitized(player, sources, label, customSourcePicker) { 26 | currentResolution = { 27 | label: label, 28 | sources: sources 29 | }; 30 | if(typeof customSourcePicker === 'function'){ 31 | return customSourcePicker(player, sources, label); 32 | } 33 | /* DECEiFER: Modified for video.js 6.x */ 34 | player.src(sources.map(function(src) { 35 | return {src: src.src, type: src.type, res: src.res}; 36 | })); 37 | return player; 38 | } 39 | 40 | /* 41 | * Resolution menu item 42 | */ 43 | var MenuItem = videojs.getComponent('MenuItem'); 44 | var ResolutionMenuItem = videojs.extend(MenuItem, { 45 | constructor: function(player, options, onClickListener, label){ 46 | this.onClickListener = onClickListener; 47 | this.label = label; 48 | // Sets this.player_, this.options_ and initializes the component 49 | MenuItem.call(this, player, options); 50 | this.src = options.src; 51 | 52 | this.on('click', this.onClick); 53 | this.on('touchstart', this.onClick); 54 | 55 | if (options.initialySelected) { 56 | this.showAsLabel(); 57 | this.selected(true); 58 | 59 | this.addClass('vjs-selected'); 60 | } 61 | }, 62 | showAsLabel: function() { 63 | // Change menu button label to the label of this item if the menu button label is provided 64 | if(this.label) { 65 | this.label.innerHTML = this.options_.label; 66 | } 67 | }, 68 | onClick: function(customSourcePicker){ 69 | this.onClickListener(this); 70 | // Remember player state 71 | var currentTime = this.player_.currentTime(); 72 | var isPaused = this.player_.paused(); 73 | this.showAsLabel(); 74 | 75 | // add .current class 76 | this.addClass('vjs-selected'); 77 | 78 | // Hide bigPlayButton 79 | if(!isPaused){ 80 | this.player_.bigPlayButton.hide(); 81 | } 82 | if(typeof customSourcePicker !== 'function' && 83 | typeof this.options_.customSourcePicker === 'function'){ 84 | customSourcePicker = this.options_.customSourcePicker; 85 | } 86 | // Change player source and wait for loadeddata event, then play video 87 | // loadedmetadata doesn't work right now for flash. 88 | // Probably because of https://github.com/videojs/video-js-swf/issues/124 89 | // If player preload is 'none' and then loadeddata not fired. So, we need timeupdate event for seek handle (timeupdate doesn't work properly with flash) 90 | var handleSeekEvent = 'loadeddata'; 91 | if(this.player_.techName_ !== 'Youtube' && this.player_.preload() === 'none' && this.player_.techName_ !== 'Flash') { 92 | handleSeekEvent = 'timeupdate'; 93 | } 94 | setSourcesSanitized(this.player_, this.src, this.options_.label, customSourcePicker).one(handleSeekEvent, function() { 95 | this.player_.currentTime(currentTime); 96 | this.player_.handleTechSeeked_(); 97 | if(!isPaused){ 98 | // Start playing and hide loadingSpinner (flash issue ?) 99 | /* DECEiFER: Modified for video.js 6.x */ 100 | this.player_.play(); 101 | this.player_.handleTechSeeked_(); 102 | } 103 | this.player_.trigger('resolutionchange'); 104 | }); 105 | } 106 | }); 107 | 108 | 109 | /* 110 | * Resolution menu button 111 | */ 112 | var MenuButton = videojs.getComponent('MenuButton'); 113 | var ResolutionMenuButton = videojs.extend(MenuButton, { 114 | constructor: function(player, options, settings, label){ 115 | this.sources = options.sources; 116 | this.label = label; 117 | this.label.innerHTML = options.initialySelectedLabel; 118 | // Sets this.player_, this.options_ and initializes the component 119 | MenuButton.call(this, player, options, settings); 120 | this.controlText('Quality'); 121 | 122 | if(settings.dynamicLabel){ 123 | this.el().appendChild(label); 124 | }else{ 125 | var staticLabel = document.createElement('span'); 126 | /* DECEiFER: Modified for video.js 6.x */ 127 | videojs.dom.addClass(staticLabel, 'vjs-resolution-button-staticlabel'); 128 | this.el().appendChild(staticLabel); 129 | } 130 | }, 131 | createItems: function(){ 132 | var menuItems = []; 133 | var labels = (this.sources && this.sources.label) || {}; 134 | var onClickUnselectOthers = function(clickedItem) { 135 | menuItems.map(function(item) { 136 | item.selected(item === clickedItem); 137 | item.removeClass('vjs-selected'); 138 | }); 139 | }; 140 | 141 | for (var key in labels) { 142 | if (labels.hasOwnProperty(key)) { 143 | menuItems.push(new ResolutionMenuItem( 144 | this.player_, 145 | { 146 | label: key, 147 | src: labels[key], 148 | initialySelected: key === this.options_.initialySelectedLabel, 149 | customSourcePicker: this.options_.customSourcePicker 150 | }, 151 | onClickUnselectOthers, 152 | this.label)); 153 | // Store menu item for API calls 154 | menuItemsHolder[key] = menuItems[menuItems.length - 1]; 155 | } 156 | } 157 | return menuItems; 158 | } 159 | }); 160 | 161 | /** 162 | * Initialize the plugin. 163 | * @param {object} [options] configuration for the plugin 164 | */ 165 | videoJsResolutionSwitcher = function(options) { 166 | var settings = videojs.mergeOptions(defaults, options), 167 | player = this, 168 | label = document.createElement('span'), 169 | groupedSrc = {}; 170 | /* DECEiFER: Modified for video.js 6.x */ 171 | videojs.dom.addClass(label, 'vjs-resolution-button-label'); 172 | 173 | /** 174 | * Updates player sources or returns current source URL 175 | * @param {Array} [src] array of sources [{src: '', type: '', label: '', res: ''}] 176 | * @returns {Object|String|Array} videojs player object if used as setter or current source URL, object, or array of sources 177 | */ 178 | player.updateSrc = function(src){ 179 | //Return current src if src is not given 180 | if(!src){ return player.src(); } 181 | // Dispose old resolution menu button before adding new sources 182 | if(player.controlBar.resolutionSwitcher){ 183 | player.controlBar.resolutionSwitcher.dispose(); 184 | delete player.controlBar.resolutionSwitcher; 185 | } 186 | //Sort sources 187 | src = src.sort(compareResolutions); 188 | groupedSrc = bucketSources(src); 189 | var choosen = chooseSrc(groupedSrc, src); 190 | var menuButton = new ResolutionMenuButton(player, { sources: groupedSrc, initialySelectedLabel: choosen.label , initialySelectedRes: choosen.res , customSourcePicker: settings.customSourcePicker}, settings, label); 191 | /* DECEiFER: Modified for video.js 6.x */ 192 | videojs.dom.addClass(menuButton.el(), 'vjs-resolution-button'); 193 | player.controlBar.resolutionSwitcher = player.controlBar.el_.insertBefore(menuButton.el_, player.controlBar.getChild('fullscreenToggle').el_); 194 | player.controlBar.resolutionSwitcher.dispose = function(){ 195 | this.parentNode.removeChild(this); 196 | }; 197 | return setSourcesSanitized(player, choosen.sources, choosen.label); 198 | }; 199 | 200 | /** 201 | * Returns current resolution or sets one when label is specified 202 | * @param {String} [label] label name 203 | * @param {Function} [customSourcePicker] custom function to choose source. Takes 3 arguments: player, sources, label. Must return player object. 204 | * @returns {Object} current resolution object {label: '', sources: []} if used as getter or player object if used as setter 205 | */ 206 | player.currentResolution = function(label, customSourcePicker){ 207 | if(label == null) { return currentResolution; } 208 | if(menuItemsHolder[label] != null){ 209 | menuItemsHolder[label].onClick(customSourcePicker); 210 | } 211 | return player; 212 | }; 213 | 214 | /** 215 | * Returns grouped sources by label, resolution and type 216 | * @returns {Object} grouped sources: { label: { key: [] }, res: { key: [] }, type: { key: [] } } 217 | */ 218 | player.getGroupedSrc = function(){ 219 | return groupedSrc; 220 | }; 221 | 222 | /** 223 | * Method used for sorting list of sources 224 | * @param {Object} a - source object with res property 225 | * @param {Object} b - source object with res property 226 | * @returns {Number} result of comparation 227 | */ 228 | function compareResolutions(a, b){ 229 | if(!a.res || !b.res){ return 0; } 230 | return (+b.res)-(+a.res); 231 | } 232 | 233 | /** 234 | * Group sources by label, resolution and type 235 | * @param {Array} src Array of sources 236 | * @returns {Object} grouped sources: { label: { key: [] }, res: { key: [] }, type: { key: [] } } 237 | */ 238 | function bucketSources(src){ 239 | var resolutions = { 240 | label: {}, 241 | res: {}, 242 | type: {} 243 | }; 244 | src.map(function(source) { 245 | initResolutionKey(resolutions, 'label', source); 246 | initResolutionKey(resolutions, 'res', source); 247 | initResolutionKey(resolutions, 'type', source); 248 | 249 | appendSourceToKey(resolutions, 'label', source); 250 | appendSourceToKey(resolutions, 'res', source); 251 | appendSourceToKey(resolutions, 'type', source); 252 | }); 253 | return resolutions; 254 | } 255 | 256 | function initResolutionKey(resolutions, key, source) { 257 | if(resolutions[key][source[key]] == null) { 258 | resolutions[key][source[key]] = []; 259 | } 260 | } 261 | 262 | function appendSourceToKey(resolutions, key, source) { 263 | resolutions[key][source[key]].push(source); 264 | } 265 | 266 | /** 267 | * Choose src if option.default is specified 268 | * @param {Object} groupedSrc {res: { key: [] }} 269 | * @param {Array} src Array of sources sorted by resolution used to find high and low res 270 | * @returns {Object} {res: string, sources: []} 271 | */ 272 | function chooseSrc(groupedSrc, src){ 273 | var selectedRes = settings['default']; // use array access as default is a reserved keyword 274 | var selectedLabel = ''; 275 | if (selectedRes === 'high') { 276 | selectedRes = src[0].res; 277 | selectedLabel = src[0].label; 278 | } else if (selectedRes === 'low' || selectedRes == null || !groupedSrc.res[selectedRes]) { 279 | // Select low-res if default is low or not set 280 | selectedRes = src[src.length - 1].res; 281 | selectedLabel = src[src.length -1].label; 282 | } else if (groupedSrc.res[selectedRes]) { 283 | selectedLabel = groupedSrc.res[selectedRes][0].label; 284 | } 285 | 286 | return {res: selectedRes, label: selectedLabel, sources: groupedSrc.res[selectedRes]}; 287 | } 288 | 289 | function initResolutionForYt(player){ 290 | // Init resolution 291 | player.tech_.ytPlayer.setPlaybackQuality('default'); 292 | 293 | // Capture events 294 | player.tech_.ytPlayer.addEventListener('onPlaybackQualityChange', function(){ 295 | player.trigger('resolutionchange'); 296 | }); 297 | 298 | // We must wait for play event 299 | player.one('play', function(){ 300 | var qualities = player.tech_.ytPlayer.getAvailableQualityLevels(); 301 | // Map youtube qualities names 302 | var _yts = { 303 | highres: {res: 1080, label: '1080', yt: 'highres'}, 304 | hd1080: {res: 1080, label: '1080', yt: 'hd1080'}, 305 | hd720: {res: 720, label: '720', yt: 'hd720'}, 306 | large: {res: 480, label: '480', yt: 'large'}, 307 | medium: {res: 360, label: '360', yt: 'medium'}, 308 | small: {res: 240, label: '240', yt: 'small'}, 309 | tiny: {res: 144, label: '144', yt: 'tiny'}, 310 | auto: {res: 0, label: 'auto', yt: 'default'} 311 | }; 312 | 313 | var _sources = []; 314 | 315 | qualities.map(function(q){ 316 | _sources.push({ 317 | src: player.src().src, 318 | type: player.src().type, 319 | label: _yts[q].label, 320 | res: _yts[q].res, 321 | _yt: _yts[q].yt 322 | }); 323 | }); 324 | 325 | groupedSrc = bucketSources(_sources); 326 | 327 | // Overwrite defualt sourcePicer function 328 | var _customSourcePicker = function(_player, _sources, _label){ 329 | player.tech_.ytPlayer.setPlaybackQuality(_sources[0]._yt); 330 | return player; 331 | }; 332 | 333 | var choosen = {label: 'auto', res: 0, sources: groupedSrc.label.auto}; 334 | var menuButton = new ResolutionMenuButton(player, { 335 | sources: groupedSrc, 336 | initialySelectedLabel: choosen.label, 337 | initialySelectedRes: choosen.res, 338 | customSourcePicker: _customSourcePicker 339 | }, settings, label); 340 | 341 | menuButton.el().classList.add('vjs-resolution-button'); 342 | player.controlBar.resolutionSwitcher = player.controlBar.addChild(menuButton); 343 | }); 344 | } 345 | 346 | player.ready(function(){ 347 | if(player.options_.sources.length > 1){ 348 | // tech: Html5 and Flash 349 | // Create resolution switcher for videos form tag inside