├── .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 | 
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
2 |
3 |
4 | basic mp4
5 |
6 |
23 |
24 |
25 |
26 |
127 |
128 |
--------------------------------------------------------------------------------
/src/components/03-video.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | AudioTrack && playsinline / 音轨 及 移动端不全屏 hls
5 |
6 |
12 |
13 |
14 |
15 |
65 |
--------------------------------------------------------------------------------
/src/components/05-video.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{isRTMP ? 'rtmp' : 'flv'}}流播放 & 自定义hotkeys
5 |
6 |
7 |
15 |
16 |
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 |
2 |
3 |
4 |
5 |
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 |
--------------------------------------------------------------------------------