├── .gitignore ├── README.md ├── babel.config.js ├── mp4_hls_rtmp.png ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html └── src ├── App.vue ├── assets └── logo.png ├── components ├── 01-video.vue ├── 03-video.vue ├── 05-video.vue └── video-player │ ├── custom-theme.css │ ├── index.js │ ├── player.vue │ └── ssr.js └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # videotest 2 | 3 | ### 效果图 4 | ![mp4_hls_rtmp.png](./mp4_hls_rtmp.png) 5 | 6 | ## Project setup 7 | ``` 8 | npm install 9 | ``` 10 | 11 | ### Compiles and hot-reloads for development 12 | ``` 13 | npm run serve 14 | ``` 15 | 16 | ### Compiles and minifies for production 17 | ``` 18 | npm run build 19 | ``` 20 | 21 | ### Run your tests 22 | ``` 23 | npm run test 24 | ``` 25 | 26 | ### Lints and fixes files 27 | ``` 28 | npm run lint 29 | ``` 30 | 31 | ### Customize configuration 32 | See [Configuration Reference](https://cli.vuejs.org/config/). 33 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /mp4_hls_rtmp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeromexiong/vue-video-demo/0b7fb379dd4eb8dd4d3d06f685b729d651b922a6/mp4_hls_rtmp.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videotest", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "core-js": "^2.6.5", 12 | "video.js": "^7.6.0", 13 | "videojs-flash": "^2.2.0", 14 | "videojs-hotkeys": "^0.2.25", 15 | "vue": "^2.6.10" 16 | }, 17 | "devDependencies": { 18 | "@vue/cli-plugin-babel": "^3.10.0", 19 | "@vue/cli-plugin-eslint": "^3.10.0", 20 | "@vue/cli-service": "^3.10.0", 21 | "babel-eslint": "^10.0.1", 22 | "eslint": "^5.16.0", 23 | "eslint-plugin-vue": "^5.0.0", 24 | "vue-template-compiler": "^2.6.10" 25 | }, 26 | "eslintConfig": { 27 | "root": true, 28 | "env": { 29 | "node": true 30 | }, 31 | "extends": [ 32 | "plugin:vue/essential", 33 | "eslint:recommended" 34 | ], 35 | "rules": {}, 36 | "parserOptions": { 37 | "parser": "babel-eslint" 38 | } 39 | }, 40 | "postcss": { 41 | "plugins": { 42 | "autoprefixer": {} 43 | } 44 | }, 45 | "browserslist": [ 46 | "> 1%", 47 | "last 2 versions" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeromexiong/vue-video-demo/0b7fb379dd4eb8dd4d3d06f685b729d651b922a6/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | videotest 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 35 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeromexiong/vue-video-demo/0b7fb379dd4eb8dd4d3d06f685b729d651b922a6/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/01-video.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 127 | 128 | -------------------------------------------------------------------------------- /src/components/03-video.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 65 | -------------------------------------------------------------------------------- /src/components/05-video.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 92 | 93 | -------------------------------------------------------------------------------- /src/components/video-player/custom-theme.css: -------------------------------------------------------------------------------- 1 | .vjs-custom-skin > .video-js { 2 | width: 100%; 3 | font-family: "PingFang SC","Helvetica Neue","Hiragino Sans GB","Segoe UI","Microsoft YaHei","微软雅黑",sans-serif; 4 | } 5 | 6 | .vjs-custom-skin > .video-js .vjs-menu-button-inline.vjs-slider-active,.vjs-custom-skin > .video-js .vjs-menu-button-inline:focus,.vjs-custom-skin > .video-js .vjs-menu-button-inline:hover,.video-js.vjs-no-flex .vjs-menu-button-inline { 7 | width: 10em 8 | } 9 | 10 | .vjs-custom-skin > .video-js .vjs-controls-disabled .vjs-big-play-button { 11 | display: none!important 12 | } 13 | 14 | .vjs-custom-skin > .video-js .vjs-control { 15 | width: 3em 16 | } 17 | 18 | .vjs-custom-skin > .video-js .vjs-control.vjs-live-control{ 19 | width: auto; 20 | padding-left: .5em; 21 | letter-spacing: .1em; 22 | } 23 | 24 | .vjs-custom-skin > .video-js .vjs-menu-button-inline:before { 25 | width: 1.5em 26 | } 27 | 28 | .vjs-menu-button-inline .vjs-menu { 29 | left: 3em 30 | } 31 | 32 | .vjs-paused.vjs-has-started.vjs-custom-skin > .video-js .vjs-big-play-button,.video-js.vjs-ended .vjs-big-play-button,.video-js.vjs-paused .vjs-big-play-button { 33 | display: block 34 | } 35 | 36 | .vjs-custom-skin > .video-js .vjs-load-progress div,.vjs-seeking .vjs-big-play-button,.vjs-waiting .vjs-big-play-button { 37 | display: none!important 38 | } 39 | 40 | .vjs-custom-skin > .video-js .vjs-mouse-display:after,.vjs-custom-skin > .video-js .vjs-play-progress:after { 41 | padding: 0 .4em .3em 42 | } 43 | 44 | .video-js.vjs-ended .vjs-loading-spinner { 45 | display: none; 46 | } 47 | 48 | .video-js.vjs-ended .vjs-big-play-button { 49 | display: block !important; 50 | } 51 | 52 | .video-js.vjs-ended .vjs-big-play-button,.video-js.vjs-paused .vjs-big-play-button,.vjs-paused.vjs-has-started.vjs-custom-skin > .video-js .vjs-big-play-button { 53 | display: block 54 | } 55 | 56 | .vjs-custom-skin > .video-js .vjs-big-play-button { 57 | top: 50%; 58 | left: 50%; 59 | margin-left: -1.5em; 60 | margin-top: -1em 61 | } 62 | 63 | .vjs-custom-skin > .video-js .vjs-big-play-button { 64 | background-color: rgba(0,0,0,0.45); 65 | font-size: 3.5em; 66 | /*border-radius: 50%;*/ 67 | height: 2em !important; 68 | line-height: 2em !important; 69 | margin-top: -1em !important 70 | } 71 | 72 | .video-js:hover .vjs-big-play-button,.vjs-custom-skin > .video-js .vjs-big-play-button:focus,.vjs-custom-skin > .video-js .vjs-big-play-button:active { 73 | background-color: rgba(36,131,213,0.9) 74 | } 75 | 76 | .vjs-custom-skin > .video-js .vjs-loading-spinner { 77 | border-color: rgba(36,131,213,0.8) 78 | } 79 | 80 | .vjs-custom-skin > .video-js .vjs-control-bar2 { 81 | background-color: #000000 82 | } 83 | 84 | .vjs-custom-skin > .video-js .vjs-control-bar { 85 | /*background-color: rgba(0,0,0,0.3) !important;*/ 86 | color: #ffffff; 87 | font-size: 14px 88 | } 89 | 90 | .vjs-custom-skin > .video-js .vjs-play-progress,.vjs-custom-skin > .video-js .vjs-volume-level { 91 | background-color: #2483d5 92 | } 93 | 94 | .vjs-custom-skin > .video-js .vjs-play-progress:before { 95 | top: -0.3em; 96 | } 97 | 98 | .vjs-custom-skin > .video-js .vjs-progress-control:hover .vjs-progress-holder { 99 | font-size: 1.3em; 100 | } 101 | 102 | .vjs-menu-button-popup.vjs-volume-menu-button-vertical .vjs-menu { 103 | left: 0em; 104 | } 105 | 106 | .vjs-custom-skin > .video-js .vjs-menu li { 107 | padding: 0; 108 | line-height: 2em; 109 | font-size: 1.1em; 110 | font-family: "PingFang SC","Helvetica Neue","Hiragino Sans GB","Segoe UI","Microsoft YaHei","微软雅黑",sans-serif; 111 | } 112 | 113 | .vjs-custom-skin > .video-js .vjs-time-tooltip, 114 | .vjs-custom-skin > .video-js .vjs-mouse-display:after, 115 | .vjs-custom-skin > .video-js .vjs-play-progress:after { 116 | border-radius: 0; 117 | font-size: 1em; 118 | padding: 0; 119 | width: 3em; 120 | height: 1.5em; 121 | line-height: 1.5em; 122 | top: -3em; 123 | } 124 | 125 | .vjs-custom-skin > .video-js .vjs-menu-button-popup .vjs-menu { 126 | width: 5em; 127 | left: -1em; 128 | } 129 | 130 | .vjs-custom-skin > .video-js .vjs-menu-button-popup.vjs-volume-menu-button-vertical .vjs-menu { 131 | left: 0; 132 | } 133 | 134 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-resolution-button .vjs-menu { 135 | /*order: 4;*/ 136 | } 137 | 138 | /*排序顺序*/ 139 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-play-control { 140 | order: 0; 141 | } 142 | 143 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-time-control { 144 | min-width: 1em; 145 | padding: 0; 146 | margin: 0 .1em; 147 | text-align: center; 148 | display: block; 149 | order: 1; 150 | } 151 | 152 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-playback-rate .vjs-playback-rate-value{ 153 | font-size: 1.2em; 154 | line-height: 2.4; 155 | } 156 | 157 | .vjs-custom-skin > .video-js .vjs-progress-control.vjs-control { 158 | order: 2; 159 | } 160 | 161 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-volume-menu-button { 162 | order: 3; 163 | } 164 | 165 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-resolution-button { 166 | order: 4; 167 | } 168 | 169 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-resolution-button .vjs-resolution-button-label { 170 | display: block; 171 | line-height: 3em; 172 | } 173 | 174 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-playback-rate { 175 | order: 5; 176 | } 177 | 178 | .vjs-custom-skin > .video-js .vjs-control-bar .vjs-fullscreen-control { 179 | order: 6; 180 | } 181 | -------------------------------------------------------------------------------- /src/components/video-player/index.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Vue-Video-Player ssr.js 4 | * Author: surmon@foxmail.com 5 | * Github: https://github.com/surmon-china/vue-video-player 6 | * Adapted from Videojs (https://github.com/videojs/video.js) 7 | */ 8 | 9 | import _videojs from 'video.js' 10 | import videoPlayer from './player.vue' 11 | 12 | const videojs = window.videojs || _videojs 13 | const install = function (Vue, config) { 14 | if (config) { 15 | if (config.options) { 16 | videoPlayer.props.globalOptions.default = () => config.options 17 | } 18 | if (config.events) { 19 | videoPlayer.props.globalEvents.default = () => config.events 20 | } 21 | } 22 | Vue.component(videoPlayer.name, videoPlayer) 23 | } 24 | 25 | const VueVideoPlayer = { videojs, videoPlayer, install } 26 | 27 | export default VueVideoPlayer 28 | export { videojs, videoPlayer, install } 29 | /** 30 | * npm install -S video.js 31 | * rtmp: npm install -S videojs-flash 32 | */ -------------------------------------------------------------------------------- /src/components/video-player/player.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 229 | -------------------------------------------------------------------------------- /src/components/video-player/ssr.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * VueVideoPlayer ssr.js 4 | * Author: surmon@foxmail.com 5 | * Github: https://github.com/surmon-china/vue-video-player 6 | */ 7 | 8 | // Require sources 9 | import videojs from 'video.js' 10 | import objectAssign from 'object-assign' 11 | 12 | // as of videojs 6.6.0 13 | const DEFAULT_EVENTS = [ 14 | 'loadeddata', 15 | 'canplay', 16 | 'canplaythrough', 17 | 'play', 18 | 'pause', 19 | 'waiting', 20 | 'playing', 21 | 'ended', 22 | 'error' 23 | ] 24 | 25 | const videoPlayerDirective = globalOptions => { 26 | 27 | // globalOptions 28 | globalOptions.events = globalOptions.events || [] 29 | globalOptions.options = globalOptions.options || {} 30 | 31 | // Get videojs instace name in directive 32 | const getInstanceName = (el, binding, vnode) => { 33 | let instanceName = null 34 | if (binding.arg) { 35 | instanceName = binding.arg 36 | } else if (vnode.data.attrs && (vnode.data.attrs.instanceName || vnode.data.attrs['instance-name'])) { 37 | instanceName = (vnode.data.attrs.instanceName || vnode.data.attrs['instance-name']) 38 | } else if (el.id) { 39 | instanceName = el.id 40 | } 41 | return instanceName || 'player' 42 | } 43 | 44 | // dom 45 | const repairDom = el => { 46 | if (!el.children.length) { 47 | const video = document.createElement('video') 48 | video.className = 'video-js' 49 | el.appendChild(video) 50 | } 51 | } 52 | 53 | // init 54 | const initPlayer = (el, binding, vnode) => { 55 | 56 | const self = vnode.context 57 | const attrs = vnode.data.attrs || {} 58 | const options = binding.value || {} 59 | const instanceName = getInstanceName(el, binding, vnode) 60 | const customEventName = attrs.customEventName || 'statechanged' 61 | let player = self[instanceName] 62 | 63 | // options 64 | const componentEvents = attrs.events || [] 65 | const playsinline = attrs.playsinline || false 66 | 67 | // ios fullscreen 68 | if (playsinline) { 69 | el.children[0].setAttribute('playsinline', playsinline) 70 | el.children[0].setAttribute('webkit-playsinline', playsinline) 71 | el.children[0].setAttribute('x5-playsinline', playsinline) 72 | el.children[0].setAttribute('x5-video-player-type', 'h5') 73 | el.children[0].setAttribute('x5-video-player-fullscreen', false) 74 | } 75 | 76 | // cross origin 77 | if (attrs.crossOrigin) { 78 | el.children[0].crossOrigin = attrs.crossOrigin 79 | el.children[0].setAttribute('crossOrigin', attrs.crossOrigin) 80 | } 81 | 82 | // initialize 83 | if (!player) { 84 | 85 | // videoOptions 86 | const videoOptions = objectAssign({}, { 87 | controls: true, 88 | controlBar: { 89 | remainingTimeDisplay: false, 90 | playToggle: {}, 91 | progressControl: {}, 92 | fullscreenToggle: {}, 93 | volumeMenuButton: { 94 | inline: false, 95 | vertical: true 96 | } 97 | }, 98 | techOrder: ['html5'], 99 | plugins: {} 100 | }, globalOptions.options, options) 101 | 102 | // plugins 103 | if (videoOptions.plugins) { 104 | delete videoOptions.plugins.__ob__ 105 | } 106 | 107 | // console.log('videoOptions', videoOptions) 108 | 109 | // eventEmit 110 | const eventEmit = (vnode, name, data) => { 111 | const handlers = (vnode.data && vnode.data.on) || 112 | (vnode.componentOptions && vnode.componentOptions.listeners) 113 | if (handlers && handlers[name]) handlers[name].fns(data) 114 | } 115 | 116 | // emit event 117 | const emitPlayerState = (event, value) => { 118 | if (event) { 119 | eventEmit(vnode, event, player) 120 | } 121 | if (value) { 122 | eventEmit(vnode, customEventName, { [event]: value }) 123 | } 124 | } 125 | 126 | // instance 127 | player = self[instanceName] = videojs(el.children[0], videoOptions, function() { 128 | 129 | // events 130 | const events = DEFAULT_EVENTS.concat(componentEvents).concat(globalOptions.events) 131 | 132 | // watch events 133 | const onEdEvents = {} 134 | for (let i = 0; i < events.length; i++) { 135 | if (typeof events[i] === 'string' && onEdEvents[events[i]] === undefined) { 136 | (event => { 137 | onEdEvents[event] = null 138 | this.on(event, () => { 139 | emitPlayerState(event, true) 140 | }) 141 | })(events[i]) 142 | } 143 | } 144 | 145 | // watch timeupdate 146 | this.on('timeupdate', function() { 147 | emitPlayerState('timeupdate', this.currentTime()) 148 | }) 149 | 150 | // player readied 151 | emitPlayerState('ready') 152 | }) 153 | } 154 | } 155 | 156 | // dispose 157 | const disposePlayer = (el, binding, vnode) => { 158 | const self = vnode.context 159 | const instanceName = getInstanceName(el, binding, vnode) 160 | const player = self[instanceName] 161 | if (player && player.dispose) { 162 | if (player.techName_ !== 'Flash') { 163 | player.pause && player.pause() 164 | } 165 | player.dispose() 166 | repairDom(el) 167 | self[instanceName] = null 168 | delete self[instanceName] 169 | } 170 | } 171 | 172 | return { 173 | inserted: initPlayer, 174 | 175 | // Bind directive 176 | bind(el, binding, vnode) { 177 | repairDom(el) 178 | }, 179 | 180 | // Parse text model change 181 | update(el, binding, vnode) { 182 | const options = binding.value || {} 183 | disposePlayer(el, binding, vnode) 184 | if (options && options.sources && options.sources.length) { 185 | initPlayer(el, binding, vnode) 186 | } 187 | }, 188 | 189 | // Destroy this directive 190 | unbind: disposePlayer 191 | } 192 | } 193 | 194 | // videoPlayer 195 | const videoPlayer = videoPlayerDirective({}) 196 | 197 | // Global quill default options 198 | const install = function (Vue, globalOptions = { 199 | options: {}, 200 | events: [] 201 | }) { 202 | 203 | // Mount quill directive for Vue global 204 | Vue.directive('video-player', videoPlayerDirective(globalOptions)) 205 | } 206 | 207 | const VueVideoPlayer = { videojs, videoPlayer, install } 208 | 209 | export default VueVideoPlayer 210 | export { videojs, videoPlayer, install } 211 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import VueVideoPlayer from './components/video-player' 4 | 5 | // require videojs style 6 | import 'video.js/dist/video-js.css' 7 | import './components/video-player/custom-theme.css' 8 | 9 | Vue.use(VueVideoPlayer, /* { 10 | options: global default options, 11 | events: global videojs events 12 | } */) 13 | Vue.config.productionTip = false 14 | 15 | new Vue({ 16 | render: h => h(App), 17 | }).$mount('#app') 18 | --------------------------------------------------------------------------------