├── .eslintignore ├── src ├── layout │ ├── helpers.js │ └── player │ │ └── base.js ├── styles │ ├── variables.scss │ ├── main.scss │ └── controls.scss ├── index.js ├── CanvasVideoControls.js └── CanvasVideo.js ├── .gitignore ├── .editorconfig ├── .babelrc ├── .eslintrc.js ├── LICENSE ├── CONTRIBUTING.md ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/*.js 2 | -------------------------------------------------------------------------------- /src/layout/helpers.js: -------------------------------------------------------------------------------- 1 | export const hidden = { 2 | display: 'none' 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | test/coverage 5 | dist 6 | yarn-error.log 7 | reports 8 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $control-button-color: rgba(255,255,255, .5); 2 | $scrubber-height: 10px; 3 | $scrubber-handle-size: 22px; 4 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | // vendor 2 | @import './node_modules/rangeslider-pure/dist/range-slider'; 3 | 4 | @import './variables'; 5 | @import './controls'; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | "last 2 versions" 9 | ] 10 | } 11 | } 12 | ] 13 | ], 14 | "plugins": [ 15 | "transform-vue-jsx", 16 | "transform-object-rest-spread" 17 | ], 18 | "env": { 19 | "test": { 20 | "plugins": [ 21 | "istanbul" 22 | ] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import CanvasVideo from './CanvasVideo' 2 | 3 | function plugin (Vue, options = {}) { 4 | Vue.component('CanvasVideo', CanvasVideo) 5 | } 6 | 7 | // Install by default if using the script tag 8 | if (typeof window !== 'undefined' && window.Vue) { 9 | window.Vue.use(plugin) 10 | } 11 | 12 | export default plugin 13 | const version = '__VERSION__' 14 | // Export all components too 15 | export { 16 | CanvasVideo, 17 | version 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | extends: 'vue', 8 | // add your custom rules here 9 | 'rules': { 10 | // allow async-await 11 | 'generator-star-spacing': 0, 12 | // allow debugger during development 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 14 | 'no-return-assign': 0, 15 | }, 16 | globals: { 17 | requestAnimationFrame: true, 18 | performance: true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/layout/player/base.js: -------------------------------------------------------------------------------- 1 | export const videoWrapStyles = { 2 | position: 'relative', 3 | overflow: 'hidden', 4 | width: '100%', 5 | height: '100%' 6 | } 7 | 8 | export const videoWrapInnerStyles = { 9 | position: 'absolute', 10 | top: 0, 11 | left: 0, 12 | width: '100%' 13 | } 14 | 15 | export const videoCanvasStyles = { 16 | position: 'absolute', 17 | top: 0, 18 | left: 0, 19 | width: '100%', 20 | height: '100%' 21 | } 22 | 23 | export const videoStyles = { 24 | position: 'absolute', 25 | top: 0, 26 | left: 0, 27 | width: '100%', 28 | height: '100%' 29 | } 30 | 31 | export const mediaCoveringStyles = { 32 | top: '50%', 33 | left: '50%', 34 | width: 'auto', 35 | height: 'auto', 36 | minWidth: '100%', 37 | minHeight: '100%', 38 | transform: 'translate(-50%,-50%)' 39 | } 40 | -------------------------------------------------------------------------------- /src/styles/controls.scss: -------------------------------------------------------------------------------- 1 | .vue-canvasvideo-controls { 2 | position: absolute; 3 | bottom: 0; 4 | width: 100%; 5 | background: rgba(0,0,0,.2); 6 | opacity: 0; 7 | transition: opacity .2s linear; 8 | } 9 | 10 | .vue-canvasvideo-controls__wrap { 11 | display: flex; 12 | align-items: center; 13 | margin: 0 15px; 14 | } 15 | 16 | .vue-canvasvideo-control__button { 17 | fill: $control-button-color; 18 | height: 30px; 19 | cursor: pointer; 20 | } 21 | 22 | .vue-canvasvideo-controls__scrubber { 23 | margin: 0 25px; 24 | width: 100%; 25 | .rangeSlider { 26 | height: $scrubber-height; 27 | } 28 | .rangeSlider__handle { 29 | top: -7px; 30 | width: $scrubber-handle-size; 31 | height: $scrubber-handle-size; 32 | &::after { 33 | width: $scrubber-handle-size / 2; 34 | height: $scrubber-handle-size / 2; 35 | } 36 | } 37 | } 38 | 39 | .vue-canvasvideo__elapsed { 40 | color: $control-button-color; 41 | font-family: "Helvetica", "Open Sans", sans-serif; 42 | white-space: nowrap; 43 | } 44 | 45 | .vue-canvasvideo__wrap--inner:hover .vue-canvasvideo-controls { 46 | opacity: 1; 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Chris Hurlburt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/chrishurlburt/vue-scrollview). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **Keep the same style** - eslint will automatically be ran before committing 11 | 12 | - **Tip** to pass lint tests easier use the `npm run lint:fix` command 13 | 14 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 15 | 16 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 17 | 18 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 19 | 20 | - **Create feature branches** - Don't ask us to pull from your master branch. 21 | 22 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 23 | 24 | - **Send coherent history** - Make sure your commits message means something 25 | 26 | 27 | ## Running Tests 28 | 29 | Launch visual tests and watch the components at the same time 30 | 31 | ``` bash 32 | $ npm run dev 33 | ``` 34 | 35 | 36 | **Happy coding**! 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-canvasvideo", 3 | "version": "1.0.0", 4 | "description": "A Vue.js component for playing videos on canvas.", 5 | "author": "Chris Hurlburt ", 6 | "main": "dist/vue-canvasvideo.common.js", 7 | "module": "dist/vue-canvasvideo.esm.js", 8 | "browser": "dist/vue-canvasvideo.js", 9 | "unpkg": "dist/vue-canvasvideo.js", 10 | "style": "dist/vue-canvasvideo.css", 11 | "files": [ 12 | "dist", 13 | "src" 14 | ], 15 | "scripts": { 16 | "build": "node build/build.js && node-sass src/styles/main.scss dist/vuecanvasvideo.min.css --output-style compressed" 17 | }, 18 | "devDependencies": { 19 | "add-asset-html-webpack-plugin": "^1.0.2", 20 | "babel-core": "^6.23.1", 21 | "babel-eslint": "^7.1.1", 22 | "babel-helper-vue-jsx-merge-props": "^2.0.2", 23 | "babel-loader": "^6.3.2", 24 | "babel-plugin-istanbul": "^4.0.0", 25 | "babel-plugin-syntax-jsx": "^6.18.0", 26 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 27 | "babel-plugin-transform-runtime": "^6.23.0", 28 | "babel-plugin-transform-vue-jsx": "^3.3.0", 29 | "babel-preset-env": "^1.1.8", 30 | "babel-preset-es2015": "^6.22.0", 31 | "babel-preset-stage-2": "^6.22.0", 32 | "buble": "^0.15.2", 33 | "clean-css": "^4.0.8", 34 | "cross-env": "^3.1.4", 35 | "css-loader": "^0.26.1", 36 | "eslint": "^3.16.1", 37 | "eslint-config-vue": "^2.0.2", 38 | "eslint-plugin-vue": "^2.0.1", 39 | "extract-text-webpack-plugin": "^2.0.0", 40 | "html-webpack-plugin": "^2.28.0", 41 | "mkdirp": "^0.5.1", 42 | "node-sass": "^4.5.3", 43 | "postcss": "^5.2.15", 44 | "postcss-cssnext": "^2.9.0", 45 | "rimraf": "^2.6.0", 46 | "rollup": "^0.41.4", 47 | "rollup-plugin-buble": "^0.15.0", 48 | "rollup-plugin-commonjs": "^7.0.0", 49 | "rollup-plugin-jsx": "^1.0.3", 50 | "rollup-plugin-node-resolve": "^2.0.0", 51 | "rollup-plugin-postcss": "^0.2.0", 52 | "rollup-plugin-replace": "^1.1.1", 53 | "rollup-plugin-scss": "^0.3.0", 54 | "rollup-plugin-vue": "^2.2.20", 55 | "style-loader": "^0.13.1", 56 | "stylefmt": "^5.1.2", 57 | "stylelint": "^7.9.0", 58 | "stylelint-config-standard": "^16.0.0", 59 | "stylelint-processor-html": "^1.0.0", 60 | "uglify-js": "^3.0.26", 61 | "uppercamelcase": "^1.1.0", 62 | "vue": "^2.4.0", 63 | "vue-loader": "^11.1.0", 64 | "vue-template-compiler": "^2.1.10", 65 | "webpack": "^3.2.0" 66 | }, 67 | "peerDependencies": { 68 | "vue": "^2.4.0" 69 | }, 70 | "repository": { 71 | "type": "git", 72 | "url": "git+https://github.com/chrishurlburt/vue-scrollview.git" 73 | }, 74 | "bugs": { 75 | "url": "https://github.com/chrishurlburt/vue-scrollview/issues" 76 | }, 77 | "homepage": "https://github.com/chrishurlburt/vue-scrollview#readme", 78 | "license": { 79 | "type": "MIT", 80 | "url": "http://www.opensource.org/licenses/mit-license.php" 81 | }, 82 | "dependencies": { 83 | "lodash.debounce": "^4.0.8", 84 | "rangeslider-pure": "^0.4.1" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-canvasvideo 2 | 3 | [![npm](https://img.shields.io/npm/v/vue-canvasvideo.svg)](https://www.npmjs.com/package/vue-canvasvideo) [![vue2](https://img.shields.io/badge/vue-2.x-brightgreen.svg)](https://vuejs.org/) 4 | 5 | > A Vue.js component for playing videos on HTML canvas. Useful for achieving autoplay videos in iOS and Safari. 6 | 7 | 8 | ## Overview 9 | 10 | ## Installation 11 | 12 | ```bash 13 | npm install --save vue-canvasvideo 14 | ``` 15 | 16 | ## Setup 17 | 18 | ### Bundler (Webpack, Rollup) 19 | 20 | ```js 21 | // in your entrypoint 22 | import Vue from 'vue' 23 | import CanvasVideo from 'vue-canvasvideo' 24 | import 'vue-canvasvideo/dist/vuecanvasvideo.min.css' 25 | 26 | Vue.use(CanvasVideo) 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Required Markup 32 | 33 | ```js 34 | 35 | // load the video and start playing automatically 36 | 40 | 41 | ``` 42 | 43 | ## Practical Use Cases 44 | 45 | vue-canvasvideo is useful for achieving autoplay video on iOS, which is especially useful for video backgrounds. As of iOS 10, autoplay video is [supported](https://webkit.org/blog/6784/new-video-policies-for-ios/) so vue-canvasvideo is intended as a fallback for older iOS/Safari versions. 46 | 47 | Although not the intended use case, vue-canvasvideo can also be used as a regular video player and optionally includes controls. 48 | 49 | vue-canvasvideo can switch seamlessly between HTML video and canvas as needed and includes an option to "cover" the element it's placed in, similar to background-size: cover in css. 50 | 51 | ## Props 52 | 53 | ```js 54 | props: { 55 | src: { // the video source 56 | type: String, 57 | required: true 58 | }, 59 | fps: { // frames per second, the playback speed 60 | type: Number, 61 | default: () => 25 62 | }, 63 | showVideo: { // switch between playback on video or canvas 64 | type: Boolean, 65 | default: () => false 66 | }, 67 | autoplay: { // automatically play the video 68 | type: Boolean, 69 | default: () => false 70 | }, 71 | loop: { // loop the video infinitely 72 | type: Boolean, 73 | default: () => false 74 | }, 75 | playPauseOnClick: { // toggle play/pause on click of video 76 | type: Boolean, 77 | default: () => false 78 | }, 79 | resetOnLast: { // reset start after complete 80 | type: Boolean, 81 | default: () => false 82 | }, 83 | cover: { // should the video cover within it's container (useful for backgrounds; cannot be used with 'square' prop) 84 | type: Boolean, 85 | default: () => false 86 | }, 87 | square: { // should the video be centered vertically in a square container (cannot be used with 'cover' prop) 88 | type: Boolean, 89 | default: () => false 90 | }, 91 | controls: { // show video playback controls 92 | type: Boolean, 93 | default: () => false 94 | } 95 | } 96 | ``` 97 | 98 | ## Development 99 | 100 | ### Build 101 | 102 | Bundle the js to the `dist` folder: 103 | 104 | ```bash 105 | npm run build 106 | ``` 107 | 108 | ## Acknowledgements 109 | Based on [https://stanko.github.io/html-canvas-video-player/](https://stanko.github.io/html-canvas-video-player/) 110 | 111 | 112 | ## License 113 | 114 | [MIT](http://opensource.org/licenses/MIT) 115 | -------------------------------------------------------------------------------- /src/CanvasVideoControls.js: -------------------------------------------------------------------------------- 1 | import rangeSlider from 'rangeslider-pure' 2 | 3 | export default { 4 | render (h) { 5 | return ( 6 | h( 7 | 'div', 8 | { 9 | attrs: { class: 'vue-canvasvideo-controls' }, 10 | on: { click: e => e.stopPropagation() } 11 | }, 12 | [ 13 | h( 14 | 'div', 15 | { 16 | attrs: { class: 'vue-canvasvideo-controls__wrap' } 17 | }, 18 | [ 19 | (!this.playing && this.renderPlay(h)), 20 | (this.playing && this.renderPause(h)), 21 | h( 22 | 'div', 23 | { attrs: { class: 'vue-canvasvideo-controls__scrubber' }}, 24 | [ 25 | h( 26 | 'input', 27 | { 28 | attrs: { 29 | class: 'vue-canvasvideo-controls__timeline', 30 | type: 'range' 31 | }, 32 | ref: 'timeline' 33 | } 34 | ) 35 | ] 36 | ), 37 | h( 38 | 'p', 39 | { attrs: { class: 'vue-canvasvideo__elapsed' }}, 40 | `${this.elapsedFormatted} / ${this.durationFormatted}` 41 | ) 42 | ] 43 | ) 44 | ] 45 | ) 46 | ) 47 | }, 48 | watch: { 49 | elapsed (elapsed) { 50 | this.$refs.timeline.rangeSlider.update({ 51 | min: 0, 52 | max: this.durationRounded, 53 | step: 0.5, 54 | value: elapsed, 55 | buffer: 0 56 | }) 57 | } 58 | }, 59 | methods: { 60 | formatSeconds (seconds) { 61 | if (seconds >= 3600) { 62 | // over an hour long, switch to hh:mm:ss 63 | return new Date(seconds * 1000).toISOString().substr(11, 8) 64 | } 65 | return new Date(seconds * 1000).toISOString().substr(14, 5) 66 | }, 67 | renderPlay (h) { 68 | return h( 69 | 'svg', 70 | { 71 | attrs: { class: 'vue-canvasvideo-control__button vue-canvasvideo-controls__button--play', viewBox: '0 0 320 389' }, 72 | on: { click: (e) => this.$emit('play', e) } 73 | }, 74 | [ 75 | h( 76 | 'path', 77 | { attrs: { d: 'M320 194.5L0 389V0' }} 78 | ) 79 | ] 80 | ) 81 | }, 82 | renderPause (h) { 83 | return h( 84 | 'svg', 85 | { 86 | attrs: { class: 'vue-canvasvideo-control__button vue-canvasvideo-controls__button--pause', viewBox: '0 0 320 389' }, 87 | on: { click: (e) => this.$emit('pause', e) } 88 | }, 89 | [ 90 | h( 91 | 'path', 92 | { 93 | attrs: { 94 | d: 'M0 389h120V0H0v389zM200 0v389h120V0H200z' 95 | } 96 | } 97 | ) 98 | ] 99 | ) 100 | } 101 | }, 102 | mounted () { 103 | rangeSlider.create(this.$refs.timeline, { 104 | value: 0, 105 | onSlide: position => this.$emit('scrubbing', position), 106 | onSlideEnd: position => this.$emit('timechange', position) 107 | }) 108 | }, 109 | computed: { 110 | durationRounded () { 111 | return Math.round(this.duration) 112 | }, 113 | elapsedFormatted () { 114 | return this.formatSeconds(this.elapsed) 115 | }, 116 | durationFormatted () { 117 | return this.formatSeconds(this.duration) 118 | } 119 | }, 120 | props: { 121 | duration: { 122 | type: Number, 123 | required: true 124 | }, 125 | elapsed: { 126 | type: Number, 127 | required: true 128 | }, 129 | playing: { 130 | type: Boolean, 131 | required: true 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/CanvasVideo.js: -------------------------------------------------------------------------------- 1 | import debounce from 'lodash.debounce' 2 | import CanvasVideoControls from './CanvasVideoControls' 3 | import { 4 | videoWrapStyles, 5 | videoWrapInnerStyles, 6 | videoCanvasStyles, 7 | videoStyles, 8 | mediaCoveringStyles 9 | } from './layout/player/base' 10 | import { hidden } from './layout/helpers' 11 | 12 | export default { 13 | render (h) { 14 | return ( 15 | h( 16 | 'div', 17 | { 18 | attrs: { class: 'vue-canvasvideo__wrap' }, 19 | style: videoWrapStyles, 20 | on: { click: () => this.videoClicked() }, 21 | ref: 'videoWrapper' 22 | }, 23 | [ 24 | h( 25 | 'div', 26 | { 27 | attrs: { class: 'vue-canvasvideo__wrap--inner' }, 28 | style: this.computedWrapStyles 29 | }, 30 | [ 31 | h( 32 | 'video', 33 | { 34 | attrs: { class: 'vue-canvasvideo__video', src: this.src }, 35 | style: this.computedVideoStyles, 36 | ref: 'video' 37 | } 38 | ), 39 | h( 40 | 'canvas', 41 | { 42 | attrs: { 43 | class: 'vue-canvasvideo__canvas', 44 | width: this.width, 45 | height: this.height 46 | }, 47 | style: this.computedCanvasStyles, 48 | ref: 'videoCanvas' 49 | } 50 | ), 51 | ( 52 | this.controls && 53 | h( 54 | CanvasVideoControls, 55 | { 56 | on: { 57 | pause: (e) => this.pause(e), 58 | play: (e) => this.play(e), 59 | scrubbing: (val) => this.scrub(val), 60 | timechange: (val) => this.jumpTime(val) 61 | }, 62 | props: { 63 | duration: this.duration, 64 | elapsed: this.elapsed, 65 | playing: this.playing 66 | } 67 | } 68 | ) 69 | ) 70 | ] 71 | ) 72 | ] 73 | ) 74 | ) 75 | }, 76 | data () { 77 | return { 78 | playing: false, 79 | scrubbing: false, 80 | aspectRatio: 1, 81 | duration: 0, 82 | elapsed: 0, 83 | lastTime: 0, 84 | width: 0, 85 | height: 0 86 | } 87 | }, 88 | methods: { 89 | init () { 90 | this.ctx = this.$refs.videoCanvas.getContext('2d') 91 | this.$refs.video.load() 92 | this.setCanvasSize() 93 | if (this.autoplay) this.play() 94 | }, 95 | bind () { 96 | const { video } = this.$refs 97 | // Draw a frame on every timeupdate 98 | video.addEventListener('timeupdate', () => this.drawFrame()) 99 | // Draw the first frame 100 | video.addEventListener('canplay', () => this.drawFrame()) 101 | video.addEventListener('loadedmetadata', () => { 102 | this.duration = video.duration 103 | // Set the canvas size to the video size once we know it... 104 | this.setCanvasSize() 105 | }) 106 | // in case 'canplay' already fired 107 | if (video.readyState >= 2) this.drawFrame() 108 | // debounce window resize 109 | window.addEventListener('resize', debounce(() => { 110 | this.setCanvasSize() 111 | this.drawFrame() 112 | }, 1000)) 113 | }, 114 | setCanvasSize () { 115 | const { video } = this.$refs 116 | this.width = video.videoWidth 117 | this.height = video.videoHeight 118 | this.aspectRatio = this.height / this.width 119 | }, 120 | play (e) { 121 | if (e) e.stopPropagation() 122 | this.lastTime = Date.now() 123 | this.playing = true 124 | this.renderVideo() 125 | this.updateTime() 126 | // @TODO: set and resync audio 127 | }, 128 | pause (e) { 129 | if (e) e.stopPropagation() 130 | this.playing = false 131 | }, 132 | videoClicked () { 133 | if (this.playPauseOnClick) this.togglePlay() 134 | }, 135 | scrub (time) { 136 | if (!this.scrubbing) { 137 | this.stopUpdateTime() 138 | this.scrubbing = true 139 | } 140 | this.setTime(time) 141 | }, 142 | togglePlay () { 143 | if (this.playing) this.pause() 144 | else this.play() 145 | }, 146 | jumpTime (time) { 147 | this.scrubbing = false 148 | this.setTime(time) 149 | this.updateTime() 150 | }, 151 | setTime (time) { 152 | this.$refs.video.currentTime = time 153 | this.elapsed = time 154 | }, 155 | updateTime () { 156 | if (!this.timeInterval) { 157 | this.timeInterval = setInterval(() => { 158 | if (!this.playing) this.stopUpdateTime() 159 | this.elapsed = this.$refs.video.currentTime 160 | }, 500) 161 | } 162 | }, 163 | stopUpdateTime () { 164 | clearInterval(this.timeInterval) 165 | this.timeInterval = false 166 | }, 167 | renderVideo () { 168 | const { video } = this.$refs 169 | const time = Date.now() 170 | const PreviousElapsed = (time - this.lastTime) / 1000 171 | const currentElapsed = video.currentTime 172 | // set video time, trigger render 173 | if (PreviousElapsed >= (1 / this.fps)) { 174 | // only render a frame if the the last frame was rendered >= 1/fps of a second ago 175 | video.currentTime = currentElapsed + PreviousElapsed 176 | this.lastTime = time 177 | } 178 | 179 | if (video.currentTime >= video.duration) { 180 | if (!this.loop) this.playing = false 181 | if (this.resetOnLast || this.loop) video.currentTime = 0 182 | } 183 | 184 | if (this.playing) this.animationFrame = window.requestAnimationFrame(() => this.renderVideo()) 185 | else window.cancelAnimationFrame(this.animationFrame) 186 | }, 187 | drawFrame () { 188 | const { video } = this.$refs 189 | this.ctx.drawImage(video, 0, 0, this.width, this.height) 190 | } 191 | }, 192 | computed: { 193 | computedWrapStyles () { 194 | const base = { paddingBottom: `${this.aspectRatio * 100}%`, ...videoWrapInnerStyles } 195 | const centerSquare = { marginTop: `${(1 - this.aspectRatio) / 2 * 100}%` } 196 | 197 | if (this.cover && !this.square) { 198 | return Object.assign({}, videoWrapInnerStyles, mediaCoveringStyles) 199 | } else if (this.square && !this.cover) { 200 | return Object.assign({}, base, centerSquare) 201 | } 202 | 203 | return base 204 | }, 205 | computedVideoStyles () { 206 | const cover = Object.assign({}, videoStyles, mediaCoveringStyles) 207 | if (this.showVideo) { 208 | if (this.cover) return cover 209 | return videoStyles 210 | } 211 | return hidden 212 | }, 213 | computedCanvasStyles () { 214 | const cover = Object.assign({}, videoCanvasStyles, mediaCoveringStyles) 215 | if (this.showVideo) return hidden 216 | if (this.cover) return cover 217 | return videoCanvasStyles 218 | } 219 | }, 220 | mounted () { 221 | if (this.cover && this.square) { 222 | console.error('[vue-canvasvideo]: The cover and square props cannot be used together.') 223 | } else { 224 | this.init() 225 | this.bind() 226 | } 227 | }, 228 | props: { 229 | src: { 230 | type: String, 231 | required: true 232 | }, 233 | fps: { 234 | type: Number, 235 | default: () => 25 236 | }, 237 | showVideo: { 238 | type: Boolean, 239 | default: () => false 240 | }, 241 | autoplay: { 242 | type: Boolean, 243 | default: () => false 244 | }, 245 | square: { 246 | type: Boolean, 247 | default: () => false 248 | }, 249 | loop: { 250 | type: Boolean, 251 | default: () => false 252 | }, 253 | playPauseOnClick: { 254 | type: Boolean, 255 | default: () => false 256 | }, 257 | resetOnLast: { 258 | type: Boolean, 259 | default: () => false 260 | }, 261 | audio: { 262 | type: Boolean, 263 | default: () => false 264 | }, 265 | cover: { 266 | type: Boolean, 267 | default: () => false 268 | }, 269 | controls: { 270 | type: Boolean, 271 | default: () => false 272 | } 273 | } 274 | } 275 | --------------------------------------------------------------------------------